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
24 changes: 22 additions & 2 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import tseslint from "typescript-eslint";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import jsxA11y from "eslint-plugin-jsx-a11y";
import sonarjs from "eslint-plugin-sonarjs";
import prettier from "eslint-config-prettier";
import globals from "globals";

Expand Down Expand Up @@ -34,6 +35,18 @@ export default tseslint.config(
react.configs.flat.recommended,
react.configs.flat["jsx-runtime"],
jsxA11y.flatConfigs.recommended,
// the rules SonarCloud runs on every PR, so they fail here first
sonarjs.configs.recommended,
// Warnings, not errors: an open-task comment is debt to see, not a reason
// to fail the build, and an async function with nothing to await (Sonar's
// S7503, which the plugin leaves to require-await) is common in older tests
// and mocks. SonarCloud flags both in new code only.
{
rules: {
"sonarjs/todo-tag": "warn",
"require-await": "warn",
},
},
{
plugins: { "react-hooks": reactHooks },
rules: reactHooks.configs.recommended.rules,
Expand Down Expand Up @@ -108,9 +121,16 @@ export default tseslint.config(
},
},
{
// Playwright fixtures call a parameter named use(), which is not a hook
// Playwright fixtures call a parameter named use(), which is not a hook.
// Specs assert through shared helpers and page objects, which the
// assertion check does not follow, and `void fixture` asks for a fixture
// that is wanted only for its setup.
files: ["integration_tests/**"],
rules: { "react-hooks/rules-of-hooks": "off" },
rules: {
"react-hooks/rules-of-hooks": "off",
"sonarjs/assertions-in-tests": "off",
"sonarjs/void-use": "off",
},
},
prettier,
);
3 changes: 3 additions & 0 deletions integration_tests/fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { TestVariationDetailsPage } from "pages/TestVariationDetailsPage";
import { TestVariationListPage } from "pages/TestVariationListPage";
import { UserListPage } from "pages/UserListPage";
import type { User } from "~client/types";
import { mockGetFavoriteProjects } from "utils/mocks";

type Options = {
// the role the signed-in user has; override with test.use({ userRole })
Expand Down Expand Up @@ -113,6 +114,8 @@ export const test = base.extend<Fixtures & Options>({
},
JSON.stringify({ ...AUTH_USER, role: userRole }),
);
// every signed-in page asks for them; a test sets its own after this
await mockGetFavoriteProjects(page, []);

await use();
},
Expand Down
17 changes: 16 additions & 1 deletion integration_tests/pages/ProjectListPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { BasePage } from "./BasePage";

export class ProjectListPage extends BasePage {
projects = this.page.locator("#projectlist-1");
addBtn = this.page.getByRole("button", { name: "add" });
// exact: the cards' "Add to favourites" would match a part of the name
addBtn = this.page.getByRole("button", { name: "Add project", exact: true });
deleteBtn = this.page.getByTestId("delete");
editBtn = this.page.getByTestId("EditIcon");
search = this.page.getByRole("searchbox", { name: "Search projects" });

// ProjectForm, inside the create or update dialog
dialog = this.modal.dialog;
Expand Down Expand Up @@ -56,6 +58,19 @@ export class ProjectListPage extends BasePage {
});
}

// "Favourites", "Recently opened", "Other projects"
section(name: string) {
return this.page.getByRole("region", { name });
}

starBtn(name: string) {
return this.project(name).getByRole("button", { name: /favourites/ });
}

buildsLink(name: string) {
return this.project(name).getByRole("link", { name: "Builds" });
}

async selectImageComparison(value: string) {
await this.imageComparison.click();
await this.page.getByRole("option", { name: value, exact: true }).click();
Expand Down
2 changes: 2 additions & 0 deletions integration_tests/pages/ProjectPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export class ProjectPage extends BasePage {
buildList: BuildList;
details: TestDetailsDialog;
selectProject = this.page.locator(`#${LOCATOR_PROJECT_PAGE_SELECT_PROJECT}`);
// the switcher's field: typing narrows the projects, Enter opens one
projectField = this.page.getByRole("combobox", { name: "Project" });
detailsDialog = this.page.getByRole("dialog");

constructor(page: Page) {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified integration_tests/test/buildMenu.spec.ts-snapshots/build-menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified integration_tests/test/oidc.spec.ts-snapshots/profile-page-sso.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions integration_tests/test/profile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
mockGetProjects,
mockUpdateUser,
} from "utils/mocks";
import { waitForPage } from "utils/waitForPage";

const project = TEST_PROJECT;

Expand All @@ -16,6 +17,7 @@ test.beforeEach(async ({ page }) => {

// eslint-disable-next-line @typescript-eslint/no-unused-vars
test("renders", async ({ profilePage, page }) => {
await waitForPage(page);
await expect(page).toHaveScreenshot("profile-page.png");
});

Expand Down
Binary file modified integration_tests/test/profile.spec.ts-snapshots/profile-page.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 19 additions & 3 deletions integration_tests/test/projec.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,13 +392,30 @@ test("can download images", async ({ openProjectPage }) => {
);
});

test("switches to another project by typing its name", async ({
openProjectPage,
page,
}) => {
const other = { ...project, id: "other-project-id", name: "Other project" };
await mockGetProjects(page, [project, other]);
await mockGetBuilds(page, other.id, []);
const projectPage = await openProjectPage(project.id);
await expect(projectPage.projectField).toHaveValue(project.name);

await projectPage.projectField.fill("other");
await page.getByRole("option", { name: "Other project" }).click();

await expect(page).toHaveURL(new RegExp(`/${other.id}$`));
await expect(projectPage.projectField).toHaveValue("Other project");
});

test("lines the Project label up with the select under it", async ({
openProjectPage,
page,
}) => {
await openProjectPage(project.id);

const label = await page.locator("#projectSelect").boundingBox();
const label = await page.locator("#project-select-label").boundingBox();
const select = await page.locator("#project-select").boundingBox();

expect(label.x).toBe(select.x);
Expand Down Expand Up @@ -458,8 +475,7 @@ const overlaidOpacity = (page) =>
.opacity();
});

const diffSwitch = (page) =>
page.getByRole("switch", { name: "Toggle diff" });
const diffSwitch = (page) => page.getByRole("switch", { name: "Toggle diff" });

test("keeps both panes and fades the image over the baseline", async ({
openProjectPage,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
155 changes: 155 additions & 0 deletions integration_tests/test/projectList.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@ import {
PIXELMATCH_DEFAULT_CONFIG,
} from "~client/constants/project";
import { ImageComparison } from "~client/types/imageComparison";
import { ProjectListPage } from "pages";
import type { Project } from "~client/types";
import {
API_URL,
mockDeleteProject,
mockDinov2Status,
mockGetBuilds,
mockGetFavoriteProjects,
mockGetProjects,
mockSaveProject,
mockToggleFavoriteProject,
} from "utils/mocks";
import { waitForPage } from "utils/waitForPage";

const project = TEST_PROJECT;

Expand All @@ -23,6 +29,7 @@ test.beforeEach(async ({ page }) => {

// eslint-disable-next-line @typescript-eslint/no-unused-vars
test("renders", async ({ projectListPage, page }) => {
await waitForPage(page);
await expect(page).toHaveScreenshot("projects-list-page.png");
});

Expand Down Expand Up @@ -158,6 +165,154 @@ test("edits a project", async ({ projectListPage, page }) => {
await expect(projectListPage.project(project.name)).toHaveCount(0);
});

const named = (
id: string,
name: string,
mainBranchName = "master",
): Project => ({
...project,
id,
name,
mainBranchName,
});
const ALPHA = named("alpha-id", "Alpha");
const BETA = named("beta-id", "Beta", "develop");
const GAMMA = named("gamma-id", "Gamma");

// opened straight, with the API's projects and favourites set up first
const openWith = async (
page: Page,
projects: Project[],
favorites: string[] = [],
path = "/projects",
) => {
await mockGetProjects(page, projects);
await mockGetFavoriteProjects(page, favorites);
await page.goto(path);
const projectListPage = new ProjectListPage(page);
await expect(projectListPage.projects.first()).toBeVisible();
return projectListPage;
};

test.describe("favourites and search", () => {
test("stars a project, tells the API and lists it first", async ({
page,
authUser,
}) => {
void authUser;
const projectListPage = await openWith(page, [ALPHA, BETA]);
await mockToggleFavoriteProject(page);

const [request] = await Promise.all([
page.waitForRequest(`${API_URL}/projects/${BETA.id}/favorite`),
projectListPage.starBtn("Beta").click(),
]);

expect(request.method()).toBe("PUT");
await expect(
projectListPage
.section("Favourites")
.getByRole("heading", { name: "Beta" }),
).toBeVisible();
await expect(projectListPage.starBtn("Beta")).toHaveAttribute(
"aria-pressed",
"true",
);
});

test("takes a project out of the favourites", async ({ page, authUser }) => {
void authUser;
const projectListPage = await openWith(page, [ALPHA, BETA], [BETA.id]);
await mockToggleFavoriteProject(page);

const [request] = await Promise.all([
page.waitForRequest(`${API_URL}/projects/${BETA.id}/favorite`),
projectListPage.starBtn("Beta").click(),
]);

expect(request.method()).toBe("DELETE");
await expect(projectListPage.section("Favourites")).toHaveCount(0);
});

test("lists the favourites the API keeps above the other projects", async ({
page,
authUser,
}) => {
void authUser;
const projectListPage = await openWith(
page,
[ALPHA, BETA, GAMMA],
[GAMMA.id],
);

await expect(
projectListPage.section("Favourites").getByRole("heading", { level: 3 }),
).toHaveText(["Gamma"]);
await expect(
projectListPage
.section("Other projects")
.getByRole("heading", { level: 3 }),
).toHaveText(["Alpha", "Beta"]);
});

test("narrows the projects to the search and keeps it in the link", async ({
page,
authUser,
}) => {
void authUser;
const projectListPage = await openWith(page, [ALPHA, BETA, GAMMA]);

await projectListPage.search.fill("develop");

await expect(projectListPage.projects).toHaveCount(1);
await expect(projectListPage.project("Beta")).toBeVisible();
await expect(page).toHaveURL(/[?&]q=develop/);
});

test("opens with the search a link carries", async ({ page, authUser }) => {
void authUser;
const projectListPage = await openWith(
page,
[ALPHA, BETA],
[],
"/projects?q=alp",
);

await expect(projectListPage.search).toHaveValue("alp");
await expect(projectListPage.projects).toHaveCount(1);
});

test("puts the cursor in the search on /", async ({ page, authUser }) => {
void authUser;
const projectListPage = await openWith(page, [ALPHA, BETA]);

await page.keyboard.press("/");

await expect(projectListPage.search).toBeFocused();
await expect(projectListPage.search).toHaveValue("");
});

test("opens a project's builds without reloading the app", async ({
page,
authUser,
}) => {
void authUser;
const projectListPage = await openWith(page, [ALPHA]);
await mockGetBuilds(page, ALPHA.id, []);
// a full page load would start over without this
await page.evaluate(() => {
(window as unknown as { kept?: boolean }).kept = true;
});

await projectListPage.buildsLink("Alpha").click();

await expect(page).toHaveURL(new RegExp(`/${ALPHA.id}$`));
expect(
await page.evaluate(() => (window as unknown as { kept?: boolean }).kept),
).toBe(true);
});
});

// The rendering-noise filter is switched on in the library's config, where a
// library that supports it offers it; the backend applies it to every library.
const savedConfig = (request: Request) =>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions integration_tests/test/register.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { test } from "fixtures";
import { expect } from "@playwright/test";
import { API_URL, mockApiError } from "utils/mocks";
import { waitForPage } from "utils/waitForPage";

// eslint-disable-next-line @typescript-eslint/no-unused-vars
test("renders", async ({ registerPage, page }) => {
await waitForPage(page);
await expect(page).toHaveScreenshot("register-page.png");
});

Expand Down
Loading
Loading