Files
computer-playbook/roles/test-e2e-playwright/tasks/02_run_one.yml
kevinveenbirkenbachandClaude Opus 5 04d4d31335 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>
2026-09-12 12:04:27 +02:00

288 lines
11 KiB
YAML

---
- name: 📌 Define per-role paths
ansible.builtin.set_fact:
_playwright_env_template: "{{ lookup('path_absolute', 'roles', application_id, 'templates/playwright.env.j2') }}"
_playwright_files_dir: "{{ lookup('path_absolute', 'roles', application_id, 'files/playwright') }}"
_playwright_spec_src: "{{ lookup('path_absolute', 'roles', application_id, 'files/playwright/playwright.spec.js') }}"
_playwright_stage_dir: "{{ TEST_E2E_PLAYWRIGHT_STAGE_BASE_DIR }}/{{ application_id }}"
_playwright_stage_tests_dir: "{{ TEST_E2E_PLAYWRIGHT_STAGE_BASE_DIR }}/{{ application_id }}/tests"
_playwright_reports_dir: "{{ TEST_E2E_PLAYWRIGHT_REPORTS_BASE_DIR }}/{{ application_id }}"
_playwright_default_package_template: "{{ lookup('path_absolute', 'roles/test-e2e-playwright/templates/package.json.j2') }}"
_playwright_default_config_js: "{{ lookup('path_absolute', 'roles/test-e2e-playwright/files/playwright.config.js') }}"
_playwright_ca_required: "{{ (TLS_MODE | lower) == 'self_signed' }}"
_playwright_ca_cert_host: "{{ CA_TRUST.cert_host }}"
_playwright_ca_wrapper_host: "{{ CA_TRUST.wrapper_host }}"
_playwright_ca_trust_name: "{{ CA_TRUST.trust_name }}"
- name: ✅ Validate CA_TRUST configuration for self-signed TLS
ansible.builtin.fail:
msg: >-
TLS_MODE=self_signed requires CA_TRUST.cert_host, CA_TRUST.wrapper_host and CA_TRUST.trust_name
to inject trust into the Playwright container.
when: >-
(_playwright_ca_required | bool) and
(
(_playwright_ca_cert_host | length == 0) or
(_playwright_ca_wrapper_host | length == 0) or
(_playwright_ca_trust_name | length == 0)
)
- name: 🔎 Check CA certificate for Playwright injection
ansible.builtin.stat:
path: "{{ _playwright_ca_cert_host }}"
register: _playwright_ca_cert_stat
when: _playwright_ca_required | bool
- name: 🔎 Check CA wrapper for Playwright injection
ansible.builtin.stat:
path: "{{ _playwright_ca_wrapper_host }}"
register: _playwright_ca_wrapper_stat
when: _playwright_ca_required | bool
- name: 🛑 Fail if CA certificate is missing on host
ansible.builtin.fail:
msg: >-
TLS_MODE=self_signed requires CA injection, but certificate is missing:
{{ _playwright_ca_cert_host }}
when: (_playwright_ca_required | bool) and not (_playwright_ca_cert_stat.stat.exists | default(false))
- name: 🛑 Fail if CA wrapper script is missing on host
ansible.builtin.fail:
msg: >-
TLS_MODE=self_signed requires CA injection, but wrapper script is missing:
{{ _playwright_ca_wrapper_host }}
when: (_playwright_ca_required | bool) and not (_playwright_ca_wrapper_stat.stat.exists | default(false))
- name: ✅ Assert required Playwright env template exists
ansible.builtin.stat:
path: "{{ _playwright_env_template }}"
register: _env_template_stat
delegate_to: localhost
- name: 🛑 Fail if Playwright env template is missing
ansible.builtin.fail:
msg: "Missing Playwright env template: {{ _playwright_env_template }}"
when: not _env_template_stat.stat.exists
- name: ✅ Assert required Playwright spec exists
ansible.builtin.stat:
path: "{{ _playwright_spec_src }}"
register: _spec_stat
delegate_to: localhost
- name: 🛑 Fail if Playwright spec is missing
ansible.builtin.fail:
msg: "Missing Playwright spec file: {{ _playwright_spec_src }}"
when: not _spec_stat.stat.exists
- name: 🔎 Check optional application vars file
ansible.builtin.stat:
path: "{{ lookup('path_absolute', 'roles', application_id, 'vars/main.yml') }}"
register: _playwright_app_vars_stat
delegate_to: localhost
- name: ⚙️ Load optional application vars
ansible.builtin.include_vars:
file: "{{ lookup('path_absolute', 'roles', application_id, 'vars/main.yml') }}"
when: _playwright_app_vars_stat.stat.exists
- name: 📁 Recreate staging directory
ansible.builtin.file:
path: "{{ _playwright_stage_dir }}"
state: absent
- name: 📁 Create staging directory
ansible.builtin.file:
path: "{{ _playwright_stage_dir }}"
state: directory
mode: "0755"
- name: 📁 Create staging tests directory
ansible.builtin.file:
path: "{{ _playwright_stage_tests_dir }}"
state: directory
mode: "0755"
- name: 🔎 Check central default package.json template
ansible.builtin.stat:
path: "{{ _playwright_default_package_template }}"
register: _default_package_stat
delegate_to: localhost
- name: 🛑 Fail when central default package.json template is missing
ansible.builtin.fail:
msg: "Missing central Playwright package.json template: {{ _playwright_default_package_template }}"
when: not _default_package_stat.stat.exists
- name: 📄 Render package.json from template
ansible.builtin.template: # nocheck: stack-host-template (per-host playwright stage dir, runs on every test host)
src: "{{ _playwright_default_package_template }}"
dest: "{{ _playwright_stage_dir }}/package.json"
mode: "0644"
- name: 🔎 Check central default playwright.config.js
ansible.builtin.stat:
path: "{{ _playwright_default_config_js }}"
register: _default_config_stat
delegate_to: localhost
- name: 🛑 Fail when central default playwright.config.js is missing
ansible.builtin.fail:
msg: "Missing central Playwright config: {{ _playwright_default_config_js }}"
when: not _default_config_stat.stat.exists
- name: 📋 Copy central default playwright.config.js
ansible.builtin.copy:
src: "{{ _playwright_default_config_js }}"
dest: "{{ _playwright_stage_dir }}/playwright.config.js"
mode: "0644"
- name: 📋 Copy Playwright tree (spec + companions + addons/ + fixtures/) to staging
ansible.builtin.copy:
src: "{{ _playwright_files_dir }}/"
dest: "{{ _playwright_stage_tests_dir }}/"
mode: "0644"
directory_mode: "0755"
- name: 📋 Copy shared Playwright helpers into staging tests directory
ansible.builtin.copy:
src: "{{ item }}"
dest: "{{ _playwright_stage_tests_dir }}/{{ item | basename }}"
mode: "0644"
when: (item | basename) != 'playwright.config.js'
with_fileglob:
- "{{ lookup('path_absolute', 'roles/test-e2e-playwright/files') }}/*.js"
- name: ✅ Ensure staging volume directory exists
ansible.builtin.file:
path: "{{ _playwright_stage_dir }}/volume"
state: directory
mode: "0755"
- name: 📄 Render .env from templates/playwright.env.j2
ansible.builtin.copy:
dest: "{{ _playwright_stage_dir }}/.env"
mode: "0644"
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') }}"
- name: ⏳ Wait until application is ready (any non-error response)
uri_retry:
url: "{{ lookup('tls', application_id, 'url.base') }}"
status_code: "{{ range(100, 500) | list }}"
validate_certs: false
follow_redirects: none
register: _wait_result
retries: "{{ TEST_E2E_PLAYWRIGHT_WAIT_RETRIES }}"
delay: "{{ TEST_E2E_PLAYWRIGHT_WAIT_DELAY }}"
until: >-
_wait_result.status is defined and
_wait_result.status | int >= 100 and
_wait_result.status | int < 500
when:
- TEST_E2E_PLAYWRIGHT_WAIT_ENABLED | bool
- not (lookup('tls', application_id, 'url.base') is search('\.onion'))
- lookup('config', application_id, 'services.frontend.enabled', true) | bool
- name: ✅ Ensure reports dir exists
ansible.builtin.file:
path: "{{ _playwright_reports_dir }}"
state: directory
mode: "0755"
- name: 📊 Collect host disk diagnostics before Playwright
ansible.builtin.script: "{{ role_path }}/files/dump_disk_diagnostics.sh"
changed_when: false
when: MODE_DEBUG | bool
- name: ⏳ Wait until the onion answers over Tor (recreate circuits on exhaustion)
ansible.builtin.script: >-
{{ role_path }}/files/wait_onion_reachable.sh
{{ lookup('tor_socks') | quote }}
{{ lookup('domain', application_id) | quote }}
{{ TEST_E2E_PLAYWRIGHT_ONION_PROBE_RETRIES | int }}
{{ TEST_E2E_PLAYWRIGHT_ONION_PROBE_DELAY | int }}
{{ lookup('container', 'svc-net-tor', 'directories.instance') | quote }}
changed_when: false
become: true
when: TEST_E2E_PLAYWRIGHT_IS_ONION | bool
- name: ▶️ Run Playwright container
ansible.builtin.command:
argv: >-
{{
['container', 'run', '--rm', '--ipc=host', '--shm-size=1g']
+ (
['--network', 'host']
if DEPLOYMENT_MODE == 'swarm'
else (
['--add-host=host.docker.internal:host-gateway']
+ (
['--add-host=' ~ lookup('tls', 'web-app-keycloak', 'domain') ~ ':host-gateway']
if 'web-app-keycloak' in group_names
else []
)
)
)
+ ['--env-file', _playwright_stage_dir ~ '/.env']
+ ['-e', 'INFINITO_PLAYWRIGHT_KEEP=' ~ test_e2e_playwright_keep]
+ ['-e', 'INFINITO_PLAYWRIGHT_GLOBAL_TIMEOUT_MS=' ~ TEST_E2E_PLAYWRIGHT_GLOBAL_TIMEOUT_MS]
+ ['-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_TIMEOUT_FACTOR=' ~ TIMEOUT_FACTOR]
+ (['-e', 'PLAYWRIGHT_ONION_TIMEOUT_MULTIPLIER=' ~ TEST_E2E_PLAYWRIGHT_ONION_TIMEOUT_MULTIPLIER] if (TEST_E2E_PLAYWRIGHT_IS_ONION | bool) else [])
+ ['-v', _playwright_stage_dir ~ ':/e2e']
+ ['-v', _playwright_stage_dir ~ '/volume:/volume']
+ ['-v', _playwright_reports_dir ~ ':/reports']
+ ['-v', TEST_E2E_PLAYWRIGHT_SHARED_NODE_MODULES ~ ':/e2e/node_modules:ro']
+ ['-v', TEST_E2E_PLAYWRIGHT_SHARED_PERSONAS ~ ':/e2e/tests/personas:ro']
+ ['-w', '/e2e']
+ [TEST_E2E_PLAYWRIGHT_IMAGE]
+ ['/bin/bash', '-lc', TEST_E2E_PLAYWRIGHT_COMMAND]
}}
timeout: "{{ lookup('timeout', TEST_E2E_PLAYWRIGHT_RUN_TIMEOUT_EFFECTIVE) | int }}"
environment: >-
{{
{
'CA_TRUST_CERT_HOST': _playwright_ca_cert_host,
'CA_TRUST_WRAPPER_HOST': _playwright_ca_wrapper_host,
'CA_TRUST_NAME': _playwright_ca_trust_name
} if (_playwright_ca_required | bool) else {}
}}
register: _playwright_run
changed_when: false
failed_when: false
- name: 💬 Print Playwright output (on failure)
ansible.builtin.debug:
msg: |
rc: {{ _playwright_run.rc | default('<absent: run was aborted>') }}
stdout:
{{ _playwright_run.stdout | default('') }}
stderr:
{{ _playwright_run.stderr | default(_playwright_run.msg | default('')) }}
when: (_playwright_run.rc | default(1)) != 0
changed_when: false
- name: 📝 Record failure
ansible.builtin.set_fact:
_playwright_failures: "{{ (_playwright_failures | default([])) + [application_id] }}"
when: (_playwright_run.rc | default(1)) != 0
- name: 🗄️ Archive Playwright reports for this variant + pass
ansible.builtin.script: >-
{{ role_path }}/files/archive_playwright_reports.sh
{{ _playwright_reports_dir | quote }}
{{ VARIANT_INDEX | int }}
{{ ASYNC_ENABLED | bool | string | lower }}
changed_when: true