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.
- package/dist/common/src/detectTests.d.ts +101 -0
- package/dist/common/src/detectTests.d.ts.map +1 -0
- package/dist/common/src/detectTests.js +662 -0
- package/dist/common/src/detectTests.js.map +1 -0
- package/dist/common/src/fileTypes.d.ts +35 -0
- package/dist/common/src/fileTypes.d.ts.map +1 -0
- package/dist/common/src/fileTypes.js +293 -0
- package/dist/common/src/fileTypes.js.map +1 -0
- package/dist/common/src/index.d.ts +10 -0
- package/dist/common/src/index.d.ts.map +1 -0
- package/dist/common/src/index.js +5 -0
- package/dist/common/src/index.js.map +1 -0
- package/dist/common/src/schemas/index.d.ts +5 -0
- package/dist/common/src/schemas/index.d.ts.map +1 -0
- package/dist/common/src/schemas/index.js +3 -0
- package/dist/common/src/schemas/index.js.map +1 -0
- package/dist/common/src/schemas/schemas.json +128117 -0
- package/dist/common/src/types/generated/checkLink_v3.d.ts +27 -0
- package/dist/common/src/types/generated/checkLink_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/checkLink_v3.js +7 -0
- package/dist/common/src/types/generated/checkLink_v3.js.map +1 -0
- package/dist/common/src/types/generated/click_v3.d.ts +16 -0
- package/dist/common/src/types/generated/click_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/click_v3.js +7 -0
- package/dist/common/src/types/generated/click_v3.js.map +1 -0
- package/dist/common/src/types/generated/config_v3.d.ts +398 -0
- package/dist/common/src/types/generated/config_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/config_v3.js +7 -0
- package/dist/common/src/types/generated/config_v3.js.map +1 -0
- package/dist/common/src/types/generated/context_v3.d.ts +108 -0
- package/dist/common/src/types/generated/context_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/context_v3.js +7 -0
- package/dist/common/src/types/generated/context_v3.js.map +1 -0
- package/dist/common/src/types/generated/dragAndDrop_v3.d.ts +37 -0
- package/dist/common/src/types/generated/dragAndDrop_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/dragAndDrop_v3.js +7 -0
- package/dist/common/src/types/generated/dragAndDrop_v3.js.map +1 -0
- package/dist/common/src/types/generated/endRecord_v3.d.ts +9 -0
- package/dist/common/src/types/generated/endRecord_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/endRecord_v3.js +7 -0
- package/dist/common/src/types/generated/endRecord_v3.js.map +1 -0
- package/dist/common/src/types/generated/find_v3.d.ts +16 -0
- package/dist/common/src/types/generated/find_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/find_v3.js +7 -0
- package/dist/common/src/types/generated/find_v3.js.map +1 -0
- package/dist/common/src/types/generated/goTo_v3.d.ts +46 -0
- package/dist/common/src/types/generated/goTo_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/goTo_v3.js +7 -0
- package/dist/common/src/types/generated/goTo_v3.js.map +1 -0
- package/dist/common/src/types/generated/httpRequest_v3.d.ts +16 -0
- package/dist/common/src/types/generated/httpRequest_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/httpRequest_v3.js +7 -0
- package/dist/common/src/types/generated/httpRequest_v3.js.map +1 -0
- package/dist/common/src/types/generated/loadCookie_v3.d.ts +16 -0
- package/dist/common/src/types/generated/loadCookie_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/loadCookie_v3.js +7 -0
- package/dist/common/src/types/generated/loadCookie_v3.js.map +1 -0
- package/dist/common/src/types/generated/loadVariables_v3.d.ts +9 -0
- package/dist/common/src/types/generated/loadVariables_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/loadVariables_v3.js +7 -0
- package/dist/common/src/types/generated/loadVariables_v3.js.map +1 -0
- package/dist/common/src/types/generated/openApi_v3.d.ts +62 -0
- package/dist/common/src/types/generated/openApi_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/openApi_v3.js +7 -0
- package/dist/common/src/types/generated/openApi_v3.js.map +1 -0
- package/dist/common/src/types/generated/record_v3.d.ts +32 -0
- package/dist/common/src/types/generated/record_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/record_v3.js +7 -0
- package/dist/common/src/types/generated/record_v3.js.map +1 -0
- package/dist/common/src/types/generated/report_v3.d.ts +174 -0
- package/dist/common/src/types/generated/report_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/report_v3.js +7 -0
- package/dist/common/src/types/generated/report_v3.js.map +1 -0
- package/dist/common/src/types/generated/resolvedTests_v3.d.ts +571 -0
- package/dist/common/src/types/generated/resolvedTests_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/resolvedTests_v3.js +7 -0
- package/dist/common/src/types/generated/resolvedTests_v3.js.map +1 -0
- package/dist/common/src/types/generated/runCode_v3.d.ts +57 -0
- package/dist/common/src/types/generated/runCode_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/runCode_v3.js +7 -0
- package/dist/common/src/types/generated/runCode_v3.js.map +1 -0
- package/dist/common/src/types/generated/runShell_v3.d.ts +56 -0
- package/dist/common/src/types/generated/runShell_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/runShell_v3.js +7 -0
- package/dist/common/src/types/generated/runShell_v3.js.map +1 -0
- package/dist/common/src/types/generated/saveCookie_v3.d.ts +16 -0
- package/dist/common/src/types/generated/saveCookie_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/saveCookie_v3.js +7 -0
- package/dist/common/src/types/generated/saveCookie_v3.js.map +1 -0
- package/dist/common/src/types/generated/screenshot_v3.d.ts +74 -0
- package/dist/common/src/types/generated/screenshot_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/screenshot_v3.js +7 -0
- package/dist/common/src/types/generated/screenshot_v3.js.map +1 -0
- package/dist/common/src/types/generated/sourceIntegration_v3.d.ts +30 -0
- package/dist/common/src/types/generated/sourceIntegration_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/sourceIntegration_v3.js +7 -0
- package/dist/common/src/types/generated/sourceIntegration_v3.js.map +1 -0
- package/dist/common/src/types/generated/spec_v3.d.ts +159 -0
- package/dist/common/src/types/generated/spec_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/spec_v3.js +7 -0
- package/dist/common/src/types/generated/spec_v3.js.map +1 -0
- package/dist/common/src/types/generated/step_v3.d.ts +1558 -0
- package/dist/common/src/types/generated/step_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/step_v3.js +7 -0
- package/dist/common/src/types/generated/step_v3.js.map +1 -0
- package/dist/common/src/types/generated/stopRecord_v3.d.ts +9 -0
- package/dist/common/src/types/generated/stopRecord_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/stopRecord_v3.js +7 -0
- package/dist/common/src/types/generated/stopRecord_v3.js.map +1 -0
- package/dist/common/src/types/generated/test_v3.d.ts +3491 -0
- package/dist/common/src/types/generated/test_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/test_v3.js +7 -0
- package/dist/common/src/types/generated/test_v3.js.map +1 -0
- package/dist/common/src/types/generated/type_v3.d.ts +54 -0
- package/dist/common/src/types/generated/type_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/type_v3.js +7 -0
- package/dist/common/src/types/generated/type_v3.js.map +1 -0
- package/dist/common/src/types/generated/wait_v3.d.ts +12 -0
- package/dist/common/src/types/generated/wait_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/wait_v3.js +7 -0
- package/dist/common/src/types/generated/wait_v3.js.map +1 -0
- package/dist/common/src/validate.d.ts +41 -0
- package/dist/common/src/validate.d.ts.map +1 -0
- package/dist/common/src/validate.js +557 -0
- package/dist/common/src/validate.js.map +1 -0
- package/package.json +6 -1
- package/.doc-detective.json +0 -1
- package/CONTRIBUTIONS.md +0 -27
- package/screenshot-boolean.png +0 -0
- 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
|