e2e replaces a Selenium suite with TypeScript tests. Locators find nodes by role and label, and assertions retry until they pass or time out. A Java or Python suite needs a rewrite, so run both suites and move one flow at a time.

Agent prompt

Migrate this project's Selenium tests to e2e, one flow at a time. Docs: https://e2e.tester.army/docs/migrate/selenium.md

  1. Read the migration guide above. It maps Selenium config and WebDriver calls to e2e.
  2. e2e tests are TypeScript. If the Selenium suite is Java, Python, or another language, ask me where the new test project should live, for example an e2e/ folder with its own package.json.
  3. If that project has no e2e config (e2e.config.ts, .mts, .js, or .mjs), run npx e2e init --yes (or the pnpm or bun equivalent) there. It writes a config and an example test, and installs nothing yet.
  4. Read the e2e skill (npx e2e guide prints it, npx e2e guide <topic> prints a topic) and follow it for every step below.
  5. 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.
  6. Set the target's app to the URL every driver.get used, and the command that starts the app if the suite starts it. Move passwords and tokens into credentials or secrets.
  7. Install dependencies.
  8. List the page objects and the flows that use them, and ask me which to move first. Suggest one page object and its flows.
  9. Rewrite those flows as .e2e.ts files under tests/ (e2e runs tests/**/*.e2e.ts by default) with the guide's tables. Prefer role, label, and test id queries over CSS and XPath. Replace every wait and Thread.sleep with a locator assertion. Narrow queries that match several nodes. Turn the page object's setup code into a fixture.
  10. 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.
  11. Run the new files with npx e2e run <file> and fix them until they pass. Keep the Selenium tests until I confirm they can go.
  12. Summarize which flows moved, which stayed in Selenium, and why.

For a migration that spans several sessions, the migrate-to-e2e skill records what moved in a progress file and keeps the old suite running until you retire each test.

Port the config

Install e2e with the quickstart. Only agent calls need a model, so the example below runs without one.
Seleniume2e
The URL in every driver.getapp: { url } on the target, then relative paths
Starting the app in a scriptapp: { url, command }, see Starting your app
One driver per browserOne target per browser, web({ browser: 'firefox' })
Selenium Gridworkers for parallelism; web({ connect }) attaches to a remote Chromium over CDP, and a browser provider such as Kernel leases hosted browsers
manage().window().setSizeweb({ viewport })
Implicit waitsactionTimeout and assertionTimeout set action and assertion deadlines
A page object for sign-intest.setup and { session }, see Signing in

Rewrite one flow

Start with one page object and the flows that use it.

Selenium
driver.get("http://localhost:3000/login");
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(visibilityOfElementLocated(By.id("username"))).sendKeys("admin@example.test");
driver.findElement(By.id("password")).sendKeys(System.getenv("ADMIN_PASSWORD"));
driver.findElement(By.xpath("//button[contains(., 'Sign in')]")).click();
wait.until(urlContains("/dashboard"));
assertEquals("Welcome back, admin!",
    driver.findElement(By.cssSelector(".greeting")).getText());
e2e
import { test } from '@e2e-dev/web';
import { expect, credentials } from 'e2e';

test('signs in', async ({ app, screen, browser }) => {
  const admin = credentials.user('admin');
  await app.open('/login');
  await screen.getByLabel('Username').fill(admin.username);
  await screen.getByLabel('Password').fill(admin.password);
  await screen.getByRole('button', 'Sign in').tap();
  await expect(browser).toHaveURL('/dashboard');
  await expect(screen.getByRole('status')).toHaveText('Welcome back, admin!');
});

Locator actions wait for their targets, so no explicit wait is left. The password comes from configured credentials.

Map each WebDriver call

Seleniume2e
driver.get(url)app.open(path)
driver.navigate().back(), .refresh()app.back(), browser.reload()
By.id('x')browser.locator('#x'); or web({ testIdAttribute: 'id' }) and screen.getByTestId('x')
By.name('x')browser.locator('[name="x"]'), not portable; prefer a role or label
By.linkText, By.partialLinkTextscreen.getByRole('link', { name }), name: /part/
By.cssSelector, By.xpathbrowser.locator('css or xpath'), not portable
findElements(...).size()await locator.count()
findElements(...).get(n)locator.nth(n)
.click().tap()
.sendKeys('text').fill('text') replaces the content
.sendKeys(Keys.ENTER).press('Enter')
.clear().clear()
new Select(el).selectByVisibleText.selectOption({ label })
.getText(), .getAttribute(), .isDisplayed().textContent(), .getAttribute() wait for the node then read one time, .isVisible() reads at once; toHaveText() retries
wait.until(visibilityOf(...))await expect(locator).toBeVisible()
wait.until(textToBe(...))await expect(locator).toHaveText(...)
wait.until(urlContains(...))await expect(browser).toHaveURL(/.../)
Actions.moveToElement.hover()
Actions.dragAndDrop.dragTo(target)
Actions.keyDown(CONTROL).sendKeys('a').press('ControlOrMeta+a')
switchTo().frame()browser.frameLocator('#frame')
switchTo().alert().accept()browser.onDialog('accept') before the action
switchTo().window()No equivalent. Popups are unsupported
executeScriptbrowser.evaluate
manage().addCookiebrowser.setCookies
getScreenshotAsapp.screenshot('label'), redacted
Thread.sleepUse a locator assertion for the condition you need
Page objectsA fixture from test.extend, or a goal for agent.act, see Page objects

findElement takes the first match. e2e fails with LOCATOR_AMBIGUOUS on several, so narrow with .filter(), .first(), or .nth().

Next