@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.
Files changed (281) hide show
  1. package/.playwright-mcp/console-2026-09-07T08-40-17-262Z.log +28 -0
  2. package/.playwright-mcp/console-2026-09-07T08-40-55-044Z.log +4 -0
  3. package/.playwright-mcp/console-2026-09-07T08-41-12-557Z.log +18 -0
  4. package/.playwright-mcp/console-2026-09-07T08-42-32-413Z.log +38 -0
  5. package/.playwright-mcp/console-2026-09-07T09-02-43-145Z.log +1 -0
  6. package/.playwright-mcp/console-2026-09-07T09-03-15-665Z.log +17 -0
  7. package/.playwright-mcp/page-2026-09-07T08-40-18-956Z.yml +261 -0
  8. package/.playwright-mcp/page-2026-09-07T08-40-55-673Z.yml +132 -0
  9. package/.playwright-mcp/page-2026-09-07T08-41-13-471Z.yml +386 -0
  10. package/.playwright-mcp/page-2026-09-07T08-42-33-250Z.yml +287 -0
  11. package/AGENTS.md +1 -1
  12. package/CLAUDE.md +6 -5
  13. package/README.md +2 -0
  14. package/docs/04-step-reference.md +15 -1
  15. package/docs/README.md +8 -5
  16. package/docs/install-varbase-e2e.md +0 -2
  17. package/docs/steps/README.md +6 -5
  18. package/docs/steps/assertion.md +90 -5
  19. package/docs/steps/drupal-drimage-improved.md +372 -0
  20. package/docs/steps/navigation.md +81 -1
  21. package/docs/steps/varbase.md +6 -65
  22. package/examples/access-denied.html +6 -0
  23. package/examples/drimage.html +55 -0
  24. package/examples/styles/drimage_improved_1000_667/public/sample.png +0 -0
  25. package/examples/styles/drimage_improved_1200_800/public/sample.png +0 -0
  26. package/examples/styles/drimage_improved_1400_933/public/sample.png +0 -0
  27. package/examples/styles/drimage_improved_1600_1067/public/sample.png +0 -0
  28. package/examples/styles/drimage_improved_1800_1200/public/sample.png +0 -0
  29. package/examples/styles/drimage_improved_2000_1333/public/sample.png +0 -0
  30. package/examples/styles/drimage_improved_200_133/public/sample.png +0 -0
  31. package/examples/styles/drimage_improved_2200_1467/public/sample.png +0 -0
  32. package/examples/styles/drimage_improved_2400_1600/public/sample.png +0 -0
  33. package/examples/styles/drimage_improved_2600_1734/public/sample.png +0 -0
  34. package/examples/styles/drimage_improved_2800_1867/public/sample.png +0 -0
  35. package/examples/styles/drimage_improved_3000_2000/public/sample.png +0 -0
  36. package/examples/styles/drimage_improved_3200_2134/public/sample.png +0 -0
  37. package/examples/styles/drimage_improved_3400_2267/public/sample.png +0 -0
  38. package/examples/styles/drimage_improved_3600_2400/public/sample.png +0 -0
  39. package/examples/styles/drimage_improved_3800_2534/public/sample.png +0 -0
  40. package/examples/styles/drimage_improved_4000_2667/public/sample.png +0 -0
  41. package/examples/styles/drimage_improved_400_267/public/sample.png +0 -0
  42. package/examples/styles/drimage_improved_600_400/public/sample.png +0 -0
  43. package/examples/styles/drimage_improved_800_533/public/sample.png +0 -0
  44. package/examples/user-login.html +12 -0
  45. package/images/varbase-e2e-logo.svg +24 -0
  46. package/package.json +1 -1
  47. package/tests/features/access.feature +33 -0
  48. package/tests/features/drupal-drimage-improved.feature +59 -0
  49. package/tests/features/http-status.feature +21 -0
  50. package/tests/features/page-title.feature +24 -0
  51. package/tests/step-definitions/assertion.steps.js +108 -19
  52. package/tests/step-definitions/drupal-drimage-improved.steps.js +486 -0
  53. package/tests/step-definitions/navigation.steps.js +101 -1
  54. package/tests/step-definitions/varbase.steps.js +0 -83
  55. package/.playwright-mcp/console-2026-08-09T19-48-04-420Z.log +0 -6
  56. package/.playwright-mcp/console-2026-08-09T19-48-21-910Z.log +0 -4
  57. package/.playwright-mcp/console-2026-08-09T19-48-37-544Z.log +0 -5
  58. package/.playwright-mcp/console-2026-08-09T20-02-58-240Z.log +0 -5
  59. package/.playwright-mcp/console-2026-08-09T20-15-05-487Z.log +0 -71
  60. package/.playwright-mcp/console-2026-08-09T20-31-11-228Z.log +0 -14
  61. package/.playwright-mcp/console-2026-08-09T20-34-03-115Z.log +0 -7
  62. package/.playwright-mcp/console-2026-08-09T20-34-34-018Z.log +0 -85
  63. package/.playwright-mcp/console-2026-08-09T20-34-34-740Z.log +0 -46
  64. package/.playwright-mcp/console-2026-08-09T20-34-35-618Z.log +0 -92
  65. package/.playwright-mcp/console-2026-08-09T22-57-59-606Z.log +0 -14
  66. package/.playwright-mcp/console-2026-08-09T23-09-53-902Z.log +0 -28
  67. package/.playwright-mcp/console-2026-08-09T23-09-55-918Z.log +0 -49
  68. package/.playwright-mcp/console-2026-08-09T23-09-57-522Z.log +0 -42
  69. package/.playwright-mcp/console-2026-08-09T23-40-26-847Z.log +0 -28
  70. package/.playwright-mcp/console-2026-08-10T00-05-53-423Z.log +0 -8
  71. package/.playwright-mcp/console-2026-08-10T08-51-59-747Z.log +0 -5
  72. package/.playwright-mcp/console-2026-08-10T09-00-26-735Z.log +0 -7
  73. package/.playwright-mcp/page-2026-08-09T19-48-23-422Z.yml +0 -288
  74. package/.playwright-mcp/page-2026-08-09T19-48-32-827Z.yml +0 -39
  75. package/.playwright-mcp/page-2026-08-09T19-48-39-150Z.yml +0 -39
  76. package/.playwright-mcp/page-2026-08-09T19-50-55-594Z.yml +0 -787
  77. package/.playwright-mcp/page-2026-08-09T19-51-28-160Z.yml +0 -391
  78. package/.playwright-mcp/page-2026-08-09T19-51-45-546Z.yml +0 -391
  79. package/.playwright-mcp/page-2026-08-09T19-51-46-085Z.yml +0 -391
  80. package/.playwright-mcp/page-2026-08-09T19-51-46-355Z.yml +0 -391
  81. package/.playwright-mcp/page-2026-08-09T19-51-49-918Z.yml +0 -391
  82. package/.playwright-mcp/page-2026-08-09T19-52-30-248Z.yml +0 -415
  83. package/.playwright-mcp/page-2026-08-09T19-52-42-510Z.yml +0 -415
  84. package/.playwright-mcp/page-2026-08-09T19-52-45-588Z.yml +0 -391
  85. package/.playwright-mcp/page-2026-08-09T19-52-54-615Z.yml +0 -391
  86. package/.playwright-mcp/page-2026-08-09T19-52-55-424Z.yml +0 -391
  87. package/.playwright-mcp/page-2026-08-09T19-52-55-553Z.yml +0 -391
  88. package/.playwright-mcp/page-2026-08-09T19-52-58-991Z.yml +0 -391
  89. package/.playwright-mcp/page-2026-08-09T19-53-29-261Z.yml +0 -415
  90. package/.playwright-mcp/page-2026-08-09T19-53-40-322Z.yml +0 -415
  91. package/.playwright-mcp/page-2026-08-09T19-53-44-146Z.yml +0 -391
  92. package/.playwright-mcp/page-2026-08-09T19-53-51-021Z.yml +0 -391
  93. package/.playwright-mcp/page-2026-08-09T19-53-51-565Z.yml +0 -391
  94. package/.playwright-mcp/page-2026-08-09T19-53-51-979Z.yml +0 -391
  95. package/.playwright-mcp/page-2026-08-09T19-53-54-883Z.yml +0 -391
  96. package/.playwright-mcp/page-2026-08-09T19-54-24-537Z.yml +0 -415
  97. package/.playwright-mcp/page-2026-08-09T20-01-07-921Z.yml +0 -404
  98. package/.playwright-mcp/page-2026-08-09T20-01-35-366Z.yml +0 -404
  99. package/.playwright-mcp/page-2026-08-09T20-01-54-083Z.yml +0 -404
  100. package/.playwright-mcp/page-2026-08-09T20-02-07-954Z.yml +0 -416
  101. package/.playwright-mcp/page-2026-08-09T20-02-24-800Z.yml +0 -404
  102. package/.playwright-mcp/page-2026-08-09T20-02-29-695Z.yml +0 -404
  103. package/.playwright-mcp/page-2026-08-09T20-02-33-819Z.yml +0 -404
  104. package/.playwright-mcp/page-2026-08-09T20-02-41-437Z.yml +0 -404
  105. package/.playwright-mcp/page-2026-08-09T20-02-53-232Z.yml +0 -416
  106. package/.playwright-mcp/page-2026-08-09T20-03-00-246Z.yml +0 -404
  107. package/.playwright-mcp/page-2026-08-09T20-03-03-297Z.yml +0 -404
  108. package/.playwright-mcp/page-2026-08-09T20-03-11-014Z.yml +0 -404
  109. package/.playwright-mcp/page-2026-08-09T20-03-21-049Z.yml +0 -416
  110. package/.playwright-mcp/page-2026-08-10T01-04-54-039Z.yml +0 -410
  111. package/.playwright-mcp/page-2026-08-10T01-05-06-967Z.png +0 -0
  112. package/.playwright-mcp/page-2026-08-10T01-05-52-136Z.yml +0 -720
  113. package/.playwright-mcp/page-2026-08-10T01-06-05-985Z.yml +0 -410
  114. package/.playwright-mcp/page-2026-08-10T01-06-25-942Z.yml +0 -726
  115. package/.playwright-mcp/page-2026-08-10T01-25-57-152Z.yml +0 -395
  116. package/.playwright-mcp/page-2026-08-10T01-26-48-853Z.yml +0 -395
  117. package/.playwright-mcp/page-2026-08-10T01-27-47-127Z.yml +0 -413
  118. package/.playwright-mcp/page-2026-08-10T01-27-57-349Z.yml +0 -413
  119. package/.playwright-mcp/page-2026-08-10T01-28-07-943Z.yml +0 -425
  120. package/.playwright-mcp/page-2026-08-10T01-28-30-462Z.yml +0 -369
  121. package/.playwright-mcp/page-2026-08-10T01-29-00-369Z.yml +0 -413
  122. package/.playwright-mcp/page-2026-08-10T01-29-08-988Z.yml +0 -413
  123. package/.playwright-mcp/page-2026-08-10T01-29-19-888Z.yml +0 -425
  124. package/.playwright-mcp/page-2026-08-10T01-29-30-189Z.yml +0 -369
  125. package/.playwright-mcp/page-2026-08-10T01-30-08-272Z.yml +0 -413
  126. package/.playwright-mcp/page-2026-08-10T01-30-16-955Z.yml +0 -413
  127. package/.playwright-mcp/page-2026-08-10T01-30-27-612Z.yml +0 -425
  128. package/.playwright-mcp/page-2026-08-10T01-30-37-943Z.yml +0 -465
  129. package/.playwright-mcp/page-2026-08-10T01-31-15-265Z.yml +0 -413
  130. package/.playwright-mcp/page-2026-08-10T01-31-24-328Z.yml +0 -413
  131. package/.playwright-mcp/page-2026-08-10T01-31-36-622Z.yml +0 -425
  132. package/.playwright-mcp/page-2026-08-10T01-31-45-965Z.yml +0 -470
  133. package/.playwright-mcp/page-2026-08-10T01-32-13-400Z.yml +0 -413
  134. package/.playwright-mcp/page-2026-08-10T01-32-23-469Z.yml +0 -413
  135. package/.playwright-mcp/page-2026-08-10T01-32-33-859Z.yml +0 -425
  136. package/.playwright-mcp/page-2026-08-10T01-32-43-461Z.yml +0 -513
  137. package/.playwright-mcp/page-2026-08-10T01-33-13-160Z.yml +0 -413
  138. package/.playwright-mcp/page-2026-08-10T01-33-23-937Z.yml +0 -413
  139. package/.playwright-mcp/page-2026-08-10T01-33-35-367Z.yml +0 -425
  140. package/.playwright-mcp/page-2026-08-10T01-33-47-266Z.yml +0 -459
  141. package/.playwright-mcp/page-2026-08-10T01-34-14-452Z.yml +0 -413
  142. package/.playwright-mcp/page-2026-08-10T01-34-26-042Z.yml +0 -413
  143. package/.playwright-mcp/page-2026-08-10T01-34-40-869Z.yml +0 -425
  144. package/.playwright-mcp/page-2026-08-10T01-34-49-587Z.yml +0 -391
  145. package/.playwright-mcp/page-2026-08-10T01-35-16-735Z.yml +0 -413
  146. package/.playwright-mcp/page-2026-08-10T01-35-24-882Z.yml +0 -413
  147. package/.playwright-mcp/page-2026-08-10T01-35-36-165Z.yml +0 -425
  148. package/.playwright-mcp/page-2026-08-10T01-35-45-644Z.yml +0 -455
  149. package/.playwright-mcp/page-2026-08-10T01-36-18-148Z.yml +0 -413
  150. package/.playwright-mcp/page-2026-08-10T01-36-26-751Z.yml +0 -413
  151. package/.playwright-mcp/page-2026-08-10T01-36-37-834Z.yml +0 -425
  152. package/.playwright-mcp/page-2026-08-10T01-36-48-078Z.yml +0 -370
  153. package/.playwright-mcp/page-2026-08-10T01-37-16-309Z.yml +0 -413
  154. package/.playwright-mcp/page-2026-08-10T01-37-28-723Z.yml +0 -413
  155. package/.playwright-mcp/page-2026-08-10T01-37-39-325Z.yml +0 -425
  156. package/.playwright-mcp/page-2026-08-10T01-37-48-278Z.yml +0 -409
  157. package/.playwright-mcp/page-2026-08-10T01-38-27-405Z.yml +0 -413
  158. package/.playwright-mcp/page-2026-08-10T01-38-36-994Z.yml +0 -413
  159. package/.playwright-mcp/page-2026-08-10T01-38-49-584Z.yml +0 -425
  160. package/.playwright-mcp/page-2026-08-10T01-38-59-289Z.yml +0 -396
  161. package/.playwright-mcp/page-2026-08-10T01-39-24-880Z.yml +0 -413
  162. package/.playwright-mcp/page-2026-08-10T01-39-35-551Z.yml +0 -413
  163. package/.playwright-mcp/page-2026-08-10T01-39-47-960Z.yml +0 -425
  164. package/.playwright-mcp/page-2026-08-10T01-39-56-844Z.yml +0 -549
  165. package/.playwright-mcp/page-2026-08-10T01-40-24-645Z.yml +0 -413
  166. package/.playwright-mcp/page-2026-08-10T01-40-33-806Z.yml +0 -413
  167. package/.playwright-mcp/page-2026-08-10T01-40-46-946Z.yml +0 -425
  168. package/.playwright-mcp/page-2026-08-10T01-40-56-503Z.yml +0 -443
  169. package/.playwright-mcp/page-2026-08-10T01-41-32-064Z.yml +0 -413
  170. package/.playwright-mcp/page-2026-08-10T01-41-40-666Z.yml +0 -413
  171. package/.playwright-mcp/page-2026-08-10T01-41-51-325Z.yml +0 -425
  172. package/.playwright-mcp/page-2026-08-10T01-41-58-790Z.yml +0 -368
  173. package/.playwright-mcp/page-2026-08-10T01-42-26-459Z.yml +0 -413
  174. package/.playwright-mcp/page-2026-08-10T01-42-38-306Z.yml +0 -413
  175. package/.playwright-mcp/page-2026-08-10T01-42-49-073Z.yml +0 -425
  176. package/.playwright-mcp/page-2026-08-10T01-42-57-453Z.yml +0 -443
  177. package/.playwright-mcp/page-2026-08-10T01-43-32-099Z.yml +0 -413
  178. package/.playwright-mcp/page-2026-08-10T01-43-42-882Z.yml +0 -413
  179. package/.playwright-mcp/page-2026-08-10T01-43-54-853Z.yml +0 -425
  180. package/.playwright-mcp/page-2026-08-10T01-44-04-373Z.yml +0 -399
  181. package/.playwright-mcp/page-2026-08-10T01-44-30-874Z.yml +0 -413
  182. package/.playwright-mcp/page-2026-08-10T01-44-39-830Z.yml +0 -413
  183. package/.playwright-mcp/page-2026-08-10T01-44-51-515Z.yml +0 -425
  184. package/.playwright-mcp/page-2026-08-10T01-44-59-974Z.yml +0 -503
  185. package/.playwright-mcp/page-2026-08-10T01-45-26-748Z.yml +0 -413
  186. package/.playwright-mcp/page-2026-08-10T01-45-36-685Z.yml +0 -413
  187. package/.playwright-mcp/page-2026-08-10T01-45-47-715Z.yml +0 -425
  188. package/.playwright-mcp/page-2026-08-10T01-45-57-895Z.yml +0 -469
  189. package/.playwright-mcp/page-2026-08-10T01-46-36-371Z.yml +0 -413
  190. package/.playwright-mcp/page-2026-08-10T01-46-49-906Z.yml +0 -413
  191. package/.playwright-mcp/page-2026-08-10T01-47-01-554Z.yml +0 -425
  192. package/.playwright-mcp/page-2026-08-10T01-47-12-712Z.yml +0 -581
  193. package/.playwright-mcp/page-2026-08-10T01-47-39-024Z.yml +0 -413
  194. package/.playwright-mcp/page-2026-08-10T01-47-49-714Z.yml +0 -413
  195. package/.playwright-mcp/page-2026-08-10T01-48-00-608Z.yml +0 -425
  196. package/.playwright-mcp/page-2026-08-10T01-48-09-739Z.yml +0 -604
  197. package/.playwright-mcp/page-2026-08-10T01-48-51-940Z.yml +0 -413
  198. package/.playwright-mcp/page-2026-08-10T01-49-01-562Z.yml +0 -413
  199. package/.playwright-mcp/page-2026-08-10T01-49-13-751Z.yml +0 -425
  200. package/.playwright-mcp/page-2026-08-10T01-49-22-503Z.yml +0 -400
  201. package/.playwright-mcp/page-2026-08-10T01-49-49-311Z.yml +0 -413
  202. package/.playwright-mcp/page-2026-08-10T01-50-00-769Z.yml +0 -413
  203. package/.playwright-mcp/page-2026-08-10T01-50-12-083Z.yml +0 -425
  204. package/.playwright-mcp/page-2026-08-10T01-50-23-765Z.yml +0 -458
  205. package/.playwright-mcp/page-2026-08-10T01-51-02-339Z.yml +0 -413
  206. package/.playwright-mcp/page-2026-08-10T01-51-12-711Z.yml +0 -413
  207. package/.playwright-mcp/page-2026-08-10T01-51-25-370Z.yml +0 -425
  208. package/.playwright-mcp/page-2026-08-10T01-51-33-936Z.yml +0 -368
  209. package/.playwright-mcp/page-2026-08-10T01-52-09-467Z.yml +0 -413
  210. package/.playwright-mcp/page-2026-08-10T01-52-21-616Z.yml +0 -413
  211. package/.playwright-mcp/page-2026-08-10T01-52-32-806Z.yml +0 -425
  212. package/.playwright-mcp/page-2026-08-10T01-52-42-382Z.yml +0 -407
  213. package/.playwright-mcp/page-2026-08-10T01-53-11-994Z.yml +0 -413
  214. package/.playwright-mcp/page-2026-08-10T01-53-21-669Z.yml +0 -413
  215. package/.playwright-mcp/page-2026-08-10T01-53-33-141Z.yml +0 -425
  216. package/.playwright-mcp/page-2026-08-10T01-53-43-582Z.yml +0 -443
  217. package/.playwright-mcp/page-2026-08-10T01-54-19-884Z.yml +0 -413
  218. package/.playwright-mcp/page-2026-08-10T01-54-32-359Z.yml +0 -413
  219. package/.playwright-mcp/page-2026-08-10T01-54-43-530Z.yml +0 -425
  220. package/.playwright-mcp/page-2026-08-10T01-54-53-891Z.yml +0 -474
  221. package/.playwright-mcp/page-2026-08-10T01-55-24-634Z.yml +0 -413
  222. package/.playwright-mcp/page-2026-08-10T01-55-35-283Z.yml +0 -413
  223. package/.playwright-mcp/page-2026-08-10T01-55-46-649Z.yml +0 -425
  224. package/.playwright-mcp/page-2026-08-10T01-55-56-585Z.yml +0 -431
  225. package/.playwright-mcp/page-2026-08-10T01-56-31-670Z.yml +0 -413
  226. package/.playwright-mcp/page-2026-08-10T01-56-40-782Z.yml +0 -413
  227. package/.playwright-mcp/page-2026-08-10T01-56-52-065Z.yml +0 -425
  228. package/.playwright-mcp/page-2026-08-10T01-57-03-705Z.yml +0 -445
  229. package/.playwright-mcp/page-2026-08-10T01-57-31-390Z.yml +0 -413
  230. package/.playwright-mcp/page-2026-08-10T01-57-44-756Z.yml +0 -413
  231. package/.playwright-mcp/page-2026-08-10T01-57-56-802Z.yml +0 -425
  232. package/.playwright-mcp/page-2026-08-10T01-58-06-937Z.yml +0 -468
  233. package/.playwright-mcp/page-2026-08-10T01-58-40-595Z.yml +0 -413
  234. package/.playwright-mcp/page-2026-08-10T01-58-51-715Z.yml +0 -413
  235. package/.playwright-mcp/page-2026-08-10T01-59-04-402Z.yml +0 -425
  236. package/.playwright-mcp/page-2026-08-10T01-59-15-638Z.yml +0 -472
  237. package/.playwright-mcp/page-2026-08-10T01-59-58-162Z.yml +0 -413
  238. package/.playwright-mcp/page-2026-08-10T02-00-09-962Z.yml +0 -413
  239. package/.playwright-mcp/page-2026-08-10T02-00-21-468Z.yml +0 -425
  240. package/.playwright-mcp/page-2026-08-10T02-00-33-248Z.yml +0 -451
  241. package/.playwright-mcp/page-2026-08-10T02-01-15-418Z.yml +0 -413
  242. package/.playwright-mcp/page-2026-08-10T02-01-27-182Z.yml +0 -413
  243. package/.playwright-mcp/page-2026-08-10T02-01-38-250Z.yml +0 -425
  244. package/.playwright-mcp/page-2026-08-10T04-20-27-979Z.yml +0 -513
  245. package/.playwright-mcp/page-2026-08-10T04-20-41-431Z.yml +0 -513
  246. package/.playwright-mcp/page-2026-08-10T04-21-46-133Z.yml +0 -406
  247. package/.playwright-mcp/page-2026-08-10T04-22-10-938Z.yml +0 -406
  248. package/.playwright-mcp/page-2026-08-10T04-23-00-626Z.yml +0 -406
  249. package/.playwright-mcp/page-2026-08-10T04-23-19-844Z.yml +0 -418
  250. package/.playwright-mcp/page-2026-08-10T04-25-00-732Z.yml +0 -455
  251. package/.playwright-mcp/page-2026-08-10T04-25-35-125Z.yml +0 -412
  252. package/.playwright-mcp/page-2026-08-10T04-25-46-640Z.yml +0 -412
  253. package/.playwright-mcp/page-2026-08-10T04-25-55-092Z.yml +0 -549
  254. package/.playwright-mcp/page-2026-08-10T04-26-26-403Z.yml +0 -406
  255. package/.playwright-mcp/page-2026-08-10T04-26-40-673Z.yml +0 -406
  256. package/.playwright-mcp/page-2026-08-10T04-26-48-427Z.yml +0 -604
  257. package/.playwright-mcp/page-2026-08-10T04-27-23-584Z.yml +0 -408
  258. package/.playwright-mcp/page-2026-08-10T04-27-34-294Z.yml +0 -408
  259. package/.playwright-mcp/page-2026-08-10T04-27-42-299Z.yml +0 -443
  260. package/.playwright-mcp/page-2026-08-10T04-28-20-032Z.yml +0 -406
  261. package/.playwright-mcp/page-2026-08-10T04-28-32-275Z.yml +0 -406
  262. package/.playwright-mcp/page-2026-08-10T04-28-41-592Z.yml +0 -581
  263. package/.playwright-mcp/page-2026-08-10T04-29-19-972Z.yml +0 -414
  264. package/.playwright-mcp/page-2026-08-10T04-29-30-961Z.yml +0 -414
  265. package/.playwright-mcp/page-2026-08-10T04-29-39-044Z.yml +0 -465
  266. package/.playwright-mcp/page-2026-08-10T04-30-13-196Z.yml +0 -402
  267. package/.playwright-mcp/page-2026-08-10T04-30-24-867Z.yml +0 -402
  268. package/.playwright-mcp/page-2026-08-10T04-30-32-372Z.yml +0 -409
  269. package/.playwright-mcp/page-2026-08-10T04-31-06-199Z.yml +0 -402
  270. package/.playwright-mcp/page-2026-08-10T04-31-17-733Z.yml +0 -402
  271. package/.playwright-mcp/page-2026-08-10T04-31-26-296Z.yml +0 -445
  272. package/.playwright-mcp/page-2026-08-10T04-32-02-456Z.yml +0 -402
  273. package/.playwright-mcp/page-2026-08-10T04-32-14-091Z.yml +0 -402
  274. package/.playwright-mcp/page-2026-08-10T04-32-23-437Z.yml +0 -396
  275. package/.playwright-mcp/page-2026-08-10T04-32-58-401Z.yml +0 -403
  276. package/.playwright-mcp/page-2026-08-10T04-33-09-818Z.yml +0 -403
  277. package/.playwright-mcp/page-2026-08-10T04-33-20-858Z.yml +0 -468
  278. package/.playwright-mcp/page-2026-08-10T04-33-53-074Z.yml +0 -407
  279. package/.playwright-mcp/page-2026-08-10T04-34-05-806Z.yml +0 -407
  280. package/.playwright-mcp/page-2026-08-10T08-52-00-165Z.yml +0 -0
  281. 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
- // and URL/path assertions.
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
+ });