Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions dev/e2e_app/macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import FlutterMacOS
import Foundation

import app_links
import file_picker
import file_saver
Expand All @@ -21,8 +22,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterLocalNotificationsPlugin.register(
with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
Expand Down
6 changes: 6 additions & 0 deletions dev/e2e_app/patrol_test/web/web_example_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ class _HomePage extends StatelessWidget {
icon: const Icon(Icons.arrow_forward),
label: const Text('Go to Page 1'),
),
ElevatedButton.icon(
key: const Key('open_popup_button'),
onPressed: () => web.window.open('about:blank', '_blank'),
icon: const Icon(Icons.open_in_new),
label: const Text('Open Popup Window'),
),
ElevatedButton.icon(
onPressed: () => context.go('/iframe'),
icon: const Icon(Icons.web),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import '../common.dart';

import 'web_example_app.dart';

void main() {
patrol('interact with form in new page while navigating Flutter app', (
$,
) async {
await $.pumpWidgetAndSettle(const WebExampleApp());

final formUrl = '${Uri.base.origin}/assets/assets/iframe_content.html';
final newPageId = await $.platform.web.openNewPage(url: formUrl);
await $.pumpAndSettle();
await Future<void>.delayed(const Duration(seconds: 2));
Comment thread
Komoszek marked this conversation as resolved.

final pages = await $.platform.web.getPages();
expect(pages.length, 2);

// Fill form in the new page
await $.platform.web.switchToPage(pageId: newPageId);
await $.pumpAndSettle();
await Future<void>.delayed(const Duration(seconds: 1));

await $.platform.web.enterText(
WebSelector(cssOrXpath: '#test-input'),
text: 'Patrol multi-page test',
);
await $.platform.web.tap(WebSelector(cssOrXpath: '#submit-button'));
await Future<void>.delayed(const Duration(seconds: 1));

// Switch back and navigate the Flutter app
await $.platform.web.switchToMainPage();
await $.pumpAndSettle();

await $('Go to Page 1').scrollTo().tap();
await $.pumpAndSettle();
await Future<void>.delayed(const Duration(seconds: 2));

expect($('This is Page 1'), findsOneWidget);

await $.platform.web.goBack();
await $.pumpAndSettle();
await Future<void>.delayed(const Duration(seconds: 2));

expect($('This is the home page'), findsOneWidget);

await $.platform.web.closePage(pageId: newPageId);

final remaining = await $.platform.web.getPages();
expect(remaining.length, 1);
});
}
104 changes: 104 additions & 0 deletions dev/e2e_app/patrol_test/web/web_example_multi_tab_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import 'dart:collection';

import '../common.dart';

import 'web_example_app.dart';

void main() {
patrol('open external page, fill form, and return to Flutter app', ($) async {
await $.pumpWidgetAndSettle(const WebExampleApp());

final formUrl = '${Uri.base.origin}/assets/assets/iframe_content.html';
final newPageId = await $.platform.web.openNewPage(url: formUrl);
await $.pumpAndSettle();
await Future<void>.delayed(const Duration(seconds: 2));
Comment thread
Komoszek marked this conversation as resolved.

await $.platform.web.switchToPage(pageId: newPageId);
await $.pumpAndSettle();
await Future<void>.delayed(const Duration(seconds: 1));

final currentTab = await $.platform.web.getCurrentPage();
expect(currentTab, newPageId);

final currentUrl = await $.platform.web.getCurrentPageUrl();
expect(currentUrl, formUrl);

await $.platform.web.enterText(
WebSelector(cssOrXpath: '#test-input'),
text: 'Hello from new tab',
);
await $.platform.web.tap(WebSelector(cssOrXpath: '#submit-button'));
await Future<void>.delayed(const Duration(seconds: 1));

await $.platform.web.switchToMainPage();
await $.pumpAndSettle();

expect(await $.platform.web.getCurrentPage(), 'page_0');
expect($('This is the home page'), findsOneWidget);

await $.platform.web.closePage(pageId: newPageId);
});

patrol('open leancode.co, accept cookies, and close tab', ($) async {
await $.pumpWidgetAndSettle(const WebExampleApp());

final newPageId = await $.platform.web.openNewPage(
url: 'https://leancode.co/',
);
await $.pumpAndSettle();
await Future<void>.delayed(const Duration(seconds: 3));

await $.platform.web.switchToPage(pageId: newPageId);
await $.pumpAndSettle();
await Future<void>.delayed(const Duration(seconds: 2));

await $.platform.web.tap(WebSelector(text: 'Accept All'));
await Future<void>.delayed(const Duration(seconds: 1));

await $.platform.web.switchToMainPage();
await $.pumpAndSettle();

expect($('This is the home page'), findsOneWidget);

await $.platform.web.closePage(pageId: newPageId);
});

patrol('cross-tab cookies persist across browser context', ($) async {
await $.pumpWidgetAndSettle(const WebExampleApp());

await $.platform.web.addCookie(
name: 'session',
value: 'abc123',
url: Uri.base.origin,
);

var cookies = await $.platform.web.getCookies();
var sessionCookie = cookies.firstWhere(
(c) => c['name'] == 'session',
orElse: LinkedHashMap<Object?, Object?>.new,
);
expect(sessionCookie['value'], 'abc123');

final formUrl = '${Uri.base.origin}/assets/assets/iframe_content.html';
final newPageId = await $.platform.web.openNewPage(url: formUrl);
await $.pumpAndSettle();
await Future<void>.delayed(const Duration(seconds: 2));

await $.platform.web.switchToPage(pageId: newPageId);
await $.pumpAndSettle();
await Future<void>.delayed(const Duration(seconds: 1));

cookies = await $.platform.web.getCookies();
sessionCookie = cookies.firstWhere(
(c) => c['name'] == 'session',
orElse: LinkedHashMap<Object?, Object?>.new,
);
expect(sessionCookie['value'], 'abc123');

await $.platform.web.switchToMainPage();
await $.pumpAndSettle();

await $.platform.web.clearCookies();
await $.platform.web.closePage(pageId: newPageId);
});
}
15 changes: 13 additions & 2 deletions docs/documentation/web.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ patrol test --device chrome --target patrol_test/login_test.dart
```

<Info>
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.
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.
</Info>

### Running Tests in your CI pipeline
Expand Down Expand Up @@ -94,4 +95,14 @@ await $.platform.web.enterTextWeb(selector: inputSelector, text: 'Hello from Pat
await $.platform.web.tapWeb(selector: buttonSelector, iframeSelector: iframeSelector);
// Window operations
await $.platform.web.resizeWindow(size: Size(800, 600));
// Multi-tab management
final pageId = await $.platform.web.openNewPage(url: 'https://example.com');
await $.platform.web.switchToPage(pageId: pageId);
final currentPage = await $.platform.web.getCurrentPage();
final pages = await $.platform.web.getPages();
await $.platform.web.closePage(pageId: pageId);
final popupPageIdFuture = $.platform.web.waitForPopup();
// ... trigger the popup ...
final popupPageId = await popupPageIdFuture;
final url = await $.platform.web.getCurrentPageUrl();
```
18 changes: 8 additions & 10 deletions packages/patrol/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## Unreleased

- Add multi-tab browser support for web tests: `openNewPage`, `closePage`, `switchToPage`, `switchToMainPage`, `getPages`, `getCurrentPage`, `getCurrentPageUrl`, `waitForPopup`. (#2871)

## 4.5.0

- Fix `appId` not being passed down on `$.platform.mobile.enterText` and `$.platform.mobile.enterTextByIndex` (#2992)
Expand All @@ -13,7 +17,6 @@
- Add `stopMockLocation` method to `PlatformAutomator` and make mockLocation method less flaky (#2937)
- Fix matching test entries to produce test summary. (#2998)


## 4.3.0

- Fix WASM compatibility by migrating conditional imports from `dart.library.html` to `dart.library.js_interop`. (#2960)
Expand Down Expand Up @@ -46,8 +49,8 @@ Patrol 4.0 is here!

Read the article announcing Patrol 4.0 [here](https://leancode.co/blog/patrol-4-0-release).

- New API for native/platform interactions:
- Introduce new way of communicating with platform (`PlatformAutomator`). (#2789)
- New API for native/platform interactions:
- Introduce new way of communicating with platform (`PlatformAutomator`). (#2789)
- Deprecate `NativeAutomator` and `NativeAutomator2`. (#2789)

- Add support for running Patrol tests on Web:
Expand All @@ -74,7 +77,7 @@ Read the article announcing Patrol 4.0 [here](https://leancode.co/blog/patrol-4-
- `--web-user-agent`, `--web-viewport`.
- Add support for configurable test directory via `test_directory` option in `pubspec.yaml`. (#2728)
- Introduces *experimental* `--full-isolation` flag that uninstall the app between each run on iOS Simulator. (#2803)
- Bump `patrol_log` to `0.6.0`.
- Bump `patrol_log` to `0.6.0`.

- **BREAKING CHANGE**
- Change default test directory from `integration_test` to `patrol_test`. (#2728)
Expand All @@ -91,7 +94,7 @@ This version requires version 3.11.0 of `patrol_cli` package.

## 3.19.0

- Fix logging for `$.native.pullToRefresh()` and `$.native.swipeBack()`. (#2707)
- Fix logging for `$.native.pullToRefresh()` and `$.native.swipeBack()`. (#2707)
- Fix `$.native.enableDarkMode()` and `$.native.disableDarkMode()` on iOS 18 simulators. (#2705)
- Add support for de, fr and pl languages for native methods that operates on strings. (#2659)
- Add support for gallery permission dialog on iOS 17. (#2659)
Expand Down Expand Up @@ -320,7 +323,6 @@ Other changes:

- Bump minimum supported Flutter version to 3.16
- **BREAKING:**

- Remove `bindingType` parameter from `patrolTest()` function. Now only
`PatrolBinding` is used and it's automatically initialized (#1882)
- Remove `nativeAutomation` parameter from `patrolTest()` function. Now it's
Expand Down Expand Up @@ -762,7 +764,6 @@ flakiness.
## 0.5.0

- Revamp scrolling and dragging (#217)

- New `MaestroTester.dragUntilExists()`
- Fixed `MaestroTester.dragUntilVisible()`'s behavior
- New `MaestroTester.scrollUntilExists` method
Expand Down Expand Up @@ -846,7 +847,6 @@ Native:
## 0.3.2

- Improve selector engine:

- Make it possible to pass a `Key` as `matching` to
`MaestroTester.call(dynamic matching)` and `MaestroFinder.$(dynamic
matching)`
Expand All @@ -859,7 +859,6 @@ matching)`
## 0.3.1

- Improve selector engine:

- Make it possible to pass a `MaestroFinder` as `matching` to
`MaestroTester.call(dynamic matching)` and `MaestroFinder.$(dynamic
matching)`
Expand All @@ -876,7 +875,6 @@ matching)`

- Introduce `Selector` class, which can be passed into `Maestro.tap(selector)`.
- Add more platform functionality:

- `Maestro.enableWifi()` and `Maestro.disableWifi()`
- `Maestro.enableCellular()` and `Maestro.disableCellular()`
- `Maestro.enableDarkMode()` and `Maestro.disableDarkMode()`
Expand Down
28 changes: 28 additions & 0 deletions packages/patrol/lib/src/platform/web/web_automator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,32 @@ abstract interface class WebAutomator {

/// Returns a list of all files downloaded during the single test.
Future<List<String>> verifyFileDownloads();

/// Opens a new browser page navigating to [url].
/// Returns the stable page ID of the new page.
Future<String> openNewPage({required String url});

/// Closes the page with the given [pageId].
Future<void> closePage({required String pageId});

/// Switches the active page to [pageId].
/// All subsequent actions will target this page until switched again.
Future<void> switchToPage({required String pageId});

/// Switches to the main page.
/// All subsequent actions will target the main page until switched again.
Future<void> switchToMainPage();

/// Returns identifiers of all open pages.
Future<List<String>> getPages();

/// Returns the ID of the currently active page.
Future<String> getCurrentPage();

/// Returns the URL of the currently active page.
Future<String> getCurrentPageUrl();

/// Waits for a popup/new page to open.
/// Returns the page ID of the newly opened page.
Future<String> waitForPopup();
}
Loading
Loading