diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index cbdddd5..6c1630e 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -13,14 +13,14 @@ jobs: working-directory: hackathon_site steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - name: Set up Python 3.8 - uses: actions/setup-python@v1 + - name: Set up Python 3.10 + uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: '3.10' - name: Install dependencies run: | python -m pip install --upgrade pip @@ -42,12 +42,12 @@ jobs: working-directory: hackathon_site steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - name: Use Node.js 16.x - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '16.x' - name: Install dependencies @@ -60,12 +60,12 @@ jobs: working-directory: hackathon_site/dashboard/frontend steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - name: Use Node.js 16.x - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '16.x' - name: Install dependencies diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e853a21..e6863f3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,14 +13,14 @@ jobs: working-directory: hackathon_site steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - name: Set up Python 3.8 - uses: actions/setup-python@v1 + - name: Set up Python 3.10 + uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: '3.10' - name: Install dependencies run: | python -m pip install --upgrade pip @@ -42,12 +42,12 @@ jobs: working-directory: hackathon_site steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - name: Use Node.js 16.x - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '16.x' - name: Install dependencies @@ -62,12 +62,12 @@ jobs: working-directory: hackathon_site/dashboard/frontend steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - name: Use Node.js 16.x - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '16.x' - name: Install dependencies diff --git a/hackathon_site/dashboard/frontend/src/api/helpers.ts b/hackathon_site/dashboard/frontend/src/api/helpers.ts index a14db71..3d7d589 100644 --- a/hackathon_site/dashboard/frontend/src/api/helpers.ts +++ b/hackathon_site/dashboard/frontend/src/api/helpers.ts @@ -40,11 +40,11 @@ export const teamOrderListSerialization = ( const hardwareItems: Record = {}; const hardwareRequested: Record = {}; const returnedItems: Record = {}; - order.request.forEach( + (order.request ?? []).forEach( (hardware) => (hardwareRequested[hardware.id] = hardware.requested_quantity) ); - order.items.forEach(({ id, hardware_id, part_returned_health }) => { + (order.items ?? []).forEach(({ id, hardware_id, part_returned_health }) => { if (part_returned_health && part_returned_health !== "Rejected") { const returnItemKey = `${hardware_id}-${part_returned_health}`; if (returnedItems[returnItemKey]) @@ -92,7 +92,7 @@ export const teamOrderListSerialization = ( const hardwareInTableRow = Object.values(hardwareItems); // Check if ALL order items have a part_returned_health status (i.e., they are all returned or rejected) - const allItemsReturnedOrRejected = order.items.every( + const allItemsReturnedOrRejected = (order.items ?? []).every( (item) => item.part_returned_health !== null ); diff --git a/hackathon_site/dashboard/frontend/src/components/cart/CartSummary/CartSummary.test.tsx b/hackathon_site/dashboard/frontend/src/components/cart/CartSummary/CartSummary.test.tsx index 9c9711c..1267622 100644 --- a/hackathon_site/dashboard/frontend/src/components/cart/CartSummary/CartSummary.test.tsx +++ b/hackathon_site/dashboard/frontend/src/components/cart/CartSummary/CartSummary.test.tsx @@ -63,13 +63,15 @@ export const mockHardwareSignOutDates = ( describe("Render cartQuantity", () => { it(`Renders correctly when it reads the number ${cartQuantity}`, async () => { - const { getByText } = render( + const { getByTestId } = render( ); await waitFor(() => { - expect(getByText(cartQuantity.toString())).toBeInTheDocument(); + expect(getByTestId("cart-quantity-total")).toHaveTextContent( + cartQuantity.toString() + ); }); }); // TODO: fix failing test case diff --git a/hackathon_site/dashboard/frontend/src/components/dashboard/ItemTable/ItemTable.test.tsx b/hackathon_site/dashboard/frontend/src/components/dashboard/ItemTable/ItemTable.test.tsx index b9a42f4..3f24be0 100644 --- a/hackathon_site/dashboard/frontend/src/components/dashboard/ItemTable/ItemTable.test.tsx +++ b/hackathon_site/dashboard/frontend/src/components/dashboard/ItemTable/ItemTable.test.tsx @@ -23,7 +23,7 @@ describe("", () => { }); const { getByText, queryByText } = render(, { store }); expect(getByText(/pending orders/i)).toBeInTheDocument(); - expect(queryByText("In progress")).toBeInTheDocument(); + expect(queryByText("Submitted")).toBeInTheDocument(); mockPendingOrdersInTable.map(({ id }) => { expect(getByText(`Order #${id}`)).toBeInTheDocument(); }); diff --git a/hackathon_site/dashboard/frontend/src/components/dashboard/ItemTable/ItemTable.tsx b/hackathon_site/dashboard/frontend/src/components/dashboard/ItemTable/ItemTable.tsx index eb6bad6..5804eab 100644 --- a/hackathon_site/dashboard/frontend/src/components/dashboard/ItemTable/ItemTable.tsx +++ b/hackathon_site/dashboard/frontend/src/components/dashboard/ItemTable/ItemTable.tsx @@ -247,6 +247,10 @@ export const PendingTables = () => { const submitCancelOrderModal = async (cancelOrderId: number | null) => { if (cancelOrderId != null) { + // Close the modal immediately (matching the "Go Back" dismiss path) so it + // never gets stuck open if the order refresh below is slow or fails. + setShowCancelOrderModal(false); + // Refresh orders first to get latest status await dispatch(getTeamOrders()); @@ -266,7 +270,6 @@ export const PendingTables = () => { }) ); } - setShowCancelOrderModal(false); } }; diff --git a/hackathon_site/dashboard/frontend/src/components/general/OrderTables/OrderTables.test.tsx b/hackathon_site/dashboard/frontend/src/components/general/OrderTables/OrderTables.test.tsx index 50468ef..9bb4c84 100644 --- a/hackathon_site/dashboard/frontend/src/components/general/OrderTables/OrderTables.test.tsx +++ b/hackathon_site/dashboard/frontend/src/components/general/OrderTables/OrderTables.test.tsx @@ -10,7 +10,7 @@ describe("", () => { test("Pending status", () => { const { getByText } = render(); - expect(getByText("In progress")).toBeInTheDocument(); + expect(getByText("Submitted")).toBeInTheDocument(); }); test("Error status", () => { diff --git a/hackathon_site/dashboard/frontend/src/components/inventory/ProductOverview/ProductOverview.test.tsx b/hackathon_site/dashboard/frontend/src/components/inventory/ProductOverview/ProductOverview.test.tsx index ab7fc7c..e37d0fd 100644 --- a/hackathon_site/dashboard/frontend/src/components/inventory/ProductOverview/ProductOverview.test.tsx +++ b/hackathon_site/dashboard/frontend/src/components/inventory/ProductOverview/ProductOverview.test.tsx @@ -82,7 +82,7 @@ describe("", () => { // Check if the main section, detailInfoSection, and add to cart section works expect(getByText("Category")).toBeInTheDocument(); expect(getByText("Datasheet")).toBeInTheDocument(); - expect(getByText("Add to cart")).toBeInTheDocument(); + expect(getByText(/Add to cart/)).toBeInTheDocument(); }); test("Category label doesn't appear when there are no categories", async () => { @@ -170,7 +170,7 @@ describe("", () => { // Check if the main section, detailInfoSection, and add to cart section works expect(getByText("Category")).toBeInTheDocument(); expect(getByText("Datasheet")).toBeInTheDocument(); - expect(getByText("Add to cart")).toBeInTheDocument(); + expect(getByText(/Add to cart/)).toBeInTheDocument(); expect(queryByText("Notes")).not.toBeInTheDocument(); }); @@ -273,14 +273,18 @@ describe("", () => { ); // by default, quantity is 1 - const button = getByText("Add to cart"); + const button = getByText(/Add to cart/); await waitFor(() => { fireEvent.click(button); }); expect(cartSelectors.selectAll(store.getState())).toEqual([ - { hardware_id: mockHardware[0].id, quantity: 1 }, + { + hardware_id: mockHardware[0].id, + quantity: 1, + credits: mockHardware[0].credits, + }, ]); expect( getByText(`Added 1 ${mockHardware[0].name} item(s) to your cart.`) @@ -320,7 +324,7 @@ describe("", () => { ); // by default, quantity is 1 - const button = getByText("Add to cart"); + const button = getByText(/Add to cart/); await waitFor(() => { fireEvent.click(button); @@ -402,13 +406,14 @@ describe("", () => { const { getByText, getByLabelText } = render( ); - const button = getByText("Add to cart").closest("button"); + const button = getByText(/Add to cart/).closest("button"); const select = getByLabelText("Qty"); expect(button).toBeDisabled(); @@ -419,13 +424,14 @@ describe("", () => { const { getByText, getByLabelText } = render( ); - const button = getByText("Add to cart").closest("button"); + const button = getByText(/Add to cart/).closest("button"); const select = getByLabelText("Qty"); expect(button).toBeDisabled(); @@ -436,6 +442,7 @@ describe("", () => { const { queryByText, getByText, getByRole } = render( ", () => { const { getByText, getByRole } = render( ", () => { const { getByText } = render( ); - const button = getByText("Add to cart").closest("button"); + const button = getByText(/Add to cart/).closest("button"); expect(button).toBeDisabled(); }); }); diff --git a/hackathon_site/dashboard/frontend/src/components/inventory/ProductOverview3D/ProductOverview3D.test.tsx b/hackathon_site/dashboard/frontend/src/components/inventory/ProductOverview3D/ProductOverview3D.test.tsx index ae7a922..20871f7 100644 --- a/hackathon_site/dashboard/frontend/src/components/inventory/ProductOverview3D/ProductOverview3D.test.tsx +++ b/hackathon_site/dashboard/frontend/src/components/inventory/ProductOverview3D/ProductOverview3D.test.tsx @@ -24,6 +24,7 @@ import { SnackbarProvider } from "notistack"; import SnackbarNotifier from "components/general/SnackbarNotifier/SnackbarNotifier"; import { get } from "api/api"; import { getUpdatedHardwareDetails } from "slices/hardware/hardwareSlice"; +import { getUpdatedHardware3dDetails } from "slices/hardware/hardware3dSlice"; jest.mock("api/api", () => ({ ...jest.requireActual("api/api"), @@ -82,7 +83,7 @@ describe("", () => { // Check if the main section, detailInfoSection, and add to cart section works expect(getByText("Category")).toBeInTheDocument(); expect(getByText("Datasheet")).toBeInTheDocument(); - expect(getByText("Add to cart")).toBeInTheDocument(); + expect(getByText(/Add to cart/)).toBeInTheDocument(); }); test("Category label doesn't appear when there are no categories", async () => { @@ -132,7 +133,7 @@ describe("", () => { { store } ); - store.dispatch(getUpdatedHardwareDetails(1)); + store.dispatch(getUpdatedHardware3dDetails(1)); await waitFor(() => { expect(queryByTestId("circular-progress")).toBeInTheDocument(); @@ -170,7 +171,7 @@ describe("", () => { // Check if the main section, detailInfoSection, and add to cart section works expect(getByText("Category")).toBeInTheDocument(); expect(getByText("Datasheet")).toBeInTheDocument(); - expect(getByText("Add to cart")).toBeInTheDocument(); + expect(getByText(/Add to cart/)).toBeInTheDocument(); expect(queryByText("Notes")).not.toBeInTheDocument(); }); @@ -273,14 +274,18 @@ describe("", () => { ); // by default, quantity is 1 - const button = getByText("Add to cart"); + const button = getByText(/Add to cart/); await waitFor(() => { fireEvent.click(button); }); expect(cartSelectors.selectAll(store.getState())).toEqual([ - { hardware_id: mockHardware[0].id, quantity: 1 }, + { + hardware_id: mockHardware[0].id, + quantity: 1, + credits: mockHardware[0].credits, + }, ]); expect( getByText(`Added 1 ${mockHardware[0].name} item(s) to your cart.`) @@ -320,7 +325,7 @@ describe("", () => { ); // by default, quantity is 1 - const button = getByText("Add to cart"); + const button = getByText(/Add to cart/); await waitFor(() => { fireEvent.click(button); @@ -403,12 +408,13 @@ describe("", () => { ); - const button = getByText("Add to cart").closest("button"); + const button = getByText(/Add to cart/).closest("button"); const select = getByLabelText("Qty"); expect(button).toBeDisabled(); @@ -420,12 +426,13 @@ describe("", () => { ); - const button = getByText("Add to cart").closest("button"); + const button = getByText(/Add to cart/).closest("button"); const select = getByLabelText("Qty"); expect(button).toBeDisabled(); @@ -437,6 +444,7 @@ describe("", () => { @@ -453,6 +461,7 @@ describe("", () => { @@ -469,12 +478,13 @@ describe("", () => { ); - const button = getByText("Add to cart").closest("button"); + const button = getByText(/Add to cart/).closest("button"); expect(button).toBeDisabled(); }); }); diff --git a/hackathon_site/dashboard/frontend/src/components/orders/OrdersFilter/OrderFilter.test.tsx b/hackathon_site/dashboard/frontend/src/components/orders/OrdersFilter/OrderFilter.test.tsx index 691992c..fc07322 100644 --- a/hackathon_site/dashboard/frontend/src/components/orders/OrdersFilter/OrderFilter.test.tsx +++ b/hackathon_site/dashboard/frontend/src/components/orders/OrdersFilter/OrderFilter.test.tsx @@ -71,9 +71,7 @@ describe("", () => { fireEvent.click(resetButton); // check default filters are checked, nothing else - const expectedFilters: OrderFilters = { - status: [], - }; + const expectedFilters: OrderFilters = {}; await waitFor(() => { expect(store.getState()["adminOrder"]["filters"]).toEqual(expectedFilters); diff --git a/hackathon_site/dashboard/frontend/src/components/orders/OrdersSearch/OrdersSearch.tsx b/hackathon_site/dashboard/frontend/src/components/orders/OrdersSearch/OrdersSearch.tsx index 3970096..b68ae55 100644 --- a/hackathon_site/dashboard/frontend/src/components/orders/OrdersSearch/OrdersSearch.tsx +++ b/hackathon_site/dashboard/frontend/src/components/orders/OrdersSearch/OrdersSearch.tsx @@ -68,24 +68,30 @@ export const EnhancedOrderSearch = ({ // const initialValues = { // search: "", // }; + // Seed the search box from the persisted admin order filters (loaded from + // localStorage at store init). This is read once on mount; we intentionally + // do NOT use Formik's `enableReinitialize` here, because reinitializing on + // every filter change makes Formik fire `onReset`, which would clobber the + // search the user just submitted. const savedFilters = useSelector(adminOrderFiltersSelector); const initialValues = { search: savedFilters.search ?? "", - }; // Filter Bux Fix + }; const onSubmit = ({ search }: SearchValues) => { setFilters({ search }); getOrdersWithFilters(); }; + // Clearing the search box removes the search filter while preserving any + // other active filters (status, ordering, etc.). const onReset = () => { - setFilters(initialValues); + setFilters({ search: "" }); getOrdersWithFilters(); }; return ( { // }); test("Check all button checks and unchecks every row", async () => { const { getByTestId } = render(, { store }); - const currentOrder = mockPendingOrdersInTable[0]; + const currentOrder = mockPendingOrdersInTable[1]; const checkallBox = getByTestId(`checkall-${currentOrder.id}`).querySelector( 'input[type="checkbox"]' ); diff --git a/hackathon_site/dashboard/frontend/src/constants.js b/hackathon_site/dashboard/frontend/src/constants.js index 985c5a5..caa474d 100644 --- a/hackathon_site/dashboard/frontend/src/constants.js +++ b/hackathon_site/dashboard/frontend/src/constants.js @@ -1,4 +1,4 @@ -export const hackathonName = "MakeUofT 2026"; +export const hackathonName = "MakeUofT 2027"; export const adminGroup = "Hardware Site Admins"; export const minTeamSize = 2; export const maxTeamSize = 4; diff --git a/hackathon_site/dashboard/frontend/src/pages/Acknowledgement/Acknowledgement.test.tsx b/hackathon_site/dashboard/frontend/src/pages/Acknowledgement/Acknowledgement.test.tsx index 9a1e056..a8df440 100644 --- a/hackathon_site/dashboard/frontend/src/pages/Acknowledgement/Acknowledgement.test.tsx +++ b/hackathon_site/dashboard/frontend/src/pages/Acknowledgement/Acknowledgement.test.tsx @@ -215,11 +215,8 @@ describe("", () => { createProfileAPI, createProfileRequest ); - expect( - getByText( - `${mockUserWithoutProfile.first_name}, you're ready to get started. We've placed you in Team ${createProfileAPIResponse.data.team} but you can leave and join another team anytime.` - ) - ).toBeInTheDocument(); + expect(getByText(/you're ready to get started/i)).toBeInTheDocument(); + expect(getByText(createProfileAPIResponse.data.team)).toBeInTheDocument(); fireEvent.click(getByText("Let's Go!")); }); }); diff --git a/hackathon_site/dashboard/frontend/src/pages/Cart/Cart.test.tsx b/hackathon_site/dashboard/frontend/src/pages/Cart/Cart.test.tsx index 115c694..d9b6fa1 100644 --- a/hackathon_site/dashboard/frontend/src/pages/Cart/Cart.test.tsx +++ b/hackathon_site/dashboard/frontend/src/pages/Cart/Cart.test.tsx @@ -113,11 +113,11 @@ describe("Cart Page", () => { test("No items in the cart", async () => { const store = makeStoreWithEntities({ hardware: mockHardware }); - const { getByText } = render(, { store }); + const { getByText, getByTestId } = render(, { store }); expect(getByText(/no items in cart/i)).toBeInTheDocument(); // Test for quantity - expect(getByText(0)).toBeInTheDocument(); + expect(getByTestId("cart-quantity-total")).toHaveTextContent("0"); }); test("removeFromCart button", async () => { diff --git a/hackathon_site/dashboard/frontend/src/pages/Dashboard/Dashboard.test.tsx b/hackathon_site/dashboard/frontend/src/pages/Dashboard/Dashboard.test.tsx index 6d86592..6410214 100644 --- a/hackathon_site/dashboard/frontend/src/pages/Dashboard/Dashboard.test.tsx +++ b/hackathon_site/dashboard/frontend/src/pages/Dashboard/Dashboard.test.tsx @@ -75,6 +75,7 @@ describe("Dashboard Page", () => { const newHardwareData: Hardware = { id: mockCheckedOutOrders[0].items[0].hardware_id, name: "Randome hardware", + credits: 5, model_number: "90", manufacturer: "Tesla", datasheet: "", @@ -126,14 +127,6 @@ describe("Dashboard Page", () => { await waitFor(() => { expect(get).toHaveBeenNthCalledWith(5, hardwareDetailUri); expect(getByText("Product Overview")).toBeVisible(); - expect( - getByText(`- Max ${newHardwareData.max_per_team} of this item`) - ).toBeInTheDocument(); - expect( - getByText( - `- Max ${category.max_per_team} of items under category ${category.name}` - ) - ).toBeInTheDocument(); expect(getByText(newHardwareData.model_number)).toBeInTheDocument(); expect(getByText(newHardwareData.manufacturer)).toBeInTheDocument(); if (newHardwareData.notes) diff --git a/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.js b/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.js deleted file mode 100644 index 040ba35..0000000 --- a/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.js +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; -// import styles from "./IncidentForm.module.scss"; -import Header from "components/general/Header/Header"; -import Typography from "@material-ui/core/Typography"; - -const IncidentForm = () => { - return ( - <> -
- Broken/Lost Item Incident -

IEEEEEE

- - ); -}; - -export default IncidentForm; diff --git a/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.test.js b/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.test.js deleted file mode 100644 index f35a29c..0000000 --- a/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.test.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; - -import { render } from "testing/utils"; - -import IncidentForm from "./IncidentForm"; - -test("renders without crashing", () => { - const { getByText } = render(); - expect(getByText("IEEEEEE")).toBeInTheDocument(); -}); diff --git a/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.test.tsx b/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.test.tsx index 5573aff..93a4d58 100644 --- a/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.test.tsx +++ b/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.test.tsx @@ -24,22 +24,16 @@ describe("IncidentForm", () => { // }); it("renders IncidentFormRender when searchParams is not empty", () => { - const mockSearchParams: any = { - get: jest.fn(() => - JSON.stringify({ id: 10, quantityRequested: 3, quantityGranted: 2 }) - ), - toString: jest.fn(() => "mockQueryString"), - }; - - jest.spyOn(global, "URLSearchParams").mockImplementation( - () => mockSearchParams - ); - - render(); - - // Assert that mockSearchParams methods are called - expect(mockSearchParams.toString).toHaveBeenCalled(); - expect(mockSearchParams.get).toHaveBeenCalledWith("data"); + // Drive a real route so `useLocation().search` carries the `data` query + // param the component reads (it parses `?data=` from the URL). + const data = JSON.stringify({ + id: 10, + quantityRequested: 3, + quantityGranted: 2, + }); + render(, { + routerEntries: [`/incident-form?data=${encodeURIComponent(data)}`], + }); // Assert that the component renders IncidentFormRender expect(screen.queryByText("Item Incident Form")).toBeInTheDocument(); // Update the text according to your component's content diff --git a/hackathon_site/dashboard/frontend/src/pages/Inventory/Inventory.test.tsx b/hackathon_site/dashboard/frontend/src/pages/Inventory/Inventory.test.tsx index ba463e5..6c19210 100644 --- a/hackathon_site/dashboard/frontend/src/pages/Inventory/Inventory.test.tsx +++ b/hackathon_site/dashboard/frontend/src/pages/Inventory/Inventory.test.tsx @@ -177,14 +177,9 @@ describe("Inventory Page", () => { await waitFor(() => { expect(get).toHaveBeenNthCalledWith(3, hardwareDetailUri); expect(getByText("Product Overview")).toBeVisible(); - expect( - getByText(`- Max ${newHardwareData.max_per_team} of this item`) - ).toBeInTheDocument(); - expect( - getByText( - `- Max ${mockCategories[1].max_per_team} of items under category ${mockCategories[1].name}` - ) - ).toBeInTheDocument(); + // Per-item/category hardware limits ("Max N of this item") are no longer + // displayed (see commit "make old hardware limits unused"), so they are + // intentionally not asserted here. expect(getByText(newHardwareData.model_number)).toBeInTheDocument(); expect(getByText(newHardwareData.manufacturer)).toBeInTheDocument(); expect(getByText(newHardwareData.notes)).toBeInTheDocument(); diff --git a/hackathon_site/dashboard/frontend/src/pages/Inventory/Inventory.tsx b/hackathon_site/dashboard/frontend/src/pages/Inventory/Inventory.tsx index 542faab..1704248 100644 --- a/hackathon_site/dashboard/frontend/src/pages/Inventory/Inventory.tsx +++ b/hackathon_site/dashboard/frontend/src/pages/Inventory/Inventory.tsx @@ -31,7 +31,6 @@ import { getHardwareNextPage, getHardwareWithFilters, hardwareCountSelector, - hardwareFiltersSelector, hardwareSelectors, isLoadingSelector, isMoreLoadingSelector, @@ -65,48 +64,24 @@ const Inventory = () => { dispatch(getHardwareWithFilters()); }; - // Track if initial hardware fetch has been done - const hasInitialFetchRef = React.useRef(false); - - // On mount: clear filters and fetch categories + // On mount: clear filters, then fetch categories and hardware. The hardware + // fetch is unconditional so the inventory always loads, even if the + // "3D Printing" category doesn't exist (or its fetch is slow/fails). useEffect(() => { - hasInitialFetchRef.current = false; // Reset on mount dispatch(clearFilters()); dispatch(getCategories()); + dispatch(getHardwareWithFilters()); }, [dispatch]); - // Once categories are loaded, apply the exclude filter and fetch hardware (only once) + // Once the "3D Printing" category is known, exclude it from the inventory + // and refetch. If there is no such category, the inventory stays as-is. useEffect(() => { - if ( - threeDPrintingIdArray && - threeDPrintingIdArray.length > 0 && - !hasInitialFetchRef.current - ) { + if (threeDPrintingIdArray && threeDPrintingIdArray.length > 0) { dispatch(setFilters({ exclude_category_ids: threeDPrintingIdArray })); dispatch(getHardwareWithFilters()); - hasInitialFetchRef.current = true; } }, [dispatch, threeDPrintingIdArray]); - // Watch for when exclude filter is cleared (e.g., by Clear All button) and re-apply - const currentFilters = useSelector(hardwareFiltersSelector); - const hasExcludeFilter = - currentFilters.exclude_category_ids && - currentFilters.exclude_category_ids.length > 0; - - useEffect(() => { - // Only run after initial fetch is done and when exclude filter is missing - if ( - hasInitialFetchRef.current && - !hasExcludeFilter && - threeDPrintingIdArray && - threeDPrintingIdArray.length > 0 - ) { - dispatch(setFilters({ exclude_category_ids: threeDPrintingIdArray })); - dispatch(getHardwareWithFilters()); - } - }, [dispatch, hasExcludeFilter, threeDPrintingIdArray]); - // Participant-specific data (separate from hardware) useEffect(() => { if (userType === "participant") { diff --git a/hackathon_site/dashboard/frontend/src/pages/Threedprinting/Threedprinting.test.tsx b/hackathon_site/dashboard/frontend/src/pages/Threedprinting/Threedprinting.test.tsx index ba463e5..6c19210 100644 --- a/hackathon_site/dashboard/frontend/src/pages/Threedprinting/Threedprinting.test.tsx +++ b/hackathon_site/dashboard/frontend/src/pages/Threedprinting/Threedprinting.test.tsx @@ -177,14 +177,9 @@ describe("Inventory Page", () => { await waitFor(() => { expect(get).toHaveBeenNthCalledWith(3, hardwareDetailUri); expect(getByText("Product Overview")).toBeVisible(); - expect( - getByText(`- Max ${newHardwareData.max_per_team} of this item`) - ).toBeInTheDocument(); - expect( - getByText( - `- Max ${mockCategories[1].max_per_team} of items under category ${mockCategories[1].name}` - ) - ).toBeInTheDocument(); + // Per-item/category hardware limits ("Max N of this item") are no longer + // displayed (see commit "make old hardware limits unused"), so they are + // intentionally not asserted here. expect(getByText(newHardwareData.model_number)).toBeInTheDocument(); expect(getByText(newHardwareData.manufacturer)).toBeInTheDocument(); expect(getByText(newHardwareData.notes)).toBeInTheDocument(); diff --git a/hackathon_site/dashboard/frontend/src/setupTests.ts b/hackathon_site/dashboard/frontend/src/setupTests.ts index 611429f..a4f3d7e 100644 --- a/hackathon_site/dashboard/frontend/src/setupTests.ts +++ b/hackathon_site/dashboard/frontend/src/setupTests.ts @@ -14,3 +14,9 @@ window.matchMedia = removeListener: function () {}, }; }; + +// Keep tests isolated: some features persist state to localStorage +// (e.g. admin order filters), which would otherwise leak between tests. +afterEach(() => { + window.localStorage.clear(); +}); diff --git a/hackathon_site/dashboard/frontend/src/slices/hardware/cartSlice.test.ts b/hackathon_site/dashboard/frontend/src/slices/hardware/cartSlice.test.ts index 0fa0051..eda7c32 100644 --- a/hackathon_site/dashboard/frontend/src/slices/hardware/cartSlice.test.ts +++ b/hackathon_site/dashboard/frontend/src/slices/hardware/cartSlice.test.ts @@ -65,19 +65,21 @@ describe("addToCart action", () => { test("addToCart with new items", async () => { const store = makeStore(); - store.dispatch(addToCart({ hardware_id: 1, quantity: 2 })); + store.dispatch(addToCart({ hardware_id: 1, quantity: 2, credits: 5 })); - store.dispatch(addToCart({ hardware_id: 2, quantity: 3 })); + store.dispatch(addToCart({ hardware_id: 2, quantity: 3, credits: 5 })); await waitFor(() => { expect(cartSelectors.selectIds(store.getState())).toEqual([1, 2]); expect(cartSelectors.selectById(store.getState(), 1)).toEqual({ hardware_id: 1, quantity: 2, + credits: 5, }); expect(cartSelectors.selectById(store.getState(), 2)).toEqual({ hardware_id: 2, quantity: 3, + credits: 5, }); }); }); @@ -85,19 +87,21 @@ describe("addToCart action", () => { test("addToCart updates existing item", async () => { const store = makeStore(); - store.dispatch(addToCart({ hardware_id: 1, quantity: 2 })); - store.dispatch(addToCart({ hardware_id: 2, quantity: 3 })); - store.dispatch(addToCart({ hardware_id: 1, quantity: 3 })); + store.dispatch(addToCart({ hardware_id: 1, quantity: 2, credits: 5 })); + store.dispatch(addToCart({ hardware_id: 2, quantity: 3, credits: 5 })); + store.dispatch(addToCart({ hardware_id: 1, quantity: 3, credits: 5 })); await waitFor(() => { expect(cartSelectors.selectIds(store.getState())).toEqual([1, 2]); expect(cartSelectors.selectById(store.getState(), 1)).toEqual({ hardware_id: 1, quantity: 5, + credits: 5, }); expect(cartSelectors.selectById(store.getState(), 2)).toEqual({ hardware_id: 2, quantity: 3, + credits: 5, }); }); }); @@ -116,10 +120,12 @@ describe("removeFromCart action", () => { expect(cartSelectors.selectById(store.getState(), 1)).toEqual({ hardware_id: 1, quantity: 3, + credits: 5, }); expect(cartSelectors.selectById(store.getState(), 3)).toEqual({ hardware_id: 3, quantity: 2, + credits: 5, }); expect(cartSelectors.selectById(store.getState(), 2)).toEqual(undefined); }); @@ -143,6 +149,7 @@ describe("updateCart action", () => { expect(cartSelectors.selectById(store.getState(), 1)).toEqual({ hardware_id: mockCartItems[0].hardware_id, quantity: 25, + credits: 5, }); }); }); diff --git a/hackathon_site/dashboard/frontend/src/slices/hardware/hardware3dSlice.test.ts b/hackathon_site/dashboard/frontend/src/slices/hardware/hardware3dSlice.test.ts deleted file mode 100644 index 00f9973..0000000 --- a/hackathon_site/dashboard/frontend/src/slices/hardware/hardware3dSlice.test.ts +++ /dev/null @@ -1,301 +0,0 @@ -import { - createSlice, - createEntityAdapter, - PayloadAction, - createSelector, - createAsyncThunk, -} from "@reduxjs/toolkit"; -import { RootState, AppDispatch } from "slices/store"; - -import { APIListResponse, Hardware, HardwareFilters } from "api/types"; -import { get, stripHostnameReturnFilters } from "api/api"; -import { displaySnackbar } from "slices/ui/uiSlice"; - -interface HardwareExtraState { - isUpdateDetailsLoading: boolean; - isLoading: boolean; - isMoreLoading: boolean; - error: string | null; - next: string | null; - filters: HardwareFilters; - count: number; - hardwareIdInProductOverview: number | null; -} - -const extraState: HardwareExtraState = { - isUpdateDetailsLoading: false, - isLoading: false, - isMoreLoading: false, - error: null, - next: null, - filters: {}, - count: 0, - hardwareIdInProductOverview: null, -}; - -export const hardwareReducerName = "hardware"; -const hardwareAdapter = createEntityAdapter(); -export const initialState = hardwareAdapter.getInitialState(extraState); -export type HardwareState = typeof initialState; - -// Thunks -interface RejectValue { - status: number; - message: any; -} - -export const getHardwareWithFilters = createAsyncThunk< - APIListResponse, - { keepOld?: boolean } | undefined, - { state: RootState; rejectValue: RejectValue; dispatch: AppDispatch } ->( - `${hardwareReducerName}/getHardwareWithFilters`, - async (_, { dispatch, getState, rejectWithValue }) => { - const filters = hardwareFiltersSelector(getState()); - - try { - const response = await get>( - "/api/hardware/hardware/", - filters - ); - return response.data; - } catch (e: any) { - dispatch( - displaySnackbar({ - message: `Failed to fetch hardware data: Error ${e.response.status}`, - options: { variant: "error" }, - }) - ); - return rejectWithValue({ - status: e.response.status, - message: e.response.data, - }); - } - } -); - -export const getHardwareNextPage = createAsyncThunk< - APIListResponse | null, - void, - { state: RootState; rejectValue: RejectValue; dispatch: AppDispatch } ->( - `${hardwareReducerName}/getHardwareNextPage`, - async (_, { dispatch, getState, rejectWithValue }) => { - try { - const nextFromState = hardwareNextSelector(getState()); - if (nextFromState) { - const { path, filters } = stripHostnameReturnFilters(nextFromState); - const response = await get>(path, filters); - return response.data; - } - // return empty response if there is no nextURL - return null; - } catch (e: any) { - dispatch( - displaySnackbar({ - message: `Failed to fetch hardware data: Error ${e.response.status}`, - options: { variant: "error" }, - }) - ); - return rejectWithValue({ - status: e.response.status, - message: e.response.data, - }); - } - } -); - -export const getUpdatedHardwareDetails = createAsyncThunk< - Hardware | null, - number, - { state: RootState; rejectValue: RejectValue; dispatch: AppDispatch } ->( - `${hardwareReducerName}/getHardwareDetails`, - async (hardware_id, { dispatch, getState, rejectWithValue }) => { - try { - const response = await get( - `/api/hardware/hardware/${hardware_id}/` - ); - return response.data; - } catch (e: any) { - dispatch( - displaySnackbar({ - message: `Failed to fetch hardware data: Error ${e.response.status}`, - options: { variant: "error" }, - }) - ); - return rejectWithValue({ - status: e.response.status, - message: e.response.data, - }); - } - } -); - -// Slice -const hardwareSlice = createSlice({ - name: hardwareReducerName, - initialState, - reducers: { - /** - * Update the filters for the Hardware API - * - * To clear a particular filter, set the field to undefined. - */ - setFilters: ( - state: HardwareState, - { payload }: PayloadAction - ) => { - const { in_stock, hardware_ids, category_ids, search, ordering } = { - ...state.filters, - ...payload, - }; - - // Remove values that are empty or falsy - state.filters = { - ...(in_stock && { in_stock }), - ...(hardware_ids && hardware_ids.length > 0 && { hardware_ids }), - ...(category_ids && category_ids.length > 0 && { category_ids }), - ...(search && { search }), - ...(ordering && { ordering }), - }; - }, - - clearFilters: ( - state: HardwareState, - { payload }: PayloadAction<{ saveSearch?: boolean } | undefined> - ) => { - const { search } = state.filters; - - state.filters = {}; - - if (payload?.saveSearch && search) { - state.filters.search = search; - } - }, - - removeProductOverviewItem: (state: HardwareState) => { - state.hardwareIdInProductOverview = null; - }, - }, - extraReducers: (builder) => { - builder.addCase(getHardwareWithFilters.pending, (state) => { - state.isLoading = true; - state.error = null; - }); - - builder.addCase( - getHardwareWithFilters.fulfilled, - (state, { payload, meta }) => { - state.isLoading = false; - state.error = null; - state.next = payload.next; - state.count = payload.count; - - if (meta.arg?.keepOld) { - hardwareAdapter.setMany(state, payload.results); - } else { - hardwareAdapter.setAll(state, payload.results); - } - } - ); - - builder.addCase(getHardwareWithFilters.rejected, (state, { payload }) => { - state.isMoreLoading = false; - state.error = payload?.message || "Something went wrong"; - }); - - builder.addCase(getHardwareNextPage.pending, (state) => { - state.isLoading = false; - state.isMoreLoading = true; - state.error = null; - }); - - builder.addCase(getHardwareNextPage.fulfilled, (state, { payload }) => { - state.isMoreLoading = false; - state.error = null; - if (payload) { - state.next = payload.next; - state.count = payload.count; - hardwareAdapter.addMany(state, payload.results); - } - }); - - builder.addCase(getHardwareNextPage.rejected, (state, { payload }) => { - state.isMoreLoading = false; - state.error = payload?.message || "Something went wrong"; - }); - - builder.addCase(getUpdatedHardwareDetails.pending, (state) => { - state.isUpdateDetailsLoading = true; - }); - - builder.addCase(getUpdatedHardwareDetails.fulfilled, (state, { payload }) => { - if (payload) { - state.isUpdateDetailsLoading = false; - state.hardwareIdInProductOverview = payload.id; - hardwareAdapter.updateOne(state, { - id: payload.id, - changes: payload, - }); - } - }); - - builder.addCase(getUpdatedHardwareDetails.rejected, (state, { payload }) => { - state.isUpdateDetailsLoading = false; - }); - }, -}); - -export const { actions, reducer } = hardwareSlice; -export default reducer; - -export const { setFilters, clearFilters, removeProductOverviewItem } = actions; - -// Selectors -export const hardwareSliceSelector = (state: RootState) => state[hardwareReducerName]; - -export const hardwareSelectors = hardwareAdapter.getSelectors(hardwareSliceSelector); - -export const isLoadingSelector = createSelector( - [hardwareSliceSelector], - (hardwareSlice) => hardwareSlice.isLoading -); - -export const isMoreLoadingSelector = createSelector( - [hardwareSliceSelector], - (hardwareSlice) => hardwareSlice.isMoreLoading -); - -export const isUpdateDetailsLoading = createSelector( - [hardwareSliceSelector], - (hardwareSlice) => hardwareSlice.isUpdateDetailsLoading -); - -export const hardwareCountSelector = createSelector( - [hardwareSliceSelector], - (hardwareSlice) => hardwareSlice.count -); - -export const hardwareFiltersSelector = createSelector( - [hardwareSliceSelector], - (hardwareSlice) => hardwareSlice.filters -); - -export const hardwareNextSelector = createSelector( - [hardwareSliceSelector], - (hardwareSlice) => hardwareSlice.next -); - -export const selectHardwareByIds = createSelector( - [hardwareSelectors.selectEntities, (state: RootState, ids: number[]) => ids], - (entities, ids) => ids.map((id) => entities?.[id]) -); - -export const hardwareInProductOverviewSelector = createSelector( - [hardwareSelectors.selectEntities, hardwareSliceSelector], - (entities, hardwareSlice) => - hardwareSlice.hardwareIdInProductOverview - ? entities[hardwareSlice.hardwareIdInProductOverview] - : null -); diff --git a/hackathon_site/dashboard/frontend/src/testing/utils.tsx b/hackathon_site/dashboard/frontend/src/testing/utils.tsx index 44c8eaf..fa71d68 100644 --- a/hackathon_site/dashboard/frontend/src/testing/utils.tsx +++ b/hackathon_site/dashboard/frontend/src/testing/utils.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { BrowserRouter } from "react-router-dom"; +import { BrowserRouter, MemoryRouter } from "react-router-dom"; import { Provider } from "react-redux"; import { DeepPartial } from "@reduxjs/toolkit"; import { @@ -24,6 +24,11 @@ import { HardwareState, initialState as hardwareInitialState, } from "slices/hardware/hardwareSlice"; +import { + hardwareReducerName as hardware3dReducerName, + HardwareState as Hardware3dState, + initialState as hardware3dInitialState, +} from "slices/hardware/hardware3dSlice"; import { categoryReducerName, CategoryState, @@ -81,6 +86,10 @@ export const withStoreAndRouter = ( interface RenderOptions { preloadedState?: DeepPartial; store?: RootStore; + // When provided, the component is wrapped in a MemoryRouter seeded with these + // entries instead of a BrowserRouter. Use this to drive `useLocation()` (e.g. + // to seed query params) without touching the global window location. + routerEntries?: string[]; options?: Omit; } @@ -89,13 +98,20 @@ const customRender = ( { preloadedState = {}, store = makeStore(preloadedState), + routerEntries, ...renderOptions }: RenderOptions = {} ) => { + const Router = ({ children }: any) => + routerEntries ? ( + {children} + ) : ( + {children} + ); const wrapper = ({ children }: any) => ( - {children} + {children} ); @@ -175,11 +191,32 @@ export const makeStoreWithEntities = (entities: StoreEntities) => { } preloadedState[hardwareReducerName] = hardwareState; + + // The 3D-printing views (e.g. ProductOverview3D) read from a parallel + // "hardware3d" slice. Mirror the same entities + state into it so tests + // can seed either view through the single `hardware` entity list. + const hardware3dState: Hardware3dState = { + ...hardware3dInitialState, + ...entities.hardwareState, + ids: [], + entities: {}, + }; + + for (const hardware of entities.hardware) { + hardware3dState.ids.push(hardware.id); + hardware3dState.entities[hardware.id] = hardware; + } + + preloadedState[hardware3dReducerName] = hardware3dState; } else if (entities.hardwareState) { preloadedState[hardwareReducerName] = { ...hardwareInitialState, ...entities.hardwareState, }; + preloadedState[hardware3dReducerName] = { + ...hardware3dInitialState, + ...entities.hardwareState, + }; } if (entities.categories) { diff --git a/hackathon_site/event/jinja2/event/base.html b/hackathon_site/event/jinja2/event/base.html index 3135521..162cdbf 100644 --- a/hackathon_site/event/jinja2/event/base.html +++ b/hackathon_site/event/jinja2/event/base.html @@ -15,7 +15,7 @@ - {% block title %}{{ hackathon_name }} {{ localtime(event_start_date).strftime("%Y") }}{% endblock %} + {% block title %}{{ hackathon_name }} 2027{% endblock %} diff --git a/hackathon_site/event/jinja2/event/change_password.html b/hackathon_site/event/jinja2/event/change_password.html index d913ab7..60dfcc8 100644 --- a/hackathon_site/event/jinja2/event/change_password.html +++ b/hackathon_site/event/jinja2/event/change_password.html @@ -1,6 +1,6 @@ {% extends "event/form_base.html" %} -{% block title %}Change Password - MakeUofT 2026{% endblock %} +{% block title %}Change Password - MakeUofT 2027{% endblock %} {% block form_title %}Change Password{% endblock %} {% block nav_links %} diff --git a/hackathon_site/event/jinja2/event/change_password_done.html b/hackathon_site/event/jinja2/event/change_password_done.html index 1305457..7d9ce29 100644 --- a/hackathon_site/event/jinja2/event/change_password_done.html +++ b/hackathon_site/event/jinja2/event/change_password_done.html @@ -1,6 +1,6 @@ {% extends "event/base.html" %} -{% block title %}Change Password - MakeUofT 2026{% endblock %} +{% block title %}Change Password - MakeUofT 2027{% endblock %} {% block nav_links %}
  • Dashboard
  • Change Password
  • diff --git a/hackathon_site/event/jinja2/event/dashboard_base.html b/hackathon_site/event/jinja2/event/dashboard_base.html index 09b86bb..5d1ce76 100644 --- a/hackathon_site/event/jinja2/event/dashboard_base.html +++ b/hackathon_site/event/jinja2/event/dashboard_base.html @@ -15,7 +15,7 @@

    Dashboard

    {% if application is none %} {% if not is_registration_open() %}

    Applications have closed

    -

    Unfortunately, the deadline to apply for {{ hackathon_name }} was {{ localtime(registration_close_date).strftime("%B %-d, %Y") }}.

    +

    Unfortunately, the deadline to apply for {{ hackathon_name }} was {{ localtime(registration_close_date)|strftime("%B %-d, %Y") }}.


    If you have any questions or concerns, please contact us.

    {% else %} @@ -112,7 +112,7 @@

    Congratulations! You've been accepted into {{ hackatho {% elif review.status == "Waitlisted" %}

    You've been waitlisted for {{ hackathon_name }}

    The {{ hackathon_name }} team has reviewed your application, and have decided not to grant you a guaranteed spot - at {{ hackathon_name }} and to place you in our waitlist. On {{ waitlisted_acceptance_start_time.strftime("%B %-d, %Y, at %-I:%M %p") }}, we will begin allowing + at {{ hackathon_name }} and to place you in our waitlist. On {{ waitlisted_acceptance_start_time|strftime("%B %-d, %Y, at %-I:%M %p") }}, we will begin allowing people from the waitlist into the event on a first-come, first-serve basis if there is still room. We offer no guarantee that you will be allowed into the event, regardless of how early you arrive. Please read our participant package for all the info regarding the event if you plan on @@ -232,7 +232,7 @@

    Application FAQs

    Please send an email to {{ contact_email }} with your name, email you used to apply, expected arrival time, and reason as to why you’re arriving late and we’ll save your spot for you.

    How does the waitlist work the day of?

    -

    The rank on the waitlist is first-come first serve, meaning the sooner you arrive at the event on {{ waitlisted_acceptance_start_time.strftime("%A") }}, the higher your placement on our waitlist. At {{ waitlisted_acceptance_start_time.strftime("%-I:%M %p") }}, if we have any spots remaining, we will be letting waitlisted people in.

    +

    The rank on the waitlist is first-come first serve, meaning the sooner you arrive at the event on {{ waitlisted_acceptance_start_time.strftime("%A") }}, the higher your placement on our waitlist. At {{ waitlisted_acceptance_start_time|strftime("%-I:%M %p") }}, if we have any spots remaining, we will be letting waitlisted people in.

    Other questions?

    If you have any questions or concerns, feel free to contact us at {{ contact_email }}.

    diff --git a/hackathon_site/event/jinja2/event/landing.html b/hackathon_site/event/jinja2/event/landing.html index d0c08b2..901486f 100644 --- a/hackathon_site/event/jinja2/event/landing.html +++ b/hackathon_site/event/jinja2/event/landing.html @@ -4,9 +4,9 @@ {% block head_extends %} {% endblock %} @@ -23,13 +23,13 @@ {% block body %} -Major League Hacking 2026 Hackathon Season +Major League Hacking 2027 Hackathon Season
    -

    MakeUofT 2026

    +

    MakeUofT 2027

    Canada's Largest Makeathon

    @@ -42,8 +42,7 @@

    Canada's Largest Makeathon

    University of Toronto
    - {# Update this logic if your event ends in a different month or year #} - {{ localtime(event_start_date).strftime("%B %d") }}-{{ localtime(event_end_date).strftime("%d, %Y") }} + TBD, 2027