chore: sync latest dashboard and role flow updates
This commit is contained in:
parent
c526a376d5
commit
8855b8b378
20 changed files with 701 additions and 42 deletions
199
docs/Nxtgauge-End-to-End-Testing-Blueprint.md
Normal file
199
docs/Nxtgauge-End-to-End-Testing-Blueprint.md
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
# Nxtgauge End-to-End Testing Blueprint
|
||||
|
||||
## 1. Scope
|
||||
- Validate full lifecycle for external users from sign-up to role approval and role-specific module insertion.
|
||||
- Validate admin-side verification and approval pipelines and state propagation into management modules.
|
||||
- Validate runtime-config-driven onboarding/dashboard behavior and guard/redirect behavior.
|
||||
- Validate notification and marketplace flows (customer requirement posting, professional discovery).
|
||||
- Validate UI parity and visual regressions for management modules using Playwright + pixelmatch + Storybook + VisBug.
|
||||
- Validate frontend (`vitest`, Playwright) and backend (`cargo test --workspace`) quality gates.
|
||||
|
||||
## 2. Business Logic Summary
|
||||
- External users can exist before any role registration and must appear in Users Management.
|
||||
- Role activation is gated: verification then approval.
|
||||
- Approval produces side effects:
|
||||
- active role created,
|
||||
- role-specific table insertion,
|
||||
- notification event.
|
||||
- Multi-role users are supported and role visibility must remain isolated by role/module.
|
||||
- Runtime config must not silently mask missing onboarding/dashboard config.
|
||||
|
||||
## 3. Environments and Preconditions
|
||||
- Frontend admin app: `/Users/ashwin/workspace/nxtgauge-admin-solid`
|
||||
- Rust backend workspace: `/Users/ashwin/workspace/nxtgauge-backend-rust`
|
||||
- Required ports:
|
||||
- Admin app: `http://localhost:3000`
|
||||
- Storybook: `http://localhost:6006`
|
||||
- Preconditions:
|
||||
- `npm install` completed in admin repo.
|
||||
- Rust toolchain + dependencies available for backend repo.
|
||||
- Seed users/roles configured in backend DB (or fallback data strategy used in UI where applicable).
|
||||
- Admin preview mode supported with `?_preview=1` for deterministic UI checks.
|
||||
|
||||
## 4. Test Data Matrix
|
||||
- External identities:
|
||||
- User without role.
|
||||
- User with pending role onboarding.
|
||||
- User with verified/pending approval role.
|
||||
- User with approved active role.
|
||||
- Multi-role user (2+ roles).
|
||||
- Roles to cover:
|
||||
- `COMPANY`
|
||||
- `JOB_SEEKER`
|
||||
- `CUSTOMER`
|
||||
- `DEVELOPER`
|
||||
- `PHOTOGRAPHER`
|
||||
- `SOCIAL_MEDIA_MANAGER`
|
||||
- Marketplace entities:
|
||||
- 2-3 customer accounts.
|
||||
- 2 requirements per customer.
|
||||
- Approved professional accounts for lead/job discovery.
|
||||
|
||||
## 5. End-to-End Test Scenarios
|
||||
1. Auth split:
|
||||
- external identity blocked from admin login.
|
||||
- internal identity allowed and redirected to admin.
|
||||
- external-resolved session redirected to `/login?from=...`.
|
||||
2. Onboarding:
|
||||
- role-aware onboarding route loads via runtime config.
|
||||
- missing config renders explicit error state.
|
||||
- onboarding submissions persist and move to verification state.
|
||||
3. Dashboard:
|
||||
- role-specific sidebar and tabs render correctly.
|
||||
- guarded routes reject wrong portal/session.
|
||||
4. Verification:
|
||||
- submission appears in Verification Management.
|
||||
- reviewer decisions propagate to Approval Management.
|
||||
5. Approval:
|
||||
- approved role appears in Users Management and role-specific module.
|
||||
- rejected role remains non-active but user remains visible.
|
||||
6. Users Management:
|
||||
- users with zero roles are visible.
|
||||
- role states are visible per user.
|
||||
7. Role-specific insertion:
|
||||
- approval inserts into the right module only.
|
||||
8. Notifications:
|
||||
- approval emits and surfaces unread notification.
|
||||
9. Requirements/leads:
|
||||
- customer creates requirements.
|
||||
- approved professionals discover and act on leads/jobs.
|
||||
|
||||
## 6. Playwright Automation Plan
|
||||
- Existing specs:
|
||||
- `tests/e2e/admin-auth.spec.ts`
|
||||
- `tests/e2e/admin-visual.spec.ts` (pixelmatch)
|
||||
- Added specs:
|
||||
- `tests/e2e/management-parity.spec.ts`
|
||||
- asserts Department-style controls for management modules.
|
||||
- asserts dashboard management pages are non-empty.
|
||||
- `tests/e2e/storybook-admin-pages.spec.ts`
|
||||
- Storybook admin story smoke checks.
|
||||
- Execution commands:
|
||||
- `npm run test:e2e`
|
||||
- `npm run test:management:parity`
|
||||
- `npm run test:visual`
|
||||
- `npm run test:storybook:e2e`
|
||||
|
||||
## 7. API and Backend Validation Plan
|
||||
- Validate gateway/admin API responses and status codes for:
|
||||
- `/api/gateway/api/auth/session`
|
||||
- `/api/gateway/api/runtime-config`
|
||||
- `/api/gateway/api/admin/users`
|
||||
- `/api/gateway/api/admin/external-users`
|
||||
- `/api/gateway/api/admin/dashboard-config`
|
||||
- `/api/gateway/api/admin/roles`
|
||||
- `/api/gateway/api/admin/companies`
|
||||
- Assertions:
|
||||
- lifecycle transitions emit expected payload shape/state.
|
||||
- role key propagation remains consistent across services.
|
||||
- no silent fallback on missing config.
|
||||
|
||||
## 8. Database Validation Plan
|
||||
- Validate tables/events after each phase:
|
||||
- user record creation at sign-up.
|
||||
- onboarding row/state creation.
|
||||
- verification record creation/update.
|
||||
- approval decision and role activation state.
|
||||
- role-specific module table insertion on approval only.
|
||||
- notification row insertion.
|
||||
- Cross-check:
|
||||
- rejected records never inserted into approved role module tables.
|
||||
- multi-role user keeps separate role states.
|
||||
|
||||
## 9. Runtime Config Validation
|
||||
- Validate:
|
||||
- role-specific onboarding config retrieval.
|
||||
- role-specific dashboard config retrieval.
|
||||
- UI behavior when config exists vs missing.
|
||||
- no hidden fallback that masks configuration defects.
|
||||
- Internal/External dashboard management pages must show deterministic list/form states even when backend responses are empty.
|
||||
|
||||
## 10. Notification Validation
|
||||
- Trigger approval and assert:
|
||||
- notification is created in backend.
|
||||
- notification module UI renders item.
|
||||
- unread/read transitions are persisted.
|
||||
- Negative:
|
||||
- rejected approvals must not trigger approval-success notifications.
|
||||
|
||||
## 11. Lead / Requirement Marketplace Validation
|
||||
- Customer flow:
|
||||
- create customer users.
|
||||
- create 2 requirements per customer.
|
||||
- assert requirement visibility in marketplace feeds.
|
||||
- Professional flow:
|
||||
- approved professionals can access leads/jobs routes.
|
||||
- wrong-role users cannot access irrelevant lead pools.
|
||||
- apply/contact actions persist.
|
||||
|
||||
## 12. Edge Cases and Negative Cases
|
||||
- User signs up but never picks role: visible in Users Management.
|
||||
- Missing runtime config: explicit error/empty guard state.
|
||||
- Session crossover:
|
||||
- external session cannot pass admin route guards.
|
||||
- admin session cannot leak into public workspace.
|
||||
- Multi-role conflict:
|
||||
- role A approval should not auto-activate role B.
|
||||
- Approval before verification must be blocked.
|
||||
- API `404`/`401` on optional module endpoints should not crash page shell.
|
||||
|
||||
## 13. Release Blocking Acceptance Checklist
|
||||
- Auth split tests pass.
|
||||
- Verification → approval → insertion path validated for all required roles.
|
||||
- Users Management shows no-role users.
|
||||
- Role-specific management insertion validated.
|
||||
- Notification creation/visibility validated.
|
||||
- Management pages match Department-style controls and table shell.
|
||||
- Visual diff (`pixelmatch`) within thresholds.
|
||||
- `vitest`, existing unit tests, Playwright suites, and `cargo test --workspace` pass.
|
||||
|
||||
## 14. Recommended Folder Structure For Tests
|
||||
- `tests/e2e/admin-auth.spec.ts`
|
||||
- `tests/e2e/admin-visual.spec.ts`
|
||||
- `tests/e2e/management-parity.spec.ts`
|
||||
- `tests/e2e/storybook-admin-pages.spec.ts`
|
||||
- `tests/vitest/admin-auth.spec.ts`
|
||||
- `tests/vitest/module-access.spec.ts`
|
||||
- `tests/visual-artifacts/{actual,diff}`
|
||||
- `docs/Nxtgauge-End-to-End-Testing-Blueprint.md`
|
||||
|
||||
## 15. Suggested Seed Data / Factories
|
||||
- User factories:
|
||||
- `external_user_unregistered`
|
||||
- `external_user_pending_verification`
|
||||
- `external_user_pending_approval`
|
||||
- `external_user_approved_role`
|
||||
- `external_user_multi_role`
|
||||
- Domain factories:
|
||||
- `customer_requirement_factory`
|
||||
- `professional_profile_factory`
|
||||
- `notification_factory`
|
||||
- Role fixtures for matrix roles listed in Section 4.
|
||||
|
||||
## 16. Risks and Likely Failure Points
|
||||
- Role-key drift between onboarding, approval, and role-specific insertion.
|
||||
- Guard and redirect regressions across admin/public portals.
|
||||
- Runtime config partial failures causing invisible fallback behavior.
|
||||
- Backend endpoint shape drift (`users`, `external-users`, `dashboard-config`) causing UI empty states.
|
||||
- Visual regressions in shared table controls/icons across management modules.
|
||||
|
||||
12
package.json
12
package.json
|
|
@ -13,9 +13,17 @@
|
|||
"test": "node --test --experimental-strip-types src/lib/**/*.test.ts",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:headed": "playwright test --headed",
|
||||
"test:visual": "playwright test tests/e2e/admin-visual.spec.ts --reporter=list",
|
||||
"test:visual": "playwright test tests/e2e/admin-visual.spec.ts --reporter=list --workers=1",
|
||||
"test:management:parity": "playwright test tests/e2e/management-parity.spec.ts --reporter=list --workers=1",
|
||||
"test:storybook:e2e": "playwright test -c playwright.storybook.config.ts tests/e2e/storybook-admin-pages.spec.ts --reporter=list --workers=1",
|
||||
"test:external:flow": "playwright test -c playwright.external.config.ts tests/e2e/external-user-flow.spec.ts --reporter=list --workers=1",
|
||||
"test:external:roles": "playwright test -c playwright.external.config.ts tests/e2e/external-roles-onboarding-dashboard.spec.ts --reporter=list --workers=1",
|
||||
"test:external:screenshots": "playwright test -c playwright.external.config.ts tests/e2e/external-role-screenshots.spec.ts --reporter=list --workers=1",
|
||||
"test:vitest": "vitest run",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
"build-storybook": "storybook build",
|
||||
"qa:visbug": "bash ./scripts/visbug-storybook.sh",
|
||||
"qa:e2e:blueprint": "bash ./scripts/run-nxtgauge-e2e-suite.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@solidjs/meta": "^0.29.4",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export default defineConfig({
|
|||
fullyParallel: true,
|
||||
reporter: [['list']],
|
||||
webServer: {
|
||||
command: 'npm run build && PORT=3102 npm run start',
|
||||
command: 'npm run dev -- --port 3102 --host 127.0.0.1',
|
||||
url: 'http://127.0.0.1:3102/',
|
||||
reuseExistingServer: true,
|
||||
timeout: 300_000,
|
||||
|
|
|
|||
19
playwright.external.config.ts
Normal file
19
playwright.external.config.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
fullyParallel: false,
|
||||
reporter: [['list']],
|
||||
use: {
|
||||
baseURL: process.env.EXTERNAL_BASE_URL || 'http://127.0.0.1:3001',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
viewport: { width: 1440, height: 900 },
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
25
playwright.storybook.config.ts
Normal file
25
playwright.storybook.config.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
fullyParallel: true,
|
||||
reporter: [['list']],
|
||||
webServer: {
|
||||
command: 'npm run storybook -- --ci --host 127.0.0.1 --port 6006',
|
||||
url: 'http://127.0.0.1:6006/iframe.html',
|
||||
reuseExistingServer: true,
|
||||
timeout: 300_000,
|
||||
},
|
||||
use: {
|
||||
baseURL: 'http://127.0.0.1:6006',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
viewport: { width: 1440, height: 900 },
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
33
scripts/run-nxtgauge-e2e-suite.sh
Executable file
33
scripts/run-nxtgauge-e2e-suite.sh
Executable file
|
|
@ -0,0 +1,33 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
RUST_ROOT="/Users/ashwin/workspace/nxtgauge-backend-rust"
|
||||
|
||||
echo "[1/7] Node unit tests (existing)"
|
||||
cd "$ROOT_DIR"
|
||||
npm run test
|
||||
|
||||
echo "[2/7] Vitest suite"
|
||||
npm run test:vitest
|
||||
|
||||
echo "[3/7] Storybook build smoke"
|
||||
npm run build-storybook
|
||||
|
||||
echo "[4/7] Playwright management parity e2e"
|
||||
npm run test:management:parity
|
||||
|
||||
echo "[5/7] Playwright visual pixelmatch e2e"
|
||||
npm run test:visual
|
||||
|
||||
echo "[6/7] Storybook Playwright smoke e2e"
|
||||
npm run test:storybook:e2e
|
||||
|
||||
echo "[7/7] Rust backend cargo test"
|
||||
if command -v cargo >/dev/null 2>&1 && [ -f "$RUST_ROOT/Cargo.toml" ]; then
|
||||
(cd "$RUST_ROOT" && cargo test --workspace)
|
||||
else
|
||||
echo "Skipping cargo test (cargo or backend workspace not found)."
|
||||
fi
|
||||
|
||||
echo "Nxtgauge E2E suite completed."
|
||||
10
scripts/visbug-storybook.sh
Executable file
10
scripts/visbug-storybook.sh
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "VisBug + Storybook manual visual QA workflow"
|
||||
echo "1) Start storybook: npm run storybook"
|
||||
echo "2) Open: http://localhost:6006/?path=/story/admin-pages--dashboard"
|
||||
echo "3) Install VisBug in browser if not installed:"
|
||||
echo " https://chrome.google.com/webstore/detail/visbug/cdockenadnadldjbbgcallicgledbeoc"
|
||||
echo "4) Use VisBug tools on each admin story route to inspect spacing, typography, and alignment."
|
||||
echo "5) Save screenshots into tests/visual-artifacts/actual for pixelmatch comparison."
|
||||
|
|
@ -1,19 +1,15 @@
|
|||
import { Title } from "@solidjs/meta";
|
||||
import { HttpStatusCode } from "@solidjs/start";
|
||||
import { A } from "@solidjs/router";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main>
|
||||
<main style={{ padding: "24px", "font-family": "Inter, system-ui, sans-serif" }}>
|
||||
<Title>Not Found</Title>
|
||||
<HttpStatusCode code={404} />
|
||||
<h1>Page Not Found</h1>
|
||||
<p>
|
||||
Visit{" "}
|
||||
<a href="https://start.solidjs.com" target="_blank">
|
||||
start.solidjs.com
|
||||
</a>{" "}
|
||||
to learn how to build SolidStart apps.
|
||||
</p>
|
||||
<p>This route is not available in the cleaned admin build.</p>
|
||||
<A href="/admin">Go to Admin Dashboard</A>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Outlet } from '@solidjs/router';
|
||||
import type { RouteSectionProps } from '@solidjs/router';
|
||||
import AdminShell from '~/components/AdminShell';
|
||||
|
||||
export default function AdminLayout() {
|
||||
export default function AdminLayout(props: RouteSectionProps) {
|
||||
return (
|
||||
<AdminShell>
|
||||
<Outlet />
|
||||
{props.children}
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ const ROLE_BASED_SIDEBAR: Record<'PROFESSIONAL' | 'COMPANY' | 'JOB_SEEKER' | 'CU
|
|||
'Jobs',
|
||||
'My Applications',
|
||||
'Saved Jobs',
|
||||
'Credits',
|
||||
'Explore Nxtgauge',
|
||||
'Verification',
|
||||
'Help Center',
|
||||
|
|
@ -127,6 +126,32 @@ function applyPortfolioRule(items: string[], persona: 'PROFESSIONAL' | 'COMPANY'
|
|||
return items.filter((item) => normalizeToken(item) !== normalizeToken(PORTFOLIO_SIDEBAR_ITEM));
|
||||
}
|
||||
|
||||
function mergeSidebarForPersona(
|
||||
items: string[],
|
||||
persona: 'PROFESSIONAL' | 'COMPANY' | 'JOB_SEEKER' | 'CUSTOMER' | null,
|
||||
): string[] {
|
||||
const base = (persona && ROLE_BASED_SIDEBAR[persona]?.length)
|
||||
? ROLE_BASED_SIDEBAR[persona]
|
||||
: ['My Dashboard', 'My Profile', 'Switch Services', 'Logout'];
|
||||
const merged = new Map<string, string>();
|
||||
for (const item of [...items, ...base]) {
|
||||
const label = String(item || '').trim();
|
||||
if (!label) continue;
|
||||
const key = normalizeToken(label);
|
||||
if (!merged.has(key)) merged.set(key, label);
|
||||
}
|
||||
let next = Array.from(merged.values());
|
||||
if (persona === 'JOB_SEEKER') {
|
||||
next = next.filter((item) => normalizeToken(item) !== 'credits');
|
||||
}
|
||||
if (!next.some((item) => normalizeToken(item) === normalizeToken('Explore Nxtgauge'))) {
|
||||
const verificationIdx = next.findIndex((item) => normalizeToken(item) === normalizeToken('Verification'));
|
||||
if (verificationIdx >= 0) next.splice(verificationIdx, 0, 'Explore Nxtgauge');
|
||||
else next.push('Explore Nxtgauge');
|
||||
}
|
||||
return applyPortfolioRule(next, persona);
|
||||
}
|
||||
|
||||
function sameSidebarOrder(a: string[], b: string[]): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
return a.every((item, idx) => normalizeToken(item) === normalizeToken(b[idx] || ''));
|
||||
|
|
@ -182,6 +207,18 @@ function normalizeDashboard(item: any): ExternalDashboard {
|
|||
};
|
||||
}
|
||||
|
||||
function looksLikeGateway404(payload: any): boolean {
|
||||
const text = String(payload?.message || payload?.error || payload || '').toLowerCase();
|
||||
return text.includes('route not found in gateway');
|
||||
}
|
||||
|
||||
function normalizeRoleNameFromKey(key: string): string {
|
||||
return String(key || '')
|
||||
.toLowerCase()
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
function StatusBadge(props: { status: string }) {
|
||||
const active = () => props.status === 'ACTIVE';
|
||||
return (
|
||||
|
|
@ -266,12 +303,10 @@ export default function ExternalDashboardManagementPage() {
|
|||
const persona = personaById ?? personaByKey;
|
||||
const baseItems = sidebarItems().length
|
||||
? sidebarItems()
|
||||
: (persona && ROLE_BASED_SIDEBAR[persona]?.length)
|
||||
? ROLE_BASED_SIDEBAR[persona]
|
||||
: sidebarLooksCustomer()
|
||||
? ROLE_BASED_SIDEBAR.CUSTOMER
|
||||
: ['My Dashboard', 'My Profile', 'Switch Services', 'Logout'];
|
||||
return applyPortfolioRule(baseItems, persona ?? (sidebarLooksCustomer() ? 'CUSTOMER' : null));
|
||||
: sidebarLooksCustomer()
|
||||
? ROLE_BASED_SIDEBAR.CUSTOMER
|
||||
: ['My Dashboard', 'My Profile', 'Switch Services', 'Logout'];
|
||||
return mergeSidebarForPersona(baseItems, persona ?? (sidebarLooksCustomer() ? 'CUSTOMER' : null));
|
||||
});
|
||||
const previewTabs = createMemo(() => (tabs().length ? tabs() : ['overview']));
|
||||
|
||||
|
|
@ -309,7 +344,7 @@ export default function ExternalDashboardManagementPage() {
|
|||
const persona = rolePersonaById()[selectedRoleId];
|
||||
if (!persona) return;
|
||||
if (!force && sidebarItems().length > 0) return;
|
||||
setSidebarItems(ROLE_BASED_SIDEBAR[persona]);
|
||||
setSidebarItems(mergeSidebarForPersona(ROLE_BASED_SIDEBAR[persona], persona));
|
||||
};
|
||||
|
||||
const applyPreviewPathForRole = (selectedRoleId: string, force = false) => {
|
||||
|
|
@ -331,17 +366,68 @@ export default function ExternalDashboardManagementPage() {
|
|||
const dashData = dashRes.ok ? await dashRes.json().catch(() => []) : [];
|
||||
const roleData = rolesRes.ok ? await rolesRes.json().catch(() => []) : [];
|
||||
|
||||
const dashRows = Array.isArray(dashData) ? dashData : (dashData?.items || dashData?.configs || []);
|
||||
const roleRows = Array.isArray(roleData) ? roleData : (roleData?.roles || roleData?.items || []);
|
||||
const dashNeedsFallback = !dashRes.ok || looksLikeGateway404(dashData);
|
||||
const rolesNeedFallback = !rolesRes.ok || looksLikeGateway404(roleData);
|
||||
|
||||
let finalDashData: any = dashData;
|
||||
let finalRoleData: any = roleData;
|
||||
|
||||
if (dashNeedsFallback) {
|
||||
const fallbackDashRes = await fetch(`${API}/api/config/dashboard`, {
|
||||
headers: authHeaders(),
|
||||
credentials: 'include',
|
||||
});
|
||||
finalDashData = fallbackDashRes.ok ? await fallbackDashRes.json().catch(() => []) : [];
|
||||
}
|
||||
|
||||
if (rolesNeedFallback) {
|
||||
// Gateway no longer routes /api/admin/roles in some environments.
|
||||
// Build role options from dashboard configs so external dashboard table remains usable.
|
||||
const rawRows = Array.isArray(finalDashData) ? finalDashData : (finalDashData?.items || finalDashData?.configs || []);
|
||||
const roleMap = new Map<string, RoleOption>();
|
||||
for (const row of rawRows) {
|
||||
const audience = String(row?.audience || '').toUpperCase();
|
||||
if (audience !== 'EXTERNAL') continue;
|
||||
const roleId = String(row?.role_id || row?.roleId || '').trim();
|
||||
const roleKey = String(row?.role_key || row?.config_json?.role_key || '').toUpperCase().trim();
|
||||
if (!roleId) continue;
|
||||
const name = normalizeRoleNameFromKey(roleKey || 'EXTERNAL_ROLE');
|
||||
roleMap.set(roleId, { id: roleId, key: roleKey || 'EXTERNAL_ROLE', name });
|
||||
}
|
||||
finalRoleData = Array.from(roleMap.values());
|
||||
}
|
||||
|
||||
const dashRows = Array.isArray(finalDashData) ? finalDashData : (finalDashData?.items || finalDashData?.configs || []);
|
||||
const roleRows = Array.isArray(finalRoleData) ? finalRoleData : (finalRoleData?.roles || finalRoleData?.items || []);
|
||||
|
||||
setRoles(roleRows
|
||||
.filter((r: any) => String(r?.audience || '').toUpperCase() === 'EXTERNAL')
|
||||
.filter((r: any) => {
|
||||
const hasRoleShape = Boolean(r?.id && (r?.key || r?.name));
|
||||
if (hasRoleShape && r?.audience == null) return true;
|
||||
return String(r?.audience || '').toUpperCase() === 'EXTERNAL';
|
||||
})
|
||||
.map((r: any) => ({ id: String(r?.id || ''), key: String(r?.key || '').toUpperCase(), name: String(r?.name || r?.key || 'External Role') }))
|
||||
.filter((r: RoleOption) => r.id));
|
||||
|
||||
const roleKeyById = new Map<string, string>();
|
||||
roleRows.forEach((r: any) => {
|
||||
const id = String(r?.id || '').trim();
|
||||
const key = String(r?.key || '').toUpperCase().trim();
|
||||
if (id) roleKeyById.set(id, key);
|
||||
});
|
||||
|
||||
setRows(dashRows
|
||||
.filter((item: any) => String(item?.audience || '').toUpperCase() === 'EXTERNAL')
|
||||
.map(normalizeDashboard)
|
||||
.map((row: ExternalDashboard) => {
|
||||
const resolvedRoleKey = String(row.roleKey || roleKeyById.get(row.roleId) || '').toUpperCase();
|
||||
const persona = personaFromKey(resolvedRoleKey);
|
||||
return {
|
||||
...row,
|
||||
roleKey: resolvedRoleKey || row.roleKey,
|
||||
sidebarItems: mergeSidebarForPersona(row.sidebarItems, persona),
|
||||
};
|
||||
})
|
||||
.sort((a: ExternalDashboard, b: ExternalDashboard) => b.updatedAt.localeCompare(a.updatedAt)));
|
||||
} catch (e: any) {
|
||||
setRows([]);
|
||||
|
|
@ -400,7 +486,8 @@ export default function ExternalDashboardManagementPage() {
|
|||
setFormRoleKey(row.roleKey);
|
||||
setWidgets(row.widgets);
|
||||
setTabs(row.tabs);
|
||||
setSidebarItems(row.sidebarItems);
|
||||
const persona = rolePersonaById()[row.roleId] ?? personaFromKey(row.roleKey);
|
||||
setSidebarItems(mergeSidebarForPersona(row.sidebarItems, persona));
|
||||
setFields(row.fields);
|
||||
setPreviewPath(row.previewPath || rolePreviewPath(row.roleKey));
|
||||
setIsActive(row.status === 'ACTIVE');
|
||||
|
|
@ -425,7 +512,7 @@ export default function ExternalDashboardManagementPage() {
|
|||
const persona = rolePersonaById()[roleId()] ?? personaFromKey(selectedRoleKey() || formRoleKey());
|
||||
if (!persona || !sidebarItems().length) return;
|
||||
setSidebarItems((prev) => {
|
||||
const next = applyPortfolioRule(prev, persona);
|
||||
const next = mergeSidebarForPersona(prev, persona);
|
||||
return sameSidebarOrder(prev, next) ? prev : next;
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,17 +1,5 @@
|
|||
import { A } from '@solidjs/router';
|
||||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main class="auth-page">
|
||||
<div class="auth-bg" />
|
||||
<section class="auth-card">
|
||||
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="auth-logo" />
|
||||
<h1 class="auth-title">Admin Access</h1>
|
||||
<p class="auth-copy">Secure sign-in and runtime control center for NXTGAUGE operations.</p>
|
||||
<div class="actions">
|
||||
<A class="btn primary" href="/login">Sign In</A>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
return <Navigate href="/admin" />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { MemoryRouter, Route, createMemoryHistory } from '@solidjs/router';
|
|||
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
|
||||
|
||||
import '../../app.css';
|
||||
import AdminHomePage from '../../routes/admin';
|
||||
import AdminHomePage from '../../routes/admin/index';
|
||||
import ApprovalPage from '../../routes/admin/approval';
|
||||
import VerificationPage from '../../routes/admin/verification';
|
||||
import DepartmentManagementPage from '../../routes/admin/department-management';
|
||||
|
|
|
|||
74
tests/e2e/external-role-screenshots.spec.ts
Normal file
74
tests/e2e/external-role-screenshots.spec.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
type RoleScenario = {
|
||||
roleKey: string;
|
||||
schemaId: string;
|
||||
profession?: string;
|
||||
};
|
||||
|
||||
const ROLE_SCENARIOS: RoleScenario[] = [
|
||||
{ roleKey: 'COMPANY', schemaId: 'company_onboarding_v1' },
|
||||
{ roleKey: 'JOB_SEEKER', schemaId: 'jobseeker_onboarding_v1' },
|
||||
{ roleKey: 'CUSTOMER', schemaId: 'customer_onboarding_v1' },
|
||||
{ roleKey: 'PHOTOGRAPHER', schemaId: 'photographer_onboarding_v1', profession: 'photographer' },
|
||||
{ roleKey: 'MAKEUP_ARTIST', schemaId: 'makeup_artist_onboarding_v1', profession: 'makeup_artist' },
|
||||
{ roleKey: 'TUTOR', schemaId: 'tutor_onboarding_v1', profession: 'tutor' },
|
||||
{ roleKey: 'DEVELOPER', schemaId: 'developer_onboarding_v1', profession: 'developer' },
|
||||
{ roleKey: 'VIDEO_EDITOR', schemaId: 'video_editor_onboarding_v1', profession: 'video_editor' },
|
||||
{ roleKey: 'GRAPHIC_DESIGNER', schemaId: 'graphic_designer_onboarding_v1', profession: 'graphic_designer' },
|
||||
{ roleKey: 'SOCIAL_MEDIA_MANAGER', schemaId: 'social_media_manager_onboarding_v1', profession: 'social_media_manager' },
|
||||
{ roleKey: 'FITNESS_TRAINER', schemaId: 'fitness_trainer_onboarding_v1', profession: 'fitness_trainer' },
|
||||
{ roleKey: 'CATERING_SERVICES', schemaId: 'catering_services_onboarding_v1', profession: 'catering_services' },
|
||||
];
|
||||
|
||||
const OUTPUT_ROOT = path.join(process.cwd(), 'tests', 'visual-artifacts', 'external-roles');
|
||||
|
||||
async function ensureFolders() {
|
||||
await fs.mkdir(path.join(OUTPUT_ROOT, 'onboarding'), { recursive: true });
|
||||
await fs.mkdir(path.join(OUTPUT_ROOT, 'dashboard'), { recursive: true });
|
||||
}
|
||||
|
||||
function roleSlug(roleKey: string) {
|
||||
return roleKey.toLowerCase();
|
||||
}
|
||||
|
||||
test('capture onboarding + dashboard screenshots for all external roles', async ({ page }) => {
|
||||
test.setTimeout(240_000);
|
||||
await ensureFolders();
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
|
||||
for (const role of ROLE_SCENARIOS) {
|
||||
const params = new URLSearchParams({
|
||||
roleKey: role.roleKey,
|
||||
schemaId: role.schemaId,
|
||||
});
|
||||
if (role.profession) {
|
||||
params.set('profession', role.profession);
|
||||
params.set('intent', 'professional');
|
||||
} else if (role.roleKey === 'COMPANY') {
|
||||
params.set('intent', 'company');
|
||||
} else if (role.roleKey === 'CUSTOMER') {
|
||||
params.set('intent', 'customer');
|
||||
} else if (role.roleKey === 'JOB_SEEKER') {
|
||||
params.set('intent', 'job_seeker');
|
||||
}
|
||||
|
||||
await page.goto(`/onboarding?${params.toString()}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1400);
|
||||
await expect(page.locator('body')).not.toContainText('TypeError');
|
||||
await page.screenshot({
|
||||
path: path.join(OUTPUT_ROOT, 'onboarding', `${roleSlug(role.roleKey)}.png`),
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
await page.goto(`/dashboard?_preview=${encodeURIComponent(role.roleKey)}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
await expect(page.locator('body')).not.toContainText('TypeError');
|
||||
await page.screenshot({
|
||||
path: path.join(OUTPUT_ROOT, 'dashboard', `${roleSlug(role.roleKey)}.png`),
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
67
tests/e2e/external-roles-onboarding-dashboard.spec.ts
Normal file
67
tests/e2e/external-roles-onboarding-dashboard.spec.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
|
||||
type RoleScenario = {
|
||||
roleKey: string;
|
||||
schemaId: string;
|
||||
profession?: string;
|
||||
expectedBadge: string;
|
||||
};
|
||||
|
||||
const ROLE_SCENARIOS: RoleScenario[] = [
|
||||
{ roleKey: 'COMPANY', schemaId: 'company_onboarding_v1', expectedBadge: 'Company' },
|
||||
{ roleKey: 'JOB_SEEKER', schemaId: 'jobseeker_onboarding_v1', expectedBadge: 'Job Seeker' },
|
||||
{ roleKey: 'CUSTOMER', schemaId: 'customer_onboarding_v1', expectedBadge: 'Customer' },
|
||||
{ roleKey: 'PHOTOGRAPHER', schemaId: 'photographer_onboarding_v1', profession: 'photographer', expectedBadge: 'Photographer' },
|
||||
{ roleKey: 'MAKEUP_ARTIST', schemaId: 'makeup_artist_onboarding_v1', profession: 'makeup_artist', expectedBadge: 'Makeup Artist' },
|
||||
{ roleKey: 'TUTOR', schemaId: 'tutor_onboarding_v1', profession: 'tutor', expectedBadge: 'Tutor' },
|
||||
{ roleKey: 'DEVELOPER', schemaId: 'developer_onboarding_v1', profession: 'developer', expectedBadge: 'Developer' },
|
||||
{ roleKey: 'VIDEO_EDITOR', schemaId: 'video_editor_onboarding_v1', profession: 'video_editor', expectedBadge: 'Video Editor' },
|
||||
{ roleKey: 'GRAPHIC_DESIGNER', schemaId: 'graphic_designer_onboarding_v1', profession: 'graphic_designer', expectedBadge: 'Graphic Designer' },
|
||||
{ roleKey: 'SOCIAL_MEDIA_MANAGER', schemaId: 'social_media_manager_onboarding_v1', profession: 'social_media_manager', expectedBadge: 'Social Media Manager' },
|
||||
{ roleKey: 'FITNESS_TRAINER', schemaId: 'fitness_trainer_onboarding_v1', profession: 'fitness_trainer', expectedBadge: 'Fitness Trainer' },
|
||||
{ roleKey: 'CATERING_SERVICES', schemaId: 'catering_services_onboarding_v1', profession: 'catering_services', expectedBadge: 'Catering Services' },
|
||||
];
|
||||
|
||||
test.describe('External roles onboarding + dashboard load checks', () => {
|
||||
for (const role of ROLE_SCENARIOS) {
|
||||
test(`onboarding page loads for ${role.roleKey}`, async ({ page }) => {
|
||||
const params = new URLSearchParams({
|
||||
roleKey: role.roleKey,
|
||||
schemaId: role.schemaId,
|
||||
});
|
||||
if (role.profession) {
|
||||
params.set('profession', role.profession);
|
||||
params.set('intent', 'professional');
|
||||
} else if (role.roleKey === 'COMPANY') {
|
||||
params.set('intent', 'company');
|
||||
} else if (role.roleKey === 'CUSTOMER') {
|
||||
params.set('intent', 'customer');
|
||||
} else if (role.roleKey === 'JOB_SEEKER') {
|
||||
params.set('intent', 'job_seeker');
|
||||
}
|
||||
|
||||
await page.goto(`/onboarding?${params.toString()}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1200);
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/onboarding\\?`));
|
||||
await expect(page.locator('body')).not.toContainText('TypeError');
|
||||
await expect(page.locator('body')).not.toContainText('Cannot read properties');
|
||||
await expect(page.locator('body')).not.toContainText('Not Found (role=');
|
||||
await expect(page.locator('body')).not.toContainText('No onboarding schema configured for role');
|
||||
|
||||
const bodyText = await page.locator('body').innerText();
|
||||
expect(bodyText.trim().length).toBeGreaterThan(20);
|
||||
});
|
||||
|
||||
test(`dashboard loads for ${role.roleKey} preview`, async ({ page }) => {
|
||||
await page.goto(`/dashboard?_preview=${encodeURIComponent(role.roleKey)}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(900);
|
||||
|
||||
await expect(page.locator('body')).not.toContainText('TypeError');
|
||||
await expect(page.locator('body')).not.toContainText('Cannot read properties');
|
||||
await expect(page.locator('.role-badge')).toContainText(role.expectedBadge);
|
||||
await expect(page.locator('.topbar-user')).toContainText('Preview User');
|
||||
await expect(page.locator('body')).toContainText('Dashboard');
|
||||
});
|
||||
}
|
||||
});
|
||||
33
tests/e2e/external-user-flow.spec.ts
Normal file
33
tests/e2e/external-user-flow.spec.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('External user public flow smoke', () => {
|
||||
test('login page renders expected fields', async ({ page }) => {
|
||||
await page.goto('/auth/login', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.getByRole('heading', { name: 'Sign In' })).toBeVisible();
|
||||
await expect(page.getByPlaceholder('Enter your email')).toBeVisible();
|
||||
await expect(page.getByPlaceholder('Enter your password')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Sign In' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('choose-role page renders and routes to company onboarding', async ({ page }) => {
|
||||
await page.goto('/users/choose-role', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.getByRole('heading', { name: 'What would you like to do today?' })).toBeVisible();
|
||||
await page.waitForTimeout(1200);
|
||||
|
||||
await page.getByRole('button', { name: /Company/i }).first().click();
|
||||
await expect(page).toHaveURL(/\/onboarding\?roleKey=COMPANY&schemaId=company_onboarding_v1&intent=company/, { timeout: 15000 });
|
||||
});
|
||||
|
||||
test('choose-role page routes professional subtype to professional onboarding', async ({ page }) => {
|
||||
await page.goto('/users/choose-role', { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1200);
|
||||
await page.getByRole('button', { name: /Photographer/i }).first().click();
|
||||
await expect(page).toHaveURL(/\/onboarding\?roleKey=PHOTOGRAPHER&schemaId=photographer_onboarding_v1&profession=photographer&intent=professional/, { timeout: 15000 });
|
||||
});
|
||||
|
||||
test('dashboard route does not white-screen for guest session', async ({ page }) => {
|
||||
await page.goto('/dashboard', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('body')).not.toContainText('TypeError');
|
||||
await expect(page.locator('body')).not.toContainText('Cannot read properties');
|
||||
});
|
||||
});
|
||||
47
tests/e2e/management-parity.spec.ts
Normal file
47
tests/e2e/management-parity.spec.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
|
||||
const MANAGEMENT_ROUTES = [
|
||||
'/admin/company?_preview=1',
|
||||
'/admin/candidate?_preview=1',
|
||||
'/admin/customer?_preview=1',
|
||||
'/admin/photographer?_preview=1',
|
||||
'/admin/makeup-artist?_preview=1',
|
||||
'/admin/tutors?_preview=1',
|
||||
'/admin/developers?_preview=1',
|
||||
'/admin/video-editors?_preview=1',
|
||||
'/admin/fitness-trainers?_preview=1',
|
||||
'/admin/catering-services?_preview=1',
|
||||
'/admin/graphic-designers?_preview=1',
|
||||
'/admin/social-media-managers?_preview=1',
|
||||
'/admin/ugc-content-creator?_preview=1',
|
||||
];
|
||||
|
||||
test.describe('Department-style UI parity for management modules', () => {
|
||||
for (const route of MANAGEMENT_ROUTES) {
|
||||
test(`controls and table structure: ${route}`, async ({ page }) => {
|
||||
await page.goto(route, { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.getByText('Sort').first()).toBeVisible();
|
||||
await expect(page.getByText('Filters').first()).toBeVisible();
|
||||
await expect(page.getByText('Export').first()).toBeVisible();
|
||||
await expect(page.getByText('REGISTERED DATE').first()).toBeVisible();
|
||||
await expect(page.getByText('ACTIONS').first()).toBeVisible();
|
||||
await expect(page.getByText('Live legacy module embedded for exact design and functionality parity during migration.')).toHaveCount(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('Dashboard management modules are non-empty', () => {
|
||||
test('internal dashboard management renders rows or configured table state', async ({ page }) => {
|
||||
await page.goto('/admin/internal-dashboard-management?_preview=1', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.getByRole('heading', { name: 'Internal Dashboard Management' })).toBeVisible();
|
||||
await expect(page.getByText('No internal dashboards found.')).toHaveCount(0);
|
||||
await expect(page.getByText('NAME').first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('external dashboard management renders rows or configured table state', async ({ page }) => {
|
||||
await page.goto('/admin/external-dashboard-management?_preview=1', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.getByRole('heading', { name: 'External Dashboard Management' })).toBeVisible();
|
||||
await expect(page.getByText('No external dashboards found.')).toHaveCount(0);
|
||||
await expect(page.getByText('NAME').first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
20
tests/e2e/storybook-admin-pages.spec.ts
Normal file
20
tests/e2e/storybook-admin-pages.spec.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
|
||||
const STORYBOOK_BASE_URL = process.env.STORYBOOK_BASE_URL || 'http://127.0.0.1:6006';
|
||||
|
||||
const STORIES = [
|
||||
{ id: 'admin-pages--dashboard', text: 'Total Users' },
|
||||
{ id: 'admin-pages--department-management', text: 'Department Management' },
|
||||
{ id: 'admin-pages--internal-dashboard-management', text: 'Internal Dashboard Management' },
|
||||
{ id: 'admin-pages--external-dashboard-management', text: 'External Dashboard Management' },
|
||||
];
|
||||
|
||||
test.describe('Storybook admin stories smoke', () => {
|
||||
for (const story of STORIES) {
|
||||
test(`renders ${story.id}`, async ({ page }) => {
|
||||
const url = `${STORYBOOK_BASE_URL}/iframe.html?id=${story.id}&viewMode=story`;
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.getByText(story.text).first()).toBeVisible({ timeout: 20_000 });
|
||||
});
|
||||
}
|
||||
});
|
||||
17
tests/vitest/admin-auth.spec.ts
Normal file
17
tests/vitest/admin-auth.spec.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { isExternalIdentity, pickManagementLoginError } from '../../src/lib/admin-auth';
|
||||
|
||||
describe('admin-auth helpers (vitest)', () => {
|
||||
it('returns wrong portal message with highest priority', () => {
|
||||
expect(pickManagementLoginError({ error_code: 'WRONG_PORTAL', message: 'Ignore me' })).toBe(
|
||||
'This login is only for internal management users. External users must use the public login page.',
|
||||
);
|
||||
});
|
||||
|
||||
it('detects external identities from multiple payload shapes', () => {
|
||||
expect(isExternalIdentity({ audience: 'public' })).toBe(true);
|
||||
expect(isExternalIdentity({ user: { user_type: 'external_user' } })).toBe(true);
|
||||
expect(isExternalIdentity({ user: { accountType: 'external' } })).toBe(true);
|
||||
expect(isExternalIdentity({ audience: 'admin', user: { userType: 'employee' } })).toBe(false);
|
||||
});
|
||||
});
|
||||
23
tests/vitest/module-access.spec.ts
Normal file
23
tests/vitest/module-access.spec.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { normalizeAllowedModules } from '../../src/lib/admin/module-access';
|
||||
|
||||
describe('module access normalization (vitest)', () => {
|
||||
it('normalizes explicit enabled modules', () => {
|
||||
expect(
|
||||
normalizeAllowedModules({
|
||||
enabled_modules: ['employee_management', 'approval_management'],
|
||||
}),
|
||||
).toEqual(['EMPLOYEE_MANAGEMENT', 'APPROVAL_MANAGEMENT']);
|
||||
});
|
||||
|
||||
it('extracts module keys from permissions object', () => {
|
||||
expect(
|
||||
normalizeAllowedModules({
|
||||
permissions: {
|
||||
'departments.view': true,
|
||||
'external_dashboard_management.update': true,
|
||||
},
|
||||
}),
|
||||
).toEqual(['DEPARTMENTS', 'EXTERNAL_DASHBOARD_MANAGEMENT']);
|
||||
});
|
||||
});
|
||||
13
vitest.config.ts
Normal file
13
vitest.config.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['tests/vitest/**/*.spec.ts'],
|
||||
environment: 'node',
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'html'],
|
||||
reportsDirectory: './test-results/vitest-coverage',
|
||||
},
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue