Playwright compatibility and conversion
UserLimit converts a script into saved steps. A script importing successfully does not mean it will behave exactly as it does in Playwright.
Important conversion differences
Imported scripts do not retain all Playwright behaviour. Assertions and page-state waits are skipped, and actions without await can be silently omitted. The saved step list therefore determines what UserLimit will run; successful execution of those steps does not establish the result of an ignored assertion.
Action conversion matrix
| Input example | Status / result | Boundary |
|---|---|---|
| await page.goto('https://your-verified-domain.invalid/catalogue'); | Supported as documented → Open | Full HTTP(S) URL required; first action must open a page. Navigation options are not preserved. |
| await page.locator('#product').click(); | Supported as documented → Click | The browser uses the first matching element; do not assume Playwright strictness. |
| await page.getByRole('link', { name: 'Canvas backpack', exact: true }).click(); | Supported as documented → Click | Role and name become a selector, not an executed source call. |
| await page.locator('#q').fill('bag'); | Converted with a difference → Type | Types character by character without clearing existing text. fill, type and pressSequentially all become Type, so an imported fill does not replace the field’s current value. |
| await page.locator('#q').fill(''); | Accepted but not executed → omitted | Empty fill does not produce a clearing step. |
| await page.locator('#consent').check(); | Converted with a difference → Click | check, uncheck, setChecked and tap all become Click; desired checked state is not preserved. |
| await page.locator('#q').press('Enter'); | Supported as documented → Press | Key combination retained. page.keyboard.press targets the focused element. |
| await page.locator('#size').selectOption({ label: 'Medium' }); | Supported as documented → Select | One option by string value, label or value object; not multi-selection. |
| await page.waitForTimeout(1000); | Supported as documented → Wait | Milliseconds; maximum 60,000 per explicit wait. Negative values fail scenario validation. |
Locators and source structure
Supported locator forms include locator, getByRole, getByText, getByLabel, getByPlaceholder, getByAltText, getByTitle and getByTestId, which uses the data-testid attribute. Chained locators narrow the target, while first, last, nth, filter with hasText/hasNotText/has/hasNot, and or are converted into selectors. Locator arguments can use literal strings, supported regular expressions or constants containing strings or locators.
await page.goto('https://your-verified-domain.invalid/catalogue');
const product = page.getByTestId('featured-product');
await product.click();
This example produces an Open step followed by a Click using the data-testid selector. A single test wrapper is also accepted, and its title can provide the scenario name, but test.step does not create a separate group of saved steps.
The supported source structure is a linear scenario with one awaited action per line. Helpers, fixtures, hooks, authentication state, multiple tabs and request interception are outside this execution model because UserLimit runs the converted steps rather than the surrounding JavaScript.
Accepted but not executed
| Source | Result / consequence |
|---|---|
| await expect(page.locator('#confirmation')).toBeVisible(); | No assertion step; no confirmed business outcome. |
| page.locator('#product').click(); | No Click step without await. |
| await page.waitForSelector('#product'); | No wait step. UserLimit waits for the element before an action. |
| await page.waitForURL('**/product'); | No URL assertion or wait. |
| await page.waitForLoadState('networkidle'); | No requested network-idle policy. |
| await page.setViewportSize({ width: 390, height: 844 }); | No viewport change; see environment reference. |
| await page.locator('#product').waitFor(); | No locator-wait step. |
| await page.screenshot({ path: 'page.png' }); | No screenshot produced by this statement. |
Other skipped calls include waitForResponse, waitForRequest, waitForNavigation, waitForEvent, bringToFront, title, url, content and pause; locator reads such as isVisible, count and textContent are not assertions. Their return values cannot control the saved scenario.
Rejected constructs
| Example | Result |
|---|---|
| if (true) { … } / for (…) { … } | Conditions and loops are rejected. |
| await Promise.all([…]); | Rejected; actions must be sequential. |
| await page.frameLocator('iframe').locator('button').click(); | Frames are rejected. |
| await page.locator('#product').hover(); | Hover is not supported. |
| await page.locator('#product').dblclick(); | Double clicks are rejected. |
| await page.locator('#file').setInputFiles('file.txt'); | File upload is rejected. |
| await page.goto('/catalogue'); | Relative navigation is rejected. |
| await page.goto(`https://${host}/`); | Interpolated template strings are rejected. |
Example conversion
await page.goto('https://your-verified-domain.invalid/catalogue');
await page.getByRole('link', { name: 'Canvas backpack', exact: true }).click();
await expect(page.getByRole('heading')).toBeVisible();
The resulting action list has two actions: Open the catalogue, then Click the product link. It does not contain a heading assertion. The scenario can therefore confirm that the actions executed, but cannot confirm that the heading became visible.
Line errors identify rejected input. Other unsupported constructs may be omitted, so a successful import does not guarantee that every source statement was converted. The troubleshooting guide can help when the source and saved actions differ.