From cdd3f91c0f98224c9255eabeeeebdbe089d005e3 Mon Sep 17 00:00:00 2001 From: Toti Date: Tue, 2 Jun 2026 10:30:44 -0300 Subject: [PATCH 1/4] feat(playwright): add @humanjs/playwright/test fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Playwright Test subpath that extends `test` with a `human` fixture — bound to the test's page, seeded from the test title, instant in CI / humanized locally — so specs skip the createHuman boilerplate. Customize via test.use({ humanOptions: { ... } }). @playwright/test is an optional peer (only the /test subpath needs it; the package root is untouched). Adds a second tsup entry + the ./test export; publint --strict clean. Docs updated in the root README, package README, and the skill. --- .changeset/playwright-test-fixture.md | 18 +++++ README.md | 20 ++++-- packages/playwright/README.md | 22 ++++++ packages/playwright/package.json | 19 +++++- packages/playwright/src/test/fixture.test.ts | 18 +++++ packages/playwright/src/test/index.ts | 72 ++++++++++++++++++++ packages/playwright/tsup.config.ts | 4 +- packages/skill/templates/skill-body.md | 18 ++++- pnpm-lock.yaml | 29 +++++--- 9 files changed, 203 insertions(+), 17 deletions(-) create mode 100644 .changeset/playwright-test-fixture.md create mode 100644 packages/playwright/src/test/fixture.test.ts create mode 100644 packages/playwright/src/test/index.ts diff --git a/.changeset/playwright-test-fixture.md b/.changeset/playwright-test-fixture.md new file mode 100644 index 0000000..a51741e --- /dev/null +++ b/.changeset/playwright-test-fixture.md @@ -0,0 +1,18 @@ +--- +"@humanjs/playwright": minor +"@humanjs/skill": patch +--- + +Add a Playwright Test fixture at the `@humanjs/playwright/test` subpath. It extends `@playwright/test`'s `test` with a ready-to-use `human` fixture — bound to the test's `page`, seeded from the test title (deterministic per test), and instant in CI / humanized locally — so specs skip the `createHuman` boilerplate: + +```ts +import { test, expect } from '@humanjs/playwright/test'; + +test('checkout', async ({ human, page }) => { + await human.goto('/cart'); + await human.click('Checkout'); + await expect(page).toHaveURL(/success/); +}); +``` + +Customize per file or project via `test.use({ humanOptions: { … } })`. `@playwright/test` is an optional peer dependency (only needed for this subpath; the package root is unaffected). diff --git a/README.md b/README.md index 33d8c93..f5f7c22 100644 --- a/README.md +++ b/README.md @@ -161,25 +161,37 @@ Click through your app, pick a personality, export to clean Playwright + HumanJS ## In tests +Use the `@humanjs/playwright/test` fixture — it extends Playwright's `test` with a ready-to-use `human`, seeded from the test title and instant in CI, humanized locally. No boilerplate: + +```ts +import { test, expect } from '@humanjs/playwright/test'; + +test('checkout flow', async ({ human, page }) => { + await human.goto('/'); + await human.click('Buy now'); + await expect(page).toHaveURL(/checkout/); +}); +``` + +The seed (the test title) makes runs deterministic; `speed: 'instant'` in CI keeps the suite fast, full humanization runs locally. Customize per file or project: `test.use({ humanOptions: { personality: 'distracted' } })`. + +Prefer to wire it yourself? Create the human explicitly: + ```ts import { test, expect } from '@playwright/test'; import { createHuman } from '@humanjs/playwright'; test('checkout flow', async ({ page }) => { const human = await createHuman(page, { - personality: 'careful', seed: test.info().title, speed: process.env.CI ? 'instant' : 'human', }); - await human.goto('/'); await human.click('Buy now'); await expect(page).toHaveURL(/checkout/); }); ``` -The `seed` makes runs deterministic. `speed: 'instant'` in CI keeps your test suite fast. - ## Compared to alternatives | | HumanJS | Playwright | ghost-cursor | diff --git a/packages/playwright/README.md b/packages/playwright/README.md index 5309aa9..594b142 100644 --- a/packages/playwright/README.md +++ b/packages/playwright/README.md @@ -54,6 +54,28 @@ await human.type('input[name="email"]', 'gonzalo@example.com'); Pass a `seed` and every random decision (path curvature, typo placement, keystroke jitter) becomes reproducible. Same seed + same personality + same value = same keystrokes. +### Playwright Test fixture + +Writing `@playwright/test` specs? Import from the `@humanjs/playwright/test` subpath instead of constructing a `Human` in every test. It extends Playwright's `test` with a `human` fixture — seeded from the test title (deterministic per test) and instant in CI / humanized locally: + +```ts +import { test, expect } from '@humanjs/playwright/test'; + +test('checkout flow', async ({ human, page }) => { + await human.goto('/'); + await human.click('Buy now'); + await expect(page).toHaveURL(/checkout/); +}); +``` + +Customize per file (or per project via `playwright.config.ts` `use`) with the `humanOptions` option: + +```ts +test.use({ humanOptions: { personality: 'distracted', speed: 'human' } }); +``` + +Requires `@playwright/test` (an optional peer — you already have it to run the tests). + ### Primitives The full `Human` surface, at a glance. Each one fires real DOM events through Playwright; the humanization wraps the timing and the path, not the dispatch. diff --git a/packages/playwright/package.json b/packages/playwright/package.json index d167a56..2c71bec 100644 --- a/packages/playwright/package.json +++ b/packages/playwright/package.json @@ -40,6 +40,16 @@ "default": "./dist/index.cjs" } }, + "./test": { + "import": { + "types": "./dist/test.d.ts", + "default": "./dist/test.js" + }, + "require": { + "types": "./dist/test.d.cts", + "default": "./dist/test.cjs" + } + }, "./package.json": "./package.json" }, "files": [ @@ -67,12 +77,19 @@ "ffmpeg-static": "^5.2.0" }, "peerDependencies": { - "playwright": ">=1.49.0" + "playwright": ">=1.49.0", + "@playwright/test": ">=1.49.0" + }, + "peerDependenciesMeta": { + "@playwright/test": { + "optional": true + } }, "publishConfig": { "access": "public" }, "devDependencies": { + "@playwright/test": "^1.60.0", "playwright": "^1.60.0" } } diff --git a/packages/playwright/src/test/fixture.test.ts b/packages/playwright/src/test/fixture.test.ts new file mode 100644 index 0000000..ce6f7a7 --- /dev/null +++ b/packages/playwright/src/test/fixture.test.ts @@ -0,0 +1,18 @@ +import { describe, it, expect as vitestExpect } from 'vitest'; +import { expect, test } from './index'; + +// The fixture's runtime behavior (seed = title, CI = instant) is exercised by +// Playwright Test itself, not vitest — these checks confirm the module wires up +// and re-exports a usable `test` (with the `human` fixture extended on) and +// `expect`, which is what would break if the subpath build or imports regress. +describe('@humanjs/playwright/test', () => { + it('re-exports a Playwright test extended with fixtures', () => { + vitestExpect(typeof test).toBe('function'); + vitestExpect(typeof test.extend).toBe('function'); + vitestExpect(typeof test.use).toBe('function'); + }); + + it('re-exports expect', () => { + vitestExpect(typeof expect).toBe('function'); + }); +}); diff --git a/packages/playwright/src/test/index.ts b/packages/playwright/src/test/index.ts new file mode 100644 index 0000000..ff2f045 --- /dev/null +++ b/packages/playwright/src/test/index.ts @@ -0,0 +1,72 @@ +/** + * Playwright Test integration — opt-in via the `@humanjs/playwright/test` + * subpath. Re-exports `test` / `expect` from `@playwright/test` with a + * pre-wired `human` fixture, so a spec never hand-rolls `createHuman`. + * + * Each test gets a {@link Human} bound to its `page`, **seeded from the test + * title** (deterministic per test, so snapshots stay stable) and **instant in + * CI / humanized locally** (fast pipelines, real-pace runs at your desk). + * Override any of that per-file or per-project with the `humanOptions` option. + * + * `@playwright/test` is an optional peer — it's how you run Playwright tests, + * so the host already provides it. Keeping the fixture on a subpath means the + * package root never pulls in the test runner. + * + * @example + * ```ts + * import { test, expect } from '@humanjs/playwright/test'; + * + * test('checkout', async ({ human, page }) => { + * await human.goto('/cart'); + * await human.click('Checkout'); + * await expect(page).toHaveURL(/success/); + * }); + * + * // Customize for a file (or a whole project via playwright.config.ts): + * test.use({ humanOptions: { personality: 'distracted', speed: 'human' } }); + * ``` + */ + +import { test as base } from '@playwright/test'; +import { type CreateHumanOptions, createHuman, type Human } from '../index'; + +/** Test-scoped fixtures added by `@humanjs/playwright/test`. */ +export interface HumanFixtures { + /** + * A {@link Human} bound to the test's `page`. Created fresh per test; by + * default seeded from the test title and run instant in CI, humanized + * locally. Tune via the `humanOptions` option. + */ + human: Human; +} + +/** Option fixture to customize the per-test {@link Human}. */ +export interface HumanOptions { + /** + * Options forwarded to `createHuman` for the `human` fixture. Set with + * `test.use({ humanOptions: { … } })` (per file) or in `playwright.config.ts` + * `use` (per project). Defaults: `seed` = the test title, `speed` = + * `'instant'` in CI else `'human'`. Anything you set here overrides those. + */ + humanOptions: CreateHumanOptions; +} + +/** + * Playwright `test` extended with the `human` fixture. Drop-in replacement + * for `@playwright/test`'s `test`. + */ +export const test = base.extend({ + humanOptions: [{}, { option: true }], + human: async ({ page, humanOptions }, use, testInfo) => { + const human = await createHuman(page, { + // Deterministic per test, CI-fast by default — both overridable since + // the spread comes last. + seed: testInfo.title, + speed: process.env.CI ? 'instant' : 'human', + ...humanOptions, + }); + await use(human); + }, +}); + +export { expect } from '@playwright/test'; diff --git a/packages/playwright/tsup.config.ts b/packages/playwright/tsup.config.ts index f0afbbd..46a484b 100644 --- a/packages/playwright/tsup.config.ts +++ b/packages/playwright/tsup.config.ts @@ -1,7 +1,9 @@ import { defineConfig } from 'tsup'; export default defineConfig({ - entry: ['src/index.ts'], + // Object form pins predictable output names: dist/index.* and dist/test.* + // (the `@humanjs/playwright/test` Playwright-fixture subpath). + entry: { index: 'src/index.ts', test: 'src/test/index.ts' }, format: ['esm', 'cjs'], dts: true, clean: true, diff --git a/packages/skill/templates/skill-body.md b/packages/skill/templates/skill-body.md index d20ab9c..c822b80 100644 --- a/packages/skill/templates/skill-body.md +++ b/packages/skill/templates/skill-body.md @@ -59,7 +59,21 @@ createHuman(page, { personality: blend('careful', 'distracted', 0.3) }); ## Determinism and CI -Set `seed` for reproducible runs (required for snapshot tests). Use `speed: 'instant'` in CI to bypass humanization and keep the suite fast — the documented test pattern: +Set `seed` for reproducible runs (required for snapshot tests). Use `speed: 'instant'` in CI to bypass humanization and keep the suite fast. + +In Playwright tests, prefer the `@humanjs/playwright/test` fixture — it provides a `human` already seeded from the test title and instant-in-CI / humanized-locally, so there's no `createHuman` boilerplate: + +```ts +import { test, expect } from '@humanjs/playwright/test'; + +test('checkout flow', async ({ human, page }) => { + await human.goto('/'); + await human.click('Buy now'); + await expect(page).toHaveURL(/checkout/); +}); +``` + +Override per file or project with `test.use({ humanOptions: { personality: 'distracted' } })`. The explicit form still works when you want full control: ```ts import { test, expect } from '@playwright/test'; @@ -70,9 +84,7 @@ test('checkout flow', async ({ page }) => { seed: test.info().title, speed: process.env.CI ? 'instant' : 'human', }); - await human.goto('/'); - await human.click('Buy now'); await expect(page).toHaveURL(/checkout/); }); ``` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f57319..996d67b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,10 +52,10 @@ importers: version: link:../../packages/core '@vercel/analytics': specifier: ^2.0.1 - version: 2.0.1(next@16.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6) + version: 2.0.1(next@16.2.6(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6) '@vercel/speed-insights': specifier: ^2.0.0 - version: 2.0.0(next@16.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6) + version: 2.0.0(next@16.2.6(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -67,7 +67,7 @@ importers: version: 1.17.0(react@19.2.6) next: specifier: ^16.2.6 - version: 16.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 16.2.6(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: specifier: ^19.2.6 version: 19.2.6 @@ -141,6 +141,9 @@ importers: specifier: ^5.2.0 version: 5.3.0 devDependencies: + '@playwright/test': + specifier: ^1.60.0 + version: 1.60.0 playwright: specifier: ^1.60.0 version: 1.60.0 @@ -957,6 +960,11 @@ packages: '@oxc-project/types@0.130.0': resolution: {integrity: sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==} + '@playwright/test@1.60.0': + resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} + engines: {node: '>=18'} + hasBin: true + '@publint/pack@0.1.4': resolution: {integrity: sha512-HDVTWq3H0uTXiU0eeSQntcVUTPP3GamzeXI41+x7uU9J65JgWQh3qWZHblR1i0npXfFtF+mxBiU2nJH8znxWnQ==} engines: {node: '>=18'} @@ -3585,6 +3593,10 @@ snapshots: '@oxc-project/types@0.130.0': {} + '@playwright/test@1.60.0': + dependencies: + playwright: 1.60.0 + '@publint/pack@0.1.4': {} '@rolldown/binding-android-arm64@1.0.1': @@ -3844,14 +3856,14 @@ snapshots: dependencies: csstype: 3.2.3 - '@vercel/analytics@2.0.1(next@16.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6)': + '@vercel/analytics@2.0.1(next@16.2.6(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6)': optionalDependencies: - next: 16.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + next: 16.2.6(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 - '@vercel/speed-insights@2.0.0(next@16.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6)': + '@vercel/speed-insights@2.0.0(next@16.2.6(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6)': optionalDependencies: - next: 16.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + next: 16.2.6(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 '@vitest/expect@4.1.7': @@ -4666,7 +4678,7 @@ snapshots: negotiator@1.0.0: {} - next@16.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + next@16.2.6(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: '@next/env': 16.2.6 '@swc/helpers': 0.5.15 @@ -4685,6 +4697,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.2.6 '@next/swc-win32-arm64-msvc': 16.2.6 '@next/swc-win32-x64-msvc': 16.2.6 + '@playwright/test': 1.60.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' From 6b0354affa95b928838c60585ff40549ec311ad2 Mon Sep 17 00:00:00 2001 From: Toti Date: Tue, 2 Jun 2026 10:40:23 -0300 Subject: [PATCH 2/4] perf(playwright): dedupe the /test subpath bundle via code splitting The standalone-bundled /test entry inlined a full copy of the library (createHuman + deps), ~64K per format. Enabling tsup `splitting` extracts the shared code into a chunk both entries import: test.js 64K -> ~650B, test.cjs 64K -> ~760B, one shared chunk per format. publint --strict clean; both entries resolve + load from a consumer. --- packages/playwright/tsup.config.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/playwright/tsup.config.ts b/packages/playwright/tsup.config.ts index 46a484b..dfcabf6 100644 --- a/packages/playwright/tsup.config.ts +++ b/packages/playwright/tsup.config.ts @@ -9,5 +9,8 @@ export default defineConfig({ clean: true, sourcemap: true, treeshake: true, - splitting: false, + // Code splitting extracts the shared library code (createHuman + deps) into a + // chunk both entries import, instead of inlining a full copy into the `/test` + // entry. Dedupes both ESM and CJS output — keeps the package tarball lean. + splitting: true, }); From 219a3972cd0aaa4867f8ff8c932981b69a6a0c6e Mon Sep 17 00:00:00 2001 From: Toti Date: Tue, 2 Jun 2026 10:43:54 -0300 Subject: [PATCH 3/4] ci: pin @playwright/test alongside playwright in the compat matrix The /test fixture bridges @playwright/test's Page into createHuman (which types Page from playwright). Pinning only playwright left @playwright/test at 1.60, so two playwright-core versions coexisted and their Page types diverged. Real installs always match the two; pin both to the matrix version so the job mirrors that. --- .github/workflows/playwright-compat.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/playwright-compat.yml b/.github/workflows/playwright-compat.yml index c9a9698..107be52 100644 --- a/.github/workflows/playwright-compat.yml +++ b/.github/workflows/playwright-compat.yml @@ -49,7 +49,12 @@ jobs: run: pnpm --filter @humanjs/core build - name: Pin Playwright to matrix version - run: pnpm --filter @humanjs/playwright add -D playwright@${{ matrix.playwright }} + # Pin BOTH playwright and @playwright/test — they ship in lockstep and a + # real install always has them on the same version. The `/test` fixture + # bridges them (its `page` comes from @playwright/test, passed to + # createHuman which types `Page` from playwright), so a version skew here + # would surface a phantom type mismatch that no real user hits. + run: pnpm --filter @humanjs/playwright add -D playwright@${{ matrix.playwright }} @playwright/test@${{ matrix.playwright }} - name: Typecheck against this Playwright version run: pnpm --filter @humanjs/playwright typecheck From 2c993f579feda694f353134523e4c14f618de501 Mon Sep 17 00:00:00 2001 From: Toti Date: Tue, 2 Jun 2026 14:10:59 -0300 Subject: [PATCH 4/4] feat(playwright): generate Playwright specs that use the human fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit toPlaywright() now emits `import { test, expect } from '@humanjs/playwright/test'` + `test.use({ humanOptions })` (carrying the recorded personality/seed/speed) instead of a per-test createHuman call — shorter generated tests that dogfood the new fixture. `page` is only in the test args when an assertion uses it. toHumanJS() (standalone script) is unchanged. codegen tests updated. --- .changeset/playwright-test-fixture.md | 2 ++ .../playwright/src/recording/codegen.test.ts | 26 ++++++++------- packages/playwright/src/recording/codegen.ts | 33 +++++++++++-------- 3 files changed, 36 insertions(+), 25 deletions(-) diff --git a/.changeset/playwright-test-fixture.md b/.changeset/playwright-test-fixture.md index a51741e..9b3dc8d 100644 --- a/.changeset/playwright-test-fixture.md +++ b/.changeset/playwright-test-fixture.md @@ -16,3 +16,5 @@ test('checkout', async ({ human, page }) => { ``` Customize per file or project via `test.use({ humanOptions: { … } })`. `@playwright/test` is an optional peer dependency (only needed for this subpath; the package root is unaffected). + +The recorder's `toPlaywright()` code export now generates specs that use this fixture — `import { test, expect } from '@humanjs/playwright/test'` plus `test.use({ humanOptions: … })` carrying the recorded personality/seed/speed — instead of a per-test `createHuman` call. (`toHumanJS()`, the standalone script export, is unchanged.) diff --git a/packages/playwright/src/recording/codegen.test.ts b/packages/playwright/src/recording/codegen.test.ts index 65f8028..5938300 100644 --- a/packages/playwright/src/recording/codegen.test.ts +++ b/packages/playwright/src/recording/codegen.test.ts @@ -143,12 +143,12 @@ describe('generateHumanJS', () => { }); describe('generatePlaywrightTest', () => { - it('emits a @playwright/test spec that uses HumanJS', () => { + it('emits a spec using the @humanjs/playwright/test fixture (no createHuman boilerplate)', () => { const out = generatePlaywrightTest(timeline([ev('click', { target: 'Buy now' })])); - expect(out).toContain("import { test } from '@playwright/test';"); - expect(out).toContain("import { createHuman } from '@humanjs/playwright';"); - expect(out).toContain("test('recorded session', async ({ page }) => {"); - expect(out).toContain('const human = await createHuman(page, {'); + expect(out).toContain("import { test } from '@humanjs/playwright/test';"); + expect(out).not.toContain('createHuman'); + expect(out).toContain('test.use({ humanOptions: {'); + expect(out).toContain("test('recorded session', async ({ human }) => {"); expect(out).toContain("await human.click('Buy now');"); expect(out).toContain('});'); }); @@ -158,7 +158,7 @@ describe('generatePlaywrightTest', () => { timeline([ev('sleep', { ms: 800 }), ev('click', { target: '#go' })]), ); expect(out).not.toContain('await sleep('); - expect(out).not.toContain('import { createHuman, sleep }'); + expect(out).not.toContain("import { sleep } from '@humanjs/playwright';"); expect(out).toContain("await human.click('#go');"); }); @@ -168,7 +168,8 @@ describe('generatePlaywrightTest', () => { { keepSleeps: true }, ); expect(out).toContain('await sleep(800);'); - expect(out).toContain("import { createHuman, sleep } from '@humanjs/playwright';"); + expect(out).toContain("import { sleep } from '@humanjs/playwright';"); + expect(out).toContain("import { test } from '@humanjs/playwright/test';"); }); it('runs instant in CI / recorded speed locally', () => { @@ -180,12 +181,12 @@ describe('generatePlaywrightTest', () => { const named = generatePlaywrightTest( timeline([ev('click', { target: '#go' })], { name: 'checkout flow' }), ); - expect(named).toContain("test('checkout flow', async ({ page }) => {"); + expect(named).toContain("test('checkout flow', async ({ human }) => {"); const overridden = generatePlaywrightTest( timeline([ev('click', { target: '#go' })], { name: 'checkout flow' }), { title: 'override' }, ); - expect(overridden).toContain("test('override', async ({ page }) => {"); + expect(overridden).toContain("test('override', async ({ human }) => {"); }); it('derives toBeVisible from reads and toHaveValue from captured inputs', () => { @@ -195,7 +196,8 @@ describe('generatePlaywrightTest', () => { ev('read', { target: '.passage' }), ]), ); - expect(out).toContain("import { expect, test } from '@playwright/test';"); + expect(out).toContain("import { expect, test } from '@humanjs/playwright/test';"); + expect(out).toContain("test('recorded session', async ({ human, page }) => {"); expect(out).toContain("await human.type('#email', 'a@b.com');"); expect(out).toContain("await expect(page.locator('#email')).toHaveValue('a@b.com');"); expect(out).toContain("await human.read('.passage');"); @@ -205,9 +207,9 @@ describe('generatePlaywrightTest', () => { it('omits the expect import (and notes it in the TODO) when nothing is assertable', () => { const out = generatePlaywrightTest(timeline([ev('click', { target: 'Buy now' })])); - expect(out).toContain("import { test } from '@playwright/test';"); + expect(out).toContain("import { test } from '@humanjs/playwright/test';"); expect(out).not.toContain('import { expect, test }'); - expect(out).toContain('TODO: assert the outcome — import { expect }'); + expect(out).toContain('TODO: assert the outcome — add `page` to the test args'); }); it('does not assert password inputs (no captured value)', () => { diff --git a/packages/playwright/src/recording/codegen.ts b/packages/playwright/src/recording/codegen.ts index 10a7d63..92b9931 100644 --- a/packages/playwright/src/recording/codegen.ts +++ b/packages/playwright/src/recording/codegen.ts @@ -276,8 +276,10 @@ function emitSteps(events: readonly TimelineEvent[], opts: EmitOptions): string /** * Generates a `@playwright/test` spec that replays the session through * HumanJS — a humanized test, not raw Playwright, since the humanization is - * the point. Runs instant in CI / recorded speed locally, drops timing - * `sleep()`s by default, and derives the assertions it safely can. + * the point. Uses the `@humanjs/playwright/test` fixture for the `human` + * (recorded personality / seed / speed applied via `test.use({ humanOptions })`), + * runs instant in CI / recorded speed locally, drops timing `sleep()`s by + * default, and derives the assertions it safely can. */ export function generatePlaywrightTest( timeline: Timeline, @@ -296,13 +298,16 @@ export function generatePlaywrightTest( // Only import `expect` if we actually emitted assertions (body has no // comments yet, so this won't match the TODO placeholder below). const hasAsserts = body.includes('await expect('); - const testImport = hasAsserts - ? "import { expect, test } from '@playwright/test';" - : "import { test } from '@playwright/test';"; - const humanImport = needsSleep - ? "import { createHuman, sleep } from '@humanjs/playwright';" - : "import { createHuman } from '@humanjs/playwright';"; + // `test` + `expect` come from the `@humanjs/playwright/test` fixture (which + // supplies the `human`); only `sleep` (when kept) stays on the package root. + const fixtureImport = hasAsserts + ? "import { expect, test } from '@humanjs/playwright/test';" + : "import { test } from '@humanjs/playwright/test';"; + const sleepImport = needsSleep ? "\nimport { sleep } from '@humanjs/playwright';" : ''; const title = options.title ?? timeline.name ?? 'recorded session'; + // `page` is only referenced by the derived assertions — omit it from the + // test args when there are none, so the generated test has no unused fixture. + const args = hasAsserts ? '{ human, page }' : '{ human }'; const baseUrlNote = baseOrigin ? ` // Set use.baseURL = ${q(baseOrigin)} in playwright.config.ts for these relative paths.\n\n` : ''; @@ -311,16 +316,18 @@ export function generatePlaywrightTest( const todo = [ hasAsserts ? ' // TODO: add assertions for the outcome of this flow, e.g.:' - : " // TODO: assert the outcome — import { expect } from '@playwright/test', e.g.:", + : " // TODO: assert the outcome — add `page` to the test args and import { expect } from '@humanjs/playwright/test', e.g.:", ' // await expect(page).toHaveURL(/dashboard/);', " // await expect(page.getByText('Welcome back')).toBeVisible();", ].join('\n'); - return `${testImport} -${humanImport} + // Recorded personality / seed / speed are applied via the fixture's + // `humanOptions` option, so the test still replays with its exact settings + // (instant in CI) while skipping the per-test createHuman boilerplate. + return `${fixtureImport}${sleepImport} -test(${q(title)}, async ({ page }) => { - const human = await createHuman(page, ${createHumanOptions(timeline, true)}); +test.use({ humanOptions: ${createHumanOptions(timeline, true)} }); +test(${q(title)}, async (${args}) => { ${baseUrlNote}${body} ${todo}