Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfd0bb7f80 | ||
|
|
2098f89584 |
No files matched your search
@@ -1,509 +0,0 @@
|
|||||||
import type { Browser, BrowserContext, Page } from "@playwright/test";
|
|
||||||
import { chromium, expect } from "@playwright/test";
|
|
||||||
import { createId } from "@paralleldrive/cuid2";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { stringify } from "superjson";
|
|
||||||
import { afterAll, beforeAll, describe, test } from "vitest";
|
|
||||||
|
|
||||||
import { hashPasswordAsync } from "../packages/auth/security";
|
|
||||||
import {
|
|
||||||
apps,
|
|
||||||
boardUserPermissions,
|
|
||||||
boards,
|
|
||||||
itemLayouts,
|
|
||||||
items,
|
|
||||||
layouts,
|
|
||||||
sections,
|
|
||||||
users,
|
|
||||||
} from "../packages/db/schema/sqlite";
|
|
||||||
import type { SqliteDatabase } from "./shared/e2e-db";
|
|
||||||
import { createSqliteDbFileAsync } from "./shared/e2e-db";
|
|
||||||
import { createHomarrContainer } from "./shared/create-homarr-container";
|
|
||||||
import { loginAsync } from "./shared/login";
|
|
||||||
import { seedAdminUserAsync } from "./shared/seed-admin-user";
|
|
||||||
|
|
||||||
const ownerCredentials = { username: "owner", password: "Comp(exP4sswOrd" };
|
|
||||||
const disabledModifierCredentials = { username: "modifier", password: "Comp(exP4sswOrd" };
|
|
||||||
const viewerCredentials = { username: "viewer", password: "Comp(exP4sswOrd" };
|
|
||||||
const boardName = "advanced-interactions-e2e";
|
|
||||||
|
|
||||||
describe("Board advanced interactions", () => {
|
|
||||||
let browser: Browser;
|
|
||||||
let baseUrl: string;
|
|
||||||
let stopContainerAsync: (() => Promise<void>) | undefined;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const { db, localMountPath } = await createSqliteDbFileAsync();
|
|
||||||
const { userId: ownerId } = await seedAdminUserAsync(db, ownerCredentials);
|
|
||||||
await seedInteractionBoardAsync(db, ownerId);
|
|
||||||
await seedCredentialsUserAsync(db, {
|
|
||||||
...disabledModifierCredentials,
|
|
||||||
enableRightClickOnWidgets: false,
|
|
||||||
boardPermission: "modify",
|
|
||||||
});
|
|
||||||
await seedCredentialsUserAsync(db, {
|
|
||||||
...viewerCredentials,
|
|
||||||
enableRightClickOnWidgets: true,
|
|
||||||
boardPermission: "view",
|
|
||||||
});
|
|
||||||
|
|
||||||
const homarrContainer = await createHomarrContainer({
|
|
||||||
environment: { AUTH_PROVIDERS: "credentials" },
|
|
||||||
mounts: { "/appdata": localMountPath },
|
|
||||||
}).start();
|
|
||||||
stopContainerAsync = async () => {
|
|
||||||
await homarrContainer.stop();
|
|
||||||
};
|
|
||||||
baseUrl = `http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`;
|
|
||||||
browser = await chromium.launch();
|
|
||||||
}, 120_000);
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await browser?.close();
|
|
||||||
await stopContainerAsync?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("opens advanced view from Shift-hover, keyboard, and the widget context menu", async () => {
|
|
||||||
const { context, page } = await openBoardAsync(browser, baseUrl, ownerCredentials);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const widget = page.locator("[data-grid-item-id][data-kind='clock'] [data-advanced-focus-source]").first();
|
|
||||||
const advancedViewTrigger = widget.getByRole("button", { name: "Open advanced view" });
|
|
||||||
const compactSurface = page.locator(".clock-wrapper").first();
|
|
||||||
const previewSurface = page.getByRole("region", { name: "Date and time advanced view" });
|
|
||||||
const manualSurface = page.getByRole("dialog", { name: "Date and time advanced view" });
|
|
||||||
const bookmarksPreviewSurface = page.getByRole("region", { name: "Bookmarks advanced view" });
|
|
||||||
const dimmingOverlay = page.locator("[data-advanced-focus-overlay]");
|
|
||||||
const otherWidget = page
|
|
||||||
.locator("[data-grid-item-id][data-kind='bookmarks'] [data-advanced-focus-source]")
|
|
||||||
.first();
|
|
||||||
const header = page.locator("header[data-advanced-focus-background]");
|
|
||||||
const boardMain = page.locator("main[data-advanced-focus-background]");
|
|
||||||
|
|
||||||
await compactSurface.evaluate((element) => {
|
|
||||||
element.setAttribute("data-lifecycle-probe", "same-instance");
|
|
||||||
});
|
|
||||||
await widget.hover();
|
|
||||||
await advancedViewTrigger.focus();
|
|
||||||
await page.keyboard.down("Shift");
|
|
||||||
await expect(previewSurface).toBeVisible({ timeout: 2_000 });
|
|
||||||
await expect(previewSurface).toHaveCSS("animation-name", "none");
|
|
||||||
const compactBounds = await widget.boundingBox();
|
|
||||||
const advancedBounds = await previewSurface.boundingBox();
|
|
||||||
if (!compactBounds || !advancedBounds) throw new Error("Advanced widget has no bounds");
|
|
||||||
expect(advancedBounds?.width).toBeGreaterThan(compactBounds?.width ?? 0);
|
|
||||||
await expect(previewSurface).toHaveAttribute("data-lifecycle-probe", "same-instance");
|
|
||||||
await expect(dimmingOverlay).toBeVisible();
|
|
||||||
await expect(dimmingOverlay).toHaveCSS("pointer-events", "none");
|
|
||||||
await expect(otherWidget).toBeVisible();
|
|
||||||
await expect(advancedViewTrigger).toBeFocused();
|
|
||||||
await expect(header).not.toHaveAttribute("inert", "");
|
|
||||||
await expect(boardMain).not.toHaveAttribute("inert", "");
|
|
||||||
|
|
||||||
await previewSurface.click({ button: "right", position: { x: 80, y: 80 } });
|
|
||||||
const portalledMenuItem = page.getByRole("menuitem", { name: "Open advanced view" });
|
|
||||||
await expect(portalledMenuItem).toBeVisible();
|
|
||||||
await portalledMenuItem.hover();
|
|
||||||
await expect(previewSurface).toBeVisible();
|
|
||||||
await page.keyboard.press("Escape");
|
|
||||||
await expect(portalledMenuItem).toBeHidden();
|
|
||||||
await expect(previewSurface).toBeVisible();
|
|
||||||
|
|
||||||
const otherBounds = await otherWidget.boundingBox();
|
|
||||||
expect(otherBounds).not.toBeNull();
|
|
||||||
if (!otherBounds) throw new Error("Bookmarks widget has no bounds");
|
|
||||||
await previewSurface.evaluate(
|
|
||||||
(element, position) => {
|
|
||||||
const probe = document.createElement("button");
|
|
||||||
probe.dataset.interactivePreviewProbe = "true";
|
|
||||||
probe.style.cssText = `position:absolute;z-index:20;width:40px;height:40px;left:${position.left}px;top:${position.top}px`;
|
|
||||||
element.append(probe);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
left: otherBounds.x + otherBounds.width / 2 - advancedBounds.x - 20,
|
|
||||||
top: otherBounds.y + otherBounds.height / 2 - advancedBounds.y - 20,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const interactivePreviewProbe = previewSurface.locator("[data-interactive-preview-probe='true']");
|
|
||||||
await interactivePreviewProbe.hover();
|
|
||||||
await expect(previewSurface).toBeVisible();
|
|
||||||
await expect(bookmarksPreviewSurface).toBeHidden();
|
|
||||||
await interactivePreviewProbe.evaluate((element) => element.remove());
|
|
||||||
await page.mouse.move(compactBounds.x + 4, compactBounds.y + 4);
|
|
||||||
await page.mouse.move(otherBounds.x + otherBounds.width / 2, otherBounds.y + otherBounds.height / 2);
|
|
||||||
await expect(previewSurface).toBeVisible();
|
|
||||||
await expect(bookmarksPreviewSurface).toBeHidden();
|
|
||||||
await page.mouse.move(4, 4);
|
|
||||||
await expect(previewSurface).toBeHidden();
|
|
||||||
await otherWidget.hover({ position: { x: 4, y: 4 } });
|
|
||||||
await expect(bookmarksPreviewSurface).toBeVisible({ timeout: 2_000 });
|
|
||||||
await page.keyboard.up("Shift");
|
|
||||||
await expect(bookmarksPreviewSurface).toBeHidden();
|
|
||||||
await expect(dimmingOverlay).toBeHidden();
|
|
||||||
await expect(compactSurface).toHaveAttribute("data-lifecycle-probe", "same-instance");
|
|
||||||
await advancedViewTrigger.focus();
|
|
||||||
await page.keyboard.press("Shift+Enter");
|
|
||||||
await expect(manualSurface).toBeVisible();
|
|
||||||
await expect(manualSurface).toHaveAttribute("aria-modal", "true");
|
|
||||||
await expect(header).toHaveAttribute("inert", "");
|
|
||||||
await expect(boardMain).toHaveAttribute("inert", "");
|
|
||||||
expect(await manualSurface.evaluate((element) => element.closest("[data-grid-item-id]"))).toBeNull();
|
|
||||||
await expect(manualSurface.locator(".clock-wrapper")).toHaveAttribute("data-lifecycle-probe", "same-instance");
|
|
||||||
await expect(dimmingOverlay).toHaveCSS("pointer-events", "auto");
|
|
||||||
expect(await page.evaluate(() => getComputedStyle(document.body).overflow)).toBe("hidden");
|
|
||||||
await expect(page.getByRole("button", { name: "Close advanced view" })).toBeFocused();
|
|
||||||
await page.keyboard.press("Tab");
|
|
||||||
await expect(page.getByRole("button", { name: "Close advanced view" })).toBeFocused();
|
|
||||||
await page.keyboard.press("Shift+Tab");
|
|
||||||
await expect(page.getByRole("button", { name: "Close advanced view" })).toBeFocused();
|
|
||||||
await expect(page.getByRole("button", { name: /advanced view is pinned|keep advanced view open/i })).toHaveCount(
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
await manualSurface.evaluate((element) => {
|
|
||||||
const mainViewport = document.createElement("div");
|
|
||||||
mainViewport.dataset.scrollbars = "y";
|
|
||||||
mainViewport.style.cssText = "height:80px;overflow-y:auto";
|
|
||||||
|
|
||||||
const probe = document.createElement("div");
|
|
||||||
probe.dataset.shiftScrollProbe = "true";
|
|
||||||
probe.style.cssText = "height:80px;overflow-y:auto";
|
|
||||||
probe.innerHTML = '<svg data-shift-scroll-target="true"></svg><div style="height:600px"></div>';
|
|
||||||
probe.addEventListener("wheel", (event) => event.stopPropagation());
|
|
||||||
mainViewport.append(probe);
|
|
||||||
mainViewport.insertAdjacentHTML("beforeend", '<div style="height:600px"></div>');
|
|
||||||
element.append(mainViewport);
|
|
||||||
|
|
||||||
const unrelated = document.createElement("div");
|
|
||||||
unrelated.dataset.shiftScrollProbe = "unrelated";
|
|
||||||
unrelated.style.cssText = "height:80px;overflow-y:hidden";
|
|
||||||
unrelated.innerHTML = '<div style="height:600px"></div>';
|
|
||||||
element.append(unrelated);
|
|
||||||
});
|
|
||||||
const scrollProbe = manualSurface.locator("[data-shift-scroll-probe='true']");
|
|
||||||
await page.keyboard.down("Shift");
|
|
||||||
const pixelEventCancelled = await scrollProbe.evaluate(
|
|
||||||
(element) =>
|
|
||||||
!element.dispatchEvent(
|
|
||||||
new WheelEvent("wheel", { bubbles: true, cancelable: true, shiftKey: true, deltaX: 120 }),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(pixelEventCancelled).toBe(true);
|
|
||||||
await scrollProbe.evaluate((element) => {
|
|
||||||
element.scrollTop = 0;
|
|
||||||
});
|
|
||||||
const lineEventCancelled = await scrollProbe
|
|
||||||
.locator("[data-shift-scroll-target='true']")
|
|
||||||
.evaluate(
|
|
||||||
(element) =>
|
|
||||||
!element.dispatchEvent(
|
|
||||||
new WheelEvent("wheel", { bubbles: true, cancelable: true, shiftKey: true, deltaX: 3, deltaMode: 1 }),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await page.keyboard.up("Shift");
|
|
||||||
expect(lineEventCancelled).toBe(true);
|
|
||||||
expect(await scrollProbe.evaluate((element) => element.scrollTop)).toBeGreaterThanOrEqual(48);
|
|
||||||
await scrollProbe.evaluate((element) => {
|
|
||||||
element.scrollTop = element.scrollHeight;
|
|
||||||
element.dispatchEvent(
|
|
||||||
new WheelEvent("wheel", { bubbles: true, cancelable: true, shiftKey: true, deltaX: 120 }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
expect(await manualSurface.locator("[data-scrollbars='y']").evaluate((element) => element.scrollTop)).toBe(120);
|
|
||||||
expect(
|
|
||||||
await manualSurface.locator("[data-shift-scroll-probe='unrelated']").evaluate((element) => element.scrollTop),
|
|
||||||
).toBe(0);
|
|
||||||
|
|
||||||
await manualSurface.click({ button: "right", position: { x: 80, y: 80 } });
|
|
||||||
await expect(portalledMenuItem).toBeVisible();
|
|
||||||
await page.keyboard.press("Escape");
|
|
||||||
await expect(portalledMenuItem).toBeHidden();
|
|
||||||
await expect(manualSurface).toBeVisible();
|
|
||||||
await page.keyboard.press("Escape");
|
|
||||||
await expect(manualSurface).toBeHidden();
|
|
||||||
await expect(advancedViewTrigger).toBeFocused();
|
|
||||||
await expect(header).not.toHaveAttribute("inert", "");
|
|
||||||
await expect(boardMain).not.toHaveAttribute("inert", "");
|
|
||||||
expect(await page.evaluate(() => getComputedStyle(document.body).overflow)).not.toBe("hidden");
|
|
||||||
|
|
||||||
await widget.click({ button: "right" });
|
|
||||||
await page.getByRole("menuitem", { name: "Open advanced view" }).click();
|
|
||||||
await expect(manualSurface).toBeVisible();
|
|
||||||
const viewport = page.viewportSize();
|
|
||||||
if (!viewport) throw new Error("Board page has no viewport");
|
|
||||||
await page.mouse.click(4, viewport.height - 4);
|
|
||||||
await expect(manualSurface).toBeHidden();
|
|
||||||
await expect(advancedViewTrigger).toBeFocused();
|
|
||||||
|
|
||||||
for (const compactOnlyKind of ["app", "iframe"]) {
|
|
||||||
const compactOnlyWidget = page
|
|
||||||
.locator(`[data-grid-item-id][data-kind='${compactOnlyKind}'] [data-advanced-focus-source]`)
|
|
||||||
.first();
|
|
||||||
await expect(compactOnlyWidget.locator("[data-advanced-focus-trigger]")).toHaveCount(0);
|
|
||||||
await compactOnlyWidget.dispatchEvent("contextmenu", { button: 2 });
|
|
||||||
await expect(page.getByRole("menuitem", { name: "Open advanced view" })).toHaveCount(0);
|
|
||||||
await page.keyboard.press("Escape");
|
|
||||||
await page.keyboard.down("Shift");
|
|
||||||
await compactOnlyWidget.hover({ position: { x: 4, y: 4 } });
|
|
||||||
await expect(previewSurface).toBeHidden();
|
|
||||||
await expect(dimmingOverlay).toBeHidden();
|
|
||||||
await page.keyboard.up("Shift");
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await context.close();
|
|
||||||
}
|
|
||||||
}, 60_000);
|
|
||||||
|
|
||||||
test("keeps compact-only iframe state and dismisses retained focus when a responsive layout remounts", async () => {
|
|
||||||
const { context, page } = await openBoardAsync(browser, baseUrl, ownerCredentials);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const iframeSlot = page.locator("[data-grid-item-id][data-kind='iframe'] [data-advanced-focus-source]").first();
|
|
||||||
const iframe = iframeSlot.locator("iframe");
|
|
||||||
await iframe.evaluate((element) => {
|
|
||||||
(element as HTMLIFrameElement).srcdoc = '<label>State <input id="state" value="initial"></label>';
|
|
||||||
(element as HTMLIFrameElement).dataset.advancedFocusProbe = "true";
|
|
||||||
});
|
|
||||||
const frame = page.frameLocator("iframe[data-advanced-focus-probe='true']");
|
|
||||||
await frame.locator("#state").fill("changed");
|
|
||||||
|
|
||||||
await expect(iframeSlot.locator("[data-advanced-focus-trigger]")).toHaveCount(0);
|
|
||||||
await iframeSlot.dispatchEvent("contextmenu", { button: 2 });
|
|
||||||
await expect(page.getByRole("menuitem", { name: "Open advanced view" })).toHaveCount(0);
|
|
||||||
await page.keyboard.press("Escape");
|
|
||||||
await page.keyboard.down("Shift");
|
|
||||||
await iframeSlot.hover({ position: { x: 4, y: 4 } });
|
|
||||||
await expect(page.getByRole("region", { name: "iFrame advanced view" })).toHaveCount(0);
|
|
||||||
await page.keyboard.up("Shift");
|
|
||||||
await expect(frame.locator("#state")).toHaveValue("changed");
|
|
||||||
|
|
||||||
const bookmarksSlot = page
|
|
||||||
.locator("[data-grid-item-id][data-kind='bookmarks'] [data-advanced-focus-source]")
|
|
||||||
.first();
|
|
||||||
await bookmarksSlot.getByRole("button", { name: "Open advanced view" }).focus();
|
|
||||||
await page.keyboard.press("Shift+Enter");
|
|
||||||
await expect(page.getByRole("dialog", { name: "Bookmarks advanced view" })).toBeVisible();
|
|
||||||
await page.setViewportSize({ width: 800, height: 900 });
|
|
||||||
await expect(page.getByRole("dialog", { name: "Bookmarks advanced view" })).toBeHidden();
|
|
||||||
await expect(page.locator("[data-grid-item-id][data-kind='iframe'] iframe")).toBeVisible();
|
|
||||||
} finally {
|
|
||||||
await context.close();
|
|
||||||
}
|
|
||||||
}, 60_000);
|
|
||||||
|
|
||||||
test("keeps advanced controls below the fixed header on short screens", async () => {
|
|
||||||
const { context, page } = await openBoardAsync(browser, baseUrl, ownerCredentials, { width: 1366, height: 768 });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const widget = page.locator("[data-grid-item-id][data-kind='clock'] [data-advanced-focus-source]").first();
|
|
||||||
await widget.getByRole("button", { name: "Open advanced view" }).focus();
|
|
||||||
await page.keyboard.press("Shift+Enter");
|
|
||||||
const closeButton = page.getByRole("button", { name: "Close advanced view" });
|
|
||||||
const bounds = await closeButton.boundingBox();
|
|
||||||
expect(bounds).not.toBeNull();
|
|
||||||
if (!bounds) return;
|
|
||||||
expect(bounds.x).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(bounds.y).toBeGreaterThanOrEqual(60);
|
|
||||||
expect(bounds.width).toBeGreaterThanOrEqual(44);
|
|
||||||
expect(bounds.height).toBeGreaterThanOrEqual(44);
|
|
||||||
expect(bounds.x + bounds.width).toBeLessThanOrEqual(1366);
|
|
||||||
expect(bounds.y + bounds.height).toBeLessThanOrEqual(768);
|
|
||||||
const surfaceBounds = await page.getByRole("dialog", { name: "Date and time advanced view" }).boundingBox();
|
|
||||||
const contentBounds = await page.locator(".clock-widget-container").boundingBox();
|
|
||||||
expect(surfaceBounds).not.toBeNull();
|
|
||||||
expect(contentBounds).not.toBeNull();
|
|
||||||
if (surfaceBounds && contentBounds) {
|
|
||||||
expect(contentBounds.y - surfaceBounds.y).toBeLessThan(32);
|
|
||||||
expect(bounds.x >= surfaceBounds.x + surfaceBounds.width || bounds.x + bounds.width <= surfaceBounds.x).toBe(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await closeButton.click();
|
|
||||||
await expect(page.getByRole("dialog", { name: "Date and time advanced view" })).toBeHidden();
|
|
||||||
} finally {
|
|
||||||
await context.close();
|
|
||||||
}
|
|
||||||
}, 60_000);
|
|
||||||
|
|
||||||
test("honors the user setting gate even when the user can modify the board", async () => {
|
|
||||||
const { context, page } = await openBoardAsync(browser, baseUrl, disabledModifierCredentials);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const widget = page.locator("[data-grid-item-id][data-kind='clock'] [data-advanced-focus-source]").first();
|
|
||||||
|
|
||||||
await widget.click({ button: "right" });
|
|
||||||
await expect(page.getByRole("menuitem", { name: "Open advanced view" })).toHaveCount(0);
|
|
||||||
} finally {
|
|
||||||
await context.close();
|
|
||||||
}
|
|
||||||
}, 60_000);
|
|
||||||
|
|
||||||
test("keeps read-only advanced viewing", async () => {
|
|
||||||
const { context, page } = await openBoardAsync(browser, baseUrl, viewerCredentials);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const widget = page.locator("[data-grid-item-id][data-kind='clock'] [data-advanced-focus-source]").first();
|
|
||||||
|
|
||||||
await widget.click({ button: "right" });
|
|
||||||
await page.getByRole("menuitem", { name: "Open advanced view" }).click();
|
|
||||||
await expect(page.getByRole("dialog", { name: "Date and time advanced view" })).toBeVisible();
|
|
||||||
await page.keyboard.press("Escape");
|
|
||||||
} finally {
|
|
||||||
await context.close();
|
|
||||||
}
|
|
||||||
}, 60_000);
|
|
||||||
});
|
|
||||||
|
|
||||||
const openBoardAsync = async (
|
|
||||||
browser: Browser,
|
|
||||||
baseUrl: string,
|
|
||||||
credentials: { username: string; password: string },
|
|
||||||
viewport = { width: 1280, height: 900 },
|
|
||||||
): Promise<{ context: BrowserContext; page: Page }> => {
|
|
||||||
const context = await browser.newContext({ reducedMotion: "reduce", viewport });
|
|
||||||
const page = await context.newPage();
|
|
||||||
|
|
||||||
await loginAsync({ page, baseUrl, credentials, destination: `/boards/${boardName}` });
|
|
||||||
await expect(page.locator(".clock-wrapper").first()).toBeVisible({
|
|
||||||
timeout: 15_000,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { context, page };
|
|
||||||
};
|
|
||||||
|
|
||||||
const seedInteractionBoardAsync = async (db: SqliteDatabase, ownerId: string) => {
|
|
||||||
const boardId = createId();
|
|
||||||
const baseLayoutId = createId();
|
|
||||||
const desktopLayoutId = createId();
|
|
||||||
const sectionId = createId();
|
|
||||||
const clockItemId = createId();
|
|
||||||
const bookmarksItemId = createId();
|
|
||||||
const iframeItemId = createId();
|
|
||||||
const appItemId = createId();
|
|
||||||
const appId = createId();
|
|
||||||
|
|
||||||
await db.insert(boards).values({ id: boardId, name: boardName, creatorId: ownerId, isPublic: false });
|
|
||||||
await db.insert(apps).values({
|
|
||||||
id: appId,
|
|
||||||
name: "Example app",
|
|
||||||
iconUrl: "/favicon.ico",
|
|
||||||
href: "https://example.com",
|
|
||||||
});
|
|
||||||
await db.insert(layouts).values([
|
|
||||||
{ id: baseLayoutId, name: "Base", columnCount: 12, breakpoint: 0, boardId },
|
|
||||||
{ id: desktopLayoutId, name: "Desktop", columnCount: 12, breakpoint: 1000, boardId },
|
|
||||||
]);
|
|
||||||
await db.insert(sections).values({ id: sectionId, kind: "empty", xOffset: 0, yOffset: 0, boardId });
|
|
||||||
await db.insert(items).values([
|
|
||||||
{
|
|
||||||
id: clockItemId,
|
|
||||||
kind: "clock",
|
|
||||||
boardId,
|
|
||||||
options: stringify({ is24HourFormat: true, showSeconds: false, showDate: true }),
|
|
||||||
advancedOptions: stringify({ title: null, customCssClasses: [], borderColor: "" }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: bookmarksItemId,
|
|
||||||
kind: "bookmarks",
|
|
||||||
boardId,
|
|
||||||
options: stringify({
|
|
||||||
title: "",
|
|
||||||
layout: "column",
|
|
||||||
hideTitle: false,
|
|
||||||
hideIcon: false,
|
|
||||||
hideHostname: false,
|
|
||||||
openNewTab: true,
|
|
||||||
withBorder: false,
|
|
||||||
items: [appId],
|
|
||||||
}),
|
|
||||||
advancedOptions: stringify({ title: null, customCssClasses: [], borderColor: "" }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: iframeItemId,
|
|
||||||
kind: "iframe",
|
|
||||||
boardId,
|
|
||||||
options: stringify({ embedUrl: "http://127.0.0.1:9", allowScrolling: true }),
|
|
||||||
advancedOptions: stringify({ title: null, customCssClasses: [], borderColor: "" }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: appItemId,
|
|
||||||
kind: "app",
|
|
||||||
boardId,
|
|
||||||
options: stringify({ appId, openInNewTab: true, showTitle: true, pingEnabled: false }),
|
|
||||||
advancedOptions: stringify({ title: null, customCssClasses: [], borderColor: "" }),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
await db.insert(itemLayouts).values(
|
|
||||||
[baseLayoutId, desktopLayoutId].flatMap((layoutId) => [
|
|
||||||
{
|
|
||||||
itemId: clockItemId,
|
|
||||||
sectionId,
|
|
||||||
layoutId,
|
|
||||||
xOffset: 0,
|
|
||||||
yOffset: 0,
|
|
||||||
width: 4,
|
|
||||||
height: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
itemId: bookmarksItemId,
|
|
||||||
sectionId,
|
|
||||||
layoutId,
|
|
||||||
xOffset: 0,
|
|
||||||
yOffset: 3,
|
|
||||||
width: 4,
|
|
||||||
height: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
itemId: iframeItemId,
|
|
||||||
sectionId,
|
|
||||||
layoutId,
|
|
||||||
xOffset: 8,
|
|
||||||
yOffset: 0,
|
|
||||||
width: 4,
|
|
||||||
height: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
itemId: appItemId,
|
|
||||||
sectionId,
|
|
||||||
layoutId,
|
|
||||||
xOffset: 4,
|
|
||||||
yOffset: 0,
|
|
||||||
width: 3,
|
|
||||||
height: 3,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
await db
|
|
||||||
.update(users)
|
|
||||||
.set({ homeBoardId: boardId, completedBoardTour: true, completedManageTour: true })
|
|
||||||
.where(eq(users.id, ownerId));
|
|
||||||
};
|
|
||||||
|
|
||||||
const seedCredentialsUserAsync = async (
|
|
||||||
db: SqliteDatabase,
|
|
||||||
input: {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
enableRightClickOnWidgets: boolean;
|
|
||||||
boardPermission: "view" | "modify";
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
const userId = createId();
|
|
||||||
const board = await db.query.boards.findFirst({ where: eq(boards.name, boardName) });
|
|
||||||
if (!board) throw new Error("Interaction board was not seeded");
|
|
||||||
|
|
||||||
await db.insert(users).values({
|
|
||||||
id: userId,
|
|
||||||
name: input.username,
|
|
||||||
provider: "credentials",
|
|
||||||
password: await hashPasswordAsync(input.password),
|
|
||||||
enableRightClickOnWidgets: input.enableRightClickOnWidgets,
|
|
||||||
homeBoardId: board.id,
|
|
||||||
completedBoardTour: true,
|
|
||||||
completedManageTour: true,
|
|
||||||
});
|
|
||||||
await db.insert(boardUserPermissions).values({
|
|
||||||
boardId: board.id,
|
|
||||||
userId,
|
|
||||||
permission: input.boardPermission,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
|
||||||
|
|
||||||
import { supportsAdvancedFocus } from "./definition";
|
|
||||||
import { widgetImports } from ".";
|
|
||||||
|
|
||||||
const advancedFocusWidgetKinds = [
|
|
||||||
"anchorNote",
|
|
||||||
"archiveTeamWarrior",
|
|
||||||
"audioStats",
|
|
||||||
"bazarr",
|
|
||||||
"beszelAlerts",
|
|
||||||
"beszelSystemGrid",
|
|
||||||
"beszelSystemTable",
|
|
||||||
"bookmarks",
|
|
||||||
"calendar",
|
|
||||||
"clock",
|
|
||||||
"coolify",
|
|
||||||
"downloads",
|
|
||||||
"firewall",
|
|
||||||
"healthMonitoring",
|
|
||||||
"immich-albumCarousel",
|
|
||||||
"immich-serverStats",
|
|
||||||
"indexerManager",
|
|
||||||
"mediaMissing",
|
|
||||||
"mediaReleases",
|
|
||||||
"mediaServer",
|
|
||||||
"mediaTranscoding",
|
|
||||||
"networkControllerStatus",
|
|
||||||
"networkControllerSummary",
|
|
||||||
"notebook",
|
|
||||||
"notifications",
|
|
||||||
"patchmon",
|
|
||||||
"releases",
|
|
||||||
"rssFeed",
|
|
||||||
"smartHome-entityState",
|
|
||||||
"smartHome-executeAutomation",
|
|
||||||
"speedtestTracker",
|
|
||||||
"stockPrice",
|
|
||||||
"systemDisks",
|
|
||||||
"systemResources",
|
|
||||||
"timetable",
|
|
||||||
"tracearr",
|
|
||||||
"traefik",
|
|
||||||
"umami",
|
|
||||||
"ups",
|
|
||||||
"uptimeKuma",
|
|
||||||
"weather",
|
|
||||||
"wud",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
describe("advanced focus support", () => {
|
|
||||||
test("is reserved for widgets with a distinct advanced experience", () => {
|
|
||||||
const actualAdvancedFocusKinds = Object.entries(widgetImports)
|
|
||||||
.filter(([, widgetImport]) => supportsAdvancedFocus(widgetImport.definition))
|
|
||||||
.map(([kind]) => kind)
|
|
||||||
.toSorted();
|
|
||||||
|
|
||||||
expect(actualAdvancedFocusKinds).toEqual([...advancedFocusWidgetKinds].toSorted());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in new issue
Block a user