diff --git a/.github/workflows/client-e2e.yml b/.github/workflows/client-e2e.yml new file mode 100644 index 0000000..ea9d370 --- /dev/null +++ b/.github/workflows/client-e2e.yml @@ -0,0 +1,91 @@ +# =============================================================== +# React Client E2E - Playwright (TypeScript) smoke suite +# =============================================================== +# - runs TypeScript Playwright tests for the React admin UI +# - Vite dev server is spawned by Playwright's webServer config +# - backend API is stubbed with page.route() (no gateway required) +# - triggered on PRs and pushes to main +# --------------------------------------------------------------- + +name: Client E2E (Playwright) + +on: + push: + branches: ["main"] + pull_request: + types: [opened, synchronize, ready_for_review] + branches: ["main"] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + playwright: + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + name: Playwright smoke + auth + runs-on: ubuntu-latest + timeout-minutes: 30 + + env: + CI: "true" + PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.playwright-browsers + + steps: + - name: โฌ‡๏ธ Checkout source + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + with: + persist-credentials: false + fetch-depth: 1 + + - name: ๐ŸŸฉ Setup Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "22" + + - name: ๐Ÿ“ฅ Install dependencies + run: npm ci --no-audit --no-fund + + - name: โš™๏ธ Generate types with orval + run: npm run generate + + - name: ๐ŸŽญ Cache Playwright browsers + id: playwright-cache + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} + key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-playwright- + + - name: ๐ŸŽญ Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: npx playwright install --with-deps chromium + + - name: ๐ŸŽญ Install Playwright system deps (cached browsers) + if: steps.playwright-cache.outputs.cache-hit == 'true' + run: npx playwright install-deps chromium + + - name: ๐Ÿงช Run Playwright tests + run: npm run e2e + + - name: ๐Ÿ“ฆ Upload Playwright HTML report + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: playwright-report + path: playwright-report + retention-days: 7 + if-no-files-found: ignore + + - name: ๐Ÿ“ฆ Upload Playwright traces on failure + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: playwright-traces + path: test-results + retention-days: 7 + if-no-files-found: ignore diff --git a/.github/workflows/client-generate.yml b/.github/workflows/client-generate.yml new file mode 100644 index 0000000..755a744 --- /dev/null +++ b/.github/workflows/client-generate.yml @@ -0,0 +1,52 @@ +# =============================================================== +# ๐Ÿงฌ Client Type Generation - verify generated types stay in sync +# =============================================================== +# - regenerates src/generated/ from the committed openapi.json +# - verifies types exist and compile +# --------------------------------------------------------------- + +name: Client Type Generation + +on: + pull_request: + branches: ["main"] + types: [opened, synchronize, reopened, ready_for_review] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + generate-types: + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + name: Generate and verify types + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + with: + persist-credentials: false + + - name: Setup Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "22" + + - name: Install dependencies + run: npm ci --no-audit --no-fund + + - name: Generate types with orval + run: npm run generate + + - name: Verify generated types exist + run: | + test -d src/generated/types || (echo "ERROR: src/generated/types/ not created" && exit 1) + echo "Generated types count: $(find src/generated/types -name '*.ts' | wc -l)" + + - name: Verify generated types compile + run: npx tsc -b --noEmit diff --git a/.github/workflows/client-lint-test.yml b/.github/workflows/client-lint-test.yml new file mode 100644 index 0000000..cfef531 --- /dev/null +++ b/.github/workflows/client-lint-test.yml @@ -0,0 +1,57 @@ +# =============================================================== +# โš›๏ธ Client Lint & Test - React Frontend Quality Gate +# =============================================================== +# - runs ESLint, Prettier, and Vitest for the React client +# - ensures code quality and test coverage for frontend +# - runs on PRs and pushes to main +# --------------------------------------------------------------- + +name: Client Lint & Test + +on: + push: + branches: ["main"] + pull_request: + types: [opened, synchronize, ready_for_review] + branches: ["main"] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + lint-and-test: + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + name: Lint & Test Client + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: โฌ‡๏ธ Checkout source + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + with: + persist-credentials: false + fetch-depth: 1 + + - name: ๐ŸŸฉ Setup Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "22" + + - name: ๐Ÿ“ฅ Install dependencies + run: npm ci --no-audit --no-fund + + - name: โš™๏ธ Generate types with orval + run: npm run generate + + - name: ๐ŸŽจ Check code formatting with Prettier + run: npm run format:check + + - name: ๐Ÿ” Lint code with ESLint + run: npm run lint + + - name: ๐Ÿงช Run tests with Vitest + run: npm run test:coverage diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..af02b32 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,27 @@ +# Build outputs +dist +build + +# Dependencies +node_modules + +# Generated files +*.min.js +*.min.css + +# Lock files +package-lock.json +pnpm-lock.yaml +yarn.lock + +# Coverage +coverage +.nyc_output + +# Logs +*.log + +# Playwright +playwright-report +test-results +.playwright/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..1a2c425 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": false, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/src/auth/AuthContext.test.tsx b/src/auth/AuthContext.test.tsx index f4ff457..3660424 100644 --- a/src/auth/AuthContext.test.tsx +++ b/src/auth/AuthContext.test.tsx @@ -258,7 +258,7 @@ describe("AuthContext", () => { email_verified: true, password_change_required: false, }; - vi.mocked(api.get).mockResolvedValueOnce(mockUser); + vi.mocked(api.get).mockResolvedValueOnce({ authenticated: true, user: mockUser }); // Hold the permissions fetch open so we can observe the loading window. let resolvePerms!: (perms: string[]) => void;