agentleFS
leather-ioleather-io/mono/.claude/skills/write-extension-e2e/SKILL.md

write-extension-e2e

Skill28 starsChanged 34 days agoMITView on GitHub

Conventions for writing and changing the extension's Playwright E2E tests in apps/extension/tests. Use when adding a spec, a page-object action, a selector or a mock, or when an E2E test is flaky. Covers readiness-owning actions, the sleep ban, selectors, mocks, popups and how to run one spec locally.

The file

We split this skill into the rules it gives an agent

3 rules. About 0 are common, 0 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Every test must end with an assertion and sit inside a top-level test.describe.

    line 42
  2. Very specific:

    When you add a flow whose first interaction depends on loaded data, add the wait inside a new page-object action and add the element's …

    line 69
  3. Very specific:

    Every failure writes test-results/<test>/error-context.md with an accessibility snapshot of the page at the moment of failure.

    line 100

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Extension E2E tests

Specs live in apps/extension/tests/specs/<feature>/<feature>.spec.ts and use the test fixture from apps/extension/tests/fixtures/fixtures.ts, which launches a fresh headless Chromium with the extension from apps/extension/dist for every test. Each test gets its own profile, so chrome.storage is never shared between tests or workers.

Run locally

pnpm --filter @leather.io/extension build:test     # rebuild dist after any src change
cd apps/extension
EXTENSION_INTEGRATION_TEST_MNEMONIC="$(gh variable get EXTENSION_INTEGRATION_TEST_MNEMONIC -R leather-io/mono)" \
  pnpm playwright test tests/specs/send/send-stx.spec.ts --workers=3

The mnemonic is a readable repository variable. Without it the whole run fails at load time. A running pnpm dev rewrites dist, so stop it before testing. CI runs 10 shards with three workers each; fullyParallel is on, so tests in one file may run on different workers.

The rules ESLint enforces

eslint.config.js applies eslint-plugin-playwright recommended rules to the tests tree plus these bans. Fix the cause, never add a suppression.

BannedUse instead
page.waitForTimeout(), delay() from utilsA locator wait, expect(locator).toBeVisible(), expect.poll() or expect(async () => …).toPass()
waitUntil: 'networkidle'A plain goto; wait for the element you need next
allInnerTexts()toHaveText, toContainText, toHaveCount on the locator
page.locator('text="X"'), raw CSS selectorsgetByTestId, getByText('X', { exact: true }), getByRole
waitForSelector()locator.waitFor() or a web-first assertion
{ force: true }Fix the locator or the UI; forced clicks hide real problems
innerText() + toEqualexpect(locator).toHaveText() so the assertion retries
In specs/**: SendCryptoAssetSelectors.SendForm, SharedComponentsSelectors.CardAvailableBalance, ActivitySelectors.TransactionActionFeeInput, ActivitySelectors.TransactionActionBalance, SettingsSelectors.UnlockWalletBtn, OnboardingSelectors.SetPasswordBtnThe page-object action named in the lint message

Every test must end with an assertion and sit inside a top-level test.describe. To assert that something does not happen, use expectToSettleAt(read, value) from tests/utils.ts: it polls until the value has held for five consecutive reads, which replaces a sleep-then-recheck. Test bodies cannot contain if: narrow optional values with assertExistence(value, message) from @leather.io/utils or a zod schema.parse(value), and move any genuine branch into a named helper outside the test. Expectations cannot sit inside callbacks or catch; assert directly so a failure cannot be swallowed.

Actions own their preconditions

Several screens validate against data that loads after the screen renders. Acting before the data is there fails silently or leaves a stale validation error. The page objects wait for that data inside the action, so a spec never has to know about it:

ActionWhat it waits for before returning
OnboardingPage.signInWithTestAccount, signInWithMixedSoftwareAndLedgerWallets, signInWithLedgerAccountSeeds storage, reloads, polls until the home tabs render; re-seeds up to three times. Dismisses the multi-wallet introducer and waits for its seen-flag to reach storage, so it cannot reappear over the next screen
GlobalPage.setupAndUseApiCallsRegisters the API mocks, then waits for the onboarding or home screen
SendPage.selectStxAndGoToSendForm, selectSip10AndGoToSendFormThe fee estimate, which the amount validator needs
SendPage.selectBtcAndGoToSendForm(expectedBalance?)The available balance and the Send max link, which only renders once the UTXO list the validator reads has loaded
SendPage.previewBitcoinTransactionA single Continue click, then the fee list
HomePage.setBitcoinIncreasedFeeRateThe RBF dialog's balance caption, which its validation closes over
HomePage.lockReopens the settings menu if a re-render closed it
SwitchAccountPage.submitNewWalletPasswordHome or the mismatch error; the password check derives a key, which is slow on CI runners
HomePage.unlockThe password screen to go away; key derivation is slow on CI runners
HomePage.setStacksTransactionActionFeeScopes the fee input to the cancel or increase-fee sheet

When you add a flow whose first interaction depends on loaded data, add the wait inside a new page-object action and add the element's selector to pageObjectOnlySelectors in eslint.config.js so specs cannot bypass it. If a readiness signal does not exist in the UI, add a data-testid in app code (see tests/selectors) rather than sleeping.

Selectors

Selectors are string constants in apps/extension/tests/selectors/*.selectors.ts, grouped per screen, and app code imports the same constants for data-testid. Add the constant first, then use it from both sides. Prefer getByTestId for anything you own and getByRole or getByText(…, { exact: true }) for popups whose markup you do not control.

Mocks

GlobalPage.setupAndUseApiCalls installs the default API mocks on the browser context. Register test-specific mocks on page or context before signInWithTestAccount: the home page fetches on mount and React Query caches the first answer. Page-level routes win over context-level ones. Mocks live in apps/extension/tests/mocks; add to an existing file when the API already has one.

RPC popups

Requests from the test app at localhost:3000 open an approver window. Wait for it with context.waitForEvent('page') in a Promise.all with the request, then act on the popup with web-first waits. Close the popup and assert on the request's promise so the test ends with an assertion. The test app pre-bundles the polyfill shims Vite would otherwise discover on the first visit (optimizeDeps.include in apps/test-app/vite.config.ts) and global setup warms it once, so the first navigation never reloads mid-test; if you add a dependency that Vite reports as "new dependencies optimized", add it to that list.

Failure output

Every failure writes test-results/<test>/error-context.md with an accessibility snapshot of the page at the moment of failure. Read it before re-running. Traces are kept on first retry.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.