@letsrunit/controller 0.3.8 → 0.3.10

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/dist/index.d.ts CHANGED
@@ -22,7 +22,7 @@ declare class Controller {
22
22
  private world;
23
23
  readonly journal: Journal;
24
24
  private readonly pendingArtifacts;
25
- static fieldSelectorIsRegistered: boolean;
25
+ static selectorsAreRegistered: boolean;
26
26
  private constructor();
27
27
  get lang(): {
28
28
  code: string;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { typeDefinitions, stepsDefinitions, toFile } from '@letsrunit/bdd';
2
2
  import { parseFeature } from '@letsrunit/gherkin';
3
3
  import { Journal } from '@letsrunit/journal';
4
- import { createFieldEngine, createDateEngine, browse, snapshot, scrollToCenter, formatHtml, screenshot, locator } from '@letsrunit/playwright';
4
+ import { createFieldEngine, createDateEngine, browse, snapshot, scrollToCenter, formatHtml, screenshot, fuzzyLocator } from '@letsrunit/playwright';
5
5
  import { omit, clean, hashKey } from '@letsrunit/utils';
6
6
  import { selectors, chromium } from '@playwright/test';
7
7
  import { Runner } from '@letsrunit/gherker';
@@ -23,7 +23,7 @@ var Controller = class _Controller {
23
23
  this.journal = journal;
24
24
  this.pendingArtifacts = pendingArtifacts;
25
25
  }
26
- static fieldSelectorIsRegistered = false;
26
+ static selectorsAreRegistered = false;
27
27
  get lang() {
28
28
  return this.world.lang ?? null;
29
29
  }
@@ -31,10 +31,11 @@ var Controller = class _Controller {
31
31
  return this.world.page;
32
32
  }
33
33
  static async registerFieldSelector() {
34
- if (this.fieldSelectorIsRegistered) return;
34
+ if (this.selectorsAreRegistered) return;
35
35
  try {
36
36
  await selectors.register("field", createFieldEngine);
37
37
  await selectors.register("date", createDateEngine);
38
+ this.selectorsAreRegistered = true;
38
39
  } catch {
39
40
  }
40
41
  }
@@ -129,11 +130,11 @@ var Controller = class _Controller {
129
130
  }
130
131
  }
131
132
  async getLocatorArgs(page, args) {
132
- const promises = args.filter((arg) => arg.getParameterType().name === "locator").map((arg) => arg.getValue(null)).filter((arg) => arg !== null).map((arg) => locator(page, arg));
133
+ const promises = args.filter((arg) => arg.getParameterType().name === "locator").map((arg) => arg.getValue(null)).filter((arg) => arg !== null).map((arg) => fuzzyLocator(page, arg));
133
134
  return Promise.all(promises);
134
135
  }
135
136
  async areAllVisible(locators) {
136
- if (locator.length === 0) return true;
137
+ if (locators.length === 0) return true;
137
138
  const visible = await Promise.all(locators.map((l) => l.isVisible()));
138
139
  return visible.every(Boolean);
139
140
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/runner/dsl.ts","../src/controller.ts","../../utils/src/path.ts"],"names":["options"],"mappings":";;;;;;;;;AAGO,IAAM,MAAA,GAAS,IAAI,MAAA,EAAc;AAExC,KAAA,MAAW,QAAQ,eAAA,EAAiB;AAClC,EAAA,MAAA,CAAO,oBAAoB,IAAI,CAAA;AACjC;AAEA,KAAA,MAAW,QAAQ,gBAAA,EAAkB;AACnC,EAAA,MAAA,CAAO,UAAA,CAAW,KAAK,IAAA,EAAM,IAAA,CAAK,YAAY,IAAA,CAAK,EAAA,EAAI,KAAK,OAAO,CAAA;AACrE;;;AC2BO,IAAM,UAAA,GAAN,MAAM,WAAA,CAAW;AAAA,EAGd,WAAA,CACE,OAAA,EACA,KAAA,EACC,OAAA,EACQ,gBAAA,EACjB;AAJQ,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AACA,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AACC,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AACQ,IAAA,IAAA,CAAA,gBAAA,GAAA,gBAAA;AAAA,EAChB;AAAA,EAPH,OAAO,yBAAA,GAAqC,KAAA;AAAA,EAS5C,IAAI,IAAA,GAA8C;AAChD,IAAA,OAAO,IAAA,CAAK,MAAM,IAAA,IAAQ,IAAA;AAAA,EAC5B;AAAA,EAEA,IAAI,IAAA,GAAa;AACf,IAAA,OAAO,KAAK,KAAA,CAAM,IAAA;AAAA,EACpB;AAAA,EAEA,aAAa,qBAAA,GAAwB;AACnC,IAAA,IAAI,KAAK,yBAAA,EAA2B;AACpC,IAAA,IAAI;AACF,MAAA,MAAM,SAAA,CAAU,QAAA,CAAS,OAAA,EAAS,iBAAiB,CAAA;AACnD,MAAA,MAAM,SAAA,CAAU,QAAA,CAAS,MAAA,EAAQ,gBAAgB,CAAA;AAAA,IACnD,CAAA,CAAA,MAAQ;AAAA,IAAC;AAAA,EACX;AAAA,EAEA,OAAO,WAAA,CACL,IAAA,EACA,OAAA,EACA,gBAAA,EACO;AACP,IAAA,MAAM,UAAU,OAAA,CAAQ,OAAA;AAExB,IAAA,OAAO;AAAA,MACL,IAAA;AAAA,MACA,YAAY,IAAA,CAAK,OAAA,EAAS,CAAC,UAAA,EAAY,SAAA,EAAW,OAAO,CAAC,CAAA;AAAA,MAC1D,SAAA,EAAW,KAAK,GAAA,EAAI;AAAA,MACpB,MAAM,MAAA,CAAO,IAAA,EAAMA,QAAAA,EAAS;AAC1B,QAAA,gBAAA,CAAiB,GAAA,CAAI,MAAA,CAAO,IAAA,EAAMA,QAAO,CAAC,CAAA;AAAA,MAC5C,CAAA;AAAA,MACA,MAAM,KAAK,IAAA,EAAc;AACvB,QAAA,MAAM,OAAA,CAAQ,KAAK,IAAI,CAAA;AAAA,MACzB,CAAA;AAAA,MACA,MAAM,IAAI,IAAA,EAAc;AACtB,QAAA,MAAM,OAAA,CAAQ,KAAK,IAAI,CAAA;AAAA,MACzB;AAAA,KACF;AAAA,EACF;AAAA,EAEA,aAAa,MAAA,CAAO,OAAA,GAA6B,EAAC,EAAwB;AACxE,IAAA,MAAM,KAAK,qBAAA,EAAsB;AAEjC,IAAA,MAAM,OAAA,GAAU,MAAM,QAAA,CAAS,MAAA,CAAO,EAAE,QAAA,EAAU,OAAA,CAAQ,QAAA,IAAY,IAAA,EAAM,CAAA;AAC5E,IAAA,MAAM,IAAA,GAAO,MAAM,MAAA,CAAO,OAAA,EAAS,OAAO,CAAA;AAE1C,IAAA,IAAI,QAAQ,KAAA,EAAO;AACjB,MAAA,IAAA,CAAK,EAAA,CAAG,SAAA,EAAW,CAAC,GAAA,KAAQ;AAC1B,QAAA,OAAA,CAAQ,IAAI,QAAA,EAAU,GAAA,CAAI,MAAK,EAAG,GAAA,CAAI,MAAM,CAAA;AAAA,MAC9C,CAAC,CAAA;AAAA,IACH;AAEA,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,IAAW,OAAA,CAAQ,GAAA,EAAI;AAC/C,IAAA,MAAM,gBAAA,uBAAuB,GAAA,EAAU;AACvC,IAAA,MAAM,KAAA,GAAQ,KAAK,WAAA,CAAY,IAAA,EAAM,EAAE,GAAG,OAAA,EAAS,OAAA,EAAQ,EAAG,gBAAgB,CAAA;AAE9E,IAAA,OAAO,IAAI,WAAA,CAAW,OAAA,EAAS,KAAA,EAAO,SAAS,gBAAgB,CAAA;AAAA,EACjE;AAAA,EAEA,MAAM,GAAA,CAAI,OAAA,EAAiB,IAAA,GAAmB,EAAC,EAAoB;AACjE,IAAA,MAAM,IAAA,CAAK,WAAW,OAAO,CAAA;AAE7B,IAAA,MAAM,EAAE,KAAA,EAAO,CAAA,EAAG,GAAG,MAAA,EAAO,GAAI,MAAM,MAAA,CAAO,GAAA,CAAI,SAAS,IAAA,CAAK,KAAA,EAAO,IAAI,IAAA,KAAS,IAAA,CAAK,QAAQ,GAAG,IAAI,GAAG,IAAI,CAAA;AAC9G,IAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,CAAK,MAAM,IAAI,CAAA;AAE3C,IAAA,OAAO,EAAE,GAAG,MAAA,EAAQ,IAAA,EAAK;AAAA,EAC3B;AAAA,EAEA,SAAS,OAAA,EAA0D;AACjE,IAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,KAAA,CAAM,OAAO,CAAA;AAClC,IAAA,MAAM,KAAA,GAAQ,MAAM,KAAA,CAAM,CAAC,SAAS,CAAC,CAAC,KAAK,GAAG,CAAA;AAE9C,IAAA,OAAO,EAAE,OAAO,KAAA,EAAM;AAAA,EACxB;AAAA,EAEA,MAAM,KAAA,GAAuB;AAC3B,IAAA,MAAM,IAAA,CAAK,QAAQ,KAAA,EAAM;AAAA,EAC3B;AAAA,EAEA,UAAU,IAAA,EAA4C;AACpD,IAAA,OAAO,MAAA,CAAO,IAAA,CACX,MAAA,CAAO,CAAC,GAAA,KAAQ,GAAA,CAAI,OAAA,KAAY,QAAA,KAAa,CAAC,IAAA,IAAQ,GAAA,CAAI,IAAA,KAAS,KAAK,CAAA,CACxE,GAAA,CAAI,CAAC,GAAA,KAAQ,CAAA,EAAG,GAAA,CAAI,IAAI,CAAA,CAAA,EAAI,GAAA,CAAI,MAAM,CAAA,CAAA,IAAM,GAAA,CAAI,OAAA,GAAU,CAAA,IAAA,EAAO,GAAA,CAAI,OAAO,KAAK,EAAA,CAAG,CAAA;AAAA,EACzF;AAAA,EAEA,MAAc,OAAA,CAAQ,IAAA,EAAuB,GAAA,EAAqD;AAChG,IAAA,MAAM,WAAW,CAAC,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,+BAA+B,CAAA,GAC7D,MAAM,IAAA,CAAK,cAAA,CAAe,KAAK,KAAA,CAAM,IAAA,EAAM,IAAA,CAAK,IAAI,IACpD,EAAC;AAEL,IAAA,IAAI,QAAA,CAAS,SAAS,CAAA,EAAG;AACvB,MAAA,MAAM,cAAA,CAAe,QAAA,CAAS,CAAC,CAAC,CAAA;AAAA,IAClC;AAEA,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,EAAI;AACtC,IAAA,MAAM,gBAAA,GAAmB,SAAA,KAAc,aAAA,GAAgB,MAAM,IAAA,CAAK,eAAe,EAAE,IAAA,EAAM,QAAA,EAAU,CAAA,GAAI,MAAA;AACvG,IAAA,MAAM,UAAA,GAAa,MAAM,IAAA,CAAK,YAAA,EAAa;AAC3C,IAAA,MAAM,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,IAAA,CAAK,IAAA,EAAM,EAAE,SAAA,EAAW,KAAA,CAAM,CAAC,gBAAA,EAAkB,UAAU,CAAC,GAAG,CAAA;AAExF,IAAA,MAAM,MAAA,GAAS,MAAM,GAAA,EAAI;AAEzB,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,EAAI;AACrC,IAAA,MAAM,eAAA,GACH,CAAC,gBAAA,IAAoB,QAAA,KAAa,aAAA,IAClC,CAAC,IAAA,CAAK,IAAA,CAAK,UAAA,CAAW,MAAM,CAAA,IAAK,QAAA,KAAa,aAAc,MAAM,IAAA,CAAK,aAAA,CAAc,QAAQ,CAAA,GAC1F,MAAM,IAAA,CAAK,cAAA,CAAe,EAAE,IAAA,EAAM,QAAA,EAAU,CAAA,GAC5C,MAAA;AAEN,IAAA,MAAM,KAAK,OAAA,CACR,KAAA,EAAM,CACN,GAAA,CAAI,KAAK,IAAA,EAAM;AAAA,MACd,MAAM,MAAA,CAAO,MAAA;AAAA,MACb,WAAW,KAAA,CAAM,CAAC,iBAAiB,GAAG,IAAA,CAAK,gBAAgB,CAAC;AAAA,KAC7D,CAAA,CACA,KAAA,CAAM,OAAO,MAAA,EAAQ,OAAO,EAC5B,KAAA,EAAM;AAET,IAAA,IAAA,CAAK,iBAAiB,KAAA,EAAM;AAE5B,IAAA,OAAO,MAAA;AAAA,EACT;AAAA,EAEA,MAAc,YAAA,GAA0C;AACtD,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,GAAO,MAAM,UAAA,CAAW,IAAA,CAAK,MAAM,IAAI,CAAA;AAC7C,MAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,aAAA,EAAe,IAAI,CAAA;AAClD,MAAA,OAAO,IAAI,KAAK,CAAC,IAAI,GAAG,QAAA,EAAU,EAAE,IAAA,EAAM,WAAA,EAAa,CAAA;AAAA,IACzD,SAAS,CAAA,EAAG;AACV,MAAA,MAAM,OAAA,GAAW,CAAA,CAAU,OAAA,IAAW,MAAA,CAAO,CAAC,CAAA;AAC9C,MAAA,MAAM,IAAA,CAAK,OAAA,CAAQ,IAAA,CAAK,CAAA,sBAAA,EAAyB,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,EAAK,CAAA,EAAA,EAAK,OAAO,CAAA,CAAA,EAAI;AAAA,QACpF,IAAA,EAAM,EAAE,MAAA,EAAQ,CAAA;AAAE,OACnB,CAAA;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAc,eAAe,OAAA,EAA4D;AACvF,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,GAAA,CACJ,MAAM,OAAA,CAAQ,GAAA,CAAI,OAAA,EAAS,MAAM,GAAA,CAAI,CAAC,GAAA,KAAQ,GAAA,CAAI,SAAA,EAAU,CAAE,KAAK,CAAC,CAAA,KAAO,CAAA,GAAI,GAAA,GAAM,IAAK,CAAC,KAAK,EAAE,CAAA,EAClG,MAAA,CAAO,OAAO,CAAA;AAEhB,MAAA,OAAO,MAAM,WAAW,IAAA,CAAK,KAAA,CAAM,MAAM,EAAE,GAAG,OAAA,EAAS,IAAA,EAAM,CAAA;AAAA,IAC/D,SAAS,CAAA,EAAG;AACV,MAAA,MAAM,OAAA,GAAW,CAAA,CAAU,OAAA,IAAW,MAAA,CAAO,CAAC,CAAA;AAC9C,MAAA,MAAM,IAAA,CAAK,OAAA,CAAQ,IAAA,CAAK,CAAA,6BAAA,EAAgC,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,EAAK,CAAA,EAAA,EAAK,OAAO,CAAA,CAAA,EAAI;AAAA,QAC3F,IAAA,EAAM,EAAE,MAAA,EAAQ,CAAA;AAAE,OACnB,CAAA;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAc,cAAA,CAAe,IAAA,EAAY,IAAA,EAA+C;AACtF,IAAA,MAAM,QAAA,GAAW,IAAA,CACd,MAAA,CAAO,CAAC,GAAA,KAAQ,GAAA,CAAI,gBAAA,EAAiB,CAAE,IAAA,KAAS,SAAS,CAAA,CACzD,GAAA,CAAI,CAAC,GAAA,KAAQ,GAAA,CAAI,QAAA,CAAiB,IAAI,CAAC,CAAA,CACvC,MAAA,CAAO,CAAC,QAAQ,GAAA,KAAQ,IAAI,CAAA,CAC5B,GAAA,CAAI,CAAC,GAAA,KAAQ,OAAA,CAAQ,IAAA,EAAM,GAAG,CAAC,CAAA;AAElC,IAAA,OAAO,OAAA,CAAQ,IAAI,QAAQ,CAAA;AAAA,EAC7B;AAAA,EAEA,MAAc,cAAc,QAAA,EAAuC;AACjE,IAAA,IAAI,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG,OAAO,IAAA;AAEjC,IAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,GAAA,CAAI,QAAA,CAAS,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,SAAA,EAAW,CAAC,CAAA;AACpE,IAAA,OAAO,OAAA,CAAQ,MAAM,OAAO,CAAA;AAAA,EAC9B;AAAA,EAEA,MAAc,WAAW,OAAA,EAAgC;AACvD,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,OAAA,CAAQ,KAAA,EAAM;AACnC,IAAA,MAAM,EAAE,IAAA,EAAM,WAAA,EAAa,YAAY,KAAA,EAAM,GAAI,aAAa,OAAO,CAAA;AAErE,IAAA,IAAI;AACF,MAAA,IAAI,IAAA,EAAM,OAAA,CAAQ,KAAA,CAAM,IAAI,CAAA;AAC5B,MAAA,IAAI,WAAA,EAAa,OAAA,CAAQ,IAAA,CAAK,WAAW,CAAA;AAEzC,MAAA,KAAA,MAAW,IAAA,IAAQ,CAAC,GAAI,UAAA,IAAc,EAAC,EAAI,GAAG,KAAK,CAAA,EAAG;AACpD,QAAA,OAAA,CAAQ,QAAQ,IAAI,CAAA;AAAA,MACtB;AAAA,IACF,CAAA,SAAE;AACA,MAAA,MAAM,QAAQ,KAAA,EAAM;AAAA,IACtB;AAAA,EACF;AACF;;;ACrOO,SAAS,SAAS,GAAA,EAA6C;AACpE,EAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,GAAG,CAAA;AAC1B,EAAA,MAAM,OAAO,CAAA,EAAG,MAAA,CAAO,QAAQ,CAAA,EAAA,EAAK,OAAO,IAAI,CAAA,CAAA;AAC/C,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,QAAA,GAAW,MAAA,CAAO,SAAS,MAAA,CAAO,IAAA;AACtD,EAAA,OAAO,EAAE,MAAM,IAAA,EAAK;AACtB","file":"index.js","sourcesContent":["import { stepsDefinitions, typeDefinitions, type World } from '@letsrunit/bdd';\nimport { Runner } from '@letsrunit/gherker';\n\nexport const runner = new Runner<World>();\n\nfor (const type of typeDefinitions) {\n runner.defineParameterType(type);\n}\n\nfor (const step of stepsDefinitions) {\n runner.defineStep(step.type, step.expression, step.fn, step.comment);\n}\n","import type { Argument } from '@cucumber/cucumber-expressions';\nimport { toFile, type World } from '@letsrunit/bdd';\nimport type { ParsedStep, StepDescription, StepResult } from '@letsrunit/gherker';\nimport { parseFeature } from '@letsrunit/gherkin';\nimport { Journal } from '@letsrunit/journal';\nimport {\n browse,\n createDateEngine,\n createFieldEngine,\n formatHtml,\n locator,\n screenshot,\n scrollToCenter,\n snapshot,\n} from '@letsrunit/playwright';\nimport { clean, hashKey, omit, type RequireOnly } from '@letsrunit/utils';\nimport {\n type Browser,\n type BrowserContextOptions,\n chromium,\n type Locator,\n type Page,\n type PageScreenshotOptions,\n selectors,\n} from '@playwright/test';\nimport { runner } from './runner';\nimport type { Result } from './types';\n\nexport interface ControllerOptions extends BrowserContextOptions {\n headless?: boolean;\n debug?: boolean;\n journal?: Journal;\n}\n\nexport interface RunOptions {\n signal?: AbortSignal;\n}\n\nexport class Controller {\n static fieldSelectorIsRegistered: boolean = false;\n\n private constructor(\n private browser: Browser,\n private world: World,\n readonly journal: Journal,\n private readonly pendingArtifacts: Set<File>,\n ) {}\n\n get lang(): { code: string; name: string } | null {\n return this.world.lang ?? null;\n }\n\n get page(): Page {\n return this.world.page;\n }\n\n static async registerFieldSelector() {\n if (this.fieldSelectorIsRegistered) return;\n try {\n await selectors.register('field', createFieldEngine);\n await selectors.register('date', createDateEngine);\n } catch {}\n }\n\n static createWorld(\n page: Page,\n options: RequireOnly<ControllerOptions, 'journal'>,\n pendingArtifacts: Set<File>,\n ): World {\n const journal = options.journal;\n\n return {\n page,\n parameters: omit(options, ['headless', 'journal', 'debug']),\n startTime: Date.now(),\n async attach(data, options) {\n pendingArtifacts.add(toFile(data, options));\n },\n async link(text: string) {\n await journal.info(text);\n },\n async log(text: string) {\n await journal.info(text);\n },\n };\n }\n\n static async launch(options: ControllerOptions = {}): Promise<Controller> {\n await this.registerFieldSelector();\n\n const browser = await chromium.launch({ headless: options.headless ?? true });\n const page = await browse(browser, options);\n\n if (options.debug) {\n page.on('console', (msg) => {\n console.log('[page]', msg.type(), msg.text());\n });\n }\n\n const journal = options.journal ?? Journal.nil();\n const pendingArtifacts = new Set<File>();\n const world = this.createWorld(page, { ...options, journal }, pendingArtifacts);\n\n return new Controller(browser, world, journal, pendingArtifacts);\n }\n\n async run(feature: string, opts: RunOptions = {}): Promise<Result> {\n await this.logFeature(feature);\n\n const { world: _, ...result } = await runner.run(feature, this.world, (...args) => this.runStep(...args), opts);\n const page = await snapshot(this.world.page);\n\n return { ...result, page };\n }\n\n validate(feature: string): { valid: boolean; steps: ParsedStep[] } {\n const steps = runner.parse(feature);\n const valid = steps.every((step) => !!step.def);\n\n return { valid, steps };\n }\n\n async close(): Promise<void> {\n await this.browser.close();\n }\n\n listSteps(type?: 'Given' | 'When' | 'Then'): string[] {\n return runner.defs\n .filter((def) => def.comment !== 'hidden' && (!type || def.type === type))\n .map((def) => `${def.type} ${def.source}` + (def.comment ? ` # ${def.comment}` : ''));\n }\n\n private async runStep(step: StepDescription, run: () => Promise<StepResult>): Promise<StepResult> {\n const locators = !step.text.match(/\\b(don't see|not contains)\\b/i)\n ? await this.getLocatorArgs(this.world.page, step.args)\n : [];\n\n if (locators.length > 0) {\n await scrollToCenter(locators[0]);\n }\n\n const urlBefore = this.world.page.url();\n const screenshotBefore = urlBefore !== 'about:blank' ? await this.makeScreenshot({ mask: locators }) : undefined;\n const htmlBefore = await this.makeHtmlFile();\n await this.journal.start(step.text, { artifacts: clean([screenshotBefore, htmlBefore]) });\n\n const result = await run();\n\n const urlAfter = this.world.page.url();\n const screenshotAfter =\n (!screenshotBefore && urlAfter !== 'about:blank') ||\n (!step.text.startsWith('Then') && urlAfter === urlBefore && (await this.areAllVisible(locators)))\n ? await this.makeScreenshot({ mask: locators })\n : undefined;\n\n await this.journal\n .batch()\n .log(step.text, {\n type: result.status,\n artifacts: clean([screenshotAfter, ...this.pendingArtifacts]),\n })\n .error(result.reason?.message)\n .flush();\n\n this.pendingArtifacts.clear();\n\n return result;\n }\n\n private async makeHtmlFile(): Promise<File | undefined> {\n try {\n const html = await formatHtml(this.world.page);\n const filename = await hashKey('{hash}.html', html);\n return new File([html], filename, { type: 'text/html' });\n } catch (e) {\n const message = (e as any).message ?? String(e);\n await this.journal.warn(`Failed to get HTML of ${this.world.page.url()}: ${message}`, {\n meta: { reason: e },\n });\n }\n }\n\n private async makeScreenshot(options?: PageScreenshotOptions): Promise<File | undefined> {\n try {\n const mask = (\n await Promise.all(options?.mask?.map((loc) => loc.isVisible().then((v) => (v ? loc : null))) ?? [])\n ).filter(Boolean) as Locator[];\n\n return await screenshot(this.world.page, { ...options, mask });\n } catch (e) {\n const message = (e as any).message ?? String(e);\n await this.journal.warn(`Failed to take screenshot of ${this.world.page.url()}: ${message}`, {\n meta: { reason: e },\n });\n }\n }\n\n private async getLocatorArgs(page: Page, args: readonly Argument[]): Promise<Locator[]> {\n const promises = args\n .filter((arg) => arg.getParameterType().name === 'locator')\n .map((arg) => arg.getValue<string>(null))\n .filter((arg) => arg !== null)\n .map((arg) => locator(page, arg));\n\n return Promise.all(promises);\n }\n\n private async areAllVisible(locators: Locator[]): Promise<boolean> {\n if (locator.length === 0) return true;\n\n const visible = await Promise.all(locators.map((l) => l.isVisible()));\n return visible.every(Boolean);\n }\n\n private async logFeature(feature: string): Promise<void> {\n const journal = this.journal.batch();\n const { name, description, background, steps } = parseFeature(feature);\n\n try {\n if (name) journal.title(name);\n if (description) journal.info(description);\n\n for (const step of [...(background ?? []), ...steps]) {\n journal.prepare(step);\n }\n } finally {\n await journal.flush();\n }\n }\n}\n","export function splitUrl(url: string): { base: string; path: string } {\n const parsed = new URL(url);\n const base = `${parsed.protocol}//${parsed.host}`;\n const path = parsed.pathname + parsed.search + parsed.hash;\n return { base, path };\n}\n\nexport function asFilename(name: string, ext?: string): string {\n const filename = name\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-') // collapse to dashes\n .replace(/^-+|-+$/g, ''); // trim dashes\n\n return filename + (ext ? `.${ext}` : '');\n}\n\n// Build a regex from the pattern (eg `/books/:id`) to extract params\nexport function pathRegexp(path: string): { regexp: RegExp, names: string[] } {\n const names: string[] = [];\n\n const escape = (s: string) => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const pattern = path\n .split('/')\n .map((seg) => {\n if (seg.startsWith(':')) {\n names.push(seg.slice(1));\n return '([^/]+)';\n }\n return escape(seg);\n })\n .join('/');\n\n const regexp = new RegExp(`^${pattern}$`);\n\n return { regexp, names };\n}\n"]}
1
+ {"version":3,"sources":["../src/runner/dsl.ts","../src/controller.ts","../../utils/src/path.ts"],"names":["options"],"mappings":"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","file":"index.js","sourcesContent":["import { stepsDefinitions, typeDefinitions, type World } from '@letsrunit/bdd';\nimport { Runner } from '@letsrunit/gherker';\n\nexport const runner = new Runner<World>();\n\nfor (const type of typeDefinitions) {\n runner.defineParameterType(type);\n}\n\nfor (const step of stepsDefinitions) {\n runner.defineStep(step.type, step.expression, step.fn, step.comment);\n}\n","import type { Argument } from '@cucumber/cucumber-expressions';\nimport { toFile, type World } from '@letsrunit/bdd';\nimport type { ParsedStep, StepDescription, StepResult } from '@letsrunit/gherker';\nimport { parseFeature } from '@letsrunit/gherkin';\nimport { Journal } from '@letsrunit/journal';\nimport {\n browse,\n createDateEngine,\n createFieldEngine,\n formatHtml,\n fuzzyLocator,\n screenshot,\n scrollToCenter,\n snapshot,\n} from '@letsrunit/playwright';\nimport { clean, hashKey, omit, type RequireOnly } from '@letsrunit/utils';\nimport {\n type Browser,\n type BrowserContextOptions,\n chromium,\n type Locator,\n type Page,\n type PageScreenshotOptions,\n selectors,\n} from '@playwright/test';\nimport { runner } from './runner';\nimport type { Result } from './types';\n\nexport interface ControllerOptions extends BrowserContextOptions {\n headless?: boolean;\n debug?: boolean;\n journal?: Journal;\n}\n\nexport interface RunOptions {\n signal?: AbortSignal;\n}\n\nexport class Controller {\n static selectorsAreRegistered: boolean = false;\n\n private constructor(\n private browser: Browser,\n private world: World,\n readonly journal: Journal,\n private readonly pendingArtifacts: Set<File>,\n ) {}\n\n get lang(): { code: string; name: string } | null {\n return this.world.lang ?? null;\n }\n\n get page(): Page {\n return this.world.page;\n }\n\n static async registerFieldSelector() {\n if (this.selectorsAreRegistered) return;\n try {\n await selectors.register('field', createFieldEngine);\n await selectors.register('date', createDateEngine);\n this.selectorsAreRegistered = true;\n } catch {}\n }\n\n static createWorld(\n page: Page,\n options: RequireOnly<ControllerOptions, 'journal'>,\n pendingArtifacts: Set<File>,\n ): World {\n const journal = options.journal;\n\n return {\n page,\n parameters: omit(options, ['headless', 'journal', 'debug']),\n startTime: Date.now(),\n async attach(data, options) {\n pendingArtifacts.add(toFile(data, options));\n },\n async link(text: string) {\n await journal.info(text);\n },\n async log(text: string) {\n await journal.info(text);\n },\n };\n }\n\n static async launch(options: ControllerOptions = {}): Promise<Controller> {\n await this.registerFieldSelector();\n\n const browser = await chromium.launch({ headless: options.headless ?? true });\n const page = await browse(browser, options);\n\n if (options.debug) {\n page.on('console', (msg) => {\n console.log('[page]', msg.type(), msg.text());\n });\n }\n\n const journal = options.journal ?? Journal.nil();\n const pendingArtifacts = new Set<File>();\n const world = this.createWorld(page, { ...options, journal }, pendingArtifacts);\n\n return new Controller(browser, world, journal, pendingArtifacts);\n }\n\n async run(feature: string, opts: RunOptions = {}): Promise<Result> {\n await this.logFeature(feature);\n\n const { world: _, ...result } = await runner.run(feature, this.world, (...args) => this.runStep(...args), opts);\n const page = await snapshot(this.world.page);\n\n return { ...result, page };\n }\n\n validate(feature: string): { valid: boolean; steps: ParsedStep[] } {\n const steps = runner.parse(feature);\n const valid = steps.every((step) => !!step.def);\n\n return { valid, steps };\n }\n\n async close(): Promise<void> {\n await this.browser.close();\n }\n\n listSteps(type?: 'Given' | 'When' | 'Then'): string[] {\n return runner.defs\n .filter((def) => def.comment !== 'hidden' && (!type || def.type === type))\n .map((def) => `${def.type} ${def.source}` + (def.comment ? ` # ${def.comment}` : ''));\n }\n\n private async runStep(step: StepDescription, run: () => Promise<StepResult>): Promise<StepResult> {\n const locators = !step.text.match(/\\b(don't see|not contains)\\b/i)\n ? await this.getLocatorArgs(this.world.page, step.args)\n : [];\n\n if (locators.length > 0) {\n await scrollToCenter(locators[0]);\n }\n\n const urlBefore = this.world.page.url();\n const screenshotBefore = urlBefore !== 'about:blank' ? await this.makeScreenshot({ mask: locators }) : undefined;\n const htmlBefore = await this.makeHtmlFile();\n await this.journal.start(step.text, { artifacts: clean([screenshotBefore, htmlBefore]) });\n\n const result = await run();\n\n const urlAfter = this.world.page.url();\n const screenshotAfter =\n (!screenshotBefore && urlAfter !== 'about:blank') ||\n (!step.text.startsWith('Then') && urlAfter === urlBefore && (await this.areAllVisible(locators)))\n ? await this.makeScreenshot({ mask: locators })\n : undefined;\n\n await this.journal\n .batch()\n .log(step.text, {\n type: result.status,\n artifacts: clean([screenshotAfter, ...this.pendingArtifacts]),\n })\n .error(result.reason?.message)\n .flush();\n\n this.pendingArtifacts.clear();\n\n return result;\n }\n\n private async makeHtmlFile(): Promise<File | undefined> {\n try {\n const html = await formatHtml(this.world.page);\n const filename = await hashKey('{hash}.html', html);\n return new File([html], filename, { type: 'text/html' });\n } catch (e) {\n const message = (e as any).message ?? String(e);\n await this.journal.warn(`Failed to get HTML of ${this.world.page.url()}: ${message}`, {\n meta: { reason: e },\n });\n }\n }\n\n private async makeScreenshot(options?: PageScreenshotOptions): Promise<File | undefined> {\n try {\n const mask = (\n await Promise.all(options?.mask?.map((loc) => loc.isVisible().then((v) => (v ? loc : null))) ?? [])\n ).filter(Boolean) as Locator[];\n\n return await screenshot(this.world.page, { ...options, mask });\n } catch (e) {\n const message = (e as any).message ?? String(e);\n await this.journal.warn(`Failed to take screenshot of ${this.world.page.url()}: ${message}`, {\n meta: { reason: e },\n });\n }\n }\n\n private async getLocatorArgs(page: Page, args: readonly Argument[]): Promise<Locator[]> {\n const promises = args\n .filter((arg) => arg.getParameterType().name === 'locator')\n .map((arg) => arg.getValue<string>(null))\n .filter((arg) => arg !== null)\n .map((arg) => fuzzyLocator(page, arg));\n\n return Promise.all(promises);\n }\n\n private async areAllVisible(locators: Locator[]): Promise<boolean> {\n if (locators.length === 0) return true;\n\n const visible = await Promise.all(locators.map((l) => l.isVisible()));\n return visible.every(Boolean);\n }\n\n private async logFeature(feature: string): Promise<void> {\n const journal = this.journal.batch();\n const { name, description, background, steps } = parseFeature(feature);\n\n try {\n if (name) journal.title(name);\n if (description) journal.info(description);\n\n for (const step of [...(background ?? []), ...steps]) {\n journal.prepare(step);\n }\n } finally {\n await journal.flush();\n }\n }\n}\n","export function splitUrl(url: string): { base: string; path: string } {\n const parsed = new URL(url);\n const base = `${parsed.protocol}//${parsed.host}`;\n const path = parsed.pathname + parsed.search + parsed.hash;\n return { base, path };\n}\n\nexport function asFilename(name: string, ext?: string): string {\n const filename = name\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-') // collapse to dashes\n .replace(/^-+|-+$/g, ''); // trim dashes\n\n return filename + (ext ? `.${ext}` : '');\n}\n\n// Build a regex from the pattern (eg `/books/:id`) to extract params\nexport function pathRegexp(path: string): { regexp: RegExp, names: string[] } {\n const names: string[] = [];\n\n const escape = (s: string) => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const pattern = path\n .split('/')\n .map((seg) => {\n if (seg.startsWith(':')) {\n names.push(seg.slice(1));\n return '([^/]+)';\n }\n return escape(seg);\n })\n .join('/');\n\n const regexp = new RegExp(`^${pattern}$`);\n\n return { regexp, names };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letsrunit/controller",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "Browser automation controller for letsrunit",
5
5
  "keywords": [
6
6
  "testing",
@@ -45,12 +45,12 @@
45
45
  },
46
46
  "packageManager": "yarn@4.10.3",
47
47
  "dependencies": {
48
- "@letsrunit/bdd": "0.3.8",
49
- "@letsrunit/gherker": "0.3.8",
50
- "@letsrunit/gherkin": "0.3.8",
51
- "@letsrunit/journal": "0.3.8",
52
- "@letsrunit/playwright": "0.3.8",
53
- "@letsrunit/utils": "0.3.8",
48
+ "@letsrunit/bdd": "0.3.10",
49
+ "@letsrunit/gherker": "0.3.10",
50
+ "@letsrunit/gherkin": "0.3.10",
51
+ "@letsrunit/journal": "0.3.10",
52
+ "@letsrunit/playwright": "0.3.10",
53
+ "@letsrunit/utils": "0.3.10",
54
54
  "@playwright/test": "^1.57.0",
55
55
  "jsdom": "^27.4.0",
56
56
  "zod": "^4.3.5"
package/src/controller.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  createDateEngine,
9
9
  createFieldEngine,
10
10
  formatHtml,
11
- locator,
11
+ fuzzyLocator,
12
12
  screenshot,
13
13
  scrollToCenter,
14
14
  snapshot,
@@ -37,7 +37,7 @@ export interface RunOptions {
37
37
  }
38
38
 
39
39
  export class Controller {
40
- static fieldSelectorIsRegistered: boolean = false;
40
+ static selectorsAreRegistered: boolean = false;
41
41
 
42
42
  private constructor(
43
43
  private browser: Browser,
@@ -55,10 +55,11 @@ export class Controller {
55
55
  }
56
56
 
57
57
  static async registerFieldSelector() {
58
- if (this.fieldSelectorIsRegistered) return;
58
+ if (this.selectorsAreRegistered) return;
59
59
  try {
60
60
  await selectors.register('field', createFieldEngine);
61
61
  await selectors.register('date', createDateEngine);
62
+ this.selectorsAreRegistered = true;
62
63
  } catch {}
63
64
  }
64
65
 
@@ -200,13 +201,13 @@ export class Controller {
200
201
  .filter((arg) => arg.getParameterType().name === 'locator')
201
202
  .map((arg) => arg.getValue<string>(null))
202
203
  .filter((arg) => arg !== null)
203
- .map((arg) => locator(page, arg));
204
+ .map((arg) => fuzzyLocator(page, arg));
204
205
 
205
206
  return Promise.all(promises);
206
207
  }
207
208
 
208
209
  private async areAllVisible(locators: Locator[]): Promise<boolean> {
209
- if (locator.length === 0) return true;
210
+ if (locators.length === 0) return true;
210
211
 
211
212
  const visible = await Promise.all(locators.map((l) => l.isVisible()));
212
213
  return visible.every(Boolean);