@plan2net/typo3-playwright-toolkit 0.1.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.
Files changed (164) hide show
  1. package/LICENSE +338 -0
  2. package/README.md +404 -0
  3. package/dist/builders/content-builder.d.ts +30 -0
  4. package/dist/builders/content-builder.d.ts.map +1 -0
  5. package/dist/builders/content-builder.js +67 -0
  6. package/dist/builders/content-builder.js.map +1 -0
  7. package/dist/builders/content-factory.d.ts +10 -0
  8. package/dist/builders/content-factory.d.ts.map +1 -0
  9. package/dist/builders/content-factory.js +23 -0
  10. package/dist/builders/content-factory.js.map +1 -0
  11. package/dist/builders/core-content.d.ts +127 -0
  12. package/dist/builders/core-content.d.ts.map +1 -0
  13. package/dist/builders/core-content.js +223 -0
  14. package/dist/builders/core-content.js.map +1 -0
  15. package/dist/builders/fields.d.ts +7 -0
  16. package/dist/builders/fields.d.ts.map +1 -0
  17. package/dist/builders/fields.js +20 -0
  18. package/dist/builders/fields.js.map +1 -0
  19. package/dist/builders/identifier.d.ts +2 -0
  20. package/dist/builders/identifier.d.ts.map +1 -0
  21. package/dist/builders/identifier.js +11 -0
  22. package/dist/builders/identifier.js.map +1 -0
  23. package/dist/builders/page-builder.d.ts +40 -0
  24. package/dist/builders/page-builder.d.ts.map +1 -0
  25. package/dist/builders/page-builder.js +119 -0
  26. package/dist/builders/page-builder.js.map +1 -0
  27. package/dist/builders/request-context.d.ts +23 -0
  28. package/dist/builders/request-context.d.ts.map +1 -0
  29. package/dist/builders/request-context.js +32 -0
  30. package/dist/builders/request-context.js.map +1 -0
  31. package/dist/checks/accessibility.d.ts +34 -0
  32. package/dist/checks/accessibility.d.ts.map +1 -0
  33. package/dist/checks/accessibility.js +90 -0
  34. package/dist/checks/accessibility.js.map +1 -0
  35. package/dist/checks/csp.d.ts +39 -0
  36. package/dist/checks/csp.d.ts.map +1 -0
  37. package/dist/checks/csp.js +179 -0
  38. package/dist/checks/csp.js.map +1 -0
  39. package/dist/checks/screenshot.d.ts +18 -0
  40. package/dist/checks/screenshot.d.ts.map +1 -0
  41. package/dist/checks/screenshot.js +125 -0
  42. package/dist/checks/screenshot.js.map +1 -0
  43. package/dist/config.d.ts +100 -0
  44. package/dist/config.d.ts.map +1 -0
  45. package/dist/config.js +68 -0
  46. package/dist/config.js.map +1 -0
  47. package/dist/contract.d.ts +9 -0
  48. package/dist/contract.d.ts.map +1 -0
  49. package/dist/contract.js +25 -0
  50. package/dist/contract.js.map +1 -0
  51. package/dist/global-setup.d.ts +20 -0
  52. package/dist/global-setup.d.ts.map +1 -0
  53. package/dist/global-setup.js +103 -0
  54. package/dist/global-setup.js.map +1 -0
  55. package/dist/global-teardown.d.ts +34 -0
  56. package/dist/global-teardown.d.ts.map +1 -0
  57. package/dist/global-teardown.js +269 -0
  58. package/dist/global-teardown.js.map +1 -0
  59. package/dist/http/api-secret.d.ts +11 -0
  60. package/dist/http/api-secret.d.ts.map +1 -0
  61. package/dist/http/api-secret.js +27 -0
  62. package/dist/http/api-secret.js.map +1 -0
  63. package/dist/http/cleanup-client.d.ts +26 -0
  64. package/dist/http/cleanup-client.d.ts.map +1 -0
  65. package/dist/http/cleanup-client.js +91 -0
  66. package/dist/http/cleanup-client.js.map +1 -0
  67. package/dist/http/off-site-headers.d.ts +13 -0
  68. package/dist/http/off-site-headers.d.ts.map +1 -0
  69. package/dist/http/off-site-headers.js +27 -0
  70. package/dist/http/off-site-headers.js.map +1 -0
  71. package/dist/http/prepare-context.d.ts +4 -0
  72. package/dist/http/prepare-context.d.ts.map +1 -0
  73. package/dist/http/prepare-context.js +6 -0
  74. package/dist/http/prepare-context.js.map +1 -0
  75. package/dist/http/record-edit.d.ts +37 -0
  76. package/dist/http/record-edit.d.ts.map +1 -0
  77. package/dist/http/record-edit.js +86 -0
  78. package/dist/http/record-edit.js.map +1 -0
  79. package/dist/http/toolkit-request.d.ts +8 -0
  80. package/dist/http/toolkit-request.d.ts.map +1 -0
  81. package/dist/http/toolkit-request.js +45 -0
  82. package/dist/http/toolkit-request.js.map +1 -0
  83. package/dist/index.d.ts +18 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +14 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/inspect/cli.d.ts +3 -0
  88. package/dist/inspect/cli.d.ts.map +1 -0
  89. package/dist/inspect/cli.js +36 -0
  90. package/dist/inspect/cli.js.map +1 -0
  91. package/dist/inspect/links.d.ts +10 -0
  92. package/dist/inspect/links.d.ts.map +1 -0
  93. package/dist/inspect/links.js +51 -0
  94. package/dist/inspect/links.js.map +1 -0
  95. package/dist/inspect/token.d.ts +5 -0
  96. package/dist/inspect/token.d.ts.map +1 -0
  97. package/dist/inspect/token.js +15 -0
  98. package/dist/inspect/token.js.map +1 -0
  99. package/dist/playwright/base-config.d.ts +4 -0
  100. package/dist/playwright/base-config.d.ts.map +1 -0
  101. package/dist/playwright/base-config.js +34 -0
  102. package/dist/playwright/base-config.js.map +1 -0
  103. package/dist/playwright/index.d.ts +3 -0
  104. package/dist/playwright/index.d.ts.map +1 -0
  105. package/dist/playwright/index.js +3 -0
  106. package/dist/playwright/index.js.map +1 -0
  107. package/dist/scenario.d.ts +46 -0
  108. package/dist/scenario.d.ts.map +1 -0
  109. package/dist/scenario.js +139 -0
  110. package/dist/scenario.js.map +1 -0
  111. package/dist/state/attempt-claim.d.ts +8 -0
  112. package/dist/state/attempt-claim.d.ts.map +1 -0
  113. package/dist/state/attempt-claim.js +47 -0
  114. package/dist/state/attempt-claim.js.map +1 -0
  115. package/dist/state/attempt-registry.d.ts +36 -0
  116. package/dist/state/attempt-registry.d.ts.map +1 -0
  117. package/dist/state/attempt-registry.js +68 -0
  118. package/dist/state/attempt-registry.js.map +1 -0
  119. package/dist/state/ensure-state.d.ts +42 -0
  120. package/dist/state/ensure-state.d.ts.map +1 -0
  121. package/dist/state/ensure-state.js +169 -0
  122. package/dist/state/ensure-state.js.map +1 -0
  123. package/dist/state/json-safe.d.ts +9 -0
  124. package/dist/state/json-safe.d.ts.map +1 -0
  125. package/dist/state/json-safe.js +18 -0
  126. package/dist/state/json-safe.js.map +1 -0
  127. package/dist/state/run-id.d.ts +6 -0
  128. package/dist/state/run-id.d.ts.map +1 -0
  129. package/dist/state/run-id.js +28 -0
  130. package/dist/state/run-id.js.map +1 -0
  131. package/dist/state/run-namespace.d.ts +33 -0
  132. package/dist/state/run-namespace.d.ts.map +1 -0
  133. package/dist/state/run-namespace.js +136 -0
  134. package/dist/state/run-namespace.js.map +1 -0
  135. package/dist/state/safe-paths.d.ts +10 -0
  136. package/dist/state/safe-paths.d.ts.map +1 -0
  137. package/dist/state/safe-paths.js +56 -0
  138. package/dist/state/safe-paths.js.map +1 -0
  139. package/dist/state/scenario-outcome.d.ts +9 -0
  140. package/dist/state/scenario-outcome.d.ts.map +1 -0
  141. package/dist/state/scenario-outcome.js +23 -0
  142. package/dist/state/scenario-outcome.js.map +1 -0
  143. package/dist/state/scenario-state.d.ts +41 -0
  144. package/dist/state/scenario-state.d.ts.map +1 -0
  145. package/dist/state/scenario-state.js +50 -0
  146. package/dist/state/scenario-state.js.map +1 -0
  147. package/dist/state/setup-lock.d.ts +22 -0
  148. package/dist/state/setup-lock.d.ts.map +1 -0
  149. package/dist/state/setup-lock.js +90 -0
  150. package/dist/state/setup-lock.js.map +1 -0
  151. package/dist/types/common.d.ts +17 -0
  152. package/dist/types/common.d.ts.map +1 -0
  153. package/dist/types/common.js +2 -0
  154. package/dist/types/common.js.map +1 -0
  155. package/dist/types/content-builder.d.ts +16 -0
  156. package/dist/types/content-builder.d.ts.map +1 -0
  157. package/dist/types/content-builder.js +2 -0
  158. package/dist/types/content-builder.js.map +1 -0
  159. package/dist/types/playwright-extensions.d.ts +8 -0
  160. package/dist/types/playwright-extensions.d.ts.map +1 -0
  161. package/dist/types/playwright-extensions.js +2 -0
  162. package/dist/types/playwright-extensions.js.map +1 -0
  163. package/package.json +88 -0
  164. package/tsconfig.base.json +13 -0
@@ -0,0 +1,86 @@
1
+ import { getToolkitConfig } from '../config.js';
2
+ import { toolkitHeaders } from '../contract.js';
3
+ /** Relative to the backend entry point, which a project may have moved. */
4
+ export const RECORD_EDIT_ROUTE = '/record/edit';
5
+ /**
6
+ * The route the backend's own edit form posts to, so a TYPO3 release that changes
7
+ * it, its request-token rules or its field names fails the suite rather than
8
+ * passing quietly.
9
+ */
10
+ export async function saveRecord(poster, context, record) {
11
+ if (!context.routeToken) {
12
+ throw new Error('[typo3-playwright-toolkit] No route token for record_edit. The backend refuses a POST ' +
13
+ 'without one and answers with a redirect to the login form. The session endpoint ' +
14
+ 'returns it — use a page from the toolkit fixtures.');
15
+ }
16
+ const action = record.identifier.startsWith('NEW') ? 'new' : 'edit';
17
+ const url = `${context.baseUrl}${context.backendPath}${RECORD_EDIT_ROUTE}` +
18
+ `?edit%5B${record.table}%5D%5B${record.target}%5D=${action}` +
19
+ `&token=${encodeURIComponent(context.routeToken)}`;
20
+ const response = await poster.post(url, {
21
+ headers: toolkitHeaders(getToolkitConfig(), context.testId),
22
+ multipart: formFields(record.data),
23
+ // The new uid only exists in the redirect; following it loses it.
24
+ maxRedirects: 0,
25
+ });
26
+ const location = redirectTarget(response);
27
+ if (undefined === location) {
28
+ throw new Error(`[typo3-playwright-toolkit] ${record.table} did not save (status ${response.status()}). ` +
29
+ `The backend answers a rejected save with a page, not a redirect: ` +
30
+ `${(await response.text()).slice(0, 300)}`);
31
+ }
32
+ const uid = uidFrom(location, record.table);
33
+ if (undefined === uid) {
34
+ throw new Error(`[typo3-playwright-toolkit] The save of ${record.table} redirected to no uid — ` +
35
+ `a rejected request-token or session redirects to the login form. Location: ${location}`);
36
+ }
37
+ return uid;
38
+ }
39
+ /** `data[table][identifier][field]`, the names FormEngine gives its inputs. */
40
+ function formFields(data) {
41
+ const fields = {};
42
+ for (const [table, rows] of Object.entries(data)) {
43
+ for (const [identifier, row] of Object.entries(rows)) {
44
+ for (const [column, value] of Object.entries(row)) {
45
+ if (undefined === value || null === value) {
46
+ continue;
47
+ }
48
+ fields[`data[${table}][${identifier}][${column}]`] = formValue(value);
49
+ }
50
+ }
51
+ }
52
+ fields.doSave = '1';
53
+ // Save without closing: that is what makes the controller redirect back to
54
+ // the record it wrote, naming its uid.
55
+ fields._savedok = '1';
56
+ return fields;
57
+ }
58
+ function formValue(value) {
59
+ if ('string' === typeof value) {
60
+ return value;
61
+ }
62
+ if ('number' === typeof value) {
63
+ return String(value);
64
+ }
65
+ if ('boolean' === typeof value) {
66
+ return value ? '1' : '0';
67
+ }
68
+ return JSON.stringify(value) ?? '';
69
+ }
70
+ function redirectTarget(response) {
71
+ if (response.status() < 300 || response.status() >= 400) {
72
+ return undefined;
73
+ }
74
+ const headers = response.headers();
75
+ return headers.location ?? headers.Location;
76
+ }
77
+ /** `edit[tt_content][42]=edit`, in whatever encoding the header carries. */
78
+ function uidFrom(location, table) {
79
+ const match = decodeURIComponent(location).match(new RegExp(`edit\\[${table}\\]\\[(\\d+)\\]`));
80
+ if (null === match) {
81
+ return undefined;
82
+ }
83
+ const uid = Number(match[1]);
84
+ return Number.isInteger(uid) && uid > 0 ? uid : undefined;
85
+ }
86
+ //# sourceMappingURL=record-edit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record-edit.js","sourceRoot":"","sources":["../../src/http/record-edit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { APIRequestContext } from '@playwright/test';
2
+ import type { ToolkitConfig } from '../config.js';
3
+ /**
4
+ * Playwright does not route an APIRequestContext, so the test ID has to be put on
5
+ * here instead. No secret: it stays explicit, on the two requests that need it.
6
+ */
7
+ export declare function toolkitRequest(request: APIRequestContext, config: ToolkitConfig, testId: string): APIRequestContext;
8
+ //# sourceMappingURL=toolkit-request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolkit-request.d.ts","sourceRoot":"","sources":["../../src/http/toolkit-request.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAGzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAKjD;;;GAGG;AACH,wBAAgB,cAAc,CAC1B,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,MAAM,GACf,iBAAiB,CA6CnB"}
@@ -0,0 +1,45 @@
1
+ import { SECRET_HEADER } from './api-secret.js';
2
+ import { TEST_ID_HEADER, browserHeaders } from '../contract.js';
3
+ const TOOLKIT_HEADERS = [TEST_ID_HEADER.toLowerCase(), SECRET_HEADER.toLowerCase()];
4
+ const SENDING_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'fetch'];
5
+ /**
6
+ * Playwright does not route an APIRequestContext, so the test ID has to be put on
7
+ * here instead. No secret: it stays explicit, on the two requests that need it.
8
+ */
9
+ export function toolkitRequest(request, config, testId) {
10
+ const site = new URL(config.testingURL).origin;
11
+ const onSite = (target) => {
12
+ const url = 'string' === typeof target ? target : String(target?.url?.() ?? '');
13
+ if (!/^[a-z][a-z0-9+.-]*:/i.test(url)) {
14
+ return true;
15
+ }
16
+ try {
17
+ return new URL(url).origin === site;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ };
23
+ const headersFor = (target, given = {}) => {
24
+ if (onSite(target)) {
25
+ return testId ? { ...given, ...browserHeaders(testId) } : { ...given };
26
+ }
27
+ return Object.fromEntries(Object.entries(given).filter(([name]) => !TOOLKIT_HEADERS.includes(name.toLowerCase())));
28
+ };
29
+ return new Proxy(request, {
30
+ get(target, property, receiver) {
31
+ const value = Reflect.get(target, property, receiver);
32
+ if ('function' !== typeof value) {
33
+ return value;
34
+ }
35
+ if ('string' !== typeof property || !SENDING_METHODS.includes(property)) {
36
+ return value.bind(target);
37
+ }
38
+ return (url, options = {}) => value.call(target, url, {
39
+ ...options,
40
+ headers: headersFor(url, options.headers),
41
+ });
42
+ },
43
+ });
44
+ }
45
+ //# sourceMappingURL=toolkit-request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolkit-request.js","sourceRoot":"","sources":["../../src/http/toolkit-request.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAG/D,MAAM,eAAe,GAAG,CAAC,cAAc,CAAC,WAAW,EAAE,EAAE,aAAa,CAAC,WAAW,EAAE,CAAC,CAAA;AACnF,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,CAAA;AAElF;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC1B,OAA0B,EAC1B,MAAqB,EACrB,MAAc;IAEd,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,CAAA;IAE9C,MAAM,MAAM,GAAG,CAAC,MAAe,EAAW,EAAE;QACxC,MAAM,GAAG,GAAG,QAAQ,KAAK,OAAO,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAE,MAAiC,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;QAE3G,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACpC,OAAO,IAAI,CAAA;QACf,CAAC;QAED,IAAI,CAAC;YACD,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,KAAK,IAAI,CAAA;QACvC,CAAC;QAAC,MAAM,CAAC;YACL,OAAO,KAAK,CAAA;QAChB,CAAC;IACL,CAAC,CAAA;IAED,MAAM,UAAU,GAAG,CAAC,MAAe,EAAE,QAAgC,EAAE,EAA0B,EAAE;QAC/F,IAAI,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;YACjB,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,GAAG,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,CAAA;QAC1E,CAAC;QAED,OAAO,MAAM,CAAC,WAAW,CACrB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,eAAe,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAC1F,CAAA;IACL,CAAC,CAAA;IAED,OAAO,IAAI,KAAK,CAAC,OAAO,EAAE;QACtB,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ;YAC1B,MAAM,KAAK,GAAY,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAA;YAE9D,IAAI,UAAU,KAAK,OAAO,KAAK,EAAE,CAAC;gBAC9B,OAAO,KAAK,CAAA;YAChB,CAAC;YACD,IAAI,QAAQ,KAAK,OAAO,QAAQ,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACtE,OAAQ,KAAyC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YAClE,CAAC;YAED,OAAO,CAAC,GAAY,EAAE,UAAgD,EAAE,EAAE,EAAE,CACvE,KAAyC,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE;gBACzD,GAAG,OAAO;gBACV,OAAO,EAAE,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC;aAC5C,CAAC,CAAA;QACV,CAAC;KACJ,CAAC,CAAA;AACN,CAAC"}
@@ -0,0 +1,18 @@
1
+ export { defineScenario, type ScenarioBuilders, type ScenarioFixtures, type SetupTools, } from './scenario.js';
2
+ export { expect } from '@playwright/test';
3
+ export { defineToolkitConfig, getToolkitConfig, type ToolkitConfig, type ToolkitConfigInput, type ToolkitPaths, type ToolkitScreenshotConfig, type ContentTypeConstructor, } from './config.js';
4
+ export { defineBasePlaywrightConfig } from './playwright/base-config.js';
5
+ export { ContentBuilder } from './builders/content-builder.js';
6
+ export { PageBuilder } from './builders/page-builder.js';
7
+ export { CoreContent, HeaderContent, TextContent, TextmediaContent, TextpicContent, ImageContent, BulletsContent, TableContent, UploadsContent, HtmlContent, DividerContent, ShortcutContent, MenuContent, type ContentTypeMap, type CoreContentTypeMap, } from './builders/core-content.js';
8
+ export type { RecordDataMap } from './http/record-edit.js';
9
+ export { newRecordIdentifier } from './builders/identifier.js';
10
+ export { takeScreenshot, waitForAnimations } from './checks/screenshot.js';
11
+ export { runAccessibilityScan, scanAccessibility, DEFAULT_SCAN_TAGS, type AccessibilityScanOptions, } from './checks/accessibility.js';
12
+ export { CspVerifier, type CspMode, type CspViolation } from './checks/csp.js';
13
+ export { TEST_ID_HEADER, TEST_ID_PATTERN } from './contract.js';
14
+ export { RUN_ID_ENV, RUN_ID_PATTERN } from './state/run-id.js';
15
+ export type { CropConfig } from './types/common.js';
16
+ export type { ContentFields, ContentBuilderInterface } from './types/content-builder.js';
17
+ export type { PageWithTestId, ContextWithTestId } from './types/playwright-extensions.js';
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,cAAc,EACd,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,UAAU,GAClB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EACH,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,GAC9B,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAA;AAExE,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AACxD,OAAO,EACH,WAAW,EACX,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,cAAc,EACd,WAAW,EACX,cAAc,EACd,eAAe,EACf,WAAW,EACX,KAAK,cAAc,EACnB,KAAK,kBAAkB,GAC1B,MAAM,4BAA4B,CAAA;AACnC,YAAY,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1E,OAAO,EACH,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,wBAAwB,GAChC,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAAE,WAAW,EAAE,KAAK,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAC/D,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAC9D,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,YAAY,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAA;AACxF,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ export { defineScenario, } from './scenario.js';
2
+ export { expect } from '@playwright/test';
3
+ export { defineToolkitConfig, getToolkitConfig, } from './config.js';
4
+ export { defineBasePlaywrightConfig } from './playwright/base-config.js';
5
+ export { ContentBuilder } from './builders/content-builder.js';
6
+ export { PageBuilder } from './builders/page-builder.js';
7
+ export { CoreContent, HeaderContent, TextContent, TextmediaContent, TextpicContent, ImageContent, BulletsContent, TableContent, UploadsContent, HtmlContent, DividerContent, ShortcutContent, MenuContent, } from './builders/core-content.js';
8
+ export { newRecordIdentifier } from './builders/identifier.js';
9
+ export { takeScreenshot, waitForAnimations } from './checks/screenshot.js';
10
+ export { runAccessibilityScan, scanAccessibility, DEFAULT_SCAN_TAGS, } from './checks/accessibility.js';
11
+ export { CspVerifier } from './checks/csp.js';
12
+ export { TEST_ID_HEADER, TEST_ID_PATTERN } from './contract.js';
13
+ export { RUN_ID_ENV, RUN_ID_PATTERN } from './state/run-id.js';
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,cAAc,GAIjB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EACH,mBAAmB,EACnB,gBAAgB,GAMnB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAA;AAExE,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AACxD,OAAO,EACH,WAAW,EACX,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,cAAc,EACd,WAAW,EACX,cAAc,EACd,eAAe,EACf,WAAW,GAGd,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1E,OAAO,EACH,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,GAEpB,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAAE,WAAW,EAAmC,MAAM,iBAAiB,CAAA;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAC/D,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/inspect/cli.ts"],"names":[],"mappings":""}
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { findStateDir, inspectLinks } from './links.js';
5
+ import { INSPECT_TOKEN_LIFETIME_MS } from './token.js';
6
+ function readSecret(stateDir) {
7
+ const fromEnvironment = process.env.PLAYWRIGHT_TOOLKIT_SECRET?.trim();
8
+ if (fromEnvironment) {
9
+ return fromEnvironment;
10
+ }
11
+ // The state directory sits in the project root, next to var/.
12
+ const file = path.join(path.dirname(stateDir), 'var/playwright/api-secret');
13
+ return fs.existsSync(file) ? fs.readFileSync(file, 'utf-8').trim() : '';
14
+ }
15
+ const stateDir = findStateDir(process.cwd());
16
+ if (undefined === stateDir) {
17
+ console.error('No .test-state directory found. Run the tests once first.');
18
+ process.exit(1);
19
+ }
20
+ const secret = readSecret(stateDir);
21
+ if ('' === secret) {
22
+ console.error('No API secret found. Run "ddev playwright-prepare" first.');
23
+ process.exit(1);
24
+ }
25
+ const links = inspectLinks(stateDir, secret);
26
+ if (0 === links.length) {
27
+ console.error('No test databases recorded. They are removed when a run passes.');
28
+ process.exit(1);
29
+ }
30
+ const filter = process.argv[2];
31
+ const shown = undefined === filter ? links : links.filter((link) => link.name.includes(filter));
32
+ console.log(`Links log you into the backend and expire in ${INSPECT_TOKEN_LIFETIME_MS / 60_000} minutes.\n`);
33
+ for (const link of shown) {
34
+ console.log(`${link.name}\n db${link.testId}\n ${link.url}\n`);
35
+ }
36
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/inspect/cli.ts"],"names":[],"mappings":";AACA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAA;AACxB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAA;AAC5B,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACvD,OAAO,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAA;AAEtD,SAAS,UAAU,CAAC,QAAgB;IAChC,MAAM,eAAe,GAAG,OAAO,CAAC,GAAG,CAAC,yBAAyB,EAAE,IAAI,EAAE,CAAA;IACrE,IAAI,eAAe,EAAE,CAAC;QAClB,OAAO,eAAe,CAAA;IAC1B,CAAC;IAED,8DAA8D;IAC9D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,2BAA2B,CAAC,CAAA;IAE3E,OAAO,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;AAC3E,CAAC;AAED,MAAM,QAAQ,GAAG,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;AAC5C,IAAI,SAAS,KAAK,QAAQ,EAAE,CAAC;IACzB,OAAO,CAAC,KAAK,CAAC,2DAA2D,CAAC,CAAA;IAC1E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACnB,CAAC;AAED,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAA;AACnC,IAAI,EAAE,KAAK,MAAM,EAAE,CAAC;IAChB,OAAO,CAAC,KAAK,CAAC,2DAA2D,CAAC,CAAA;IAC1E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACnB,CAAC;AAED,MAAM,KAAK,GAAG,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;AAC5C,IAAI,CAAC,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC;IACrB,OAAO,CAAC,KAAK,CAAC,iEAAiE,CAAC,CAAA;IAChF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACnB,CAAC;AAED,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AAC9B,MAAM,KAAK,GAAG,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA;AAE/F,OAAO,CAAC,GAAG,CACP,gDAAgD,yBAAyB,GAAG,MAAM,aAAa,CAClG,CAAA;AACD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;IACvB,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,IAAI,SAAS,IAAI,CAAC,MAAM,OAAO,IAAI,CAAC,GAAG,IAAI,CAAC,CAAA;AACpE,CAAC"}
@@ -0,0 +1,10 @@
1
+ export interface InspectLink {
2
+ runId: string;
3
+ key: string;
4
+ name: string;
5
+ testId: string;
6
+ url: string;
7
+ }
8
+ export declare function findStateDir(from: string): string | undefined;
9
+ export declare function inspectLinks(stateDir: string, secret: string, now?: number): InspectLink[];
10
+ //# sourceMappingURL=links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../../src/inspect/links.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;CACd;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAiB7D;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,WAAW,EAAE,CAqBtG"}
@@ -0,0 +1,51 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { readAttemptsFrom } from '../state/attempt-registry.js';
4
+ import { listRunIds, runLastActiveMs, runsRoot } from '../state/run-namespace.js';
5
+ import { inspectUrl } from './token.js';
6
+ export function findStateDir(from) {
7
+ let directory = path.resolve(from);
8
+ for (;;) {
9
+ const candidate = path.join(directory, '.test-state');
10
+ // The API secret is read from the parent of whatever matches here, so a
11
+ // directory without runs/ resolves it against the wrong root.
12
+ if (fs.existsSync(path.join(candidate, 'runs'))) {
13
+ return candidate;
14
+ }
15
+ const parent = path.dirname(directory);
16
+ if (parent === directory) {
17
+ return undefined;
18
+ }
19
+ directory = parent;
20
+ }
21
+ }
22
+ export function inspectLinks(stateDir, secret, now = Date.now()) {
23
+ const runs = listRunIds(stateDir)
24
+ .map((runId) => ({ runId, runDir: path.join(runsRoot(stateDir), runId) }))
25
+ .sort((a, b) => runLastActiveMs(b.runDir) - runLastActiveMs(a.runDir));
26
+ return runs.flatMap(({ runId, runDir }) => {
27
+ const testingURL = readTestingUrl(path.join(runDir, 'meta.json'));
28
+ if (undefined === testingURL) {
29
+ return [];
30
+ }
31
+ return readAttemptsFrom(path.join(runDir, 'attempts.jsonl'))
32
+ .filter((attempt) => 'preflight' !== attempt.key)
33
+ .map((attempt) => ({
34
+ runId,
35
+ key: attempt.key,
36
+ name: attempt.name ?? attempt.key,
37
+ testId: attempt.testId,
38
+ url: inspectUrl(testingURL, secret, attempt.testId, now),
39
+ }));
40
+ });
41
+ }
42
+ function readTestingUrl(metaFile) {
43
+ try {
44
+ const meta = JSON.parse(fs.readFileSync(metaFile, 'utf-8'));
45
+ return 'string' === typeof meta.testingURL ? meta.testingURL : undefined;
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ }
51
+ //# sourceMappingURL=links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.js","sourceRoot":"","sources":["../../src/inspect/links.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /** Long enough to pick a link from the list, short enough that a pasted one dies. */
2
+ export declare const INSPECT_TOKEN_LIFETIME_MS = 900000;
3
+ export declare function mintInspectToken(secret: string, testId: string, expiresAt: number): string;
4
+ export declare function inspectUrl(testingURL: string, secret: string, testId: string, now: number): string;
5
+ //# sourceMappingURL=token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../src/inspect/token.ts"],"names":[],"mappings":"AAIA,qFAAqF;AACrF,eAAO,MAAM,yBAAyB,SAAU,CAAA;AAEhD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAI1F;AAED,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAQlG"}
@@ -0,0 +1,15 @@
1
+ import { createHmac } from 'node:crypto';
2
+ const PURPOSE = 'inspect';
3
+ /** Long enough to pick a link from the list, short enough that a pasted one dies. */
4
+ export const INSPECT_TOKEN_LIFETIME_MS = 900_000;
5
+ export function mintInspectToken(secret, testId, expiresAt) {
6
+ const signature = createHmac('sha256', secret).update(`${PURPOSE}:${testId}:${expiresAt}`).digest('hex');
7
+ return `${expiresAt}.${signature}`;
8
+ }
9
+ export function inspectUrl(testingURL, secret, testId, now) {
10
+ const expiresAt = Math.floor((now + INSPECT_TOKEN_LIFETIME_MS) / 1000);
11
+ return (`${testingURL}/typo3/test-api/inspect` +
12
+ `?id=${encodeURIComponent(testId)}` +
13
+ `&t=${encodeURIComponent(mintInspectToken(secret, testId, expiresAt))}`);
14
+ }
15
+ //# sourceMappingURL=token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token.js","sourceRoot":"","sources":["../../src/inspect/token.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,MAAM,OAAO,GAAG,SAAS,CAAA;AAEzB,qFAAqF;AACrF,MAAM,CAAC,MAAM,yBAAyB,GAAG,OAAO,CAAA;AAEhD,MAAM,UAAU,gBAAgB,CAAC,MAAc,EAAE,MAAc,EAAE,SAAiB;IAC9E,MAAM,SAAS,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,GAAG,OAAO,IAAI,MAAM,IAAI,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAExG,OAAO,GAAG,SAAS,IAAI,SAAS,EAAE,CAAA;AACtC,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,UAAkB,EAAE,MAAc,EAAE,MAAc,EAAE,GAAW;IACtF,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,yBAAyB,CAAC,GAAG,IAAI,CAAC,CAAA;IAEtE,OAAO,CACH,GAAG,UAAU,yBAAyB;QACtC,OAAO,kBAAkB,CAAC,MAAM,CAAC,EAAE;QACnC,MAAM,kBAAkB,CAAC,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,EAAE,CAC1E,CAAA;AACL,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { type PlaywrightTestConfig } from '@playwright/test';
2
+ import type { ToolkitConfig } from '../config.js';
3
+ export declare function defineBasePlaywrightConfig(toolkitConfig: ToolkitConfig, overrides?: PlaywrightTestConfig): PlaywrightTestConfig;
4
+ //# sourceMappingURL=base-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-config.d.ts","sourceRoot":"","sources":["../../src/playwright/base-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAKjD,wBAAgB,0BAA0B,CACtC,aAAa,EAAE,aAAa,EAC5B,SAAS,GAAE,oBAAyB,GACrC,oBAAoB,CA+BtB"}
@@ -0,0 +1,34 @@
1
+ import { defineConfig } from '@playwright/test';
2
+ const GLOBAL_SETUP = '@plan2net/typo3-playwright-toolkit/global-setup';
3
+ const GLOBAL_TEARDOWN = '@plan2net/typo3-playwright-toolkit/global-teardown';
4
+ export function defineBasePlaywrightConfig(toolkitConfig, overrides = {}) {
5
+ const threshold = toolkitConfig.screenshot?.threshold ?? 0.2;
6
+ const maxDiffPixelRatio = toolkitConfig.screenshot?.maxDiffPixelRatio ?? 0.005;
7
+ const { use, expect, ...rest } = overrides;
8
+ return defineConfig({
9
+ globalSetup: GLOBAL_SETUP,
10
+ globalTeardown: GLOBAL_TEARDOWN,
11
+ fullyParallel: true,
12
+ forbidOnly: !!process.env.CI,
13
+ workers: 2,
14
+ ...rest,
15
+ // Merged key by key, not spread wholesale: Playwright reads `use` and
16
+ // `expect` as whole objects, so overriding one key would drop the base URL,
17
+ // tracing or the screenshot tolerances along with it.
18
+ expect: {
19
+ timeout: 5000,
20
+ ...expect,
21
+ toHaveScreenshot: { maxDiffPixelRatio, threshold, ...expect?.toHaveScreenshot },
22
+ toMatchSnapshot: { maxDiffPixelRatio, threshold, ...expect?.toMatchSnapshot },
23
+ },
24
+ use: {
25
+ baseURL: toolkitConfig.testingURL,
26
+ ignoreHTTPSErrors: true,
27
+ // Serves past context.route, so its requests would carry no test ID.
28
+ serviceWorkers: 'block',
29
+ trace: process.env.CI ? 'retain-on-failure' : 'on-first-retry',
30
+ ...use,
31
+ },
32
+ });
33
+ }
34
+ //# sourceMappingURL=base-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-config.js","sourceRoot":"","sources":["../../src/playwright/base-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA6B,MAAM,kBAAkB,CAAA;AAG1E,MAAM,YAAY,GAAG,iDAAiD,CAAA;AACtE,MAAM,eAAe,GAAG,oDAAoD,CAAA;AAE5E,MAAM,UAAU,0BAA0B,CACtC,aAA4B,EAC5B,YAAkC,EAAE;IAEpC,MAAM,SAAS,GAAG,aAAa,CAAC,UAAU,EAAE,SAAS,IAAI,GAAG,CAAA;IAC5D,MAAM,iBAAiB,GAAG,aAAa,CAAC,UAAU,EAAE,iBAAiB,IAAI,KAAK,CAAA;IAE9E,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,SAAS,CAAA;IAE1C,OAAO,YAAY,CAAC;QAChB,WAAW,EAAE,YAAY;QACzB,cAAc,EAAE,eAAe;QAC/B,aAAa,EAAE,IAAI;QACnB,UAAU,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE;QAC5B,OAAO,EAAE,CAAC;QACV,GAAG,IAAI;QACP,sEAAsE;QACtE,4EAA4E;QAC5E,sDAAsD;QACtD,MAAM,EAAE;YACJ,OAAO,EAAE,IAAI;YACb,GAAG,MAAM;YACT,gBAAgB,EAAE,EAAE,iBAAiB,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE,gBAAgB,EAAE;YAC/E,eAAe,EAAE,EAAE,iBAAiB,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE,eAAe,EAAE;SAChF;QACD,GAAG,EAAE;YACD,OAAO,EAAE,aAAa,CAAC,UAAU;YACjC,iBAAiB,EAAE,IAAI;YACvB,qEAAqE;YACrE,cAAc,EAAE,OAAgB;YAChC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAE,mBAA6B,CAAC,CAAC,CAAE,gBAA0B;YACpF,GAAG,GAAG;SACT;KACJ,CAAC,CAAA;AACN,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { defineBasePlaywrightConfig } from './base-config.js';
2
+ export { defineToolkitConfig, type ToolkitConfig } from '../config.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/playwright/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAA;AAC7D,OAAO,EAAE,mBAAmB,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA"}
@@ -0,0 +1,3 @@
1
+ export { defineBasePlaywrightConfig } from './base-config.js';
2
+ export { defineToolkitConfig } from '../config.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/playwright/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAA;AAC7D,OAAO,EAAE,mBAAmB,EAAsB,MAAM,cAAc,CAAA"}
@@ -0,0 +1,46 @@
1
+ import { type APIRequestContext, type Browser, type Page } from '@playwright/test';
2
+ import { type ToolkitConfig } from './config.js';
3
+ import { ContentBuilder } from './builders/content-builder.js';
4
+ import { PageBuilder } from './builders/page-builder.js';
5
+ export interface ScenarioBuilders {
6
+ page(): PageBuilder;
7
+ content(): ContentBuilder;
8
+ }
9
+ export interface SetupTools {
10
+ testId: string;
11
+ attempt: number;
12
+ signal: AbortSignal;
13
+ page: Page;
14
+ request: APIRequestContext;
15
+ builders: ScenarioBuilders;
16
+ }
17
+ export interface ScenarioFixtures<S> {
18
+ state: S;
19
+ testId: string;
20
+ }
21
+ interface ResolvedScenario<S> {
22
+ data: S;
23
+ testId: string;
24
+ }
25
+ /**
26
+ * What the backend shows beside the site name, so whoever opens an inspect link
27
+ * can tell which scenario they are looking at. The scenario key cannot do that
28
+ * job: it is a sanitised path with a hash on the end.
29
+ */
30
+ export declare function scenarioName(file: string): string;
31
+ export declare function openAuthenticatedPage(browser: Browser, config: ToolkitConfig, testId: string, name?: string): Promise<{
32
+ page: Page;
33
+ routeToken: string;
34
+ backendPath: string;
35
+ close: () => Promise<void>;
36
+ }>;
37
+ /**
38
+ * One test file is one scenario: the first test that needs state runs the setup,
39
+ * the rest wait for it or are skipped if it failed.
40
+ */
41
+ export declare function defineScenario<S = Record<string, never>>(setup?: (tools: SetupTools) => Promise<S>): import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & ScenarioFixtures<S> & {
42
+ resolvedScenario: ResolvedScenario<S>;
43
+ automaticAccessibilityScan: void;
44
+ }, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
45
+ export {};
46
+ //# sourceMappingURL=scenario.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scenario.d.ts","sourceRoot":"","sources":["../src/scenario.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,iBAAiB,EAAE,KAAK,OAAO,EAAE,KAAK,IAAI,EAAiB,MAAM,kBAAkB,CAAA;AAE/G,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAA;AAKlE,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AAOxD,MAAM,WAAW,gBAAgB;IAC7B,IAAI,IAAI,WAAW,CAAA;IACnB,OAAO,IAAI,cAAc,CAAA;CAC5B;AAED,MAAM,WAAW,UAAU;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,WAAW,CAAA;IACnB,IAAI,EAAE,IAAI,CAAA;IACV,OAAO,EAAE,iBAAiB,CAAA;IAC1B,QAAQ,EAAE,gBAAgB,CAAA;CAC7B;AAED,MAAM,WAAW,gBAAgB,CAAC,CAAC;IAC/B,KAAK,EAAE,CAAC,CAAA;IACR,MAAM,EAAE,MAAM,CAAA;CACjB;AAED,UAAU,gBAAgB,CAAC,CAAC;IACxB,IAAI,EAAE,CAAC,CAAA;IACP,MAAM,EAAE,MAAM,CAAA;CACjB;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKjD;AAED,wBAAsB,qBAAqB,CACvC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,MAAM,EACd,IAAI,SAAK,GACV,OAAO,CAAC;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAgD9F;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC;sBAEjD,gBAAgB,CAAC,CAAC,CAAC;gCAA8B,IAAI;yGAkFtG"}
@@ -0,0 +1,139 @@
1
+ import { test as base } from '@playwright/test';
2
+ import { toolkitHeaders } from './contract.js';
3
+ import { getToolkitConfig } from './config.js';
4
+ import { ensureState } from './state/ensure-state.js';
5
+ import { applyScenarioOutcome, recordTestFailure } from './state/scenario-outcome.js';
6
+ import { sanitizeScenarioKey } from './state/run-namespace.js';
7
+ import { DEFAULT_BACKEND_PATH } from './builders/request-context.js';
8
+ import { ContentBuilder } from './builders/content-builder.js';
9
+ import { PageBuilder } from './builders/page-builder.js';
10
+ import { applyToolkitHeaders } from './http/off-site-headers.js';
11
+ import { prepareScenarioContext } from './http/prepare-context.js';
12
+ import { toolkitRequest } from './http/toolkit-request.js';
13
+ import { runAccessibilityScan, shouldScanAutomatically } from './checks/accessibility.js';
14
+ /**
15
+ * What the backend shows beside the site name, so whoever opens an inspect link
16
+ * can tell which scenario they are looking at. The scenario key cannot do that
17
+ * job: it is a sanitised path with a hash on the end.
18
+ */
19
+ export function scenarioName(file) {
20
+ const fileName = file.split('/').pop() ?? file;
21
+ const stripped = fileName.replace(/\.(spec|test)\.[cm]?[jt]sx?$/i, '');
22
+ return '' === stripped ? fileName : stripped;
23
+ }
24
+ export async function openAuthenticatedPage(browser, config, testId, name = '') {
25
+ const headers = toolkitHeaders(config, testId);
26
+ const context = await browser.newContext({ ignoreHTTPSErrors: true });
27
+ // The close handle is only returned on success, so a failure closes its own context.
28
+ try {
29
+ await applyToolkitHeaders(context, config, testId);
30
+ context.testId = testId;
31
+ const page = await context.newPage();
32
+ page.testId = testId;
33
+ const response = await page.request.post(`${config.testingURL}/typo3/test-api/session`, {
34
+ headers,
35
+ data: { name },
36
+ });
37
+ if (!response.ok()) {
38
+ throw new Error(`[setup] Session creation failed (${response.status()}): ${await response.text()}`);
39
+ }
40
+ const session = (await response.json());
41
+ await context.addCookies([
42
+ {
43
+ name: session.cookieName,
44
+ value: session.cookieValue,
45
+ domain: new URL(config.testingURL).hostname,
46
+ path: '/',
47
+ httpOnly: true,
48
+ sameSite: 'Strict',
49
+ },
50
+ ]);
51
+ return {
52
+ page,
53
+ routeToken: session.tokens?.record_edit ?? '',
54
+ backendPath: session.backendPath ?? DEFAULT_BACKEND_PATH,
55
+ close: () => context.close(),
56
+ };
57
+ }
58
+ catch (error) {
59
+ await context.close();
60
+ throw error;
61
+ }
62
+ }
63
+ /**
64
+ * One test file is one scenario: the first test that needs state runs the setup,
65
+ * the rest wait for it or are skipped if it failed.
66
+ */
67
+ export function defineScenario(setup) {
68
+ return base.extend({
69
+ resolvedScenario: async ({ browser }, use, testInfo) => {
70
+ const config = getToolkitConfig();
71
+ const key = sanitizeScenarioKey(testInfo.file);
72
+ const name = scenarioName(testInfo.file);
73
+ const outcome = await ensureState(config, {
74
+ key,
75
+ name,
76
+ triggerId: testInfo.testId,
77
+ setup: async ({ testId, attempt, signal }) => {
78
+ if (!setup) {
79
+ return {};
80
+ }
81
+ const label = attempt > 1 ? `${name} #${attempt}` : name;
82
+ const session = await openAuthenticatedPage(browser, config, testId, label);
83
+ const builderContext = {
84
+ testId,
85
+ baseUrl: config.testingURL,
86
+ backendPath: session.backendPath,
87
+ routeToken: session.routeToken,
88
+ };
89
+ try {
90
+ return await setup({
91
+ testId,
92
+ attempt,
93
+ signal,
94
+ page: session.page,
95
+ request: toolkitRequest(session.page.request, config, testId),
96
+ builders: {
97
+ page: () => new PageBuilder(session.page, builderContext),
98
+ content: () => new ContentBuilder(session.page, builderContext),
99
+ },
100
+ });
101
+ }
102
+ finally {
103
+ await session.close();
104
+ }
105
+ },
106
+ });
107
+ const data = applyScenarioOutcome(outcome, (reason) => testInfo.skip(true, reason));
108
+ await use({ data, testId: outcome.status === 'ready' ? outcome.testId : '' });
109
+ if (testInfo.status !== testInfo.expectedStatus) {
110
+ recordTestFailure(config, key, testInfo.error?.message ?? `test ${testInfo.status}`);
111
+ }
112
+ },
113
+ state: async ({ resolvedScenario }, use) => {
114
+ await use(resolvedScenario.data);
115
+ },
116
+ testId: async ({ resolvedScenario }, use) => {
117
+ await use(resolvedScenario.testId);
118
+ },
119
+ context: async ({ context, testId }, use) => {
120
+ await prepareScenarioContext(context, getToolkitConfig(), testId);
121
+ await use(context);
122
+ },
123
+ request: async ({ request, testId }, use) => {
124
+ await use(toolkitRequest(request, getToolkitConfig(), testId));
125
+ },
126
+ automaticAccessibilityScan: [
127
+ async ({ page }, use, testInfo) => {
128
+ await use();
129
+ const config = getToolkitConfig();
130
+ const navigated = page.url().startsWith('http');
131
+ if (shouldScanAutomatically({ status: testInfo.status ?? '', navigated }, config.accessibility)) {
132
+ await runAccessibilityScan(page);
133
+ }
134
+ },
135
+ { auto: true },
136
+ ],
137
+ });
138
+ }
139
+ //# sourceMappingURL=scenario.js.map