@letsrunit/bdd 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # BDD Package (`@letsrunit/bdd`)
2
+
3
+ ## Installation
4
+
5
+ ```bash
6
+ npm install @letsrunit/bdd
7
+ # or
8
+ yarn add @letsrunit/bdd
9
+ ```
10
+
11
+ Standard BDD step definitions and Playwright utilities for `letsrunit`. It provides the building blocks for creating and executing Gherkin-based automation.
12
+
13
+ ## Exported Components
14
+
15
+ ### `stepsDefinitions`
16
+
17
+ An array of `StepDefinition` objects that define the standard steps available in the platform. These include:
18
+
19
+ - **Assert**: Steps for verifying page state (e.g., `I should see "text"`, `the URL should be "url"`).
20
+ - **Navigation**: Steps for moving between pages (e.g., `I go to "url"`, `I go back`).
21
+ - **Mouse**: Click and hover interactions (e.g., `I click "button"`, `I click on the text "link"`).
22
+ - **Form**: Filling and submitting forms (e.g., `I fill "input" with "value"`, `I select "option" from "select"`).
23
+ - **Keyboard**: Typing and pressing keys.
24
+ - **Mailbox**: Steps for interacting with emails (e.g., `I open the latest email`, `I click the link in the email`).
25
+ - **Clipboard**: Steps for verifying and interacting with the clipboard.
26
+
27
+ ### Custom Parameter Types
28
+
29
+ The package exports several Cucumber parameter types to make steps more readable and powerful:
30
+
31
+ - `{text}`: For matching quoted or unquoted text.
32
+ - `{selector}`: For matching Playwright-compatible selectors.
33
+ - `{url}`: For matching URLs.
34
+
35
+ ### `toFile(path)`
36
+
37
+ A utility to convert a local path or URL to a `File` object, useful for testing file uploads.
38
+
39
+ ## Testing
40
+
41
+ Run tests for this package:
42
+
43
+ ```bash
44
+ yarn test
45
+ ```
@@ -0,0 +1,37 @@
1
+ import * as _letsrunit_gherkin from '@letsrunit/gherkin';
2
+ import * as _letsrunit_utils from '@letsrunit/utils';
3
+ import { World as World$1 } from '@cucumber/cucumber';
4
+ import { BrowserContextOptions, Page } from '@playwright/test';
5
+ import { Readable } from 'node:stream';
6
+
7
+ declare const typeDefinitions: (_letsrunit_gherkin.ParameterTypeDefinition<_letsrunit_utils.Scalar | _letsrunit_utils.Scalar[]> | _letsrunit_gherkin.ParameterTypeDefinition<_letsrunit_gherkin.KeyCombo> | _letsrunit_gherkin.ParameterTypeDefinition<boolean>)[];
8
+
9
+ interface World extends World$1<BrowserContextOptions> {
10
+ page: Page;
11
+ startTime: number;
12
+ pathParams?: Record<string, string>;
13
+ lang?: {
14
+ code: string;
15
+ name: string;
16
+ };
17
+ [_: string]: any;
18
+ }
19
+ type StepHandler = (this: World, ...args: any[]) => Promise<void> | void;
20
+ type StepType = 'Given' | 'When' | 'Then';
21
+ interface StepDefinition {
22
+ type: StepType;
23
+ expression: string | RegExp;
24
+ fn: StepHandler;
25
+ comment?: string;
26
+ }
27
+
28
+ declare const stepsDefinitions: StepDefinition[];
29
+
30
+ type AttachData = string | Buffer | Readable;
31
+ type AttachOptions = string | {
32
+ mediaType: string;
33
+ fileName?: string;
34
+ };
35
+ declare function toFile(data: AttachData, options?: AttachOptions): File;
36
+
37
+ export { type StepDefinition, type StepHandler, type StepType, type World, stepsDefinitions, toFile, typeDefinitions };
package/dist/index.js ADDED
@@ -0,0 +1,339 @@
1
+ import { locatorParameter, valueParameter, keysParameter, booleanParameter, enumParameter } from '@letsrunit/gherkin';
2
+ import { locator, setFieldValue, waitAfterInteraction, suppressInterferences, waitForIdle } from '@letsrunit/playwright';
3
+ import { expect } from '@playwright/test';
4
+ import { asFilename, eventually, splitUrl, pathRegexp, sleep, textToHtml } from '@letsrunit/utils';
5
+ import { receiveMail, toEml } from '@letsrunit/mailbox';
6
+ import ISO6391 from 'iso-639-1';
7
+ import metascraper from 'metascraper';
8
+ import metascraperLang from 'metascraper-lang';
9
+
10
+ var __defProp = Object.defineProperty;
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var typeDefinitions = [
16
+ locatorParameter(),
17
+ valueParameter(),
18
+ keysParameter(),
19
+ booleanParameter("visible", "hidden"),
20
+ booleanParameter("enabled", "disabled", /((?:en|dis)abled)/),
21
+ booleanParameter("checked", "unchecked", /((?:un)?checked)/),
22
+ booleanParameter("contains", "not contains", /((?:not )?contains)/),
23
+ booleanParameter("check", "uncheck", /((?:un)?check)/),
24
+ booleanParameter("focus", "blur"),
25
+ enumParameter(["click", "double-click", "right-click", "hover"], /((?:double-|right-)?click|hover)/)
26
+ ];
27
+
28
+ // src/steps/assert.ts
29
+ var assert_exports = {};
30
+ __export(assert_exports, {
31
+ contain: () => contain,
32
+ see: () => see
33
+ });
34
+ function expectOrNot(actual, toBe) {
35
+ return toBe ? expect(actual) : expect(actual).not;
36
+ }
37
+
38
+ // src/steps/wrappers.ts
39
+ function Given(expression, fn, comment) {
40
+ return { type: "Given", expression, fn, comment };
41
+ }
42
+ function When(expression, fn, comment) {
43
+ return { type: "When", expression, fn, comment };
44
+ }
45
+ function Then(expression, fn, comment) {
46
+ return { type: "Then", expression, fn, comment };
47
+ }
48
+
49
+ // src/steps/assert.ts
50
+ var WAIT_TIMEOUT = 5e3;
51
+ var see = Then(
52
+ "The page {contains|not contains} {locator}",
53
+ async function(visible, selector) {
54
+ const el = await locator(this.page, selector);
55
+ await expectOrNot(el, visible).toBeVisible({ timeout: WAIT_TIMEOUT });
56
+ }
57
+ );
58
+ var contain = Then(
59
+ "{locator} {contains|not contains} {locator}",
60
+ async function(selector, contain2, child) {
61
+ const el = await locator(this.page, selector);
62
+ const childElement = el.locator(child);
63
+ await expectOrNot(childElement, contain2).toBeAttached({ timeout: WAIT_TIMEOUT });
64
+ }
65
+ );
66
+
67
+ // src/steps/clipboard.ts
68
+ var clipboard_exports = {};
69
+ __export(clipboard_exports, {
70
+ copy: () => copy,
71
+ paste: () => paste
72
+ });
73
+ var TIMEOUT = 500;
74
+ async function copyInput(el) {
75
+ try {
76
+ return await el.inputValue();
77
+ } catch {
78
+ }
79
+ }
80
+ async function copyLink(el) {
81
+ try {
82
+ const tag = await el.evaluate((n) => n.tagName.toLowerCase());
83
+ const href = tag === "a" ? await el.getAttribute("href") : null;
84
+ if (href) {
85
+ return href.startsWith("mailto:") ? href.replace(/^mailto:/i, "") : href;
86
+ }
87
+ } catch {
88
+ }
89
+ }
90
+ async function copyText(el) {
91
+ return await el.textContent() ?? null;
92
+ }
93
+ var copy = When("I copy {locator} to the clipboard", async function(selector) {
94
+ const el = await locator(this.page, selector);
95
+ let value = await copyInput(el) ?? await copyLink(el) ?? await copyText(el);
96
+ this.clipboard = { value };
97
+ });
98
+ var paste = When("I paste from the clipboard into {locator}", async function(selector) {
99
+ const el = await locator(this.page, selector);
100
+ const value = this.clipboard?.value || "";
101
+ await el.fill(String(value), { timeout: TIMEOUT });
102
+ });
103
+
104
+ // src/steps/form.ts
105
+ var form_exports = {};
106
+ __export(form_exports, {
107
+ check: () => check,
108
+ clear: () => clear,
109
+ focus: () => focus,
110
+ set: () => set,
111
+ setRange: () => setRange
112
+ });
113
+ var TIMEOUT2 = 500;
114
+ var DELAY = 500;
115
+ var set = When("I set {locator} to {value}", async function(selector, value) {
116
+ const el = await locator(this.page, selector);
117
+ await setFieldValue(el, value, { timeout: TIMEOUT2 });
118
+ await sleep(DELAY);
119
+ });
120
+ var setRange = When(
121
+ "I set {locator} to range of {value} to {value}",
122
+ async function(selector, from, to) {
123
+ const el = await locator(this.page, selector);
124
+ await setFieldValue(el, { from, to }, { timeout: TIMEOUT2 });
125
+ await sleep(DELAY);
126
+ }
127
+ );
128
+ var clear = When("I clear {locator}", async function(selector) {
129
+ const el = await locator(this.page, selector);
130
+ await setFieldValue(el, null, { timeout: TIMEOUT2 });
131
+ await sleep(DELAY);
132
+ });
133
+ var check = When(
134
+ "I {check|uncheck} {locator}",
135
+ async function(check2, selector) {
136
+ const el = await locator(this.page, selector);
137
+ await setFieldValue(el, check2, { timeout: TIMEOUT2 });
138
+ await sleep(DELAY);
139
+ },
140
+ "For checkbox input or switch component"
141
+ );
142
+ var focus = When("I {focus|blur} {locator}", async function(focus2, selector) {
143
+ const el = await locator(this.page, selector);
144
+ if (focus2) {
145
+ await el.focus({ timeout: TIMEOUT2 });
146
+ } else {
147
+ await el.blur({ timeout: TIMEOUT2 });
148
+ }
149
+ });
150
+
151
+ // src/steps/keyboard.ts
152
+ var keyboard_exports = {};
153
+ __export(keyboard_exports, {
154
+ press: () => press,
155
+ type: () => type
156
+ });
157
+ var DELAY2 = 500;
158
+ var press = When("I press {keys}", async function(combo) {
159
+ for (const m of combo.modifiers) await this.page.keyboard.down(m);
160
+ await this.page.keyboard.press(combo.key);
161
+ for (const m of combo.modifiers.toReversed()) await this.page.keyboard.up(m);
162
+ await sleep(DELAY2);
163
+ });
164
+ var type = When("I type {string}", async function(value) {
165
+ await this.page.keyboard.type(value, { delay: 200 });
166
+ await sleep(DELAY2);
167
+ });
168
+
169
+ // src/steps/mailbox.ts
170
+ var mailbox_exports = {};
171
+ __export(mailbox_exports, {
172
+ receive: () => receive,
173
+ view: () => view
174
+ });
175
+ var MAX_RECEIVE_WAIT = 12e4;
176
+ var view = Given(
177
+ `I'm viewing an email sent to {string} with subject {string}`,
178
+ async function(address, subject) {
179
+ const emails = await receiveMail(address, { full: true, after: this.startTime, subject, limit: 1 });
180
+ if (emails.length === 0) {
181
+ throw new Error(`Did not receive an email with subject "${subject}"`);
182
+ }
183
+ const email = emails[0];
184
+ await this.page.goto("about:blank", { waitUntil: "load" });
185
+ await this.page.setContent(email.html ?? textToHtml(email.text), { waitUntil: "domcontentloaded" });
186
+ }
187
+ );
188
+ var receive = Then(
189
+ "I received an email sent to {string} with subject {string}",
190
+ async function(address, subject) {
191
+ const emails = await receiveMail(address, {
192
+ after: this.startTime,
193
+ full: true,
194
+ subject,
195
+ wait: true,
196
+ timeout: MAX_RECEIVE_WAIT,
197
+ limit: 1
198
+ });
199
+ if (emails.length === 0) {
200
+ throw new Error(`Did not receive an email with subject "${subject}"`);
201
+ }
202
+ const email = emails[0];
203
+ this.attach(toEml(email), {
204
+ mediaType: "message/rfc822",
205
+ fileName: asFilename(email.subject, "eml")
206
+ });
207
+ }
208
+ );
209
+
210
+ // src/steps/mouse.ts
211
+ var mouse_exports = {};
212
+ __export(mouse_exports, {
213
+ click: () => click,
214
+ clickHold: () => clickHold,
215
+ scroll: () => scroll
216
+ });
217
+ var TIMEOUT3 = 2500;
218
+ async function press2(el, action) {
219
+ if (action === "hover") {
220
+ await el.hover({ timeout: TIMEOUT3 });
221
+ } else {
222
+ await el.click({
223
+ button: action === "right-click" ? "right" : "left",
224
+ clickCount: action === "double-click" ? 2 : 1,
225
+ timeout: TIMEOUT3
226
+ });
227
+ }
228
+ }
229
+ var click = When(
230
+ "I {click|double-click|right-click|hover} {locator}",
231
+ async function(action, selector) {
232
+ const prevUrl = this.page.url();
233
+ const el = await locator(this.page, selector);
234
+ await press2(el, action);
235
+ await waitAfterInteraction(this.page, el, { prevUrl });
236
+ }
237
+ );
238
+ var clickHold = When(
239
+ "I {click|double-click|right-click|hover} {locator} while holding {keys}",
240
+ async function(action, selector, combo) {
241
+ const prevUrl = this.page.url();
242
+ const el = await locator(this.page, selector);
243
+ const keys = [...combo.modifiers, combo.key];
244
+ for (const m of keys) await this.page.keyboard.down(m);
245
+ await press2(el, action);
246
+ for (const m of keys.reverse()) await this.page.keyboard.up(m);
247
+ await waitAfterInteraction(this.page, el, { prevUrl });
248
+ }
249
+ );
250
+ var scroll = When("I scroll {locator} into view", async function(selector) {
251
+ const el = await locator(this.page, selector);
252
+ await el.scrollIntoViewIfNeeded({ timeout: TIMEOUT3 });
253
+ });
254
+
255
+ // src/steps/navigation.ts
256
+ var navigation_exports = {};
257
+ __export(navigation_exports, {
258
+ assertPath: () => assertPath,
259
+ back: () => back,
260
+ navHome: () => navHome,
261
+ navPath: () => navPath,
262
+ popupClosed: () => popupClosed
263
+ });
264
+ var scrapeLang = metascraper([metascraperLang()]);
265
+ async function getLang(page) {
266
+ const html = "html" in page ? page.html : await page.content();
267
+ const url = typeof page.url === "function" ? page.url() : page.url;
268
+ const { lang = null } = await scrapeLang({ html, url });
269
+ if (!lang) return null;
270
+ const code = lang.substring(0, 2);
271
+ const name = ISO6391.getName(code) || code;
272
+ return { code, name };
273
+ }
274
+
275
+ // src/steps/navigation.ts
276
+ async function openPage(world, path) {
277
+ const { page } = world;
278
+ const result = await page.goto(path);
279
+ expect(result?.status()).toBeLessThan(400);
280
+ await waitForIdle(page);
281
+ world.lang ??= await getLang(page) || void 0;
282
+ }
283
+ var navHome = Given("I'm on the homepage", async function() {
284
+ await openPage(this, "/");
285
+ });
286
+ var navPath = Given("I'm on page {string}", async function(path) {
287
+ await openPage(this, path);
288
+ });
289
+ var popupClosed = Given("all popups are closed", async function() {
290
+ await suppressInterferences(this.page, { lang: this.lang?.code });
291
+ });
292
+ var assertPath = Then("I should be on page {string}", async function(expectedPath) {
293
+ await eventually(async () => {
294
+ const { path: actualPath } = splitUrl(this.page.url());
295
+ if (expectedPath.includes(":")) {
296
+ const { regexp, names } = pathRegexp(expectedPath);
297
+ expect(actualPath, `Expected path ${actualPath} to match pattern ${expectedPath}`).toMatch(regexp);
298
+ const match = actualPath.match(regexp);
299
+ this.pathParams = Object.fromEntries(names.map((name, i) => [name, decodeURIComponent(match[i + 1])]));
300
+ } else {
301
+ expect(actualPath).toEqual(expectedPath);
302
+ delete this.pathParams;
303
+ }
304
+ });
305
+ });
306
+ var back = When("I go back to the previous page", async function() {
307
+ await this.page.goBack();
308
+ await waitForIdle(this.page);
309
+ });
310
+
311
+ // src/steps/index.ts
312
+ var stepsDefinitions = [
313
+ ...Object.values(assert_exports),
314
+ ...Object.values(navigation_exports),
315
+ ...Object.values(mouse_exports),
316
+ ...Object.values(form_exports),
317
+ ...Object.values(keyboard_exports),
318
+ ...Object.values(mailbox_exports),
319
+ ...Object.values(clipboard_exports)
320
+ ];
321
+
322
+ // src/utils/file.ts
323
+ function toFile(data, options) {
324
+ const opt = typeof options === "string" ? { mediaType: options } : options ?? { mediaType: "application/octet-stream" };
325
+ const type2 = opt.mediaType || "application/octet-stream";
326
+ const name = opt.fileName || `attachment-${Date.now()}`;
327
+ if (isReadable(data)) {
328
+ throw new Error("toFile does not support Readable streams; provide Buffer or string");
329
+ }
330
+ const part = typeof data === "string" ? data : new Uint8Array(data);
331
+ return new File([part], name, { type: type2 });
332
+ }
333
+ function isReadable(value) {
334
+ return !!value && typeof value === "object" && (typeof value.pipe === "function" || typeof value.read === "function");
335
+ }
336
+
337
+ export { stepsDefinitions, toFile, typeDefinitions };
338
+ //# sourceMappingURL=index.js.map
339
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/parameters.ts","../src/steps/assert.ts","../src/utils/test-helpers.ts","../src/steps/wrappers.ts","../src/steps/clipboard.ts","../src/steps/form.ts","../src/steps/keyboard.ts","../src/steps/mailbox.ts","../src/steps/mouse.ts","../src/steps/navigation.ts","../src/utils/get-lang.ts","../src/steps/index.ts","../src/utils/file.ts"],"names":["contain","locator","TIMEOUT","check","focus","DELAY","sleep","press","expect","type"],"mappings":"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","file":"index.js","sourcesContent":["import { booleanParameter, enumParameter, keysParameter, locatorParameter, valueParameter } from '@letsrunit/gherkin';\n\nexport const typeDefinitions = [\n locatorParameter(),\n valueParameter(),\n keysParameter(),\n\n booleanParameter('visible', 'hidden'),\n booleanParameter('enabled', 'disabled', /((?:en|dis)abled)/),\n booleanParameter('checked', 'unchecked', /((?:un)?checked)/),\n booleanParameter('contains', 'not contains', /((?:not )?contains)/),\n\n booleanParameter('check', 'uncheck', /((?:un)?check)/),\n booleanParameter('focus', 'blur'),\n\n enumParameter(['click', 'double-click', 'right-click', 'hover'], /((?:double-|right-)?click|hover)/),\n];\n","import { locator } from '@letsrunit/playwright';\nimport { expectOrNot } from '../utils/test-helpers';\nimport { Then } from './wrappers';\n\nconst WAIT_TIMEOUT = 5000;\n\nexport const see = Then(\n 'The page {contains|not contains} {locator}',\n async function (visible: boolean, selector: string) {\n const el = await locator(this.page, selector);\n await expectOrNot(el, visible).toBeVisible({ timeout: WAIT_TIMEOUT });\n },\n);\n\nexport const contain = Then(\n '{locator} {contains|not contains} {locator}',\n async function (selector: string, contain: boolean, child: string) {\n const el = await locator(this.page, selector);\n const childElement = el.locator(child);\n await expectOrNot(childElement, contain).toBeAttached({ timeout: WAIT_TIMEOUT });\n },\n);\n","import { expect, type Expect } from '@playwright/test';\n\nexport function expectOrNot<T>(\n actual: T,\n toBe: boolean\n): ReturnType<Expect<T>> | ReturnType<ReturnType<Expect<T>>['not']> {\n return toBe ? expect(actual) : expect(actual).not;\n}\n","import type { StepDefinition, StepHandler } from '../types';\n\nexport function Given(expression: string | RegExp, fn: StepHandler, comment?: string): StepDefinition {\n return { type: 'Given', expression, fn, comment };\n}\n\nexport function When(expression: string | RegExp, fn: StepHandler, comment?: string): StepDefinition {\n return { type: 'When', expression, fn, comment };\n}\n\nexport function Then(expression: string | RegExp, fn: StepHandler, comment?: string): StepDefinition {\n return { type: 'Then', expression, fn, comment };\n}\n","import { locator } from '@letsrunit/playwright';\nimport type { Locator } from '@playwright/test';\nimport { When } from './wrappers';\n\nconst TIMEOUT = 500;\n\nasync function copyInput(el: Locator): Promise<string | undefined> {\n try {\n return await el.inputValue();\n } catch {}\n}\n\nasync function copyLink(el: Locator): Promise<string | undefined> {\n try {\n const tag = await el.evaluate<string>((n: Element) => n.tagName.toLowerCase());\n const href = tag === 'a' ? await el.getAttribute('href') : null;\n if (href) {\n return href.startsWith('mailto:') ? href.replace(/^mailto:/i, '') : href;\n }\n } catch {}\n}\n\nasync function copyText(el: Locator): Promise<string | null> {\n return (await el.textContent()) ?? null;\n}\n\nexport const copy = When('I copy {locator} to the clipboard', async function (selector: string) {\n const el = await locator(this.page, selector);\n let value = (await copyInput(el)) ?? (await copyLink(el)) ?? (await copyText(el));\n\n this.clipboard = { value };\n});\n\nexport const paste = When('I paste from the clipboard into {locator}', async function (selector: string) {\n const el = await locator(this.page, selector);\n const value = this.clipboard?.value || '';\n\n await el.fill(String(value), { timeout: TIMEOUT });\n});\n","import { locator, setFieldValue } from '@letsrunit/playwright';\nimport { type Scalar, sleep } from '@letsrunit/utils';\nimport { When } from './wrappers';\n\nconst TIMEOUT = 500;\nconst DELAY = 500;\n\nexport const set = When('I set {locator} to {value}', async function (selector: string, value: Scalar | Scalar[]) {\n const el = await locator(this.page, selector);\n await setFieldValue(el, value, { timeout: TIMEOUT });\n await sleep(DELAY);\n});\n\nexport const setRange = When(\n 'I set {locator} to range of {value} to {value}',\n async function (selector: string, from: Scalar, to: Scalar) {\n const el = await locator(this.page, selector);\n await setFieldValue(el, { from, to }, { timeout: TIMEOUT });\n await sleep(DELAY);\n },\n);\n\n\nexport const clear = When('I clear {locator}', async function (selector) {\n const el = await locator(this.page, selector);\n await setFieldValue(el, null, { timeout: TIMEOUT });\n await sleep(DELAY);\n});\n\nexport const check = When(\n 'I {check|uncheck} {locator}',\n async function (check: boolean, selector: string) {\n const el = await locator(this.page, selector);\n await setFieldValue(el, check, { timeout: TIMEOUT });\n await sleep(DELAY);\n },\n 'For checkbox input or switch component',\n);\n\nexport const focus = When('I {focus|blur} {locator}', async function (focus: boolean, selector: string) {\n const el = await locator(this.page, selector);\n\n if (focus) {\n await el.focus({ timeout: TIMEOUT });\n } else {\n await el.blur({ timeout: TIMEOUT });\n }\n});\n","import type { KeyCombo } from '@letsrunit/gherkin';\nimport { sleep } from '@letsrunit/utils';\nimport { When } from './wrappers';\n\nconst DELAY = 500;\n\nexport const press = When('I press {keys}', async function (combo: KeyCombo) {\n // Hold modifiers\n for (const m of combo.modifiers) await this.page.keyboard.down(m);\n\n // Press final key\n await this.page.keyboard.press(combo.key);\n\n // Release modifiers (reverse order)\n for (const m of combo.modifiers.toReversed()) await this.page.keyboard.up(m);\n\n await sleep(DELAY);\n});\n\nexport const type = When('I type {string}', async function (value: string) {\n await this.page.keyboard.type(value, { delay: 200 });\n await sleep(DELAY);\n});\n","import { receiveMail, toEml } from '@letsrunit/mailbox';\nimport { asFilename, textToHtml } from '@letsrunit/utils';\nimport { Given, Then } from './wrappers';\n\nconst MAX_RECEIVE_WAIT = 120_000; // 2 minutes\n\nexport const view = Given(\n `I'm viewing an email sent to {string} with subject {string}`,\n async function (address: string, subject: string) {\n const emails = await receiveMail(address, { full: true, after: this.startTime, subject, limit: 1 });\n if (emails.length === 0) {\n throw new Error(`Did not receive an email with subject \"${subject}\"`);\n }\n\n const email = emails[0];\n\n await this.page.goto('about:blank', { waitUntil: 'load' });\n await this.page.setContent(email.html ?? textToHtml(email.text!), { waitUntil: 'domcontentloaded' });\n },\n);\n\nexport const receive = Then(\n 'I received an email sent to {string} with subject {string}',\n async function (address: string, subject: string) {\n const emails = await receiveMail(address, {\n after: this.startTime,\n full: true,\n subject,\n wait: true,\n timeout: MAX_RECEIVE_WAIT,\n limit: 1,\n });\n\n if (emails.length === 0) {\n throw new Error(`Did not receive an email with subject \"${subject}\"`);\n }\n\n const email = emails[0];\n\n this.attach(toEml(email), {\n mediaType: 'message/rfc822',\n fileName: asFilename(email.subject, 'eml'),\n });\n },\n);\n","import type { KeyCombo } from '@letsrunit/gherkin';\nimport { locator, waitAfterInteraction } from '@letsrunit/playwright';\nimport type { Locator } from '@playwright/test';\nimport { When } from './wrappers';\n\nconst TIMEOUT = 2500;\n\ntype MouseAction = 'click' | 'double-click' | 'right-click' | 'hover';\n\nasync function press(el: Locator, action: MouseAction) {\n if (action === 'hover') {\n await el.hover({ timeout: TIMEOUT });\n } else {\n await el.click({\n button: action === 'right-click' ? 'right' : 'left',\n clickCount: action === 'double-click' ? 2 : 1,\n timeout: TIMEOUT,\n });\n }\n}\n\nexport const click = When(\n 'I {click|double-click|right-click|hover} {locator}',\n async function (action: MouseAction, selector: string) {\n const prevUrl = this.page.url();\n\n const el = await locator(this.page, selector);\n await press(el, action);\n\n await waitAfterInteraction(this.page, el, { prevUrl });\n },\n);\n\nexport const clickHold = When(\n 'I {click|double-click|right-click|hover} {locator} while holding {keys}',\n async function (action: MouseAction, selector: string, combo: KeyCombo) {\n const prevUrl = this.page.url();\n\n const el = await locator(this.page, selector);\n const keys = [...combo.modifiers, combo.key];\n\n for (const m of keys) await this.page.keyboard.down(m);\n await press(el, action);\n for (const m of keys.reverse()) await this.page.keyboard.up(m);\n\n await waitAfterInteraction(this.page, el, { prevUrl });\n },\n);\n\nexport const scroll = When('I scroll {locator} into view', async function (selector: string) {\n const el = await locator(this.page, selector);\n await el.scrollIntoViewIfNeeded({ timeout: TIMEOUT });\n});\n","import { suppressInterferences, waitForIdle } from '@letsrunit/playwright';\nimport { eventually, pathRegexp, splitUrl } from '@letsrunit/utils';\nimport { expect } from '@playwright/test';\nimport { World } from '../types';\nimport { getLang } from '../utils/get-lang';\nimport { Given, Then, When } from './wrappers';\n\nasync function openPage(world: World, path: string): Promise<void> {\n const { page } = world;\n\n const result = await page.goto(path);\n expect(result?.status()).toBeLessThan(400);\n\n await waitForIdle(page);\n\n world.lang ??= (await getLang(page)) || undefined;\n}\n\nexport const navHome = Given(\"I'm on the homepage\", async function () {\n await openPage(this, '/');\n});\n\nexport const navPath = Given(\"I'm on page {string}\", async function (path: string) {\n await openPage(this, path);\n});\n\nexport const popupClosed = Given('all popups are closed', async function (){\n await suppressInterferences(this.page, { lang: this.lang?.code });\n});\n\nexport const assertPath = Then('I should be on page {string}', async function (expectedPath: string) {\n await eventually(async () => {\n const { path: actualPath } = splitUrl(this.page.url());\n\n if (expectedPath.includes(':')) {\n const { regexp, names } = pathRegexp(expectedPath);\n\n expect(actualPath, `Expected path ${actualPath} to match pattern ${expectedPath}`).toMatch(regexp);\n\n const match = actualPath.match(regexp);\n this.pathParams = Object.fromEntries(names.map((name, i) => [name, decodeURIComponent(match![i + 1])]));\n } else {\n expect(actualPath).toEqual(expectedPath);\n delete this.pathParams;\n }\n });\n});\n\nexport const back = When('I go back to the previous page', async function () {\n await this.page.goBack();\n await waitForIdle(this.page);\n});\n","import type { Snapshot } from '@letsrunit/playwright';\nimport { Page } from '@playwright/test';\nimport ISO6391 from 'iso-639-1';\nimport metascraper from 'metascraper';\nimport metascraperLang from 'metascraper-lang';\n\nconst scrapeLang = metascraper([metascraperLang()]);\n\nexport async function getLang(\n page: Pick<Page, 'content' | 'url'> | Snapshot,\n): Promise<{ code: string; name: string} | null> {\n const html = 'html' in page ? page.html : await page.content();\n const url = typeof page.url === 'function' ? page.url() : page.url;\n\n const { lang = null } = await scrapeLang({ html, url });\n if (!lang) return null;\n\n const code = lang.substring(0, 2);\n const name = ISO6391.getName(code) || code;\n\n return { code, name };\n}\n","import type { StepDefinition } from '../types';\nimport * as assert from './assert';\nimport * as clipboard from './clipboard';\nimport * as form from './form';\nimport * as keyboard from './keyboard';\nimport * as mailbox from './mailbox';\nimport * as mouse from './mouse';\nimport * as navigation from './navigation';\n\n// The order matters for the LLM\nexport const stepsDefinitions: StepDefinition[] = [\n ...Object.values(assert),\n ...Object.values(navigation),\n ...Object.values(mouse),\n ...Object.values(form),\n ...Object.values(keyboard),\n ...Object.values(mailbox),\n ...Object.values(clipboard),\n];\n","import type { Readable } from 'node:stream';\n\ntype AttachData = string | Buffer | Readable;\ntype AttachOptions = string | {\n mediaType: string;\n fileName?: string;\n};\n\nexport function toFile(data: AttachData, options?: AttachOptions): File {\n const opt =\n typeof options === 'string'\n ? { mediaType: options }\n : options ?? { mediaType: 'application/octet-stream' };\n\n const type = opt.mediaType || 'application/octet-stream';\n const name = opt.fileName || `attachment-${Date.now()}`;\n\n if (isReadable(data)) {\n throw new Error('toFile does not support Readable streams; provide Buffer or string');\n }\n\n const part = typeof data === 'string' ? data : new Uint8Array(data);\n\n return new File([part], name, { type });\n}\n\nfunction isReadable(value: unknown): value is Readable {\n return (\n !!value &&\n typeof value === 'object' &&\n (typeof (value as any).pipe === 'function' || typeof (value as any).read === 'function')\n );\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@letsrunit/bdd",
3
+ "version": "0.1.0",
4
+ "description": "BDD step definitions for browser automation with letsrunit",
5
+ "keywords": [
6
+ "testing",
7
+ "bdd",
8
+ "gherkin",
9
+ "playwright",
10
+ "letsrunit"
11
+ ],
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/letsrunit/letsrunit.git",
16
+ "directory": "packages/bdd"
17
+ },
18
+ "bugs": "https://github.com/letsrunit/letsrunit/issues",
19
+ "homepage": "https://github.com/letsrunit/letsrunit#readme",
20
+ "private": false,
21
+ "type": "module",
22
+ "main": "./dist/index.js",
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "src",
29
+ "README.md"
30
+ ],
31
+ "scripts": {
32
+ "build": "../../node_modules/.bin/tsup",
33
+ "test": "vitest run",
34
+ "test:cov": "vitest run --coverage",
35
+ "typecheck": "tsc --noEmit",
36
+ "gen:stub": "tsx scripts/generate-stub.ts"
37
+ },
38
+ "packageManager": "yarn@4.10.3",
39
+ "dependencies": {
40
+ "@cucumber/cucumber-expressions": "^18.1.0",
41
+ "@letsrunit/gherkin": "workspace:*",
42
+ "@letsrunit/mailbox": "workspace:*",
43
+ "@letsrunit/playwright": "workspace:*",
44
+ "@letsrunit/utils": "workspace:*",
45
+ "@playwright/test": "^1.57.0",
46
+ "iso-639-1": "^3.1.5",
47
+ "metascraper": "^5.49.15",
48
+ "metascraper-lang": "^5.49.15"
49
+ },
50
+ "peerDependencies": {
51
+ "@cucumber/cucumber": "^12.0.0"
52
+ },
53
+ "devDependencies": {
54
+ "@cucumber/cucumber": "^12.5.0",
55
+ "vitest": "^4.0.17"
56
+ },
57
+ "module": "./dist/index.js",
58
+ "types": "./dist/index.d.ts",
59
+ "exports": {
60
+ ".": {
61
+ "types": "./dist/index.d.ts",
62
+ "import": "./dist/index.js"
63
+ }
64
+ }
65
+ }
package/src/_stub.ts ADDED
@@ -0,0 +1,13 @@
1
+ // Generated for IDE indexing (WebStorm), never executed.
2
+ import { defineParameterType } from '@cucumber/cucumber';
3
+
4
+ defineParameterType({ name: 'locator', regexp: /((?:(?:the )?\w+(?: "[^"]*")?|`([^`]+|\\.)*`)(?: with(?:in|out)? (?:(?:the )?\w+(?: "[^"]*")?|`([^`]+|\\.)*`))*)/, transformer: (s: string) => s });
5
+ defineParameterType({ name: 'value', regexp: /"((?:[^"\\]+|\\.)*)"|(-?\d+(?:\.\d+)?)|date (?:of )?((?:today|tomorrow|yesterday|\d+ \w+ (?:ago|from now))(?: (?:at )?\d\d?:\d\d(?:\d\d)?)?|"((?:[^"\\]+|\\.)*)")/, transformer: (s: string) => s });
6
+ defineParameterType({ name: 'keys', regexp: /"([^"]+)"|'([^']+)'/, transformer: (s: string) => s });
7
+ defineParameterType({ name: 'visible|hidden', regexp: /(visible|hidden)/, transformer: (s: string) => s });
8
+ defineParameterType({ name: 'enabled|disabled', regexp: /((?:en|dis)abled)/, transformer: (s: string) => s });
9
+ defineParameterType({ name: 'checked|unchecked', regexp: /((?:un)?checked)/, transformer: (s: string) => s });
10
+ defineParameterType({ name: 'contains|not contains', regexp: /((?:not )?contains)/, transformer: (s: string) => s });
11
+ defineParameterType({ name: 'check|uncheck', regexp: /((?:un)?check)/, transformer: (s: string) => s });
12
+ defineParameterType({ name: 'focus|blur', regexp: /(focus|blur)/, transformer: (s: string) => s });
13
+ defineParameterType({ name: 'click|double-click|right-click|hover', regexp: /((?:double-|right-)?click|hover)/, transformer: (s: string) => s });
package/src/define.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { defineParameterType, Given, When, Then } from '@cucumber/cucumber';
2
+ import { typeDefinitions } from './parameters';
3
+ import { stepsDefinitions } from './steps';
4
+ import { sanitizeStepDefinition } from '@letsrunit/gherkin';
5
+
6
+ for (const type of typeDefinitions) {
7
+ defineParameterType(type);
8
+ }
9
+
10
+ for (const step of stepsDefinitions) {
11
+ const def = step.type === 'Given' ? Given : step.type === 'When' ? When : Then;
12
+ def(sanitizeStepDefinition(step.expression), step.fn);
13
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './parameters';
2
+ export * from './steps';
3
+ export * from './types';
4
+ export { toFile } from './utils/file'
@@ -0,0 +1,17 @@
1
+ import { booleanParameter, enumParameter, keysParameter, locatorParameter, valueParameter } from '@letsrunit/gherkin';
2
+
3
+ export const typeDefinitions = [
4
+ locatorParameter(),
5
+ valueParameter(),
6
+ keysParameter(),
7
+
8
+ booleanParameter('visible', 'hidden'),
9
+ booleanParameter('enabled', 'disabled', /((?:en|dis)abled)/),
10
+ booleanParameter('checked', 'unchecked', /((?:un)?checked)/),
11
+ booleanParameter('contains', 'not contains', /((?:not )?contains)/),
12
+
13
+ booleanParameter('check', 'uncheck', /((?:un)?check)/),
14
+ booleanParameter('focus', 'blur'),
15
+
16
+ enumParameter(['click', 'double-click', 'right-click', 'hover'], /((?:double-|right-)?click|hover)/),
17
+ ];
@@ -0,0 +1,22 @@
1
+ import { locator } from '@letsrunit/playwright';
2
+ import { expectOrNot } from '../utils/test-helpers';
3
+ import { Then } from './wrappers';
4
+
5
+ const WAIT_TIMEOUT = 5000;
6
+
7
+ export const see = Then(
8
+ 'The page {contains|not contains} {locator}',
9
+ async function (visible: boolean, selector: string) {
10
+ const el = await locator(this.page, selector);
11
+ await expectOrNot(el, visible).toBeVisible({ timeout: WAIT_TIMEOUT });
12
+ },
13
+ );
14
+
15
+ export const contain = Then(
16
+ '{locator} {contains|not contains} {locator}',
17
+ async function (selector: string, contain: boolean, child: string) {
18
+ const el = await locator(this.page, selector);
19
+ const childElement = el.locator(child);
20
+ await expectOrNot(childElement, contain).toBeAttached({ timeout: WAIT_TIMEOUT });
21
+ },
22
+ );
@@ -0,0 +1,39 @@
1
+ import { locator } from '@letsrunit/playwright';
2
+ import type { Locator } from '@playwright/test';
3
+ import { When } from './wrappers';
4
+
5
+ const TIMEOUT = 500;
6
+
7
+ async function copyInput(el: Locator): Promise<string | undefined> {
8
+ try {
9
+ return await el.inputValue();
10
+ } catch {}
11
+ }
12
+
13
+ async function copyLink(el: Locator): Promise<string | undefined> {
14
+ try {
15
+ const tag = await el.evaluate<string>((n: Element) => n.tagName.toLowerCase());
16
+ const href = tag === 'a' ? await el.getAttribute('href') : null;
17
+ if (href) {
18
+ return href.startsWith('mailto:') ? href.replace(/^mailto:/i, '') : href;
19
+ }
20
+ } catch {}
21
+ }
22
+
23
+ async function copyText(el: Locator): Promise<string | null> {
24
+ return (await el.textContent()) ?? null;
25
+ }
26
+
27
+ export const copy = When('I copy {locator} to the clipboard', async function (selector: string) {
28
+ const el = await locator(this.page, selector);
29
+ let value = (await copyInput(el)) ?? (await copyLink(el)) ?? (await copyText(el));
30
+
31
+ this.clipboard = { value };
32
+ });
33
+
34
+ export const paste = When('I paste from the clipboard into {locator}', async function (selector: string) {
35
+ const el = await locator(this.page, selector);
36
+ const value = this.clipboard?.value || '';
37
+
38
+ await el.fill(String(value), { timeout: TIMEOUT });
39
+ });
@@ -0,0 +1,48 @@
1
+ import { locator, setFieldValue } from '@letsrunit/playwright';
2
+ import { type Scalar, sleep } from '@letsrunit/utils';
3
+ import { When } from './wrappers';
4
+
5
+ const TIMEOUT = 500;
6
+ const DELAY = 500;
7
+
8
+ export const set = When('I set {locator} to {value}', async function (selector: string, value: Scalar | Scalar[]) {
9
+ const el = await locator(this.page, selector);
10
+ await setFieldValue(el, value, { timeout: TIMEOUT });
11
+ await sleep(DELAY);
12
+ });
13
+
14
+ export const setRange = When(
15
+ 'I set {locator} to range of {value} to {value}',
16
+ async function (selector: string, from: Scalar, to: Scalar) {
17
+ const el = await locator(this.page, selector);
18
+ await setFieldValue(el, { from, to }, { timeout: TIMEOUT });
19
+ await sleep(DELAY);
20
+ },
21
+ );
22
+
23
+
24
+ export const clear = When('I clear {locator}', async function (selector) {
25
+ const el = await locator(this.page, selector);
26
+ await setFieldValue(el, null, { timeout: TIMEOUT });
27
+ await sleep(DELAY);
28
+ });
29
+
30
+ export const check = When(
31
+ 'I {check|uncheck} {locator}',
32
+ async function (check: boolean, selector: string) {
33
+ const el = await locator(this.page, selector);
34
+ await setFieldValue(el, check, { timeout: TIMEOUT });
35
+ await sleep(DELAY);
36
+ },
37
+ 'For checkbox input or switch component',
38
+ );
39
+
40
+ export const focus = When('I {focus|blur} {locator}', async function (focus: boolean, selector: string) {
41
+ const el = await locator(this.page, selector);
42
+
43
+ if (focus) {
44
+ await el.focus({ timeout: TIMEOUT });
45
+ } else {
46
+ await el.blur({ timeout: TIMEOUT });
47
+ }
48
+ });
@@ -0,0 +1,19 @@
1
+ import type { StepDefinition } from '../types';
2
+ import * as assert from './assert';
3
+ import * as clipboard from './clipboard';
4
+ import * as form from './form';
5
+ import * as keyboard from './keyboard';
6
+ import * as mailbox from './mailbox';
7
+ import * as mouse from './mouse';
8
+ import * as navigation from './navigation';
9
+
10
+ // The order matters for the LLM
11
+ export const stepsDefinitions: StepDefinition[] = [
12
+ ...Object.values(assert),
13
+ ...Object.values(navigation),
14
+ ...Object.values(mouse),
15
+ ...Object.values(form),
16
+ ...Object.values(keyboard),
17
+ ...Object.values(mailbox),
18
+ ...Object.values(clipboard),
19
+ ];
@@ -0,0 +1,23 @@
1
+ import type { KeyCombo } from '@letsrunit/gherkin';
2
+ import { sleep } from '@letsrunit/utils';
3
+ import { When } from './wrappers';
4
+
5
+ const DELAY = 500;
6
+
7
+ export const press = When('I press {keys}', async function (combo: KeyCombo) {
8
+ // Hold modifiers
9
+ for (const m of combo.modifiers) await this.page.keyboard.down(m);
10
+
11
+ // Press final key
12
+ await this.page.keyboard.press(combo.key);
13
+
14
+ // Release modifiers (reverse order)
15
+ for (const m of combo.modifiers.toReversed()) await this.page.keyboard.up(m);
16
+
17
+ await sleep(DELAY);
18
+ });
19
+
20
+ export const type = When('I type {string}', async function (value: string) {
21
+ await this.page.keyboard.type(value, { delay: 200 });
22
+ await sleep(DELAY);
23
+ });
@@ -0,0 +1,45 @@
1
+ import { receiveMail, toEml } from '@letsrunit/mailbox';
2
+ import { asFilename, textToHtml } from '@letsrunit/utils';
3
+ import { Given, Then } from './wrappers';
4
+
5
+ const MAX_RECEIVE_WAIT = 120_000; // 2 minutes
6
+
7
+ export const view = Given(
8
+ `I'm viewing an email sent to {string} with subject {string}`,
9
+ async function (address: string, subject: string) {
10
+ const emails = await receiveMail(address, { full: true, after: this.startTime, subject, limit: 1 });
11
+ if (emails.length === 0) {
12
+ throw new Error(`Did not receive an email with subject "${subject}"`);
13
+ }
14
+
15
+ const email = emails[0];
16
+
17
+ await this.page.goto('about:blank', { waitUntil: 'load' });
18
+ await this.page.setContent(email.html ?? textToHtml(email.text!), { waitUntil: 'domcontentloaded' });
19
+ },
20
+ );
21
+
22
+ export const receive = Then(
23
+ 'I received an email sent to {string} with subject {string}',
24
+ async function (address: string, subject: string) {
25
+ const emails = await receiveMail(address, {
26
+ after: this.startTime,
27
+ full: true,
28
+ subject,
29
+ wait: true,
30
+ timeout: MAX_RECEIVE_WAIT,
31
+ limit: 1,
32
+ });
33
+
34
+ if (emails.length === 0) {
35
+ throw new Error(`Did not receive an email with subject "${subject}"`);
36
+ }
37
+
38
+ const email = emails[0];
39
+
40
+ this.attach(toEml(email), {
41
+ mediaType: 'message/rfc822',
42
+ fileName: asFilename(email.subject, 'eml'),
43
+ });
44
+ },
45
+ );
@@ -0,0 +1,53 @@
1
+ import type { KeyCombo } from '@letsrunit/gherkin';
2
+ import { locator, waitAfterInteraction } from '@letsrunit/playwright';
3
+ import type { Locator } from '@playwright/test';
4
+ import { When } from './wrappers';
5
+
6
+ const TIMEOUT = 2500;
7
+
8
+ type MouseAction = 'click' | 'double-click' | 'right-click' | 'hover';
9
+
10
+ async function press(el: Locator, action: MouseAction) {
11
+ if (action === 'hover') {
12
+ await el.hover({ timeout: TIMEOUT });
13
+ } else {
14
+ await el.click({
15
+ button: action === 'right-click' ? 'right' : 'left',
16
+ clickCount: action === 'double-click' ? 2 : 1,
17
+ timeout: TIMEOUT,
18
+ });
19
+ }
20
+ }
21
+
22
+ export const click = When(
23
+ 'I {click|double-click|right-click|hover} {locator}',
24
+ async function (action: MouseAction, selector: string) {
25
+ const prevUrl = this.page.url();
26
+
27
+ const el = await locator(this.page, selector);
28
+ await press(el, action);
29
+
30
+ await waitAfterInteraction(this.page, el, { prevUrl });
31
+ },
32
+ );
33
+
34
+ export const clickHold = When(
35
+ 'I {click|double-click|right-click|hover} {locator} while holding {keys}',
36
+ async function (action: MouseAction, selector: string, combo: KeyCombo) {
37
+ const prevUrl = this.page.url();
38
+
39
+ const el = await locator(this.page, selector);
40
+ const keys = [...combo.modifiers, combo.key];
41
+
42
+ for (const m of keys) await this.page.keyboard.down(m);
43
+ await press(el, action);
44
+ for (const m of keys.reverse()) await this.page.keyboard.up(m);
45
+
46
+ await waitAfterInteraction(this.page, el, { prevUrl });
47
+ },
48
+ );
49
+
50
+ export const scroll = When('I scroll {locator} into view', async function (selector: string) {
51
+ const el = await locator(this.page, selector);
52
+ await el.scrollIntoViewIfNeeded({ timeout: TIMEOUT });
53
+ });
@@ -0,0 +1,52 @@
1
+ import { suppressInterferences, waitForIdle } from '@letsrunit/playwright';
2
+ import { eventually, pathRegexp, splitUrl } from '@letsrunit/utils';
3
+ import { expect } from '@playwright/test';
4
+ import { World } from '../types';
5
+ import { getLang } from '../utils/get-lang';
6
+ import { Given, Then, When } from './wrappers';
7
+
8
+ async function openPage(world: World, path: string): Promise<void> {
9
+ const { page } = world;
10
+
11
+ const result = await page.goto(path);
12
+ expect(result?.status()).toBeLessThan(400);
13
+
14
+ await waitForIdle(page);
15
+
16
+ world.lang ??= (await getLang(page)) || undefined;
17
+ }
18
+
19
+ export const navHome = Given("I'm on the homepage", async function () {
20
+ await openPage(this, '/');
21
+ });
22
+
23
+ export const navPath = Given("I'm on page {string}", async function (path: string) {
24
+ await openPage(this, path);
25
+ });
26
+
27
+ export const popupClosed = Given('all popups are closed', async function (){
28
+ await suppressInterferences(this.page, { lang: this.lang?.code });
29
+ });
30
+
31
+ export const assertPath = Then('I should be on page {string}', async function (expectedPath: string) {
32
+ await eventually(async () => {
33
+ const { path: actualPath } = splitUrl(this.page.url());
34
+
35
+ if (expectedPath.includes(':')) {
36
+ const { regexp, names } = pathRegexp(expectedPath);
37
+
38
+ expect(actualPath, `Expected path ${actualPath} to match pattern ${expectedPath}`).toMatch(regexp);
39
+
40
+ const match = actualPath.match(regexp);
41
+ this.pathParams = Object.fromEntries(names.map((name, i) => [name, decodeURIComponent(match![i + 1])]));
42
+ } else {
43
+ expect(actualPath).toEqual(expectedPath);
44
+ delete this.pathParams;
45
+ }
46
+ });
47
+ });
48
+
49
+ export const back = When('I go back to the previous page', async function () {
50
+ await this.page.goBack();
51
+ await waitForIdle(this.page);
52
+ });
@@ -0,0 +1,13 @@
1
+ import type { StepDefinition, StepHandler } from '../types';
2
+
3
+ export function Given(expression: string | RegExp, fn: StepHandler, comment?: string): StepDefinition {
4
+ return { type: 'Given', expression, fn, comment };
5
+ }
6
+
7
+ export function When(expression: string | RegExp, fn: StepHandler, comment?: string): StepDefinition {
8
+ return { type: 'When', expression, fn, comment };
9
+ }
10
+
11
+ export function Then(expression: string | RegExp, fn: StepHandler, comment?: string): StepDefinition {
12
+ return { type: 'Then', expression, fn, comment };
13
+ }
package/src/types.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { World as BaseWorld } from '@cucumber/cucumber';
2
+ import type { BrowserContextOptions, Page } from '@playwright/test';
3
+
4
+ export interface World extends BaseWorld<BrowserContextOptions> {
5
+ page: Page;
6
+ startTime: number;
7
+ pathParams?: Record<string, string>;
8
+ lang?: {
9
+ code: string;
10
+ name: string;
11
+ };
12
+ [_: string]: any;
13
+ }
14
+
15
+ export type StepHandler = (this: World, ...args: any[]) => Promise<void> | void;
16
+ export type StepType = 'Given' | 'When' | 'Then';
17
+
18
+ export interface StepDefinition {
19
+ type: StepType;
20
+ expression: string | RegExp;
21
+ fn: StepHandler;
22
+ comment?: string;
23
+ }
@@ -0,0 +1,33 @@
1
+ import type { Readable } from 'node:stream';
2
+
3
+ type AttachData = string | Buffer | Readable;
4
+ type AttachOptions = string | {
5
+ mediaType: string;
6
+ fileName?: string;
7
+ };
8
+
9
+ export function toFile(data: AttachData, options?: AttachOptions): File {
10
+ const opt =
11
+ typeof options === 'string'
12
+ ? { mediaType: options }
13
+ : options ?? { mediaType: 'application/octet-stream' };
14
+
15
+ const type = opt.mediaType || 'application/octet-stream';
16
+ const name = opt.fileName || `attachment-${Date.now()}`;
17
+
18
+ if (isReadable(data)) {
19
+ throw new Error('toFile does not support Readable streams; provide Buffer or string');
20
+ }
21
+
22
+ const part = typeof data === 'string' ? data : new Uint8Array(data);
23
+
24
+ return new File([part], name, { type });
25
+ }
26
+
27
+ function isReadable(value: unknown): value is Readable {
28
+ return (
29
+ !!value &&
30
+ typeof value === 'object' &&
31
+ (typeof (value as any).pipe === 'function' || typeof (value as any).read === 'function')
32
+ );
33
+ }
@@ -0,0 +1,22 @@
1
+ import type { Snapshot } from '@letsrunit/playwright';
2
+ import { Page } from '@playwright/test';
3
+ import ISO6391 from 'iso-639-1';
4
+ import metascraper from 'metascraper';
5
+ import metascraperLang from 'metascraper-lang';
6
+
7
+ const scrapeLang = metascraper([metascraperLang()]);
8
+
9
+ export async function getLang(
10
+ page: Pick<Page, 'content' | 'url'> | Snapshot,
11
+ ): Promise<{ code: string; name: string} | null> {
12
+ const html = 'html' in page ? page.html : await page.content();
13
+ const url = typeof page.url === 'function' ? page.url() : page.url;
14
+
15
+ const { lang = null } = await scrapeLang({ html, url });
16
+ if (!lang) return null;
17
+
18
+ const code = lang.substring(0, 2);
19
+ const name = ISO6391.getName(code) || code;
20
+
21
+ return { code, name };
22
+ }
@@ -0,0 +1,8 @@
1
+ import { expect, type Expect } from '@playwright/test';
2
+
3
+ export function expectOrNot<T>(
4
+ actual: T,
5
+ toBe: boolean
6
+ ): ReturnType<Expect<T>> | ReturnType<ReturnType<Expect<T>>['not']> {
7
+ return toBe ? expect(actual) : expect(actual).not;
8
+ }