From b0a2580d4c8f9f6d69128f4c06392e803c7d98d6 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 12 Dec 2022 17:14:29 +0100 Subject: [PATCH] fix: improve a11y snapshot handling if the tree is not correct Bug #9404 --- log.txt | 66 +++++++++++++++++++ .../src/common/Accessibility.ts | 5 +- test/src/accessibility.spec.ts | 45 +++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 log.txt diff --git a/log.txt b/log.txt new file mode 100644 index 0000000000000..d1c32c34e36a4 --- /dev/null +++ b/log.txt @@ -0,0 +1,66 @@ +{"method":"Target.setDiscoverTargets","params":{"discover":true,"filter":[{"type":"tab","exclude":true},{}]},"id":1} +{"method":"Target.setAutoAttach","params":{"waitForDebuggerOnStart":true,"flatten":true,"autoAttach":true},"id":2} +{"method":"Target.targetCreated","params":{"targetInfo":{"targetId":"3865277997D34277C67FFCB9FD035F3F","type":"page","title":"about:blank","url":"about:blank","attached":false,"canAccessOpener":false,"browserContextId":"F75153BCAFA953F1C5665720E36C04F3"}}} +{"method":"Target.targetCreated","params":{"targetInfo":{"targetId":"a5ba6d70-6d8e-4904-a4a3-868ec82188e9","type":"browser","title":"","url":"","attached":true,"canAccessOpener":false}}} +{"id":1,"result":{}} +{"method":"Target.targetInfoChanged","params":{"targetInfo":{"targetId":"3865277997D34277C67FFCB9FD035F3F","type":"page","title":"about:blank","url":"about:blank","attached":true,"canAccessOpener":false,"browserContextId":"F75153BCAFA953F1C5665720E36C04F3"}}} +{"method":"Target.attachedToTarget","params":{"sessionId":"FEDDC46AC3D53C681F9B9564C21A9EC5","targetInfo":{"targetId":"3865277997D34277C67FFCB9FD035F3F","type":"page","title":"about:blank","url":"about:blank","attached":true,"canAccessOpener":false,"browserContextId":"F75153BCAFA953F1C5665720E36C04F3"},"waitingForDebugger":false}} +{"sessionId":"FEDDC46AC3D53C681F9B9564C21A9EC5","method":"Target.setAutoAttach","params":{"waitForDebuggerOnStart":true,"flatten":true,"autoAttach":true},"id":3} +{"sessionId":"FEDDC46AC3D53C681F9B9564C21A9EC5","method":"Runtime.runIfWaitingForDebugger","id":4} +{"id":2,"result":{}} +{"id":3,"result":{},"sessionId":"FEDDC46AC3D53C681F9B9564C21A9EC5"} +{"id":4,"result":{},"sessionId":"FEDDC46AC3D53C681F9B9564C21A9EC5"} +{"method":"Target.createBrowserContext","params":{},"id":5} +{"id":5,"result":{"browserContextId":"BF47612F1ED97F6BB3C0452DFBB8F47A"}} +{"method":"Target.createTarget","params":{"url":"about:blank","browserContextId":"BF47612F1ED97F6BB3C0452DFBB8F47A"},"id":6} +{"method":"Target.targetCreated","params":{"targetInfo":{"targetId":"48B09248C7A7DA714D3BFD65B3CE1DDC","type":"page","title":"","url":"about:blank","attached":false,"canAccessOpener":false,"browserContextId":"BF47612F1ED97F6BB3C0452DFBB8F47A"}}} +{"method":"Target.targetInfoChanged","params":{"targetInfo":{"targetId":"48B09248C7A7DA714D3BFD65B3CE1DDC","type":"page","title":"","url":"about:blank","attached":true,"canAccessOpener":false,"browserContextId":"BF47612F1ED97F6BB3C0452DFBB8F47A"}}} +{"method":"Target.attachedToTarget","params":{"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553","targetInfo":{"targetId":"48B09248C7A7DA714D3BFD65B3CE1DDC","type":"page","title":"","url":"about:blank","attached":true,"canAccessOpener":false,"browserContextId":"BF47612F1ED97F6BB3C0452DFBB8F47A"},"waitingForDebugger":true}} +{"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553","method":"Target.setAutoAttach","params":{"waitForDebuggerOnStart":true,"flatten":true,"autoAttach":true},"id":7} +{"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553","method":"Runtime.runIfWaitingForDebugger","id":8} +{"id":6,"result":{"targetId":"48B09248C7A7DA714D3BFD65B3CE1DDC"}} +{"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553","method":"Page.enable","id":9} +{"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553","method":"Page.getFrameTree","id":10} +{"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553","method":"Performance.enable","id":11} +{"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553","method":"Log.enable","id":12} +{"method":"Target.targetInfoChanged","params":{"targetInfo":{"targetId":"48B09248C7A7DA714D3BFD65B3CE1DDC","type":"page","title":"about:blank","url":"about:blank","attached":true,"canAccessOpener":false,"browserContextId":"BF47612F1ED97F6BB3C0452DFBB8F47A"}}} +{"id":7,"result":{},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"id":8,"result":{},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"id":9,"result":{},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"id":10,"result":{"frameTree":{"frame":{"id":"48B09248C7A7DA714D3BFD65B3CE1DDC","loaderId":"4C415EAEA62DED51CD60840C63E6A52E","url":"about:blank","domainAndRegistry":"","securityOrigin":"://","mimeType":"text/html","adFrameStatus":{"adFrameType":"none"},"secureContextType":"InsecureScheme","crossOriginIsolatedContextType":"NotIsolated","gatedAPIFeatures":[]}}},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"id":11,"result":{},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"id":12,"result":{},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553","method":"Page.setLifecycleEventsEnabled","params":{"enabled":true},"id":13} +{"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553","method":"Runtime.enable","id":14} +{"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553","method":"Network.enable","id":15} +{"method":"Page.lifecycleEvent","params":{"frameId":"48B09248C7A7DA714D3BFD65B3CE1DDC","loaderId":"4C415EAEA62DED51CD60840C63E6A52E","name":"commit","timestamp":443737.739752},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"method":"Page.lifecycleEvent","params":{"frameId":"48B09248C7A7DA714D3BFD65B3CE1DDC","loaderId":"4C415EAEA62DED51CD60840C63E6A52E","name":"DOMContentLoaded","timestamp":443737.739899},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"method":"Page.lifecycleEvent","params":{"frameId":"48B09248C7A7DA714D3BFD65B3CE1DDC","loaderId":"4C415EAEA62DED51CD60840C63E6A52E","name":"load","timestamp":443737.740936},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"method":"Page.lifecycleEvent","params":{"frameId":"48B09248C7A7DA714D3BFD65B3CE1DDC","loaderId":"4C415EAEA62DED51CD60840C63E6A52E","name":"networkAlmostIdle","timestamp":443737.741322},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"method":"Page.lifecycleEvent","params":{"frameId":"48B09248C7A7DA714D3BFD65B3CE1DDC","loaderId":"4C415EAEA62DED51CD60840C63E6A52E","name":"networkIdle","timestamp":443737.741322},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"id":13,"result":{},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"method":"Runtime.executionContextCreated","params":{"context":{"id":1,"origin":"://","name":"","uniqueId":"5879705773559576409.7620334582749855786","auxData":{"isDefault":true,"type":"default","frameId":"48B09248C7A7DA714D3BFD65B3CE1DDC"}}},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553","method":"Runtime.evaluate","params":{"expression":"(() => {\n const module = {};\n \"use strict\";\nvar __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __getOwnPropNames = Object.getOwnPropertyNames;\nvar __hasOwnProp = Object.prototype.hasOwnProperty;\nvar __export = (target, all) => {\n for (var name in all)\n __defProp(target, name, { get: all[name], enumerable: true });\n};\nvar __copyProps = (to, from, except, desc) => {\n if (from && typeof from === \"object\" || typeof from === \"function\") {\n for (let key of __getOwnPropNames(from))\n if (!__hasOwnProp.call(to, key) && key !== except)\n __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });\n }\n return to;\n};\nvar __toCommonJS = (mod) => __copyProps(__defProp({}, \"__esModule\", { value: true }), mod);\n\n// src/injected/injected.ts\nvar injected_exports = {};\n__export(injected_exports, {\n default: () => injected_default\n});\nmodule.exports = __toCommonJS(injected_exports);\n\n// src/common/Errors.ts\nvar CustomError = class extends Error {\n constructor(message) {\n super(message);\n this.name = this.constructor.name;\n Error.captureStackTrace(this, this.constructor);\n }\n};\nvar TimeoutError = class extends CustomError {\n};\nvar ProtocolError = class extends CustomError {\n #code;\n #originalMessage = \"\";\n set code(code) {\n this.#code = code;\n }\n get code() {\n return this.#code;\n }\n set originalMessage(originalMessage) {\n this.#originalMessage = originalMessage;\n }\n get originalMessage() {\n return this.#originalMessage;\n }\n};\nvar errors = Object.freeze({\n TimeoutError,\n ProtocolError\n});\n\n// src/util/DeferredPromise.ts\nfunction createDeferredPromise(opts) {\n let isResolved = false;\n let isRejected = false;\n let resolver;\n let rejector;\n const taskPromise = new Promise((resolve, reject) => {\n resolver = resolve;\n rejector = reject;\n });\n const timeoutId = opts && opts.timeout > 0 ? setTimeout(() => {\n isRejected = true;\n rejector(new TimeoutError(opts.message));\n }, opts.timeout) : void 0;\n return Object.assign(taskPromise, {\n resolved: () => {\n return isResolved;\n },\n finished: () => {\n return isResolved || isRejected;\n },\n resolve: (value) => {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n isResolved = true;\n resolver(value);\n },\n reject: (err) => {\n clearTimeout(timeoutId);\n isRejected = true;\n rejector(err);\n }\n });\n}\n\n// src/util/assert.ts\nvar assert = (value, message) => {\n if (!value) {\n throw new Error(message);\n }\n};\n\n// src/injected/Poller.ts\nvar MutationPoller = class {\n #fn;\n #root;\n #observer;\n #promise;\n constructor(fn, root) {\n this.#fn = fn;\n this.#root = root;\n }\n async start() {\n const promise = this.#promise = createDeferredPromise();\n const result = await this.#fn();\n if (result) {\n promise.resolve(result);\n return;\n }\n this.#observer = new MutationObserver(async () => {\n const result2 = await this.#fn();\n if (!result2) {\n return;\n }\n promise.resolve(result2);\n await this.stop();\n });\n this.#observer.observe(this.#root, {\n childList: true,\n subtree: true,\n attributes: true\n });\n }\n async stop() {\n assert(this.#promise, \"Polling never started.\");\n if (!this.#promise.finished()) {\n this.#promise.reject(new Error(\"Polling stopped\"));\n }\n if (this.#observer) {\n this.#observer.disconnect();\n this.#observer = void 0;\n }\n }\n result() {\n assert(this.#promise, \"Polling never started.\");\n return this.#promise;\n }\n};\nvar RAFPoller = class {\n #fn;\n #promise;\n constructor(fn) {\n this.#fn = fn;\n }\n async start() {\n const promise = this.#promise = createDeferredPromise();\n const result = await this.#fn();\n if (result) {\n promise.resolve(result);\n return;\n }\n const poll = async () => {\n if (promise.finished()) {\n return;\n }\n const result2 = await this.#fn();\n if (!result2) {\n window.requestAnimationFrame(poll);\n return;\n }\n promise.resolve(result2);\n await this.stop();\n };\n window.requestAnimationFrame(poll);\n }\n async stop() {\n assert(this.#promise, \"Polling never started.\");\n if (!this.#promise.finished()) {\n this.#promise.reject(new Error(\"Polling stopped\"));\n }\n }\n result() {\n assert(this.#promise, \"Polling never started.\");\n return this.#promise;\n }\n};\nvar IntervalPoller = class {\n #fn;\n #ms;\n #interval;\n #promise;\n constructor(fn, ms) {\n this.#fn = fn;\n this.#ms = ms;\n }\n async start() {\n const promise = this.#promise = createDeferredPromise();\n const result = await this.#fn();\n if (result) {\n promise.resolve(result);\n return;\n }\n this.#interval = setInterval(async () => {\n const result2 = await this.#fn();\n if (!result2) {\n return;\n }\n promise.resolve(result2);\n await this.stop();\n }, this.#ms);\n }\n async stop() {\n assert(this.#promise, \"Polling never started.\");\n if (!this.#promise.finished()) {\n this.#promise.reject(new Error(\"Polling stopped\"));\n }\n if (this.#interval) {\n clearInterval(this.#interval);\n this.#interval = void 0;\n }\n }\n result() {\n assert(this.#promise, \"Polling never started.\");\n return this.#promise;\n }\n};\n\n// src/injected/TextContent.ts\nvar TRIVIAL_VALUE_INPUT_TYPES = /* @__PURE__ */ new Set([\"checkbox\", \"image\", \"radio\"]);\nvar isNonTrivialValueNode = (node) => {\n if (node instanceof HTMLSelectElement) {\n return true;\n }\n if (node instanceof HTMLTextAreaElement) {\n return true;\n }\n if (node instanceof HTMLInputElement && !TRIVIAL_VALUE_INPUT_TYPES.has(node.type)) {\n return true;\n }\n return false;\n};\nvar UNSUITABLE_NODE_NAMES = /* @__PURE__ */ new Set([\"SCRIPT\", \"STYLE\"]);\nvar isSuitableNodeForTextMatching = (node) => {\n return !UNSUITABLE_NODE_NAMES.has(node.nodeName) && !document.head?.contains(node);\n};\nvar textContentCache = /* @__PURE__ */ new WeakMap();\nvar eraseFromCache = (node) => {\n while (node) {\n textContentCache.delete(node);\n if (node instanceof ShadowRoot) {\n node = node.host;\n } else {\n node = node.parentNode;\n }\n }\n};\nvar observedNodes = /* @__PURE__ */ new WeakSet();\nvar textChangeObserver = new MutationObserver((mutations) => {\n for (const mutation of mutations) {\n eraseFromCache(mutation.target);\n }\n});\nvar createTextContent = (root) => {\n let value = textContentCache.get(root);\n if (value) {\n return value;\n }\n value = { full: \"\", immediate: [] };\n if (!isSuitableNodeForTextMatching(root)) {\n return value;\n }\n let currentImmediate = \"\";\n if (isNonTrivialValueNode(root)) {\n value.full = root.value;\n value.immediate.push(root.value);\n root.addEventListener(\n \"input\",\n (event) => {\n eraseFromCache(event.target);\n },\n { once: true, capture: true }\n );\n } else {\n for (let child = root.firstChild; child; child = child.nextSibling) {\n if (child.nodeType === Node.TEXT_NODE) {\n value.full += child.nodeValue ?? \"\";\n currentImmediate += child.nodeValue ?? \"\";\n continue;\n }\n if (currentImmediate) {\n value.immediate.push(currentImmediate);\n }\n currentImmediate = \"\";\n if (child.nodeType === Node.ELEMENT_NODE) {\n value.full += createTextContent(child).full;\n }\n }\n if (currentImmediate) {\n value.immediate.push(currentImmediate);\n }\n if (root instanceof Element && root.shadowRoot) {\n value.full += createTextContent(root.shadowRoot).full;\n }\n if (!observedNodes.has(root)) {\n textChangeObserver.observe(root, {\n childList: true,\n characterData: true\n });\n observedNodes.add(root);\n }\n }\n textContentCache.set(root, value);\n return value;\n};\n\n// src/injected/TextQuerySelector.ts\nvar TextQuerySelector_exports = {};\n__export(TextQuerySelector_exports, {\n textQuerySelector: () => textQuerySelector,\n textQuerySelectorAll: () => textQuerySelectorAll\n});\nvar textQuerySelector = (root, selector) => {\n for (const node of root.childNodes) {\n if (node instanceof Element && isSuitableNodeForTextMatching(node)) {\n let matchedNode;\n if (node.shadowRoot) {\n matchedNode = textQuerySelector(node.shadowRoot, selector);\n } else {\n matchedNode = textQuerySelector(node, selector);\n }\n if (matchedNode) {\n return matchedNode;\n }\n }\n }\n if (root instanceof Element) {\n const textContent = createTextContent(root);\n if (textContent.full.includes(selector)) {\n return root;\n }\n }\n return null;\n};\nvar textQuerySelectorAll = (root, selector) => {\n let results = [];\n for (const node of root.childNodes) {\n if (node instanceof Element) {\n let matchedNodes;\n if (node.shadowRoot) {\n matchedNodes = textQuerySelectorAll(node.shadowRoot, selector);\n } else {\n matchedNodes = textQuerySelectorAll(node, selector);\n }\n results = results.concat(matchedNodes);\n }\n }\n if (results.length > 0) {\n return results;\n }\n if (root instanceof Element) {\n const textContent = createTextContent(root);\n if (textContent.full.includes(selector)) {\n return [root];\n }\n }\n return [];\n};\n\n// src/injected/XPathQuerySelector.ts\nvar XPathQuerySelector_exports = {};\n__export(XPathQuerySelector_exports, {\n xpathQuerySelector: () => xpathQuerySelector,\n xpathQuerySelectorAll: () => xpathQuerySelectorAll\n});\nvar xpathQuerySelector = (root, selector) => {\n const doc = root.ownerDocument || document;\n const result = doc.evaluate(\n selector,\n root,\n null,\n XPathResult.FIRST_ORDERED_NODE_TYPE\n );\n return result.singleNodeValue;\n};\nvar xpathQuerySelectorAll = (root, selector) => {\n const doc = root.ownerDocument || document;\n const iterator = doc.evaluate(\n selector,\n root,\n null,\n XPathResult.ORDERED_NODE_ITERATOR_TYPE\n );\n const array = [];\n let item;\n while (item = iterator.iterateNext()) {\n array.push(item);\n }\n return array;\n};\n\n// src/injected/PierceQuerySelector.ts\nvar PierceQuerySelector_exports = {};\n__export(PierceQuerySelector_exports, {\n pierceQuerySelector: () => pierceQuerySelector,\n pierceQuerySelectorAll: () => pierceQuerySelectorAll\n});\nvar pierceQuerySelector = (root, selector) => {\n let found = null;\n const search = (root2) => {\n const iter = document.createTreeWalker(root2, NodeFilter.SHOW_ELEMENT);\n do {\n const currentNode = iter.currentNode;\n if (currentNode.shadowRoot) {\n search(currentNode.shadowRoot);\n }\n if (currentNode instanceof ShadowRoot) {\n continue;\n }\n if (currentNode !== root2 && !found && currentNode.matches(selector)) {\n found = currentNode;\n }\n } while (!found && iter.nextNode());\n };\n if (root instanceof Document) {\n root = root.documentElement;\n }\n search(root);\n return found;\n};\nvar pierceQuerySelectorAll = (element, selector) => {\n const result = [];\n const collect = (root) => {\n const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);\n do {\n const currentNode = iter.currentNode;\n if (currentNode.shadowRoot) {\n collect(currentNode.shadowRoot);\n }\n if (currentNode instanceof ShadowRoot) {\n continue;\n }\n if (currentNode !== root && currentNode.matches(selector)) {\n result.push(currentNode);\n }\n } while (iter.nextNode());\n };\n if (element instanceof Document) {\n element = element.documentElement;\n }\n collect(element);\n return result;\n};\n\n// src/injected/util.ts\nvar util_exports = {};\n__export(util_exports, {\n checkVisibility: () => checkVisibility,\n createFunction: () => createFunction\n});\nvar createdFunctions = /* @__PURE__ */ new Map();\nvar createFunction = (functionValue) => {\n let fn = createdFunctions.get(functionValue);\n if (fn) {\n return fn;\n }\n fn = new Function(`return ${functionValue}`)();\n createdFunctions.set(functionValue, fn);\n return fn;\n};\nvar HIDDEN_VISIBILITY_VALUES = [\"hidden\", \"collapse\"];\nvar checkVisibility = (node, visible) => {\n if (!node) {\n return visible === false;\n }\n if (visible === void 0) {\n return node;\n }\n const element = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;\n const style = window.getComputedStyle(element);\n const isVisible = style && !HIDDEN_VISIBILITY_VALUES.includes(style.visibility) && !isBoundingBoxEmpty(element);\n return visible === isVisible ? node : false;\n};\nfunction isBoundingBoxEmpty(element) {\n const rect = element.getBoundingClientRect();\n return rect.width === 0 || rect.height === 0;\n}\n\n// src/injected/injected.ts\nvar PuppeteerUtil = Object.freeze({\n ...util_exports,\n ...TextQuerySelector_exports,\n ...XPathQuerySelector_exports,\n ...PierceQuerySelector_exports,\n createDeferredPromise,\n createTextContent,\n IntervalPoller,\n isSuitableNodeForTextMatching,\n MutationPoller,\n RAFPoller\n});\nvar injected_default = PuppeteerUtil;\n\n return module.exports.default;\n })()\n//# sourceURL=pptr://__puppeteer_evaluation_script__","contextId":1,"returnByValue":false,"awaitPromise":true,"userGesture":true},"id":16} +{"id":14,"result":{},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"id":15,"result":{},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553","method":"Page.addScriptToEvaluateOnNewDocument","params":{"source":"//# sourceURL=pptr://__puppeteer_evaluation_script__","worldName":"__puppeteer_utility_world__"},"id":17} +{"id":16,"result":{"result":{"type":"object","className":"Object","description":"Object","objectId":"-5745115842279068394.1.1"}},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"id":17,"result":{"identifier":"1"},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553","method":"Page.createIsolatedWorld","params":{"frameId":"48B09248C7A7DA714D3BFD65B3CE1DDC","worldName":"__puppeteer_utility_world__","grantUniveralAccess":true},"id":18} +{"method":"Runtime.executionContextCreated","params":{"context":{"id":2,"origin":"","name":"__puppeteer_utility_world__","uniqueId":"-2592145237230597014.1919684880438813548","auxData":{"isDefault":false,"type":"isolated","frameId":"48B09248C7A7DA714D3BFD65B3CE1DDC"}}},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553","method":"Runtime.evaluate","params":{"expression":"(() => {\n const module = {};\n \"use strict\";\nvar __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __getOwnPropNames = Object.getOwnPropertyNames;\nvar __hasOwnProp = Object.prototype.hasOwnProperty;\nvar __export = (target, all) => {\n for (var name in all)\n __defProp(target, name, { get: all[name], enumerable: true });\n};\nvar __copyProps = (to, from, except, desc) => {\n if (from && typeof from === \"object\" || typeof from === \"function\") {\n for (let key of __getOwnPropNames(from))\n if (!__hasOwnProp.call(to, key) && key !== except)\n __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });\n }\n return to;\n};\nvar __toCommonJS = (mod) => __copyProps(__defProp({}, \"__esModule\", { value: true }), mod);\n\n// src/injected/injected.ts\nvar injected_exports = {};\n__export(injected_exports, {\n default: () => injected_default\n});\nmodule.exports = __toCommonJS(injected_exports);\n\n// src/common/Errors.ts\nvar CustomError = class extends Error {\n constructor(message) {\n super(message);\n this.name = this.constructor.name;\n Error.captureStackTrace(this, this.constructor);\n }\n};\nvar TimeoutError = class extends CustomError {\n};\nvar ProtocolError = class extends CustomError {\n #code;\n #originalMessage = \"\";\n set code(code) {\n this.#code = code;\n }\n get code() {\n return this.#code;\n }\n set originalMessage(originalMessage) {\n this.#originalMessage = originalMessage;\n }\n get originalMessage() {\n return this.#originalMessage;\n }\n};\nvar errors = Object.freeze({\n TimeoutError,\n ProtocolError\n});\n\n// src/util/DeferredPromise.ts\nfunction createDeferredPromise(opts) {\n let isResolved = false;\n let isRejected = false;\n let resolver;\n let rejector;\n const taskPromise = new Promise((resolve, reject) => {\n resolver = resolve;\n rejector = reject;\n });\n const timeoutId = opts && opts.timeout > 0 ? setTimeout(() => {\n isRejected = true;\n rejector(new TimeoutError(opts.message));\n }, opts.timeout) : void 0;\n return Object.assign(taskPromise, {\n resolved: () => {\n return isResolved;\n },\n finished: () => {\n return isResolved || isRejected;\n },\n resolve: (value) => {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n isResolved = true;\n resolver(value);\n },\n reject: (err) => {\n clearTimeout(timeoutId);\n isRejected = true;\n rejector(err);\n }\n });\n}\n\n// src/util/assert.ts\nvar assert = (value, message) => {\n if (!value) {\n throw new Error(message);\n }\n};\n\n// src/injected/Poller.ts\nvar MutationPoller = class {\n #fn;\n #root;\n #observer;\n #promise;\n constructor(fn, root) {\n this.#fn = fn;\n this.#root = root;\n }\n async start() {\n const promise = this.#promise = createDeferredPromise();\n const result = await this.#fn();\n if (result) {\n promise.resolve(result);\n return;\n }\n this.#observer = new MutationObserver(async () => {\n const result2 = await this.#fn();\n if (!result2) {\n return;\n }\n promise.resolve(result2);\n await this.stop();\n });\n this.#observer.observe(this.#root, {\n childList: true,\n subtree: true,\n attributes: true\n });\n }\n async stop() {\n assert(this.#promise, \"Polling never started.\");\n if (!this.#promise.finished()) {\n this.#promise.reject(new Error(\"Polling stopped\"));\n }\n if (this.#observer) {\n this.#observer.disconnect();\n this.#observer = void 0;\n }\n }\n result() {\n assert(this.#promise, \"Polling never started.\");\n return this.#promise;\n }\n};\nvar RAFPoller = class {\n #fn;\n #promise;\n constructor(fn) {\n this.#fn = fn;\n }\n async start() {\n const promise = this.#promise = createDeferredPromise();\n const result = await this.#fn();\n if (result) {\n promise.resolve(result);\n return;\n }\n const poll = async () => {\n if (promise.finished()) {\n return;\n }\n const result2 = await this.#fn();\n if (!result2) {\n window.requestAnimationFrame(poll);\n return;\n }\n promise.resolve(result2);\n await this.stop();\n };\n window.requestAnimationFrame(poll);\n }\n async stop() {\n assert(this.#promise, \"Polling never started.\");\n if (!this.#promise.finished()) {\n this.#promise.reject(new Error(\"Polling stopped\"));\n }\n }\n result() {\n assert(this.#promise, \"Polling never started.\");\n return this.#promise;\n }\n};\nvar IntervalPoller = class {\n #fn;\n #ms;\n #interval;\n #promise;\n constructor(fn, ms) {\n this.#fn = fn;\n this.#ms = ms;\n }\n async start() {\n const promise = this.#promise = createDeferredPromise();\n const result = await this.#fn();\n if (result) {\n promise.resolve(result);\n return;\n }\n this.#interval = setInterval(async () => {\n const result2 = await this.#fn();\n if (!result2) {\n return;\n }\n promise.resolve(result2);\n await this.stop();\n }, this.#ms);\n }\n async stop() {\n assert(this.#promise, \"Polling never started.\");\n if (!this.#promise.finished()) {\n this.#promise.reject(new Error(\"Polling stopped\"));\n }\n if (this.#interval) {\n clearInterval(this.#interval);\n this.#interval = void 0;\n }\n }\n result() {\n assert(this.#promise, \"Polling never started.\");\n return this.#promise;\n }\n};\n\n// src/injected/TextContent.ts\nvar TRIVIAL_VALUE_INPUT_TYPES = /* @__PURE__ */ new Set([\"checkbox\", \"image\", \"radio\"]);\nvar isNonTrivialValueNode = (node) => {\n if (node instanceof HTMLSelectElement) {\n return true;\n }\n if (node instanceof HTMLTextAreaElement) {\n return true;\n }\n if (node instanceof HTMLInputElement && !TRIVIAL_VALUE_INPUT_TYPES.has(node.type)) {\n return true;\n }\n return false;\n};\nvar UNSUITABLE_NODE_NAMES = /* @__PURE__ */ new Set([\"SCRIPT\", \"STYLE\"]);\nvar isSuitableNodeForTextMatching = (node) => {\n return !UNSUITABLE_NODE_NAMES.has(node.nodeName) && !document.head?.contains(node);\n};\nvar textContentCache = /* @__PURE__ */ new WeakMap();\nvar eraseFromCache = (node) => {\n while (node) {\n textContentCache.delete(node);\n if (node instanceof ShadowRoot) {\n node = node.host;\n } else {\n node = node.parentNode;\n }\n }\n};\nvar observedNodes = /* @__PURE__ */ new WeakSet();\nvar textChangeObserver = new MutationObserver((mutations) => {\n for (const mutation of mutations) {\n eraseFromCache(mutation.target);\n }\n});\nvar createTextContent = (root) => {\n let value = textContentCache.get(root);\n if (value) {\n return value;\n }\n value = { full: \"\", immediate: [] };\n if (!isSuitableNodeForTextMatching(root)) {\n return value;\n }\n let currentImmediate = \"\";\n if (isNonTrivialValueNode(root)) {\n value.full = root.value;\n value.immediate.push(root.value);\n root.addEventListener(\n \"input\",\n (event) => {\n eraseFromCache(event.target);\n },\n { once: true, capture: true }\n );\n } else {\n for (let child = root.firstChild; child; child = child.nextSibling) {\n if (child.nodeType === Node.TEXT_NODE) {\n value.full += child.nodeValue ?? \"\";\n currentImmediate += child.nodeValue ?? \"\";\n continue;\n }\n if (currentImmediate) {\n value.immediate.push(currentImmediate);\n }\n currentImmediate = \"\";\n if (child.nodeType === Node.ELEMENT_NODE) {\n value.full += createTextContent(child).full;\n }\n }\n if (currentImmediate) {\n value.immediate.push(currentImmediate);\n }\n if (root instanceof Element && root.shadowRoot) {\n value.full += createTextContent(root.shadowRoot).full;\n }\n if (!observedNodes.has(root)) {\n textChangeObserver.observe(root, {\n childList: true,\n characterData: true\n });\n observedNodes.add(root);\n }\n }\n textContentCache.set(root, value);\n return value;\n};\n\n// src/injected/TextQuerySelector.ts\nvar TextQuerySelector_exports = {};\n__export(TextQuerySelector_exports, {\n textQuerySelector: () => textQuerySelector,\n textQuerySelectorAll: () => textQuerySelectorAll\n});\nvar textQuerySelector = (root, selector) => {\n for (const node of root.childNodes) {\n if (node instanceof Element && isSuitableNodeForTextMatching(node)) {\n let matchedNode;\n if (node.shadowRoot) {\n matchedNode = textQuerySelector(node.shadowRoot, selector);\n } else {\n matchedNode = textQuerySelector(node, selector);\n }\n if (matchedNode) {\n return matchedNode;\n }\n }\n }\n if (root instanceof Element) {\n const textContent = createTextContent(root);\n if (textContent.full.includes(selector)) {\n return root;\n }\n }\n return null;\n};\nvar textQuerySelectorAll = (root, selector) => {\n let results = [];\n for (const node of root.childNodes) {\n if (node instanceof Element) {\n let matchedNodes;\n if (node.shadowRoot) {\n matchedNodes = textQuerySelectorAll(node.shadowRoot, selector);\n } else {\n matchedNodes = textQuerySelectorAll(node, selector);\n }\n results = results.concat(matchedNodes);\n }\n }\n if (results.length > 0) {\n return results;\n }\n if (root instanceof Element) {\n const textContent = createTextContent(root);\n if (textContent.full.includes(selector)) {\n return [root];\n }\n }\n return [];\n};\n\n// src/injected/XPathQuerySelector.ts\nvar XPathQuerySelector_exports = {};\n__export(XPathQuerySelector_exports, {\n xpathQuerySelector: () => xpathQuerySelector,\n xpathQuerySelectorAll: () => xpathQuerySelectorAll\n});\nvar xpathQuerySelector = (root, selector) => {\n const doc = root.ownerDocument || document;\n const result = doc.evaluate(\n selector,\n root,\n null,\n XPathResult.FIRST_ORDERED_NODE_TYPE\n );\n return result.singleNodeValue;\n};\nvar xpathQuerySelectorAll = (root, selector) => {\n const doc = root.ownerDocument || document;\n const iterator = doc.evaluate(\n selector,\n root,\n null,\n XPathResult.ORDERED_NODE_ITERATOR_TYPE\n );\n const array = [];\n let item;\n while (item = iterator.iterateNext()) {\n array.push(item);\n }\n return array;\n};\n\n// src/injected/PierceQuerySelector.ts\nvar PierceQuerySelector_exports = {};\n__export(PierceQuerySelector_exports, {\n pierceQuerySelector: () => pierceQuerySelector,\n pierceQuerySelectorAll: () => pierceQuerySelectorAll\n});\nvar pierceQuerySelector = (root, selector) => {\n let found = null;\n const search = (root2) => {\n const iter = document.createTreeWalker(root2, NodeFilter.SHOW_ELEMENT);\n do {\n const currentNode = iter.currentNode;\n if (currentNode.shadowRoot) {\n search(currentNode.shadowRoot);\n }\n if (currentNode instanceof ShadowRoot) {\n continue;\n }\n if (currentNode !== root2 && !found && currentNode.matches(selector)) {\n found = currentNode;\n }\n } while (!found && iter.nextNode());\n };\n if (root instanceof Document) {\n root = root.documentElement;\n }\n search(root);\n return found;\n};\nvar pierceQuerySelectorAll = (element, selector) => {\n const result = [];\n const collect = (root) => {\n const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);\n do {\n const currentNode = iter.currentNode;\n if (currentNode.shadowRoot) {\n collect(currentNode.shadowRoot);\n }\n if (currentNode instanceof ShadowRoot) {\n continue;\n }\n if (currentNode !== root && currentNode.matches(selector)) {\n result.push(currentNode);\n }\n } while (iter.nextNode());\n };\n if (element instanceof Document) {\n element = element.documentElement;\n }\n collect(element);\n return result;\n};\n\n// src/injected/util.ts\nvar util_exports = {};\n__export(util_exports, {\n checkVisibility: () => checkVisibility,\n createFunction: () => createFunction\n});\nvar createdFunctions = /* @__PURE__ */ new Map();\nvar createFunction = (functionValue) => {\n let fn = createdFunctions.get(functionValue);\n if (fn) {\n return fn;\n }\n fn = new Function(`return ${functionValue}`)();\n createdFunctions.set(functionValue, fn);\n return fn;\n};\nvar HIDDEN_VISIBILITY_VALUES = [\"hidden\", \"collapse\"];\nvar checkVisibility = (node, visible) => {\n if (!node) {\n return visible === false;\n }\n if (visible === void 0) {\n return node;\n }\n const element = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;\n const style = window.getComputedStyle(element);\n const isVisible = style && !HIDDEN_VISIBILITY_VALUES.includes(style.visibility) && !isBoundingBoxEmpty(element);\n return visible === isVisible ? node : false;\n};\nfunction isBoundingBoxEmpty(element) {\n const rect = element.getBoundingClientRect();\n return rect.width === 0 || rect.height === 0;\n}\n\n// src/injected/injected.ts\nvar PuppeteerUtil = Object.freeze({\n ...util_exports,\n ...TextQuerySelector_exports,\n ...XPathQuerySelector_exports,\n ...PierceQuerySelector_exports,\n createDeferredPromise,\n createTextContent,\n IntervalPoller,\n isSuitableNodeForTextMatching,\n MutationPoller,\n RAFPoller\n});\nvar injected_default = PuppeteerUtil;\n\n return module.exports.default;\n })()\n//# sourceURL=pptr://__puppeteer_evaluation_script__","contextId":2,"returnByValue":false,"awaitPromise":true,"userGesture":true},"id":19} +{"id":18,"result":{"executionContextId":2},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553","method":"Emulation.setDeviceMetricsOverride","params":{"mobile":false,"width":800,"height":600,"deviceScaleFactor":1,"screenOrientation":{"angle":0,"type":"portraitPrimary"}},"id":20} +{"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553","method":"Emulation.setTouchEmulationEnabled","params":{"enabled":false},"id":21} +{"id":19,"result":{"result":{"type":"object","className":"Object","description":"Object","objectId":"-5745115842279068394.2.1"}},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"method":"Page.frameResized","params":{},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"id":20,"result":{},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"id":21,"result":{},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553","method":"Runtime.callFunctionOn","params":{"functionDeclaration":"() => {\n return 7 * 3;\n }\n//# sourceURL=pptr://__puppeteer_evaluation_script__\n","executionContextId":1,"arguments":[],"returnByValue":true,"awaitPromise":true,"userGesture":true},"id":22} +{"id":22,"result":{"result":{"type":"number","value":21,"description":"21"}},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"method":"Target.disposeBrowserContext","params":{"browserContextId":"BF47612F1ED97F6BB3C0452DFBB8F47A"},"id":23} +{"method":"Inspector.detached","params":{"reason":"Render process gone."},"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553"} +{"method":"Target.targetInfoChanged","params":{"targetInfo":{"targetId":"48B09248C7A7DA714D3BFD65B3CE1DDC","type":"other","title":"about:blank","url":"about:blank","attached":false,"canAccessOpener":false,"browserContextId":"BF47612F1ED97F6BB3C0452DFBB8F47A"}}} +{"method":"Target.detachedFromTarget","params":{"sessionId":"01E0DB1AC4FC7B2B49D5E9ED35AFF553","targetId":"48B09248C7A7DA714D3BFD65B3CE1DDC"}} +{"method":"Target.targetDestroyed","params":{"targetId":"48B09248C7A7DA714D3BFD65B3CE1DDC"}} +{"id":23,"result":{}} diff --git a/packages/puppeteer-core/src/common/Accessibility.ts b/packages/puppeteer-core/src/common/Accessibility.ts index ea540c01d799d..a2c4846206d23 100644 --- a/packages/puppeteer-core/src/common/Accessibility.ts +++ b/packages/puppeteer-core/src/common/Accessibility.ts @@ -564,7 +564,10 @@ class AXNode { } for (const node of nodeById.values()) { for (const childId of node.payload.childIds || []) { - node.children.push(nodeById.get(childId)!); + const child = nodeById.get(childId); + if (child) { + node.children.push(child); + } } } return nodeById.values().next().value; diff --git a/test/src/accessibility.spec.ts b/test/src/accessibility.spec.ts index 3f4ec0163fee5..277fea86a0fb7 100644 --- a/test/src/accessibility.spec.ts +++ b/test/src/accessibility.spec.ts @@ -163,6 +163,51 @@ describe('Accessibility', function () { ) ).toEqual(golden); }); + it('get snapshots while the tree is re-calculated', async () => { + // see https://github.com/puppeteer/puppeteer/issues/9404 + const {page} = getTestState(); + + await page.setContent( + ` + + + + + + Accessible name + aria-expanded puppeteer bug + + + + +

Some content

+ + + ` + ); + async function getAccessibleName(page: any, element: any) { + return (await page.accessibility.snapshot({root: element})).name; + } + const button = await page.$('button'); + expect(await getAccessibleName(page, button)).toEqual('Show'); + await button?.click(); + await page.waitForSelector('aria/Hide'); + }); it('roledescription', async () => { const {page} = getTestState();