@cronn/playwright-utils 0.7.0 → 0.9.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 +86 -2
- package/dist/index.mjs +296 -22
- package/package.json +15 -16
package/dist/index.d.mts
CHANGED
|
@@ -591,6 +591,33 @@ export declare class RouteInterceptorFixture {
|
|
|
591
591
|
intercept(filter: RouteFilter): RouteInterceptor;
|
|
592
592
|
}
|
|
593
593
|
//#endregion
|
|
594
|
+
//#region src/security/csp-report.d.ts
|
|
595
|
+
/**
|
|
596
|
+
* Returns the `Content-Security-Policy` header of a response, or an empty
|
|
597
|
+
* string if the response or the header is missing.
|
|
598
|
+
*/
|
|
599
|
+
export declare function getCspHeader(response: Response | null): Promise<string>;
|
|
600
|
+
/**
|
|
601
|
+
* Evaluates a `Content-Security-Policy` header against a set of recommended
|
|
602
|
+
* directives and returns the result as a Markdown report.
|
|
603
|
+
*
|
|
604
|
+
* The report lists all directives of the header, followed by recommendations
|
|
605
|
+
* for missing or weak directives. Missing fetch directives are evaluated using
|
|
606
|
+
* `default-src`, just like the browser falls back to it. Nonces are masked as
|
|
607
|
+
* `[NONCE_0]`, `[NONCE_1]`, … to keep the report deterministic for file snapshots.
|
|
608
|
+
*
|
|
609
|
+
* @example
|
|
610
|
+
* ```ts
|
|
611
|
+
* const response = await page.goto("/");
|
|
612
|
+
* const cspHeader = await getCspHeader(response);
|
|
613
|
+
*
|
|
614
|
+
* expect(cspReport(cspHeader)).toMatchTextFile({
|
|
615
|
+
* fileExtension: "md",
|
|
616
|
+
* });
|
|
617
|
+
* ```
|
|
618
|
+
*/
|
|
619
|
+
export declare function cspReport(header: string): string;
|
|
620
|
+
//#endregion
|
|
594
621
|
//#region src/file.d.ts
|
|
595
622
|
/**
|
|
596
623
|
* Resolves the given path segments against the root directory of the closest
|
|
@@ -606,7 +633,64 @@ export declare class RouteInterceptorFixture {
|
|
|
606
633
|
*/
|
|
607
634
|
export declare function resolveFromPackageRoot(...paths: Array<string>): string;
|
|
608
635
|
//#endregion
|
|
609
|
-
//#region src/page-object-model/
|
|
636
|
+
//#region src/page-object-model/form-filler.d.ts
|
|
637
|
+
interface Fillable<TValue> {
|
|
638
|
+
fill: Filler<TValue>;
|
|
639
|
+
}
|
|
640
|
+
type Filler<TValue> = (value: TValue) => Promise<void>;
|
|
641
|
+
type FormFields<TData> = { [K in keyof TData]-?: Fillable<Exclude<TData[K], undefined>> | Filler<Exclude<TData[K], undefined>>; };
|
|
642
|
+
/**
|
|
643
|
+
* Creates a filler function that fills a form declaratively from a data
|
|
644
|
+
* object, e.g. as the `fill` method of a page object.
|
|
645
|
+
*
|
|
646
|
+
* Each key of `fields` maps a key of the data to a form input: either an
|
|
647
|
+
* object with a `fill` method (such as a `Locator` or a nested page object) or
|
|
648
|
+
* a filler function receiving the value. Fields are filled one after another
|
|
649
|
+
* in the order of the keys in `fields`, and fields whose value is `undefined`
|
|
650
|
+
* are skipped.
|
|
651
|
+
*
|
|
652
|
+
* The type of the data is either given explicitly as type argument or inferred
|
|
653
|
+
* from `fields`. When inferred, filler functions need an explicit parameter
|
|
654
|
+
* type, otherwise their value is `unknown`. In both cases, the returned
|
|
655
|
+
* function accepts partial data.
|
|
656
|
+
*
|
|
657
|
+
* As the returned function is a filler itself, it can be used as a field of
|
|
658
|
+
* another form filler to fill nested data.
|
|
659
|
+
*
|
|
660
|
+
* @param fields - Mapping from each key of the data to the input to fill
|
|
661
|
+
* @returns A function filling the form with the given data
|
|
662
|
+
* @example
|
|
663
|
+
* ```ts
|
|
664
|
+
* const { checkbox, textbox } = roleLocators(page);
|
|
665
|
+
*
|
|
666
|
+
* const registrationForm = {
|
|
667
|
+
* fill: formFiller<Registration>({
|
|
668
|
+
* name: textbox("Name"),
|
|
669
|
+
* acceptTerms: (accepted) => checkbox("Accept terms").setChecked(accepted),
|
|
670
|
+
* }),
|
|
671
|
+
* };
|
|
672
|
+
* ```
|
|
673
|
+
*/
|
|
674
|
+
export declare function formFiller<TData extends object>(fields: FormFields<TData>): Filler<Partial<TData>>;
|
|
675
|
+
//#endregion
|
|
676
|
+
//#region src/page-object-model/extend-locator.d.ts
|
|
677
|
+
type WithoutLocatorKeys = Partial<Record<Exclude<keyof Locator, "toString">, never>>;
|
|
678
|
+
type ExtendedLocator<TExtensions extends WithoutLocatorKeys> = Locator & TExtensions;
|
|
679
|
+
/**
|
|
680
|
+
* Extends a `Locator` with custom properties and functions while retaining the full `Locator` API.
|
|
681
|
+
*
|
|
682
|
+
* Extensions must not override members of `Locator`. Locators derived from the extended locator
|
|
683
|
+
* (e.g. via `first()`, `filter()` or `locator()`) are plain `Locator`s without the extensions.
|
|
684
|
+
*
|
|
685
|
+
* Methods of `Locator` are bound to the original locator on every access, so accessing the same method
|
|
686
|
+
* twice returns different function instances (e.g. `extended.click !== extended.click`).
|
|
687
|
+
*
|
|
688
|
+
* @param locator The locator to extend
|
|
689
|
+
* @param createExtensions Creates the extensions, receiving the original locator
|
|
690
|
+
*/
|
|
691
|
+
export declare function extendLocator<TExtensions extends object & WithoutLocatorKeys>(locator: Locator, createExtensions: (locator: Locator) => TExtensions): ExtendedLocator<TExtensions>;
|
|
692
|
+
//#endregion
|
|
693
|
+
//#region src/page-object-model/role-locators.d.ts
|
|
610
694
|
type RoleLocators = Record<Role, RoleLocator>;
|
|
611
695
|
interface RoleLocator {
|
|
612
696
|
(options?: RoleLocatorOptions): Locator;
|
|
@@ -673,4 +757,4 @@ export declare function maskedValueWithIndex(name: string): (index: number) => s
|
|
|
673
757
|
*/
|
|
674
758
|
type PlaywrightTarget = Page | Locator;
|
|
675
759
|
//#endregion
|
|
676
|
-
export type { ConsoleMessageFilter, ConsoleMessageLevel, CspFixtures, CspViolation, HttpMethod, PlaywrightTarget, ResponseHandler, RoleLocators, RouteFilter };
|
|
760
|
+
export type { ConsoleMessageFilter, ConsoleMessageLevel, CspFixtures, CspViolation, ExtendedLocator, FormFields, HttpMethod, PlaywrightTarget, ResponseHandler, RoleLocators, RouteFilter };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { expect } from "@playwright/test";
|
|
2
|
+
import { markdownTable } from "markdown-table";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { packageDirectorySync } from "package-directory";
|
|
4
5
|
import { maskString, stringNormalizer } from "@cronn/playwright-file-snapshots";
|
|
@@ -770,6 +771,225 @@ function modifyTextBody(modifier) {
|
|
|
770
771
|
});
|
|
771
772
|
}
|
|
772
773
|
//#endregion
|
|
774
|
+
//#region src/normalizers/masked-value.ts
|
|
775
|
+
/**
|
|
776
|
+
* Formats a name as a masked placeholder, e.g. `maskedValue("base url")`
|
|
777
|
+
* returns `[BASE_URL]`.
|
|
778
|
+
*
|
|
779
|
+
* @param name - Name of the masked value
|
|
780
|
+
*/
|
|
781
|
+
function maskedValue(name) {
|
|
782
|
+
return `[${name.toUpperCase().replace(/[ -.:,;]/g, "_")}]`;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Creates a factory for indexed masked placeholders, e.g. `[USER_1]`.
|
|
786
|
+
*
|
|
787
|
+
* @param name - Name of the masked value
|
|
788
|
+
* @returns A function mapping an index to a masked placeholder
|
|
789
|
+
*/
|
|
790
|
+
function maskedValueWithIndex(name) {
|
|
791
|
+
return (index) => maskedValue(`${name}_${index}`);
|
|
792
|
+
}
|
|
793
|
+
//#endregion
|
|
794
|
+
//#region src/security/csp-report.ts
|
|
795
|
+
const RECOMMENDED_DIRECTIVES = [
|
|
796
|
+
{ name: "upgrade-insecure-requests" },
|
|
797
|
+
{ name: "trusted-types" },
|
|
798
|
+
{
|
|
799
|
+
name: "require-trusted-types-for",
|
|
800
|
+
values: ["'script'"]
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
name: "default-src",
|
|
804
|
+
values: ["'self'"]
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
name: "script-src",
|
|
808
|
+
requireNonce: true,
|
|
809
|
+
tolerated: ["'strict-dynamic'"],
|
|
810
|
+
fallback: "default-src",
|
|
811
|
+
optional: ["script-src-elem", "script-src-attr"]
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
name: "script-src-elem",
|
|
815
|
+
requireNonce: true,
|
|
816
|
+
tolerated: ["'strict-dynamic'"],
|
|
817
|
+
optional: true
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
name: "script-src-attr",
|
|
821
|
+
values: ["'none'"],
|
|
822
|
+
optional: true
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
name: "style-src",
|
|
826
|
+
requireNonce: true,
|
|
827
|
+
fallback: "default-src",
|
|
828
|
+
optional: ["style-src-elem", "style-src-attr"]
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
name: "style-src-elem",
|
|
832
|
+
requireNonce: true,
|
|
833
|
+
optional: true
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
name: "style-src-attr",
|
|
837
|
+
values: ["'none'"],
|
|
838
|
+
optional: true
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
name: "img-src",
|
|
842
|
+
values: ["'self'", "data:"],
|
|
843
|
+
fallback: "default-src"
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
name: "font-src",
|
|
847
|
+
values: ["'self'"],
|
|
848
|
+
fallback: "default-src"
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
name: "connect-src",
|
|
852
|
+
values: ["'self'"],
|
|
853
|
+
fallback: "default-src"
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
name: "object-src",
|
|
857
|
+
values: ["'none'"]
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
name: "base-uri",
|
|
861
|
+
values: ["'self'"]
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
name: "form-action",
|
|
865
|
+
values: ["'self'"]
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
name: "frame-ancestors",
|
|
869
|
+
values: ["'none'"]
|
|
870
|
+
}
|
|
871
|
+
];
|
|
872
|
+
const NONCE_PLACEHOLDER = "'nonce-[RANDOM]'";
|
|
873
|
+
const MASKED_NONCE = maskedValueWithIndex("NONCE");
|
|
874
|
+
/**
|
|
875
|
+
* Returns the `Content-Security-Policy` header of a response, or an empty
|
|
876
|
+
* string if the response or the header is missing.
|
|
877
|
+
*/
|
|
878
|
+
async function getCspHeader(response) {
|
|
879
|
+
return await response?.headerValue("content-security-policy") ?? "";
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Evaluates a `Content-Security-Policy` header against a set of recommended
|
|
883
|
+
* directives and returns the result as a Markdown report.
|
|
884
|
+
*
|
|
885
|
+
* The report lists all directives of the header, followed by recommendations
|
|
886
|
+
* for missing or weak directives. Missing fetch directives are evaluated using
|
|
887
|
+
* `default-src`, just like the browser falls back to it. Nonces are masked as
|
|
888
|
+
* `[NONCE_0]`, `[NONCE_1]`, … to keep the report deterministic for file snapshots.
|
|
889
|
+
*
|
|
890
|
+
* @example
|
|
891
|
+
* ```ts
|
|
892
|
+
* const response = await page.goto("/");
|
|
893
|
+
* const cspHeader = await getCspHeader(response);
|
|
894
|
+
*
|
|
895
|
+
* expect(cspReport(cspHeader)).toMatchTextFile({
|
|
896
|
+
* fileExtension: "md",
|
|
897
|
+
* });
|
|
898
|
+
* ```
|
|
899
|
+
*/
|
|
900
|
+
function cspReport(header) {
|
|
901
|
+
const { directives, recommendations } = evaluateCsp(parseCsp(header));
|
|
902
|
+
return [
|
|
903
|
+
"# CSP Report",
|
|
904
|
+
"## Detected Directives",
|
|
905
|
+
directives.length === 0 ? "None" : markdownTable([["Directive", "Value"], ...maskNonces(directives).map((directive) => [code(directive.name), formatValues(directive.values)])]),
|
|
906
|
+
"## Recommendations",
|
|
907
|
+
recommendations.length === 0 ? "None" : recommendations.map((recommendation) => `- ${code(recommendation.directive)}: ${recommendation.message}`).join("\n")
|
|
908
|
+
].join("\n\n").concat("\n");
|
|
909
|
+
}
|
|
910
|
+
function parseCsp(header) {
|
|
911
|
+
return header.split(";").map((directive) => directive.trim()).filter((directive) => directive !== "").map((directive) => {
|
|
912
|
+
const [name = "", ...values] = directive.split(/\s+/);
|
|
913
|
+
return {
|
|
914
|
+
name: name.toLowerCase(),
|
|
915
|
+
values
|
|
916
|
+
};
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
function evaluateCsp(directives) {
|
|
920
|
+
const directivesByName = /* @__PURE__ */ new Map();
|
|
921
|
+
for (const directive of directives) if (!directivesByName.has(directive.name)) directivesByName.set(directive.name, directive);
|
|
922
|
+
return {
|
|
923
|
+
directives,
|
|
924
|
+
recommendations: RECOMMENDED_DIRECTIVES.flatMap((recommended) => evaluateDirective(recommended, directivesByName).map((message) => ({
|
|
925
|
+
directive: recommended.name,
|
|
926
|
+
message
|
|
927
|
+
})))
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
function evaluateDirective(recommended, directivesByName) {
|
|
931
|
+
const directive = directivesByName.get(recommended.name);
|
|
932
|
+
if (directive === void 0 && isOptional(recommended, directivesByName)) return [];
|
|
933
|
+
const fallback = recommended.fallback !== void 0 ? directivesByName.get(recommended.fallback) : void 0;
|
|
934
|
+
const actual = directive ?? fallback;
|
|
935
|
+
if (actual === void 0) return [`Missing, add ${code([recommended.name, ...formatRecommendedValues(recommended)].join(" "))}`];
|
|
936
|
+
const inherited = directive === void 0 ? ` (inherited from ${code(actual.name)})` : "";
|
|
937
|
+
const messages = [];
|
|
938
|
+
const recommendedValues = recommended.values;
|
|
939
|
+
const extraValues = findExtraValues(recommended, actual.values);
|
|
940
|
+
const hasRecommendedValue = extraValues.length < actual.values.length;
|
|
941
|
+
if (recommendedValues !== void 0 && extraValues.length > 0 && !hasRecommendedValue) messages.push(`Add ${formatValues(recommendedValues)}`);
|
|
942
|
+
if (recommended.requireNonce === true && !actual.values.some(isNonce)) messages.push(`Add a nonce source`);
|
|
943
|
+
for (const value of extraValues) messages.push(`Remove ${code(value)}${inherited}`);
|
|
944
|
+
return messages;
|
|
945
|
+
}
|
|
946
|
+
function isOptional(recommended, directivesByName) {
|
|
947
|
+
if (Array.isArray(recommended.optional)) return recommended.optional.every((name) => directivesByName.has(name));
|
|
948
|
+
return recommended.optional === true;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Returns all sources which are neither recommended nor tolerated. Directives
|
|
952
|
+
* without recommended sources accept any value, and `'none'` is always
|
|
953
|
+
* accepted as it is stricter than any recommendation.
|
|
954
|
+
*/
|
|
955
|
+
function findExtraValues(recommended, values) {
|
|
956
|
+
if (recommended.values === void 0 && recommended.requireNonce !== true || isNone(values)) return [];
|
|
957
|
+
const acceptedValues = [...recommended.values ?? [], ...recommended.tolerated ?? []];
|
|
958
|
+
return values.filter((value) => !acceptedValues.includes(value) && !(recommended.requireNonce === true && isNonce(value)));
|
|
959
|
+
}
|
|
960
|
+
function formatRecommendedValues(recommended) {
|
|
961
|
+
return [...recommended.requireNonce === true ? [NONCE_PLACEHOLDER] : [], ...recommended.values ?? []];
|
|
962
|
+
}
|
|
963
|
+
function maskNonces(directives) {
|
|
964
|
+
const nonceIndices = /* @__PURE__ */ new Map();
|
|
965
|
+
function maskNonce(value) {
|
|
966
|
+
if (!isNonce(value)) return value;
|
|
967
|
+
let index = nonceIndices.get(value);
|
|
968
|
+
if (index === void 0) {
|
|
969
|
+
index = nonceIndices.size;
|
|
970
|
+
nonceIndices.set(value, index);
|
|
971
|
+
}
|
|
972
|
+
return `'nonce-${MASKED_NONCE(index)}'`;
|
|
973
|
+
}
|
|
974
|
+
return directives.map((directive) => ({
|
|
975
|
+
...directive,
|
|
976
|
+
values: directive.values.map(maskNonce)
|
|
977
|
+
}));
|
|
978
|
+
}
|
|
979
|
+
function isNone(values) {
|
|
980
|
+
return values.length === 1 && values[0] === "'none'";
|
|
981
|
+
}
|
|
982
|
+
function isNonce(value) {
|
|
983
|
+
return value.startsWith("'nonce-");
|
|
984
|
+
}
|
|
985
|
+
function formatValues(values) {
|
|
986
|
+
if (values.length === 0) return "_none_";
|
|
987
|
+
return code(values.join(" "));
|
|
988
|
+
}
|
|
989
|
+
function code(text) {
|
|
990
|
+
return `\`${text.replaceAll("|", "\\|")}\``;
|
|
991
|
+
}
|
|
992
|
+
//#endregion
|
|
773
993
|
//#region src/file.ts
|
|
774
994
|
/**
|
|
775
995
|
* Resolves the given path segments against the root directory of the closest
|
|
@@ -789,7 +1009,81 @@ function resolveFromPackageRoot(...paths) {
|
|
|
789
1009
|
return path.resolve(packageDir, ...paths);
|
|
790
1010
|
}
|
|
791
1011
|
//#endregion
|
|
792
|
-
//#region src/page-object-model/
|
|
1012
|
+
//#region src/page-object-model/form-filler.ts
|
|
1013
|
+
/**
|
|
1014
|
+
* Creates a filler function that fills a form declaratively from a data
|
|
1015
|
+
* object, e.g. as the `fill` method of a page object.
|
|
1016
|
+
*
|
|
1017
|
+
* Each key of `fields` maps a key of the data to a form input: either an
|
|
1018
|
+
* object with a `fill` method (such as a `Locator` or a nested page object) or
|
|
1019
|
+
* a filler function receiving the value. Fields are filled one after another
|
|
1020
|
+
* in the order of the keys in `fields`, and fields whose value is `undefined`
|
|
1021
|
+
* are skipped.
|
|
1022
|
+
*
|
|
1023
|
+
* The type of the data is either given explicitly as type argument or inferred
|
|
1024
|
+
* from `fields`. When inferred, filler functions need an explicit parameter
|
|
1025
|
+
* type, otherwise their value is `unknown`. In both cases, the returned
|
|
1026
|
+
* function accepts partial data.
|
|
1027
|
+
*
|
|
1028
|
+
* As the returned function is a filler itself, it can be used as a field of
|
|
1029
|
+
* another form filler to fill nested data.
|
|
1030
|
+
*
|
|
1031
|
+
* @param fields - Mapping from each key of the data to the input to fill
|
|
1032
|
+
* @returns A function filling the form with the given data
|
|
1033
|
+
* @example
|
|
1034
|
+
* ```ts
|
|
1035
|
+
* const { checkbox, textbox } = roleLocators(page);
|
|
1036
|
+
*
|
|
1037
|
+
* const registrationForm = {
|
|
1038
|
+
* fill: formFiller<Registration>({
|
|
1039
|
+
* name: textbox("Name"),
|
|
1040
|
+
* acceptTerms: (accepted) => checkbox("Accept terms").setChecked(accepted),
|
|
1041
|
+
* }),
|
|
1042
|
+
* };
|
|
1043
|
+
* ```
|
|
1044
|
+
*/
|
|
1045
|
+
function formFiller(fields) {
|
|
1046
|
+
return async (data) => {
|
|
1047
|
+
for (const [key, filler] of Object.entries(fields)) {
|
|
1048
|
+
const value = data[key];
|
|
1049
|
+
if (value === void 0) continue;
|
|
1050
|
+
if (typeof filler === "function") await filler(value);
|
|
1051
|
+
else await filler.fill(value);
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
//#endregion
|
|
1056
|
+
//#region src/page-object-model/extend-locator.ts
|
|
1057
|
+
/**
|
|
1058
|
+
* Extends a `Locator` with custom properties and functions while retaining the full `Locator` API.
|
|
1059
|
+
*
|
|
1060
|
+
* Extensions must not override members of `Locator`. Locators derived from the extended locator
|
|
1061
|
+
* (e.g. via `first()`, `filter()` or `locator()`) are plain `Locator`s without the extensions.
|
|
1062
|
+
*
|
|
1063
|
+
* Methods of `Locator` are bound to the original locator on every access, so accessing the same method
|
|
1064
|
+
* twice returns different function instances (e.g. `extended.click !== extended.click`).
|
|
1065
|
+
*
|
|
1066
|
+
* @param locator The locator to extend
|
|
1067
|
+
* @param createExtensions Creates the extensions, receiving the original locator
|
|
1068
|
+
*/
|
|
1069
|
+
function extendLocator(locator, createExtensions) {
|
|
1070
|
+
const extensions = createExtensions(locator);
|
|
1071
|
+
return new Proxy(locator, {
|
|
1072
|
+
get(target, property) {
|
|
1073
|
+
if (isOwnKeyOf(extensions, property)) return extensions[property];
|
|
1074
|
+
const value = Reflect.get(target, property, target);
|
|
1075
|
+
return typeof value === "function" && property !== "constructor" ? value.bind(target) : value;
|
|
1076
|
+
},
|
|
1077
|
+
has(target, property) {
|
|
1078
|
+
return isOwnKeyOf(extensions, property) || Reflect.has(target, property);
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
function isOwnKeyOf(object, key) {
|
|
1083
|
+
return Object.hasOwn(object, key);
|
|
1084
|
+
}
|
|
1085
|
+
//#endregion
|
|
1086
|
+
//#region src/page-object-model/role-locators.ts
|
|
793
1087
|
const ROLES = [
|
|
794
1088
|
"alert",
|
|
795
1089
|
"alertdialog",
|
|
@@ -916,26 +1210,6 @@ function roleLocators(locator, defaultOptions = DEFAULT_OPTIONS) {
|
|
|
916
1210
|
return Object.fromEntries(ROLES.map((role) => [role, getByRole(role)]));
|
|
917
1211
|
}
|
|
918
1212
|
//#endregion
|
|
919
|
-
//#region src/normalizers/masked-value.ts
|
|
920
|
-
/**
|
|
921
|
-
* Formats a name as a masked placeholder, e.g. `maskedValue("base url")`
|
|
922
|
-
* returns `[BASE_URL]`.
|
|
923
|
-
*
|
|
924
|
-
* @param name - Name of the masked value
|
|
925
|
-
*/
|
|
926
|
-
function maskedValue(name) {
|
|
927
|
-
return `[${name.toUpperCase().replace(/[ -.:,;]/g, "_")}]`;
|
|
928
|
-
}
|
|
929
|
-
/**
|
|
930
|
-
* Creates a factory for indexed masked placeholders, e.g. `[USER_1]`.
|
|
931
|
-
*
|
|
932
|
-
* @param name - Name of the masked value
|
|
933
|
-
* @returns A function mapping an index to a masked placeholder
|
|
934
|
-
*/
|
|
935
|
-
function maskedValueWithIndex(name) {
|
|
936
|
-
return (index) => maskedValue(`${name}_${index}`);
|
|
937
|
-
}
|
|
938
|
-
//#endregion
|
|
939
1213
|
//#region src/normalizers/mask-base-url.ts
|
|
940
1214
|
/**
|
|
941
1215
|
* Creates a normalizer that replaces occurrences of the base URL with
|
|
@@ -951,4 +1225,4 @@ function identity(value) {
|
|
|
951
1225
|
return value;
|
|
952
1226
|
}
|
|
953
1227
|
//#endregion
|
|
954
|
-
export { ConsoleCaptor, CspCaptor, RouteInterceptor, RouteInterceptorFixture, captureConsole, captureCspViolations, createFetchAdapter, cspFixtures, hasLogLevel, interceptRoute, isCI, maskBaseURL, maskedValue, maskedValueWithIndex, matchPath, modifyJsonBody, modifyTextBody, resolveFromPackageRoot, roleLocators };
|
|
1228
|
+
export { ConsoleCaptor, CspCaptor, RouteInterceptor, RouteInterceptorFixture, captureConsole, captureCspViolations, createFetchAdapter, cspFixtures, cspReport, extendLocator, formFiller, getCspHeader, 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.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Utilities for writing tests with Playwright",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"playwright",
|
|
@@ -29,33 +29,34 @@
|
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
+
"markdown-table": "^3.0.4",
|
|
32
33
|
"package-directory": "^8.2.0"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
36
|
"@arethetypeswrong/core": "0.18.5",
|
|
36
|
-
"@changesets/cli": "3.0.
|
|
37
|
-
"@cronn/element-snapshot": "0.
|
|
37
|
+
"@changesets/cli": "3.0.3",
|
|
38
|
+
"@cronn/element-snapshot": "0.32.1",
|
|
38
39
|
"@cronn/playwright-file-snapshots": "2.2.2",
|
|
39
40
|
"@playwright/test": "1.63.0",
|
|
40
|
-
"@types/node": "24.
|
|
41
|
+
"@types/node": "24.19.1",
|
|
41
42
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
42
|
-
"@vitest/coverage-v8": "5.0.
|
|
43
|
-
"eslint": "10.
|
|
43
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
44
|
+
"eslint": "10.12.0",
|
|
44
45
|
"eslint-config-prettier": "10.1.8",
|
|
45
46
|
"eslint-plugin-check-file": "3.3.2",
|
|
46
47
|
"eslint-plugin-unused-imports": "4.4.1",
|
|
47
|
-
"knip": "6.
|
|
48
|
-
"oxc-minify": "0.
|
|
49
|
-
"oxfmt": "0.
|
|
50
|
-
"publint": "0.3.
|
|
48
|
+
"knip": "6.39.0",
|
|
49
|
+
"oxc-minify": "0.153.0",
|
|
50
|
+
"oxfmt": "0.72.0",
|
|
51
|
+
"publint": "0.3.25",
|
|
51
52
|
"rimraf": "6.1.3",
|
|
52
53
|
"tsdown": "0.23.0",
|
|
53
|
-
"turbo": "2.
|
|
54
|
+
"turbo": "2.11.7",
|
|
54
55
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
55
|
-
"typescript-eslint": "8.
|
|
56
|
+
"typescript-eslint": "8.71.1",
|
|
56
57
|
"vitepress": "2.0.0-alpha.20",
|
|
57
58
|
"vitepress-plugin-group-icons": "1.7.6",
|
|
58
|
-
"vitest": "5.0.
|
|
59
|
+
"vitest": "5.0.3"
|
|
59
60
|
},
|
|
60
61
|
"peerDependencies": {
|
|
61
62
|
"@cronn/playwright-file-snapshots": "^2",
|
|
@@ -64,7 +65,7 @@
|
|
|
64
65
|
"devEngines": {
|
|
65
66
|
"packageManager": {
|
|
66
67
|
"name": "pnpm",
|
|
67
|
-
"version": "12.
|
|
68
|
+
"version": "12.11.2",
|
|
68
69
|
"onFail": "download"
|
|
69
70
|
},
|
|
70
71
|
"runtime": {
|
|
@@ -90,9 +91,7 @@
|
|
|
90
91
|
"lint:check": "eslint . --max-warnings=0",
|
|
91
92
|
"lint:fix": "eslint . --max-warnings=0 --fix",
|
|
92
93
|
"playwright:install": "playwright install chromium",
|
|
93
|
-
"pnpm:check": "pnpm dedupe --check",
|
|
94
94
|
"pnpm:clean": "pnpm pm clean && pnpm store prune",
|
|
95
|
-
"pnpm:fix": "pnpm dedupe",
|
|
96
95
|
"release": "pnpm turbo run build && pnpm changeset publish",
|
|
97
96
|
"test:integration": "playwright test",
|
|
98
97
|
"test:unit": "vitest run",
|