name: Automated Tests on: pull_request: push: branches: - master workflow_dispatch: schedule: - cron: "0 2 * * *" permissions: contents: read concurrency: group: frontend-tests-${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }} cancel-in-progress: true jobs: format-tests: # CI runs on the repository's self-hosted runner pool. runs-on: [self-hosted, Linux, X64, pleno, frontend] timeout-minutes: 15 steps: - name: Repair self-hosted workspace permissions shell: bash run: | if [[ -d "$GITHUB_WORKSPACE" ]]; then sudo -n chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true foreign_entry="$(find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 2 ! -user "$(id -u)" -print -quit 2>/dev/null || true)" if [[ -n "$foreign_entry" ]]; then trash="$GITHUB_WORKSPACE/../_workspace-trash-$GITHUB_RUN_ID-$GITHUB_JOB" rm -rf "$trash" 2>/dev/null || true mv "$GITHUB_WORKSPACE" "$trash" 2>/dev/null || true mkdir -p "$GITHUB_WORKSPACE" fi fi - name: Checkout repository uses: actions/checkout@v5 - name: Setup Node.js uses: actions/setup-node@v5 with: node-version: 22 - name: Check AI workflow sync run: node scripts/sync-ai-workflow.mjs --check - name: Install dependencies run: npm ci --legacy-peer-deps - name: Check frontend test formatting run: npm run format:tests:check build-and-unit: needs: format-tests runs-on: [self-hosted, Linux, X64, pleno, frontend] timeout-minutes: 30 steps: - name: Repair self-hosted workspace permissions shell: bash run: | if [[ -d "$GITHUB_WORKSPACE" ]]; then sudo -n chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true foreign_entry="$(find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 2 ! -user "$(id -u)" -print -quit 2>/dev/null || true)" if [[ -n "$foreign_entry" ]]; then trash="$GITHUB_WORKSPACE/../_workspace-trash-$GITHUB_RUN_ID-$GITHUB_JOB" rm -rf "$trash" 2>/dev/null || true mv "$GITHUB_WORKSPACE" "$trash" 2>/dev/null || true mkdir -p "$GITHUB_WORKSPACE" fi fi - name: Checkout repository uses: actions/checkout@v5 - name: Setup Node.js uses: actions/setup-node@v5 with: node-version: 22 - name: Install dependencies run: npm ci --legacy-peer-deps - name: Lint run: npm run lint - name: Check i18n source consistency run: npm run i18n:v2:check - name: Build sanity check run: npm run build - name: Unit tests run: npm run test:unit env: VITEST_BATCH_SIZE: 5 e2e-pr: if: github.event_name != 'schedule' needs: build-and-unit name: E2E-pr-${{ matrix.suite }}-${{ matrix.project }} runs-on: [self-hosted, Linux, X64, pleno, frontend, docker] timeout-minutes: 30 strategy: fail-fast: false max-parallel: 4 matrix: suite: [core, changed] project: [chromium-desktop, chromium-mobile] env: PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-pr-${{ matrix.suite }}-${{ matrix.project }} PLAYWRIGHT_REPORTER_MODE: line-html steps: - name: Repair self-hosted workspace permissions shell: bash run: | if [[ -d "$GITHUB_WORKSPACE" ]]; then sudo -n chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true foreign_entry="$(find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 2 ! -user "$(id -u)" -print -quit 2>/dev/null || true)" if [[ -n "$foreign_entry" ]]; then trash="$GITHUB_WORKSPACE/../_workspace-trash-$GITHUB_RUN_ID-$GITHUB_JOB" rm -rf "$trash" 2>/dev/null || true mv "$GITHUB_WORKSPACE" "$trash" 2>/dev/null || true mkdir -p "$GITHUB_WORKSPACE" fi fi - name: Checkout repository uses: actions/checkout@v5 with: fetch-depth: 0 - name: Resolve Playwright diff refs id: playwright-diff shell: bash env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} EVENT_NAME: ${{ github.event_name }} HEAD_SHA: ${{ github.sha }} PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} PUSH_BEFORE_SHA: ${{ github.event.before }} run: | set -euo pipefail zero_sha="0000000000000000000000000000000000000000" if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_BASE_SHA" ]]; then base_ref="$PR_BASE_SHA" elif [[ -z "$PUSH_BEFORE_SHA" || "$PUSH_BEFORE_SHA" == "$zero_sha" ]]; then git fetch --no-tags --prune origin "$DEFAULT_BRANCH" base_ref="origin/$DEFAULT_BRANCH" else base_ref="$PUSH_BEFORE_SHA" fi echo "base=$base_ref" >> "$GITHUB_OUTPUT" echo "head=$HEAD_SHA" >> "$GITHUB_OUTPUT" - name: Setup Node.js uses: actions/setup-node@v5 with: node-version: 22 - name: Run Playwright PR suite in container shell: bash env: DIFF_BASE_REF: ${{ steps.playwright-diff.outputs.base }} DIFF_HEAD_REF: ${{ steps.playwright-diff.outputs.head }} MATRIX_SUITE: ${{ matrix.suite }} MATRIX_PROJECT: ${{ matrix.project }} RUN_ID: ${{ github.run_id }} run: | set -euo pipefail case "$MATRIX_SUITE" in core) suite_offset=0 ;; changed) suite_offset=10 ;; *) echo "Unsupported Playwright PR suite: $MATRIX_SUITE" >&2; exit 1 ;; esac case "$MATRIX_PROJECT" in chromium-desktop) project_offset=1 ;; chromium-mobile) project_offset=2 ;; *) echo "Unsupported Playwright PR project: $MATRIX_PROJECT" >&2; exit 1 ;; esac port_seed=$((20000 + (RUN_ID % 20000) + suite_offset + project_offset)) playwright_dev_port="$( PORT_SEED="$port_seed" node - <<'NODE' const net = require("node:net"); const start = Number(process.env.PORT_SEED || 20000); const candidates = Array.from({ length: 200 }, (_, index) => start + index); function tryPort(index) { if (index >= candidates.length) { console.error("Unable to find a free Playwright dev-server port."); process.exit(1); } const port = candidates[index]; const server = net.createServer(); server.once("error", () => tryPort(index + 1)); server.listen(port, "127.0.0.1", () => { server.close(() => { console.log(port); }); }); } tryPort(0); NODE )" if docker info >/dev/null 2>&1; then docker_cmd=(docker) elif sudo -n docker info >/dev/null 2>&1; then docker_cmd=(sudo docker) else echo "Docker is not available to the runner user, and sudo docker is not available." >&2 exit 1 fi mkdir -p output/playwright "${docker_cmd[@]}" run --rm --ipc=host --network host \ --volume "$PWD:/source:ro" \ --volume "$PWD/output/playwright:/work/output/playwright" \ --workdir /work \ --env HOME=/tmp \ --env CI="${CI:-}" \ --env PLAYWRIGHT_ARTIFACT_NAMESPACE="$PLAYWRIGHT_ARTIFACT_NAMESPACE" \ --env PLAYWRIGHT_REPORTER_MODE="$PLAYWRIGHT_REPORTER_MODE" \ --env PLAYWRIGHT_DEV_PORT="$playwright_dev_port" \ --env MATRIX_SUITE="$MATRIX_SUITE" \ --env MATRIX_PROJECT="$MATRIX_PROJECT" \ --env DIFF_BASE_REF="$DIFF_BASE_REF" \ --env DIFF_HEAD_REF="$DIFF_HEAD_REF" \ mcr.microsoft.com/playwright:v1.58.2-noble \ bash -lc ' set -euo pipefail tar --exclude=./output/playwright -C /source -cf - . | tar -C /work -xf - git config --global --add safe.directory /work npm ci --legacy-peer-deps ulimit -n 16384 || true if [[ "$MATRIX_SUITE" == "core" ]]; then npx playwright test --grep @smoke --project="$MATRIX_PROJECT" npm run test:e2e:pr -- --core-only --project="$MATRIX_PROJECT" else npm run test:e2e:pr -- --changed-only --project="$MATRIX_PROJECT" --base="$DIFF_BASE_REF" --head="$DIFF_HEAD_REF" fi ' - name: Upload Playwright report if: failure() || cancelled() continue-on-error: true uses: actions/upload-artifact@v4 with: name: playwright-report-pr-${{ matrix.suite }}-${{ matrix.project }} path: | output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}-* output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }} if-no-files-found: ignore retention-days: 1 e2e-full: if: > always() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch) && needs.build-and-unit.result == 'success' && (github.event_name == 'schedule' || needs.e2e-pr.result == 'success') needs: [build-and-unit, e2e-pr] name: E2E-full-${{ matrix.role }}-${{ matrix.browser_label }}-${{ matrix.device }} runs-on: [self-hosted, Linux, X64, pleno, frontend, docker] timeout-minutes: 60 strategy: fail-fast: false max-parallel: 4 matrix: role: [customer, subuser, admin, superuser] browser: [chromium, firefox, webkit] device: [mobile, tablet, desktop] include: - browser: chromium browser_label: Chromium browser_install: chromium - browser: firefox browser_label: Firefox browser_install: firefox - browser: webkit browser_label: WebKit browser_install: webkit env: PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-full-${{ matrix.role }}-${{ matrix.browser }}-${{ matrix.device }} PLAYWRIGHT_REPORTER_MODE: line-html steps: - name: Repair self-hosted workspace permissions shell: bash run: | if [[ -d "$GITHUB_WORKSPACE" ]]; then sudo -n chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true foreign_entry="$(find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 2 ! -user "$(id -u)" -print -quit 2>/dev/null || true)" if [[ -n "$foreign_entry" ]]; then trash="$GITHUB_WORKSPACE/../_workspace-trash-$GITHUB_RUN_ID-$GITHUB_JOB" rm -rf "$trash" 2>/dev/null || true mv "$GITHUB_WORKSPACE" "$trash" 2>/dev/null || true mkdir -p "$GITHUB_WORKSPACE" fi fi - name: Checkout repository uses: actions/checkout@v5 - name: Setup Node.js uses: actions/setup-node@v5 with: node-version: 22 - name: Run full Playwright slice in container shell: bash env: MATRIX_ROLE: ${{ matrix.role }} MATRIX_BROWSER: ${{ matrix.browser }} MATRIX_DEVICE: ${{ matrix.device }} RUN_ID: ${{ github.run_id }} run: | set -euo pipefail case "$MATRIX_ROLE" in customer) role_offset=0 ;; subuser) role_offset=100 ;; admin) role_offset=200 ;; superuser) role_offset=300 ;; *) echo "Unsupported Playwright role: $MATRIX_ROLE" >&2; exit 1 ;; esac case "$MATRIX_BROWSER" in chromium) browser_offset=0 ;; firefox) browser_offset=30 ;; webkit) browser_offset=60 ;; *) echo "Unsupported Playwright browser: $MATRIX_BROWSER" >&2; exit 1 ;; esac case "$MATRIX_DEVICE" in mobile) device_offset=1 ;; tablet) device_offset=2 ;; desktop) device_offset=3 ;; *) echo "Unsupported Playwright device: $MATRIX_DEVICE" >&2; exit 1 ;; esac port_seed=$((20000 + (RUN_ID % 20000) + role_offset + browser_offset + device_offset)) playwright_dev_port="$( PORT_SEED="$port_seed" node - <<'NODE' const net = require("node:net"); const start = Number(process.env.PORT_SEED || 20000); const candidates = Array.from({ length: 200 }, (_, index) => start + index); function tryPort(index) { if (index >= candidates.length) { console.error("Unable to find a free Playwright dev-server port."); process.exit(1); } const port = candidates[index]; const server = net.createServer(); server.once("error", () => tryPort(index + 1)); server.listen(port, "127.0.0.1", () => { server.close(() => { console.log(port); }); }); } tryPort(0); NODE )" if docker info >/dev/null 2>&1; then docker_cmd=(docker) elif sudo -n docker info >/dev/null 2>&1; then docker_cmd=(sudo docker) else echo "Docker is not available to the runner user, and sudo docker is not available." >&2 exit 1 fi mkdir -p output/playwright "${docker_cmd[@]}" run --rm --ipc=host --network host \ --volume "$PWD:/source:ro" \ --volume "$PWD/output/playwright:/work/output/playwright" \ --workdir /work \ --env HOME=/tmp \ --env CI="${CI:-}" \ --env PLAYWRIGHT_ARTIFACT_NAMESPACE="$PLAYWRIGHT_ARTIFACT_NAMESPACE" \ --env PLAYWRIGHT_REPORTER_MODE="$PLAYWRIGHT_REPORTER_MODE" \ --env PLAYWRIGHT_DEV_PORT="$playwright_dev_port" \ --env MATRIX_ROLE="$MATRIX_ROLE" \ --env MATRIX_BROWSER="$MATRIX_BROWSER" \ --env MATRIX_DEVICE="$MATRIX_DEVICE" \ mcr.microsoft.com/playwright:v1.58.2-noble \ bash -lc ' set -euo pipefail tar --exclude=./output/playwright -C /source -cf - . | tar -C /work -xf - git config --global --add safe.directory /work npm ci --legacy-peer-deps ulimit -n 16384 || true npm run test:e2e:full:slice -- --role="$MATRIX_ROLE" --project="$MATRIX_BROWSER-$MATRIX_DEVICE" ' - name: Upload Playwright report if: failure() || cancelled() continue-on-error: true uses: actions/upload-artifact@v4 with: name: playwright-report-full-${{ matrix.role }}-${{ matrix.browser }}-${{ matrix.device }} path: | output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}/report output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}/test-results output/playwright/test-lists/${{ matrix.role }}-${{ matrix.browser }}-${{ matrix.device }}.txt if-no-files-found: ignore retention-days: 1