How to port a Playwright browser test in vuer_oss test/tests/e2e/ to k6/browser under test/tests/k6/, and — more importantly — the hard limits of k6/browser that make some tests unportable. Established while porting 12 test files on FKITDEV-9200; limits verified against @types/k6 2.0.1. Harness wiring lives in k6-e2e-harness-vuer-oss.
The mechanical part
Page objects port roughly 1:1:
| Playwright | k6 |
|---|---|
import { ... } from '@playwright/test' | import { ... } from 'k6/browser' (type Locator, type Page) |
vuerUrls.oss() | oss() from ./lib/env.ts |
readonly class fields | drop the modifier |
import './page.js' / extensionless | explicit .ts on relative imports (tsconfig has allowImportingTsExtensions) |
test.describe / test() / beforeEach | one export default async function () |
beforeEach body | inlined at the head of each case block |
test.step() | no equivalent — inline it |
storageState fixture | no equivalent — every file calls loginOperator(page) |
expect(v).toBe(y) | check(v, { '<original test() name verbatim>': v => v === y }) |
| registration | one export {} line in all.ts + one options.scenarios entry |
Keep the check name verbatim
Use the original
test()name, character for character, as thecheck()key. That is the only thread left connecting a k6 failure back to the Playwright test it came from — there is no other mapping.
Waiting — the two rules that actually bite
k6 does not auto-wait on navigation the way Playwright does.
await Promise.all([page.waitForNavigation(), locator.click()])But do NOT add that wrapper when the click is already followed by
page.waitForURL(...)The second waiter can miss an already-completed navigation and hang. One waiter per navigation, not two.
Playwright's
expect()auto-retries; k6'scheck()does notAnything in the original that relied on
expect()polling needs an explicit waiting locator (visible()/hidden()) or a polling helper before thecheck(). A straightexpect → checktranslation of a retried assertion is a flaky test.
Hard limits of k6/browser
Verified against the @types/k6 2.0.1 type surface.
1-to-1
Browser↔BrowserContext
newContext()throws if a context is already open (browser/index.d.ts:754-766). Two consequences:
- A second actor in a test must be a second page on the same context, not a second context.
- Every test must close its context, or all subsequent tests die.
| Missing capability | Detail |
|---|---|
| File uploads | No waitForEvent('filechooser'), no FileChooser, no Locator.setInputFiles. page.waitForEvent() accepts only 'console' | 'request' | 'response'. File-upload tests are not portable. |
| Coverage | No page.coverage.*. The Playwright V8→istanbul fixture (test/tests/playwright/fixtures.ts) has no k6 equivalent. |
| Network interception | No page.route(). |
| Node | No Node built-ins, no require of server/** — see the seeding blocker in k6-e2e-harness-vuer-oss. |
:has-text() selector | Absent from the k6 type surface. Use the option form: locator('button', { hasText: '…' }). |
Scheduling limits (they belong to the harness, repeated here because they surface during porting): scenarios with no startTime all start at t=0 and run concurrently, and shared-iterations defaults to maxDuration: 10m.
Faithful-porting caveats
Some assertions in the originals are vacuous. They were reproduced as-is rather than silently fixed — if you “fix” them during a port you are changing test semantics under cover of a migration.
hasEmailError()/hasRightsError()return!!page.locator(...)— always truthy, they assert nothing.- Several
expect().toHaveText()calls inopen-hours.test.tsare missing theirawait, so they never assert.
Related
- k6-e2e-harness-vuer-oss — harness wiring, CI gap, ordering, seeding blocker
- FKITDEV-9200 — the migration ticket and what has been ported so far
- vuer_oss · vuer_docker
- typescript-in-vuer-repos — explicit
.tsimports, and why nothing typechecks this