feat: user invite flow, deactivate/delete, favicon, dashboard loading fix, admin full-width
- Invite flow: admin can invite users by email with role selection; accept-invite page sets password and creates the account; 72-hour token expiry; E2E tests - User deactivate/reactivate/delete: new tRPC procedures + UI buttons; deactivation revokes all active sessions immediately; delete cascades vacation/broadcast records; isActive field added via migration 20260402000000_user_isactive - Auth: block login for inactive users with audit entry - Favicon: SVG favicon + ICO/PNG fallbacks (16, 32, 180, 192, 512px); manifest updated - Dashboard: GridLayout dynamic-import loading skeleton prevents blank dark area on first login before react-grid-layout chunk is cached - Admin users: remove max-w-5xl constraint so table uses full page width - Dev: docker container restart workflow documented in LEARNINGS.md; Prisma generate must run inside the container after schema changes (named node_modules volume) Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Unit tests for getDashboardLayout — bug #27 regression guard.
|
||||
*
|
||||
* Verifies that getDashboardLayout returns null (not an empty layout) when the
|
||||
* stored dashboardLayout is an empty object or contains only unknown widget
|
||||
* types. This allows the client to fall back to the default stat-cards layout.
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("../lib/audit.js", () => ({ createAuditEntry: vi.fn() }));
|
||||
vi.mock("../lib/system-settings-runtime.js", () => ({ resolveSystemSettingsRuntime: vi.fn() }));
|
||||
|
||||
const { getDashboardLayout } = await import(
|
||||
"../router/user-self-service-procedure-support.js"
|
||||
);
|
||||
|
||||
function makeCtx(dashboardLayout: unknown) {
|
||||
return {
|
||||
db: {
|
||||
user: {
|
||||
findUnique: vi.fn().mockResolvedValue(
|
||||
dashboardLayout !== undefined ? { dashboardLayout, updatedAt: new Date() } : null,
|
||||
),
|
||||
},
|
||||
} as never,
|
||||
dbUser: { id: "user_1" },
|
||||
session: null,
|
||||
};
|
||||
}
|
||||
|
||||
describe("getDashboardLayout — #27 regression", () => {
|
||||
it("returns null when dashboardLayout is null (new user)", async () => {
|
||||
const ctx = makeCtx(null);
|
||||
const result = await getDashboardLayout(ctx as never);
|
||||
expect(result.layout).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when dashboardLayout is an empty object", async () => {
|
||||
const ctx = makeCtx({});
|
||||
const result = await getDashboardLayout(ctx as never);
|
||||
expect(result.layout).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when dashboardLayout has no valid widget types", async () => {
|
||||
const ctx = makeCtx({
|
||||
version: 1,
|
||||
widgets: [{ type: "old-unknown-widget-type", id: "w1", x: 0, y: 0, w: 4, h: 3 }],
|
||||
});
|
||||
const result = await getDashboardLayout(ctx as never);
|
||||
expect(result.layout).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when dashboardLayout has an empty widgets array", async () => {
|
||||
const ctx = makeCtx({ version: 2, gridCols: 12, widgets: [] });
|
||||
const result = await getDashboardLayout(ctx as never);
|
||||
expect(result.layout).toBeNull();
|
||||
});
|
||||
|
||||
it("returns the layout when it contains at least one valid widget", async () => {
|
||||
const ctx = makeCtx({
|
||||
version: 2,
|
||||
gridCols: 12,
|
||||
widgets: [{ type: "stat-cards", id: "w1", x: 0, y: 0, w: 12, h: 3 }],
|
||||
});
|
||||
const result = await getDashboardLayout(ctx as never);
|
||||
expect(result.layout).not.toBeNull();
|
||||
expect(result.layout?.widgets).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user