wick-dom-observer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/main.yml +12 -0
- package/LICENSE +21 -0
- package/README.md +530 -0
- package/assets/log-observed-dissapear.png +0 -0
- package/assets/overview.png +0 -0
- package/assets/spinner.png +0 -0
- package/assets/timelines.png +0 -0
- package/assets/toast.png +0 -0
- package/cypress/e2e/modalTableDemo.cy.js +115 -0
- package/cypress/e2e/spinnerAndToast.cy.js +313 -0
- package/cypress/public/demo.html +847 -0
- package/cypress/public/modal-table-demo.html +454 -0
- package/cypress/support/commands.js +25 -0
- package/cypress/support/e2e.js +20 -0
- package/cypress.config.js +13 -0
- package/package.json +33 -0
- package/src/index.d.ts +144 -0
- package/src/index.js +2 -0
- package/src/utils.js +509 -0
- package/src/watcher.js +178 -0
package/src/utils.js
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true when value is a non-null plain object (and not an array).
|
|
3
|
+
*/
|
|
4
|
+
export function isPlainObject(value) {
|
|
5
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Detects whether a value looks like a Cypress click options object.
|
|
10
|
+
*
|
|
11
|
+
* The check is heuristic and based on known click option keys.
|
|
12
|
+
*/
|
|
13
|
+
export function isClickOptions(value) {
|
|
14
|
+
if (!isPlainObject(value)) return false
|
|
15
|
+
|
|
16
|
+
const knownKeys = [
|
|
17
|
+
'altKey',
|
|
18
|
+
'animationDistanceThreshold',
|
|
19
|
+
'button',
|
|
20
|
+
'cmdKey',
|
|
21
|
+
'ctrlKey',
|
|
22
|
+
'force',
|
|
23
|
+
'log',
|
|
24
|
+
'metaKey',
|
|
25
|
+
'multiple',
|
|
26
|
+
'release',
|
|
27
|
+
'scrollBehavior',
|
|
28
|
+
'shiftKey',
|
|
29
|
+
'timeout',
|
|
30
|
+
'waitForAnimations',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
return Object.keys(value).some((key) => knownKeys.includes(key))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns true when value is a supported Cypress click position string.
|
|
38
|
+
*/
|
|
39
|
+
export function isPosition(value) {
|
|
40
|
+
return (
|
|
41
|
+
typeof value === 'string' &&
|
|
42
|
+
[
|
|
43
|
+
'topLeft',
|
|
44
|
+
'top',
|
|
45
|
+
'topRight',
|
|
46
|
+
'left',
|
|
47
|
+
'center',
|
|
48
|
+
'right',
|
|
49
|
+
'bottomLeft',
|
|
50
|
+
'bottom',
|
|
51
|
+
'bottomRight',
|
|
52
|
+
].includes(value)
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Normalizes optional command arguments into a valid `.click()` signature.
|
|
58
|
+
*
|
|
59
|
+
* Supports:
|
|
60
|
+
* - []
|
|
61
|
+
* - [options]
|
|
62
|
+
* - [position]
|
|
63
|
+
* - [position, options]
|
|
64
|
+
* - [x, y]
|
|
65
|
+
* - [x, y, options]
|
|
66
|
+
*
|
|
67
|
+
* Throws when the shape is invalid.
|
|
68
|
+
*/
|
|
69
|
+
export function parseClickArgs(args) {
|
|
70
|
+
if (args.length === 0) return []
|
|
71
|
+
|
|
72
|
+
if (args.length === 1) {
|
|
73
|
+
const [a] = args
|
|
74
|
+
|
|
75
|
+
if (isClickOptions(a)) return [a]
|
|
76
|
+
if (isPosition(a)) return [a]
|
|
77
|
+
|
|
78
|
+
throw new Error(
|
|
79
|
+
'clickAndWatchForElement(): invalid arguments. Expected click options or a click position.'
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (args.length === 2) {
|
|
84
|
+
const [a, b] = args
|
|
85
|
+
|
|
86
|
+
if (isPosition(a) && isClickOptions(b)) return [a, b]
|
|
87
|
+
if (typeof a === 'number' && typeof b === 'number') return [a, b]
|
|
88
|
+
|
|
89
|
+
throw new Error(
|
|
90
|
+
'clickAndWatchForElement(): invalid arguments. Expected (position, options) or (x, y).'
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (args.length === 3) {
|
|
95
|
+
const [a, b, c] = args
|
|
96
|
+
|
|
97
|
+
if (typeof a === 'number' && typeof b === 'number' && isClickOptions(c)) {
|
|
98
|
+
return [a, b, c]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
throw new Error(
|
|
102
|
+
'clickAndWatchForElement(): invalid arguments. Expected (x, y, options).'
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new Error(
|
|
107
|
+
'clickAndWatchForElement(): too many arguments. Supported signatures are clickAndWatchForElement(config), clickAndWatchForElement(config, options), clickAndWatchForElement(config, position), clickAndWatchForElement(config, position, options), clickAndWatchForElement(config, x, y), clickAndWatchForElement(config, x, y, options).'
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Validates element-watch command configuration and throws descriptive errors
|
|
113
|
+
* when required properties are missing or malformed.
|
|
114
|
+
*/
|
|
115
|
+
export function validateSpinnerConfig(config) {
|
|
116
|
+
if (!config || typeof config !== 'object') {
|
|
117
|
+
throw new Error('clickAndWatchForElement(): spinner config is required.')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!config.selector || typeof config.selector !== 'string') {
|
|
121
|
+
throw new Error('clickAndWatchForElement(): config.selector must be a non-empty string.')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof config.assert !== 'function') {
|
|
125
|
+
throw new Error('clickAndWatchForElement(): config.assert must be a function.')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (config.action !== undefined && typeof config.action !== 'function') {
|
|
129
|
+
throw new Error('clickAndWatchForElement(): config.action must be a function when provided.')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (
|
|
133
|
+
config.appear !== undefined &&
|
|
134
|
+
config.appear !== 'optional' &&
|
|
135
|
+
config.appear !== 'required'
|
|
136
|
+
) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
'clickAndWatchForElement(): config.appear must be "optional" or "required".'
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (config.timeout !== undefined && config.timeout < 0) {
|
|
143
|
+
throw new Error('clickAndWatchForElement(): config.timeout must be >= 0.')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (config.pollingInterval !== undefined && config.pollingInterval <= 0) {
|
|
147
|
+
throw new Error('clickAndWatchForElement(): config.pollingInterval must be > 0.')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (
|
|
151
|
+
config.mustLast !== undefined &&
|
|
152
|
+
(typeof config.mustLast !== 'number' || config.mustLast < 0)
|
|
153
|
+
) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
'clickAndWatchForElement(): config.mustLast must be a number >= 0 when provided.'
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Executes assertion callback in "quiet" mode for polling/retry cycles.
|
|
162
|
+
*
|
|
163
|
+
* It suppresses noisy per-retry assertion logs while preserving pass/fail
|
|
164
|
+
* behavior (including support for negated assertions like `.not`).
|
|
165
|
+
*/
|
|
166
|
+
function doesAssertionPass(assertFn, $el) {
|
|
167
|
+
if (typeof assertFn !== 'function') return true
|
|
168
|
+
|
|
169
|
+
// Run retry assertions quietly so Cypress does not log every failed retry.
|
|
170
|
+
// This preserves behavior (same pass/fail + timeout), but reduces log noise.
|
|
171
|
+
const originalAssert = chai.Assertion.prototype.assert
|
|
172
|
+
chai.Assertion.prototype.assert = function (expr) {
|
|
173
|
+
const flags = this && this.__flags ? this.__flags : {}
|
|
174
|
+
const negate = Boolean(flags.negate)
|
|
175
|
+
const passed = negate ? !expr : Boolean(expr)
|
|
176
|
+
|
|
177
|
+
if (!passed) {
|
|
178
|
+
throw new Error('assertion failed')
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
assertFn($el)
|
|
184
|
+
return true
|
|
185
|
+
} catch (_error) {
|
|
186
|
+
return false
|
|
187
|
+
} finally {
|
|
188
|
+
chai.Assertion.prototype.assert = originalAssert
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Ensures a found element remains connected to the DOM for at least `mustLast`.
|
|
194
|
+
*
|
|
195
|
+
* The timing starts at `foundAt` (or now if omitted). Rejects if the element is
|
|
196
|
+
* removed before the minimum duration is reached.
|
|
197
|
+
*/
|
|
198
|
+
export function ensureSpinnerLastsAtLeast(
|
|
199
|
+
win,
|
|
200
|
+
node,
|
|
201
|
+
mustLast,
|
|
202
|
+
pollingInterval,
|
|
203
|
+
foundAt
|
|
204
|
+
) {
|
|
205
|
+
if (mustLast <= 0) return Cypress.Promise.resolve()
|
|
206
|
+
|
|
207
|
+
const appearedAt = foundAt != null ? foundAt : Date.now()
|
|
208
|
+
|
|
209
|
+
return new Cypress.Promise((resolve, reject) => {
|
|
210
|
+
function check() {
|
|
211
|
+
const elapsed = Date.now() - appearedAt
|
|
212
|
+
|
|
213
|
+
if (!node.isConnected) {
|
|
214
|
+
reject(
|
|
215
|
+
new Error(
|
|
216
|
+
`clickAndWatchForElement(): spinner was not visible for the minimum time (mustLast: ${mustLast}ms). It was no longer in the DOM when we checked (${elapsed}ms after it appeared).`
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (elapsed >= mustLast) {
|
|
223
|
+
resolve()
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
win.setTimeout(check, Math.min(pollingInterval, mustLast - elapsed))
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
check()
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Waits for an element to appear using a MutationObserver + polling fallback.
|
|
236
|
+
*
|
|
237
|
+
* Resolves as soon as an element matching `selector` exists and passes `assert`.
|
|
238
|
+
* If `mustLastOptions` is provided, starts minimum-duration validation immediately.
|
|
239
|
+
* Rejects on timeout.
|
|
240
|
+
*/
|
|
241
|
+
export function waitForSpinnerAppearWithObserver(
|
|
242
|
+
win,
|
|
243
|
+
selector,
|
|
244
|
+
timeout,
|
|
245
|
+
mustLastOptions,
|
|
246
|
+
pollingInterval,
|
|
247
|
+
assert,
|
|
248
|
+
action
|
|
249
|
+
) {
|
|
250
|
+
return new Cypress.Promise((resolve, reject) => {
|
|
251
|
+
let timeoutId
|
|
252
|
+
let pollId
|
|
253
|
+
let settled = false
|
|
254
|
+
|
|
255
|
+
function cleanup() {
|
|
256
|
+
settled = true
|
|
257
|
+
observer.disconnect()
|
|
258
|
+
if (timeoutId) win.clearTimeout(timeoutId)
|
|
259
|
+
if (pollId) win.clearTimeout(pollId)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function findFirstElement() {
|
|
263
|
+
const el = win.document.querySelector(selector)
|
|
264
|
+
if (!el) return null
|
|
265
|
+
|
|
266
|
+
const $el = Cypress.$(el)
|
|
267
|
+
return { el, $el }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function tryResolveFromDom() {
|
|
271
|
+
if (settled) return
|
|
272
|
+
|
|
273
|
+
const result = findFirstElement()
|
|
274
|
+
if (!result) return
|
|
275
|
+
|
|
276
|
+
const { el, $el } = result
|
|
277
|
+
|
|
278
|
+
if (!doesAssertionPass(assert, $el)) {
|
|
279
|
+
// Element exists but does not satisfy assertion yet.
|
|
280
|
+
// Keep waiting until it does or until timeout.
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (typeof action === 'function') {
|
|
285
|
+
try {
|
|
286
|
+
action($el)
|
|
287
|
+
} catch (error) {
|
|
288
|
+
cleanup()
|
|
289
|
+
reject(error)
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
cleanup()
|
|
295
|
+
|
|
296
|
+
const foundAt = Date.now()
|
|
297
|
+
let mustLastPromise = null
|
|
298
|
+
|
|
299
|
+
if (
|
|
300
|
+
mustLastOptions &&
|
|
301
|
+
mustLastOptions.mustLast != null &&
|
|
302
|
+
mustLastOptions.mustLast > 0
|
|
303
|
+
) {
|
|
304
|
+
mustLastPromise = ensureSpinnerLastsAtLeast(
|
|
305
|
+
win,
|
|
306
|
+
el,
|
|
307
|
+
mustLastOptions.mustLast,
|
|
308
|
+
mustLastOptions.pollingInterval,
|
|
309
|
+
foundAt
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
resolve({ $el, foundAt, mustLastPromise })
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function schedulePoll() {
|
|
317
|
+
if (settled) return
|
|
318
|
+
pollId = win.setTimeout(() => {
|
|
319
|
+
tryResolveFromDom()
|
|
320
|
+
schedulePoll()
|
|
321
|
+
}, pollingInterval)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const observer = new win.MutationObserver(() => {
|
|
325
|
+
tryResolveFromDom()
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
observer.observe(win.document.body, {
|
|
329
|
+
childList: true,
|
|
330
|
+
subtree: true,
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
// Try immediately in case spinner already exists.
|
|
334
|
+
tryResolveFromDom()
|
|
335
|
+
// Keep retrying assertions even without further DOM mutations.
|
|
336
|
+
schedulePoll()
|
|
337
|
+
|
|
338
|
+
timeoutId = win.setTimeout(() => {
|
|
339
|
+
cleanup()
|
|
340
|
+
reject(
|
|
341
|
+
new Error(
|
|
342
|
+
`clickAndWatchForElement(): spinner "${selector}" did not appear within ${timeout}ms.`
|
|
343
|
+
)
|
|
344
|
+
)
|
|
345
|
+
}, timeout)
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Runs the shared element watch lifecycle used by both commands.
|
|
351
|
+
*
|
|
352
|
+
* Flow:
|
|
353
|
+
* 1) wait for appear (optional or required),
|
|
354
|
+
* 2) optionally enforce `mustLast`,
|
|
355
|
+
* 3) optionally wait for disappearance.
|
|
356
|
+
*
|
|
357
|
+
* Also updates Cypress command logs when provided.
|
|
358
|
+
*/
|
|
359
|
+
export function runElementWatchFlow(options) {
|
|
360
|
+
const {
|
|
361
|
+
win,
|
|
362
|
+
selector,
|
|
363
|
+
timeout,
|
|
364
|
+
pollingInterval,
|
|
365
|
+
appear,
|
|
366
|
+
disappear,
|
|
367
|
+
mustLastOptions,
|
|
368
|
+
assert,
|
|
369
|
+
action,
|
|
370
|
+
log,
|
|
371
|
+
} = options
|
|
372
|
+
|
|
373
|
+
const appearPromise = waitForSpinnerAppearWithObserver(
|
|
374
|
+
win,
|
|
375
|
+
selector,
|
|
376
|
+
timeout,
|
|
377
|
+
mustLastOptions,
|
|
378
|
+
pollingInterval,
|
|
379
|
+
assert,
|
|
380
|
+
action
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
return appearPromise
|
|
384
|
+
.catch((error) => {
|
|
385
|
+
if (appear === 'optional') {
|
|
386
|
+
if (log) {
|
|
387
|
+
log.set({
|
|
388
|
+
message: `${selector} not observed (optional)`,
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
return null
|
|
392
|
+
}
|
|
393
|
+
throw error
|
|
394
|
+
})
|
|
395
|
+
.then((result) => {
|
|
396
|
+
if (!result) return false
|
|
397
|
+
|
|
398
|
+
const mustLastPromise =
|
|
399
|
+
result && result.mustLastPromise != null
|
|
400
|
+
? result.mustLastPromise
|
|
401
|
+
: null
|
|
402
|
+
|
|
403
|
+
if (mustLastPromise) {
|
|
404
|
+
return mustLastPromise.then(() => true)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return true
|
|
408
|
+
})
|
|
409
|
+
.then((wasObserved) => {
|
|
410
|
+
if (!wasObserved) return null
|
|
411
|
+
|
|
412
|
+
if (log) {
|
|
413
|
+
log.set({ message: `${selector} observed` })
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (!disappear) return null
|
|
417
|
+
|
|
418
|
+
return pollForSpinnerState({
|
|
419
|
+
win,
|
|
420
|
+
selector,
|
|
421
|
+
timeout,
|
|
422
|
+
pollingInterval,
|
|
423
|
+
mode: 'disappear',
|
|
424
|
+
}).then(() => {
|
|
425
|
+
if (log) {
|
|
426
|
+
log.set({ message: `${selector} observed and disappeared` })
|
|
427
|
+
}
|
|
428
|
+
expect(true, `${selector} disappeared`).to.be.true
|
|
429
|
+
})
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Polls for element state transitions.
|
|
435
|
+
*
|
|
436
|
+
* - `mode: 'appear'` waits for first matching element to exist (and optionally
|
|
437
|
+
* satisfy `assert`).
|
|
438
|
+
* - `mode: 'disappear'` waits until no matching visible element remains.
|
|
439
|
+
*
|
|
440
|
+
* Rejects when timeout is reached.
|
|
441
|
+
*/
|
|
442
|
+
export function pollForSpinnerState(options) {
|
|
443
|
+
const {
|
|
444
|
+
win,
|
|
445
|
+
selector,
|
|
446
|
+
timeout,
|
|
447
|
+
pollingInterval,
|
|
448
|
+
mode,
|
|
449
|
+
assert,
|
|
450
|
+
} = options
|
|
451
|
+
|
|
452
|
+
const startedAt = Date.now()
|
|
453
|
+
|
|
454
|
+
return new Cypress.Promise((resolve, reject) => {
|
|
455
|
+
function check() {
|
|
456
|
+
const el = win.document.querySelector(selector)
|
|
457
|
+
const elapsed = Date.now() - startedAt
|
|
458
|
+
|
|
459
|
+
if (mode === 'appear') {
|
|
460
|
+
if (el) {
|
|
461
|
+
const $el = Cypress.$(el)
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
if (assert) assert($el)
|
|
465
|
+
resolve($el)
|
|
466
|
+
return
|
|
467
|
+
} catch (error) {
|
|
468
|
+
// Spinner exists but assertion is not satisfied yet.
|
|
469
|
+
// Keep polling until timeout.
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (elapsed >= timeout) {
|
|
474
|
+
reject(
|
|
475
|
+
new Error(
|
|
476
|
+
`clickAndWatchForElement(): spinner "${selector}" did not appear and satisfy assertions within ${timeout}ms.`
|
|
477
|
+
)
|
|
478
|
+
)
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
win.setTimeout(check, pollingInterval)
|
|
483
|
+
return
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const matchingElements = Array.from(win.document.querySelectorAll(selector))
|
|
487
|
+
const hasVisibleElement = matchingElements.some((node) => Cypress.dom.isVisible(node))
|
|
488
|
+
|
|
489
|
+
// Treat spinner as disappeared when no matching element exists OR all are hidden.
|
|
490
|
+
if (matchingElements.length === 0 || !hasVisibleElement) {
|
|
491
|
+
resolve(null)
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (elapsed >= timeout) {
|
|
496
|
+
reject(
|
|
497
|
+
new Error(
|
|
498
|
+
`clickAndWatchForElement(): spinner "${selector}" did not disappear (removed or hidden) within ${timeout}ms.`
|
|
499
|
+
)
|
|
500
|
+
)
|
|
501
|
+
return
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
win.setTimeout(check, pollingInterval)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
check()
|
|
508
|
+
})
|
|
509
|
+
}
|
package/src/watcher.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import * as utils from './utils'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ensures `expect(subject).to.be.visible()` works consistently for subjects used
|
|
5
|
+
* inside this plugin (jQuery collections, native nodes, or nullable values).
|
|
6
|
+
*
|
|
7
|
+
* The command assertion callbacks receive sync values (not Cypress chains),
|
|
8
|
+
* so we normalize the subject to a jQuery collection and evaluate visibility via
|
|
9
|
+
* `Cypress.dom.isVisible()` for every matched element.
|
|
10
|
+
*/
|
|
11
|
+
function ensureVisibleAssertionSupport() {
|
|
12
|
+
chai.Assertion.addChainableMethod(
|
|
13
|
+
'visible',
|
|
14
|
+
function () {
|
|
15
|
+
const subject = this._obj
|
|
16
|
+
const $elements =
|
|
17
|
+
subject && subject.jquery
|
|
18
|
+
? subject
|
|
19
|
+
: subject != null
|
|
20
|
+
? Cypress.$(subject)
|
|
21
|
+
: Cypress.$()
|
|
22
|
+
|
|
23
|
+
const isVisible =
|
|
24
|
+
$elements.length > 0 &&
|
|
25
|
+
$elements.toArray().every((el) => Cypress.dom.isVisible(el))
|
|
26
|
+
|
|
27
|
+
this.assert(
|
|
28
|
+
isVisible,
|
|
29
|
+
'expected #{this} to be visible',
|
|
30
|
+
'expected #{this} not to be visible'
|
|
31
|
+
)
|
|
32
|
+
},
|
|
33
|
+
function () { }
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ensureVisibleAssertionSupport()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
// -----------------------------------------------------------------------
|
|
41
|
+
// CUSTOM COMMAND clickAndWatchForElement
|
|
42
|
+
// -----------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Clicks the target subject and watches for the configured element lifecycle.
|
|
46
|
+
*
|
|
47
|
+
* The command starts observing before click to reliably catch fast elements,
|
|
48
|
+
* then applies the same watch flow rules (appear optional/required, optional
|
|
49
|
+
* minimum duration, and optional disappearance check).
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
Cypress.Commands.add(
|
|
53
|
+
'clickAndWatchForElement',
|
|
54
|
+
{ prevSubject: 'element' },
|
|
55
|
+
(subject, config, ...rawArgs) => {
|
|
56
|
+
utils.validateSpinnerConfig(config)
|
|
57
|
+
|
|
58
|
+
const clickArgs = utils.parseClickArgs(rawArgs)
|
|
59
|
+
const timeout =
|
|
60
|
+
config.timeout != null
|
|
61
|
+
? config.timeout
|
|
62
|
+
: Cypress.config('defaultCommandTimeout')
|
|
63
|
+
const pollingInterval =
|
|
64
|
+
config.pollingInterval != null ? config.pollingInterval : 10
|
|
65
|
+
const appear = config.appear || 'optional'
|
|
66
|
+
const disappear = config.disappear || false
|
|
67
|
+
const mustLast = config.mustLast
|
|
68
|
+
|
|
69
|
+
const log = Cypress.log({
|
|
70
|
+
name: 'clickAndWatchForElement',
|
|
71
|
+
message: config.selector,
|
|
72
|
+
consoleProps() {
|
|
73
|
+
return {
|
|
74
|
+
selector: config.selector,
|
|
75
|
+
timeout,
|
|
76
|
+
pollingInterval,
|
|
77
|
+
appear,
|
|
78
|
+
disappear,
|
|
79
|
+
mustLast,
|
|
80
|
+
clickArgs,
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const mustLastOptions =
|
|
86
|
+
appear === 'required' && mustLast != null && mustLast > 0
|
|
87
|
+
? { mustLast, pollingInterval }
|
|
88
|
+
: null
|
|
89
|
+
|
|
90
|
+
return cy.window({ log: false, timeout }).then((win) => {
|
|
91
|
+
// Start observing before click to catch very short-lived elements.
|
|
92
|
+
const watchFlowPromise = utils.runElementWatchFlow({
|
|
93
|
+
win,
|
|
94
|
+
selector: config.selector,
|
|
95
|
+
timeout,
|
|
96
|
+
pollingInterval,
|
|
97
|
+
appear,
|
|
98
|
+
disappear,
|
|
99
|
+
mustLastOptions,
|
|
100
|
+
assert: config.assert,
|
|
101
|
+
action: config.action,
|
|
102
|
+
log,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
return cy
|
|
106
|
+
.wrap(subject, { log: false, timeout })
|
|
107
|
+
.click(...clickArgs)
|
|
108
|
+
.then({ timeout }, () => watchFlowPromise)
|
|
109
|
+
.then(() => cy.wrap(subject, { log: false, timeout }))
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
// -----------------------------------------------------------------------
|
|
116
|
+
// CUSTOM COMMAND watchForElement
|
|
117
|
+
// -----------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Watches for the configured element lifecycle without any click action.
|
|
121
|
+
*
|
|
122
|
+
* Use this when the element may appear automatically (for example on page
|
|
123
|
+
* load, delayed startup banners, or background-triggered signals).
|
|
124
|
+
*/
|
|
125
|
+
Cypress.Commands.add('watchForElement', (config) => {
|
|
126
|
+
utils.validateSpinnerConfig(config)
|
|
127
|
+
|
|
128
|
+
const timeout =
|
|
129
|
+
config.timeout != null
|
|
130
|
+
? config.timeout
|
|
131
|
+
: Cypress.config('defaultCommandTimeout')
|
|
132
|
+
const pollingInterval =
|
|
133
|
+
config.pollingInterval != null ? config.pollingInterval : 10
|
|
134
|
+
const appear = config.appear || 'optional'
|
|
135
|
+
const disappear = config.disappear || false
|
|
136
|
+
const mustLast = config.mustLast
|
|
137
|
+
|
|
138
|
+
const log = Cypress.log({
|
|
139
|
+
name: 'watchForElement',
|
|
140
|
+
message: config.selector,
|
|
141
|
+
consoleProps() {
|
|
142
|
+
return {
|
|
143
|
+
selector: config.selector,
|
|
144
|
+
timeout,
|
|
145
|
+
pollingInterval,
|
|
146
|
+
appear,
|
|
147
|
+
disappear,
|
|
148
|
+
mustLast,
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const mustLastOptions =
|
|
154
|
+
appear === 'required' && mustLast != null && mustLast > 0
|
|
155
|
+
? { mustLast, pollingInterval }
|
|
156
|
+
: null
|
|
157
|
+
|
|
158
|
+
return cy.window({ log: false, timeout }).then((win) => {
|
|
159
|
+
const watchFlowPromise = utils.runElementWatchFlow({
|
|
160
|
+
win,
|
|
161
|
+
selector: config.selector,
|
|
162
|
+
timeout,
|
|
163
|
+
pollingInterval,
|
|
164
|
+
appear,
|
|
165
|
+
disappear,
|
|
166
|
+
mustLastOptions,
|
|
167
|
+
assert: config.assert,
|
|
168
|
+
action: config.action,
|
|
169
|
+
log,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
return cy
|
|
173
|
+
.wrap(null, { log: false, timeout })
|
|
174
|
+
.then({ timeout }, () => watchFlowPromise)
|
|
175
|
+
.then(() => cy.wrap(null, { log: false, timeout }))
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|