@vardot/varbase-e2e 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/AGENTS.md +148 -0
  2. package/CLAUDE.md +544 -0
  3. package/README.md +22 -0
  4. package/bin/generate-reports.js +363 -0
  5. package/bin/init-varbase-e2e.js +330 -0
  6. package/bin/postinstall.js +26 -0
  7. package/cucumber.js +125 -0
  8. package/docs/00-quick-start.md +102 -0
  9. package/docs/01-getting-started.md +102 -0
  10. package/docs/02-bbr-smart-waits.md +77 -0
  11. package/docs/03-selector-registry.md +95 -0
  12. package/docs/04-step-reference.md +801 -0
  13. package/docs/05-web-first-assertions.md +91 -0
  14. package/docs/06-network-and-dialogs.md +83 -0
  15. package/docs/07-auth-state.md +75 -0
  16. package/docs/08-clock-mocking.md +59 -0
  17. package/docs/09-api-testing.md +114 -0
  18. package/docs/10-accessibility.md +167 -0
  19. package/docs/11-debugging.md +129 -0
  20. package/docs/12-ai-agent-guide.md +338 -0
  21. package/docs/13-faq.md +236 -0
  22. package/docs/14-recipes-cookbook.md +255 -0
  23. package/docs/15-tag-conventions.md +76 -0
  24. package/docs/16-ci-cd.md +90 -0
  25. package/docs/README.md +125 -0
  26. package/docs/advanced-screenshots/then-i-save-fullscreen-screenshot.md +36 -0
  27. package/docs/advanced-screenshots/then-i-save-width-x-height-screenshot.md +34 -0
  28. package/docs/advanced-screenshots/when-i-save-screenshot-name-filename.md +45 -0
  29. package/docs/advanced-screenshots.md +52 -0
  30. package/docs/advanced-selectors.md +18 -0
  31. package/docs/api-step-definitions.md +45 -0
  32. package/docs/global-settings.md +267 -0
  33. package/docs/install-varbase-e2e/ddev-varbase-e2e.md +52 -0
  34. package/docs/install-varbase-e2e.md +34 -0
  35. package/docs/overview.md +40 -0
  36. package/docs/step-definitions/given-i-am-anonymous-user.md +67 -0
  37. package/docs/step-definitions/given-i-am-homepage.md +68 -0
  38. package/docs/step-definitions/given-i-am-specific-page.md +72 -0
  39. package/docs/step-definitions/then-i-should-be.md +56 -0
  40. package/docs/step-definitions/then-i-should-see.md +62 -0
  41. package/docs/step-definitions/then-item-checkbox.md +54 -0
  42. package/docs/step-definitions/then-item-link-should-contain-url.md +84 -0
  43. package/docs/step-definitions/then-item-should-contain.md +67 -0
  44. package/docs/step-definitions/then-radio-button-value-should-be-selected.md +68 -0
  45. package/docs/step-definitions/then-response-status-code-should-be-number.md +69 -0
  46. package/docs/step-definitions/then-value-radio-button-selected.md +62 -0
  47. package/docs/step-definitions/when-i-attach-file-file-name-element.md +71 -0
  48. package/docs/step-definitions/when-i-check-checkbox.md +60 -0
  49. package/docs/step-definitions/when-i-click-link.md +51 -0
  50. package/docs/step-definitions/when-i-click-operation-text-row.md +71 -0
  51. package/docs/step-definitions/when-i-fill.md +59 -0
  52. package/docs/step-definitions/when-i-go-homepage.md +60 -0
  53. package/docs/step-definitions/when-i-go-specific-page.md +67 -0
  54. package/docs/step-definitions/when-i-move-backward-one-page.md +49 -0
  55. package/docs/step-definitions/when-i-move-forward-one-page.md +58 -0
  56. package/docs/step-definitions/when-i-press-button.md +58 -0
  57. package/docs/step-definitions/when-i-reload-page.md +51 -0
  58. package/docs/step-definitions/when-i-scroll.md +73 -0
  59. package/docs/step-definitions/when-i-select-option-select-list.md +53 -0
  60. package/docs/step-definitions/when-i-select-radio-button-value.md +62 -0
  61. package/docs/step-definitions/when-i-uncheck-checkbox.md +57 -0
  62. package/docs/step-definitions/when-i-wait-ajax-finish.md +64 -0
  63. package/docs/step-definitions/when-i-wait.md +62 -0
  64. package/docs/step-definitions.md +82 -0
  65. package/docs/steps/README.md +137 -0
  66. package/docs/steps/a11y.md +731 -0
  67. package/docs/steps/action.md +181 -0
  68. package/docs/steps/api.md +679 -0
  69. package/docs/steps/assertion.md +328 -0
  70. package/docs/steps/auth.md +108 -0
  71. package/docs/steps/clock.md +171 -0
  72. package/docs/steps/cookie.md +293 -0
  73. package/docs/steps/debug.md +52 -0
  74. package/docs/steps/dialog.md +226 -0
  75. package/docs/steps/drupal-canvas.md +414 -0
  76. package/docs/steps/drupal-ckeditor.md +118 -0
  77. package/docs/steps/drupal-core.md +688 -0
  78. package/docs/steps/drupal-layout-builder.md +437 -0
  79. package/docs/steps/drupal-media.md +107 -0
  80. package/docs/steps/drupal-moderation.md +81 -0
  81. package/docs/steps/drupal-paragraphs.md +36 -0
  82. package/docs/steps/element.md +455 -0
  83. package/docs/steps/field.md +648 -0
  84. package/docs/steps/file-download.md +235 -0
  85. package/docs/steps/form.md +344 -0
  86. package/docs/steps/iframe.md +248 -0
  87. package/docs/steps/input.md +220 -0
  88. package/docs/steps/javascript.md +111 -0
  89. package/docs/steps/keyboard.md +107 -0
  90. package/docs/steps/link.md +217 -0
  91. package/docs/steps/metatag.md +99 -0
  92. package/docs/steps/modal.md +232 -0
  93. package/docs/steps/navigation.md +275 -0
  94. package/docs/steps/network.md +284 -0
  95. package/docs/steps/path.md +203 -0
  96. package/docs/steps/response.md +102 -0
  97. package/docs/steps/responsive.md +139 -0
  98. package/docs/steps/rest.md +140 -0
  99. package/docs/steps/screenshot.md +211 -0
  100. package/docs/steps/scroll.md +266 -0
  101. package/docs/steps/selectors.md +848 -0
  102. package/docs/steps/storage.md +254 -0
  103. package/docs/steps/table.md +216 -0
  104. package/docs/steps/varbase.md +391 -0
  105. package/docs/steps/video.md +111 -0
  106. package/docs/steps/wait.md +553 -0
  107. package/docs/steps/web-first.md +289 -0
  108. package/docs/steps/xml.md +505 -0
  109. package/docs/steps/yaml.md +951 -0
  110. package/examples/a11y.html +31 -0
  111. package/examples/about-us.html +11 -0
  112. package/examples/account.html +32 -0
  113. package/examples/ajax-wait-examples.html +147 -0
  114. package/examples/contact-us.html +10 -0
  115. package/examples/cookies.html +10 -0
  116. package/examples/date.html +12 -0
  117. package/examples/dynamic-examples.html +498 -0
  118. package/examples/element.html +11 -0
  119. package/examples/example-api.json +28 -0
  120. package/examples/existing-page.html +11 -0
  121. package/examples/field.html +24 -0
  122. package/examples/index.html +37 -0
  123. package/examples/keyboard.html +11 -0
  124. package/examples/links.html +9 -0
  125. package/examples/modal.html +14 -0
  126. package/examples/path.html +3 -0
  127. package/examples/table.html +13 -0
  128. package/examples/test--modal-dialogs.html +252 -0
  129. package/examples/test--then--i-should--not--see-text-in-element.html +48 -0
  130. package/examples/test--then--i-should--see-text-in-element.html +48 -0
  131. package/examples/test--then--i-should--see-text-matching-pattern.html +31 -0
  132. package/examples/test--then--i-should-see-text-in-table-row.html +232 -0
  133. package/examples/test--then--the-checkbox-checked.html +68 -0
  134. package/examples/test--then--the-element-should_not-contain-cssproperty.html +47 -0
  135. package/examples/test--then--the-link-should-contain.html +21 -0
  136. package/examples/test--when--i-additionally-select.html +27 -0
  137. package/examples/test--when--i-attach-file-to-feild.html +50 -0
  138. package/examples/test--when--i-check-uncheck-checkbox.html +47 -0
  139. package/examples/test--when--i-click-link.html +18 -0
  140. package/examples/test--when--i-click-text-in-table-row.html +178 -0
  141. package/examples/test--when--i-fill-in.html +48 -0
  142. package/examples/test--when--i-press-button.html +31 -0
  143. package/examples/test--when--i-reload-page.html +27 -0
  144. package/examples/test--when--i-scroll-functionality.html +177 -0
  145. package/examples/test--when--i-scroll-left-right-functionality.html +149 -0
  146. package/examples/test--when--i-select-option-from-dropdownlist.html +35 -0
  147. package/examples/test--when--i-select-radio-button.html +95 -0
  148. package/examples/test--when-i-wait-max-of-time.html +39 -0
  149. package/examples/test--when-i-wait-page-until-is-loaded.html +47 -0
  150. package/examples/test--when-i-wait-specific-time.html +39 -0
  151. package/examples/test-acceptable-url-path.html +11 -0
  152. package/examples/video-demo.html +43 -0
  153. package/package.json +56 -0
  154. package/playwright.config.ts +39 -0
  155. package/tests/assets/test-document.pdf +14 -0
  156. package/tests/assets/vardot.png +0 -0
  157. package/tests/features/a11y.feature +39 -0
  158. package/tests/features/ajax-wait-examples.feature +24 -0
  159. package/tests/features/api-step-definitions-individual.feature +185 -0
  160. package/tests/features/complete-api-testing-examples.feature +205 -0
  161. package/tests/features/cookie.feature +13 -0
  162. package/tests/features/date.feature +11 -0
  163. package/tests/features/dynamic-content-examples.feature +156 -0
  164. package/tests/features/element.feature +16 -0
  165. package/tests/features/field.feature +25 -0
  166. package/tests/features/keyboard.feature +10 -0
  167. package/tests/features/link.feature +11 -0
  168. package/tests/features/modal-advanced-example.feature +54 -0
  169. package/tests/features/modal-basic-example.feature +42 -0
  170. package/tests/features/modal-error-handling-example.feature +37 -0
  171. package/tests/features/modal.feature +11 -0
  172. package/tests/features/path.feature +16 -0
  173. package/tests/features/response.feature +8 -0
  174. package/tests/features/responsive.feature +10 -0
  175. package/tests/features/table.feature +18 -0
  176. package/tests/features/test--given--i-am-an-anonymous-user.feature +10 -0
  177. package/tests/features/test--given--i-am-on-homepage.feature +16 -0
  178. package/tests/features/test--then--i-save-screenshot.feature +33 -0
  179. package/tests/features/test--then--i-see-locator.feature +320 -0
  180. package/tests/features/test--then--i-should--not--see-text-in-element.feature +13 -0
  181. package/tests/features/test--then--i-should--see-text-in-element-by-attr.feature +10 -0
  182. package/tests/features/test--then--i-should--see-text-in-element.feature +10 -0
  183. package/tests/features/test--then--i-should--see-text-matching-pattern-in-element.feature +13 -0
  184. package/tests/features/test--then--i-should--see-text-matching-pattern.feature +18 -0
  185. package/tests/features/test--then--i-should-be-on-page.feature +10 -0
  186. package/tests/features/test--then--i-should-be-on-the-homepage.feature +10 -0
  187. package/tests/features/test--then--i-should-not-see-an-element-by-attr.feature +8 -0
  188. package/tests/features/test--then--i-should-not-see-an-element.feature +8 -0
  189. package/tests/features/test--then--i-should-see-an-element-by-attr.feature +8 -0
  190. package/tests/features/test--then--i-should-see-an-element.feature +8 -0
  191. package/tests/features/test--then--i-should-see-n-elements.feature +24 -0
  192. package/tests/features/test--then--i-should-see-text-in-table-row.feature +96 -0
  193. package/tests/features/test--then--print-current-url-and-response.feature +21 -0
  194. package/tests/features/test--then--the-checkbox-is-checked.feature +18 -0
  195. package/tests/features/test--then--the-checkbox-should-be-checked.feature +16 -0
  196. package/tests/features/test--then--the-element-should-contain-cssproperty.feature +9 -0
  197. package/tests/features/test--then--the-element-should-not-contain-cssproperty.feature +10 -0
  198. package/tests/features/test--then--the-field-should-contain-value.feature +11 -0
  199. package/tests/features/test--then--the-response-should-contain.feature +11 -0
  200. package/tests/features/test--then--the-response-status-code-should-be.feature +31 -0
  201. package/tests/features/test--then--the-url-should-match.feature +7 -0
  202. package/tests/features/test--then-the-link-should-contain-by-attribute.feature +18 -0
  203. package/tests/features/test--then-the-link-should-contain.feature +7 -0
  204. package/tests/features/test--when--i-additionally-select.feature +30 -0
  205. package/tests/features/test--when--i-attach-file-to-feild.feature +9 -0
  206. package/tests/features/test--when--i-check-checkbox.feature +21 -0
  207. package/tests/features/test--when--i-click-link-by-attribute.feature +21 -0
  208. package/tests/features/test--when--i-click-link.feature +9 -0
  209. package/tests/features/test--when--i-click-text-in-table-row.feature +37 -0
  210. package/tests/features/test--when--i-fill-in-by-attr.feature +31 -0
  211. package/tests/features/test--when--i-fill-in-table-by-attr.feature +19 -0
  212. package/tests/features/test--when--i-fill-in.feature +32 -0
  213. package/tests/features/test--when--i-follow.feature +22 -0
  214. package/tests/features/test--when--i-go-to-homepage.feature +9 -0
  215. package/tests/features/test--when--i-move-forward-backward-one-page.feature +14 -0
  216. package/tests/features/test--when--i-press-button-by-attribute.feature +18 -0
  217. package/tests/features/test--when--i-press-button.feature +13 -0
  218. package/tests/features/test--when--i-reload-page.feature +11 -0
  219. package/tests/features/test--when--i-scroll-left-right.feature +48 -0
  220. package/tests/features/test--when--i-scroll.feature +48 -0
  221. package/tests/features/test--when--i-select-option-from-dropdownlist.feature +21 -0
  222. package/tests/features/test--when--i-select-radio-button.feature +37 -0
  223. package/tests/features/test--when--i-wait-page-is-loaded.feature +8 -0
  224. package/tests/features/test--when--i-wait-time.feature +21 -0
  225. package/tests/features/video.feature +18 -0
  226. package/tests/features/wait.feature +7 -0
  227. package/tests/features/yaml.feature +102 -0
  228. package/tests/selectors/_canonical-keys.json +113 -0
  229. package/tests/selectors/back-end-selectors.json +19 -0
  230. package/tests/selectors/cms-contentful-admin.json +35 -0
  231. package/tests/selectors/cms-craft-admin.json +39 -0
  232. package/tests/selectors/cms-drupal-cms-gin.json +56 -0
  233. package/tests/selectors/cms-drupal-core-claro.json +50 -0
  234. package/tests/selectors/cms-generic-admin.json +34 -0
  235. package/tests/selectors/cms-ghost-admin.json +34 -0
  236. package/tests/selectors/cms-joomla-admin.json +41 -0
  237. package/tests/selectors/cms-magento2-admin.json +32 -0
  238. package/tests/selectors/cms-prestashop-admin.json +38 -0
  239. package/tests/selectors/cms-shopify-admin.json +25 -0
  240. package/tests/selectors/cms-strapi-admin.json +42 -0
  241. package/tests/selectors/cms-typo3-admin.json +41 -0
  242. package/tests/selectors/cms-woocommerce-front.json +50 -0
  243. package/tests/selectors/cms-wordpress-admin.json +31 -0
  244. package/tests/selectors/framework-ant-design.json +80 -0
  245. package/tests/selectors/framework-bootstrap.json +86 -0
  246. package/tests/selectors/framework-bulma.json +74 -0
  247. package/tests/selectors/framework-chakra.json +58 -0
  248. package/tests/selectors/framework-foundation.json +51 -0
  249. package/tests/selectors/framework-material-ui.json +72 -0
  250. package/tests/selectors/framework-shadcn.json +51 -0
  251. package/tests/selectors/framework-tailwind.json +40 -0
  252. package/tests/selectors/framework-vuetify.json +61 -0
  253. package/tests/selectors/front-end-selectors.json +16 -0
  254. package/tests/selectors/homepage-selectors.json +13 -0
  255. package/tests/step-definitions/a11y.steps.js +800 -0
  256. package/tests/step-definitions/action.steps.js +167 -0
  257. package/tests/step-definitions/api.steps.js +732 -0
  258. package/tests/step-definitions/assertion.steps.js +352 -0
  259. package/tests/step-definitions/auth.steps.js +126 -0
  260. package/tests/step-definitions/clock.steps.js +128 -0
  261. package/tests/step-definitions/cookie.steps.js +216 -0
  262. package/tests/step-definitions/debug.steps.js +39 -0
  263. package/tests/step-definitions/dialog.steps.js +193 -0
  264. package/tests/step-definitions/drupal-canvas.steps.js +764 -0
  265. package/tests/step-definitions/drupal-ckeditor.steps.js +108 -0
  266. package/tests/step-definitions/drupal-core.steps.js +665 -0
  267. package/tests/step-definitions/drupal-helpers.js +371 -0
  268. package/tests/step-definitions/drupal-layout-builder.steps.js +466 -0
  269. package/tests/step-definitions/drupal-media.steps.js +99 -0
  270. package/tests/step-definitions/drupal-moderation.steps.js +71 -0
  271. package/tests/step-definitions/drupal-paragraphs.steps.js +50 -0
  272. package/tests/step-definitions/element.steps.js +356 -0
  273. package/tests/step-definitions/field.steps.js +653 -0
  274. package/tests/step-definitions/file-download.steps.js +211 -0
  275. package/tests/step-definitions/form.steps.js +333 -0
  276. package/tests/step-definitions/iframe.steps.js +228 -0
  277. package/tests/step-definitions/input.steps.js +202 -0
  278. package/tests/step-definitions/javascript.steps.js +232 -0
  279. package/tests/step-definitions/keyboard.steps.js +112 -0
  280. package/tests/step-definitions/link.steps.js +142 -0
  281. package/tests/step-definitions/metatag.steps.js +91 -0
  282. package/tests/step-definitions/modal.steps.js +244 -0
  283. package/tests/step-definitions/navigation.steps.js +218 -0
  284. package/tests/step-definitions/network.steps.js +280 -0
  285. package/tests/step-definitions/path.steps.js +141 -0
  286. package/tests/step-definitions/response.steps.js +98 -0
  287. package/tests/step-definitions/responsive.steps.js +125 -0
  288. package/tests/step-definitions/rest.steps.js +111 -0
  289. package/tests/step-definitions/screenshot.steps.js +423 -0
  290. package/tests/step-definitions/scroll.steps.js +189 -0
  291. package/tests/step-definitions/selectors.steps.js +1003 -0
  292. package/tests/step-definitions/storage.steps.js +194 -0
  293. package/tests/step-definitions/table.steps.js +181 -0
  294. package/tests/step-definitions/varbase-e2e.js +897 -0
  295. package/tests/step-definitions/varbase.steps.js +458 -0
  296. package/tests/step-definitions/video.steps.js +178 -0
  297. package/tests/step-definitions/wait.steps.js +400 -0
  298. package/tests/step-definitions/web-first.steps.js +298 -0
  299. package/tests/step-definitions/xml.steps.js +411 -0
  300. package/tests/step-definitions/yaml.steps.js +913 -0
  301. package/tsconfig.json +26 -0
@@ -0,0 +1,732 @@
1
+ const { Given, When, Then, Before } = require('@cucumber/cucumber');
2
+ const axios = require('axios');
3
+ const assert = require('assert');
4
+ const { friendly } = require('./varbase-e2e');
5
+
6
+ // Global variables to store API response and request data
7
+ let apiResponse = null;
8
+ let apiRequestData = null;
9
+ let apiHeaders = {};
10
+ let baseURL = '';
11
+ let authorization = '';
12
+ let placeHolders = {};
13
+
14
+ Before(async function () {
15
+ // Reset API state before each scenario
16
+ apiResponse = null;
17
+ apiRequestData = null;
18
+ apiHeaders = {};
19
+ authorization = '';
20
+ placeHolders = {};
21
+ });
22
+
23
+ /**
24
+ * Helper function to replace placeholders in text
25
+ */
26
+ function replacePlaceHolder(string) {
27
+ for (const [key, value] of Object.entries(placeHolders)) {
28
+ string = string.replace(new RegExp(key, 'g'), value);
29
+ }
30
+ return string;
31
+ }
32
+
33
+ /**
34
+ * Helper function to prepare URL by replacing placeholders and trimming slashes
35
+ */
36
+ function prepareUrl(url) {
37
+ return replacePlaceHolder(url).replace(/^\/+/, '');
38
+ }
39
+
40
+ /**
41
+ * Helper function to get nested property from object using dot notation.
42
+ */
43
+ function getNestedProperty(obj, path) {
44
+ return path.split('.').reduce((current, prop) => {
45
+ return current && current[prop] !== undefined ? current[prop] : undefined;
46
+ }, obj);
47
+ }
48
+
49
+ /**
50
+ * Adds Basic Authentication header to the next request.
51
+ *
52
+ * Example #1: Given I am authenticating as "admin" with "password123" password
53
+ * Example #2: Given we are authenticating as "user@example.com" with "secret" password
54
+ * Example #3: Given I am authenticating as "api-user" with "pass!word" password
55
+ * Example #4: Given we are authenticating as "reader" with "readonly" password
56
+ *
57
+ */
58
+ Given(/^(?:I am|we are) authenticating as "([^"]*)" with "([^"]*)" password$/, function (username, password) {
59
+ delete apiHeaders['Authorization'];
60
+ authorization = Buffer.from(username + ':' + password).toString('base64');
61
+ apiHeaders['Authorization'] = 'Basic ' + authorization;
62
+ });
63
+
64
+ /**
65
+ * Sets a single HTTP request header.
66
+ *
67
+ * Example #1: Given I set header "Content-Type" with value "application/json"
68
+ * Example #2: Given I set header "Authorization" with value "Bearer token123"
69
+ * Example #3: Given I set header "Accept" with value "application/xml"
70
+ * Example #4: Given I set header "Accept" with value "application/json"
71
+ * Example #5: Given we set header "X-Api-Key" with value "abcd-1234"
72
+ * Example #6: Given I set header "User-Agent" with value "test-runner"
73
+ * Example #7: Given we set header "Accept-Language" with value "en-US"
74
+ * Example #8: Given I set header "If-None-Match" with value "etag-xyz"
75
+ *
76
+ */
77
+ Given(/^(?:I |we )?set header "([^"]*)" with value "([^"]*)"$/, function (name, value) {
78
+ apiHeaders[name] = replacePlaceHolder(value);
79
+ });
80
+
81
+ /**
82
+ * Set the base URL for API calls. Accepts full URLs or paths relative to LAUNCH_URL.
83
+ *
84
+ * Example #1: Given the API base URL is "https://jsonplaceholder.typicode.com"
85
+ * Example #2: Given I set the API base URL to "https://api.example.com/v1"
86
+ * Example #3: Given the base URL is "http://localhost:3000/api"
87
+ * Example #4: Given the API base URL is "https://un.org/api"
88
+ * Example #5: Given I set the API base URL to "/api/v2"
89
+ * Example #6: Given the base URL is "https://staging.example.com/api"
90
+ * Example #7: Given the API base URL is "http://127.0.0.1:8080"
91
+ *
92
+ */
93
+ Given(/^(?:the API base URL is|I set the API base URL to|the base URL is) "([^"]*)"$/, function (url) {
94
+ // Check if the input is a full URL (contains protocol)
95
+ if (url.match(/^https?:\/\//)) {
96
+ baseURL = url.replace(/\/$/, ''); // Remove trailing slash
97
+ } else {
98
+ // If not a full URL, combine with the LAUNCH_URL env variable or default
99
+ const launchUrl = (process.env.LAUNCH_URL || 'http://localhost:8080').replace(/\/$/, '');
100
+ baseURL = (launchUrl + '/' + url).replace(/\/+/g, '/').replace(/\/$/, '');
101
+ baseURL = baseURL.replace(':/', '://');
102
+ }
103
+ });
104
+
105
+ /**
106
+ * Set a request header. Alternative syntax.
107
+ *
108
+ * Example #1: Given I set the header "Content-Type" to "application/json"
109
+ * Example #2: Given I set the header "Authorization" to "Bearer token123"
110
+ * Example #3: Given the header "Accept" is "application/json"
111
+ * Example #4: Given we set the header "X-Api-Key" to "abcd-1234"
112
+ * Example #5: Given I set the header "If-Match" to "etag-123"
113
+ * Example #6: Given the header "User-Agent" is "test-runner"
114
+ * Example #7: Given we set the header "Accept-Language" to "en-US"
115
+ * Example #8: Given I set the header "Cache-Control" to "no-cache"
116
+ *
117
+ */
118
+ Given(/^(?:I set the header|we set the header|the header) "([^"]*)" (?:to|is) "([^"]*)"$/, function (headerName, headerValue) {
119
+ apiHeaders[headerName] = replacePlaceHolder(headerValue);
120
+ });
121
+
122
+ /**
123
+ * Set multiple headers using a table.
124
+ *
125
+ * Example #1: Given I set the following headers:
126
+ * | Content-Type | application/json |
127
+ * | Authorization | Bearer token123 |
128
+ * | Accept | application/json |
129
+ *
130
+ * Example #2: Given we set the following headers:
131
+ * | Content-Type | application/json |
132
+ * | X-Api-Key | abcd-1234 |
133
+ * | Accept-Language | en-US |
134
+ *
135
+ * Example #3: Given I set the following headers:
136
+ * | User-Agent | test-runner |
137
+ * | Cache-Control | no-cache |
138
+ * | If-None-Match | etag-xyz |
139
+ *
140
+ * Example #4: Given we set the following headers:
141
+ * | Authorization | Bearer {{token}} |
142
+ * | Accept | application/json |
143
+ * | X-Request-ID | req-001 |
144
+ */
145
+ Given(/^(?:I|we) set the following headers:$/, function (table) {
146
+ table.rows().forEach(row => {
147
+ apiHeaders[row[0]] = replacePlaceHolder(row[1]);
148
+ });
149
+ });
150
+
151
+ /**
152
+ * Set request body data for POST/PUT requests.
153
+ *
154
+ * Example #1: Given I set the request body to '{"name": "John", "email": "john@example.com"}'
155
+ * Example #2: Given the request body is '{"title": "Test Post", "body": "This is a test"}'
156
+ * Example #3: Given we set the request body to '{"org": "Vardot", "active": true}'
157
+ * Example #4: Given I set the request body to '{"id": 1, "tags": ["qa","api"]}'
158
+ * Example #5: Given the request body is '{"username": "admin", "password": "secret"}'
159
+ * Example #6: Given we set the request body to '{"email": "hello@un.org"}'
160
+ * Example #7: Given I set the request body to '{"status": "published"}'
161
+ */
162
+ Given(/^(?:I set the request body to|we set the request body to|the request body is) '([^']*)'$/, function (jsonData) {
163
+ try {
164
+ const processedData = replacePlaceHolder(jsonData);
165
+ apiRequestData = JSON.parse(processedData);
166
+ } catch (error) {
167
+ throw friendly(`Invalid JSON in request body: ${error.message}`);
168
+ }
169
+ });
170
+
171
+ /**
172
+ * Set request body using a table.
173
+ *
174
+ * Example #1: Given I set the request body with:
175
+ * | name | John Doe |
176
+ * | email | john@example.com |
177
+ * | age | 30 |
178
+ *
179
+ * Example #2: Given we set the request body with:
180
+ * | org | Vardot |
181
+ * | country | UN |
182
+ * | active | true |
183
+ *
184
+ * Example #3: Given I set the request body with:
185
+ * | title | Hello World |
186
+ * | body | First post |
187
+ * | userId | 1 |
188
+ *
189
+ * Example #4: Given we set the request body with:
190
+ * | status | published |
191
+ * | priority | 5 |
192
+ * | featured | false |
193
+ */
194
+ Given(/^(?:I|we) set the request body with:$/, function (table) {
195
+ apiRequestData = {};
196
+ table.rows().forEach(row => {
197
+ let value = replacePlaceHolder(row[1]);
198
+
199
+ // Try to parse as number or boolean, otherwise keep as string
200
+ if (value === 'true') value = true;
201
+ else if (value === 'false') value = false;
202
+ else if (!isNaN(value) && !isNaN(parseFloat(value))) value = parseFloat(value);
203
+
204
+ apiRequestData[row[0]] = value;
205
+ });
206
+ });
207
+
208
+ /**
209
+ * Sends HTTP request to specific relative URL.
210
+ *
211
+ * Example #1: When I send a GET request to "/users"
212
+ * Example #2: When we send a POST request to "/posts"
213
+ * Example #3: When I send a PUT request to "/users/1"
214
+ * Example #4: When we send a DELETE request to "/posts/1"
215
+ * Example #5: When I send a GET request to "/posts?userId=1"
216
+ * Example #6: When we send a PATCH request to "/users/42"
217
+ * Example #7: When I send a GET request to "/health"
218
+ * Example #8: When we send a HEAD request to "/users"
219
+ * Example #9: When I send a OPTIONS request to "/api"
220
+ * Example #10: When we send a GET request to "/posts/{{postId}}"
221
+ */
222
+ When(/^(?:I |we )?send a ([A-Z]+) request to "([^"]+)"$/, async function (method, endpoint) {
223
+ const url = baseURL + '/' + prepareUrl(endpoint);
224
+
225
+ try {
226
+ apiResponse = await axios({
227
+ method: method,
228
+ url: url,
229
+ headers: apiHeaders,
230
+ data: apiRequestData,
231
+ validateStatus: function (status) {
232
+ return true; // Don't throw on any status code
233
+ }
234
+ });
235
+ } catch (error) {
236
+ throw friendly(`${method} request failed: ${error.message}`);
237
+ }
238
+ });
239
+
240
+ /**
241
+ * Sends HTTP request to specific URL with field values from Table.
242
+ *
243
+ * Example #1: When I send a POST request to "/users" with values:
244
+ * | name | John Doe |
245
+ * | email | john@example.com |
246
+ * | age | 30 |
247
+ *
248
+ * Example #2: When we send a POST request to "/users" with values:
249
+ * | name | Alice |
250
+ * | email | alice@un.org |
251
+ * | role | editor |
252
+ *
253
+ * Example #3: When I send a PUT request to "/users/1" with values:
254
+ * | name | Updated Name |
255
+ * | active | true |
256
+ *
257
+ * Example #4: When we send a POST request to "/posts" with values:
258
+ * | title | Welcome |
259
+ * | body | Hello Vardot|
260
+ * | userId | 1 |
261
+ */
262
+ When(/^(?:I |we )?send a ([A-Z]+) request to "([^"]+)" with values:$/, async function (method, endpoint, table) {
263
+ const url = baseURL + '/' + prepareUrl(endpoint);
264
+ const fields = {};
265
+
266
+ const rowsHash = table.rowsHash();
267
+ Object.keys(rowsHash).forEach(key => {
268
+ fields[key] = replacePlaceHolder(rowsHash[key]);
269
+ });
270
+
271
+ try {
272
+ apiResponse = await axios({
273
+ method: method,
274
+ url: url,
275
+ headers: { ...apiHeaders, 'Content-Type': 'application/json' },
276
+ data: JSON.stringify(fields),
277
+ validateStatus: function (status) {
278
+ return true; // Don't throw on any status code
279
+ }
280
+ });
281
+ } catch (error) {
282
+ throw friendly(`${method} request with values failed: ${error.message}`);
283
+ }
284
+ });
285
+
286
+ /**
287
+ * Sends HTTP request to specific URL with raw body from PyString.
288
+ *
289
+ * Example #1: When I send a POST request to "/users" with body:
290
+ * """
291
+ * {
292
+ * "name": "John Doe",
293
+ * "email": "john@example.com"
294
+ * }
295
+ * """
296
+ *
297
+ * Example #2: When we send a POST request to "/posts" with body:
298
+ * """
299
+ * {
300
+ * "title": "Hello",
301
+ * "body": "World",
302
+ * "userId": 1
303
+ * }
304
+ * """
305
+ *
306
+ * Example #3: When I send a PUT request to "/users/1" with body:
307
+ * """
308
+ * {
309
+ * "name": "Updated",
310
+ * "org": "Vardot"
311
+ * }
312
+ * """
313
+ *
314
+ * Example #4: When we send a POST request to "/contacts" with body:
315
+ * """
316
+ * {
317
+ * "email": "info@un.org",
318
+ * "subscribe": true
319
+ * }
320
+ * """
321
+ */
322
+ When(/^(?:I |we )?send a ([A-Z]+) request to "([^"]+)" with body:$/, async function (method, endpoint, docString) {
323
+ const url = baseURL + '/' + prepareUrl(endpoint);
324
+ const body = replacePlaceHolder(docString.trim());
325
+
326
+ // Parse JSON if the body looks like JSON, otherwise send as string
327
+ let requestData = body;
328
+ try {
329
+ // Check if it's JSON by trying to parse it
330
+ requestData = JSON.parse(body);
331
+ } catch (error) {
332
+ // If parsing fails, use the raw string
333
+ requestData = body;
334
+ }
335
+
336
+ // Set appropriate Content-Type header if not already set
337
+ const headers = { ...apiHeaders };
338
+ if (!headers['Content-Type'] && !headers['content-type']) {
339
+ if (typeof requestData === 'object') {
340
+ headers['Content-Type'] = 'application/json';
341
+ }
342
+ }
343
+
344
+ try {
345
+ apiResponse = await axios({
346
+ method: method,
347
+ url: url,
348
+ headers: headers,
349
+ data: requestData,
350
+ validateStatus: function (status) {
351
+ return true; // Don't throw on any status code
352
+ }
353
+ });
354
+ } catch (error) {
355
+ throw friendly(`${method} request with body failed: ${error.message}`);
356
+ }
357
+ });
358
+
359
+ /**
360
+ * Sends HTTP request to specific URL with form data.
361
+ *
362
+ * Example #1: When I send a POST request to "/login" with form data:
363
+ * """
364
+ * username=admin
365
+ * password=secret
366
+ * remember=true
367
+ * """
368
+ *
369
+ * Example #2: When we send a POST request to "/subscribe" with form data:
370
+ * """
371
+ * email=hello@un.org
372
+ * list=newsletter
373
+ * """
374
+ *
375
+ * Example #3: When I send a POST request to "/contact" with form data:
376
+ * """
377
+ * name=Alice
378
+ * org=Vardot
379
+ * message=Hello
380
+ * """
381
+ *
382
+ * Example #4: When we send a PUT request to "/profile" with form data:
383
+ * """
384
+ * name=Updated
385
+ * country=UN
386
+ * """
387
+ */
388
+ When(/^(?:I |we )?send a ([A-Z]+) request to "([^"]+)" with form data:$/, async function (method, endpoint, docString) {
389
+ const url = baseURL + '/' + prepareUrl(endpoint);
390
+ const body = replacePlaceHolder(docString.trim());
391
+
392
+ // Parse form data
393
+ const fields = {};
394
+ body.split('\n').forEach(line => {
395
+ const [key, value] = line.split('=');
396
+ if (key && value !== undefined) {
397
+ fields[key.trim()] = value.trim();
398
+ }
399
+ });
400
+
401
+ // Convert to URL encoded format
402
+ const formData = Object.keys(fields)
403
+ .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(fields[key]))
404
+ .join('&');
405
+
406
+ try {
407
+ apiResponse = await axios({
408
+ method: method,
409
+ url: url,
410
+ headers: {
411
+ ...apiHeaders,
412
+ 'Content-Type': 'application/x-www-form-urlencoded'
413
+ },
414
+ data: formData,
415
+ validateStatus: function (status) {
416
+ return true; // Don't throw on any status code
417
+ }
418
+ });
419
+ } catch (error) {
420
+ throw friendly(`${method} request with form data failed: ${error.message}`);
421
+ }
422
+ });
423
+
424
+ /**
425
+ * Checks that API response has specific status code.
426
+ *
427
+ * Example #1: Then the API response code should be 200
428
+ * Example #2: Then API response code should be 404
429
+ * Example #3: Then the API response code should be 201
430
+ * Example #4: Then API response code should be 204
431
+ * Example #5: Then the API response code should be 400
432
+ * Example #6: Then API response code should be 401
433
+ * Example #7: Then the API response code should be 403
434
+ * Example #8: Then API response code should be 500
435
+ * Example #9: Then the API response code should be 301
436
+ * Example #10: Then API response code should be 302
437
+ */
438
+ Then(/^(?:the )?API response code should be (\d+)$/, function (expectedCode) {
439
+ if (!apiResponse) {
440
+ throw friendly('No API response available. Make sure to send a request first.');
441
+ }
442
+
443
+ const expected = parseInt(expectedCode);
444
+ const actual = parseInt(apiResponse.status);
445
+ assert.strictEqual(actual, expected,
446
+ `Expected status code ${expected}, but got ${actual}`);
447
+ });
448
+
449
+ /**
450
+ * Checks that API response body contains specific text.
451
+ *
452
+ * Example #1: Then the API response should contain "success"
453
+ * Example #2: Then API response should contain "John Doe"
454
+ * Example #3: Then the API response should contain "Vardot"
455
+ * Example #4: Then API response should contain "UN"
456
+ * Example #5: Then the API response should contain "published"
457
+ * Example #6: Then API response should contain "id"
458
+ * Example #7: Then the API response should contain "email"
459
+ * Example #8: Then API response should contain "hello@un.org"
460
+ */
461
+ Then(/^(?:the )?API response should contain "([^"]*)"$/, function (expectedText) {
462
+ if (!apiResponse) {
463
+ throw friendly('No API response available. Make sure to send a request first.');
464
+ }
465
+
466
+ const responseBody = typeof apiResponse.data === 'string' ?
467
+ apiResponse.data : JSON.stringify(apiResponse.data);
468
+
469
+ const expectedRegexp = new RegExp(expectedText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
470
+ assert(expectedRegexp.test(responseBody),
471
+ `Response should contain "${expectedText}", but got: ${responseBody}`);
472
+ });
473
+
474
+ /**
475
+ * Checks that API response body doesn't contain specific text.
476
+ *
477
+ * Example #1: Then the API response should not contain "error"
478
+ * Example #2: Then API response should not contain "failed"
479
+ * Example #3: Then the API response should not contain "forbidden"
480
+ * Example #4: Then API response should not contain "unauthorized"
481
+ * Example #5: Then the API response should not contain "password"
482
+ * Example #6: Then API response should not contain "secret"
483
+ * Example #7: Then the API response should not contain "exception"
484
+ * Example #8: Then API response should not contain "stack trace"
485
+ */
486
+ Then(/^(?:the )?API response should not contain "([^"]*)"$/, function (unexpectedText) {
487
+ if (!apiResponse) {
488
+ throw friendly('No API response available. Make sure to send a request first.');
489
+ }
490
+
491
+ const responseBody = typeof apiResponse.data === 'string' ?
492
+ apiResponse.data : JSON.stringify(apiResponse.data);
493
+
494
+ const expectedRegexp = new RegExp(unexpectedText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
495
+ assert(!expectedRegexp.test(responseBody),
496
+ `Response should not contain "${unexpectedText}", but it does. Response: ${responseBody}`);
497
+ });
498
+
499
+ /**
500
+ * Checks that API response body contains JSON from PyString.
501
+ *
502
+ * Example #1: Then the API response should contain json:
503
+ * """
504
+ * {
505
+ * "name": "John Doe",
506
+ * "email": "john@example.com"
507
+ * }
508
+ * """
509
+ *
510
+ * Example #2: Then API response should contain json:
511
+ * """
512
+ * {
513
+ * "org": "Vardot",
514
+ * "active": true
515
+ * }
516
+ * """
517
+ *
518
+ * Example #3: Then the API response should contain json:
519
+ * """
520
+ * {
521
+ * "id": 1,
522
+ * "title": "Hello"
523
+ * }
524
+ * """
525
+ *
526
+ * Example #4: Then API response should contain json:
527
+ * """
528
+ * {
529
+ * "email": "hello@un.org"
530
+ * }
531
+ * """
532
+ */
533
+ Then(/^(?:the )?API response should contain json:$/, function (docString) {
534
+ if (!apiResponse) {
535
+ throw friendly('No API response available. Make sure to send a request first.');
536
+ }
537
+
538
+ let expectedJson;
539
+ let actualJson;
540
+
541
+ try {
542
+ expectedJson = JSON.parse(replacePlaceHolder(docString.trim()));
543
+ } catch (error) {
544
+ throw friendly(`Cannot convert expected JSON: ${docString}`);
545
+ }
546
+
547
+ try {
548
+ actualJson = typeof apiResponse.data === 'string' ?
549
+ JSON.parse(apiResponse.data) : apiResponse.data;
550
+ } catch (error) {
551
+ throw friendly(`Cannot convert actual response to JSON: ${apiResponse.data}`);
552
+ }
553
+
554
+ // Check that expected properties exist in actual response and match values
555
+ Object.keys(expectedJson).forEach(key => {
556
+ assert(actualJson.hasOwnProperty(key),
557
+ `Expected property "${key}" not found in response. Actual response: ${JSON.stringify(actualJson, null, 2)}`);
558
+ assert.deepStrictEqual(actualJson[key], expectedJson[key],
559
+ `Property "${key}" mismatch. Expected: ${expectedJson[key]}, Actual: ${actualJson[key]}`);
560
+ });
561
+ });
562
+
563
+ /**
564
+ * Alternative syntax for JSON property verification.
565
+ *
566
+ * Example #1: Then the JSON response should have "name" equal to "John Doe"
567
+ * Example #2: Then the API response should have "id" equal to 1
568
+ * Example #3: Then the JSON property "active" should be true
569
+ * Example #4: Then the JSON response should have "org" equal to "Vardot"
570
+ * Example #5: Then the API response should have "email" equal to "hello@un.org"
571
+ * Example #6: Then the JSON property "count" should be 42
572
+ * Example #7: Then the JSON response should have "user.role" equal to "admin"
573
+ * Example #8: Then the JSON property "published" should be false
574
+ */
575
+ Then(/^(?:the JSON response should have|the API response should have|the JSON property) "([^"]*)" (?:equal to|should be) (.+)$/, function (propertyPath, expectedValue) {
576
+ if (!apiResponse) {
577
+ throw friendly('No API response available. Make sure to send a request first.');
578
+ }
579
+
580
+ // Replace placeholders in expected value first
581
+ const processedExpectedValue = replacePlaceHolder(expectedValue);
582
+
583
+ // Parse expected value (remove quotes if string, parse numbers/booleans)
584
+ let parsedExpectedValue = processedExpectedValue;
585
+ if (processedExpectedValue.startsWith('"') && processedExpectedValue.endsWith('"')) {
586
+ parsedExpectedValue = processedExpectedValue.slice(1, -1);
587
+ } else if (processedExpectedValue === 'true') {
588
+ parsedExpectedValue = true;
589
+ } else if (processedExpectedValue === 'false') {
590
+ parsedExpectedValue = false;
591
+ } else if (!isNaN(processedExpectedValue) && !isNaN(parseFloat(processedExpectedValue))) {
592
+ parsedExpectedValue = parseFloat(processedExpectedValue);
593
+ }
594
+
595
+ // Get the actual value from response
596
+ const actualValue = getNestedProperty(apiResponse.data, propertyPath);
597
+
598
+ assert.strictEqual(actualValue, parsedExpectedValue,
599
+ `Expected "${propertyPath}" to be ${parsedExpectedValue}, but got ${actualValue}`);
600
+ });
601
+
602
+ /**
603
+ * Verify a JSON property exists.
604
+ *
605
+ * Example #1: Then the JSON response should have property "id"
606
+ * Example #2: Then the API response should contain property "user.email"
607
+ * Example #3: Then the JSON response should have property "title"
608
+ * Example #4: Then the API response should contain property "data"
609
+ * Example #5: Then the JSON response should have property "user.org"
610
+ * Example #6: Then the API response should contain property "meta.total"
611
+ * Example #7: Then the JSON response should have property "createdAt"
612
+ * Example #8: Then the API response should contain property "links.self"
613
+ */
614
+ Then(/^(?:the JSON response should have property|the API response should contain property) "([^"]*)"$/, function (propertyPath) {
615
+ if (!apiResponse) {
616
+ throw friendly('No API response available. Make sure to send a request first.');
617
+ }
618
+
619
+ const actualValue = getNestedProperty(apiResponse.data, propertyPath);
620
+
621
+ assert(actualValue !== undefined,
622
+ `Expected property "${propertyPath}" to exist in response, but it doesn't`);
623
+ });
624
+
625
+ /**
626
+ * Verify a JSON property does not exist.
627
+ *
628
+ * Example #1: Then the JSON response should not have property "password"
629
+ * Example #2: Then the API response should not contain property "secret"
630
+ * Example #3: Then the JSON response should not have property "token"
631
+ * Example #4: Then the API response should not contain property "apiKey"
632
+ * Example #5: Then the JSON response should not have property "user.password"
633
+ * Example #6: Then the API response should not contain property "internal"
634
+ * Example #7: Then the JSON response should not have property "debug"
635
+ * Example #8: Then the API response should not contain property "stack"
636
+ */
637
+ Then(/^(?:the JSON response should not have property|the API response should not contain property) "([^"]*)"$/, function (propertyPath) {
638
+ if (!apiResponse) {
639
+ throw friendly('No API response available. Make sure to send a request first.');
640
+ }
641
+
642
+ const actualValue = getNestedProperty(apiResponse.data, propertyPath);
643
+
644
+ assert(actualValue === undefined,
645
+ `Expected property "${propertyPath}" not to exist in response, but it does`);
646
+ });
647
+
648
+ /**
649
+ * Verify the response is valid JSON.
650
+ *
651
+ * Two equivalent phrasings.
652
+ *
653
+ * Example #1: Then the response should be valid JSON
654
+ * Example #2: Then the API response should be valid JSON
655
+ * Example #3: And the response should be valid JSON
656
+ * Example #4: When I send a GET request to "/api/users"
657
+ * Then the API response should be valid JSON
658
+ * Example #5: When I send a POST request to "/api/orders"
659
+ * Then the response should be valid JSON
660
+ */
661
+ Then(/^(?:the response should be valid JSON|the API response should be valid JSON)$/, function () {
662
+ if (!apiResponse) {
663
+ throw friendly('No API response available. Make sure to send a request first.');
664
+ }
665
+
666
+ assert(typeof apiResponse.data === 'object' && apiResponse.data !== null,
667
+ 'Response should be valid JSON');
668
+ });
669
+
670
+ /**
671
+ * Verify response header value.
672
+ *
673
+ * Example #1: Then the response header "Content-Type" should be "application/json"
674
+ * Example #2: Then the header "Cache-Control" should contain "no-cache"
675
+ * Example #3: Then the response header "Content-Type" should contain "charset=utf-8"
676
+ * Example #4: Then the header "X-Powered-By" should be "Vardot"
677
+ * Example #5: Then the response header "ETag" should contain "etag"
678
+ * Example #6: Then the header "Location" should contain "/users/1"
679
+ * Example #7: Then the response header "Access-Control-Allow-Origin" should be "*"
680
+ * Example #8: Then the header "Content-Length" should contain "0"
681
+ */
682
+ Then(/^(?:the response header|the header) "([^"]*)" should (?:be|contain) "([^"]*)"$/, function (headerName, expectedValue) {
683
+ if (!apiResponse) {
684
+ throw friendly('No API response available. Make sure to send a request first.');
685
+ }
686
+
687
+ const actualValue = apiResponse.headers[headerName.toLowerCase()];
688
+
689
+ assert(actualValue && actualValue.includes(expectedValue),
690
+ `Expected header "${headerName}" to contain "${expectedValue}", but got "${actualValue}"`);
691
+ });
692
+
693
+ /**
694
+ * Prints the most recent API response body to stdout for debugging.
695
+ *
696
+ * Strip before merging — this is a developer aid only.
697
+ *
698
+ * Example #1: Then print API response
699
+ * Example #2: When I send a GET request to "/api/users"
700
+ * Then print API response
701
+ * Example #3: And print API response
702
+ * Example #4: When I send a POST request to "/api/login"
703
+ * Then the API response code should be 200
704
+ * And print API response
705
+ * Example #5: When I send a DELETE request to "/api/orders/1"
706
+ * Then print API response
707
+ */
708
+ Then(/^print API response$/, function () {
709
+ if (!apiResponse) {
710
+ console.log('No API response available');
711
+ return;
712
+ }
713
+
714
+ console.log(`${apiResponse.config.method.toUpperCase()} ${apiResponse.config.url} => ${apiResponse.status}:`);
715
+ console.log(typeof apiResponse.data === 'string' ? apiResponse.data : JSON.stringify(apiResponse.data, null, 2));
716
+ });
717
+
718
+ /**
719
+ * Sets placeholder for replacement in URLs, requests, and responses.
720
+ *
721
+ * Example #1: Given I set placeholder "{{userId}}" to "123"
722
+ * Example #2: Given we set placeholder "{{token}}" to "abcd-1234"
723
+ * Example #3: Given I set placeholder "{{postId}}" to "42"
724
+ * Example #4: Given we set placeholder "{{org}}" to "Vardot"
725
+ * Example #5: Given I set placeholder "{{email}}" to "hello@un.org"
726
+ * Example #6: Given we set placeholder "{{apiKey}}" to "secret-xyz"
727
+ * Example #7: Given I set placeholder "{{version}}" to "v2"
728
+ * Example #8: Given we set placeholder "{{locale}}" to "en-US"
729
+ */
730
+ Given(/^(?:I|we) set placeholder "([^"]*)" to "([^"]*)"$/, function (placeholder, value) {
731
+ placeHolders[placeholder] = value;
732
+ });