Implicit
jg-testing
Deterministic waits, timeouts, retries, and repeat runs for Vitest and Playwright.
- Usage
- Loads automatically
When to use it
It loads on its own when a task touches Vitest or Playwright tests. Call it by name to load it before such a task.
When it loads
Your agent loads the skill on its own when a request like this touches its area.
Add an end-to-end test for the checkout flow.
Finds elements by role, waits on assertions instead of sleeps, fixes the clock, and repeats new tests when your instructions require it.
SKILL.md
What the agent reads
The complete instructions your agent loads when a task touches its area.
Application
The agent loads this skill on its own when a task touches its area. This skill’s area is Vitest and Playwright tests, their configuration, and flaky-test handling.
- It adds domain rules to the task. It never authorizes actions, widens scope, or replaces the workflow or approvals of a skill the developer invoked.
- It ranks with the repository’s coding instructions. When they conflict, the coding instructions win, except for the exceptions this skill states for its own area.
- The repository’s established conventions win over this skill’s naming, layout, and style rules. Its correctness and safety rules, and the exceptions it states for its own area, still apply.
- Check the installed versions first. This skill was written for Vitest 4.1 and 5.0, and Playwright 1.64. An item marked with a minimum version, written like (18+), applies only when that version or a later one is installed. For an older installed version, follow that version’s official documentation.
- Before relying on one of this skill’s version-specific items, check it against the current official guidance when the installed version of the technology is newer than the one this skill was written for, or when the version bullet gives a review month for something without an installed version, such as a hosted service or browser behavior, and that month is more than six months before the current date. Where an item differs from that guidance, follow the guidance. Version-specific items are API, syntax, and feature recommendations, and anything marked with a minimum version. This skill’s conventions and requirements, such as its naming, layout, style, reuse, and safety rules, keep their usual precedence even where official guidance suggests otherwise. Report each item that looks outdated in the summary, with the installed version and what the current guidance says, and offer there to open an issue on the jg-skills repository,
https://github.com/jesusgraterol/jg-skills:- Never pause or stop a run for the offer, including an autonomous run. When no one can answer, include the pre-filled link described below instead of asking.
- First search the repository’s open issues, and skip the offer when one already covers the item. When the search can’t run, say so next to the link, so the developer checks the existing issues before submitting.
- Open an issue only after the developer agrees, because it is public. Title it
<skill>: <item> looks outdated on <technology> <version>, and include only the skill name, the outdated item, the installed version, and a link to the official documentation, never code, file paths, or project details. Add no label. - Use
gh issue createwhenghis installed and signed in. Otherwise give the developer a pre-filled link,https://github.com/jesusgraterol/jg-skills/issues/new?title=…&body=…, with both values URL-encoded.
- This skill isn’t a complete list. Where it says nothing, follow the official guidance for the installed version, and in new code the task touches, prefer the features that guidance currently recommends over older ones that do the same job, unless the repository has an established pattern for that job. Before using a newer approach instead of one this skill gives, confirm it in that version’s official documentation, never from memory alone, and report the item as outdated as described above. When that documentation can’t be reached, keep this skill’s approach and report that the newer approach couldn’t be confirmed, without pausing the run.
- Rules that say “prefer” are defaults: departing from one with a stated reason is fine. Rules that say “never”, “always”, or “must” are requirements.
- Nothing in this skill requires adding a dependency, enabling a compiler or tool, upgrading, or migrating existing code.
Repository rules first
Exception to this skill’s precedence: the repository’s testing guidelines and established test conventions win over every rule in this skill, including its correctness rules. For example, follow a repository’s retry setup or suite tags even where they differ from the defaults below. This skill holds the runner mechanics behind the testing rules in the repository’s coding instructions.
Waiting and locating
- Wait on assertions that retry until their condition holds, never on fixed sleeps such as
page.waitForTimeout()orsetTimeout. - In Playwright, prefer web-first assertions such as
await expect(locator).toBeVisible(). Useexpect.poll()for values outside the page; it retries for 5 seconds by default. - Give
toPass()an explicittimeout. Its default is 0, so it otherwise retries until the test itself times out. The config can set one for all tests withexpect: { toPass: { timeout } }. - In Vitest,
expect.poll()andvi.waitFor()retry for 1 second by default, every 50 milliseconds.vi.waitFor()advances fake timers on its own. - Avoid
waitUntil: 'networkidle', which Playwright discourages, and the deprecatedwaitForNavigation(); usewaitForURL(). ReplacewaitForSelector()with locators and assertions. - Locate elements the way users find them, in this order:
getByRole,getByText,getByLabel,getByPlaceholder,getByAltText,getByTitle, andgetByTestIdonly when nothing user-facing is stable. Never locate by CSS structure, generated class names, or position. - Locators are strict, so an action on one that matches several elements fails. Narrow it with
filter({ hasText, has })or a more specific role and name.
Time, environment, and isolation
- In Playwright, call
page.clock.install()(1.45+) beforepage.goto()and before any other clock call, then preferpage.clock.setFixedTime(). UserunFor()orfastForward()only when the test needs time to pass. - In Vitest,
vi.useFakeTimers()fakes timers and dates, andvi.setSystemTime()sets the date with or without them. When timers are scheduled from promises, advance them with the async variants, such asvi.advanceTimersByTimeAsync()orvi.runAllTimersAsync(). - Stub environment variables and globals with
vi.stubEnv()andvi.stubGlobal(), withunstubEnvs: trueandunstubGlobals: truein the config so each test starts clean. A value assigned directly toprocess.envor a global can’t be restored automatically. - Playwright’s
timezoneIdandlocaleoptions affect only the browser. Set theTZenvironment variable for the test runner itself. - Reduced-motion emulation,
contextOptions: { reducedMotion: 'reduce' }, changes only theprefers-reduced-motionmedia query, so animations the page doesn’t tie to it still run. Before reading the colors, sizes, or positions of anything that animates, wait for its end state with a retrying assertion, such asawait expect(locator).toHaveCSS('opacity', '1').toHaveScreenshot()already disables animations and hides the caret. - Read measurements that you compare with each other, such as two elements’ widths, in one
page.evaluate()call, so the page can’t change between the reads. - Make per-worker data unique with
testInfo.workerIndex, and per-test data withtestInfo.testId, addingtestInfo.retrywhen each attempt needs its own data.parallelIndexis reused when a worker restarts, so it can collide. - Prefer isolated tests to serial mode. When tests must share setup, put it in a fixture.
- Inside
test.concurrent, use theexpectfrom the test context, so assertions report against the right test.
Browser checks
- Check for horizontal overflow on the whole document:
document.documentElement.scrollWidth <= document.documentElement.clientWidth. - Scope
AxeBuilderto the area under test withinclude(), and assert thatviolationsis empty. For WCAG 2.2 AA, passwithTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa']), because each tag holds only the rules added at its own level and version. - axe doesn’t check that focus is visible, so assert the focus indicator itself, such as its outline, after moving focus with the keyboard.
- Register
pageerrorandconsolelisteners beforepage.goto(), and assert that the page logged no unexpected errors. - Prefer
screenshot: 'only-on-failure'in the config, and take screenshots inside a test only when they are compared or reviewed.
For mobile behavior:
- Emulate phones with device descriptors, such as
devices['Pixel 7']anddevices['iPhone 15']. An iPhone descriptor selects WebKit, so addbrowserName: 'chromium'where only Chromium is installed, and never treat a Chromium run as proof of Safari behavior. Set the color scheme withpage.emulateMedia({ colorScheme }). - With
hasTouch, assert thatmatchMedia('(pointer: coarse)').matchesbefore measuring touch targets withboundingBox(). - Read the viewport meta tag and the computed font size of fields with
page.evaluate(). - Simulate safe-area insets in Chromium through a CDP session with the experimental
Emulation.setSafeAreaInsetsOverridecommand. - Approximate the on-screen keyboard by shrinking the viewport height with
page.setViewportSize(). - Check that the system back action closes the topmost sheet or dialog before it leaves the route: press Escape for one that closes on close requests, and call
page.goBack()for one backed by a history entry.
Repeat runs
When the coding instructions require repeat runs for new or changed end-to-end and integration tests, run each repetition with retries off, so a retry can’t hide a failure:
- Playwright:
--repeat-each=3 --retries=0on the test files. - Vitest 5.0.3+:
--repeats=2 --retry=0.--repeatscounts runs after the first, so this gives three. Earlier 5.0 releases could hide a failed repeat when retries were on. - Vitest 4: run the test file three times rather than using its per-test
repeatsoption, which would leave a repeat setting in the test code.
Timeouts
Use the narrowest scope that covers the slow operation:
- One assertion or action: its own
timeoutoption. - One test: in Playwright,
test.setTimeout()ortest.slow()inside the test; in Vitest, the test’stimeoutoption. - One hook: in Playwright,
test.setTimeout()insidebeforeAllorafterAllsets that hook’s timeout, while insidebeforeEachit sets the test’s, becausebeforeEachshares the test’s timeout.test.slow()isn’t allowed inbeforeAllorafterAll. In Vitest, pass the timeout as the hook’s last argument; the defaulthookTimeoutis 10 seconds. - One suite:
test.describe.configure({ timeout })in Playwright, or thedescribeblock’stimeoutoption in Vitest. Both set the timeout of each test in the suite, not one budget for the whole suite.
Retries and flaky reporting
- In Playwright, configure retries for CI only, such as
retries: process.env.CI ? 2 : 0, withtrace: 'on-first-retry', or'retain-on-failure-and-retries'(1.59+) to keep a trace of every failed attempt. - To fail a CI run that has flaky passes, set
failOnFlakyTests: !!process.env.CI(1.52+). Add it only when the repository wants that policy. - In Vitest,
retrytakes a number, or{ count, delay, condition }(4.1+) for retries with a delay or only on matching errors. - A test that writes data is retry-safe only when it removes exactly what an earlier attempt left, uses data unique to each attempt, or writes idempotently. Clean up in fixture teardown or
afterEach, which still run after a timeout. Otherwise turn its retries off withtest.describe.configure({ retries: 0 })and a comment giving the reason. - Set
forbidOnly: !!process.env.CIin the Playwright config, so a committedtest.onlyfails CI. Vitest already does this throughallowOnly, which defaults to!process.env.CI.
Tags and selection
- In Playwright, tag tests with (1.42+)
{ tag: '@name' }and select them with--grepand--grep-invert. - In Vitest, (4.1+) declare tags under
test.tagsin the config, attach them with{ tags: [...] }, and select tests with--tags-filter. - Keep runs that find no tests failing: never pass
--passWithNoTeststo Vitest or--pass-with-no-teststo Playwright in a required run, so a wrong filter can’t pass silently.
Vitest specifics
- Tests that need no DOM run in the default
nodeenvironment. Add// @vitest-environment nodeonly when the project’s default differs, and// @vitest-environment jsdomfor behavior a simulated DOM can verify. Keep the comment’s exact syntax. - Browser Mode is not an environment; it needs a project with
browserconfigured. Use it, or the end-to-end runner, for behavior that needs a real browser. test.eachtitles support%s,%d,%i,%f,%j,%o,%O,%c,%%,%#for the 0-based index,%$for the 1-based index, and$namefor properties of object cases.%pis not supported and prints as is.- Vitest 4 removed the
workspaceoption and thevitest.workspacefile; useprojectsin the config. It also removedpoolOptions, whose settings are now top-level options, andenvironmentMatchGlobsandpoolMatchGlobs, which are now projects with their own settings. - Vitest 5 changes behavior that older tests rely on:
clearMocksdefaults to true.vi.mock()andvi.hoisted()must be at the top level of the file.- An un-awaited
resolvesorrejectsassertion fails the test. toHaveTextContent()requires an exact match; usetoMatchTextContent()for partial or regex matches.expect.poll()rejects on timeout.