diff --git a/.gitignore b/.gitignore index 9bcb369..42935ab 100644 --- a/.gitignore +++ b/.gitignore @@ -132,12 +132,4 @@ dist .pnp.* -type-clusters/* - -# ---> Playwright (root E2E suite) -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ -tests/.auth/ -tests/.env.test \ No newline at end of file +type-clusters/* \ No newline at end of file diff --git a/apps/admin-web/package.json b/apps/admin-web/package.json index 13ba458..7802a7a 100644 --- a/apps/admin-web/package.json +++ b/apps/admin-web/package.json @@ -6,7 +6,6 @@ "dev": "vite dev", "build": "vite build", "preview": "npm run build && wrangler dev", - "test": "vitest run", "typecheck": "tsc --noEmit", "deploy": "npm run build && wrangler deploy", "cf-typegen": "wrangler types" diff --git a/apps/backend/package.json b/apps/backend/package.json index d89bbcf..d94b4e9 100755 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -3,7 +3,6 @@ "version": "1.0.0", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", "build": "rimraf ./dist && tsc --project tsconfig.json && tsc-alias -p tsconfig.json", "build2": "rimraf ./dist && tsc", "db:seed": "tsx src/db/seed.ts", diff --git a/apps/web-ui/README.md b/apps/web-ui/README.md index af18c1d..7a2b648 100644 --- a/apps/web-ui/README.md +++ b/apps/web-ui/README.md @@ -17,14 +17,6 @@ To build this application for production: bun --bun run build ``` -## Testing - -This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with: - -```bash -bun --bun run test -``` - ## Styling This project uses [Tailwind CSS](https://tailwindcss.com/) for styling. diff --git a/apps/web-ui/package.json b/apps/web-ui/package.json index 5439052..170f790 100644 --- a/apps/web-ui/package.json +++ b/apps/web-ui/package.json @@ -6,7 +6,6 @@ "dev": "vite dev", "build": "vite build", "preview": "npm run build && wrangler dev", - "test": "vitest run", "typecheck": "tsc --noEmit", "deploy": "npm run build && wrangler deploy", "cf-typegen": "wrangler types" diff --git a/package.json b/package.json index 5d930ee..64b4d6d 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,6 @@ "dev": "turbo run dev --parallel", "lint": "turbo run lint", "typecheck": "bash typecheck", - "test": "turbo run test", - "test:e2e": "playwright test", - "test:e2e:headed": "playwright test --headed", - "test:e2e:ui": "playwright test --ui", "web-ui": "bun run --filter web-ui", "web-ui:dev": "bun run web-ui dev", "web-ui:build": "bun run web-ui build", diff --git a/playwright.config.ts b/playwright.config.ts deleted file mode 100644 index 674dcc1..0000000 --- a/playwright.config.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { defineConfig, devices } from '@playwright/test' -import { loadTestEnv } from './tests/helpers/env' - -loadTestEnv() - -const baseURL = process.env.TEST_BASE_URL || 'http://localhost:4175' -const authStatePath = 'tests/.auth/staff.json' - -export default defineConfig({ - testDir: './tests', - // Long, dependent workflows run sequentially in a single worker. - fullyParallel: false, - workers: 1, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 1 : 0, - // Generous budget for multi-step admin workflows. - timeout: 120_000, - expect: { timeout: 15_000 }, - reporter: [['list'], ['html', { open: 'never' }]], - use: { - baseURL, - // Show the browser by default so runs are watchable; CI stays headless. - // Override with `HEADED=0` to force headless, or `HEADED=1` to force headed. - headless: process.env.HEADED === '1' ? false : process.env.HEADED === '0' ? true : !!process.env.CI, - trace: 'retain-on-failure', - screenshot: 'only-on-failure', - video: 'retain-on-failure', - actionTimeout: 20_000, - navigationTimeout: 45_000, - }, - projects: [ - { - name: 'setup', - testMatch: /auth\.setup\.ts/, - use: { ...devices['Desktop Chrome'] }, - }, - { - name: 'chromium', - testMatch: /.*\.spec\.ts/, - dependencies: ['setup'], - use: { ...devices['Desktop Chrome'], storageState: authStatePath }, - }, - ], -}) diff --git a/tests/.env.test.example b/tests/.env.test.example deleted file mode 100644 index c2c6319..0000000 --- a/tests/.env.test.example +++ /dev/null @@ -1,11 +0,0 @@ -# Copy this file to `tests/.env.test` and fill in the values. -# `tests/.env.test` is gitignored. - -# Base URL of the running admin-web instance. -# Defaults to http://localhost:4175 (admin-web's Vite port) when unset. -TEST_BASE_URL=http://localhost:4175 - -# Staff credentials used to log into the admin panel. -# The suite skips (with a clear message) when either is empty. -TEST_STAFF_NAME= -TEST_STAFF_PASSWORD= diff --git a/tests/README.md b/tests/README.md deleted file mode 100644 index ce06029..0000000 --- a/tests/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# admin-web E2E tests (Playwright) - -End-to-end tests that drive the admin panel (`apps/admin-web`) through real -browser workflows. Long, multi-step tasks run sequentially in a **single -browser instance** (one worker, one page), mirroring how an admin works. - -## Setup - -1. Install Playwright + the browser (one time, from the repo root): - - ```bash - npm i -D @playwright/test - npx playwright install chromium - ``` - -2. Start the admin panel and note its URL (defaults to `http://localhost:4175`): - - ```bash - npm run admin-web:dev - ``` - -3. Configure credentials — copy the template and fill it in: - - ```bash - cp tests/.env.test.example tests/.env.test - # then set TEST_STAFF_NAME / TEST_STAFF_PASSWORD (and TEST_BASE_URL if needed) - ``` - - `tests/.env.test` is gitignored. When credentials are missing the whole - suite **skips** with a clear message instead of failing. - -## Running - -Run from the **repo root** (recommended — picks up `playwright.config.ts`): - -```bash -bun run test:e2e # all specs (browser visible locally) -bun run test:e2e:headed # explicit headed run -bun run test:e2e:ui # Playwright UI mode (debug/inspect, time-travel) -bun run test:e2e tests/specs/product-lifecycle.spec.ts # one workflow -``` - -Run from inside `tests/` — you must point at the root config, otherwise -Playwright finds no config and skips the login setup: - -```bash -cd tests -bun run --cwd .. test:e2e # easiest -bunx playwright test -c ../playwright.config.ts -bunx playwright test -c ../playwright.config.ts --headed -bunx playwright show-report ../playwright-report -``` - -> ⚠️ Don't run a bare `bunx playwright test` from `tests/` — it runs without the -> config (no auth setup, no `baseURL`). Always pass `-c ../playwright.config.ts`. - -The browser is **visible by default** locally (headed). It only runs headless in -CI, or when you opt in explicitly: - -```bash -HEADED=0 bun run test:e2e # force headless locally -HEADED=1 bun run test:e2e # force headed -bunx playwright test -c ../playwright.config.ts --headed --slow-mo=500 # watch slowly -``` - -Point the tests at a different UI URL (e.g. a hosted environment): - -```bash -TEST_BASE_URL=https://admin.example.com bun run test:e2e -``` - -## How it works - -- `auth.setup.ts` logs in once (`data-testid` login fields) and saves the - session to `tests/.auth/staff.json`. The `chromium` project reuses that - `storageState`, so specs start already authenticated. -- `tests/helpers/admin-app.ts` is a small page object with the reusable - actions (add/update product, create slot, suspend SKU, dialog handling). -- Specs live in `tests/specs/`. `product-lifecycle.spec.ts` is the long - single-instance workflow: **create product → update it → create a slot for it - → suspend it**, plus a persistence re-check. - -## ⚠️ Warning - -These tests **create real catalog records** (products, slots) on whatever -backend the running admin-web instance points at. Use a non-production backend -when running them. - -## Adding more workflows - -Add a sibling spec under `tests/specs/` (matching `*.spec.ts`) and reuse the -helpers in `tests/helpers/admin-app.ts`. Prefer `data-testid` selectors for new -UI; the admin-web components expose test ids where placeholder/role locators -would be brittle. diff --git a/tests/auth.setup.ts b/tests/auth.setup.ts deleted file mode 100644 index e1bf261..0000000 --- a/tests/auth.setup.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { test as setup, expect } from '@playwright/test' -import { CREDENTIALS_HINT, STAFF_NAME, STAFF_PASSWORD, hasStaffCredentials, login } from './helpers/admin-app' - -const AUTH_STATE_PATH = 'tests/.auth/staff.json' - -/** - * Logs into the admin panel once and persists the session (JWT lives in - * localStorage) so every spec runs already authenticated. - */ -setup('authenticate staff user', async ({ page }) => { - if (!hasStaffCredentials()) { - setup.skip(true, CREDENTIALS_HINT) - } - - await login(page, STAFF_NAME, STAFF_PASSWORD) - // Sidebar entry (exact) — the dashboard body also has "Products …" tiles. - await expect(page.getByRole('button', { name: 'Products', exact: true })).toBeVisible() - - await page.context().storageState({ path: AUTH_STATE_PATH }) -}) diff --git a/tests/helpers/admin-app.ts b/tests/helpers/admin-app.ts deleted file mode 100644 index 69cb359..0000000 --- a/tests/helpers/admin-app.ts +++ /dev/null @@ -1,295 +0,0 @@ -import { expect, type Locator, type Page } from '@playwright/test' -import { loadTestEnv } from './env' - -// Populate process.env from tests/.env.test as soon as this module loads, so -// specs can branch on credentials at module scope. -loadTestEnv() - -export const STAFF_NAME = process.env.TEST_STAFF_NAME ?? '' -export const STAFF_PASSWORD = process.env.TEST_STAFF_PASSWORD ?? '' - -export const hasStaffCredentials = (): boolean => - Boolean(process.env.TEST_STAFF_NAME && process.env.TEST_STAFF_PASSWORD) - -export const CREDENTIALS_HINT = - 'Set TEST_STAFF_NAME and TEST_STAFF_PASSWORD in tests/.env.test (see tests/.env.test.example)' - -/** Collected `window.alert`/`confirm` messages, auto-accepted as they appear. */ -export type DialogLog = string[] - -/** - * Auto-accept every dialog (the admin forms confirm success via window.alert) - * and record the messages so tests can assert on them. - */ -export function trackDialogs(page: Page): DialogLog { - const messages: DialogLog = [] - page.on('dialog', async (dialog) => { - messages.push(dialog.message()) - await dialog.accept() - }) - return messages -} - -/** Wait until a recorded dialog message contains `substring`. */ -export async function expectDialog(messages: DialogLog, substring: string): Promise { - await expect - .poll(() => messages.some((message) => message.includes(substring)), { - message: `Expected a dialog containing "${substring}". Recorded: ${JSON.stringify(messages)}`, - }) - .toBe(true) -} - -// --------------------------------------------------------------------------- -// Hydration-safe interactions -// --------------------------------------------------------------------------- -// -// These pages are SSR'd and React-controlled. Interacting before hydration -// completes silently fails: a `fill()` sets the DOM value and its assertion -// passes (the DOM really does hold the value), but React — not yet attached — -// never records it, and hydration then resets the controlled input to its -// empty state. So we must WAIT FOR HYDRATION, not just for the value. -// -// React attaches `__reactFiber$…` / `__reactProps$…` markers to elements when it -// adopts them during hydration — the reliable, framework-level signal. -export async function waitForHydration(page: Page, selector: string): Promise { - await page.waitForFunction( - (sel) => { - const el = document.querySelector(sel) - if (!el) return false - return Object.keys(el).some( - (key) => key.startsWith('__reactFiber$') || key.startsWith('__reactProps$'), - ) - }, - selector, - { timeout: 20_000 }, - ) -} - -/** Wait for the app container to be hydrated (for pages with no stable test id yet). */ -export async function waitForAppHydration(page: Page): Promise { - await page.waitForFunction( - () => { - const el = document.querySelector('#app') || document.body - return Object.keys(el).some( - (key) => key.startsWith('__reactContainer$') || key.startsWith('__reactFiber$'), - ) - }, - undefined, - { timeout: 20_000 }, - ) -} - -export async function fillStable(locator: Locator, value: string): Promise { - await expect(async () => { - await locator.fill(value) - await expect(locator).toHaveValue(value) - }).toPass({ timeout: 15_000 }) -} - -export async function selectStable(locator: Locator, value: string): Promise { - await expect(async () => { - await locator.selectOption(value) - await expect(locator).toHaveValue(value) - }).toPass({ timeout: 15_000 }) -} - -/** - * Click a control that should navigate, retrying until the URL matches. - * Guards against clicks that land before the element's handler is hydrated - * (which are silently swallowed rather than erroring). - */ -export async function clickUntilUrl(page: Page, locator: Locator, url: RegExp): Promise { - await expect(async () => { - await locator.click() - await page.waitForURL(url, { timeout: 3_000 }) - }).toPass({ timeout: 20_000 }) -} - -/** - * Like {@link clickUntilUrl} but dispatches the click straight at the element. - * Needed for fixed-position controls the TanStack devtools overlay sits on top - * of (e.g. the bottom-right slots FAB) — a normal click would be intercepted. - */ -export async function dispatchClickUntilUrl(page: Page, locator: Locator, url: RegExp): Promise { - await expect(async () => { - await locator.dispatchEvent('click') - await page.waitForURL(url, { timeout: 3_000 }) - }).toPass({ timeout: 20_000 }) -} - -// --------------------------------------------------------------------------- -// Auth -// --------------------------------------------------------------------------- - -export async function login(page: Page, name: string, password: string): Promise { - await page.goto('/login') - await waitForHydration(page, '[data-testid="login-button"]') - - await fillStable(page.getByTestId('login-name-input'), name) - await fillStable(page.getByTestId('login-password-input'), password) - - await page.getByTestId('login-button').click() - await expect(page).toHaveURL(/\/dashboard/) -} - -// --------------------------------------------------------------------------- -// Products -// --------------------------------------------------------------------------- - -export interface NewProductInput { - name: string - price: number - marketPrice?: number - quantity: string -} - -export async function openAddProduct(page: Page): Promise { - await page.goto('/dashboard/products') - await waitForAppHydration(page) - await clickUntilUrl( - page, - page.getByRole('button', { name: 'Add Product' }), - /\/dashboard\/products\/new/, - ) - await waitForHydration(page, '[data-testid="product-submit-button"]') - await expect(page.getByPlaceholder('Enter product name')).toBeVisible() -} - -export async function fillNewProduct( - page: Page, - { name, price, marketPrice, quantity }: NewProductInput, -): Promise { - await fillStable(page.getByPlaceholder('Enter product name'), name) - - // Store is a native