Skip to content
Merged
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
3 changes: 3 additions & 0 deletions integration_tests/pages/BasePage.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { Page } from "@playwright/test";
import { Header } from "./Header";
import { Modal } from "./Modal";
import { Notification } from "./Notification";

export abstract class BasePage {
header: Header;
modal: Modal;
notification: Notification;

constructor(public page: Page) {
this.page = page;
this.header = new Header(page);
this.modal = new Modal(page);
this.notification = new Notification(page);
}
Expand Down
24 changes: 24 additions & 0 deletions integration_tests/pages/Header.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Page } from "@playwright/test";
import {
HEADER_HELP_MENU_ID,
HEADER_HELP_MENU_BUTTON_TEST_ID,
} from "~client/components/Header.locators";
import { TAKE_TOUR_BUTTON_TEST_ID } from "~client/components/GuidedTour.locators";
import { TourModal } from "./TourModal";

export class Header {
helpMenuButton = this.page.getByTestId(HEADER_HELP_MENU_BUTTON_TEST_ID);
helpMenu = this.page.getByTestId(HEADER_HELP_MENU_ID);
takeTourButton = this.helpMenu.getByTestId(TAKE_TOUR_BUTTON_TEST_ID);

constructor(public page: Page) {
this.page = page;
}

async startTour(): Promise<TourModal> {
await this.helpMenuButton.click();
await this.takeTourButton.click();

return new TourModal(this.page);
}
}
2 changes: 2 additions & 0 deletions integration_tests/pages/LoginPage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BasePage } from "./BasePage";
import { LOCATOR_LOGIN_FORM } from "~client/constants/help";

export class LoginPage extends BasePage {
email = this.page.getByTestId("email");
Expand All @@ -7,4 +8,5 @@ export class LoginPage extends BasePage {
showPasswordBtn = this.page.locator(
"[aria-label='toggle password visibility']",
);
loginForm = this.page.locator(`#${LOCATOR_LOGIN_FORM}`);
}
2 changes: 2 additions & 0 deletions integration_tests/pages/ProjectPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { Page } from "@playwright/test";
import { BasePage } from "./BasePage";
import { TestRunList } from "./components/TestRunList";
import { BuildList } from "./components/BuildList";
import { LOCATOR_PROJECT_PAGE_SELECT_PROJECT } from "~client/constants/help";

export class ProjectPage extends BasePage {
testRunList: TestRunList;
buildList: BuildList;
selectProject = this.page.locator(`#${LOCATOR_PROJECT_PAGE_SELECT_PROJECT}`);

constructor(page: Page) {
super(page);
Expand Down
5 changes: 4 additions & 1 deletion integration_tests/pages/TestVariationListPage.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { BasePage } from "./BasePage";
import { LOCATOR_TEST_VARIATION_LIST_PAGE_SELECT_PROJECT } from "~client/constants/help";

export class TestVariationListPage extends BasePage {}
export class TestVariationListPage extends BasePage {
selectProject = this.page.locator(`#${LOCATOR_TEST_VARIATION_LIST_PAGE_SELECT_PROJECT}`);
}
34 changes: 34 additions & 0 deletions integration_tests/pages/TourModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Page } from "@playwright/test";

export class TourModal {
joyrideOverlay = this.page.locator('.react-joyride__tooltip');
skipButton = this.joyrideOverlay.getByRole("button", { name: /skip/i });
nextButton = this.joyrideOverlay.getByRole("button", { name: /next/i });
lastButton = this.joyrideOverlay.getByRole("button", { name: /last/i });
backButton = this.joyrideOverlay.getByRole("button", { name: /back/i });
closeButton = this.joyrideOverlay.getByRole("button", { name: /close/i });

constructor(public page: Page) {
this.page = page;
}

async skipTour() {
await this.skipButton.click();
}

async clickNext() {
await this.nextButton.click();
}

async clickLast() {
await this.lastButton.click();
}

async clickBack() {
await this.backButton.click();
}

async clickClose() {
await this.closeButton.click();
}
}
1 change: 1 addition & 0 deletions integration_tests/pages/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./Header";
export * from "./LoginPage";
export * from "./ProfilePage";
export * from "./ProjectListPage";
Expand Down
137 changes: 137 additions & 0 deletions integration_tests/test/guidedTour.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { expect } from "@playwright/test";
import { test } from "fixtures";
import {
TEST_BUILD_FAILED,
TEST_BUILD_PASSED,
TEST_BUILD_UNRESOLVED,
TEST_PROJECT,
TEST_RUN_APPROVED,
TEST_RUN_NEW,
TEST_RUN_OK,
TEST_UNRESOLVED,
TEST_VARIATION_ONE,
TEST_VARIATION_TWO,
} from "~client/_test/test.data.helper";
import {
LOGIN_PAGE_STEPS,
PROJECT_LIST_PAGE_STEPS,
PROJECT_PAGE_STEPS,
TEST_VARIATION_LIST_PAGE,
} from "~client/constants/help";
import {
mockGetBuildDetails,
mockGetBuilds,
mockGetProjects,
mockGetTestRuns,
mockGetTestVariations,
mockImage,
mockTestRun,
} from "utils/mocks";

const project = TEST_PROJECT;

test.beforeEach(async ({ page }) => {
await mockGetProjects(page, [project]);
await mockGetBuilds(page, project.id, [
TEST_BUILD_FAILED,
TEST_BUILD_PASSED,
TEST_BUILD_UNRESOLVED,
]);
await mockGetBuildDetails(page, TEST_BUILD_FAILED);
await mockGetTestRuns(page, TEST_BUILD_FAILED.id, [
TEST_UNRESOLVED,
TEST_RUN_APPROVED,
TEST_RUN_NEW,
TEST_RUN_OK,
]);
await mockTestRun(page, TEST_UNRESOLVED);
await mockGetTestVariations(page, project.id, [
TEST_VARIATION_ONE,
TEST_VARIATION_TWO,
]);
await mockImage(page, "baseline.png");
await mockImage(page, "diff.png");
await mockImage(page, "image.png");
await mockImage(page, "baseline1.png");
await mockImage(page, "baseline2.png");
});

test.describe("Guided Tour", () => {
test("should allow skipping the tour", async ({ loginPage }) => {
const tour = await loginPage.header.startTour();
await expect(tour.joyrideOverlay).toBeVisible();

await tour.skipTour();

await expect(tour.joyrideOverlay).not.toBeVisible();
});

test.describe("Login Page", () => {
test("should display all tour steps", async ({ loginPage }) => {
const tour = await loginPage.header.startTour();

await expect(tour.joyrideOverlay).toContainText(LOGIN_PAGE_STEPS[0].content as string);
await tour.clickNext();

await expect(tour.joyrideOverlay).toContainText(LOGIN_PAGE_STEPS[1].content as string);
await tour.clickLast();

await expect(tour.joyrideOverlay).not.toBeVisible();
});
});

test.describe("Project List Page", () => {
test("should display all tour steps", async ({ projectListPage }) => {
const tour = await projectListPage.header.startTour();

await expect(tour.joyrideOverlay).toContainText(PROJECT_LIST_PAGE_STEPS[0].title as string);
await tour.clickLast();

await expect(tour.joyrideOverlay).not.toBeVisible();
});
});

test.describe("Project Page", () => {
test("should display all tour steps", async ({ openProjectPage }) => {
const projectPage = await openProjectPage(project.id);
await projectPage.buildList.getBuildLocator(TEST_BUILD_FAILED.number).click();

const tour = await projectPage.header.startTour();

await expect(tour.joyrideOverlay).toContainText(PROJECT_PAGE_STEPS[0].content as string);
await tour.clickNext();

await expect(tour.joyrideOverlay).toContainText(PROJECT_PAGE_STEPS[1].content as string);
await tour.clickNext();

await expect(tour.joyrideOverlay).toContainText(PROJECT_PAGE_STEPS[2].content as string);
await tour.clickNext();

await expect(tour.joyrideOverlay).toContainText(PROJECT_PAGE_STEPS[3].content as string);
await tour.clickNext();

await expect(tour.joyrideOverlay).toContainText(PROJECT_PAGE_STEPS[4].content as string);
await tour.clickLast();

await expect(tour.joyrideOverlay).not.toBeVisible();
});
});

test.describe("Test Variation List Page", () => {
test("should display all tour steps", async ({ openTestVariationListPage }) => {
const variationListPage = await openTestVariationListPage(project.id);
const tour = await variationListPage.header.startTour();

await expect(tour.joyrideOverlay).toContainText(TEST_VARIATION_LIST_PAGE[0].title);
await tour.clickNext();

await expect(tour.joyrideOverlay).toContainText(TEST_VARIATION_LIST_PAGE[1].content);
await tour.clickNext();

await expect(tour.joyrideOverlay).toContainText(TEST_VARIATION_LIST_PAGE[2].content);
await tour.clickLast();

await expect(tour.joyrideOverlay).not.toBeVisible();
});
});
});
1 change: 1 addition & 0 deletions src/components/GuidedTour.locators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const TAKE_TOUR_BUTTON_TEST_ID = "takeTourButton";
89 changes: 46 additions & 43 deletions src/components/GuidedTour.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,71 +3,74 @@ import Joyride, { CallBackProps, STATUS } from "react-joyride";
import { Button } from "@mui/material";
import { useHelpState } from "../contexts";
import { LiveHelp } from "@mui/icons-material";
import { TAKE_TOUR_BUTTON_TEST_ID } from "./GuidedTour.locators";

const GuidedTour: FunctionComponent = () => {
const [run, setRun] = React.useState(false);
const { helpSteps } = useHelpState();

const getHelpSteps = React.useCallback(() => {
const [firstStep] = helpSteps;

//Below line is to prevent application breaking if element is not present for any reason (e.g. if the user deletes build or if there is no data.)
if (
firstStep &&
document.getElementById(firstStep.target.toString().slice(1))
) {
for (const step of helpSteps) {
step.disableBeacon = true;
step.hideCloseButton = true;
}
if (!helpSteps?.length) {
return [];
}

return helpSteps;
for (const step of helpSteps) {
step.disableBeacon = true;
step.hideCloseButton = true;
}

return [];
return helpSteps;
}, [helpSteps]);

const handleJoyrideCallback = ({ status }: CallBackProps) => {
const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED];

if (finishedStatuses.includes(status)) {
if (status === STATUS.FINISHED || status === STATUS.SKIPPED) {
setRun(false);
}
};

const handleClickStart = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();

const [firstStep] = helpSteps;
if (firstStep && typeof firstStep.target === 'string') {
const targetId = firstStep.target.startsWith('#')
? firstStep.target.slice(1)
: firstStep.target;

if (!document.getElementById(targetId)) {
return;
}
}

setRun(true);
};

return (
<React.Fragment>
<Button startIcon={<LiveHelp />} onClick={handleClickStart}>
<Joyride
callback={handleJoyrideCallback}
continuous={true}
run={run}
scrollToFirstStep={true}
showProgress={true}
showSkipButton={true}
steps={getHelpSteps()}
disableCloseOnEsc={true}
styles={{
options: {
zIndex: 10000,
},
buttonNext: {
color: "#3f51b5",
backgroundColor: "",
},
buttonBack: {
color: "#3f51b5",
},
}}
/>
Take a tour
</Button>
</React.Fragment>
<Button startIcon={<LiveHelp />} onClick={handleClickStart} data-testid={TAKE_TOUR_BUTTON_TEST_ID}>
<Joyride
callback={handleJoyrideCallback}
continuous={true}
run={run}
scrollToFirstStep={true}
showProgress={true}
showSkipButton={true}
steps={getHelpSteps()}
disableCloseOnEsc={true}
styles={{
options: {
zIndex: 10000,
},
buttonNext: {
color: "#3f51b5",
backgroundColor: "",
},
buttonBack: {
color: "#3f51b5",
},
}}
/>
Take a tour
</Button>
);
};

Expand Down
4 changes: 4 additions & 0 deletions src/components/Header.locators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const HEADER_HELP_MENU_ID = "headerHelpMenu";
export const HEADER_AVATAR_MENU_ID = "headerAvatarMenu";
export const HEADER_HELP_MENU_BUTTON_TEST_ID = "helpMenuButton";
export const HEADER_LOGOUT_BUTTON_TEST_ID = "logoutBtn";
Loading