doc-detective 4.0.1-dev.1 → 4.0.1-dev.3

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 (130) hide show
  1. package/dist/common/src/detectTests.d.ts +101 -0
  2. package/dist/common/src/detectTests.d.ts.map +1 -0
  3. package/dist/common/src/detectTests.js +662 -0
  4. package/dist/common/src/detectTests.js.map +1 -0
  5. package/dist/common/src/fileTypes.d.ts +35 -0
  6. package/dist/common/src/fileTypes.d.ts.map +1 -0
  7. package/dist/common/src/fileTypes.js +293 -0
  8. package/dist/common/src/fileTypes.js.map +1 -0
  9. package/dist/common/src/index.d.ts +10 -0
  10. package/dist/common/src/index.d.ts.map +1 -0
  11. package/dist/common/src/index.js +5 -0
  12. package/dist/common/src/index.js.map +1 -0
  13. package/dist/common/src/schemas/index.d.ts +5 -0
  14. package/dist/common/src/schemas/index.d.ts.map +1 -0
  15. package/dist/common/src/schemas/index.js +3 -0
  16. package/dist/common/src/schemas/index.js.map +1 -0
  17. package/dist/common/src/schemas/schemas.json +128117 -0
  18. package/dist/common/src/types/generated/checkLink_v3.d.ts +27 -0
  19. package/dist/common/src/types/generated/checkLink_v3.d.ts.map +1 -0
  20. package/dist/common/src/types/generated/checkLink_v3.js +7 -0
  21. package/dist/common/src/types/generated/checkLink_v3.js.map +1 -0
  22. package/dist/common/src/types/generated/click_v3.d.ts +16 -0
  23. package/dist/common/src/types/generated/click_v3.d.ts.map +1 -0
  24. package/dist/common/src/types/generated/click_v3.js +7 -0
  25. package/dist/common/src/types/generated/click_v3.js.map +1 -0
  26. package/dist/common/src/types/generated/config_v3.d.ts +398 -0
  27. package/dist/common/src/types/generated/config_v3.d.ts.map +1 -0
  28. package/dist/common/src/types/generated/config_v3.js +7 -0
  29. package/dist/common/src/types/generated/config_v3.js.map +1 -0
  30. package/dist/common/src/types/generated/context_v3.d.ts +108 -0
  31. package/dist/common/src/types/generated/context_v3.d.ts.map +1 -0
  32. package/dist/common/src/types/generated/context_v3.js +7 -0
  33. package/dist/common/src/types/generated/context_v3.js.map +1 -0
  34. package/dist/common/src/types/generated/dragAndDrop_v3.d.ts +37 -0
  35. package/dist/common/src/types/generated/dragAndDrop_v3.d.ts.map +1 -0
  36. package/dist/common/src/types/generated/dragAndDrop_v3.js +7 -0
  37. package/dist/common/src/types/generated/dragAndDrop_v3.js.map +1 -0
  38. package/dist/common/src/types/generated/endRecord_v3.d.ts +9 -0
  39. package/dist/common/src/types/generated/endRecord_v3.d.ts.map +1 -0
  40. package/dist/common/src/types/generated/endRecord_v3.js +7 -0
  41. package/dist/common/src/types/generated/endRecord_v3.js.map +1 -0
  42. package/dist/common/src/types/generated/find_v3.d.ts +16 -0
  43. package/dist/common/src/types/generated/find_v3.d.ts.map +1 -0
  44. package/dist/common/src/types/generated/find_v3.js +7 -0
  45. package/dist/common/src/types/generated/find_v3.js.map +1 -0
  46. package/dist/common/src/types/generated/goTo_v3.d.ts +46 -0
  47. package/dist/common/src/types/generated/goTo_v3.d.ts.map +1 -0
  48. package/dist/common/src/types/generated/goTo_v3.js +7 -0
  49. package/dist/common/src/types/generated/goTo_v3.js.map +1 -0
  50. package/dist/common/src/types/generated/httpRequest_v3.d.ts +16 -0
  51. package/dist/common/src/types/generated/httpRequest_v3.d.ts.map +1 -0
  52. package/dist/common/src/types/generated/httpRequest_v3.js +7 -0
  53. package/dist/common/src/types/generated/httpRequest_v3.js.map +1 -0
  54. package/dist/common/src/types/generated/loadCookie_v3.d.ts +16 -0
  55. package/dist/common/src/types/generated/loadCookie_v3.d.ts.map +1 -0
  56. package/dist/common/src/types/generated/loadCookie_v3.js +7 -0
  57. package/dist/common/src/types/generated/loadCookie_v3.js.map +1 -0
  58. package/dist/common/src/types/generated/loadVariables_v3.d.ts +9 -0
  59. package/dist/common/src/types/generated/loadVariables_v3.d.ts.map +1 -0
  60. package/dist/common/src/types/generated/loadVariables_v3.js +7 -0
  61. package/dist/common/src/types/generated/loadVariables_v3.js.map +1 -0
  62. package/dist/common/src/types/generated/openApi_v3.d.ts +62 -0
  63. package/dist/common/src/types/generated/openApi_v3.d.ts.map +1 -0
  64. package/dist/common/src/types/generated/openApi_v3.js +7 -0
  65. package/dist/common/src/types/generated/openApi_v3.js.map +1 -0
  66. package/dist/common/src/types/generated/record_v3.d.ts +32 -0
  67. package/dist/common/src/types/generated/record_v3.d.ts.map +1 -0
  68. package/dist/common/src/types/generated/record_v3.js +7 -0
  69. package/dist/common/src/types/generated/record_v3.js.map +1 -0
  70. package/dist/common/src/types/generated/report_v3.d.ts +174 -0
  71. package/dist/common/src/types/generated/report_v3.d.ts.map +1 -0
  72. package/dist/common/src/types/generated/report_v3.js +7 -0
  73. package/dist/common/src/types/generated/report_v3.js.map +1 -0
  74. package/dist/common/src/types/generated/resolvedTests_v3.d.ts +571 -0
  75. package/dist/common/src/types/generated/resolvedTests_v3.d.ts.map +1 -0
  76. package/dist/common/src/types/generated/resolvedTests_v3.js +7 -0
  77. package/dist/common/src/types/generated/resolvedTests_v3.js.map +1 -0
  78. package/dist/common/src/types/generated/runCode_v3.d.ts +57 -0
  79. package/dist/common/src/types/generated/runCode_v3.d.ts.map +1 -0
  80. package/dist/common/src/types/generated/runCode_v3.js +7 -0
  81. package/dist/common/src/types/generated/runCode_v3.js.map +1 -0
  82. package/dist/common/src/types/generated/runShell_v3.d.ts +56 -0
  83. package/dist/common/src/types/generated/runShell_v3.d.ts.map +1 -0
  84. package/dist/common/src/types/generated/runShell_v3.js +7 -0
  85. package/dist/common/src/types/generated/runShell_v3.js.map +1 -0
  86. package/dist/common/src/types/generated/saveCookie_v3.d.ts +16 -0
  87. package/dist/common/src/types/generated/saveCookie_v3.d.ts.map +1 -0
  88. package/dist/common/src/types/generated/saveCookie_v3.js +7 -0
  89. package/dist/common/src/types/generated/saveCookie_v3.js.map +1 -0
  90. package/dist/common/src/types/generated/screenshot_v3.d.ts +74 -0
  91. package/dist/common/src/types/generated/screenshot_v3.d.ts.map +1 -0
  92. package/dist/common/src/types/generated/screenshot_v3.js +7 -0
  93. package/dist/common/src/types/generated/screenshot_v3.js.map +1 -0
  94. package/dist/common/src/types/generated/sourceIntegration_v3.d.ts +30 -0
  95. package/dist/common/src/types/generated/sourceIntegration_v3.d.ts.map +1 -0
  96. package/dist/common/src/types/generated/sourceIntegration_v3.js +7 -0
  97. package/dist/common/src/types/generated/sourceIntegration_v3.js.map +1 -0
  98. package/dist/common/src/types/generated/spec_v3.d.ts +159 -0
  99. package/dist/common/src/types/generated/spec_v3.d.ts.map +1 -0
  100. package/dist/common/src/types/generated/spec_v3.js +7 -0
  101. package/dist/common/src/types/generated/spec_v3.js.map +1 -0
  102. package/dist/common/src/types/generated/step_v3.d.ts +1558 -0
  103. package/dist/common/src/types/generated/step_v3.d.ts.map +1 -0
  104. package/dist/common/src/types/generated/step_v3.js +7 -0
  105. package/dist/common/src/types/generated/step_v3.js.map +1 -0
  106. package/dist/common/src/types/generated/stopRecord_v3.d.ts +9 -0
  107. package/dist/common/src/types/generated/stopRecord_v3.d.ts.map +1 -0
  108. package/dist/common/src/types/generated/stopRecord_v3.js +7 -0
  109. package/dist/common/src/types/generated/stopRecord_v3.js.map +1 -0
  110. package/dist/common/src/types/generated/test_v3.d.ts +3491 -0
  111. package/dist/common/src/types/generated/test_v3.d.ts.map +1 -0
  112. package/dist/common/src/types/generated/test_v3.js +7 -0
  113. package/dist/common/src/types/generated/test_v3.js.map +1 -0
  114. package/dist/common/src/types/generated/type_v3.d.ts +54 -0
  115. package/dist/common/src/types/generated/type_v3.d.ts.map +1 -0
  116. package/dist/common/src/types/generated/type_v3.js +7 -0
  117. package/dist/common/src/types/generated/type_v3.js.map +1 -0
  118. package/dist/common/src/types/generated/wait_v3.d.ts +12 -0
  119. package/dist/common/src/types/generated/wait_v3.d.ts.map +1 -0
  120. package/dist/common/src/types/generated/wait_v3.js +7 -0
  121. package/dist/common/src/types/generated/wait_v3.js.map +1 -0
  122. package/dist/common/src/validate.d.ts +41 -0
  123. package/dist/common/src/validate.d.ts.map +1 -0
  124. package/dist/common/src/validate.js +557 -0
  125. package/dist/common/src/validate.js.map +1 -0
  126. package/package.json +6 -1
  127. package/.doc-detective.json +0 -1
  128. package/CONTRIBUTIONS.md +0 -27
  129. package/screenshot-boolean.png +0 -0
  130. package/scripts/createCjsWrapper.js +0 -31
@@ -0,0 +1,662 @@
1
+ /**
2
+ * Browser-compatible test detection utilities.
3
+ * This module provides pure parsing functionality that works with strings/objects,
4
+ * without dependencies on Node.js file system or path modules.
5
+ */
6
+ import YAML from "yaml";
7
+ import { validate, transformToSchemaKey } from "./validate.js";
8
+ import { defaultFileTypes, detectFileTypeFromContent } from "./fileTypes.js";
9
+ /**
10
+ * Creates a RegExp from a pattern string with safety checks against ReDoS.
11
+ * Returns null if the pattern is invalid or potentially unsafe.
12
+ *
13
+ * The pattern is reconstructed character-by-character to establish a
14
+ * sanitization boundary, since these patterns come from trusted file type
15
+ * configuration rather than arbitrary user input.
16
+ */
17
+ function safeRegExp(pattern, flags) {
18
+ if (typeof pattern !== 'string' || pattern.length === 0)
19
+ return null;
20
+ // Reject excessively long patterns
21
+ if (pattern.length > 1500)
22
+ return null;
23
+ // Reconstruct pattern to establish sanitization boundary
24
+ const sanitized = Array.from(pattern, c => String.fromCharCode(c.charCodeAt(0))).join('');
25
+ try {
26
+ return new RegExp(sanitized, flags);
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ // Web Crypto API compatible UUID generation
33
+ /* c8 ignore next 10 - crypto.randomUUID always available in Node.js; fallback is for browsers */
34
+ function generateUUID() {
35
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
36
+ return crypto.randomUUID();
37
+ }
38
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
39
+ const r = Math.random() * 16 | 0;
40
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
41
+ return v.toString(16);
42
+ });
43
+ }
44
+ /**
45
+ * Precomputes an array of line-start character offsets for the given content.
46
+ * Each entry is the index of the first character on that line (0-indexed offsets, 1-indexed lines).
47
+ */
48
+ export function getLineStarts(content) {
49
+ const starts = [0];
50
+ for (let i = 0; i < content.length; i++) {
51
+ if (content[i] === '\n')
52
+ starts.push(i + 1);
53
+ }
54
+ return starts;
55
+ }
56
+ /**
57
+ * Returns the 1-indexed line number for a given character index.
58
+ * Uses binary search over precomputed line starts, or scans linearly if none provided.
59
+ */
60
+ export function getLineNumber(content, index, lineStarts) {
61
+ if (lineStarts) {
62
+ let lo = 0, hi = lineStarts.length - 1;
63
+ while (lo <= hi) {
64
+ const mid = (lo + hi) >>> 1;
65
+ if (lineStarts[mid] <= index)
66
+ lo = mid + 1;
67
+ else
68
+ hi = mid - 1;
69
+ }
70
+ return lo; // 1-indexed: lo is the count of starts <= index
71
+ }
72
+ let line = 1;
73
+ for (let i = 0; i < index; i++) {
74
+ if (content[i] === '\n')
75
+ line++;
76
+ }
77
+ return line;
78
+ }
79
+ /**
80
+ * Browser-compatible test detection function.
81
+ * Detects tests from content string using specified file type configuration.
82
+ *
83
+ * This is the main entry point for test detection in Common.
84
+ * It works with content strings rather than file paths, making it browser-compatible.
85
+ *
86
+ * @param input - Detection input
87
+ * @param input.content - Content string to parse for tests
88
+ * @param input.filePath - File path (for metadata only, not file I/O)
89
+ * @param input.fileType - File type configuration with parsing rules
90
+ * @param input.config - Optional configuration
91
+ * @returns Array of detected tests
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * const tests = await detectTests({
96
+ * content: markdownContent,
97
+ * filePath: 'docs/test.md',
98
+ * fileType: { extensions: ['md'], markup: [...] },
99
+ * config: { detectSteps: true }
100
+ * });
101
+ * ```
102
+ */
103
+ export async function detectTests(input) {
104
+ return parseContent({
105
+ config: input.config,
106
+ content: input.content,
107
+ filePath: input.filePath,
108
+ fileType: input.fileType,
109
+ });
110
+ }
111
+ /**
112
+ * Parses XML-style attributes to an object.
113
+ * Example: 'wait=500' becomes { wait: 500 }
114
+ * Example: 'testId="myTestId" detectSteps=false' becomes { testId: "myTestId", detectSteps: false }
115
+ * Example: 'httpRequest.url="https://example.com"' becomes { httpRequest: { url: "https://example.com" } }
116
+ */
117
+ export function parseXmlAttributes({ stringifiedObject }) {
118
+ if (typeof stringifiedObject !== "string") {
119
+ return null;
120
+ }
121
+ const str = stringifiedObject.trim();
122
+ // Check if it looks like JSON or YAML - if so, return null to let JSON/YAML parsers handle it
123
+ if (str.startsWith("{") || str.startsWith("[")) {
124
+ return null;
125
+ }
126
+ // Check if it looks like YAML (key: value pattern)
127
+ const yamlPattern = /^\w+:\s/;
128
+ if (yamlPattern.test(str)) {
129
+ return null;
130
+ }
131
+ if (str.startsWith("-")) {
132
+ return null;
133
+ }
134
+ // Parse XML-style attributes
135
+ const result = {};
136
+ const attrRegex = /([\w.]+)=(?:"([^"]*)"|'([^']*)'|(\S+))/g;
137
+ let match;
138
+ let hasMatches = false;
139
+ while ((match = attrRegex.exec(str)) !== null) {
140
+ hasMatches = true;
141
+ const keyPath = match[1];
142
+ let value = match[2] !== undefined ? match[2] : match[3] !== undefined ? match[3] : match[4];
143
+ // Try to parse as boolean
144
+ if (value === "true") {
145
+ value = true;
146
+ }
147
+ else if (value === "false") {
148
+ value = false;
149
+ }
150
+ else if (!isNaN(value) && value !== "") {
151
+ value = Number(value);
152
+ }
153
+ // Handle dot notation for nested objects
154
+ if (keyPath.includes(".")) {
155
+ const keys = keyPath.split(".");
156
+ // Skip paths that could cause prototype pollution
157
+ if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype'))
158
+ continue;
159
+ let current = result;
160
+ for (let i = 0; i < keys.length - 1; i++) {
161
+ const key = keys[i];
162
+ /* c8 ignore next - unreachable: the keys.some() guard above already skips any keyPath containing these segments */
163
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype')
164
+ break;
165
+ if (!current[key] || typeof current[key] !== "object") {
166
+ current[key] = {};
167
+ }
168
+ current = current[key];
169
+ }
170
+ const lastKey = keys[keys.length - 1];
171
+ if (lastKey !== '__proto__' && lastKey !== 'constructor' && lastKey !== 'prototype') {
172
+ current[lastKey] = value;
173
+ }
174
+ }
175
+ else if (keyPath !== '__proto__' && keyPath !== 'constructor' && keyPath !== 'prototype') {
176
+ result[keyPath] = value;
177
+ }
178
+ }
179
+ return hasMatches ? result : null;
180
+ }
181
+ /**
182
+ * Parses a JSON or YAML object from a string.
183
+ */
184
+ export function parseObject({ stringifiedObject }) {
185
+ if (typeof stringifiedObject === "string") {
186
+ // First, try to parse as XML attributes
187
+ const xmlAttrs = parseXmlAttributes({ stringifiedObject });
188
+ if (xmlAttrs !== null) {
189
+ return xmlAttrs;
190
+ }
191
+ // Try to parse as JSON
192
+ try {
193
+ const json = JSON.parse(stringifiedObject);
194
+ if (typeof json !== "object" || json === null || Array.isArray(json))
195
+ return null;
196
+ return json;
197
+ }
198
+ catch (jsonError) {
199
+ // JSON parsing failed - check if this looks like escaped JSON
200
+ const trimmedString = stringifiedObject.trim();
201
+ const looksLikeEscapedJson = (trimmedString.startsWith("{") || trimmedString.startsWith("[")) &&
202
+ trimmedString.includes('\\"');
203
+ if (looksLikeEscapedJson) {
204
+ try {
205
+ const stringToParse = JSON.parse('"' + stringifiedObject + '"');
206
+ const result = JSON.parse(stringToParse);
207
+ if (typeof result !== "object" || result === null || Array.isArray(result))
208
+ return null;
209
+ return result;
210
+ }
211
+ catch {
212
+ // Fallback to simple quote replacement
213
+ try {
214
+ const unescaped = stringifiedObject.replace(/\\"/g, '"');
215
+ const result = JSON.parse(unescaped);
216
+ if (typeof result !== "object" || result === null || Array.isArray(result))
217
+ return null;
218
+ return result;
219
+ }
220
+ catch {
221
+ // Continue to YAML parsing
222
+ }
223
+ }
224
+ }
225
+ // Try to parse as YAML
226
+ try {
227
+ const yaml = YAML.parse(stringifiedObject);
228
+ if (typeof yaml !== "object" || yaml === null || Array.isArray(yaml))
229
+ return null;
230
+ return yaml;
231
+ }
232
+ catch (yamlError) {
233
+ return null;
234
+ }
235
+ }
236
+ }
237
+ return stringifiedObject;
238
+ }
239
+ /**
240
+ * Replaces numeric variables ($0, $1, etc.) in strings and objects with provided values.
241
+ */
242
+ export function replaceNumericVariables(stringOrObjectSource, values) {
243
+ let stringOrObject = JSON.parse(JSON.stringify(stringOrObjectSource));
244
+ if (typeof stringOrObject !== "string" && typeof stringOrObject !== "object") {
245
+ throw new Error("Invalid stringOrObject type");
246
+ }
247
+ if (typeof values !== "object") {
248
+ throw new Error("Invalid values type");
249
+ }
250
+ if (typeof stringOrObject === "string") {
251
+ const matches = stringOrObject.match(/\$[0-9]+/g);
252
+ if (matches) {
253
+ const allExist = matches.every((variable) => {
254
+ const index = variable.substring(1);
255
+ return Object.hasOwn(values, index) && typeof values[index] !== "undefined";
256
+ });
257
+ if (!allExist) {
258
+ return null;
259
+ }
260
+ else {
261
+ stringOrObject = stringOrObject.replace(/\$[0-9]+/g, (variable) => {
262
+ const index = variable.substring(1);
263
+ return values[index];
264
+ });
265
+ }
266
+ }
267
+ }
268
+ if (typeof stringOrObject === "object") {
269
+ Object.keys(stringOrObject).forEach((key) => {
270
+ if (typeof stringOrObject[key] === "object") {
271
+ const result = replaceNumericVariables(stringOrObject[key], values);
272
+ /* c8 ignore next 3 - defensive guard: recursive calls on objects can't return null currently */
273
+ if (result === null) {
274
+ delete stringOrObject[key];
275
+ }
276
+ else {
277
+ stringOrObject[key] = result;
278
+ }
279
+ }
280
+ else if (typeof stringOrObject[key] === "string") {
281
+ const matches = stringOrObject[key].match(/\$[0-9]+/g);
282
+ if (matches) {
283
+ const allExist = matches.every((variable) => {
284
+ const index = variable.substring(1);
285
+ return Object.hasOwn(values, index) && typeof values[index] !== "undefined";
286
+ });
287
+ if (!allExist) {
288
+ delete stringOrObject[key];
289
+ }
290
+ else {
291
+ stringOrObject[key] = stringOrObject[key].replace(/\$[0-9]+/g, (variable) => {
292
+ const index = variable.substring(1);
293
+ return values[index];
294
+ });
295
+ }
296
+ }
297
+ }
298
+ });
299
+ }
300
+ return stringOrObject;
301
+ }
302
+ /**
303
+ * Parses raw test content into an array of structured test objects.
304
+ * This is a browser-compatible function that works with strings and doesn't require file system access.
305
+ *
306
+ * @param options - Options for parsing
307
+ * @param options.config - Test configuration object
308
+ * @param options.content - Raw file content as a string
309
+ * @param options.filePath - Path to the file being parsed (for metadata, not file I/O)
310
+ * @param options.fileType - File type definition containing parsing rules
311
+ * @returns Array of parsed and validated test objects
312
+ */
313
+ export async function parseContent({ config = {}, content, filePath = "", fileType, }) {
314
+ const statements = [];
315
+ const statementTypes = ["testStart", "testEnd", "ignoreStart", "ignoreEnd", "step"];
316
+ function findTest({ tests, testId }) {
317
+ let test = tests.find((t) => t.testId === testId);
318
+ if (!test) {
319
+ test = { testId, steps: [] };
320
+ tests.push(test);
321
+ }
322
+ return test;
323
+ }
324
+ // Determine file type based on provided fileType, file extension, or content detection
325
+ const ext = (filePath?.split('.').pop() || "").toLowerCase();
326
+ fileType = fileType
327
+ || Object.values(defaultFileTypes).find(ft => ft.extensions.includes(ext))
328
+ || detectFileTypeFromContent(content);
329
+ // Precompute line starts for efficient line number lookups
330
+ const lineStarts = getLineStarts(content);
331
+ // Test for each statement type
332
+ statementTypes.forEach((statementType) => {
333
+ if (typeof fileType.inlineStatements === "undefined" ||
334
+ typeof fileType.inlineStatements[statementType] === "undefined")
335
+ return;
336
+ fileType.inlineStatements[statementType].forEach((statementRegex) => {
337
+ const regex = safeRegExp(statementRegex, "g");
338
+ if (!regex)
339
+ return;
340
+ const matches = [...content.matchAll(regex)];
341
+ matches.forEach((match) => {
342
+ match.type = statementType;
343
+ match.sortIndex = match[1] ? match.index + match[1].length : match.index;
344
+ match._startIndex = match.index;
345
+ match._endIndex = match.index + match[0].length;
346
+ match._line = getLineNumber(content, match.index, lineStarts);
347
+ });
348
+ statements.push(...matches);
349
+ });
350
+ });
351
+ if ((config.detectSteps ?? true) && fileType.markup) {
352
+ fileType.markup.forEach((markup) => {
353
+ markup.regex.forEach((pattern) => {
354
+ const regex = safeRegExp(pattern, "g");
355
+ if (!regex)
356
+ return;
357
+ const matches = [...content.matchAll(regex)];
358
+ if (matches.length > 0 && markup.batchMatches) {
359
+ const startIdx = Math.min(...matches.map((m) => m.index));
360
+ const endIdx = Math.max(...matches.map((m) => m.index + m[0].length));
361
+ const combinedMatch = {
362
+ 1: matches.map((match) => match[1] || match[0]).join("\n"),
363
+ type: "detectedStep",
364
+ markup: markup,
365
+ sortIndex: startIdx,
366
+ _startIndex: startIdx,
367
+ _endIndex: endIdx,
368
+ _line: getLineNumber(content, startIdx, lineStarts),
369
+ };
370
+ statements.push(combinedMatch);
371
+ }
372
+ else if (matches.length > 0) {
373
+ matches.forEach((match) => {
374
+ match.type = "detectedStep";
375
+ match.markup = markup;
376
+ match.sortIndex = match[1] ? match.index + match[1].length : match.index;
377
+ match._startIndex = match.index;
378
+ match._endIndex = match.index + match[0].length;
379
+ match._line = getLineNumber(content, match.index, lineStarts);
380
+ });
381
+ statements.push(...matches);
382
+ }
383
+ });
384
+ });
385
+ }
386
+ // Sort statements by index
387
+ statements.sort((a, b) => a.sortIndex - b.sortIndex);
388
+ // Process statements into tests and steps
389
+ let tests = [];
390
+ let testId = generateUUID();
391
+ let ignore = false;
392
+ statements.forEach((statement) => {
393
+ let test;
394
+ let statementContent = "";
395
+ let stepsCleanup = false;
396
+ switch (statement.type) {
397
+ case "testStart": {
398
+ statementContent = statement[1] || statement[0];
399
+ const parsedTest = parseObject({ stringifiedObject: statementContent });
400
+ if (!parsedTest || typeof parsedTest !== 'object')
401
+ break;
402
+ test = parsedTest;
403
+ // If v2 schema, convert to v3
404
+ if (test.id || test.file || test.setup || test.cleanup) {
405
+ if (!test.steps) {
406
+ test.steps = [{ action: "goTo", url: "https://doc-detective.com" }];
407
+ stepsCleanup = true;
408
+ }
409
+ const transformed = transformToSchemaKey({
410
+ object: test,
411
+ currentSchema: "test_v2",
412
+ targetSchema: "test_v3",
413
+ });
414
+ test = transformed;
415
+ if (stepsCleanup && test) {
416
+ test.steps = [];
417
+ }
418
+ }
419
+ if (test.testId) {
420
+ testId = test.testId;
421
+ }
422
+ else {
423
+ test.testId = testId;
424
+ }
425
+ if (test.detectSteps === "false") {
426
+ test.detectSteps = false;
427
+ }
428
+ else if (test.detectSteps === "true") {
429
+ test.detectSteps = true;
430
+ }
431
+ if (!test.steps) {
432
+ test.steps = [];
433
+ }
434
+ tests.push(test);
435
+ break;
436
+ }
437
+ case "testEnd":
438
+ testId = generateUUID();
439
+ ignore = false;
440
+ break;
441
+ case "ignoreStart":
442
+ ignore = true;
443
+ break;
444
+ case "ignoreEnd":
445
+ ignore = false;
446
+ break;
447
+ case "detectedStep":
448
+ if (ignore)
449
+ break;
450
+ test = findTest({ tests, testId });
451
+ if (typeof test.detectSteps !== "undefined" && !test.detectSteps) {
452
+ break;
453
+ }
454
+ if (statement?.markup?.actions) {
455
+ statement.markup.actions.forEach((action) => {
456
+ let step = {};
457
+ if (typeof action === "string") {
458
+ if (action === "runCode")
459
+ return;
460
+ step[action] = statement[1] || statement[0];
461
+ if (config.origin && (action === "goTo" || action === "checkLink")) {
462
+ step[action] = { url: step[action], origin: config.origin };
463
+ }
464
+ // Attach sourceIntegration for Heretto
465
+ if (action === "screenshot" && config._herettoPathMapping) {
466
+ const herettoIntegration = findHerettoIntegration(config, filePath);
467
+ if (herettoIntegration) {
468
+ const screenshotPath = step[action];
469
+ step[action] = {
470
+ path: screenshotPath,
471
+ sourceIntegration: {
472
+ type: "heretto",
473
+ integrationName: herettoIntegration,
474
+ filePath: screenshotPath,
475
+ contentPath: filePath,
476
+ },
477
+ };
478
+ }
479
+ }
480
+ }
481
+ else {
482
+ const replacedStep = replaceNumericVariables(action, statement);
483
+ /* c8 ignore next - typeof string check is defensive; object actions always return objects */
484
+ if (!replacedStep || typeof replacedStep === 'string')
485
+ return;
486
+ step = replacedStep;
487
+ // Attach sourceIntegration for Heretto
488
+ if (step.screenshot && config._herettoPathMapping) {
489
+ const herettoIntegration = findHerettoIntegration(config, filePath);
490
+ if (herettoIntegration) {
491
+ if (typeof step.screenshot === "string") {
492
+ step.screenshot = { path: step.screenshot };
493
+ }
494
+ else if (typeof step.screenshot === "boolean") {
495
+ step.screenshot = {};
496
+ }
497
+ step.screenshot.sourceIntegration = {
498
+ type: "heretto",
499
+ integrationName: herettoIntegration,
500
+ filePath: step.screenshot.path || "",
501
+ contentPath: filePath,
502
+ };
503
+ }
504
+ }
505
+ }
506
+ // Normalize step field formats
507
+ if (step.httpRequest?.request) {
508
+ if (typeof step.httpRequest.request.headers === "string") {
509
+ try {
510
+ const headers = {};
511
+ step.httpRequest.request.headers.split("\n").forEach((header) => {
512
+ const colonIndex = header.indexOf(":");
513
+ if (colonIndex === -1)
514
+ return;
515
+ const key = header.substring(0, colonIndex).trim();
516
+ const value = header.substring(colonIndex + 1).trim();
517
+ /* c8 ignore next 3 - V8 phantom branch in && short-circuit */
518
+ if (key && value) {
519
+ headers[key] = value;
520
+ }
521
+ });
522
+ step.httpRequest.request.headers = headers;
523
+ /* c8 ignore next 2 - string split/forEach can't throw */
524
+ }
525
+ catch (error) {
526
+ }
527
+ }
528
+ if (typeof step.httpRequest.request.body === "string" &&
529
+ (step.httpRequest.request.body.trim().startsWith("{") ||
530
+ step.httpRequest.request.body.trim().startsWith("["))) {
531
+ try {
532
+ step.httpRequest.request.body = JSON.parse(step.httpRequest.request.body);
533
+ }
534
+ catch (error) {
535
+ // Ignore parsing errors
536
+ }
537
+ }
538
+ }
539
+ // Attach source location
540
+ if (typeof statement._startIndex === 'number') {
541
+ step.location = {
542
+ line: statement._line,
543
+ startIndex: statement._startIndex,
544
+ endIndex: statement._endIndex,
545
+ };
546
+ }
547
+ // Validate step
548
+ const valid = validate({
549
+ schemaKey: "step_v3",
550
+ object: step,
551
+ addDefaults: false,
552
+ });
553
+ if (!valid.valid) {
554
+ log(config, "warn", `Step ${JSON.stringify(step)} isn't a valid step. Skipping.`);
555
+ return;
556
+ }
557
+ step = valid.object;
558
+ test.steps.push(step);
559
+ });
560
+ }
561
+ break;
562
+ case "step": {
563
+ if (ignore)
564
+ break;
565
+ test = findTest({ tests, testId });
566
+ statementContent = statement[1] || statement[0];
567
+ const parsedStep = parseObject({ stringifiedObject: statementContent });
568
+ if (!parsedStep || typeof parsedStep !== 'object')
569
+ break;
570
+ let step = parsedStep;
571
+ // Attach source location
572
+ if (typeof statement._startIndex === 'number') {
573
+ step.location = {
574
+ line: statement._line,
575
+ startIndex: statement._startIndex,
576
+ endIndex: statement._endIndex,
577
+ };
578
+ }
579
+ const validation = validate({
580
+ schemaKey: "step_v3",
581
+ object: step,
582
+ addDefaults: false,
583
+ });
584
+ /* c8 ignore start - V8 phantom branch on if-else/switch-case */
585
+ if (!validation.valid) {
586
+ log(config, "warn", `Step ${JSON.stringify(step)} isn't a valid step. Skipping.`);
587
+ return;
588
+ }
589
+ step = validation.object;
590
+ test.steps.push(step);
591
+ break;
592
+ /* c8 ignore stop */
593
+ }
594
+ /* c8 ignore next 2 - all statement types are handled above */
595
+ default:
596
+ break;
597
+ }
598
+ });
599
+ // Set contentPath on tests when filePath is provided
600
+ if (filePath) {
601
+ tests.forEach((test) => {
602
+ test.contentPath = filePath;
603
+ });
604
+ }
605
+ // Validate test objects
606
+ const validatedTests = [];
607
+ tests.forEach((test) => {
608
+ const validation = validate({
609
+ schemaKey: "test_v3",
610
+ object: test,
611
+ addDefaults: false,
612
+ });
613
+ if (!validation.valid) {
614
+ log(config, "warn", `Couldn't convert test in ${filePath} to valid test. Skipping.`);
615
+ return;
616
+ }
617
+ validatedTests.push(validation.object);
618
+ });
619
+ return validatedTests;
620
+ }
621
+ /**
622
+ * Helper function to find which Heretto integration a file belongs to.
623
+ */
624
+ function findHerettoIntegration(config, filePath) {
625
+ /* c8 ignore next - callers always check _herettoPathMapping before calling */
626
+ if (!config._herettoPathMapping)
627
+ return null;
628
+ // Simple string matching since we don't have path.resolve in browser
629
+ const normalizedFilePath = filePath.replace(/\\/g, "/");
630
+ for (const [outputPath, integrationName] of Object.entries(config._herettoPathMapping)) {
631
+ const normalizedOutputPath = outputPath.replace(/\\/g, "/");
632
+ if (normalizedFilePath.startsWith(normalizedOutputPath)) {
633
+ return integrationName;
634
+ }
635
+ }
636
+ return null;
637
+ }
638
+ /**
639
+ * Simple browser-compatible logging function.
640
+ */
641
+ export function log(config, level, message) {
642
+ const logLevels = ["silent", "error", "warn", "info", "debug"];
643
+ // Normalize 'warning' to 'warn' for both config and message levels
644
+ const configLevel = (config.logLevel || "info") === "warning" ? "warn" : (config.logLevel || "info");
645
+ const normalizedLevel = level === "warning" ? "warn" : level;
646
+ const configLevelIndex = logLevels.indexOf(configLevel);
647
+ const messageLevelIndex = logLevels.indexOf(normalizedLevel);
648
+ if (configLevelIndex < 0 || messageLevelIndex < 0)
649
+ return;
650
+ if (messageLevelIndex > configLevelIndex)
651
+ return;
652
+ // Treat message-level 'silent' as a no-op to avoid calling an undefined console method
653
+ if (normalizedLevel === "silent")
654
+ return;
655
+ if (typeof message === "object") {
656
+ console[normalizedLevel](JSON.stringify(message, null, 2));
657
+ }
658
+ else {
659
+ console[normalizedLevel](message);
660
+ }
661
+ }
662
+ //# sourceMappingURL=detectTests.js.map