diff --git a/.github/workflows/fw-lite.yaml b/.github/workflows/fw-lite.yaml index 1096b5d193..a42d7e8a7e 100644 --- a/.github/workflows/fw-lite.yaml +++ b/.github/workflows/fw-lite.yaml @@ -24,6 +24,15 @@ on: - develop - main - feat/sync-morph-types #just for now ensure PRs to this branch have checks run + +# Only cancel superseded PR runs; never cancel release builds triggered by a push. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + env: VIEWER_BUILD_OUTPUT_DIR: backend/FwLite/FwLiteShared/wwwroot/viewer jobs: @@ -146,12 +155,24 @@ jobs: exit 1 fi rm -f src/locales/en.po.old - - name: Set up Playwright dependencies + - name: Cache Playwright browsers + uses: actions/cache@v4 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('frontend/pnpm-lock.yaml') }} + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' working-directory: frontend run: pnpm exec playwright install --with-deps - - name: Run snapshot tests + - name: Install Playwright system deps + if: steps.playwright-cache.outputs.cache-hit == 'true' + working-directory: frontend + run: pnpm exec playwright install-deps + - name: Run UI tests working-directory: frontend/viewer - run: task playwright-test-standalone + run: task test:ui-standalone + - name: Build viewer working-directory: frontend/viewer run: pnpm run build @@ -170,6 +191,9 @@ jobs: frontend-component-unit-tests: runs-on: ubuntu-latest + permissions: + contents: read + checks: write steps: - name: Checkout uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -186,9 +210,20 @@ jobs: run: | pnpm install # vitest is configured to use playwright for stories and other browser-based tests - - name: Set up Playwright dependencies + - name: Cache Playwright browsers + uses: actions/cache@v4 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('frontend/pnpm-lock.yaml') }} + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' working-directory: frontend/viewer run: pnpm exec playwright install --with-deps + - name: Install Playwright system deps + if: steps.playwright-cache.outputs.cache-hit == 'true' + working-directory: frontend/viewer + run: pnpm exec playwright install-deps - name: vitest working-directory: frontend/viewer run: pnpm run test --retry=2 --reporter=default --reporter=junit --outputFile.junit=test-results/vitest-results.xml @@ -459,3 +494,124 @@ jobs: run: | sleep 10 curl -X POST https://lexbox.org/api/fwlite-release/new-release + + + e2e-test: + name: E2E Tests + needs: [frontend] + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + FW_LITE_BINARY_PATH: ${{ github.workspace }}/backend/FwLite/artifacts/publish/FwLiteWeb/release_linux-x64/FwLiteWeb + steps: + - name: Checkout + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + submodules: true + - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + with: + name: fw-lite-viewer-js + path: ${{ env.VIEWER_BUILD_OUTPUT_DIR }} + - uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0 + with: + dotnet-version: '10.x' + - name: Publish FwLiteWeb (linux-x64) + working-directory: backend/FwLite/FwLiteWeb + run: dotnet publish -r linux-x64 --artifacts-path ../artifacts -p:PublishSingleFile=true + + - name: set execute permissions + shell: bash + run: chmod +x ${{ env.FW_LITE_BINARY_PATH }} + + - name: Install Task + uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611 #v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + with: + package_json_file: 'frontend/package.json' + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + with: + node-version-file: './frontend/package.json' + cache: 'pnpm' + cache-dependency-path: './frontend/pnpm-lock.yaml' + - name: Install frontend deps + working-directory: frontend + run: pnpm install + - name: Test FwLite launcher + working-directory: frontend/viewer + run: task test:launcher + + - uses: ./.github/actions/setup-k8s + with: + lexbox-api-tag: develop + ingress-controller-port: '6579' # http; only used by the action's own curl-verify step + repo-token: ${{ secrets.GITHUB_TOKEN }} + + # MSAL refuses any non-https authority. ingress-nginx serves a snake-oil + # cert that FwLite trusts via --environment Development and playwright via + # ignoreHTTPSErrors. + - name: Forward ingress HTTPS port + shell: bash + run: | + kubectl port-forward service/ingress-nginx-controller 6580:443 -n languagedepot >/tmp/https-port-forward.log 2>&1 & + # Wait for the port-forward to actually be listening + for i in $(seq 1 30); do + if curl -sk https://localhost:6580 >/dev/null 2>&1; then + echo "✅ HTTPS port-forward ready on :6580" + exit 0 + fi + sleep 1 + done + echo "❌ HTTPS port-forward did not become ready" + cat /tmp/https-port-forward.log + exit 1 + + - name: Cache Playwright browsers + uses: actions/cache@v4 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('frontend/pnpm-lock.yaml') }} + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + working-directory: frontend/viewer + run: pnpm exec playwright install --with-deps + - name: Install Playwright system deps + if: steps.playwright-cache.outputs.cache-hit == 'true' + working-directory: frontend/viewer + run: pnpm exec playwright install-deps + + - name: Run E2E tests + working-directory: frontend/viewer + env: + TEST_SERVER_PROTOCOL: https + TEST_SERVER_PORT: 6580 + run: task test:e2e + + - name: Upload Playwright results/traces (on failure) + if: failure() + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: fw-lite-e2e-test-results + if-no-files-found: ignore + path: frontend/viewer/tests/e2e/test-results/ + + - name: Capture k8s pod logs (on failure) + if: failure() + shell: bash + run: | + mkdir -p k8s-logs + for app in lexbox ui hg db fw-headless; do + kubectl describe pods -l "app=${app}" -n languagedepot > k8s-logs/describe-${app}.txt 2>&1 || true + kubectl logs -l "app=${app}" -n languagedepot --prefix --all-containers --tail=-1 > k8s-logs/logs-${app}.txt 2>&1 || true + done + kubectl logs -l 'app.kubernetes.io/name=ingress-nginx' -n languagedepot --prefix --all-containers --tail=-1 > k8s-logs/logs-ingress.txt 2>&1 || true + + - name: Upload k8s logs (on failure) + if: failure() + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: fw-lite-e2e-k8s-logs + if-no-files-found: ignore + path: k8s-logs/*.txt diff --git a/.github/workflows/integration-test.yaml b/.github/workflows/integration-test.yaml index 2e36c075d5..cd88ccda3c 100644 --- a/.github/workflows/integration-test.yaml +++ b/.github/workflows/integration-test.yaml @@ -63,6 +63,7 @@ jobs: timeout-minutes: 120 name: Dotnet tests on ${{ inputs.runs-on }} for Mercurial ${{ inputs.hg-version }} on ${{ inputs.environment }} permissions: + contents: read checks: write environment: name: ${{ inputs.environment }} diff --git a/.github/workflows/release-pipeline.yaml b/.github/workflows/release-pipeline.yaml index 8ab5dd0a73..769c4b9f19 100644 --- a/.github/workflows/release-pipeline.yaml +++ b/.github/workflows/release-pipeline.yaml @@ -5,6 +5,8 @@ on: - main workflow_dispatch: +permissions: + contents: read jobs: set-version: diff --git a/AGENTS.md b/AGENTS.md index e1e541f747..8a11c5a6a0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,7 +56,7 @@ Key documentation for this project: - ✅ **`MiniLcm.Tests`, `LcmCrdt.Tests`, `FwDataMiniLcmBridge.Tests` need no infrastructure** — these are the unit/conformance tests for the two `IMiniLcmApi` implementations; run them filtered to your change like any unit test (this is where a `*TestsBase` change should be verified, including its FwData side). The FwData/LCM-backed selections load a real project (a handful of tests = seconds, a whole class = a minute or two), so keep the filter tight; don't reach for the whole class or project. - ✅ **FwLite integration tests** (e.g. `FwLiteProjectSync.Tests`) need no infrastructure but are slow. FwData runs **in-process via liblcm** — these tests create real FieldWorks projects with no external setup (e.g. the `ProjectTemplateTests.GenerateTemplate` regen tool). Only FwHeadless's hg/Mercurial sync and the lexbox stack (last bullet) need running infrastructure — *not* FwData itself. Run a **targeted selection** (specific tests, not necessarily whole classes) when you touched critical sync code **and believe the work is finished** — not on every iteration. Waiting on tests burns time; be deliberate about which runs buy real signal. - ✅ **`backend/Testing` contains unit tests too** — only tests marked `Category=Integration|FlakyIntegration|RequiresDb` (and the `Testing.Browser` namespace) need infrastructure. Its unit tests are fine to run: `task test:unit -- ` excludes those categories for you. -- ✅ **FwLite viewer Playwright tests MAY be run** — they're cheap: `task playwright-test-standalone -- ` (from `frontend/viewer/`) auto-starts the vite dev server with the in-browser demo project; no lexbox stack, chromium only. Always filter to relevant tests; details in `frontend/viewer/AGENTS.md`. +- ✅ **FwLite viewer Playwright tests MAY be run** — they're cheap: `task test:ui-standalone -- ` (from `frontend/viewer/`) auto-starts the vite dev server with the in-browser demo project; no lexbox stack, chromium only. Always filter to relevant tests; details in `frontend/viewer/AGENTS.md`. - ❌ **Do NOT run tests that need the lexbox stack** unless the user explicitly asks: LexBox integration tests (`Category=Integration`/`FlakyIntegration`, `Testing.Browser`) and the lexbox frontend Playwright suite (`frontend/tests`). The local stack is usually down or torn down between sessions and results aren't trustworthy — rely on CI for these. ### Questions? diff --git a/backend/FwLite/FwLiteWeb/FwLiteWebServer.cs b/backend/FwLite/FwLiteWeb/FwLiteWebServer.cs index 29978b9dfa..627baac039 100644 --- a/backend/FwLite/FwLiteWeb/FwLiteWebServer.cs +++ b/backend/FwLite/FwLiteWeb/FwLiteWebServer.cs @@ -70,6 +70,7 @@ public static WebApplication SetupAppServer(WebApplicationOptions options, Actio options.AddFilter(new LockedProjectFilter()); options.EnableDetailedErrors = true; }).AddJsonProtocol(); + builder.Services.AddHealthChecks(); configure?.Invoke(builder); var app = builder.Build(); @@ -124,6 +125,7 @@ public static WebApplication SetupAppServer(WebApplicationOptions options, Actio app.MapImport(); app.MapAuthRoutes(); app.MapMiniLcmRoutes("/api/mini-lcm"); + app.MapHealthChecks("/health"); app.MapStaticAssets(); app.MapRazorComponents() diff --git a/backend/FwLite/FwLiteWeb/Routes/AuthRoutes.cs b/backend/FwLite/FwLiteWeb/Routes/AuthRoutes.cs index a9617c55d4..4c71ea002c 100644 --- a/backend/FwLite/FwLiteWeb/Routes/AuthRoutes.cs +++ b/backend/FwLite/FwLiteWeb/Routes/AuthRoutes.cs @@ -18,6 +18,10 @@ public static IEndpointConventionBuilder MapAuthRoutes(this WebApplication app) async (AuthService authService, string authority, IOptions options, [FromHeader] string referer) => { var returnUrl = new Uri(referer).PathAndQuery; + if (returnUrl.StartsWith("/api/auth/login", StringComparison.Ordinal)) + { + returnUrl = "/"; + } if (options.Value.SystemWebViewLogin) { throw new NotSupportedException("System web view login is not supported for this endpoint"); diff --git a/backend/FwLite/FwLiteWeb/Routes/ProjectRoutes.cs b/backend/FwLite/FwLiteWeb/Routes/ProjectRoutes.cs index 5006b4f58a..196adfc049 100644 --- a/backend/FwLite/FwLiteWeb/Routes/ProjectRoutes.cs +++ b/backend/FwLite/FwLiteWeb/Routes/ProjectRoutes.cs @@ -90,6 +90,13 @@ [FromQuery] UserProjectRole? role _ => Results.InternalServerError("DownloadProjectByCodeResult enum value not handled, please inform FW Lite devs") }; }); + group.MapDelete("/crdt/{code}", + async (CrdtProjectsService projectService, string code) => + { + if (!projectService.ProjectExists(code)) return Results.NotFound($"Project {code} not found"); + await projectService.DeleteProject(code); + return Results.Ok(); + }); return group; } diff --git a/deployment/gha/lexbox.patch.yaml b/deployment/gha/lexbox.patch.yaml index d1878e72ae..103d42031d 100644 --- a/deployment/gha/lexbox.patch.yaml +++ b/deployment/gha/lexbox.patch.yaml @@ -11,6 +11,12 @@ spec: env: - name: Email__BaseUrl value: "http://localhost:6579" + # Trust the kind cluster's pod network so X-Forwarded-Proto from the + # ingress-nginx controller is honored — otherwise lexbox sees the request + # as HTTP and emits HTTP URLs in /.well-known/openid-configuration, which + # breaks the FwLite OAuth flow over the HTTPS port-forward used in CI. + - name: ForwardedHeadersOptions__KnownNetworks__0 + value: "0.0.0.0/0" volumeMounts: - mountPath: /frontend name: gql-schema diff --git a/frontend/viewer/.gitignore b/frontend/viewer/.gitignore index 5d7e95578e..e801ccfb84 100644 --- a/frontend/viewer/.gitignore +++ b/frontend/viewer/.gitignore @@ -29,3 +29,4 @@ storybook-static screenshots/ __screenshots__/ eslint_report.json +**/*-html-report diff --git a/frontend/viewer/AGENTS.md b/frontend/viewer/AGENTS.md index 9e0c35be08..aa0c7caa24 100644 --- a/frontend/viewer/AGENTS.md +++ b/frontend/viewer/AGENTS.md @@ -29,22 +29,29 @@ task fw-lite:has-stale-generated-types The generated files are located in `src/lib/dotnet-types/generated-types/`. -### E2E Testing (Playwright) +### Testing + +| Suite | Location | Runnable here? | +|---|---|---| +| UI | `tests/ui/` | ✅ Yes — auto-starts a dev server with in-memory demo; no infra needed | +| E2E | `tests/e2e/` | ❌ Needs a Lexbox kind cluster + published FwLiteWeb binary | +| Launcher | `tests/launcher/` | ❌ Needs a published FwLiteWeb binary | + +**Don't run E2E or Launcher tests unless you've explicitly set up that infrastructure** — they fail loudly without it and the setup isn't part of normal dev. + +UI tests (the runnable ones) — from `frontend/viewer/`: -To run E2E tests for the viewer: ```bash -# From frontend/viewer/ directory -# Automatically starts dev server if needed -# For debugging e.g. with Chrome MCP: dev server/demo project will be available at port 5173 & path /testing/project-view/browse +# Dev server / demo project: http://localhost:5173/testing/project-view (useful for Chrome MCP debugging). -# Filter by test name (the ONLY RIGHT choice if testing specific features or changes) e.g. -task playwright-test-standalone -- entries-list +# Filter by test name (the ONLY RIGHT choice when testing specific features or changes), e.g. +task test:ui-standalone -- entries-list -# All tests -task playwright-test-standalone +# All UI tests +task test:ui-standalone -# In UI mode -task playwright-test-standalone -- entries-list --ui +# Playwright UI mode +task test:ui-standalone -- entries-list --ui ``` ## Tech Stack @@ -63,7 +70,7 @@ task playwright-test-standalone -- entries-list --ui | `src/lib/` | Shared components, entry editor | | `src/locales/` | i18n translation files (JSON) | | `.storybook/` | Component storybook | -| `tests/` | Playwright E2E tests | +| `tests/` | Playwright (UI + e2e) and Vitest (launcher) tests | ## i18n (Lingui) diff --git a/frontend/viewer/Taskfile.yml b/frontend/viewer/Taskfile.yml index d9faef2d29..6ecd37c244 100644 --- a/frontend/viewer/Taskfile.yml +++ b/frontend/viewer/Taskfile.yml @@ -42,20 +42,29 @@ tasks: test-unit: cmd: pnpm test --project unit - playwright-test: - desc: 'runs playwright tests against already running server' - cmd: pnpm run test:playwright {{.CLI_ARGS}} - playwright-test-standalone: + test:ui: + desc: 'runs UI tests against already running server' + cmd: pnpm run test:ui {{.CLI_ARGS}} + test:ui-standalone: aliases: [pts] - desc: 'runs playwright tests and runs dev automatically, run ui mode by calling with -- --ui or use --update-snapshots' + desc: 'runs UI tests and runs dev automatically; --ui for playwright UI mode, --update-snapshots to refresh visual baselines' env: AUTO_START_SERVER: true - cmd: pnpm run test:playwright {{.CLI_ARGS}} + cmd: pnpm run test:ui {{.CLI_ARGS}} generate-marketing-screenshots: desc: 'they should be in the screenshots folder' env: AUTO_START_SERVER: true MARKETING_SCREENSHOTS: true - cmd: pnpm run test:playwright {{.CLI_ARGS}} - playwright-test-report: - cmd: pnpm run test:playwright-report + cmd: pnpm run test:ui snapshots {{.CLI_ARGS}} + + test:e2e: + cmd: pnpm run test:e2e {{.CLI_ARGS}} + test:launcher: + desc: 'tests the fw lite launcher class against a real subprocess; FW_LITE_BINARY_PATH must point to a built binary' + cmd: pnpm test:launcher --run + test:build-launcher: + desc: 'publishes a self-contained FwLiteWeb binary into ./dist/fw-lite-server; prerequisite for test:e2e and test:launcher' + deps: [build] + cmds: + - dotnet publish ../../backend/FwLite/FwLiteWeb/FwLiteWeb.csproj --configuration Release --self-contained --output ./dist/fw-lite-server diff --git a/frontend/viewer/package.json b/frontend/viewer/package.json index 645a52abe2..d3b2a2d395 100644 --- a/frontend/viewer/package.json +++ b/frontend/viewer/package.json @@ -13,16 +13,17 @@ "build": "vite build", "build-ffmpeg-worker": "vite build --config vite.config.ffmpeg-worker.ts", "preview": "vite preview", - "pretest:playwright": "playwright install", - "test:playwright": "playwright test", - "test:playwright-report": "playwright show-report html-test-results", - "test:playwright-record": "playwright codegen", - "test": "vitest run", - "test:ui": "vitest --ui", + "pretest:ui": "playwright install chromium", + "test:ui": "playwright test -c ./tests/ui/playwright.config.ts", + "pretest:e2e": "playwright install chromium", + "test:e2e": "playwright test -c ./tests/e2e/playwright.config.ts", + "test": "vitest run --project=!launcher", "test:watch": "vitest", "test:storybook": "vitest --project=storybook", "test:unit": "vitest --project=unit", "test:browser": "vitest --project=browser", + "test:launcher": "vitest --project=launcher", + "test:vitest-ui": "vitest --ui", "check": "svelte-check", "format": "prettier --write .", "format:check": "prettier --check .", diff --git a/frontend/viewer/src/ShadcnProjectView.svelte b/frontend/viewer/src/ShadcnProjectView.svelte index eb2b167e33..2e7535fd92 100644 --- a/frontend/viewer/src/ShadcnProjectView.svelte +++ b/frontend/viewer/src/ShadcnProjectView.svelte @@ -1,17 +1,3 @@ - -