Skip to content

Commit ff2de6d

Browse files
committed
trying more ideas
1 parent 66eed8d commit ff2de6d

6 files changed

Lines changed: 97 additions & 61 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"scripts": {
1212
"build:scripts": "yarn dlx tsdown@0.20.0 scripts/*.jsx -d _scripts --no-clean --ext .mjs",
1313
"build:generate-llms": "node _scripts/generate_llms.mjs",
14-
"build:res": "rescript build --warn-error +3+8+11+12+26+27+31+32+33+34+35+39+44+45+110",
14+
"build:res": "rescript build --warn-error +3+8+11+12+26+27+31+32+33+34+35+39+44+45+110 && node scripts/postprocess-e2e.mjs",
1515
"build:sync-bundles": "node scripts/sync-playground-bundles.mjs",
1616
"build:update-index": "yarn build:generate-llms && node _scripts/generate_feed.mjs > public/blog/feed.xml",
1717
"build:vite": "react-router build",

playwright.config.mjs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,22 @@ const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:8788";
2121
const useLocalServer = !process.env.PLAYWRIGHT_BASE_URL;
2222

2323
export default defineConfig({
24+
/**
25+
* Apply a Babel plugin to rewrite the first parameter of ReScript-compiled
26+
* test callbacks from `async param => …` to `async ({ ...param }) => …`.
27+
*
28+
* Playwright requires the first parameter of every test/hook callback to use
29+
* object destructuring syntax so it can statically determine which fixtures
30+
* the test depends on. ReScript always compiles record-pattern arguments to a
31+
* plain identifier (`param`), which Playwright rejects.
32+
*
33+
* The plugin uses object-rest destructuring (`{ ...param }`) so the callback
34+
* body is completely unchanged — `param.page` etc. continue to work.
35+
*/
36+
build: {
37+
babelPlugins: [["./src/bindings/playwright-babel-plugin.mjs"]],
38+
},
39+
2440
/**
2541
* Start Wrangler Pages dev server automatically for local runs so that
2642
* `yarn e2e` works after `yarn build` with zero extra setup.

scripts/postprocess-e2e.mjs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env node
2+
/**
3+
* postprocess-e2e.mjs
4+
*
5+
* Rewrites ReScript-compiled e2e test files so that Playwright can parse
6+
* fixture dependencies from the test callback signatures.
7+
*
8+
* Playwright calls `fn.toString()` on every test/hook callback and requires
9+
* the first parameter to use object destructuring syntax, e.g.:
10+
*
11+
* async ({ page }) => { … }
12+
*
13+
* ReScript always compiles record-pattern arguments to a plain identifier:
14+
*
15+
* async param => { … }
16+
*
17+
* This script rewrites every occurrence of `async param =>` to
18+
* `async ({ ...param }) =>` in the generated e2e .jsx files. Using object-rest
19+
* (`{ ...param }`) satisfies Playwright's `{…}` check while leaving the
20+
* callback body completely unchanged — `param.page` etc. continue to work
21+
* because `param` is still in scope with all the same properties.
22+
*/
23+
24+
import { readFileSync, writeFileSync } from "node:fs";
25+
import { readdir } from "node:fs/promises";
26+
import { join } from "node:path";
27+
import { fileURLToPath } from "node:url";
28+
29+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
30+
const e2eDir = join(__dirname, "..", "e2e");
31+
32+
const files = (await readdir(e2eDir)).filter((f) => f.endsWith(".test.jsx"));
33+
34+
if (files.length === 0) {
35+
console.error("postprocess-e2e: no .test.jsx files found in e2e/");
36+
process.exit(1);
37+
}
38+
39+
for (const file of files) {
40+
const filePath = join(e2eDir, file);
41+
const original = readFileSync(filePath, "utf8");
42+
const rewritten = original.replaceAll(
43+
"async param =>",
44+
"async ({ ...param }) =>",
45+
);
46+
47+
if (rewritten !== original) {
48+
writeFileSync(filePath, rewritten, "utf8");
49+
console.log(`postprocess-e2e: patched ${file}`);
50+
} else {
51+
console.log(`postprocess-e2e: no changes needed in ${file}`);
52+
}
53+
}

src/bindings/Playwright.res

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,41 +27,48 @@ external context: page => browserContext = "context"
2727
* automatically when CHROMATIC_PROJECT_TOKEN is present in the environment;
2828
* when the token is absent every `takeSnapshot` call is a no-op so the suite
2929
* still runs as ordinary Playwright tests.
30+
*
31+
* `test` and all hooks are bound directly to `@chromatic-com/playwright` so
32+
* that Playwright's source-location tracking (which captures frame[1] of a
33+
* fresh `Error.captureStackTrace`) points at the test file rather than any
34+
* intermediate wrapper. ReScript's compiled callbacks are rewritten into the
35+
* required object-destructuring form by a Babel plugin configured in
36+
* `playwright.config.mjs`.
3037
* ─────────────────────────────────────────────────────────────────────────────
3138
*/
32-
@module("./playwright-shim.mjs")
39+
@module("@chromatic-com/playwright")
3340
external test: (string, fixtures => promise<unit>) => unit = "test"
3441

3542
/** Group related tests under a shared label. */
36-
@module("./playwright-shim.mjs") @scope("test")
43+
@module("@chromatic-com/playwright") @scope("test")
3744
external describe: (string, unit => unit) => unit = "describe"
3845

3946
/** Run a hook before every test in the current scope. */
40-
@module("./playwright-shim.mjs") @scope("test")
47+
@module("@chromatic-com/playwright") @scope("test")
4148
external beforeEach: (fixtures => promise<unit>) => unit = "beforeEach"
4249

4350
/** Run a hook after every test in the current scope. */
44-
@module("./playwright-shim.mjs") @scope("test")
51+
@module("@chromatic-com/playwright") @scope("test")
4552
external afterEach: (fixtures => promise<unit>) => unit = "afterEach"
4653

4754
/** Run a hook once before all tests in the current scope (worker-level). */
48-
@module("./playwright-shim.mjs") @scope("test")
55+
@module("@chromatic-com/playwright") @scope("test")
4956
external beforeAll: (fixtures => promise<unit>) => unit = "beforeAll"
5057

5158
/** Run a hook once after all tests in the current scope (worker-level). */
52-
@module("./playwright-shim.mjs") @scope("test")
59+
@module("@chromatic-com/playwright") @scope("test")
5360
external afterAll: (fixtures => promise<unit>) => unit = "afterAll"
5461

5562
/**
5663
* Mark a test as the only one that should run in this file while debugging.
5764
* Tests decorated with `only` must not be committed — set `forbidOnly: true`
5865
* in playwright.config.mjs to enforce this in CI.
5966
*/
60-
@module("./playwright-shim.mjs") @scope("test")
67+
@module("@chromatic-com/playwright") @scope("test")
6168
external only: (string, fixtures => promise<unit>) => unit = "only"
6269

6370
/** Skip a test unconditionally. */
64-
@module("./playwright-shim.mjs") @scope("test")
71+
@module("@chromatic-com/playwright") @scope("test")
6572
external skip: (string, fixtures => promise<unit>) => unit = "skip"
6673

6774
/**

src/bindings/playwright-shim.mjs

Lines changed: 12 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,26 @@
11
/**
22
* playwright-shim.mjs
33
*
4-
* Playwright's fixture-injection system inspects the source of the callback
5-
* passed to `test()` and requires the first parameter to use object
6-
* destructuring syntax, e.g. `async ({ page }) => {}`.
4+
* Provides `takeSnapshot` (with viewport-clipping and automatic testInfo
5+
* injection) and re-exports `expect` from `@chromatic-com/playwright`.
76
*
8-
* ReScript always compiles record-destructuring arguments to the non-
9-
* destructuring form `async param => { let page = param.page; … }`, which
10-
* Playwright rejects with "First argument must use the object destructuring
11-
* pattern".
7+
* `test` and all hook functions are bound directly to `@chromatic-com/playwright`
8+
* in `Playwright.res` so that Playwright's source-location tracking (which
9+
* captures frame[1] of a fresh `Error.captureStackTrace`) points at the test
10+
* file rather than any intermediate wrapper.
1211
*
13-
* This shim sits between the ReScript bindings and `@chromatic-com/playwright`.
14-
* Every function that accepts a `fixtures` callback is wrapped so that the
15-
* outer function Playwright sees uses real destructuring syntax, and the inner
16-
* ReScript-compiled callback is called with the same object.
17-
*
18-
* The ReScript `Playwright.res` bindings point at this file via
19-
* `@module("./playwright-shim.mjs")` instead of pointing directly at
20-
* `@chromatic-com/playwright`.
12+
* ReScript's compiled test callbacks (`async param => { let page = param.page; … }`)
13+
* are rewritten into the object-destructuring form Playwright requires by the
14+
* Babel plugin in `playwright-babel-plugin.mjs`, configured via `transform` in
15+
* `playwright.config.mjs`.
2116
*/
2217

2318
import {
24-
test as _test,
19+
test,
2520
expect,
2621
takeSnapshot as _takeSnapshot,
2722
} from "@chromatic-com/playwright";
2823

29-
/** Wrap a ReScript fixtures-callback so Playwright sees destructuring syntax. */
30-
function wrapFn(fn) {
31-
return async ({ page, context }) => fn({ page, context });
32-
}
33-
34-
/**
35-
* `test(name, fn)` — register a single test.
36-
* Attach the same helper methods that Playwright's `test` object exposes so
37-
* that `@scope("test")` bindings in ReScript continue to work correctly.
38-
*/
39-
export function test(name, fn) {
40-
return _test(name, wrapFn(fn));
41-
}
42-
43-
/** `test.describe(name, fn)` — group tests; no fixture injection needed. */
44-
test.describe = (name, fn) => _test.describe(name, fn);
45-
46-
/** `test.only(name, fn)` — run only this test while debugging. */
47-
test.only = (name, fn) => _test.only(name, wrapFn(fn));
48-
49-
/** `test.skip(name, fn)` — unconditionally skip a test. */
50-
test.skip = (name, fn) => _test.skip(name, wrapFn(fn));
51-
52-
/** `test.beforeEach(fn)` — run before every test in the current scope. */
53-
test.beforeEach = (fn) => _test.beforeEach(wrapFn(fn));
54-
55-
/** `test.afterEach(fn)` — run after every test in the current scope. */
56-
test.afterEach = (fn) => _test.afterEach(wrapFn(fn));
57-
58-
/** `test.beforeAll(fn)` — run once before all tests in the current scope. */
59-
test.beforeAll = (fn) => _test.beforeAll(wrapFn(fn));
60-
61-
/** `test.afterAll(fn)` — run once after all tests in the current scope. */
62-
test.afterAll = (fn) => _test.afterAll(wrapFn(fn));
63-
6424
/**
6525
* `takeSnapshot(page, name)` — capture a Chromatic visual snapshot.
6626
*
@@ -97,7 +57,7 @@ export async function takeSnapshot(page, name) {
9757
}, viewportHeight);
9858

9959
try {
100-
await _takeSnapshot(page, name, _test.info());
60+
await _takeSnapshot(page, name, test.info());
10161
} finally {
10262
// Always restore original styles, even if takeSnapshot throws.
10363
await page.evaluate((prev) => {

0 commit comments

Comments
 (0)