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
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:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user