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:

Playwrightk6
import { ... } from '@playwright/test'import { ... } from 'k6/browser' (type Locator, type Page)
vuerUrls.oss()oss() from ./lib/env.ts
readonly class fieldsdrop the modifier
import './page.js' / extensionlessexplicit .ts on relative imports (tsconfig has allowImportingTsExtensions)
test.describe / test() / beforeEachone export default async function ()
beforeEach bodyinlined at the head of each case block
test.step()no equivalent — inline it
storageState fixtureno equivalent — every file calls loginOperator(page)
expect(v).toBe(y)check(v, { '<original test() name verbatim>': v => v === y })
registrationone export {} line in all.ts + one options.scenarios entry

Keep the check name verbatim

Use the original test() name, character for character, as the check() 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's check() does not

Anything in the original that relied on expect() polling needs an explicit waiting locator (visible() / hidden()) or a polling helper before the check(). A straight expect → check translation 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 BrowserBrowserContext

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 capabilityDetail
File uploadsNo waitForEvent('filechooser'), no FileChooser, no Locator.setInputFiles. page.waitForEvent() accepts only 'console' | 'request' | 'response'. File-upload tests are not portable.
CoverageNo page.coverage.*. The Playwright V8→istanbul fixture (test/tests/playwright/fixtures.ts) has no k6 equivalent.
Network interceptionNo page.route().
NodeNo Node built-ins, no require of server/** — see the seeding blocker in k6-e2e-harness-vuer-oss.
:has-text() selectorAbsent 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 in open-hours.test.ts are missing their await, so they never assert.