Merge pull request #240 from Sea-Haven-Industries/chore/playwright-ci-workers
Some checks are pending
Frontend checks / static (push) Waiting to run
Frontend checks / build (push) Waiting to run
Frontend checks / unit (push) Waiting to run
Frontend checks / Visual regression (push) Waiting to run
Frontend checks / browser-smoke (push) Waiting to run
Frontend checks / governance (push) Waiting to run
Frontend checks / ci-complete (push) Blocked by required conditions
Deploy Web / Resolve target (push) Waiting to run
Deploy Web / Deploy SPA to (push) Blocked by required conditions

chore: run Playwright smoke with 2 workers
This commit is contained in:
Adam Moussa 2026-09-21 15:17:29 +00:00 • committed by GitHub
commit 69a92cafda
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 47 additions and 18 deletions

View file

@ -82,6 +82,29 @@ jobs:
if-no-files-found: ignore if-no-files-found: ignore
retention-days: 14 retention-days: 14
browser-smoke:
name: browser-smoke
runs-on: ubuntu-latest
container: mcr.microsoft.com/playwright:v1.61.1-noble
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
cache: npm
- run: npm ci
- run: npm run test:e2e
- name: Upload smoke artifacts
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: browser-smoke-artifacts
path: |
test-results
playwright-report
if-no-files-found: ignore
retention-days: 14
governance: governance:
# Repo-owned gates: godfile ratchet, changed-file maintainability, # Repo-owned gates: godfile ratchet, changed-file maintainability,
# Terraform fmt/validate, import-plan guard, HCP run guard, CloudFront # Terraform fmt/validate, import-plan guard, HCP run guard, CloudFront
@ -150,7 +173,7 @@ jobs:
ci-complete: ci-complete:
name: ci-complete name: ci-complete
if: always() if: always()
needs: [static, build, unit, visual, governance] needs: [static, build, unit, visual, browser-smoke, governance]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: All required jobs passed - name: All required jobs passed

View file

@ -31,8 +31,8 @@ build (`tsc -b && vite build`), unit tests (`vitest run`), and the governance
checks (`npm run governance`), including G13 app/Terraform isolation. **Do not checks (`npm run governance`), including G13 app/Terraform isolation. **Do not
claim a task is done until `npm run verify` is green locally.** CI runs the same claim a task is done until `npm run verify` is green locally.** CI runs the same
gates as parallel jobs in [`.github/workflows/ci.yaml`](.github/workflows/ci.yaml) gates as parallel jobs in [`.github/workflows/ci.yaml`](.github/workflows/ci.yaml)
(`static`, `build`, `unit`, `visual`, `governance`) with `ci-complete` failing if (`static`, `build`, `unit`, `visual`, `browser-smoke`, `governance`) with
any of those jobs did not succeed. `ci-complete` failing if any of those jobs did not succeed.
## Non-negotiable rules (enforced; do not work around) ## Non-negotiable rules (enforced; do not work around)

View file

@ -63,9 +63,10 @@ isolation). A task is not done until this is green.
- **CI ([`.github/workflows/ci.yaml`](.github/workflows/ci.yaml)):** this - **CI ([`.github/workflows/ci.yaml`](.github/workflows/ci.yaml)):** this
repository owns every job. `static` (`format:check` + `lint`), `build` repository owns every job. `static` (`format:check` + `lint`), `build`
(`tsc -b && vite build`), `unit` (`vitest run` in four shards), `visual` (`tsc -b && vite build`), `unit` (`vitest run` in four shards), `visual`
(Playwright visual), and `governance` (`npm run governance`, with Terraform (Playwright visual), `browser-smoke` (`npm run test:e2e`), and `governance`
1.16.0) run in parallel. `ci-complete` fails unless all of those jobs (`npm run governance`, with Terraform 1.16.0) run in parallel. `ci-complete`
succeeded and is the required merge-queue check. Live G13 runs on fails unless all of those jobs succeeded and is the required merge-queue
check. Live G13 runs on
`pull_request` (merge-base range), `merge_group` (each queued PR as a `pull_request` (merge-base range), `merge_group` (each queued PR as a
first-parent commit), and locally. It skips `push`. A Terraform-only PR first-parent commit), and locally. It skips `push`. A Terraform-only PR
stacked with an app-only PR still passes; a mixed change set still fails. stacked with an app-only PR still passes; a mixed change set still fails.

View file

@ -150,7 +150,8 @@ No stored AWS keys — OIDC only. Infrastructure and content deploy separately:
- **CI** ([`.github/workflows/ci.yaml`](.github/workflows/ci.yaml)) — on push - **CI** ([`.github/workflows/ci.yaml`](.github/workflows/ci.yaml)) — on push
and PRs to `main`, runs format, lint, build, sharded unit tests, visual and PRs to `main`, runs format, lint, build, sharded unit tests, visual
regression, and `npm run governance` as parallel jobs, then `ci-complete`. regression, Playwright smoke, and `npm run governance` as parallel jobs, then
`ci-complete`.
Conventions and gates are documented under Conventions and gates are documented under
[`AGENTS.md`](AGENTS.md), [`QUALITY_GATES.md`](QUALITY_GATES.md), [`AGENTS.md`](AGENTS.md), [`QUALITY_GATES.md`](QUALITY_GATES.md),
[`ARCHITECTURE_AND_CODE_QUALITY.md`](ARCHITECTURE_AND_CODE_QUALITY.md), and [`ARCHITECTURE_AND_CODE_QUALITY.md`](ARCHITECTURE_AND_CODE_QUALITY.md), and

View file

@ -50,6 +50,10 @@ function multipartBoundary(request: Request): string | undefined {
return /^multipart\/form-data; boundary=(.+)$/.exec(contentType)?.[1]; return /^multipart\/form-data; boundary=(.+)$/.exec(contentType)?.[1];
} }
function multipartFileName(body: string): string | undefined {
return /filename="([^"]+)"/.exec(body)?.[1];
}
async function hideQueryDevtools(page: Page) { async function hideQueryDevtools(page: Page) {
await page.addStyleTag({ await page.addStyleTag({
content: content:
@ -141,7 +145,7 @@ test.describe("Completion uploads on a mobile viewport", () => {
const request = route.request(); const request = route.request();
const body = request.postDataBuffer()?.toString("latin1") ?? ""; const body = request.postDataBuffer()?.toString("latin1") ?? "";
uploads.push({ contentType: request.headers()["content-type"] ?? "", body }); uploads.push({ contentType: request.headers()["content-type"] ?? "", body });
const fileName = /filename="([^"]+)"/.exec(body)?.[1] ?? "file"; const fileName = multipartFileName(body) ?? "file";
const item = { id: 900 + media.length, category: 3, url: `/Assets/${fileName}`, fileName }; const item = { id: 900 + media.length, category: 3, url: `/Assets/${fileName}`, fileName };
media.push(item); media.push(item);
return fulfillJson(route, item); return fulfillJson(route, item);
@ -156,18 +160,18 @@ test.describe("Completion uploads on a mobile viewport", () => {
const panel = await openCompletionTab(page, row.woNumber); const panel = await openCompletionTab(page, row.woNumber);
const chooser = page.waitForEvent("filechooser"); const chooser = page.waitForEvent("filechooser");
await panel.getByRole("button", { name: /Drag files here or click to browse/ }).click(); await panel.getByRole("button", { name: /Drag files here or click to browse/ }).click();
await ( const files = [
await chooser
).setFiles([
{ name: "IMG_0001.jpg", mimeType: "image/jpeg", buffer: JPEG }, { name: "IMG_0001.jpg", mimeType: "image/jpeg", buffer: JPEG },
{ name: "VID_0002.MP4", mimeType: "video/mp4", buffer: MP4 }, { name: "VID_0002.MP4", mimeType: "video/mp4", buffer: MP4 },
{ name: "IMG_1587.MOV", mimeType: "video/quicktime", buffer: MOV }, { name: "IMG_1587.MOV", mimeType: "video/quicktime", buffer: MOV },
]); ];
await (await chooser).setFiles(files);
await expect.poll(() => uploads.length).toBe(3); await expect.poll(() => uploads.length).toBe(files.length);
for (const [index, name] of ["IMG_0001.jpg", "VID_0002.MP4", "IMG_1587.MOV"].entries()) { for (const { name } of files) {
expect(uploads[index]!.contentType).toMatch(/^multipart\/form-data; boundary=/); const upload = uploads.find((item) => multipartFileName(item.body) === name);
expect(uploads[index]!.body).toContain(`filename="${name}"`); expect(upload, `multipart upload for ${name}`).toBeTruthy();
expect(upload!.contentType).toMatch(/^multipart\/form-data; boundary=/);
} }
await panel.getByRole("combobox", { name: "Category for IMG_0001.jpg" }).selectOption("Before"); await panel.getByRole("combobox", { name: "Category for IMG_0001.jpg" }).selectOption("Before");

View file

@ -6,7 +6,7 @@ export default defineConfig({
fullyParallel: true, fullyParallel: true,
forbidOnly: Boolean(process.env.CI), forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 2 : undefined,
reporter: process.env.CI ? "github" : "list", reporter: process.env.CI ? "github" : "list",
use: { use: {
// Port 4173 isolates Playwright from the app's normal port 3000 server. // Port 4173 isolates Playwright from the app's normal port 3000 server.

View file

@ -6,7 +6,7 @@ export default defineConfig({
fullyParallel: false, fullyParallel: false,
forbidOnly: true, forbidOnly: true,
retries: 0, retries: 0,
workers: 1, workers: 2,
reporter: [["list"], ["html", { outputFolder: "playwright-report-visual", open: "never" }]], reporter: [["list"], ["html", { outputFolder: "playwright-report-visual", open: "never" }]],
outputDir: "test-results/visual", outputDir: "test-results/visual",
snapshotPathTemplate: "{testDir}/__screenshots__/{testFilePath}/{arg}{ext}", snapshotPathTemplate: "{testDir}/__screenshots__/{testFilePath}/{arg}{ext}",