misc: pass the related command log to the createSnapshot function (#30244)

* passing related log to the createSnapshot function

* update changelog

* update system test snapshot
This commit is contained in:
Matt Schile
2024-09-16 13:34:08 -06:00
committed by GitHub
parent 095b3daefa
commit f3242ea7f3
8 changed files with 241 additions and 737 deletions
+4
View File
@@ -3,6 +3,10 @@
_Released 9/10/2024 (PENDING)_
**Misc:**
- Pass along the related log to the `createSnapshot` function for protocol usage. Addressed in [#30244](https://github.com/cypress-io/cypress/pull/30244).
**Dependency Updates:**
- Update `@cypress/request` from `3.0.1` to `3.0.4`. Addressed in [#30194](https://github.com/cypress-io/cypress/pull/30194).
@@ -390,7 +390,7 @@ describe('src/cypress/log', function () {
const log = this.log({ '$el': div })
const result = log.snapshot()
expect(this.cy.createSnapshot).to.be.calledWith(undefined, div)
expect(this.cy.createSnapshot).to.be.calledWith(undefined, div, undefined, log)
expect(result).to.equal(log)
})
@@ -436,7 +436,7 @@ describe('src/cypress/log', function () {
const log = this.log({ '$el': div })
const result = log.snapshot()
expect(this.cy.createSnapshot).to.be.calledWith(undefined, div)
expect(this.cy.createSnapshot).to.be.calledWith(undefined, div, undefined, log)
expect(result).to.equal(log)
})
@@ -450,7 +450,7 @@ describe('src/cypress/log', function () {
const log = this.log({ '$el': div })
const result = log.snapshot()
expect(this.cy.createSnapshot).to.be.calledWith(undefined, div)
expect(this.cy.createSnapshot).to.be.calledWith(undefined, div, undefined, log)
expect(result).to.equal(log)
})
@@ -177,41 +177,6 @@ context('cy.origin log', { browser: '!webkit' }, () => {
.wait(1500)
})
it('when run mode with protocol enabled', { numTestsKeptInMemory: 0, protocolEnabled: true }, () => {
// Verify the log is also fired in the primary origin.
expect(logsToVerify.length).to.eq(11)
expect(logsToVerify[1].get('name')).to.equal('log 1')
expect(logsToVerify[1].get('snapshots')).to.be.undefined
expect(logsToVerify[2].get('name')).to.equal('log 2')
expect(logsToVerify[2].get('snapshots')).to.have.length(1)
expect(logsToVerify[3].get('name')).to.equal('log 3')
expect(logsToVerify[3].get('snapshots')).to.have.length(1)
expect(logsToVerify[4].get('name')).to.equal('log 4')
expect(logsToVerify[4].get('snapshots')).to.have.length(1)
expect(logsToVerify[5].get('name')).to.equal('log 5')
expect(logsToVerify[5].get('snapshots')).to.have.length(1)
expect(logsToVerify[6].get('name')).to.equal('log 6')
expect(logsToVerify[6].get('snapshots')).to.be.undefined
expect(logsToVerify[7].get('name')).to.equal('log 7')
expect(logsToVerify[7].get('snapshots')).to.have.length(1)
expect(logsToVerify[8].get('name')).to.equal('log 8')
expect(logsToVerify[8].get('snapshots')).to.have.length(1)
expect(logsToVerify[9].get('name')).to.equal('log 9')
expect(logsToVerify[9].get('snapshots')).to.have.length(1)
expect(logsToVerify[10].get('name')).to.equal('log 10')
expect(logsToVerify[10].get('snapshots')).to.have.length(1)
})
it('when run mode with protocol disabled', { numTestsKeptInMemory: 0, protocolEnabled: false }, () => {
// Verify the log is also fired in the primary origin.
expect(logsToVerify.length).to.eq(11)
@@ -280,41 +245,6 @@ context('cy.origin log', { browser: '!webkit' }, () => {
.wait(1500)
})
it('when run mode with protocol enabled', { numTestsKeptInMemory: 0, protocolEnabled: true }, () => {
// Verify the log is also fired in the primary origin.
expect(logsToVerify.length).to.eq(11)
expect(logsToVerify[1].get('name')).to.equal('log 1')
expect(logsToVerify[1].get('snapshots')).to.have.length(1)
expect(logsToVerify[2].get('name')).to.equal('log 2')
expect(logsToVerify[2].get('snapshots')).to.have.length(1)
expect(logsToVerify[3].get('name')).to.equal('log 3')
expect(logsToVerify[3].get('snapshots')).to.have.length(1)
expect(logsToVerify[4].get('name')).to.equal('log 4')
expect(logsToVerify[4].get('snapshots')).to.have.length(1)
expect(logsToVerify[5].get('name')).to.equal('log 5')
expect(logsToVerify[5].get('snapshots')).to.have.length(1)
expect(logsToVerify[6].get('name')).to.equal('log 6')
expect(logsToVerify[6].get('snapshots')).to.have.length(1)
expect(logsToVerify[7].get('name')).to.equal('log 7')
expect(logsToVerify[7].get('snapshots')).to.have.length(2)
expect(logsToVerify[8].get('name')).to.equal('log 8')
expect(logsToVerify[8].get('snapshots')).to.have.length(2)
expect(logsToVerify[9].get('name')).to.equal('log 9')
expect(logsToVerify[9].get('snapshots')).to.have.length(1)
expect(logsToVerify[10].get('name')).to.equal('log 10')
expect(logsToVerify[10].get('snapshots')).to.have.length(1)
})
it('when run mode with protocol disabled', { numTestsKeptInMemory: 0, protocolEnabled: false }, () => {
// Verify the log is also fired in the primary origin.
expect(logsToVerify.length).to.eq(11)
-1
View File
@@ -18,7 +18,6 @@
"@cypress/unique-selector": "0.0.5",
"@cypress/webpack-dev-server": "0.0.0-development",
"@cypress/webpack-preprocessor": "0.0.0-development",
"@medv/finder": "3.1.0",
"@packages/config": "0.0.0-development",
"@packages/network": "0.0.0-development",
"@packages/rewriter": "0.0.0-development",
+2 -148
View File
@@ -4,95 +4,12 @@ import type { $Cy } from '../cypress/cy'
import type { StateFunc } from '../cypress/state'
import $dom from '../dom'
import { create as createSnapshotsCSS } from './snapshots_css'
import { finder } from '@medv/finder'
import type { Log } from '../cypress/log'
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()
@@ -315,48 +232,7 @@ 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) => {
const createSnapshot = (name?, $elToHighlight?, preprocessedSnapshot?, relatedLog?: Log) => {
Cypress.action('cy:snapshot', name)
// when using cy.origin() and in a transitionary state, state('document')
// can be undefined, resulting in a bizarre snapshot of the entire Cypress
@@ -370,28 +246,6 @@ export const create = ($$: $Cy['$$'], state: StateFunc) => {
const timestamp = performance.now() + performance.timeOrigin
// if the protocol has been enabled, our snapshot is just the name, timestamp, and highlighted elements,
// also make sure numTestsKeptInMemory is 0, otherwise we will want the full snapshot
// (the driver test's set numTestsKeptInMemory to 1 in run mode to verify the snapshots)
if (Cypress.config('protocolEnabled') && Cypress.config('numTestsKeptInMemory') === 0) {
const snapshot: {
name: string
timestamp: number
elementsToHighlight?: {
selector: string | string []
frameId: string
}[]
} = { name, timestamp }
if (isJqueryElement($elToHighlight)) {
snapshot.elementsToHighlight = $dom.unwrap($elToHighlight).flatMap((el: HTMLElement) => buildSelectorArray(el))
}
Cypress.action('cy:protocol-snapshot')
return snapshot
}
try {
const {
$body,
+3 -3
View File
@@ -2,7 +2,7 @@ import _, { DebouncedFunc } from 'lodash'
import $ from 'jquery'
import clone from 'clone'
import { HIGHLIGHT_ATTR } from '../cy/snapshots'
import { HIGHLIGHT_ATTR, type ISnapshots } from '../cy/snapshots'
import $dom from '../dom'
import $utils from './utils'
import $errUtils from './error_utils'
@@ -239,7 +239,7 @@ const defaults = function (state: StateFunc, config, obj) {
}
export class Log {
createSnapshot: Function
createSnapshot: ISnapshots['createSnapshot']
state: StateFunc
config: any
fireChangeEvent: DebouncedFunc<((log) => (void | undefined))>
@@ -411,7 +411,7 @@ export class Log {
this.set('next', null)
}
const snapshot = this.createSnapshot(name, this.get('$el'))
const snapshot = this.createSnapshot(name, this.get('$el'), undefined, this)
this.addSnapshot(snapshot, options)
File diff suppressed because it is too large Load Diff
-5
View File
@@ -4761,11 +4761,6 @@
lodash "^4.17.15"
tmp-promise "^3.0.2"
"@medv/finder@3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@medv/finder/-/finder-3.1.0.tgz#e157c68f166ade9f113a1314603365bf81dd8b8c"
integrity sha512-ojkXjR3K0Zz3jnCR80tqPL+0yvbZk/lEodb6RIVjLz7W8RVA2wrw8ym/CzCpXO9SYVUIKHFUpc7jvf8UKfIM3w==
"@microsoft/fetch-event-source@2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz#9ceecc94b49fbaa15666e38ae8587f64acce007d"