rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI
CI / Unit Tests (pull_request) Successful in 5m46s
CI / Lint (pull_request) Failing after 3m49s
CI / E2E Tests (pull_request) Has been skipped
CI / Fresh-Linux Docker Deploy (pull_request) Has been skipped
CI / Assistant Split Regression (pull_request) Failing after 35s
CI / Architecture Guardrails (pull_request) Failing after 2m14s
CI / Typecheck (pull_request) Successful in 4m22s
CI / Build (pull_request) Has been skipped
CI / Release Images (pull_request) Has been skipped
CI / Unit Tests (pull_request) Successful in 5m46s
CI / Lint (pull_request) Failing after 3m49s
CI / E2E Tests (pull_request) Has been skipped
CI / Fresh-Linux Docker Deploy (pull_request) Has been skipped
CI / Assistant Split Regression (pull_request) Failing after 35s
CI / Architecture Guardrails (pull_request) Failing after 2m14s
CI / Typecheck (pull_request) Successful in 4m22s
CI / Build (pull_request) Has been skipped
CI / Release Images (pull_request) Has been skipped
- @capakraken/* → @nexus/* across 12 packages (root + 11 workspaces),
1551 import lines migrated via codemod
- User-visible brand strings renamed (emails, page titles, PWA
manifest, mobile header, MFA backup-codes header, tooltips, signin
page, invite page, weekly digest, install prompt)
- TOTP issuer "CapaKraken" → "Nexus" (existing secrets still valid;
re-enrollment relabels them in users' authenticator apps)
- Function rename: assertCapaKrakenDbTarget → assertNexusDbTarget
- LocalStorage migration shim in apps/web/src/app/layout.tsx copies
capakraken_* → nexus_* on first load (guarded by nexus_migrated_v1
sentinel; runs once per browser, then never again)
- Service-worker cache name capakraken-v2 → nexus-v2 with one-time
caches.delete('capakraken-v2') from the same shim
- Email-domain fixtures @capakraken.{dev,app} → @nexus.{dev,app} in
seed data, e2e specs, SMTP default fallback
- Dockerfile.dev / Dockerfile.prod / all .github/workflows/*.yml
pnpm --filter @capakraken/* → @nexus/*
- README, CLAUDE.md, LEARNINGS.md, all docs/*.md, .env.example,
tooling/deploy/.env.production.example brand sweep
Phase 1 deliberately leaves untouched (handled in Phase 3 cutover):
- PostgreSQL DB name "capakraken" and POSTGRES_USER "capakraken"
- Volume names capakraken_pgdata etc.
- Compose project name "capakraken" / "capakraken-prod"
- db-target-guard default expectedDatabase
- env-var CAPAKRAKEN_EXPECTED_DB_NAME
- Container DNS names in docker-compose.ci.yml
Quality gates green: pnpm typecheck (7/7), pnpm test:unit (7/7),
pnpm lint (0 errors), check:exports/imports/architecture all pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import { test, expect } from "./a11y-fixture.js";
|
||||
test.describe("Accessibility (axe-core)", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/auth/signin");
|
||||
await page.fill('input[type="email"]', "admin@capakraken.dev");
|
||||
await page.fill('input[type="email"]', "admin@nexus.dev");
|
||||
await page.fill('input[type="password"]', "admin123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/(dashboard|resources)/, { timeout: 15_000 });
|
||||
|
||||
+17
-14
@@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test";
|
||||
test.describe("Admin Pages", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/auth/signin");
|
||||
await page.fill('input[type="email"]', "admin@capakraken.dev");
|
||||
await page.fill('input[type="email"]', "admin@nexus.dev");
|
||||
await page.fill('input[type="password"]', "admin123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/(dashboard|resources)/);
|
||||
@@ -12,39 +12,42 @@ test.describe("Admin Pages", () => {
|
||||
test("settings page loads", async ({ page }) => {
|
||||
await page.goto("/admin/settings");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(page.locator("h1", { hasText: /System Settings/i })).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("h1", { hasText: /System Settings/i })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test("users page loads with user list", async ({ page }) => {
|
||||
await page.goto("/admin/users");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(page.locator("h1", { hasText: /User Management/i })).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("h1", { hasText: /User Management/i })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
// Should show a table with at least the admin user
|
||||
await expect(page.locator("table")).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("text=admin@capakraken.dev")).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("text=admin@nexus.dev")).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test("roles page loads", async ({ page }) => {
|
||||
await page.goto("/roles");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(
|
||||
page.locator("h1").filter({ hasText: /Roles/i }),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("h1").filter({ hasText: /Roles/i })).toBeVisible({ timeout: 10000 });
|
||||
// Should show table or list of roles
|
||||
await expect(
|
||||
page.locator("table").or(page.locator("text=No roles")),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("table").or(page.locator("text=No roles"))).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test("blueprints page loads", async ({ page }) => {
|
||||
await page.goto("/admin/blueprints");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(
|
||||
page.locator("h1").filter({ hasText: /Blueprints/i }),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("h1").filter({ hasText: /Blueprints/i })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
// Should show blueprint cards or list from seed data
|
||||
await expect(
|
||||
page.locator("table")
|
||||
page
|
||||
.locator("table")
|
||||
.or(page.locator("text=3D Content Production"))
|
||||
.or(page.locator("text=No blueprints")),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
|
||||
@@ -40,24 +40,28 @@ async function signIn(page: Page, email: string, password: string) {
|
||||
test.describe("Allocations", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await freezeBrowserTime(page);
|
||||
await signIn(page, "admin@capakraken.dev", "admin123");
|
||||
await signIn(page, "admin@nexus.dev", "admin123");
|
||||
await page.goto("/allocations");
|
||||
});
|
||||
|
||||
test("seeded assignments stay visibly rendered on first load", async ({ page }) => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(
|
||||
page.locator("h1").filter({ hasText: /Allocations|Planning/i }),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("h1").filter({ hasText: /Allocations|Planning/i })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
await expect(page.getByTestId("allocations-table")).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByTestId("allocations-empty-state")).toHaveCount(0);
|
||||
await expect(page.getByTestId("allocation-group-header").first()).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByTestId("allocation-group-header").first()).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
await expect(page.getByTestId("allocation-row").first()).toBeVisible({ timeout: 10000 });
|
||||
expect(await page.getByTestId("allocation-row").count()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("explicitly restrictive filters show a visible empty state and can be reset", async ({ page }) => {
|
||||
test("explicitly restrictive filters show a visible empty state and can be reset", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
const projectFilter = page.getByPlaceholder("Filter by project…");
|
||||
@@ -83,21 +87,23 @@ test.describe("Allocations", () => {
|
||||
await expect(newBtn).toBeVisible({ timeout: 10000 });
|
||||
await newBtn.click();
|
||||
await expect(page.getByTestId("allocation-modal")).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByRole("heading", { name: /New (Assignment|Open Demand)/i })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: /New (Assignment|Open Demand)/i }),
|
||||
).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
});
|
||||
|
||||
test("filter by status works", async ({ page }) => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
// Look for status filter chips or dropdown
|
||||
const statusFilter = page.locator("button", { hasText: /Proposed|Confirmed|Active|Status/i }).first();
|
||||
const statusFilter = page
|
||||
.locator("button", { hasText: /Proposed|Confirmed|Active|Status/i })
|
||||
.first();
|
||||
if ((await statusFilter.count()) > 0) {
|
||||
await statusFilter.click();
|
||||
await page.waitForTimeout(300);
|
||||
// After clicking a status filter, the page should still show the table
|
||||
await expect(
|
||||
page.locator("table").or(page.locator("text=No allocations")),
|
||||
).toBeVisible();
|
||||
await expect(page.locator("table").or(page.locator("text=No allocations"))).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -108,17 +114,17 @@ test.describe("Allocations", () => {
|
||||
await colToggle.click();
|
||||
await page.waitForTimeout(300);
|
||||
// A panel or dropdown with column checkboxes should appear
|
||||
await expect(
|
||||
page.locator("input[type='checkbox']").first(),
|
||||
).toBeVisible({ timeout: 3000 });
|
||||
await expect(page.locator("input[type='checkbox']").first()).toBeVisible({ timeout: 3000 });
|
||||
await page.keyboard.press("Escape");
|
||||
}
|
||||
});
|
||||
|
||||
test("viewer sees a visible access error instead of an empty allocations page", async ({ browser }) => {
|
||||
test("viewer sees a visible access error instead of an empty allocations page", async ({
|
||||
browser,
|
||||
}) => {
|
||||
const page = await browser.newPage();
|
||||
await freezeBrowserTime(page);
|
||||
await signIn(page, "viewer@capakraken.dev", "viewer123");
|
||||
await signIn(page, "viewer@nexus.dev", "viewer123");
|
||||
await page.goto("/allocations");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
async function signIn(page: Page) {
|
||||
await page.goto("/auth/signin");
|
||||
await page.fill('input[type="email"]', "admin@capakraken.dev");
|
||||
await page.fill('input[type="email"]', "admin@nexus.dev");
|
||||
await page.fill('input[type="password"]', "admin123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/(dashboard|resources)/);
|
||||
@@ -71,8 +71,8 @@ test.describe("Analytics / Insights", () => {
|
||||
test("insights page loads without errors", async ({ page }) => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
// Page should render some heading or content area — not a hard 404
|
||||
await expect(
|
||||
page.locator("h1").or(page.locator("main")).first(),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("h1").or(page.locator("main")).first()).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { execFileSync } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const ADMIN_EMAIL = "admin@capakraken.dev";
|
||||
const ADMIN_EMAIL = "admin@nexus.dev";
|
||||
const ADMIN_PASSWORD = "admin123";
|
||||
const CURRENT_CONVERSATION_ID = "assistant-e2e-current";
|
||||
const DB_WORKDIR = resolve(process.cwd(), "../../packages/db");
|
||||
@@ -159,7 +159,9 @@ test.describe("Assistant approvals", () => {
|
||||
`);
|
||||
});
|
||||
|
||||
test("renders the pending approval inbox and handles cross-conversation actions", async ({ page }) => {
|
||||
test("renders the pending approval inbox and handles cross-conversation actions", async ({
|
||||
page,
|
||||
}) => {
|
||||
const suffix = Date.now();
|
||||
const currentClientName = `E2E Approval Client Current ${suffix}`;
|
||||
const otherClientName = `E2E Approval Client Other ${suffix}`;
|
||||
@@ -210,14 +212,22 @@ test.describe("Assistant approvals", () => {
|
||||
await expect(page.getByText(currentApproval.summary)).toBeVisible();
|
||||
await expect(page.getByText(otherApproval.summary)).toBeVisible();
|
||||
|
||||
const currentCard = page.locator('[data-testid="assistant-approval-card"][data-conversation-scope="current"]').first();
|
||||
const otherCard = page.locator('[data-testid="assistant-approval-card"][data-conversation-scope="other"]').first();
|
||||
const currentCard = page
|
||||
.locator('[data-testid="assistant-approval-card"][data-conversation-scope="current"]')
|
||||
.first();
|
||||
const otherCard = page
|
||||
.locator('[data-testid="assistant-approval-card"][data-conversation-scope="other"]')
|
||||
.first();
|
||||
await expect(currentCard).toContainText("This chat");
|
||||
await expect(otherCard).toContainText("Other chat");
|
||||
|
||||
await otherCard.getByTestId("assistant-approval-cancel").click();
|
||||
await expect(page.getByText(`Aktion verworfen: ${otherApproval.summary}`)).toBeVisible();
|
||||
await expect(page.locator(`[data-testid="assistant-approval-card"][data-approval-id="${otherApproval.id}"]`)).toHaveCount(0);
|
||||
await expect(
|
||||
page.locator(
|
||||
`[data-testid="assistant-approval-card"][data-approval-id="${otherApproval.id}"]`,
|
||||
),
|
||||
).toHaveCount(0);
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ test.describe("Authentication", () => {
|
||||
|
||||
test("admin can sign in", async ({ page }) => {
|
||||
await page.goto("/auth/signin");
|
||||
await page.fill('input[type="email"]', "admin@capakraken.dev");
|
||||
await page.fill('input[type="email"]', "admin@nexus.dev");
|
||||
await page.fill('input[type="password"]', "admin123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/(dashboard|resources)/);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
async function signIn(page: Page) {
|
||||
await page.goto("/auth/signin");
|
||||
await page.fill('input[type="email"]', "admin@capakraken.dev");
|
||||
await page.fill('input[type="email"]', "admin@nexus.dev");
|
||||
await page.fill('input[type="password"]', "admin123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/(dashboard|resources)/);
|
||||
@@ -16,9 +16,7 @@ test.describe("Bench Board", () => {
|
||||
|
||||
test("bench board page loads with heading", async ({ page }) => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(
|
||||
page.locator("h1", { hasText: "Bench Board" }),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("h1", { hasText: "Bench Board" })).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test("date range filter inputs are visible", async ({ page }) => {
|
||||
@@ -32,7 +30,8 @@ test.describe("Bench Board", () => {
|
||||
test("shows bench results or no-resources empty state", async ({ page }) => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(
|
||||
page.locator("table")
|
||||
page
|
||||
.locator("table")
|
||||
.or(page.locator("text=No resources on bench"))
|
||||
.or(page.locator("text=No results"))
|
||||
.first(),
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test";
|
||||
test.describe("Dashboard", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/auth/signin");
|
||||
await page.fill('input[type="email"]', "admin@capakraken.dev");
|
||||
await page.fill('input[type="email"]', "admin@nexus.dev");
|
||||
await page.fill('input[type="password"]', "admin123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/(dashboard|resources)/);
|
||||
@@ -31,7 +31,9 @@ test.describe("Dashboard", () => {
|
||||
const addBtn = page.locator("button", { hasText: /Add Widget/i });
|
||||
if ((await addBtn.count()) > 0) {
|
||||
await addBtn.click();
|
||||
await expect(page.locator("text=Add Widget").or(page.locator("text=Available Widgets"))).toBeVisible();
|
||||
await expect(
|
||||
page.locator("text=Add Widget").or(page.locator("text=Available Widgets")),
|
||||
).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -21,9 +21,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`, {
|
||||
@@ -39,7 +46,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 } }));
|
||||
@@ -60,7 +71,7 @@ async function enableMfaForSession(page: Page): Promise<TOTP> {
|
||||
if (!data?.secret) throw new Error(`generateTotpSecret failed: ${JSON.stringify(genRes)}`);
|
||||
|
||||
const totp = new TOTP({
|
||||
issuer: "CapaKraken",
|
||||
issuer: "Nexus",
|
||||
algorithm: "SHA1",
|
||||
digits: 6,
|
||||
period: 30,
|
||||
@@ -92,7 +103,9 @@ test.describe("MFA — setup flow (account/security page)", () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
// Clean up: disable MFA if a test enabled it
|
||||
if (totp) {
|
||||
await disableMfaForSession(page).catch(() => {/* already disabled or admin override */});
|
||||
await disableMfaForSession(page).catch(() => {
|
||||
/* already disabled or admin override */
|
||||
});
|
||||
totp = null;
|
||||
}
|
||||
});
|
||||
@@ -106,7 +119,7 @@ test.describe("MFA — setup flow (account/security page)", () => {
|
||||
|
||||
expect(data?.secret).toBeTruthy();
|
||||
expect(data?.uri).toMatch(/^otpauth:\/\/totp\//);
|
||||
expect(data?.uri).toContain("CapaKraken");
|
||||
expect(data?.uri).toContain("Nexus");
|
||||
});
|
||||
|
||||
test("verifyAndEnableTotp accepts a valid code and enables MFA", async ({ page }) => {
|
||||
@@ -137,9 +150,9 @@ test.describe("MFA — setup flow (account/security page)", () => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Click the enable/setup button if MFA is not yet enabled
|
||||
const setupBtn = page.getByRole("button", { name: /set up/i }).or(
|
||||
page.getByRole("button", { name: /enable.*mfa/i }),
|
||||
);
|
||||
const setupBtn = page
|
||||
.getByRole("button", { name: /set up/i })
|
||||
.or(page.getByRole("button", { name: /enable.*mfa/i }));
|
||||
|
||||
if (await setupBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
|
||||
await setupBtn.click();
|
||||
@@ -233,9 +246,10 @@ test.describe("MFA — login flow", () => {
|
||||
|
||||
// Should show error and remain on TOTP step
|
||||
await expect(
|
||||
page.getByText(/invalid.*code|incorrect.*token|try again/i).or(
|
||||
page.locator("[data-error]"),
|
||||
).first(),
|
||||
page
|
||||
.getByText(/invalid.*code|incorrect.*token|try again/i)
|
||||
.or(page.locator("[data-error]"))
|
||||
.first(),
|
||||
).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Should NOT have navigated away
|
||||
@@ -248,7 +262,9 @@ test.describe("MFA — login flow", () => {
|
||||
test.describe("MFA — users without MFA enabled", () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test("login for MFA-less user goes straight to dashboard without TOTP prompt", async ({ page }) => {
|
||||
test("login for MFA-less user goes straight to dashboard without TOTP prompt", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/auth/signin");
|
||||
await page.fill('input[type="email"]', "manager@planarchy.dev");
|
||||
await page.fill('input[type="password"]', "manager123");
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* Auth: e2e/dev-system/.auth/admin.json (created by global-setup.ts)
|
||||
*
|
||||
* Run:
|
||||
* pnpm --filter @capakraken/web exec playwright test \
|
||||
* pnpm --filter @nexus/web exec playwright test \
|
||||
* --config playwright.dev.config.ts \
|
||||
* e2e/dev-system/nav-smoke.spec.ts
|
||||
*/
|
||||
|
||||
@@ -27,10 +27,10 @@ test.describe("RBAC — admin routes (admin session)", () => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await expect(page.locator("table")).toBeVisible({ timeout: 10000 });
|
||||
// Seed users have planarchy.dev or capakraken.dev email domains
|
||||
await expect(
|
||||
page.locator("text=/planarchy\\.dev|capakraken\\.dev/").first(),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
// Seed users have planarchy.dev or nexus.dev email domains
|
||||
await expect(page.locator("text=/planarchy\\.dev|capakraken\\.dev/").first()).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test("admin can access /admin/system-roles without errors", async ({ page }) => {
|
||||
@@ -99,9 +99,10 @@ test.describe("RBAC — allocations permitted for admin", () => {
|
||||
await page.goto("/allocations");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await expect(
|
||||
page.locator("text=/do not have permission to view allocations/i"),
|
||||
).toHaveCount(0, { timeout: 8000 });
|
||||
await expect(page.locator("text=/do not have permission to view allocations/i")).toHaveCount(
|
||||
0,
|
||||
{ timeout: 8000 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,9 +113,10 @@ test.describe("RBAC — allocations permitted for manager", () => {
|
||||
await page.goto("/allocations");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await expect(
|
||||
page.locator("text=/do not have permission to view allocations/i"),
|
||||
).toHaveCount(0, { timeout: 8000 });
|
||||
await expect(page.locator("text=/do not have permission to view allocations/i")).toHaveCount(
|
||||
0,
|
||||
{ timeout: 8000 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -10,22 +10,26 @@ async function signIn(page: Page, email: string, password: string) {
|
||||
|
||||
test.describe("Estimates", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await signIn(page, "admin@capakraken.dev", "admin123");
|
||||
await signIn(page, "admin@nexus.dev", "admin123");
|
||||
await page.goto("/estimates");
|
||||
});
|
||||
|
||||
test("estimate list loads", async ({ page }) => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(page.getByRole("heading", { name: /estimate workspace/i })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
await expect(page.getByPlaceholder("Search by estimate or opportunity")).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
await expect(
|
||||
page.getByRole("heading", { name: /estimate workspace/i }),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect(
|
||||
page.getByPlaceholder("Search by estimate or opportunity"),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect(
|
||||
page.locator("text=No estimates yet").or(
|
||||
page.locator("text=Select an estimate to inspect the current version, demand lines, and summary metrics."),
|
||||
),
|
||||
page
|
||||
.locator("text=No estimates yet")
|
||||
.or(
|
||||
page.locator(
|
||||
"text=Select an estimate to inspect the current version, demand lines, and summary metrics.",
|
||||
),
|
||||
),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
@@ -44,8 +48,13 @@ test.describe("Estimates", () => {
|
||||
await page.locator("button", { hasText: /New Estimate/i }).click();
|
||||
|
||||
// Step 1: Setup — fill a name
|
||||
await expect(page.getByRole("button", { name: /Step 1 Setup/i })).toBeVisible({ timeout: 5000 });
|
||||
const nameInput = page.locator('input[placeholder*="name"]').or(page.locator('input[name="name"]')).first();
|
||||
await expect(page.getByRole("button", { name: /Step 1 Setup/i })).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
const nameInput = page
|
||||
.locator('input[placeholder*="name"]')
|
||||
.or(page.locator('input[name="name"]'))
|
||||
.first();
|
||||
if ((await nameInput.count()) > 0) {
|
||||
await nameInput.fill(`E2E Estimate ${Date.now()}`);
|
||||
}
|
||||
@@ -90,9 +99,7 @@ test.describe("Estimates", () => {
|
||||
|
||||
test("shows the empty-state fallback when no estimates exist", async ({ page }) => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(
|
||||
page.locator("text=No estimates yet"),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("text=No estimates yet")).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test("shows an estimate-not-found fallback for unknown workspaces", async ({ page }) => {
|
||||
@@ -103,12 +110,14 @@ test.describe("Estimates", () => {
|
||||
|
||||
test("shows the restricted workspace fallback for viewers", async ({ browser }) => {
|
||||
const page = await browser.newPage();
|
||||
await signIn(page, "viewer@capakraken.dev", "viewer123");
|
||||
await signIn(page, "viewer@nexus.dev", "viewer123");
|
||||
await page.goto("/estimates/missing-estimate");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await expect(
|
||||
page.locator("text=Your role can access the estimate list, but not the detailed financial workspace."),
|
||||
page.locator(
|
||||
"text=Your role can access the estimate list, but not the detailed financial workspace.",
|
||||
),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await page.close();
|
||||
|
||||
@@ -2,14 +2,16 @@ import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
async function signInAsAdmin(page: Page) {
|
||||
await page.goto("/auth/signin");
|
||||
await page.fill('input[type="email"]', "admin@capakraken.dev");
|
||||
await page.fill('input[type="email"]', "admin@nexus.dev");
|
||||
await page.fill('input[type="password"]', "admin123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/(dashboard|resources)/);
|
||||
}
|
||||
|
||||
test.describe("Holiday Calendar Editor", () => {
|
||||
test("creates a city calendar, previews a holiday, blocks duplicates and confirms deletion", async ({ page }) => {
|
||||
test("creates a city calendar, previews a holiday, blocks duplicates and confirms deletion", async ({
|
||||
page,
|
||||
}) => {
|
||||
const suffix = Date.now().toString();
|
||||
const calendarName = `E2E City Calendar ${suffix}`;
|
||||
const holidayName = `E2E Local Holiday ${suffix}`;
|
||||
@@ -21,11 +23,18 @@ test.describe("Holiday Calendar Editor", () => {
|
||||
|
||||
await page.getByTestId("holiday-calendar-name-input").fill(calendarName);
|
||||
await page.getByTestId("holiday-calendar-scope-select").selectOption("CITY");
|
||||
await page.getByTestId("holiday-calendar-country-select").selectOption({ label: "Germany (DE)" });
|
||||
await page
|
||||
.getByTestId("holiday-calendar-country-select")
|
||||
.selectOption({ label: "Germany (DE)" });
|
||||
await page.getByTestId("holiday-calendar-city-select").selectOption({ label: "Muenchen" });
|
||||
await page.getByTestId("holiday-calendar-create-button").click();
|
||||
|
||||
await expect(page.getByTestId(/holiday-calendar-row-/).filter({ hasText: calendarName }).first()).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId(/holiday-calendar-row-/)
|
||||
.filter({ hasText: calendarName })
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: calendarName })).toBeVisible();
|
||||
await expect(page.getByTestId("holiday-entry-create-button")).toBeVisible();
|
||||
|
||||
@@ -44,10 +53,15 @@ test.describe("Holiday Calendar Editor", () => {
|
||||
await page.getByTestId("holiday-entry-name-input").fill(`${holidayName} Duplicate`);
|
||||
await page.getByTestId("holiday-entry-create-button").click();
|
||||
|
||||
await expect(page.getByText("A holiday entry for this calendar and date already exists")).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("A holiday entry for this calendar and date already exists"),
|
||||
).toBeVisible();
|
||||
|
||||
page.once("dialog", (dialog) => dialog.accept());
|
||||
await page.getByTestId(/holiday-entry-delete-/).first().click();
|
||||
await page
|
||||
.getByTestId(/holiday-entry-delete-/)
|
||||
.first()
|
||||
.click();
|
||||
await expect(page.getByText(holidayName).first()).not.toBeVisible();
|
||||
|
||||
page.once("dialog", (dialog) => dialog.accept());
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test";
|
||||
test.describe("Navigation", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/auth/signin");
|
||||
await page.fill('input[type="email"]', "admin@capakraken.dev");
|
||||
await page.fill('input[type="email"]', "admin@nexus.dev");
|
||||
await page.fill('input[type="password"]', "admin123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/(dashboard|resources)/);
|
||||
@@ -28,7 +28,7 @@ test.describe("Navigation", () => {
|
||||
|
||||
test("all nav routes resolve — no 404 (smoke)", async ({ page }) => {
|
||||
// Complements the click-based test above with a direct-navigation check
|
||||
// covering every sidebar destination. Uses admin@capakraken.dev (ADMIN role).
|
||||
// covering every sidebar destination. Uses admin@nexus.dev (ADMIN role).
|
||||
const routes = [
|
||||
// Already covered by click test but included for completeness
|
||||
"/dashboard",
|
||||
@@ -79,7 +79,10 @@ test.describe("Navigation", () => {
|
||||
}
|
||||
|
||||
// Expand again — the button should still be visible as an icon
|
||||
const expandBtn = page.locator("nav button").filter({ has: page.locator("svg") }).last();
|
||||
const expandBtn = page
|
||||
.locator("nav button")
|
||||
.filter({ has: page.locator("svg") })
|
||||
.last();
|
||||
await expandBtn.click();
|
||||
await page.waitForTimeout(300);
|
||||
const boxExpanded = await nav.boundingBox();
|
||||
@@ -113,7 +116,10 @@ test.describe("Navigation", () => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// The hamburger button should be visible on mobile
|
||||
const hamburgerBtn = page.locator("button").filter({ has: page.locator("svg") }).first();
|
||||
const hamburgerBtn = page
|
||||
.locator("button")
|
||||
.filter({ has: page.locator("svg") })
|
||||
.first();
|
||||
await expect(hamburgerBtn).toBeVisible({ timeout: 5000 });
|
||||
|
||||
await hamburgerBtn.click();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
async function signIn(page: Page) {
|
||||
await page.goto("/auth/signin");
|
||||
await page.fill('input[type="email"]', "admin@capakraken.dev");
|
||||
await page.fill('input[type="email"]', "admin@nexus.dev");
|
||||
await page.fill('input[type="password"]', "admin123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/(dashboard|resources)/);
|
||||
@@ -74,9 +74,9 @@ test.describe("Project Detail Page", () => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// BudgetStatusCard renders budget-related content
|
||||
await expect(
|
||||
page.locator("text=Budget").or(page.locator("text=budget")).first(),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("text=Budget").or(page.locator("text=budget")).first()).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test("unknown project id shows not-found state", async ({ page }) => {
|
||||
@@ -85,7 +85,11 @@ test.describe("Project Detail Page", () => {
|
||||
|
||||
// Server-side notFound() triggers the Next.js 404 page
|
||||
await expect(
|
||||
page.locator("text=404").or(page.locator("text=Not Found")).or(page.locator("text=not found")).first(),
|
||||
page
|
||||
.locator("text=404")
|
||||
.or(page.locator("text=Not Found"))
|
||||
.or(page.locator("text=not found"))
|
||||
.first(),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test";
|
||||
test.describe("Projects", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/auth/signin");
|
||||
await page.fill('input[type="email"]', "manager@capakraken.dev");
|
||||
await page.fill('input[type="email"]', "manager@nexus.dev");
|
||||
await page.fill('input[type="password"]', "manager123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/resources/);
|
||||
@@ -26,9 +26,16 @@ test.describe("Projects", () => {
|
||||
// Step 1: Blueprint selection
|
||||
await expect(page.locator("text=Select Blueprint")).toBeVisible();
|
||||
// Select the first available blueprint
|
||||
const blueprintCard = page.locator("[data-blueprint-id]").first()
|
||||
.or(page.locator("button").filter({ hasText: /Blueprint|Production/ }).first());
|
||||
if (await blueprintCard.count() > 0) {
|
||||
const blueprintCard = page
|
||||
.locator("[data-blueprint-id]")
|
||||
.first()
|
||||
.or(
|
||||
page
|
||||
.locator("button")
|
||||
.filter({ hasText: /Blueprint|Production/ })
|
||||
.first(),
|
||||
);
|
||||
if ((await blueprintCard.count()) > 0) {
|
||||
await blueprintCard.click();
|
||||
} else {
|
||||
// Click next without blueprint if none shown
|
||||
@@ -37,16 +44,21 @@ test.describe("Projects", () => {
|
||||
}
|
||||
|
||||
// Step 2: Timeline — set project dates
|
||||
await expect(page.locator("text=Timeline").or(page.locator("text=Project Dates"))).toBeVisible({ timeout: 5000 });
|
||||
const projectNameInput = page.locator('input[placeholder*="name"]').or(page.locator('input[name="name"]')).first();
|
||||
if (await projectNameInput.count() > 0) {
|
||||
await expect(page.locator("text=Timeline").or(page.locator("text=Project Dates"))).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
const projectNameInput = page
|
||||
.locator('input[placeholder*="name"]')
|
||||
.or(page.locator('input[name="name"]'))
|
||||
.first();
|
||||
if ((await projectNameInput.count()) > 0) {
|
||||
await projectNameInput.fill(`E2E Test Project ${Date.now()}`);
|
||||
}
|
||||
await page.locator("button", { hasText: "Next" }).click();
|
||||
|
||||
// Step 3: Staffing demand
|
||||
await expect(
|
||||
page.locator("text=Staffing").or(page.locator("text=Demand").or(page.locator("text=Roles")))
|
||||
page.locator("text=Staffing").or(page.locator("text=Demand").or(page.locator("text=Roles"))),
|
||||
).toBeVisible({ timeout: 5000 });
|
||||
await page.locator("button", { hasText: "Next" }).click();
|
||||
|
||||
@@ -56,11 +68,13 @@ test.describe("Projects", () => {
|
||||
|
||||
// Step 5: Review
|
||||
await page.waitForTimeout(500);
|
||||
const reviewOrFinish = page.locator("text=Review").or(page.locator("button", { hasText: /Create|Finish|Submit/ }));
|
||||
const reviewOrFinish = page
|
||||
.locator("text=Review")
|
||||
.or(page.locator("button", { hasText: /Create|Finish|Submit/ }));
|
||||
await expect(reviewOrFinish).toBeVisible({ timeout: 5000 });
|
||||
// Don't actually submit — just close
|
||||
const cancelBtn = page.locator("button", { hasText: /Cancel|Close/ }).first();
|
||||
if (await cancelBtn.count() > 0) {
|
||||
if ((await cancelBtn.count()) > 0) {
|
||||
await cancelBtn.click();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
async function signIn(page: Page) {
|
||||
await page.goto("/auth/signin");
|
||||
await page.fill('input[type="email"]', "admin@capakraken.dev");
|
||||
await page.fill('input[type="email"]', "admin@nexus.dev");
|
||||
await page.fill('input[type="password"]', "admin123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/(dashboard|resources)/);
|
||||
@@ -16,16 +16,17 @@ test.describe("Chargeability Report", () => {
|
||||
|
||||
test("chargeability forecast page loads with heading", async ({ page }) => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(
|
||||
page.locator("h1", { hasText: "Chargeability Forecast" }),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("h1", { hasText: "Chargeability Forecast" })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test("filter controls are present", async ({ page }) => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
// Should have at least one filter (e.g., chapter, period, resource search)
|
||||
await expect(
|
||||
page.locator('input[type="text"]')
|
||||
page
|
||||
.locator('input[type="text"]')
|
||||
.or(page.locator('input[type="search"]'))
|
||||
.or(page.locator("select"))
|
||||
.first(),
|
||||
@@ -64,9 +65,9 @@ test.describe("Report Builder", () => {
|
||||
|
||||
test("report builder page loads with heading", async ({ page }) => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Report Builder" }),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByRole("heading", { name: "Report Builder" })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test("entity selector is present with expected options", async ({ page }) => {
|
||||
@@ -78,9 +79,9 @@ test.describe("Report Builder", () => {
|
||||
|
||||
test("run report button is visible", async ({ page }) => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(
|
||||
page.locator("button", { hasText: /Run|Export|Generate/i }).first(),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("button", { hasText: /Run|Export|Generate/i }).first()).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test("running a default report produces output or empty state", async ({ page }) => {
|
||||
@@ -90,7 +91,11 @@ test.describe("Report Builder", () => {
|
||||
await runBtn.click();
|
||||
await page.waitForTimeout(1500);
|
||||
await expect(
|
||||
page.locator("table").or(page.locator("text=No rows")).or(page.locator("text=0 rows")).first(),
|
||||
page
|
||||
.locator("table")
|
||||
.or(page.locator("text=No rows"))
|
||||
.or(page.locator("text=0 rows"))
|
||||
.first(),
|
||||
).toBeVisible({ timeout: 15000 });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test";
|
||||
test.describe("Resources", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/auth/signin");
|
||||
await page.fill('input[type="email"]', "manager@capakraken.dev");
|
||||
await page.fill('input[type="email"]', "manager@nexus.dev");
|
||||
await page.fill('input[type="password"]', "manager123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/(dashboard|resources)/);
|
||||
@@ -21,10 +21,11 @@ test.describe("Resources", () => {
|
||||
await expect(rows.first()).toBeVisible();
|
||||
|
||||
const firstRowText = (await rows.first().textContent()) ?? "";
|
||||
const searchTerm = firstRowText
|
||||
.split(/\s+/)
|
||||
.map((token) => token.replace(/[^A-Za-z0-9@._-]/g, "").trim())
|
||||
.find((token) => token.length >= 3) ?? "EMP";
|
||||
const searchTerm =
|
||||
firstRowText
|
||||
.split(/\s+/)
|
||||
.map((token) => token.replace(/[^A-Za-z0-9@._-]/g, "").trim())
|
||||
.find((token) => token.length >= 3) ?? "EMP";
|
||||
|
||||
const searchInput = page.locator('input[type="search"]');
|
||||
await searchInput.fill(searchTerm);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
async function signIn(page: Page) {
|
||||
await page.goto("/auth/signin");
|
||||
await page.fill('input[type="email"]', "admin@capakraken.dev");
|
||||
await page.fill('input[type="email"]', "admin@nexus.dev");
|
||||
await page.fill('input[type="password"]', "admin123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/(dashboard|resources)/);
|
||||
@@ -16,15 +16,16 @@ test.describe("Scenario Planning", () => {
|
||||
|
||||
test("scenarios page loads with heading", async ({ page }) => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(
|
||||
page.locator("h1", { hasText: /Scenario Planning/i }),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("h1", { hasText: /Scenario Planning/i })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test("shows scenarios list or empty state", async ({ page }) => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(
|
||||
page.locator("table")
|
||||
page
|
||||
.locator("table")
|
||||
.or(page.locator("text=No scenarios"))
|
||||
.or(page.locator("text=Create a project first"))
|
||||
.or(page.locator("[data-testid]"))
|
||||
|
||||
@@ -29,7 +29,7 @@ test("signin page renders credential inputs and submit button", async ({ page })
|
||||
|
||||
test("admin login succeeds and redirects away from signin", async ({ page }) => {
|
||||
await page.goto("/auth/signin");
|
||||
await page.fill('input[type="email"]', "admin@capakraken.dev");
|
||||
await page.fill('input[type="email"]', "admin@nexus.dev");
|
||||
await page.fill('input[type="password"]', "admin123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).not.toHaveURL(/\/auth\/signin/, { timeout: 15_000 });
|
||||
@@ -37,7 +37,7 @@ test("admin login succeeds and redirects away from signin", async ({ page }) =>
|
||||
|
||||
test("authenticated user sees app shell nav", async ({ page }) => {
|
||||
await page.goto("/auth/signin");
|
||||
await page.fill('input[type="email"]', "admin@capakraken.dev");
|
||||
await page.fill('input[type="email"]', "admin@nexus.dev");
|
||||
await page.fill('input[type="password"]', "admin123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).not.toHaveURL(/\/auth\/signin/, { timeout: 15_000 });
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test";
|
||||
test.describe("Staffing", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/auth/signin");
|
||||
await page.fill('input[type="email"]', "admin@capakraken.dev");
|
||||
await page.fill('input[type="email"]', "admin@nexus.dev");
|
||||
await page.fill('input[type="password"]', "admin123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/(dashboard|resources)/);
|
||||
@@ -12,7 +12,9 @@ test.describe("Staffing", () => {
|
||||
|
||||
test("staffing page loads with search form", async ({ page }) => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(page.locator("h1", { hasText: /Staffing Suggestions/i })).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("h1", { hasText: /Staffing Suggestions/i })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
// Search form should have skill input, date fields, and a search button
|
||||
await expect(page.locator("text=How scoring works")).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
@@ -20,9 +22,9 @@ test.describe("Staffing", () => {
|
||||
test("search form has default skill tags", async ({ page }) => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
// The StaffingPanel pre-populates with TypeScript and React skill tags
|
||||
await expect(
|
||||
page.locator("text=TypeScript").or(page.locator("text=React")),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("text=TypeScript").or(page.locator("text=React"))).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test("submitting search returns suggestions or empty state", async ({ page }) => {
|
||||
@@ -34,7 +36,9 @@ test.describe("Staffing", () => {
|
||||
await page.waitForTimeout(1000);
|
||||
// After search, should show either suggestion cards or a "no suggestions" message
|
||||
await expect(
|
||||
page.locator("text=/Score|Availability|No suggestions|No matching/i").first()
|
||||
page
|
||||
.locator("text=/Score|Availability|No suggestions|No matching/i")
|
||||
.first()
|
||||
.or(page.locator("[data-suggestion]").first())
|
||||
.or(page.locator("table").first()),
|
||||
).toBeVisible({ timeout: 15000 });
|
||||
|
||||
@@ -393,9 +393,9 @@ try {
|
||||
await cleanupStaleE2eArtifacts();
|
||||
await ensureE2eDatabaseContainer();
|
||||
}
|
||||
await run("pnpm", ["--filter", "@capakraken/db", "db:push"], workspaceRoot);
|
||||
await run("pnpm", ["--filter", "@capakraken/db", "db:seed"], workspaceRoot);
|
||||
await run("pnpm", ["--filter", "@capakraken/db", "db:seed:holidays"], workspaceRoot);
|
||||
await run("pnpm", ["--filter", "@nexus/db", "db:push"], workspaceRoot);
|
||||
await run("pnpm", ["--filter", "@nexus/db", "db:seed"], workspaceRoot);
|
||||
await run("pnpm", ["--filter", "@nexus/db", "db:seed:holidays"], workspaceRoot);
|
||||
rmSync(webDistDirPath, { recursive: true, force: true });
|
||||
|
||||
const server = spawn("pnpm", ["exec", "next", "dev", "-p", e2ePort], {
|
||||
|
||||
+285
-158
@@ -133,7 +133,7 @@ function createTimelineSegmentScenario(suffix: string): TimelineSegmentScenario
|
||||
data: {
|
||||
eid: ${JSON.stringify(`e2e.timeline.${suffix}`)},
|
||||
displayName: ${JSON.stringify(`E2E Timeline ${suffix}`)},
|
||||
email: ${JSON.stringify(`e2e.timeline.${suffix}@capakraken.dev`)},
|
||||
email: ${JSON.stringify(`e2e.timeline.${suffix}@nexus.dev`)},
|
||||
chapter: "E2E",
|
||||
lcrCents: 5000,
|
||||
ucrCents: 9000,
|
||||
@@ -208,7 +208,7 @@ function createTimelineDemandScenario(suffix: string): TimelineDemandScenario {
|
||||
data: {
|
||||
eid: ${JSON.stringify(`e2e.timeline.demand.${suffix}`)},
|
||||
displayName: ${JSON.stringify(`E2E Timeline Demand Resource ${suffix}`)},
|
||||
email: ${JSON.stringify(`e2e.timeline.demand.${suffix}@capakraken.dev`)},
|
||||
email: ${JSON.stringify(`e2e.timeline.demand.${suffix}@nexus.dev`)},
|
||||
chapter: "E2E",
|
||||
lcrCents: 5000,
|
||||
ucrCents: 9000,
|
||||
@@ -341,7 +341,9 @@ function listScenarioAssignments(projectId: string) {
|
||||
}
|
||||
|
||||
function listScenarioDemands(projectId: string) {
|
||||
return runDbJson<Array<{ id: string; startDate: string; endDate: string; headcount: number; status: string }>>(`
|
||||
return runDbJson<
|
||||
Array<{ id: string; startDate: string; endDate: string; headcount: number; status: string }>
|
||||
>(`
|
||||
const demands = await prisma.demandRequirement.findMany({
|
||||
where: { projectId: ${JSON.stringify(projectId)} },
|
||||
orderBy: [{ startDate: "asc" }, { endDate: "asc" }],
|
||||
@@ -448,10 +450,7 @@ async function openAllocationContextMenuAtOffset(
|
||||
);
|
||||
}
|
||||
|
||||
async function openContextMenuAtCenter(
|
||||
page: Page,
|
||||
locator: ReturnType<Page["locator"]>,
|
||||
) {
|
||||
async function openContextMenuAtCenter(page: Page, locator: ReturnType<Page["locator"]>) {
|
||||
const target = await resolveAllocationContextMenuTarget(locator);
|
||||
const box = await readBoundingBox(target);
|
||||
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2, { button: "right" });
|
||||
@@ -511,9 +510,7 @@ async function listRenderedAllocationSegments(
|
||||
row: ReturnType<Page["locator"]>,
|
||||
allocationId?: string,
|
||||
) {
|
||||
const selector = allocationId
|
||||
? `[data-allocation-id="${allocationId}"]`
|
||||
: "[data-allocation-id]";
|
||||
const selector = allocationId ? `[data-allocation-id="${allocationId}"]` : "[data-allocation-id]";
|
||||
return row.locator(selector).evaluateAll((elements) =>
|
||||
elements.map((element) => {
|
||||
const htmlElement = element as HTMLElement;
|
||||
@@ -536,17 +533,13 @@ function escapeRegex(value: string) {
|
||||
|
||||
async function signInAsAdmin(page: Page) {
|
||||
await page.goto("/auth/signin");
|
||||
await page.fill('input[type="email"]', "admin@capakraken.dev");
|
||||
await page.fill('input[type="email"]', "admin@nexus.dev");
|
||||
await page.fill('input[type="password"]', "admin123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/(dashboard|resources)/);
|
||||
}
|
||||
|
||||
async function findVisibleTimelineEntryId(
|
||||
page: Page,
|
||||
selector: string,
|
||||
minimumWidth = 24,
|
||||
) {
|
||||
async function findVisibleTimelineEntryId(page: Page, selector: string, minimumWidth = 24) {
|
||||
return page.locator(selector).evaluateAll((elements, minimum) => {
|
||||
for (const element of elements) {
|
||||
if (!(element instanceof HTMLElement)) continue;
|
||||
@@ -600,9 +593,9 @@ async function findVisibleAllocationSegmentForResize(
|
||||
);
|
||||
const stickyHeaderBottom = scrollContainer
|
||||
? Array.from(scrollContainer.querySelectorAll<HTMLElement>(".sticky.top-0")).reduce(
|
||||
(maxBottom, element) => Math.max(maxBottom, element.getBoundingClientRect().bottom),
|
||||
0,
|
||||
)
|
||||
(maxBottom, element) => Math.max(maxBottom, element.getBoundingClientRect().bottom),
|
||||
0,
|
||||
)
|
||||
: 0;
|
||||
const safeTop = stickyHeaderBottom > 0 ? stickyHeaderBottom + 8 : 48;
|
||||
const candidates: Array<{
|
||||
@@ -611,8 +604,11 @@ async function findVisibleAllocationSegmentForResize(
|
||||
segmentEnd: string | null;
|
||||
score: number;
|
||||
}> = [];
|
||||
let fallback: { allocationId: string; segmentStart: string | null; segmentEnd: string | null } | null =
|
||||
null;
|
||||
let fallback: {
|
||||
allocationId: string;
|
||||
segmentStart: string | null;
|
||||
segmentEnd: string | null;
|
||||
} | null = null;
|
||||
|
||||
for (const element of elements) {
|
||||
if (!(element instanceof HTMLElement)) continue;
|
||||
@@ -829,13 +825,20 @@ async function switchToProjectView(page: Page, readySelector?: string) {
|
||||
await expect(page.locator(readySelector).first()).toBeVisible();
|
||||
} else {
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const projectRows = await page.getByTestId("timeline-project-resource-row-canvas").count();
|
||||
const projectBars = await page.locator("[data-timeline-entry-type='project-bar']").count();
|
||||
const demandBars = await page.locator("[data-timeline-entry-type='demand']").count();
|
||||
const emptyStates = await page.getByText(/No projects in this time range/).count();
|
||||
return projectRows + projectBars + demandBars + emptyStates;
|
||||
}, { timeout: 10_000 })
|
||||
.poll(
|
||||
async () => {
|
||||
const projectRows = await page
|
||||
.getByTestId("timeline-project-resource-row-canvas")
|
||||
.count();
|
||||
const projectBars = await page
|
||||
.locator("[data-timeline-entry-type='project-bar']")
|
||||
.count();
|
||||
const demandBars = await page.locator("[data-timeline-entry-type='demand']").count();
|
||||
const emptyStates = await page.getByText(/No projects in this time range/).count();
|
||||
return projectRows + projectBars + demandBars + emptyStates;
|
||||
},
|
||||
{ timeout: 10_000 },
|
||||
)
|
||||
.not.toBe(0);
|
||||
}
|
||||
await expect(page.getByTestId("timeline-resource-row-canvas")).toHaveCount(0);
|
||||
@@ -906,22 +909,21 @@ test.describe("Timeline", () => {
|
||||
await expect(page.locator("text=/\\d+ resources/")).toBeVisible();
|
||||
});
|
||||
|
||||
test("view toggle stays disabled until the initial timeline load becomes interactive", async ({ page }) => {
|
||||
test("view toggle stays disabled until the initial timeline load becomes interactive", async ({
|
||||
page,
|
||||
}) => {
|
||||
const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
||||
const scenario = createTimelineSegmentScenario(suffix);
|
||||
|
||||
try {
|
||||
await page.goto(
|
||||
`/timeline?startDate=2026-04-01&days=31&eids=${scenario.resourceEid}`,
|
||||
{ waitUntil: "domcontentloaded" },
|
||||
);
|
||||
await page.goto(`/timeline?startDate=2026-04-01&days=31&eids=${scenario.resourceEid}`, {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
|
||||
const projectButton = page.getByRole("button", { name: "Project view" });
|
||||
const resourceButton = page.getByRole("button", { name: "Resource view" });
|
||||
const resourceRowSelector =
|
||||
`[data-testid="timeline-resource-row-canvas"][data-resource-eid="${scenario.resourceEid}"]`;
|
||||
const projectRowSelector =
|
||||
`[data-testid="timeline-project-resource-row-canvas"][data-project-id="${scenario.projectId}"][data-resource-id="${scenario.resourceId}"]`;
|
||||
const resourceRowSelector = `[data-testid="timeline-resource-row-canvas"][data-resource-eid="${scenario.resourceEid}"]`;
|
||||
const projectRowSelector = `[data-testid="timeline-project-resource-row-canvas"][data-project-id="${scenario.projectId}"][data-resource-id="${scenario.resourceId}"]`;
|
||||
|
||||
await expect(projectButton).toBeDisabled();
|
||||
await expect(resourceButton).toBeDisabled();
|
||||
@@ -951,9 +953,9 @@ test.describe("Timeline", () => {
|
||||
|
||||
test("keeps timeline data populated after navigating from allocations", async ({ page }) => {
|
||||
await page.goto("/allocations");
|
||||
await expect(
|
||||
page.locator("h1").filter({ hasText: /Allocations|Planning/i }),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("h1").filter({ hasText: /Allocations|Planning/i })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
await page.locator('nav a >> text="Timeline"').first().click();
|
||||
await expect(page).toHaveURL(/\/timeline/);
|
||||
@@ -1046,7 +1048,10 @@ test.describe("Timeline", () => {
|
||||
if (!projectAllocationBox) {
|
||||
throw new Error("Expected a project allocation block to be available");
|
||||
}
|
||||
await page.mouse.move(projectAllocationBox.x + (projectAllocationBox.width / 2), projectHoverBox.y + 20);
|
||||
await page.mouse.move(
|
||||
projectAllocationBox.x + projectAllocationBox.width / 2,
|
||||
projectHoverBox.y + 20,
|
||||
);
|
||||
await expect(heatmapTooltip).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
@@ -1071,7 +1076,9 @@ test.describe("Timeline", () => {
|
||||
.first();
|
||||
await allocation.click({ button: "right" });
|
||||
|
||||
await expect(page.getByTestId("timeline-allocation-popover-loading")).toHaveCount(0, { timeout: 2_000 });
|
||||
await expect(page.getByTestId("timeline-allocation-popover-loading")).toHaveCount(0, {
|
||||
timeout: 2_000,
|
||||
});
|
||||
const popover = page.getByTestId("timeline-allocation-popover");
|
||||
await expect(popover).toBeVisible();
|
||||
await expect(page.getByTestId("timeline-allocation-popover-error")).toHaveCount(0);
|
||||
@@ -1103,12 +1110,16 @@ test.describe("Timeline", () => {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
|
||||
const row = page.locator('[data-testid="timeline-resource-row-canvas"][data-resource-eid="bruce.banner"]').first();
|
||||
const row = page
|
||||
.locator('[data-testid="timeline-resource-row-canvas"][data-resource-eid="bruce.banner"]')
|
||||
.first();
|
||||
await expect(row).toBeVisible();
|
||||
|
||||
const holidayBlock = row.locator(
|
||||
'[data-testid="timeline-vacation-block"][data-vacation-type="PUBLIC_HOLIDAY"][data-vacation-note="Karfreitag"]',
|
||||
).first();
|
||||
const holidayBlock = row
|
||||
.locator(
|
||||
'[data-testid="timeline-vacation-block"][data-vacation-type="PUBLIC_HOLIDAY"][data-vacation-note="Karfreitag"]',
|
||||
)
|
||||
.first();
|
||||
await expect(holidayBlock).toBeVisible();
|
||||
|
||||
const rowBox = await row.boundingBox();
|
||||
@@ -1129,7 +1140,9 @@ test.describe("Timeline", () => {
|
||||
|
||||
const holidayTooltip = page
|
||||
.locator("div.fixed.pointer-events-none.rounded-xl.border.border-amber-700\\/50")
|
||||
.or(page.locator("div.fixed.pointer-events-none.rounded-xl").filter({ hasText: "Karfreitag" }))
|
||||
.or(
|
||||
page.locator("div.fixed.pointer-events-none.rounded-xl").filter({ hasText: "Karfreitag" }),
|
||||
)
|
||||
.first();
|
||||
|
||||
await expect(holidayTooltip).toBeVisible();
|
||||
@@ -1278,9 +1291,7 @@ test.describe("Timeline", () => {
|
||||
expect(result.maxGap).toBeLessThan(24);
|
||||
});
|
||||
|
||||
test("allocation resize shows a live preview before mouseup", async ({
|
||||
page,
|
||||
}) => {
|
||||
test("allocation resize shows a live preview before mouseup", async ({ page }) => {
|
||||
await page.goto("/timeline?startDate=2026-04-01&days=31", {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
@@ -1358,9 +1369,7 @@ test.describe("Timeline", () => {
|
||||
expect(secondResize.rightEdgeGain).toBeGreaterThan(48);
|
||||
});
|
||||
|
||||
test("allocation start resize shows a live preview before mouseup", async ({
|
||||
page,
|
||||
}) => {
|
||||
test("allocation start resize shows a live preview before mouseup", async ({ page }) => {
|
||||
await page.goto("/timeline?startDate=2026-04-01&days=31", {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
@@ -1394,18 +1403,17 @@ test.describe("Timeline", () => {
|
||||
await page.goto(`/timeline?startDate=2026-04-01&days=30&eids=${scenario.resourceEid}`, {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
const resourceRowSelector =
|
||||
`[data-testid="timeline-resource-row-canvas"][data-resource-eid="${scenario.resourceEid}"]`;
|
||||
const projectRowSelector =
|
||||
`[data-testid="timeline-project-resource-row-canvas"][data-project-id="${scenario.projectId}"][data-resource-id="${scenario.resourceId}"]`;
|
||||
const projectAllocationSelector =
|
||||
`${projectRowSelector} [data-timeline-entry-type="allocation"][data-allocation-id="${scenario.assignmentId}"]`;
|
||||
const resourceRowSelector = `[data-testid="timeline-resource-row-canvas"][data-resource-eid="${scenario.resourceEid}"]`;
|
||||
const projectRowSelector = `[data-testid="timeline-project-resource-row-canvas"][data-project-id="${scenario.projectId}"][data-resource-id="${scenario.resourceId}"]`;
|
||||
const projectAllocationSelector = `${projectRowSelector} [data-timeline-entry-type="allocation"][data-allocation-id="${scenario.assignmentId}"]`;
|
||||
|
||||
await expect(page.locator(resourceRowSelector)).toBeVisible();
|
||||
await expect(
|
||||
page.locator(
|
||||
`[data-timeline-entry-type="allocation"][data-allocation-id="${scenario.assignmentId}"]`,
|
||||
).first(),
|
||||
page
|
||||
.locator(
|
||||
`[data-timeline-entry-type="allocation"][data-allocation-id="${scenario.assignmentId}"]`,
|
||||
)
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
|
||||
await switchToProjectView(page, projectRowSelector);
|
||||
@@ -1427,19 +1435,22 @@ test.describe("Timeline", () => {
|
||||
expect(resizeEnd.rightEdgeGain).toBeGreaterThan(48);
|
||||
let rightResizeAssignments: Array<{ id: string; startDate: string; endDate: string }> = [];
|
||||
await expect
|
||||
.poll(() => {
|
||||
rightResizeAssignments = listScenarioAssignments(scenario.projectId);
|
||||
if (rightResizeAssignments.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
.poll(
|
||||
() => {
|
||||
rightResizeAssignments = listScenarioAssignments(scenario.projectId);
|
||||
if (rightResizeAssignments.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [assignment] = rightResizeAssignments;
|
||||
if (!assignment || assignment.id !== scenario.assignmentId) {
|
||||
return null;
|
||||
}
|
||||
const [assignment] = rightResizeAssignments;
|
||||
if (!assignment || assignment.id !== scenario.assignmentId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return assignment.endDate;
|
||||
}, { timeout: 15_000 })
|
||||
return assignment.endDate;
|
||||
},
|
||||
{ timeout: 15_000 },
|
||||
)
|
||||
.not.toBe("2026-04-17");
|
||||
expect(rightResizeAssignments).toHaveLength(1);
|
||||
expect(rightResizeAssignments[0]?.id).toBe(scenario.assignmentId);
|
||||
@@ -1451,19 +1462,22 @@ test.describe("Timeline", () => {
|
||||
expect(resizeStart.leftEdgeGain).toBeGreaterThan(36);
|
||||
let leftResizeAssignments: Array<{ id: string; startDate: string; endDate: string }> = [];
|
||||
await expect
|
||||
.poll(() => {
|
||||
leftResizeAssignments = listScenarioAssignments(scenario.projectId);
|
||||
if (leftResizeAssignments.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
.poll(
|
||||
() => {
|
||||
leftResizeAssignments = listScenarioAssignments(scenario.projectId);
|
||||
if (leftResizeAssignments.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [assignment] = leftResizeAssignments;
|
||||
if (!assignment || assignment.id !== scenario.assignmentId) {
|
||||
return null;
|
||||
}
|
||||
const [assignment] = leftResizeAssignments;
|
||||
if (!assignment || assignment.id !== scenario.assignmentId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return assignment.startDate;
|
||||
}, { timeout: 15_000 })
|
||||
return assignment.startDate;
|
||||
},
|
||||
{ timeout: 15_000 },
|
||||
)
|
||||
.not.toBe("2026-04-06");
|
||||
expect(leftResizeAssignments).toHaveLength(1);
|
||||
expect(leftResizeAssignments[0]?.id).toBe(scenario.assignmentId);
|
||||
@@ -1479,15 +1493,12 @@ test.describe("Timeline", () => {
|
||||
const scenario = createTimelineDemandScenario(suffix);
|
||||
|
||||
try {
|
||||
await page.goto(
|
||||
`/timeline?startDate=2026-04-01&days=31&projectIds=${scenario.projectId}`,
|
||||
{ waitUntil: "domcontentloaded" },
|
||||
);
|
||||
await page.goto(`/timeline?startDate=2026-04-01&days=31&projectIds=${scenario.projectId}`, {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await ensureOpenDemandVisibilityEnabled(page);
|
||||
const demandRowSelector =
|
||||
`[data-project-demand-row="true"][data-project-id="${scenario.projectId}"]`;
|
||||
const demandSelector =
|
||||
`${demandRowSelector} [data-timeline-entry-type="demand"][data-allocation-id="${scenario.demandId}"]`;
|
||||
const demandRowSelector = `[data-project-demand-row="true"][data-project-id="${scenario.projectId}"]`;
|
||||
const demandSelector = `${demandRowSelector} [data-timeline-entry-type="demand"][data-allocation-id="${scenario.demandId}"]`;
|
||||
|
||||
await switchToProjectView(page, demandRowSelector);
|
||||
await expect(page.getByText(scenario.projectName).first()).toBeVisible();
|
||||
@@ -1505,19 +1516,22 @@ test.describe("Timeline", () => {
|
||||
status: string;
|
||||
}> = [];
|
||||
await expect
|
||||
.poll(() => {
|
||||
rightResizeDemands = listScenarioDemands(scenario.projectId);
|
||||
if (rightResizeDemands.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
.poll(
|
||||
() => {
|
||||
rightResizeDemands = listScenarioDemands(scenario.projectId);
|
||||
if (rightResizeDemands.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [demand] = rightResizeDemands;
|
||||
if (!demand || demand.id !== scenario.demandId) {
|
||||
return null;
|
||||
}
|
||||
const [demand] = rightResizeDemands;
|
||||
if (!demand || demand.id !== scenario.demandId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return demand.endDate;
|
||||
}, { timeout: 15_000 })
|
||||
return demand.endDate;
|
||||
},
|
||||
{ timeout: 15_000 },
|
||||
)
|
||||
.not.toBe("2026-04-16");
|
||||
expect(rightResizeDemands).toHaveLength(1);
|
||||
expect(rightResizeDemands[0]?.id).toBe(scenario.demandId);
|
||||
@@ -1538,19 +1552,22 @@ test.describe("Timeline", () => {
|
||||
status: string;
|
||||
}> = [];
|
||||
await expect
|
||||
.poll(() => {
|
||||
leftResizeDemands = listScenarioDemands(scenario.projectId);
|
||||
if (leftResizeDemands.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
.poll(
|
||||
() => {
|
||||
leftResizeDemands = listScenarioDemands(scenario.projectId);
|
||||
if (leftResizeDemands.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [demand] = leftResizeDemands;
|
||||
if (!demand || demand.id !== scenario.demandId) {
|
||||
return null;
|
||||
}
|
||||
const [demand] = leftResizeDemands;
|
||||
if (!demand || demand.id !== scenario.demandId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return demand.startDate;
|
||||
}, { timeout: 15_000 })
|
||||
return demand.startDate;
|
||||
},
|
||||
{ timeout: 15_000 },
|
||||
)
|
||||
.not.toBe("2026-04-07");
|
||||
expect(leftResizeDemands).toHaveLength(1);
|
||||
expect(leftResizeDemands[0]?.id).toBe(scenario.demandId);
|
||||
@@ -1630,7 +1647,11 @@ test.describe("Timeline", () => {
|
||||
);
|
||||
await expect(resizedSegment).toBeVisible();
|
||||
|
||||
await dragLocatorBy(page, resizedSegment.locator('[data-allocation-interaction="body"]'), -dayWidth);
|
||||
await dragLocatorBy(
|
||||
page,
|
||||
resizedSegment.locator('[data-allocation-interaction="body"]'),
|
||||
-dayWidth,
|
||||
);
|
||||
await releaseMouse(page);
|
||||
|
||||
await waitForScenarioAssignments(scenario.projectId, [
|
||||
@@ -1674,9 +1695,21 @@ test.describe("Timeline", () => {
|
||||
{ startDate: "2026-04-11", endDate: "2026-04-17" },
|
||||
]);
|
||||
|
||||
const leftSplit = row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first();
|
||||
const rightSplit = row.locator('[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]').first();
|
||||
const nextWeekSegment = row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first();
|
||||
const leftSplit = row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]',
|
||||
)
|
||||
.first();
|
||||
const rightSplit = row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]',
|
||||
)
|
||||
.first();
|
||||
const nextWeekSegment = row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]',
|
||||
)
|
||||
.first();
|
||||
await expect(leftSplit).toBeVisible();
|
||||
await expect(rightSplit).toBeVisible();
|
||||
await expect(nextWeekSegment).toBeVisible();
|
||||
@@ -1704,22 +1737,42 @@ test.describe("Timeline", () => {
|
||||
]);
|
||||
|
||||
await expect(
|
||||
row.locator('[data-allocation-segment-start="2026-04-08"][data-allocation-segment-end="2026-04-08"]').first(),
|
||||
row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-08"][data-allocation-segment-end="2026-04-08"]',
|
||||
)
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
|
||||
await page.reload({ waitUntil: "domcontentloaded" });
|
||||
await expect(row).toBeVisible();
|
||||
await expect(
|
||||
row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first(),
|
||||
row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]',
|
||||
)
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
row.locator('[data-allocation-segment-start="2026-04-08"][data-allocation-segment-end="2026-04-08"]').first(),
|
||||
row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-08"][data-allocation-segment-end="2026-04-08"]',
|
||||
)
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
row.locator('[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]').first(),
|
||||
row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]',
|
||||
)
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first(),
|
||||
row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]',
|
||||
)
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
} finally {
|
||||
cleanupTimelineSegmentScenario(scenario.projectId, scenario.resourceId);
|
||||
@@ -1769,9 +1822,21 @@ test.describe("Timeline", () => {
|
||||
await page.reload({ waitUntil: "domcontentloaded" });
|
||||
await expect(row).toBeVisible();
|
||||
|
||||
const leftSplit = row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first();
|
||||
const fridayBridge = row.locator('[data-allocation-segment-start="2026-04-10"][data-allocation-segment-end="2026-04-10"]').first();
|
||||
const mondaySegment = row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first();
|
||||
const leftSplit = row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]',
|
||||
)
|
||||
.first();
|
||||
const fridayBridge = row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-10"][data-allocation-segment-end="2026-04-10"]',
|
||||
)
|
||||
.first();
|
||||
const mondaySegment = row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]',
|
||||
)
|
||||
.first();
|
||||
await expect(leftSplit).toBeVisible();
|
||||
await expect(fridayBridge).toBeVisible();
|
||||
await expect(mondaySegment).toBeVisible();
|
||||
@@ -1797,13 +1862,25 @@ test.describe("Timeline", () => {
|
||||
await page.reload({ waitUntil: "domcontentloaded" });
|
||||
await expect(row).toBeVisible();
|
||||
await expect(
|
||||
row.locator('[data-allocation-segment-start="2026-04-08"][data-allocation-segment-end="2026-04-09"]').first(),
|
||||
row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-08"][data-allocation-segment-end="2026-04-09"]',
|
||||
)
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
row.locator('[data-allocation-segment-start="2026-04-10"][data-allocation-segment-end="2026-04-10"]').first(),
|
||||
row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-10"][data-allocation-segment-end="2026-04-10"]',
|
||||
)
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first(),
|
||||
row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]',
|
||||
)
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
} finally {
|
||||
cleanupTimelineSegmentScenario(scenario.projectId, scenario.resourceId);
|
||||
@@ -1850,9 +1927,21 @@ test.describe("Timeline", () => {
|
||||
{ startDate: "2026-04-09", endDate: "2026-04-17" },
|
||||
]);
|
||||
|
||||
const leftSplit = row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first();
|
||||
const rightSplit = row.locator('[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]').first();
|
||||
const mondaySegment = row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first();
|
||||
const leftSplit = row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]',
|
||||
)
|
||||
.first();
|
||||
const rightSplit = row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]',
|
||||
)
|
||||
.first();
|
||||
const mondaySegment = row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]',
|
||||
)
|
||||
.first();
|
||||
await expect(leftSplit).toBeVisible();
|
||||
await expect(rightSplit).toBeVisible();
|
||||
await expect(mondaySegment).toBeVisible();
|
||||
@@ -1870,8 +1959,16 @@ test.describe("Timeline", () => {
|
||||
{ startDate: "2026-04-09", endDate: "2026-04-17" },
|
||||
]);
|
||||
|
||||
const resizedRightSplit = row.locator('[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]').first();
|
||||
await dragLocatorBy(page, resizedRightSplit.locator('[data-allocation-handle="end"]'), -dayWidth);
|
||||
const resizedRightSplit = row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]',
|
||||
)
|
||||
.first();
|
||||
await dragLocatorBy(
|
||||
page,
|
||||
resizedRightSplit.locator('[data-allocation-handle="end"]'),
|
||||
-dayWidth,
|
||||
);
|
||||
await releaseMouse(page);
|
||||
|
||||
await waitForScenarioAssignments(scenario.projectId, [
|
||||
@@ -1883,9 +1980,11 @@ test.describe("Timeline", () => {
|
||||
await page.reload({ waitUntil: "domcontentloaded" });
|
||||
await expect(row).toBeVisible();
|
||||
|
||||
const mondaySegmentAfterReload = row.locator(
|
||||
'[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]',
|
||||
).first();
|
||||
const mondaySegmentAfterReload = row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]',
|
||||
)
|
||||
.first();
|
||||
await expect(mondaySegmentAfterReload).toBeVisible();
|
||||
|
||||
const mondayCarveDateInputs = page.locator('input[placeholder="dd/mm/yyyy"]');
|
||||
@@ -1951,9 +2050,21 @@ test.describe("Timeline", () => {
|
||||
{ startDate: "2026-04-09", endDate: "2026-04-17" },
|
||||
]);
|
||||
|
||||
const leftSplit = row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first();
|
||||
const rightSplit = row.locator('[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]').first();
|
||||
const mondaySegment = row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first();
|
||||
const leftSplit = row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]',
|
||||
)
|
||||
.first();
|
||||
const rightSplit = row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]',
|
||||
)
|
||||
.first();
|
||||
const mondaySegment = row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]',
|
||||
)
|
||||
.first();
|
||||
await expect(leftSplit).toBeVisible();
|
||||
await expect(rightSplit).toBeVisible();
|
||||
await expect(mondaySegment).toBeVisible();
|
||||
@@ -1968,7 +2079,11 @@ test.describe("Timeline", () => {
|
||||
]);
|
||||
|
||||
await expect(
|
||||
row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first(),
|
||||
row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]',
|
||||
)
|
||||
.first(),
|
||||
).toHaveCount(0);
|
||||
await expect(rightSplit).toBeVisible();
|
||||
await expect(mondaySegment).toBeVisible();
|
||||
@@ -1976,13 +2091,25 @@ test.describe("Timeline", () => {
|
||||
await page.reload({ waitUntil: "domcontentloaded" });
|
||||
await expect(row).toBeVisible();
|
||||
await expect(
|
||||
row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first(),
|
||||
row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]',
|
||||
)
|
||||
.first(),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
row.locator('[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]').first(),
|
||||
row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]',
|
||||
)
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first(),
|
||||
row
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]',
|
||||
)
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
} finally {
|
||||
cleanupTimelineSegmentScenario(scenario.projectId, scenario.resourceId);
|
||||
@@ -2029,13 +2156,14 @@ test.describe("Timeline", () => {
|
||||
{ startDate: "2026-04-09", endDate: "2026-04-17" },
|
||||
]);
|
||||
|
||||
const mondaySegment = resourceRow.locator(
|
||||
'[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]',
|
||||
).first();
|
||||
const mondaySegment = resourceRow
|
||||
.locator(
|
||||
'[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]',
|
||||
)
|
||||
.first();
|
||||
await expect(mondaySegment).toBeVisible();
|
||||
|
||||
const projectRowSelector =
|
||||
`[data-testid="timeline-project-resource-row-canvas"][data-project-id="${scenario.projectId}"][data-resource-id="${scenario.resourceId}"]`;
|
||||
const projectRowSelector = `[data-testid="timeline-project-resource-row-canvas"][data-project-id="${scenario.projectId}"][data-resource-id="${scenario.resourceId}"]`;
|
||||
await switchToProjectView(page, projectRowSelector);
|
||||
|
||||
let mondayAssignment: { id: string; startDate: string; endDate: string } | null = null;
|
||||
@@ -2072,7 +2200,9 @@ test.describe("Timeline", () => {
|
||||
await expect(page.getByText(scenario.projectName).first()).toBeVisible();
|
||||
|
||||
const projectAllocationAfterReload = page
|
||||
.locator(`[data-timeline-entry-type="allocation"][data-allocation-id="${mondayAssignment!.id}"]`)
|
||||
.locator(
|
||||
`[data-timeline-entry-type="allocation"][data-allocation-id="${mondayAssignment!.id}"]`,
|
||||
)
|
||||
.first();
|
||||
await expect(projectAllocationAfterReload).toBeVisible();
|
||||
await openContextMenuAtCenter(page, projectAllocationAfterReload);
|
||||
@@ -2093,15 +2223,12 @@ test.describe("Timeline", () => {
|
||||
const scenario = createTimelineDemandScenario(suffix);
|
||||
|
||||
try {
|
||||
await page.goto(
|
||||
`/timeline?startDate=2026-04-01&days=31&projectIds=${scenario.projectId}`,
|
||||
{ waitUntil: "domcontentloaded" },
|
||||
);
|
||||
await page.goto(`/timeline?startDate=2026-04-01&days=31&projectIds=${scenario.projectId}`, {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await ensureOpenDemandVisibilityEnabled(page);
|
||||
const demandRowSelector =
|
||||
`[data-project-demand-row="true"][data-project-id="${scenario.projectId}"]`;
|
||||
const demandSelector =
|
||||
`${demandRowSelector} [data-timeline-entry-type="demand"][data-allocation-id="${scenario.demandId}"]`;
|
||||
const demandRowSelector = `[data-project-demand-row="true"][data-project-id="${scenario.projectId}"]`;
|
||||
const demandSelector = `${demandRowSelector} [data-timeline-entry-type="demand"][data-allocation-id="${scenario.demandId}"]`;
|
||||
|
||||
await switchToProjectView(page, demandRowSelector);
|
||||
await expect(page.locator(demandSelector)).toBeVisible();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
async function signInAsAdmin(page: Page) {
|
||||
await page.goto("/auth/signin");
|
||||
await page.fill('input[type="email"]', "admin@capakraken.dev");
|
||||
await page.fill('input[type="email"]', "admin@nexus.dev");
|
||||
await page.fill('input[type="password"]', "admin123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/(dashboard|resources)/);
|
||||
@@ -27,9 +27,9 @@ test.describe("Vacations", () => {
|
||||
|
||||
test("request vacation button is visible", async ({ page }) => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(
|
||||
page.locator("button", { hasText: /Request Vacation/i }),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("button", { hasText: /Request Vacation/i })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test("request vacation is blocked without linked resource", async ({ page }) => {
|
||||
@@ -37,7 +37,9 @@ test.describe("Vacations", () => {
|
||||
const reqBtn = page.locator("button", { hasText: /Request Vacation/i });
|
||||
await expect(reqBtn).toBeDisabled();
|
||||
await expect(
|
||||
page.getByText("Your account is not linked to a resource. Please contact an administrator."),
|
||||
page.getByText(
|
||||
"Your account is not linked to a resource. Please contact an administrator.",
|
||||
),
|
||||
).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
@@ -57,11 +59,18 @@ test.describe("Vacations", () => {
|
||||
|
||||
test("team calendar tab renders", async ({ page }) => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.locator("button", { hasText: "Team Calendar" }).or(page.locator("text=Team Calendar")).first().click();
|
||||
await page
|
||||
.locator("button", { hasText: "Team Calendar" })
|
||||
.or(page.locator("text=Team Calendar"))
|
||||
.first()
|
||||
.click();
|
||||
await page.waitForTimeout(500);
|
||||
// Calendar view should appear
|
||||
await expect(
|
||||
page.locator("table").or(page.locator("[data-calendar]")).or(page.locator("text=Mon").or(page.locator("text=Week"))),
|
||||
page
|
||||
.locator("table")
|
||||
.or(page.locator("[data-calendar]"))
|
||||
.or(page.locator("text=Mon").or(page.locator("text=Week"))),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
@@ -75,11 +84,15 @@ test.describe("Vacations", () => {
|
||||
await expect(filters.nth(2)).toHaveValue("");
|
||||
});
|
||||
|
||||
test("vacation request preview excludes regional public holidays from deducted days", async ({ page }) => {
|
||||
test("vacation request preview excludes regional public holidays from deducted days", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.getByRole("button", { name: /request vacation/i }).click();
|
||||
|
||||
await expect(page.getByLabel(/^type/i).locator("option", { hasText: /Public Holiday/i })).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByLabel(/^type/i).locator("option", { hasText: /Public Holiday/i }),
|
||||
).toHaveCount(0);
|
||||
await page.getByLabel(/resource/i).selectOption({ label: "Bruce Banner (bruce.banner)" });
|
||||
await page.getByLabel(/^type/i).selectOption("ANNUAL");
|
||||
await fillDisplayDate(page, /start date/i, "2026-01-06");
|
||||
@@ -89,9 +102,13 @@ test.describe("Vacations", () => {
|
||||
await expect(page.getByTestId("vacation-preview-requested-days")).toHaveText("1");
|
||||
await expect(page.getByTestId("vacation-preview-effective-days")).toHaveText("0");
|
||||
await expect(page.getByTestId("vacation-preview-deducted-days")).toHaveText("0");
|
||||
await expect(page.getByTestId("vacation-preview-public-holidays")).toContainText("2026-01-06");
|
||||
await expect(page.getByTestId("vacation-preview-public-holidays")).toContainText(
|
||||
"2026-01-06",
|
||||
);
|
||||
await expect(page.getByTestId("vacation-preview-holiday-basis")).toContainText("Germany");
|
||||
await expect(page.getByTestId("vacation-preview-holiday-sources")).toContainText("Holiday Calendar");
|
||||
await expect(page.getByTestId("vacation-preview-holiday-sources")).toContainText(
|
||||
"Holiday Calendar",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user