Files
homarr/e2e/lazy-widgets.spec.ts
T
+12 fcd7c979df feat!: release v2 (#6545)
Co-authored-by: Artur <145776561+arturict@users.noreply.github.com>
Co-authored-by: arturict <arturict@users.noreply.github.com>
Co-authored-by: Thomas <thomas@ajnart.fr>
Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com>
Co-authored-by: Danura30082 <danura30082+github@gmail.com>
Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com>
Co-authored-by: ajnart <thomas@ajnart.fr>
Co-authored-by: ajnart <thomas@ajnart.dev>
Co-authored-by: chenjun <chenjun@foxmail.com>
Co-authored-by: Wikibear <wikibear@users.noreply.github.com>
Co-authored-by: Bergasha <bergasha@gmail.com>
Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com>
Co-authored-by: Lucas Araujo <lucas.lra@gmail.com>
Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com>
Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social>
Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com>
Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com>
Co-authored-by: thrawnx <tight.bulb9384@fastmail.com>
Co-authored-by: Chris <58918656+nordada@users.noreply.github.com>
Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com>
Co-authored-by: Yanis <naamaneyanis@gmail.com>
Co-authored-by: Wikibear <me@sej.de>
Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
2026-10-02 21:00:14 +02:00

201 lines
7.2 KiB
TypeScript

import { createId } from "@paralleldrive/cuid2";
import { chromium, expect } from "@playwright/test";
import type { Route } from "@playwright/test";
import { eq } from "drizzle-orm";
import { stringify } from "superjson";
import { describe, test } from "vitest";
import * as sqliteSchema from "../packages/db/schema/sqlite";
import { createHomarrContainer } from "./shared/create-homarr-container";
import { createSqliteDbFileAsync } from "./shared/e2e-db";
import { loginAsync } from "./shared/login";
import { seedAdminUserAsync } from "./shared/seed-admin-user";
const adminCredentials = {
username: "admin",
password: "Comp(exP4sswOrd",
};
describe("lazy widget application graph", () => {
test("renders an authenticated board and preserves first-use UI behavior after reload", async () => {
const { db, localMountPath } = await createSqliteDbFileAsync();
const { userId } = await seedAdminUserAsync(db, adminCredentials);
const boardId = createId();
const layoutId = createId();
const sectionId = createId();
const clockItemId = createId();
const downloadsItemId = createId();
const mockIntegrationId = createId();
await db.insert(sqliteSchema.boards).values({
id: boardId,
name: "lazy-widget-board",
creatorId: userId,
});
await db.insert(sqliteSchema.layouts).values({
id: layoutId,
name: "Base",
boardId,
columnCount: 12,
});
await db.insert(sqliteSchema.sections).values({
id: sectionId,
boardId,
kind: "empty",
xOffset: 0,
yOffset: 0,
});
await db.insert(sqliteSchema.integrations).values({
id: mockIntegrationId,
name: "Mock downloads",
url: "http://mock.local",
kind: "mock",
});
await db.insert(sqliteSchema.items).values([
{
id: clockItemId,
boardId,
kind: "clock",
options: stringify({ showSeconds: true }),
},
{
id: downloadsItemId,
boardId,
kind: "downloads",
options: stringify({}),
},
]);
await db.insert(sqliteSchema.itemLayouts).values([
{
itemId: clockItemId,
sectionId,
layoutId,
xOffset: 0,
yOffset: 0,
width: 4,
height: 2,
},
{
itemId: downloadsItemId,
sectionId,
layoutId,
xOffset: 0,
yOffset: 2,
width: 8,
height: 5,
},
]);
await db.insert(sqliteSchema.integrationItems).values({
itemId: downloadsItemId,
integrationId: mockIntegrationId,
});
await db.update(sqliteSchema.users).set({ homeBoardId: boardId }).where(eq(sqliteSchema.users.id, userId));
const homarrContainer = await createHomarrContainer({
environment: {
AUTH_PROVIDERS: "credentials",
UNSAFE_ENABLE_MOCK_INTEGRATION: "true",
},
mounts: {
"/appdata": localMountPath,
},
}).start();
const baseUrl = `http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`;
const browser = await chromium.launch();
const context = await browser.newContext({ serviceWorkers: "block" });
const page = await context.newPage();
const pageErrors: Error[] = [];
const hydrationErrors: string[] = [];
page.on("pageerror", (error) => pageErrors.push(error));
const hydrationErrorPattern = /hydrat|did not match|react\.dev\/errors\/4(18|2[1235])|react error #4(18|2[1235])/i;
page.on("console", (message) => {
if (message.type() === "error" && hydrationErrorPattern.test(message.text())) {
hydrationErrors.push(message.text());
}
});
let releaseRefresh: (() => void) | undefined;
try {
await loginAsync({ page, baseUrl, credentials: adminCredentials });
const visibleBoard = page.locator("[data-homarr-dev-benchmark-board]").filter({ visible: true }).first();
await expect(visibleBoard).toBeVisible({ timeout: 30_000 });
const clockWidget = page.locator(
`[data-id="${clockItemId}"] .clock-widget-container, [data-grid-item-id="${clockItemId}"] .clock-widget-container`,
);
await expect(clockWidget.filter({ visible: true }).first()).toBeVisible({ timeout: 30_000 });
await expect(clockWidget.locator("time").filter({ visible: true }).first()).not.toHaveText("--:--", {
timeout: 30_000,
});
const downloadsWidget = page
.locator(`[data-id="${downloadsItemId}"], [data-grid-item-id="${downloadsItemId}"]`)
.filter({ visible: true })
.first();
const cachedDownload = downloadsWidget.getByText("Big.Buck.Bunny.2008.1080p.BluRay.x264");
await expect(cachedDownload).toBeVisible({ timeout: 30_000 });
await expect(page.locator("[data-homarr-dev-benchmark-spotlight-preloaded]")).toHaveCount(1, {
timeout: 15_000,
});
await page.keyboard.press("Control+K");
await expect(page.locator("[data-homarr-dev-benchmark-spotlight-feedback]")).toHaveCount(0);
await expect(page.locator("[data-homarr-dev-benchmark-spotlight]")).toBeVisible({ timeout: 15_000 });
await page.keyboard.press("Escape");
await expect(page.locator("[data-homarr-dev-benchmark-spotlight]")).not.toBeVisible();
let refreshRequestStarted = false;
const refreshGate = new Promise<void>((resolve) => {
releaseRefresh = resolve;
});
const downloadsRequestPattern = "**/*widget.downloads.getJobsAndStatuses*";
const delayDownloadsRefresh = async (route: Route) => {
refreshRequestStarted = true;
await refreshGate;
await route.continue();
};
await page.route(downloadsRequestPattern, delayDownloadsRefresh);
await page.reload({ waitUntil: "domcontentloaded" });
await expect(clockWidget.filter({ visible: true }).first()).toBeVisible({ timeout: 30_000 });
await expect(clockWidget.locator("time").filter({ visible: true }).first()).not.toHaveText("--:--", {
timeout: 30_000,
});
await expect.poll(() => refreshRequestStarted, { timeout: 10_000 }).toBe(true);
await expect(cachedDownload).toBeVisible({ timeout: 5_000 });
const refreshedDownloadsResponse = page.waitForResponse(
(response) => response.url().includes("widget.downloads.getJobsAndStatuses") && response.ok(),
);
releaseRefresh();
releaseRefresh = undefined;
await refreshedDownloadsResponse;
await page.unroute(downloadsRequestPattern, delayDownloadsRefresh);
expect(pageErrors).toEqual([]);
expect(hydrationErrors).toEqual([]);
const serviceWorkerContext = await browser.newContext();
const serviceWorkerPage = await serviceWorkerContext.newPage();
try {
await serviceWorkerPage.goto(`${baseUrl}/auth/login`);
await expect
.poll(
async () =>
await serviceWorkerPage.evaluate(async () => {
const registrations = await navigator.serviceWorker.getRegistrations();
return registrations[0]?.scope ?? null;
}),
{ timeout: 15_000 },
)
.toBe(`${baseUrl}/`);
} finally {
await serviceWorkerContext.close();
}
} finally {
releaseRefresh?.();
await browser.close();
await homarrContainer.stop();
}
}, 180_000);
});