From 7a7a28f9d6f414f363345d748439de064219330a Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Tue, 12 Mar 2024 12:29:55 -0400 Subject: [PATCH] feat: Support shadow DOM recursive selectors inside cypress snapshots sent to protocol (#28823) * chore: support new protocol structure for shadow DOM elements * chore: capture scroll events inside shadow DOM that occur on shadow element scroll containers * ignore scroll event listener test in webkit as fixture that uses CSSStyleSheet to help create scrollbox throws illegal constructor * change structure of capture to be array of strings and assume shadowDom traversal similar to axe-core [run ci] * fix issue where closed shawdow doms were causing application to crash [run ci] * add actionability tests for protocol. Add synthetic input events in order to capture nested shadow DOM inputs * build binary [run ci] * remove actionability from driver [run ci] * add changelog entry for feature, add serialization comments * address comments from code review [run ci] * remove unreachable code * remove additional dead code --------- Co-authored-by: Jennifer Shehane --- .circleci/workflows.yml | 20 +- cli/CHANGELOG.md | 3 +- packages/driver/cypress/e2e/cy/snapshot.cy.js | 25 + .../cypress/fixtures/shadow-dom-type.html | 1 + packages/driver/src/cy/snapshots.ts | 149 +++- system-tests/__snapshots__/protocol_spec.js | 770 +++++++++++++++++- .../protocol/cypress/e2e/shadow-dom.cy.js | 11 + .../cypress/fixtures/shadow-dom-closed.html | 18 + .../protocol/cypress/fixtures/shadow-dom.html | 18 + system-tests/test/protocol_spec.js | 2 +- yarn.lock | 7 +- 11 files changed, 951 insertions(+), 73 deletions(-) create mode 100644 system-tests/projects/protocol/cypress/e2e/shadow-dom.cy.js create mode 100644 system-tests/projects/protocol/cypress/fixtures/shadow-dom-closed.html create mode 100644 system-tests/projects/protocol/cypress/fixtures/shadow-dom.html diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index 19956a2b04ae..cf488a610b3b 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -29,7 +29,10 @@ mainBuildFilters: &mainBuildFilters - develop - /^release\/\d+\.\d+\.\d+$/ # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - - 'update-v8-snapshot-cache-on-develop' + - 'cacie/dep/electron-27' + - 'feat/protocol_shadow_dom_support' + - 'publish-binary' + - 'em/circle2' # usually we don't build Mac app - it takes a long time # but sometimes we want to really confirm we are doing the right thing @@ -40,7 +43,9 @@ macWorkflowFilters: &darwin-workflow-filters - equal: [ develop, << pipeline.git.branch >> ] # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] - - equal: [ 'mschile/service_worker', << pipeline.git.branch >> ] + - equal: [ 'cacie/dep/electron-27', << pipeline.git.branch >> ] + - equal: [ 'feat/protocol_shadow_dom_support', << pipeline.git.branch >> ] + - equal: [ 'ryanm/fix/service-worker-capture', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -51,7 +56,9 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters - equal: [ develop, << pipeline.git.branch >> ] # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] - - equal: [ 'mschile/service_worker', << pipeline.git.branch >> ] + - equal: [ 'cacie/dep/electron-27', << pipeline.git.branch >> ] + - equal: [ 'feat/protocol_shadow_dom_support', << pipeline.git.branch >> ] + - equal: [ 'em/circle2', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -74,7 +81,10 @@ windowsWorkflowFilters: &windows-workflow-filters - equal: [ develop, << pipeline.git.branch >> ] # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] - - equal: [ 'mschile/service_worker', << pipeline.git.branch >> ] + - equal: [ 'cacie/dep/electron-27', << pipeline.git.branch >> ] + - equal: [ 'feat/protocol_shadow_dom_support', << pipeline.git.branch >> ] + - equal: [ 'lerna-optimize-tasks', << pipeline.git.branch >> ] + - equal: [ 'mschile/mochaEvents_win_sep', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -144,7 +154,7 @@ commands: name: Set environment variable to determine whether or not to persist artifacts command: | echo "Setting SHOULD_PERSIST_ARTIFACTS variable" - echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "mschile/service_worker" ]]; then + echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "publish-binary" && "$CIRCLE_BRANCH" != "feat/protocol_shadow_dom_support" && "$CIRCLE_BRANCH" != "cacie/dep/electron-27" ]]; then export SHOULD_PERSIST_ARTIFACTS=true fi' >> "$BASH_ENV" # You must run `setup_should_persist_artifacts` command and be using bash before running this command diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index e4538c2844fe..006582e5f42c 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,10 +1,11 @@ -## 13.6.7 +## 13.7.0 _Released 3/12/2024 (PENDING)_ **Features:** +- Added shadow DOM snapshot support within Test Replay in order to highlight elements correctly within the Cypress reporter. Addressed in [#28823](https://github.com/cypress-io/cypress/pull/28823). - Added TypeScript support for [Vue 2.7+](https://github.com/vuejs/vue/blob/main/CHANGELOG.md#270-2022-07-01). Addresses [#28591](https://github.com/cypress-io/cypress/issues/28591). **Performance:** diff --git a/packages/driver/cypress/e2e/cy/snapshot.cy.js b/packages/driver/cypress/e2e/cy/snapshot.cy.js index f6733a9af47c..81bd45fd6dfc 100644 --- a/packages/driver/cypress/e2e/cy/snapshot.cy.js +++ b/packages/driver/cypress/e2e/cy/snapshot.cy.js @@ -255,6 +255,31 @@ describe('driver/src/cy/snapshots', () => { expect(name).to.equal('snapshot') expect(timestamp).to.be.a('number') }) + + it('captures shadow DOM selectors structure properly', { + protocolEnabled: true, + }, () => { + cy.visit('/fixtures/shadow-dom-type.html') + cy.window().then((win) => { + win.__cypressProtocolMetadata = { frameId: 'test-frame-id' } + + cy.get('#shadow-dom-input', { + includeShadowDom: true, + }).then((shadowDomSlot) => { + const { elementsToHighlight, name, timestamp } = cy.createSnapshot('snapshot', shadowDomSlot) + + expect(elementsToHighlight?.length).to.equal(1) + const elementToHighlight = elementsToHighlight[0] + + expect(elementToHighlight.selector.length).to.equal(2) + expect(elementToHighlight.selector[0]).to.equal('#element') + expect(elementToHighlight.selector[1]).to.equal('#shadow-dom-input') + expect(elementToHighlight.frameId).to.equal('test-frame-id') + expect(name).to.equal('snapshot') + expect(timestamp).to.be.a('number') + }) + }) + }) }) }) diff --git a/packages/driver/cypress/fixtures/shadow-dom-type.html b/packages/driver/cypress/fixtures/shadow-dom-type.html index 75ff6a161bd7..57826c10733e 100644 --- a/packages/driver/cypress/fixtures/shadow-dom-type.html +++ b/packages/driver/cypress/fixtures/shadow-dom-type.html @@ -10,6 +10,7 @@ this._shadow = this.attachShadow({mode: "open"}); const input = document.createElement("input"); + input.id = 'shadow-dom-input' this._shadow.appendChild(input); } } diff --git a/packages/driver/src/cy/snapshots.ts b/packages/driver/src/cy/snapshots.ts index f1f122718c52..9c553221f1c8 100644 --- a/packages/driver/src/cy/snapshots.ts +++ b/packages/driver/src/cy/snapshots.ts @@ -10,6 +10,89 @@ export const HIGHLIGHT_ATTR = 'data-cypress-el' export const FINAL_SNAPSHOT_NAME = 'final state' +type SelectorNode = { + frameId?: string + selector: string + ownerDoc: Document | ShadowRoot + host?: SelectorNode +} + +const returnShadowRootIfShadowDomNode = (node: Element): ShadowRoot | null => { + // the shadowRoot object property only lives on the node context OUTSIDE the shadow DOM, meaning that + // node.parentNode.host.shadowRoot works. Oddly, this is considered an instance of an Object and not + // a ShadowRoot, so checking for the shadowRoot on the host property is likely safe. + const isNodeShadowRoot = (n: any) => !!n?.host?.shadowRoot + + let parent = node && node.parentNode + + while (parent) { + if (isNodeShadowRoot(parent)) { + return parent as ShadowRoot + } + + parent = parent.parentNode + } + + return null +} + +function findSelectorForElement (elem: Element, root: Document | ShadowRoot) { + // finder tries to find the shortest unique selector to an element, + // but since we are more concerned with speed, we set the threshold to 1 and maxNumberOfTries to 0 + // @see https://github.com/antonmedv/finder/issues/75 + return finder(elem, { root: root as unknown as Element, threshold: 1, maxNumberOfTries: 0 }) +} + +/** + * Builds a recursive structure of selectors in order to re-identify during Test Replay. + * + * @param elem - an HTML Element that lives within the shadow DOM or the regular DOM + * @returns SelectorNode if the selector can be discovered. For regular elements, this should only be one object deep, but for shadow DOM + * elements, the SelectorNode tree could be N levels deep until the root is discovered + */ +function constructElementSelectorTree (elem: Element): SelectorNode | undefined { + try { + const ownerDoc = elem.ownerDocument + const elWindow = ownerDoc.defaultView + + if (elWindow === null) { + return undefined + } + + // finder will return a string if it can find the selector. + // otherwise, an error will throw and we will fall back to shadowDom lookup. + const selector = findSelectorForElement(elem, ownerDoc) + + const frameId = elWindow['__cypressProtocolMetadata']?.frameId + + return { selector, frameId, ownerDoc: elem.ownerDocument, host: undefined } + } catch { + // the element may not always be found since it's possible for the element to be removed from the DOM + // Or maybe its in the shadow DOM. + // If it is a shadow DOM element, return the ShadowRoot as well to relate the node to the root document + try { + const shadowRoot = returnShadowRootIfShadowDomNode(elem) + + // If we have a shadow DOM element, get the frameId and unique selector of the ShadowRoot + // see https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot + if (shadowRoot) { + // Look up the details of the shadowRoot to see which element the ShadowRoot is bound to, i.e. the host. + const hostDetails = constructElementSelectorTree(shadowRoot.host) + + // look up our element inside the context of the ShadowRoot + const selectorFromShadowWorld = findSelectorForElement(elem, shadowRoot) + + // gives us enough information to associate the shadow element to the ShadowRoot/host to reconstruct in Test Replay + return { selector: selectorFromShadowWorld, frameId: undefined, ownerDoc: shadowRoot, host: hostDetails } + } + } catch { + return undefined + } + } + + return undefined +} + export const create = ($$: $Cy['$$'], state: StateFunc) => { const snapshotsCss = createSnapshotsCSS($$, state) const snapshotsMap = new WeakMap() @@ -232,6 +315,47 @@ export const create = ($$: $Cy['$$'], state: StateFunc) => { return $dom.isElement($el) && $dom.isJquery($el) } + const buildSelectorArray = (el: HTMLElement) => { + // flatten selector to only include selector string values, which we can imply is a shadowRoot if other values exist in the tree + // this keeps the structure similar to axe-core + // @see https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#results-object -> target + const selectors: string[] | undefined = [] + let frameId: string | undefined + const flattenElementSelectorTree = (el: SelectorNode | undefined): void => { + if (el) { + selectors.unshift(el?.selector) + + if (el?.host) { + flattenElementSelectorTree(el.host) + } else { + frameId = el.frameId + } + } + } + + const elToHighlight = constructElementSelectorTree(el) + + flattenElementSelectorTree(elToHighlight) + + let selector: string | string[] | undefined + + switch (selectors.length) { + case 0: + selector = undefined + break + case 1: + selector = selectors[0] + break + default: + selector = selectors + } + + return selector ? [{ + selector, + frameId, + }] : [] + } + const createSnapshot = (name, $elToHighlight, preprocessedSnapshot) => { Cypress.action('cy:snapshot', name) // when using cy.origin() and in a transitionary state, state('document') @@ -254,34 +378,13 @@ export const create = ($$: $Cy['$$'], state: StateFunc) => { name: string timestamp: number elementsToHighlight?: { - selector: string + selector: string | string [] frameId: string }[] } = { name, timestamp } if (isJqueryElement($elToHighlight)) { - snapshot.elementsToHighlight = $dom.unwrap($elToHighlight).flatMap((el: HTMLElement) => { - try { - const ownerDoc = el.ownerDocument - const elWindow = ownerDoc.defaultView - - if (elWindow === null) { - return [] - } - - // finder tries to find the shortest unique selector to an element, - // but since we are more concerned with speed, we set the threshold to 1 and maxNumberOfTries to 0 - // @ts-expect-error because 'root' can be either Document or Element but is defined as Element - // @see https://github.com/antonmedv/finder/issues/75 - const selector = finder(el, { root: ownerDoc, threshold: 1, maxNumberOfTries: 0 }) - const frameId = elWindow['__cypressProtocolMetadata']?.frameId - - return [{ selector, frameId }] - } catch { - // the element may not always be found since it's possible for the element to be removed from the DOM - return [] - } - }) + snapshot.elementsToHighlight = $dom.unwrap($elToHighlight).flatMap((el: HTMLElement) => buildSelectorArray(el)) } Cypress.action('cy:protocol-snapshot') diff --git a/system-tests/__snapshots__/protocol_spec.js b/system-tests/__snapshots__/protocol_spec.js index 5fa970ef5a92..5d9c117bb776 100644 --- a/system-tests/__snapshots__/protocol_spec.js +++ b/system-tests/__snapshots__/protocol_spec.js @@ -8,6 +8,13 @@ exports['e2e events'] = ` "readonly": false, "memory": false }, + { + "name": "/path/to/name", + "open": true, + "inTransaction": false, + "readonly": false, + "memory": false + }, { "name": "/path/to/name", "open": true, @@ -17,6 +24,7 @@ exports['e2e events'] = ` } ], "afterSpec": [ + true, true, true ], @@ -614,6 +622,66 @@ exports['e2e events'] = ` "retries": 0, "_slow": 10000, "timestamp": "Any.Number" + }, + { + "_testConfig": { + "testConfigList": [], + "unverifiedTestConfig": {}, + "applied": "complete" + }, + "id": "r3", + "order": 1, + "title": "has protocol events with shadow DOM selectors", + "pending": false, + "body": "() => {\\n cy.visit('cypress/fixtures/shadow-dom.html');\\n cy.get('#in-shadow', {\\n includeShadowDom: true\\n }).should('exist');\\n }", + "type": "test", + "wallClockStartedAt": "Any.ISODate", + "file": null, + "invocationDetails": { + "function": "Suite.eval", + "fileUrl": "http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js", + "originalFile": "webpack://protocol-sample-project/./cypress/e2e/shadow-dom.cy.js", + "relativeFile": "cypress/e2e/shadow-dom.cy.js", + "absoluteFile": "/path/to/absoluteFile", + "line": 2, + "column": 2, + "whitespace": " ", + "stack": "Error\\n at Suite.eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:10:3)\\n at eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:9:1)\\n at eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:23:12)\\n at eval ()" + }, + "currentRetry": 0, + "retries": 0, + "_slow": 10000, + "timestamp": "Any.Number" + }, + { + "_testConfig": { + "testConfigList": [], + "unverifiedTestConfig": {}, + "applied": "complete" + }, + "id": "r4", + "order": 2, + "title": "does not have cypress errors when visiting closed shadow roots", + "pending": false, + "body": "() => {\\n cy.visit('cypress/fixtures/shadow-dom-closed.html');\\n cy.get('#in-shadow', {\\n includeShadowDom: true\\n }).should('not.exist');\\n }", + "type": "test", + "wallClockStartedAt": "Any.ISODate", + "file": null, + "invocationDetails": { + "function": "Suite.eval", + "fileUrl": "http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js", + "originalFile": "webpack://protocol-sample-project/./cypress/e2e/shadow-dom.cy.js", + "relativeFile": "cypress/e2e/shadow-dom.cy.js", + "absoluteFile": "/path/to/absoluteFile", + "line": 7, + "column": 2, + "whitespace": " ", + "stack": "Error\\n at Suite.eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:16:3)\\n at eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:9:1)\\n at eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:23:12)\\n at eval ()" + }, + "currentRetry": 0, + "retries": 0, + "_slow": 10000, + "timestamp": "Any.Number" } ], "preAfterTest": [ @@ -1393,6 +1461,104 @@ exports['e2e events'] = ` "_slow": 10000 }, "options": {} + }, + { + "test": { + "_cypressTestStatusInfo": { + "strategy": "detect-flake-and-pass-on-threshold", + "shouldAttemptsContinue": false, + "attempts": 1, + "outerStatus": "passed" + }, + "_testConfig": { + "testConfigList": [], + "unverifiedTestConfig": {}, + "applied": "complete" + }, + "id": "r3", + "order": 1, + "title": "has protocol events with shadow DOM selectors", + "state": "passed", + "pending": false, + "body": "() => {\\n cy.visit('cypress/fixtures/shadow-dom.html');\\n cy.get('#in-shadow', {\\n includeShadowDom: true\\n }).should('exist');\\n }", + "type": "test", + "duration": "Any.Number", + "wallClockStartedAt": "Any.ISODate", + "timings": { + "lifecycle": "Any.Number", + "test": { + "fnDuration": "Any.Number", + "afterFnDuration": "Any.Number" + } + }, + "file": null, + "invocationDetails": { + "function": "Suite.eval", + "fileUrl": "http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js", + "originalFile": "webpack://protocol-sample-project/./cypress/e2e/shadow-dom.cy.js", + "relativeFile": "cypress/e2e/shadow-dom.cy.js", + "absoluteFile": "/path/to/absoluteFile", + "line": 2, + "column": 2, + "whitespace": " ", + "stack": "Error\\n at Suite.eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:10:3)\\n at eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:9:1)\\n at eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:23:12)\\n at eval ()" + }, + "final": true, + "currentRetry": 0, + "retries": 0, + "_slow": 10000 + }, + "options": { + "nextTestHasTestIsolationOn": true + } + }, + { + "test": { + "_cypressTestStatusInfo": { + "strategy": "detect-flake-and-pass-on-threshold", + "shouldAttemptsContinue": false, + "attempts": 1, + "outerStatus": "passed" + }, + "_testConfig": { + "testConfigList": [], + "unverifiedTestConfig": {}, + "applied": "complete" + }, + "id": "r4", + "order": 2, + "title": "does not have cypress errors when visiting closed shadow roots", + "state": "passed", + "pending": false, + "body": "() => {\\n cy.visit('cypress/fixtures/shadow-dom-closed.html');\\n cy.get('#in-shadow', {\\n includeShadowDom: true\\n }).should('not.exist');\\n }", + "type": "test", + "duration": "Any.Number", + "wallClockStartedAt": "Any.ISODate", + "timings": { + "lifecycle": "Any.Number", + "test": { + "fnDuration": "Any.Number", + "afterFnDuration": "Any.Number" + } + }, + "file": null, + "invocationDetails": { + "function": "Suite.eval", + "fileUrl": "http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js", + "originalFile": "webpack://protocol-sample-project/./cypress/e2e/shadow-dom.cy.js", + "relativeFile": "cypress/e2e/shadow-dom.cy.js", + "absoluteFile": "/path/to/absoluteFile", + "line": 7, + "column": 2, + "whitespace": " ", + "stack": "Error\\n at Suite.eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:16:3)\\n at eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:9:1)\\n at eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:23:12)\\n at eval ()" + }, + "final": true, + "currentRetry": 0, + "retries": 0, + "_slow": 10000 + }, + "options": {} } ], "afterTest": [ @@ -2140,6 +2306,98 @@ exports['e2e events'] = ` "currentRetry": 0, "retries": 0, "_slow": 10000 + }, + { + "_cypressTestStatusInfo": { + "strategy": "detect-flake-and-pass-on-threshold", + "shouldAttemptsContinue": false, + "attempts": 1, + "outerStatus": "passed" + }, + "_testConfig": { + "testConfigList": [], + "unverifiedTestConfig": {}, + "applied": "complete" + }, + "id": "r3", + "order": 1, + "title": "has protocol events with shadow DOM selectors", + "state": "passed", + "pending": false, + "body": "() => {\\n cy.visit('cypress/fixtures/shadow-dom.html');\\n cy.get('#in-shadow', {\\n includeShadowDom: true\\n }).should('exist');\\n }", + "type": "test", + "duration": "Any.Number", + "wallClockStartedAt": "Any.ISODate", + "wallClockDuration": "Any.Number", + "timings": { + "lifecycle": "Any.Number", + "test": { + "fnDuration": "Any.Number", + "afterFnDuration": "Any.Number" + } + }, + "file": null, + "invocationDetails": { + "function": "Suite.eval", + "fileUrl": "http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js", + "originalFile": "webpack://protocol-sample-project/./cypress/e2e/shadow-dom.cy.js", + "relativeFile": "cypress/e2e/shadow-dom.cy.js", + "absoluteFile": "/path/to/absoluteFile", + "line": 2, + "column": 2, + "whitespace": " ", + "stack": "Error\\n at Suite.eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:10:3)\\n at eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:9:1)\\n at eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:23:12)\\n at eval ()" + }, + "final": true, + "currentRetry": 0, + "retries": 0, + "_slow": 10000 + }, + { + "_cypressTestStatusInfo": { + "strategy": "detect-flake-and-pass-on-threshold", + "shouldAttemptsContinue": false, + "attempts": 1, + "outerStatus": "passed" + }, + "_testConfig": { + "testConfigList": [], + "unverifiedTestConfig": {}, + "applied": "complete" + }, + "id": "r4", + "order": 2, + "title": "does not have cypress errors when visiting closed shadow roots", + "state": "passed", + "pending": false, + "body": "() => {\\n cy.visit('cypress/fixtures/shadow-dom-closed.html');\\n cy.get('#in-shadow', {\\n includeShadowDom: true\\n }).should('not.exist');\\n }", + "type": "test", + "duration": "Any.Number", + "wallClockStartedAt": "Any.ISODate", + "wallClockDuration": "Any.Number", + "timings": { + "lifecycle": "Any.Number", + "test": { + "fnDuration": "Any.Number", + "afterFnDuration": "Any.Number" + } + }, + "file": null, + "invocationDetails": { + "function": "Suite.eval", + "fileUrl": "http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js", + "originalFile": "webpack://protocol-sample-project/./cypress/e2e/shadow-dom.cy.js", + "relativeFile": "cypress/e2e/shadow-dom.cy.js", + "absoluteFile": "/path/to/absoluteFile", + "line": 7, + "column": 2, + "whitespace": " ", + "stack": "Error\\n at Suite.eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:16:3)\\n at eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:9:1)\\n at eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:23:12)\\n at eval ()" + }, + "final": true, + "currentRetry": 0, + "retries": 0, + "_slow": 10000 } ], "addRunnables": [ @@ -2949,45 +3207,149 @@ exports['e2e events'] = ` ], "runtimeConfig": {}, "totalUnfilteredTests": 0 - } - ], - "connectToBrowser": [ - true, - true - ], - "commandLogAdded": [ - { - "id": "log-http://localhost:3131-1", - "event": false, - "hookId": "h1", - "instrument": "command", - "hidden": false, - "message": "http://localhost:3131/index.html", - "name": "visit", - "renderProps": {}, - "state": "pending", - "testId": "r3", - "timeout": 60000, - "type": "parent", - "url": "", - "wallClockStartedAt": "Any.ISODate", - "testCurrentRetry": 0, - "createdAtTimestamp": "Any.Number", - "updatedAtTimestamp": "Any.Number" }, { - "id": "log-http://localhost:3131-2", - "event": false, - "hookId": "r3", - "instrument": "command", - "hidden": false, - "message": "300, 200", - "name": "viewport", - "renderProps": {}, - "state": "pending", - "testId": "r3", - "timeout": 4000, - "type": "parent", + "id": "r1", + "title": "", + "root": true, + "pending": false, + "type": "suite", + "file": "cypress/e2e/shadow-dom.cy.js", + "retries": -1, + "_slow": 10000, + "hooks": [], + "tests": [], + "suites": [ + { + "id": "r2", + "title": "protocol events w/ shadow DOM", + "root": false, + "pending": false, + "type": "suite", + "file": null, + "invocationDetails": { + "function": "eval", + "fileUrl": "http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js", + "originalFile": "webpack://protocol-sample-project/./cypress/e2e/shadow-dom.cy.js", + "relativeFile": "cypress/e2e/shadow-dom.cy.js", + "absoluteFile": "/path/to/absoluteFile", + "line": 1, + "column": 0, + "whitespace": " ", + "stack": "Error\\n at eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:9:1)\\n at eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:23:12)\\n at eval ()" + }, + "retries": -1, + "_slow": 10000, + "hooks": [], + "tests": [ + { + "id": "r3", + "title": "has protocol events with shadow DOM selectors", + "pending": false, + "body": "() => {\\n cy.visit('cypress/fixtures/shadow-dom.html');\\n cy.get('#in-shadow', {\\n includeShadowDom: true\\n }).should('exist');\\n }", + "type": "test", + "file": null, + "invocationDetails": { + "function": "Suite.eval", + "fileUrl": "http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js", + "originalFile": "webpack://protocol-sample-project/./cypress/e2e/shadow-dom.cy.js", + "relativeFile": "cypress/e2e/shadow-dom.cy.js", + "absoluteFile": "/path/to/absoluteFile", + "line": 2, + "column": 2, + "whitespace": " ", + "stack": "Error\\n at Suite.eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:10:3)\\n at eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:9:1)\\n at eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:23:12)\\n at eval ()" + }, + "currentRetry": 0, + "retries": -1, + "_slow": 10000, + "hooks": [], + "_testConfig": { + "testConfigList": [], + "unverifiedTestConfig": {} + }, + "_titlePath": [ + "protocol events w/ shadow DOM", + "has protocol events with shadow DOM selectors" + ] + }, + { + "id": "r4", + "title": "does not have cypress errors when visiting closed shadow roots", + "pending": false, + "body": "() => {\\n cy.visit('cypress/fixtures/shadow-dom-closed.html');\\n cy.get('#in-shadow', {\\n includeShadowDom: true\\n }).should('not.exist');\\n }", + "type": "test", + "file": null, + "invocationDetails": { + "function": "Suite.eval", + "fileUrl": "http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js", + "originalFile": "webpack://protocol-sample-project/./cypress/e2e/shadow-dom.cy.js", + "relativeFile": "cypress/e2e/shadow-dom.cy.js", + "absoluteFile": "/path/to/absoluteFile", + "line": 7, + "column": 2, + "whitespace": " ", + "stack": "Error\\n at Suite.eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:16:3)\\n at eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:9:1)\\n at eval (http://localhost:2121/__cypress/tests?p=cypress/e2e/shadow-dom.cy.js:23:12)\\n at eval ()" + }, + "currentRetry": 0, + "retries": -1, + "_slow": 10000, + "hooks": [], + "_testConfig": { + "testConfigList": [], + "unverifiedTestConfig": {} + }, + "_titlePath": [ + "protocol events w/ shadow DOM", + "does not have cypress errors when visiting closed shadow roots" + ] + } + ], + "suites": [] + } + ], + "runtimeConfig": {}, + "totalUnfilteredTests": 0 + } + ], + "connectToBrowser": [ + true, + true, + true + ], + "commandLogAdded": [ + { + "id": "log-http://localhost:3131-1", + "event": false, + "hookId": "h1", + "instrument": "command", + "hidden": false, + "message": "http://localhost:3131/index.html", + "name": "visit", + "renderProps": {}, + "state": "pending", + "testId": "r3", + "timeout": 60000, + "type": "parent", + "url": "", + "wallClockStartedAt": "Any.ISODate", + "testCurrentRetry": 0, + "createdAtTimestamp": "Any.Number", + "updatedAtTimestamp": "Any.Number" + }, + { + "id": "log-http://localhost:3131-2", + "event": false, + "hookId": "r3", + "instrument": "command", + "hidden": false, + "message": "300, 200", + "name": "viewport", + "renderProps": {}, + "state": "pending", + "testId": "r3", + "timeout": 4000, + "type": "parent", "url": "http://localhost:3131/index.html", "wallClockStartedAt": "Any.ISODate", "testCurrentRetry": 0, @@ -3717,6 +4079,123 @@ exports['e2e events'] = ` "createdAtTimestamp": "Any.Number", "updatedAtTimestamp": "Any.Number", "highlightAttr": "data-cypress-el" + }, + { + "id": "log-http://localhost:2121-1", + "event": false, + "hookId": "r3", + "instrument": "command", + "hidden": false, + "message": "cypress/fixtures/shadow-dom.html", + "name": "visit", + "renderProps": {}, + "state": "pending", + "testId": "r3", + "timeout": 60000, + "type": "parent", + "url": "", + "wallClockStartedAt": "Any.ISODate", + "testCurrentRetry": 0, + "createdAtTimestamp": "Any.Number", + "updatedAtTimestamp": "Any.Number" + }, + { + "id": "log-http://localhost:2121-2", + "event": false, + "hookId": "r3", + "instrument": "command", + "hidden": false, + "message": "#in-shadow", + "name": "get", + "renderProps": {}, + "state": "pending", + "testId": "r3", + "timeout": 4000, + "type": "parent", + "url": "http://localhost:2121/cypress/fixtures/shadow-dom.html", + "wallClockStartedAt": "Any.ISODate", + "testCurrentRetry": 0, + "createdAtTimestamp": "Any.Number", + "updatedAtTimestamp": "Any.Number" + }, + { + "id": "log-http://localhost:2121-3", + "event": false, + "hookId": "r3", + "instrument": "command", + "hidden": false, + "message": "expected **** to exist in the DOM", + "name": "assert", + "numElements": 1, + "renderProps": {}, + "state": "pending", + "testId": "r3", + "timeout": 0, + "type": "child", + "url": "http://localhost:2121/cypress/fixtures/shadow-dom.html", + "visible": true, + "wallClockStartedAt": "Any.ISODate", + "testCurrentRetry": 0, + "createdAtTimestamp": "Any.Number", + "updatedAtTimestamp": "Any.Number", + "highlightAttr": "data-cypress-el" + }, + { + "id": "log-http://localhost:2121-4", + "event": false, + "hookId": "r4", + "instrument": "command", + "hidden": false, + "message": "cypress/fixtures/shadow-dom-closed.html", + "name": "visit", + "renderProps": {}, + "state": "pending", + "testId": "r4", + "timeout": 60000, + "type": "parent", + "url": "", + "wallClockStartedAt": "Any.ISODate", + "testCurrentRetry": 0, + "createdAtTimestamp": "Any.Number", + "updatedAtTimestamp": "Any.Number" + }, + { + "id": "log-http://localhost:2121-5", + "event": false, + "hookId": "r4", + "instrument": "command", + "hidden": false, + "message": "#in-shadow", + "name": "get", + "renderProps": {}, + "state": "pending", + "testId": "r4", + "timeout": 4000, + "type": "parent", + "url": "http://localhost:2121/cypress/fixtures/shadow-dom-closed.html", + "wallClockStartedAt": "Any.ISODate", + "testCurrentRetry": 0, + "createdAtTimestamp": "Any.Number", + "updatedAtTimestamp": "Any.Number" + }, + { + "id": "log-http://localhost:2121-6", + "event": false, + "hookId": "r4", + "instrument": "command", + "hidden": false, + "message": "expected **#in-shadow** not to exist in the DOM", + "name": "assert", + "renderProps": {}, + "state": "pending", + "testId": "r4", + "timeout": 0, + "type": "child", + "url": "http://localhost:2121/cypress/fixtures/shadow-dom-closed.html", + "wallClockStartedAt": "Any.ISODate", + "testCurrentRetry": 0, + "createdAtTimestamp": "Any.Number", + "updatedAtTimestamp": "Any.Number" } ], "commandLogChanged": [ @@ -3766,6 +4245,30 @@ exports['e2e events'] = ` "createdAtTimestamp": "Any.Number", "updatedAtTimestamp": "Any.Number" }, + { + "id": "log-http://localhost:2121-1", + "event": false, + "hookId": "r3", + "instrument": "command", + "hidden": false, + "message": "cypress/fixtures/shadow-dom.html", + "name": "visit", + "renderProps": {}, + "state": "passed", + "testId": "r3", + "timeout": 60000, + "type": "parent", + "url": "cypress/fixtures/shadow-dom.html", + "wallClockStartedAt": "Any.ISODate", + "testCurrentRetry": 0, + "snapshots": [ + { + "timestamp": "Any.Number" + } + ], + "createdAtTimestamp": "Any.Number", + "updatedAtTimestamp": "Any.Number" + }, { "id": "log-http://localhost:2121-10", "event": false, @@ -4084,6 +4587,41 @@ exports['e2e events'] = ` "updatedAtTimestamp": "Any.Number", "highlightAttr": "data-cypress-el" }, + { + "id": "log-http://localhost:2121-2", + "event": false, + "hookId": "r3", + "instrument": "command", + "hidden": false, + "message": "#in-shadow", + "name": "get", + "numElements": 1, + "renderProps": {}, + "state": "passed", + "testId": "r3", + "timeout": 4000, + "type": "parent", + "url": "http://localhost:2121/cypress/fixtures/shadow-dom.html", + "visible": true, + "wallClockStartedAt": "Any.ISODate", + "testCurrentRetry": 0, + "snapshots": [ + { + "timestamp": "Any.Number", + "elementsToHighlight": [ + { + "selector": [ + "#shadow-root", + "#in-shadow" + ] + } + ] + } + ], + "createdAtTimestamp": "Any.Number", + "updatedAtTimestamp": "Any.Number", + "highlightAttr": "data-cypress-el" + }, { "id": "log-http://localhost:2121-20", "event": false, @@ -4384,6 +4922,41 @@ exports['e2e events'] = ` "updatedAtTimestamp": "Any.Number", "highlightAttr": "data-cypress-el" }, + { + "id": "log-http://localhost:2121-3", + "event": false, + "hookId": "r3", + "instrument": "command", + "hidden": false, + "message": "expected **** to exist in the DOM", + "name": "assert", + "numElements": 1, + "renderProps": {}, + "state": "passed", + "testId": "r3", + "timeout": 0, + "type": "child", + "url": "http://localhost:2121/cypress/fixtures/shadow-dom.html", + "visible": true, + "wallClockStartedAt": "Any.ISODate", + "testCurrentRetry": 0, + "snapshots": [ + { + "timestamp": "Any.Number", + "elementsToHighlight": [ + { + "selector": [ + "#shadow-root", + "#in-shadow" + ] + } + ] + } + ], + "createdAtTimestamp": "Any.Number", + "updatedAtTimestamp": "Any.Number", + "highlightAttr": "data-cypress-el" + }, { "id": "log-http://localhost:2121-30", "event": false, @@ -4654,6 +5227,30 @@ exports['e2e events'] = ` }, "highlightAttr": "data-cypress-el" }, + { + "id": "log-http://localhost:2121-4", + "event": false, + "hookId": "r4", + "instrument": "command", + "hidden": false, + "message": "cypress/fixtures/shadow-dom-closed.html", + "name": "visit", + "renderProps": {}, + "state": "passed", + "testId": "r4", + "timeout": 60000, + "type": "parent", + "url": "cypress/fixtures/shadow-dom-closed.html", + "wallClockStartedAt": "Any.ISODate", + "testCurrentRetry": 0, + "snapshots": [ + { + "timestamp": "Any.Number" + } + ], + "createdAtTimestamp": "Any.Number", + "updatedAtTimestamp": "Any.Number" + }, { "id": "log-http://localhost:2121-5", "event": false, @@ -4686,6 +5283,33 @@ exports['e2e events'] = ` "updatedAtTimestamp": "Any.Number", "highlightAttr": "data-cypress-el" }, + { + "id": "log-http://localhost:2121-5", + "event": false, + "hookId": "r4", + "instrument": "command", + "hidden": false, + "message": "#in-shadow", + "name": "get", + "numElements": 0, + "renderProps": {}, + "state": "passed", + "testId": "r4", + "timeout": 4000, + "type": "parent", + "url": "http://localhost:2121/cypress/fixtures/shadow-dom-closed.html", + "visible": true, + "wallClockStartedAt": "Any.ISODate", + "testCurrentRetry": 0, + "snapshots": [ + { + "timestamp": "Any.Number" + } + ], + "createdAtTimestamp": "Any.Number", + "updatedAtTimestamp": "Any.Number", + "highlightAttr": "data-cypress-el" + }, { "id": "log-http://localhost:2121-6", "event": false, @@ -4718,6 +5342,30 @@ exports['e2e events'] = ` "updatedAtTimestamp": "Any.Number", "highlightAttr": "data-cypress-el" }, + { + "id": "log-http://localhost:2121-6", + "event": false, + "hookId": "r4", + "instrument": "command", + "hidden": false, + "message": "expected **#in-shadow** not to exist in the DOM", + "name": "assert", + "renderProps": {}, + "state": "passed", + "testId": "r4", + "timeout": 0, + "type": "child", + "url": "http://localhost:2121/cypress/fixtures/shadow-dom-closed.html", + "wallClockStartedAt": "Any.ISODate", + "testCurrentRetry": 0, + "snapshots": [ + { + "timestamp": "Any.Number" + } + ], + "createdAtTimestamp": "Any.Number", + "updatedAtTimestamp": "Any.Number" + }, { "id": "log-http://localhost:2121-7", "event": false, @@ -5028,6 +5676,22 @@ exports['e2e events'] = ` "url": "http://localhost:2121/cypress/fixtures/dom-with-browser-interactions.html", "timestamp": "Any.Number" }, + { + "url": "", + "timestamp": "Any.Number" + }, + { + "url": "http://localhost:2121/cypress/fixtures/shadow-dom.html", + "timestamp": "Any.Number" + }, + { + "url": "", + "timestamp": "Any.Number" + }, + { + "url": "http://localhost:2121/cypress/fixtures/shadow-dom-closed.html", + "timestamp": "Any.Number" + }, { "url": "", "timestamp": "Any.Number" @@ -5102,6 +5766,38 @@ exports['e2e events'] = ` "loading": true, "timestamp": "Any.Number" }, + { + "loading": false, + "timestamp": "Any.Number" + }, + { + "loading": true, + "timestamp": "Any.Number" + }, + { + "loading": false, + "timestamp": "Any.Number" + }, + { + "loading": true, + "timestamp": "Any.Number" + }, + { + "loading": false, + "timestamp": "Any.Number" + }, + { + "loading": true, + "timestamp": "Any.Number" + }, + { + "loading": false, + "timestamp": "Any.Number" + }, + { + "loading": true, + "timestamp": "Any.Number" + }, { "loading": false, "timestamp": "Any.Number" diff --git a/system-tests/projects/protocol/cypress/e2e/shadow-dom.cy.js b/system-tests/projects/protocol/cypress/e2e/shadow-dom.cy.js new file mode 100644 index 000000000000..fe90be93fa39 --- /dev/null +++ b/system-tests/projects/protocol/cypress/e2e/shadow-dom.cy.js @@ -0,0 +1,11 @@ +describe('protocol events w/ shadow DOM', () => { + it('has protocol events with shadow DOM selectors', () => { + cy.visit('cypress/fixtures/shadow-dom.html') + cy.get('#in-shadow', { includeShadowDom: true }).should('exist') + }) + + it('does not have cypress errors when visiting closed shadow roots', () => { + cy.visit('cypress/fixtures/shadow-dom-closed.html') + cy.get('#in-shadow', { includeShadowDom: true }).should('not.exist') + }) +}) diff --git a/system-tests/projects/protocol/cypress/fixtures/shadow-dom-closed.html b/system-tests/projects/protocol/cypress/fixtures/shadow-dom-closed.html new file mode 100644 index 000000000000..8ded19b5876c --- /dev/null +++ b/system-tests/projects/protocol/cypress/fixtures/shadow-dom-closed.html @@ -0,0 +1,18 @@ + + + + Shadow DOM Closed Fixture + + +

Shadow DOM Closed Fixture

+
+ + + diff --git a/system-tests/projects/protocol/cypress/fixtures/shadow-dom.html b/system-tests/projects/protocol/cypress/fixtures/shadow-dom.html new file mode 100644 index 000000000000..10228e326537 --- /dev/null +++ b/system-tests/projects/protocol/cypress/fixtures/shadow-dom.html @@ -0,0 +1,18 @@ + + + + Shadow DOM Fixture + + +

Shadow DOM Fixture

+
+ + + diff --git a/system-tests/test/protocol_spec.js b/system-tests/test/protocol_spec.js index ff1ed2bb739a..fc30ed626111 100644 --- a/system-tests/test/protocol_spec.js +++ b/system-tests/test/protocol_spec.js @@ -40,7 +40,7 @@ describe('capture-protocol', () => { return systemTests.exec(this, { key: 'f858a2bc-b469-4e48-be67-0876339ee7e1', project: 'protocol', - spec: 'protocol.cy.js,test-isolation.cy.js', + spec: 'protocol.cy.js,test-isolation.cy.js,shadow-dom.cy.js', record: true, expectedExitCode: 0, port: 2121, diff --git a/yarn.lock b/yarn.lock index 4b7bbeb8c650..afc0de050e74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29377,12 +29377,7 @@ tiny-relative-date@^1.3.0: resolved "https://registry.yarnpkg.com/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07" integrity sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A== -tinycolor2@^1.1.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803" - integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA== - -tinycolor2@^1.6.0: +tinycolor2@^1.1.2, tinycolor2@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e" integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==