tests/build.spec.ts
Rama main · réplica de lectura del repositorio original.
import { expect, type Page } from "@playwright/test";
import {
Build,
BuildReview,
ScreenshotDiff,
} from "../apps/backend/src/database/models";
import {
BuildScenario,
createBuildScenario,
createFallbackBaselineScenario,
createSiblingBuildsScenario,
createVariantSwitchersScenario,
} from "../apps/backend/src/database/seeds";
import { loggedTest } from "./logged-test";
import { ensureTeamOwner, phoneScreenshot, screenshot } from "./util";
const buildExamples: {
name: string;
getNumber: (builds: BuildScenario) => number;
compare?: false;
/**
* Assert every thumbnail in the diff list actually decoded. Only worth doing
* on a build whose snapshots point at the CDN image fixtures: a key nobody
* uploaded renders as a broken image, and the screenshots below then burn
* their whole stabilization budget waiting on the CDN to answer the miss.
*/
expectLoadedImages?: true;
/** Also capture the build as a phone shows it: once is enough. */
phone?: true;
}[] = [
{ name: "orphan", getNumber: (b) => b.orphanBuild.number },
{ name: "reference", getNumber: (b) => b.referenceBuild.number },
{
name: "expired",
getNumber: (b) => b.expiredBuild.number,
compare: false,
},
{
name: "aborted",
getNumber: (b) => b.abortedBuild.number,
compare: false,
},
{ name: "error", getNumber: (b) => b.errorBuild.number, compare: false },
{
name: "changes detected",
getNumber: (b) => b.diffDetectedBuild.number,
expectLoadedImages: true,
phone: true,
},
{ name: "rejected", getNumber: (b) => b.rejectedBuild.number },
{
name: "scheduled",
getNumber: (b) => b.pendingBuild.number,
compare: false,
},
{
name: "in progress",
getNumber: (b) => b.inProgressBuild.number,
compare: false,
},
{ name: "empty", getNumber: (b) => b.emptyBuild.number, compare: false },
{ name: "stable", getNumber: (b) => b.stableBuild.number },
{
name: "stable with fail screenshots",
getNumber: (b) => b.failBuild.number,
},
{
name: "stable with removed screenshots",
getNumber: (b) => b.removedBuild.number,
},
{ name: "subset", getNumber: (b) => b.subsetBuild.number },
];
function getStartButton(page: Page) {
return page.getByRole("button", {
name: /^(Start review|Browse snapshots|Browse test failures)/,
});
}
buildExamples.forEach((build) => {
loggedTest(build.name, async ({ page, auth, team, project, builds }) => {
await ensureTeamOwner({ team: team.team, user: auth.user });
const number = build.getNumber(builds);
await page.goto(`/${team.account.slug}/${project.name}/builds/${number}`);
await expect(page.getByText(`Build ${number}`)).toBeVisible();
if (build.compare !== false) {
await expect(page).toHaveURL(new RegExp(`/builds/${number}/overview$`));
await expect(getStartButton(page)).toBeVisible();
if (build.expectLoadedImages) {
// `shown` guards against passing before the virtualized diff list has
// mounted any thumbnail, and `broken` names the offending URLs rather
// than reporting a bare boolean.
await expect
.poll(() =>
page.evaluate(() => {
const fixtures = Array.from(document.images).filter((img) =>
/-\d+x\d+\.(png|jpg)/.test(img.currentSrc),
);
return {
shown: fixtures.length > 0,
broken: fixtures
.filter((img) => !(img.complete && img.naturalWidth > 0))
.map((img) => new URL(img.currentSrc).pathname),
};
}),
)
.toEqual({ shown: true, broken: [] });
}
await screenshot(page, `build-overview-${build.name}`, {
replacements: {
[team.account.slug]: "acme",
},
});
if (build.phone) {
await phoneScreenshot(page, `build-overview-${build.name}`, {
ready: (phone) => expect(getStartButton(phone)).toBeVisible(),
replacements: { [team.account.slug]: "acme" },
});
}
// Starting the review replaces the overview in history (the whole build
// view is a single history entry), so navigate forward rather than relying
// on `goBack` to return to the overview.
await page.keyboard.press("Enter");
await expect(page).toHaveURL(
new RegExp(`/builds/${number}/(?!overview)[^/]+$`),
);
// The pane's own heading, not any of the other places the word appears
// (the view-mode button, the sidebar's changes count).
await expect(
page.getByRole("heading", { name: "Changes", exact: true }),
).toBeVisible();
}
await screenshot(page, `build-${build.name}`, {
replacements: {
[team.account.slug]: "acme",
},
});
if (build.phone) {
await phoneScreenshot(page, `build-${build.name}`, {
ready: (phone) =>
expect(
phone.getByRole("heading", { name: "Changes", exact: true }),
).toBeVisible(),
replacements: { [team.account.slug]: "acme" },
});
}
});
});
loggedTest(
"subset build ignores removed snapshots",
async ({ page, auth, team, project, builds }) => {
await ensureTeamOwner({ team: team.team, user: auth.user });
const { number } = builds.subsetBuild;
await page.goto(
`/${team.account.slug}/${project.name}/builds/${number}/overview`,
);
// The build has 3 unchanged, 1 changed and 2 removed snapshots. Because it
// only uploaded a subset, the removals mean "test not run", not "snapshot
// deleted": they must not show up as something to review.
await expect(page.getByText("A single visual change")).toBeVisible();
await expect(page.getByText("2 removed")).toHaveCount(0);
await expect(page.getByText("Skipped", { exact: true })).toBeVisible();
await expect(page.getByText("Removed", { exact: true })).toHaveCount(0);
},
);
loggedTest(
"total screenshots is what the build is billed for",
async ({ page, auth, team, project }) => {
await ensureTeamOwner({ team: team.team, user: auth.user });
// Four diffs, one of them removed: the build uploaded three screenshots.
const { build } = await createVariantSwitchersScenario({
projectId: project.id,
});
await page.goto(
`/${team.account.slug}/${project.name}/builds/${build.number}/overview`,
);
await page.getByRole("tab", { name: "Info" }).click();
await expect(
page.locator("dt:has-text('Total screenshots') + dd"),
).toHaveText("3");
},
);
loggedTest(
"snapshot compared against a fallback baseline",
async ({ page, auth, team, project }) => {
await ensureTeamOwner({ team: team.team, user: auth.user });
const { build } = await createFallbackBaselineScenario({
projectId: project.id,
});
await page.goto(
`/${team.account.slug}/${project.name}/builds/${build.number}`,
);
await page
.getByRole("button", { name: /^(Start review|Browse snapshots)/ })
.click();
// The metadata sidebar states which baseline the snapshot was compared
// against, since it differs from the snapshot's own name.
// The snapshot is titled by its own name, and the metadata sidebar names
// the baseline it was compared against.
await expect(
page.getByRole("heading", { name: "home-variant-b.png" }),
).toBeVisible();
await expect(page.getByText("Baseline home.png")).toBeVisible();
await screenshot(page, "build-fallback-baseline", {
replacements: {
[team.account.slug]: "acme",
},
});
},
);
loggedTest(
"offers the next build of the commit once a review is submitted",
async ({ page, auth, team, project }) => {
await ensureTeamOwner({ team: team.team, user: auth.user });
const { defaultBuild, storybookBuild, docsBuild, docsProject } =
await createSiblingBuildsScenario({ projectId: project.id });
await page.goto(
`/${team.account.slug}/${project.name}/builds/${defaultBuild.number}`,
);
await page.getByRole("button", { name: "Submit review" }).click();
await page.getByRole("button", { name: "Approve" }).click();
// Two more builds ran on the same commit and nobody has reviewed them, so
// finishing this one hands the reviewer straight to the next.
//
// The prompt waits on the review mutation's response, and the server only
// answers it once the build notifications and the automations have been
// enqueued — a broker round-trip on top of the request.
const dialog = page.getByRole("dialog", { name: "Review the next build" });
await expect(dialog).toBeVisible({ timeout: 15_000 });
await expect(
dialog.getByText("2 more builds ran on this commit"),
).toBeVisible();
// The row carries what the reviewer needs to choose: which build it is,
// which project it ran in, and where its review stands.
await expect(
dialog.getByRole("link", {
name: `storybook ${team.account.slug}/${project.name} Changes detected`,
}),
).toBeVisible();
// The commit reaches beyond this project, and that build is offered with a
// link into its own project.
await expect(
dialog.getByRole("link", {
name: `default ${team.account.slug}/${docsProject.name} Changes detected`,
}),
).toHaveAttribute(
"href",
`/${team.account.slug}/${docsProject.name}/builds/${docsBuild.number}/overview`,
);
await screenshot(page, "build-next-review-dialog", {
replacements: {
[team.account.slug]: "acme",
},
});
await dialog.getByRole("link", { name: "Review next build" }).click();
await expect(page).toHaveURL(
new RegExp(`/builds/${storybookBuild.number}/overview$`),
);
// The prompt belongs to the build it was raised on: it must not survive
// the jump and keep pointing at the build the reviewer just left.
await expect(dialog).toBeHidden();
// Landing on the next build means reviewing *its* snapshots: starting the
// review has to open a snapshot of the build being looked at, not one the
// previous build left in the list.
const storybookDiff = await ScreenshotDiff.query()
.findOne({ buildId: storybookBuild.id })
.throwIfNotFound();
await page
.getByRole("button", { name: /^(Start review|Browse snapshots)/ })
.click();
await expect(page).toHaveURL(
new RegExp(`/builds/${storybookBuild.number}/${storybookDiff.id}$`),
);
await expect(page.getByRole("heading", { name: "home.png" })).toBeVisible();
},
);
loggedTest(
"prompts for the next build from the review dialog too",
async ({ page, auth, team, project }) => {
await ensureTeamOwner({ team: team.team, user: auth.user });
const { defaultBuild } = await createSiblingBuildsScenario({
projectId: project.id,
});
await page.goto(
`/${team.account.slug}/${project.name}/builds/${defaultBuild.number}`,
);
await page
.getByRole("button", { name: /^(Start review|Browse snapshots)/ })
.click();
// Marking the build's only change opens the review dialog on its own. That
// dialog hosts a review form outside the page's children — it has to reach
// the same next-build prompt the header popover does. IconButtons carry no
// accessible name, so the thumb icon locates the button.
await page.locator("button:has(.lucide-thumbs-up)").first().click();
const reviewDialog = page.getByRole("dialog", {
name: "Submit your review",
});
await expect(reviewDialog).toBeVisible();
await reviewDialog.getByRole("button", { name: "Approve" }).click();
const dialog = page.getByRole("dialog", { name: "Review the next build" });
await expect(dialog).toBeVisible({ timeout: 15_000 });
await expect(
dialog.getByRole("link", { name: "Review next build" }),
).toBeVisible();
},
);
loggedTest(
"sets the next build apart from the others awaiting a review",
async ({ page, auth, team, project }) => {
await ensureTeamOwner({ team: team.team, user: auth.user });
const { defaultBuild, docsProject, extraBuilds } =
await createSiblingBuildsScenario({
projectId: project.id,
extraSuites: [
"a11y",
"e2e-chrome",
"e2e-firefox",
"e2e-webkit",
"unit",
],
});
const [a11yBuild, chromeBuild] = extraBuilds;
if (!a11yBuild || !chromeBuild) {
throw new Error("The scenario was seeded with the extra suites");
}
await Build.query()
.findById(a11yBuild.id)
.patch({ conclusion: "no-changes" });
await page.goto(
`/${team.account.slug}/${project.name}/builds/${defaultBuild.number}`,
);
await page.getByRole("button", { name: "Submit review" }).click();
await page.getByRole("button", { name: "Approve" }).click();
const dialog = page.getByRole("dialog", { name: "Review the next build" });
await expect(dialog).toBeVisible({ timeout: 15_000 });
await expect(
dialog.getByText("6 more builds ran on this commit"),
).toBeVisible();
// A suite with nothing to review is no stop on the way, so it stays out.
const rows = dialog.getByRole("table").getByRole("link");
await expect(rows.filter({ hasText: "a11y" })).toHaveCount(0);
// The dialog's action does not name the build it goes to, so that build
// is listed on its own.
const chromeBuildURL = `/${team.account.slug}/${project.name}/builds/${chromeBuild.number}/overview`;
await expect(
dialog.getByRole("table", { name: "Up next" }).getByRole("link"),
).toHaveAttribute("href", chromeBuildURL);
await expect(
dialog.getByRole("link", { name: "Review next build" }),
).toHaveAttribute("href", chromeBuildURL);
// Six builds await a review and five are listed: the last one, in the
// other project, folds.
await expect(rows).toHaveCount(5);
const docsRow = rows.filter({ hasText: docsProject.name });
await expect(docsRow).toHaveCount(0);
await screenshot(page, "build-next-review-dialog-folded", {
replacements: {
[team.account.slug]: "acme",
},
});
await dialog.getByRole("button", { name: "Show 1 more build" }).click();
await expect(rows).toHaveCount(6);
// The button is gone, so focus moves to the build it was hiding.
await expect(docsRow).toBeFocused();
},
);
loggedTest(
"points an outdated build to the latest build of its suite",
async ({ page, auth, team, project }) => {
await ensureTeamOwner({ team: team.team, user: auth.user });
const { defaultBuild, newerDefaultBuild } =
await createSiblingBuildsScenario({
projectId: project.id,
withNewerBuild: true,
});
if (!newerDefaultBuild) {
throw new Error("The scenario was seeded with a newer build");
}
await page.goto(
`/${team.account.slug}/${project.name}/builds/${defaultBuild.number}`,
);
const banner = page
.getByRole("alert")
.filter({ hasText: "A newer build exists for branch feat/sparkle." });
await expect(banner).toBeVisible();
await expect(
banner.getByRole("link", {
name: `Go to build ${newerDefaultBuild.number}`,
}),
).toHaveAttribute(
"href",
`/${team.account.slug}/${project.name}/builds/${newerDefaultBuild.number}/overview`,
);
await screenshot(page, "build-newer-build-banner", {
replacements: {
[team.account.slug]: "acme",
},
});
// The latest build has nothing newer to point to.
await page.goto(
`/${team.account.slug}/${project.name}/builds/${newerDefaultBuild.number}`,
);
await expect(
page.getByRole("link", {
name: `Build ${newerDefaultBuild.number}`,
exact: true,
}),
).toBeVisible();
await expect(page.getByText("A newer build exists")).toHaveCount(0);
},
);
loggedTest(
"offers the latest build once an outdated one is reviewed",
async ({ page, auth, team, project }) => {
await ensureTeamOwner({ team: team.team, user: auth.user });
const { defaultBuild, newerDefaultBuild } =
await createSiblingBuildsScenario({
projectId: project.id,
withNewerBuild: true,
});
if (!newerDefaultBuild) {
throw new Error("The scenario was seeded with a newer build");
}
await page.goto(
`/${team.account.slug}/${project.name}/builds/${defaultBuild.number}`,
);
await page.getByRole("button", { name: "Submit review" }).click();
await page.getByRole("button", { name: "Approve" }).click();
const dialog = page.getByRole("dialog", {
name: "Review the latest build",
});
await expect(dialog).toBeVisible({ timeout: 15_000 });
await expect(
dialog.getByText(
`Build ${newerDefaultBuild.number} is the latest of branch feat/sparkle.`,
),
).toBeVisible();
// The later commit only ran `default`: the commit's other suites are still
// the latest of their own and wait for a review, so they stay listed.
await expect(
dialog.getByText("2 other suites of this commit still need your review."),
).toBeVisible();
await expect(
dialog.getByRole("link", {
name: `storybook ${team.account.slug}/${project.name} Changes detected`,
}),
).toBeVisible();
await screenshot(page, "build-newer-build-dialog", {
replacements: {
[team.account.slug]: "acme",
},
});
await dialog.getByRole("link", { name: "Review latest build" }).click();
await expect(page).toHaveURL(
new RegExp(`/builds/${newerDefaultBuild.number}/overview$`),
);
// Both directions of the suite's history agree: the build the reviewer is
// sent to offers to carry over what they just approved.
await expect(
page.getByRole("dialog", { name: "Previous approvals detected" }),
).toBeVisible();
},
);
loggedTest(
"does not send the reviewer to a newer build with nothing to review",
async ({ page, auth, team, project }) => {
await ensureTeamOwner({ team: team.team, user: auth.user });
const { defaultBuild, newerDefaultBuild } =
await createSiblingBuildsScenario({
projectId: project.id,
withNewerBuild: true,
});
if (!newerDefaultBuild) {
throw new Error("The scenario was seeded with a newer build");
}
await Build.query()
.findById(newerDefaultBuild.id)
.patch({ conclusion: "no-changes" });
await page.goto(
`/${team.account.slug}/${project.name}/builds/${defaultBuild.number}`,
);
await page.getByRole("button", { name: "Submit review" }).click();
await page.getByRole("button", { name: "Approve" }).click();
// The latest build has no changes: the prompt falls back to the commit's
// suites still awaiting a review.
const dialog = page.getByRole("dialog", { name: "Review the next build" });
await expect(dialog).toBeVisible({ timeout: 15_000 });
await expect(
dialog.getByText("2 more builds ran on this commit"),
).toBeVisible();
},
);
loggedTest(
"header switches between the builds of a commit",
async ({ page, auth, team, project }) => {
await ensureTeamOwner({ team: team.team, user: auth.user });
const { defaultBuild, storybookBuild, docsBuild, docsProject } =
await createSiblingBuildsScenario({ projectId: project.id });
await page.goto(
`/${team.account.slug}/${project.name}/builds/${defaultBuild.number}`,
);
const trigger = page.getByRole("button", { name: "Switch build" });
await trigger.click();
// Every build of the commit is listed with where its review stands, the
// one being looked at included, and each names the project it ran in — the
// two projects of the commit both call their suite `default`. The menu kit
// is a listbox driven by a search field, so its rows are options rather
// than menu items.
const menu = page.getByRole("listbox");
await expect(
menu.getByRole("option", {
name: `Changes detected ${defaultBuild.name} #${defaultBuild.number} ${team.account.slug}/${project.name}`,
}),
).toBeVisible();
await expect(
menu.getByRole("option", {
name: `Changes detected ${storybookBuild.name} #${storybookBuild.number} ${team.account.slug}/${project.name}`,
}),
).toBeVisible();
const docsItem = menu.getByRole("option", {
name: `Changes detected ${docsBuild.name} #${docsBuild.number} ${team.account.slug}/${docsProject.name}`,
});
await expect(docsItem).toBeVisible();
// The click leaves the pointer on the trigger, and the trigger's tooltip
// can be up over the menu. Step off and wait for it to go, because the
// capture's own step-off lands inside the tooltip's close delay. The menu
// is portaled too, hence the tooltip's text rather than its portal.
await page.mouse.move(0, 0);
await expect(page.getByText("Switch build")).toBeHidden();
await screenshot(page, "build-switcher", {
replacements: {
[team.account.slug]: "acme",
},
});
// Switching lands on the build of the other project, not on a build of
// this one that happens to share its number.
await docsItem.click();
await expect(page).toHaveURL(
new RegExp(
`/${team.account.slug}/${docsProject.name}/builds/${docsBuild.number}/overview$`,
),
);
},
);
loggedTest(
"build sidebar marks a review an agent submitted for its reviewer",
async ({ page, auth, team, project, builds }) => {
// The seeded approval has no author, so it renders as "Unknown user" with
// no avatar to badge. Give it one, and an agent.
await BuildReview.query()
.patch({ userId: auth.user.id, agent: "claude-code" })
.where({ buildId: builds.acceptedBuild.id });
await page.goto(
`/${team.account.slug}/${project.name}/builds/${builds.acceptedBuild.number}`,
);
const badge = page.getByRole("img", {
name: "Reviewed through Claude Code on behalf of this user",
});
await expect(badge.first()).toBeVisible();
await screenshot(page, "build-agent-review");
},
);
loggedTest(
"undoes and redoes review marks with the keyboard",
async ({ page, auth, team, project, builds }) => {
await ensureTeamOwner({ team: team.team, user: auth.user });
await page.goto(
`/${team.account.slug}/${project.name}/builds/${builds.diffDetectedBuild.number}`,
);
// A standalone changed snapshot, so the mark lands on it alone: the other
// changed ones are bundled under a "3 similar" group, which is marked as
// a whole.
await page
.getByRole("button", { name: "dummy-375x1440.png" })
.first()
.click();
// `aria-pressed` narrows it to the toolbar's own accept button: a diff
// list header carries the same thumb once its whole group is accepted, and
// it comes first in the document.
const acceptButton = page.locator(
"button[aria-pressed]:has(.lucide-thumbs-up)",
);
// Marking a snapshot takes the reviewer to the next one, which is why a
// mistake is always noticed from somewhere else: undo has to put the mark
// back *and* bring its snapshot back on screen.
const firstDiffURL = page.url();
await acceptButton.click();
await expect(page).not.toHaveURL(firstDiffURL);
const secondDiffURL = page.url();
await acceptButton.click();
await expect(page).not.toHaveURL(secondDiffURL);
await page.keyboard.press("ControlOrMeta+z");
await expect(page).toHaveURL(secondDiffURL);
await expect(acceptButton).toHaveAttribute("aria-pressed", "false");
await page.keyboard.press("ControlOrMeta+z");
await expect(page).toHaveURL(firstDiffURL);
await expect(acceptButton).toHaveAttribute("aria-pressed", "false");
// Shift is the only thing telling redo apart from undo, so a redo that
// also read as an undo would walk the reviewer backwards here instead of
// forwards.
await page.keyboard.press("ControlOrMeta+Shift+z");
await expect(page).toHaveURL(firstDiffURL);
await expect(acceptButton).toHaveAttribute("aria-pressed", "true");
await page.keyboard.press("ControlOrMeta+Shift+z");
await expect(page).toHaveURL(secondDiffURL);
await expect(acceptButton).toHaveAttribute("aria-pressed", "true");
},
);
loggedTest(
"seats the variant switchers and a long name on one toolbar",
async ({ page, auth, team, project }) => {
await ensureTeamOwner({ team: team.team, user: auth.user });
const { build, variantKey } = await createVariantSwitchersScenario({
projectId: project.id,
});
await page.goto(
`/${team.account.slug}/${project.name}/builds/${build.number}`,
);
await page
.getByRole("button", { name: /^(Start review|Browse snapshots)/ })
.click();
// Both switchers offer every sibling, whichever one the review opened on.
// Scoped to the group: the title beside it is a link to the test, and its
// name starts with "chromium/" — unscoped, so would the browser button.
const variants = page.getByRole("group", { name: "Snapshot variants" });
for (const name of ["Chromium", "Firefox", "390", "1280"]) {
await expect(variants.getByRole("link", { name })).toBeVisible();
}
// The color scheme cannot be switched — every sibling is dark — so it is
// stated in the Metadata panel rather than offered in the toolbar.
await expect(variants.getByText("Dark")).toHaveCount(0);
await expect(page.getByText("Dark", { exact: true })).toBeVisible();
// The question this build exists to answer: the name is 70-odd characters
// and shares the row with the switchers. `line-clamp-2` swallows a third
// line without a trace, so overflow is the only thing that tells us it
// cropped.
const title = page.getByRole("heading", {
name: new RegExp(variantKey.replaceAll("/", "\\/")),
});
await expect(title).toBeVisible();
await expect
.poll(() =>
title.evaluate((el) => el.scrollHeight - el.clientHeight <= 1),
)
.toBe(true);
await screenshot(page, "build-variant-switchers", {
replacements: {
[team.account.slug]: "acme",
},
});
},
);
loggedTest(
"marks the variants holding a change",
async ({ page, auth, team, project }) => {
await ensureTeamOwner({ team: team.team, user: auth.user });
const { build } = await createVariantSwitchersScenario({
projectId: project.id,
});
await page.goto(
`/${team.account.slug}/${project.name}/builds/${build.number}`,
);
await page
.getByRole("button", { name: /^(Start review|Browse snapshots)/ })
.click();
const variants = page.getByRole("group", { name: "Snapshot variants" });
const segment = (name: string) =>
variants.getByRole("link", { name, exact: true });
// Every marker the switchers can draw, in one toolbar: the reviewer sees
// which siblings are worth the jump before making it. The status rides the
// accessible name because the tooltip carrying it is out of the
// accessibility tree.
await expect(segment("Chromium, Changed")).toBeVisible();
await expect(segment("Firefox, Removed")).toBeVisible();
await expect(segment("390px, Added")).toBeVisible();
await expect(segment("1280px, Changed")).toBeVisible();
await screenshot(page, "build-variant-statuses", {
replacements: {
[team.account.slug]: "acme",
},
});
// And the other half of the rule: Firefox's 390 sibling is unchanged, so
// that segment says nothing at all rather than marking itself "unchanged".
await segment("Firefox, Removed").click();
await expect(segment("390px")).toBeVisible();
await expect(segment("Firefox, Removed")).toHaveAttribute(
"aria-current",
"page",
);
await screenshot(page, "build-variant-statuses-unchanged", {
replacements: {
[team.account.slug]: "acme",
},
});
},
);
loggedTest(
"keeps the other variant when switching one",
async ({ page, auth, team, project }) => {
await ensureTeamOwner({ team: team.team, user: auth.user });
const { build, variantKey } = await createVariantSwitchersScenario({
projectId: project.id,
});
await page.goto(
`/${team.account.slug}/${project.name}/builds/${build.number}`,
);
await page
.getByRole("button", { name: /^(Start review|Browse snapshots)/ })
.click();
const variants = page.getByRole("group", { name: "Snapshot variants" });
const snapshotName = (browser: string, width: number) =>
new RegExp(
`^${browser}/${variantKey.replaceAll("/", "\\/")} vw-${width}\\.png$`,
);
// The build is a 2×2: two browsers, two viewports. Walking one dimension
// has to leave the other where it was — the reviewer asked for a different
// browser, not a different viewport.
await expect(
page.getByRole("heading", { name: snapshotName("chromium", 1280) }),
).toBeVisible();
await variants.getByRole("link", { name: "Firefox" }).click();
await expect(
page.getByRole("heading", { name: snapshotName("firefox", 1280) }),
).toBeVisible();
await variants.getByRole("link", { name: "390" }).click();
await expect(
page.getByRole("heading", { name: snapshotName("firefox", 390) }),
).toBeVisible();
// And back the other way, so this cannot pass on the diff list's order.
await variants.getByRole("link", { name: "Chromium" }).click();
await expect(
page.getByRole("heading", { name: snapshotName("chromium", 390) }),
).toBeVisible();
},
);
loggedTest(
"filters from a variant segment and follows it",
async ({ page, auth, team, project }) => {
await ensureTeamOwner({ team: team.team, user: auth.user });
const { build, variantKey } = await createVariantSwitchersScenario({
projectId: project.id,
});
await page.goto(
`/${team.account.slug}/${project.name}/builds/${build.number}`,
);
await page
.getByRole("button", { name: /^(Start review|Browse snapshots)/ })
.click();
const variants = page.getByRole("group", { name: "Snapshot variants" });
const snapshotName = (browser: string, width: number) =>
new RegExp(
`^${browser}/${variantKey.replaceAll("/", "\\/")} vw-${width}\\.png$`,
);
await expect(
page.getByRole("heading", { name: snapshotName("chromium", 1280) }),
).toBeVisible();
// Filtering on a viewport the snapshot on screen does not have would drop
// that snapshot out of the list under it, so the filter takes the reviewer
// to the sibling it names — the same place a left click would have gone.
await variants
.getByRole("link", { name: "390px, Added" })
.click({ button: "right" });
await expect(
page.getByRole("option", { name: "Add to filters" }),
).toBeVisible();
await screenshot(page, "build-variant-filter-menu", {
replacements: {
[team.account.slug]: "acme",
},
});
await page.getByRole("option", { name: "Add to filters" }).click();
await expect(
page.getByRole("heading", { name: snapshotName("chromium", 390) }),
).toBeVisible();
const viewportChip = page.getByRole("button", {
name: "Remove Viewport filter",
});
await expect(viewportChip).toBeVisible();
// Only the 390 siblings are left in the list.
await expect(page.getByText(/vw-1280\.png/)).toHaveCount(0);
// And back off the same segment, which now reads as the way out.
await variants
.getByRole("link", { name: "390px, Added" })
.click({ button: "right" });
await page.getByRole("option", { name: "Remove from filters" }).click();
await expect(viewportChip).toHaveCount(0);
// Re-opened, the row has to read the filter as it stands now: the menu is
// held at what it said only while it animates out.
await variants
.getByRole("link", { name: "390px, Added" })
.click({ button: "right" });
await expect(
page.getByRole("option", { name: "Add to filters" }),
).toBeVisible();
},
);
loggedTest(
"says which of the two emptied the snapshot list",
async ({ page, auth, team, project }) => {
await ensureTeamOwner({ team: team.team, user: auth.user });
const builds = await createBuildScenario({ projectId: project.id });
await page.goto(
`/${team.account.slug}/${project.name}/builds/${builds.diffDetectedBuild.number}`,
);
await page
.getByRole("button", {
name: /^(Browse test failures|Start review|Browse snapshots)/,
})
.click();
// Two categories that do not overlap: the stories captured on Firefox are
// never the hero ones. An intersection is the only way to empty the list
// without emptying the build, which is exactly the case the build's own
// "no screenshots" state gets wrong.
const addFilter = async (category: string, value: RegExp) => {
await page.keyboard.press("f");
await page.getByRole("option", { name: category }).click();
await page.getByRole("option", { name: value }).click();
await page.keyboard.press("Escape");
};
await addFilter("Story kind", /gallery-hero/);
await addFilter("Browser", /firefox/);
// Not "follow one of our quickstart guides": the build has screenshots, the
// filters just leave none, and the reviewer needs to be told which.
await expect(
page.getByText("No screenshot matches all the filters."),
).toBeVisible();
await page.getByRole("button", { name: "Clear filters" }).click();
await expect(
page.getByText("No screenshot matches all the filters."),
).toHaveCount(0);
},
);