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.
@@ -0,0 +1,12 @@
1
+ name: Cypress Tests
2
+
3
+ on: push
4
+
5
+ jobs:
6
+ cypress-run:
7
+ runs-on: ubuntu-22.04
8
+ steps:
9
+ - name: Checkout
10
+ uses: actions/checkout@v4
11
+ - name: Cypress run
12
+ uses: cypress-io/github-action@v6
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,530 @@
1
+ # wick-dom-observer
2
+
3
+ This plugin adds two Cypress commands to reliably detect UI elements that may appear/disappear quickly: `clickAndWatchForElement` (click + observe) and `watchForElement` (observe only). It supports required/optional appearance, optional disappearance checks, custom timeout/polling, and minimum visible duration (`mustLast`) with a synchronous assertion callback.
4
+
5
+ ![wick-dom-observer overview](assets/overview.png)
6
+
7
+ This is useful when, for example, a spinner may appear and disappear too quickly for a normal Cypress assertion like:
8
+
9
+ ```js
10
+ cy.get('button').click()
11
+ cy.get('.spinner').should('exist')
12
+ ```
13
+
14
+ ## Use Cases
15
+
16
+ Use this plugin to reliably test short-lived UI feedback such as **spinners**, **toasts**, and **popup panels**.
17
+
18
+ - Validate elements that appear/disappear too fast for normal Cypress retries and Cypress queue.
19
+ - Verify actions (save/submit/pay) show feedback before the UI is interactive again.
20
+ - Reduce flakiness when loaders/notifications exist only for milliseconds.
21
+ - Support optional indicators (appear in some states, absent in others) with `appear: 'optional'`.
22
+ - Enforce required appearance/disappearance windows with `appear: 'required'` + `disappear: true`.
23
+ - Ensure an element stays visible long enough with `mustLast`.
24
+
25
+ Useful for page-load announcement panels that may appear for some users but not others: one test can handle both valid outcomes without flaky failures.
26
+
27
+ ### Examples
28
+
29
+ - **Save Profile:** spinner flashes for ~40ms; `clickAndWatchForElement` still catches it.
30
+ - **Generate Report:** overlay appears while export starts; test confirms visible feedback first.
31
+ - **Page Load Announcement:** popup may appear or not; `watchForElement` + `appear: 'optional'` keeps one stable flow.
32
+ - **Delete Item:** warning badge appears only when related records exist; optional mode covers both paths.
33
+ - **Pay Now:** processing indicator must appear then disappear; required + disappear enforces timing.
34
+ - **2FA Verification:** spinner must remain visible at least `800ms`; `mustLast: 800` validates UX.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ npm install --save-dev wick-dom-observer
40
+ ```
41
+
42
+ ## Register the command
43
+
44
+ In your Cypress support file:
45
+
46
+ ```js
47
+ require('wick-dom-observer')
48
+ ```
49
+
50
+ ## Command API
51
+
52
+ ```js
53
+ cy.get(subject).clickAndWatchForElement(config)
54
+ cy.get(subject).clickAndWatchForElement(config, options)
55
+ cy.get(subject).clickAndWatchForElement(config, position)
56
+ cy.get(subject).clickAndWatchForElement(config, position, options)
57
+ cy.get(subject).clickAndWatchForElement(config, x, y)
58
+ cy.get(subject).clickAndWatchForElement(config, x, y, options)
59
+
60
+ cy.watchForElement(config)
61
+ ```
62
+
63
+ ## Config
64
+
65
+ The `config` argument is mandatory for both `clickAndWatchForElement` and `watchForElement`:
66
+
67
+ | Option | Mandatory | Default | Description |
68
+ |------------|-----------|---------|-------------|
69
+ | `selector` | **Yes** | — | CSS selector for the spinner element to poll for. Must be a non-empty string. |
70
+ | `assert` | **Yes** | — | **Synchronous callback** `($el) => { ... }` that runs when the spinner is found. Use it to assert on the element (e.g. visibility, class, text). |
71
+ | `action` | No | — | **Synchronous callback** `($el) => { ... }` executed **after `assert` passes**. Useful for side effects like clicking a close button. If `assert` is not executed, `action` is skipped. |
72
+ | `timeout` | No | `Cypress.config('defaultCommandTimeout')` | Max time (ms) to wait for the spinner to appear or disappear. Must be ≥ 0. |
73
+ | `appear` | No | `'optional'` | `'optional'` — spinner may or may not appear; `'required'` — command fails if the spinner does not appear and satisfy `assert` within `timeout`. |
74
+ | `disappear`| No | `false` | If `true`, after the spinner appears and passes `assert`, the command also waits for it to disappear within `timeout`. |
75
+ | `pollingInterval` | No | `10` | Polling interval in ms (used for the disappear phase). Must be > 0. |
76
+ | `mustLast` | No | — | When `appear` is `'required'`, minimum time (ms) the spinner must stay in the DOM. If it is removed before `mustLast` ms, the command fails with an error. Must be ≥ 0 when provided. |
77
+
78
+ ### Assert and Action Callback Functions
79
+
80
+ The `assert` callback must be synchronous.
81
+
82
+ The `action` callback (when provided) must also be synchronous.
83
+
84
+ Execution order is: `assert($el)` -> `action($el)`.
85
+
86
+ If `assert` is not executed (for example when `appear: 'optional'` and the element never appears), `action` is skipped.
87
+
88
+ Do not use Cypress commands inside `assert` or `action` (e.g., `cy.wrap()`, `cy.should()`); use `expect()` assertions plus plain JavaScript or **jQuery** logic.
89
+
90
+
91
+ ### Relationship between `timeout` and Cypress default timeout
92
+
93
+ - **Default behavior:** If `config.timeout` is omitted, both commands use Cypress `defaultCommandTimeout` for internal waits.
94
+ - **Plugin timeout scope:** `config.timeout` controls the command's internal appear/disappear wait windows.
95
+ - **Cypress chain scope:** Cypress still enforces command-chain timeout limits for both:
96
+ - `cy.get(...).clickAndWatchForElement(...)`
97
+ - `cy.watchForElement(...)`
98
+
99
+ > ⚠️ **Important (`clickAndWatchForElement`)**: setting `{ timeout: ... }` only on the parent `cy.get(...)` does **not** extend the full command flow.
100
+ >
101
+ > ⚠️ **Important (`watchForElement`)**: there is no parent `cy.get(...)`; the command chain timeout is determined by Cypress command timeout rules and the command's own `config.timeout`.
102
+
103
+ #### Timeline view
104
+
105
+ ![clickAndWatchForElement timeout timeline](assets/timelines.png)
106
+
107
+ The timeline above maps how timeout values are consumed during the command flow:
108
+
109
+ 1. `cy.get(...)` resolves the subject using Cypress timeout rules.
110
+ 2. `clickAndWatchForElement(...)` starts observation before click.
111
+ 3. Click happens.
112
+ 4. Appear phase runs until spinner is observed (or timeout).
113
+ 5. Optional disappear phase runs when `disappear: true`.
114
+
115
+ For `watchForElement(...)`, steps are the same except there is no click step (it starts observing immediately).
116
+
117
+ Practical rule: keep `defaultCommandTimeout` high enough for the full command chain, and use `config.timeout` to tune element-watch behavior.
118
+
119
+
120
+ > ⚠️ **Recommended setup for longer waits:** set `defaultCommandTimeout` at test (or suite) level for slow spinners/elements testing.
121
+
122
+
123
+ #### Example 1:
124
+ `defaultCommandTimeout: 10000`, `appear` not provided (by default `"optional"`) and `disappear` not provided (by default `false`):
125
+
126
+ ```js
127
+ it('waits for spinner', { defaultCommandTimeout: 10000 }, () => {
128
+ cy.get('button').clickAndWatchForElement({
129
+ selector: '.spinner',
130
+ timeout: 10000, // plugin internal timeout (can be omitted if same as defaultCommandTimeout)
131
+ assert: ($el) => {
132
+ expect($el).to.be.visible()
133
+ },
134
+ })
135
+ })
136
+ ```
137
+
138
+ What this test does:
139
+
140
+ - Sets Cypress command timeout to `10000ms` for this test only.
141
+ - Clicks `button`.
142
+ - Waits up to `10000ms` for `.spinner` to appear and satisfy `assert`.
143
+
144
+ Expected result:
145
+
146
+ - **Passes** if the spinner becomes visible within 10s.
147
+ - **Fails** with a clickAndWatchForElement timeout error if spinner does not appear (or does not satisfy `assert`) within 10s.
148
+
149
+ #### Example 2:
150
+ `defaultCommandTimeout` not provided (by default Cypress.config `defaultCommandTimeout`), `appear:"optional"` and `disappear: false`:
151
+
152
+ ```js
153
+ it('handles optional spinner without waiting for disappearance', () => {
154
+ cy.get('[data-cy="save-button"]').clickAndWatchForElement({
155
+ selector: '.loading-spinner',
156
+ timeout: 1000,
157
+ pollingInterval: 10,
158
+ appear: 'optional',
159
+ disappear: false,
160
+ assert: ($el) => {
161
+ expect($el).to.be.visible()
162
+ },
163
+ })
164
+ })
165
+ ```
166
+
167
+ What this full example does:
168
+
169
+ - Clicks `[data-cy="save-button"]`.
170
+ - Checks for `.loading-spinner`.
171
+ - If spinner appears within `1000ms`, it must be visible (`assert`).
172
+ - If spinner does not appear, the command still passes because `appear: 'optional'`.
173
+ - It does **not** wait for disappearance because `disappear: false`.
174
+
175
+ Expected result:
176
+
177
+ - **Passes** when spinner appears and is visible.
178
+ - **Also passes** when spinner does not appear at all (optional behavior).
179
+ - **Fails** only if spinner appears but does not satisfy `assert` before timeout.
180
+
181
+ #### Example 3:
182
+ Validate that a super-fast spinner is required to appear and then required to disappear, with multiple assertions, when clicking on a canvas at specific x, y coordinates:
183
+
184
+ ```js
185
+ it('validates super-fast spinner after canvas click coordinates', () => {
186
+ cy.get('#spinnerCanvas').clickAndWatchForElement(
187
+ {
188
+ selector: '.spinner',
189
+ appear: 'required',
190
+ disappear: true,
191
+ timeout: 1200,
192
+ pollingInterval: 10,
193
+ assert: ($el) => {
194
+ expect($el).to.be.visible()
195
+ expect($el).to.have.length(1)
196
+ expect($el).to.have.class('spinner')
197
+ expect($el).to.have.class('loading')
198
+ expect($el).to.have.attr('data-from', 'spinnerCanvas')
199
+ expect($el.text()).to.eq('Loading')
200
+ },
201
+ },
202
+ 20,
203
+ 20
204
+ )
205
+ })
206
+ ```
207
+
208
+ What this full example does:
209
+
210
+ - Clicks the canvas at coordinates `(20, 20)`.
211
+ - Requires `.spinner` to appear (`appear: 'required'`).
212
+ - Runs multiple assertions in `assert` to validate visibility, classes, source attribute, and text.
213
+ - Waits for spinner disappearance because `disappear: true`.
214
+
215
+ Expected result:
216
+
217
+ - **Passes** when the fast spinner appears, satisfies all assertions, and disappears in time.
218
+ - **Fails** if spinner never appears, does not match expected attributes/content, or does not disappear before timeout.
219
+
220
+ #### Example 4:
221
+ Watch an element that may appear on page load (no click action):
222
+
223
+ ```js
224
+ it('observes startup banner if it appears', () => {
225
+ cy.visit('/modal-table-demo.html')
226
+
227
+ cy.watchForElement({
228
+ selector: '[data-cy="ad-overlay"]',
229
+ appear: 'optional',
230
+ disappear: false,
231
+ timeout: 1500,
232
+ pollingInterval: 10,
233
+ assert: ($el) => {
234
+ expect($el).to.be.visible()
235
+ },
236
+ })
237
+ })
238
+ ```
239
+
240
+ What this full example does:
241
+
242
+ - Visits `/modal-table-demo.html`.
243
+ - Watches for `[data-cy="ad-overlay"]` without any click action.
244
+ - Treats appearance as optional (`appear: 'optional'`), so the test supports both states.
245
+ - If the element appears within timeout, it must be visible.
246
+
247
+ Expected result:
248
+
249
+ - **Passes** when the overlay appears and satisfies the visibility assertion.
250
+ - **Also passes** when the overlay does not appear at all (valid optional behavior).
251
+ - **Fails** only if the overlay appears but does not satisfy `assert` before timeout.
252
+
253
+
254
+
255
+ #### Example 5:
256
+ **Important flow:** handle optional page-load announcement overlay and still validate the required data-loading spinner happening in the background:
257
+
258
+ ```js
259
+ it('handles optional startup overlay and validates load-data spinner flow', () => {
260
+ cy.visit('/modal-table-demo.html')
261
+
262
+ // Overlay appears only in some runs/users, so keep it optional.
263
+ cy.watchForElement({
264
+ selector: '[data-cy="ad-overlay"]',
265
+ appear: 'optional',
266
+ disappear: false,
267
+ timeout: 1500,
268
+ pollingInterval: 10,
269
+ assert: ($el) => {
270
+ expect($el).to.be.visible()
271
+ expect($el.find('[data-cy="close-ad-btn"]')).to.be.visible()
272
+ },
273
+ action: ($el) => {
274
+ // Close the overlay via DOM click and verify the same `$el` is no longer visible.
275
+ const closeBtnEl = $el.find('[data-cy="close-ad-btn"]')[0]
276
+ if (closeBtnEl) closeBtnEl.click()
277
+ expect($el).to.not.be.visible()
278
+ },
279
+ })
280
+
281
+ // Validate that the load-data spinner appears, matches UI expectations, and then disappears.
282
+ cy.get('[data-cy="load-data-btn"]').clickAndWatchForElement({
283
+ selector: '[data-cy="service-spinner"]',
284
+ appear: 'required',
285
+ disappear: true,
286
+ timeout: 10000,
287
+ assert: ($el) => {
288
+ expect($el).to.be.visible()
289
+ expect($el.closest('body').find('[data-cy="loading-row"] .loading-label')).to.be.visible()
290
+ expect($el.closest('body').find('[data-cy="load-data-btn"]').prop('disabled')).to.eq(true)
291
+ },
292
+ })
293
+
294
+ // Additional assertions on the page: After completion, verify loading UI is gone and the action button is enabled again.
295
+ cy.get('.loading-label').should('not.be.visible')
296
+ cy.get('[data-cy="load-data-btn"]').should('be.enabled')
297
+ })
298
+ ```
299
+
300
+ What this full example does:
301
+
302
+ - Visits the page where the startup overlay may appear or not.
303
+ - Uses `watchForElement` with `appear: 'optional'` to support both valid startup states.
304
+ - Uses `action` to close the overlay only when `assert` passes, then verifies the overlay is hidden.
305
+ - Validates the main load-data spinner as **required** and waits for disappearance.
306
+ - Confirms post-load UI state (`.loading-label` hidden and button enabled).
307
+
308
+ Expected result:
309
+
310
+ - **Passes** when overlay appears and is handled, then spinner flow completes successfully.
311
+ - **Also passes** when overlay does not appear and spinner flow still completes successfully.
312
+ - **Fails** if optional overlay appears but is invalid, or if required spinner behavior/related assertions fail.
313
+
314
+ #### Example 6:
315
+ Require a toast to appear and disappear after a click:
316
+
317
+ ```js
318
+ it('validates created toast appears and is removed', () => {
319
+ cy.get('#toastBtn1').clickAndWatchForElement({
320
+ selector: '.toast[data-from="toastBtn1"]',
321
+ appear: 'required',
322
+ disappear: true,
323
+ timeout: 4000,
324
+ pollingInterval: 10,
325
+ assert: ($el) => {
326
+ expect($el).to.be.visible()
327
+ expect($el).to.have.attr('data-from', 'toastBtn1')
328
+ expect($el.find('.toast-message').text()).to.contain('removed after 1000ms')
329
+ },
330
+ })
331
+ })
332
+ ```
333
+
334
+ What this full example does:
335
+
336
+ - Clicks `#toastBtn1` to trigger a toast.
337
+ - Requires the toast to appear (`appear: 'required'`).
338
+ - Asserts visibility, source attribute, and expected message text.
339
+ - Waits for disappearance because `disappear: true`.
340
+
341
+ Expected result:
342
+
343
+ - **Passes** when the toast appears with the expected content and disappears within timeout.
344
+ - **Fails** if the toast does not appear, does not match assertions, or does not disappear in time.
345
+
346
+
347
+ ## Other Examples
348
+
349
+ ### Basic
350
+
351
+ ```js
352
+ cy.get('button').clickAndWatchForElement({
353
+ selector: '.loading-spinner',
354
+ assert: ($el) => {
355
+ expect($el).to.be.visible()
356
+ },
357
+ })
358
+ ```
359
+
360
+ ### Watch only (no click)
361
+
362
+ ```js
363
+ cy.watchForElement({
364
+ selector: '.loading-spinner',
365
+ appear: 'optional',
366
+ disappear: false,
367
+ assert: ($el) => {
368
+ expect($el).to.be.visible()
369
+ },
370
+ })
371
+ ```
372
+
373
+ ### Require appearance and disappearance
374
+
375
+ ```js
376
+ cy.get('button').clickAndWatchForElement({
377
+ selector: '.loading-spinner',
378
+ timeout: 1000,
379
+ pollingInterval: 10,
380
+ appear: 'required',
381
+ disappear: true,
382
+ assert: ($el) => {
383
+ expect($el).to.be.visible()
384
+ expect($el).to.have.class('loading')
385
+ },
386
+ })
387
+ ```
388
+
389
+ ### Require spinner to stay visible for a minimum time
390
+
391
+ When `appear` is `'required'`, use `mustLast` to fail if the spinner is removed too soon:
392
+
393
+ ```js
394
+ cy.get('button').clickAndWatchForElement({
395
+ selector: '.spinner',
396
+ appear: 'required',
397
+ mustLast: 2000, // spinner must stay in the DOM for at least 2000ms
398
+ assert: ($el) => {
399
+ expect($el).to.be.visible()
400
+ },
401
+ })
402
+ // If the spinner disappears before 2000ms, the command fails with:
403
+ // "spinner was not visible for the minimum time (mustLast: 2000ms). It disappeared after Xms."
404
+ ```
405
+
406
+ > ℹ️ **Note on `mustLast` accuracy:** `mustLast` is not an exact millisecond measure; it has a margin of error. This is usually unimportant, since the goal is to assert that the spinner stays visible *long enough for the user to notice*, not to measure duration precisely. Accuracy can be looser when:
407
+ > - `mustLast` is very small (e.g. 10–20 ms),
408
+ > - `pollingInterval` is large (the check runs every `pollingInterval` ms), or
409
+ > - the element is removed from the DOM right around the threshold.
410
+ >
411
+ > For typical values (e.g. `mustLast` ≥ 50–100 ms and default `pollingInterval`), the check is reliable.
412
+
413
+ ### With click options
414
+
415
+ ```js
416
+ cy.get('button').clickAndWatchForElement(
417
+ {
418
+ selector: '.loading-spinner',
419
+ assert: ($el) => {
420
+ expect($el).to.be.visible()
421
+ },
422
+ },
423
+ // Parameters for standard cy.click():
424
+ { force: true }
425
+ )
426
+ ```
427
+
428
+ ### With click position
429
+
430
+ ```js
431
+ cy.get('button').clickAndWatchForElement(
432
+ {
433
+ selector: '.loading-spinner',
434
+ assert: ($el) => {
435
+ expect($el).to.be.visible()
436
+ },
437
+ },
438
+ // Parameters for standard cy.click():
439
+ 'topRight',
440
+ { force: true }
441
+ )
442
+ ```
443
+
444
+ ### With x/y coordinates in a canvas
445
+
446
+ ```js
447
+ cy.get('canvas').clickAndWatchForElement(
448
+ {
449
+ selector: '.loading-spinner',
450
+ disappear: true,
451
+ assert: ($el) => {
452
+ expect($el).to.be.visible()
453
+ },
454
+ },
455
+ // Parameters for standard cy.click():
456
+ 20,
457
+ 40,
458
+ { force: true }
459
+ )
460
+ ```
461
+
462
+
463
+ ## Command log
464
+
465
+ The commands add Cypress log entries named `clickAndWatchForElement` or `watchForElement` and update their message to one of:
466
+
467
+ - `selector observed`
468
+ - `selector observed and disappeared`
469
+ - `selector not observed (optional)`
470
+
471
+ Example command log output:
472
+
473
+ ![Cypress command log showing "selector observed and disappeared"](assets/log-observed-dissapear.png)
474
+
475
+
476
+ ## Run the package Cypress examples
477
+
478
+ This repo includes example tests for the command in `cypress/e2e/clickSpinner.cy.js`, using the demo page `cypress/public/demo.html`.
479
+
480
+ ### Open Cypress UI (interactive)
481
+
482
+ ```bash
483
+ npm run cy:open
484
+ ```
485
+
486
+ This starts the local static server on port `3030` and opens Cypress.
487
+
488
+ ### Run examples headless
489
+
490
+ ```bash
491
+ npm run cy:run
492
+ ```
493
+
494
+ This starts the local static server and runs all Cypress specs in headless mode.
495
+
496
+ ### Run only the spinner example spec
497
+
498
+ ```bash
499
+ npm run cy:run -- --spec "cypress/e2e/clickSpinner.cy.js"
500
+ ```
501
+
502
+ Useful when you only want to validate `clickAndWatchForElement()` behavior.
503
+
504
+
505
+ ## Notes
506
+
507
+ - **This package detects elements directly from DOM changes and does not rely on `cy.intercept()` or network stubbing.** (Finally!)
508
+
509
+ - Very small `pollingInterval` values can increase test overhead. `10ms` is supported, but you may prefer `20ms` or `25ms` in many suites.
510
+
511
+ - If you use a custom `timeout` in config, ensure it is not greater than the Cypress command timeout for that chain, or the test will fail with a Cypress timeout before the command’s internal timeout is used (see [Relationship between `timeout` and Cypress default timeout](#relationship-between-timeout-and-cypress-default-timeout)).
512
+
513
+ - **Fast spinners:** The command uses a `MutationObserver` to detect when the spinner element is added to the DOM, so very short-lived spinners (e.g. under 60ms) are still detected reliably. **You do not need to increase spinner duration or polling frequency to avoid flakiness.**
514
+
515
+ - **`mustLast`:** The value is not exact; there is a margin of error (especially for small values like 10–20 ms or when `pollingInterval` is high). The parameter is meant to ensure the spinner is visible long enough to be noticed, not to measure duration precisely. See the note under [Require spinner to stay visible for a minimum time](#require-spinner-to-stay-visible-for-a-minimum-time).
516
+
517
+
518
+ ## Changelog
519
+
520
+ ### 1.0.0
521
+
522
+ - Initial release: `cy.clickAndWatchForElement()`, and `cy.watchForElement()` custom commands for Cypress.
523
+ - Supports detection of elements with `MutationObserver` for reliability, with options for required/optional appearance, custom timeouts, polling interval, `mustLast`, disappearance, and optional click signatures (`clickAndWatchForElement`) or observer-only mode (`watchForElement`).
524
+ - Provides flexible config and assert callback for spinner validation.
525
+ - Handles very short-lived spinners and prevents test flakiness due to rapid DOM changes.
526
+ - Command log integration with meaningful messages on assertion/disappearance.
527
+ - **Entirely DOM-based**: does not rely on intercepts, network stubbing, or external synchronization mechanisms.
528
+
529
+
530
+
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,115 @@
1
+
2
+ // Function to assert the spinner is visible and the loading label is not visible and the button is not disabled
3
+ // Could go in an utils.js file if needed.
4
+ const assertLoadCampaignDataSpinner = ($el) => {
5
+ // EXAMPLE OF HOW TO CHECK MULTIPLE ASSERTIONS
6
+
7
+ // Assert spinner should be visible (MAIN ASSERTION)
8
+ // -------------------------------------------------
9
+ expect($el).to.be.visible()
10
+
11
+
12
+ // Note:No needed when using traversal
13
+ const $root = $el.closest('body')
14
+
15
+ // Assert loading label should be visible
16
+ // --------------------------------------
17
+
18
+ // // Using traversal (option 1)
19
+ // const labelFetching = $el.closest('.loading-row').find('.loading-label')
20
+
21
+ // Using global AUT search using data-cy selector (option 2)
22
+ const labelFetching = $root.find('[data-cy="loading-row"] .loading-label')
23
+
24
+ expect(labelFetching).to.be.visible()
25
+ expect(labelFetching.text()).to.contain('Fetching latest campaign analytics')
26
+
27
+ // Log the assertion successfully passed
28
+ Cypress.log({ name: 'assert', message: `.loading-label text: expected "${labelFetching.text()}" exist and is correct"` })
29
+
30
+
31
+ // Assert if the button is disabled
32
+ //---------------------------------
33
+
34
+ // // Using traversal (option 1)
35
+ // const loadDataBtn = $el.closest('.content').find('[data-cy="load-data-btn"]')
36
+
37
+ // Using global AUT search using data-cy selector (option 2)
38
+ const loadDataBtn = $root.find('[data-cy="load-data-btn"]')
39
+
40
+ expect(loadDataBtn).to.be.visible()
41
+ expect(loadDataBtn.prop('disabled')).to.eq(true)
42
+
43
+ // Log the assertion successfully passed
44
+ Cypress.log({ name: 'assert', message: '[data-cy="load-data-btn"] found and disabled=true' })
45
+ }
46
+
47
+ describe('modal table demo', () => {
48
+
49
+ it('CASE 1: test load campaign data SPINNER + loading label + button disabled', () => {
50
+ cy.visit('/modal-table-demo.html', {
51
+ // onBeforeLoad(win) {
52
+ // win.Math.random = () => 0.2 // (page load banner always shown)
53
+ // },
54
+ onBeforeLoad(win) {
55
+ win.Math.random = () => 0.8 // (page load banner never shown)
56
+ },
57
+ })
58
+
59
+ // Click the button and watch for the spinner to appear and disappear
60
+ cy.get('[data-cy="load-data-btn"]').clickAndWatchForElement({
61
+ selector: '[data-cy="service-spinner"]',
62
+ appear: 'required',
63
+ disappear: true,
64
+ timeout: 10000, // plugin internal timeout (can be omitted if same as defaultCommandTimeout)
65
+ assert: assertLoadCampaignDataSpinner // function to assert the spinner is visible and the loading label is not visible and the button is not disabled
66
+ })
67
+
68
+ // At this point we know the spinner is not visible (controlled by the plugin)
69
+ // So we can assert if we want the loading label is not visible and the button is not disabled
70
+ cy.get('.loading-label').should('not.be.visible')
71
+ cy.get('[data-cy="load-data-btn"]').should('be.enabled')
72
+
73
+ // Other assertions can be added here if needed (like on the table etc)
74
+ })
75
+
76
+
77
+ // Run the test 4 times to get about 50% chance of the page load banner appearing or not appearing.
78
+ for (let i = 0; i < 4; i++) {
79
+
80
+ it(`CASE 2 - Try ${i + 1}: watchForElement for ANNOYING OVERLAY - 50% TIMES DOES NOT SHOW (optional) & ALSO load campaign data spinner flow`, () => {
81
+ cy.visit('/modal-table-demo.html')
82
+
83
+ // Observe startup banner modal that may or may not appear on page load (about 50%).
84
+ cy.watchForElement({
85
+ selector: '[data-cy="ad-overlay"]',
86
+ appear: 'optional',
87
+ disappear: false,
88
+ timeout: 1500,
89
+ pollingInterval: 10,
90
+ assert: ($el) => {
91
+ expect($el).to.be.visible()
92
+ expect($el.find('[data-cy="close-ad-btn"]')).to.be.visible()
93
+ },
94
+ action: ($el) => {
95
+ // Close the overlay via DOM click and verify the same `$el` is no longer visible.
96
+ const closeBtnEl = $el.find('[data-cy="close-ad-btn"]')[0]
97
+ if (closeBtnEl) closeBtnEl.click()
98
+ expect($el).to.not.be.visible()
99
+ }
100
+ })
101
+
102
+ // Reuse CASE 1 flow: click and watch spinner + extra assertions.
103
+ cy.get('[data-cy="load-data-btn"]').clickAndWatchForElement({
104
+ selector: '[data-cy="service-spinner"]',
105
+ appear: 'required',
106
+ disappear: true,
107
+ timeout: 10000,
108
+ assert: assertLoadCampaignDataSpinner
109
+ })
110
+
111
+ cy.get('.loading-label').should('not.be.visible')
112
+ cy.get('[data-cy="load-data-btn"]').should('be.enabled')
113
+ })
114
+ }
115
+ })