@harnessed-ts/gherkin 0.0.0-stage → 0.6.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/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # @harnessed-ts/gherkin
2
+
3
+ ## 0.6.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [e324e94]
8
+ - @harnessed-ts/core@0.6.0
9
+
10
+ ## 0.5.0
11
+
12
+ ### Patch Changes
13
+
14
+ - @harnessed-ts/core@0.5.0
15
+
16
+ ## 0.4.0
17
+
18
+ ### Minor Changes
19
+
20
+ - 9666a3d: New `@harnessed-ts/gherkin`: a scenario-scoped world, a typed page registry (`definePages`) and a `{page}` parameter type (`pageParameter`) that rejects an unknown page name at step matching, with adapters for playwright-bdd (`/playwright-bdd`, `withWorld`), cucumber-js (`/cucumber`, `HarnessedWorld`), @badeball/cypress-cucumber-preprocessor (`/cypress`, `cypressWorld`) and ember-cli-yadda or plain Yadda (`/yadda`, `yaddaWorld`, with a `$page` term from `pageDictionary`). `@harnessed-ts/playwright/bdd` re-exports `withWorld` from it, deprecated for one release.
21
+
22
+ ### Patch Changes
23
+
24
+ - Updated dependencies [9666a3d]
25
+ - Updated dependencies [9666a3d]
26
+ - Updated dependencies [1823546]
27
+ - @harnessed-ts/core@0.4.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joe Gaudet, Jay Seo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,27 @@
1
- # Temporary Holding Version
1
+ # @harnessed-ts/gherkin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Gherkin support for [harnessed](https://github.com/joegaudet/harnessed#readme)
4
+ harnesses: a scenario-scoped world for the pages a scenario builds up, a typed
5
+ page registry, and a `{page}` parameter type that matches only registered
6
+ names, so a typo in a feature fails at step matching.
7
+
8
+ ```ts
9
+ import { definePages, pageParameter } from '@harnessed-ts/gherkin'
10
+
11
+ export const pages = definePages({ checkout: CheckoutPage })
12
+ defineParameterType(pageParameter(pages)) // the runner's own defineParameterType
13
+ ```
14
+
15
+ One adapter per runner gives the world a scenario's lifetime:
16
+
17
+ | Import | Runner | Gives you |
18
+ | -------------------------------------- | --------------------------------------- | ------------------------------ |
19
+ | `@harnessed-ts/gherkin/playwright-bdd` | playwright-bdd | `withWorld`, a `world` fixture |
20
+ | `@harnessed-ts/gherkin/cucumber` | cucumber-js | `HarnessedWorld` |
21
+ | `@harnessed-ts/gherkin/cypress` | @badeball/cypress-cucumber-preprocessor | `cypressWorld` |
22
+ | `@harnessed-ts/gherkin/yadda` | ember-cli-yadda | `yaddaWorld`, `pageDictionary` |
23
+
24
+ Each runner is an optional peer dependency; install the one you use. Setup for
25
+ each, with examples: [Using with Gherkin](https://github.com/joegaudet/harnessed#using-with-gherkin).
26
+
27
+ MIT © Joe Gaudet, Jay Seo
@@ -0,0 +1,40 @@
1
+ // src/index.ts
2
+ function definePages(pages) {
3
+ const names = Object.keys(pages).sort((a, b) => b.length - a.length);
4
+ if (names.length === 0) {
5
+ throw new Error("harnessed: definePages() needs at least one page.");
6
+ }
7
+ const has = (name) => Object.prototype.hasOwnProperty.call(pages, name);
8
+ return {
9
+ names,
10
+ has,
11
+ open(name, env) {
12
+ if (!has(name)) {
13
+ throw new Error(`harnessed: no page named "${name}". Registered: ${names.join(", ")}.`);
14
+ }
15
+ const Page = pages[name];
16
+ return new Page(env);
17
+ }
18
+ };
19
+ }
20
+ function escape(text) {
21
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22
+ }
23
+ function pageParameter(registry, options = {}) {
24
+ return {
25
+ name: options.name ?? "page",
26
+ regexp: new RegExp(registry.names.map(escape).join("|")),
27
+ transformer: (match) => {
28
+ if (!registry.has(match)) throw new Error(`harnessed: no page named "${match}".`);
29
+ return match;
30
+ },
31
+ useForSnippets: false
32
+ };
33
+ }
34
+ function createWorld() {
35
+ return {};
36
+ }
37
+
38
+ export { createWorld, definePages, pageParameter };
39
+ //# sourceMappingURL=chunk-6AIO5EWN.js.map
40
+ //# sourceMappingURL=chunk-6AIO5EWN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";AAkCO,SAAS,YAA+B,KAAA,EAA2B;AACxE,EAAA,MAAM,KAAA,GAAS,MAAA,CAAO,IAAA,CAAK,KAAK,CAAA,CAA8B,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,CAAE,MAAA,GAAS,CAAA,CAAE,MAAM,CAAA;AAChG,EAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AAGtB,IAAA,MAAM,IAAI,MAAM,mDAAmD,CAAA;AAAA,EACrE;AACA,EAAA,MAAM,GAAA,GAAM,CAAC,IAAA,KACX,MAAA,CAAO,UAAU,cAAA,CAAe,IAAA,CAAK,OAAO,IAAI,CAAA;AAClD,EAAA,OAAO;AAAA,IACL,KAAA;AAAA,IACA,GAAA;AAAA,IACA,IAAA,CAAK,MAAM,GAAA,EAAK;AAEd,MAAA,IAAI,CAAC,GAAA,CAAI,IAAI,CAAA,EAAG;AACd,QAAA,MAAM,IAAI,MAAM,CAAA,0BAAA,EAA6B,IAAI,kBAAkB,KAAA,CAAM,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA,CAAG,CAAA;AAAA,MACxF;AACA,MAAA,MAAM,IAAA,GAAO,MAAM,IAAI,CAAA;AACvB,MAAA,OAAO,IAAI,KAAK,GAAG,CAAA;AAAA,IACrB;AAAA,GACF;AACF;AAcA,SAAS,OAAO,IAAA,EAAsB;AACpC,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,qBAAA,EAAuB,MAAM,CAAA;AACnD;AAYO,SAAS,aAAA,CACd,QAAA,EACA,OAAA,GAA6B,EAAC,EACa;AAC3C,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,QAAQ,IAAA,IAAQ,MAAA;AAAA,IACtB,MAAA,EAAQ,IAAI,MAAA,CAAO,QAAA,CAAS,KAAA,CAAM,IAAI,MAAM,CAAA,CAAE,IAAA,CAAK,GAAG,CAAC,CAAA;AAAA,IACvD,aAAa,CAAA,KAAA,KAAS;AAGpB,MAAA,IAAI,CAAC,QAAA,CAAS,GAAA,CAAI,KAAK,CAAA,QAAS,IAAI,KAAA,CAAM,CAAA,0BAAA,EAA6B,KAAK,CAAA,EAAA,CAAI,CAAA;AAChF,MAAA,OAAO,KAAA;AAAA,IACT,CAAA;AAAA,IACA,cAAA,EAAgB;AAAA,GAClB;AACF;AAMO,SAAS,WAAA,GAA6B;AAC3C,EAAA,OAAO,EAAC;AACV","file":"chunk-6AIO5EWN.js","sourcesContent":["import type { EnvConfig } from '@harnessed-ts/core'\n\n/**\n * Gherkin support for harnessed: what every runner needs, with no runner in it.\n *\n * - A **world** — the scenario-scoped bag where steps keep the pages a scenario\n * builds up. Steps are separate functions, so the obvious place for a harness\n * is a module-level `let`, which leaks into the next scenario the moment a\n * runner reuses a worker. Each runner adapter gives the bag the right lifetime.\n * - A **page registry** — the pages a feature can name, so a step reads\n * `Given I open the checkout page` and gets a `CheckoutPage`, typed.\n * - A **`{page}` parameter type** built from the registry, so a feature naming\n * an unknown page fails at step matching, where the typo is, rather than deep\n * inside a step.\n *\n * Step bodies stay hand-written: the feature's wording is the team's, not a\n * mirror of method names.\n */\n\n/** Anything a page registry can construct: a page or component harness class. */\nexport type PageConstructor<T = object> = new (env: EnvConfig) => T\n\nexport type PageMap = Record<string, PageConstructor>\n\n/** The pages a feature can name. */\nexport interface PageRegistry<P extends PageMap> {\n /** Every registered name, longest first — the order a matcher must try them. */\n readonly names: ReadonlyArray<keyof P & string>\n /** Whether `name` is registered: narrows a string from a step to a page name. */\n has(name: string): name is keyof P & string\n /** Constructs the named page with the scenario's env. */\n open<N extends keyof P & string>(name: N, env: EnvConfig): InstanceType<P[N]>\n}\n\nexport function definePages<P extends PageMap>(pages: P): PageRegistry<P> {\n const names = (Object.keys(pages) as Array<keyof P & string>).sort((a, b) => b.length - a.length)\n if (names.length === 0) {\n // `{page}` is an alternation of the names; with none it is the empty\n // pattern, which matches anywhere and would let any step through.\n throw new Error('harnessed: definePages() needs at least one page.')\n }\n const has = (name: string): name is keyof P & string =>\n Object.prototype.hasOwnProperty.call(pages, name)\n return {\n names,\n has,\n open(name, env) {\n // Own names only: `constructor` and the rest of Object.prototype are not pages.\n if (!has(name)) {\n throw new Error(`harnessed: no page named \"${name}\". Registered: ${names.join(', ')}.`)\n }\n const Page = pages[name]\n return new Page(env) as InstanceType<P[typeof name]>\n },\n }\n}\n\n/**\n * A cucumber-expression parameter type, in the shape every runner's\n * `defineParameterType` takes: @cucumber/cucumber, playwright-bdd and\n * @badeball/cypress-cucumber-preprocessor alike.\n */\nexport interface ParameterTypeDefinition<T> {\n name: string\n regexp: RegExp\n transformer: (match: string) => T\n useForSnippets?: boolean\n}\n\nfunction escape(text: string): string {\n return text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * `{page}` for step patterns: matches exactly the registered names (longest\n * first, so `checkout summary` wins over `checkout`) and hands the step the\n * name, typed. Name it something else with `{ name }`.\n *\n * ```ts\n * defineParameterType(pageParameter(pages))\n * Given('I open the {page} page', async function (name) { … pages.open(name, env) })\n * ```\n */\nexport function pageParameter<P extends PageMap>(\n registry: PageRegistry<P>,\n options: { name?: string } = {},\n): ParameterTypeDefinition<keyof P & string> {\n return {\n name: options.name ?? 'page',\n regexp: new RegExp(registry.names.map(escape).join('|')),\n transformer: match => {\n // The regexp only admits registered names, so this cannot fail; it is\n // here so a hand-built pattern that bypasses it still gets a clear error.\n if (!registry.has(match)) throw new Error(`harnessed: no page named \"${match}\".`)\n return match\n },\n useForSnippets: false,\n }\n}\n\n/**\n * A fresh, empty world. Adapters call this once per scenario; it exists so the\n * bag's type is written once, where the steps declare it.\n */\nexport function createWorld<W>(): Partial<W> {\n return {}\n}\n"]}
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ var cucumber = require('@cucumber/cucumber');
4
+
5
+ // src/cucumber.ts
6
+
7
+ // src/index.ts
8
+ function createWorld() {
9
+ return {};
10
+ }
11
+
12
+ // src/cucumber.ts
13
+ var HarnessedWorld = class extends cucumber.World {
14
+ /** The scenario's bag. Starts empty; cucumber builds a new world per scenario. */
15
+ bag = createWorld();
16
+ /** The env every harness in the scenario is built with. Set it in a `Before`. */
17
+ env;
18
+ /** The pages features can name. Assign the registry in a subclass. */
19
+ pages;
20
+ constructor(options) {
21
+ super(options);
22
+ }
23
+ /** Constructs a registered page with the scenario's env. */
24
+ open(name) {
25
+ if (this.pages === void 0) {
26
+ throw new Error("harnessed: this world has no page registry. Assign `pages` in a subclass.");
27
+ }
28
+ if (this.env === void 0) {
29
+ throw new Error("harnessed: this world has no env yet. Set `this.env` in a Before hook.");
30
+ }
31
+ return this.pages.open(name, this.env);
32
+ }
33
+ };
34
+
35
+ exports.HarnessedWorld = HarnessedWorld;
36
+ //# sourceMappingURL=cucumber.cjs.map
37
+ //# sourceMappingURL=cucumber.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/cucumber.ts"],"names":["World"],"mappings":";;;;;;;AAwGO,SAAS,WAAA,GAA6B;AAC3C,EAAA,OAAO,EAAC;AACV;;;ACxEO,IAAM,cAAA,GAAN,cAGGA,cAAA,CAAM;AAAA;AAAA,EAEd,MAAkB,WAAA,EAAe;AAAA;AAAA,EAEjC,GAAA;AAAA;AAAA,EAEA,KAAA;AAAA,EAEA,YAAY,OAAA,EAAwB;AAClC,IAAA,KAAA,CAAM,OAAO,CAAA;AAAA,EACf;AAAA;AAAA,EAGA,KAAiC,IAAA,EAA6B;AAC5D,IAAA,IAAI,IAAA,CAAK,UAAU,MAAA,EAAW;AAC5B,MAAA,MAAM,IAAI,MAAM,2EAA2E,CAAA;AAAA,IAC7F;AACA,IAAA,IAAI,IAAA,CAAK,QAAQ,MAAA,EAAW;AAC1B,MAAA,MAAM,IAAI,MAAM,wEAAwE,CAAA;AAAA,IAC1F;AACA,IAAA,OAAO,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,IAAA,EAAM,KAAK,GAAG,CAAA;AAAA,EACvC;AACF","file":"cucumber.cjs","sourcesContent":["import type { EnvConfig } from '@harnessed-ts/core'\n\n/**\n * Gherkin support for harnessed: what every runner needs, with no runner in it.\n *\n * - A **world** — the scenario-scoped bag where steps keep the pages a scenario\n * builds up. Steps are separate functions, so the obvious place for a harness\n * is a module-level `let`, which leaks into the next scenario the moment a\n * runner reuses a worker. Each runner adapter gives the bag the right lifetime.\n * - A **page registry** — the pages a feature can name, so a step reads\n * `Given I open the checkout page` and gets a `CheckoutPage`, typed.\n * - A **`{page}` parameter type** built from the registry, so a feature naming\n * an unknown page fails at step matching, where the typo is, rather than deep\n * inside a step.\n *\n * Step bodies stay hand-written: the feature's wording is the team's, not a\n * mirror of method names.\n */\n\n/** Anything a page registry can construct: a page or component harness class. */\nexport type PageConstructor<T = object> = new (env: EnvConfig) => T\n\nexport type PageMap = Record<string, PageConstructor>\n\n/** The pages a feature can name. */\nexport interface PageRegistry<P extends PageMap> {\n /** Every registered name, longest first — the order a matcher must try them. */\n readonly names: ReadonlyArray<keyof P & string>\n /** Whether `name` is registered: narrows a string from a step to a page name. */\n has(name: string): name is keyof P & string\n /** Constructs the named page with the scenario's env. */\n open<N extends keyof P & string>(name: N, env: EnvConfig): InstanceType<P[N]>\n}\n\nexport function definePages<P extends PageMap>(pages: P): PageRegistry<P> {\n const names = (Object.keys(pages) as Array<keyof P & string>).sort((a, b) => b.length - a.length)\n if (names.length === 0) {\n // `{page}` is an alternation of the names; with none it is the empty\n // pattern, which matches anywhere and would let any step through.\n throw new Error('harnessed: definePages() needs at least one page.')\n }\n const has = (name: string): name is keyof P & string =>\n Object.prototype.hasOwnProperty.call(pages, name)\n return {\n names,\n has,\n open(name, env) {\n // Own names only: `constructor` and the rest of Object.prototype are not pages.\n if (!has(name)) {\n throw new Error(`harnessed: no page named \"${name}\". Registered: ${names.join(', ')}.`)\n }\n const Page = pages[name]\n return new Page(env) as InstanceType<P[typeof name]>\n },\n }\n}\n\n/**\n * A cucumber-expression parameter type, in the shape every runner's\n * `defineParameterType` takes: @cucumber/cucumber, playwright-bdd and\n * @badeball/cypress-cucumber-preprocessor alike.\n */\nexport interface ParameterTypeDefinition<T> {\n name: string\n regexp: RegExp\n transformer: (match: string) => T\n useForSnippets?: boolean\n}\n\nfunction escape(text: string): string {\n return text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * `{page}` for step patterns: matches exactly the registered names (longest\n * first, so `checkout summary` wins over `checkout`) and hands the step the\n * name, typed. Name it something else with `{ name }`.\n *\n * ```ts\n * defineParameterType(pageParameter(pages))\n * Given('I open the {page} page', async function (name) { … pages.open(name, env) })\n * ```\n */\nexport function pageParameter<P extends PageMap>(\n registry: PageRegistry<P>,\n options: { name?: string } = {},\n): ParameterTypeDefinition<keyof P & string> {\n return {\n name: options.name ?? 'page',\n regexp: new RegExp(registry.names.map(escape).join('|')),\n transformer: match => {\n // The regexp only admits registered names, so this cannot fail; it is\n // here so a hand-built pattern that bypasses it still gets a clear error.\n if (!registry.has(match)) throw new Error(`harnessed: no page named \"${match}\".`)\n return match\n },\n useForSnippets: false,\n }\n}\n\n/**\n * A fresh, empty world. Adapters call this once per scenario; it exists so the\n * bag's type is written once, where the steps declare it.\n */\nexport function createWorld<W>(): Partial<W> {\n return {}\n}\n","import { World } from '@cucumber/cucumber'\nimport type { IWorldOptions } from '@cucumber/cucumber'\nimport type { EnvConfig } from '@harnessed-ts/core'\nimport { createWorld } from './index'\nimport type { PageMap, PageRegistry } from './index'\n\n/**\n * A cucumber-js World with the harnessed pieces on it. Cucumber constructs one\n * per scenario, so everything here is scenario-scoped by construction.\n *\n * ```ts\n * const pageMap = { checkout: CheckoutPage }\n * const pages = definePages(pageMap)\n *\n * class AppWorld extends HarnessedWorld<{ checkout: CheckoutPage }, typeof pageMap> {\n * override pages = pages\n * }\n * setWorldConstructor(AppWorld)\n * defineParameterType(pageParameter(pages))\n *\n * // One browser for the run; a page per scenario.\n * let browser: Browser\n * BeforeAll(async () => {\n * browser = await chromium.launch()\n * })\n * Before(async function (this: AppWorld) {\n * this.env = pw(await browser.newPage()) // or wdio(browser), …\n * })\n * Given('I open the {page} page', async function (this: AppWorld, name: 'checkout') {\n * this.bag.checkout = this.open(name)\n * await this.bag.checkout.goto()\n * })\n * ```\n */\nexport class HarnessedWorld<\n W = Record<string, unknown>,\n P extends PageMap = PageMap,\n> extends World {\n /** The scenario's bag. Starts empty; cucumber builds a new world per scenario. */\n bag: Partial<W> = createWorld<W>()\n /** The env every harness in the scenario is built with. Set it in a `Before`. */\n env: EnvConfig | undefined\n /** The pages features can name. Assign the registry in a subclass. */\n pages: PageRegistry<P> | undefined\n\n constructor(options: IWorldOptions) {\n super(options)\n }\n\n /** Constructs a registered page with the scenario's env. */\n open<N extends keyof P & string>(name: N): InstanceType<P[N]> {\n if (this.pages === undefined) {\n throw new Error('harnessed: this world has no page registry. Assign `pages` in a subclass.')\n }\n if (this.env === undefined) {\n throw new Error('harnessed: this world has no env yet. Set `this.env` in a Before hook.')\n }\n return this.pages.open(name, this.env)\n }\n}\n"]}
@@ -0,0 +1,44 @@
1
+ import { World } from '@cucumber/cucumber';
2
+ import type { IWorldOptions } from '@cucumber/cucumber';
3
+ import type { EnvConfig } from '@harnessed-ts/core';
4
+ import type { PageMap, PageRegistry } from './index';
5
+ /**
6
+ * A cucumber-js World with the harnessed pieces on it. Cucumber constructs one
7
+ * per scenario, so everything here is scenario-scoped by construction.
8
+ *
9
+ * ```ts
10
+ * const pageMap = { checkout: CheckoutPage }
11
+ * const pages = definePages(pageMap)
12
+ *
13
+ * class AppWorld extends HarnessedWorld<{ checkout: CheckoutPage }, typeof pageMap> {
14
+ * override pages = pages
15
+ * }
16
+ * setWorldConstructor(AppWorld)
17
+ * defineParameterType(pageParameter(pages))
18
+ *
19
+ * // One browser for the run; a page per scenario.
20
+ * let browser: Browser
21
+ * BeforeAll(async () => {
22
+ * browser = await chromium.launch()
23
+ * })
24
+ * Before(async function (this: AppWorld) {
25
+ * this.env = pw(await browser.newPage()) // or wdio(browser), …
26
+ * })
27
+ * Given('I open the {page} page', async function (this: AppWorld, name: 'checkout') {
28
+ * this.bag.checkout = this.open(name)
29
+ * await this.bag.checkout.goto()
30
+ * })
31
+ * ```
32
+ */
33
+ export declare class HarnessedWorld<W = Record<string, unknown>, P extends PageMap = PageMap> extends World {
34
+ /** The scenario's bag. Starts empty; cucumber builds a new world per scenario. */
35
+ bag: Partial<W>;
36
+ /** The env every harness in the scenario is built with. Set it in a `Before`. */
37
+ env: EnvConfig | undefined;
38
+ /** The pages features can name. Assign the registry in a subclass. */
39
+ pages: PageRegistry<P> | undefined;
40
+ constructor(options: IWorldOptions);
41
+ /** Constructs a registered page with the scenario's env. */
42
+ open<N extends keyof P & string>(name: N): InstanceType<P[N]>;
43
+ }
44
+ //# sourceMappingURL=cucumber.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cucumber.d.ts","sourceRoot":"","sources":["../src/cucumber.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAC1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAEnD,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,cAAc,CACzB,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3B,CAAC,SAAS,OAAO,GAAG,OAAO,CAC3B,SAAQ,KAAK;IACb,kFAAkF;IAClF,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAAmB;IAClC,iFAAiF;IACjF,GAAG,EAAE,SAAS,GAAG,SAAS,CAAA;IAC1B,sEAAsE;IACtE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAAA;gBAEtB,OAAO,EAAE,aAAa;IAIlC,4DAA4D;IAC5D,IAAI,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAS9D"}
@@ -0,0 +1,28 @@
1
+ import { createWorld } from './chunk-6AIO5EWN.js';
2
+ import { World } from '@cucumber/cucumber';
3
+
4
+ var HarnessedWorld = class extends World {
5
+ /** The scenario's bag. Starts empty; cucumber builds a new world per scenario. */
6
+ bag = createWorld();
7
+ /** The env every harness in the scenario is built with. Set it in a `Before`. */
8
+ env;
9
+ /** The pages features can name. Assign the registry in a subclass. */
10
+ pages;
11
+ constructor(options) {
12
+ super(options);
13
+ }
14
+ /** Constructs a registered page with the scenario's env. */
15
+ open(name) {
16
+ if (this.pages === void 0) {
17
+ throw new Error("harnessed: this world has no page registry. Assign `pages` in a subclass.");
18
+ }
19
+ if (this.env === void 0) {
20
+ throw new Error("harnessed: this world has no env yet. Set `this.env` in a Before hook.");
21
+ }
22
+ return this.pages.open(name, this.env);
23
+ }
24
+ };
25
+
26
+ export { HarnessedWorld };
27
+ //# sourceMappingURL=cucumber.js.map
28
+ //# sourceMappingURL=cucumber.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cucumber.ts"],"names":[],"mappings":";;;AAkCO,IAAM,cAAA,GAAN,cAGG,KAAA,CAAM;AAAA;AAAA,EAEd,MAAkB,WAAA,EAAe;AAAA;AAAA,EAEjC,GAAA;AAAA;AAAA,EAEA,KAAA;AAAA,EAEA,YAAY,OAAA,EAAwB;AAClC,IAAA,KAAA,CAAM,OAAO,CAAA;AAAA,EACf;AAAA;AAAA,EAGA,KAAiC,IAAA,EAA6B;AAC5D,IAAA,IAAI,IAAA,CAAK,UAAU,MAAA,EAAW;AAC5B,MAAA,MAAM,IAAI,MAAM,2EAA2E,CAAA;AAAA,IAC7F;AACA,IAAA,IAAI,IAAA,CAAK,QAAQ,MAAA,EAAW;AAC1B,MAAA,MAAM,IAAI,MAAM,wEAAwE,CAAA;AAAA,IAC1F;AACA,IAAA,OAAO,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,IAAA,EAAM,KAAK,GAAG,CAAA;AAAA,EACvC;AACF","file":"cucumber.js","sourcesContent":["import { World } from '@cucumber/cucumber'\nimport type { IWorldOptions } from '@cucumber/cucumber'\nimport type { EnvConfig } from '@harnessed-ts/core'\nimport { createWorld } from './index'\nimport type { PageMap, PageRegistry } from './index'\n\n/**\n * A cucumber-js World with the harnessed pieces on it. Cucumber constructs one\n * per scenario, so everything here is scenario-scoped by construction.\n *\n * ```ts\n * const pageMap = { checkout: CheckoutPage }\n * const pages = definePages(pageMap)\n *\n * class AppWorld extends HarnessedWorld<{ checkout: CheckoutPage }, typeof pageMap> {\n * override pages = pages\n * }\n * setWorldConstructor(AppWorld)\n * defineParameterType(pageParameter(pages))\n *\n * // One browser for the run; a page per scenario.\n * let browser: Browser\n * BeforeAll(async () => {\n * browser = await chromium.launch()\n * })\n * Before(async function (this: AppWorld) {\n * this.env = pw(await browser.newPage()) // or wdio(browser), …\n * })\n * Given('I open the {page} page', async function (this: AppWorld, name: 'checkout') {\n * this.bag.checkout = this.open(name)\n * await this.bag.checkout.goto()\n * })\n * ```\n */\nexport class HarnessedWorld<\n W = Record<string, unknown>,\n P extends PageMap = PageMap,\n> extends World {\n /** The scenario's bag. Starts empty; cucumber builds a new world per scenario. */\n bag: Partial<W> = createWorld<W>()\n /** The env every harness in the scenario is built with. Set it in a `Before`. */\n env: EnvConfig | undefined\n /** The pages features can name. Assign the registry in a subclass. */\n pages: PageRegistry<P> | undefined\n\n constructor(options: IWorldOptions) {\n super(options)\n }\n\n /** Constructs a registered page with the scenario's env. */\n open<N extends keyof P & string>(name: N): InstanceType<P[N]> {\n if (this.pages === undefined) {\n throw new Error('harnessed: this world has no page registry. Assign `pages` in a subclass.')\n }\n if (this.env === undefined) {\n throw new Error('harnessed: this world has no env yet. Set `this.env` in a Before hook.')\n }\n return this.pages.open(name, this.env)\n }\n}\n"]}
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ function createWorld() {
5
+ return {};
6
+ }
7
+
8
+ // src/cypress.ts
9
+ function isHookRegistrar(value) {
10
+ return typeof value === "function";
11
+ }
12
+ function globalBeforeEach() {
13
+ const hook = Reflect.get(globalThis, "beforeEach");
14
+ if (!isHookRegistrar(hook)) {
15
+ throw new Error(
16
+ "harnessed: cypressWorld() found no global beforeEach. Call it at the top level of a step definitions file, which Cypress bundles into the spec, or pass { beforeEach }."
17
+ );
18
+ }
19
+ return hook;
20
+ }
21
+ function cypressWorld(options = {}) {
22
+ const beforeEach = options.beforeEach ?? globalBeforeEach();
23
+ let current;
24
+ beforeEach(() => {
25
+ current = createWorld();
26
+ });
27
+ return {
28
+ world() {
29
+ if (current === void 0) {
30
+ throw new Error(
31
+ "harnessed: no scenario is running yet, so there is no world. Call world() inside a step, not at the top level of a step definitions file."
32
+ );
33
+ }
34
+ return current;
35
+ }
36
+ };
37
+ }
38
+
39
+ exports.cypressWorld = cypressWorld;
40
+ //# sourceMappingURL=cypress.cjs.map
41
+ //# sourceMappingURL=cypress.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/cypress.ts"],"names":[],"mappings":";;;AAwGO,SAAS,WAAA,GAA6B;AAC3C,EAAA,OAAO,EAAC;AACV;;;ACrFA,SAAS,gBAAgB,KAAA,EAAwC;AAC/D,EAAA,OAAO,OAAO,KAAA,KAAU,UAAA;AAC1B;AAEA,SAAS,gBAAA,GAAkC;AACzC,EAAA,MAAM,IAAA,GAAgB,OAAA,CAAQ,GAAA,CAAI,UAAA,EAAY,YAAY,CAAA;AAC1D,EAAA,IAAI,CAAC,eAAA,CAAgB,IAAI,CAAA,EAAG;AAC1B,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KAEF;AAAA,EACF;AACA,EAAA,OAAO,IAAA;AACT;AAiCO,SAAS,YAAA,CAAgB,OAAA,GAA+B,EAAC,EAAoB;AAClF,EAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,UAAA,IAAc,gBAAA,EAAiB;AAC1D,EAAA,IAAI,OAAA;AACJ,EAAA,UAAA,CAAW,MAAM;AACf,IAAA,OAAA,GAAU,WAAA,EAAe;AAAA,EAC3B,CAAC,CAAA;AACD,EAAA,OAAO;AAAA,IACL,KAAA,GAAQ;AACN,MAAA,IAAI,YAAY,MAAA,EAAW;AACzB,QAAA,MAAM,IAAI,KAAA;AAAA,UACR;AAAA,SAEF;AAAA,MACF;AACA,MAAA,OAAO,OAAA;AAAA,IACT;AAAA,GACF;AACF","file":"cypress.cjs","sourcesContent":["import type { EnvConfig } from '@harnessed-ts/core'\n\n/**\n * Gherkin support for harnessed: what every runner needs, with no runner in it.\n *\n * - A **world** — the scenario-scoped bag where steps keep the pages a scenario\n * builds up. Steps are separate functions, so the obvious place for a harness\n * is a module-level `let`, which leaks into the next scenario the moment a\n * runner reuses a worker. Each runner adapter gives the bag the right lifetime.\n * - A **page registry** — the pages a feature can name, so a step reads\n * `Given I open the checkout page` and gets a `CheckoutPage`, typed.\n * - A **`{page}` parameter type** built from the registry, so a feature naming\n * an unknown page fails at step matching, where the typo is, rather than deep\n * inside a step.\n *\n * Step bodies stay hand-written: the feature's wording is the team's, not a\n * mirror of method names.\n */\n\n/** Anything a page registry can construct: a page or component harness class. */\nexport type PageConstructor<T = object> = new (env: EnvConfig) => T\n\nexport type PageMap = Record<string, PageConstructor>\n\n/** The pages a feature can name. */\nexport interface PageRegistry<P extends PageMap> {\n /** Every registered name, longest first — the order a matcher must try them. */\n readonly names: ReadonlyArray<keyof P & string>\n /** Whether `name` is registered: narrows a string from a step to a page name. */\n has(name: string): name is keyof P & string\n /** Constructs the named page with the scenario's env. */\n open<N extends keyof P & string>(name: N, env: EnvConfig): InstanceType<P[N]>\n}\n\nexport function definePages<P extends PageMap>(pages: P): PageRegistry<P> {\n const names = (Object.keys(pages) as Array<keyof P & string>).sort((a, b) => b.length - a.length)\n if (names.length === 0) {\n // `{page}` is an alternation of the names; with none it is the empty\n // pattern, which matches anywhere and would let any step through.\n throw new Error('harnessed: definePages() needs at least one page.')\n }\n const has = (name: string): name is keyof P & string =>\n Object.prototype.hasOwnProperty.call(pages, name)\n return {\n names,\n has,\n open(name, env) {\n // Own names only: `constructor` and the rest of Object.prototype are not pages.\n if (!has(name)) {\n throw new Error(`harnessed: no page named \"${name}\". Registered: ${names.join(', ')}.`)\n }\n const Page = pages[name]\n return new Page(env) as InstanceType<P[typeof name]>\n },\n }\n}\n\n/**\n * A cucumber-expression parameter type, in the shape every runner's\n * `defineParameterType` takes: @cucumber/cucumber, playwright-bdd and\n * @badeball/cypress-cucumber-preprocessor alike.\n */\nexport interface ParameterTypeDefinition<T> {\n name: string\n regexp: RegExp\n transformer: (match: string) => T\n useForSnippets?: boolean\n}\n\nfunction escape(text: string): string {\n return text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * `{page}` for step patterns: matches exactly the registered names (longest\n * first, so `checkout summary` wins over `checkout`) and hands the step the\n * name, typed. Name it something else with `{ name }`.\n *\n * ```ts\n * defineParameterType(pageParameter(pages))\n * Given('I open the {page} page', async function (name) { … pages.open(name, env) })\n * ```\n */\nexport function pageParameter<P extends PageMap>(\n registry: PageRegistry<P>,\n options: { name?: string } = {},\n): ParameterTypeDefinition<keyof P & string> {\n return {\n name: options.name ?? 'page',\n regexp: new RegExp(registry.names.map(escape).join('|')),\n transformer: match => {\n // The regexp only admits registered names, so this cannot fail; it is\n // here so a hand-built pattern that bypasses it still gets a clear error.\n if (!registry.has(match)) throw new Error(`harnessed: no page named \"${match}\".`)\n return match\n },\n useForSnippets: false,\n }\n}\n\n/**\n * A fresh, empty world. Adapters call this once per scenario; it exists so the\n * bag's type is written once, where the steps declare it.\n */\nexport function createWorld<W>(): Partial<W> {\n return {}\n}\n","import { createWorld } from './index'\n\n/**\n * How a hook gets registered: Mocha's `beforeEach`, which Cypress provides as a\n * global, or anything with its shape.\n */\nexport type HookRegistrar = (fn: () => void) => void\n\nexport interface CypressWorldOptions {\n /**\n * Registers the reset that runs before each scenario. Defaults to the global\n * `beforeEach` Cypress provides in a spec bundle.\n */\n beforeEach?: HookRegistrar\n}\n\nexport interface CypressWorld<W> {\n /** The current scenario's bag. Read it inside a step, never at module level. */\n world(): Partial<W>\n}\n\nfunction isHookRegistrar(value: unknown): value is HookRegistrar {\n return typeof value === 'function'\n}\n\nfunction globalBeforeEach(): HookRegistrar {\n const hook: unknown = Reflect.get(globalThis, 'beforeEach')\n if (!isHookRegistrar(hook)) {\n throw new Error(\n 'harnessed: cypressWorld() found no global beforeEach. Call it at the top level of a ' +\n 'step definitions file, which Cypress bundles into the spec, or pass { beforeEach }.',\n )\n }\n return hook\n}\n\n/**\n * A scenario-scoped world for @badeball/cypress-cucumber-preprocessor.\n *\n * Every scenario in a feature runs in the same spec bundle, so a module-level\n * `let` in a step file carries the last scenario's pages into the next one. This\n * registers a `beforeEach` that starts each scenario with an empty world; steps\n * run as Cypress commands, in order, so each one sees what the steps before it\n * left there. Steps reach the world through `world()`, never through `this`.\n *\n * Harness calls are promises, so a step runs them inside the Cypress driver's\n * bridge (`cy.harnessEnv`, `cy.harness`) — one command each:\n *\n * ```ts\n * import { defineParameterType, When } from '@badeball/cypress-cucumber-preprocessor'\n *\n * const pages = definePages({ checkout: CheckoutPage })\n * defineParameterType(pageParameter(pages))\n * const { world } = cypressWorld<{ checkout: CheckoutPage }>()\n *\n * When('I open the {page} page', (name: 'checkout') => {\n * cy.harnessEnv(async env => {\n * const page = pages.open(name, env)\n * await page.goto()\n * world().checkout = page\n * })\n * })\n * ```\n *\n * Call it once, at the top level of a step definitions file, and share the\n * `world` it returns: each call is a separate world.\n */\nexport function cypressWorld<W>(options: CypressWorldOptions = {}): CypressWorld<W> {\n const beforeEach = options.beforeEach ?? globalBeforeEach()\n let current: Partial<W> | undefined\n beforeEach(() => {\n current = createWorld<W>()\n })\n return {\n world() {\n if (current === undefined) {\n throw new Error(\n 'harnessed: no scenario is running yet, so there is no world. Call world() inside ' +\n 'a step, not at the top level of a step definitions file.',\n )\n }\n return current\n },\n }\n}\n"]}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * How a hook gets registered: Mocha's `beforeEach`, which Cypress provides as a
3
+ * global, or anything with its shape.
4
+ */
5
+ export type HookRegistrar = (fn: () => void) => void;
6
+ export interface CypressWorldOptions {
7
+ /**
8
+ * Registers the reset that runs before each scenario. Defaults to the global
9
+ * `beforeEach` Cypress provides in a spec bundle.
10
+ */
11
+ beforeEach?: HookRegistrar;
12
+ }
13
+ export interface CypressWorld<W> {
14
+ /** The current scenario's bag. Read it inside a step, never at module level. */
15
+ world(): Partial<W>;
16
+ }
17
+ /**
18
+ * A scenario-scoped world for @badeball/cypress-cucumber-preprocessor.
19
+ *
20
+ * Every scenario in a feature runs in the same spec bundle, so a module-level
21
+ * `let` in a step file carries the last scenario's pages into the next one. This
22
+ * registers a `beforeEach` that starts each scenario with an empty world; steps
23
+ * run as Cypress commands, in order, so each one sees what the steps before it
24
+ * left there. Steps reach the world through `world()`, never through `this`.
25
+ *
26
+ * Harness calls are promises, so a step runs them inside the Cypress driver's
27
+ * bridge (`cy.harnessEnv`, `cy.harness`) — one command each:
28
+ *
29
+ * ```ts
30
+ * import { defineParameterType, When } from '@badeball/cypress-cucumber-preprocessor'
31
+ *
32
+ * const pages = definePages({ checkout: CheckoutPage })
33
+ * defineParameterType(pageParameter(pages))
34
+ * const { world } = cypressWorld<{ checkout: CheckoutPage }>()
35
+ *
36
+ * When('I open the {page} page', (name: 'checkout') => {
37
+ * cy.harnessEnv(async env => {
38
+ * const page = pages.open(name, env)
39
+ * await page.goto()
40
+ * world().checkout = page
41
+ * })
42
+ * })
43
+ * ```
44
+ *
45
+ * Call it once, at the top level of a step definitions file, and share the
46
+ * `world` it returns: each call is a separate world.
47
+ */
48
+ export declare function cypressWorld<W>(options?: CypressWorldOptions): CypressWorld<W>;
49
+ //# sourceMappingURL=cypress.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cypress.d.ts","sourceRoot":"","sources":["../src/cypress.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,EAAE,EAAE,MAAM,IAAI,KAAK,IAAI,CAAA;AAEpD,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,UAAU,CAAC,EAAE,aAAa,CAAA;CAC3B;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,gFAAgF;IAChF,KAAK,IAAI,OAAO,CAAC,CAAC,CAAC,CAAA;CACpB;AAiBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,GAAE,mBAAwB,GAAG,YAAY,CAAC,CAAC,CAAC,CAiBlF"}
@@ -0,0 +1,36 @@
1
+ import { createWorld } from './chunk-6AIO5EWN.js';
2
+
3
+ // src/cypress.ts
4
+ function isHookRegistrar(value) {
5
+ return typeof value === "function";
6
+ }
7
+ function globalBeforeEach() {
8
+ const hook = Reflect.get(globalThis, "beforeEach");
9
+ if (!isHookRegistrar(hook)) {
10
+ throw new Error(
11
+ "harnessed: cypressWorld() found no global beforeEach. Call it at the top level of a step definitions file, which Cypress bundles into the spec, or pass { beforeEach }."
12
+ );
13
+ }
14
+ return hook;
15
+ }
16
+ function cypressWorld(options = {}) {
17
+ const beforeEach = options.beforeEach ?? globalBeforeEach();
18
+ let current;
19
+ beforeEach(() => {
20
+ current = createWorld();
21
+ });
22
+ return {
23
+ world() {
24
+ if (current === void 0) {
25
+ throw new Error(
26
+ "harnessed: no scenario is running yet, so there is no world. Call world() inside a step, not at the top level of a step definitions file."
27
+ );
28
+ }
29
+ return current;
30
+ }
31
+ };
32
+ }
33
+
34
+ export { cypressWorld };
35
+ //# sourceMappingURL=cypress.js.map
36
+ //# sourceMappingURL=cypress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cypress.ts"],"names":[],"mappings":";;;AAqBA,SAAS,gBAAgB,KAAA,EAAwC;AAC/D,EAAA,OAAO,OAAO,KAAA,KAAU,UAAA;AAC1B;AAEA,SAAS,gBAAA,GAAkC;AACzC,EAAA,MAAM,IAAA,GAAgB,OAAA,CAAQ,GAAA,CAAI,UAAA,EAAY,YAAY,CAAA;AAC1D,EAAA,IAAI,CAAC,eAAA,CAAgB,IAAI,CAAA,EAAG;AAC1B,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KAEF;AAAA,EACF;AACA,EAAA,OAAO,IAAA;AACT;AAiCO,SAAS,YAAA,CAAgB,OAAA,GAA+B,EAAC,EAAoB;AAClF,EAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,UAAA,IAAc,gBAAA,EAAiB;AAC1D,EAAA,IAAI,OAAA;AACJ,EAAA,UAAA,CAAW,MAAM;AACf,IAAA,OAAA,GAAU,WAAA,EAAe;AAAA,EAC3B,CAAC,CAAA;AACD,EAAA,OAAO;AAAA,IACL,KAAA,GAAQ;AACN,MAAA,IAAI,YAAY,MAAA,EAAW;AACzB,QAAA,MAAM,IAAI,KAAA;AAAA,UACR;AAAA,SAEF;AAAA,MACF;AACA,MAAA,OAAO,OAAA;AAAA,IACT;AAAA,GACF;AACF","file":"cypress.js","sourcesContent":["import { createWorld } from './index'\n\n/**\n * How a hook gets registered: Mocha's `beforeEach`, which Cypress provides as a\n * global, or anything with its shape.\n */\nexport type HookRegistrar = (fn: () => void) => void\n\nexport interface CypressWorldOptions {\n /**\n * Registers the reset that runs before each scenario. Defaults to the global\n * `beforeEach` Cypress provides in a spec bundle.\n */\n beforeEach?: HookRegistrar\n}\n\nexport interface CypressWorld<W> {\n /** The current scenario's bag. Read it inside a step, never at module level. */\n world(): Partial<W>\n}\n\nfunction isHookRegistrar(value: unknown): value is HookRegistrar {\n return typeof value === 'function'\n}\n\nfunction globalBeforeEach(): HookRegistrar {\n const hook: unknown = Reflect.get(globalThis, 'beforeEach')\n if (!isHookRegistrar(hook)) {\n throw new Error(\n 'harnessed: cypressWorld() found no global beforeEach. Call it at the top level of a ' +\n 'step definitions file, which Cypress bundles into the spec, or pass { beforeEach }.',\n )\n }\n return hook\n}\n\n/**\n * A scenario-scoped world for @badeball/cypress-cucumber-preprocessor.\n *\n * Every scenario in a feature runs in the same spec bundle, so a module-level\n * `let` in a step file carries the last scenario's pages into the next one. This\n * registers a `beforeEach` that starts each scenario with an empty world; steps\n * run as Cypress commands, in order, so each one sees what the steps before it\n * left there. Steps reach the world through `world()`, never through `this`.\n *\n * Harness calls are promises, so a step runs them inside the Cypress driver's\n * bridge (`cy.harnessEnv`, `cy.harness`) — one command each:\n *\n * ```ts\n * import { defineParameterType, When } from '@badeball/cypress-cucumber-preprocessor'\n *\n * const pages = definePages({ checkout: CheckoutPage })\n * defineParameterType(pageParameter(pages))\n * const { world } = cypressWorld<{ checkout: CheckoutPage }>()\n *\n * When('I open the {page} page', (name: 'checkout') => {\n * cy.harnessEnv(async env => {\n * const page = pages.open(name, env)\n * await page.goto()\n * world().checkout = page\n * })\n * })\n * ```\n *\n * Call it once, at the top level of a step definitions file, and share the\n * `world` it returns: each call is a separate world.\n */\nexport function cypressWorld<W>(options: CypressWorldOptions = {}): CypressWorld<W> {\n const beforeEach = options.beforeEach ?? globalBeforeEach()\n let current: Partial<W> | undefined\n beforeEach(() => {\n current = createWorld<W>()\n })\n return {\n world() {\n if (current === undefined) {\n throw new Error(\n 'harnessed: no scenario is running yet, so there is no world. Call world() inside ' +\n 'a step, not at the top level of a step definitions file.',\n )\n }\n return current\n },\n }\n}\n"]}
package/dist/index.cjs ADDED
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ function definePages(pages) {
5
+ const names = Object.keys(pages).sort((a, b) => b.length - a.length);
6
+ if (names.length === 0) {
7
+ throw new Error("harnessed: definePages() needs at least one page.");
8
+ }
9
+ const has = (name) => Object.prototype.hasOwnProperty.call(pages, name);
10
+ return {
11
+ names,
12
+ has,
13
+ open(name, env) {
14
+ if (!has(name)) {
15
+ throw new Error(`harnessed: no page named "${name}". Registered: ${names.join(", ")}.`);
16
+ }
17
+ const Page = pages[name];
18
+ return new Page(env);
19
+ }
20
+ };
21
+ }
22
+ function escape(text) {
23
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
24
+ }
25
+ function pageParameter(registry, options = {}) {
26
+ return {
27
+ name: options.name ?? "page",
28
+ regexp: new RegExp(registry.names.map(escape).join("|")),
29
+ transformer: (match) => {
30
+ if (!registry.has(match)) throw new Error(`harnessed: no page named "${match}".`);
31
+ return match;
32
+ },
33
+ useForSnippets: false
34
+ };
35
+ }
36
+ function createWorld() {
37
+ return {};
38
+ }
39
+
40
+ exports.createWorld = createWorld;
41
+ exports.definePages = definePages;
42
+ exports.pageParameter = pageParameter;
43
+ //# sourceMappingURL=index.cjs.map
44
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AAkCO,SAAS,YAA+B,KAAA,EAA2B;AACxE,EAAA,MAAM,KAAA,GAAS,MAAA,CAAO,IAAA,CAAK,KAAK,CAAA,CAA8B,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,CAAE,MAAA,GAAS,CAAA,CAAE,MAAM,CAAA;AAChG,EAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AAGtB,IAAA,MAAM,IAAI,MAAM,mDAAmD,CAAA;AAAA,EACrE;AACA,EAAA,MAAM,GAAA,GAAM,CAAC,IAAA,KACX,MAAA,CAAO,UAAU,cAAA,CAAe,IAAA,CAAK,OAAO,IAAI,CAAA;AAClD,EAAA,OAAO;AAAA,IACL,KAAA;AAAA,IACA,GAAA;AAAA,IACA,IAAA,CAAK,MAAM,GAAA,EAAK;AAEd,MAAA,IAAI,CAAC,GAAA,CAAI,IAAI,CAAA,EAAG;AACd,QAAA,MAAM,IAAI,MAAM,CAAA,0BAAA,EAA6B,IAAI,kBAAkB,KAAA,CAAM,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA,CAAG,CAAA;AAAA,MACxF;AACA,MAAA,MAAM,IAAA,GAAO,MAAM,IAAI,CAAA;AACvB,MAAA,OAAO,IAAI,KAAK,GAAG,CAAA;AAAA,IACrB;AAAA,GACF;AACF;AAcA,SAAS,OAAO,IAAA,EAAsB;AACpC,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,qBAAA,EAAuB,MAAM,CAAA;AACnD;AAYO,SAAS,aAAA,CACd,QAAA,EACA,OAAA,GAA6B,EAAC,EACa;AAC3C,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,QAAQ,IAAA,IAAQ,MAAA;AAAA,IACtB,MAAA,EAAQ,IAAI,MAAA,CAAO,QAAA,CAAS,KAAA,CAAM,IAAI,MAAM,CAAA,CAAE,IAAA,CAAK,GAAG,CAAC,CAAA;AAAA,IACvD,aAAa,CAAA,KAAA,KAAS;AAGpB,MAAA,IAAI,CAAC,QAAA,CAAS,GAAA,CAAI,KAAK,CAAA,QAAS,IAAI,KAAA,CAAM,CAAA,0BAAA,EAA6B,KAAK,CAAA,EAAA,CAAI,CAAA;AAChF,MAAA,OAAO,KAAA;AAAA,IACT,CAAA;AAAA,IACA,cAAA,EAAgB;AAAA,GAClB;AACF;AAMO,SAAS,WAAA,GAA6B;AAC3C,EAAA,OAAO,EAAC;AACV","file":"index.cjs","sourcesContent":["import type { EnvConfig } from '@harnessed-ts/core'\n\n/**\n * Gherkin support for harnessed: what every runner needs, with no runner in it.\n *\n * - A **world** — the scenario-scoped bag where steps keep the pages a scenario\n * builds up. Steps are separate functions, so the obvious place for a harness\n * is a module-level `let`, which leaks into the next scenario the moment a\n * runner reuses a worker. Each runner adapter gives the bag the right lifetime.\n * - A **page registry** — the pages a feature can name, so a step reads\n * `Given I open the checkout page` and gets a `CheckoutPage`, typed.\n * - A **`{page}` parameter type** built from the registry, so a feature naming\n * an unknown page fails at step matching, where the typo is, rather than deep\n * inside a step.\n *\n * Step bodies stay hand-written: the feature's wording is the team's, not a\n * mirror of method names.\n */\n\n/** Anything a page registry can construct: a page or component harness class. */\nexport type PageConstructor<T = object> = new (env: EnvConfig) => T\n\nexport type PageMap = Record<string, PageConstructor>\n\n/** The pages a feature can name. */\nexport interface PageRegistry<P extends PageMap> {\n /** Every registered name, longest first — the order a matcher must try them. */\n readonly names: ReadonlyArray<keyof P & string>\n /** Whether `name` is registered: narrows a string from a step to a page name. */\n has(name: string): name is keyof P & string\n /** Constructs the named page with the scenario's env. */\n open<N extends keyof P & string>(name: N, env: EnvConfig): InstanceType<P[N]>\n}\n\nexport function definePages<P extends PageMap>(pages: P): PageRegistry<P> {\n const names = (Object.keys(pages) as Array<keyof P & string>).sort((a, b) => b.length - a.length)\n if (names.length === 0) {\n // `{page}` is an alternation of the names; with none it is the empty\n // pattern, which matches anywhere and would let any step through.\n throw new Error('harnessed: definePages() needs at least one page.')\n }\n const has = (name: string): name is keyof P & string =>\n Object.prototype.hasOwnProperty.call(pages, name)\n return {\n names,\n has,\n open(name, env) {\n // Own names only: `constructor` and the rest of Object.prototype are not pages.\n if (!has(name)) {\n throw new Error(`harnessed: no page named \"${name}\". Registered: ${names.join(', ')}.`)\n }\n const Page = pages[name]\n return new Page(env) as InstanceType<P[typeof name]>\n },\n }\n}\n\n/**\n * A cucumber-expression parameter type, in the shape every runner's\n * `defineParameterType` takes: @cucumber/cucumber, playwright-bdd and\n * @badeball/cypress-cucumber-preprocessor alike.\n */\nexport interface ParameterTypeDefinition<T> {\n name: string\n regexp: RegExp\n transformer: (match: string) => T\n useForSnippets?: boolean\n}\n\nfunction escape(text: string): string {\n return text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * `{page}` for step patterns: matches exactly the registered names (longest\n * first, so `checkout summary` wins over `checkout`) and hands the step the\n * name, typed. Name it something else with `{ name }`.\n *\n * ```ts\n * defineParameterType(pageParameter(pages))\n * Given('I open the {page} page', async function (name) { … pages.open(name, env) })\n * ```\n */\nexport function pageParameter<P extends PageMap>(\n registry: PageRegistry<P>,\n options: { name?: string } = {},\n): ParameterTypeDefinition<keyof P & string> {\n return {\n name: options.name ?? 'page',\n regexp: new RegExp(registry.names.map(escape).join('|')),\n transformer: match => {\n // The regexp only admits registered names, so this cannot fail; it is\n // here so a hand-built pattern that bypasses it still gets a clear error.\n if (!registry.has(match)) throw new Error(`harnessed: no page named \"${match}\".`)\n return match\n },\n useForSnippets: false,\n }\n}\n\n/**\n * A fresh, empty world. Adapters call this once per scenario; it exists so the\n * bag's type is written once, where the steps declare it.\n */\nexport function createWorld<W>(): Partial<W> {\n return {}\n}\n"]}
@@ -0,0 +1,60 @@
1
+ import type { EnvConfig } from '@harnessed-ts/core';
2
+ /**
3
+ * Gherkin support for harnessed: what every runner needs, with no runner in it.
4
+ *
5
+ * - A **world** — the scenario-scoped bag where steps keep the pages a scenario
6
+ * builds up. Steps are separate functions, so the obvious place for a harness
7
+ * is a module-level `let`, which leaks into the next scenario the moment a
8
+ * runner reuses a worker. Each runner adapter gives the bag the right lifetime.
9
+ * - A **page registry** — the pages a feature can name, so a step reads
10
+ * `Given I open the checkout page` and gets a `CheckoutPage`, typed.
11
+ * - A **`{page}` parameter type** built from the registry, so a feature naming
12
+ * an unknown page fails at step matching, where the typo is, rather than deep
13
+ * inside a step.
14
+ *
15
+ * Step bodies stay hand-written: the feature's wording is the team's, not a
16
+ * mirror of method names.
17
+ */
18
+ /** Anything a page registry can construct: a page or component harness class. */
19
+ export type PageConstructor<T = object> = new (env: EnvConfig) => T;
20
+ export type PageMap = Record<string, PageConstructor>;
21
+ /** The pages a feature can name. */
22
+ export interface PageRegistry<P extends PageMap> {
23
+ /** Every registered name, longest first — the order a matcher must try them. */
24
+ readonly names: ReadonlyArray<keyof P & string>;
25
+ /** Whether `name` is registered: narrows a string from a step to a page name. */
26
+ has(name: string): name is keyof P & string;
27
+ /** Constructs the named page with the scenario's env. */
28
+ open<N extends keyof P & string>(name: N, env: EnvConfig): InstanceType<P[N]>;
29
+ }
30
+ export declare function definePages<P extends PageMap>(pages: P): PageRegistry<P>;
31
+ /**
32
+ * A cucumber-expression parameter type, in the shape every runner's
33
+ * `defineParameterType` takes: @cucumber/cucumber, playwright-bdd and
34
+ * @badeball/cypress-cucumber-preprocessor alike.
35
+ */
36
+ export interface ParameterTypeDefinition<T> {
37
+ name: string;
38
+ regexp: RegExp;
39
+ transformer: (match: string) => T;
40
+ useForSnippets?: boolean;
41
+ }
42
+ /**
43
+ * `{page}` for step patterns: matches exactly the registered names (longest
44
+ * first, so `checkout summary` wins over `checkout`) and hands the step the
45
+ * name, typed. Name it something else with `{ name }`.
46
+ *
47
+ * ```ts
48
+ * defineParameterType(pageParameter(pages))
49
+ * Given('I open the {page} page', async function (name) { … pages.open(name, env) })
50
+ * ```
51
+ */
52
+ export declare function pageParameter<P extends PageMap>(registry: PageRegistry<P>, options?: {
53
+ name?: string;
54
+ }): ParameterTypeDefinition<keyof P & string>;
55
+ /**
56
+ * A fresh, empty world. Adapters call this once per scenario; it exists so the
57
+ * bag's type is written once, where the steps declare it.
58
+ */
59
+ export declare function createWorld<W>(): Partial<W>;
60
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAEnD;;;;;;;;;;;;;;;GAeG;AAEH,iFAAiF;AACjF,MAAM,MAAM,eAAe,CAAC,CAAC,GAAG,MAAM,IAAI,KAAK,GAAG,EAAE,SAAS,KAAK,CAAC,CAAA;AAEnE,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;AAErD,oCAAoC;AACpC,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,OAAO;IAC7C,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAA;IAC/C,iFAAiF;IACjF,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,MAAM,CAAC,GAAG,MAAM,CAAA;IAC3C,yDAAyD;IACzD,IAAI,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,SAAS,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;CAC9E;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,OAAO,EAAE,KAAK,EAAE,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAqBxE;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAuB,CAAC,CAAC;IACxC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,CAAC,CAAA;IACjC,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAMD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,OAAO,EAC7C,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,EACzB,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9B,uBAAuB,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,CAY3C;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAE3C"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createWorld, definePages, pageParameter } from './chunk-6AIO5EWN.js';
2
+ //# sourceMappingURL=index.js.map
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ function createWorld() {
5
+ return {};
6
+ }
7
+
8
+ // src/playwright-bdd.ts
9
+ function withWorld(base) {
10
+ const extended = base.extend({
11
+ // Playwright requires the object-destructuring pattern here: it reads the
12
+ // destructured names to work out what this fixture depends on, and rejects a
13
+ // plain parameter outright. Empty means it depends on nothing.
14
+ world: async ({}, use) => {
15
+ await use(createWorld());
16
+ }
17
+ });
18
+ return extended;
19
+ }
20
+
21
+ exports.withWorld = withWorld;
22
+ //# sourceMappingURL=playwright-bdd.cjs.map
23
+ //# sourceMappingURL=playwright-bdd.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/playwright-bdd.ts"],"names":[],"mappings":";;;AAwGO,SAAS,WAAA,GAA6B;AAC3C,EAAA,OAAO,EAAC;AACV;;;AC7DO,SAAS,UAAgB,IAAA,EAA0B;AACxD,EAAA,MAAM,QAAA,GAAY,KAAqB,MAAA,CAAwB;AAAA;AAAA;AAAA;AAAA,IAI7D,KAAA,EAAO,OAAO,EAAC,EAAG,GAAA,KAA8C;AAC9D,MAAA,MAAM,GAAA,CAAI,aAAgB,CAAA;AAAA,IAC5B;AAAA,GACD,CAAA;AACD,EAAA,OAAO,QAAA;AACT","file":"playwright-bdd.cjs","sourcesContent":["import type { EnvConfig } from '@harnessed-ts/core'\n\n/**\n * Gherkin support for harnessed: what every runner needs, with no runner in it.\n *\n * - A **world** — the scenario-scoped bag where steps keep the pages a scenario\n * builds up. Steps are separate functions, so the obvious place for a harness\n * is a module-level `let`, which leaks into the next scenario the moment a\n * runner reuses a worker. Each runner adapter gives the bag the right lifetime.\n * - A **page registry** — the pages a feature can name, so a step reads\n * `Given I open the checkout page` and gets a `CheckoutPage`, typed.\n * - A **`{page}` parameter type** built from the registry, so a feature naming\n * an unknown page fails at step matching, where the typo is, rather than deep\n * inside a step.\n *\n * Step bodies stay hand-written: the feature's wording is the team's, not a\n * mirror of method names.\n */\n\n/** Anything a page registry can construct: a page or component harness class. */\nexport type PageConstructor<T = object> = new (env: EnvConfig) => T\n\nexport type PageMap = Record<string, PageConstructor>\n\n/** The pages a feature can name. */\nexport interface PageRegistry<P extends PageMap> {\n /** Every registered name, longest first — the order a matcher must try them. */\n readonly names: ReadonlyArray<keyof P & string>\n /** Whether `name` is registered: narrows a string from a step to a page name. */\n has(name: string): name is keyof P & string\n /** Constructs the named page with the scenario's env. */\n open<N extends keyof P & string>(name: N, env: EnvConfig): InstanceType<P[N]>\n}\n\nexport function definePages<P extends PageMap>(pages: P): PageRegistry<P> {\n const names = (Object.keys(pages) as Array<keyof P & string>).sort((a, b) => b.length - a.length)\n if (names.length === 0) {\n // `{page}` is an alternation of the names; with none it is the empty\n // pattern, which matches anywhere and would let any step through.\n throw new Error('harnessed: definePages() needs at least one page.')\n }\n const has = (name: string): name is keyof P & string =>\n Object.prototype.hasOwnProperty.call(pages, name)\n return {\n names,\n has,\n open(name, env) {\n // Own names only: `constructor` and the rest of Object.prototype are not pages.\n if (!has(name)) {\n throw new Error(`harnessed: no page named \"${name}\". Registered: ${names.join(', ')}.`)\n }\n const Page = pages[name]\n return new Page(env) as InstanceType<P[typeof name]>\n },\n }\n}\n\n/**\n * A cucumber-expression parameter type, in the shape every runner's\n * `defineParameterType` takes: @cucumber/cucumber, playwright-bdd and\n * @badeball/cypress-cucumber-preprocessor alike.\n */\nexport interface ParameterTypeDefinition<T> {\n name: string\n regexp: RegExp\n transformer: (match: string) => T\n useForSnippets?: boolean\n}\n\nfunction escape(text: string): string {\n return text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * `{page}` for step patterns: matches exactly the registered names (longest\n * first, so `checkout summary` wins over `checkout`) and hands the step the\n * name, typed. Name it something else with `{ name }`.\n *\n * ```ts\n * defineParameterType(pageParameter(pages))\n * Given('I open the {page} page', async function (name) { … pages.open(name, env) })\n * ```\n */\nexport function pageParameter<P extends PageMap>(\n registry: PageRegistry<P>,\n options: { name?: string } = {},\n): ParameterTypeDefinition<keyof P & string> {\n return {\n name: options.name ?? 'page',\n regexp: new RegExp(registry.names.map(escape).join('|')),\n transformer: match => {\n // The regexp only admits registered names, so this cannot fail; it is\n // here so a hand-built pattern that bypasses it still gets a clear error.\n if (!registry.has(match)) throw new Error(`harnessed: no page named \"${match}\".`)\n return match\n },\n useForSnippets: false,\n }\n}\n\n/**\n * A fresh, empty world. Adapters call this once per scenario; it exists so the\n * bag's type is written once, where the steps declare it.\n */\nexport function createWorld<W>(): Partial<W> {\n return {}\n}\n","import type { TestType } from '@playwright/test'\nimport { createWorld } from './index'\n\n/**\n * A scenario-scoped bag for the pages a Gherkin scenario builds up.\n *\n * Steps are separate functions, so the obvious place to put a harness is a\n * module-level `let` — which breaks the moment Playwright reuses a worker,\n * because the next scenario inherits the last one's state. A fixture is\n * per-scenario, so it cannot leak.\n */\nexport interface WorldFixture<W> {\n world: Partial<W>\n}\n\n/**\n * A structural stand-in for \"some Playwright test object\", used to call `extend`.\n *\n * Deliberately not used as a bound on `withWorld`'s `T`. Playwright's real\n * `TestType` is not assignable to this one — its concrete fixture args do not\n * satisfy an index-signature record — so constraining `T` rejects the very thing\n * callers pass. The conditional return type is what gives misuse a readable\n * error instead.\n */\ntype AnyTestType = TestType<Record<string, unknown>, Record<string, unknown>>\n\n/**\n * The base test type with the world fixture added. Written as a conditional so\n * the base's own fixtures survive — returning the base type unchanged would make\n * `world` invisible to every step that destructures it.\n */\nexport type WithWorld<T, W> =\n T extends TestType<infer TestArgs, infer WorkerArgs>\n ? TestType<TestArgs & WorldFixture<W>, WorkerArgs>\n : never\n\n/**\n * Adds a `world` fixture to any Playwright-shaped `test` — plain\n * `@playwright/test` or playwright-bdd's.\n *\n * ```ts\n * export const test = withWorld<World, typeof apiTest>(apiTest)\n * const { Given, When, Then } = createBdd(test)\n * ```\n */\nexport function withWorld<W, T>(base: T): WithWorld<T, W> {\n const extended = (base as AnyTestType).extend<WorldFixture<W>>({\n // Playwright requires the object-destructuring pattern here: it reads the\n // destructured names to work out what this fixture depends on, and rejects a\n // plain parameter outright. Empty means it depends on nothing.\n world: async ({}, use: (value: Partial<W>) => Promise<void>) => {\n await use(createWorld<W>())\n },\n })\n return extended as unknown as WithWorld<T, W>\n}\n"]}
@@ -0,0 +1,29 @@
1
+ import type { TestType } from '@playwright/test';
2
+ /**
3
+ * A scenario-scoped bag for the pages a Gherkin scenario builds up.
4
+ *
5
+ * Steps are separate functions, so the obvious place to put a harness is a
6
+ * module-level `let` — which breaks the moment Playwright reuses a worker,
7
+ * because the next scenario inherits the last one's state. A fixture is
8
+ * per-scenario, so it cannot leak.
9
+ */
10
+ export interface WorldFixture<W> {
11
+ world: Partial<W>;
12
+ }
13
+ /**
14
+ * The base test type with the world fixture added. Written as a conditional so
15
+ * the base's own fixtures survive — returning the base type unchanged would make
16
+ * `world` invisible to every step that destructures it.
17
+ */
18
+ export type WithWorld<T, W> = T extends TestType<infer TestArgs, infer WorkerArgs> ? TestType<TestArgs & WorldFixture<W>, WorkerArgs> : never;
19
+ /**
20
+ * Adds a `world` fixture to any Playwright-shaped `test` — plain
21
+ * `@playwright/test` or playwright-bdd's.
22
+ *
23
+ * ```ts
24
+ * export const test = withWorld<World, typeof apiTest>(apiTest)
25
+ * const { Given, When, Then } = createBdd(test)
26
+ * ```
27
+ */
28
+ export declare function withWorld<W, T>(base: T): WithWorld<T, W>;
29
+ //# sourceMappingURL=playwright-bdd.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright-bdd.d.ts","sourceRoot":"","sources":["../src/playwright-bdd.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAGhD;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;CAClB;AAaD;;;;GAIG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,EAAE,CAAC,IACxB,CAAC,SAAS,QAAQ,CAAC,MAAM,QAAQ,EAAE,MAAM,UAAU,CAAC,GAChD,QAAQ,CAAC,QAAQ,GAAG,YAAY,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,GAChD,KAAK,CAAA;AAEX;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAUxD"}
@@ -0,0 +1,18 @@
1
+ import { createWorld } from './chunk-6AIO5EWN.js';
2
+
3
+ // src/playwright-bdd.ts
4
+ function withWorld(base) {
5
+ const extended = base.extend({
6
+ // Playwright requires the object-destructuring pattern here: it reads the
7
+ // destructured names to work out what this fixture depends on, and rejects a
8
+ // plain parameter outright. Empty means it depends on nothing.
9
+ world: async ({}, use) => {
10
+ await use(createWorld());
11
+ }
12
+ });
13
+ return extended;
14
+ }
15
+
16
+ export { withWorld };
17
+ //# sourceMappingURL=playwright-bdd.js.map
18
+ //# sourceMappingURL=playwright-bdd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/playwright-bdd.ts"],"names":[],"mappings":";;;AA6CO,SAAS,UAAgB,IAAA,EAA0B;AACxD,EAAA,MAAM,QAAA,GAAY,KAAqB,MAAA,CAAwB;AAAA;AAAA;AAAA;AAAA,IAI7D,KAAA,EAAO,OAAO,EAAC,EAAG,GAAA,KAA8C;AAC9D,MAAA,MAAM,GAAA,CAAI,aAAgB,CAAA;AAAA,IAC5B;AAAA,GACD,CAAA;AACD,EAAA,OAAO,QAAA;AACT","file":"playwright-bdd.js","sourcesContent":["import type { TestType } from '@playwright/test'\nimport { createWorld } from './index'\n\n/**\n * A scenario-scoped bag for the pages a Gherkin scenario builds up.\n *\n * Steps are separate functions, so the obvious place to put a harness is a\n * module-level `let` — which breaks the moment Playwright reuses a worker,\n * because the next scenario inherits the last one's state. A fixture is\n * per-scenario, so it cannot leak.\n */\nexport interface WorldFixture<W> {\n world: Partial<W>\n}\n\n/**\n * A structural stand-in for \"some Playwright test object\", used to call `extend`.\n *\n * Deliberately not used as a bound on `withWorld`'s `T`. Playwright's real\n * `TestType` is not assignable to this one — its concrete fixture args do not\n * satisfy an index-signature record — so constraining `T` rejects the very thing\n * callers pass. The conditional return type is what gives misuse a readable\n * error instead.\n */\ntype AnyTestType = TestType<Record<string, unknown>, Record<string, unknown>>\n\n/**\n * The base test type with the world fixture added. Written as a conditional so\n * the base's own fixtures survive — returning the base type unchanged would make\n * `world` invisible to every step that destructures it.\n */\nexport type WithWorld<T, W> =\n T extends TestType<infer TestArgs, infer WorkerArgs>\n ? TestType<TestArgs & WorldFixture<W>, WorkerArgs>\n : never\n\n/**\n * Adds a `world` fixture to any Playwright-shaped `test` — plain\n * `@playwright/test` or playwright-bdd's.\n *\n * ```ts\n * export const test = withWorld<World, typeof apiTest>(apiTest)\n * const { Given, When, Then } = createBdd(test)\n * ```\n */\nexport function withWorld<W, T>(base: T): WithWorld<T, W> {\n const extended = (base as AnyTestType).extend<WorldFixture<W>>({\n // Playwright requires the object-destructuring pattern here: it reads the\n // destructured names to work out what this fixture depends on, and rejects a\n // plain parameter outright. Empty means it depends on nothing.\n world: async ({}, use: (value: Partial<W>) => Promise<void>) => {\n await use(createWorld<W>())\n },\n })\n return extended as unknown as WithWorld<T, W>\n}\n"]}
package/dist/yadda.cjs ADDED
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ var Yadda = require('yadda');
4
+
5
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
6
+
7
+ var Yadda__default = /*#__PURE__*/_interopDefault(Yadda);
8
+
9
+ // src/yadda.ts
10
+
11
+ // src/index.ts
12
+ function escape(text) {
13
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14
+ }
15
+ function pageParameter(registry, options = {}) {
16
+ return {
17
+ name: options.name ?? "page",
18
+ regexp: new RegExp(registry.names.map(escape).join("|")),
19
+ transformer: (match) => {
20
+ if (!registry.has(match)) throw new Error(`harnessed: no page named "${match}".`);
21
+ return match;
22
+ },
23
+ useForSnippets: false
24
+ };
25
+ }
26
+ function createWorld() {
27
+ return {};
28
+ }
29
+
30
+ // src/yadda.ts
31
+ var extended = /* @__PURE__ */ new WeakMap();
32
+ function pageDictionary(registry, options = {}) {
33
+ const { regexp, transformer } = pageParameter(registry);
34
+ const term = options.term ?? "page";
35
+ const dictionary = options.dictionary ?? new Yadda__default.default.Dictionary();
36
+ const pattern = `(${regexp.source})`;
37
+ const terms = extended.get(dictionary) ?? /* @__PURE__ */ new Map();
38
+ extended.set(dictionary, terms);
39
+ const existing = terms.get(term);
40
+ if (existing === pattern) return dictionary;
41
+ if (existing !== void 0) {
42
+ throw new Error(
43
+ `harnessed: this dictionary already defines $${term} with other pages. Give the second registry its own term.`
44
+ );
45
+ }
46
+ const convert = (match, next, ..._unused) => {
47
+ if (typeof match !== "string" || typeof next !== "function") {
48
+ throw new Error(`harnessed: $${term} expects one capture and Yadda's callback.`);
49
+ }
50
+ let page;
51
+ try {
52
+ page = transformer(match);
53
+ } catch (error) {
54
+ next(error);
55
+ return;
56
+ }
57
+ next(null, page);
58
+ };
59
+ dictionary.define(term, pattern, convert);
60
+ terms.set(term, pattern);
61
+ return dictionary;
62
+ }
63
+ function yaddaWorld() {
64
+ return {
65
+ steps(options) {
66
+ return createSteps(options);
67
+ }
68
+ };
69
+ }
70
+ function createSteps(options) {
71
+ const { pages, env } = options;
72
+ const language = options.language ?? Yadda__default.default.localisation.English;
73
+ const library = language.localise(
74
+ new Yadda__default.default.ContextParamLibrary(options.dictionary ?? pageDictionary(pages))
75
+ );
76
+ const world = createWorld();
77
+ const open = (name) => pages.open(name, env());
78
+ const wrap = (step) => async (context, ...rest) => {
79
+ await step({ world, open, context }, ...rest.slice(0, -1));
80
+ };
81
+ const promise = { mode: "promise" };
82
+ const anyKeyword = `(?:${["given", "when", "then"].map((keyword) => language.translate(keyword)).join("|")})`;
83
+ return {
84
+ world,
85
+ library,
86
+ given(signature, step) {
87
+ library.given(signature, wrap(step), void 0, promise);
88
+ return this;
89
+ },
90
+ when(signature, step) {
91
+ library.when(signature, wrap(step), void 0, promise);
92
+ return this;
93
+ },
94
+ then(signature, step) {
95
+ library.then(signature, wrap(step), void 0, promise);
96
+ return this;
97
+ },
98
+ step(signature, step) {
99
+ const signatures = Array.isArray(signature) ? signature : [signature];
100
+ library.define(
101
+ signatures.map((one) => withKeyword(anyKeyword, one)),
102
+ wrap(step),
103
+ void 0,
104
+ promise
105
+ );
106
+ return this;
107
+ }
108
+ };
109
+ }
110
+ function withKeyword(keyword, signature) {
111
+ const source = signature instanceof RegExp ? signature.source : signature;
112
+ return `^(?:\\s)*${keyword}\\s+${source.replace(/^\^/, "")}`;
113
+ }
114
+
115
+ exports.pageDictionary = pageDictionary;
116
+ exports.yaddaWorld = yaddaWorld;
117
+ //# sourceMappingURL=yadda.cjs.map
118
+ //# sourceMappingURL=yadda.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/yadda.ts"],"names":["Yadda"],"mappings":"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","file":"yadda.cjs","sourcesContent":["import type { EnvConfig } from '@harnessed-ts/core'\n\n/**\n * Gherkin support for harnessed: what every runner needs, with no runner in it.\n *\n * - A **world** — the scenario-scoped bag where steps keep the pages a scenario\n * builds up. Steps are separate functions, so the obvious place for a harness\n * is a module-level `let`, which leaks into the next scenario the moment a\n * runner reuses a worker. Each runner adapter gives the bag the right lifetime.\n * - A **page registry** — the pages a feature can name, so a step reads\n * `Given I open the checkout page` and gets a `CheckoutPage`, typed.\n * - A **`{page}` parameter type** built from the registry, so a feature naming\n * an unknown page fails at step matching, where the typo is, rather than deep\n * inside a step.\n *\n * Step bodies stay hand-written: the feature's wording is the team's, not a\n * mirror of method names.\n */\n\n/** Anything a page registry can construct: a page or component harness class. */\nexport type PageConstructor<T = object> = new (env: EnvConfig) => T\n\nexport type PageMap = Record<string, PageConstructor>\n\n/** The pages a feature can name. */\nexport interface PageRegistry<P extends PageMap> {\n /** Every registered name, longest first — the order a matcher must try them. */\n readonly names: ReadonlyArray<keyof P & string>\n /** Whether `name` is registered: narrows a string from a step to a page name. */\n has(name: string): name is keyof P & string\n /** Constructs the named page with the scenario's env. */\n open<N extends keyof P & string>(name: N, env: EnvConfig): InstanceType<P[N]>\n}\n\nexport function definePages<P extends PageMap>(pages: P): PageRegistry<P> {\n const names = (Object.keys(pages) as Array<keyof P & string>).sort((a, b) => b.length - a.length)\n if (names.length === 0) {\n // `{page}` is an alternation of the names; with none it is the empty\n // pattern, which matches anywhere and would let any step through.\n throw new Error('harnessed: definePages() needs at least one page.')\n }\n const has = (name: string): name is keyof P & string =>\n Object.prototype.hasOwnProperty.call(pages, name)\n return {\n names,\n has,\n open(name, env) {\n // Own names only: `constructor` and the rest of Object.prototype are not pages.\n if (!has(name)) {\n throw new Error(`harnessed: no page named \"${name}\". Registered: ${names.join(', ')}.`)\n }\n const Page = pages[name]\n return new Page(env) as InstanceType<P[typeof name]>\n },\n }\n}\n\n/**\n * A cucumber-expression parameter type, in the shape every runner's\n * `defineParameterType` takes: @cucumber/cucumber, playwright-bdd and\n * @badeball/cypress-cucumber-preprocessor alike.\n */\nexport interface ParameterTypeDefinition<T> {\n name: string\n regexp: RegExp\n transformer: (match: string) => T\n useForSnippets?: boolean\n}\n\nfunction escape(text: string): string {\n return text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * `{page}` for step patterns: matches exactly the registered names (longest\n * first, so `checkout summary` wins over `checkout`) and hands the step the\n * name, typed. Name it something else with `{ name }`.\n *\n * ```ts\n * defineParameterType(pageParameter(pages))\n * Given('I open the {page} page', async function (name) { … pages.open(name, env) })\n * ```\n */\nexport function pageParameter<P extends PageMap>(\n registry: PageRegistry<P>,\n options: { name?: string } = {},\n): ParameterTypeDefinition<keyof P & string> {\n return {\n name: options.name ?? 'page',\n regexp: new RegExp(registry.names.map(escape).join('|')),\n transformer: match => {\n // The regexp only admits registered names, so this cannot fail; it is\n // here so a hand-built pattern that bypasses it still gets a clear error.\n if (!registry.has(match)) throw new Error(`harnessed: no page named \"${match}\".`)\n return match\n },\n useForSnippets: false,\n }\n}\n\n/**\n * A fresh, empty world. Adapters call this once per scenario; it exists so the\n * bag's type is written once, where the steps declare it.\n */\nexport function createWorld<W>(): Partial<W> {\n return {}\n}\n","import type { EnvConfig } from '@harnessed-ts/core'\nimport Yadda from 'yadda'\nimport { createWorld, pageParameter } from './index'\nimport type { PageMap, PageRegistry } from './index'\n\n/**\n * The page terms each dictionary was given, and the pattern each one matches.\n * Yadda refuses to define a term twice, and ember-cli-yadda calls a steps\n * module's default export once per scenario, so extending a module-level\n * dictionary there would throw from the second scenario on. Copying it instead\n * is no answer: Yadda's `merge` drops converters.\n */\nconst extended = new WeakMap<Yadda.Dictionary, Map<string, string>>()\n\n/**\n * A Yadda dictionary with a `$page` term built from a page registry — the same\n * names and pattern as `{page}`, so the two cannot drift. A step reading\n * `I open the $page page` matches only a registered name (longest first), and a\n * feature naming an unknown page fails as an undefined step, where the typo is.\n *\n * Pass `dictionary` to add the term to one that already defines others, and\n * `term` to call it something else. Adding the same term to the same dictionary\n * again returns it unchanged, so it is safe inside a per-scenario steps function.\n */\nexport function pageDictionary<P extends PageMap>(\n registry: PageRegistry<P>,\n options: { term?: string; dictionary?: Yadda.Dictionary } = {},\n): Yadda.Dictionary {\n const { regexp, transformer } = pageParameter(registry)\n const term = options.term ?? 'page'\n const dictionary = options.dictionary ?? new Yadda.Dictionary()\n const pattern = `(${regexp.source})`\n const terms = extended.get(dictionary) ?? new Map<string, string>()\n extended.set(dictionary, terms)\n const existing = terms.get(term)\n if (existing === pattern) return dictionary\n if (existing !== undefined) {\n throw new Error(\n `harnessed: this dictionary already defines $${term} with other pages. ` +\n 'Give the second registry its own term.',\n )\n }\n // A callback converter, not an async one: Yadda tells an async converter by\n // its constructor's name, which a transpiler that rewrites async functions (an\n // Ember build's Babel) changes. A callback converter's captures are its arity\n // less the callback, so this names exactly two parameters and checks them;\n // they are optional, and the rest parameter there, only because Yadda types a\n // callback converter as any number of captures followed by the callback.\n const convert = (\n match?: string | Yadda.ConverterCallback,\n next?: string | Yadda.ConverterCallback,\n ..._unused: Array<string | Yadda.ConverterCallback>\n ): void => {\n if (typeof match !== 'string' || typeof next !== 'function') {\n throw new Error(`harnessed: $${term} expects one capture and Yadda's callback.`)\n }\n let page: keyof P & string\n try {\n page = transformer(match)\n } catch (error) {\n next(error)\n return\n }\n next(null, page)\n }\n dictionary.define(term, pattern, convert)\n terms.set(term, pattern)\n return dictionary\n}\n\n/** What a harnessed Yadda step receives first. */\nexport interface YaddaScenario<W, P extends PageMap> {\n /** The scenario's bag: empty when the scenario starts, shared by its steps. */\n readonly world: Partial<W>\n /** Constructs a registered page with the scenario's env. */\n open<N extends keyof P & string>(name: N): InstanceType<P[N]>\n /**\n * Yadda's own context for the step: `step` (the line being run) and whatever\n * the runner passed in — ember-cli-yadda's `ctx`, and the QUnit test context.\n */\n readonly context: Readonly<Record<string, unknown>>\n}\n\n/** A step: the scenario first, then the values its signature captured. */\nexport type YaddaStep<W, P extends PageMap, A extends unknown[]> = (\n scenario: YaddaScenario<W, P>,\n ...args: A\n) => unknown\n\nexport interface YaddaStepsOptions<P extends PageMap> {\n /** The pages features can name. Defines `$page` unless `dictionary` is given. */\n pages: PageRegistry<P>\n /**\n * The env pages are built with. Called each time a step opens a page, so it\n * can read per-test state — `() => ember()` in an application test.\n */\n env: () => EnvConfig\n /** The dictionary signatures expand against. Defaults to `pageDictionary(pages)`. */\n dictionary?: Yadda.Dictionary\n /** The step keywords' language. Defaults to English. */\n language?: Yadda.Language\n}\n\n/** A step library whose steps share one world. */\nexport interface YaddaSteps<W, P extends PageMap> {\n /** The world this library's steps share. */\n readonly world: Partial<W>\n /** The Yadda library to run: what an ember-cli-yadda steps module returns. */\n readonly library: Yadda.BaseLibrary\n given<A extends unknown[]>(signature: Yadda.Signature, step: YaddaStep<W, P, A>): this\n when<A extends unknown[]>(signature: Yadda.Signature, step: YaddaStep<W, P, A>): this\n then<A extends unknown[]>(signature: Yadda.Signature, step: YaddaStep<W, P, A>): this\n /** A step that matches after any keyword: `Given`, `When`, `Then`, `And`, `But`. */\n step<A extends unknown[]>(signature: Yadda.Signature, step: YaddaStep<W, P, A>): this\n}\n\n/** Fixes the world's type; `steps` then infers the pages from the registry. */\nexport interface YaddaWorld<W> {\n /** A fresh step library with a fresh, empty world. Build one per scenario. */\n steps<P extends PageMap>(options: YaddaStepsOptions<P>): YaddaSteps<W, P>\n}\n\n/**\n * Yadda steps with a scenario-scoped world and the page registry. Steps are\n * plain (arrow) functions taking the scenario first, then what their signature\n * captured; every step is awaited.\n *\n * The world belongs to the library `steps()` builds, so build one per scenario —\n * which is what ember-cli-yadda already does: it calls a steps module's default\n * export inside each scenario's test.\n *\n * ```ts\n * // tests/acceptance/steps/checkout-steps.ts\n * const pages = definePages({ checkout: CheckoutPage })\n * const checkout = yaddaWorld<{ checkout: CheckoutPage }>()\n *\n * export default function (assert: Assert) {\n * const steps = checkout.steps({ pages, env: () => ember() })\n * steps.when('I open the $page page', async ({ world, open }, name: 'checkout') => {\n * world.checkout = open(name)\n * await world.checkout.goto()\n * })\n * return steps.library\n * }\n * ```\n *\n * The world's type is a call of its own because TypeScript cannot take one type\n * argument explicitly and infer another; `steps` infers the pages.\n */\nexport function yaddaWorld<W = Record<string, unknown>>(): YaddaWorld<W> {\n return {\n steps<P extends PageMap>(options: YaddaStepsOptions<P>) {\n return createSteps<W, P>(options)\n },\n }\n}\n\nfunction createSteps<W, P extends PageMap>(options: YaddaStepsOptions<P>): YaddaSteps<W, P> {\n const { pages, env } = options\n const language = options.language ?? Yadda.localisation.English\n const library = language.localise(\n new Yadda.ContextParamLibrary(options.dictionary ?? pageDictionary(pages)),\n )\n const world = createWorld<W>()\n const open = <N extends keyof P & string>(name: N): InstanceType<P[N]> => pages.open(name, env())\n\n // Yadda passes the step's context first and, in promise mode, its completion\n // callback last; a harnessed step sees neither, only the scenario and the\n // captured values. Promise mode is explicit so Yadda never guesses from the\n // wrapper's arity.\n const wrap =\n <A extends unknown[]>(step: YaddaStep<W, P, A>) =>\n async (context: Record<string, unknown>, ...rest: unknown[]): Promise<void> => {\n await step({ world, open, context }, ...(rest.slice(0, -1) as A))\n }\n const promise: Yadda.DefineOptions = { mode: 'promise' }\n\n // Every step keyword the language has, as one alternation: `the world is\n // empty` is the same step after `Given` as after `Then`.\n const anyKeyword = `(?:${['given', 'when', 'then'].map(keyword => language.translate(keyword)).join('|')})`\n\n return {\n world,\n library,\n given(signature, step) {\n library.given(signature, wrap(step), undefined, promise)\n return this\n },\n when(signature, step) {\n library.when(signature, wrap(step), undefined, promise)\n return this\n },\n then(signature, step) {\n library.then(signature, wrap(step), undefined, promise)\n return this\n },\n step(signature, step) {\n const signatures = Array.isArray(signature) ? signature : [signature]\n library.define(\n signatures.map(one => withKeyword(anyKeyword, one)),\n wrap(step),\n undefined,\n promise,\n )\n return this\n },\n }\n}\n\n/** Prefixes a signature with a keyword pattern, the way a Yadda language does. */\nfunction withKeyword(keyword: string, signature: string | RegExp): string {\n const source = signature instanceof RegExp ? signature.source : signature\n return `^(?:\\\\s)*${keyword}\\\\s+${source.replace(/^\\^/, '')}`\n}\n"]}
@@ -0,0 +1,90 @@
1
+ import type { EnvConfig } from '@harnessed-ts/core';
2
+ import Yadda from 'yadda';
3
+ import type { PageMap, PageRegistry } from './index';
4
+ /**
5
+ * A Yadda dictionary with a `$page` term built from a page registry — the same
6
+ * names and pattern as `{page}`, so the two cannot drift. A step reading
7
+ * `I open the $page page` matches only a registered name (longest first), and a
8
+ * feature naming an unknown page fails as an undefined step, where the typo is.
9
+ *
10
+ * Pass `dictionary` to add the term to one that already defines others, and
11
+ * `term` to call it something else. Adding the same term to the same dictionary
12
+ * again returns it unchanged, so it is safe inside a per-scenario steps function.
13
+ */
14
+ export declare function pageDictionary<P extends PageMap>(registry: PageRegistry<P>, options?: {
15
+ term?: string;
16
+ dictionary?: Yadda.Dictionary;
17
+ }): Yadda.Dictionary;
18
+ /** What a harnessed Yadda step receives first. */
19
+ export interface YaddaScenario<W, P extends PageMap> {
20
+ /** The scenario's bag: empty when the scenario starts, shared by its steps. */
21
+ readonly world: Partial<W>;
22
+ /** Constructs a registered page with the scenario's env. */
23
+ open<N extends keyof P & string>(name: N): InstanceType<P[N]>;
24
+ /**
25
+ * Yadda's own context for the step: `step` (the line being run) and whatever
26
+ * the runner passed in — ember-cli-yadda's `ctx`, and the QUnit test context.
27
+ */
28
+ readonly context: Readonly<Record<string, unknown>>;
29
+ }
30
+ /** A step: the scenario first, then the values its signature captured. */
31
+ export type YaddaStep<W, P extends PageMap, A extends unknown[]> = (scenario: YaddaScenario<W, P>, ...args: A) => unknown;
32
+ export interface YaddaStepsOptions<P extends PageMap> {
33
+ /** The pages features can name. Defines `$page` unless `dictionary` is given. */
34
+ pages: PageRegistry<P>;
35
+ /**
36
+ * The env pages are built with. Called each time a step opens a page, so it
37
+ * can read per-test state — `() => ember()` in an application test.
38
+ */
39
+ env: () => EnvConfig;
40
+ /** The dictionary signatures expand against. Defaults to `pageDictionary(pages)`. */
41
+ dictionary?: Yadda.Dictionary;
42
+ /** The step keywords' language. Defaults to English. */
43
+ language?: Yadda.Language;
44
+ }
45
+ /** A step library whose steps share one world. */
46
+ export interface YaddaSteps<W, P extends PageMap> {
47
+ /** The world this library's steps share. */
48
+ readonly world: Partial<W>;
49
+ /** The Yadda library to run: what an ember-cli-yadda steps module returns. */
50
+ readonly library: Yadda.BaseLibrary;
51
+ given<A extends unknown[]>(signature: Yadda.Signature, step: YaddaStep<W, P, A>): this;
52
+ when<A extends unknown[]>(signature: Yadda.Signature, step: YaddaStep<W, P, A>): this;
53
+ then<A extends unknown[]>(signature: Yadda.Signature, step: YaddaStep<W, P, A>): this;
54
+ /** A step that matches after any keyword: `Given`, `When`, `Then`, `And`, `But`. */
55
+ step<A extends unknown[]>(signature: Yadda.Signature, step: YaddaStep<W, P, A>): this;
56
+ }
57
+ /** Fixes the world's type; `steps` then infers the pages from the registry. */
58
+ export interface YaddaWorld<W> {
59
+ /** A fresh step library with a fresh, empty world. Build one per scenario. */
60
+ steps<P extends PageMap>(options: YaddaStepsOptions<P>): YaddaSteps<W, P>;
61
+ }
62
+ /**
63
+ * Yadda steps with a scenario-scoped world and the page registry. Steps are
64
+ * plain (arrow) functions taking the scenario first, then what their signature
65
+ * captured; every step is awaited.
66
+ *
67
+ * The world belongs to the library `steps()` builds, so build one per scenario —
68
+ * which is what ember-cli-yadda already does: it calls a steps module's default
69
+ * export inside each scenario's test.
70
+ *
71
+ * ```ts
72
+ * // tests/acceptance/steps/checkout-steps.ts
73
+ * const pages = definePages({ checkout: CheckoutPage })
74
+ * const checkout = yaddaWorld<{ checkout: CheckoutPage }>()
75
+ *
76
+ * export default function (assert: Assert) {
77
+ * const steps = checkout.steps({ pages, env: () => ember() })
78
+ * steps.when('I open the $page page', async ({ world, open }, name: 'checkout') => {
79
+ * world.checkout = open(name)
80
+ * await world.checkout.goto()
81
+ * })
82
+ * return steps.library
83
+ * }
84
+ * ```
85
+ *
86
+ * The world's type is a call of its own because TypeScript cannot take one type
87
+ * argument explicitly and infer another; `steps` infers the pages.
88
+ */
89
+ export declare function yaddaWorld<W = Record<string, unknown>>(): YaddaWorld<W>;
90
+ //# sourceMappingURL=yadda.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"yadda.d.ts","sourceRoot":"","sources":["../src/yadda.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAWpD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,OAAO,EAC9C,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,EACzB,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,KAAK,CAAC,UAAU,CAAA;CAAO,GAC7D,KAAK,CAAC,UAAU,CAyClB;AAED,kDAAkD;AAClD,MAAM,WAAW,aAAa,CAAC,CAAC,EAAE,CAAC,SAAS,OAAO;IACjD,+EAA+E;IAC/E,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IAC1B,4DAA4D;IAC5D,IAAI,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7D;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CACpD;AAED,0EAA0E;AAC1E,MAAM,MAAM,SAAS,CAAC,CAAC,EAAE,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,OAAO,EAAE,IAAI,CACjE,QAAQ,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,EAC7B,GAAG,IAAI,EAAE,CAAC,KACP,OAAO,CAAA;AAEZ,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS,OAAO;IAClD,iFAAiF;IACjF,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,CAAA;IACtB;;;OAGG;IACH,GAAG,EAAE,MAAM,SAAS,CAAA;IACpB,qFAAqF;IACrF,UAAU,CAAC,EAAE,KAAK,CAAC,UAAU,CAAA;IAC7B,wDAAwD;IACxD,QAAQ,CAAC,EAAE,KAAK,CAAC,QAAQ,CAAA;CAC1B;AAED,kDAAkD;AAClD,MAAM,WAAW,UAAU,CAAC,CAAC,EAAE,CAAC,SAAS,OAAO;IAC9C,4CAA4C;IAC5C,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IAC1B,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,WAAW,CAAA;IACnC,KAAK,CAAC,CAAC,SAAS,OAAO,EAAE,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAA;IACtF,IAAI,CAAC,CAAC,SAAS,OAAO,EAAE,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAA;IACrF,IAAI,CAAC,CAAC,SAAS,OAAO,EAAE,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAA;IACrF,oFAAoF;IACpF,IAAI,CAAC,CAAC,SAAS,OAAO,EAAE,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAA;CACtF;AAED,+EAA+E;AAC/E,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,8EAA8E;IAC9E,KAAK,CAAC,CAAC,SAAS,OAAO,EAAE,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;CAC1E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,UAAU,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,UAAU,CAAC,CAAC,CAAC,CAMvE"}
package/dist/yadda.js ADDED
@@ -0,0 +1,90 @@
1
+ import { pageParameter, createWorld } from './chunk-6AIO5EWN.js';
2
+ import Yadda from 'yadda';
3
+
4
+ var extended = /* @__PURE__ */ new WeakMap();
5
+ function pageDictionary(registry, options = {}) {
6
+ const { regexp, transformer } = pageParameter(registry);
7
+ const term = options.term ?? "page";
8
+ const dictionary = options.dictionary ?? new Yadda.Dictionary();
9
+ const pattern = `(${regexp.source})`;
10
+ const terms = extended.get(dictionary) ?? /* @__PURE__ */ new Map();
11
+ extended.set(dictionary, terms);
12
+ const existing = terms.get(term);
13
+ if (existing === pattern) return dictionary;
14
+ if (existing !== void 0) {
15
+ throw new Error(
16
+ `harnessed: this dictionary already defines $${term} with other pages. Give the second registry its own term.`
17
+ );
18
+ }
19
+ const convert = (match, next, ..._unused) => {
20
+ if (typeof match !== "string" || typeof next !== "function") {
21
+ throw new Error(`harnessed: $${term} expects one capture and Yadda's callback.`);
22
+ }
23
+ let page;
24
+ try {
25
+ page = transformer(match);
26
+ } catch (error) {
27
+ next(error);
28
+ return;
29
+ }
30
+ next(null, page);
31
+ };
32
+ dictionary.define(term, pattern, convert);
33
+ terms.set(term, pattern);
34
+ return dictionary;
35
+ }
36
+ function yaddaWorld() {
37
+ return {
38
+ steps(options) {
39
+ return createSteps(options);
40
+ }
41
+ };
42
+ }
43
+ function createSteps(options) {
44
+ const { pages, env } = options;
45
+ const language = options.language ?? Yadda.localisation.English;
46
+ const library = language.localise(
47
+ new Yadda.ContextParamLibrary(options.dictionary ?? pageDictionary(pages))
48
+ );
49
+ const world = createWorld();
50
+ const open = (name) => pages.open(name, env());
51
+ const wrap = (step) => async (context, ...rest) => {
52
+ await step({ world, open, context }, ...rest.slice(0, -1));
53
+ };
54
+ const promise = { mode: "promise" };
55
+ const anyKeyword = `(?:${["given", "when", "then"].map((keyword) => language.translate(keyword)).join("|")})`;
56
+ return {
57
+ world,
58
+ library,
59
+ given(signature, step) {
60
+ library.given(signature, wrap(step), void 0, promise);
61
+ return this;
62
+ },
63
+ when(signature, step) {
64
+ library.when(signature, wrap(step), void 0, promise);
65
+ return this;
66
+ },
67
+ then(signature, step) {
68
+ library.then(signature, wrap(step), void 0, promise);
69
+ return this;
70
+ },
71
+ step(signature, step) {
72
+ const signatures = Array.isArray(signature) ? signature : [signature];
73
+ library.define(
74
+ signatures.map((one) => withKeyword(anyKeyword, one)),
75
+ wrap(step),
76
+ void 0,
77
+ promise
78
+ );
79
+ return this;
80
+ }
81
+ };
82
+ }
83
+ function withKeyword(keyword, signature) {
84
+ const source = signature instanceof RegExp ? signature.source : signature;
85
+ return `^(?:\\s)*${keyword}\\s+${source.replace(/^\^/, "")}`;
86
+ }
87
+
88
+ export { pageDictionary, yaddaWorld };
89
+ //# sourceMappingURL=yadda.js.map
90
+ //# sourceMappingURL=yadda.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/yadda.ts"],"names":[],"mappings":"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","file":"yadda.js","sourcesContent":["import type { EnvConfig } from '@harnessed-ts/core'\nimport Yadda from 'yadda'\nimport { createWorld, pageParameter } from './index'\nimport type { PageMap, PageRegistry } from './index'\n\n/**\n * The page terms each dictionary was given, and the pattern each one matches.\n * Yadda refuses to define a term twice, and ember-cli-yadda calls a steps\n * module's default export once per scenario, so extending a module-level\n * dictionary there would throw from the second scenario on. Copying it instead\n * is no answer: Yadda's `merge` drops converters.\n */\nconst extended = new WeakMap<Yadda.Dictionary, Map<string, string>>()\n\n/**\n * A Yadda dictionary with a `$page` term built from a page registry — the same\n * names and pattern as `{page}`, so the two cannot drift. A step reading\n * `I open the $page page` matches only a registered name (longest first), and a\n * feature naming an unknown page fails as an undefined step, where the typo is.\n *\n * Pass `dictionary` to add the term to one that already defines others, and\n * `term` to call it something else. Adding the same term to the same dictionary\n * again returns it unchanged, so it is safe inside a per-scenario steps function.\n */\nexport function pageDictionary<P extends PageMap>(\n registry: PageRegistry<P>,\n options: { term?: string; dictionary?: Yadda.Dictionary } = {},\n): Yadda.Dictionary {\n const { regexp, transformer } = pageParameter(registry)\n const term = options.term ?? 'page'\n const dictionary = options.dictionary ?? new Yadda.Dictionary()\n const pattern = `(${regexp.source})`\n const terms = extended.get(dictionary) ?? new Map<string, string>()\n extended.set(dictionary, terms)\n const existing = terms.get(term)\n if (existing === pattern) return dictionary\n if (existing !== undefined) {\n throw new Error(\n `harnessed: this dictionary already defines $${term} with other pages. ` +\n 'Give the second registry its own term.',\n )\n }\n // A callback converter, not an async one: Yadda tells an async converter by\n // its constructor's name, which a transpiler that rewrites async functions (an\n // Ember build's Babel) changes. A callback converter's captures are its arity\n // less the callback, so this names exactly two parameters and checks them;\n // they are optional, and the rest parameter there, only because Yadda types a\n // callback converter as any number of captures followed by the callback.\n const convert = (\n match?: string | Yadda.ConverterCallback,\n next?: string | Yadda.ConverterCallback,\n ..._unused: Array<string | Yadda.ConverterCallback>\n ): void => {\n if (typeof match !== 'string' || typeof next !== 'function') {\n throw new Error(`harnessed: $${term} expects one capture and Yadda's callback.`)\n }\n let page: keyof P & string\n try {\n page = transformer(match)\n } catch (error) {\n next(error)\n return\n }\n next(null, page)\n }\n dictionary.define(term, pattern, convert)\n terms.set(term, pattern)\n return dictionary\n}\n\n/** What a harnessed Yadda step receives first. */\nexport interface YaddaScenario<W, P extends PageMap> {\n /** The scenario's bag: empty when the scenario starts, shared by its steps. */\n readonly world: Partial<W>\n /** Constructs a registered page with the scenario's env. */\n open<N extends keyof P & string>(name: N): InstanceType<P[N]>\n /**\n * Yadda's own context for the step: `step` (the line being run) and whatever\n * the runner passed in — ember-cli-yadda's `ctx`, and the QUnit test context.\n */\n readonly context: Readonly<Record<string, unknown>>\n}\n\n/** A step: the scenario first, then the values its signature captured. */\nexport type YaddaStep<W, P extends PageMap, A extends unknown[]> = (\n scenario: YaddaScenario<W, P>,\n ...args: A\n) => unknown\n\nexport interface YaddaStepsOptions<P extends PageMap> {\n /** The pages features can name. Defines `$page` unless `dictionary` is given. */\n pages: PageRegistry<P>\n /**\n * The env pages are built with. Called each time a step opens a page, so it\n * can read per-test state — `() => ember()` in an application test.\n */\n env: () => EnvConfig\n /** The dictionary signatures expand against. Defaults to `pageDictionary(pages)`. */\n dictionary?: Yadda.Dictionary\n /** The step keywords' language. Defaults to English. */\n language?: Yadda.Language\n}\n\n/** A step library whose steps share one world. */\nexport interface YaddaSteps<W, P extends PageMap> {\n /** The world this library's steps share. */\n readonly world: Partial<W>\n /** The Yadda library to run: what an ember-cli-yadda steps module returns. */\n readonly library: Yadda.BaseLibrary\n given<A extends unknown[]>(signature: Yadda.Signature, step: YaddaStep<W, P, A>): this\n when<A extends unknown[]>(signature: Yadda.Signature, step: YaddaStep<W, P, A>): this\n then<A extends unknown[]>(signature: Yadda.Signature, step: YaddaStep<W, P, A>): this\n /** A step that matches after any keyword: `Given`, `When`, `Then`, `And`, `But`. */\n step<A extends unknown[]>(signature: Yadda.Signature, step: YaddaStep<W, P, A>): this\n}\n\n/** Fixes the world's type; `steps` then infers the pages from the registry. */\nexport interface YaddaWorld<W> {\n /** A fresh step library with a fresh, empty world. Build one per scenario. */\n steps<P extends PageMap>(options: YaddaStepsOptions<P>): YaddaSteps<W, P>\n}\n\n/**\n * Yadda steps with a scenario-scoped world and the page registry. Steps are\n * plain (arrow) functions taking the scenario first, then what their signature\n * captured; every step is awaited.\n *\n * The world belongs to the library `steps()` builds, so build one per scenario —\n * which is what ember-cli-yadda already does: it calls a steps module's default\n * export inside each scenario's test.\n *\n * ```ts\n * // tests/acceptance/steps/checkout-steps.ts\n * const pages = definePages({ checkout: CheckoutPage })\n * const checkout = yaddaWorld<{ checkout: CheckoutPage }>()\n *\n * export default function (assert: Assert) {\n * const steps = checkout.steps({ pages, env: () => ember() })\n * steps.when('I open the $page page', async ({ world, open }, name: 'checkout') => {\n * world.checkout = open(name)\n * await world.checkout.goto()\n * })\n * return steps.library\n * }\n * ```\n *\n * The world's type is a call of its own because TypeScript cannot take one type\n * argument explicitly and infer another; `steps` infers the pages.\n */\nexport function yaddaWorld<W = Record<string, unknown>>(): YaddaWorld<W> {\n return {\n steps<P extends PageMap>(options: YaddaStepsOptions<P>) {\n return createSteps<W, P>(options)\n },\n }\n}\n\nfunction createSteps<W, P extends PageMap>(options: YaddaStepsOptions<P>): YaddaSteps<W, P> {\n const { pages, env } = options\n const language = options.language ?? Yadda.localisation.English\n const library = language.localise(\n new Yadda.ContextParamLibrary(options.dictionary ?? pageDictionary(pages)),\n )\n const world = createWorld<W>()\n const open = <N extends keyof P & string>(name: N): InstanceType<P[N]> => pages.open(name, env())\n\n // Yadda passes the step's context first and, in promise mode, its completion\n // callback last; a harnessed step sees neither, only the scenario and the\n // captured values. Promise mode is explicit so Yadda never guesses from the\n // wrapper's arity.\n const wrap =\n <A extends unknown[]>(step: YaddaStep<W, P, A>) =>\n async (context: Record<string, unknown>, ...rest: unknown[]): Promise<void> => {\n await step({ world, open, context }, ...(rest.slice(0, -1) as A))\n }\n const promise: Yadda.DefineOptions = { mode: 'promise' }\n\n // Every step keyword the language has, as one alternation: `the world is\n // empty` is the same step after `Given` as after `Then`.\n const anyKeyword = `(?:${['given', 'when', 'then'].map(keyword => language.translate(keyword)).join('|')})`\n\n return {\n world,\n library,\n given(signature, step) {\n library.given(signature, wrap(step), undefined, promise)\n return this\n },\n when(signature, step) {\n library.when(signature, wrap(step), undefined, promise)\n return this\n },\n then(signature, step) {\n library.then(signature, wrap(step), undefined, promise)\n return this\n },\n step(signature, step) {\n const signatures = Array.isArray(signature) ? signature : [signature]\n library.define(\n signatures.map(one => withKeyword(anyKeyword, one)),\n wrap(step),\n undefined,\n promise,\n )\n return this\n },\n }\n}\n\n/** Prefixes a signature with a keyword pattern, the way a Yadda language does. */\nfunction withKeyword(keyword: string, signature: string | RegExp): string {\n const source = signature instanceof RegExp ? signature.source : signature\n return `^(?:\\\\s)*${keyword}\\\\s+${source.replace(/^\\^/, '')}`\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,111 @@
1
1
  {
2
2
  "name": "@harnessed-ts/gherkin",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.6.0",
4
+ "description": "Gherkin support for harnessed: a scenario-scoped world, a typed page registry and a {page} parameter type, with adapters for playwright-bdd, cucumber-js, Cypress cucumber and ember-cli-yadda.",
5
+ "keywords": [
6
+ "testing",
7
+ "harness",
8
+ "gherkin",
9
+ "cucumber",
10
+ "bdd",
11
+ "playwright-bdd"
12
+ ],
13
+ "license": "MIT",
14
+ "contributors": [
15
+ {
16
+ "name": "Joe Gaudet",
17
+ "url": "https://github.com/joegaudet"
18
+ },
19
+ {
20
+ "name": "Jay Seo",
21
+ "url": "https://github.com/jayseo5953"
22
+ }
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/joegaudet/harnessed.git",
27
+ "directory": "packages/gherkin"
28
+ },
29
+ "homepage": "https://github.com/joegaudet/harnessed#readme",
30
+ "bugs": "https://github.com/joegaudet/harnessed/issues",
31
+ "type": "module",
32
+ "main": "./dist/index.cjs",
33
+ "module": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js",
39
+ "require": "./dist/index.cjs"
40
+ },
41
+ "./playwright-bdd": {
42
+ "types": "./dist/playwright-bdd.d.ts",
43
+ "import": "./dist/playwright-bdd.js",
44
+ "require": "./dist/playwright-bdd.cjs"
45
+ },
46
+ "./cucumber": {
47
+ "types": "./dist/cucumber.d.ts",
48
+ "import": "./dist/cucumber.js",
49
+ "require": "./dist/cucumber.cjs"
50
+ },
51
+ "./cypress": {
52
+ "types": "./dist/cypress.d.ts",
53
+ "import": "./dist/cypress.js",
54
+ "require": "./dist/cypress.cjs"
55
+ },
56
+ "./yadda": {
57
+ "types": "./dist/yadda.d.ts",
58
+ "import": "./dist/yadda.js",
59
+ "require": "./dist/yadda.cjs"
60
+ },
61
+ "./package.json": "./package.json"
62
+ },
63
+ "files": [
64
+ "dist",
65
+ "CHANGELOG.md",
66
+ "README.md",
67
+ "LICENSE"
68
+ ],
69
+ "engines": {
70
+ "node": ">=22.12.0"
71
+ },
72
+ "dependencies": {
73
+ "@harnessed-ts/core": "^0.6.0"
74
+ },
75
+ "peerDependencies": {
76
+ "@badeball/cypress-cucumber-preprocessor": ">=20",
77
+ "@cucumber/cucumber": ">=10",
78
+ "@playwright/test": ">=1.43",
79
+ "yadda": ">=3"
80
+ },
81
+ "publishConfig": {
82
+ "access": "public"
83
+ },
84
+ "devDependencies": {
85
+ "@cucumber/cucumber": "~13.2.1",
86
+ "@cucumber/cucumber-expressions": "~20.0.0",
87
+ "@playwright/test": "^1.63.0",
88
+ "typescript": "^6.0.3",
89
+ "vitest": "^5.0.3",
90
+ "yadda": "^3.2.0"
91
+ },
92
+ "peerDependenciesMeta": {
93
+ "@playwright/test": {
94
+ "optional": true
95
+ },
96
+ "@cucumber/cucumber": {
97
+ "optional": true
98
+ },
99
+ "@badeball/cypress-cucumber-preprocessor": {
100
+ "optional": true
101
+ },
102
+ "yadda": {
103
+ "optional": true
104
+ }
105
+ },
106
+ "scripts": {
107
+ "build": "tsup",
108
+ "clean": "rm -rf dist *.tsbuildinfo",
109
+ "test": "vitest run"
110
+ }
6
111
  }