feat(skills): add migrate-to-e2e for moving a suite one slice at a time

Merged
KA
14d ago
+359-9
  • 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.md or mobile.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.md progress 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 Next slice
  • A port counts as done when it passes 3/3 with --repeat-each 3 on 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. Like create-verification-skill, it is not shipped in the package and e2e guide does 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.md and CONTRIBUTING.md
  • docs/migrate/maestro.mdx now says Maestro matches text case-insensitively. Both benchmark arms lost runs on getByText('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 . --list from this branch lists migrate-to-e2e beside e2e and create-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.
SessionArmFlows ported (total)ProofWall time
1skill4 (4)3/3 iOS + 3/3 Android each, 24/24119 min (CI wiring included)
1guide prompt4 (4)3/3 iOS + 3/3 Android each33 min
2skill4 (8)24/24 new, slice 1 rerun first, 16/16 full suite57 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 Next slice.
    • Both arms preserved every Maestro assertion.
    • The skill is slower per session. Session 1 spent time on CI wiring and inventorying all 22 flows.
  • Skill changes from the benchmark, already in this PR:

    • CI wiring is now proposed to the user instead of done unasked.
    • With no e2e MCP 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 booted
    • e2e init writes e2e@^0.19 under pnpm's minimumReleaseAge
  • A fresh-context review found eight issues, all fixed:

    • init without --yes fails 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 default testDir
    • 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 filee2eExplicit assertionse2e --repeat-each 5Playwright, same build
login.e2e.ts (smoke; 1 test skipped in both)tests/login.e2e.ts5 → 510/102/2
change-password.e2e.tstests/change-password.e2e.ts1 → 15/51/1
change-theme.e2e.tstests/change-theme.e2e.ts20 → 20, plus 3 expect.poll for waitForFunction30/306/6
  • playwright test --list still lists the same 425 entries after the e2e files went in, and e2e list lists only tests/, 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 --list repeats 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 .ts file loads as an ES module, so the generated Prisma client fails on __dirname and @calcom/features fails 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 waitForFunction becomes an expect.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.command doesn't inherit the shell's environment, so the skill says to load the old config's .env in the e2e config. It also covers monorepo placement, Playwright files already named *.e2e.ts, init's test:e2e script, and keeping a source helper the e2e suite still uses.
  • Playwright guide rows added from the same run:
    • page.click(selector) and the other page methods that take a selector act on the first match. Ported to a strict locator they fail with LOCATOR_AMBIGUOUS.
    • A fixed storageState: { cookies } becomes browser.setCookies.
    • waitForResponse takes a glob tested against the full URL, so Playwright's '/api/x' never matches. docs/reference/web.mdx now lists the ? wildcard and the \ escape.
    • waitForURL takes 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 handles ERR_MODULE_NOT_FOUND but not ERR_UNSUPPORTED_DIR_IMPORT (packages/e2e/src/config/esm-hooks.ts:203). The one-condition fix got past that error locally.
    • waitForURL reads a string as an exact URL and waitForResponse reads it as a glob.