import { expect, test, type Page } from "@playwright/test"; const browserErrors = new WeakMap(); test.beforeAll(async ({ request }) => { const dashboard = (await (await request.get("/api/dashboard")).json()) as { favorites: Array<{ id: string }>; }; for (const favorite of dashboard.favorites) { await request.delete(`/api/favorites/${favorite.id}`); } for (const [index, [name, url]] of [ ["Google", "https://google.com"], ["YouTube", "https://youtube.com"], ["GitHub", "https://github.com"], ].entries()) { await request.post("/api/favorites", { data: { name, url, icon: null, categoryId: null, sortOrder: index }, }); } }); 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("img", { name: "ITWorx.tech" }).first(), ).toBeVisible(); await expect( page.locator('img[src="/dockdeck-mark-v2.svg"]').first(), ).toBeVisible(); await expect( page.getByText("DockDeck", { exact: true }).first(), ).toBeVisible(); await expect( page.getByLabel("Ontwikkeld door Jens van ITWorx.tech").first(), ).toBeVisible(); await expect(page.getByText(/App-API vergrendeld/i)).toHaveCount(0); await expect(page.getByText(/App-detail optioneel/i)).toHaveCount(0); await expect(page.getByText("Niet gekoppeld", { exact: true })).toHaveCount( 0, ); 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(); const favoriteFolder = page.locator(".rail-folder-toggle").first(); await expect(favoriteFolder).toContainText("Losse favorieten"); await expect(favoriteFolder).toContainText("3"); await expect(favoriteFolder).toHaveAttribute("aria-expanded", "false"); await favoriteFolder.click(); await expect(favoriteFolder).toHaveAttribute("aria-expanded", "true"); await expect( page.getByRole("link", { name: /Google/ }).first(), ).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("launch history stays local, filters stale items, and remains optional", async ({ page, context, request, }, testInfo) => { test.skip( testInfo.project.name !== "desktop-chromium", "De launcherhistorieflow wordt eenmaal uitgevoerd", ); const dashboard = (await (await request.get("/api/dashboard")).json()) as { apps: Array<{ id: string; displayName: string; visible: boolean; localUrl: string | null; localUrlOverride: string | null; externalUrl: string | null; externalUrlOverride: string | null; }>; favorites: Array<{ id: string; name: string }>; preferences: { externalAccess: boolean }; }; const currentUrl = (app: (typeof dashboard.apps)[number]) => dashboard.preferences.externalAccess ? app.externalUrlOverride || app.externalUrl || app.localUrlOverride || app.localUrl : app.localUrlOverride || app.localUrl; const visibleApp = dashboard.apps.find( (app) => app.visible && currentUrl(app), ); const hiddenApp = dashboard.apps.find( (app) => !app.visible && currentUrl(app), ); const noUrlApp = dashboard.apps.find( (app) => app.visible && !currentUrl(app), ); const favorite = dashboard.favorites[0]; expect(favorite).toBeDefined(); const openedAt = Date.now() - 30_000; const initialHistory = [ ...(visibleApp ? [{ key: `app:${visibleApp.id}`, lastOpenedAt: openedAt, count: 3 }] : []), { key: `favorite:${favorite!.id}`, lastOpenedAt: openedAt - 1_000, count: 7, }, ...(hiddenApp ? [ { key: `app:${hiddenApp.id}`, lastOpenedAt: openedAt + 1_000, count: 99, }, ] : []), ...(noUrlApp ? [ { key: `app:${noUrlApp.id}`, lastOpenedAt: openedAt + 1_500, count: 80, }, ] : []), { key: "favorite:deleted-from-dashboard", lastOpenedAt: openedAt + 2_000, count: 100, }, ]; await context.addInitScript((history) => { if (window.location.hostname === "127.0.0.1") { window.localStorage.setItem( "dockdeck.launch-history.v1", JSON.stringify(history), ); } }, initialHistory); await context.route("https://**/favicon.ico", (route) => route.fulfill({ status: 200, contentType: "image/svg+xml", body: '', }), ); await page.goto("/"); const historySection = page.getByTestId("launch-history"); await expect(historySection).toBeVisible(); const historyCards = historySection.locator(".launch-history-card"); await expect(historyCards).toHaveCount(visibleApp ? 2 : 1); if (visibleApp) { await expect(historyCards.first()).toHaveAttribute( "data-launch-key", `app:${visibleApp.id}`, ); } if (hiddenApp) { await expect( historySection.locator(`[data-launch-key="app:${hiddenApp.id}"]`), ).toHaveCount(0); } if (noUrlApp) { await expect( historySection.locator(`[data-launch-key="app:${noUrlApp.id}"]`), ).toHaveCount(0); } await expect( historySection.locator( '[data-launch-key="favorite:deleted-from-dashboard"]', ), ).toHaveCount(0); await historySection.getByRole("button", { name: "Veel gebruikt" }).click(); await expect(historyCards.first()).toHaveAttribute( "data-launch-key", `favorite:${favorite!.id}`, ); await historySection.getByRole("button", { name: "Verbergen" }).click(); await expect(historySection.locator(".launch-history-grid")).toHaveCount(0); await historySection.getByRole("button", { name: "Tonen" }).click(); await expect(historyCards).toHaveCount(visibleApp ? 2 : 1); const retained = await page.evaluate(() => { const value = window.localStorage.getItem("dockdeck.launch-history.v1"); return value ? JSON.parse(value) : []; }); expect(retained.map((item: { key: string }) => item.key).sort()).toEqual( [ ...(visibleApp ? [`app:${visibleApp.id}`] : []), `favorite:${favorite!.id}`, ].sort(), ); await historySection .getByRole("button", { name: "Recente geschiedenis wissen" }) .click(); await expect(historySection.locator(".launch-history-empty")).toBeVisible(); await expect .poll(() => page.evaluate(() => window.localStorage.getItem("dockdeck.launch-history.v1"), ), ) .toBeNull(); if (hiddenApp) { await page.keyboard.press("Control+K"); await page .getByLabel("Zoek apps, favorieten of Google") .fill(hiddenApp.displayName); const hiddenResult = page .locator(".result-group [role=option]") .filter({ hasText: hiddenApp.displayName }) .first(); const hiddenPopupPromise = context.waitForEvent("page"); await hiddenResult.click(); const hiddenPopup = await hiddenPopupPromise; await hiddenPopup.close(); await expect .poll(() => page.evaluate(() => window.localStorage.getItem("dockdeck.launch-history.v1"), ), ) .toBeNull(); } await expect(page.locator(".sync-status-button").first()).toBeEnabled(); const apiRequests: string[] = []; context.on("request", (apiRequest) => { const url = new URL(apiRequest.url()); if (url.pathname.startsWith("/api/")) { apiRequests.push(`${apiRequest.method()} ${url.pathname}`); } }); await page.keyboard.press("Control+K"); const quickLink = page .locator(".quick-links-grid") .getByRole("link", { name: new RegExp(favorite!.name) }) .first(); const popupPromise = context.waitForEvent("page"); await quickLink.click(); const popup = await popupPromise; await popup.close(); await expect .poll(() => page.evaluate(() => { const value = window.localStorage.getItem("dockdeck.launch-history.v1"); return value ? JSON.parse(value) : []; }), ) .toMatchObject([{ key: `favorite:${favorite!.id}`, count: 1 }]); const localHistory = await page.evaluate(() => { const value = window.localStorage.getItem("dockdeck.launch-history.v1"); return value ? JSON.parse(value) : []; }); expect(Object.keys(localHistory[0]).sort()).toEqual([ "count", "key", "lastOpenedAt", ]); expect( await page.evaluate(() => Object.keys(window.localStorage).sort()), ).toEqual(["dockdeck.launch-history.v1"]); expect(apiRequests.filter((request) => !request.startsWith("GET "))).toEqual( [], ); expect(apiRequests).not.toContain("GET /api/export"); expect( apiRequests.some((request) => request.includes("launch-history")), ).toBe(false); }); test("favorietenbeheer kan zelf mappen voor de favorietenbalk aanmaken", async ({ page, request, }, testInfo) => { test.skip( testInfo.project.name !== "desktop-chromium", "De desktopflow wordt eenmaal uitgevoerd", ); const folderName = "Werk"; const initialDashboard = (await ( await request.get("/api/dashboard") ).json()) as { categories: Array<{ id: string; name: string }>; favorites: Array<{ id: string; name: string }>; }; for (const category of initialDashboard.categories.filter( (item) => item.name === folderName, )) { await request.delete(`/api/categories/${category.id}`); } await page.goto("/"); await page .getByRole("button", { name: "Favorieten en mappen beheren" }) .click(); await expect( page.getByRole("heading", { name: "Favorieten", exact: true }), ).toBeVisible(); await page.getByRole("button", { name: "Nieuwe map" }).click(); await page.getByLabel("Naam van nieuwe favorietenmap").fill(folderName); await page.getByRole("button", { name: "Map opslaan" }).click(); const refreshedDashboard = (await ( await request.get("/api/dashboard") ).json()) as { categories: Array<{ id: string; name: string }>; favorites: Array<{ id: string; name: string }>; }; const folder = refreshedDashboard.categories.find( (item) => item.name === folderName, ); const google = refreshedDashboard.favorites.find( (item) => item.name === "Google", ); expect(folder).toBeTruthy(); expect(google).toBeTruthy(); const googleCard = page.getByRole("article").filter({ hasText: "Google" }); await googleCard.locator("summary").click(); await googleCard.getByLabel("Map (categorie)").selectOption(folder!.id); await page .locator(".settings-topbar nav") .getByRole("button", { name: "Dashboard" }) .click(); const railFolder = page .locator(".rail-folder-toggle") .filter({ hasText: folderName }); await expect(railFolder).toBeVisible(); await expect(railFolder).toContainText("1"); await request.patch(`/api/favorites/${google!.id}`, { data: { categoryId: null }, }); await request.delete(`/api/categories/${folder!.id}`); }); 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("apps can be dragged within a category and the order is persisted", async ({ page, request, }, testInfo) => { test.skip( testInfo.project.name !== "desktop-chromium", "De dragflow wordt eenmaal uitgevoerd", ); const dashboard = (await (await request.get("/api/dashboard")).json()) as { apps: Array<{ id: string; displayName: string; visible: boolean; categoryId: string | null; sortOrder: number; }>; }; const [sourceApp, targetApp] = dashboard.apps; expect(sourceApp).toBeTruthy(); expect(targetApp).toBeTruthy(); const categoryResponse = await request.post("/api/categories", { data: { name: `Sorteerproef ${Date.now()}`, icon: "Layers3", sortOrder: 10_000, }, }); expect(categoryResponse.ok()).toBeTruthy(); const category = (await categoryResponse.json()) as { id: string }; const otherCategoryResponse = await request.post("/api/categories", { data: { name: `Sorteerproef elders ${Date.now()}`, icon: "Layers3", sortOrder: 10_000, }, }); expect(otherCategoryResponse.ok()).toBeTruthy(); const otherCategory = (await otherCategoryResponse.json()) as { id: string }; try { await request.patch(`/api/apps/${sourceApp!.id}`, { data: { visible: true, categoryId: category.id, sortOrder: 0 }, }); await request.patch(`/api/apps/${targetApp!.id}`, { data: { visible: true, categoryId: category.id, sortOrder: 10 }, }); await page.goto("/"); const appGrid = page .locator(".dashboard-groups .app-grid") .filter({ hasText: sourceApp!.displayName }) .filter({ hasText: targetApp!.displayName }); const sourceCard = appGrid .locator(".sortable-list-item") .filter({ hasText: sourceApp!.displayName }); const targetCard = appGrid .locator(".sortable-list-item") .filter({ hasText: targetApp!.displayName }); await expect(sourceCard).toBeVisible(); await expect(targetCard).toBeVisible(); const sourceHandle = sourceCard.locator(".sortable-handle"); const sourceBounds = await sourceHandle.boundingBox(); const targetBounds = await targetCard.boundingBox(); expect(sourceBounds).not.toBeNull(); expect(targetBounds).not.toBeNull(); await page.mouse.move( sourceBounds!.x + sourceBounds!.width / 2, sourceBounds!.y + sourceBounds!.height / 2, ); await page.mouse.down(); await page.mouse.move( targetBounds!.x + targetBounds!.width * 0.9, targetBounds!.y + targetBounds!.height / 2, { steps: 5 }, ); await page.mouse.up(); await expect .poll(async () => { const current = (await ( await request.get("/api/dashboard") ).json()) as { apps: Array<{ id: string; categoryId: string | null }>; }; return current.apps .filter((app) => app.categoryId === category.id) .map((app) => app.id); }) .toEqual([targetApp!.id, sourceApp!.id]); await request.patch(`/api/apps/${targetApp!.id}`, { data: { categoryId: otherCategory.id, sortOrder: 0 }, }); await page.reload(); const sourceCardAfterSplit = page .locator(".dashboard-groups .sortable-list-item") .filter({ hasText: sourceApp!.displayName }); const otherCategoryCard = page .locator(".dashboard-groups .sortable-list-item") .filter({ hasText: targetApp!.displayName }); const sourceHandleAfterSplit = sourceCardAfterSplit.locator(".sortable-handle"); const sourceBoundsAfterSplit = await sourceHandleAfterSplit.boundingBox(); const otherCategoryBounds = await otherCategoryCard.boundingBox(); expect(sourceBoundsAfterSplit).not.toBeNull(); expect(otherCategoryBounds).not.toBeNull(); await page.mouse.move( sourceBoundsAfterSplit!.x + sourceBoundsAfterSplit!.width / 2, sourceBoundsAfterSplit!.y + sourceBoundsAfterSplit!.height / 2, ); await page.mouse.down(); await page.mouse.move( otherCategoryBounds!.x + otherCategoryBounds!.width / 2, otherCategoryBounds!.y + otherCategoryBounds!.height / 2, { steps: 5 }, ); await page.mouse.up(); const unchangedCategories = (await ( await request.get("/api/dashboard") ).json()) as { apps: Array<{ id: string; categoryId: string | null }>; }; expect( unchangedCategories.apps.find((app) => app.id === sourceApp!.id) ?.categoryId, ).toBe(category.id); expect( unchangedCategories.apps.find((app) => app.id === targetApp!.id) ?.categoryId, ).toBe(otherCategory.id); expect(page.context().pages()).toHaveLength(1); expect( await page.evaluate(() => window.localStorage.getItem("dockdeck.launch-history.v1"), ), ).toBeNull(); } finally { await request.patch(`/api/apps/${sourceApp!.id}`, { data: { visible: sourceApp!.visible, categoryId: sourceApp!.categoryId, sortOrder: sourceApp!.sortOrder, }, }); await request.patch(`/api/apps/${targetApp!.id}`, { data: { visible: targetApp!.visible, categoryId: targetApp!.categoryId, sortOrder: targetApp!.sortOrder, }, }); await request.delete(`/api/categories/${otherCategory.id}`); await request.delete(`/api/categories/${category.id}`); } }); test("Mijn projecten-filter toont alleen gemarkeerde eigen apps en onthoudt de keuze", 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()) as { apps: Array<{ id: string; displayName: string }>; }; const [personalApp, otherApp] = dashboard.apps; expect(personalApp).toBeTruthy(); expect(otherApp).toBeTruthy(); await request.patch(`/api/apps/${personalApp!.id}`, { data: { visible: true, personalProject: true }, }); await request.patch(`/api/apps/${otherApp!.id}`, { data: { visible: true, personalProject: false }, }); await page.goto("/"); const focusToggle = page.getByRole("button", { name: /Mijn projecten/ }); const personalTile = page .locator(".app-tile") .filter({ hasText: personalApp!.displayName }); const otherTile = page .locator(".app-tile") .filter({ hasText: otherApp!.displayName }); await expect(focusToggle).toHaveAttribute("aria-pressed", "false"); await expect(personalTile).toBeVisible(); await expect(otherTile).toBeVisible(); await focusToggle.click(); await expect(focusToggle).toHaveAttribute("aria-pressed", "true"); await expect(personalTile).toBeVisible(); await expect(otherTile).toHaveCount(0); await page.reload(); await expect(focusToggle).toHaveAttribute("aria-pressed", "true"); await expect(personalTile).toBeVisible(); await expect(otherTile).toHaveCount(0); }); 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("Full HD zijbalken zijn compact, inklapbaar en onthouden hun stand", async ({ page, }, testInfo) => { test.skip( testInfo.project.name !== "fullhd-chromium", "Deze controle hoort bij de Full HD-layout", ); await page.setViewportSize({ width: 1860, height: 1080 }); await page.goto("/"); const workspace = page.locator(".dashboard-workspace"); const leftRail = page.locator(".dashboard-rail"); const widgetDock = page.locator(".dashboard-insights-panel"); const categoryTabs = page.locator(".category-tabs"); const openRailBox = (await leftRail.boundingBox())!; const railIntroBox = (await page.locator(".rail-intro").boundingBox())!; expect(openRailBox.width).toBeLessThanOrEqual(260); expect(railIntroBox.y - openRailBox.y).toBeLessThanOrEqual(32); const railCollapseBox = (await page .getByRole("button", { name: "Linkerzijbalk inklappen" }) .boundingBox())!; expect(railCollapseBox.y - openRailBox.y).toBeLessThanOrEqual(32); await expect( page.getByRole("button", { name: "Linkerzijbalk inklappen" }), ).toHaveCSS("position", "static"); await expect(widgetDock).not.toHaveAttribute("open", ""); await expect(widgetDock).toHaveCSS("position", "fixed"); expect((await widgetDock.boundingBox())!.width).toBeLessThanOrEqual(72); const categoryOverflow = await categoryTabs.evaluate( (element) => element.scrollWidth - element.clientWidth, ); await expect(categoryTabs).toHaveCSS("flex-wrap", "wrap"); expect(categoryOverflow).toBeLessThanOrEqual(1); await page.getByRole("button", { name: "Linkerzijbalk inklappen" }).click(); await expect(workspace).toHaveClass(/rail-collapsed/); expect((await leftRail.boundingBox())!.width).toBeLessThanOrEqual(72); await widgetDock.locator("summary").click(); await expect(widgetDock).toHaveAttribute("open", ""); expect((await widgetDock.boundingBox())!.width).toBeLessThanOrEqual(460); await page.reload(); await expect(workspace).toHaveClass(/rail-collapsed/); await expect(widgetDock).toHaveAttribute("open", ""); await openSettings(page, testInfo.project.name); const settingsLayout = page.locator(".settings-layout"); const openSettingsRailBox = (await page .locator(".settings-sidebar") .boundingBox())!; const settingsEyebrowBox = (await page .locator(".settings-sidebar-header .eyebrow") .boundingBox())!; expect(settingsEyebrowBox.y - openSettingsRailBox.y).toBeLessThanOrEqual(32); const settingsCollapseBox = (await page .getByRole("button", { name: "Instellingenzijbalk inklappen" }) .boundingBox())!; expect(settingsCollapseBox.y - openSettingsRailBox.y).toBeLessThanOrEqual(32); await expect( page.getByRole("button", { name: "Instellingenzijbalk inklappen" }), ).toHaveCSS("position", "static"); await page .getByRole("button", { name: "Instellingenzijbalk inklappen" }) .click(); await expect(settingsLayout).toHaveClass(/sidebar-collapsed/); expect((await page.locator(".settings-sidebar").boundingBox())!.width).toBe( 72, ); }); 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 if (testInfo.project.name === "ultrawide-chromium") { await page.screenshot({ path: `${screenshotRoot}/dashboard-dark-ultrawide-3440x1440.png`, }); } else { await page.screenshot({ path: `${screenshotRoot}/dashboard-dark-fullhd-1920x1080.png`, }); } });