--- - 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('') }} 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