diff --git a/.forgejo/workflows/usecases.yaml b/.forgejo/workflows/usecases.yaml new file mode 100644 index 0000000..1652c3d --- /dev/null +++ b/.forgejo/workflows/usecases.yaml @@ -0,0 +1,38 @@ +# Uses the fleet's ubuntu-latest container label (node:20-bookworm). +# Archive checkout follows railiance-forge's non-root-host runner guidance. +name: Use-case regression gate + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + usecase-regressions: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Check out the triggering revision and prepare test tools + run: | + set -eu + apt-get update + apt-get install -y --no-install-recommends python3 python3-venv make ca-certificates curl + python3 -m venv /tmp/usecase-bootstrap + /tmp/usecase-bootstrap/bin/pip install uv==0.5.9 + SHORT=$(printf '%s' "$GITHUB_SHA" | cut -c1-12) + mkdir -p source + curl -fsSL "https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${SHORT}.tar.gz" -o /tmp/usecase-source.tar.gz + tar -xzf /tmp/usecase-source.tar.gz -C source --strip-components=1 + cd source + /tmp/usecase-bootstrap/bin/uv sync --locked --python 3.12 + npm ci --prefix scripts + cd scripts + npx playwright install --with-deps chromium + - name: Run full application and established use-case gate + run: | + set -eu + export PATH="/tmp/usecase-bootstrap/bin:$PATH" + cd source + make check + echo "JUnit report: source/var/test-results/check.xml" diff --git a/.gitignore b/.gitignore index 41e5283..1931c6b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ **/*cookie*.txt docs/migration/samples/_local/ scripts/node_modules/ -scripts/package-lock.json # Local env overrides .env diff --git a/AGENTS.md b/AGENTS.md index a03d35c..03d07a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -246,3 +246,18 @@ To create a new workplan: 1. Write the file following the format above 2. Run `statehub fix-consistency` locally; ask the operator only if the CLI or State Hub API is unavailable. + +## Established use-case regression gate + +For changes to established product behavior or its tests, run `make check` before +commit: it includes the required real-browser journeys and registry coverage +validation. `make test` is the fast iteration command and explicitly deselects +browser tests; it is not the full acceptance gate. Install locked Node/browser +prerequisites with `make setup-usecases` (see docs/usecases/README.md). + +Established journeys live in docs/usecases/registry.json, with user-confirmed +behavior contracts, synthetic fixtures and mandatory fast/browser test node IDs. +Do not skip/xfail or silently remove a protected journey to pass the gate. Update +contract, mapping and tests together for intentional user-approved changes. +Never use private protocols, live accounts, production URLs or email sends in +regression tests. Automated success does not mark transfer accepted by the user. diff --git a/Makefile b/Makefile index 9f51c6e..9fc1984 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help sync migrate run test lint shell +.PHONY: help sync migrate run test lint shell check test-usecases test-browser setup-usecases .DEFAULT_GOAL := help @@ -23,3 +23,16 @@ lint: ## ruff shell: ## Django shell uv run manage.py shell + +setup-usecases: ## Install locked Node deps and Chromium for use-case checks + npm ci --prefix scripts + cd scripts && npx playwright install chromium + +test-usecases: ## Required established journeys (fast + real browser), JUnit report + uv run pytest --verify-usecases -m usecase --junitxml=var/test-results/usecases.xml + +test-browser: ## Browser journeys against isolated test app + uv run pytest --verify-usecases -m browser --junitxml=var/test-results/browser.xml + +check: ## Full regression gate, including every established browser journey + uv run pytest --verify-usecases -m '' --junitxml=var/test-results/check.xml diff --git a/README.md b/README.md index fabbebc..5530a53 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ spaces/pages with Title/Abstract/Visual + page copy/transfer — uv sync uv run manage.py migrate make run # http://127.0.0.1:8008 -make test +make test # fast tests +make setup-usecases +make check # required full regression gate, including browser use cases ``` With `DEBUG` and `OIDC_ENABLED=false` (defaults), **Sign in** uses a local @@ -45,3 +47,10 @@ dev-claims form. Platform OIDC: set env vars in `docs/dev.md`. ## License Target Revenue Source License (TRSL V1C1). See [`LICENSE`](LICENSE). + +## Established use cases + +[Use-case collection and test infrastructure](docs/usecases/README.md) protects +user-confirmed workflows from regression, starting with ReichelAg weekly calls. +Run `make check` before accepting changes; automated success and productive +transfer acceptance remain separate. diff --git a/SCOPE.md b/SCOPE.md index 783e33f..50b7e3d 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -89,3 +89,9 @@ Delivery: `docs/deploy.md`; residual records: `docs/intakes/csoc-residuals.md`. First user-confirmed review case: ReichelAg weekly founder-call protocols (UC-008), full duplication and manual editing plus clipboard copy for external email. See docs/usecases/reichelag-weekly-call.md; live transfer is pending. + +Established use-case regression protection is in docs/usecases/registry.json +and tests/usecases/, starting with ReichelAg. `make check` runs fast/application +and actual-browser journeys with mandatory test coverage validation and JUnit +reports. Forgejo workflow is configured; remote runner/required-merge-check +acceptance remains CSOC-IN-0005. User transfer acceptance stays separate. diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index c881c8b..f1a6721 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -16,6 +16,7 @@ | workplan | CSOC-WP-0006 | finished | — | workplans/CSOC-WP-0006-stage-1-space-page-capabilities.md | | workplan | CSOC-WP-0007 | finished | — | workplans/CSOC-WP-0007-safe-transfer-readiness.md | | workplan | CSOC-WP-0008 | active | — | workplans/CSOC-WP-0008-production-transfer-acceptance.md | +| workplan | CSOC-WP-0009 | finished | — | workplans/CSOC-WP-0009-usecase-regression-infrastructure.md | | task | CSOC-WP-0001-T01 | done | — | workplans/CSOC-WP-0001-bubble-io-exit-assessment.md | | task | CSOC-WP-0001-T02 | done | — | workplans/CSOC-WP-0001-bubble-io-exit-assessment.md | | task | CSOC-WP-0001-T03 | done | — | workplans/CSOC-WP-0001-bubble-io-exit-assessment.md | @@ -62,7 +63,12 @@ | task | CSOC-WP-0008-T08 | progress | — | workplans/CSOC-WP-0008-production-transfer-acceptance.md | | task | CSOC-WP-0008-T09 | progress | — | workplans/CSOC-WP-0008-production-transfer-acceptance.md | | task | CSOC-WP-0008-T10 | done | — | workplans/CSOC-WP-0008-production-transfer-acceptance.md | +| task | CSOC-WP-0009-T01 | done | — | workplans/CSOC-WP-0009-usecase-regression-infrastructure.md | +| task | CSOC-WP-0009-T02 | done | — | workplans/CSOC-WP-0009-usecase-regression-infrastructure.md | +| task | CSOC-WP-0009-T03 | done | — | workplans/CSOC-WP-0009-usecase-regression-infrastructure.md | +| task | CSOC-WP-0009-T04 | done | — | workplans/CSOC-WP-0009-usecase-regression-infrastructure.md | | intake | CSOC-IN-0001 | closed | blue | docs/intakes/csoc-residuals.md | | intake | CSOC-IN-0002 | closed | green | docs/intakes/csoc-residuals.md | | intake | CSOC-IN-0003 | open | blue | docs/intakes/csoc-residuals.md | | intake | CSOC-IN-0004 | open | green | docs/intakes/csoc-residuals.md | +| intake | CSOC-IN-0005 | open | green | docs/intakes/csoc-residuals.md | diff --git a/docs/intakes/csoc-residuals.md b/docs/intakes/csoc-residuals.md index 53dff53..038e578 100644 --- a/docs/intakes/csoc-residuals.md +++ b/docs/intakes/csoc-residuals.md @@ -142,3 +142,38 @@ notes: | Pickup: docs/session-pickup.md state_hub_intake_id: "019ffac3-fc7d-7850-93b6-b0d289881ec7" ``` + +--- + +## CSOC-IN-0005 — Activate required Forgejo use-case regression check + +```yaml +id: CSOC-IN-0005 +kind: intake +title: "Verify Forgejo use-case runner, retain reports and require the regression check" +lane: green +status: open +priority: high +owner: coulomb-social +repo: coulomb-social +origin: residual +origin_ref: CSOC-WP-0009 +description: | + Local make check and the ReichelAg fast/real-browser contract pass. + .forgejo/workflows/usecases.yaml configures main push, PR and manual runs + on the existing ubuntu-latest container label. The source SHA archive is + publicly readable (HTTP 200 checked 2026-10-06); no credential embedded. + + Through the Forgejo/railiance-forge owner lane, verify repo Actions and + runner admission, Python/Node/browser dependency bootstrap and the exact + usecase-regressions job on a triggering commit. Record a successful remote + run with the commit SHA, make check result and JUnit report receipt. + Configure report retention/artifacts and make the named job a required + merge check. Confirm a failing journey prevents merge and a passing repair + unblocks it. Do not claim branch enforcement from the local gate alone. +notes: | + Infrastructure home: docs/usecases/README.md. + Runner/profile changes and protected-branch administration require the + appropriate owner path; no shared runner or production change made here. +state_hub_intake_id: "01a1110f-e2ec-736f-8209-e14425b62373" +``` diff --git a/docs/session-pickup.md b/docs/session-pickup.md index 2be59c4..5680bb9 100644 --- a/docs/session-pickup.md +++ b/docs/session-pickup.md @@ -1,3 +1,14 @@ +# Use-case regression gate — 2026-10-06 + +WP-0009 establishes the use-case collection/test infrastructure for ReichelAg +UC-008. See docs/usecases/README.md and registry.json. Before accepting changes, +run `make check` (includes required real-browser journey); `make test` is fast +only. Synthetic fixtures and test DB/content are isolated from private notes. +CI runner/admission/required-check receipts remain CSOC-IN-0005; real-user +transfer/deployment remain WP-0008. + +--- + # Current implementation focus — 2026-10-06 CSOC-WP-0008 now targets the first **user-reviewed use-case transfer off Bubble**. diff --git a/docs/usecases/README.md b/docs/usecases/README.md new file mode 100644 index 0000000..e6862fa --- /dev/null +++ b/docs/usecases/README.md @@ -0,0 +1,118 @@ +# Use-case collection and regression infrastructure + +This collection records the jobs people actually do in coulomb.social and keeps +established behavior working while it is rebuilt and moved off Bubble. +Start with **CSOC-UC-008, ReichelAg weekly founder calls**: duplicate the full +previous protocol, edit the new agenda/notes manually, copy for external email. + +## Collection + +- `catalog.md`: discovery backlog, including inferred candidates. +- `registry.json`: executable index of established journeys and required tests. +- `reichelag-weekly-call.md`: user-confirmed behavior and remaining manual review. +- `first-transfer-review.md`: real-user migration acceptance walkthrough. +- `tests/usecases/fixtures/`: synthetic scenarios; never private protocol content. + +Confirmation, regression protection and transfer acceptance are independent. +`established: true` means a user-confirmed behavior contract must keep passing; +it does **not** mean the use case has been accepted for productive transfer. +Only the user's review can grant that, with an explicit acceptance reference. + +## Run the gate + +Requires Python 3.12+, uv, Node 20+ and npm. Locked Python/Node dependencies are +tracked. First setup (Chromium OS libraries must be available): + +```bash +uv sync --locked +make setup-usecases +# On a fresh Linux CI runner, install browser OS libraries too: +cd scripts && npx playwright install --with-deps chromium +``` + +From the repository root: + +```bash +make test # Fast application checks; browser tests explicitly deselected +make test-usecases # All registered journey tests, including actual browser UI +make test-browser # Only browser journeys, still validates registry coverage +make check # Required full application + established use-case/browser gate +``` + +`make check` is the release/change validation command. Browser tests are required, +not conditionally skipped when Node, Playwright or Chromium is absent. A missing +dependency or test mapping fails the gate. JUnit results are written under +`var/test-results/` (gitignored). The ordinary `make test` is intentionally the +fast iteration command, not proof that all established journeys still work. + +The registry validates **collected node IDs before deselection**: deleting or +renaming a protected test, removing its use-case marker, or giving it the wrong +browser/fast layer fails with `Use-case regression contract failed`. Disabling a +protected check with skip/xfail is also rejected. Update mappings deliberately +when refactoring; do not delete a required behavior to turn a failure green. + +## What ReichelAg protection covers + +Fast test: real application POSTs, full protocol duplication, edit/source +independence, rendered clipboard controls and reader access. + +Browser test: isolated Django DB and temporary content root; synthetic writer, +reader and outsider; actual sign-in/list/page/duplicate/edit/save/reopen screens; +real static clipboard script; heading/list export as HTML/plain text; blocked +clipboard manual selection; sample rich email-editor consumption; reader mutation +and outsider read denial. It checks that browser writes persist and the original +protocol remains unchanged. Unexpected external browser requests are blocked. + +Clipboard writes are intercepted using native browser ClipboardItem objects; +there is no OS clipboard export of private notes, external issuer or email send. +The test uses developer sign-in in its isolated test instance. It does **not** +prove production OIDC, restart/backup recovery, actual ReichelAg corpus fidelity, +or compatibility with the user's usual mail client. Those remain WP-0008 review +and deployment tasks. + +The smaller `node scripts/test-copy-content.mjs` check remains available for +clipboard component debugging; the required browser gate exercises the actual +Django screens instead of substituting a hand-built page. + +## Add the next established use case + +1. Give it a stable `CSOC-UC-NNN` ID. Describe actor, goal, original steps, + expected outcome, evidence confidence, privacy and migration differences. + Keep an inferred case in the catalogue until the user confirms it. +2. Write a synthetic fixture with `synthetic: true` and matching `usecase_id`. + No real business content, member identifiers, tokens or recordings in fixtures. +3. Add fast and real-browser tests with `@pytest.mark.usecase("CSOC-UC-NNN")`; + browser tests also have `@pytest.mark.browser`. Use the isolated live-server + pattern; drive visible user steps rather than recreating app markup. +4. Register exact test node IDs, narrative/fixture paths and the preserved behavior + contract in `registry.json`; run `make check`. Established tests cannot skip. +5. Separately record real-user review/transfer acceptance with its receipt. + If behavior changes intentionally, update the user-approved contract and tests + together; keep unresolved gaps in live work records. + +## CI and operational boundary + +`.forgejo/workflows/usecases.yaml` requests the existing fleet ubuntu-latest +container runner on main pushes, pull requests and manual dispatch. It uses the +triggering SHA archive, locked installs, browser OS dependencies and `make check`. +It neither deploys the app nor imports production data. No credentials are +embedded; archive checkout needs existing public/read admission. + +Local gate success is not a remote CI receipt. Confirm the runner label can run +the job and the repository has Actions enabled; preserve reports/artifacts and +make `usecase-regressions` a required merge check through the Forgejo owner lane. +That external admission/enforcement work is tracked as **CSOC-IN-0005**. Until +confirmed, the workflow is configured and the local gate is mandatory by this +repository's documented process; protected-branch enforcement is not claimed. + +## Verification receipt — 2026-10-06 + +- `make check`: 88 passed, including the actual ReichelAg browser journey. +- `make test-usecases` equivalent command: 2 passed (fast + browser), 86 deselected. +- JUnit XML: `var/test-results/check.xml` (88 tests, 0 failures/errors) and + `usecases.xml` (2 tests, 0 failures/errors). +- Deliberately collecting only `tests/test_reichelag_workflow.py` with + `--verify-usecases` returns exit 4 and identifies the missing browser check. +- Ruff passes changed Python infrastructure/test files; whitespace check passes. +- Remote Forgejo run and merge enforcement are not asserted; CSOC-IN-0005 owns + those receipts. Existing repository-wide lint debt remains WP-0008-T07. diff --git a/docs/usecases/catalog.md b/docs/usecases/catalog.md index cdcefad..4e56565 100644 --- a/docs/usecases/catalog.md +++ b/docs/usecases/catalog.md @@ -7,6 +7,10 @@ Bubble inventory/capability map; no fresh authenticated Bubble observation has been performed in this session. All transfer decisions are **pending user review**. ReichelAg weekly founder calls are now the first user-confirmed workflow: see reichelag-weekly-call.md. +Executable regression protection for established cases: registry.json and +[the test infrastructure](README.md). `make check` includes required browser +journeys; candidate entries below are not all established or protected yet. + ## Transfer workflow For every journey: capture a real example and original steps → describe actor, diff --git a/docs/usecases/registry.json b/docs/usecases/registry.json new file mode 100644 index 0000000..28c1914 --- /dev/null +++ b/docs/usecases/registry.json @@ -0,0 +1,31 @@ +{ + "schema_version": 1, + "cases": [ + { + "id": "CSOC-UC-008", + "title": "ReichelAg weekly founder-call protocols", + "established": true, + "confirmation": "user-confirmed", + "transfer_status": "pending-user-review", + "narrative": "docs/usecases/reichelag-weekly-call.md", + "fixture": "tests/usecases/fixtures/reichelag-weekly-call.json", + "contract": [ + "Duplicate the complete previous protocol without changing the original", + "Edit and save the next agenda/protocol by hand and reopen it", + "Copy only rendered content with headings and bullet structure for external email", + "Fall back to selected manual text when clipboard writes are denied", + "Readers may read and copy but not modify; outsiders cannot read", + "The app does not send the email or need Bubble for these steps" + ], + "tests": { + "fast": ["tests/test_reichelag_workflow.py::test_full_protocol_duplicate_edit_and_clipboard_control"], + "browser": ["tests/usecases/test_reichelag_browser.py::test_weekly_call_through_real_ui"] + }, + "manual_review": [ + "Reconcile the latest actual ReichelAg protocol and its links/assets", + "Paste into the user's usual mail client and review presentation", + "Confirm live persistence and measured paired restore before transfer" + ] + } + ] +} diff --git a/docs/usecases/reichelag-weekly-call.md b/docs/usecases/reichelag-weekly-call.md index d2aeb9e..5577b5a 100644 --- a/docs/usecases/reichelag-weekly-call.md +++ b/docs/usecases/reichelag-weekly-call.md @@ -79,3 +79,12 @@ and reader copy visibility. New workflow test passes Ruff. HTML/plain payload construction, plain-only fallback and denied/manual selection. Clipboard writes are intercepted with synthetic content; no real clipboard or email recipient is used. This does not certify pasting into the user's mail client. + + +## Continuous regression protection + +Registered in registry.json with a synthetic scenario and mandatory fast/browser +node IDs. `make check` drives the actual isolated app through duplication, manual +editing, reopen, HTML/plain/manual clipboard export and reader/outsider checks. +See README.md for setup, reports and extending protection to further cases. +This protects established behavior without marking productive transfer accepted. diff --git a/pyproject.toml b/pyproject.toml index 7bcef01..01a39bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,11 @@ DJANGO_SETTINGS_MODULE = "coulomb_social.settings.test" python_files = ["test_*.py", "tests.py"] python_classes = ["Test*"] python_functions = ["test_*"] -addopts = "--tb=short -q" +addopts = "--tb=short -q -m 'not browser'" +markers = [ + "usecase(id): regression protection for a registered user journey", + "browser: mandatory in make check; needs Node, Playwright and Chromium", +] testpaths = ["tests", "coulomb_social"] [tool.ruff] diff --git a/scripts/package-lock.json b/scripts/package-lock.json new file mode 100644 index 0000000..f487e90 --- /dev/null +++ b/scripts/package-lock.json @@ -0,0 +1,57 @@ +{ + "name": "coulomb-social-scripts", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "coulomb-social-scripts", + "dependencies": { + "playwright": "1.62.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + } + } +} diff --git a/scripts/package.json b/scripts/package.json index ecf078f..23b3dd6 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -4,6 +4,6 @@ "type": "module", "description": "Local tooling for auth capture and design extract (no app runtime)", "dependencies": { - "playwright": "^1.49.0" + "playwright": "1.62.1" } } diff --git a/scripts/test-usecase-browser.mjs b/scripts/test-usecase-browser.mjs new file mode 100644 index 0000000..3963bc0 --- /dev/null +++ b/scripts/test-usecase-browser.mjs @@ -0,0 +1,141 @@ +// Real UI traversal with synthetic protocols; no production hosts or email. +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import { chromium } from 'playwright'; + +const base = new URL(process.argv[2]); +if (base.protocol !== 'http:' || !['localhost', '127.0.0.1'].includes(base.hostname)) { + throw new Error('Use-case browser runner requires an isolated loopback test server'); +} +const scenario = JSON.parse(fs.readFileSync(process.argv[3], 'utf8')); +assert.equal(scenario.synthetic, true); +const browser = await chromium.launch({headless: true}); +try { + const context = await browser.newContext(); + // Fail closed on unexpected integrations. Only the isolated app can be contacted. + await context.route('**/*', route => { + if (new URL(route.request().url()).origin === base.origin) return route.continue(); + return route.abort(); + }); + await context.addInitScript(() => { + window.clipboardMode = 'rich'; + window.clipboardReceipts = []; + Object.defineProperty(navigator, 'clipboard', {configurable: true, value: { + write: async items => { + if (window.clipboardMode !== 'rich') throw new Error('Rich copy blocked'); + const item = items[0]; + window.clipboardReceipts.push({ + html: await (await item.getType('text/html')).text(), + text: await (await item.getType('text/plain')).text(), + }); + }, + writeText: async text => { + if (window.clipboardMode === 'denied') throw new Error('Copy blocked'); + window.clipboardReceipts.push({text}); + }, + }}); + }); + const page = await context.newPage(); + page.setDefaultTimeout(10000); + const jsErrors = []; + page.on('pageerror', error => jsErrors.push(error.message)); + const detail = `${base.origin}/app/spaces/${scenario.space_slug}/`; + const original = `${detail}?page=${scenario.previous_slug}`; + async function login(role) { + await page.goto(`${base.origin}/auth/dev-login/`); + await page.locator('input[name=subject]').fill(scenario[`${role}_subject`]); + await page.locator('input[name=tenant]').fill(scenario.tenant); + await page.getByRole('button', {name: 'Establish session'}).click(); + await page.waitForURL('**/app/'); + } + await login('writer'); + assert.match(await page.locator('main').innerText(), new RegExp(scenario.space_title)); + await page.locator(`a[href="/app/spaces/${scenario.space_slug}/"]`).click(); + await page.getByRole('link', {name: scenario.previous_title, exact: true}).click(); + await page.getByRole('link', {name: 'Duplicate page', exact: true}).click(); + await page.getByLabel('New page slug').fill(scenario.next_slug); + await page.locator('input[name=title]').fill(scenario.next_title); + await page.getByRole('button', {name: 'Create duplicate'}).click(); + await page.waitForURL(`**?page=${scenario.next_slug}`); + assert.match(await page.locator('#page-content').innerText(), /Completed point/); + await page.getByRole('link', {name: 'Edit page', exact: true}).click(); + assert.equal(await page.getByLabel('Body (markdown)').inputValue(), scenario.previous_body); + await page.getByLabel('Body (markdown)').fill(scenario.next_body); + await page.getByRole('button', {name: 'Save page'}).click(); + await page.waitForURL(`**?page=${scenario.next_slug}`); + await page.reload(); + assert.match(await page.locator('#page-content').innerText(), /Follow-up point/); + assert.doesNotMatch(await page.locator('#page-content').innerText(), /Completed point/); + // Execute the actual static script loaded by the real detail page, not a fixture DOM. + for (const mode of ['rich', 'plain', 'denied']) { + await page.evaluate(mode => { + window.clipboardMode = mode; + window.clipboardReceipts = []; + }, mode); + await page.getByRole('button', {name: 'Copy content to clipboard'}).click(); + await page.waitForFunction(() => !document.getElementById('copy-page-content').disabled); + const result = await page.evaluate(() => ({ + receipts: window.clipboardReceipts, + status: document.getElementById('copy-content-status').textContent, + manual: document.getElementById('copy-content-manual').value, + hidden: document.getElementById('copy-content-manual').hidden, + selected: document.getElementById('copy-content-manual').selectionEnd, + })); + const text = mode === 'denied' ? result.manual : result.receipts[0].text; + assert.match(text, /Next weekly call/); + assert.match(text, /- Follow-up point/); + assert.match(text, / - Nested detail/); + assert.match(text, /- Another agenda item/); + assert.match(text, /New notes with emphasis/); + assert.doesNotMatch(text, /Edit page|Duplicate page|Source:|content-plane|Sign out/); + if (mode === 'denied') { + assert.equal(result.receipts.length, 0); + assert.equal(result.hidden, false); + assert.equal(result.selected, text.length); + assert.match(result.status, /Ctrl\+C or Cmd\+C/); + assert.doesNotMatch(result.status, /Content copied/); + } else { + assert.match(result.status, /Content copied/); + if (mode === 'rich') { + const html = result.receipts[0].html; + assert.match(html, /