diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 6b00aef47..e1e6dce9d 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -134,170 +134,32 @@ jobs: working-directory: src/renderer run: npm run build - - name: Clean up processes - if: always() - run: | - pkill -f node || true - - cypress-tests: - runs-on: ubuntu-latest - needs: build - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Volta - uses: volta-cli/action@v4 - - - name: Install Node.js 22 npm 12 - run: | - volta install node@22 - node --version - volta install npm@12 - npm --version - - - name: Clean up memory (kill any existing node processes) - run: | - pkill -f node || true - - - name: Install root dependencies - run: npm ci - - - name: Install renderer dependencies + # The --spec list in cy:run-ct-smoke is generated from the @smoke tags. + # Fail early if someone tagged a spec without regenerating it, otherwise + # the new spec silently never runs on PRs. + - name: Check smoke spec list matches @smoke tags + run: node env-config/smokeSpecs.cjs + + # No Vite warm-up step: cypress/config/ct-optimize-deps.json declares + # every dep the suite touches, so Vite pre-bundles them all at dev-server + # start and never re-optimizes (and reloads the AUT) mid-run. This step + # used to run the entire suite to force that discovery, which doubled the + # job. If reload-related flake reappears, regenerate the list rather than + # reinstating a warm-up run — see env-config/ctOptimizeDeps.cjs. + + # Pull requests run the @smoke subset (10 specs, ~2.5 min). Merges to + # develop/main and manual runs run everything (~14 min), so nothing loses + # coverage before it lands. + - name: Run Cypress smoke tests + if: github.event_name == 'pull_request' working-directory: src/renderer - run: npm ci + run: npm run cy:run-ct-smoke - - name: Stamp build with date and time - run: npm run stamp - - - name: Make Environment (.env.local) - working-directory: src/renderer - run: | - echo "VITE_DOMAIN=${{ env.DOMAIN }}" > .env.local - echo "VITE_CLIENTID=${{ env.CLIENT_ID }}" >> .env.local - echo "VITE_ENDPOINT=${{ env.APP }}" >> .env.local - echo "VITE_CALLBACK=${{ env.CALLBACK }}" >> .env.local - echo "VITE_HOST=${{ env.HOST }}" >> .env.local - echo "VITE_HELP=${{ env.HELP }}" >> .env.local - echo "VITE_COMMUNITY=${{ env.COMMUNITY }}" >> .env.local - echo "VITE_OPENNOTES=${{ env.OPEN_NOTES }}" >> .env.local - echo "VITE_RESOURCES=${{ env.RESOURCES }}" >> .env.local - echo "VITE_OPENCONTENT=${{ env.OPEN_CONTENT }}" >> .env.local - echo "VITE_COURSE=${{ env.COURSE }}" >> .env.local - echo "VITE_VIDEO_TRAINING=${{ env.VIDEOS }}" >> .env.local - echo "VITE_WALK_THRU=${{ env.WALK_THRU }}" >> .env.local - echo "VITE_AKUO=${{ env.AKUO }}" >> .env.local - echo "VITE_FLAT=${{ env.FLAT }}" >> .env.local - echo "VITE_HIERARCHICAL=${{ env.HIERARCHICAL }}" >> .env.local - echo "VITE_GEN_FLAT=${{ env.GEN_FLAT }}" >> .env.local - echo "VITE_GEN_HIERARCHICAL=${{ env.GEN_HIERARCHICAL }}" >> .env.local - echo "VITE_GOOGLE_SAMPLES=${{ env.GOOGLE_SAMPLES }}" >> .env.local - echo "VITE_SNAGID=${{ env.SNAG_ID }}" >> .env.local - echo "VITE_SIZELIMIT=${{ env.SIZELIMIT }}" >> .env.local - echo "VITE_SITE_TITLE=${{ env.NAME }}" >> .env.local - - - name: Create auth0-variables.json - working-directory: src/renderer - run: | - echo '{"apiIdentifier":"${{ env.API_ID }}","auth0Domain":"${{ env.DOMAIN }}","webClientId":"${{ env.CLIENT_ID }}"}' > src/auth/auth0-variables.json - - - name: Create index.html - run: | - echo "VITE_SITE_TITLE=${{ env.NAME }}" > .indexVar.json - echo "VITE_CALLBACK=${{ env.CALLBACK }}" >> .indexVar.json - echo "VITE_HOST=${{ env.HOST }}" >> .indexVar.json - echo "VITE_DOMAIN=${{ env.DOMAIN }}" >> .indexVar.json - echo "VITE_FILES=${{ env.FILES }}" >> .indexVar.json - node env-config/indexTemplate.cjs dev .indexVar.json - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Clean up disk space before tests - run: | - # Clean up system packages - sudo apt-get clean || true - sudo rm -rf /var/lib/apt/lists/* || true - - # Clean up Docker aggressively - docker system prune -af --volumes || true - docker builder prune -af || true - - # Remove cache directories to free up space - rm -rf node_modules/.cache || true - rm -rf src/renderer/node_modules/.cache || true - - - name: Check disk space - run: | - df -h - docker system df - - - name: Verify environment files exist - working-directory: src/renderer - run: | - echo "Checking for .env.local:" - ls -la .env.local || echo ".env.local not found!" - echo "Checking for auth0-variables.json:" - ls -la src/auth/auth0-variables.json || echo "auth0-variables.json not found!" - - - name: Build Docker image and start container - env: - DOCKER_BUILDKIT: 1 - run: | - docker build -t apm-vite-renderer -f src/renderer/Dockerfile . - docker run -d -p 3000:3000 --name apm-vite-renderer apm-vite-renderer - echo "Container status:" - docker ps -a | grep apm-vite-renderer || true - echo "Container logs after startup:" - docker logs apm-vite-renderer || true - - - name: Wait for app to be ready - run: | - timeout=60 - elapsed=0 - while ! curl -f http://localhost:3000 > /dev/null 2>&1; do - if [ $elapsed -ge $timeout ]; then - echo "App failed to start within $timeout seconds" - echo "Container status:" - docker ps -a | grep apm-vite-renderer || true - echo "Container logs:" - docker logs apm-vite-renderer || true - exit 1 - fi - # Check if container is still running - if ! docker ps | grep -q apm-vite-renderer; then - echo "Container has stopped!" - echo "Container logs:" - docker logs apm-vite-renderer || true - exit 1 - fi - echo "Waiting for app to start... ($elapsed/$timeout seconds)" - sleep 2 - elapsed=$((elapsed + 2)) - done - echo "App is ready!" - - - name: Warm up Vite for Cypress tests - working-directory: src/renderer - run: npm run cy:run-ct-fast || true - - - name: Run Cypress tests + - name: Run full Cypress suite + if: github.event_name != 'pull_request' working-directory: src/renderer run: npm run cy:run-ct - - name: Clean up Docker containers - if: always() - run: | - docker stop apm-vite-renderer || true - docker rm apm-vite-renderer || true - - - name: Clean up Docker system after tests - if: always() - run: | - docker system prune -af --volumes || true - docker builder prune -af || true - - name: Upload Cypress screenshots on failure if: failure() uses: actions/upload-artifact@v4 diff --git a/docs/cypress-ci-performance.md b/docs/cypress-ci-performance.md new file mode 100644 index 000000000..90fdb0cad --- /dev/null +++ b/docs/cypress-ci-performance.md @@ -0,0 +1,303 @@ +# Cypress CI performance + +How the component test suite was cut from ~27 minutes per pull request to ~3, +what was actually slow, and what to do when it drifts. + +Measured 2026-09-01 against `develop` at 9f0c318, Cypress 15.21.0, Chrome 152 +headless, 46 spec files / 537 tests. + +--- + +## Results + +| Event | Before | After | +| ------------------------- | --------------------- | ----------- | +| Pull request | 2 × 13:39 ≈ **27:18** | ≈ **3:00** | +| Merge to `develop`/`main` | 2 × 13:39 ≈ **27:18** | ≈ **13:39** | + +Pull requests run a 10-spec `@smoke` subset (measured **2:36**). Merges and +manual runs still run everything, so nothing lands without full coverage. + +Merges improve purely by removing a duplicated run — no coverage was traded. + +--- + +## What was actually slow + +### 1. CI ran the whole suite twice + +`.github/workflows/dev.yml` had two consecutive steps: + +```yaml +- name: Warm up Vite for Cypress tests + run: npm run cy:run-ct-fast || true # <- this was the ENTIRE suite +- name: Run Cypress tests + run: npm run cy:run-ct +``` + +`cy:run-ct-fast` was `npm run cy:run-ct -- --config video=false screenshot=false`. +The `|| true` hid it. The warm-up was serving a real purpose (see §4) — it just +cost a second full suite to do it. + +### 2. Cypress's reported total hides half the runtime + +- Cypress reported **06:39** for 537 tests. +- Wall clock was **13:39**. + +The missing **~7:00** is fixed per-spec overhead — browser launch and bundle +load, roughly **9 seconds per spec** across 46 specs. It never appears in the +duration column, which is why spec _count_ matters as much as test count, and +why the subset is selected with `--spec` rather than filtered in-browser. + +> When measuring, trust wall clock, not Cypress's summary. Add the elapsed time +> to your PowerShell prompt: +> +> ```powershell +> function prompt { +> $t = Get-Date -Format 'HH:mm:ss' +> $last = Get-History -Count 1 +> $dur = '' +> if ($last -and $last.EndExecutionTime) { +> $dur = ' +' + ($last.EndExecutionTime - $last.StartExecutionTime).ToString('mm\:ss') +> } +> Write-Host "[$t$dur]" -NoNewline -ForegroundColor DarkGray +> " $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) " +> } +> ``` + +#### Make it permanent + +Your profile is `C:\Users\gtrih\OneDrive\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1` and doesn't exist yet: + +```powershell +New-Item -ItemType File -Path $PROFILE -Force +notepad $PROFILE +``` + +Paste the function in, save, and open a new window. (`-Force` here creates the parent directory; it's safe because the file doesn't exist — avoid it on a profile you already have, since it truncates.) + +### 3. Five specs are 75% of all test time + +| Spec | Time | Tests | +| --------------------------------------------------- | -------- | ------ | +| `PassageDetailPhraseBackTranslate.edit.cy.tsx` | 1:37 | 16 | +| `PassageDetailPhraseBackTranslate.cy.tsx` | 1:32 | 15 | +| `PassageDetailPhraseBackTranslate.selection.cy.tsx` | 1:15 | 8 | +| `PassageDetailPhraseBackTranslate.playback.cy.tsx` | 0:26 | 4 | +| `PassageDetailPhraseBackTranslate.defects.cy.tsx` | 0:11 | 5 | +| **PBT subtotal** | **5:01** | **48** | +| All other 41 specs | 1:38 | 489 | + +**48 tests take five minutes; the other 489 take 98 seconds.** Individual +offenders: "records backwards, each take against its own region" 16.0s, "marks +the step complete once every segment is recorded" 18.3s, three tests in +`.selection` at 18–20s each. + +This is real-time audio playback, not sleeps. See [Open items](#open-items). + +### 4. 138 of 161 Vite dependencies were discovered lazily + +`optimizeDeps.include` in `cypress/config/local.config.ts` declared **23** deps. +The dep cache from a full run (`node_modules/.vite-ct/deps/_metadata.json`) +showed Vite actually optimized **161**. + +Every dep Vite has to discover mid-run triggers a re-optimize and an AUT reload. +That reload leaves **two copies of React** in the page, so the spec that +triggers it fails with: + +``` + +Cannot read properties of null (reading 'useMemo') + +``` + +This is why the whole-suite warm-up existed: it forced discovery of all 161 +before the graded run. The fix is to declare them instead of discovering them — +see [ct-optimize-deps.json](#generated-files). + +--- + +## Why a curated subset, and not affected-test selection + +The better answer to "what should run per commit" is usually: map changed files +to affected specs. That does not work here. + +Building the import graph over `src/renderer/src` (1,070 app files): + +- **36 of 46 specs each transitively import 620–720 files.** +- The cause is three barrel files, each with a 621-file transitive closure and + reachable from each other: + +| Barrel | Imported by | +| ---------------------- | ----------- | +| `src/crud/index.ts` | 253 files | +| `src/utils/index.ts` | 193 files | +| `src/control/index.ts` | 128 files | + +Any component importing `from '../crud'` depends on 621 files. So almost any +commit "affects" almost every spec, and selection degrades to running +everything. + +Breaking those barrels would make genuine affected-test selection possible and +is worth doing on its own merits — but it is a much larger change than a +curated subset. + +--- + +## How it works now + +### Per-commit smoke subset + +10 specs / 195 tests, chosen by crossing feature area against six-month churn in +`git log`, so the picks track where regressions actually land. + +| Spec | Tests | Covers churn in | +| ------------------------------------------ | ----- | -------------------------------------- | +| `Sheet/PassageCard.cy.tsx` | 42 | ScriptureTable (33), PlanSheet (17) | +| `routes/SwitchTeams.cy.tsx` | 40 | SwitchTeams (15), team/routing | +| `App/OrgHead.cy.tsx` | 30 | app chrome; 8 co-changes | +| `PassageDetail/mobile/MobileWorkflowSteps` | 26 | highest co-change spec (12) | +| `Sheet/PlanView.cy.tsx` | 25 | plan screen | +| `control/RecordButton.cy.tsx` | 10 | MediaRecord (33) | +| `PlayButton.cy.tsx` | 9 | WSAudioPlayer (56), useWaveSurfer (24) | +| `PBT.cy.tsx` (2 of 5 describes) | 6 | PassageDetailContext (21) | +| `StepEditor/StepEditor.cy.tsx` | 5 | workflow steps | +| `burrito/BurritoWrapper.cy.tsx` | 3 | burrito entry point | + +Only the two non-recording describes in `PBT.cy.tsx` are tagged. Those are 10.8s +of that spec's 92s; the three recording-heavy describes still run in the full +suite. + +### Tagging a spec into the smoke set + +Add the tag to a top-level `describe` (it cascades to nested tests), then +regenerate the spec list: + +```ts +describe('MyComponent', { tags: '@smoke' }, () => { +``` + +```powershell +cd src\renderer +npm run cy:smoke-specs:write +``` + +### Why `--spec` and not just `grepTags` + +`@cypress/grep` has a `grepFilterSpecs` flag that should skip whole spec files +containing no matching test. **It does not work in Cypress 15 component mode** — +the runner resolves the spec list before `setupNodeEvents` runs, so the filtered +`specPattern` the plugin returns is discarded. Verified by calling the plugin +directly: it computes the correct 10 files, and Cypress ignores them. + +Since every loaded spec costs ~9s whether or not its tests run, the file list +has to be narrowed on the command line. `grepTags=@smoke` is still passed — it +does the filtering _within_ each listed file, which is what keeps `PBT.cy.tsx` +down to 6 tests. + +The plugin stays registered in `local.config.ts` because it does work for +`--e2e`. + +--- + +## Commands + +Run from `src/renderer`: + +| Command | Purpose | +| ------------------------------ | ----------------------------------------------------------- | +| `npm run cy:run-ct-smoke` | 10-spec smoke subset (~2:36) | +| `npm run cy:run-ct` | Full suite, minus `@known-defect` (~13:39) | +| `npm run cy:run-ct-all` | Full suite including `@known-defect` | +| `npm run cy:smoke-specs` | Check `--spec` list matches `@smoke` tags (exit 1 if stale) | +| `npm run cy:smoke-specs:write` | Regenerate the `--spec` list | +| `npm run cy:ct-deps` | Check the Vite dep list is complete | +| `npm run cy:ct-deps:write` | Regenerate it from the last full run's cache | + +--- + +## Generated files + +Both are derived artifacts with a check mode suitable for CI or a pre-commit +hook. Neither should be hand-edited. + +### `src/renderer/cypress/config/ct-optimize-deps.json` + +The 161 dependencies Vite must pre-bundle. Generated by +`env-config/ctOptimizeDeps.cjs` from `node_modules/.vite-ct/deps/_metadata.json`. + +**Regenerate after a FULL run**, not a smoke run — a smoke run only exercises 10 +specs and would shrink the list, reintroducing the reload failure. + +The check reports missing and extra deps separately. Only _missing_ is a +failure; extras are expected if the cache came from a partial run. + +### The `--spec` list in `cy:run-ct-smoke` + +Generated by `env-config/smokeSpecs.cjs` from the `@smoke` tags, using +`find-test-names` — the same parser `@cypress/grep` uses, so the script and the +runner can never disagree about which files carry the tag. + +CI runs the check before the tests, so tagging a spec without regenerating fails +the build instead of silently never running the new spec. + +Both scripts refuse to write an empty list. An empty `--spec` makes Cypress run +_everything_, which would turn a "fast" smoke job into the full suite without +anyone noticing. + +--- + +## Open items + +### Mock the clock in the PBT specs + +**The biggest remaining win.** Five minutes of a 6:39 suite is spent waiting for +audio to play in real time. Mocking the clock or the audio element in the PBT +harness would cut the full suite to roughly two minutes — with no coverage +traded, and it helps every merge build, not just pull requests. + +### 41 seconds of hard-coded `cy.wait()` + +95 calls, concentrated in `PBT.defects` (13.5s), `.playback` (6.6s), `.edit` +(4.5s) and `PlanBar` (3.6s). Converting to assertion-based waits is pure +speedup. + +### Does the CI job need the Docker container? + +The `cypress-tests` job builds a Docker image, starts a container on port 3000, +and waits for it — but only runs **component** tests, which mount components +directly. Searching the component-test paths found nothing needing that origin: + +- The only `cy.visit` is in `loginByAuth0`, an **e2e** command. +- `VITE_CALLBACK: 'http://localhost:3000/callback'` in `cypress/support/component.tsx` + is a config string, never fetched. +- The localization fetch in `store/localization/actions.tsx` is + `appPath() + '/localization/...'`, and `appPath()` returns `''` on web — so it + resolves against the AUT's own origin (Cypress's dev server serving `public/`), + not port 3000. + +Component tests were reported failing without the container in the past. That +predates the `optimizeDeps` fix, and the reload failure in §4 is a strong +candidate for what was actually breaking. **Not yet removed** — worth confirming +a local full run passes with nothing on port 3000 before touching it. + +### Break the `crud` / `utils` / `control` barrels + +Would enable real affected-test selection and cut cold-start bundling. + +--- + +## Verifying the dependency fix + +The `optimizeDeps` change has not been exercised end to end. To confirm: + +```powershell +cd src\renderer +Remove-Item -Recurse -Force node_modules\.vite-ct +npm run cy:run-ct-smoke +``` + +Expect a slower first spec (161 deps bundled up front) and **no** +`optimized dependencies changed, reloading` in the output. If that message +appears, regenerate the list from a full run rather than reinstating a warm-up +step. diff --git a/docs/pwa.md b/docs/pwa.md new file mode 100644 index 000000000..611a98769 --- /dev/null +++ b/docs/pwa.md @@ -0,0 +1,39 @@ +what is necessary to take a react 19 app and make it into a progressive web app? + +To convert a **React 19** application into a **Progressive Web App (PWA)**, you must implement three core components: a **Web App Manifest**, a **Service Worker**, and proper **registration** of that worker. + +**1. Web App Manifest** +Create a `manifest.json` file in your `public` directory. This JSON file provides metadata such as the app name, icons, start URL, and display mode (e.g., `standalone` to hide the browser address bar). You must link this file in your `index.html` header using a `` tag. + +**2. Service Worker Implementation** +A service worker is a script that runs in the background to enable offline functionality, caching, and push notifications. You can manage this manually or use libraries like **Workbox** for easier caching and precaching strategies. Ensure the service worker script is registered and handles network requests appropriately to allow the app to function without an internet connection. + +**3. Service Worker Registration** +In your main entry file (typically `src/index.js` or `src/main.js`), you must register the service worker. Change the default registration call from `unregister()` to `register()` to ensure the service worker is activated as a persistent background process. This step is critical for the browser to detect the app as installable and for PWA features to activate. + +**4. React 19 Specifics** +While **React 19** introduces new features for efficient rendering and state management, the fundamental PWA requirements remain unchanged. However, React 19’s improved performance and rendering capabilities contribute to the fast, responsive user interfaces expected in PWAs. Ensure your build process (e.g., via **Create React App** or a custom Vite/Webpack setup) correctly bundles the service worker and manifest files into the production build. + +What I changed +File Change +src/renderer/vite.config.ts Added VitePWA (generateSW, registerType: 'prompt', precache the shell only, /api/ navigation denylist, no cross-origin runtime caching, exclude lazy term dictionaries, 25 MiB precache limit for the large main bundle) +src/renderer/src/components/PwaUpdatePrompt.tsx New web-only component: registers the SW and shows a localized "reload to update" snackbar on new deploys +src/renderer/src/auth/Root.tsx Mounts guarded by !isElectron +src/renderer/public/ New icons: pwa-192x192.png, pwa-512x512.png, pwa-maskable-512x512.png, apple-touch-icon.png, favicon.ico (generated from resources/icon.png) +env-config/index.html + src/renderer/index.html PWA meta tags (theme-color, apple-touch-icon, mobile-web-app-capable) added to the template so they survive changeEnv regeneration +src/renderer/src/vite-env.d.ts vite-plugin-pwa/client types reference +localization/TranscriberAdmin-en-1.2.xliff + generated model/reducers/JSON Two new strings (main.pwaReload, main.pwaUpdateReady) via the XLIFF pipeline +Verification (all green) +npm run typecheck ✅ and npm run build ✅ — dist/sw.js + manifest.webmanifest generated, manifest link auto-injected. +Served the build and drove it in a real browser: SW registered and reached activated, scope /, 45 shell entries precached; manifest is installable (standalone + 192/512/maskable icons). +Confirmed the generated SW has the SPA navigation fallback with the /api/ denylist and no cross-origin runtime caching — so Auth0, /api/\*, and S3 media all pass through to the network untouched. +changeEnv dev confirmed the PWA meta tags survive index.html regeneration. +Lint passed on the changed files. +Answering your original question +No, it does not break the web app, and it does not touch Electron at all — the desktop build uses the separate root electron.vite.config.ts and the prompt component is guarded by !isElectron. The web app keeps working exactly as before (login, Orbit sync, media) because nothing cross-origin is cached; the PWA only adds installability, faster repeat loads, and an app-shell that survives reconnects. + +One caveat carried over from the plan: true offline data is not delivered here — the web build is online-first (offline: isElectron), and the offline data machinery is Electron-gated. That's a separate, larger effort. + +Two things for whoever owns the web deploy +The production host must provide SPA fallback rewrites to /index.html and serve sw.js/manifest.webmanifest over HTTPS with short cache-control (the repo doesn't contain that hosting config). +The precache is ~21 MB, almost entirely the existing 18 MB main bundle — the PWA just caches what the site already ships; reducing that bundle is a separate optimization. diff --git a/env-config/ctOptimizeDeps.cjs b/env-config/ctOptimizeDeps.cjs new file mode 100644 index 000000000..b21a417ad --- /dev/null +++ b/env-config/ctOptimizeDeps.cjs @@ -0,0 +1,98 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +// +// Keeps cypress/config/ct-optimize-deps.json in sync with the dependencies +// Vite actually pre-bundles for component tests. +// +// Why this exists: if a dep is missing from optimizeDeps.include, Vite does not +// discover it until the spec that imports it loads, then re-optimizes and +// reloads the AUT mid-run. The reload leaves two copies of React in the page, +// so the spec fails with "Cannot read properties of null (reading 'useMemo')" +// rather than merely running slow. Listing every dep up front is what makes a +// separate whole-suite warm-up run unnecessary. +// +// The source of truth is the dep cache Vite writes during a real run, so +// regenerate after a FULL run (npm run cy:run-ct), not a smoke run — a smoke +// run only exercises 10 specs and would shrink the list. +// +// node env-config/ctOptimizeDeps.cjs compare cache to the checked-in list +// node env-config/ctOptimizeDeps.cjs --write rewrite the list from the cache +// +const fs = require('fs'); +const path = require('path'); + +const REPO = path.resolve(__dirname, '..'); +const RENDERER = path.join(REPO, 'src', 'renderer'); +const META = path.join( + RENDERER, + 'node_modules', + '.vite-ct', + 'deps', + '_metadata.json' +); +const LIST = path.join(RENDERER, 'cypress', 'config', 'ct-optimize-deps.json'); + +// Cypress injects its own mount entry; it is not an app dependency and is not +// resolvable from the config, so listing it would break the dev server. +const EXCLUDE = new Set(['cypress/react']); + +if (!fs.existsSync(META)) { + console.error( + `No Vite CT dep cache at ${META}.\n` + + `Run a full component test run first (cd src/renderer && npm run cy:run-ct).` + ); + process.exit(1); +} + +const meta = JSON.parse(fs.readFileSync(META, 'utf-8')); +const cached = Object.keys(meta.optimized || {}) + .filter((d) => !EXCLUDE.has(d)) + .sort(); + +if (cached.length === 0) { + console.error( + `${META} lists no optimized deps. Refusing to write an empty list.` + ); + process.exit(1); +} + +const current = fs.existsSync(LIST) + ? JSON.parse(fs.readFileSync(LIST, 'utf-8')) + : []; + +const missing = cached.filter((d) => !current.includes(d)); +const extra = current.filter((d) => !cached.includes(d)); + +if (missing.length === 0 && extra.length === 0) { + console.log(`ct-optimize-deps.json is up to date (${cached.length} deps).`); + process.exit(0); +} + +// `extra` is reported but is not a failure on its own: a smoke run, or a run +// that skipped specs, legitimately optimizes fewer deps than the full suite. +// Only `missing` means a spec would trigger a mid-run reload. +if (missing.length) { + console.error(`ct-optimize-deps.json is missing ${missing.length} dep(s):`); + missing.forEach((d) => console.error(` + ${d}`)); +} +if (extra.length) { + console.log( + `\n${extra.length} dep(s) in the list were not optimized by this run ` + + `(expected if it was not a full run):` + ); + extra.forEach((d) => console.log(` - ${d}`)); +} + +if (!process.argv.includes('--write')) { + if (missing.length) { + console.error( + `\nRun a full suite, then: node env-config/ctOptimizeDeps.cjs --write` + ); + process.exit(1); + } + process.exit(0); +} + +fs.writeFileSync(LIST, JSON.stringify(cached, null, 2) + '\n'); +console.log( + `\nWrote ${cached.length} deps to cypress/config/ct-optimize-deps.json` +); diff --git a/env-config/smokeSpecs.cjs b/env-config/smokeSpecs.cjs new file mode 100644 index 000000000..b4f4ad725 --- /dev/null +++ b/env-config/smokeSpecs.cjs @@ -0,0 +1,113 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +// +// Keeps the --spec list in src/renderer's `cy:run-ct-smoke` script in sync with +// the specs that actually carry an @smoke tag. +// +// Why a --spec list at all, rather than just --env grepTags=@smoke: in Cypress +// 15 component mode the runner resolves the spec list before setupNodeEvents +// runs, so @cypress/grep's `grepFilterSpecs` cannot drop non-matching files. +// Every spec that loads costs ~9s of fixed browser+bundle overhead whether or +// not any of its tests run, so the file list has to be narrowed on the command +// line. grepTags still does the filtering *within* each listed file, which is +// what keeps partially-tagged specs (e.g. the PBT ones) down to their @smoke +// describes. +// +// node env-config/smokeSpecs.cjs check package.json is current (exit 1 if not) +// node env-config/smokeSpecs.cjs --write rewrite the script with the current list +// +const fs = require('fs'); +const path = require('path'); + +const REPO = path.resolve(__dirname, '..'); +const RENDERER = path.join(REPO, 'src', 'renderer'); +const SPEC_ROOT = path.join(RENDERER, 'src'); +const PKG = path.join(RENDERER, 'package.json'); +const SCRIPT = 'cy:run-ct-smoke'; +const TAG = '@smoke'; + +// The same parser @cypress/grep uses, so this script and the runner always +// agree on which files carry the tag. It lives under the renderer's tree. +const findTestNames = path.join(RENDERER, 'node_modules', 'find-test-names'); +let getTestNames; +try { + ({ getTestNames } = require(findTestNames)); +} catch { + console.error( + `Cannot load find-test-names from ${findTestNames}.\n` + + `Run "npm ci" in src/renderer first.` + ); + process.exit(1); +} + +function walk(dir, acc = []) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) walk(full, acc); + else if (/\.cy\.(js|jsx|ts|tsx)$/.test(entry.name)) acc.push(full); + } + return acc; +} + +// Tags can sit on a suite or an individual test, and nest, so collect the whole +// tree rather than just the top level. +function hasTag(node) { + if ((node.tags || []).includes(TAG)) return true; + return [...(node.suites || []), ...(node.tests || [])].some(hasTag); +} + +function taggedSpecs() { + const found = []; + for (const file of walk(SPEC_ROOT)) { + const rel = path.relative(RENDERER, file).split(path.sep).join('/'); + let parsed; + try { + parsed = getTestNames(fs.readFileSync(file, 'utf8'), true); + } catch (err) { + // Never silently drop a spec we failed to read — a parse error here + // would otherwise look identical to "this spec has no @smoke tests". + console.error(`Could not parse ${rel}: ${err.message.split('\n')[0]}`); + process.exit(1); + } + if ((parsed.structure || []).some(hasTag)) found.push(rel); + } + return found.sort(); +} + +function buildScript(specs) { + return ( + 'cypress run --component --browser chrome ' + + '--config-file cypress/config/local.config.ts ' + + `--spec "${specs.join(',')}" ` + + `--env grepTags=${TAG},grepOmitFiltered=true` + ); +} + +const specs = taggedSpecs(); +if (specs.length === 0) { + console.error( + `No spec carries a ${TAG} tag. Refusing to write an empty --spec list ` + + `(Cypress would fall back to running everything).` + ); + process.exit(1); +} + +const pkg = JSON.parse(fs.readFileSync(PKG, 'utf8')); +const current = pkg.scripts[SCRIPT]; +const wanted = buildScript(specs); + +if (current === wanted) { + console.log(`${SCRIPT} is up to date (${specs.length} specs).`); + process.exit(0); +} + +if (!process.argv.includes('--write')) { + console.error(`${SCRIPT} is out of date. Tagged ${TAG} specs are now:`); + specs.forEach((s) => console.error(` ${s}`)); + console.error(`\nRun: node env-config/smokeSpecs.cjs --write`); + process.exit(1); +} + +pkg.scripts[SCRIPT] = wanted; +fs.writeFileSync(PKG, JSON.stringify(pkg, null, 2) + '\n'); +console.log(`Updated ${SCRIPT} with ${specs.length} specs:`); +specs.forEach((s) => console.log(` ${s}`)); diff --git a/src/renderer/cypress/config/ct-optimize-deps.json b/src/renderer/cypress/config/ct-optimize-deps.json new file mode 100644 index 000000000..b950ab51a --- /dev/null +++ b/src/renderer/cypress/config/ct-optimize-deps.json @@ -0,0 +1,163 @@ +[ + "@auth0/auth0-react", + "@bugsnag/js", + "@bugsnag/plugin-react", + "@cypress/grep", + "@fingerprintjs/fingerprintjs", + "@fortawesome/free-regular-svg-icons", + "@fortawesome/free-solid-svg-icons", + "@fortawesome/react-fontawesome", + "@hello-pangea/dnd", + "@mui/icons-material", + "@mui/icons-material/AccessTime", + "@mui/icons-material/AccountCircle", + "@mui/icons-material/Add", + "@mui/icons-material/ArrowBack", + "@mui/icons-material/ArrowDropDown", + "@mui/icons-material/ArrowRightAlt", + "@mui/icons-material/CancelOutlined", + "@mui/icons-material/Check", + "@mui/icons-material/CheckBox", + "@mui/icons-material/CheckBoxOutlineBlank", + "@mui/icons-material/CheckBoxOutlineBlankOutlined", + "@mui/icons-material/CheckBoxOutlined", + "@mui/icons-material/CheckCircle", + "@mui/icons-material/ChevronLeft", + "@mui/icons-material/ChevronRight", + "@mui/icons-material/Clear", + "@mui/icons-material/Close", + "@mui/icons-material/Cloud", + "@mui/icons-material/CloudDownload", + "@mui/icons-material/CloudOff", + "@mui/icons-material/CloudUpload", + "@mui/icons-material/ContentCopy", + "@mui/icons-material/Delete", + "@mui/icons-material/DeleteOutline", + "@mui/icons-material/DescriptionOutlined", + "@mui/icons-material/DragIndicator", + "@mui/icons-material/Edit", + "@mui/icons-material/EditNote", + "@mui/icons-material/EditSquare", + "@mui/icons-material/ExitToApp", + "@mui/icons-material/ExpandLess", + "@mui/icons-material/ExpandMore", + "@mui/icons-material/FilterList", + "@mui/icons-material/FlightClass", + "@mui/icons-material/Folder", + "@mui/icons-material/FormatBold", + "@mui/icons-material/GetAppOutlined", + "@mui/icons-material/Group", + "@mui/icons-material/HelpOutline", + "@mui/icons-material/Image", + "@mui/icons-material/Info", + "@mui/icons-material/LibraryBooks", + "@mui/icons-material/Link", + "@mui/icons-material/List", + "@mui/icons-material/Loop", + "@mui/icons-material/MenuBook", + "@mui/icons-material/MoreVert", + "@mui/icons-material/NavigateBefore", + "@mui/icons-material/NavigateNext", + "@mui/icons-material/OfflinePin", + "@mui/icons-material/OfflineShare", + "@mui/icons-material/Pageview", + "@mui/icons-material/Pause", + "@mui/icons-material/People", + "@mui/icons-material/Person", + "@mui/icons-material/PlayArrow", + "@mui/icons-material/PlayArrowOutlined", + "@mui/icons-material/PublicOffOutlined", + "@mui/icons-material/PublicOutlined", + "@mui/icons-material/PublishedWithChanges", + "@mui/icons-material/RadioButtonChecked", + "@mui/icons-material/RadioButtonUnchecked", + "@mui/icons-material/RecordVoiceOver", + "@mui/icons-material/Remove", + "@mui/icons-material/RemoveRedEye", + "@mui/icons-material/Replay", + "@mui/icons-material/Report", + "@mui/icons-material/Search", + "@mui/icons-material/Settings", + "@mui/icons-material/SettingsBackupRestore", + "@mui/icons-material/SettingsVoice", + "@mui/icons-material/SkipPrevious", + "@mui/icons-material/SpeakerNotes", + "@mui/icons-material/Stop", + "@mui/icons-material/SupportAgent", + "@mui/icons-material/Sync", + "@mui/icons-material/SystemUpdateAlt", + "@mui/icons-material/Undo", + "@mui/icons-material/Visibility", + "@mui/icons-material/VisibilityOff", + "@mui/icons-material/Warning", + "@mui/icons-material/ZoomIn", + "@mui/icons-material/ZoomOut", + "@mui/material", + "@mui/material/Alert", + "@mui/material/Autocomplete", + "@mui/material/Box", + "@mui/material/CircularProgress", + "@mui/material/Dialog", + "@mui/material/DialogActions", + "@mui/material/DialogContent", + "@mui/material/DialogContentText", + "@mui/material/DialogTitle", + "@mui/material/Fab", + "@mui/material/IconButton", + "@mui/material/LinearProgress", + "@mui/material/Paper", + "@mui/material/Tab", + "@mui/material/Tabs", + "@mui/material/TextField", + "@mui/material/Typography", + "@mui/material/styles", + "@mui/x-data-grid", + "@mui/x-data-grid/locales", + "@mui/x-tree-view", + "@orbit/coordinator", + "@orbit/core", + "@orbit/indexeddb", + "@orbit/indexeddb-bucket", + "@orbit/jsonapi", + "@orbit/memory", + "@orbit/records", + "@orbit/serializers", + "@redux-devtools/extension", + "@uiw/react-color-colorful", + "@wavesurfer/react", + "@xmldom/xmldom", + "array-move", + "axios", + "browser-image-compression", + "jszip", + "jwt-decode", + "lodash", + "luxon", + "mui-language-picker", + "path-browserify", + "process", + "react", + "react-dom", + "react-dom/client", + "react-draggable", + "react-file-drop", + "react-icons/fa", + "react-icons/io", + "react-localization", + "react-markdown", + "react-redux", + "react-router-dom", + "react-string-replace", + "react/jsx-dev-runtime", + "react/jsx-runtime", + "redux", + "redux-thunk", + "remark-gfm", + "url-parse", + "usfm-grammar-web/dist/bundle.mjs", + "wavesurfer.js", + "wavesurfer.js/dist/plugins/regions", + "wavesurfer.js/dist/plugins/timeline", + "wavesurfer.js/dist/plugins/zoom", + "xpath" +] diff --git a/src/renderer/cypress/config/local.config.ts b/src/renderer/cypress/config/local.config.ts index bcad38ee6..6c424d112 100644 --- a/src/renderer/cypress/config/local.config.ts +++ b/src/renderer/cypress/config/local.config.ts @@ -3,6 +3,8 @@ import { devServer } from '@cypress/vite-dev-server'; import { baseConfig } from './base.config'; import tasks from '../support/tasks'; import muteBrowserAudio from '../support/muteBrowserAudio'; +// eslint-disable-next-line @typescript-eslint/no-require-imports +const cypressGrepPlugin = require('@cypress/grep/src/plugin'); import viteConfig from '../../vite.config'; import * as fs from 'fs'; import * as path from 'path'; @@ -88,6 +90,20 @@ const testViteConfig = { ], // Pre-bundle common CT deps so the first spec does not hit "optimized dependencies // changed, reloading" mid-run (which can flake the first assertion attempt). + // Pre-bundle CT deps so no spec hits "optimized dependencies changed, + // reloading" mid-run. That reload is not just slow: it leaves the AUT with + // two copies of React ("Cannot read properties of null (reading 'useMemo')" + // inside ThemeProvider), so the spec that triggers it fails outright. + // + // This list has to cover every dep the *whole* run touches, not just the + // common ones — Vite discovers the rest lazily, one spec at a time, and each + // discovery is another reload. A hand-written list of the obvious ones is + // what made a separate whole-suite warm-up run necessary. + // + // ct-optimize-deps.json is generated from a completed run's dep cache: + // node env-config/ctOptimizeDeps.cjs --write + // Regenerate it if flaky "reloading"-related failures reappear after adding + // a dependency. See env-config/ctOptimizeDeps.cjs. optimizeDeps: { ...viteConfig.optimizeDeps, // Do not hold the first spec import until crawl-end — Chrome times out @@ -104,126 +120,12 @@ const testViteConfig = { ...(Array.isArray(viteConfig.optimizeDeps?.include) ? viteConfig.optimizeDeps.include : []), - 'react', - 'react-dom', - 'react/jsx-runtime', - 'cypress/react', - '@cypress/grep', - '@mui/material', - '@mui/material/styles', - '@mui/icons-material/PlayArrowOutlined', - '@mui/icons-material/PlayArrow', - '@mui/icons-material/Pause', - '@mui/icons-material/Stop', - '@mui/icons-material/GetAppOutlined', - '@mui/icons-material/ChevronLeft', - '@mui/icons-material/Loop', - '@mui/icons-material/ArrowRightAlt', - '@mui/icons-material/AccessTime', - '@mui/icons-material/Undo', - '@mui/icons-material/SettingsVoice', - '@mui/icons-material/MoreVert', - '@mui/icons-material/Settings', - '@mui/icons-material/List', - '@mui/icons-material/DeleteOutline', - '@mui/icons-material/CloudUpload', - '@bugsnag/js', - '@bugsnag/plugin-react', - // Deep-mount harnesses (e.g. cypress/support/pbtHarness.tsx) bring the - // store and Orbit in. Without these, the first spec that imports one - // triggers a mid-run re-optimize, and the reload that follows leaves the - // AUT with two copies of React ("Cannot read properties of null - // (reading 'useMemo')" inside ThemeProvider). - 'react-redux', - 'redux', - 'redux-thunk', - 'lodash', - 'react-is', - '@orbit/memory', - '@orbit/records', - '@orbit/coordinator', - '@orbit/indexeddb', - '@orbit/jsonapi', - 'wavesurfer.js', - 'wavesurfer.js/dist/plugins/regions', - 'wavesurfer.js/dist/plugins/timeline', - 'wavesurfer.js/dist/plugins/zoom', - '@wavesurfer/react', - // Captured from a CT run: "new dependencies optimized" during the first - // spec import. If these are only discovered then, Vite reloads and Chrome - // reports Failed to fetch dynamically imported module. - 'react-router-dom', - 'react-localization', - '@redux-devtools/extension', - '@mui/icons-material/Replay', - '@mui/icons-material/SkipPrevious', - '@mui/icons-material/Close', - '@mui/material/Alert', - '@mui/material/DialogActions', - 'luxon', - 'path-browserify', - 'react-icons/fa', - '@orbit/core', - '@mui/x-data-grid/locales', - 'mui-language-picker', - 'axios', - '@mui/icons-material/CheckBoxOutlineBlank', - '@mui/icons-material/CheckBoxOutlined', - '@mui/icons-material/Delete', - '@mui/icons-material/Edit', - '@fingerprintjs/fingerprintjs', - 'react-icons/io', - '@mui/icons-material/Remove', - '@mui/icons-material/Add', - '@mui/icons-material/ZoomIn', - '@mui/icons-material/ZoomOut', - '@mui/icons-material/Pageview', - 'url-parse', - 'process', - '@mui/icons-material/Visibility', - '@auth0/auth0-react', - 'jwt-decode', - '@mui/material/Paper', - 'react-draggable', - '@mui/icons-material/ArrowDropDown', - '@mui/material/TextField', - '@mui/material/Autocomplete', - '@mui/icons-material/SupportAgent', - '@mui/material/Dialog', - '@mui/material/DialogTitle', - '@mui/material/DialogContent', - '@mui/icons-material/Info', - '@orbit/indexeddb-bucket', - '@mui/icons-material/NavigateBefore', - '@mui/icons-material/NavigateNext', - '@mui/x-data-grid', - '@orbit/serializers', - '@mui/icons-material/ExpandMore', - '@mui/icons-material/Sync', - '@mui/icons-material/Check', - 'browser-image-compression', - '@mui/material/Box', - '@fortawesome/free-solid-svg-icons', - '@fortawesome/free-regular-svg-icons', - '@fortawesome/react-fontawesome', - '@mui/icons-material/VisibilityOff', - 'array-move', - 'react-file-drop', - '@xmldom/xmldom', - 'xpath', - '@hello-pangea/dnd', - '@mui/icons-material/DragIndicator', - '@mui/icons-material/ChevronRight', - '@mui/icons-material/ExpandLess', - 'jszip', - '@mui/x-tree-view', - '@mui/icons-material/RemoveRedEye', - '@mui/icons-material/CheckBoxOutlineBlankOutlined', - '@mui/icons-material/ContentCopy', - '@mui/icons-material/Link', - 'react-markdown', - 'remark-gfm', - 'usfm-grammar-web/dist/bundle.mjs', + ...(JSON.parse( + fs.readFileSync( + path.resolve(__dirname, 'ct-optimize-deps.json'), + 'utf-8' + ) + ) as string[]), ], }, // Component tests and the app dev server run different vite configs. Sharing @@ -278,7 +180,17 @@ const config = { tasks(on); // Recording specs play real audio; keep the run silent. muteBrowserAudio(on); - return config; + // @cypress/grep's browser side is registered in support/commands.ts; this + // is its plugin half, which implements `grepFilterSpecs`. + // + // Note: in Cypress 15 component mode the rewritten specPattern this + // returns is ignored — the runner has already resolved the spec list by + // the time setupNodeEvents runs, so all specs load even when none of + // their tests match. That costs ~9s of fixed per-spec overhead each + // (browser + bundle), which is why cy:run-ct-smoke selects files with + // --spec instead and leaves grepTags to filter *within* those files. + // Left registered because it does work for --e2e. + return cypressGrepPlugin(config); }, devServer(devServerConfig: Cypress.DevServerConfig) { return devServer({ diff --git a/src/renderer/package.json b/src/renderer/package.json index 3321a6844..b938846fb 100644 --- a/src/renderer/package.json +++ b/src/renderer/package.json @@ -21,7 +21,11 @@ "cy:run-ct": "cypress run --component --browser chrome --config-file cypress/config/local.config.ts --env grepTags=-@known-defect", "cy:run-ct-known-defects": "cypress run --component --browser chrome --config-file cypress/config/local.config.ts --env grepTags=@known-defect", "cy:run-ct-all": "cypress run --component --browser chrome --config-file cypress/config/local.config.ts", - "cy:run-ct-fast": "npm run cy:run-ct -- --config video=false screenshot=false", + "cy:run-ct-smoke": "cypress run --component --browser chrome --config-file cypress/config/local.config.ts --spec \"src/burrito/BurritoWrapper.cy.tsx,src/components/App/OrgHead.cy.tsx,src/components/PassageDetail/PassageDetailPhraseBackTranslate.cy.tsx,src/components/PassageDetail/mobile/MobileWorkflowSteps.cy.tsx,src/components/PlayButton.cy.tsx,src/components/Sheet/PassageCard.cy.tsx,src/components/Sheet/PlanView.cy.tsx,src/components/StepEditor/StepEditor.cy.tsx,src/control/RecordButton.cy.tsx,src/routes/SwitchTeams.cy.tsx\" --env grepTags=@smoke,grepOmitFiltered=true", + "cy:smoke-specs": "cd ../.. && node env-config/smokeSpecs.cjs", + "cy:smoke-specs:write": "cd ../.. && node env-config/smokeSpecs.cjs --write", + "cy:ct-deps": "cd ../.. && node env-config/ctOptimizeDeps.cjs", + "cy:ct-deps:write": "cd ../.. && node env-config/ctOptimizeDeps.cjs --write", "cy:docker": "docker-compose up --abort-on-container-exit --exit-code-from cypress", "cy:docker:build": "docker-compose up --build --abort-on-container-exit --exit-code-from cypress", "cy:docker:down": "docker-compose down", diff --git a/src/renderer/src/burrito/BurritoWrapper.cy.tsx b/src/renderer/src/burrito/BurritoWrapper.cy.tsx index 646dd44fc..f08a35c84 100644 --- a/src/renderer/src/burrito/BurritoWrapper.cy.tsx +++ b/src/renderer/src/burrito/BurritoWrapper.cy.tsx @@ -200,7 +200,7 @@ const mockStore = createStore( applyMiddleware(thunk as never) ); -describe('BurritoWrapper', () => { +describe('BurritoWrapper', { tags: '@smoke' }, () => { const createInitialState = ( memory: Memory, overrides: Record = {} diff --git a/src/renderer/src/components/App/OrgHead.cy.tsx b/src/renderer/src/components/App/OrgHead.cy.tsx index 27850a751..557b22d7e 100644 --- a/src/renderer/src/components/App/OrgHead.cy.tsx +++ b/src/renderer/src/components/App/OrgHead.cy.tsx @@ -187,7 +187,7 @@ const mockStore = createStore( }) ); -describe('OrgHead', () => { +describe('OrgHead', { tags: '@smoke' }, () => { let mockTeamDelete: ReturnType; beforeEach(() => { diff --git a/src/renderer/src/components/PassageDetail/PassageDetailPhraseBackTranslate.cy.tsx b/src/renderer/src/components/PassageDetail/PassageDetailPhraseBackTranslate.cy.tsx index 02cced253..449b802e9 100644 --- a/src/renderer/src/components/PassageDetail/PassageDetailPhraseBackTranslate.cy.tsx +++ b/src/renderer/src/components/PassageDetail/PassageDetailPhraseBackTranslate.cy.tsx @@ -50,7 +50,7 @@ const SEGMENTS_SHORT_LAST = [ afterEach(() => pbtCleanup()); -describe('PBT listen pass', () => { +describe('PBT listen pass', { tags: '@smoke' }, () => { beforeEach(() => { mountPbt({ segments: SEGMENTS }); waitForPbtReady(); @@ -87,7 +87,7 @@ describe('PBT listen pass', () => { }); }); -describe('PBT record enablement', () => { +describe('PBT record enablement', { tags: '@smoke' }, () => { beforeEach(() => { mountPbt({ segments: SEGMENTS }); waitForPbtReady(); diff --git a/src/renderer/src/components/PassageDetail/mobile/MobileWorkflowSteps.cy.tsx b/src/renderer/src/components/PassageDetail/mobile/MobileWorkflowSteps.cy.tsx index 73933a662..0a7292734 100644 --- a/src/renderer/src/components/PassageDetail/mobile/MobileWorkflowSteps.cy.tsx +++ b/src/renderer/src/components/PassageDetail/mobile/MobileWorkflowSteps.cy.tsx @@ -327,7 +327,7 @@ const mountMobileWorkflowSteps = ({ ); }; -describe('MobileWorkflowSteps', () => { +describe('MobileWorkflowSteps', { tags: '@smoke' }, () => { describe('step progression mode', () => { it('renders workflow step parallelograms and current step label', () => { mountMobileWorkflowSteps({ isStepProgression: true }); diff --git a/src/renderer/src/components/PlayButton.cy.tsx b/src/renderer/src/components/PlayButton.cy.tsx index 22ce16e9c..e702edea3 100644 --- a/src/renderer/src/components/PlayButton.cy.tsx +++ b/src/renderer/src/components/PlayButton.cy.tsx @@ -8,7 +8,7 @@ import bugsnagClient from '../auth/bugsnagClient'; import Memory from '@orbit/memory'; import Coordinator from '@orbit/coordinator'; -describe('PlayButton', () => { +describe('PlayButton', { tags: '@smoke' }, () => { let mockOnPlayStatus: ReturnType; let mockOnPlayEnd: ReturnType; diff --git a/src/renderer/src/components/Sheet/PassageCard.cy.tsx b/src/renderer/src/components/Sheet/PassageCard.cy.tsx index 4315bfe52..9ced534ae 100644 --- a/src/renderer/src/components/Sheet/PassageCard.cy.tsx +++ b/src/renderer/src/components/Sheet/PassageCard.cy.tsx @@ -83,7 +83,7 @@ const mockStore = createStore( }) ); -describe('PassageCard', () => { +describe('PassageCard', { tags: '@smoke' }, () => { let mockHandleViewStep: ReturnType; let mockOnPlayStatus: ReturnType; diff --git a/src/renderer/src/components/Sheet/PlanView.cy.tsx b/src/renderer/src/components/Sheet/PlanView.cy.tsx index 1587ca490..d410bc03a 100644 --- a/src/renderer/src/components/Sheet/PlanView.cy.tsx +++ b/src/renderer/src/components/Sheet/PlanView.cy.tsx @@ -126,7 +126,7 @@ const mockStore = createStore( }) ); -describe('PlanView', () => { +describe('PlanView', { tags: '@smoke' }, () => { let mockHandlePublish: ReturnType; let mockHandleGraphic: ReturnType; diff --git a/src/renderer/src/components/StepEditor/StepEditor.cy.tsx b/src/renderer/src/components/StepEditor/StepEditor.cy.tsx index eb2b97f3a..211b17866 100644 --- a/src/renderer/src/components/StepEditor/StepEditor.cy.tsx +++ b/src/renderer/src/components/StepEditor/StepEditor.cy.tsx @@ -195,7 +195,7 @@ const mountStepEditor = (memory: Memory) => { ); }; -describe('StepEditor (Edit Workflow)', () => { +describe('StepEditor (Edit Workflow)', { tags: '@smoke' }, () => { it('loads org workflow steps and shows the top Add control', () => { const memory = createWorkflowStepMemory(TEST_ORG_ID, [ { id: 'wfs-1', name: 'Alpha Stage', sequencenum: 0 }, diff --git a/src/renderer/src/control/RecordButton.cy.tsx b/src/renderer/src/control/RecordButton.cy.tsx index 85a24b3e0..0ff41fdd5 100644 --- a/src/renderer/src/control/RecordButton.cy.tsx +++ b/src/renderer/src/control/RecordButton.cy.tsx @@ -36,7 +36,7 @@ const mountRecordButton = ( ); }; -describe('RecordButton', () => { +describe('RecordButton', { tags: '@smoke' }, () => { it('renders the default icon button when not recording', () => { mountRecordButton({ recording: false, diff --git a/src/renderer/src/routes/SwitchTeams.cy.tsx b/src/renderer/src/routes/SwitchTeams.cy.tsx index 5dbb2907d..17c9508f8 100644 --- a/src/renderer/src/routes/SwitchTeams.cy.tsx +++ b/src/renderer/src/routes/SwitchTeams.cy.tsx @@ -313,7 +313,7 @@ beforeEach(() => { }); }); -describe('SwitchTeams sections and cards', () => { +describe('SwitchTeams sections and cards', { tags: '@smoke' }, () => { it('renders the picker inside the app chrome', () => { mountSwitchTeams(); @@ -380,7 +380,7 @@ describe('SwitchTeams sections and cards', () => { }); }); -describe('SwitchTeams header actions', () => { +describe('SwitchTeams header actions', { tags: '@smoke' }, () => { it('shows Add Team when online and connected', () => { mountSwitchTeams(); @@ -457,7 +457,7 @@ describe('SwitchTeams header actions', () => { }); }); -describe('SwitchTeams add team dialog', () => { +describe('SwitchTeams add team dialog', { tags: '@smoke' }, () => { it('opens the Add Team dialog and closes it again on cancel', () => { mountSwitchTeams(); @@ -493,7 +493,7 @@ describe('SwitchTeams add team dialog', () => { }); }); -describe('SwitchTeams shared content creator dialog', () => { +describe('SwitchTeams shared content creator dialog', { tags: '@smoke' }, () => { const openDialog = () => { mountSwitchTeams({ global: { offline: false }, sharedContentAdmin: true }); cy.get('#contentCreator').click(); @@ -532,7 +532,7 @@ describe('SwitchTeams shared content creator dialog', () => { }); }); -describe('SwitchTeams import dialog', () => { +describe('SwitchTeams import dialog', { tags: '@smoke' }, () => { it('opens the import dialog from the Import button', () => { // Online keeps offerPtf true so ImportTab only shows type selection; offline + // browser runs electron import in useEffect, gets invalid data, and closes @@ -545,7 +545,7 @@ describe('SwitchTeams import dialog', () => { }); }); -describe('SwitchTeams settings button visibility', () => { +describe('SwitchTeams settings button visibility', { tags: '@smoke' }, () => { const teams = [createTeam('team-a', 'Alpha Team')]; it('shows the settings button on the personal card and on an administered team', () => { @@ -585,7 +585,7 @@ describe('SwitchTeams settings button visibility', () => { }); }); -describe('SwitchTeams settings dialog', () => { +describe('SwitchTeams settings dialog', { tags: '@smoke' }, () => { const teams = [createTeam('team-a', 'Alpha Team')]; it('opens personal settings without a name field or delete section', () => { @@ -658,7 +658,7 @@ describe('SwitchTeams settings dialog', () => { }); }); -describe('SwitchTeams PAP-like guard', () => { +describe('SwitchTeams PAP-like guard', { tags: '@smoke' }, () => { const papLike = { teams: [], personalTeam: PERSONAL_TEAM,