From ccdae41df8d11fb2ffa3f582e23bfa1174abcac1 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Wed, 19 Feb 2025 13:45:11 +0100 Subject: [PATCH] Take screenshots from pub_integration tests (store locally). --- .../lib/src/pub_puppeteer_helpers.dart | 8 +- .../lib/src/screenshot_utils.dart | 102 ++++++++++++++++++ pkg/pub_integration/lib/src/test_browser.dart | 3 + pkg/pub_integration/test/browser_test.dart | 11 ++ .../test/fake_sign_in_test.dart | 11 ++ .../test/pkg_admin_page_test.dart | 3 + pkg/pub_integration/test/report_test.dart | 4 + .../test/search_completition_test.dart | 5 + .../test/search_update_test.dart | 4 + 9 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 pkg/pub_integration/lib/src/screenshot_utils.dart diff --git a/pkg/pub_integration/lib/src/pub_puppeteer_helpers.dart b/pkg/pub_integration/lib/src/pub_puppeteer_helpers.dart index bac1403aae..fb35770c42 100644 --- a/pkg/pub_integration/lib/src/pub_puppeteer_helpers.dart +++ b/pkg/pub_integration/lib/src/pub_puppeteer_helpers.dart @@ -2,10 +2,10 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'dart:async'; -import 'dart:io'; import 'package:puppeteer/puppeteer.dart'; +import 'screenshot_utils.dart'; import 'test_browser.dart'; const webmastersReadonlyScope = @@ -46,10 +46,6 @@ Future listingPageInfo(Page page) async { } extension PubPageExt on Page { - Future saveScreenshot(String path) async { - await File(path).writeAsBytes(await screenshot()); - } - Future waitFocusAndType(String selector, String text) async { await waitForSelector(selector, timeout: Duration(seconds: 5)); await focus(selector); @@ -221,7 +217,7 @@ extension PubPageExt on Page { return; } } - await saveScreenshot('layout-timeout.png'); + await writeScreenshotToFile('layout-timeout.png'); throw TimeoutException('Did not have a stable layout in $timeout.'); } diff --git a/pkg/pub_integration/lib/src/screenshot_utils.dart b/pkg/pub_integration/lib/src/screenshot_utils.dart new file mode 100644 index 0000000000..fc00c37e83 --- /dev/null +++ b/pkg/pub_integration/lib/src/screenshot_utils.dart @@ -0,0 +1,102 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:puppeteer/puppeteer.dart'; + +// Default screen with 16:10 ratio. +final desktopDeviceViewport = DeviceViewport(width: 1280, height: 800); + +final _screenshotDir = Platform.environment['PUB_SCREENSHOT_DIR']; +final _isScreenshotDirSet = + _screenshotDir != null && _screenshotDir!.isNotEmpty; + +// Set this variable to enable screenshot files to be updated with new takes. +// The default is to throw an exception to prevent accidental overrides from +// separate tests. +final _allowScreeshotUpdates = + Platform.environment['PUB_SCREENSHOT_UPDATE'] == '1'; + +// Note: The default values are the last, so we don't need reset +// the original values after taking the screenshots. +final _themes = ['dark', 'light']; +final _viewports = { + 'mobile': DeviceViewport(width: 400, height: 800), + 'tablet': DeviceViewport(width: 768, height: 1024), + 'desktop': desktopDeviceViewport, +}; + +extension ScreenshotPageExt on Page { + Future writeScreenshotToFile(String path) async { + await File(path).writeAsBytes(await screenshot()); + } + + /// Takes screenshots **if** `PUB_SCREENSHOT_DIR` environment variable is set. + /// + /// Iterates over viewports and themes, and generates screenshot files with the + /// following pattern: + /// - `PUB_SCREENSHOT_DIR/$prefix-desktop-dark.png` + /// - `PUB_SCREENSHOT_DIR/$prefix-desktop-light.png` + /// - `PUB_SCREENSHOT_DIR/$prefix-mobile-dark.png` + /// - `PUB_SCREENSHOT_DIR/$prefix-mobile-light.png` + /// - `PUB_SCREENSHOT_DIR/$prefix-tablet-dark.png` + /// - `PUB_SCREENSHOT_DIR/$prefix-tablet-light.png` + Future takeScreenshots({ + required String selector, + required String prefix, + }) async { + final handle = await $(selector); + await handle.takeScreenshots(prefix); + } +} + +extension ScreenshotElementHandleExt on ElementHandle { + /// Takes screenshots **if** `PUB_SCREENSHOT_DIR` environment variable is set. + /// + /// Iterates over viewports and themes, and generates screenshot files with the + /// following pattern: + /// - `PUB_SCREENSHOT_DIR/$prefix-desktop-dark.png` + /// - `PUB_SCREENSHOT_DIR/$prefix-desktop-light.png` + /// - `PUB_SCREENSHOT_DIR/$prefix-mobile-dark.png` + /// - `PUB_SCREENSHOT_DIR/$prefix-mobile-light.png` + /// - `PUB_SCREENSHOT_DIR/$prefix-tablet-dark.png` + /// - `PUB_SCREENSHOT_DIR/$prefix-tablet-light.png` + Future takeScreenshots(String prefix) async { + final body = await page.$('body'); + final bodyClassAttr = + (await body.evaluate('el => el.getAttribute("class")')) as String; + final bodyClasses = bodyClassAttr.split(' '); + + for (final vp in _viewports.entries) { + await page.setViewport(vp.value); + + for (final theme in _themes) { + final newClasses = [ + ...bodyClasses.where((c) => !c.endsWith('-theme')), + '$theme-theme', + ]; + await body.evaluate('(el, v) => el.setAttribute("class", v)', + args: [newClasses.join(' ')]); + + // The presence of the element is verified, continue only if screenshots are enabled. + if (!_isScreenshotDirSet) continue; + + final path = p.join(_screenshotDir!, '$prefix-${vp.key}-$theme.png'); + await _writeScreenshotToFile(path); + } + } + } + + Future _writeScreenshotToFile(String path) async { + final file = File(path); + final exists = await file.exists(); + if (exists && !_allowScreeshotUpdates) { + throw Exception('Screenshot update is detected in: $path'); + } + await file.parent.create(recursive: true); + await File(path).writeAsBytes(await screenshot()); + } +} diff --git a/pkg/pub_integration/lib/src/test_browser.dart b/pkg/pub_integration/lib/src/test_browser.dart index eb89c877a2..05b3b4dd8c 100644 --- a/pkg/pub_integration/lib/src/test_browser.dart +++ b/pkg/pub_integration/lib/src/test_browser.dart @@ -9,6 +9,8 @@ import 'package:_pub_shared/validation/html/html_validation.dart'; import 'package:path/path.dart' as p; import 'package:puppeteer/puppeteer.dart'; +import 'screenshot_utils.dart'; + /// Creates and tracks the headless Chrome environment, its temp directories and /// and uncaught exceptions. class TestBrowser { @@ -90,6 +92,7 @@ class TestBrowser { userDataDir: userDataDir.path, headless: !_displayBrowser, devTools: false, + defaultViewport: desktopDeviceViewport, ); // Update the default permissions like clipboard access. diff --git a/pkg/pub_integration/test/browser_test.dart b/pkg/pub_integration/test/browser_test.dart index 5127211a57..9c87960dda 100644 --- a/pkg/pub_integration/test/browser_test.dart +++ b/pkg/pub_integration/test/browser_test.dart @@ -7,6 +7,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:pub_integration/src/fake_test_context_provider.dart'; import 'package:pub_integration/src/pub_puppeteer_helpers.dart'; +import 'package:pub_integration/src/screenshot_utils.dart'; import 'package:pub_integration/src/test_browser.dart'; import 'package:puppeteer/puppeteer.dart'; import 'package:test/test.dart'; @@ -69,6 +70,8 @@ void main() { await user.withBrowserPage( (page) async { await page.gotoOrigin('/packages/retry'); + await page.takeScreenshots( + prefix: 'package-page/readme-page', selector: 'body'); // check pub score final pubScoreElem = await page @@ -94,6 +97,14 @@ void main() { await page.gotoOrigin('/packages/retry/license'); await checkHeaderTitle(); + + await page.gotoOrigin('/packages/retry/versions'); + await page.takeScreenshots( + prefix: 'package-page/versions-page', selector: 'body'); + + await page.gotoOrigin('/packages/retry/score'); + await page.takeScreenshots( + prefix: 'package-page/score-page', selector: 'body'); }, ); }); diff --git a/pkg/pub_integration/test/fake_sign_in_test.dart b/pkg/pub_integration/test/fake_sign_in_test.dart index 416e8ad252..d54a5fedd5 100644 --- a/pkg/pub_integration/test/fake_sign_in_test.dart +++ b/pkg/pub_integration/test/fake_sign_in_test.dart @@ -6,6 +6,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:pub_integration/src/fake_test_context_provider.dart'; +import 'package:pub_integration/src/screenshot_utils.dart'; import 'package:pub_integration/src/test_browser.dart'; import 'package:test/test.dart'; @@ -50,6 +51,13 @@ void main() { String? firstSessionId; // sign-in page await browserSession.withBrowserPage((page) async { + await page.gotoOrigin('/'); + await page.takeScreenshots( + selector: '.site-header', + prefix: 'landing-page/site-header-public'); + await page.takeScreenshots( + selector: '.site-footer', prefix: 'landing-page/site-footer'); + { final rs = await page.gotoOrigin('/sign-in?fake-email=user@pub.dev'); final cookies = await page.cookies(); @@ -63,6 +71,9 @@ void main() { firstSessionId = cookies.firstWhere((c) => c.name == 'PUB_SID_INSECURE').value; } + await page.takeScreenshots( + selector: '.site-header', + prefix: 'landing-page/site-header-authenticated'); // same user sign-in with redirect { diff --git a/pkg/pub_integration/test/pkg_admin_page_test.dart b/pkg/pub_integration/test/pkg_admin_page_test.dart index 2dbf17453d..27eebe98cb 100644 --- a/pkg/pub_integration/test/pkg_admin_page_test.dart +++ b/pkg/pub_integration/test/pkg_admin_page_test.dart @@ -9,6 +9,7 @@ import 'dart:io' show Platform; import 'package:http/http.dart' as http; import 'package:pub_integration/src/fake_test_context_provider.dart'; import 'package:pub_integration/src/pub_puppeteer_helpers.dart'; +import 'package:pub_integration/src/screenshot_utils.dart'; import 'package:pub_integration/src/test_browser.dart'; import 'package:test/test.dart'; @@ -52,6 +53,8 @@ void main() { // github publishing await user.withBrowserPage((page) async { await page.gotoOrigin('/packages/test_pkg/admin'); + await page.takeScreenshots( + prefix: 'package-page/admin-page', selector: 'body'); await page.waitAndClick('#-pkg-admin-automated-github-enabled'); await page.waitForLayout([ diff --git a/pkg/pub_integration/test/report_test.dart b/pkg/pub_integration/test/report_test.dart index bb3bf5d676..055430b284 100644 --- a/pkg/pub_integration/test/report_test.dart +++ b/pkg/pub_integration/test/report_test.dart @@ -8,6 +8,7 @@ import 'package:_pub_shared/data/admin_api.dart'; import 'package:http/http.dart' as http; import 'package:pub_integration/src/fake_test_context_provider.dart'; import 'package:pub_integration/src/pub_puppeteer_helpers.dart'; +import 'package:pub_integration/src/screenshot_utils.dart'; import 'package:pub_integration/src/test_browser.dart'; import 'package:test/test.dart'; @@ -58,6 +59,9 @@ void main() { (page) async { await page.gotoOrigin('/report?subject=package:oxygen'); await page.waitAndClick('.report-page-direct-report'); + await page.takeScreenshots( + prefix: 'report-page/direct-report', + selector: '#report-page-form'); await page.waitFocusAndType('#report-email', 'reporter@pub.dev'); await page.waitFocusAndType( '#report-message', 'Huston, we have a problem.'); diff --git a/pkg/pub_integration/test/search_completition_test.dart b/pkg/pub_integration/test/search_completition_test.dart index 2dcab4025e..421cd9acba 100644 --- a/pkg/pub_integration/test/search_completition_test.dart +++ b/pkg/pub_integration/test/search_completition_test.dart @@ -6,6 +6,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:pub_integration/src/fake_test_context_provider.dart'; +import 'package:pub_integration/src/screenshot_utils.dart'; import 'package:pub_integration/src/test_browser.dart'; import 'package:puppeteer/puppeteer.dart'; import 'package:test/test.dart'; @@ -47,6 +48,8 @@ void main() { await page.gotoOrigin('/'); await page.keyboard.type('is:un'); await Future.delayed(Duration(milliseconds: 200)); + await page.takeScreenshots( + selector: 'body', prefix: 'landing-page/search-completion'); await page.keyboard.press(Key.enter); await Future.delayed(Duration(milliseconds: 200)); await page.keyboard.press(Key.enter); @@ -62,6 +65,8 @@ void main() { // go to the end of the input field and start typing await page.keyboard.press(Key.arrowDown); await page.keyboard.type(' -sdk:fl'); + await page.takeScreenshots( + selector: 'body', prefix: 'listing-page/search-completion'); await Future.delayed(Duration(milliseconds: 200)); await page.keyboard.press(Key.enter); await Future.delayed(Duration(milliseconds: 200)); diff --git a/pkg/pub_integration/test/search_update_test.dart b/pkg/pub_integration/test/search_update_test.dart index da10a95150..ba197dbc43 100644 --- a/pkg/pub_integration/test/search_update_test.dart +++ b/pkg/pub_integration/test/search_update_test.dart @@ -7,6 +7,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:pub_integration/src/fake_test_context_provider.dart'; import 'package:pub_integration/src/pub_puppeteer_helpers.dart'; +import 'package:pub_integration/src/screenshot_utils.dart'; import 'package:pub_integration/src/test_browser.dart'; import 'package:puppeteer/puppeteer.dart'; import 'package:test/test.dart'; @@ -99,6 +100,9 @@ void main() { await _waitOneSecond(); expect(await flutterCB3.boundingBox, isNotNull); + await page.takeScreenshots( + selector: '.search-form', prefix: 'listing-page/search-form'); + // click Flutter await flutterCB3.clickAndWaitOneResponse(); await _waitOneSecond();