QA Interview Hub · Knowledge base · Quizzes
Playwright
Interaction: actions, tabs, frames, files, dialogs
Actions on elements: what to use when
The main actions and nuances that are asked about in interviews:
fill clears the field and inserts the value at once. This is the standard for inputs.pressSequentially types character by character with keyboard events. It is needed for fields with autocomplete or masks that react to every keypress. The old type is deprecated.press('Enter') and combinations like press('ControlOrMeta+A'): a modifier that works on both Windows and macOS.check and uncheck for checkboxes and radios: they verify that the state really changed. setChecked(bool) is convenient in parameterised tests.selectOption for a native select: by value, label or an array for multiple. A custom dropdown made of divs doesn't work like that: there you click the option with getByRole('option').hover, dblclick, click({ button: 'right' }), click({ modifiers: ['Shift'] }).dragTo(target) for drag-and-drop. If the DnD library is non-standard, it is done manually through page.mouse.down, move, up.
await page.getByLabel('City').pressSequentially('Lvi', { delay: 100 });
await page.getByRole('option', { name: 'Lviv' }).click();
await page.getByLabel('Currency').selectOption({ label: 'UAH' });
await page.getByTestId('card-1').dragTo(page.getByTestId('column-done'));
For date fields fill expects the format the input accepts: for type="date" it is 2026-10-08, regardless of locale.
In an interview: For ordinary fields I use fill, and pressSequentially only when the field reacts to every keypress, like autocomplete. For a native select, selectOption, for custom dropdowns a click on option. check is better than click for checkboxes, because it verifies that the state changed.
New tabs, popups and iframes
A new tab is a new Page in the same context. It has to be "caught" with an event, and the promise is created before the click, otherwise the event may arrive before I start listening:
const popupPromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'Agreement' }).click();
const popup = await popupPromise;
await popup.waitForLoadState();
await expect(popup).toHaveURL(/contract/);
page.waitForEvent('popup') catches tabs opened from a specific page. context.waitForEvent('page') catches any new page in the context. A simplified option for tests where the tab doesn't matter is to remove target="_blank", but then the real behaviour isn't checked.
An iframe requires an explicit move into its document. There are two ways:
const card = page.frameLocator('#payment-iframe');
await card.getByLabel('Card number').fill('4242424242424242');
const frame = page.locator('#payment-iframe').contentFrame();
await frame.getByRole('button', { name: 'Pay' }).click();
frameLocator and contentFrame() are lazy, like ordinary locators, and wait for the frame to appear. Cross-origin frames, for example a payment widget from another domain, work the same way, because Playwright drives the browser from outside.
Nested frames go as a chain: page.frameLocator('#outer').frameLocator('#inner').
In an interview: For a new tab I first create the waitForEvent('popup') promise, then click, then await the promise, otherwise you can miss the event. For an iframe I use frameLocator or contentFrame(), and cross-origin frames of payment systems work without workarounds.
File uploads and downloads
Upload is done with setInputFiles on an input with type="file", even if it is hidden under a pretty button:
await page.getByLabel('Document').setInputFiles('fixtures/passport.pdf');
await page.getByLabel('Documents').setInputFiles(['a.pdf', 'b.pdf']);
await page.getByLabel('Document').setInputFiles({
name: 'report.csv',
mimeType: 'text/csv',
buffer: Buffer.from('id,amount\n1,100'),
});
await page.getByLabel('Document').setInputFiles([]);
The buffer variant lets you generate the file right in the test, without storing it in the repository. An empty array clears the selection. If there is no input in the DOM and the dialog opens dynamically, you catch the filechooser event: page.waitForEvent('filechooser'), then fileChooser.setFiles(...).
A download is checked through the download event:
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export CSV' }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe('transactions.csv');
const filePath = test.info().outputPath(download.suggestedFilename());
await download.saveAs(filePath);
Then the file can be read through fs and its content checked: the number of rows, amounts, encoding. This is important in fintech, where reports for accounting are part of the requirements. Downloaded files are deleted when the context closes, so saveAs is needed if the file must be kept.
In an interview: For upload I use setInputFiles, often with a buffer to generate the file in the test. For download I catch the download event, save the file to the test's outputPath and check not only the name but also the content.
alert, confirm and prompt dialogs
Playwright automatically dismisses native browser dialogs by default. That is, confirm will return false, and if a test expects a deletion after confirmation, it won't happen. To control a dialog, you subscribe to the event before the action:
page.once('dialog', async (dialog) => {
expect(dialog.type()).toBe('confirm');
expect(dialog.message()).toContain('Delete the account?');
await dialog.accept();
});
await page.getByRole('button', { name: 'Delete' }).click();
await expect(page.getByText('Account deleted')).toBeVisible();
For prompt you pass text to accept: dialog.accept('Reason'). To refuse, use dialog.dismiss().
A trap: if there is a dialog listener, Playwright no longer closes the dialog itself. If you forget accept or dismiss in the handler, the page hangs, because a native dialog blocks JavaScript execution. So I use page.once, not page.on, when a dialog is expected once, so the handler doesn't affect other steps.
A separate case is beforeunload, the "Leave the page?" dialog. In tests it appears on page.close({ runBeforeUnload: true }) and is handled the same way.
Custom modal windows made in HTML are not browser dialogs. For them there are ordinary locators: getByRole('dialog').
In an interview: Playwright dismisses native dialogs by default, so for confirm I register page.once('dialog') in advance and call accept. If there is a listener, the dialog must be closed in it, otherwise the page hangs.