1141 lines
38 KiB
TypeScript
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`,
|
|
});
|
|
}
|
|
});
|