Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions src/app/components/elements/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface TabsProps {
expandable?: boolean;
singleLine?: boolean;
style?: TabStyle;
dataTestId?: string;
}

function callOrString(stringOrTabFunction: StringOrTabFunction | undefined, tabTitle: string, tabIndex: number) {
Expand All @@ -33,8 +34,8 @@ function callOrString(stringOrTabFunction: StringOrTabFunction | undefined, tabT
}

// e.g.: Tab 1 | Tab 2 | Tab 3
const TabNavbar = ({singleLine, children, tabTitleClass, activeTab, changeTab, className}: TabsProps & {activeTab: number; changeTab: (i: number) => void;}) => {
return <Nav tabs className={classNames(className, "flex-wrap", {"guaranteed-single-line": singleLine})}>
const TabNavbar = ({singleLine, children, tabTitleClass, activeTab, changeTab, className, dataTestId}: TabsProps & {activeTab: number; changeTab: (i: number) => void;}) => {
return <Nav tabs className={classNames(className, "flex-wrap", {"guaranteed-single-line": singleLine})} data-testid={dataTestId}>
{Object.keys(children).map((tabTitle, mapIndex) => {
const tabIndex = mapIndex + 1;
const linkClasses = callOrString(tabTitleClass, tabTitle, tabIndex);
Expand All @@ -53,7 +54,7 @@ const TabNavbar = ({singleLine, children, tabTitleClass, activeTab, changeTab, c
};

// e.g.: (Tab 1|Tab 2|Tab 3), i.e. as joined buttons
const ButtonNavbar = ({children, activeTab, changeTab, tabTitleClass="", className}: TabsProps & {activeTab: number; changeTab: (i: number) => void}) => {
const ButtonNavbar = ({children, activeTab, changeTab, tabTitleClass="", className, dataTestId}: TabsProps & {activeTab: number; changeTab: (i: number) => void}) => {
const gliderRef = useRef<HTMLSpanElement>(null);
const numberOfTabs = Object.keys(children).length;
const buttonRefs = useRef<(HTMLButtonElement | null)[]>(Array(numberOfTabs).fill(null));
Expand All @@ -68,7 +69,7 @@ const ButtonNavbar = ({children, activeTab, changeTab, tabTitleClass="", classNa
gliderRef.current.style.transform = `translate(${xOffset}px, ${yOffset}px)`;
}

return <div className={"w-100 text-center"}>
return <div className={"w-100 text-center"} data-testid={dataTestId}>
<ButtonGroup className={classNames(className, "selector-tabs")}>
{Object.keys(children).map((tabTitle, i) =>
<Button key={i} innerRef={el => buttonRefs.current[i] = el}
Expand All @@ -84,8 +85,8 @@ const ButtonNavbar = ({children, activeTab, changeTab, tabTitleClass="", classNa
</div>;
};

const DropdownNavbar = ({children, activeTab, changeTab, tabTitleClass, className}: TabsProps & {activeTab: number; changeTab: (i: number) => void}) => {
return <div className={classNames(className, "mt-3 mb-1")}>
const DropdownNavbar = ({children, activeTab, changeTab, tabTitleClass, className, dataTestId}: TabsProps & {activeTab: number; changeTab: (i: number) => void}) => {
return <div className={classNames(className, "mt-3 mb-1")} data-testid={dataTestId}>
{Object.keys(children).map((tabTitle, i) =>
<AffixButton key={tabTitle} color="tint" className={classNames("btn-dropdown me-2 mb-2", tabTitleClass, {"active": activeTab === i + 1})} onClick={() => changeTab(i + 1)} affix={{
affix: "icon-chevron-right",
Expand All @@ -99,8 +100,8 @@ const DropdownNavbar = ({children, activeTab, changeTab, tabTitleClass, classNam
</div>;
};

const CardsNavbar = ({children, activeTab, changeTab, tabTitleClass=""}: TabsProps & {activeTab: number; changeTab: (i: number) => void}) => {
return <div className="d-flex card-tabs">
const CardsNavbar = ({children, activeTab, changeTab, tabTitleClass="", dataTestId}: TabsProps & {activeTab: number; changeTab: (i: number) => void}) => {
return <div className="d-flex card-tabs" data-testid={dataTestId}>
{Object.keys(children).map((tabTitle, i) =>
<button key={i} className={classNames(tabTitleClass, "flex-grow-1 py-3 card-tab", {"active": activeTab === i + 1})} onClick={() => changeTab(i + 1)} type="button">
<Markup encoding={"latex"}>{tabTitle}</Markup>
Expand Down Expand Up @@ -143,12 +144,12 @@ export const Tabs = (props: TabsProps) => {
{expandButton}
<div className={classNames(className, innerClasses, `tab-style-${style}`, "position-relative")}>
{style === "tabs"
? <TabNavbar {...props} className="no-print" activeTab={activeTab} changeTab={changeTab}>{children}</TabNavbar>
? <TabNavbar {...props} className="no-print" activeTab={activeTab} changeTab={changeTab} dataTestId="tab-navbar">{children}</TabNavbar>
: style === "buttons"
? <ButtonNavbar {...props} activeTab={activeTab} changeTab={changeTab}>{children}</ButtonNavbar>
? <ButtonNavbar {...props} activeTab={activeTab} changeTab={changeTab} dataTestId="tab-navbar">{children}</ButtonNavbar>
: style === "dropdowns"
? <DropdownNavbar {...props} className={classNames({"no-print": isPhy}, tabNavbarClass)} activeTab={activeTab} changeTab={changeTab}>{children}</DropdownNavbar>
: <CardsNavbar {...props} activeTab={activeTab} changeTab={changeTab}>{children}</CardsNavbar>
? <DropdownNavbar {...props} className={classNames({"no-print": isPhy}, tabNavbarClass)} activeTab={activeTab} changeTab={changeTab} dataTestId="tab-navbar">{children}</DropdownNavbar>
: <CardsNavbar {...props} activeTab={activeTab} changeTab={changeTab} dataTestId="tab-navbar">{children}</CardsNavbar>
}
<ExpandableParentContext.Provider value={true}>
<TabContent activeTab={activeTab} className={tabContentClass}>
Expand All @@ -165,7 +166,7 @@ export const Tabs = (props: TabsProps) => {
{children}
</DropdownNavbar>
}
<TabPane key={tabTitle} tabId={tabIndex}>
<TabPane key={tabTitle} tabId={tabIndex} data-testid={tabIndex === activeTab ? "active-tab-pane" : undefined}>
{tabBody as ReactNode}
</TabPane>
</React.Fragment>;
Expand Down
4 changes: 2 additions & 2 deletions src/app/components/pages/quizzes/MyQuizzes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ const AssignedQuizTable = ({quizzes, boardOrder, setBoardOrder, emptyMessage}: {
<th/>
</tr>
</thead>
<tbody>
<tbody data-testid="assigned-quizzes">
{quizzes.map(quiz => {
return <TrLink to={quiz.link} key={quiz.id} className={classNames("align-middle", {"completed": quiz.status === QuizStatus.Complete}, {"overdue": quiz.status === QuizStatus.Overdue})}>
<td>
Expand Down Expand Up @@ -248,7 +248,7 @@ const PracticeQuizTable = ({quizzes, boardOrder, setBoardOrder, emptyMessage}: {
<th/>
</tr>
</thead>
<tbody>
<tbody data-testid="practice-quizzes">
{quizzes.map(quiz => {
return <TrLink to={quiz.link} key={quiz.id} tabIndex={0} className={classNames("align-middle", {"completed": quiz.status === QuizStatus.Complete})}>
<td>
Expand Down
1 change: 1 addition & 0 deletions src/app/services/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1276,6 +1276,7 @@ export const PATHS = {
}),
// Common paths
MANAGE_GROUPS: "/groups",
MY_TESTS: "/tests",
TEST: "/test/assignment",
PREVIEW_TEST: "/test/preview",
};
Expand Down
78 changes: 78 additions & 0 deletions src/mocks/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6191,9 +6191,87 @@ export const mockQuizAssignments = [
startDate: DAYS_AGO(new Date(SOME_FIXED_FUTURE_DATE), 3),
completedDate: DAYS_AGO(new Date(SOME_FIXED_FUTURE_DATE), 2)
}
},
{
id: 11,
quizId: "test-quiz-assignment-3",
quizSummary: {
id: "test-quiz-assignment-3",
title: "Test Quiz Assignment 3",
type: "isaacQuiz",
tags: [],
url: "/isaac-api/api/quiz/test-quiz-assignment-3",
visibleToStudents: false,
hiddenFromRoles: [
"TEACHER",
"STUDENT"
]
},
groupId: 2,
ownerId: mockUser.id,
assignerSummary: buildMockUserSummary(mockUser, false),
creationDate: DAYS_AGO(new Date(SOME_FIXED_FUTURE_DATE), 5),
dueDate: DAYS_AGO(new Date(SOME_FIXED_FUTURE_DATE), -5),
quizFeedbackMode: "DETAILED_FEEDBACK",
}
];

export const mockFreeAttempts = [
{
"id": 8,
"userId": 9,
"quizId": "test-free-attempt-1",
"quizSummary": {
"id": "test-free-attempt-1",
"title": "Practice Quiz 1",
"type": "isaacQuiz",
"tags": [],
"url": "/isaac-api/api/quiz/test-free-attempt-1",
"audience": [
{
"stage": [
"a_level"
]
},
{
"stage": [
"university"
]
}
],
"hiddenFromRoles": []
},
"startDate": DAYS_AGO(new Date(SOME_FIXED_FUTURE_DATE), 5),
},
{
"id": 9,
"userId": 9,
"quizId": "test-free-attempt-2",
"quizSummary": {
"id": "test-free-attempt-2",
"title": "Practice Quiz 2",
"type": "isaacQuiz",
"tags": [],
"url": "/isaac-api/api/quiz/test-free-attempt-2",
"audience": [
{
"stage": [
"a_level"
]
},
{
"stage": [
"university"
]
}
],
"hiddenFromRoles": []
},
"startDate": DAYS_AGO(new Date(SOME_FIXED_FUTURE_DATE), 5),
"completedDate": DAYS_AGO(new Date(SOME_FIXED_FUTURE_DATE), 2)
},
];

export const mockGroups = [
{
id: 2,
Expand Down
8 changes: 7 additions & 1 deletion src/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {
mockProgress,
mockLLMMarkedRegressionTestQuestion,
mockLLMMarkedValidationResponse,
mockSearchResults
mockSearchResults,
mockFreeAttempts
} from "./data";
import {API_PATH} from "../app/services";
import {produce} from "immer";
Expand Down Expand Up @@ -83,6 +84,11 @@ export const handlers = [
status: 200,
});
}),
http.get(API_PATH + "/quiz/free_attempts", () => {
return HttpResponse.json(mockFreeAttempts, {
status: 200,
});
}),
http.get(API_PATH + "/quiz/:quizId/rubric", ({ params }) => {
const quizId = params.quizId as string;
if (quizId in mockRubrics) {
Expand Down
81 changes: 81 additions & 0 deletions src/test/pages/MyQuizzes.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { MyQuizzes } from "../../app/components/pages/quizzes/MyQuizzes";
import { PATHS } from "../../app/services";
import { renderTestEnvironment, waitForLoaded } from "../testUtils";
import {screen, waitFor, within} from "@testing-library/react";

describe("My Tests", () => {

const renderMyTests = async () => {
await renderTestEnvironment({
PageComponent: MyQuizzes,
initalRouteEntries: [PATHS.MY_TESTS],
extraEndpoints: [],
});

await waitForLoaded();
await waitFor(() => {
expect(screen.queryByText("No tests match your filters.")).not.toBeInTheDocument();
});
};

const switchToPracticeTestsTab = async () => {
const tabs = await screen.findByTestId("tab-navbar");
const practiceTestsButton = await within(tabs).findByRole("button", {name: /Practice tests/i});
practiceTestsButton.click();
};

it("starts on the Assigned Tests tab", async () => {
await renderMyTests();
const tabs = await screen.findByTestId("tab-navbar");
const assignedTestsButton = await within(tabs).findByRole("button", {name: /Assigned tests/i});
expect(assignedTestsButton.parentElement).toHaveClass("active");
});

it("tests that are assigned, incomplete, and before their due date appear on load", async () => {
await renderMyTests();
const tabs = await screen.findByTestId("active-tab-pane");
const quizzes = await within(tabs).findByTestId("assigned-quizzes");
expect(quizzes.children).toHaveLength(1);
expect(quizzes).toHaveTextContent("Test Quiz Assignment 3");
});

it("tests that are assigned and completed appear when the Completed filter is selected", async () => {
await renderMyTests();

const completedFilter = await screen.findByLabelText("Complete");
completedFilter.click();

const tabs = await screen.findByTestId("active-tab-pane");
const quizzes = await within(tabs).findByTestId("assigned-quizzes");
expect(quizzes.children).toHaveLength(3);
expect(quizzes).toHaveTextContent("Test Quiz Assignment 1");
expect(quizzes).toHaveTextContent("Test Quiz Assignment 2");
expect(quizzes).toHaveTextContent("Test Quiz Assignment 3");
});

it("practice tests appear in the Practice Tests tab", async () => {
await renderMyTests();
await switchToPracticeTestsTab();

const tabs = await screen.findByTestId("active-tab-pane");
const quizzes = await within(tabs).findByTestId("practice-quizzes");

expect(quizzes.children).toHaveLength(1);
expect(quizzes).toHaveTextContent("Practice Quiz 1");
});

it("practice tests that are completed appear when the Completed filter is selected", async () => {
await renderMyTests();
await switchToPracticeTestsTab();

const completedFilter = await screen.findByLabelText("Complete");
completedFilter.click();

const tabs = await screen.findByTestId("active-tab-pane");
const quizzes = await within(tabs).findByTestId("practice-quizzes");

expect(quizzes.children).toHaveLength(2);
expect(quizzes).toHaveTextContent("Practice Quiz 1");
expect(quizzes).toHaveTextContent("Practice Quiz 2");
});
});