Webinar: Mastering Patrol & AI: Next-Level E2E Testing. Register now

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:

  1. Flutter Web Server: Patrol starts a Flutter web server that serves your application
  2. Playwright Test Runner: Patrol automatically launches Chromium using Playwright
  3. Platform Actions Bridge: For platform automation calls (like $.platform.web.grantPermissions()), your Dart test code sends requests to Playwright with actions to be executed
  4. 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.dart

When 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 true

Supported 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 IDs

Closing 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();
});

On this page