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
46 changes: 21 additions & 25 deletions e2e/assets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ test.describe.serial("phase 5 assets", () => {
}
});

test("can create, edit, and update status for an asset", async ({ page }) => {
test("can create, edit, and update status for an asset", async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name !== "chromium",
"Desktop flow: phones use asset cards and the asset back bar",
);
const email = process.env.E2E_AUTH_EMAIL;
const password = process.env.E2E_AUTH_PASSWORD;

Expand All @@ -41,16 +47,16 @@ test.describe.serial("phase 5 assets", () => {
await fillRequiredFields(page);
await page.getByRole("button", { name: "Create asset" }).click();
await expect(
page.getByRole("heading", { name: `${baseName} created` }),
page.getByRole("heading", { name: baseName, exact: true }),
).toBeVisible({
timeout: 20_000,
});

await page.getByRole("button", { name: "View asset" }).click();
await page.getByRole("button", { name: "Open asset" }).click();
await page.waitForURL(/\/assets\/[^/]+$/, { timeout: 20_000 });
await expect(page.getByRole("heading", { name: baseName })).toBeVisible();

await page.getByRole("link", { name: "Edit" }).click();
await page.getByRole("link", { name: "Edit", exact: true }).click();
await page.waitForURL(/\/assets\/.+\/edit/, { timeout: 15_000 });

const nameInput = page.getByLabel(/Name/i);
Expand All @@ -64,44 +70,34 @@ test.describe.serial("phase 5 assets", () => {

await page.getByLabel("Change status").click();
await page.getByRole("option", { name: "Retired" }).click();
await expect(
page
.locator("[data-slot='badge']")
.filter({ hasText: "Retired" })
.first(),
).toBeVisible();
await expect(page.getByLabel("Change status")).toHaveText("Retired");

await page.goto("/assets");
await page.getByLabel("Search assets").fill(updatedName);
await expect(
page.locator("tbody tr").filter({ hasText: updatedName }),
).toBeVisible();

await page.getByLabel("Filter by status").click();
await page.getByRole("option", { name: "Retired" }).click();
await page
.getByRole("navigation", { name: "Status" })
.getByRole("button", { name: /^Retired/ })
.click();
await expect(
page.locator("tbody tr").filter({ hasText: updatedName }),
).toBeVisible();

await page
.locator("tbody tr")
.filter({ hasText: updatedName })
.first()
.click();
// Table rows open on click; the name cell is the safest hit target.
await page.getByRole("cell", { name: updatedName, exact: true }).click();
await page.waitForURL(/\/assets\/.+$/, { timeout: 15_000 });
await expect(
page.getByRole("heading", { name: updatedName }),
).toBeVisible();
await expect(
page
.locator("[data-slot='badge']")
.filter({ hasText: "Retired" })
.first(),
).toBeVisible();
await expect(page.getByLabel("Change status")).toHaveText("Retired");

await page.getByRole("button", { name: "Delete" }).click();
await page.getByRole("button", { name: "More actions" }).click();
await page.getByRole("menuitem", { name: "Delete…" }).click();
await expect(
page.getByRole("heading", { name: "Delete asset" }),
page.getByRole("heading", { name: "Delete asset?" }),
).toBeVisible();
await page.getByRole("button", { name: "Delete asset" }).click();
await page.waitForURL(
Expand Down
4 changes: 2 additions & 2 deletions e2e/attachments.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ test.describe.serial("phase 6 attachments", () => {
await page.getByRole("button", { name: "Create asset" }).click();

await expect(
page.getByRole("heading", { name: `${assetName} created` }),
page.getByRole("heading", { name: assetName, exact: true }),
).toBeVisible({
timeout: 20_000,
});
Expand Down Expand Up @@ -83,7 +83,7 @@ test.describe.serial("phase 6 attachments", () => {
timeout: 15_000,
});

await page.getByRole("button", { name: "View asset" }).click();
await page.getByRole("button", { name: "Open asset" }).click();
await page.waitForURL(/\/assets\/.+$/, { timeout: 20_000 });
await expect(page.getByRole("heading", { name: assetName })).toBeVisible();
});
Expand Down
15 changes: 10 additions & 5 deletions e2e/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test, type Page } from "@playwright/test";
import { getLeafPathSegment, type Landing } from "./helpers";
import { getLeafPathSegment, hideNextDevTools, type Landing } from "./helpers";

async function getUnauthenticatedLanding(page: Page): Promise<Landing> {
await page.goto("/dashboard");
Expand Down Expand Up @@ -70,7 +70,9 @@ test.describe.serial("auth flow", () => {

const email = `playwright-invalid-${Date.now()}@example.com`;
await page.getByLabel("Email").fill(email);
await page.getByLabel("Password").fill("not-the-right-password");
await page
.getByLabel("Password", { exact: true })
.fill("not-the-right-password");

await expectNoLocalAuthProxyRequests(page, async () => {
await page.getByRole("button", { name: "Sign in" }).click();
Expand Down Expand Up @@ -98,8 +100,9 @@ test.describe.serial("auth flow", () => {
);

await expect.poll(() => getLeafPathSegment(page.url())).toBe("login");
await hideNextDevTools(page);
await page.getByLabel("Email").fill(email!);
await page.getByLabel("Password").fill(password!);
await page.getByLabel("Password", { exact: true }).fill(password!);

await expectNoLocalAuthProxyRequests(page, async () => {
await page.getByRole("button", { name: "Sign in" }).click();
Expand All @@ -111,14 +114,16 @@ test.describe.serial("auth flow", () => {
);
});

await page.getByLabel("Open user menu").click();
// The shell mounts a user menu per layout (sidebar, rail, phone bar);
// only the one for this viewport is visible.
await page.getByLabel("Open user menu").filter({ visible: true }).click();
const logoutResponse = page.waitForResponse(
(res) =>
res.url().endsWith("/api/auth/logout") &&
res.request().method() === "POST",
{ timeout: 10_000 },
);
await page.getByRole("menuitem", { name: "Log out" }).click();
await page.getByRole("menuitem", { name: "Sign out" }).click();
await logoutResponse;
await page.waitForURL(
(url) => getLeafPathSegment(url.toString()) === "login",
Expand Down
96 changes: 60 additions & 36 deletions e2e/categories-tags-locations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ test.describe.serial("phase 3 pages", () => {

test("admin can create category, tag, and nested locations", async ({
page,
}) => {
}, testInfo) => {
test.skip(
testInfo.project.name !== "chromium",
"Desktop flow: phones replace the location tree with a picker",
);
const email = process.env.E2E_AUTH_EMAIL;
const password = process.env.E2E_AUTH_PASSWORD;

Expand All @@ -39,56 +43,76 @@ test.describe.serial("phase 3 pages", () => {
const childName = `PW Child ${stamp}`;
const grandchildName = `PW Bin ${stamp}`;

// /categories and /tags redirect to the taxonomy tabs.
await page.goto("/categories");
await page.getByRole("button", { name: "Add Category" }).click();
const categoryDialog = page.getByRole("dialog");
await page.waitForURL(/\/taxonomy\?tab=categories/);
await page.getByRole("button", { name: "New category…" }).first().click();
const categoryDialog = page.getByRole("dialog", { name: "New category" });
await categoryDialog.getByLabel("Name").fill(categoryName);
await categoryDialog.getByLabel("Prefix").fill("PW");
await categoryDialog.getByLabel("Description").fill("Playwright category");
await categoryDialog
.getByRole("textbox", { name: "Color" })
.fill("#2563EB");
await page.getByRole("button", { name: "Create Category" }).click();
await expect(page.getByText(categoryName)).toBeVisible();
.getByRole("button", { name: "Create category" })
.click();
await expect(page.getByRole("link", { name: categoryName })).toBeVisible();

await page.goto("/tags");
await page.getByRole("button", { name: "Add Tag" }).click();
const tagDialog = page.getByRole("dialog");
await page.waitForURL(/\/taxonomy\?tab=tags/);
await page.getByRole("button", { name: "New keyword…" }).first().click();
const tagDialog = page.getByRole("dialog", { name: "New keyword" });
await tagDialog.getByLabel("Name").fill(tagName);
await tagDialog.getByRole("textbox", { name: "Color" }).fill("#E11D48");
await page.getByRole("button", { name: "Create Tag" }).click();
await expect(page.getByText(tagName)).toBeVisible();
await tagDialog.getByRole("button", { name: "Create keyword" }).click();
await expect(
page.getByRole("link", { name: `Assets with keyword ${tagName}` }),
).toBeVisible();

// Locations: a site, a location inside it, and one inside that. Each new
// location becomes the selection and its row is revealed in the tree.
const tree = page.getByRole("list", { name: "Location tree" });
// A row's select button reads as its name followed by its asset count.
const treeRow = (name: string) =>
tree.getByRole("button", { name: new RegExp(`^${name}\\s*\\d+$`) });
const detail = page.getByTestId("location-detail");

await page.goto("/locations");
await page
.getByRole("button", { name: "Add root location" })
.first()
.click();
let locationDialog = page.getByRole("dialog");
await page.getByRole("button", { name: "New location…" }).first().click();
let locationDialog = page.getByRole("dialog", { name: "New location" });
await locationDialog.getByLabel("Name").fill(rootName);
await page.getByRole("button", { name: "Create location" }).click();
await locationDialog
.getByRole("button", { name: "Create location" })
.click();
await expect(
page.getByRole("button", { name: `Actions for ${rootName}` }),
detail.getByRole("heading", { level: 2, name: rootName }),
).toBeVisible();
await expect(treeRow(rootName)).toHaveAttribute("aria-current", "true");

await page.getByRole("button", { name: `Actions for ${rootName}` }).click();
await page.getByRole("menuitem", { name: "Add child" }).click();
locationDialog = page.getByRole("dialog");
await locationDialog.getByLabel("Name").fill(childName);
await page.getByRole("button", { name: "Create location" }).click();
await expect(
page.getByRole("button", { name: `Actions for ${childName}` }),
).toBeVisible();
for (const [parentName, name] of [
[rootName, childName],
[childName, grandchildName],
]) {
await page.getByRole("button", { name: "Location actions" }).click();
await page
.getByRole("menuitem", { name: "New location inside…" })
.click();
locationDialog = page.getByRole("dialog", { name: "New location" });
await locationDialog.getByLabel("Name").fill(name);
await locationDialog
.getByRole("button", { name: "Create location" })
.click();
await expect(
detail.getByRole("heading", {
level: 2,
name: `${name} in ${parentName}`,
}),
).toBeVisible();
await expect(treeRow(name)).toHaveAttribute("aria-current", "true");
}

await page
.getByRole("button", { name: `Actions for ${childName}` })
.click();
await page.getByRole("menuitem", { name: "Add child" }).click();
locationDialog = page.getByRole("dialog");
await locationDialog.getByLabel("Name").fill(grandchildName);
await page.getByRole("button", { name: "Create location" }).click();
// A location with something inside it cannot be deleted.
await treeRow(rootName).click();
await page.getByRole("button", { name: "Location actions" }).click();
await expect(
page.getByRole("button", { name: `Actions for ${grandchildName}` }),
).toBeVisible();
page.getByRole("menuitem", { name: /Delete…/ }),
).toHaveAttribute("aria-disabled", "true");
});
});
44 changes: 18 additions & 26 deletions e2e/dashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import {
signIn,
} from "./helpers";

// The dashboard title is today's date, e.g. "Saturday 7 November".
const DATE_HEADING =
/^(Mon|Tues|Wednes|Thurs|Fri|Satur|Sun)day \d{1,2} [A-Z][a-z]+$/;

function getStatusLabel(status: "active" | "retired") {
return status === "retired" ? "Retired" : "Active";
}
Expand Down Expand Up @@ -38,11 +42,11 @@ async function createAsset(

await page.getByRole("button", { name: "Create asset" }).click();
await expect(
page.getByRole("heading", { name: `${options.name} created` }),
page.getByRole("heading", { name: options.name, exact: true }),
).toBeVisible({
timeout: 20_000,
});
await page.getByRole("button", { name: "View asset" }).click();
await page.getByRole("button", { name: "Open asset" }).click();
await page.waitForURL(/\/assets\/(?!new$)[^/]+$/, { timeout: 20_000 });
}

Expand All @@ -69,7 +73,7 @@ test.describe.serial("phase 10 dashboard", () => {
await page.goto("/");
await page.waitForURL(/\/dashboard$/, { timeout: 20_000 });
await expect(
page.getByRole("heading", { name: /Welcome back|Dashboard/ }),
page.getByRole("heading", { level: 1, name: DATE_HEADING }),
).toBeVisible();
});

Expand Down Expand Up @@ -108,35 +112,23 @@ test.describe.serial("phase 10 dashboard", () => {

await page.goto("/dashboard");
await expect(
page.getByRole("heading", { name: /Welcome back|Dashboard/ }),
page.getByRole("heading", { level: 1, name: DATE_HEADING }),
).toBeVisible();
await expect(
page.locator('[data-dashboard-stat="total"] [data-dashboard-stat-value]'),
).toHaveText(/\d+/);
await expect(
page.locator(
'[data-dashboard-stat="active"] [data-dashboard-stat-value]',
),
).toHaveText(/\d+/);
await expect(
page.locator(
'[data-dashboard-stat="retired"] [data-dashboard-stat-value]',
),
).toHaveText(/\d+/);

const recentAssetsSection = page
.locator("section")
.filter({ hasText: "Recent assets" })
.first();
const stats = page.getByTestId("dashboard-stats-bar");
await expect(stats).toContainText(/\d+\s*Assets/);
await expect(stats).toContainText(/\d+\s*Retired/);

const recentAssetsSection = page.getByRole("region", {
name: "Recently updated",
});
await expect(
recentAssetsSection.getByText(scheduledAssetName),
).toBeVisible();
await expect(recentAssetsSection.getByText(retiredAssetName)).toBeVisible();

const serviceQueueSection = page
.locator("section")
.filter({ hasText: "Service queue" })
.first();
const serviceQueueSection = page.getByRole("region", {
name: "Service due",
});
await expect(serviceQueueSection).toBeVisible();
await expect(
serviceQueueSection.getByText(scheduledAssetName),
Expand Down
Loading
Loading