|
| 1 | +'use strict'; |
| 2 | +const chai = require('chai'); |
| 3 | +const expect = chai.expect; |
| 4 | + |
| 5 | +// SDK-6463 regression test for the accessibility Cypress plugin's afterEach hook. |
| 6 | +// A hung/slow accessibility scan or results-save must NOT fail the afterEach hook, |
| 7 | +// because a failing afterEach makes Cypress skip all remaining tests in the spec |
| 8 | +// (they surface as "skipped"). The two cy.wrap(..., {timeout: 30000}) chains must |
| 9 | +// tolerate a timeout (catch + log) instead of letting it bubble up. |
| 10 | + |
| 11 | +const PLUGIN_PATH = require.resolve('../../../../../bin/accessibility-automation/cypress/index.js'); |
| 12 | +const WRAP_TIMEOUT_SIM_MS = 20; // stand-in for the real 30000ms so the test runs fast |
| 13 | + |
| 14 | +// chainable that mimics Cypress command chaining (.then unwraps nested chainables) |
| 15 | +function chain(promise) { |
| 16 | + return { |
| 17 | + _promise: promise, |
| 18 | + then(onF, onR) { |
| 19 | + return chain(promise.then( |
| 20 | + (v) => { const r = onF ? onF(v) : v; return (r && r._promise) ? r._promise : r; }, |
| 21 | + onR |
| 22 | + )); |
| 23 | + }, |
| 24 | + catch(onR) { return chain(promise.catch(onR)); }, |
| 25 | + performScan() { return this; }, |
| 26 | + performScanSubjectQuery() { return this; }, |
| 27 | + }; |
| 28 | +} |
| 29 | + |
| 30 | +// fake window. mode: 'hang' (scan never finishes), 'scanOnly' (scan ok, save hangs), 'ok' |
| 31 | +function makeWin(mode) { |
| 32 | + const listeners = {}; |
| 33 | + const echo = { A11Y_SCAN: 'A11Y_SCAN_FINISHED', A11Y_SAVE_RESULTS: 'A11Y_RESULTS_SAVED' }; |
| 34 | + return { |
| 35 | + location: { protocol: 'http:' }, |
| 36 | + document: { querySelector: () => ({ id: 'accessibility-automation-element' }) }, |
| 37 | + addEventListener(type, cb) { (listeners[type] = listeners[type] || []).push(cb); }, |
| 38 | + removeEventListener(type, cb) { listeners[type] = (listeners[type] || []).filter((f) => f !== cb); }, |
| 39 | + dispatchEvent(e) { |
| 40 | + const done = echo[e.type]; |
| 41 | + const shouldEcho = mode === 'ok' || (mode === 'scanOnly' && e.type === 'A11Y_SCAN'); |
| 42 | + if (shouldEcho && done) (listeners[done] || []).forEach((cb) => cb({ detail: {} })); |
| 43 | + return true; |
| 44 | + }, |
| 45 | + }; |
| 46 | +} |
| 47 | + |
| 48 | +describe('accessibility-automation/cypress afterEach (SDK-6463)', () => { |
| 49 | + let capturedAfterEach; |
| 50 | + let theWin; |
| 51 | + const unhandled = []; |
| 52 | + const onUnhandled = (reason) => unhandled.push(reason && reason.message ? reason.message : String(reason)); |
| 53 | + |
| 54 | + before(() => { |
| 55 | + process.on('unhandledRejection', onUnhandled); |
| 56 | + |
| 57 | + global.CustomEvent = class CustomEvent { constructor(type, init) { this.type = type; this.detail = init && init.detail; } }; |
| 58 | + global.window = { location: { protocol: 'http:' } }; |
| 59 | + global.Cypress = { |
| 60 | + env: (k) => ({ |
| 61 | + BROWSERSTACK_LOGS: false, |
| 62 | + IS_ACCESSIBILITY_EXTENSION_LOADED: 'true', |
| 63 | + ACCESSIBILITY_EXTENSION_PATH: '/some/ext/path', |
| 64 | + OS: 'win', |
| 65 | + })[k], |
| 66 | + browser: { isHeaded: true }, |
| 67 | + platform: 'linux', |
| 68 | + Commands: { add() {}, overwrite() {}, addQuery() {} }, |
| 69 | + on() {}, |
| 70 | + mocha: { getRunner: () => ({ suite: { ctx: { currentTest: { title: 'TC landing', invocationDetails: { relativeFile: 'src/e2e/landing.cy.ts' } } } } }) }, |
| 71 | + }; |
| 72 | + global.cy = { |
| 73 | + state: () => null, |
| 74 | + wrap: (value, opts) => { |
| 75 | + if (value && typeof value.then === 'function') { |
| 76 | + const realTimeout = (opts && opts.timeout) || 0; |
| 77 | + const waitMs = realTimeout ? Math.min(realTimeout, WRAP_TIMEOUT_SIM_MS) : WRAP_TIMEOUT_SIM_MS; |
| 78 | + const timed = new Promise((resolve, reject) => { |
| 79 | + let done = false; |
| 80 | + value.then((v) => { if (!done) { done = true; resolve(v); } }, (e) => { if (!done) { done = true; reject(e); } }); |
| 81 | + setTimeout(() => { if (!done) { done = true; reject(new Error(`cy.wrap() timed out waiting ${realTimeout}ms to complete.`)); } }, waitMs); |
| 82 | + }); |
| 83 | + return chain(timed); |
| 84 | + } |
| 85 | + return chain(Promise.resolve(value)); |
| 86 | + }, |
| 87 | + window: () => chain(Promise.resolve(theWin)), |
| 88 | + task: () => chain(Promise.resolve({ testRunUuid: 'uuid-123' })), |
| 89 | + on() {}, |
| 90 | + }; |
| 91 | + |
| 92 | + // Temporarily capture the plugin's global afterEach registration without |
| 93 | + // registering it as a real mocha hook, then restore mocha's own globals. |
| 94 | + const realAfterEach = global.afterEach; |
| 95 | + const realBefore = global.before; |
| 96 | + const realBeforeEach = global.beforeEach; |
| 97 | + global.afterEach = (fn) => { capturedAfterEach = fn; }; |
| 98 | + global.before = () => {}; |
| 99 | + global.beforeEach = () => {}; |
| 100 | + try { |
| 101 | + delete require.cache[PLUGIN_PATH]; |
| 102 | + require(PLUGIN_PATH); |
| 103 | + } finally { |
| 104 | + global.afterEach = realAfterEach; |
| 105 | + global.before = realBefore; |
| 106 | + global.beforeEach = realBeforeEach; |
| 107 | + } |
| 108 | + }); |
| 109 | + |
| 110 | + after(() => { |
| 111 | + process.removeListener('unhandledRejection', onUnhandled); |
| 112 | + delete global.Cypress; delete global.cy; delete global.window; delete global.CustomEvent; |
| 113 | + }); |
| 114 | + |
| 115 | + function runHook(mode) { |
| 116 | + unhandled.length = 0; |
| 117 | + theWin = makeWin(mode); |
| 118 | + capturedAfterEach(); // invoke the real hook callback (fire-and-forget, as Cypress does) |
| 119 | + return new Promise((r) => setTimeout(r, WRAP_TIMEOUT_SIM_MS + 100)).then(() => |
| 120 | + unhandled.filter((m) => /cy\.wrap\(\) timed out/.test(m))); |
| 121 | + } |
| 122 | + |
| 123 | + it('captures the real afterEach hook from the plugin', () => { |
| 124 | + expect(capturedAfterEach).to.be.a('function'); |
| 125 | + }); |
| 126 | + |
| 127 | + it('does not fail the hook when the accessibility scan never finishes', async () => { |
| 128 | + const timeouts = await runHook('hang'); |
| 129 | + expect(timeouts, 'an uncaught cy.wrap timeout would fail the hook and skip remaining tests').to.have.length(0); |
| 130 | + }); |
| 131 | + |
| 132 | + it('does not fail the hook when saving results never finishes', async () => { |
| 133 | + const timeouts = await runHook('scanOnly'); |
| 134 | + expect(timeouts).to.have.length(0); |
| 135 | + }); |
| 136 | + |
| 137 | + it('completes normally on the happy path', async () => { |
| 138 | + const timeouts = await runHook('ok'); |
| 139 | + expect(timeouts).to.have.length(0); |
| 140 | + }); |
| 141 | +}); |
0 commit comments