Skip to content

Commit b8c7ead

Browse files
fix(axe): the axe runner could never resolve @playwright/test (#338)
The runner is executed by absolute path from `${GITHUB_WORKSPACE}/gates/` while the packages it needs are installed into `server/apps/<app>/node_modules`. CommonJS resolves a module's own `require` calls by walking up from the directory of the ENTRY FILE, not from the process cwd, and those two paths are siblings — so the walk never reaches the app's node_modules and `require('@playwright/test')` throws MODULE_NOT_FOUND on the first repo that ever opted in. That is docudesk#423: `enable-axe: true`, Playwright green, 82 e2e tests passed, and then the axe step died on `Cannot find module '@playwright/test'` — a failure with nothing to do with accessibility. The step above it printed `@axe-core/playwright resolved.` on the same run. It used `node -e`, which resolves from the cwd the step had just `cd`-ed into, so it answered a question nobody asked: it measured whether the package was on disk, never whether the runner could reach it. It would have printed the same line on every future break of this kind. Two changes, one root cause: * `NODE_PATH="${PWD}/node_modules"` on the runner invocation. NODE_PATH is a global fallback consulted regardless of where the entry file sits, which is exactly the property the cross-tree layout needs. * the control now resolves through `createRequire` anchored ON THE RUNNER, and checks both `@axe-core/playwright` and `@playwright/test`. It therefore fails precisely when the runner would. Measured on a reconstruction of the runner's real layout, with the actual `axe-run.cjs`: runner, no NODE_PATH -> Error: Cannot find module '@playwright/test' runner, NODE_PATH set -> ::error::axe-runner: AXE_BASE_URL is empty. old control -> PASS in both arms (this is the defect) new probe, no NODE_PATH-> exit 1, MODULE_NOT_FOUND new probe, NODE_PATH -> PASS The second line is the proof: the runner gets past resolution and reaches its own env validation. The third is why this went unseen. Both `run:` blocks stay far under the ~20 KB per-step limit that made this workflow unresolvable fleet-wide (2.1 KB and 2.6 KB). Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
1 parent 78d882a commit b8c7ead

1 file changed

Lines changed: 46 additions & 1 deletion

File tree

.github/workflows/quality.yml

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2888,7 +2888,45 @@ jobs:
28882888
# --no-save: this checkout is the app's own tree, and the report is
28892889
# the artefact we want out of it — not a mutated package.json.
28902890
npm install --no-save --no-audit --no-fund "@axe-core/playwright@${{ inputs.axe-version }}"
2891-
node -e "const m=require('@axe-core/playwright'); if (typeof (m.AxeBuilder||m.default)!=='function') { console.error('::error::@axe-core/playwright installed but exports no AxeBuilder constructor.'); process.exit(1);} console.log('@axe-core/playwright resolved.');"
2891+
2892+
# ── this control used to pass for the wrong reason ─────────────────
2893+
#
2894+
# The runner is executed BY ABSOLUTE PATH from OUTSIDE this package
2895+
# tree (`${GITHUB_WORKSPACE}/gates/...`, see the next step), and
2896+
# CommonJS resolves a module's own `require` calls by walking up from
2897+
# the DIRECTORY OF THE FILE — never from the process cwd. `gates/` is
2898+
# a sibling of `server/`, so that walk never reaches
2899+
# `server/apps/<app>/node_modules` and nothing the step above
2900+
# installed is reachable.
2901+
#
2902+
# `node -e`, which is what this control was, resolves from the cwd —
2903+
# here `server/apps/<app>` — so it found the package the runner could
2904+
# not and printed success. docudesk#423 died one step later on
2905+
# `Cannot find module '@playwright/test'` with this control green
2906+
# directly above it.
2907+
#
2908+
# NODE_PATH is a global fallback consulted whatever directory the
2909+
# entry file sits in, so it is what makes the cross-tree invocation
2910+
# resolve at all. The probe then asks the question through
2911+
# `createRequire` ANCHORED ON THE RUNNER, so it resolves exactly the
2912+
# way the runner does and fails exactly when the runner would.
2913+
export NODE_PATH="${PWD}/node_modules"
2914+
RUNNER="${GITHUB_WORKSPACE}/gates/hydra-gates/scripts/axe-run.cjs" \
2915+
node -e "
2916+
const { createRequire } = require('module');
2917+
const req = createRequire(process.env.RUNNER);
2918+
const m = req('@axe-core/playwright');
2919+
if (typeof (m.AxeBuilder || m.default) !== 'function') {
2920+
console.error('::error::@axe-core/playwright installed but exports no AxeBuilder constructor.');
2921+
process.exit(1);
2922+
}
2923+
const pw = req('@playwright/test');
2924+
if (!pw.chromium) {
2925+
console.error('::error::@playwright/test resolved from the axe runner but exports no chromium.');
2926+
process.exit(1);
2927+
}
2928+
console.log('axe runner resolves @playwright/test and @axe-core/playwright.');
2929+
"
28922930
28932931
- name: Run axe-core against the app routes
28942932
id: axe
@@ -2942,6 +2980,13 @@ jobs:
29422980
# `bash -e` would abort on a failing node before `RC=$?` ever ran, so
29432981
# the cleanup would be skipped and the reason lost. Same `&& / ||`
29442982
# idiom the hydra-gates job uses for the same reason.
2983+
# NODE_PATH, because the entry file is in `gates/` and the packages
2984+
# are in `server/apps/<app>/node_modules`. CommonJS resolves from the
2985+
# entry file's own directory upwards, and those two paths are
2986+
# siblings — without this the runner cannot require `@playwright/test`
2987+
# no matter what the install step put on disk. See the probe in that
2988+
# step for the measurement.
2989+
NODE_PATH="${PWD}/node_modules" \
29452990
node "${GITHUB_WORKSPACE}/gates/hydra-gates/scripts/axe-run.cjs" && RC=0 || RC=$?
29462991
exit "$RC"
29472992

0 commit comments

Comments
 (0)