tests/project-ignored.spec.ts
Rama main · réplica de lectura del repositorio original.
import { expect, type Page } from "@playwright/test";
import {
IgnoredChange,
ScreenshotDiff,
} from "../apps/backend/src/database/models";
import {
createIgnoredChangeScenario,
createReviewableChangeScenario,
createSortableIgnoredChangesScenario,
} from "../apps/backend/src/database/seeds";
import { loggedTest } from "./logged-test";
import { ensureTeamOwner, screenshot } from "./util";
loggedTest.beforeEach(async ({ auth, team }) => {
await ensureTeamOwner({ team: team.team, user: auth.user });
});
loggedTest(
"ignored changes empty state",
async ({ page, team, project, auth }) => {
void auth;
await page.goto(`/${team.account.slug}/${project.name}`);
// The tab is reachable from the project navigation.
await page.getByRole("tab", { name: "Ignored" }).click();
await expect(
page.getByRole("heading", { name: "Nothing is ignored yet" }),
).toBeVisible();
// The empty state teaches the three parts of the flow.
await expect(
page.getByRole("heading", { name: "Flag the change" }),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Argos matches it exactly" }),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Keep the list honest" }),
).toBeVisible();
// The action comes first, with the docs link quietly beneath it.
await expect(
page.getByRole("link", { name: "Configure auto-ignore" }),
).toHaveAttribute(
"href",
`/${team.account.slug}/${project.name}/settings/flaky-detection`,
);
await expect(
page.getByRole("link", { name: "Learn more" }),
).toHaveAttribute(
"href",
"https://argos-ci.com/docs/learn/reliability-and-flakiness/ignored-changes",
);
await screenshot(page, "project-ignored-empty");
},
);
loggedTest(
"auto-ignored changes are badged",
async ({ page, team, project, auth }) => {
const { test } = await createIgnoredChangeScenario({
projectId: project.id,
userId: auth.user.id,
auto: true,
});
await page.goto(`/${team.account.slug}/${project.name}/ignored`);
const row = page.getByRole("row").filter({ hasText: test.name });
// The bot stays named as the author, with the badge alongside it.
await expect(row.getByText("Argos Bot")).toBeVisible();
const badge = row.getByText("Auto", { exact: true });
await expect(badge).toBeVisible();
// The tooltip explains it and offers the setting that governs it. The
// first pointer event on a freshly mounted virtualized row can be swallowed
// by the measuring re-render, so the hover is retried rather than asserted
// on a single attempt.
await expect(async () => {
await page.mouse.move(0, 0);
await badge.hover();
await expect(
page.getByRole("heading", { name: "Ignored automatically" }),
).toBeVisible({ timeout: 2_000 });
}).toPass();
await expect(
page.getByRole("link", { name: "Configure auto-ignore" }),
).toHaveAttribute(
"href",
`/${team.account.slug}/${project.name}/settings/flaky-detection`,
);
},
);
loggedTest("ignored changes list", async ({ page, team, project, auth }) => {
const { test } = await createIgnoredChangeScenario({
projectId: project.id,
userId: auth.user.id,
});
await page.goto(`/${team.account.slug}/${project.name}/ignored`);
await expect(
page.getByRole("heading", { name: "Ignored changes" }),
).toBeVisible();
const row = page.getByRole("row").filter({ hasText: test.name });
await expect(row).toBeVisible();
// Ignored by the seeded user, and it has fired 4 times since.
await expect(row.getByText("Kyle Bertolino")).toBeVisible();
await expect(row.getByText("4", { exact: true })).toBeVisible();
// The thumbnail is served from the CDN and actually renders.
await expect
.poll(() =>
row
.getByRole("img")
.first()
.evaluate((el: HTMLImageElement) => el.complete && el.naturalWidth > 0),
)
.toBe(true);
await screenshot(page, "project-ignored-list");
});
loggedTest(
"sorting ignored changes by column",
async ({ page, team, project, auth }) => {
await createSortableIgnoredChangesScenario({
projectId: project.id,
userId: auth.user.id,
});
await page.goto(`/${team.account.slug}/${project.name}/ignored`);
// Only the data rows link to their test; the header row holds buttons.
const rows = page.getByRole("row").filter({ has: page.getByRole("link") });
const header = (name: string) => page.getByRole("columnheader", { name });
await expect(header("Ignored")).toHaveAttribute("aria-sort", "descending");
await expect(rows).toContainText([
"dashboard.png",
"settings.png",
"checkout.png",
]);
await page.getByRole("button", { name: "Occurrences" }).click();
await expect(header("Occurrences")).toHaveAttribute(
"aria-sort",
"descending",
);
await expect(header("Ignored")).toHaveAttribute("aria-sort", "none");
await expect(rows).toContainText([
"settings.png",
"checkout.png",
"dashboard.png",
]);
await screenshot(page, "project-ignored-sorted");
// A second click flips the order, and the URL keeps it across a reload.
await page.getByRole("button", { name: "Occurrences" }).click();
await expect(rows).toContainText([
"dashboard.png",
"checkout.png",
"settings.png",
]);
await page.reload();
await expect(header("Occurrences")).toHaveAttribute(
"aria-sort",
"ascending",
);
await expect(rows).toContainText([
"dashboard.png",
"checkout.png",
"settings.png",
]);
await page.getByRole("button", { name: "Last seen" }).click();
await expect(rows).toContainText([
"checkout.png",
"dashboard.png",
"settings.png",
]);
},
);
loggedTest(
"unignoring a change removes it from the list, and Undo puts it back",
async ({ page, team, project, auth }) => {
const { test } = await createIgnoredChangeScenario({
projectId: project.id,
userId: auth.user.id,
});
await page.goto(`/${team.account.slug}/${project.name}/ignored`);
const row = page.getByRole("row").filter({ hasText: test.name });
// Revealed on hover, so it stays in the tab order: asserted on opacity,
// which Playwright's `toBeVisible` would not catch.
const unignore = row.getByRole("button", { name: "Unignore" });
await expect(unignore).toHaveCSS("opacity", "0");
await expect(async () => {
await page.mouse.move(0, 0);
await row.hover();
await expect(unignore).toHaveCSS("opacity", "1", { timeout: 2_000 });
}).toPass();
await unignore.click();
const dialog = page.getByRole("alertdialog");
await expect(
dialog.getByRole("heading", { name: "Unignore change" }),
).toBeVisible();
await dialog.getByRole("button", { name: "Unignore change" }).click();
// The row leaves the list and the page falls back to the empty state.
await expect(
page.getByRole("heading", { name: "Nothing is ignored yet" }),
).toBeVisible();
// The confirmation toast offers a way back.
await page.getByRole("button", { name: "Undo" }).click();
await expect(
page.getByRole("row").filter({ hasText: test.name }),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Nothing is ignored yet" }),
).toBeHidden();
},
);
loggedTest(
"ignoring and unignoring a change from the build toolbar",
async ({ page, team, project, auth }) => {
const { build } = await createIgnoredChangeScenario({
projectId: project.id,
userId: auth.user.id,
});
// The build page shows its snapshots only once the build has concluded.
await build.$query().patch({ conclusion: "changes-detected" });
const diff = await ScreenshotDiff.query()
.findOne({ buildId: build.id })
.throwIfNotFound();
await page.goto(
`/${team.account.slug}/${project.name}/builds/${build.number}/${diff.id}`,
);
const flag = page
.getByRole("button", { name: "Ignore change" })
.and(page.locator("[aria-pressed]"));
const dialog = page.getByRole("dialog");
await expect(flag).toHaveAttribute("aria-pressed", "true");
await flag.click();
await expect(
dialog.getByRole("heading", { name: "Unignore Change" }),
).toBeVisible();
await dialog.getByRole("button", { name: "Unignore Change" }).click();
await expect(flag).toHaveAttribute("aria-pressed", "false");
await expect(page.getByText("Change unignored")).toBeVisible();
await flag.click();
await expect(
dialog.getByRole("heading", { name: "Ignore Change" }),
).toBeVisible();
await dialog.getByRole("button", { name: "Ignore Change" }).click();
await expect(flag).toHaveAttribute("aria-pressed", "true");
await page.goto(`/${team.account.slug}/${project.name}/ignored`);
await expect(
page.getByRole("row").filter({ hasText: "penelope-argos.jpg" }),
).toBeVisible();
},
);
loggedTest(
"ignoring a change skips the dialog once it is dismissed for the session",
async ({ page, team, project, auth }) => {
const { build } = await createIgnoredChangeScenario({
projectId: project.id,
userId: auth.user.id,
});
await build.$query().patch({ conclusion: "changes-detected" });
// Drop the ignore so the flag starts on the side this test presses from.
await IgnoredChange.query().where("projectId", project.id).delete();
const diff = await ScreenshotDiff.query()
.findOne({ buildId: build.id })
.throwIfNotFound();
await page.goto(
`/${team.account.slug}/${project.name}/builds/${build.number}/${diff.id}`,
);
const flag = page
.getByRole("button", { name: "Ignore change" })
.and(page.locator("[aria-pressed]"));
await expect(flag).toHaveAttribute("aria-pressed", "false");
await page.evaluate(() => {
window.sessionStorage.setItem("ignoreChangeDontShowAgain", "true");
});
await flag.click();
await expect(page.getByRole("dialog")).toBeHidden();
await expect(flag).toHaveAttribute("aria-pressed", "true");
await page.goto(`/${team.account.slug}/${project.name}/ignored`);
await expect(
page.getByRole("row").filter({ hasText: "penelope-argos.jpg" }),
).toBeVisible();
},
);
loggedTest(
"ignoring a snapshot still to review keeps the flag when it comes back",
async ({ page, team, project }) => {
const { build, tests } = await createReviewableChangeScenario({
projectId: project.id,
});
const [ignored] = tests;
const diff = await ScreenshotDiff.query()
.findOne({ buildId: build.id, testId: ignored.id })
.throwIfNotFound();
const diffURL = `/${team.account.slug}/${project.name}/builds/${build.number}/${diff.id}`;
await page.goto(diffURL);
const flag = page
.getByRole("button", { name: "Ignore change" })
.and(page.locator("[aria-pressed]"));
await expect(flag).toHaveAttribute("aria-pressed", "false");
await flag.click();
await page
.getByRole("dialog")
.getByRole("button", { name: "Ignore Change" })
.click();
// Ignoring a snapshot still to review also marks it accepted, which moves
// to the next one and takes this toolbar with it.
await expect(page).not.toHaveURL(diffURL);
await openSnapshot(page, ignored.name, "Accepted");
await expect(page).toHaveURL(diffURL);
await expect(flag).toHaveAttribute("aria-pressed", "true");
},
);
loggedTest(
"ignoring an approved snapshot leaves it selected",
async ({ page, team, project }) => {
const { build, tests } = await createReviewableChangeScenario({
projectId: project.id,
});
const [approved] = tests;
const diff = await ScreenshotDiff.query()
.findOne({ buildId: build.id, testId: approved.id })
.throwIfNotFound();
await page.goto(
`/${team.account.slug}/${project.name}/builds/${build.number}/${diff.id}`,
);
await page.locator("button:has(.lucide-thumbs-up)").click();
const card = await openSnapshot(page, approved.name, "Accepted");
// The thick ring says which snapshot is being looked at. Asserted on the
// class because nothing else carries it.
const selected = card.locator("[class~='ring-3']");
await expect(selected).toBeVisible();
await page
.getByRole("button", { name: "Ignore change" })
.and(page.locator("[aria-pressed]"))
.click();
await page
.getByRole("dialog")
.getByRole("button", { name: "Ignore Change" })
.click();
await expect(
page
.getByRole("button", { name: "Ignore change" })
.and(page.locator("[aria-pressed]")),
).toHaveAttribute("aria-pressed", "true");
await expect(selected).toBeVisible();
},
);
/**
* Click a snapshot in the sidebar, opening its group when collapsed — where a
* reviewed one lands — and the list itself, which a deep link leaves on Info.
*/
async function openSnapshot(page: Page, name: string, group: string) {
await page.getByRole("tab", { name: "Snapshots" }).click();
const card = page.getByRole("button", { name });
await expect(async () => {
if (!(await card.isVisible())) {
await page.getByRole("button", { name: group }).click();
}
await expect(card).toBeVisible({ timeout: 1_000 });
}).toPass();
await card.click();
return card;
}