feat(skills): add migrate-to-e2e for moving a suite one slice at a time
Merged
KA
14d ago+359+359+012345678901234567890123456789-9-9-0123456789
- New consumer skill
skills/migrate-to-e2e/SKILL.md. It moves a Playwright, Cypress, Selenium, Detox, or Maestro suite to e2e one slice per session. The old suite keeps running beside the new tests until the user retires each old test. - Before the inventory or any code, the agent reads two docs from a table of links: the migration guide for the detected runner and the engine page (
web.mdormobile.md). The skill points at the guides for the API mapping instead of restating it, so a mapping change edits only the guide. - Progress lives in a
MIGRATION.mdprogress file, which the next session resumes from:- one row per source file (per test when a file's tests differ in status), with its assertion count before and after and its pass count per target
- statuses:
todo,ported,retired,blocked,stays - the shared helpers ported, the gaps found, and the
Nextslice
- A port counts as done when it passes 3/3 with
--repeat-each 3on every target and has at least the source's assertion count. - The agent asks the user before it deletes a source test or helper, edits the old runner's config or CI, or changes app code.
- Installed with
npx skills add tester-army/e2e --skill migrate-to-e2e. Likecreate-verification-skill, it is not shipped in the package ande2e guidedoes not read it. - Wiring:
- a "Migrate an existing suite" section in
docs/coding-agents.mdx - a one-line pointer under the prompt on each
docs/migrate/*.mdx - entries in
AGENTS.mdandCONTRIBUTING.md
- a "Migrate an existing suite" section in
docs/migrate/maestro.mdxnow says Maestro matches text case-insensitively. Both benchmark arms lost runs ongetByText('Mute Account')against "Mute account".
flowchart TD
S[session starts] --> L{MIGRATION.md?}
L -- no --> D[read guide + engine page] --> I[inventory, all tests todo] --> H[harness + smoke test]
L -- yes --> R[read progress file + guide, recheck, run ported tests]
R --> HD{harness done?}
HD -- no --> H
HD -- yes --> P[pick slice, write Next]
H --> P --> T[port helpers, then each test] --> V[3/3 per target, assertion count] --> O[update progress file, hand over, ask what to retire]
Verified
Ran it locally: yes
npx skills add . --listfrom this branch listsmigrate-to-e2ebesidee2eandcreate-verification-skill.- I benchmarked the skill against the Maestro guide's existing copy-paste prompt on the Bluesky social-app Maestro suite (
bluesky-social/social-app@eac1e13, 22 flows). Setup: e2e 0.20.0, @e2e-dev/mobile 0.12.0, an iPhone 17 simulator on iOS 26.5, and an Android API 36 emulator. Each arm ran as a fresh-context agent with the same model and the same session preamble, which allowed no questions and no deletes.
| Session | Arm | Flows ported (total) | Proof | Wall time |
|---|---|---|---|---|
| 1 | skill | 4 (4) | 3/3 iOS + 3/3 Android each, 24/24 | 119 min (CI wiring included) |
| 1 | guide prompt | 4 (4) | 3/3 iOS + 3/3 Android each | 33 min |
| 2 | skill | 4 (8) | 24/24 new, slice 1 rerun first, 16/16 full suite | 57 min |
-
The benchmark stopped after three sessions: the system cleared its temp directory, which held both clones and the builds. It is not a full 22-flow comparison.
-
What it showed:
- The skill's resume path worked: session 2 read the progress file, reran slice 1, and took the
Nextslice. - Both arms preserved every Maestro assertion.
- The skill is slower per session. Session 1 spent time on CI wiring and inventorying all 22 flows.
- The skill's resume path worked: session 2 read the progress file, reran slice 1, and took the
-
Skill changes from the benchmark, already in this PR:
- CI wiring is now proposed to the user instead of done unasked.
- With no
e2eMCP tools, the agent reads the trace page instead of building a bridge (session 2 built one). - A command in the progress file that cannot run here says so.
- An unclear source step (a tap on a disabled control, a coordinate tap that may miss) keeps its intent, and the agent asks the user.
-
e2e gaps both arms hit, to file separately:
- a tap on a 1x1 pt node rounds outside its box
app.clearState()doesn't select a device when two are bootede2e initwritese2e@^0.19under pnpm'sminimumReleaseAge
-
A fresh-context review found eight issues, all fixed:
initwithout--yesfails in an agent's shell- a resumed session could skip an unfinished harness
- a helper could be deleted without confirmation
- WebdriverIO was claimed without a guide
- the
tests/rule conflicted with Playwright's defaulttestDir - the status-label claims only fit two of the guides
- two docs wording issues
-
I also ran the skill on a Playwright suite:
calcom/cal.com@54343aa, 314 tests in 60 files (425 test-project pairs across 7 Playwright projects), Postgres and Mailhog in Docker, the production build, e2e 0.20.0 and @e2e-dev/web 0.14.0. One session: inventory, harness, and one slice.
| Source file | e2e | Explicit assertions | e2e --repeat-each 5 | Playwright, same build |
|---|---|---|---|---|
login.e2e.ts (smoke; 1 test skipped in both) | tests/login.e2e.ts | 5 → 5 | 10/10 | 2/2 |
change-password.e2e.ts | tests/change-password.e2e.ts | 1 → 1 | 5/5 | 1/1 |
change-theme.e2e.ts | tests/change-theme.e2e.ts | 20 → 20, plus 3 expect.poll for waitForFunction | 30/30 | 6/6 |
playwright test --liststill lists the same 425 entries after the e2e files went in, ande2e listlists onlytests/, so neither runner collects the other's.- The source skips its logout test as "extremely flaky". Unskipped, the e2e port passed 3/3.
- Skill changes from the cal.com run, in this PR:
- The progress file has one row per source file. A row per test would have been 314 rows. Grep found 291 of the 314 tests, so the skill now counts with the runner's list mode and warns that
--listrepeats each test once per project. - cal.com's
users.create()is a Prisma factory that imports the app's code. It doesn't load under e2e: every.tsfile loads as an ES module, so the generated Prisma client fails on__dirnameand@calcom/featuresfails on a named import from CommonJS@sentry/nextjs. The skill now says to run such a helper unchanged in a child process with the repo's TypeScript runner. Here that was ts-node, behind a JSON-lines fixture. - The assertion count is now "at least the source's", because a
waitForFunctionbecomes anexpect.poll. - A test the source skips is now ported in full and stays skipped.
- Hidden test-id twins take
{ visible: true }. A second toast stacked over the first only showed up in the repeat runs, and.last()fixed it. app.commanddoesn't inherit the shell's environment, so the skill says to load the old config's.envin the e2e config. It also covers monorepo placement, Playwright files already named*.e2e.ts, init'stest:e2escript, and keeping a source helper the e2e suite still uses.
- The progress file has one row per source file. A row per test would have been 314 rows. Grep found 291 of the 314 tests, so the skill now counts with the runner's list mode and warns that
- Playwright guide rows added from the same run:
page.click(selector)and the otherpagemethods that take a selector act on the first match. Ported to a strict locator they fail withLOCATOR_AMBIGUOUS.- A fixed
storageState: { cookies }becomesbrowser.setCookies. waitForResponsetakes a glob tested against the full URL, so Playwright's'/api/x'never matches.docs/reference/web.mdxnow lists the?wildcard and the\escape.waitForURLtakes one exact URL, so'**/x'never matches.
- e2e gaps from cal.com, to file separately:
- The loader resolves a workspace package's file subpath (
@calcom/lib/constants) but not its directory subpath (@calcom/prisma/enums). It handlesERR_MODULE_NOT_FOUNDbut notERR_UNSUPPORTED_DIR_IMPORT(packages/e2e/src/config/esm-hooks.ts:203). The one-condition fix got past that error locally. waitForURLreads a string as an exact URL andwaitForResponsereads it as a glob.
- The loader resolves a workspace package's file subpath (