rename(phase 3): compose/DB/infra + stray code refs capakraken → nexus (#62)
CI / Lint (push) Successful in 3m4s
CI / Typecheck (push) Successful in 3m6s
CI / Architecture Guardrails (push) Successful in 3m8s
CI / Assistant Split Regression (push) Successful in 3m48s
CI / Build (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled

rename(phase 3): compose/DB/infra + stray code refs capakraken → nexus (#62)

Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
This commit was merged in pull request #62.
This commit is contained in:
2026-05-21 20:07:18 +02:00
committed by Hartmut
parent b41c1d2501
commit 19aeb2ba04
44 changed files with 406 additions and 187 deletions
@@ -12,7 +12,7 @@
* - Creates a temporary test user via tRPC (admin session) for isolation.
* - Cleans up the test user in afterAll.
* - Uses an empty storageState to ensure no cross-user localStorage bleed.
* - localStorage key is user-scoped: "capakraken_dashboard_v1_{userId}".
* - localStorage key is user-scoped: "nexus_dashboard_v1_{userId}".
*/
import { expect, test, type Browser, type Page } from "@playwright/test";
@@ -20,9 +20,16 @@ import { STORAGE_STATE } from "../../playwright.dev.config.js";
// ─── tRPC helpers ─────────────────────────────────────────────────────────────
type TrpcResult = { result?: { data?: unknown }; error?: { data?: { code?: string }; message?: string } };
type TrpcResult = {
result?: { data?: unknown };
error?: { data?: { code?: string }; message?: string };
};
async function trpcMutation(page: Page, procedure: string, input: unknown = null): Promise<TrpcResult> {
async function trpcMutation(
page: Page,
procedure: string,
input: unknown = null,
): Promise<TrpcResult> {
return page.evaluate(
async ({ procedure, input }) => {
const res = await fetch(`/api/trpc/${procedure}?batch=1`, {
@@ -38,7 +45,11 @@ async function trpcMutation(page: Page, procedure: string, input: unknown = null
);
}
async function trpcQuery(page: Page, procedure: string, input: unknown = null): Promise<TrpcResult> {
async function trpcQuery(
page: Page,
procedure: string,
input: unknown = null,
): Promise<TrpcResult> {
return page.evaluate(
async ({ procedure, input }) => {
const encodedInput = encodeURIComponent(JSON.stringify({ "0": { json: input } }));
@@ -128,7 +139,9 @@ test.describe("Dashboard — widget management", () => {
// Default layout should show at least the stat-cards widget
// (from createDefaultDashboardLayout in useDashboardLayout)
await expect(page.locator('[data-testid="widget-stat-cards"], .react-grid-item').first()).toBeVisible({
await expect(
page.locator('[data-testid="widget-stat-cards"], .react-grid-item').first(),
).toBeVisible({
timeout: 8000,
});
});
@@ -138,16 +151,21 @@ test.describe("Dashboard — widget management", () => {
await navigateToDashboard(page);
// Open modal
await page.getByRole("button", { name: /add widget/i }).first().click();
await page
.getByRole("button", { name: /add widget/i })
.first()
.click();
// Verify modal is open
await expect(page.getByRole("heading", { name: /add widget/i })).toBeVisible({ timeout: 5000 });
await expect(page.getByRole("heading", { name: /add widget/i })).toBeVisible({
timeout: 5000,
});
// Verify widget entries are visible in the modal
// The catalog has 11 widgets; check for at least 5 visible buttons inside the modal
const widgetButtons = page.locator(
'[role="dialog"] button, .fixed button[type="button"]',
).filter({ hasText: /./ });
const widgetButtons = page
.locator('[role="dialog"] button, .fixed button[type="button"]')
.filter({ hasText: /./ });
// Count items in the grid (the ×-close button is excluded by checking for icon content)
const modalContent = page.locator(".fixed.inset-0 .grid");
@@ -166,10 +184,16 @@ test.describe("Dashboard — widget management", () => {
const initialCount = await page.locator(".react-grid-item").count();
// Open modal and add "Resource Table" widget
await page.getByRole("button", { name: /add widget/i }).first().click();
await page
.getByRole("button", { name: /add widget/i })
.first()
.click();
await expect(page.locator(".fixed.inset-0")).toBeVisible({ timeout: 5000 });
await page.locator(".fixed.inset-0 button").filter({ hasText: /resource table/i }).click();
await page
.locator(".fixed.inset-0 button")
.filter({ hasText: /resource table/i })
.click();
// Modal should close after adding
await expect(page.locator(".fixed.inset-0")).not.toBeVisible({ timeout: 5000 });
@@ -184,9 +208,15 @@ test.describe("Dashboard — widget management", () => {
await navigateToDashboard(page);
// Add a recognizable widget
await page.getByRole("button", { name: /add widget/i }).first().click();
await page
.getByRole("button", { name: /add widget/i })
.first()
.click();
await expect(page.locator(".fixed.inset-0")).toBeVisible({ timeout: 5000 });
await page.locator(".fixed.inset-0 button").filter({ hasText: /project overview/i }).click();
await page
.locator(".fixed.inset-0 button")
.filter({ hasText: /project overview/i })
.click();
await expect(page.locator(".fixed.inset-0")).not.toBeVisible({ timeout: 5000 });
const countAfterAdd = await page.locator(".react-grid-item").count();
@@ -214,19 +244,23 @@ test.describe("Dashboard — widget management", () => {
// Read the admin's localStorage key to verify it is user-scoped
const adminUserId = await adminPage.evaluate(async () => {
const res = await fetch("/api/trpc/user.me?batch=1&input=" + encodeURIComponent(JSON.stringify({ "0": { json: null } })), {
credentials: "include",
});
const body = await res.json() as [{ result?: { data?: { json?: { id?: string } } } }];
const res = await fetch(
"/api/trpc/user.me?batch=1&input=" +
encodeURIComponent(JSON.stringify({ "0": { json: null } })),
{
credentials: "include",
},
);
const body = (await res.json()) as [{ result?: { data?: { json?: { id?: string } } } }];
return body[0]?.result?.data?.json?.id ?? null;
});
// Verify admin has a user-scoped storage key (not shared "capakraken_dashboard_v1")
// Verify admin has a user-scoped storage key (not shared "nexus_dashboard_v1")
if (adminUserId) {
const storageKey = await adminPage.evaluate((userId) => {
// Check both old (unscoped) and new (user-scoped) key formats
const oldKey = "capakraken_dashboard_v1";
const newKey = `capakraken_dashboard_v1_${userId}`;
const oldKey = "nexus_dashboard_v1";
const newKey = `nexus_dashboard_v1_${userId}`;
const oldValue = localStorage.getItem(oldKey);
const newValue = localStorage.getItem(newKey);
return { oldKey: oldValue !== null, newKey: newValue !== null };
@@ -244,8 +278,13 @@ test.describe("Dashboard — widget management", () => {
// Inject the admin's storage key to simulate same browser
await newUserPage.evaluate(
({ key, value }) => { localStorage.setItem(key, value ?? ""); },
{ key: `capakraken_dashboard_v1_${adminUserId}`, value: JSON.stringify({ version: 2, gridCols: 12, widgets: [] }) },
({ key, value }) => {
localStorage.setItem(key, value ?? "");
},
{
key: `nexus_dashboard_v1_${adminUserId}`,
value: JSON.stringify({ version: 2, gridCols: 12, widgets: [] }),
},
);
// Log in as test user
@@ -262,7 +301,10 @@ test.describe("Dashboard — widget management", () => {
const gridItems = await newUserPage.locator(".react-grid-item").count();
// Either show default layout (≥1 widget) OR the properly-scoped empty state with Add Widget CTA
// The key check: the test user's Add Widget button should still work
await newUserPage.getByRole("button", { name: /add widget/i }).first().click();
await newUserPage
.getByRole("button", { name: /add widget/i })
.first()
.click();
// Modal must show widgets to choose from
const modalContent = newUserPage.locator(".fixed.inset-0 .grid");