Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ jobs:
- run: npm run check:workspaces
- run: npm run typecheck
- run: npm test

e2e:
name: E2E
uses: ./.github/workflows/e2e.yml
5 changes: 5 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ permissions:
contents: read

jobs:
e2e:
name: E2E
uses: ./.github/workflows/e2e.yml

deploy:
name: Build & Deploy
needs: e2e
runs-on: ubuntu-latest

steps:
Expand Down
70 changes: 70 additions & 0 deletions .github/workflows/e2e-update-screenshots.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: E2E — update screenshots

# Regenerates the e2e screenshot baselines in the same Playwright image that
# compares them, and commits them to the PR branch.
#
# Trigger: add the `update-screenshots` label to a PR (the label is removed
# again when the run ends). Only baselines that changed or are missing are
# rewritten. Review the new images in the PR diff before merging.
#
# A push made with GITHUB_TOKEN does not start other workflows: re-run the
# CI checks on the PR afterwards (or push any commit).

on:
pull_request:
types: [labeled]

permissions:
contents: write
pull-requests: write

concurrency:
group: e2e-update-screenshots-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
update:
name: Update screenshot baselines
if: github.event.label.name == 'update-screenshots' && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 20
container:
image: mcr.microsoft.com/playwright:v1.63.0-noble
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- name: Build Studio and Hub
run: npm run build:web && npm run build:hub
- name: Regenerate baselines
# Failures other than screenshot diffs still fail the run below; the
# baselines are written either way.
run: npm run e2e -w @radical/e2e -- --update-snapshots=changed --retries=0 || true
- name: Verify
run: npm run e2e -w @radical/e2e -- --retries=0
- name: Commit
run: |
git config --global --add safe.directory "$GITHUB_WORKSPACE"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add apps/e2e/tests
if git diff --cached --quiet; then
echo "Screenshots are up to date."
else
git commit -m "test(e2e): update screenshot baselines"
git push
fi
- name: Remove the label
if: always()
env:
GH_TOKEN: ${{ github.token }}
run: |
curl -sS -X DELETE \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels/update-screenshots"
42 changes: 42 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: E2E

# End-to-end regression suite (apps/e2e): Playwright against the production
# web builds of Studio and Hub, including screenshot comparison.
#
# Called by ci.yml and deploy.yml. Runs inside the Playwright image so fonts
# and rendering match the committed screenshot baselines; keep the image tag
# equal to the @playwright/test version in apps/e2e/package.json.

on:
workflow_call:

permissions:
contents: read

jobs:
e2e:
name: E2E (Playwright)
runs-on: ubuntu-latest
timeout-minutes: 20
container:
image: mcr.microsoft.com/playwright:v1.63.0-noble
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- name: Build Studio and Hub
run: npm run build:web && npm run build:hub
- name: Run the suite
run: npm run e2e -w @radical/e2e
- name: Upload report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: e2e-report
path: |
apps/e2e/playwright-report/
apps/e2e/test-results/
retention-days: 14
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,8 @@ infra/override.tf.json
# Note: infra/.terraform.lock.hcl IS committed (pins provider versions)
# Note: packages/hub-catalogue/catalogue/**/*.radical IS committed — it is the canonical
# concept catalogue served at hub.radical.tools/hub/ (index.json is generated)

# E2E (Playwright) run output; baselines under apps/e2e/tests/__screenshots__ are committed
apps/e2e/test-results/
apps/e2e/playwright-report/
apps/e2e/blob-report/
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ cd radical.tools
npm install
npm run dev # Electron app with hot-reload
npm test # Run tests
npm run e2e # End-to-end suite (Playwright), see apps/e2e/README.md
npm run typecheck # Type-check
```

Expand All @@ -40,6 +41,7 @@ apps/
tests/ — Vitest unit tests
vscode/ — VS Code extension
hub/ — Architecture Hub read-only viewer
e2e/ — End-to-end regression suite (Playwright)
mcp/ — MCP server (empty for now)
web/ — Marketing site (radical.tools)
packages/
Expand All @@ -66,7 +68,7 @@ packages/
## Daily workflow (for maintainers)

The `main` branch is protected — direct pushes are rejected. All changes go through a PR.
CI (`Type-check & Test`) must pass before merging.
CI (`Type-check & Test` and `E2E`) must pass before merging.

Recommended git aliases (set up once globally):

Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ apps/
hub/ Architecture Hub (hub.radical.tools): read-only viewer,
built on @radical/ui
vscode/ VS Code extension; bundles the Studio web build as its webview
e2e/ End-to-end regression suite (Playwright) for the web builds
of Studio and Hub, with screenshot baselines
mcp/ MCP server for radical models (empty for now)
web/ Marketing site and manual (radical.tools)
packages/
Expand Down Expand Up @@ -170,8 +172,14 @@ npm test

# Watch mode
npm run test:watch

# End-to-end regression suite (Playwright) against the web builds
npm run e2e
```

See [apps/e2e/README.md](apps/e2e/README.md) for the e2e suite and its
screenshot baselines.

## Contributing

Contributions are welcome: code, docs, bug reports, and new patterns for the [Architecture Hub](https://hub.radical.tools). Start with [CONTRIBUTING.md](CONTRIBUTING.md) and look for issues labelled [`good first issue`](https://github.com/radicaltools/radical.tools/labels/good%20first%20issue).
Expand Down
73 changes: 73 additions & 0 deletions apps/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# @radical/e2e

End-to-end regression suite for the web builds of Studio and Hub, written
with [Playwright](https://playwright.dev). It drives the production bundles
(`vite preview` of each app's `out/`), so it checks what gets deployed.

## Running

From the repo root:

```bash
npm run e2e # build Studio + Hub, run the suite
npm run e2e -w @radical/e2e # run it against the existing builds
npm run e2e -w @radical/e2e -- -g "undo" # one test by name
npm run e2e:ui -w @radical/e2e # Playwright UI mode
npm run e2e:report -w @radical/e2e # open the last HTML report
```

The first time, install Chromium: `npx playwright install chromium`.

## What it covers

| Spec | |
|---|---|
| `studio/boot` | Welcome screen, the Fintech sample, a new empty model |
| `studio/editing` | Palette drop, rename, Alt-drag connect, forbidden relation, undo/redo, delete, reload |
| `studio/views` | Static, nested, dynamic and table views; sample treemap, matrix, wiki; Viewer and Presenter perspectives |
| `studio/layout` | Smart Layout: no overlapping siblings, children inside their parent, same result on every run |
| `studio/presentation` | Playing slides, keyboard navigation, slide deep links |
| `studio/export` | PNG and SVG export produce real files |
| `studio/known-issues` | Open bugs, marked `test.fail` (see below) |
| `hub/hub` | Landing page, categories, search, concept canvas / wiki / table, `.radical` download |

Every test also fails on an uncaught exception or a `console.error`, and
blocks network requests that leave `localhost`. Hub's requests to
`hub.radical.tools` are answered from this build's `out/hub/`, so the suite
tests the catalogue that is about to be deployed.

Tests start from a known document instead of clicking one together: see
`fixtures/bookstore.radical` and `Studio.seed()` in `support/fixtures.ts`.

## Screenshots

Visual checks use `toHaveScreenshot`. Baselines live in
`tests/**/__screenshots__/` and come **only from Linux**, from the
`mcr.microsoft.com/playwright` image that CI runs in: fonts and anti-aliasing
differ between systems, so a baseline made on macOS would never match CI. On
macOS and Windows the screenshot assertions are skipped and everything else
still runs (`E2E_VISUAL=1` forces them, for local experiments only).

Screenshot tests call `studio.freezeTime()` first. The page clock (timers,
`requestAnimationFrame`) then only moves when the test advances it, so
animations and the live layout render the same frame on every run.

**Updating baselines** after an intended visual change: add the
`update-screenshots` label to the PR. The `E2E — update screenshots` workflow
regenerates the baselines that changed, commits them to the branch and removes
the label. Check the images in the PR diff, then re-run the CI checks (a bot
push does not start them).

## Known issues

`tests/studio/known-issues.spec.ts` holds tests for bugs that are not fixed
yet. Each one asserts the correct behaviour and is marked `test.fail`, so it
passes while the bug is there. When a fix lands, Playwright reports "expected
to fail, but passed": delete the `test.fail` line and the test becomes a
regression guard.

## Upgrading Playwright

`@playwright/test` is pinned to an exact version. Bump it together with the
image tag in `.github/workflows/e2e.yml` and `e2e-update-screenshots.yml`,
then regenerate the baselines.
67 changes: 67 additions & 0 deletions apps/e2e/fixtures/bookstore.radical
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"nodes": [
{ "id": "customer", "type": "person", "label": "Customer",
"description": "Buys books online", "x": 355, "y": 0,
"width": 150, "height": 170, "collapsed": false },
{ "id": "bookstore", "type": "system", "label": "Bookstore",
"description": "Online book shop", "x": 0, "y": 270,
"width": 860, "height": 350, "collapsed": false },
{ "id": "web", "type": "webapp", "label": "Web App", "technology": "React",
"description": "Catalogue and checkout UI", "parentId": "bookstore",
"x": 30, "y": 120, "width": 210, "height": 140, "collapsed": false },
{ "id": "api", "type": "container", "label": "API", "technology": "Node.js",
"description": "Orders and catalogue API", "parentId": "bookstore",
"x": 290, "y": 120, "width": 300, "height": 200, "collapsed": false },
{ "id": "db", "type": "database", "label": "Orders DB", "technology": "PostgreSQL",
"description": "Orders and stock", "parentId": "bookstore",
"x": 640, "y": 155, "width": 190, "height": 130, "collapsed": false },
{ "id": "payments", "type": "system", "label": "Payment Provider", "external": true,
"description": "Card payments", "x": 960, "y": 355,
"width": 300, "height": 180, "collapsed": false }
],
"relations": [
{ "id": "r1", "sourceId": "customer", "targetId": "web", "label": "browses and orders", "technology": "HTTPS" },
{ "id": "r2", "sourceId": "web", "targetId": "api", "label": "calls", "technology": "JSON/HTTPS" },
{ "id": "r3", "sourceId": "api", "targetId": "db", "label": "reads/writes", "technology": "SQL" },
{ "id": "r4", "sourceId": "api", "targetId": "payments", "label": "charges card", "technology": "REST" }
],
"sequences": [
{ "id": "seq-order", "name": "Place Order", "relationIds": ["r1", "r2", "r3", "r4"],
"stepDescriptions": ["submits the order", "posts the order", "stores the order", "charges the card"] }
],
"views": [
{ "id": "v-context", "name": "System Context", "kind": "static",
"nodeIds": ["customer", "bookstore", "payments"],
"positions": {
"customer": { "x": 355, "y": 0, "width": 150, "height": 170 },
"bookstore": { "x": 250, "y": 270, "width": 360, "height": 260 },
"payments": { "x": 710, "y": 310, "width": 300, "height": 180 }
},
"hiddenRelationIds": [] },
{ "id": "v-containers", "name": "Containers", "kind": "static",
"nodeIds": ["customer", "bookstore", "web", "api", "db", "payments"],
"positions": {
"customer": { "x": 355, "y": 0, "width": 150, "height": 170 },
"bookstore": { "x": 0, "y": 270, "width": 860, "height": 350 },
"web": { "x": 30, "y": 120, "width": 210, "height": 140 },
"api": { "x": 290, "y": 120, "width": 300, "height": 200 },
"db": { "x": 640, "y": 155, "width": 190, "height": 130 },
"payments": { "x": 960, "y": 355, "width": 300, "height": 180 }
},
"hiddenRelationIds": [] },
{ "id": "v-order", "name": "Place Order Flow", "kind": "dynamic",
"nodeIds": ["customer", "web", "api", "db", "payments"],
"sequenceId": "seq-order", "positions": {}, "hiddenRelationIds": [] },
{ "id": "v-table", "name": "Element Table", "kind": "table",
"nodeIds": [], "positions": {}, "hiddenRelationIds": [] }
],
"snapshots": [],
"presentations": [
{ "id": "pres-main", "name": "Main", "slides": [
{ "id": "slide-1", "name": "Context", "snapshotId": null, "viewId": "v-context",
"viewport": { "x": 0, "y": 0, "zoom": 1 } },
{ "id": "slide-2", "name": "Containers", "snapshotId": null, "viewId": "v-containers",
"viewport": { "x": 0, "y": 0, "zoom": 1 } }
] }
]
}
20 changes: 20 additions & 0 deletions apps/e2e/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@radical/e2e",
"version": "0.0.0",
"private": true,
"description": "End-to-end regression suite (Playwright) for the web builds of Studio and Hub.",
"type": "module",
"scripts": {
"e2e": "playwright test",
"e2e:ui": "playwright test --ui",
"e2e:report": "playwright show-report",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@playwright/test": "1.63.0",
"@radical/hub": "*",
"@types/node": "^20.19.39",
"radical-model": "*",
"typescript": "^5.3.3"
}
}
Loading
Loading