diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c20802f95..d7a0fe8af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,59 +43,59 @@ jobs: env: # The dot reporter of the test steps (scripts/pytest_dot_reporter.py). PYTHONPATH: scripts + # 16 workers on the 64-core runners, as upstream runs its tests there; two on + # the 3-core macOS runner, upstream's default of half the cores. + WORKERS: ${{ startsWith(matrix.os, 'macos') && '2' || '16' }} strategy: fail-fast: false matrix: - # ubuntu-22.04, not ubuntu-latest: the 24.04 image has no WebGL for headless - # Firefox (CA-008, CA-009). - os: [ubuntu-22.04, windows-latest, macos-latest] - python-version: ['3.14'] + # Linux and Windows run on the 64-core runners of the organization, as + # upstream's tests_primary.yml; macOS on the hosted runner. + # 3.12 is our most used Python version (base) + os: [playwright-x64-ubuntu24-64-core, playwright-x64-server2025-64-core, macos-15-xlarge] + python-version: ['3.12'] browser: [chromium, firefox, webkit] - exclude: - # WebKit on standard macOS-latest (currently macos-15-arm64) is unstable; - # upstream pins paid macos-15-xlarge for cross-browser webkit too. - - os: macos-latest - browser: webkit include: - - os: windows-latest + - os: playwright-x64-server2025-64-core python-version: '3.10' browser: chromium - - os: macos-latest + - os: macos-15-xlarge python-version: '3.10' browser: chromium - - os: ubuntu-22.04 + - os: playwright-x64-ubuntu24-64-core python-version: '3.10' browser: chromium - - os: macos-15-xlarge - python-version: '3.14' - browser: webkit - - os: windows-latest + - os: playwright-x64-server2025-64-core python-version: '3.11' browser: chromium - - os: macos-latest + - os: macos-15-xlarge python-version: '3.11' browser: chromium - - os: ubuntu-22.04 + - os: playwright-x64-ubuntu24-64-core python-version: '3.11' browser: chromium - - os: windows-latest - python-version: '3.12' - browser: chromium - - os: macos-latest - python-version: '3.12' - browser: chromium - - os: ubuntu-22.04 - python-version: '3.12' - browser: chromium - - os: windows-latest + - os: playwright-x64-server2025-64-core python-version: '3.13' browser: chromium - - os: macos-latest + - os: macos-15-xlarge python-version: '3.13' browser: chromium - - os: ubuntu-22.04 + - os: playwright-x64-ubuntu24-64-core python-version: '3.13' browser: chromium + - os: playwright-x64-server2025-64-core + python-version: '3.14' + browser: chromium + - os: macos-15-xlarge + python-version: '3.14' + browser: chromium + - os: playwright-x64-ubuntu24-64-core + python-version: '3.14' + browser: chromium + # Intel macOS, once: *-large is Intel, *-xlarge is arm64, as upstream notes. + - os: macos-15-large + python-version: '3.12' + browser: chromium runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -119,34 +119,34 @@ jobs: - name: Test Wheel Installation run: pytest tests/test_installation.py --browser=${{ matrix.browser }} - name: Test Sync API - if: matrix.os != 'ubuntu-22.04' - run: pytest tests/sync --browser=${{ matrix.browser }} -n auto -p pytest_dot_reporter -rfE --color=yes + if: runner.os != 'Linux' + run: pytest tests/sync --browser=${{ matrix.browser }} -n ${{ env.WORKERS }} -p pytest_dot_reporter -rfE --color=yes - name: Test Sync API - if: matrix.os == 'ubuntu-22.04' - run: xvfb-run pytest tests/sync --browser=${{ matrix.browser }} -n auto -p pytest_dot_reporter -rfE --color=yes + if: runner.os == 'Linux' + run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" pytest tests/sync --browser=${{ matrix.browser }} -n ${{ env.WORKERS }} -p pytest_dot_reporter -rfE --color=yes - name: Test Async API # Also after a failed sync step: the two suites report on their own. - if: ${{ !cancelled() && steps.install.outcome == 'success' && matrix.os != 'ubuntu-22.04' }} - run: pytest tests/async --browser=${{ matrix.browser }} -n auto -p pytest_dot_reporter -rfE --color=yes + if: ${{ !cancelled() && steps.install.outcome == 'success' && runner.os != 'Linux' }} + run: pytest tests/async --browser=${{ matrix.browser }} -n ${{ env.WORKERS }} -p pytest_dot_reporter -rfE --color=yes - name: Test Async API - if: ${{ !cancelled() && steps.install.outcome == 'success' && matrix.os == 'ubuntu-22.04' }} - run: xvfb-run pytest tests/async --browser=${{ matrix.browser }} -n auto -p pytest_dot_reporter -rfE --color=yes + if: ${{ !cancelled() && steps.install.outcome == 'success' && runner.os == 'Linux' }} + run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" pytest tests/async --browser=${{ matrix.browser }} -n ${{ env.WORKERS }} -p pytest_dot_reporter -rfE --color=yes test-stable: name: Stable - timeout-minutes: 90 + # The branded browsers on the 3-core macOS runner take longer than the bundled ones. + timeout-minutes: 120 env: # The dot reporter of the test steps (scripts/pytest_dot_reporter.py). PYTHONPATH: scripts + WORKERS: ${{ startsWith(matrix.os, 'macos') && '2' || '16' }} strategy: fail-fast: false matrix: - os: [ubuntu-22.04, windows-latest, macos-latest] + os: [playwright-x64-ubuntu24-64-core, playwright-x64-server2025-64-core, macos-15-xlarge] browser-channel: [chrome] include: - - os: windows-latest - browser-channel: msedge - - os: macos-latest + - os: playwright-x64-server2025-64-core browser-channel: msedge runs-on: ${{ matrix.os }} steps: @@ -167,18 +167,18 @@ jobs: - name: Common Tests run: pytest tests/common --browser=chromium --browser-channel=${{ matrix.browser-channel }} - name: Test Sync API - if: matrix.os != 'ubuntu-22.04' - run: pytest tests/sync --browser=chromium --browser-channel=${{ matrix.browser-channel }} -n auto -p pytest_dot_reporter -rfE --color=yes + if: runner.os != 'Linux' + run: pytest tests/sync --browser=chromium --browser-channel=${{ matrix.browser-channel }} -n ${{ env.WORKERS }} -p pytest_dot_reporter -rfE --color=yes - name: Test Sync API - if: matrix.os == 'ubuntu-22.04' - run: xvfb-run pytest tests/sync --browser=chromium --browser-channel=${{ matrix.browser-channel }} -n auto -p pytest_dot_reporter -rfE --color=yes + if: runner.os == 'Linux' + run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" pytest tests/sync --browser=chromium --browser-channel=${{ matrix.browser-channel }} -n ${{ env.WORKERS }} -p pytest_dot_reporter -rfE --color=yes - name: Test Async API # Also after a failed sync step: the two suites report on their own. - if: ${{ !cancelled() && steps.install.outcome == 'success' && matrix.os != 'ubuntu-22.04' }} - run: pytest tests/async --browser=chromium --browser-channel=${{ matrix.browser-channel }} -n auto -p pytest_dot_reporter -rfE --color=yes + if: ${{ !cancelled() && steps.install.outcome == 'success' && runner.os != 'Linux' }} + run: pytest tests/async --browser=chromium --browser-channel=${{ matrix.browser-channel }} -n ${{ env.WORKERS }} -p pytest_dot_reporter -rfE --color=yes - name: Test Async API - if: ${{ !cancelled() && steps.install.outcome == 'success' && matrix.os == 'ubuntu-22.04' }} - run: xvfb-run pytest tests/async --browser=chromium --browser-channel=${{ matrix.browser-channel }} -n auto -p pytest_dot_reporter -rfE --color=yes + if: ${{ !cancelled() && steps.install.outcome == 'success' && runner.os == 'Linux' }} + run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" pytest tests/async --browser=chromium --browser-channel=${{ matrix.browser-channel }} -n ${{ env.WORKERS }} -p pytest_dot_reporter -rfE --color=yes test_examples: name: Examples diff --git a/local-requirements.txt b/local-requirements.txt index 5c93259e7..1518f808e 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -4,6 +4,7 @@ build==1.6.1 mypy==2.4.0 objgraph==3.6.2 Pillow==12.3.0 +pixelmatch==0.3.0 pre-commit==3.5.0 pyOpenSSL==26.0.0 pytest==9.0.3 diff --git a/tests/async/library/test_browsercontext_locale.py b/tests/async/library/test_browsercontext_locale.py index 87b18d3a8..8004d3dc5 100644 --- a/tests/async/library/test_browsercontext_locale.py +++ b/tests/async/library/test_browsercontext_locale.py @@ -130,6 +130,7 @@ async def test_LC_007_should_work_for_multiple_pages_sharing_same_process( await context.close() +@pytest.mark.slow async def test_LC_008_should_be_isolated_between_contexts( browser: Browser, server: Server ) -> None: diff --git a/tests/async/library/test_browsercontext_pages.py b/tests/async/library/test_browsercontext_pages.py index e91ef985f..477d8a181 100644 --- a/tests/async/library/test_browsercontext_pages.py +++ b/tests/async/library/test_browsercontext_pages.py @@ -166,6 +166,7 @@ async def test_BC_058_should_return_bounding_box_with_page_scale( await context.close() +@pytest.mark.slow async def test_BC_059_should_not_leak_listeners_during_navigation_of_20_pages( context_factory: Callable[..., Awaitable[BrowserContext]], server: Server ) -> None: diff --git a/tests/async/library/test_defaultbrowsercontext_2.py b/tests/async/library/test_defaultbrowsercontext_2.py index f37a01c2f..9cef4a518 100644 --- a/tests/async/library/test_defaultbrowsercontext_2.py +++ b/tests/async/library/test_defaultbrowsercontext_2.py @@ -210,6 +210,7 @@ async def test_DC_024_should_accept_relative_user_data_dir( await context.close() +@pytest.mark.slow async def test_DC_025_should_restore_state_from_user_data_dir( browser_type: BrowserType, server: Server, diff --git a/tests/async/library/test_har.py b/tests/async/library/test_har.py index feedfa939..7451940b0 100644 --- a/tests/async/library/test_har.py +++ b/tests/async/library/test_har.py @@ -1326,6 +1326,7 @@ async def test_HA_057_should_close_the_context_when_saving_the_har_fails( await browser.close() +@pytest.mark.slow async def test_HA_058_should_support_har_larger_than_512_mb( context_factory: Callable[..., Awaitable[BrowserContext]], server: Server, diff --git a/tests/async/library/test_headful.py b/tests/async/library/test_headful.py index 2d27ad0ac..815cc1c65 100644 --- a/tests/async/library/test_headful.py +++ b/tests/async/library/test_headful.py @@ -121,6 +121,7 @@ async def test_HE_001_should_have_default_url_when_launching_browser_smoke( assert urls == ["about:blank"] +@pytest.mark.slow async def test_HE_002_should_close_browser_with_beforeunload_page( launch_persistent: LaunchPersistent, server: Server ) -> None: diff --git a/tests/async/library/test_tracing.py b/tests/async/library/test_tracing.py index 05610e8c8..53eee4a21 100644 --- a/tests/async/library/test_tracing.py +++ b/tests/async/library/test_tracing.py @@ -1145,23 +1145,30 @@ async def test_TR_039_should_not_flush_console_events( page = await context.new_page() test_id = "TR-039" await context.tracing.start(name=test_id) + promise: "asyncio.Future[None]" = page._loop.create_future() counter = 0 - def predicate(_: Any) -> bool: + def on_console(_: Any) -> None: nonlocal counter counter += 1 - return counter == 100 + if counter == 100: + promise.set_result(None) - async with page.expect_event("console", predicate=predicate): - await page.evaluate( - """() => { + # Python: a listener and a future, as upstream: expect_event() would be an + # action of its own in the trace, which flushes the console events. + page.on("console", on_console) + + await page.evaluate( + """() => { window.builtins.setTimeout(() => { for (let i = 0; i < 100; ++i) console.log('hello ' + i); }, 10); return 31415926; }""" - ) + ) + + await promise def read_trace() -> str: trace_name = next( diff --git a/tests/async/library/test_video.py b/tests/async/library/test_video.py index 33ee76483..3926e2e37 100644 --- a/tests/async/library/test_video.py +++ b/tests/async/library/test_video.py @@ -819,6 +819,7 @@ async def test_VI_030_screencast_should_work_with_video_trace( assert is_almost_red(pixel) is True +@pytest.mark.slow async def test_VI_031_should_save_as_video(browser: Browser, tmp_path: Path) -> None: videos_path = tmp_path size: ViewportSize = {"width": 320, "height": 240} diff --git a/tests/async/page/test_page_screenshot.py b/tests/async/page/test_page_screenshot.py index 49cd0ea06..1a2d20f5b 100644 --- a/tests/async/page/test_page_screenshot.py +++ b/tests/async/page/test_page_screenshot.py @@ -654,6 +654,7 @@ async def test_SS_038_page_screenshot_should_work_with_array_deleted( @pytest.mark.usefixtures("skip_firefox_headed") +@pytest.mark.slow async def test_SS_039_page_screenshot_should_take_full_page_screenshots_during_navigation( page: Page, server: Server ) -> None: diff --git a/tests/conftest.py b/tests/conftest.py index a53c9b405..aeba82edc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,17 @@ import threading import traceback from pathlib import Path -from typing import Any, Callable, Dict, Generator, List, Optional, cast +from typing import ( + Any, + Callable, + Dict, + Generator, + Iterable, + List, + Optional, + Tuple, + cast, +) import greenlet import pytest @@ -103,10 +113,37 @@ def pytest_timeout_cancel_timer(item: pytest.Item) -> Optional[bool]: return True +_REPO = _dirname.parent + +# The waits that every run has, which say nothing about a hang: the reader of the +# driver's pipe, and the main task of the sync API's event loop. +_IDLE_COROUTINES = ( + "PipeTransport.run", + "PlaywrightContextManager.__enter__..main", +) + + +def _own_frames(frames: Iterable[Tuple[Any, int]]) -> List[str]: + """The frames of this repository, formatted: the test and the client. The + frames of pytest, asyncio and the other packages around them are left out.""" + summary = traceback.StackSummary.extract(frames) + own = [ + f + for f in summary + if f.filename.startswith(str(_REPO)) + and f"{os.sep}env{os.sep}" not in f.filename + and not f.filename.endswith(f"tests{os.sep}server.py") + ] + return summary.__class__.from_list(own).format() + + def _dump_stacks_and_exit(item: pytest.Item, settings: pytest_timeout.Settings) -> None: - """Writes the stacks of the process to its standard error and ends the - process, as the thread method of pytest-timeout does; xdist replaces the - worker.""" + """Writes to the standard error where the process waits, and ends the process, + as the thread method of pytest-timeout does; xdist replaces the worker. + + Each thread, each greenlet that waits for the event loop under the sync API, + and each pending task of the event loop gets its frames in this repository. + One without any, such as the idle event loop, is left out.""" try: capman = cast( Optional[CaptureManager], @@ -122,6 +159,12 @@ def _dump_stacks_and_exit(item: pytest.Item, settings: pytest_timeout.Settings) def title(text: str, sep: str = "~") -> None: stderr.write(f"\n{sep * 10} {text} {sep * 10}\n") + def write(name: str, frames: Iterable[Tuple[Any, int]]) -> None: + lines = _own_frames(frames) + if lines: + title(name) + stderr.write("".join(lines)) + title(f"Timeout ({settings.timeout}s): {item.nodeid}", "+") if captured_stdout: title("Captured stdout") @@ -137,15 +180,22 @@ def title(text: str, sep: str = "~") -> None: (t.name for t in threading.enumerate() if t.ident == thread_ident), "", ) - title(f"Stack of {thread_name} ({thread_ident})") - stderr.write("".join(traceback.format_stack(frame))) + # walk_stack() yields the innermost frame first. + write(f"Thread {thread_name}", reversed(list(traceback.walk_stack(frame)))) for obj in gc.get_objects(): if isinstance(obj, greenlet.greenlet) and obj.gr_frame is not None: - title(f"Stack of {obj!r}, switched away") - stderr.write("".join(traceback.format_stack(obj.gr_frame))) + write( + f"{type(obj).__name__}, waiting for the event loop", + reversed(list(traceback.walk_stack(obj.gr_frame))), + ) elif isinstance(obj, asyncio.Task) and not obj.done(): - title(f"Stack of {obj!r}") - obj.print_stack(file=stderr) + coroutine = obj.get_coro() + name = getattr(coroutine, "__qualname__", repr(coroutine)) + if name in _IDLE_COROUTINES: + continue + # Python: the frames of the task, from the coroutine out to the + # awaited one, as Task.print_stack() prints them. + write(f"Task {name}", ((f, f.f_lineno) for f in obj.get_stack())) title("Timeout", "+") except Exception: traceback.print_exc() diff --git a/tests/helpers/image_compare.py b/tests/helpers/image_compare.py index bc4a95d80..2ae7d3726 100644 --- a/tests/helpers/image_compare.py +++ b/tests/helpers/image_compare.py @@ -14,13 +14,15 @@ """Image helpers for the screenshot tests, in both flavors. -compare_images() is a port of comparePNGs() in tests/config/comparator.ts, which -compares with the ssim-cie94 comparator of compareImages() in -packages/utils/comparators.ts. compare() and the classes and functions above it are -a port of packages/utils/image_tools: compare.ts, stats.ts, colorUtils.ts and -imageChannel.ts. The diff image that compare() draws is not ported. assert_to_match_golden() is expect(buffer).toMatchSnapshot(name, { maxDiffPixels }) -against tests/golden-/. is_lossless_webp() is a port of +against tests/golden-/: the matcher of @playwright/test compares with +pixelmatch at the threshold 0.2 (packages/utils/comparators.ts, compareImages()), as +the configs of the upstream tests set no other comparator. compare_images() is a port +of comparePNGs() in tests/config/comparator.ts, which compares with the ssim-cie94 +comparator and no threshold. compare() and the classes and functions above it are a +port of packages/utils/image_tools: compare.ts, stats.ts, colorUtils.ts and +imageChannel.ts; count_different_pixels() is utils.compare() of the tests. The diff +image that compare() draws is not ported. is_lossless_webp() is a port of isLosslessWebp() in packages/utils/webp/webp.ts. """ @@ -34,6 +36,8 @@ from typing import List, Optional, Sequence, Tuple, cast from PIL import Image +from pixelmatch import pixelmatch +from pixelmatch.contrib.PIL import from_PIL_to_raw_data _tests_dir = Path(__file__).parent.parent @@ -513,10 +517,28 @@ def compare_images( def assert_to_match_golden( browser_name: str, actual: bytes, golden_name: str, max_diff_pixels: int = 0 ) -> None: - """expect(actual).toMatchSnapshot(golden_name, { maxDiffPixels }).""" + """expect(actual).toMatchSnapshot(golden_name, { maxDiffPixels }): pixelmatch at the + threshold 0.2, as the matcher compares when the config sets no comparator.""" golden = (_tests_dir / f"golden-{browser_name}" / golden_name).read_bytes() - error = compare_images(actual, golden, max_diff_pixels) - assert error is None, f"Screenshot comparison failed for {golden_name}: {error}" + actual_image = decode_image(actual) + golden_image = decode_image(golden) + assert actual_image.size == golden_image.size, ( + f"Expected an image {golden_image.width}px by {golden_image.height}px," + f" received {actual_image.width}px by {actual_image.height}px." + ) + width, height = golden_image.size + count = pixelmatch( + from_PIL_to_raw_data(golden_image), + from_PIL_to_raw_data(actual_image), + width, + height, + threshold=0.2, + ) + ratio = math.ceil(count / (width * height) * 100) / 100 + assert count <= max_diff_pixels, ( + f"Screenshot comparison failed for {golden_name}: {count} pixels" + f" (ratio {ratio:.2f} of all image pixels) are different." + ) def is_lossless_webp(buffer: bytes) -> bool: diff --git a/tests/helpers/static_https_server.py b/tests/helpers/static_https_server.py index 3ed3f3484..ac53e0368 100644 --- a/tests/helpers/static_https_server.py +++ b/tests/helpers/static_https_server.py @@ -21,6 +21,7 @@ import functools import socket +import socketserver import ssl import threading from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer @@ -43,6 +44,13 @@ def __init__(self, context: ssl.SSLContext, directory: Path) -> None: functools.partial(_QuietHandler, directory=str(directory)), ) + def server_bind(self) -> None: + # Not the server_bind() of HTTPServer, which looks up the name of the host + # (socket.getfqdn): a reverse lookup that can hang on a CI runner. + socketserver.TCPServer.server_bind(self) + self.server_name = "localhost" + self.server_port = self.socket.getsockname()[1] + def get_request(self) -> Tuple[socket.socket, Any]: sock, address = self.socket.accept() # The TLS handshake runs on the thread of the request, not on the accept diff --git a/tests/sync/library/test_browsercontext_locale.py b/tests/sync/library/test_browsercontext_locale.py index 1790f1822..c764dd843 100644 --- a/tests/sync/library/test_browsercontext_locale.py +++ b/tests/sync/library/test_browsercontext_locale.py @@ -137,6 +137,7 @@ def test_LC_007_should_work_for_multiple_pages_sharing_same_process( context.close() +@pytest.mark.slow def test_LC_008_should_be_isolated_between_contexts( browser: Browser, server: Server ) -> None: diff --git a/tests/sync/library/test_browsercontext_pages.py b/tests/sync/library/test_browsercontext_pages.py index 56a01affd..f3581244d 100644 --- a/tests/sync/library/test_browsercontext_pages.py +++ b/tests/sync/library/test_browsercontext_pages.py @@ -165,6 +165,7 @@ def test_BC_058_should_return_bounding_box_with_page_scale( context.close() +@pytest.mark.slow def test_BC_059_should_not_leak_listeners_during_navigation_of_20_pages( context_factory: Callable[..., BrowserContext], server: Server, diff --git a/tests/sync/library/test_defaultbrowsercontext_2.py b/tests/sync/library/test_defaultbrowsercontext_2.py index a17854348..4cb90a939 100644 --- a/tests/sync/library/test_defaultbrowsercontext_2.py +++ b/tests/sync/library/test_defaultbrowsercontext_2.py @@ -193,6 +193,7 @@ def test_DC_024_should_accept_relative_user_data_dir( context.close() +@pytest.mark.slow def test_DC_025_should_restore_state_from_user_data_dir( browser_type: BrowserType, server: Server, diff --git a/tests/sync/library/test_har.py b/tests/sync/library/test_har.py index b34612373..ac0e740a6 100644 --- a/tests/sync/library/test_har.py +++ b/tests/sync/library/test_har.py @@ -1322,6 +1322,7 @@ def test_HA_057_should_close_the_context_when_saving_the_har_fails( browser.close() +@pytest.mark.slow def test_HA_058_should_support_har_larger_than_512_mb( context_factory: Callable[..., BrowserContext], server: Server, diff --git a/tests/sync/library/test_headful.py b/tests/sync/library/test_headful.py index 003ce23a2..9500c1ab7 100644 --- a/tests/sync/library/test_headful.py +++ b/tests/sync/library/test_headful.py @@ -112,6 +112,7 @@ def test_HE_001_should_have_default_url_when_launching_browser_smoke( assert urls == ["about:blank"] +@pytest.mark.slow def test_HE_002_should_close_browser_with_beforeunload_page( launch_persistent: LaunchPersistent, server: Server ) -> None: diff --git a/tests/sync/library/test_tracing.py b/tests/sync/library/test_tracing.py index abe040122..6f08f8a1b 100644 --- a/tests/sync/library/test_tracing.py +++ b/tests/sync/library/test_tracing.py @@ -1169,23 +1169,30 @@ def test_TR_039_should_not_flush_console_events( page = context.new_page() test_id = "TR-039" context.tracing.start(name=test_id) + promise: "asyncio.Future[None]" = page._loop.create_future() counter = 0 - def predicate(_: Any) -> bool: + def on_console(_: Any) -> None: nonlocal counter counter += 1 - return counter == 100 + if counter == 100: + promise.set_result(None) - with page.expect_event("console", predicate=predicate): - page.evaluate( - """() => { + # Python: a listener and a future, as upstream: expect_event() would be an + # action of its own in the trace, which flushes the console events. + page.on("console", on_console) + + page.evaluate( + """() => { window.builtins.setTimeout(() => { for (let i = 0; i < 100; ++i) console.log('hello ' + i); }, 10); return 31415926; }""" - ) + ) + + wait_for(page, promise) def read_trace() -> str: trace_name = next( diff --git a/tests/sync/library/test_video.py b/tests/sync/library/test_video.py index 265637bea..4f393f6a9 100644 --- a/tests/sync/library/test_video.py +++ b/tests/sync/library/test_video.py @@ -833,6 +833,7 @@ def test_VI_030_screencast_should_work_with_video_trace( assert is_almost_red(pixel) is True +@pytest.mark.slow def test_VI_031_should_save_as_video(browser: Browser, tmp_path: Path) -> None: videos_path = tmp_path size: ViewportSize = {"width": 320, "height": 240} diff --git a/tests/sync/page/test_page_screenshot.py b/tests/sync/page/test_page_screenshot.py index 5a1f1cb8f..3d7e434ba 100644 --- a/tests/sync/page/test_page_screenshot.py +++ b/tests/sync/page/test_page_screenshot.py @@ -652,6 +652,7 @@ def test_SS_038_page_screenshot_should_work_with_array_deleted( @pytest.mark.usefixtures("skip_firefox_headed") +@pytest.mark.slow def test_SS_039_page_screenshot_should_take_full_page_screenshots_during_navigation( page: Page, server: Server, sync_gather: Callable[..., List[Any]] ) -> None: