Files

1141 lines
38 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
const browserErrors = new WeakMap<Page, string[]>();
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: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"></svg>',
}),
);
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`,
});
}
});