> ## Documentation Index
> Fetch the complete documentation index at: https://e2e.tester.army/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# From Cypress

> Replace Cypress command chains with async tests and keep your data-cy attributes.

export const migrationPrompt = `Migrate this project's Cypress tests to e2e, one spec at a time. Docs: \`https://e2e.tester.army/docs/migrate/cypress.md\`

1. Read the migration guide above. It maps Cypress config, commands, and assertions to e2e.
2. If the project has no e2e config (\`e2e.config.ts\`, \`.mts\`, \`.js\`, or \`.mjs\`), run \`npx e2e init --yes\` (or the pnpm or bun equivalent). It writes a config and an example test, and installs nothing yet.
3. Read the e2e skill (\`npx e2e guide\` prints it, \`npx e2e guide <topic>\` prints a topic) and follow it for every step below.
4. If you ran init, ask me which model to use for agent steps: a ChatGPT, GitHub Copilot, OpenCode Console, or SuperGrok subscription, an API key, or a local model. Configure it with its provider package. If it needs a sign-in, give me the \`npx e2e login\` command and wait until I confirm.
5. Port \`cypress.config.*\` into the e2e config: \`baseUrl\` and the dev server command become the target's \`app\`, and if the specs select by \`data-cy\`, set \`web({ testIdAttribute: 'data-cy' })\` so \`screen.getByTestId\` reads it. Keep Cypress installed while both suites run.
6. Install dependencies.
7. List the Cypress end-to-end specs and ask me which one to move first. Suggest one that breaks often or waits with \`cy.wait(ms)\`. Component tests have no e2e equivalent; leave them in Cypress.
8. Rewrite that spec as a \`.e2e.ts\` file under \`tests/\` (e2e runs \`tests/**/*.e2e.ts\` by default) with the guide's tables, one awaited call per line. Where \`cy.contains\` matched a fragment, pass \`{ exact: false }\`. Replace each \`cy.wait(ms)\` with an assertion on the state the test waits for.
9. Keep navigation and the final assertions deterministic, with \`app.open()\` and locator assertions. Where a run of scripted interactions breaks often, propose an \`agent.act\` goal for it instead.
10. Run the new file with \`npx e2e run <file>\` and fix it until it passes. Keep the Cypress spec until I confirm it can go.
11. Summarize which tests moved, which stayed in Cypress, and why.`;

e2e replaces Cypress command chains with async TypeScript tests that run in
Node. Both suites can stay in the project while you move one spec at a time.

<SetupPrompt>

<Prompt description={migrationPrompt} children={migrationPrompt} actions={["copy", "cursor"]} />

</SetupPrompt>

For a migration that spans several sessions, the [`migrate-to-e2e` skill](https://e2e.tester.army/docs/coding-agents#migrate-an-existing-suite) records what moved in a progress file and keeps the old suite running until you retire each test.

## Port the config

<Note>Install e2e and configure a model with the [quickstart](https://e2e.tester.army/docs/quickstart). The example below uses an agent call.</Note>

Keep your `data-cy` attributes. The config below reads them as test ids.

| Cypress | e2e |
| --- | --- |
| `baseUrl` | `app: { url }` on the target |
| `start-server-and-test` | `app: { url, command }`, see [Starting your app](https://e2e.tester.army/docs/web#starting-your-app) |
| `viewportWidth`, `viewportHeight` | `web({ viewport })` |
| `data-cy` selectors | `web({ testIdAttribute: 'data-cy' })`, then `screen.getByTestId` |
| `retries` | `retries` |
| `env`, `Cypress.env()` | `process.env` |
| `cypress.env.json` secrets | `credentials` and `secrets`, see [Signing in](https://e2e.tester.army/docs/authentication) |
| `cypress run --browser firefox` | A second target with `web({ browser: 'firefox' })` |
| Component testing | No equivalent |

```ts title="e2e.config.ts"

export default {
  targets: [{
    engine: web({ testIdAttribute: 'data-cy' }),
    app: { url: 'http://localhost:3000' },
  }],
} satisfies E2EConfig;
```

## Rewrite one spec

Start with a spec that often breaks.

```js title="Cypress"
describe('todos', () => {
  it('completes one', () => {
    cy.visit('/todos');
    cy.get('[data-cy=new-todo]').type('Buy milk{enter}');
    cy.get('[data-cy=new-todo]').type('Walk the dog{enter}');
    cy.contains('li', 'Buy milk').find('[data-cy=toggle]').click();
    cy.get('[data-cy=todo]').should('have.length', 2);
    cy.contains('1 item left').should('be.visible');
  });
});
```

```ts title="e2e"

describe('todos', () => {
  test('completes one', async ({ app, agent, screen }) => {
    await app.open('/todos');
    await agent.act('add todos "Buy milk" and "Walk the dog"');
    await expect(screen.getByTestId('todo')).toHaveCount(2);
    await agent.act('complete "Buy milk"');
    await expect(screen.getByText('1 item left')).toBeVisible();
  });
});
```

## Map each command

| Cypress | e2e |
| --- | --- |
| `cy.visit(path)` | `app.open(path)` |
| `cy.get('[data-cy=x]')` | `screen.getByTestId('x')` |
| `cy.get('css')` | `browser.locator('css')`, not portable; prefer a role or label |
| `cy.contains('text')` | `screen.getByText('text', { exact: false })` |
| `cy.contains('li', 'text')` | `screen.getByRole('listitem').filter({ hasText: 'text' })` |
| `.find()`, `.within()` | Chain a query off the locator: `row.getByRole(...)` |
| `.first()`, `.eq(n)` | `.first()`, `.nth(n)` |
| `.click()`, `.dblclick()`, `.rightclick()` | `.tap()`, `.doubleTap()` (or `.dblclick()`), `.secondaryTap()` |
| `.type('text')` | `.fill('text')` replaces the content |
| `.type('{enter}')` | `.press('Enter')` |
| `.clear()`, `.check()`, `.select()` | `.clear()`, `.check()`, `.selectOption()` |
| `.selectFile()` | `.setInputFiles()` |
| `.should('be.visible')` | `await expect(locator).toBeVisible()` |
| `.should('have.text', t)`, `.should('contain', t)` | `.toHaveText(t)`, `.toContainText(t)` |
| `.should('have.length', n)` | `.toHaveCount(n)` |
| `.should('have.value', v)` | `.toHaveValue(v)` |
| `.should('not.exist')` | `.toHaveCount(0)`; `toBeHidden()` also accepts a node that is still in the DOM |
| `cy.url().should('include', p)` | `expect(browser).toHaveURL(/p/)` |
| `cy.title()` | `expect(browser).toHaveTitle()` |
| `cy.intercept(url, body)` | `browser.route(url, route => route.fulfill({ json: body }))` |
| `cy.wait('@alias')` | `browser.waitForResponse(url)` |
| `cy.on('window:confirm')` | `browser.onDialog('accept')` |
| `cy.window()`, `cy.document()` | `browser.evaluate(() => ...)` |
| `cy.setCookie` | `browser.setCookies` |
| `cy.viewport` | `browser.setViewport` |
| `cy.session` | `test.setup` and `{ session }` |
| `cy.task`, `cy.exec`, `cy.request` | Node code: `fetch`, `fs`, your database client |
| `cy.fixture` | `import data from './fixture.json'` |
| `Cypress.Commands.add` | A function, or a fixture from `test.extend` |
| `cy.wait(ms)` | Use a locator assertion for the condition you need |
| `cypress open` | `npx e2e run --headed`, plus `--debug` for the step table |

Two things differ from Cypress:

- There is no chain. Await each call on its own line. It returns a value:
  `const text = await locator.textContent()`.
- Actions and single-node reads and assertions need one match. Several fail
  with `LOCATOR_AMBIGUOUS`. Narrow with `.filter()`, `.first()`, or `.nth()`.
  See [screen](https://e2e.tester.army/docs/reference/screen).

## Next

<CardGroup cols={2}>
  <Card title="Core concepts" icon="pen-to-square" href="/core-concepts">
    Goals, assertions, and locators instead of scripted selectors.
  </Card>
  <Card title="Working with the browser" icon="globe" href="/browser">
    Routes, dialogs, downloads, iframes, and page evaluation.
  </Card>
</CardGroup>
