From aad1e1f2ddb0fc779184474efbfeaedb0622e210 Mon Sep 17 00:00:00 2001 From: EdwardSage Date: Sat, 19 Sep 2026 11:58:18 +0800 Subject: [PATCH 1/4] Allow Playwright to target production --- playwright.config.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index a612808..efc95c6 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,14 +1,19 @@ import { defineConfig, devices } from '@playwright/test'; +const externalBaseURL = process.env.PLAYWRIGHT_BASE_URL?.trim(); +const localBaseURL = 'http://127.0.0.1:4173'; + export default defineConfig({ testDir: './tests/e2e', fullyParallel: true, reporter: 'html', - use: { baseURL: 'http://127.0.0.1:4173', trace: 'on-first-retry' }, - webServer: { - command: 'npm run dev -- --host 127.0.0.1 --port 4173', - url: 'http://127.0.0.1:4173', - reuseExistingServer: !process.env.CI, - }, + use: { baseURL: externalBaseURL || localBaseURL, trace: 'on-first-retry' }, + webServer: externalBaseURL + ? undefined + : { + command: 'npm run dev -- --host 127.0.0.1 --port 4173', + url: localBaseURL, + reuseExistingServer: !process.env.CI, + }, projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], }); From 36ceff9e2d6d02f26d82840d8e631ad85727e808 Mon Sep 17 00:00:00 2001 From: EdwardSage Date: Sat, 19 Sep 2026 11:58:20 +0800 Subject: [PATCH 2/4] Add production smoke gate --- .github/workflows/ci.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f678086..5c6c5c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,3 +35,31 @@ jobs: - run: npm ci --no-audit --no-fund - run: npx playwright install --with-deps chromium - run: npm run test:e2e + + + production-smoke: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + needs: [verify, browser-smoke] + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 22 + cache: npm + - run: npm ci --no-audit --no-fund + - run: npx playwright install --with-deps chromium + - name: Run production smoke against workers.dev + env: + PLAYWRIGHT_BASE_URL: https://flowextract.edwardxie421.workers.dev + run: | + for attempt in $(seq 1 12); do + echo "Production smoke attempt $attempt/12" + if npx playwright test tests/e2e/production.spec.ts --project=chromium; then + exit 0 + fi + if [ "$attempt" -lt 12 ]; then + sleep 10 + fi + done + exit 1 From 8f2d431c9acd55c327faa90b15d2b89f299094b1 Mon Sep 17 00:00:00 2001 From: EdwardSage Date: Sat, 19 Sep 2026 11:58:22 +0800 Subject: [PATCH 3/4] Add production extraction smoke test --- tests/e2e/production.spec.ts | 107 +++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 tests/e2e/production.spec.ts diff --git a/tests/e2e/production.spec.ts b/tests/e2e/production.spec.ts new file mode 100644 index 0000000..8cbf04a --- /dev/null +++ b/tests/e2e/production.spec.ts @@ -0,0 +1,107 @@ +import { expect, test } from '@playwright/test'; + +test.skip(!process.env.PLAYWRIGHT_BASE_URL, 'Production smoke only runs against the configured live URL.'); + +test('production supports manual AI Chat extraction and preserves API mode', async ({ page }) => { + const syntheticProject = { + id: 'production-smoke-v011', + name: 'Production smoke V0.1.1', + updatedAt: '2099-01-01T00:00:00.000Z', + document: { + id: 'production-smoke-document', + name: 'synthetic-invoice.pdf', + mimeType: 'application/pdf', + size: 256, + createdAt: '2099-01-01T00:00:00.000Z', + text: [ + 'Redwood Demo Company', + 'Invoice Number: INV-2026-0919', + 'Date: 2026-09-19', + 'Amount: 1333.80', + ].join('\n'), + pages: ['Redwood Demo Company\nInvoice Number: INV-2026-0919\nDate: 2026-09-19\nAmount: 1333.80'], + sourceKind: 'pdf', + ocrUsed: false, + }, + schema: { + id: 'production-smoke-schema', + name: 'Invoice fields', + updatedAt: '2099-01-01T00:00:00.000Z', + fields: [ + { id: 'f1', name: 'Customer Name', key: 'customer_name', type: 'string', required: true, description: '', rules: {} }, + { id: 'f2', name: 'Invoice Number', key: 'invoice_number', type: 'string', required: true, description: '', rules: {} }, + { id: 'f3', name: 'Date', key: 'date', type: 'date', required: true, description: '', rules: {} }, + { id: 'f4', name: 'Amount', key: 'amount', type: 'number', required: true, description: '', rules: { min: 0 } }, + ], + }, + }; + + await page.goto('/'); + await expect(page.getByText('v0.1.1')).toBeVisible(); + + await page.evaluate(async (project) => { + const db = await new Promise((resolve, reject) => { + const request = indexedDB.open('flowextract', 1); + request.onupgradeneeded = () => { + const database = request.result; + if (!database.objectStoreNames.contains('projects')) { + const store = database.createObjectStore('projects', { keyPath: 'id' }); + store.createIndex('updatedAt', 'updatedAt'); + } + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + + await new Promise((resolve, reject) => { + const tx = db.transaction('projects', 'readwrite'); + tx.objectStore('projects').put(project); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + tx.onabort = () => reject(tx.error); + }); + db.close(); + }, syntheticProject); + + await page.reload(); + + await expect(page.getByRole('textbox', { name: /Project/i })).toHaveValue('Production smoke V0.1.1'); + await expect(page.getByRole('button', { name: /AI Chat/i })).toHaveAttribute('aria-pressed', 'true'); + + const prompt = page.getByLabel('Generated extraction prompt'); + await expect(prompt).toContainText('INV-2026-0919'); + await expect(prompt).toContainText('key=amount'); + + await page.getByLabel('AI chat response').fill(JSON.stringify({ + customer_name: 'Redwood Demo Company', + invoice_number: 'INV-2026-0919', + date: '2026-09-19', + amount: 1333.8, + })); + await page.getByRole('button', { name: 'Import & validate' }).click(); + + await expect(page.getByDisplayValue('1333.8')).toBeVisible(); + await expect(page.getByText('0 issues')).toBeVisible(); + + await page.getByDisplayValue('1333.8').fill('1333.81'); + await expect(page.getByText('corrected', { exact: true })).toBeVisible(); + + const downloadPromise = page.waitForEvent('download'); + await page.getByRole('button', { name: 'JSON' }).click(); + const download = await downloadPromise; + const stream = await download.createReadStream(); + if (!stream) throw new Error('JSON export did not produce a readable download.'); + + let exported = ''; + for await (const chunk of stream) exported += chunk.toString(); + expect(JSON.parse(exported)).toEqual({ + customer_name: 'Redwood Demo Company', + invoice_number: 'INV-2026-0919', + date: '2026-09-19', + amount: 1333.81, + }); + + await page.getByRole('button', { name: /^API/i }).click(); + await expect(page.getByPlaceholder('Stored in memory only')).toBeVisible(); + await expect(page.getByRole('option', { name: /Qwen \(Alibaba Cloud\) - Verified in Beijing/i })).toBeAttached(); +}); From 6ac39112c43c2c6a59729f420644da25da5ece72 Mon Sep 17 00:00:00 2001 From: EdwardSage Date: Sat, 19 Sep 2026 11:58:36 +0800 Subject: [PATCH 4/4] Fix production prompt assertions --- tests/e2e/production.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/production.spec.ts b/tests/e2e/production.spec.ts index 8cbf04a..67f76ae 100644 --- a/tests/e2e/production.spec.ts +++ b/tests/e2e/production.spec.ts @@ -69,8 +69,8 @@ test('production supports manual AI Chat extraction and preserves API mode', asy await expect(page.getByRole('button', { name: /AI Chat/i })).toHaveAttribute('aria-pressed', 'true'); const prompt = page.getByLabel('Generated extraction prompt'); - await expect(prompt).toContainText('INV-2026-0919'); - await expect(prompt).toContainText('key=amount'); + await expect(prompt).toHaveValue(/INV-2026-0919/); + await expect(prompt).toHaveValue(/key=amount/); await page.getByLabel('AI chat response').fill(JSON.stringify({ customer_name: 'Redwood Demo Company',