import { expect, test, type Page } from "@playwright/test"; const browserErrors = new WeakMap(); 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`, }); } });