E2E_API Tests #4859
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: E2E_API Tests | |
| on: | |
| # Runs once a day only, to keep Actions cost down. Pull requests and pushes | |
| # to develop no longer trigger this suite -- use the "Run workflow" button | |
| # (workflow_dispatch) below to test a branch on demand. | |
| # Target start time is 9:00 AM BDT. GitHub queues scheduled runs and | |
| # releases them once capacity frees up: on this repository that delay has | |
| # been roughly 70 minutes every day, so 02:00 UTC (nominally 8:00 AM BDT) | |
| # is what actually starts the run near 9:00 AM BDT. Check the real start | |
| # times of recent scheduled runs before "correcting" this to 03:00 UTC. | |
| schedule: | |
| # 02:00 UTC -- the gating run, on the pinned WordPress/WooCommerce versions. | |
| - cron: 0 2 * * * | |
| # 14:00 UTC -- the canary run, on whatever WordPress/WooCommerce is current. | |
| # It warns and never gates: see WP_STACK below and the continue-on-error on | |
| # the test jobs. Change this cron and you must change the WP_STACK expression. | |
| - cron: 0 14 * * * | |
| # workflow can be manually triggered | |
| workflow_dispatch: | |
| inputs: | |
| testsuite: | |
| description: Choose which test suite to run | |
| default: All | |
| type: choice | |
| options: | |
| - E2E | |
| - API | |
| - All | |
| wp_stack: | |
| description: 'pinned = the versions committed in .wp-env*.json; latest = resolve WordPress and WooCommerce to current (failures do not gate)' | |
| default: pinned | |
| type: choice | |
| options: | |
| - pinned | |
| - latest | |
| # Cancels all previous workflow runs for pull requests that have not completed. | |
| concurrency: | |
| # The concurrency group contains the workflow name and the branch name for pull requests or the commit hash for any other events. | |
| # The stack is part of the group so the pinned run and the latest-canary never cancel | |
| # each other on the same commit. | |
| group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}-${{ inputs.wp_stack || github.event.schedule || 'pinned' }} | |
| cancel-in-progress: true | |
| # workflow permissions | |
| permissions: | |
| checks: write | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| # workflow environment variables | |
| env: | |
| # Which WordPress/WooCommerce versions this run installs. | |
| # pinned -- the versions committed in .wp-env*.json (the gating lane) | |
| # latest -- whatever WordPress and WooCommerce are current (the canary lane) | |
| # Sourced from the workflow_dispatch input, else from which cron fired, else pinned. | |
| # `inputs.wp_stack` is empty (falsy) on scheduled runs, so the fallbacks apply. | |
| WP_STACK: ${{ inputs.wp_stack || (github.event.schedule == '0 14 * * *' && 'latest') || 'pinned' }} | |
| ADMIN: admin | |
| ADMIN_PASSWORD: password | |
| VENDOR: vendor1 | |
| VENDOR2: vendor2 | |
| CUSTOMER: customer1 | |
| CUSTOMER2: customer2 | |
| USER_PASSWORD: 01dokan01 | |
| GMAP: ${{secrets.GMAP}} | |
| MAPBOX: ${{secrets.MAPBOX}} | |
| LICENSE_KEY: ${{secrets.LICENSE_KEY}} | |
| # Stripe Express test keys + the real EXPRESS connected accounts (all secrets) so the money/checkout | |
| # specs RUN in CI. The connected accounts MUST belong to the Express test platform — the Connect-suite | |
| # STRIPE_VENDOR*_ACCT secrets are a different platform and cannot receive Express transfers, so these | |
| # use distinct STRIPE_EXPRESS_VENDOR*_ACCT secret names. | |
| TEST_PUBLISH_KEY_STRIPE_EXPRESS: ${{secrets.TEST_PUBLISH_KEY_STRIPE_EXPRESS}} | |
| TEST_SECRET_KEY_STRIPE_EXPRESS: ${{secrets.TEST_SECRET_KEY_STRIPE_EXPRESS}} | |
| STRIPE_EXPRESS_VENDOR1_ACCT: ${{secrets.STRIPE_EXPRESS_VENDOR1_ACCT}} | |
| STRIPE_EXPRESS_VENDOR2_ACCT: ${{secrets.STRIPE_EXPRESS_VENDOR2_ACCT}} | |
| STRIPE_EXPRESS_REQUIRED: true | |
| BASE_URL: http://localhost:9999 | |
| CI: true | |
| HEADLESS: true | |
| # TODO: Remove this after testing | |
| DOKAN_PRO: true | |
| NO_SETUP: false | |
| FORCE_COLOR: 1 | |
| DB_HOST_NAME: localhost | |
| DB_USER_NAME: root | |
| DB_USER_PASSWORD: password | |
| DB_PORT: 9998 | |
| DATABASE: tests-wordpress | |
| DB_PREFIX: wp | |
| PR_NUMBER: ${{ github.event.number }} | |
| SHA: ${{ github.event.pull_request.head.sha }} | |
| SYSTEM_INFO: ./tests/pw/playwright/systemInfo.json | |
| API_TEST_RESULT: ./tests/pw/playwright-report/api/summary-report/results.json | |
| E2E_TEST_RESULT: ./tests/pw/playwright-report/e2e/summary-report/results.json | |
| API_COVERAGE: ./tests/pw/playwright-report/api/coverage-report/coverage.json | |
| E2E_COVERAGE: ./tests/pw/playwright-report/e2e/coverage-report/coverage.json | |
| jobs: | |
| # Build dokan-lite and dokan-pro once. Two parallel jobs (build_lite, build_pro) | |
| # so the slower of the two (~6 min) gates downstream jobs instead of ~12 min serial. | |
| # Each consumer job downloads the tar artifacts and skips its own build steps. | |
| build_lite: | |
| name: build dokan-lite | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout testing repo | |
| uses: actions/checkout@v4 | |
| - name: Use desired version of NodeJS | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| - name: Composer install and build (Dokan-lite) | |
| run: | | |
| composer i || composer update | |
| composer i --no-dev -o || composer update --no-dev -o | |
| # node_modules cache: skips ~2-3 min of `npm ci` on cache hit. | |
| # Keyed on the lockfile so any dependency change invalidates the | |
| # cache automatically. Same key is restored in build_pro to skip | |
| # the redundant lite install there too. | |
| - name: Cache lite node_modules | |
| id: lite-nm | |
| uses: actions/cache@v4 | |
| with: | |
| path: node_modules | |
| key: lite-nm-${{ runner.os }}-${{ hashFiles('package-lock.json') }} | |
| - name: Npm install (Dokan-lite) | |
| if: steps.lite-nm.outputs.cache-hit != 'true' | |
| run: | | |
| npm ci --prefer-offline --no-audit --no-fund || npm i | |
| - name: Npm build (Dokan-lite) | |
| run: | | |
| npm run build | |
| # Tar the runtime tree (excludes node_modules, .git, dev-only output). | |
| # tar preserves perms + symlinks unlike upload-artifact's zip path. | |
| - name: Tar dokan-lite build output | |
| run: | | |
| tar --exclude=./.git \ | |
| --exclude=./node_modules \ | |
| --exclude=./tests/pw/node_modules \ | |
| --exclude=./tests/pw/playwright \ | |
| --exclude=./tests/pw/playwright-report \ | |
| --exclude=./tests/pw/wp-data \ | |
| --exclude=./tests/pw/test-results \ | |
| -czf /tmp/dokan-lite-build.tar.gz . | |
| - name: Upload dokan-lite build artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: dokan-lite-build | |
| path: /tmp/dokan-lite-build.tar.gz | |
| retention-days: 1 | |
| compression-level: 0 | |
| build_pro: | |
| name: build dokan-pro | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| # Only run when secrets are available (i.e. not a fork PR). Same gate as today. | |
| if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository | |
| steps: | |
| # Pro's webpack.config.js calls getDokanLitePath() which resolves | |
| # ../dokan-lite from inside the pro folder, and also requires | |
| # lite-root files like webpack-dependency-mapping.js that pull in | |
| # lodash from lite's node_modules. So lite needs both source AND | |
| # node_modules in this job — but NOT a full build (we never use | |
| # lite's assets/). Replicate the layout: lite at runner root with | |
| # a self-symlink, pro as sibling. | |
| - name: Checkout dokan-lite source (for pro webpack path resolution) | |
| uses: actions/checkout@v4 | |
| - name: Prepare dokan-lite folder | |
| run: | | |
| ln -s . dokan-lite | |
| - name: Checkout Dokan Pro | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: getdokan/dokan-pro | |
| ref: develop | |
| path: dokan-pro | |
| token: ${{ secrets.DOKAN_PRO_TOKEN }} | |
| - name: Checkout Dokan Invoice | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: getdokan/dokan-invoice | |
| ref: develop | |
| path: dokan-invoice | |
| - name: Use desired version of NodeJS | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| cache-dependency-path: | | |
| package-lock.json | |
| dokan-pro/package-lock.json | |
| # Share the same node_modules cache that build_lite produced. | |
| # Same key, so a cache hit here means pro skips the ~3 min of | |
| # redundant lite install entirely. On cache miss we still fall | |
| # back to npm ci (e.g., after a lockfile change). | |
| - name: Cache lite node_modules | |
| id: lite-nm | |
| uses: actions/cache@v4 | |
| with: | |
| path: node_modules | |
| key: lite-nm-${{ runner.os }}-${{ hashFiles('package-lock.json') }} | |
| # Install lite's node_modules so dokan-pro/webpack.config.js can | |
| # require lite-root JS helpers (webpack-dependency-mapping.js etc). | |
| # Skip `npm run build` — we don't ship lite's assets from this job. | |
| - name: Install dokan-lite npm deps (no build, skip on cache hit) | |
| if: steps.lite-nm.outputs.cache-hit != 'true' | |
| run: | | |
| npm ci --prefer-offline --no-audit --no-fund || npm i | |
| # Cache pro's own node_modules separately (its lockfile is independent). | |
| - name: Cache pro node_modules | |
| id: pro-nm | |
| uses: actions/cache@v4 | |
| with: | |
| path: dokan-pro/node_modules | |
| key: pro-nm-${{ runner.os }}-${{ hashFiles('dokan-pro/package-lock.json') }} | |
| - name: Build Dokan Pro | |
| working-directory: dokan-pro | |
| run: | | |
| composer i --no-dev -o | |
| if [ "${{ steps.pro-nm.outputs.cache-hit }}" != "true" ]; then | |
| npm ci --prefer-offline --no-audit --no-fund || npm i | |
| fi | |
| npm run build | |
| # Fetch only tests/plugins from test_utils branch (premium plugins not in default branch) | |
| - name: Fetch WooCommerce premium plugins from test_utils | |
| working-directory: dokan-pro | |
| run: | | |
| git fetch origin test_utils | |
| git checkout origin/test_utils -- tests/plugins | |
| # Extract any zipped premium plugins collected from test_utils | |
| - name: Collect WooCommerce premium plugins | |
| run: | | |
| PLUGINS_DIR="dokan-pro/tests/plugins" | |
| for zip in "$PLUGINS_DIR"/*.zip; do | |
| [ -f "$zip" ] || continue | |
| plugin_dir="${zip%.zip}" | |
| if [ ! -d "$plugin_dir" ]; then | |
| echo "Extracting: $zip" | |
| unzip -q "$zip" -d "$PLUGINS_DIR" | |
| fi | |
| done | |
| echo "Available premium plugins:" | |
| ls -la "$PLUGINS_DIR" | |
| - name: Tar dokan-pro + dokan-invoice | |
| run: | | |
| tar --exclude='dokan-pro/.git' \ | |
| --exclude='dokan-pro/node_modules' \ | |
| --exclude='dokan-invoice/.git' \ | |
| -czf /tmp/dokan-pro-build.tar.gz dokan-pro dokan-invoice | |
| - name: Upload dokan-pro build artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: dokan-pro-build | |
| path: /tmp/dokan-pro-build.tar.gz | |
| retention-days: 1 | |
| compression-level: 0 | |
| e2e_tests: | |
| name: e2e tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 75 | |
| # The canary lane warns, it never gates. `env` is not readable from a job-level | |
| # continue-on-error, so the WP_STACK expression is repeated here. | |
| continue-on-error: ${{ inputs.wp_stack == 'latest' || github.event.schedule == '0 14 * * *' }} | |
| # Wait for both build jobs. build_pro is conditional (fork PRs) and may not run; | |
| # `always() && build_lite succeeded` covers the fork-PR path too. | |
| needs: [build_lite, build_pro] | |
| if: always() && needs.build_lite.result == 'success' && (needs.build_pro.result == 'success' || needs.build_pro.result == 'skipped') | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] | |
| shardTotal: [12] | |
| steps: | |
| - name: Download dokan-lite build artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: dokan-lite-build | |
| path: /tmp/ | |
| - name: Extract dokan-lite build | |
| id: clone-dokan-lite | |
| run: | | |
| tar -xzf /tmp/dokan-lite-build.tar.gz | |
| rm /tmp/dokan-lite-build.tar.gz | |
| ln -s . dokan-lite | |
| - name: Download dokan-pro build artifact | |
| if: needs.build_pro.result == 'success' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: dokan-pro-build | |
| path: /tmp/ | |
| - name: Extract dokan-pro build | |
| if: needs.build_pro.result == 'success' | |
| run: | | |
| tar -xzf /tmp/dokan-pro-build.tar.gz | |
| rm /tmp/dokan-pro-build.tar.gz | |
| - name: Use desired version of NodeJS | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| cache-dependency-path: tests/pw/package-lock.json | |
| - name: Cache test deps (tests/pw/node_modules) | |
| id: pw-nm | |
| uses: actions/cache@v4 | |
| with: | |
| path: tests/pw/node_modules | |
| key: pw-nm-${{ runner.os }}-${{ hashFiles('tests/pw/package-lock.json') }} | |
| - name: Install test dependencies | |
| if: steps.pw-nm.outputs.cache-hit != 'true' | |
| working-directory: tests/pw | |
| run: | | |
| npm ci --prefer-offline --no-audit --no-fund || npm i | |
| - name: Create wp debuglog file | |
| id: debug-log | |
| working-directory: tests/pw | |
| run: | | |
| mkdir -p wp-data && touch wp-data/debug.log | |
| # Use CI config (../../dokan-pro) when dokan-pro present; lite-only when not (fork PR) | |
| - name: Prepare wp-env config | |
| working-directory: tests/pw | |
| run: | | |
| if [ -d "../../dokan-pro" ]; then | |
| echo "Dokan Pro present, using .wp-env.ci.json" | |
| cp .wp-env.ci.json .wp-env.override.json | |
| else | |
| echo "Dokan Pro not present, using lite-only wp-env config" | |
| cp .wp-env.json .wp-env.override.json | |
| fi | |
| # Canary lane: drop the version pins so wp-env resolves whatever | |
| # WordPress and WooCommerce are current. Gating lane is untouched. | |
| if [ "$WP_STACK" = "latest" ]; then | |
| jq '.env.tests.core = null | |
| | .env.tests.plugins |= map(sub("/woocommerce\\.[0-9][0-9.]*\\.zip$"; "/woocommerce.zip"))' \ | |
| .wp-env.override.json > .wp-env.override.json.tmp | |
| mv .wp-env.override.json.tmp .wp-env.override.json | |
| echo "WP_STACK=latest — core and WooCommerce unpinned" | |
| fi | |
| # Route Docker Hub pulls through Google's public pull-through mirror. The wp-env images | |
| # (mariadb, phpmyadmin, wordpress, composer — all official library/* images) then come | |
| # from the mirror instead of registry-1.docker.io, dodging Docker Hub's anonymous | |
| # pull-rate-limit / connection timeouts that randomly fail one shard's `wp-env start`. | |
| # No credentials needed; Docker falls back to Docker Hub if the mirror misses an image. | |
| - name: Route Docker Hub pulls through a mirror (avoids anonymous rate-limit/timeouts) | |
| run: | | |
| # Merge into any existing daemon.json (jq is preinstalled on the runner) so we | |
| # never clobber the runner's own Docker config; create it only if absent. | |
| sudo mkdir -p /etc/docker | |
| if [ -s /etc/docker/daemon.json ]; then | |
| sudo jq '. + {"registry-mirrors": ["https://mirror.gcr.io"]}' /etc/docker/daemon.json | sudo tee /etc/docker/daemon.json.tmp >/dev/null | |
| sudo mv /etc/docker/daemon.json.tmp /etc/docker/daemon.json | |
| else | |
| echo '{ "registry-mirrors": ["https://mirror.gcr.io"] }' | sudo tee /etc/docker/daemon.json >/dev/null | |
| fi | |
| sudo systemctl restart docker | |
| # Wait for the daemon socket instead of a fixed sleep. | |
| for i in $(seq 1 15); do docker info >/dev/null 2>&1 && break; sleep 1; done | |
| # wp-env pulls four Docker Hub library images on every run: mariadb:lts and | |
| # phpmyadmin via compose, plus the wordpress / wordpress:cli build bases. | |
| # Anonymous Docker Hub pulls from shared runner IPs randomly time out even | |
| # with the gcr mirror configured (the daemon silently falls back to | |
| # registry-1.docker.io when the mirror misses), so pre-seed the images: | |
| # restore them from the actions cache, else pull from AWS's public mirror | |
| # of Docker official images and retag. Once the images exist locally, | |
| # compose/build skip the Docker Hub pull entirely. The ISO week in the | |
| # cache key stops the floating tags (lts, latest) from pinning forever. | |
| - name: Compute wp-env image list and cache key | |
| id: wp-env-images | |
| working-directory: tests/pw | |
| run: | | |
| PHP=$(jq -r '.env.tests.phpVersion // empty' .wp-env.override.json) | |
| if [ -z "$PHP" ]; then PHP=$(jq -r '.env.tests.phpVersion // empty' .wp-env.json); fi | |
| IMAGES="mariadb:lts phpmyadmin:latest wordpress${PHP:+:php$PHP} wordpress:cli${PHP:+-php$PHP}" | |
| echo "list=$IMAGES" >> "$GITHUB_OUTPUT" | |
| echo "key=wp-env-images-v1-php${PHP:-default}-$(date +%G-%V)" >> "$GITHUB_OUTPUT" | |
| echo "images: $IMAGES" | |
| - name: Restore wp-env docker images from cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/wp-env-images | |
| key: ${{ steps.wp-env-images.outputs.key }} | |
| - name: Pre-seed wp-env docker images | |
| run: | | |
| IMAGES="${{ steps.wp-env-images.outputs.list }}" | |
| TAR="$HOME/.cache/wp-env-images/images.tar" | |
| if [ -f "$TAR" ]; then | |
| docker load -i "$TAR" | |
| else | |
| mkdir -p "$(dirname "$TAR")" | |
| complete=true | |
| for img in $IMAGES; do | |
| pulled=false | |
| for attempt in 1 2 3; do | |
| if docker pull "public.ecr.aws/docker/library/$img"; then pulled=true; break; fi | |
| sleep 15 | |
| done | |
| if [ "$pulled" = true ]; then | |
| docker tag "public.ecr.aws/docker/library/$img" "$img" | |
| docker rmi "public.ecr.aws/docker/library/$img" >/dev/null | |
| else | |
| echo "::warning::could not pre-seed $img from the ECR mirror; wp-env will pull it from Docker Hub" | |
| complete=false | |
| fi | |
| done | |
| # Only cache a complete set so a partial failure is retried next run. | |
| if [ "$complete" = true ]; then docker save -o "$TAR" $IMAGES; fi | |
| fi | |
| docker image ls | |
| - name: Start WordPress Env | |
| id: wp-env | |
| uses: nick-fields/retry@v3 | |
| with: | |
| timeout_minutes: 6 | |
| max_attempts: 3 | |
| # Space the attempts out: Docker Hub's anonymous throttle window | |
| # outlives back-to-back retries, so waiting beats hammering. | |
| retry_wait_seconds: 90 | |
| retry_on: error | |
| command: | | |
| cd tests/pw | |
| npm run start:env | |
| # What actually got installed, not what we asked for. On the canary lane this is | |
| # the whole point of the run; on the gating lane it is the proof the pins held. | |
| - name: Record resolved WordPress / WooCommerce versions | |
| working-directory: tests/pw | |
| run: | | |
| # `--` is required: without it npm swallows wp-cli's own flags, so | |
| # `--field=version` never reaches wp-cli, `wp plugin get` prints its | |
| # full field/value table, no line starts with a digit, and the grep | |
| # below yields "unknown" for WooCommerce on every single run. | |
| ver() { npm run --silent wp-env -- run tests-cli "$@" 2>/dev/null | tr -d '\r' | grep -Eo '^[0-9]+\.[0-9.]+' | head -n1; } | |
| WP=$(ver wp core version) | |
| WC=$(ver wp plugin get woocommerce --field=version) | |
| echo "stack=$WP_STACK wordpress=${WP:-unknown} woocommerce=${WC:-unknown}" | |
| echo "- \`$WP_STACK\` lane — WordPress **${WP:-unknown}**, WooCommerce **${WC:-unknown}**" >> "$GITHUB_STEP_SUMMARY" | |
| # Persist for merge-reports, which runs on a clean runner with no | |
| # wp-env stack and so cannot resolve these itself. Rides along in the | |
| # existing tests/pw/playwright artifact — no new upload needed. | |
| mkdir -p playwright | |
| printf '{"stack":"%s","wordpress":"%s","woocommerce":"%s"}\n' \ | |
| "$WP_STACK" "${WP:-unknown}" "${WC:-unknown}" > playwright/resolved-versions.json | |
| - name: Activate WooCommerce premium plugins | |
| if: needs.build_pro.result == 'success' | |
| working-directory: tests/pw | |
| run: | | |
| npm run wp-env run tests-cli wp plugin activate \ | |
| woocommerce-bookings \ | |
| woocommerce-subscriptions \ | |
| woocommerce-product-addons \ | |
| woocommerce-simple-auctions \ | |
| woocommerce-pdf-invoices-packing-slips \ | |
| dokan-pro \ | |
| dokan-invoice | |
| - name: Enable Dokan New React UI (vendor dashboard + product editor) | |
| working-directory: tests/pw | |
| run: | | |
| npm run wp-env run tests-cli wp eval ' | |
| $appearance = get_option("dokan_appearance", []); | |
| $appearance["vendor_layout_style"] = "latest"; | |
| $appearance["vendor_product_editor"] = "latest"; | |
| update_option("dokan_appearance", $appearance); | |
| echo "Dokan appearance set to latest UI" . PHP_EOL; | |
| ' | |
| - name: Get installed Playwright version | |
| id: playwright-version | |
| working-directory: tests/pw | |
| run: | | |
| echo "PLAYWRIGHT_VERSION=$(npm ls @playwright/test --json | jq --raw-output '.dependencies["@playwright/test"].version')" >> "$GITHUB_ENV" | |
| - name: Cache playwright binaries | |
| id: playwright-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cache/ms-playwright | |
| key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} | |
| restore-keys: | | |
| ${{ runner.os }}-playwright- | |
| - name: Install Playwright browser binaries & OS dependencies | |
| id: pw-install | |
| if: steps.playwright-cache.outputs.cache-hit != 'true' | |
| working-directory: tests/pw | |
| run: | | |
| npx playwright install --with-deps chromium | |
| - name: Run docker setup | |
| working-directory: tests/pw | |
| run: npm run docker:setup | |
| # Resolve the spec list for this shard from the committed duration baseline. | |
| # Falls back to alphabetical sharding if the baseline is missing. | |
| - name: Resolve balanced shard spec list | |
| id: shard-specs | |
| working-directory: tests/pw | |
| run: | | |
| node ./utils/getShardSpecs.js ${{ matrix.shardIndex }} ${{ matrix.shardTotal }} > /tmp/shard-specs.txt | |
| echo "spec count: $(wc -l < /tmp/shard-specs.txt)" | |
| # Emit a per-shard manifest (no GITHUB_STEP_SUMMARY write here — that | |
| # would clutter the run page with 12 collapsed cards). merge-reports | |
| # collects these manifests from artifacts and renders one consolidated | |
| # shard feature map below the quality report. | |
| - name: Write shard feature manifest | |
| if: always() | |
| working-directory: tests/pw | |
| run: | | |
| if [ -s /tmp/shard-specs.txt ]; then | |
| mkdir -p playwright | |
| node -e ' | |
| const fs = require("fs"); | |
| const specs = fs.readFileSync("/tmp/shard-specs.txt", "utf8").split("\n").filter(Boolean); | |
| const features = [...new Set(specs.map(s => s.replace(/^tests\/e2e\//, "").split("/")[0]))].sort(); | |
| fs.writeFileSync("playwright/shard-features.json", JSON.stringify({ | |
| shardIndex: ${{ matrix.shardIndex }}, | |
| shardTotal: ${{ matrix.shardTotal }}, | |
| specCount: specs.length, | |
| features | |
| }, null, 2)); | |
| ' | |
| fi | |
| # continue-on-error: the isolation re-run below decides this shard's result. | |
| # `outcome` still reports the true first-pass result, so every later step that | |
| # keys off steps.e2e-test.outcome keeps working unchanged. | |
| - name: 🧪 Run e2e tests-${{ matrix.shardIndex }} | |
| id: e2e-test | |
| continue-on-error: true | |
| if: success() && (github.event_name != 'workflow_dispatch' || ( github.event_name == 'workflow_dispatch' && (github.event.inputs.testsuite == 'E2E' || github.event.inputs.testsuite == 'All'))) | |
| timeout-minutes: 60 | |
| working-directory: tests/pw | |
| run: | | |
| # Pass the resolved spec list as positional args (xargs -d to preserve newlines) | |
| if [ -s /tmp/shard-specs.txt ]; then | |
| xargs -a /tmp/shard-specs.txt -d '\n' npm run test:e2e -- | |
| else | |
| # No baseline — fall back to Playwright's built-in alphabetical sharding | |
| npm run test:e2e -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} | |
| fi | |
| # Rescue the first-pass traces/screenshots BEFORE anything else can delete them. | |
| # | |
| # Playwright CLEARS `outputDir` (playwright/e2e/test-artifacts, playwright.config.ts:15) at | |
| # the START of every run, and two more runs follow this one: the isolation re-run below and | |
| # the coverage run further down. By the time "Archive test artifacts" uploads | |
| # tests/pw/playwright, the trace for the failure that actually mattered is long gone — | |
| # verified on run 31362069120, whose shard-8 artifact contains only shard-features.json and | |
| # systemInfo.json. Every CI-only failure therefore had to be diagnosed by guesswork from the | |
| # error message alone. | |
| # | |
| # This is the same clobbering the summary-report steps below already work around; traces were | |
| # simply never covered. Copy, don't move: the isolation re-run's own artifacts are still | |
| # wanted, and a copy cannot break the run if the directory is absent. | |
| - name: 🧾 Preserve first-pass traces before the re-run wipes them | |
| if: always() && steps.e2e-test.outcome == 'failure' | |
| working-directory: tests/pw | |
| run: | | |
| if [ -d playwright/e2e/test-artifacts ]; then | |
| mkdir -p playwright/e2e/first-pass-artifacts | |
| cp -R playwright/e2e/test-artifacts/. playwright/e2e/first-pass-artifacts/ || true | |
| echo "preserved $(find playwright/e2e/first-pass-artifacts -type f | wc -l) file(s)" | |
| else | |
| echo "no test-artifacts directory — nothing to preserve" | |
| fi | |
| # Re-run ONLY the failed tests, after the whole shard has finished, without | |
| # their shard-mates. This separates a transient failure from a reproducible one. | |
| # | |
| # Guard on a non-empty failedTests list: if the first pass died from a crash or a | |
| # timeout rather than test failures, `--last-failed` would select zero tests and | |
| # "pass", turning an infrastructure failure green. No recorded failures => no re-run, | |
| # and the gate step below keeps the shard red. | |
| # | |
| # --reporter=list overrides the config reporters on purpose: the blob reporter would | |
| # otherwise emit a second set of results for these same tests and merge-reports would | |
| # double-count them. The merged HTML report therefore keeps showing the FIRST-pass | |
| # failures, which is what actually happened in shard context. | |
| - name: 🔁 Re-run shard failures in isolation | |
| id: e2e-recheck | |
| if: always() && steps.e2e-test.outcome == 'failure' | |
| timeout-minutes: 30 | |
| working-directory: tests/pw | |
| run: | | |
| LAST_RUN=playwright/e2e/test-artifacts/.last-run.json | |
| if [ ! -f "$LAST_RUN" ] || [ "$(node -e "try{process.stdout.write(String((require('./'+process.argv[1]).failedTests||[]).length))}catch(e){process.stdout.write('0')}" "$LAST_RUN")" = "0" ]; then | |
| echo "::warning::No recorded test failures — the first pass failed for another reason (crash/timeout). Not re-running." | |
| exit 1 | |
| fi | |
| set -o pipefail | |
| NO_SETUP=true npx playwright test --project=e2e_tests --last-failed \ | |
| --reporter=list 2>&1 | tee /tmp/recheck.log | |
| # Always say out loud which tests were rescued by the isolation re-run. A test that | |
| # fails with its shard-mates but passes alone is the signature of shard-state | |
| # pollution (an earlier spec leaving a global option/module/product dirty), NOT a | |
| # timing flake — Playwright's in-place `retries` cannot green that class, this can. | |
| - name: 📋 Report isolation re-run outcome | |
| if: always() && steps.e2e-test.outcome == 'failure' | |
| working-directory: tests/pw | |
| run: | | |
| { | |
| echo "### Shard ${{ matrix.shardIndex }} — failures re-run in isolation" | |
| echo | |
| if [ "${{ steps.e2e-recheck.outcome }}" = "success" ]; then | |
| echo "⚠️ **These tests failed alongside their shard-mates but passed alone.**" | |
| echo "The shard is reported green. Treat this as suspected shard-state pollution" | |
| echo "and investigate the polluter — verify by running the earlier spec and reading" | |
| echo "the global state back, not by re-running the test that failed." | |
| else | |
| echo "❌ **The re-run failed too — reproducible failures, not flakes.**" | |
| fi | |
| echo | |
| echo '```' | |
| if [ -f /tmp/recheck.log ]; then | |
| grep -E '^[[:space:]]+(✓|✘|×)' /tmp/recheck.log | head -50 | |
| else | |
| echo "(no re-run log — first pass failed without recorded test failures)" | |
| fi | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| # The actual gate. The shard is red unless the isolation re-run cleared every failure. | |
| - name: Fail the shard when the isolation re-run did not clear | |
| if: always() && steps.e2e-test.outcome == 'failure' && steps.e2e-recheck.outcome != 'success' | |
| run: | | |
| echo "::error::Shard ${{ matrix.shardIndex }} failed and the isolation re-run did not clear it." | |
| exit 1 | |
| # Snapshot the e2e summary before coverage runs — both projects use | |
| # the same playwright.config.ts and write summaryReporter output to | |
| # the same results.json, so the coverage teardown otherwise | |
| # overwrites the actual e2e shard summary with its own (1-test) | |
| # report and the merge step under-counts e2e by ~1000 tests. | |
| - name: Preserve e2e summary before coverage | |
| if: always() && steps.e2e-test.outcome != 'cancelled' | |
| working-directory: tests/pw | |
| run: | | |
| if [ -f playwright-report/e2e/summary-report/results.json ]; then | |
| cp playwright-report/e2e/summary-report/results.json \ | |
| playwright-report/e2e/summary-report/results.e2e.bak.json | |
| fi | |
| # Coverage walks the suite a second time. Skip on PRs to halve the | |
| # per-shard test runtime; develop pushes still compute it. | |
| - name: 🧪 Run e2e coverage | |
| if: | | |
| always() | |
| && (steps.e2e-test.outcome == 'success' || steps.e2e-test.outcome == 'failure') | |
| && github.event_name != 'pull_request' | |
| working-directory: tests/pw | |
| run: | | |
| npm run test:e2e:coverage | |
| - name: Restore e2e summary after coverage | |
| if: always() && steps.e2e-test.outcome != 'cancelled' | |
| working-directory: tests/pw | |
| run: | | |
| if [ -f playwright-report/e2e/summary-report/results.e2e.bak.json ]; then | |
| mv playwright-report/e2e/summary-report/results.e2e.bak.json \ | |
| playwright-report/e2e/summary-report/results.json | |
| fi | |
| - name: Backup Database | |
| if: always() && steps.wp-env.outcome == 'success' | |
| working-directory: tests/pw | |
| run: | | |
| npm run wp-env run tests-cli wp db export wp-data/db.sql | |
| - name: Archive test artifacts (screenshots, HTML snapshots, Reports) | |
| if: always() && steps.debug-log.outcome == 'success' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-artifact-e2e-${{ matrix.shardIndex }} | |
| path: | | |
| tests/pw/wp-data | |
| tests/pw/playwright | |
| tests/pw/playwright-report | |
| if-no-files-found: ignore | |
| retention-days: 1 | |
| api_tests: | |
| name: api tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| # See e2e_tests: the canary lane warns, it never gates. | |
| continue-on-error: ${{ inputs.wp_stack == 'latest' || github.event.schedule == '0 14 * * *' }} | |
| needs: [build_lite, build_pro] | |
| if: always() && needs.build_lite.result == 'success' && (needs.build_pro.result == 'success' || needs.build_pro.result == 'skipped') | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shardIndex: [1] | |
| shardTotal: [1] | |
| steps: | |
| - name: Download dokan-lite build artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: dokan-lite-build | |
| path: /tmp/ | |
| - name: Extract dokan-lite build | |
| id: clone-dokan-lite | |
| run: | | |
| tar -xzf /tmp/dokan-lite-build.tar.gz | |
| rm /tmp/dokan-lite-build.tar.gz | |
| ln -s . dokan-lite | |
| - name: Download dokan-pro build artifact | |
| if: needs.build_pro.result == 'success' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: dokan-pro-build | |
| path: /tmp/ | |
| - name: Extract dokan-pro build | |
| if: needs.build_pro.result == 'success' | |
| run: | | |
| tar -xzf /tmp/dokan-pro-build.tar.gz | |
| rm /tmp/dokan-pro-build.tar.gz | |
| - name: Use desired version of NodeJS | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| cache-dependency-path: tests/pw/package-lock.json | |
| - name: Cache test deps (tests/pw/node_modules) | |
| id: pw-nm | |
| uses: actions/cache@v4 | |
| with: | |
| path: tests/pw/node_modules | |
| key: pw-nm-${{ runner.os }}-${{ hashFiles('tests/pw/package-lock.json') }} | |
| - name: Install test dependencies | |
| if: steps.pw-nm.outputs.cache-hit != 'true' | |
| working-directory: tests/pw | |
| run: | | |
| npm ci --prefer-offline --no-audit --no-fund || npm i | |
| - name: Create wp debuglog file | |
| id: debug-log | |
| working-directory: tests/pw | |
| run: | | |
| mkdir -p wp-data && touch wp-data/debug.log | |
| - name: Prepare wp-env config | |
| working-directory: tests/pw | |
| run: | | |
| if [ -d "../../dokan-pro" ]; then | |
| echo "Dokan Pro present, using .wp-env.ci.json" | |
| cp .wp-env.ci.json .wp-env.override.json | |
| else | |
| echo "Dokan Pro not present, using lite-only wp-env config" | |
| cp .wp-env.json .wp-env.override.json | |
| fi | |
| # Canary lane: drop the version pins so wp-env resolves whatever | |
| # WordPress and WooCommerce are current. Gating lane is untouched. | |
| if [ "$WP_STACK" = "latest" ]; then | |
| jq '.env.tests.core = null | |
| | .env.tests.plugins |= map(sub("/woocommerce\\.[0-9][0-9.]*\\.zip$"; "/woocommerce.zip"))' \ | |
| .wp-env.override.json > .wp-env.override.json.tmp | |
| mv .wp-env.override.json.tmp .wp-env.override.json | |
| echo "WP_STACK=latest — core and WooCommerce unpinned" | |
| fi | |
| # Route Docker Hub pulls through Google's public pull-through mirror. The wp-env images | |
| # (mariadb, phpmyadmin, wordpress, composer — all official library/* images) then come | |
| # from the mirror instead of registry-1.docker.io, dodging Docker Hub's anonymous | |
| # pull-rate-limit / connection timeouts that randomly fail one shard's `wp-env start`. | |
| # No credentials needed; Docker falls back to Docker Hub if the mirror misses an image. | |
| - name: Route Docker Hub pulls through a mirror (avoids anonymous rate-limit/timeouts) | |
| run: | | |
| # Merge into any existing daemon.json (jq is preinstalled on the runner) so we | |
| # never clobber the runner's own Docker config; create it only if absent. | |
| sudo mkdir -p /etc/docker | |
| if [ -s /etc/docker/daemon.json ]; then | |
| sudo jq '. + {"registry-mirrors": ["https://mirror.gcr.io"]}' /etc/docker/daemon.json | sudo tee /etc/docker/daemon.json.tmp >/dev/null | |
| sudo mv /etc/docker/daemon.json.tmp /etc/docker/daemon.json | |
| else | |
| echo '{ "registry-mirrors": ["https://mirror.gcr.io"] }' | sudo tee /etc/docker/daemon.json >/dev/null | |
| fi | |
| sudo systemctl restart docker | |
| # Wait for the daemon socket instead of a fixed sleep. | |
| for i in $(seq 1 15); do docker info >/dev/null 2>&1 && break; sleep 1; done | |
| # wp-env pulls four Docker Hub library images on every run: mariadb:lts and | |
| # phpmyadmin via compose, plus the wordpress / wordpress:cli build bases. | |
| # Anonymous Docker Hub pulls from shared runner IPs randomly time out even | |
| # with the gcr mirror configured (the daemon silently falls back to | |
| # registry-1.docker.io when the mirror misses), so pre-seed the images: | |
| # restore them from the actions cache, else pull from AWS's public mirror | |
| # of Docker official images and retag. Once the images exist locally, | |
| # compose/build skip the Docker Hub pull entirely. The ISO week in the | |
| # cache key stops the floating tags (lts, latest) from pinning forever. | |
| - name: Compute wp-env image list and cache key | |
| id: wp-env-images | |
| working-directory: tests/pw | |
| run: | | |
| PHP=$(jq -r '.env.tests.phpVersion // empty' .wp-env.override.json) | |
| if [ -z "$PHP" ]; then PHP=$(jq -r '.env.tests.phpVersion // empty' .wp-env.json); fi | |
| IMAGES="mariadb:lts phpmyadmin:latest wordpress${PHP:+:php$PHP} wordpress:cli${PHP:+-php$PHP}" | |
| echo "list=$IMAGES" >> "$GITHUB_OUTPUT" | |
| echo "key=wp-env-images-v1-php${PHP:-default}-$(date +%G-%V)" >> "$GITHUB_OUTPUT" | |
| echo "images: $IMAGES" | |
| - name: Restore wp-env docker images from cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/wp-env-images | |
| key: ${{ steps.wp-env-images.outputs.key }} | |
| - name: Pre-seed wp-env docker images | |
| run: | | |
| IMAGES="${{ steps.wp-env-images.outputs.list }}" | |
| TAR="$HOME/.cache/wp-env-images/images.tar" | |
| if [ -f "$TAR" ]; then | |
| docker load -i "$TAR" | |
| else | |
| mkdir -p "$(dirname "$TAR")" | |
| complete=true | |
| for img in $IMAGES; do | |
| pulled=false | |
| for attempt in 1 2 3; do | |
| if docker pull "public.ecr.aws/docker/library/$img"; then pulled=true; break; fi | |
| sleep 15 | |
| done | |
| if [ "$pulled" = true ]; then | |
| docker tag "public.ecr.aws/docker/library/$img" "$img" | |
| docker rmi "public.ecr.aws/docker/library/$img" >/dev/null | |
| else | |
| echo "::warning::could not pre-seed $img from the ECR mirror; wp-env will pull it from Docker Hub" | |
| complete=false | |
| fi | |
| done | |
| # Only cache a complete set so a partial failure is retried next run. | |
| if [ "$complete" = true ]; then docker save -o "$TAR" $IMAGES; fi | |
| fi | |
| docker image ls | |
| - name: Start WordPress Env | |
| id: wp-env | |
| uses: nick-fields/retry@v3 | |
| with: | |
| timeout_minutes: 6 | |
| max_attempts: 3 | |
| # Space the attempts out: Docker Hub's anonymous throttle window | |
| # outlives back-to-back retries, so waiting beats hammering. | |
| retry_wait_seconds: 90 | |
| retry_on: error | |
| command: | | |
| cd tests/pw | |
| npm run start:env | |
| # What actually got installed, not what we asked for. On the canary lane this is | |
| # the whole point of the run; on the gating lane it is the proof the pins held. | |
| - name: Record resolved WordPress / WooCommerce versions | |
| working-directory: tests/pw | |
| run: | | |
| # `--` is required: without it npm swallows wp-cli's own flags, so | |
| # `--field=version` never reaches wp-cli, `wp plugin get` prints its | |
| # full field/value table, no line starts with a digit, and the grep | |
| # below yields "unknown" for WooCommerce on every single run. | |
| ver() { npm run --silent wp-env -- run tests-cli "$@" 2>/dev/null | tr -d '\r' | grep -Eo '^[0-9]+\.[0-9.]+' | head -n1; } | |
| WP=$(ver wp core version) | |
| WC=$(ver wp plugin get woocommerce --field=version) | |
| echo "stack=$WP_STACK wordpress=${WP:-unknown} woocommerce=${WC:-unknown}" | |
| echo "- \`$WP_STACK\` lane — WordPress **${WP:-unknown}**, WooCommerce **${WC:-unknown}**" >> "$GITHUB_STEP_SUMMARY" | |
| # Persist for merge-reports, which runs on a clean runner with no | |
| # wp-env stack and so cannot resolve these itself. Rides along in the | |
| # existing tests/pw/playwright artifact — no new upload needed. | |
| mkdir -p playwright | |
| printf '{"stack":"%s","wordpress":"%s","woocommerce":"%s"}\n' \ | |
| "$WP_STACK" "${WP:-unknown}" "${WC:-unknown}" > playwright/resolved-versions.json | |
| - name: Activate WooCommerce premium plugins | |
| if: needs.build_pro.result == 'success' | |
| working-directory: tests/pw | |
| run: | | |
| npm run wp-env run tests-cli wp plugin activate \ | |
| woocommerce-bookings \ | |
| woocommerce-subscriptions \ | |
| woocommerce-product-addons \ | |
| woocommerce-simple-auctions \ | |
| woocommerce-pdf-invoices-packing-slips \ | |
| dokan-pro \ | |
| dokan-invoice | |
| - name: Enable Dokan New React UI (vendor dashboard + product editor) | |
| working-directory: tests/pw | |
| run: | | |
| npm run wp-env run tests-cli wp eval ' | |
| $appearance = get_option("dokan_appearance", []); | |
| $appearance["vendor_layout_style"] = "latest"; | |
| $appearance["vendor_product_editor"] = "latest"; | |
| update_option("dokan_appearance", $appearance); | |
| echo "Dokan appearance set to latest UI" . PHP_EOL; | |
| ' | |
| - name: Get installed Playwright version | |
| id: playwright-version | |
| working-directory: tests/pw | |
| run: | | |
| echo "PLAYWRIGHT_VERSION=$(npm ls @playwright/test --json | jq --raw-output '.dependencies["@playwright/test"].version')" >> "$GITHUB_ENV" | |
| - name: Cache playwright binaries | |
| id: playwright-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cache/ms-playwright | |
| key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} | |
| restore-keys: | | |
| ${{ runner.os }}-playwright- | |
| - name: Install Playwright browser binaries & OS dependencies | |
| id: pw-install | |
| if: steps.playwright-cache.outputs.cache-hit != 'true' | |
| working-directory: tests/pw | |
| run: | | |
| npx playwright install --with-deps chromium | |
| - name: Run docker setup | |
| working-directory: tests/pw | |
| run: npm run docker:setup | |
| - name: 🧪 Run api tests-${{ matrix.shardIndex }} | |
| id: api-test | |
| if: always() && steps.wp-env.outcome == 'success' && ( github.event_name != 'workflow_dispatch' || ( github.event_name == 'workflow_dispatch' && (github.event.inputs.testsuite == 'API' || github.event.inputs.testsuite == 'All'))) | |
| timeout-minutes: 15 | |
| working-directory: tests/pw | |
| run: | | |
| npm run test:api -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} | |
| - name: Backup Database | |
| if: always() && steps.wp-env.outcome == 'success' | |
| working-directory: tests/pw | |
| run: | | |
| npm run wp-env run tests-cli wp db export wp-data/db.sql | |
| # playwright-report/e2e is excluded: the docker:setup step runs the | |
| # site/auth/env setup projects through playwright.config.ts, which | |
| # writes setup-run blob/summary/spec-duration files under | |
| # playwright-report/e2e/. Shipping those in this artifact made the | |
| # merge-reports job count the api job's setup run as an extra e2e | |
| # shard (inflating e2e totals) and fed its blob into the html report. | |
| - name: Archive test artifacts (screenshots, HTML snapshots, Reports) | |
| if: always() && steps.debug-log.outcome == 'success' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-artifact-api | |
| path: | | |
| tests/pw/wp-data | |
| tests/pw/playwright | |
| tests/pw/playwright-report | |
| !tests/pw/playwright-report/e2e/** | |
| if-no-files-found: ignore | |
| retention-days: 1 | |
| merge-reports: | |
| # Merge reports after playwright-tests, even if some shards have failed | |
| if: ${{ !cancelled() }} | |
| needs: [e2e_tests, api_tests] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| # Override reports path specifically for this job | |
| env: | |
| SYSTEM_INFO: ./tests/pw/all-reports/test-artifact-api/playwright/systemInfo.json | |
| API_TEST_RESULT: ./tests/pw/all-reports/test-artifact-api/playwright-report/api/summary-report/results.json | |
| API_COVERAGE: ./tests/pw/all-reports/test-artifact-api/playwright-report/api/coverage-report/coverage.json | |
| E2E_TEST_RESULT: ./tests/pw/all-reports/merged-summary.json | |
| E2E_COVERAGE: ./tests/pw/all-reports/merged-coverage.json | |
| steps: | |
| - name: Checkout testing repo | |
| id: clone-dokan-lite | |
| uses: actions/checkout@v4 | |
| - name: Prepare dokan-lite folder | |
| run: | | |
| ln -s . dokan-lite | |
| - name: Checkout Dokan Pro | |
| if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: getdokan/dokan-pro | |
| ref: develop | |
| path: dokan-pro | |
| token: ${{ secrets.DOKAN_PRO_TOKEN }} | |
| - name: Use desired version of NodeJS | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| cache-dependency-path: tests/pw/package-lock.json | |
| - name: Cache test deps (tests/pw/node_modules) | |
| id: pw-nm | |
| uses: actions/cache@v4 | |
| with: | |
| path: tests/pw/node_modules | |
| key: pw-nm-${{ runner.os }}-${{ hashFiles('tests/pw/package-lock.json') }} | |
| - name: Install test dependencies | |
| if: steps.pw-nm.outputs.cache-hit != 'true' | |
| working-directory: tests/pw | |
| run: | | |
| npm ci --prefer-offline --no-audit --no-fund || npm i | |
| - name: Download all test artifacts from GitHub Actions | |
| id: download-test-artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: tests/pw/all-reports | |
| pattern: test-artifact-* | |
| # Runs before every other summary write in this job so the stack the suite | |
| # actually ran on is the first thing on the run page. The shards each record | |
| # their own line too; this is the consolidated one. Disagreement between | |
| # shards is worth surfacing rather than hiding — on the canary lane it means | |
| # WordPress or WooCommerce shipped a release mid-run. | |
| - name: Report resolved stack at the top of the summary | |
| if: always() && steps.download-test-artifacts.outcome == 'success' | |
| run: | | |
| FILES=$(find tests/pw/all-reports -name resolved-versions.json 2>/dev/null | sort) | |
| COUNT=$(printf '%s\n' "$FILES" | grep -c . || true) | |
| if [ "${COUNT:-0}" -eq 0 ]; then | |
| printf '> Resolved stack: **unavailable** — no job reported one.\n\n' >> "$GITHUB_STEP_SUMMARY" | |
| exit 0 | |
| fi | |
| COMBOS=$(printf '%s\n' "$FILES" | xargs jq -r '"\(.stack)|\(.wordpress)|\(.woocommerce)"' | sort -u) | |
| printf '%s\n' "$COMBOS" | while IFS='|' read -r stack wp wc; do | |
| printf '> **`%s` lane — WordPress %s, WooCommerce %s** _(%s jobs reporting)_\n' \ | |
| "$stack" "$wp" "$wc" "$COUNT" >> "$GITHUB_STEP_SUMMARY" | |
| done | |
| if [ "$(printf '%s\n' "$COMBOS" | wc -l)" -gt 1 ]; then | |
| printf '> \n> ⚠️ Jobs did **not** all run the same stack — results are not comparable across shards.\n' >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| printf '\n' >> "$GITHUB_STEP_SUMMARY" | |
| # download-artifact silently does nothing when no artifacts match | |
| # (e.g. all upstream test jobs were skipped after a build failure), | |
| # so the all-reports dir may not exist yet. Pre-create it so the | |
| # subsequent move/merge steps don't crash on a missing cwd. | |
| - name: Move all blob report folder contents into a single folder | |
| if: always() && steps.download-test-artifacts.outcome == 'success' | |
| run: | | |
| mkdir -p tests/pw/all-reports/all-blob-reports | |
| cd tests/pw/all-reports | |
| find . -type f -path "*/blob-report/*" -exec mv {} all-blob-reports/ \; | |
| - name: Generate HTML Report | |
| id: generate-html-report | |
| working-directory: tests/pw | |
| run: | | |
| npx playwright merge-reports --reporter html ./all-reports/all-blob-reports | |
| mv playwright-report ./all-reports/html-report | |
| - name: Clean all extracted blob reports (except ZIPs) | |
| working-directory: tests/pw | |
| run: | | |
| find all-reports/all-blob-reports -mindepth 1 ! -name "*.zip" -exec rm -rf {} + | |
| - name: Generate merged summary report | |
| id: merge-summary-report | |
| if: steps.download-test-artifacts.outcome == 'success' | |
| working-directory: tests/pw | |
| run: | | |
| REPORT_TYPE=e2e npx ts-node ./utils/mergeSummaryReport.ts | |
| # No coverage on PR runs (skipped per-shard above), so the merge | |
| # would exit 1 on missing inputs. Quality report handles missing | |
| # E2E_COVERAGE via an existsSync guard. | |
| - name: Generate merged coverage report | |
| id: merge-coverage-report | |
| if: | | |
| steps.download-test-artifacts.outcome == 'success' | |
| && github.event_name != 'pull_request' | |
| working-directory: tests/pw | |
| run: | | |
| REPORT_TYPE=e2e npx ts-node ./utils/mergeCoverageSummary.ts | |
| # Aggregate per-shard spec-durations.json files into a single | |
| # baseline. Uploaded as artifact so maintainers can periodically | |
| # commit it to tests/pw/utils/shard-durations.json to keep | |
| # balanced sharding accurate as the suite evolves. | |
| - name: Aggregate spec-duration baseline | |
| id: aggregate-baseline | |
| if: always() && steps.download-test-artifacts.outcome == 'success' | |
| working-directory: tests/pw | |
| run: | | |
| node ./utils/aggregateSpecDurations.js \ | |
| ./all-reports \ | |
| ./all-reports/shard-durations.json || true | |
| - name: Upload spec-duration baseline | |
| if: always() && steps.aggregate-baseline.outcome == 'success' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: shard-durations-baseline | |
| path: tests/pw/all-reports/shard-durations.json | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| - name: Prepare test summary | |
| id: prepare-test-summary | |
| if: always() && steps.merge-summary-report.outcome == 'success' | |
| uses: actions/github-script@v7 | |
| with: | |
| result-encoding: string | |
| script: | | |
| const script = require("./tests/pw/utils/gitTestSummary.ts") | |
| return await script({github, context, core}) | |
| # Render the branded HTML quality report (full design, downloadable | |
| # as artifact) plus a markdown variant for the GitHub job summary. | |
| # GitHub strips <style>/CSS from step summaries, so the HTML can't | |
| # render inline — the markdown uses shields.io badges, themed | |
| # Mermaid charts, and HTML tables that GH does render natively. | |
| # | |
| # NOTE: no `working-directory` here — the job-level env block sets | |
| # API_TEST_RESULT etc. as repo-root-relative paths, so the script | |
| # must run from the repo root for those to resolve. | |
| - name: Generate Quality Report | |
| id: quality-report | |
| if: always() && steps.download-test-artifacts.outcome == 'success' | |
| env: | |
| ARTIFACTS_DIR: ./tests/pw/all-reports | |
| OUTPUT_FILE: ./tests/pw/all-reports/quality-report.html | |
| SUMMARY_FILE: ./tests/pw/all-reports/quality-report.md | |
| run: | | |
| node ./tests/pw/utils/generateQualityReport.js | |
| - name: Upload Quality Report | |
| if: always() && steps.quality-report.outcome == 'success' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: quality-report | |
| path: tests/pw/all-reports/quality-report.html | |
| if-no-files-found: ignore | |
| retention-days: 30 | |
| - name: Append Quality Report to job summary | |
| if: always() && steps.quality-report.outcome == 'success' | |
| run: | | |
| if [ -f tests/pw/all-reports/quality-report.md ]; then | |
| cat tests/pw/all-reports/quality-report.md >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| # Consolidate per-shard feature manifests into one table so reviewers | |
| # see "which features ran in which shard" in a single place at the | |
| # top of the run page — without polluting each shard's job page. | |
| - name: Append shard feature map to job summary | |
| if: always() && steps.download-test-artifacts.outcome == 'success' | |
| working-directory: tests/pw | |
| run: | | |
| node -e ' | |
| const fs = require("fs"); | |
| const path = require("path"); | |
| const baseDir = "./all-reports"; | |
| if (!fs.existsSync(baseDir)) process.exit(0); | |
| const shards = []; | |
| for (const entry of fs.readdirSync(baseDir)) { | |
| const featuresPath = path.join(baseDir, entry, "playwright", "shard-features.json"); | |
| if (fs.existsSync(featuresPath)) { | |
| try { | |
| shards.push(JSON.parse(fs.readFileSync(featuresPath, "utf8"))); | |
| } catch (e) { /* skip malformed */ } | |
| } | |
| } | |
| if (shards.length === 0) process.exit(0); | |
| shards.sort((a, b) => a.shardIndex - b.shardIndex); | |
| const lines = [ | |
| "", | |
| "## Shard Feature Map", | |
| "", | |
| "| Shard | Specs | Features tested |", | |
| "|-------|-------|------------------|", | |
| ]; | |
| for (const s of shards) { | |
| lines.push(`| ${s.shardIndex}/${s.shardTotal} | ${s.specCount} | ${s.features.join(", ")} |`); | |
| } | |
| lines.push(""); | |
| fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, lines.join("\n")); | |
| ' | |
| - name: Upload final test artifact | |
| if: always() && steps.download-test-artifacts.outcome == 'success' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: final-test-artifact | |
| path: tests/pw/all-reports | |
| if-no-files-found: ignore | |
| retention-days: 7 |