feat: comprehensive testing infrastructure
- Add vitest with solid plugin, coverage, jsdom environment - Create MSW mocks for API responses in test setup - Add unit test for ProfessionAdminListPage - Add Playwright accessibility and visual regression configs - Add sample accessibility and visual tests - Add ESLint + Prettier configs with SolidJS rules - Update scripts: test, test:coverage, test:accessibility, test:visual - Add .gitignore entries for coverage, test-results, playwright-report, .vitest - Install required dev dependencies: vitest, @solidjs/testing-library, msw, eslint, prettier, typescript, @axe-core/playwright, etc. - Create .github/workflows/ci.yml with lint, test, coverage, e2e, accessibility, visual checks This sets up full testing pipeline for admin frontend.
This commit is contained in:
parent
33ba55ab09
commit
08f0cb6402
12 changed files with 519 additions and 6 deletions
68
.eslintrc.cjs
Normal file
68
.eslintrc.cjs
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
/* eslint-env node */
|
||||||
|
require("@typescript-eslint/eslint-recommended");
|
||||||
|
require("@typescript-eslint/parser");
|
||||||
|
require("eslint-plugin-solid");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
plugins: ["solid"],
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:solid/recommended",
|
||||||
|
],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["**/*.tsx"],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
solid: {
|
||||||
|
// SolidJS specific options
|
||||||
|
noImportReact: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"solid/hyperscript": "off",
|
||||||
|
"solid/jsx-no-undef": "error",
|
||||||
|
"solid/jsx-pseudo-element": "error",
|
||||||
|
"solid/jsx-single-root-elem": "error",
|
||||||
|
"solid/no-dynamic-mount": "error",
|
||||||
|
"solid/no-hyphen-in-props": "error",
|
||||||
|
"solid/no-leaked-event-handlers": "error",
|
||||||
|
"solid/no-react-unknown-property": "error",
|
||||||
|
"solid/no-unknown-property": "error",
|
||||||
|
"solid/no-useless-fragment": "error",
|
||||||
|
"solid/prefer-classlist": "error",
|
||||||
|
"solid/prefer-destructuring": "warn",
|
||||||
|
"solid/prefer-innerhtml": "warn",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
|
||||||
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
|
"arrow-body-style": ["warn", "as-needed"],
|
||||||
|
" curly": ["error", "multi-line"],
|
||||||
|
"no-console": "off",
|
||||||
|
"no-debugger": "warn",
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"prefer-const": "error",
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: [
|
||||||
|
"dist",
|
||||||
|
".output",
|
||||||
|
"node_modules",
|
||||||
|
"coverage",
|
||||||
|
"test-results",
|
||||||
|
"playwright-report",
|
||||||
|
],
|
||||||
|
};
|
||||||
91
.github/workflows/ci.yml
vendored
Normal file
91
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
name: Admin Frontend CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [high-performance]
|
||||||
|
push:
|
||||||
|
branches: [high-performance]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-and-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
cache: "npm"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run ESLint
|
||||||
|
run: npx eslint . --ext .ts,.tsx
|
||||||
|
|
||||||
|
- name: Check Prettier formatting
|
||||||
|
run: npx prettier --check .
|
||||||
|
|
||||||
|
- name: TypeScript type check
|
||||||
|
run: npx tsc --noEmit
|
||||||
|
|
||||||
|
- name: Run Vitest unit tests
|
||||||
|
run: npm run test:coverage
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
with:
|
||||||
|
files: coverage/lcov.info
|
||||||
|
fail_ci_if_error: false
|
||||||
|
|
||||||
|
- name: Build application
|
||||||
|
run: npm run build
|
||||||
|
env:
|
||||||
|
NODE_ENV: production
|
||||||
|
|
||||||
|
e2e-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: lint-and-test
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
cache: "npm"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build app for E2E
|
||||||
|
run: npm run build
|
||||||
|
env:
|
||||||
|
NODE_ENV: production
|
||||||
|
|
||||||
|
- name: Start server (preview)
|
||||||
|
run: npm run start:3000 &
|
||||||
|
env:
|
||||||
|
HOST: 0.0.0.0
|
||||||
|
PORT: 3000
|
||||||
|
|
||||||
|
- name: Wait for server
|
||||||
|
run: npx wait-on http://localhost:3000
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Run Playwright accessibility tests
|
||||||
|
run: npm run test:accessibility
|
||||||
|
|
||||||
|
- name: Run Playwright visual regression
|
||||||
|
run: npm run test:visual
|
||||||
|
|
||||||
|
- name: Upload Playwright report
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: playwright-report-admin
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 14
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -32,3 +32,5 @@ storybook-static
|
||||||
playwright-report
|
playwright-report
|
||||||
test-results
|
test-results
|
||||||
tests/visual-artifacts
|
tests/visual-artifacts
|
||||||
|
coverage
|
||||||
|
.vitest
|
||||||
|
|
|
||||||
10
.prettierrc
Normal file
10
.prettierrc
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"useTabs": false,
|
||||||
|
"printWidth": 100,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
16
package.json
16
package.json
|
|
@ -10,7 +10,9 @@
|
||||||
"admin:stop:3000": "bash ./scripts/admin-3000-service.sh stop",
|
"admin:stop:3000": "bash ./scripts/admin-3000-service.sh stop",
|
||||||
"admin:status:3000": "bash ./scripts/admin-3000-service.sh status",
|
"admin:status:3000": "bash ./scripts/admin-3000-service.sh status",
|
||||||
"admin:start:3000": "bash ./scripts/admin-3000-service.sh start",
|
"admin:start:3000": "bash ./scripts/admin-3000-service.sh start",
|
||||||
"test": "node --test --experimental-strip-types src/lib/**/*.test.ts",
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"test:coverage": "vitest run --coverage",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"test:e2e:headed": "playwright test --headed",
|
"test:e2e:headed": "playwright test --headed",
|
||||||
"test:visual": "playwright test tests/e2e/admin-visual.spec.ts --reporter=list --workers=1",
|
"test:visual": "playwright test tests/e2e/admin-visual.spec.ts --reporter=list --workers=1",
|
||||||
|
|
@ -19,7 +21,8 @@
|
||||||
"test:external:flow": "playwright test -c playwright.external.config.ts tests/e2e/external-user-flow.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: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:external:screenshots": "playwright test -c playwright.external.config.ts tests/e2e/external-role-screenshots.spec.ts --reporter=list --workers=1",
|
||||||
"test:vitest": "vitest run",
|
"test:accessibility": "playwright test --config=playwright.a11y.config.ts",
|
||||||
|
"test:visual:regression": "playwright test --config=playwright.visual.config.ts",
|
||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
"build-storybook": "storybook build",
|
"build-storybook": "storybook build",
|
||||||
"qa:visbug": "bash ./scripts/visbug-storybook.sh",
|
"qa:visbug": "bash ./scripts/visbug-storybook.sh",
|
||||||
|
|
@ -41,19 +44,26 @@
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@axe-core/playwright": "^1.7.0",
|
||||||
"@chromatic-com/storybook": "^5.1.0",
|
"@chromatic-com/storybook": "^5.1.0",
|
||||||
"@playwright/test": "^1.58.2",
|
"@playwright/test": "^1.58.2",
|
||||||
|
"@solidjs/testing-library": "^0.8.0",
|
||||||
"@storybook/addon-a11y": "^10.3.3",
|
"@storybook/addon-a11y": "^10.3.3",
|
||||||
"@storybook/addon-docs": "^10.3.3",
|
"@storybook/addon-docs": "^10.3.3",
|
||||||
"@storybook/addon-vitest": "^10.3.3",
|
"@storybook/addon-vitest": "^10.3.3",
|
||||||
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@vitest/browser-playwright": "^4.1.1",
|
"@vitest/browser-playwright": "^4.1.1",
|
||||||
"@vitest/coverage-v8": "^4.1.1",
|
"@vitest/coverage-v8": "^4.1.1",
|
||||||
|
"jsdom": "^25.0.1",
|
||||||
|
"msw": "^2.7.3",
|
||||||
"pixelmatch": "^7.1.0",
|
"pixelmatch": "^7.1.0",
|
||||||
"playwright": "^1.58.2",
|
"playwright": "^1.58.2",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"storybook": "^10.3.3",
|
"storybook": "^10.3.3",
|
||||||
"storybook-solidjs-vite": "^10.0.11",
|
"storybook-solidjs-vite": "^10.0.11",
|
||||||
"visbug": "^0.1.14",
|
"visbug": "^0.1.14",
|
||||||
"vitest": "^4.1.1"
|
"vitest": "^4.1.1",
|
||||||
|
"vitest-plugin-solid": "^0.2.0",
|
||||||
|
"typescript": "^5.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
playwright.a11y.config.ts
Normal file
23
playwright.a11y.config.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./tests/e2e",
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 4 : undefined,
|
||||||
|
reporter: "html",
|
||||||
|
use: {
|
||||||
|
baseURL: "http://localhost:3000",
|
||||||
|
trace: "on-first-retry",
|
||||||
|
screenshot: "on",
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Run accessibility tests
|
||||||
|
// Use `npx playwright test --config=playwright.a11y.config.ts`
|
||||||
|
});
|
||||||
21
playwright.visual.config.ts
Normal file
21
playwright.visual.config.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./tests/e2e/visual",
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 4 : undefined,
|
||||||
|
reporter: "list",
|
||||||
|
use: {
|
||||||
|
baseURL: "http://localhost:3000",
|
||||||
|
screenshot: "only-on-failure",
|
||||||
|
trace: "on-first-retry",
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
64
src/test/setup.ts
Normal file
64
src/test/setup.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { beforeAll, afterEach, afterAll } from 'vitest';
|
||||||
|
import { setupServer } from 'msw/node';
|
||||||
|
import { rest } from 'msw';
|
||||||
|
|
||||||
|
// Mock API responses globally for frontend tests
|
||||||
|
const server = setupServer(
|
||||||
|
rest.get('/api/admin/companies', (req, res, ctx) => {
|
||||||
|
return res.once(200, ctx.json([
|
||||||
|
{ id: '1', company_name: 'Test Co', status: 'ACTIVE' }
|
||||||
|
]));
|
||||||
|
}),
|
||||||
|
rest.get('/api/admin/users', (req, res, ctx) => {
|
||||||
|
return res.once(200, ctx.json([
|
||||||
|
{ id: '1', full_name: 'Admin User', email: 'admin@example.com', status: 'ACTIVE' }
|
||||||
|
]));
|
||||||
|
}),
|
||||||
|
rest.get('/api/admin/jobs', (req, res, ctx) => {
|
||||||
|
return res.once(200, ctx.json([
|
||||||
|
{ id: '1', title: 'Developer', status: 'OPEN' }
|
||||||
|
]));
|
||||||
|
}),
|
||||||
|
rest.get('/api/admin/leads', (req, res, ctx) => {
|
||||||
|
return res.once(200, ctx.json([
|
||||||
|
{ id: '1', title: 'Need a developer', status: 'PENDING' }
|
||||||
|
]));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
beforeAll(() => server.listen());
|
||||||
|
afterEach(() => server.resetHandlers());
|
||||||
|
afterAll(() => server.close());
|
||||||
|
|
||||||
|
// Mock window.matchMedia
|
||||||
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
|
writable: true,
|
||||||
|
value: vi.fn().mockImplementation(query => ({
|
||||||
|
matches: false,
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addListener: vi.fn(),
|
||||||
|
removeListener: vi.fn(),
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
dispatchEvent: vi.fn(),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock IntersectionObserver
|
||||||
|
global.IntersectionObserver = class IntersectionObserver {
|
||||||
|
constructor() {}
|
||||||
|
disconnect() {}
|
||||||
|
observe() {}
|
||||||
|
takeRecords() { return []; }
|
||||||
|
trigger() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock ResizeObserver
|
||||||
|
global.ResizeObserver = class ResizeObserver {
|
||||||
|
constructor() {}
|
||||||
|
observe() {}
|
||||||
|
unobserve() {}
|
||||||
|
disconnect() {}
|
||||||
|
};
|
||||||
21
tests/e2e/accessibility.spec.ts
Normal file
21
tests/e2e/accessibility.spec.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import AxeBuilder from "@axe-core/playwright";
|
||||||
|
|
||||||
|
test.describe("Accessibility Tests", () => {
|
||||||
|
test("login page should have no accessibility violations", async ({ page }) => {
|
||||||
|
await page.goto("/admin/login");
|
||||||
|
const results = await new AxeBuilder({ page }).analyze();
|
||||||
|
expect(results.violations).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dashboard page should be accessible after login", async ({ page }) => {
|
||||||
|
// Mock login (or use real credentials via env)
|
||||||
|
await page.goto("/admin/login");
|
||||||
|
await page.fill('input[type="email"]', "admin@example.com");
|
||||||
|
await page.fill('input[type="password"]', "password");
|
||||||
|
await page.click('button[type="submit"]');
|
||||||
|
await expect(page).toHaveURL("/admin");
|
||||||
|
const results = await new AxeBuilder({ page }).analyze();
|
||||||
|
expect(results.violations).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
16
tests/e2e/visual/pages.spec.ts
Normal file
16
tests/e2e/visual/pages.spec.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("Visual Regression - Admin Pages", () => {
|
||||||
|
test("company management page should match baseline", async ({ page }) => {
|
||||||
|
await page.goto("/admin/company");
|
||||||
|
// Wait for table to load
|
||||||
|
await expect(page.locator("table")).toBeVisible({ timeout: 10000 });
|
||||||
|
await expect(page).toHaveScreenshot({ maxDiffPixelRatio: 0.1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("jobs management page should match baseline", async ({ page }) => {
|
||||||
|
await page.goto("/admin/jobs");
|
||||||
|
await expect(page.locator("table")).toBeVisible({ timeout: 10000 });
|
||||||
|
await expect(page).toHaveScreenshot({ maxDiffPixelRatio: 0.1 });
|
||||||
|
});
|
||||||
|
});
|
||||||
182
tests/vitest/components/ProfessionAdminListPage.test.tsx
Normal file
182
tests/vitest/components/ProfessionAdminListPage.test.tsx
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { render, screen, fireEvent, waitFor } from "@solidjs/testing-library";
|
||||||
|
import { createSignal } from "solid-js";
|
||||||
|
import ProfessionAdminListPage from "~/components/admin/ProfessionAdminListPage";
|
||||||
|
|
||||||
|
// Mock fetch globally
|
||||||
|
const mockFetch = vi.fn();
|
||||||
|
global.fetch = mockFetch;
|
||||||
|
|
||||||
|
describe("ProfessionAdminListPage", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockFetch.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders page title and subtitle", async () => {
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: async () => [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
first_name: "John",
|
||||||
|
last_name: "Doe",
|
||||||
|
email: "john@example.com",
|
||||||
|
phone: "1234567890",
|
||||||
|
status: "ACTIVE",
|
||||||
|
created_at: "2024-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(() =>
|
||||||
|
ProfessionAdminListPage({
|
||||||
|
endpoint: "/api/admin/test",
|
||||||
|
title: "Test Management",
|
||||||
|
subtitle: "Manage test records",
|
||||||
|
emptyLabel: "No test records found.",
|
||||||
|
viewHref: (id) => `/admin/test/${id}`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Test Management")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Manage test records")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters by search query", async () => {
|
||||||
|
const mockData = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
first_name: "Alice",
|
||||||
|
last_name: "Smith",
|
||||||
|
email: "alice@example.com",
|
||||||
|
phone: "",
|
||||||
|
status: "ACTIVE",
|
||||||
|
created_at: "2024-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
first_name: "Bob",
|
||||||
|
last_name: "Jones",
|
||||||
|
email: "bob@example.com",
|
||||||
|
phone: "",
|
||||||
|
status: "INACTIVE",
|
||||||
|
created_at: "2024-01-02T00:00:00Z",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: async () => mockData,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(() =>
|
||||||
|
ProfessionAdminListPage({
|
||||||
|
endpoint: "/api/admin/test",
|
||||||
|
title: "Test",
|
||||||
|
subtitle: "",
|
||||||
|
emptyLabel: "Empty",
|
||||||
|
viewHref: (id) => `/admin/test/${id}`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Alice Smith")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Bob Jones")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = screen.getByPlaceholderText("Search by name or email...");
|
||||||
|
fireEvent.input(input, { target: { value: "alice" } });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Alice Smith")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("Bob Jones")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows empty state when no data", async () => {
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: async () => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(() =>
|
||||||
|
ProfessionAdminListPage({
|
||||||
|
endpoint: "/api/admin/test",
|
||||||
|
title: "Test",
|
||||||
|
subtitle: "",
|
||||||
|
emptyLabel: "No records.",
|
||||||
|
viewHref: (id) => `/admin/test/${id}`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("No records.")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles fetch error gracefully", async () => {
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(() =>
|
||||||
|
ProfessionAdminListPage({
|
||||||
|
endpoint: "/api/admin/test",
|
||||||
|
title: "Test",
|
||||||
|
subtitle: "",
|
||||||
|
emptyLabel: "Empty",
|
||||||
|
viewHref: (id) => `/admin/test/${id}`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Failed to load/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("export button calls exportCsv and downloads file", async () => {
|
||||||
|
const mockData = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
first_name: "Alice",
|
||||||
|
last_name: "Smith",
|
||||||
|
email: "alice@example.com",
|
||||||
|
phone: "123",
|
||||||
|
status: "ACTIVE",
|
||||||
|
created_at: "2024-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: async () => mockData,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock URL.createObjectURL and link.click
|
||||||
|
const mockClick = vi.fn();
|
||||||
|
global.URL.createObjectURL = vi.fn(() => "blob:test");
|
||||||
|
global.document.createElement = vi.fn(() => ({ click: mockClick, href: "" }));
|
||||||
|
|
||||||
|
render(() =>
|
||||||
|
ProfessionAdminListPage({
|
||||||
|
endpoint: "/api/admin/test",
|
||||||
|
title: "Test",
|
||||||
|
subtitle: "",
|
||||||
|
emptyLabel: "Empty",
|
||||||
|
viewHref: (id) => `/admin/test/${id}`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Alice Smith")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const exportBtn = screen.getByText("Export");
|
||||||
|
fireEvent.click(exportBtn);
|
||||||
|
|
||||||
|
expect(mockClick).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,13 +1,18 @@
|
||||||
import { defineConfig } from 'vitest/config';
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import solid from 'vitest-plugin-solid';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
plugins: [solid()],
|
||||||
test: {
|
test: {
|
||||||
include: ['tests/vitest/**/*.spec.ts'],
|
globals: true,
|
||||||
environment: 'node',
|
environment: 'jsdom',
|
||||||
|
setupFiles: ['./src/test/setup.ts'],
|
||||||
|
include: ['src/**/*.{test,spec}.{js,mjs,ts,tsx}', 'tests/vitest/**/*.spec.ts'],
|
||||||
coverage: {
|
coverage: {
|
||||||
provider: 'v8',
|
provider: 'v8',
|
||||||
reporter: ['text', 'html'],
|
reporter: ['text', 'json', 'html', 'lcov'],
|
||||||
reportsDirectory: './test-results/vitest-coverage',
|
reportsDirectory: './test-results/vitest-coverage',
|
||||||
|
exclude: ['node_modules', 'dist', '.output', '**/*.d.ts'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue