@cronn/playwright-utils 0.7.0 → 0.8.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/dist/index.d.mts CHANGED
@@ -606,7 +606,64 @@ export declare class RouteInterceptorFixture {
606
606
  */
607
607
  export declare function resolveFromPackageRoot(...paths: Array<string>): string;
608
608
  //#endregion
609
- //#region src/page-object-model/locator-extensions.d.ts
609
+ //#region src/page-object-model/form-filler.d.ts
610
+ interface Fillable<TValue> {
611
+ fill: Filler<TValue>;
612
+ }
613
+ type Filler<TValue> = (value: TValue) => Promise<void>;
614
+ type FormFields<TData> = { [K in keyof TData]-?: Fillable<Exclude<TData[K], undefined>> | Filler<Exclude<TData[K], undefined>>; };
615
+ /**
616
+ * Creates a filler function that fills a form declaratively from a data
617
+ * object, e.g. as the `fill` method of a page object.
618
+ *
619
+ * Each key of `fields` maps a key of the data to a form input: either an
620
+ * object with a `fill` method (such as a `Locator` or a nested page object) or
621
+ * a filler function receiving the value. Fields are filled one after another
622
+ * in the order of the keys in `fields`, and fields whose value is `undefined`
623
+ * are skipped.
624
+ *
625
+ * The type of the data is either given explicitly as type argument or inferred
626
+ * from `fields`. When inferred, filler functions need an explicit parameter
627
+ * type, otherwise their value is `unknown`. In both cases, the returned
628
+ * function accepts partial data.
629
+ *
630
+ * As the returned function is a filler itself, it can be used as a field of
631
+ * another form filler to fill nested data.
632
+ *
633
+ * @param fields - Mapping from each key of the data to the input to fill
634
+ * @returns A function filling the form with the given data
635
+ * @example
636
+ * ```ts
637
+ * const { checkbox, textbox } = roleLocators(page);
638
+ *
639
+ * const registrationForm = {
640
+ * fill: formFiller<Registration>({
641
+ * name: textbox("Name"),
642
+ * acceptTerms: (accepted) => checkbox("Accept terms").setChecked(accepted),
643
+ * }),
644
+ * };
645
+ * ```
646
+ */
647
+ export declare function formFiller<TData extends object>(fields: FormFields<TData>): Filler<Partial<TData>>;
648
+ //#endregion
649
+ //#region src/page-object-model/extend-locator.d.ts
650
+ type WithoutLocatorKeys = Partial<Record<Exclude<keyof Locator, "toString">, never>>;
651
+ type ExtendedLocator<TExtensions extends WithoutLocatorKeys> = Locator & TExtensions;
652
+ /**
653
+ * Extends a `Locator` with custom properties and functions while retaining the full `Locator` API.
654
+ *
655
+ * Extensions must not override members of `Locator`. Locators derived from the extended locator
656
+ * (e.g. via `first()`, `filter()` or `locator()`) are plain `Locator`s without the extensions.
657
+ *
658
+ * Methods of `Locator` are bound to the original locator on every access, so accessing the same method
659
+ * twice returns different function instances (e.g. `extended.click !== extended.click`).
660
+ *
661
+ * @param locator The locator to extend
662
+ * @param createExtensions Creates the extensions, receiving the original locator
663
+ */
664
+ export declare function extendLocator<TExtensions extends object & WithoutLocatorKeys>(locator: Locator, createExtensions: (locator: Locator) => TExtensions): ExtendedLocator<TExtensions>;
665
+ //#endregion
666
+ //#region src/page-object-model/role-locators.d.ts
610
667
  type RoleLocators = Record<Role, RoleLocator>;
611
668
  interface RoleLocator {
612
669
  (options?: RoleLocatorOptions): Locator;
@@ -673,4 +730,4 @@ export declare function maskedValueWithIndex(name: string): (index: number) => s
673
730
  */
674
731
  type PlaywrightTarget = Page | Locator;
675
732
  //#endregion
676
- export type { ConsoleMessageFilter, ConsoleMessageLevel, CspFixtures, CspViolation, HttpMethod, PlaywrightTarget, ResponseHandler, RoleLocators, RouteFilter };
733
+ export type { ConsoleMessageFilter, ConsoleMessageLevel, CspFixtures, CspViolation, ExtendedLocator, FormFields, HttpMethod, PlaywrightTarget, ResponseHandler, RoleLocators, RouteFilter };
package/dist/index.mjs CHANGED
@@ -789,7 +789,81 @@ function resolveFromPackageRoot(...paths) {
789
789
  return path.resolve(packageDir, ...paths);
790
790
  }
791
791
  //#endregion
792
- //#region src/page-object-model/locator-extensions.ts
792
+ //#region src/page-object-model/form-filler.ts
793
+ /**
794
+ * Creates a filler function that fills a form declaratively from a data
795
+ * object, e.g. as the `fill` method of a page object.
796
+ *
797
+ * Each key of `fields` maps a key of the data to a form input: either an
798
+ * object with a `fill` method (such as a `Locator` or a nested page object) or
799
+ * a filler function receiving the value. Fields are filled one after another
800
+ * in the order of the keys in `fields`, and fields whose value is `undefined`
801
+ * are skipped.
802
+ *
803
+ * The type of the data is either given explicitly as type argument or inferred
804
+ * from `fields`. When inferred, filler functions need an explicit parameter
805
+ * type, otherwise their value is `unknown`. In both cases, the returned
806
+ * function accepts partial data.
807
+ *
808
+ * As the returned function is a filler itself, it can be used as a field of
809
+ * another form filler to fill nested data.
810
+ *
811
+ * @param fields - Mapping from each key of the data to the input to fill
812
+ * @returns A function filling the form with the given data
813
+ * @example
814
+ * ```ts
815
+ * const { checkbox, textbox } = roleLocators(page);
816
+ *
817
+ * const registrationForm = {
818
+ * fill: formFiller<Registration>({
819
+ * name: textbox("Name"),
820
+ * acceptTerms: (accepted) => checkbox("Accept terms").setChecked(accepted),
821
+ * }),
822
+ * };
823
+ * ```
824
+ */
825
+ function formFiller(fields) {
826
+ return async (data) => {
827
+ for (const [key, filler] of Object.entries(fields)) {
828
+ const value = data[key];
829
+ if (value === void 0) continue;
830
+ if (typeof filler === "function") await filler(value);
831
+ else await filler.fill(value);
832
+ }
833
+ };
834
+ }
835
+ //#endregion
836
+ //#region src/page-object-model/extend-locator.ts
837
+ /**
838
+ * Extends a `Locator` with custom properties and functions while retaining the full `Locator` API.
839
+ *
840
+ * Extensions must not override members of `Locator`. Locators derived from the extended locator
841
+ * (e.g. via `first()`, `filter()` or `locator()`) are plain `Locator`s without the extensions.
842
+ *
843
+ * Methods of `Locator` are bound to the original locator on every access, so accessing the same method
844
+ * twice returns different function instances (e.g. `extended.click !== extended.click`).
845
+ *
846
+ * @param locator The locator to extend
847
+ * @param createExtensions Creates the extensions, receiving the original locator
848
+ */
849
+ function extendLocator(locator, createExtensions) {
850
+ const extensions = createExtensions(locator);
851
+ return new Proxy(locator, {
852
+ get(target, property) {
853
+ if (isOwnKeyOf(extensions, property)) return extensions[property];
854
+ const value = Reflect.get(target, property, target);
855
+ return typeof value === "function" && property !== "constructor" ? value.bind(target) : value;
856
+ },
857
+ has(target, property) {
858
+ return isOwnKeyOf(extensions, property) || Reflect.has(target, property);
859
+ }
860
+ });
861
+ }
862
+ function isOwnKeyOf(object, key) {
863
+ return Object.hasOwn(object, key);
864
+ }
865
+ //#endregion
866
+ //#region src/page-object-model/role-locators.ts
793
867
  const ROLES = [
794
868
  "alert",
795
869
  "alertdialog",
@@ -951,4 +1025,4 @@ function identity(value) {
951
1025
  return value;
952
1026
  }
953
1027
  //#endregion
954
- export { ConsoleCaptor, CspCaptor, RouteInterceptor, RouteInterceptorFixture, captureConsole, captureCspViolations, createFetchAdapter, cspFixtures, hasLogLevel, interceptRoute, isCI, maskBaseURL, maskedValue, maskedValueWithIndex, matchPath, modifyJsonBody, modifyTextBody, resolveFromPackageRoot, roleLocators };
1028
+ export { ConsoleCaptor, CspCaptor, RouteInterceptor, RouteInterceptorFixture, captureConsole, captureCspViolations, createFetchAdapter, cspFixtures, extendLocator, formFiller, hasLogLevel, interceptRoute, isCI, maskBaseURL, maskedValue, maskedValueWithIndex, matchPath, modifyJsonBody, modifyTextBody, resolveFromPackageRoot, roleLocators };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cronn/playwright-utils",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Utilities for writing tests with Playwright",
5
5
  "keywords": [
6
6
  "playwright",
@@ -33,29 +33,29 @@
33
33
  },
34
34
  "devDependencies": {
35
35
  "@arethetypeswrong/core": "0.18.5",
36
- "@changesets/cli": "3.0.2",
36
+ "@changesets/cli": "3.0.3",
37
37
  "@cronn/element-snapshot": "0.30.0",
38
38
  "@cronn/playwright-file-snapshots": "2.2.2",
39
39
  "@playwright/test": "1.63.0",
40
- "@types/node": "24.13.4",
40
+ "@types/node": "24.19.0",
41
41
  "@typescript/native": "npm:typescript@^7.0.2",
42
- "@vitest/coverage-v8": "5.0.0",
43
- "eslint": "10.10.0",
42
+ "@vitest/coverage-v8": "5.0.2",
43
+ "eslint": "10.11.0",
44
44
  "eslint-config-prettier": "10.1.8",
45
45
  "eslint-plugin-check-file": "3.3.2",
46
46
  "eslint-plugin-unused-imports": "4.4.1",
47
- "knip": "6.35.1",
48
- "oxc-minify": "0.149.0",
49
- "oxfmt": "0.67.0",
47
+ "knip": "6.38.0",
48
+ "oxc-minify": "0.151.0",
49
+ "oxfmt": "0.70.0",
50
50
  "publint": "0.3.24",
51
51
  "rimraf": "6.1.3",
52
52
  "tsdown": "0.23.0",
53
- "turbo": "2.10.12",
53
+ "turbo": "2.11.5",
54
54
  "typescript": "npm:@typescript/typescript6@^6.0.2",
55
- "typescript-eslint": "8.70.0",
55
+ "typescript-eslint": "8.70.1",
56
56
  "vitepress": "2.0.0-alpha.20",
57
57
  "vitepress-plugin-group-icons": "1.7.6",
58
- "vitest": "5.0.0"
58
+ "vitest": "5.0.2"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@cronn/playwright-file-snapshots": "^2",
@@ -64,7 +64,7 @@
64
64
  "devEngines": {
65
65
  "packageManager": {
66
66
  "name": "pnpm",
67
- "version": "12.5.1",
67
+ "version": "12.9.1",
68
68
  "onFail": "download"
69
69
  },
70
70
  "runtime": {
@@ -90,9 +90,7 @@
90
90
  "lint:check": "eslint . --max-warnings=0",
91
91
  "lint:fix": "eslint . --max-warnings=0 --fix",
92
92
  "playwright:install": "playwright install chromium",
93
- "pnpm:check": "pnpm dedupe --check",
94
93
  "pnpm:clean": "pnpm pm clean && pnpm store prune",
95
- "pnpm:fix": "pnpm dedupe",
96
94
  "release": "pnpm turbo run build && pnpm changeset publish",
97
95
  "test:integration": "playwright test",
98
96
  "test:unit": "vitest run",