Flutter Web Testing
Patrol provides support for running integration tests on Flutter web applications using Playwright, a powerful browser automation framework. This allows you to handle native web browser interactions in your tests.
How it Works
When you run Patrol tests on web, the following happens:
- Flutter Web Server: Patrol starts a Flutter web server that serves your application
- Playwright Test Runner: Patrol automatically launches Chromium using Playwright
- Platform Actions Bridge: For platform automation calls (like
$.platform.web.grantPermissions()), your Dart test code sends requests to Playwright with actions to be executed - Results Collection: Test results are collected and reported back in Patrol's standard format
This architecture allows you to write the same Patrol tests that work across mobile and web platforms, with Playwright handling browser-specific interactions.
Prerequisites
Before running Patrol tests on web, ensure you have installed Node.js.
Running Tests on Web
To run your Patrol tests on web, use the --device flag with chrome, e.g.:
patrol test --device chrome --target patrol_test/login_test.dartWhen you first run tests on web, Patrol will automatically install Node.js dependencies including Playwright and its browser binaries. This may take a moment.
Running Tests in your CI pipeline
CI environments typically do not provide a graphical display. For this reason, it's recommended to run Patrol web tests in headless mode.
By default, Patrol runs web tests in headed mode (with a visible browser window).
In CI pipelines (e.g. GitHub Actions, GitLab CI), it's recommended to enable headless mode using --web-headless true:
patrol test \
--device chrome \
--target patrol_test/login_test.dart \
--web-headless trueSupported Native Actions on Web
Patrol supports the following platform actions through Playwright:
// Dark mode
await $.platform.web.enableDarkMode();
await $.platform.web.disableDarkMode();
// Keyboard input
await $.platform.web.pressKey(key: 'a');
await $.platform.web.pressKeyCombo(keys: ['Control', 'a']);
// Permissions
await $.platform.web.grantPermissions(permissions: ['clipboard-read', 'clipboard-write']);
await $.platform.web.clearPermissions();
// Clipboard
await $.platform.web.setClipboard(text: 'test');
final clipboard = await $.platform.web.getClipboard();
// Browser navigation
await $.platform.web.goBack();
await $.platform.web.goForward();
// Cookies
await $.platform.web.addCookie(name: 'test_cookie', value: 'cookie_value', url: 'http://localhost:8080');
final cookies = await $.platform.web.getCookies();
await $.platform.web.clearCookies();
// File operations
final fileContent = utf8.encode('Hello from Patrol file upload test!');
final file = UploadFileData(
name: 'example_file.txt',
content: fileContent,
mimeType: 'text/plain',
);
await $.platform.web.uploadFile(files: [file]);
final downloads = await $.platform.web.verifyFileDownloads();
// Dialogs - because of blocking nature of dialogs, dialogs need to be subscribed to before triggering
final dialogFuture = $.platform.web.acceptNextDialog();
// ... trigger the dialog ...
final dialogText = await dialogFuture;
final dialogFuture = $.platform.web.dismissNextDialog();
// ... trigger the dialog ...
final dialogText = await dialogFuture;
// Web element interactions (for iframes and external HTML)
await $.platform.web.scrollToWeb(selector: inputSelector, iframeSelector: iframeSelector);
await $.platform.web.enterTextWeb(selector: inputSelector, text: 'Hello from Patrol!', iframeSelector: iframeSelector);
await $.platform.web.tapWeb(selector: buttonSelector, iframeSelector: iframeSelector);
// Window operations
await $.platform.web.resizeWindow(size: Size(800, 600));See Multi-page support below for working with multiple browser pages (tabs).
Multi-page support
Some tests need more than one browser page: a link that opens a new tab, an OAuth popup, or an external site you want to check alongside your app. Patrol lets you open, switch between, inspect, and close these pages from your test.
Every open page has a stable ID that Patrol returns when the page opens. The page your test starts on is the initial page. It hosts the Flutter app under test, is always open, and can't be closed. One page is active at a time, and platform actions (taps, text entry, navigation, and so on) run against it. The initial page is active when the test starts.
Opening a new page
openNewPage opens a page, navigates it to a URL, and returns its ID. It doesn't switch to
the new page for you, so call switchToPage when you want to act on it:
final pageId = await $.platform.web.openNewPage(url: 'https://example.com');
await $.platform.web.switchToPage(pageId: pageId);Switching between pages
switchToPage makes a page active, and switchToInitialPage takes you back to the Flutter app.
Actions after the switch run against the newly active page:
await $.platform.web.switchToPage(pageId: pageId);
// ... interact with that page ...
await $.platform.web.switchToInitialPage();Inspecting pages
final currentPage = await $.platform.web.getCurrentPage(); // active page ID
final url = await $.platform.web.getCurrentPageUrl(); // active page URL
final pages = await $.platform.web.getPages(); // all open page IDsClosing pages
closePage closes a page by ID. You can't close the initial page, and closing the active page
switches you back to the initial page:
await $.platform.web.closePage(pageId: pageId);Handling popups
When your app opens a page you didn't open yourself (for example a button that spawns a popup
window), use waitForPopup to catch it. Just like dialogs,
subscribe before triggering the action, since the popup can appear before you get to await it:
final popupFuture = $.platform.web.waitForPopup();
await $.platform.web.tap(WebSelector(cssOrXpath: '#open-popup'));
final popupId = await popupFuture;
await $.platform.web.switchToPage(pageId: popupId);Example
Opening a page, checking its URL, then cleaning up:
patrol('opens an external page in a new tab', ($) async {
await $.pumpWidgetAndSettle(const MyApp());
final pageId = await $.platform.web.openNewPage(url: 'https://example.com');
await $.platform.web.switchToPage(pageId: pageId);
final url = await $.platform.web.getCurrentPageUrl();
expect(url, contains('example.com'));
await $.platform.web.closePage(pageId: pageId);
await $.platform.web.switchToInitialPage();
});