@vardot/varbase-e2e 2.0.2 → 2.0.4
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/.playwright-mcp/console-2026-09-07T08-40-17-262Z.log +28 -0
- package/.playwright-mcp/console-2026-09-07T08-40-55-044Z.log +4 -0
- package/.playwright-mcp/console-2026-09-07T08-41-12-557Z.log +18 -0
- package/.playwright-mcp/console-2026-09-07T08-42-32-413Z.log +38 -0
- package/.playwright-mcp/console-2026-09-07T09-02-43-145Z.log +1 -0
- package/.playwright-mcp/console-2026-09-07T09-03-15-665Z.log +17 -0
- package/.playwright-mcp/page-2026-09-07T08-40-18-956Z.yml +261 -0
- package/.playwright-mcp/page-2026-09-07T08-40-55-673Z.yml +132 -0
- package/.playwright-mcp/page-2026-09-07T08-41-13-471Z.yml +386 -0
- package/.playwright-mcp/page-2026-09-07T08-42-33-250Z.yml +287 -0
- package/AGENTS.md +1 -1
- package/CLAUDE.md +6 -5
- package/README.md +2 -0
- package/docs/04-step-reference.md +15 -1
- package/docs/README.md +8 -5
- package/docs/install-varbase-e2e.md +0 -2
- package/docs/steps/README.md +6 -5
- package/docs/steps/assertion.md +90 -5
- package/docs/steps/drupal-drimage-improved.md +372 -0
- package/docs/steps/navigation.md +81 -1
- package/docs/steps/varbase.md +6 -65
- package/examples/access-denied.html +6 -0
- package/examples/drimage.html +55 -0
- package/examples/styles/drimage_improved_1000_667/public/sample.png +0 -0
- package/examples/styles/drimage_improved_1200_800/public/sample.png +0 -0
- package/examples/styles/drimage_improved_1400_933/public/sample.png +0 -0
- package/examples/styles/drimage_improved_1600_1067/public/sample.png +0 -0
- package/examples/styles/drimage_improved_1800_1200/public/sample.png +0 -0
- package/examples/styles/drimage_improved_2000_1333/public/sample.png +0 -0
- package/examples/styles/drimage_improved_200_133/public/sample.png +0 -0
- package/examples/styles/drimage_improved_2200_1467/public/sample.png +0 -0
- package/examples/styles/drimage_improved_2400_1600/public/sample.png +0 -0
- package/examples/styles/drimage_improved_2600_1734/public/sample.png +0 -0
- package/examples/styles/drimage_improved_2800_1867/public/sample.png +0 -0
- package/examples/styles/drimage_improved_3000_2000/public/sample.png +0 -0
- package/examples/styles/drimage_improved_3200_2134/public/sample.png +0 -0
- package/examples/styles/drimage_improved_3400_2267/public/sample.png +0 -0
- package/examples/styles/drimage_improved_3600_2400/public/sample.png +0 -0
- package/examples/styles/drimage_improved_3800_2534/public/sample.png +0 -0
- package/examples/styles/drimage_improved_4000_2667/public/sample.png +0 -0
- package/examples/styles/drimage_improved_400_267/public/sample.png +0 -0
- package/examples/styles/drimage_improved_600_400/public/sample.png +0 -0
- package/examples/styles/drimage_improved_800_533/public/sample.png +0 -0
- package/examples/user-login.html +12 -0
- package/images/varbase-e2e-logo.svg +24 -0
- package/package.json +1 -1
- package/tests/features/access.feature +33 -0
- package/tests/features/drupal-drimage-improved.feature +59 -0
- package/tests/features/http-status.feature +21 -0
- package/tests/features/page-title.feature +24 -0
- package/tests/step-definitions/assertion.steps.js +108 -19
- package/tests/step-definitions/drupal-drimage-improved.steps.js +486 -0
- package/tests/step-definitions/navigation.steps.js +101 -1
- package/tests/step-definitions/varbase.steps.js +0 -83
- package/.playwright-mcp/console-2026-08-09T19-48-04-420Z.log +0 -6
- package/.playwright-mcp/console-2026-08-09T19-48-21-910Z.log +0 -4
- package/.playwright-mcp/console-2026-08-09T19-48-37-544Z.log +0 -5
- package/.playwright-mcp/console-2026-08-09T20-02-58-240Z.log +0 -5
- package/.playwright-mcp/console-2026-08-09T20-15-05-487Z.log +0 -71
- package/.playwright-mcp/console-2026-08-09T20-31-11-228Z.log +0 -14
- package/.playwright-mcp/console-2026-08-09T20-34-03-115Z.log +0 -7
- package/.playwright-mcp/console-2026-08-09T20-34-34-018Z.log +0 -85
- package/.playwright-mcp/console-2026-08-09T20-34-34-740Z.log +0 -46
- package/.playwright-mcp/console-2026-08-09T20-34-35-618Z.log +0 -92
- package/.playwright-mcp/console-2026-08-09T22-57-59-606Z.log +0 -14
- package/.playwright-mcp/console-2026-08-09T23-09-53-902Z.log +0 -28
- package/.playwright-mcp/console-2026-08-09T23-09-55-918Z.log +0 -49
- package/.playwright-mcp/console-2026-08-09T23-09-57-522Z.log +0 -42
- package/.playwright-mcp/console-2026-08-09T23-40-26-847Z.log +0 -28
- package/.playwright-mcp/console-2026-08-10T00-05-53-423Z.log +0 -8
- package/.playwright-mcp/console-2026-08-10T08-51-59-747Z.log +0 -5
- package/.playwright-mcp/console-2026-08-10T09-00-26-735Z.log +0 -7
- package/.playwright-mcp/page-2026-08-09T19-48-23-422Z.yml +0 -288
- package/.playwright-mcp/page-2026-08-09T19-48-32-827Z.yml +0 -39
- package/.playwright-mcp/page-2026-08-09T19-48-39-150Z.yml +0 -39
- package/.playwright-mcp/page-2026-08-09T19-50-55-594Z.yml +0 -787
- package/.playwright-mcp/page-2026-08-09T19-51-28-160Z.yml +0 -391
- package/.playwright-mcp/page-2026-08-09T19-51-45-546Z.yml +0 -391
- package/.playwright-mcp/page-2026-08-09T19-51-46-085Z.yml +0 -391
- package/.playwright-mcp/page-2026-08-09T19-51-46-355Z.yml +0 -391
- package/.playwright-mcp/page-2026-08-09T19-51-49-918Z.yml +0 -391
- package/.playwright-mcp/page-2026-08-09T19-52-30-248Z.yml +0 -415
- package/.playwright-mcp/page-2026-08-09T19-52-42-510Z.yml +0 -415
- package/.playwright-mcp/page-2026-08-09T19-52-45-588Z.yml +0 -391
- package/.playwright-mcp/page-2026-08-09T19-52-54-615Z.yml +0 -391
- package/.playwright-mcp/page-2026-08-09T19-52-55-424Z.yml +0 -391
- package/.playwright-mcp/page-2026-08-09T19-52-55-553Z.yml +0 -391
- package/.playwright-mcp/page-2026-08-09T19-52-58-991Z.yml +0 -391
- package/.playwright-mcp/page-2026-08-09T19-53-29-261Z.yml +0 -415
- package/.playwright-mcp/page-2026-08-09T19-53-40-322Z.yml +0 -415
- package/.playwright-mcp/page-2026-08-09T19-53-44-146Z.yml +0 -391
- package/.playwright-mcp/page-2026-08-09T19-53-51-021Z.yml +0 -391
- package/.playwright-mcp/page-2026-08-09T19-53-51-565Z.yml +0 -391
- package/.playwright-mcp/page-2026-08-09T19-53-51-979Z.yml +0 -391
- package/.playwright-mcp/page-2026-08-09T19-53-54-883Z.yml +0 -391
- package/.playwright-mcp/page-2026-08-09T19-54-24-537Z.yml +0 -415
- package/.playwright-mcp/page-2026-08-09T20-01-07-921Z.yml +0 -404
- package/.playwright-mcp/page-2026-08-09T20-01-35-366Z.yml +0 -404
- package/.playwright-mcp/page-2026-08-09T20-01-54-083Z.yml +0 -404
- package/.playwright-mcp/page-2026-08-09T20-02-07-954Z.yml +0 -416
- package/.playwright-mcp/page-2026-08-09T20-02-24-800Z.yml +0 -404
- package/.playwright-mcp/page-2026-08-09T20-02-29-695Z.yml +0 -404
- package/.playwright-mcp/page-2026-08-09T20-02-33-819Z.yml +0 -404
- package/.playwright-mcp/page-2026-08-09T20-02-41-437Z.yml +0 -404
- package/.playwright-mcp/page-2026-08-09T20-02-53-232Z.yml +0 -416
- package/.playwright-mcp/page-2026-08-09T20-03-00-246Z.yml +0 -404
- package/.playwright-mcp/page-2026-08-09T20-03-03-297Z.yml +0 -404
- package/.playwright-mcp/page-2026-08-09T20-03-11-014Z.yml +0 -404
- package/.playwright-mcp/page-2026-08-09T20-03-21-049Z.yml +0 -416
- package/.playwright-mcp/page-2026-08-10T01-04-54-039Z.yml +0 -410
- package/.playwright-mcp/page-2026-08-10T01-05-06-967Z.png +0 -0
- package/.playwright-mcp/page-2026-08-10T01-05-52-136Z.yml +0 -720
- package/.playwright-mcp/page-2026-08-10T01-06-05-985Z.yml +0 -410
- package/.playwright-mcp/page-2026-08-10T01-06-25-942Z.yml +0 -726
- package/.playwright-mcp/page-2026-08-10T01-25-57-152Z.yml +0 -395
- package/.playwright-mcp/page-2026-08-10T01-26-48-853Z.yml +0 -395
- package/.playwright-mcp/page-2026-08-10T01-27-47-127Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-27-57-349Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-28-07-943Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-28-30-462Z.yml +0 -369
- package/.playwright-mcp/page-2026-08-10T01-29-00-369Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-29-08-988Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-29-19-888Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-29-30-189Z.yml +0 -369
- package/.playwright-mcp/page-2026-08-10T01-30-08-272Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-30-16-955Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-30-27-612Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-30-37-943Z.yml +0 -465
- package/.playwright-mcp/page-2026-08-10T01-31-15-265Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-31-24-328Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-31-36-622Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-31-45-965Z.yml +0 -470
- package/.playwright-mcp/page-2026-08-10T01-32-13-400Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-32-23-469Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-32-33-859Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-32-43-461Z.yml +0 -513
- package/.playwright-mcp/page-2026-08-10T01-33-13-160Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-33-23-937Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-33-35-367Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-33-47-266Z.yml +0 -459
- package/.playwright-mcp/page-2026-08-10T01-34-14-452Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-34-26-042Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-34-40-869Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-34-49-587Z.yml +0 -391
- package/.playwright-mcp/page-2026-08-10T01-35-16-735Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-35-24-882Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-35-36-165Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-35-45-644Z.yml +0 -455
- package/.playwright-mcp/page-2026-08-10T01-36-18-148Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-36-26-751Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-36-37-834Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-36-48-078Z.yml +0 -370
- package/.playwright-mcp/page-2026-08-10T01-37-16-309Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-37-28-723Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-37-39-325Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-37-48-278Z.yml +0 -409
- package/.playwright-mcp/page-2026-08-10T01-38-27-405Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-38-36-994Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-38-49-584Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-38-59-289Z.yml +0 -396
- package/.playwright-mcp/page-2026-08-10T01-39-24-880Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-39-35-551Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-39-47-960Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-39-56-844Z.yml +0 -549
- package/.playwright-mcp/page-2026-08-10T01-40-24-645Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-40-33-806Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-40-46-946Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-40-56-503Z.yml +0 -443
- package/.playwright-mcp/page-2026-08-10T01-41-32-064Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-41-40-666Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-41-51-325Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-41-58-790Z.yml +0 -368
- package/.playwright-mcp/page-2026-08-10T01-42-26-459Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-42-38-306Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-42-49-073Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-42-57-453Z.yml +0 -443
- package/.playwright-mcp/page-2026-08-10T01-43-32-099Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-43-42-882Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-43-54-853Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-44-04-373Z.yml +0 -399
- package/.playwright-mcp/page-2026-08-10T01-44-30-874Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-44-39-830Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-44-51-515Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-44-59-974Z.yml +0 -503
- package/.playwright-mcp/page-2026-08-10T01-45-26-748Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-45-36-685Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-45-47-715Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-45-57-895Z.yml +0 -469
- package/.playwright-mcp/page-2026-08-10T01-46-36-371Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-46-49-906Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-47-01-554Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-47-12-712Z.yml +0 -581
- package/.playwright-mcp/page-2026-08-10T01-47-39-024Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-47-49-714Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-48-00-608Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-48-09-739Z.yml +0 -604
- package/.playwright-mcp/page-2026-08-10T01-48-51-940Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-49-01-562Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-49-13-751Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-49-22-503Z.yml +0 -400
- package/.playwright-mcp/page-2026-08-10T01-49-49-311Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-50-00-769Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-50-12-083Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-50-23-765Z.yml +0 -458
- package/.playwright-mcp/page-2026-08-10T01-51-02-339Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-51-12-711Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-51-25-370Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-51-33-936Z.yml +0 -368
- package/.playwright-mcp/page-2026-08-10T01-52-09-467Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-52-21-616Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-52-32-806Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-52-42-382Z.yml +0 -407
- package/.playwright-mcp/page-2026-08-10T01-53-11-994Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-53-21-669Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-53-33-141Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-53-43-582Z.yml +0 -443
- package/.playwright-mcp/page-2026-08-10T01-54-19-884Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-54-32-359Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-54-43-530Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-54-53-891Z.yml +0 -474
- package/.playwright-mcp/page-2026-08-10T01-55-24-634Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-55-35-283Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-55-46-649Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-55-56-585Z.yml +0 -431
- package/.playwright-mcp/page-2026-08-10T01-56-31-670Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-56-40-782Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-56-52-065Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-57-03-705Z.yml +0 -445
- package/.playwright-mcp/page-2026-08-10T01-57-31-390Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-57-44-756Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-57-56-802Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-58-06-937Z.yml +0 -468
- package/.playwright-mcp/page-2026-08-10T01-58-40-595Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-58-51-715Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T01-59-04-402Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T01-59-15-638Z.yml +0 -472
- package/.playwright-mcp/page-2026-08-10T01-59-58-162Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T02-00-09-962Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T02-00-21-468Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T02-00-33-248Z.yml +0 -451
- package/.playwright-mcp/page-2026-08-10T02-01-15-418Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T02-01-27-182Z.yml +0 -413
- package/.playwright-mcp/page-2026-08-10T02-01-38-250Z.yml +0 -425
- package/.playwright-mcp/page-2026-08-10T04-20-27-979Z.yml +0 -513
- package/.playwright-mcp/page-2026-08-10T04-20-41-431Z.yml +0 -513
- package/.playwright-mcp/page-2026-08-10T04-21-46-133Z.yml +0 -406
- package/.playwright-mcp/page-2026-08-10T04-22-10-938Z.yml +0 -406
- package/.playwright-mcp/page-2026-08-10T04-23-00-626Z.yml +0 -406
- package/.playwright-mcp/page-2026-08-10T04-23-19-844Z.yml +0 -418
- package/.playwright-mcp/page-2026-08-10T04-25-00-732Z.yml +0 -455
- package/.playwright-mcp/page-2026-08-10T04-25-35-125Z.yml +0 -412
- package/.playwright-mcp/page-2026-08-10T04-25-46-640Z.yml +0 -412
- package/.playwright-mcp/page-2026-08-10T04-25-55-092Z.yml +0 -549
- package/.playwright-mcp/page-2026-08-10T04-26-26-403Z.yml +0 -406
- package/.playwright-mcp/page-2026-08-10T04-26-40-673Z.yml +0 -406
- package/.playwright-mcp/page-2026-08-10T04-26-48-427Z.yml +0 -604
- package/.playwright-mcp/page-2026-08-10T04-27-23-584Z.yml +0 -408
- package/.playwright-mcp/page-2026-08-10T04-27-34-294Z.yml +0 -408
- package/.playwright-mcp/page-2026-08-10T04-27-42-299Z.yml +0 -443
- package/.playwright-mcp/page-2026-08-10T04-28-20-032Z.yml +0 -406
- package/.playwright-mcp/page-2026-08-10T04-28-32-275Z.yml +0 -406
- package/.playwright-mcp/page-2026-08-10T04-28-41-592Z.yml +0 -581
- package/.playwright-mcp/page-2026-08-10T04-29-19-972Z.yml +0 -414
- package/.playwright-mcp/page-2026-08-10T04-29-30-961Z.yml +0 -414
- package/.playwright-mcp/page-2026-08-10T04-29-39-044Z.yml +0 -465
- package/.playwright-mcp/page-2026-08-10T04-30-13-196Z.yml +0 -402
- package/.playwright-mcp/page-2026-08-10T04-30-24-867Z.yml +0 -402
- package/.playwright-mcp/page-2026-08-10T04-30-32-372Z.yml +0 -409
- package/.playwright-mcp/page-2026-08-10T04-31-06-199Z.yml +0 -402
- package/.playwright-mcp/page-2026-08-10T04-31-17-733Z.yml +0 -402
- package/.playwright-mcp/page-2026-08-10T04-31-26-296Z.yml +0 -445
- package/.playwright-mcp/page-2026-08-10T04-32-02-456Z.yml +0 -402
- package/.playwright-mcp/page-2026-08-10T04-32-14-091Z.yml +0 -402
- package/.playwright-mcp/page-2026-08-10T04-32-23-437Z.yml +0 -396
- package/.playwright-mcp/page-2026-08-10T04-32-58-401Z.yml +0 -403
- package/.playwright-mcp/page-2026-08-10T04-33-09-818Z.yml +0 -403
- package/.playwright-mcp/page-2026-08-10T04-33-20-858Z.yml +0 -468
- package/.playwright-mcp/page-2026-08-10T04-33-53-074Z.yml +0 -407
- package/.playwright-mcp/page-2026-08-10T04-34-05-806Z.yml +0 -407
- package/.playwright-mcp/page-2026-08-10T08-52-00-165Z.yml +0 -0
- package/.playwright-mcp/page-2026-08-10T08-52-17-430Z.yml +0 -410
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// -----------------------------------------------------------------------------
|
|
4
|
+
// Drimage Improved step definitions: assertions for the dynamic responsive
|
|
5
|
+
// images the drimage_improved module renders.
|
|
6
|
+
//
|
|
7
|
+
// The steps carry no CSS selectors. The module's formatter markup is stable —
|
|
8
|
+
// a wrapper carrying `data-drimage_improved`, a <picture> with an optional
|
|
9
|
+
// webp <source>, the <img> on an SVG placeholder, and a <noscript> fallback
|
|
10
|
+
// inside the wrapper (the module template) or right after it (the vartheme_bs5
|
|
11
|
+
// component) — and its JS swaps the placeholder for a derivative under
|
|
12
|
+
// /styles/drimage_improved_*. The pack knows all of that, so a tester writes
|
|
13
|
+
// "the drimage images should be loaded" and nothing else. Themes re-class the
|
|
14
|
+
// <img> freely, so nothing here relies on `drimage-image`.
|
|
15
|
+
//
|
|
16
|
+
// The module exists to make images fast, so the default budget is 2 seconds.
|
|
17
|
+
// Ported from the drimage_improved project's own suites so every site using
|
|
18
|
+
// the module gets the steps from @vardot/varbase-e2e.
|
|
19
|
+
// -----------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const { Then } = require('@cucumber/cucumber');
|
|
22
|
+
const { smartSettle, friendly } = require('./varbase-e2e');
|
|
23
|
+
|
|
24
|
+
const WRAPPER = '[data-drimage_improved]';
|
|
25
|
+
const IMAGE = `${WRAPPER} img`;
|
|
26
|
+
const WEBP_SOURCE = 'picture source[type="image/webp"]'; // inside a wrapper
|
|
27
|
+
const DERIVATIVE = '/styles/drimage_improved_';
|
|
28
|
+
|
|
29
|
+
// "drimage", "drimage improved", "dynamic", "dynamic responsive", "responsive"
|
|
30
|
+
// or nothing — the qualifier names the same images every time.
|
|
31
|
+
const Q = '(?:drimage improved |drimage |dynamic responsive |dynamic |responsive )?';
|
|
32
|
+
|
|
33
|
+
function budgetOf(secondsStr) {
|
|
34
|
+
const n = secondsStr ? parseInt(secondsStr, 10) : 0;
|
|
35
|
+
return n > 0 ? n * 1000 : 2000;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// How a tester points at ONE Drimage image without a selector:
|
|
39
|
+
// the drimage image "Hero" alt text or title, whichever matches
|
|
40
|
+
// the drimage image titled "Hero" title attribute
|
|
41
|
+
// the drimage image captioned "Our team in 2026" the <figcaption> of its <figure>
|
|
42
|
+
// the drimage image with the alt text "Hero" alt only
|
|
43
|
+
// the first drimage image / the 3rd / the last order in the page
|
|
44
|
+
// the drimage image number 3 same, as a number
|
|
45
|
+
// Captured as: ordinal, mode, mode value, plain quoted value, number.
|
|
46
|
+
const ORDINAL = '(first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth|last|\\d+(?:st|nd|rd|th))';
|
|
47
|
+
const TARGET = `the (?:${ORDINAL} )?${Q}image(?: (titled|captioned|with the alt text|with the title|with the caption) "([^"]*)"| "([^"]*)"| number (\\d+))?`;
|
|
48
|
+
const WORDS = { first: 1, second: 2, third: 3, fourth: 4, fifth: 5, sixth: 6, seventh: 7, eighth: 8, ninth: 9, tenth: 10 };
|
|
49
|
+
|
|
50
|
+
function targetOf(ordinal, mode, modeValue, plain, number) {
|
|
51
|
+
const t = { ordinal: null, field: null, value: null };
|
|
52
|
+
if (ordinal) t.ordinal = ordinal === 'last' ? 'last' : (WORDS[ordinal] || parseInt(ordinal, 10));
|
|
53
|
+
if (number) t.ordinal = parseInt(number, 10);
|
|
54
|
+
if (mode) {
|
|
55
|
+
t.field = mode === 'titled' || mode === 'with the title' ? 'title'
|
|
56
|
+
: mode === 'captioned' || mode === 'with the caption' ? 'caption' : 'alt';
|
|
57
|
+
t.value = modeValue;
|
|
58
|
+
} else if (plain !== undefined && plain !== null) {
|
|
59
|
+
t.field = 'any';
|
|
60
|
+
t.value = plain;
|
|
61
|
+
}
|
|
62
|
+
return t;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function describeTarget(t) {
|
|
66
|
+
const lead = t.ordinal === 'last' ? 'the last Drimage image' : t.ordinal ? `Drimage image number ${t.ordinal}` : 'the Drimage image';
|
|
67
|
+
if (t.field === 'any') return `${lead} "${t.value}"`;
|
|
68
|
+
if (t.field) return `${lead} with the ${t.field === 'alt' ? 'alt text' : t.field} "${t.value}"`;
|
|
69
|
+
return lead;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Which of the snapshot's images the target names, as 0-based indices. Text
|
|
73
|
+
// matches are exact after trimming, then a contains match as a fallback so a
|
|
74
|
+
// long caption can be named by a distinctive part of it.
|
|
75
|
+
function select(images, t) {
|
|
76
|
+
let pool = images.map((img, i) => ({ ...img, i }));
|
|
77
|
+
if (t.field) {
|
|
78
|
+
const v = t.value.trim();
|
|
79
|
+
const fields = t.field === 'any' ? ['alt', 'title', 'caption'] : [t.field];
|
|
80
|
+
const exact = pool.filter((x) => fields.some((f) => (x[f] || '').trim() === v));
|
|
81
|
+
pool = exact.length ? exact : pool.filter((x) => fields.some((f) => (x[f] || '').includes(v)));
|
|
82
|
+
}
|
|
83
|
+
if (t.ordinal === 'last') pool = pool.slice(-1);
|
|
84
|
+
else if (t.ordinal) pool = pool.slice(t.ordinal - 1, t.ordinal);
|
|
85
|
+
return pool.map((x) => x.i);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// One snapshot of every Drimage image on the page, taken in the browser.
|
|
89
|
+
function snapshot(page) {
|
|
90
|
+
return page.evaluate(({ image, derivative }) =>
|
|
91
|
+
Array.from(document.querySelectorAll(image)).map((img) => {
|
|
92
|
+
const src = img.currentSrc || img.src || '';
|
|
93
|
+
const m = src.match(/\/styles\/drimage_improved_(?:focal_)?(\d+)_(\d+)/);
|
|
94
|
+
const figure = img.closest('figure');
|
|
95
|
+
const caption = figure && figure.querySelector('figcaption');
|
|
96
|
+
return {
|
|
97
|
+
alt: img.getAttribute('alt') || '',
|
|
98
|
+
title: img.getAttribute('title') || '',
|
|
99
|
+
caption: caption ? caption.textContent.trim() : '',
|
|
100
|
+
inFigure: figure !== null,
|
|
101
|
+
src,
|
|
102
|
+
derivative: src.includes(derivative),
|
|
103
|
+
derivativeWidth: m ? Number(m[1]) : 0,
|
|
104
|
+
renderedWidth: Math.round(img.clientWidth * (window.devicePixelRatio || 1)),
|
|
105
|
+
loading: img.getAttribute('loading') || '',
|
|
106
|
+
decoded: img.complete && img.naturalWidth > 0,
|
|
107
|
+
};
|
|
108
|
+
}), { image: IMAGE, derivative: DERIVATIVE });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Settle the page the BBR way, then poll the browser-side predicate until it
|
|
112
|
+
// holds or the budget runs out. Returns the last snapshot for the message.
|
|
113
|
+
async function settleThenWait(page, budget, predicate, arg) {
|
|
114
|
+
await smartSettle(page, budget);
|
|
115
|
+
try {
|
|
116
|
+
await page.waitForFunction(predicate, arg, { timeout: budget, polling: 100 });
|
|
117
|
+
return null;
|
|
118
|
+
} catch (cause) {
|
|
119
|
+
return { cause, images: await snapshot(page).catch(() => []) };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function describe(images) {
|
|
124
|
+
if (!images.length) return 'no Drimage images on the page';
|
|
125
|
+
return images
|
|
126
|
+
.map((i) => `"${i.alt || '(no alt)'}": ${i.derivative ? 'derivative' : 'placeholder'}, ${i.decoded ? 'decoded' : 'not decoded'}`)
|
|
127
|
+
.join('; ');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// A Drimage image counts as loaded once its src is a real derivative (the
|
|
131
|
+
// placeholder has been swapped) AND the bitmap decoded. The SVG placeholder
|
|
132
|
+
// also decodes, so naturalWidth alone cannot tell the two apart.
|
|
133
|
+
const LOADED = ({ image, derivative, indices }) => {
|
|
134
|
+
const imgs = Array.from(document.querySelectorAll(image)).filter((img, i) => indices === null || indices.includes(i));
|
|
135
|
+
if (!imgs.length) return false;
|
|
136
|
+
const swapped = imgs.filter((img) => (img.currentSrc || img.src || '').includes(derivative));
|
|
137
|
+
if (!swapped.length) return false;
|
|
138
|
+
return swapped.every((img) => img.complete && img.naturalWidth > 0);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Assert the Drimage images on the page loaded: at least one has swapped its
|
|
143
|
+
* placeholder for a real derivative, and every swapped one decoded a bitmap.
|
|
144
|
+
*
|
|
145
|
+
* Attribute assertions cannot see a broken Drimage: the placeholder never
|
|
146
|
+
* swapping, or a derivative the server failed to generate (a 4xx or 5xx on the
|
|
147
|
+
* /styles/drimage_improved_* request), still leaves every class and attribute
|
|
148
|
+
* in place. The SVG placeholder even decodes, so this checks the src is a real
|
|
149
|
+
* derivative before trusting naturalWidth. Below-the-fold images still on their
|
|
150
|
+
* placeholder are not counted against the page: lazy loading has not reached
|
|
151
|
+
* them and is not supposed to have.
|
|
152
|
+
*
|
|
153
|
+
* Settles the page first (the harness BBR wait) so the module's setTimeout
|
|
154
|
+
* swap has fired, then polls. Default budget 2 seconds: the module exists to
|
|
155
|
+
* make images fast, so a longer wait is hiding a problem, not tolerating one.
|
|
156
|
+
*
|
|
157
|
+
* Example #1: Then the drimage images should be loaded
|
|
158
|
+
* Example #2: Then the drimage improved images should be loaded within 2 seconds
|
|
159
|
+
* Example #3: Then the dynamic responsive images should be loaded
|
|
160
|
+
* Example #4: And the responsive images should be loaded within 1 second
|
|
161
|
+
* Example #5: Then the images should be loaded
|
|
162
|
+
*/
|
|
163
|
+
Then(new RegExp(`^the ${Q}images should be loaded(?: within (\\d+) seconds?)?$`), async function (sec) {
|
|
164
|
+
const failed = await settleThenWait(this.page, budgetOf(sec), LOADED, { image: IMAGE, derivative: DERIVATIVE, indices: null });
|
|
165
|
+
if (failed) {
|
|
166
|
+
throw friendly(`Expected the Drimage images to load, but saw ${describe(failed.images)}`, failed.cause);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Settle, snapshot, and resolve a single-image target. Throws the friendly
|
|
171
|
+
// "no such image" when nothing matches, listing what is on the page.
|
|
172
|
+
async function resolveTarget(page, t) {
|
|
173
|
+
await smartSettle(page, 2000);
|
|
174
|
+
const images = await snapshot(page);
|
|
175
|
+
const indices = select(images, t);
|
|
176
|
+
if (!indices.length) {
|
|
177
|
+
throw friendly(`${describeTarget(t)} is not on the page; the Drimage images present are ${describe(images)}`);
|
|
178
|
+
}
|
|
179
|
+
return { images, indices, picked: indices.map((i) => images[i]) };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Assert one Drimage image swapped its placeholder for a real derivative and
|
|
184
|
+
* decoded the bitmap. Name it by alt text or title in quotes, by `titled`,
|
|
185
|
+
* `captioned` or `with the alt text`, by its order in the page, or by number —
|
|
186
|
+
* the human ways of pointing at a picture, never a class or an id.
|
|
187
|
+
*
|
|
188
|
+
* Example #1: Then the drimage image "Team collaborating in a modern glass-walled office" should be loaded
|
|
189
|
+
* Example #2: Then the first drimage image should be loaded
|
|
190
|
+
* Example #3: Then the drimage image captioned "Our team in the new office" should be loaded within 2 seconds
|
|
191
|
+
* Example #4: Then the dynamic responsive image titled "Vardot team" should be loaded
|
|
192
|
+
* Example #5: Then the drimage image number 2 should be loaded within 1 second
|
|
193
|
+
*/
|
|
194
|
+
Then(new RegExp(`^${TARGET} should be loaded(?: within (\\d+) seconds?)?$`), async function (ordinal, mode, modeValue, plain, number, sec) {
|
|
195
|
+
const t = targetOf(ordinal, mode, modeValue, plain, number);
|
|
196
|
+
const { indices } = await resolveTarget(this.page, t);
|
|
197
|
+
const failed = await settleThenWait(this.page, budgetOf(sec), LOADED, { image: IMAGE, derivative: DERIVATIVE, indices });
|
|
198
|
+
if (failed) {
|
|
199
|
+
throw friendly(`Expected ${describeTarget(t)} to load, but saw ${describe(indices.map((i) => failed.images[i]).filter(Boolean))}`, failed.cause);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Assert the page renders at least one Drimage image: the formatter wrapper
|
|
205
|
+
* carrying the module's data object is in the DOM.
|
|
206
|
+
*
|
|
207
|
+
* This is the formatter check — did the field display actually go through
|
|
208
|
+
* Drimage Improved — before any loading question.
|
|
209
|
+
*
|
|
210
|
+
* Example #1: Then the drimage images should be rendered
|
|
211
|
+
* Example #2: Then the drimage improved images should be rendered within 2 seconds
|
|
212
|
+
* Example #3: Then the dynamic responsive images should be rendered
|
|
213
|
+
* Example #4: And the responsive images should be rendered within 1 second
|
|
214
|
+
* Example #5: Then the images should be rendered
|
|
215
|
+
*/
|
|
216
|
+
Then(new RegExp(`^the ${Q}images should be rendered(?: within (\\d+) seconds?)?$`), async function (sec) {
|
|
217
|
+
const failed = await settleThenWait(this.page, budgetOf(sec), (sel) => document.querySelector(sel) !== null, WRAPPER);
|
|
218
|
+
if (failed) {
|
|
219
|
+
throw friendly('Expected at least one Drimage image on the page, but no drimage_improved formatter output is in the DOM', failed.cause);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Assert the Drimage images offer a WebP source: every rendered wrapper has a
|
|
225
|
+
* <source type="image/webp"> in its <picture>.
|
|
226
|
+
*
|
|
227
|
+
* Present when core WebP or ImageAPI Optimize WebP is on; this is how a test
|
|
228
|
+
* proves the setting reached the front end.
|
|
229
|
+
*
|
|
230
|
+
* Example #1: Then the drimage images should offer webp
|
|
231
|
+
* Example #2: Then the drimage improved images should offer webp within 2 seconds
|
|
232
|
+
* Example #3: Then the dynamic responsive images should offer webp
|
|
233
|
+
* Example #4: And the responsive images should offer webp within 1 second
|
|
234
|
+
* Example #5: Then the images should offer webp
|
|
235
|
+
*/
|
|
236
|
+
Then(new RegExp(`^the ${Q}images should offer webp(?: within (\\d+) seconds?)?$`), async function (sec) {
|
|
237
|
+
const failed = await settleThenWait(this.page, budgetOf(sec), ({ wrapper, source }) => {
|
|
238
|
+
const wrappers = Array.from(document.querySelectorAll(wrapper));
|
|
239
|
+
return wrappers.length > 0 && wrappers.every((w) => w.querySelector(source) !== null);
|
|
240
|
+
}, { wrapper: WRAPPER, source: WEBP_SOURCE });
|
|
241
|
+
if (failed) {
|
|
242
|
+
throw friendly('Expected every Drimage image to carry a WebP <source>, but at least one <picture> has none', failed.cause);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Assert the Drimage images carry a <noscript> fallback, so a visitor without
|
|
248
|
+
* JavaScript still gets an image. The module template puts it inside the
|
|
249
|
+
* wrapper; the vartheme_bs5 component puts it right after — both count.
|
|
250
|
+
*
|
|
251
|
+
* Example #1: Then the drimage images should have a noscript fallback
|
|
252
|
+
* Example #2: Then the drimage improved images should have a noscript fallback within 2 seconds
|
|
253
|
+
* Example #3: Then the dynamic responsive images should have a noscript fallback
|
|
254
|
+
* Example #4: And the responsive images should have a noscript fallback within 1 second
|
|
255
|
+
* Example #5: Then the images should have a noscript fallback
|
|
256
|
+
*/
|
|
257
|
+
Then(new RegExp(`^the ${Q}images should have a noscript fallback(?: within (\\d+) seconds?)?$`), async function (sec) {
|
|
258
|
+
const failed = await settleThenWait(this.page, budgetOf(sec), (wrapper) => {
|
|
259
|
+
const wrappers = Array.from(document.querySelectorAll(wrapper));
|
|
260
|
+
const hasFallback = (w) => w.querySelector('noscript') !== null
|
|
261
|
+
|| (w.nextElementSibling !== null && w.nextElementSibling.tagName === 'NOSCRIPT');
|
|
262
|
+
return wrappers.length > 0 && wrappers.every(hasFallback);
|
|
263
|
+
}, WRAPPER);
|
|
264
|
+
if (failed) {
|
|
265
|
+
throw friendly('Expected every Drimage image to carry a <noscript> fallback, but at least one has none', failed.cause);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Assert no Drimage image on the page is broken: every one that swapped to a
|
|
271
|
+
* derivative decoded it. Images still on their placeholder pass, so this is
|
|
272
|
+
* the safe check for a long page where lazy loading has not reached the
|
|
273
|
+
* bottom.
|
|
274
|
+
*
|
|
275
|
+
* Example #1: Then no drimage image should be broken
|
|
276
|
+
* Example #2: Then no drimage improved image should be broken within 2 seconds
|
|
277
|
+
* Example #3: Then no dynamic responsive image should be broken
|
|
278
|
+
* Example #4: And no responsive image should be broken within 1 second
|
|
279
|
+
* Example #5: Then no image should be broken
|
|
280
|
+
*/
|
|
281
|
+
Then(new RegExp(`^no ${Q}image should be broken(?: within (\\d+) seconds?)?$`), async function (sec) {
|
|
282
|
+
const failed = await settleThenWait(this.page, budgetOf(sec), ({ image, derivative }) =>
|
|
283
|
+
Array.from(document.querySelectorAll(image))
|
|
284
|
+
.filter((img) => (img.currentSrc || img.src || '').includes(derivative))
|
|
285
|
+
.every((img) => img.complete && img.naturalWidth > 0), { image: IMAGE, derivative: DERIVATIVE });
|
|
286
|
+
if (failed) {
|
|
287
|
+
const broken = failed.images.filter((i) => i.derivative && !i.decoded);
|
|
288
|
+
throw friendly(`Expected no broken Drimage image, but ${describe(broken)}`, failed.cause);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Assert every loaded Drimage image is served at a derivative width that fits
|
|
294
|
+
* where it is rendered: no wider than the rendered width (times the device
|
|
295
|
+
* pixel ratio) plus one threshold step, and no narrower than the rendered
|
|
296
|
+
* width, within the module's upscale / downscale clamps.
|
|
297
|
+
*
|
|
298
|
+
* This is the responsive-image check. Resize the viewport with the harness's
|
|
299
|
+
* `set the viewport to the "xs" breakpoint` and assert again: the module
|
|
300
|
+
* requests a smaller derivative for the smaller viewport, and a page that
|
|
301
|
+
* ships a 1920px derivative into a 375px phone fails here. The threshold,
|
|
302
|
+
* upscale, downscale and multiplier are read from each wrapper's own data
|
|
303
|
+
* object, so the test follows the site's settings rather than hardcoding them.
|
|
304
|
+
*
|
|
305
|
+
* Example #1: Then the drimage images should be sized for the viewport
|
|
306
|
+
* Example #2: Then the drimage improved images should be sized for the viewport within 2 seconds
|
|
307
|
+
* Example #3: Then the dynamic responsive images should be sized for the viewport
|
|
308
|
+
* Example #4: And the responsive images should be sized for the viewport within 1 second
|
|
309
|
+
* Example #5: Then the images should be sized for the viewport
|
|
310
|
+
*/
|
|
311
|
+
Then(new RegExp(`^the ${Q}images should be sized for the viewport(?: within (\\d+) seconds?)?$`), async function (sec) {
|
|
312
|
+
const failed = await settleThenWait(this.page, budgetOf(sec), ({ wrapper, derivative }) => {
|
|
313
|
+
const wrappers = Array.from(document.querySelectorAll(wrapper));
|
|
314
|
+
const loaded = wrappers.filter((w) => {
|
|
315
|
+
const img = w.querySelector('img');
|
|
316
|
+
return img && (img.currentSrc || img.src || '').includes(derivative);
|
|
317
|
+
});
|
|
318
|
+
if (!loaded.length) return false;
|
|
319
|
+
return loaded.every((w) => {
|
|
320
|
+
const img = w.querySelector('img');
|
|
321
|
+
const data = JSON.parse(w.getAttribute('data-drimage_improved') || '{}');
|
|
322
|
+
const threshold = Number(data.threshold) || 200;
|
|
323
|
+
const upscale = Number(data.upscale) || 0;
|
|
324
|
+
const downscale = Number(data.downscale) || Infinity;
|
|
325
|
+
const m = (img.currentSrc || img.src).match(/\/styles\/drimage_improved_(?:focal_)?(\d+)_/);
|
|
326
|
+
if (!m) return false;
|
|
327
|
+
const served = Number(m[1]);
|
|
328
|
+
const rendered = img.clientWidth * (window.devicePixelRatio || 1) * (Number(data.multiplier) || 1);
|
|
329
|
+
const wanted = Math.min(downscale, Math.max(upscale, rendered));
|
|
330
|
+
return served >= Math.floor(wanted) - threshold && served <= Math.ceil(wanted) + threshold;
|
|
331
|
+
});
|
|
332
|
+
}, { wrapper: WRAPPER, derivative: DERIVATIVE });
|
|
333
|
+
if (failed) {
|
|
334
|
+
const loaded = failed.images.filter((i) => i.derivative);
|
|
335
|
+
const detail = loaded.length
|
|
336
|
+
? loaded.map((i) => `"${i.alt || '(no alt)'}": served ${i.derivativeWidth}px for ${i.renderedWidth}px rendered`).join('; ')
|
|
337
|
+
: 'no Drimage image has loaded a derivative yet';
|
|
338
|
+
throw friendly(`Expected every Drimage derivative to fit its rendered width, but ${detail}`, failed.cause);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Assert the Drimage images defer offscreen loading. Each wrapper's own data
|
|
344
|
+
* object must ask for lazy loading, and every image must be either still on
|
|
345
|
+
* its placeholder (the module has not rendered it because it is not near the
|
|
346
|
+
* viewport yet — that deferral IS the lazy loading) or carry `loading="lazy"`,
|
|
347
|
+
* which the module sets the moment it renders one.
|
|
348
|
+
*
|
|
349
|
+
* Learnt on demo.varbase.vardot.com: the module puts no `loading` attribute
|
|
350
|
+
* on an image it has not reached, and hidden slider slides never get one, so
|
|
351
|
+
* "every img has loading=lazy" would fail a page that is lazy loading
|
|
352
|
+
* perfectly. An image that swapped to a derivative WITHOUT the attribute, or a
|
|
353
|
+
* wrapper configured eager, is what fails here.
|
|
354
|
+
*
|
|
355
|
+
* Pair it with "should still be a placeholder" on a below-the-fold image, a
|
|
356
|
+
* scroll step, and "should be loaded" to prove the deferral end to end.
|
|
357
|
+
*
|
|
358
|
+
* Example #1: Then the drimage images should use lazy loading
|
|
359
|
+
* Example #2: Then the drimage improved images should use lazy loading within 2 seconds
|
|
360
|
+
* Example #3: Then the dynamic responsive images should use lazy loading
|
|
361
|
+
* Example #4: And the responsive images should use lazy loading within 1 second
|
|
362
|
+
* Example #5: Then the images should use lazy loading
|
|
363
|
+
*/
|
|
364
|
+
Then(new RegExp(`^the ${Q}images should use lazy loading(?: within (\\d+) seconds?)?$`), async function (sec) {
|
|
365
|
+
const failed = await settleThenWait(this.page, budgetOf(sec), ({ wrapper, derivative }) => {
|
|
366
|
+
const wrappers = Array.from(document.querySelectorAll(wrapper));
|
|
367
|
+
if (!wrappers.length) return false;
|
|
368
|
+
return wrappers.every((w) => {
|
|
369
|
+
const img = w.querySelector('img');
|
|
370
|
+
if (!img) return false;
|
|
371
|
+
const data = JSON.parse(w.getAttribute('data-drimage_improved') || '{}');
|
|
372
|
+
if (data.lazyload && data.lazyload !== 'lazy' && data.lazyload !== 'legacy') return false;
|
|
373
|
+
const swapped = (img.currentSrc || img.src || '').includes(derivative);
|
|
374
|
+
return !swapped || img.getAttribute('loading') === 'lazy' || data.lazyload === 'legacy';
|
|
375
|
+
});
|
|
376
|
+
}, { wrapper: WRAPPER, derivative: DERIVATIVE });
|
|
377
|
+
if (failed) {
|
|
378
|
+
const eager = failed.images.filter((i) => i.derivative && i.loading !== 'lazy');
|
|
379
|
+
const detail = failed.images.length === 0
|
|
380
|
+
? 'no Drimage image is on the page'
|
|
381
|
+
: eager.length
|
|
382
|
+
? `these loaded a derivative without it: ${eager.map((i) => `"${i.alt || '(no alt)'}" (loading="${i.loading || 'not set'}")`).join(', ')}`
|
|
383
|
+
: 'at least one wrapper is configured not to lazy load';
|
|
384
|
+
throw friendly(`Expected the Drimage images to lazy load, but ${detail}`, failed.cause);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Assert a Drimage image has NOT loaded yet: it is still on the module's SVG
|
|
390
|
+
* placeholder because it sits outside the viewport and lazy loading has not
|
|
391
|
+
* reached it. Name it any of the ways the loaded step accepts.
|
|
392
|
+
*
|
|
393
|
+
* This is the intersection check. Settle the page, look once — no waiting for
|
|
394
|
+
* something not to happen — then scroll it into view and assert it loads.
|
|
395
|
+
*
|
|
396
|
+
* Example #1: Then the drimage image "Team meeting around a table" should still be a placeholder
|
|
397
|
+
* Example #2: Then the last drimage image should not be loaded yet
|
|
398
|
+
* Example #3: Then the drimage image captioned "Our team in the new office" should still be a placeholder
|
|
399
|
+
* Example #4: And the responsive image number 2 should not be loaded yet
|
|
400
|
+
* Example #5: Then the second image should still be a placeholder
|
|
401
|
+
*/
|
|
402
|
+
Then(new RegExp(`^${TARGET} should (?:still be a placeholder|not be loaded yet)$`), async function (ordinal, mode, modeValue, plain, number) {
|
|
403
|
+
const t = targetOf(ordinal, mode, modeValue, plain, number);
|
|
404
|
+
const { picked } = await resolveTarget(this.page, t);
|
|
405
|
+
if (picked.some((i) => i.derivative)) {
|
|
406
|
+
throw friendly(`Expected ${describeTarget(t)} to still be a placeholder, but it already loaded a derivative: ${describe(picked)}`);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Assert a Drimage image has a caption: its <figure> carries a <figcaption>,
|
|
412
|
+
* and when a text is given, that is the caption (exact after trimming).
|
|
413
|
+
*
|
|
414
|
+
* Example #1: Then the first drimage image should have the caption "Our team in the new office"
|
|
415
|
+
* Example #2: Then the drimage image "Team meeting around a table" should have a caption
|
|
416
|
+
* Example #3: Then the drimage image titled "Vardot team" should have the caption "Our team in the new office"
|
|
417
|
+
* Example #4: Then the last dynamic responsive image should have a caption
|
|
418
|
+
* Example #5: Then the drimage image number 2 should have the caption "Our team in the new office"
|
|
419
|
+
*/
|
|
420
|
+
Then(new RegExp(`^${TARGET} should have (?:a caption|the caption "([^"]*)")$`), async function (ordinal, mode, modeValue, plain, number, text) {
|
|
421
|
+
const t = targetOf(ordinal, mode, modeValue, plain, number);
|
|
422
|
+
const { picked } = await resolveTarget(this.page, t);
|
|
423
|
+
const bad = picked.filter((i) => !i.caption || (text != null && i.caption !== text.trim()));
|
|
424
|
+
if (bad.length) {
|
|
425
|
+
const seen = bad.map((i) => (i.caption ? `"${i.caption}"` : (i.inFigure ? 'a figure with no figcaption' : 'no figure at all'))).join(', ');
|
|
426
|
+
throw friendly(text != null
|
|
427
|
+
? `Expected ${describeTarget(t)} to have the caption "${text}", but saw ${seen}`
|
|
428
|
+
: `Expected ${describeTarget(t)} to have a caption, but saw ${seen}`);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Assert a Drimage image's alt text — what a screen reader says and what
|
|
434
|
+
* search engines index. Name the image by order, title or caption, then check
|
|
435
|
+
* the alt an editor typed reached the front end.
|
|
436
|
+
*
|
|
437
|
+
* Example #1: Then the first drimage image should have the alt text "Team collaborating in a modern glass-walled office"
|
|
438
|
+
* Example #2: Then the drimage image captioned "Our team in the new office" should have the alt text "Team meeting around a table"
|
|
439
|
+
* Example #3: Then the last dynamic responsive image should have the alt text "Team meeting around a table"
|
|
440
|
+
* Example #4: Then the drimage image number 1 should have the alt text "Team collaborating in a modern glass-walled office"
|
|
441
|
+
* Example #5: Then the drimage image titled "Vardot team" should have the alt text "Team meeting around a table"
|
|
442
|
+
*/
|
|
443
|
+
Then(new RegExp(`^${TARGET} should have the alt text "([^"]*)"$`), async function (ordinal, mode, modeValue, plain, number, text) {
|
|
444
|
+
const t = targetOf(ordinal, mode, modeValue, plain, number);
|
|
445
|
+
const { picked } = await resolveTarget(this.page, t);
|
|
446
|
+
const bad = picked.filter((i) => i.alt !== text.trim());
|
|
447
|
+
if (bad.length) {
|
|
448
|
+
throw friendly(`Expected ${describeTarget(t)} to have the alt text "${text}", but saw ${bad.map((i) => (i.alt ? `"${i.alt}"` : 'an empty alt')).join(', ')}`);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Assert a Drimage image's title attribute, the tooltip text.
|
|
454
|
+
*
|
|
455
|
+
* Example #1: Then the drimage image "Team meeting around a table" should have the title "Vardot team"
|
|
456
|
+
* Example #2: Then the second drimage image should have the title "Vardot team"
|
|
457
|
+
* Example #3: Then the drimage image captioned "Our team in the new office" should have the title "Vardot team"
|
|
458
|
+
* Example #4: Then the last dynamic responsive image should have the title "Vardot team"
|
|
459
|
+
* Example #5: Then the drimage image number 2 should have the title "Vardot team"
|
|
460
|
+
*/
|
|
461
|
+
Then(new RegExp(`^${TARGET} should have the title "([^"]*)"$`), async function (ordinal, mode, modeValue, plain, number, text) {
|
|
462
|
+
const t = targetOf(ordinal, mode, modeValue, plain, number);
|
|
463
|
+
const { picked } = await resolveTarget(this.page, t);
|
|
464
|
+
const bad = picked.filter((i) => i.title !== text.trim());
|
|
465
|
+
if (bad.length) {
|
|
466
|
+
throw friendly(`Expected ${describeTarget(t)} to have the title "${text}", but saw ${bad.map((i) => (i.title ? `"${i.title}"` : 'no title')).join(', ')}`);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Assert a Drimage image is wrapped in a <figure>, the semantic element the
|
|
472
|
+
* theme's image component renders so a caption has somewhere to live.
|
|
473
|
+
*
|
|
474
|
+
* Example #1: Then the drimage image "Team collaborating in a modern glass-walled office" should be in a figure
|
|
475
|
+
* Example #2: Then the first drimage image should be in a figure
|
|
476
|
+
* Example #3: Then the drimage image captioned "Our team in the new office" should be in a figure
|
|
477
|
+
* Example #4: Then the last dynamic responsive image should be in a figure
|
|
478
|
+
* Example #5: Then the drimage image number 2 should be in a figure
|
|
479
|
+
*/
|
|
480
|
+
Then(new RegExp(`^${TARGET} should be in a figure$`), async function (ordinal, mode, modeValue, plain, number) {
|
|
481
|
+
const t = targetOf(ordinal, mode, modeValue, plain, number);
|
|
482
|
+
const { picked } = await resolveTarget(this.page, t);
|
|
483
|
+
if (picked.some((i) => !i.inFigure)) {
|
|
484
|
+
throw friendly(`Expected ${describeTarget(t)} to be inside a <figure>, but it is not`);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// All navigation step definitions live in this file: anonymous user, going
|
|
4
4
|
// to the homepage / a specific path, browser history (back/forward), reload,
|
|
5
|
-
//
|
|
5
|
+
// URL/path assertions, and the access-control assertions (is this path
|
|
6
|
+
// refused or allowed for the current user).
|
|
6
7
|
|
|
7
8
|
const { Given, When, Then } = require('@cucumber/cucumber');
|
|
8
9
|
const assert = require('assert');
|
|
@@ -216,3 +217,102 @@ Then(/^(the )*url should( not)* match "([^"]*)?"$/, async function (theCase, not
|
|
|
216
217
|
assert.ok(regex.test(currentUrl), `URL "${currentUrl}" should match "${pattern}" but it does not.`);
|
|
217
218
|
}
|
|
218
219
|
});
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Access control — is this path refused or allowed for the current user
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
// A refusal is not one signal. A site can answer 403, answer 404 to keep a
|
|
226
|
+
// route unguessable, answer 200 with an access-denied page, or redirect to the
|
|
227
|
+
// login form — all four are "you may not see this". This reads the response
|
|
228
|
+
// status and, when the status alone is inconclusive, the rendered page.
|
|
229
|
+
const DENIED_MARKERS = 'h1:has-text("Access denied"), #user-login-form, form.user-login-form, form.user-login, input[name="pass"]';
|
|
230
|
+
|
|
231
|
+
async function accessReport(page, response) {
|
|
232
|
+
const status = response ? response.status() : 0;
|
|
233
|
+
const url = page.url();
|
|
234
|
+
const markers = await page.locator(DENIED_MARKERS).count().catch(() => 0);
|
|
235
|
+
const body = (await page.locator('body').innerText().catch(() => '') || '').toLowerCase();
|
|
236
|
+
const text = body.includes('access denied') || body.includes('you are not authorized');
|
|
237
|
+
return {
|
|
238
|
+
status,
|
|
239
|
+
url,
|
|
240
|
+
denied: status === 403 || status === 404 || markers > 0 || /\/user\/login/.test(url) || text,
|
|
241
|
+
why: status === 403 || status === 404 ? `HTTP ${status}`
|
|
242
|
+
: markers > 0 ? 'an access-denied page or a log-in form'
|
|
243
|
+
: /\/user\/login/.test(url) ? 'a redirect to the log-in form'
|
|
244
|
+
: text ? 'access-denied text in the page' : `HTTP ${status} and a rendered page`,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Assert the current user may NOT open a path: the site refuses it.
|
|
250
|
+
*
|
|
251
|
+
* Four refusals count, because a site picks its own: HTTP 403, HTTP 404 (used
|
|
252
|
+
* to keep a route unguessable), HTTP 200 with an access-denied page, and a
|
|
253
|
+
* redirect to the log-in form. Asserting only on 403 makes a scenario pass or
|
|
254
|
+
* fail on how the site is configured rather than on who the user is.
|
|
255
|
+
*
|
|
256
|
+
* Example #1: Then I am denied access to "/admin/config"
|
|
257
|
+
* Example #2: Then I should be refused "/node/add/article"
|
|
258
|
+
* Example #3: And we should be denied access to "/admin/people"
|
|
259
|
+
* Example #4: Given I am an anonymous user
|
|
260
|
+
* Then I am denied access to "/admin/modules"
|
|
261
|
+
* Example #5: Then we am denied access to "/admin/reports/status"
|
|
262
|
+
*
|
|
263
|
+
*/
|
|
264
|
+
Then(/^(I |we )*(?:am|should be) (?:denied access to|refused) "([^"]*)?"$/, async function (pronounCase, path) {
|
|
265
|
+
const response = await this.page.goto(this.launchUrl + path, { waitUntil: 'domcontentloaded' }).catch(() => null);
|
|
266
|
+
const report = await accessReport(this.page, response);
|
|
267
|
+
assert.ok(report.denied, `Expected "${path}" to be refused, but the site answered ${report.why}.`);
|
|
268
|
+
await smartSettle(this.page, 2000);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Assert the current user MAY open a path: the site answers it and renders it.
|
|
273
|
+
*
|
|
274
|
+
* The positive half of a permission scenario, and it earns its place — a role
|
|
275
|
+
* that can reach nothing at all passes every refusal assertion in the suite,
|
|
276
|
+
* so the refusals only mean something next to this.
|
|
277
|
+
*
|
|
278
|
+
* Example #1: Then I should be allowed "/admin/content"
|
|
279
|
+
* Example #2: Then I am granted access to "/node/add/article"
|
|
280
|
+
* Example #3: And we should be allowed "/admin/content/media"
|
|
281
|
+
* Example #4: Given I am a logged in user with the username "webmaster" user
|
|
282
|
+
* Then I should be allowed "/admin/content"
|
|
283
|
+
* Example #5: Then we should be allowed "/user"
|
|
284
|
+
*
|
|
285
|
+
*/
|
|
286
|
+
Then(/^(I |we )*(?:am|should be) (?:allowed|granted access to) "([^"]*)?"$/, async function (pronounCase, path) {
|
|
287
|
+
const response = await this.page.goto(this.launchUrl + path, { waitUntil: 'domcontentloaded' }).catch(() => null);
|
|
288
|
+
const report = await accessReport(this.page, response);
|
|
289
|
+
assert.ok(report.status >= 200 && report.status < 300, `Expected "${path}" to be allowed, but the site answered HTTP ${report.status}.`);
|
|
290
|
+
assert.ok(!report.denied, `Expected "${path}" to be allowed, but the site answered ${report.why}.`);
|
|
291
|
+
await smartSettle(this.page, 2000);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Assert the page already open is a refusal — an access-denied page, or the
|
|
296
|
+
* log-in form the site redirected to.
|
|
297
|
+
*
|
|
298
|
+
* Use it when the refusal is the result of an action rather than of a visit:
|
|
299
|
+
* submit a form, click an operation, follow a link, then assert the wall. It
|
|
300
|
+
* navigates nothing, so nothing about the failed attempt is lost.
|
|
301
|
+
*
|
|
302
|
+
* Example #1: Then I should be denied access
|
|
303
|
+
* Example #2: Then the page should be access restricted
|
|
304
|
+
* Example #3: When I click "Edit"
|
|
305
|
+
* Then I should be denied access
|
|
306
|
+
* Example #4: And we should be denied access
|
|
307
|
+
* Example #5: Then the page should be access restricted
|
|
308
|
+
*
|
|
309
|
+
*/
|
|
310
|
+
Then(/^(?:(?:I |we )*should be denied access|(?:the )*page should be access restricted)$/, async function () {
|
|
311
|
+
await waitForPageLoad(this.page, (this.minWaitTime && this.minWaitTime.page) || 8000);
|
|
312
|
+
const report = await accessReport(this.page, null);
|
|
313
|
+
const markers = await this.page.locator(DENIED_MARKERS).count().catch(() => 0);
|
|
314
|
+
const body = (await this.page.locator('body').innerText().catch(() => '') || '').toLowerCase();
|
|
315
|
+
const denied = markers > 0 || /\/user\/login/.test(report.url)
|
|
316
|
+
|| body.includes('access denied') || body.includes('you are not authorized');
|
|
317
|
+
assert.ok(denied, `Expected an access-denied page or a log-in form, but "${report.url}" rendered normally.`);
|
|
318
|
+
});
|