525 lines
18 KiB
TypeScript
525 lines
18 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
|
|
|
const browserErrors = new WeakMap<Page, string[]>();
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
const errors: string[] = [];
|
|
browserErrors.set(page, errors);
|
|
page.on("console", (message) => {
|
|
if (message.type() === "error") errors.push(message.text());
|
|
});
|
|
page.on("pageerror", (error) => errors.push(error.message));
|
|
});
|
|
|
|
test.afterEach(async ({ page }) => {
|
|
expect(browserErrors.get(page) ?? []).toEqual([]);
|
|
});
|
|
|
|
const openSettings = async (
|
|
page: import("@playwright/test").Page,
|
|
projectName: string,
|
|
) => {
|
|
if (["mobile-chromium", "tablet-chromium"].includes(projectName)) {
|
|
await page
|
|
.getByRole("navigation", { name: "Mobiele navigatie" })
|
|
.getByRole("button", { name: "Instellingen" })
|
|
.click();
|
|
return;
|
|
}
|
|
await page.getByRole("button", { name: "Instellingen openen" }).click();
|
|
};
|
|
|
|
test("dashboard en Quick Launch vormen de primaire startflow", async ({
|
|
page,
|
|
context,
|
|
}, testInfo) => {
|
|
test.skip(
|
|
testInfo.project.name !== "desktop-chromium",
|
|
"De desktopflow wordt eenmaal uitgevoerd",
|
|
);
|
|
|
|
await page.goto("/");
|
|
await expect(page.getByRole("main")).toBeVisible();
|
|
await expect(
|
|
page.getByRole("navigation", { name: "Dashboardnavigatie" }),
|
|
).toBeVisible();
|
|
await expect(page.getByLabel(/graden.*Mol/)).toBeVisible();
|
|
await expect(
|
|
page.getByRole("complementary", {
|
|
name: "Favorieten en deckstatus",
|
|
}),
|
|
).toBeVisible();
|
|
await expect(page.getByText("Live inzicht", { exact: true })).toBeVisible();
|
|
await page.locator(".dashboard-insights-panel > summary").click();
|
|
await expect(
|
|
page.getByRole("region", { name: "Servicepanelen" }),
|
|
).toBeVisible();
|
|
|
|
const refreshResponse = page.waitForResponse(
|
|
(response) =>
|
|
response.url().endsWith("/api/discovery/sync") &&
|
|
response.request().method() === "POST",
|
|
);
|
|
await page
|
|
.getByRole("button", { name: "Statussen vernieuwen" })
|
|
.first()
|
|
.click();
|
|
expect((await refreshResponse).status()).toBe(200);
|
|
|
|
await page.keyboard.press("Control+K");
|
|
const search = page.getByLabel("Zoek apps, favorieten of Google");
|
|
await expect(search).toBeFocused();
|
|
await search.fill("Google");
|
|
const favoriteResults = page
|
|
.locator(".result-group")
|
|
.filter({ has: page.getByRole("heading", { name: "Favorieten" }) });
|
|
await expect(
|
|
favoriteResults.locator(".app-icon-image, .app-brand-icon"),
|
|
).toHaveCount(1);
|
|
await search.fill("Plex");
|
|
await expect(
|
|
page.getByRole("option", { name: /Plex Media Server/ }),
|
|
).toBeVisible();
|
|
|
|
const googlePopupPromise = context.waitForEvent("page");
|
|
await page.getByRole("option", { name: /Zoeken op Google naar/ }).click();
|
|
const googlePopup = await googlePopupPromise;
|
|
expect(googlePopup.url()).toContain("google.com/search");
|
|
await googlePopup.close();
|
|
});
|
|
|
|
test("dashboardcategorieën filteren de zichtbare snelkoppelingen", async ({
|
|
page,
|
|
request,
|
|
}, testInfo) => {
|
|
test.skip(
|
|
testInfo.project.name !== "desktop-chromium",
|
|
"De desktopflow wordt eenmaal uitgevoerd",
|
|
);
|
|
|
|
const dashboard = await (await request.get("/api/dashboard")).json();
|
|
const [firstApp, secondApp] = dashboard.apps;
|
|
const [firstCategory, secondCategory] = dashboard.categories;
|
|
expect(firstApp).toBeTruthy();
|
|
expect(secondApp).toBeTruthy();
|
|
expect(firstCategory).toBeTruthy();
|
|
expect(secondCategory).toBeTruthy();
|
|
await request.patch(`/api/apps/${firstApp.id}`, {
|
|
data: { visible: true, categoryId: firstCategory.id },
|
|
});
|
|
await request.patch(`/api/apps/${secondApp.id}`, {
|
|
data: { visible: true, categoryId: secondCategory.id },
|
|
});
|
|
|
|
await page.goto("/");
|
|
const categoryButton = page
|
|
.locator(".category-tabs button")
|
|
.filter({ hasText: firstCategory.name })
|
|
.first();
|
|
await categoryButton.click();
|
|
|
|
await expect(categoryButton).toHaveClass(/active/);
|
|
await expect(page.locator(".dashboard-groups .app-section")).toHaveCount(1);
|
|
await expect(page.locator(".dashboard-groups .app-section h2")).toContainText(
|
|
firstCategory.name,
|
|
);
|
|
});
|
|
|
|
test("compact appbeheer opent een toegankelijke detail-drawer", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
test.skip(
|
|
testInfo.project.name !== "desktop-chromium",
|
|
"De desktopflow wordt eenmaal uitgevoerd",
|
|
);
|
|
|
|
await page.goto("/");
|
|
await openSettings(page, testInfo.project.name);
|
|
await expect(
|
|
page.getByRole("heading", { name: "App Beheer", exact: true }),
|
|
).toBeVisible();
|
|
await page.getByLabel("Apps zoeken").fill("Plex");
|
|
const row = page.getByRole("article").filter({ hasText: "Plex" });
|
|
await expect(row).toBeVisible();
|
|
const visibilitySwitch = row.getByRole("switch");
|
|
const knob = visibilitySwitch.locator(".visibility-toggle-track > span");
|
|
const initialVisibility = await visibilitySwitch.getAttribute("aria-checked");
|
|
const initialKnob = await knob.boundingBox();
|
|
await visibilitySwitch.click();
|
|
await expect(visibilitySwitch).toHaveAttribute(
|
|
"aria-checked",
|
|
initialVisibility === "true" ? "false" : "true",
|
|
);
|
|
await expect
|
|
.poll(async () => {
|
|
const movedKnob = await knob.boundingBox();
|
|
return Math.abs((movedKnob?.x ?? 0) - (initialKnob?.x ?? 0));
|
|
})
|
|
.toBeGreaterThan(18);
|
|
await visibilitySwitch.click();
|
|
await row.getByRole("button", { name: /Bewerken/ }).click();
|
|
|
|
const drawer = page.getByRole("dialog", { name: /Plex/ });
|
|
await expect(drawer).toBeVisible();
|
|
await expect(drawer.getByLabel("Weergavenaam")).toHaveValue(/Plex/);
|
|
await expect(
|
|
drawer.getByText("Automatisch ontdekt", { exact: true }),
|
|
).toBeVisible();
|
|
await drawer.getByRole("button", { name: "Sluiten" }).click();
|
|
await expect(drawer).toBeHidden();
|
|
await expect(row.getByRole("button", { name: /Bewerken/ })).toBeFocused();
|
|
});
|
|
|
|
test("weergavevoorkeuren en geavanceerde read-only status blijven werken", async ({
|
|
page,
|
|
request,
|
|
}, testInfo) => {
|
|
test.skip(
|
|
testInfo.project.name !== "desktop-chromium",
|
|
"De desktopflow wordt eenmaal uitgevoerd",
|
|
);
|
|
|
|
await request.patch("/api/preferences", {
|
|
data: { reducedMotion: false, pollingIntervalMs: 30000 },
|
|
});
|
|
await page.goto("/");
|
|
await openSettings(page, testInfo.project.name);
|
|
const settingsNavigation = page.getByRole("navigation", {
|
|
name: "Instellingensecties",
|
|
});
|
|
|
|
await settingsNavigation.getByRole("button", { name: "Weergave" }).click();
|
|
await page.getByRole("button", { name: /Haven/ }).click();
|
|
await expect(page.locator("html")).toHaveAttribute("data-theme", "harbor");
|
|
await page.locator(".appearance-advanced > summary").click();
|
|
await page.getByRole("switch", { name: "Verminderde beweging" }).click();
|
|
await page.getByLabel("Verversingsinterval").selectOption("60000");
|
|
await expect(page.locator("html")).toHaveAttribute(
|
|
"data-reduced-motion",
|
|
"true",
|
|
);
|
|
await page.reload();
|
|
await expect(page.locator("html")).toHaveAttribute("data-theme", "harbor");
|
|
await expect(page.locator("html")).toHaveAttribute(
|
|
"data-reduced-motion",
|
|
"true",
|
|
);
|
|
|
|
await openSettings(page, testInfo.project.name);
|
|
await page.getByLabel("Instellingen zoeken").fill("diagnostiek");
|
|
await expect(
|
|
page
|
|
.getByRole("navigation", { name: "Instellingensecties" })
|
|
.getByRole("button", { name: "Algemeen" }),
|
|
).toBeVisible();
|
|
await expect(
|
|
page
|
|
.getByRole("navigation", { name: "Instellingensecties" })
|
|
.getByRole("button", { name: "Apps" }),
|
|
).toHaveCount(0);
|
|
await page
|
|
.getByRole("navigation", { name: "Instellingensecties" })
|
|
.getByRole("button", { name: "Algemeen" })
|
|
.click();
|
|
await expect(
|
|
page.getByRole("region", { name: "Paneelstudio" }),
|
|
).toBeVisible();
|
|
await page.getByRole("button", { name: "Systeemstatus" }).click();
|
|
await page.getByRole("button", { name: "Diagnostiek uitvoeren" }).click();
|
|
await expect(page.locator(".diagnostics-result")).toContainText("Database");
|
|
});
|
|
|
|
test("achtergrondpresets en een geüploade wallpaper wijzigen de zichtbare canvas", async ({
|
|
page,
|
|
request,
|
|
}, testInfo) => {
|
|
test.skip(
|
|
testInfo.project.name !== "desktop-chromium",
|
|
"De visuele achtergrondflow wordt eenmaal uitgevoerd",
|
|
);
|
|
|
|
await request.delete("/api/wallpaper");
|
|
await request.patch("/api/preferences", {
|
|
data: {
|
|
backgroundStyle: "aurora",
|
|
wallpaperFit: "contain",
|
|
wallpaperPositionX: 50,
|
|
wallpaperPositionY: 50,
|
|
wallpaperZoom: 100,
|
|
},
|
|
});
|
|
await page.goto("/");
|
|
|
|
const selectBackground = async (
|
|
name: "Aurora" | "Blauwdruk" | "Minimaal",
|
|
) => {
|
|
await openSettings(page, testInfo.project.name);
|
|
await page
|
|
.getByRole("navigation", { name: "Instellingensecties" })
|
|
.getByRole("button", { name: "Weergave" })
|
|
.click();
|
|
await page.locator(".appearance-advanced > summary").click();
|
|
await page.getByRole("button", { name: new RegExp(`^${name}`) }).click();
|
|
await expect(page.locator("html")).toHaveAttribute(
|
|
"data-background",
|
|
{ Aurora: "aurora", Blauwdruk: "grid", Minimaal: "minimal" }[name],
|
|
);
|
|
await page
|
|
.locator(".settings-topbar nav")
|
|
.getByRole("button", { name: "Dashboard" })
|
|
.click();
|
|
return page.locator(".dashboard-canvas").evaluate((element) => ({
|
|
image: getComputedStyle(element).backgroundImage,
|
|
size: getComputedStyle(element).backgroundSize,
|
|
}));
|
|
};
|
|
|
|
const aurora = await selectBackground("Aurora");
|
|
const grid = await selectBackground("Blauwdruk");
|
|
const minimal = await selectBackground("Minimaal");
|
|
expect(new Set([aurora.image, grid.image, minimal.image]).size).toBe(3);
|
|
expect(grid.size).toContain("40px");
|
|
|
|
await openSettings(page, testInfo.project.name);
|
|
await page
|
|
.getByRole("navigation", { name: "Instellingensecties" })
|
|
.getByRole("button", { name: "Weergave" })
|
|
.click();
|
|
await page.locator(".appearance-advanced > summary").click();
|
|
await page
|
|
.locator('.wallpaper-settings input[type="file"]')
|
|
.setInputFiles("public/dockdeck-icon.png");
|
|
await expect(
|
|
page.locator(".wallpaper-editor-preview .wallpaper-preview-image"),
|
|
).toBeVisible();
|
|
await page.getByRole("button", { name: "Vullend" }).click();
|
|
await page.getByLabel("Horizontale wallpaperfocus").fill("32");
|
|
await page.getByLabel("Verticale wallpaperfocus").fill("68");
|
|
await page.getByLabel("Wallpaperzoom").fill("120");
|
|
await page.getByRole("button", { name: "Toepassen" }).click();
|
|
await page
|
|
.locator(".settings-topbar nav")
|
|
.getByRole("button", { name: "Dashboard" })
|
|
.click();
|
|
await expect(page.locator(".dashboard")).toHaveClass(/has-wallpaper/);
|
|
const wallpaperStage = page.locator(".dashboard-wallpaper-stage");
|
|
const wallpaperImage = wallpaperStage.locator(".dashboard-wallpaper-image");
|
|
const wallpaperAmbient = wallpaperStage.locator(
|
|
".dashboard-wallpaper-ambient",
|
|
);
|
|
await expect(wallpaperStage).toBeVisible();
|
|
await expect(wallpaperImage).toHaveAttribute("src", /\/api\/wallpaper/);
|
|
await expect(wallpaperAmbient).toHaveAttribute("src", /\/api\/wallpaper/);
|
|
await expect
|
|
.poll(() =>
|
|
wallpaperImage.evaluate((element) => ({
|
|
fit: getComputedStyle(element).objectFit,
|
|
position: getComputedStyle(element).objectPosition,
|
|
transform: getComputedStyle(element).transform,
|
|
})),
|
|
)
|
|
.toMatchObject({
|
|
fit: "cover",
|
|
position: "32% 68%",
|
|
});
|
|
const stageBeforeScroll = await wallpaperStage.boundingBox();
|
|
const canvasBackground = await page
|
|
.locator(".dashboard-canvas")
|
|
.evaluate((element) => ({
|
|
color: getComputedStyle(element).backgroundColor,
|
|
image: getComputedStyle(element).backgroundImage,
|
|
}));
|
|
expect(stageBeforeScroll?.x).toBe(0);
|
|
expect(stageBeforeScroll?.width).toBe(testInfo.project.use.viewport!.width);
|
|
expect(stageBeforeScroll?.y).toBe(72);
|
|
expect(stageBeforeScroll?.height).toBe(
|
|
testInfo.project.use.viewport!.height - 72,
|
|
);
|
|
expect(canvasBackground.image).toBe("none");
|
|
expect(canvasBackground.color).toBe("rgba(0, 0, 0, 0)");
|
|
await page.evaluate(() => window.scrollTo(0, 900));
|
|
await page.waitForTimeout(100);
|
|
const stageAfterScroll = await wallpaperStage.boundingBox();
|
|
expect(stageBeforeScroll?.height).toBeLessThanOrEqual(
|
|
testInfo.project.use.viewport!.height,
|
|
);
|
|
expect(stageAfterScroll?.y).toBeLessThanOrEqual(stageBeforeScroll!.y);
|
|
expect(stageAfterScroll?.y).toBeGreaterThanOrEqual(71);
|
|
|
|
await request.delete("/api/wallpaper");
|
|
await request.patch("/api/preferences", {
|
|
data: {
|
|
backgroundStyle: "aurora",
|
|
wallpaperFit: "contain",
|
|
wallpaperPositionX: 50,
|
|
wallpaperPositionY: 50,
|
|
wallpaperZoom: 100,
|
|
},
|
|
});
|
|
});
|
|
|
|
test("dashboard en instellingen blijven bruikbaar zonder horizontale overflow", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
await page.goto("/");
|
|
const compact = ["mobile-chromium", "tablet-chromium"].includes(
|
|
testInfo.project.name,
|
|
);
|
|
await expect(
|
|
page.getByRole("navigation", {
|
|
name: compact ? "Mobiele navigatie" : "Dashboardnavigatie",
|
|
}),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByLabel(
|
|
testInfo.project.name === "mobile-chromium"
|
|
? "Zoek apps en favorieten mobiel"
|
|
: "Zoek apps, favorieten of Google",
|
|
),
|
|
).toBeVisible();
|
|
if (testInfo.project.name === "ultrawide-chromium") {
|
|
const widgetDock = page.locator(".dashboard-insights-panel");
|
|
await expect(widgetDock).toHaveAttribute("open", "");
|
|
const dockBox = await widgetDock.boundingBox();
|
|
const groupsBox = await page.locator(".dashboard-groups").boundingBox();
|
|
expect(dockBox).not.toBeNull();
|
|
expect(groupsBox).not.toBeNull();
|
|
expect(dockBox!.x).toBeGreaterThan(groupsBox!.x);
|
|
}
|
|
await openSettings(page, testInfo.project.name);
|
|
await expect(
|
|
page.getByRole("heading", { name: "App Beheer", exact: true }),
|
|
).toBeVisible();
|
|
const overflow = await page.evaluate(
|
|
() => document.documentElement.scrollWidth - window.innerWidth,
|
|
);
|
|
expect(overflow).toBeLessThanOrEqual(1);
|
|
});
|
|
|
|
test("legt de finale Stitch-compositie op de canonieke viewports vast", async ({
|
|
page,
|
|
request,
|
|
}, testInfo) => {
|
|
const dashboardResponse = await request.get("/api/dashboard");
|
|
const dashboard = (await dashboardResponse.json()) as {
|
|
apps: Array<{ id: string; technicalName: string }>;
|
|
categories: Array<{ id: string; name: string }>;
|
|
favorites: Array<{ id: string }>;
|
|
};
|
|
let smartHome = dashboard.categories.find(
|
|
(category) => category.name === "Smart Home",
|
|
);
|
|
if (!smartHome) {
|
|
const response = await request.post("/api/categories", {
|
|
data: { name: "Smart Home", icon: "House", sortOrder: 20 },
|
|
});
|
|
smartHome = (await response.json()) as { id: string; name: string };
|
|
}
|
|
const media = dashboard.categories.find(
|
|
(category) => category.name === "Media",
|
|
);
|
|
const tools = dashboard.categories.find(
|
|
(category) => category.name === "Tools",
|
|
);
|
|
for (const app of dashboard.apps) {
|
|
const isHome = /home/i.test(app.technicalName);
|
|
const isMedia = /plex|immich/i.test(app.technicalName);
|
|
await request.patch(`/api/apps/${app.id}`, {
|
|
data: {
|
|
visible: !/paperless/i.test(app.technicalName),
|
|
categoryId: isHome
|
|
? smartHome.id
|
|
: isMedia
|
|
? (media?.id ?? null)
|
|
: (tools?.id ?? null),
|
|
},
|
|
});
|
|
}
|
|
if (dashboard.favorites.length === 0) {
|
|
for (const [index, favorite] of [
|
|
["Google", "https://google.com"],
|
|
["YouTube", "https://youtube.com"],
|
|
["GitHub", "https://github.com"],
|
|
].entries()) {
|
|
await request.post("/api/favorites", {
|
|
data: {
|
|
name: favorite[0],
|
|
url: favorite[1],
|
|
icon: null,
|
|
categoryId: null,
|
|
sortOrder: index,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
await request.patch("/api/preferences", { data: { theme: "dark" } });
|
|
await page.goto("/");
|
|
await expect(page.getByRole("main")).toBeVisible();
|
|
const inferredAppIcons = page.locator(".app-tile .app-brand-icon");
|
|
expect(await inferredAppIcons.count()).toBeGreaterThan(0);
|
|
await expect(page.locator(".app-monogram")).toHaveCount(0);
|
|
|
|
const screenshotRoot = "docs/design/dockdeck-canon-v1/implementation";
|
|
if (testInfo.project.name === "desktop-chromium") {
|
|
await page.screenshot({
|
|
path: `${screenshotRoot}/dashboard-dark-desktop-1440x900.png`,
|
|
});
|
|
await page.keyboard.press("Control+K");
|
|
await page.getByLabel("Zoek apps, favorieten of Google").fill("Plex");
|
|
await page.screenshot({
|
|
path: `${screenshotRoot}/quick-launch-dark-desktop-1440x900.png`,
|
|
});
|
|
await page.keyboard.press("Escape");
|
|
await openSettings(page, testInfo.project.name);
|
|
await page.screenshot({
|
|
path: `${screenshotRoot}/settings-apps-dark-desktop-1440x900.png`,
|
|
});
|
|
await page
|
|
.getByRole("button", { name: /Bewerken/ })
|
|
.first()
|
|
.click();
|
|
await page.screenshot({
|
|
path: `${screenshotRoot}/app-drawer-dark-desktop-1440x900.png`,
|
|
});
|
|
await page
|
|
.getByRole("dialog")
|
|
.getByRole("button", { name: "Sluiten", exact: true })
|
|
.click();
|
|
await request.patch("/api/preferences", { data: { theme: "light" } });
|
|
await page.goto("/");
|
|
await page.screenshot({
|
|
path: `${screenshotRoot}/dashboard-light-desktop-1440x900.png`,
|
|
});
|
|
await page.keyboard.press("Control+K");
|
|
await page.getByLabel("Zoek apps, favorieten of Google").fill("Plex");
|
|
await page.screenshot({
|
|
path: `${screenshotRoot}/quick-launch-light-desktop-1440x900.png`,
|
|
});
|
|
await page.keyboard.press("Escape");
|
|
await openSettings(page, testInfo.project.name);
|
|
await page.screenshot({
|
|
path: `${screenshotRoot}/settings-apps-light-desktop-1440x900.png`,
|
|
});
|
|
} else if (testInfo.project.name === "mobile-chromium") {
|
|
await page.screenshot({
|
|
path: `${screenshotRoot}/dashboard-dark-mobile-390x844.png`,
|
|
});
|
|
await openSettings(page, testInfo.project.name);
|
|
await page
|
|
.getByRole("button", { name: /Bewerken/ })
|
|
.first()
|
|
.click();
|
|
await page.screenshot({
|
|
path: `${screenshotRoot}/app-drawer-mobile-390x844.png`,
|
|
});
|
|
} else if (testInfo.project.name === "tablet-chromium") {
|
|
await page.screenshot({
|
|
path: `${screenshotRoot}/dashboard-dark-tablet-768x1024.png`,
|
|
});
|
|
} else {
|
|
await page.screenshot({
|
|
path: `${screenshotRoot}/dashboard-dark-ultrawide-3440x1440.png`,
|
|
});
|
|
}
|
|
});
|