",
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/bahmutov/cypress-cdp/issues"
27 | },
28 | "homepage": "https://github.com/bahmutov/cypress-cdp#readme",
29 | "devDependencies": {
30 | "cypress": "13.17.0",
31 | "prettier": "3.5.3",
32 | "semantic-release": "24.2.5",
33 | "typescript": "^5.6.3"
34 | },
35 | "dependencies": {
36 | "devtools-protocol": "^0.0.1468520"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/public/app.js:
--------------------------------------------------------------------------------
1 | const btn_one = document.querySelector('#one')
2 | const btn_two = document.querySelector('#two')
3 | const btn_three = document.querySelector('#three')
4 | const output_one = document.querySelector('#output')
5 | const output_two = document.querySelector('#output-two')
6 | const output_three = document.querySelector('#output-three')
7 |
8 | // add event listeners after a short delay
9 | setTimeout(() => {
10 | btn_one.addEventListener('click', () => {
11 | output.innerText = 'clicked'
12 | })
13 | }, 1000)
14 |
15 | // add event listeners after a long delay
16 | setTimeout(() => {
17 | btn_two.addEventListener('click', () => {
18 | output_two.innerText = 'clicked'
19 | })
20 | }, 5000)
21 |
22 | setTimeout(() => {
23 | let new_button = document.createElement('button')
24 | new_button.id = 'three'
25 | new_button.innerText = 'Click me three!'
26 | new_button.addEventListener('click', () => {
27 | output_three.innerText = 'clicked'
28 | })
29 | btn_three.replaceWith(new_button)
30 | }, 1000)
31 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 | This is the text
11 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 |
22 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/public/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Satisfy', cursive;
3 | div:not(:last-child) {
4 | margin-bottom: 1rem;
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"],
3 | "automerge": true,
4 | "major": {
5 | "automerge": false
6 | },
7 | "minor": {
8 | "automerge": true
9 | },
10 | "prConcurrentLimit": 3,
11 | "prHourlyLimit": 2,
12 | "schedule": ["after 10pm and before 5am on every weekday", "every weekend"],
13 | "masterIssue": true,
14 | "labels": ["type: dependencies", "renovate"]
15 | }
16 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for cypress-cdp 1.3
2 | // Project: https://github.com/bahmutov/cypress-cdp#readme
3 | // Definitions by: Anthony Froissant
4 | // Louis Loiseau-Billon
5 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
6 | // copied from https://github.com/DefinitelyTyped/DefinitelyTyped/pull/66237
7 |
8 | ///
9 |
10 | import type Protocol from 'devtools-protocol/types/protocol'
11 | import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping'
12 |
13 | declare global {
14 | // Augment the Cypress namespace to include type definitions for cypress-cdp.
15 | namespace Cypress {
16 | namespace CDP {
17 | //#region CDP
18 | type RdpCommands = ProtocolMapping.Commands
19 | type RdpCommandNames = keyof RdpCommands
20 |
21 | type CdpCommandFnParams =
22 | RdpCommands[RdpCommandName]['paramsType'][number] extends never
23 | ? Record
24 | : RdpCommands[RdpCommandName]['paramsType'][number]
25 | type CdpCommandFnReturnType =
26 | RdpCommands[RdpCommandName]['returnType']
27 | interface CdpCommandFnOptions {
28 | log: LogConfig
29 | }
30 | type CdpCommandFn = (
31 | rdpCommand: RdpCommandName,
32 | params?: CdpCommandFnParams,
33 | options?: CdpCommandFnOptions,
34 | ) => Chainable>
35 | //#endregion
36 |
37 | //#region getCDPNodeId
38 | type getCDPNodeIdFn = (selector: string) => Chainable
39 | //#endregion
40 |
41 | //#region hasEventListeners
42 | interface hasEventListenersFnOptions {
43 | log?: LogConfig
44 | timeout?: number
45 | type?: string
46 | }
47 | type hasEventListenersFn = (
48 | selector: string,
49 | options?: hasEventListenersFnOptions,
50 | ) => Chainable
51 | //#endregion
52 | }
53 |
54 | interface Chainable {
55 | /**
56 | * Custom command to send a RDP command to Chrome DevTools.
57 | * @example
58 | * const selector = 'button#one'
59 | * cy.CDP('Runtime.evaluate', {
60 | * expression: 'frames[0].document.querySelector("' + selector + '")',
61 | * }).should((v) => {
62 | * expect(v.result).to.have.property('objectId')
63 | * })
64 | */
65 | CDP: CDP.CdpCommandFn
66 |
67 | /**
68 | * Custom command to return the internal element's Node Id that can be used to query the run-time properties, for example the rendered font.
69 | * @example
70 | * cy.getCDPNodeId('body').then((nodeId) => {
71 | * cy.CDP('CSS.getPlatformFontsForNode', {
72 | * nodeId
73 | * });
74 | * });
75 | */
76 | getCDPNodeId: CDP.getCDPNodeIdFn
77 |
78 | /**
79 | * Custom command to get listeners attached to a DOM element.
80 | * @example
81 | * cy.hasEventListeners('button#one')
82 | * @example
83 | * cy.hasEventListeners('button#one', { type: 'click' })
84 | */
85 | hasEventListeners: CDP.hasEventListenersFn
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | Cypress.Commands.add('CDP', (rdpCommand, params, options = {}) => {
4 | const logOptions = {
5 | name: 'CDP',
6 | message: rdpCommand,
7 | }
8 |
9 | if (rdpCommand === 'DOM.querySelector') {
10 | logOptions.message += ' ' + params.selector
11 | } else {
12 | const limitN = 60
13 | const paramsStringified = params ? JSON.stringify(params) : ''
14 | logOptions.message +=
15 | paramsStringified.length > limitN
16 | ? ' ' + paramsStringified.slice(0, limitN) + '...'
17 | : ' ' + paramsStringified
18 | }
19 |
20 | let log
21 | if (options.log !== false) {
22 | log = Cypress.log(logOptions)
23 | }
24 |
25 | const getValue = () => {
26 | return Cypress.automation('remote:debugger:protocol', {
27 | command: rdpCommand,
28 | params,
29 | })
30 | }
31 |
32 | const resolveValue = () => {
33 | return Cypress.Promise.try(getValue).then((value) => {
34 | return cy.verifyUpcomingAssertions(value, options, {
35 | onRetry: resolveValue,
36 | })
37 | })
38 | }
39 |
40 | return resolveValue().then((value) => {
41 | if (options.log !== false) {
42 | logOptions.consoleProps = () => {
43 | return {
44 | result: value,
45 | }
46 | }
47 | log.snapshot().end()
48 | }
49 |
50 | return value
51 | })
52 | })
53 |
54 | let eventListenerStatus // track most recent failure mode
55 | let eventListenerRun
56 | let maxTimeout
57 | Cypress.Commands.add('hasEventListeners', (selector, options = {}) => {
58 | let retryOpts
59 | if (options.timeout === 0) {
60 | // is a retry
61 | retryOpts = options
62 | } else {
63 | // set up the timer
64 | eventListenerStatus = ''
65 | let runId = Date.now()
66 | eventListenerRun = runId // prevent failing the next test
67 | maxTimeout = options.timeout ?? Cypress.config().defaultCommandTimeout
68 | retryOpts = { ...options, timeout: 0, log: false }
69 | setTimeout(() => {
70 | if (eventListenerRun === runId && eventListenerStatus !== 'Passed') {
71 | throw new Error(eventListenerStatus)
72 | }
73 | }, maxTimeout)
74 | }
75 | const logOptions = {
76 | name: 'hasEventListeners',
77 | message: `checking element "${selector}"`,
78 | }
79 | let log
80 | if (options.log !== false) {
81 | log = Cypress.log(logOptions)
82 | }
83 |
84 | cy.get(selector, { log: false, timeout: maxTimeout }).should(($el) => {
85 | if ($el.length !== 1) {
86 | throw new Error(`Need a single element with selector "${selector}`)
87 | }
88 | })
89 |
90 | const escapedSelector = JSON.stringify(selector)
91 | cy.CDP(
92 | 'Runtime.evaluate',
93 | {
94 | expression: 'Cypress.$(' + escapedSelector + ')[0]',
95 | },
96 | { log: false, timeout: maxTimeout },
97 | )
98 | .should((v) => {
99 | if (!v || !v.result || !v.result.objectId) {
100 | throw new Error(`Cannot find element "${selector}"`)
101 | }
102 | })
103 | .then((v) => {
104 | const objectId = v.result.objectId
105 | cy.CDP(
106 | 'DOMDebugger.getEventListeners',
107 | {
108 | objectId,
109 | depth: -1,
110 | pierce: true,
111 | },
112 | {
113 | log: false,
114 | },
115 | ).then((v) => {
116 | if (!v.listeners) {
117 | eventListenerStatus = 'No listeners'
118 | cy.hasEventListeners(selector, retryOpts)
119 | return
120 | }
121 | if (!v.listeners.length) {
122 | eventListenerStatus = 'Zero listeners'
123 | cy.hasEventListeners(selector, retryOpts)
124 | return
125 | }
126 | if (options.type) {
127 | const filtered = v.listeners.filter((l) => l.type === options.type)
128 | if (!filtered.length) {
129 | eventListenerStatus = `Zero listeners of type "${options.type}"`
130 | cy.hasEventListeners(selector, retryOpts)
131 | return
132 | }
133 | }
134 | eventListenerStatus = 'Passed'
135 | if (options.log !== false) {
136 | logOptions.consoleProps = () => {
137 | return {
138 | result: v.listeners,
139 | }
140 | }
141 | }
142 | })
143 | })
144 | })
145 |
146 | Cypress.Commands.add('getCDPNodeId', (selector) => {
147 | cy.CDP('DOM.enable')
148 | cy.CDP('CSS.enable')
149 | cy.CDP('DOM.getDocument', {
150 | depth: 50,
151 | pierce: true,
152 | }).then((doc) => {
153 | // let's get the application iframe
154 | cy.CDP('DOM.querySelector', {
155 | nodeId: doc.root.nodeId,
156 | selector: 'iframe.aut-iframe',
157 | }).then((iframeQueryResult) => {
158 | cy.CDP('DOM.describeNode', {
159 | nodeId: iframeQueryResult.nodeId,
160 | }).then((iframeDescription) => {
161 | cy.CDP('DOM.resolveNode', {
162 | backendNodeId: iframeDescription.node.contentDocument.backendNodeId,
163 | }).then((contentDocRemoteObject) => {
164 | cy.CDP('DOM.requestNode', {
165 | objectId: contentDocRemoteObject.object.objectId,
166 | }).then((contentDocNode) => {
167 | cy.CDP('DOM.querySelector', {
168 | nodeId: contentDocNode.nodeId,
169 | selector,
170 | }).its('nodeId')
171 | })
172 | })
173 | })
174 | })
175 | })
176 | })
177 |
--------------------------------------------------------------------------------