@cronn/playwright-utils 0.8.0 → 0.10.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
@@ -591,6 +591,65 @@ 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
621
+ //#region src/security/response-header-report.d.ts
622
+ type ResponseHeaderType = "document" | "api";
623
+ interface ResponseHeaderReportOptions {
624
+ type: ResponseHeaderType;
625
+ }
626
+ /**
627
+ * Returns all headers of a response, or an empty object if the response is
628
+ * missing. For page responses, this uses `response.allHeaders()`, which in
629
+ * contrast to `response.headers()` includes security-related headers.
630
+ */
631
+ export declare function getResponseHeaders(response: Response | APIResponse | null): Promise<Record<string, string>>;
632
+ /**
633
+ * Evaluates the headers of a response against a set of recommended security
634
+ * headers and returns the result as a Markdown report.
635
+ *
636
+ * The report lists all evaluated headers of the response, followed by
637
+ * recommendations for missing or deviating headers and for discouraged headers
638
+ * to remove. Unrelated headers like `date` are omitted to keep the report
639
+ * deterministic for file snapshots.
640
+ *
641
+ * @example
642
+ * ```ts
643
+ * const response = await page.goto("/");
644
+ * const headers = await getResponseHeaders(response);
645
+ *
646
+ * expect(responseHeaderReport(headers, { type: "document" })).toMatchTextFile({
647
+ * fileExtension: "md",
648
+ * });
649
+ * ```
650
+ */
651
+ export declare function responseHeaderReport(headers: Record<string, string>, options: ResponseHeaderReportOptions): string;
652
+ //#endregion
594
653
  //#region src/file.d.ts
595
654
  /**
596
655
  * Resolves the given path segments against the root directory of the closest
@@ -730,4 +789,4 @@ export declare function maskedValueWithIndex(name: string): (index: number) => s
730
789
  */
731
790
  type PlaywrightTarget = Page | Locator;
732
791
  //#endregion
733
- export type { ConsoleMessageFilter, ConsoleMessageLevel, CspFixtures, CspViolation, ExtendedLocator, FormFields, HttpMethod, PlaywrightTarget, ResponseHandler, RoleLocators, RouteFilter };
792
+ export type { ConsoleMessageFilter, ConsoleMessageLevel, CspFixtures, CspViolation, ExtendedLocator, FormFields, HttpMethod, PlaywrightTarget, ResponseHandler, ResponseHeaderReportOptions, ResponseHeaderType, 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,420 @@ 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/utils/markdown.ts
795
+ const NONE = "None";
796
+ /**
797
+ * Creates a Markdown document with a top-level title and a second-level
798
+ * heading for each section.
799
+ */
800
+ function markdownDocument(title, sections) {
801
+ return [`# ${title}`, ...sections.flatMap((section) => [`## ${section.heading}`, section.content])].join("\n\n").concat("\n");
802
+ }
803
+ /**
804
+ * Creates a Markdown table, or `None` if there are no rows.
805
+ */
806
+ function markdownTableOrNone(header, rows) {
807
+ return rows.length === 0 ? NONE : markdownTable([header, ...rows]);
808
+ }
809
+ /**
810
+ * Creates a Markdown list with an item per entry formatted as
811
+ * `` `name`: message ``, or `None` if there are no entries.
812
+ */
813
+ function markdownEntriesOrNone(entries) {
814
+ return entries.length === 0 ? NONE : entries.map((entry) => `- ${code(entry.name)}: ${entry.message}`).join("\n");
815
+ }
816
+ /**
817
+ * Formats a value as inline code, or as `_none_` if the value is blank.
818
+ */
819
+ function codeOrNone(text) {
820
+ return text.trim() === "" ? "_none_" : code(text);
821
+ }
822
+ /**
823
+ * Formats a text as inline code. Pipes are escaped to keep tables intact.
824
+ */
825
+ function code(text) {
826
+ return `\`${text.replaceAll("|", "\\|")}\``;
827
+ }
828
+ //#endregion
829
+ //#region src/security/csp-report.ts
830
+ const RECOMMENDED_DIRECTIVES = [
831
+ { name: "upgrade-insecure-requests" },
832
+ { name: "trusted-types" },
833
+ {
834
+ name: "require-trusted-types-for",
835
+ values: ["'script'"]
836
+ },
837
+ {
838
+ name: "default-src",
839
+ values: ["'self'"]
840
+ },
841
+ {
842
+ name: "script-src",
843
+ requireNonce: true,
844
+ tolerated: ["'strict-dynamic'"],
845
+ fallback: "default-src",
846
+ optional: ["script-src-elem", "script-src-attr"]
847
+ },
848
+ {
849
+ name: "script-src-elem",
850
+ requireNonce: true,
851
+ tolerated: ["'strict-dynamic'"],
852
+ optional: true
853
+ },
854
+ {
855
+ name: "script-src-attr",
856
+ values: ["'none'"],
857
+ optional: true
858
+ },
859
+ {
860
+ name: "style-src",
861
+ requireNonce: true,
862
+ fallback: "default-src",
863
+ optional: ["style-src-elem", "style-src-attr"]
864
+ },
865
+ {
866
+ name: "style-src-elem",
867
+ requireNonce: true,
868
+ optional: true
869
+ },
870
+ {
871
+ name: "style-src-attr",
872
+ values: ["'none'"],
873
+ optional: true
874
+ },
875
+ {
876
+ name: "img-src",
877
+ values: ["'self'", "data:"],
878
+ fallback: "default-src"
879
+ },
880
+ {
881
+ name: "font-src",
882
+ values: ["'self'"],
883
+ fallback: "default-src"
884
+ },
885
+ {
886
+ name: "connect-src",
887
+ values: ["'self'"],
888
+ fallback: "default-src"
889
+ },
890
+ {
891
+ name: "object-src",
892
+ values: ["'none'"]
893
+ },
894
+ {
895
+ name: "base-uri",
896
+ values: ["'self'"]
897
+ },
898
+ {
899
+ name: "form-action",
900
+ values: ["'self'"]
901
+ },
902
+ {
903
+ name: "frame-ancestors",
904
+ values: ["'none'"]
905
+ }
906
+ ];
907
+ const NONCE_PLACEHOLDER = "'nonce-[RANDOM]'";
908
+ const MASKED_NONCE = maskedValueWithIndex("NONCE");
909
+ /**
910
+ * Returns the `Content-Security-Policy` header of a response, or an empty
911
+ * string if the response or the header is missing.
912
+ */
913
+ async function getCspHeader(response) {
914
+ return await response?.headerValue("content-security-policy") ?? "";
915
+ }
916
+ /**
917
+ * Evaluates a `Content-Security-Policy` header against a set of recommended
918
+ * directives and returns the result as a Markdown report.
919
+ *
920
+ * The report lists all directives of the header, followed by recommendations
921
+ * for missing or weak directives. Missing fetch directives are evaluated using
922
+ * `default-src`, just like the browser falls back to it. Nonces are masked as
923
+ * `[NONCE_0]`, `[NONCE_1]`, … to keep the report deterministic for file snapshots.
924
+ *
925
+ * @example
926
+ * ```ts
927
+ * const response = await page.goto("/");
928
+ * const cspHeader = await getCspHeader(response);
929
+ *
930
+ * expect(cspReport(cspHeader)).toMatchTextFile({
931
+ * fileExtension: "md",
932
+ * });
933
+ * ```
934
+ */
935
+ function cspReport(header) {
936
+ const { directives, recommendations } = evaluateCsp(parseCsp(header));
937
+ return markdownDocument("CSP Report", [{
938
+ heading: "Detected Directives",
939
+ content: markdownTableOrNone(["Directive", "Value"], maskNonces(directives).map((directive) => [code(directive.name), formatValues(directive.values)]))
940
+ }, {
941
+ heading: "Recommendations",
942
+ content: markdownEntriesOrNone(recommendations)
943
+ }]);
944
+ }
945
+ /**
946
+ * Masks all nonce sources of a `Content-Security-Policy` header as
947
+ * `'nonce-[NONCE_0]'`, `'nonce-[NONCE_1]'`, … and normalizes its formatting.
948
+ */
949
+ function maskCspNonces(header) {
950
+ return maskNonces(parseCsp(header)).map((directive) => [directive.name, ...directive.values].join(" ")).join("; ");
951
+ }
952
+ function parseCsp(header) {
953
+ return header.split(";").map((directive) => directive.trim()).filter((directive) => directive !== "").map((directive) => {
954
+ const [name = "", ...values] = directive.split(/\s+/);
955
+ return {
956
+ name: name.toLowerCase(),
957
+ values
958
+ };
959
+ });
960
+ }
961
+ function evaluateCsp(directives) {
962
+ const directivesByName = /* @__PURE__ */ new Map();
963
+ for (const directive of directives) if (!directivesByName.has(directive.name)) directivesByName.set(directive.name, directive);
964
+ return {
965
+ directives,
966
+ recommendations: RECOMMENDED_DIRECTIVES.flatMap((recommended) => evaluateDirective(recommended, directivesByName).map((message) => ({
967
+ name: recommended.name,
968
+ message
969
+ })))
970
+ };
971
+ }
972
+ function evaluateDirective(recommended, directivesByName) {
973
+ const directive = directivesByName.get(recommended.name);
974
+ if (directive === void 0 && isOptional(recommended, directivesByName)) return [];
975
+ const fallback = recommended.fallback !== void 0 ? directivesByName.get(recommended.fallback) : void 0;
976
+ const actual = directive ?? fallback;
977
+ if (actual === void 0) return [`Missing, add ${code([recommended.name, ...formatRecommendedValues(recommended)].join(" "))}`];
978
+ const inherited = directive === void 0 ? ` (inherited from ${code(actual.name)})` : "";
979
+ const messages = [];
980
+ const recommendedValues = recommended.values;
981
+ const extraValues = findExtraValues(recommended, actual.values);
982
+ const hasRecommendedValue = extraValues.length < actual.values.length;
983
+ if (recommendedValues !== void 0 && extraValues.length > 0 && !hasRecommendedValue) messages.push(`Add ${formatValues(recommendedValues)}`);
984
+ if (recommended.requireNonce === true && !actual.values.some(isNonce)) messages.push(`Add a nonce source`);
985
+ for (const value of extraValues) messages.push(`Remove ${code(value)}${inherited}`);
986
+ return messages;
987
+ }
988
+ function isOptional(recommended, directivesByName) {
989
+ if (Array.isArray(recommended.optional)) return recommended.optional.every((name) => directivesByName.has(name));
990
+ return recommended.optional === true;
991
+ }
992
+ /**
993
+ * Returns all sources which are neither recommended nor tolerated. Directives
994
+ * without recommended sources accept any value, and `'none'` is always
995
+ * accepted as it is stricter than any recommendation.
996
+ */
997
+ function findExtraValues(recommended, values) {
998
+ if (recommended.values === void 0 && recommended.requireNonce !== true || isNone(values)) return [];
999
+ const acceptedValues = [...recommended.values ?? [], ...recommended.tolerated ?? []];
1000
+ return values.filter((value) => !acceptedValues.includes(value) && !(recommended.requireNonce === true && isNonce(value)));
1001
+ }
1002
+ function formatRecommendedValues(recommended) {
1003
+ return [...recommended.requireNonce === true ? [NONCE_PLACEHOLDER] : [], ...recommended.values ?? []];
1004
+ }
1005
+ function maskNonces(directives) {
1006
+ const nonceIndices = /* @__PURE__ */ new Map();
1007
+ function maskNonce(value) {
1008
+ if (!isNonce(value)) return value;
1009
+ let index = nonceIndices.get(value);
1010
+ if (index === void 0) {
1011
+ index = nonceIndices.size;
1012
+ nonceIndices.set(value, index);
1013
+ }
1014
+ return `'nonce-${MASKED_NONCE(index)}'`;
1015
+ }
1016
+ return directives.map((directive) => ({
1017
+ ...directive,
1018
+ values: directive.values.map(maskNonce)
1019
+ }));
1020
+ }
1021
+ function isNone(values) {
1022
+ return values.length === 1 && values[0] === "'none'";
1023
+ }
1024
+ function isNonce(value) {
1025
+ return value.startsWith("'nonce-");
1026
+ }
1027
+ function formatValues(values) {
1028
+ return codeOrNone(values.join(" "));
1029
+ }
1030
+ //#endregion
1031
+ //#region src/security/response-header-report.ts
1032
+ const ACTION_ORDER = [
1033
+ "add",
1034
+ "change",
1035
+ "remove"
1036
+ ];
1037
+ const MIN_HSTS_MAX_AGE = 31536e3;
1038
+ const PERMISSIONS_POLICY = "geolocation=(), microphone=(), camera=()";
1039
+ const RECOMMENDED_COMMON_HEADERS = [
1040
+ { name: "cache-control" },
1041
+ { name: "content-security-policy" },
1042
+ { name: "content-type" },
1043
+ {
1044
+ name: "cross-origin-opener-policy",
1045
+ recommended: "same-origin"
1046
+ },
1047
+ {
1048
+ name: "cross-origin-resource-policy",
1049
+ recommended: "same-origin"
1050
+ },
1051
+ {
1052
+ name: "strict-transport-security",
1053
+ recommended: `max-age=${MIN_HSTS_MAX_AGE}; includeSubDomains`,
1054
+ isAccepted: isStrictHsts
1055
+ },
1056
+ {
1057
+ name: "x-content-type-options",
1058
+ recommended: "nosniff"
1059
+ },
1060
+ { name: "x-robots-tag" }
1061
+ ];
1062
+ const DISCOURAGED_COMMON_HEADERS = ["access-control-allow-origin"];
1063
+ const RECOMMENDED_DOCUMENT_HEADERS = [
1064
+ ...RECOMMENDED_COMMON_HEADERS,
1065
+ {
1066
+ name: "permissions-policy",
1067
+ recommended: PERMISSIONS_POLICY,
1068
+ isAccepted: isRestrictivePermissionsPolicy
1069
+ },
1070
+ {
1071
+ name: "referrer-policy",
1072
+ recommended: "no-referrer"
1073
+ },
1074
+ {
1075
+ name: "x-frame-options",
1076
+ recommended: "DENY"
1077
+ }
1078
+ ];
1079
+ const DISCOURAGED_DOCUMENT_HEADERS = [...DISCOURAGED_COMMON_HEADERS, "x-xss-protection"];
1080
+ const RECOMMENDED_HEADERS = {
1081
+ document: RECOMMENDED_DOCUMENT_HEADERS,
1082
+ api: RECOMMENDED_COMMON_HEADERS
1083
+ };
1084
+ const DISCOURAGED_HEADERS = {
1085
+ document: DISCOURAGED_DOCUMENT_HEADERS,
1086
+ api: DISCOURAGED_COMMON_HEADERS
1087
+ };
1088
+ /**
1089
+ * Returns all headers of a response, or an empty object if the response is
1090
+ * missing. For page responses, this uses `response.allHeaders()`, which in
1091
+ * contrast to `response.headers()` includes security-related headers.
1092
+ */
1093
+ async function getResponseHeaders(response) {
1094
+ if (response === null) return {};
1095
+ return "allHeaders" in response ? await response.allHeaders() : response.headers();
1096
+ }
1097
+ /**
1098
+ * Evaluates the headers of a response against a set of recommended security
1099
+ * headers and returns the result as a Markdown report.
1100
+ *
1101
+ * The report lists all evaluated headers of the response, followed by
1102
+ * recommendations for missing or deviating headers and for discouraged headers
1103
+ * to remove. Unrelated headers like `date` are omitted to keep the report
1104
+ * deterministic for file snapshots.
1105
+ *
1106
+ * @example
1107
+ * ```ts
1108
+ * const response = await page.goto("/");
1109
+ * const headers = await getResponseHeaders(response);
1110
+ *
1111
+ * expect(responseHeaderReport(headers, { type: "document" })).toMatchTextFile({
1112
+ * fileExtension: "md",
1113
+ * });
1114
+ * ```
1115
+ */
1116
+ function responseHeaderReport(headers, options) {
1117
+ const recommendedHeaders = RECOMMENDED_HEADERS[options.type];
1118
+ const discouragedHeaders = DISCOURAGED_HEADERS[options.type];
1119
+ const headersByName = new Map(Object.entries(headers).map(([name, value]) => [name.toLowerCase(), value]));
1120
+ const detectedHeaders = [...recommendedHeaders.map((recommended) => recommended.name), ...discouragedHeaders].flatMap((name) => {
1121
+ const value = headersByName.get(name);
1122
+ return value === void 0 ? [] : [{
1123
+ name,
1124
+ value
1125
+ }];
1126
+ });
1127
+ const recommendations = [...recommendedHeaders.flatMap((recommended) => evaluateHeader(recommended, headersByName.get(recommended.name))), ...discouragedHeaders.filter((name) => headersByName.has(name)).map((name) => ({
1128
+ name,
1129
+ action: "remove",
1130
+ message: "Remove header"
1131
+ }))].sort((a, b) => ACTION_ORDER.indexOf(a.action) - ACTION_ORDER.indexOf(b.action));
1132
+ return markdownDocument("Response Header Report", [{
1133
+ heading: "Detected Headers",
1134
+ content: markdownTableOrNone(["Header", "Value"], detectedHeaders.map((header) => [code(header.name), formatValue(header)]))
1135
+ }, {
1136
+ heading: "Recommendations",
1137
+ content: markdownEntriesOrNone(recommendations)
1138
+ }]);
1139
+ }
1140
+ function evaluateHeader(recommended, value) {
1141
+ if (value === void 0) return [{
1142
+ name: recommended.name,
1143
+ action: "add",
1144
+ message: recommended.recommended === void 0 ? "Missing, add header" : `Missing, add ${code(recommended.recommended)}`
1145
+ }];
1146
+ if (recommended.recommended === void 0 || isAcceptedValue(recommended, value)) return [];
1147
+ return [{
1148
+ name: recommended.name,
1149
+ action: "change",
1150
+ message: `Change value to ${code(recommended.recommended)}`
1151
+ }];
1152
+ }
1153
+ /**
1154
+ * Accepts a value if it passes the custom check of the header, or otherwise
1155
+ * if it equals the recommended value ignoring case and whitespace.
1156
+ */
1157
+ function isAcceptedValue(recommended, value) {
1158
+ if (recommended.isAccepted !== void 0) return recommended.isAccepted(value);
1159
+ return recommended.recommended === void 0 || normalizeValue(value) === normalizeValue(recommended.recommended);
1160
+ }
1161
+ function isStrictHsts(value) {
1162
+ const directives = normalizeValue(value).split("; ");
1163
+ const maxAge = directives.find((directive) => directive.startsWith("max-age="))?.slice(8).replaceAll("\"", "");
1164
+ return maxAge !== void 0 && Number(maxAge) >= MIN_HSTS_MAX_AGE && directives.includes("includesubdomains");
1165
+ }
1166
+ /**
1167
+ * Accepts a policy if it restricts at least the recommended permissions, so
1168
+ * stricter policies restricting further permissions are accepted as well.
1169
+ */
1170
+ function isRestrictivePermissionsPolicy(value) {
1171
+ const actualPermissions = parsePermissionsPolicy(value);
1172
+ return Array.from(parsePermissionsPolicy(PERMISSIONS_POLICY)).every((permission) => actualPermissions.has(permission));
1173
+ }
1174
+ function parsePermissionsPolicy(value) {
1175
+ return new Set(value.split(",").map(normalizeValue));
1176
+ }
1177
+ /**
1178
+ * Normalizes a header value for comparison by ignoring case, whitespace and
1179
+ * empty `;`-separated parts.
1180
+ */
1181
+ function normalizeValue(value) {
1182
+ return value.toLowerCase().split(";").map((part) => part.trim().replaceAll(/\s+/g, " ")).filter((part) => part !== "").join("; ");
1183
+ }
1184
+ function formatValue(header) {
1185
+ return codeOrNone(header.name === "content-security-policy" ? maskCspNonces(header.value) : header.value);
1186
+ }
1187
+ //#endregion
773
1188
  //#region src/file.ts
774
1189
  /**
775
1190
  * Resolves the given path segments against the root directory of the closest
@@ -990,26 +1405,6 @@ function roleLocators(locator, defaultOptions = DEFAULT_OPTIONS) {
990
1405
  return Object.fromEntries(ROLES.map((role) => [role, getByRole(role)]));
991
1406
  }
992
1407
  //#endregion
993
- //#region src/normalizers/masked-value.ts
994
- /**
995
- * Formats a name as a masked placeholder, e.g. `maskedValue("base url")`
996
- * returns `[BASE_URL]`.
997
- *
998
- * @param name - Name of the masked value
999
- */
1000
- function maskedValue(name) {
1001
- return `[${name.toUpperCase().replace(/[ -.:,;]/g, "_")}]`;
1002
- }
1003
- /**
1004
- * Creates a factory for indexed masked placeholders, e.g. `[USER_1]`.
1005
- *
1006
- * @param name - Name of the masked value
1007
- * @returns A function mapping an index to a masked placeholder
1008
- */
1009
- function maskedValueWithIndex(name) {
1010
- return (index) => maskedValue(`${name}_${index}`);
1011
- }
1012
- //#endregion
1013
1408
  //#region src/normalizers/mask-base-url.ts
1014
1409
  /**
1015
1410
  * Creates a normalizer that replaces occurrences of the base URL with
@@ -1025,4 +1420,4 @@ function identity(value) {
1025
1420
  return value;
1026
1421
  }
1027
1422
  //#endregion
1028
- export { ConsoleCaptor, CspCaptor, RouteInterceptor, RouteInterceptorFixture, captureConsole, captureCspViolations, createFetchAdapter, cspFixtures, extendLocator, formFiller, hasLogLevel, interceptRoute, isCI, maskBaseURL, maskedValue, maskedValueWithIndex, matchPath, modifyJsonBody, modifyTextBody, resolveFromPackageRoot, roleLocators };
1423
+ export { ConsoleCaptor, CspCaptor, RouteInterceptor, RouteInterceptorFixture, captureConsole, captureCspViolations, createFetchAdapter, cspFixtures, cspReport, extendLocator, formFiller, getCspHeader, getResponseHeaders, hasLogLevel, interceptRoute, isCI, maskBaseURL, maskedValue, maskedValueWithIndex, matchPath, modifyJsonBody, modifyTextBody, resolveFromPackageRoot, responseHeaderReport, roleLocators };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cronn/playwright-utils",
3
- "version": "0.8.0",
3
+ "version": "0.10.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
37
  "@changesets/cli": "3.0.3",
37
- "@cronn/element-snapshot": "0.30.0",
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.19.0",
41
+ "@types/node": "24.19.1",
41
42
  "@typescript/native": "npm:typescript@^7.0.2",
42
- "@vitest/coverage-v8": "5.0.2",
43
- "eslint": "10.11.0",
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.38.0",
48
- "oxc-minify": "0.151.0",
49
- "oxfmt": "0.70.0",
50
- "publint": "0.3.24",
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.11.5",
54
+ "turbo": "2.11.7",
54
55
  "typescript": "npm:@typescript/typescript6@^6.0.2",
55
- "typescript-eslint": "8.70.1",
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.2"
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.9.1",
68
+ "version": "12.11.2",
68
69
  "onFail": "download"
69
70
  },
70
71
  "runtime": {