Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f5659c4
fix(plugins): skip built-in plugins in getDataFromProviders
MuhammadKhalilzadeh Feb 24, 2026
0adb816
fix(plugins): add guard for built-in plugins in downloadAndLoadPlugin
MuhammadKhalilzadeh Feb 24, 2026
bce96b8
fix(plugins): use concise error logging instead of dumping full Axios…
MuhammadKhalilzadeh Feb 24, 2026
6243ae7
test(setup): add jsdom environment stubs for MUI and React components
MuhammadKhalilzadeh Feb 24, 2026
3197bc7
test(setup): add renderWithProviders test utility
MuhammadKhalilzadeh Feb 24, 2026
527bc62
test(vite): exclude e2e tests and configure test environment
MuhammadKhalilzadeh Feb 24, 2026
c58db69
test(Alert): add unit tests for Alert component
MuhammadKhalilzadeh Feb 24, 2026
a7812eb
test(Checkbox): add unit tests for Checkbox component
MuhammadKhalilzadeh Feb 24, 2026
95f84e5
test(Field): add unit tests for Field component
MuhammadKhalilzadeh Feb 24, 2026
b6572e7
test(ProtectedRoute): add unit tests for ProtectedRoute component
MuhammadKhalilzadeh Feb 24, 2026
439f1e6
test(Login): add unit tests for Login page
MuhammadKhalilzadeh Feb 24, 2026
fa0d8b4
chore(deps): add Playwright, axe-core, and testing-library/dom
MuhammadKhalilzadeh Feb 24, 2026
3c05666
chore(gitignore): add Playwright artifact ignore patterns
MuhammadKhalilzadeh Feb 24, 2026
8a5077f
test(e2e): add Playwright config with project dependencies
MuhammadKhalilzadeh Feb 24, 2026
e715039
test(e2e): add global auth setup and authenticated page fixture
MuhammadKhalilzadeh Feb 24, 2026
cf78ddc
test(e2e): add authentication spec
MuhammadKhalilzadeh Feb 24, 2026
8b9dd2d
test(e2e): add navigation spec
MuhammadKhalilzadeh Feb 24, 2026
4936bc9
test(e2e): add vendors spec
MuhammadKhalilzadeh Feb 24, 2026
7cacc85
test(seed): add backend test data seed script
MuhammadKhalilzadeh Feb 24, 2026
43c398f
ci(frontend): add unit and component test job to frontend-checks
MuhammadKhalilzadeh Feb 24, 2026
bbb2852
ci(e2e): add end-to-end test workflow
MuhammadKhalilzadeh Feb 24, 2026
52c9fd0
fix(e2e): reuse existing server to avoid port conflict in CI
MuhammadKhalilzadeh Feb 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
name: E2E Tests

permissions:
contents: read

on:
# Run manually or on specific branches/paths
workflow_dispatch:
pull_request:
branches: ['master', 'develop']
paths:
- 'Clients/**'
- 'Servers/**'

jobs:
e2e-tests:
name: Playwright E2E Tests
runs-on: ubuntu-latest
timeout-minutes: 20

services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: verifywise_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

redis:
image: redis:7
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- uses: actions/checkout@v6

- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: '20'

# ---- Backend setup ----
- name: Install backend dependencies
working-directory: Servers
run: npm ci --legacy-peer-deps

- name: Build backend
working-directory: Servers
run: npm run build

- name: Run migrations
working-directory: Servers
env:
DB_HOST: localhost
DB_PORT: 5432
DB_NAME: verifywise_test
DB_USER: postgres
DB_PASSWORD: postgres
run: npx sequelize db:migrate

- name: Start backend
working-directory: Servers
env:
PORT: 3000
NODE_ENV: test
DB_HOST: localhost
DB_PORT: 5432
DB_NAME: verifywise_test
DB_USER: postgres
DB_PASSWORD: postgres
REDIS_HOST: localhost
REDIS_PORT: 6379
JWT_SECRET: test-jwt-secret-for-e2e
REFRESH_TOKEN_SECRET: test-refresh-secret-for-e2e
ENCRYPTION_KEY: test-encryption-key-32chars!!
MULTI_TENANCY_ENABLED: false
run: |
node dist/index.js &
# Wait for backend to be ready
for i in $(seq 1 30); do
curl -s http://localhost:3000/api/users/check/exists && break
sleep 1
done

# ---- Frontend setup ----
- name: Install frontend dependencies
working-directory: Clients
run: npm ci --legacy-peer-deps

- name: Install Playwright browsers
working-directory: Clients
run: npx playwright install --with-deps chromium

- name: Start frontend
working-directory: Clients
env:
VITE_APP_API_BASE_URL: http://localhost:3000
run: |
npm run dev:vite &
# Wait for frontend to be ready
for i in $(seq 1 30); do
curl -s http://localhost:5173 && break
sleep 1
done

# ---- Run E2E tests ----
- name: Run Playwright tests
working-directory: Clients
env:
E2E_BASE_URL: http://localhost:5173
run: npx playwright test

- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v6
with:
name: playwright-report
path: Clients/playwright-report/
retention-days: 14

- name: Upload test results
if: always()
uses: actions/upload-artifact@v6
with:
name: playwright-results
path: Clients/test-results/
retention-days: 7
29 changes: 29 additions & 0 deletions .github/workflows/frontend-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,35 @@ jobs:
path: audit-results.json
retention-days: 30

unit-and-component-tests:
name: Unit & Component Tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: Clients

steps:
- uses: actions/checkout@v6

- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: '20'

- name: Install dependencies
run: npm ci --legacy-peer-deps

- name: Run tests with coverage
run: npx vitest run --coverage

- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v6
with:
name: frontend-coverage-report
path: Clients/coverage/
retention-days: 14

dependency-review:
name: Dependency Review
runs-on: ubuntu-latest
Expand Down
9 changes: 8 additions & 1 deletion Clients/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,11 @@ dist-ssr
tsconfig.app.tsbuildinfo
tsconfig.node.tsbuildinfo

coverage/
coverage/

# Playwright
test-results/
playwright-report/
blob-report/
playwright/.cache/
e2e/.auth/
62 changes: 62 additions & 0 deletions Clients/e2e/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { test, expect } from "@playwright/test";

const TEST_EMAIL = process.env.E2E_EMAIL || "verifywise@email.com";
const TEST_PASSWORD = process.env.E2E_PASSWORD || "Verifywise#1";

test.describe("Authentication", () => {
test("login page renders correctly", async ({ page }) => {
await page.goto("/login");

await expect(page.getByText("Log in to your account")).toBeVisible();
await expect(
page.getByPlaceholder("name.surname@companyname.com")
).toBeVisible();
await expect(page.getByPlaceholder("Enter your password")).toBeVisible();
await expect(page.getByRole("button", { name: /sign in/i })).toBeVisible();
});

test("redirects to /login when accessing protected route without auth", async ({
page,
}) => {
await page.goto("/vendors");
await expect(page).toHaveURL(/\/login/);
});

test("successful login redirects to dashboard", async ({ page }) => {
await page.goto("/login");

await page
.getByPlaceholder("name.surname@companyname.com")
.fill(TEST_EMAIL);
await page.getByPlaceholder("Enter your password").fill(TEST_PASSWORD);
await page.getByRole("button", { name: /sign in/i }).click();

// Should redirect to the dashboard
await expect(page).toHaveURL("/", { timeout: 15_000 });
});

test("shows error on invalid credentials", async ({ page }) => {
await page.goto("/login");

await page
.getByPlaceholder("name.surname@companyname.com")
.fill("bad@email.com");
await page.getByPlaceholder("Enter your password").fill("wrongpassword");
await page.getByRole("button", { name: /sign in/i }).click();

// An error alert should appear
await expect(page.getByRole("alert")).toBeVisible({ timeout: 10_000 });
});

test("forgot password link navigates correctly", async ({ page }) => {
await page.goto("/login");
await page.getByText("Forgot password").click();
await expect(page).toHaveURL(/\/forgot-password/);
});

test("register link navigates correctly", async ({ page }) => {
await page.goto("/login");
await page.getByText("Register here").click();
await expect(page).toHaveURL(/\/register/);
});
});
27 changes: 27 additions & 0 deletions Clients/e2e/fixtures/auth.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { test as base, expect, type Page } from "@playwright/test";

/**
* Extended test fixture that provides an already-authenticated page.
*
* Authentication state is injected via Playwright's storageState in
* playwright.config.ts (set by e2e/global.setup.ts). The `authedPage`
* fixture simply verifies the page is authenticated and ready to use.
*
* Usage:
* import { test, expect } from "../fixtures/auth.fixture";
* test("my test", async ({ authedPage }) => { ... });
*/
export const test = base.extend<{ authedPage: Page }>({
authedPage: async ({ page }, use) => {
// storageState is already loaded by Playwright config.
// Navigate to the dashboard to trigger Redux rehydration.
await page.goto("/");

// Wait for the page to finish loading (not stuck on /login)
await expect(page).not.toHaveURL(/\/login/, { timeout: 15_000 });

await use(page);
},
});

export { expect };
31 changes: 31 additions & 0 deletions Clients/e2e/global.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { test as setup, expect } from "@playwright/test";

/**
* Global setup: logs in once via the real UI and saves browser storage state.
* All tests that need authentication reuse this state instead of
* logging in through the UI every time.
*
* This uses the actual login flow (not localStorage injection) because
* the app's version-based cache invalidation in store.ts wipes any
* manually-set persist:* keys on page load if the version doesn't match.
*/

const TEST_EMAIL = process.env.E2E_EMAIL || "verifywise@email.com";
const TEST_PASSWORD = process.env.E2E_PASSWORD || "Verifywise#1";
const AUTH_STATE_PATH = "e2e/.auth/user.json";

setup("authenticate", async ({ page }) => {
// 1. Login via the real UI
await page.goto("/login");
await page
.getByPlaceholder("name.surname@companyname.com")
.fill(TEST_EMAIL);
await page.getByPlaceholder("Enter your password").fill(TEST_PASSWORD);
await page.getByRole("button", { name: /sign in/i }).click();

// 2. Wait for redirect to dashboard (confirms login succeeded)
await expect(page).toHaveURL("/", { timeout: 15_000 });

// 3. Save storage state (localStorage + cookies) for reuse by other tests
await page.context().storageState({ path: AUTH_STATE_PATH });
});
53 changes: 53 additions & 0 deletions Clients/e2e/navigation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { test, expect } from "./fixtures/auth.fixture";

test.describe("Navigation", () => {
test("dashboard loads after login", async ({ authedPage: page }) => {
// The dashboard should be visible after login
await expect(page.locator("body")).not.toBeEmpty();
await expect(page).toHaveURL("/");
});

test("can navigate to vendors page", async ({ authedPage: page }) => {
await page.goto("/vendors");
await expect(page).toHaveURL(/\/vendors/);
// Should see page content (not redirected to login)
await expect(page.locator("body")).not.toBeEmpty();
});

test("can navigate to risk management page", async ({ authedPage: page }) => {
await page.goto("/risk-management");
await expect(page).toHaveURL(/\/risk-management/);
});

test("can navigate to tasks page", async ({ authedPage: page }) => {
await page.goto("/tasks");
await expect(page).toHaveURL(/\/tasks/);
});

test("can navigate to model inventory page", async ({
authedPage: page,
}) => {
await page.goto("/model-inventory");
await expect(page).toHaveURL(/\/model-inventory/);
});

test("can navigate to policies page", async ({ authedPage: page }) => {
await page.goto("/policies");
await expect(page).toHaveURL(/\/policies/);
});

test("can navigate to file manager page", async ({ authedPage: page }) => {
await page.goto("/file-manager");
await expect(page).toHaveURL(/\/file-manager/);
});

test("can navigate to settings page", async ({ authedPage: page }) => {
await page.goto("/settings");
await expect(page).toHaveURL(/\/settings/);
});

test("can navigate to training page", async ({ authedPage: page }) => {
await page.goto("/training");
await expect(page).toHaveURL(/\/training/);
});
});
Loading
Loading