150 lines
5.6 KiB
Python
150 lines
5.6 KiB
Python
"""
|
|
Step 28 — First-load network failure: clear error + in-app retry
|
|
|
|
On a first load nothing is cached yet, so the DB worker downloads sqlite3.wasm
|
|
over the network. These tests simulate that download failing and verify the
|
|
app shows a network-specific error with a working "Try again" button instead
|
|
of the misleading "browser not compatible" dead end.
|
|
|
|
The wasm-route tests are Chromium-only: Playwright cannot intercept requests
|
|
made inside a dedicated worker on Firefox, so the download failure cannot be
|
|
simulated there. The retry machinery itself is covered on both browsers by
|
|
test_worker_load_failure_retry, which breaks worker creation instead.
|
|
|
|
- test_wasm_fetch_failure_shows_network_error (chromium): blocking
|
|
sqlite3.wasm shows the db-error overlay tagged DB_LOAD_FAILED with a retry
|
|
button
|
|
- test_retry_recovers_once_network_is_back (chromium): unblocking the request
|
|
and clicking "Try again" initializes the app in place, without a page reload
|
|
- test_retry_fails_again_while_still_offline (chromium): retrying with the
|
|
network still down shows the error overlay again (retry stays available)
|
|
- test_worker_load_failure_retry (both browsers): a DB worker that fails to
|
|
load shows the error overlay with retry; fixing the worker and clicking
|
|
"Try again" brings the app up in place
|
|
"""
|
|
import pytest
|
|
from playwright.sync_api import Browser, Page, expect
|
|
|
|
WASM_ROUTE = "**/sqlite3.wasm"
|
|
|
|
# Wrap window.Worker so the DB worker initially points at a 404 URL (the
|
|
# worker fails to load, like on a broken network). window.__fixWorker()
|
|
# restores normal behaviour for the retry.
|
|
_BREAK_WORKER = """
|
|
(() => {
|
|
const RealWorker = window.Worker;
|
|
let broken = true;
|
|
window.__fixWorker = () => { broken = false; };
|
|
window.Worker = function (url, opts) {
|
|
let target = url;
|
|
if (broken) {
|
|
// Corrupt the pathname (not the raw string: Vite dev appends a query
|
|
// string, and appending there would leave the path resolvable)
|
|
const u = new URL(url, window.location.href);
|
|
u.pathname += '.broken-by-test';
|
|
target = u;
|
|
}
|
|
return new RealWorker(target, opts);
|
|
};
|
|
})();
|
|
"""
|
|
|
|
|
|
@pytest.fixture
|
|
def wasm_blocked_page(browser: Browser):
|
|
"""A page in its own context (service workers blocked so route interception
|
|
reliably sees the worker's wasm fetch) where sqlite3.wasm requests fail."""
|
|
context = browser.new_context(
|
|
viewport={"width": 1280, "height": 720},
|
|
service_workers="block",
|
|
)
|
|
page = context.new_page()
|
|
page.route(WASM_ROUTE, lambda route: route.abort())
|
|
yield page
|
|
context.close()
|
|
|
|
|
|
def _wait_for_error_overlay(page: Page):
|
|
overlay = page.locator('[data-testid="db-error"]')
|
|
expect(overlay).to_be_visible(timeout=20000)
|
|
return overlay
|
|
|
|
|
|
@pytest.mark.only_browser("chromium")
|
|
def test_wasm_fetch_failure_shows_network_error(wasm_blocked_page, app_url: str):
|
|
"""A failed sqlite3.wasm download shows the error overlay with the
|
|
network-specific message (DB_LOAD_FAILED) and a retry button."""
|
|
page = wasm_blocked_page
|
|
page.goto(app_url)
|
|
|
|
_wait_for_error_overlay(page)
|
|
|
|
err = page.evaluate("document.documentElement.getAttribute('data-db-error')")
|
|
assert err and err.startswith("DB_LOAD_FAILED"), (
|
|
f"Expected a DB_LOAD_FAILED tagged error, got: {err!r}"
|
|
)
|
|
expect(page.locator('[data-testid="db-retry"]')).to_be_visible()
|
|
|
|
|
|
@pytest.mark.only_browser("chromium")
|
|
def test_retry_recovers_once_network_is_back(wasm_blocked_page, app_url: str):
|
|
"""Unblocking the wasm request and clicking 'Try again' brings the app up
|
|
in place — no page reload involved."""
|
|
page = wasm_blocked_page
|
|
page.goto(app_url)
|
|
_wait_for_error_overlay(page)
|
|
|
|
# Marker survives only if no navigation/reload happens during the retry
|
|
page.evaluate("window.__retryMarker = true")
|
|
|
|
page.unroute(WASM_ROUTE)
|
|
page.locator('[data-testid="db-retry"]').click()
|
|
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=20000,
|
|
)
|
|
expect(page.locator('[data-testid="db-error"]')).not_to_be_visible()
|
|
assert page.evaluate("window.__retryMarker") is True, (
|
|
"The retry must recover in place, not through a page reload"
|
|
)
|
|
|
|
|
|
@pytest.mark.only_browser("chromium")
|
|
def test_retry_fails_again_while_still_offline(wasm_blocked_page, app_url: str):
|
|
"""Retrying while the network is still down fails again and keeps the
|
|
retry button available."""
|
|
page = wasm_blocked_page
|
|
page.goto(app_url)
|
|
_wait_for_error_overlay(page)
|
|
|
|
page.locator('[data-testid="db-retry"]').click()
|
|
|
|
# The overlay disappears (loading spinner) then comes back with the button
|
|
_wait_for_error_overlay(page)
|
|
expect(page.locator('[data-testid="db-retry"]')).to_be_visible()
|
|
|
|
|
|
def test_worker_load_failure_retry(page: Page, app_url: str):
|
|
"""Cross-browser: a DB worker that fails to load shows the error overlay
|
|
with a retry button, and retrying after the 'network' recovers initializes
|
|
the app in place."""
|
|
page.add_init_script(_BREAK_WORKER)
|
|
page.goto(app_url)
|
|
|
|
_wait_for_error_overlay(page)
|
|
expect(page.locator('[data-testid="db-retry"]')).to_be_visible()
|
|
|
|
page.evaluate("window.__retryMarker = true")
|
|
page.evaluate("window.__fixWorker()")
|
|
page.locator('[data-testid="db-retry"]').click()
|
|
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=20000,
|
|
)
|
|
expect(page.locator('[data-testid="db-error"]')).not_to_be_visible()
|
|
assert page.evaluate("window.__retryMarker") is True, (
|
|
"The retry must recover in place, not through a page reload"
|
|
)
|