mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
fix(test-e2e-playwright): give every Playwright timeout one source
The config held literals for values vars/main.yml already owned, and the two drifted without a sound. actionTimeout carried `|| 30_000` for its whole life while nothing ever set PLAYWRIGHT_ACTION_TIMEOUT, so every run used the unscaled 30 s. expect had no entry at all, leaving Playwright's built-in 5 s to govern every assertion over Tor while its neighbours were scaled by the onion multiplier. That is what killed the second Nextcloud login attempt: an expect(usernameField).toBeVisible() that gave up after 5 s inside a 150 s budget. vars/main.yml now owns the values in TEST_E2E_PLAYWRIGHT_CONFIG_TIMEOUTS, the harness renders that map into the staged .env both the deploy and scripts/tests/e2e/rerun-spec.sh already pass, and the config demands each name through requiredTimeout() instead of inventing a fallback. The two `-e` flags that fed the old pair are gone with it. scripts/lint/playwright.sh stages the same config and runs `playwright test --list`, which loads it, but built its stub env only from each role's playwright.env.j2 and no such template carries a timeout. Without this it would have aborted all 87 specs on a missing variable. It now takes the names from the same map; the value is a placeholder because --list consumes none of them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c4e9b46519
commit
04d4d31335
5 files changed
+127
-8
No files matched your search
@@ -1,5 +1,6 @@
|
||||
const { defineConfig } = require("@playwright/test");
|
||||
|
||||
// nocheck: env-default a role may export its own base variable and no APP_BASE_URL
|
||||
const baseURL = process.env.APP_BASE_URL || "http://127.0.0.1";
|
||||
|
||||
const keepAll = (process.env.INFINITO_PLAYWRIGHT_KEEP || "").toLowerCase() === "true";
|
||||
@@ -21,12 +22,30 @@ function onionSecureOrigins() {
|
||||
|
||||
const onionSecure = onionSecureOrigins();
|
||||
|
||||
const globalTimeout = parseInt(process.env.INFINITO_PLAYWRIGHT_GLOBAL_TIMEOUT_MS || "0", 10);
|
||||
/**
|
||||
* The timeout the harness renders into the staged .env under this name.
|
||||
*
|
||||
* Args:
|
||||
* name: the PLAYWRIGHT_*_TIMEOUT variable to read.
|
||||
*/
|
||||
function requiredTimeout(name) {
|
||||
const value = Number(process.env[name]);
|
||||
if (!Number.isFinite(value) || value <= 0) {
|
||||
throw new Error(
|
||||
`${name} is missing from the environment. The harness writes every ` +
|
||||
"timeout into the staged .env, so this run was started outside it."
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const globalTimeout = parseInt(process.env.INFINITO_PLAYWRIGHT_GLOBAL_TIMEOUT_MS || 0, 10);
|
||||
|
||||
module.exports = defineConfig({
|
||||
testDir: "./tests",
|
||||
testMatch: "**/*.@(spec|test).js",
|
||||
timeout: Number(process.env.PLAYWRIGHT_TEST_TIMEOUT) || 300_000,
|
||||
timeout: requiredTimeout("PLAYWRIGHT_TEST_TIMEOUT"),
|
||||
expect: { timeout: requiredTimeout("PLAYWRIGHT_EXPECT_TIMEOUT") },
|
||||
...(globalTimeout > 0 ? { globalTimeout } : {}),
|
||||
retries: 2,
|
||||
workers: Number(process.env.PLAYWRIGHT_WORKERS) || 1,
|
||||
@@ -52,8 +71,8 @@ module.exports = defineConfig({
|
||||
: undefined,
|
||||
// Fail fast instead of hanging until the per-test timeout when a target is
|
||||
// unreachable (e.g. an onion service that is not yet published).
|
||||
navigationTimeout: Number(process.env.PLAYWRIGHT_NAVIGATION_TIMEOUT) || 60_000,
|
||||
actionTimeout: Number(process.env.PLAYWRIGHT_ACTION_TIMEOUT) || 30_000,
|
||||
navigationTimeout: requiredTimeout("PLAYWRIGHT_NAVIGATION_TIMEOUT"),
|
||||
actionTimeout: requiredTimeout("PLAYWRIGHT_ACTION_TIMEOUT"),
|
||||
trace: keepAll ? "on" : "retain-on-failure",
|
||||
screenshot: keepAll ? "on" : "only-on-failure",
|
||||
video: keepAll ? "on" : "retain-on-failure"
|
||||
|
||||
@@ -163,7 +163,11 @@
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ _playwright_stage_dir }}/.env"
|
||||
mode: "0644"
|
||||
content: "{{ lookup('template', _playwright_env_template) }}"
|
||||
content: |
|
||||
{{ lookup('template', _playwright_env_template) }}
|
||||
{% for name, value in TEST_E2E_PLAYWRIGHT_CONFIG_TIMEOUTS.items() %}
|
||||
{{ name }}={{ value }}
|
||||
{% endfor %}
|
||||
vars:
|
||||
application_id: "{{ application_id }}"
|
||||
domains: "{{ lookup('domains') }}"
|
||||
@@ -232,10 +236,8 @@
|
||||
+ ['-e', 'PLAYWRIGHT_PROXY=' ~ TEST_E2E_PLAYWRIGHT_PROXY]
|
||||
+ ['-e', 'PLAYWRIGHT_WORKERS=' ~ test_e2e_playwright_workers]
|
||||
+ ['-e', 'PLAYWRIGHT_FULLY_PARALLEL=' ~ TEST_E2E_PLAYWRIGHT_FULLY_PARALLEL]
|
||||
+ ['-e', 'PLAYWRIGHT_TEST_TIMEOUT=' ~ (TEST_E2E_PLAYWRIGHT_ONION_TEST_TIMEOUT if (TEST_E2E_PLAYWRIGHT_IS_ONION | bool) else test_e2e_playwright_test_timeout)]
|
||||
+ ['-e', 'PLAYWRIGHT_TIMEOUT_FACTOR=' ~ TIMEOUT_FACTOR]
|
||||
+ (['-e', 'PLAYWRIGHT_ONION_TIMEOUT_MULTIPLIER=' ~ TEST_E2E_PLAYWRIGHT_ONION_TIMEOUT_MULTIPLIER] if (TEST_E2E_PLAYWRIGHT_IS_ONION | bool) else [])
|
||||
+ ['-e', 'PLAYWRIGHT_NAVIGATION_TIMEOUT=' ~ (TEST_E2E_PLAYWRIGHT_ONION_NAVIGATION_TIMEOUT if (TEST_E2E_PLAYWRIGHT_IS_ONION | bool) else TEST_E2E_PLAYWRIGHT_NAVIGATION_TIMEOUT)]
|
||||
+ ['-v', _playwright_stage_dir ~ ':/e2e']
|
||||
+ ['-v', _playwright_stage_dir ~ '/volume:/volume']
|
||||
+ ['-v', _playwright_reports_dir ~ ':/reports']
|
||||
|
||||
@@ -43,3 +43,13 @@ TEST_E2E_PLAYWRIGHT_ONION_PROBE_RETRIES: 40
|
||||
TEST_E2E_PLAYWRIGHT_ONION_PROBE_DELAY: 15
|
||||
TEST_E2E_PLAYWRIGHT_NAVIGATION_TIMEOUT: 60000
|
||||
TEST_E2E_PLAYWRIGHT_ONION_NAVIGATION_TIMEOUT: 180000
|
||||
TEST_E2E_PLAYWRIGHT_EXPECT_TIMEOUT: 10000
|
||||
TEST_E2E_PLAYWRIGHT_ONION_EXPECT_TIMEOUT: 30000
|
||||
TEST_E2E_PLAYWRIGHT_ACTION_TIMEOUT: 30000
|
||||
TEST_E2E_PLAYWRIGHT_ONION_ACTION_TIMEOUT: 90000
|
||||
|
||||
TEST_E2E_PLAYWRIGHT_CONFIG_TIMEOUTS:
|
||||
PLAYWRIGHT_TEST_TIMEOUT: "{{ TEST_E2E_PLAYWRIGHT_ONION_TEST_TIMEOUT if (TEST_E2E_PLAYWRIGHT_IS_ONION | bool) else test_e2e_playwright_test_timeout }}"
|
||||
PLAYWRIGHT_EXPECT_TIMEOUT: "{{ TEST_E2E_PLAYWRIGHT_ONION_EXPECT_TIMEOUT if (TEST_E2E_PLAYWRIGHT_IS_ONION | bool) else TEST_E2E_PLAYWRIGHT_EXPECT_TIMEOUT }}"
|
||||
PLAYWRIGHT_NAVIGATION_TIMEOUT: "{{ TEST_E2E_PLAYWRIGHT_ONION_NAVIGATION_TIMEOUT if (TEST_E2E_PLAYWRIGHT_IS_ONION | bool) else TEST_E2E_PLAYWRIGHT_NAVIGATION_TIMEOUT }}"
|
||||
PLAYWRIGHT_ACTION_TIMEOUT: "{{ TEST_E2E_PLAYWRIGHT_ONION_ACTION_TIMEOUT if (TEST_E2E_PLAYWRIGHT_IS_ONION | bool) else TEST_E2E_PLAYWRIGHT_ACTION_TIMEOUT }}"
|
||||
@@ -34,6 +34,17 @@ fi
|
||||
HELPERS_SRC="${REPO_ROOT}/roles/test-e2e-playwright/files"
|
||||
PERSONAS_SRC="${HELPERS_SRC}/personas"
|
||||
PLAYWRIGHT_CONFIG_SRC="${HELPERS_SRC}/playwright.config.js"
|
||||
ROLE_VARS_SRC="${REPO_ROOT}/roles/test-e2e-playwright/vars/main.yml"
|
||||
|
||||
mapfile -t CONFIG_TIMEOUT_ENV < <(
|
||||
sed -n '/^TEST_E2E_PLAYWRIGHT_CONFIG_TIMEOUTS:/,/^[^[:space:]#]/p' "${ROLE_VARS_SRC}" |
|
||||
grep -oE '^[[:space:]]+[A-Z][A-Z0-9_]+:' | tr -d ' :' | sed 's/$/=1/'
|
||||
)
|
||||
|
||||
if [[ ${#CONFIG_TIMEOUT_ENV[@]} -eq 0 ]]; then
|
||||
echo "lint-playwright: no timeouts read from ${ROLE_VARS_SRC}; every config load would throw" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
for asset in "${PERSONAS_SRC}" "${PLAYWRIGHT_CONFIG_SRC}"; do
|
||||
if [[ ! -e "${asset}" ]]; then
|
||||
@@ -125,7 +136,7 @@ lint_one_role() {
|
||||
|
||||
ln -sfn "${REPO_ROOT}/node_modules" "${stage_dir}/node_modules"
|
||||
|
||||
local stub_env=()
|
||||
local stub_env=("${CONFIG_TIMEOUT_ENV[@]}")
|
||||
local env_template="roles/${role}/templates/playwright.env.j2"
|
||||
if [[ -f "${env_template}" ]]; then
|
||||
local key
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""One source owns every Playwright timeout, and the config keeps no copy.
|
||||
|
||||
The config and the role each held a literal for the same value. They drifted
|
||||
without a sound: ``actionTimeout`` carried ``|| 30_000`` while nothing ever set
|
||||
the variable, so every run used the unscaled 30 s, and ``expect`` had no entry
|
||||
at all, leaving Playwright's built-in 5 s to govern every assertion over Tor
|
||||
while its neighbours were scaled. The second login attempt in
|
||||
``web-app-nextcloud`` died on ``expect(usernameField).toBeVisible()`` after 5 s
|
||||
with 150 s around it.
|
||||
|
||||
``vars/main.yml`` now owns the values, the harness renders them into the staged
|
||||
``.env`` that both the deploy and ``scripts/tests/e2e/rerun-spec.sh`` pass, and
|
||||
the config demands them rather than inventing one.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unittest
|
||||
|
||||
from utils.cache.files import read_text
|
||||
from utils.cache.yaml import load_yaml_any
|
||||
from utils.roles.mapping import ROLE_FILE_VARS_MAIN
|
||||
|
||||
from . import PROJECT_ROOT
|
||||
|
||||
_ROLE = PROJECT_ROOT / "roles/test-e2e-playwright"
|
||||
_CONFIG = _ROLE / "files/playwright.config.js"
|
||||
_VARS = _ROLE / ROLE_FILE_VARS_MAIN
|
||||
_LINT = PROJECT_ROOT / "scripts/lint/playwright.sh"
|
||||
_SPOT = "TEST_E2E_PLAYWRIGHT_CONFIG_TIMEOUTS"
|
||||
|
||||
_REQUIRED = re.compile(r'requiredTimeout\("(PLAYWRIGHT_\w+)"\)')
|
||||
_ANY_READ = re.compile(r"process\.env\.(PLAYWRIGHT_\w*TIMEOUT\w*)")
|
||||
|
||||
|
||||
class TestConfigTimeoutsHaveOneSource(unittest.TestCase):
|
||||
def test_the_config_demands_exactly_what_the_role_renders(self) -> None:
|
||||
demanded = set(_REQUIRED.findall(read_text(str(_CONFIG))))
|
||||
self.assertTrue(
|
||||
demanded,
|
||||
"no requiredTimeout call found in playwright.config.js; the regex "
|
||||
"no longer matches how the config takes its timeouts, so this test "
|
||||
"has stopped guarding anything",
|
||||
)
|
||||
rendered = set(load_yaml_any(str(_VARS))[_SPOT])
|
||||
self.assertEqual(
|
||||
demanded,
|
||||
rendered,
|
||||
f"the config and {_SPOT} must name the same timeouts: a demanded "
|
||||
"one nobody renders aborts every run, and a rendered one nobody "
|
||||
"demands is dead weight that reads as configuration",
|
||||
)
|
||||
|
||||
def test_the_lint_stager_reads_the_same_map(self) -> None:
|
||||
self.assertIn(
|
||||
_SPOT,
|
||||
read_text(str(_LINT)),
|
||||
f"{_LINT.name} stages the config and runs 'playwright test --list', "
|
||||
"which loads it and therefore hits the same demand. It has to take "
|
||||
f"the names from {_SPOT}, or a rename leaves every role red with a "
|
||||
"missing-variable abort that has nothing to do with its spec",
|
||||
)
|
||||
|
||||
def test_the_config_keeps_no_timeout_of_its_own(self) -> None:
|
||||
stray = set(_ANY_READ.findall(read_text(str(_CONFIG))))
|
||||
self.assertEqual(
|
||||
set(),
|
||||
stray,
|
||||
"these timeouts are read straight from the environment, so a "
|
||||
"literal beside them becomes a second source the role cannot see: "
|
||||
+ ", ".join(sorted(stray)),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user