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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
repository-cache: true
- name: Run E2E and visual tests
working-directory: examples/react
run: bazelisk test //:e2e_test //:visual_test //:component_test //:component_visual_test //:remote_integration_test --test_output=errors
run: bazelisk test //:e2e_test //:visual_test //:component_test //:component_visual_test //:remote_integration_test //:native_config_test //:native_visual_test --test_output=errors
- name: Upload visual test artifacts
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ owns typechecking and bundling; the rules own execution and baseline updates.
```starlark
load("@rules_web_e2e//e2e:defs.bzl", "web_e2e_test")
load("@rules_web_e2e//component:defs.bzl", "browser_shell", "component_browser_test")
load("@rules_web_e2e//vrt:defs.bzl", "component_visual_test")
load("@rules_web_e2e//vrt:defs.bzl", "component_visual_test", "visual_test")

web_e2e_test(
name = "editor_e2e",
tests = ":compiled_e2e_specs",
server = ":editor_test_server",
config = ":compiled_playwright_config", # use.baseURL + optional webServer
)

browser_shell(
Expand All @@ -42,6 +42,7 @@ component_visual_test(
| Input | Contract |
| --------------------------- | -------------------------------------------------------------------------------------------- |
| `tests` | Built ESM specs and dependencies; source files are typechecked/transpiled by the producer |
| `config` | Compiled native Playwright config: base URL, optional webServer, fixtures, reporters, and timeouts |
| `server` | Compiled adapter returning a ready URL and cleanup callback |
| `shell` | Built HTML/JS/CSS directory plus its entry point; served without a bundler |
| `base_url` / `base_url_env` | Existing application endpoint, replacing `server` or `shell` |
Expand All @@ -57,6 +58,10 @@ const matching: VisualMatching = {threshold: 0.1, maxDiffPixels: 0}
export default matching
```

Use `visual_test(tests = ":compiled_visual_specs", config = ":compiled_playwright_config", ...)`
for page screenshots and interaction-driven VRT with native `toHaveScreenshot`.
It shares `matching`, `baselines`, `baseline_dir`, and `.update` with gallery VRT.

Native component specs mount registered visuals with
`mount('moduleId/visualId', props)`. The same gallery supplies generated VRT
captures. `ComponentVisualModule` and `installVisualGallery` are exported from
Expand Down
39 changes: 30 additions & 9 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ browser, server lifecycle, reports, and baseline updates.
```starlark
load("@rules_web_e2e//e2e:defs.bzl", "web_e2e_test")
load("@rules_web_e2e//component:defs.bzl", "component_browser_test")
load("@rules_web_e2e//vrt:defs.bzl", "component_visual_test")
load("@rules_web_e2e//vrt:defs.bzl", "component_visual_test", "visual_test")
```

| Attribute | Default | Contract |
| --------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------ |
| `name` | Required | Test target name |
| `tests` | Required for E2E/component | Target containing compiled ESM `*.spec.js` and its dependencies; source JavaScript is rejected |
| `tests` | Required except gallery VRT | Target containing compiled ESM `*.spec.js` and its dependencies; source JavaScript is rejected |
| `server` | Unset | Target supplying one compiled default `ServerAdapter` export, with its runtime dependencies/data |
| `shell` | Unset | `browser_shell` target supplying built HTML, JavaScript, CSS, and other assets |
| `base_url` | Unset | Existing HTTP(S) app URL; caller owns readiness and lifetime |
Expand All @@ -24,32 +24,41 @@ load("@rules_web_e2e//vrt:defs.bzl", "component_visual_test")
| `config` | Generated by the runner | Optional compiled ESM module exporting native Playwright configuration |
| `data` | `[]` | Additional declared runtime files |
| `env`, `env_inherit` | `{}`, `[]` | Explicit values and inherited variable names |
| `network_origins_env` | `[]` | Explicit variable names containing optional extra HTTP(S) origins; unset values add nothing |
| `args` | `[]` | Default test selection flags; same allowlist as `--test_arg` |
| `network_origins` | `[]` | Extra HTTP(S) origins permitted through the browser tunnel |
| `execution_timeout_seconds` | `180` | Deadline per Playwright invocation; discovery and capture have separate limits |
| `timeout` | `"long"` | Independent Bazel test timeout category |
| `tags` | `[]` | Additional tags; local/manual/uncached restrictions remain |

Choose exactly one of `server`, `shell`, `base_url`, or `base_url_env`.
Choose one of `server`, `shell`, `base_url`, or `base_url_env`, or supply only a
`config` with `use.baseURL` and optional native `webServer`. These source attributes
are mutually exclusive; the config-only form avoids repeating server settings.
E2E selects compiled `*.spec.js` excluding `*.browser.spec.js` and
`*.visual.spec.js`; component tests select `*.browser.spec.js`. Helpers can be
included in the compiled target without being treated as specs. The producer
must include typechecking in its build graph; `ts_project` emitted outputs do
this, and the rules also request available `transitive_typecheck` outputs.
Shell producers must similarly depend on their typecheck action.

Only `component_visual_test` accepts:
Both `component_visual_test` and `visual_test` accept:

| Attribute | Default | Contract |
| -------------- | ------------------- | ------------------------------------------------------------------ |
| `matching` | Exact pixel budget | Compiled module exporting `VisualMatching` |
| `baselines` | `[]` | Existing PNG input labels |
| `baseline_dir` | `"__screenshots__"` | Package-relative directory exclusively owned by this visual target |

Visual tests generate captures from the gallery; they do not need `tests`.
An explicit `<name>.update` captures all enabled visuals before replacing PNGs
`component_visual_test` generates captures from the gallery and rejects `tests`.
`visual_test` runs compiled `*.spec.js` containing native `toHaveScreenshot`
assertions, including clicks and page navigation, without a gallery protocol.
An explicit `<name>.update` runs the entire visual target before replacing PNGs
and deleting stale PNGs. Other files remain. Missing baselines fail comparison.
Visual filters are rejected. E2E/component targets forward `--grep`,
`--grep-invert`, `--project`, and `--shard` via `--test_arg`.
`--grep-invert`, `--project`, and `--shard` via `--test_arg`. They also accept
explicit declared spec paths (source `.spec.ts`/`.spec.tsx` names map to compiled
`.spec.js`) and `--pass-with-no-tests` for tag-filtered CI shards. These options
remain forbidden for visual targets.

All targets also create `<name>_sources` and `<name>_inputs`; reserve those names.
`visual` and `component` are private mode switches, not caller attributes.
Expand Down Expand Up @@ -125,14 +134,26 @@ and readiness hooks remain in the visual declarations.

## Optional Playwright configuration

Most call sites need no config. To add fixtures, global setup, timeouts, or E2E
A config-only target sets `use.baseURL`; Playwright owns its optional `webServer`
startup, readiness checks, and teardown. Multiple servers are supported. Relative
server working directories, global setup/teardown, and reporter modules resolve
against the compiled config. Existing servers are never reused. Include server
executables, assets, and setup modules in the config's runfiles or `data`.
The runner inspects the config in a separate process before allocating a browser,
then Playwright loads it normally: keep top-level config evaluation declarative.
Projects must share the same origin; additional service origins still require an
explicit network allowlist. Compiled `.js` and `.mjs` config outputs are accepted.

Most built-shell and adapter call sites need no config. To add fixtures, global setup, timeouts, or E2E
projects, supply a compiled module exporting `PlaywrightTestConfig` from
`@playwright/test`. ESM config/spec graphs must include their `package.json`
module markers and runtime dependencies.

Defaults: Chromium, headless, one worker, no retries, 30-second tests,
1280×720, en-US, UTC, light theme, reduced motion. Component tests block service
workers. VRT disables animations, hides the caret, and captures at CSS scale.
workers. VRT disables animations, hides the caret, and defaults to CSS-scale
screenshots. Set `expect.toHaveScreenshot.scale = 'device'` in the compiled
Playwright config to preserve device-pixel baselines at higher pixel densities.
The runner retains ownership of discovery, browser connection, output paths,
required list/JUnit reports, and snapshot policy when composing overrides. Visual targets reject
Playwright projects; use separate targets and baseline directories instead.
Expand Down
5 changes: 4 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,14 @@ storage, separate from committed baselines.
| ------------------------ | ----------------------------------------------------- |
| `web_e2e_test` | Native specs against a managed server or existing URL |
| `component_browser_test` | Native mounts through a consumer gallery |
| `visual_test` | Native screenshot specs and baseline updates |
| `component_visual_test` | Generated visual captures and baseline updates |

See the [API reference](api.md) for attributes and configuration helpers.

Managed-server mode must own startup, readiness, ports, and teardown. Deployed
Server adapters own readiness and teardown; a config-only target delegates
its native `webServer` lifecycle to Playwright. Both paths share the same
runfiles staging, browser container, and exact-origin tunnel. Deployed
mode must explicitly opt into network access and consumer-provided auth setup;
it must not silently fall back to a local service or ambient credentials.

Expand Down
18 changes: 17 additions & 1 deletion docs/e2e.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ web_e2e_test(
)
```

Alternatively supply a built `shell` or an existing URL. See the
For an existing Playwright setup, pass `config = ":compiled_config"` instead of
`server`. Set `use.baseURL` and optional `webServer` in that config; Playwright
starts and stops the declared server. Declare its executable and assets as data.
A built `shell` or existing URL is also supported. See the
[setup guide](getting-started.md) and [all attributes](api.md).
The runner selects emitted `*.spec.js`, excluding component/visual specs.
It supplies `baseURL` and `VRT_APP_URL`; use `page.goto('./')` to preserve a
Expand Down Expand Up @@ -117,3 +120,16 @@ options work with VRT, whose `.update` remains explicit.
`//:remote_integration_test` in the React example starts an independent fixture
on a random port and verifies base paths, interactions, blocked undeclared
origins, and caller-owned server lifetime. CI needs no public test site.

## Interaction-driven visual tests

Use `visual_test` from `@rules_web_e2e//vrt:defs.bzl` for ordinary Playwright specs
that click around and call `expect(page).toHaveScreenshot('saved.png')`. Pass the
compiled specs, config (or server/shell/URL), matching policy, and baseline inputs.
`bazel run //:visual_test.update` replaces baselines only after the full suite succeeds.
The [native example](../examples/react/native.visual.spec.ts) exercises this path.

If an extra service endpoint changes between environments, declare its variable
name in `network_origins_env = ["AUTH_ORIGIN"]`. Only named, nonempty variables
are read, and each must be an exact HTTP(S) origin without paths, credentials,
or wildcards. Static endpoints remain in `network_origins`.
38 changes: 37 additions & 1 deletion examples/react/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,46 @@ load("@npm//:defs.bzl", "npm_link_all_packages")
load("@rules_web_e2e//component:defs.bzl", "browser_shell", "component_browser_test")
load("@rules_web_e2e//e2e:defs.bzl", "web_e2e_test")
load("@rules_web_e2e//playwright:defs.bzl", "playwright_runtime")
load("@rules_web_e2e//vrt:defs.bzl", "component_visual_test")
load("@rules_web_e2e//vrt:defs.bzl", "component_visual_test", "visual_test")

npm_link_all_packages(name = "node_modules")

js_library(
name = "native_config",
srcs = ["native.config.js"],
data = ["package.json"],
deps = [":typecheck_project"],
)

js_library(
name = "native_specs",
srcs = ["native.spec.js"],
deps = [":typecheck_project"],
)

js_library(
name = "native_visual_specs",
srcs = ["native.visual.spec.js"],
deps = [":typecheck_project"],
)

web_e2e_test(
name = "native_config_test",
config = ":native_config",
tests = ":native_specs",
)

visual_test(
name = "native_visual_test",
baseline_dir = "__native_screenshots__",
baselines = glob(
["__native_screenshots__/*.png"],
allow_empty = True,
),
config = ":native_config",
tests = ":native_visual_specs",
)

npm_link_package(
name = "node_modules/@rules-web-e2e/vrt",
src = "@rules_web_e2e//runtime:package",
Expand Down
Binary file added examples/react/__native_screenshots__/saved.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions examples/react/native-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {createServer} from 'node:http'

createServer((_, response) => {
response.setHeader('content-type', 'text/html')
response.end(
'<button style="width:200px;height:80px" onclick="this.textContent=\'Saved\'">Save</button>'
)
}).listen(Number(process.env.PORT), '127.0.0.1')
19 changes: 19 additions & 0 deletions examples/react/native.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type {PlaywrightTestConfig} from '@playwright/test'

const seed = process.env.TEST_TMPDIR ?? 'native-example'
const port =
20_000 +
[...seed].reduce(
(hash, character) => (hash * 31 + character.charCodeAt(0)) % 20_000,
0
)
const baseURL = `http://127.0.0.1:${port}`
const config: PlaywrightTestConfig = {
use: {baseURL},
webServer: {
command: `"${process.execPath}" native-server.js`,
url: baseURL,
env: {PORT: String(port)},
},
}
export default config
9 changes: 9 additions & 0 deletions examples/react/native.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {expect, test} from '@playwright/test'

test('native webServer serves the declared config endpoint', async ({page}) => {
await page.goto('/')
await page.getByRole('button', {name: 'Save', exact: true}).click()
await expect(
page.getByRole('button', {name: 'Saved', exact: true})
).toBeVisible()
})
9 changes: 9 additions & 0 deletions examples/react/native.visual.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {expect, test} from '@playwright/test'

test('native specs own the interaction before capture', async ({page}) => {
await page.goto('/')
await page.getByRole('button', {name: 'Save', exact: true}).click()
await expect(
page.getByRole('button', {name: 'Saved', exact: true})
).toHaveScreenshot('saved.png')
})
Loading