@xylentis/testgen 0.0.0-stage → 1.0.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.
@@ -0,0 +1,427 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { InvalidSelectorError, parseCSS } from './cssParser.js';
17
+ export { InvalidSelectorError, isInvalidSelectorError } from './cssParser.js';
18
+ const kNestedSelectorNames = new Set(['internal:has', 'internal:has-not', 'internal:and', 'internal:or', 'internal:chain', 'left-of', 'right-of', 'above', 'below', 'near']);
19
+ const kNestedSelectorNamesWithDistance = new Set(['left-of', 'right-of', 'above', 'below', 'near']);
20
+ export const customCSSNames = new Set(['not', 'is', 'where', 'has', 'scope', 'light', 'visible', 'text', 'text-matches', 'text-is', 'has-text', 'above', 'below', 'right-of', 'left-of', 'near', 'nth-match']);
21
+ export function parseSelector(selector) {
22
+ const parsedStrings = parseSelectorString(selector);
23
+ const parts = [];
24
+ for (const part of parsedStrings.parts) {
25
+ if (part.name === 'css' || part.name === 'css:light') {
26
+ if (part.name === 'css:light')
27
+ part.body = ':light(' + part.body + ')';
28
+ const parsedCSS = parseCSS(part.body, customCSSNames);
29
+ parts.push({
30
+ name: 'css',
31
+ body: parsedCSS.selector,
32
+ source: part.body
33
+ });
34
+ continue;
35
+ }
36
+ if (kNestedSelectorNames.has(part.name)) {
37
+ let innerSelector;
38
+ let distance;
39
+ try {
40
+ const unescaped = JSON.parse('[' + part.body + ']');
41
+ if (!Array.isArray(unescaped) || unescaped.length < 1 || unescaped.length > 2 || typeof unescaped[0] !== 'string')
42
+ throw new InvalidSelectorError(`Malformed selector: ${part.name}=` + part.body);
43
+ innerSelector = unescaped[0];
44
+ if (unescaped.length === 2) {
45
+ if (typeof unescaped[1] !== 'number' || !kNestedSelectorNamesWithDistance.has(part.name))
46
+ throw new InvalidSelectorError(`Malformed selector: ${part.name}=` + part.body);
47
+ distance = unescaped[1];
48
+ }
49
+ }
50
+ catch (e) {
51
+ throw new InvalidSelectorError(`Malformed selector: ${part.name}=` + part.body);
52
+ }
53
+ const nested = { name: part.name, source: part.body, body: { parsed: parseSelector(innerSelector), distance } };
54
+ const lastFrame = [...nested.body.parsed.parts].reverse().find(part => part.name === 'internal:control' && part.body === 'enter-frame');
55
+ const lastFrameIndex = lastFrame ? nested.body.parsed.parts.indexOf(lastFrame) : -1;
56
+ // The "any-frame" token applies to the whole selector, so nested selectors must not repeat it.
57
+ const outerParts = parts[0]?.name === 'internal:control' && parts[0].body === 'any-frame' ? parts.slice(1) : parts;
58
+ // Allow nested selectors to start with the same frame selector.
59
+ if (lastFrameIndex !== -1 && selectorPartsEqual(nested.body.parsed.parts.slice(0, lastFrameIndex + 1), outerParts.slice(0, lastFrameIndex + 1))) {
60
+ nested.body.parsed.parts.splice(0, lastFrameIndex + 1);
61
+ if (nested.body.parsed.capture !== undefined) {
62
+ if (nested.body.parsed.capture <= lastFrameIndex)
63
+ throw new InvalidSelectorError(`Can not capture the selector before diving into the frame. Only use * after the last frame has been selected`);
64
+ // The capture refers to a part index, so it shifts along with the removed prefix.
65
+ nested.body.parsed.capture -= lastFrameIndex + 1;
66
+ }
67
+ }
68
+ parts.push(nested);
69
+ continue;
70
+ }
71
+ parts.push({ ...part, source: part.body });
72
+ }
73
+ if (kNestedSelectorNames.has(parts[0].name))
74
+ throw new InvalidSelectorError(`"${parts[0].name}" selector cannot be first`);
75
+ return {
76
+ capture: parsedStrings.capture,
77
+ parts
78
+ };
79
+ }
80
+ // Matches in any frame of the subtree, instead of the frame itself. Only allowed as the first token.
81
+ export const kAnyFrameSelector = 'internal:control=any-frame';
82
+ // Splits a selector into per-frame chunks separated by "enter-frame" boundaries.
83
+ // The optional leading "any-frame" token is consumed and reported separately.
84
+ export function splitSelectorByFrame(selectorText) {
85
+ const selector = parseSelector(selectorText);
86
+ const chunks = [];
87
+ let chunk = {
88
+ parts: [],
89
+ };
90
+ let anyFrame = false;
91
+ let chunkStartIndex = 0;
92
+ for (let i = 0; i < selector.parts.length; ++i) {
93
+ const part = selector.parts[i];
94
+ if (part.name === 'internal:control' && part.body === 'any-frame') {
95
+ // The starting frame applies to the whole selector, so the token only makes sense as the very first one.
96
+ if (i !== 0)
97
+ throw new InvalidSelectorError(`"${part.body}" is only allowed as the first selector token, while parsing selector ${selectorText}`);
98
+ anyFrame = true;
99
+ chunkStartIndex = i + 1;
100
+ continue;
101
+ }
102
+ if (part.name === 'internal:control' && part.body === 'enter-frame') {
103
+ const lastPart = chunk.parts[chunk.parts.length - 1];
104
+ if (!lastPart || (lastPart.name === 'internal:control' && lastPart.body === 'enter-frame'))
105
+ throw new InvalidSelectorError('Selector cannot start with entering frame, select the iframe first');
106
+ chunks.push(chunk);
107
+ chunk = { parts: [] };
108
+ chunkStartIndex = i + 1;
109
+ continue;
110
+ }
111
+ if (selector.capture === i)
112
+ chunk.capture = i - chunkStartIndex;
113
+ chunk.parts.push(part);
114
+ }
115
+ if (!chunk.parts.length) {
116
+ if (anyFrame && !chunks.length)
117
+ throw new InvalidSelectorError(`Selector cannot be empty after frameLocator(), while parsing selector ${selectorText}`);
118
+ throw new InvalidSelectorError(`Selector cannot end with entering frame, while parsing selector ${selectorText}`);
119
+ }
120
+ const lastPart = chunk.parts[chunk.parts.length - 1];
121
+ if (lastPart.name === 'internal:control' && lastPart.body === 'enter-frame')
122
+ throw new InvalidSelectorError(`Selector cannot end with entering frame, while parsing selector ${selectorText}`);
123
+ chunks.push(chunk);
124
+ if (typeof selector.capture === 'number' && typeof chunks[chunks.length - 1].capture !== 'number')
125
+ throw new InvalidSelectorError(`Can not capture the selector before diving into the frame. Only use * after the last frame has been selected`);
126
+ return { anyFrame, chunks };
127
+ }
128
+ function selectorPartsEqual(list1, list2) {
129
+ return stringifySelector({ parts: list1 }) === stringifySelector({ parts: list2 });
130
+ }
131
+ export function stringifySelector(selector, forceEngineName) {
132
+ if (typeof selector === 'string')
133
+ return selector;
134
+ return selector.parts.map((p, i) => {
135
+ let includeEngine = true;
136
+ if (!forceEngineName && i !== selector.capture) {
137
+ if (p.name === 'css')
138
+ includeEngine = false;
139
+ else if (p.name === 'xpath' && (p.source.startsWith('//') || p.source.startsWith('..')))
140
+ includeEngine = false;
141
+ }
142
+ const prefix = includeEngine ? p.name + '=' : '';
143
+ return `${i === selector.capture ? '*' : ''}${prefix}${p.source}`;
144
+ }).join(' >> ');
145
+ }
146
+ export function visitAllSelectorParts(selector, visitor) {
147
+ const visit = (selector, nested) => {
148
+ for (const part of selector.parts) {
149
+ visitor(part, nested);
150
+ if (kNestedSelectorNames.has(part.name))
151
+ visit(part.body.parsed, true);
152
+ }
153
+ };
154
+ visit(selector, false);
155
+ }
156
+ function parseSelectorString(selector) {
157
+ let index = 0;
158
+ let quote;
159
+ let start = 0;
160
+ const result = { parts: [] };
161
+ const append = () => {
162
+ const part = selector.substring(start, index).trim();
163
+ const eqIndex = part.indexOf('=');
164
+ let name;
165
+ let body;
166
+ if (eqIndex !== -1 && part.substring(0, eqIndex).trim().match(/^[a-zA-Z_0-9-+:*]+$/)) {
167
+ name = part.substring(0, eqIndex).trim();
168
+ body = part.substring(eqIndex + 1);
169
+ }
170
+ else if (part.length > 1 && part[0] === '"' && part[part.length - 1] === '"') {
171
+ name = 'text';
172
+ body = part;
173
+ }
174
+ else if (part.length > 1 && part[0] === "'" && part[part.length - 1] === "'") {
175
+ name = 'text';
176
+ body = part;
177
+ }
178
+ else if (/^\(*\/\//.test(part) || part.startsWith('..')) {
179
+ // If selector starts with '//' or '//' prefixed with multiple opening
180
+ // parenthesis, consider xpath. @see https://github.com/microsoft/playwright/issues/817
181
+ // If selector starts with '..', consider xpath as well.
182
+ name = 'xpath';
183
+ body = part;
184
+ }
185
+ else {
186
+ name = 'css';
187
+ body = part;
188
+ }
189
+ let capture = false;
190
+ if (name[0] === '*') {
191
+ capture = true;
192
+ name = name.substring(1);
193
+ }
194
+ result.parts.push({ name, body });
195
+ if (capture) {
196
+ if (result.capture !== undefined)
197
+ throw new InvalidSelectorError(`Only one of the selectors can capture using * modifier`);
198
+ result.capture = result.parts.length - 1;
199
+ }
200
+ };
201
+ if (!selector.includes('>>')) {
202
+ index = selector.length;
203
+ append();
204
+ return result;
205
+ }
206
+ const shouldIgnoreTextSelectorQuote = () => {
207
+ const prefix = selector.substring(start, index);
208
+ const match = prefix.match(/^\s*text\s*=(.*)$/);
209
+ // Must be a text selector with some text before the quote.
210
+ return !!match && !!match[1];
211
+ };
212
+ while (index < selector.length) {
213
+ const c = selector[index];
214
+ if (c === '\\' && index + 1 < selector.length) {
215
+ index += 2;
216
+ }
217
+ else if (c === quote) {
218
+ quote = undefined;
219
+ index++;
220
+ }
221
+ else if (!quote && (c === '"' || c === '\'' || c === '`') && !shouldIgnoreTextSelectorQuote()) {
222
+ quote = c;
223
+ index++;
224
+ }
225
+ else if (!quote && c === '>' && selector[index + 1] === '>') {
226
+ append();
227
+ index += 2;
228
+ start = index;
229
+ }
230
+ else {
231
+ index++;
232
+ }
233
+ }
234
+ append();
235
+ return result;
236
+ }
237
+ export function parseAttributeSelector(selector, allowUnquotedStrings) {
238
+ let wp = 0;
239
+ let EOL = selector.length === 0;
240
+ const next = () => selector[wp] || '';
241
+ const eat1 = () => {
242
+ const result = next();
243
+ ++wp;
244
+ EOL = wp >= selector.length;
245
+ return result;
246
+ };
247
+ const syntaxError = (stage) => {
248
+ if (EOL)
249
+ throw new InvalidSelectorError(`Unexpected end of selector while parsing selector \`${selector}\``);
250
+ throw new InvalidSelectorError(`Error while parsing selector \`${selector}\` - unexpected symbol "${next()}" at position ${wp}` + (stage ? ' during ' + stage : ''));
251
+ };
252
+ function skipSpaces() {
253
+ while (!EOL && /\s/.test(next()))
254
+ eat1();
255
+ }
256
+ function isCSSNameChar(char) {
257
+ // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
258
+ return (char >= '\u0080') // non-ascii
259
+ || (char >= '\u0030' && char <= '\u0039') // digit
260
+ || (char >= '\u0041' && char <= '\u005a') // uppercase letter
261
+ || (char >= '\u0061' && char <= '\u007a') // lowercase letter
262
+ || (char >= '\u0030' && char <= '\u0039') // digit
263
+ || char === '\u005f' // "_"
264
+ || char === '\u002d'; // "-"
265
+ }
266
+ function readIdentifier() {
267
+ let result = '';
268
+ skipSpaces();
269
+ while (!EOL && isCSSNameChar(next()))
270
+ result += eat1();
271
+ return result;
272
+ }
273
+ function readQuotedString(quote) {
274
+ let result = eat1();
275
+ if (result !== quote)
276
+ syntaxError('parsing quoted string');
277
+ while (!EOL && next() !== quote) {
278
+ if (next() === '\\')
279
+ eat1();
280
+ result += eat1();
281
+ }
282
+ if (next() !== quote)
283
+ syntaxError('parsing quoted string');
284
+ result += eat1();
285
+ return result;
286
+ }
287
+ function readRegularExpression() {
288
+ if (eat1() !== '/')
289
+ syntaxError('parsing regular expression');
290
+ let source = '';
291
+ let inClass = false;
292
+ // https://262.ecma-international.org/11.0/#sec-literals-regular-expression-literals
293
+ while (!EOL) {
294
+ if (next() === '\\') {
295
+ source += eat1();
296
+ if (EOL)
297
+ syntaxError('parsing regular expression');
298
+ }
299
+ else if (inClass && next() === ']') {
300
+ inClass = false;
301
+ }
302
+ else if (!inClass && next() === '[') {
303
+ inClass = true;
304
+ }
305
+ else if (!inClass && next() === '/') {
306
+ break;
307
+ }
308
+ source += eat1();
309
+ }
310
+ if (eat1() !== '/')
311
+ syntaxError('parsing regular expression');
312
+ let flags = '';
313
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
314
+ while (!EOL && next().match(/[dgimsuvy]/))
315
+ flags += eat1();
316
+ try {
317
+ return new RegExp(source, flags);
318
+ }
319
+ catch (e) {
320
+ throw new InvalidSelectorError(`Error while parsing selector \`${selector}\`: ${e.message}`);
321
+ }
322
+ }
323
+ function readAttributeToken() {
324
+ let token = '';
325
+ skipSpaces();
326
+ if (next() === `'` || next() === `"`)
327
+ token = readQuotedString(next()).slice(1, -1);
328
+ else
329
+ token = readIdentifier();
330
+ if (!token)
331
+ syntaxError('parsing property path');
332
+ return token;
333
+ }
334
+ function readOperator() {
335
+ skipSpaces();
336
+ let op = '';
337
+ if (!EOL)
338
+ op += eat1();
339
+ if (!EOL && (op !== '='))
340
+ op += eat1();
341
+ if (!['=', '*=', '^=', '$=', '|=', '~='].includes(op))
342
+ syntaxError('parsing operator');
343
+ return op;
344
+ }
345
+ function readAttribute() {
346
+ // skip leading [
347
+ eat1();
348
+ // read attribute name:
349
+ // foo.bar
350
+ // 'foo' . "ba zz"
351
+ const jsonPath = [];
352
+ jsonPath.push(readAttributeToken());
353
+ skipSpaces();
354
+ while (next() === '.') {
355
+ eat1();
356
+ jsonPath.push(readAttributeToken());
357
+ skipSpaces();
358
+ }
359
+ // check property is truthy: [enabled]
360
+ if (next() === ']') {
361
+ eat1();
362
+ return { name: jsonPath.join('.'), jsonPath, op: '<truthy>', value: null, caseSensitive: false };
363
+ }
364
+ const operator = readOperator();
365
+ let value = undefined;
366
+ let caseSensitive = true;
367
+ skipSpaces();
368
+ if (next() === '/') {
369
+ if (operator !== '=')
370
+ throw new InvalidSelectorError(`Error while parsing selector \`${selector}\` - cannot use ${operator} in attribute with regular expression`);
371
+ value = readRegularExpression();
372
+ }
373
+ else if (next() === `'` || next() === `"`) {
374
+ value = readQuotedString(next()).slice(1, -1);
375
+ skipSpaces();
376
+ if (next() === 'i' || next() === 'I') {
377
+ caseSensitive = false;
378
+ eat1();
379
+ }
380
+ else if (next() === 's' || next() === 'S') {
381
+ caseSensitive = true;
382
+ eat1();
383
+ }
384
+ }
385
+ else {
386
+ value = '';
387
+ while (!EOL && (isCSSNameChar(next()) || next() === '+' || next() === '.'))
388
+ value += eat1();
389
+ if (value === 'true') {
390
+ value = true;
391
+ }
392
+ else if (value === 'false') {
393
+ value = false;
394
+ }
395
+ else {
396
+ if (!allowUnquotedStrings) {
397
+ value = +value;
398
+ if (Number.isNaN(value))
399
+ syntaxError('parsing attribute value');
400
+ }
401
+ }
402
+ }
403
+ skipSpaces();
404
+ if (next() !== ']')
405
+ syntaxError('parsing attribute value');
406
+ eat1();
407
+ if (operator !== '=' && typeof value !== 'string')
408
+ throw new InvalidSelectorError(`Error while parsing selector \`${selector}\` - cannot use ${operator} in attribute with non-string matching value - ${value}`);
409
+ return { name: jsonPath.join('.'), jsonPath, op: operator, value, caseSensitive };
410
+ }
411
+ const result = {
412
+ name: '',
413
+ attributes: [],
414
+ };
415
+ result.name = readIdentifier();
416
+ skipSpaces();
417
+ while (next() === '[') {
418
+ result.attributes.push(readAttribute());
419
+ skipSpaces();
420
+ }
421
+ if (!EOL)
422
+ syntaxError(undefined);
423
+ if (!result.name && !result.attributes.length)
424
+ throw new InvalidSelectorError(`Error while parsing selector \`${selector}\` - selector cannot be empty`);
425
+ return result;
426
+ }
427
+ //# sourceMappingURL=selectorParser.js.map
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ // NOTE: this function should not be used to escape any selectors.
17
+ export function escapeWithQuotes(text, char = '\'') {
18
+ const stringified = JSON.stringify(text);
19
+ const escapedText = stringified.substring(1, stringified.length - 1).replace(/\\"/g, '"');
20
+ if (char === '\'')
21
+ return char + escapedText.replace(/[']/g, '\\\'') + char;
22
+ if (char === '"')
23
+ return char + escapedText.replace(/["]/g, '\\"') + char;
24
+ if (char === '`')
25
+ return char + escapedText.replace(/[`]/g, '\\`') + char;
26
+ throw new Error('Invalid escape char');
27
+ }
28
+ export function escapeTemplateString(text) {
29
+ return text
30
+ .replace(/\\/g, '\\\\')
31
+ .replace(/`/g, '\\`')
32
+ .replace(/\$\{/g, '\\${');
33
+ }
34
+ export function isString(obj) {
35
+ return typeof obj === 'string' || obj instanceof String;
36
+ }
37
+ export function toTitleCase(name) {
38
+ return name.charAt(0).toUpperCase() + name.substring(1);
39
+ }
40
+ export function toSnakeCase(name) {
41
+ // E.g. ignoreHTTPSErrors => ignore_https_errors.
42
+ return name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/([A-Z])([A-Z][a-z])/g, '$1_$2').toLowerCase();
43
+ }
44
+ export function formatObject(value, indent = ' ', mode = 'multiline') {
45
+ if (typeof value === 'string')
46
+ return escapeWithQuotes(value, '\'');
47
+ if (Array.isArray(value))
48
+ return `[${value.map(o => formatObject(o)).join(', ')}]`;
49
+ if (typeof value === 'object') {
50
+ const keys = Object.keys(value).filter(key => key !== 'timeout' && value[key] !== undefined).sort();
51
+ if (!keys.length)
52
+ return '{}';
53
+ const tokens = [];
54
+ for (const key of keys)
55
+ tokens.push(`${key}: ${formatObject(value[key])}`);
56
+ if (mode === 'multiline')
57
+ return `{\n${tokens.map(t => indent + t).join(`,\n`)}\n}`;
58
+ return `{ ${tokens.join(', ')} }`;
59
+ }
60
+ return String(value);
61
+ }
62
+ export function formatObjectOrVoid(value, indent = ' ') {
63
+ const result = formatObject(value, indent);
64
+ return result === '{}' ? '' : result;
65
+ }
66
+ export function quoteCSSAttributeValue(text) {
67
+ return `"${text.replace(/["\\]/g, char => '\\' + char)}"`;
68
+ }
69
+ let normalizedWhitespaceCache;
70
+ export function cacheNormalizedWhitespaces() {
71
+ normalizedWhitespaceCache = new Map();
72
+ }
73
+ export function normalizeWhiteSpace(text) {
74
+ let result = normalizedWhitespaceCache?.get(text);
75
+ if (result === undefined) {
76
+ result = text.replace(/[\u200b\u00ad]/g, '').trim().replace(/\s+/g, ' ');
77
+ normalizedWhitespaceCache?.set(text, result);
78
+ }
79
+ return result;
80
+ }
81
+ export function normalizeEscapedRegexQuotes(source) {
82
+ // This function reverses the effect of escapeRegexForSelector below.
83
+ // Odd number of backslashes followed by the quote -> remove unneeded backslash.
84
+ return source.replace(/(^|[^\\])(\\\\)*\\(['"`])/g, '$1$2$3');
85
+ }
86
+ function escapeRegexForSelector(re) {
87
+ // Unicode mode does not allow "identity character escapes", so we do not escape and
88
+ // hope that it does not contain quotes and/or >> signs.
89
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Character_escape
90
+ // TODO: rework RE usages in internal selectors away from literal representation to json, e.g. {source,flags}.
91
+ if (re.unicode || re.unicodeSets)
92
+ return String(re);
93
+ // Even number of backslashes followed by the quote -> insert a backslash.
94
+ return String(re).replace(/(^|[^\\])(\\\\)*(["'`])/g, '$1$2\\$3').replace(/>>/g, '\\>\\>');
95
+ }
96
+ export function escapeForTextSelector(text, exact) {
97
+ if (typeof text !== 'string')
98
+ return escapeRegexForSelector(text);
99
+ return `${JSON.stringify(text)}${exact ? 's' : 'i'}`;
100
+ }
101
+ export function escapeForAttributeSelector(value, exact) {
102
+ if (typeof value !== 'string')
103
+ return escapeRegexForSelector(value);
104
+ // TODO: this should actually be
105
+ // cssEscape(value).replace(/\\ /g, ' ')
106
+ // However, our attribute selectors do not conform to CSS parsing spec,
107
+ // so we escape them differently.
108
+ return `"${value.replace(/\\/g, '\\\\').replace(/["]/g, '\\"')}"${exact ? 's' : 'i'}`;
109
+ }
110
+ export function trimString(input, cap, suffix = '') {
111
+ if (input.length <= cap)
112
+ return input;
113
+ const chars = [...input];
114
+ if (chars.length > cap)
115
+ return chars.slice(0, cap - suffix.length).join('') + suffix;
116
+ return chars.join('');
117
+ }
118
+ export function trimStringWithEllipsis(input, cap) {
119
+ return trimString(input, cap, '\u2026');
120
+ }
121
+ export function truncateDataUrl(url) {
122
+ // Data URLs can carry megabytes of base64 payload, which is never useful in
123
+ // human/AI-facing output. Keep the media type prefix for context, drop the data.
124
+ if (!url.startsWith('data:'))
125
+ return url;
126
+ const comma = url.indexOf(',');
127
+ if (comma === -1)
128
+ return url;
129
+ return url.slice(0, comma + 1) + '\u2026';
130
+ }
131
+ export function escapeRegExp(s) {
132
+ // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
133
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
134
+ }
135
+ const escaped = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', '\'': '&#39;' };
136
+ export function escapeHTMLAttribute(s) {
137
+ return s.replace(/[&<>"']/ug, char => escaped[char]);
138
+ }
139
+ export function escapeHTML(s) {
140
+ return s.replace(/[&<]/ug, char => escaped[char]);
141
+ }
142
+ export function longestCommonSubstring(s1, s2) {
143
+ const n = s1.length;
144
+ const m = s2.length;
145
+ let maxLen = 0;
146
+ let endingIndex = 0;
147
+ // Initialize a 2D array with zeros
148
+ const dp = Array(n + 1)
149
+ .fill(null)
150
+ .map(() => Array(m + 1).fill(0));
151
+ // Build the dp table
152
+ for (let i = 1; i <= n; i++) {
153
+ for (let j = 1; j <= m; j++) {
154
+ if (s1[i - 1] === s2[j - 1]) {
155
+ dp[i][j] = dp[i - 1][j - 1] + 1;
156
+ if (dp[i][j] > maxLen) {
157
+ maxLen = dp[i][j];
158
+ endingIndex = i;
159
+ }
160
+ }
161
+ }
162
+ }
163
+ // Extract the longest common substring
164
+ return s1.slice(endingIndex - maxLen, endingIndex);
165
+ }
166
+ export function parseRegex(regex) {
167
+ if (regex[0] !== '/')
168
+ throw new Error(`Invalid regex, must start with '/': ${regex}`);
169
+ const lastSlash = regex.lastIndexOf('/');
170
+ if (lastSlash <= 0)
171
+ throw new Error(`Invalid regex, must end with '/' followed by optional flags: ${regex}`);
172
+ const source = regex.slice(1, lastSlash);
173
+ const flags = regex.slice(lastSlash + 1);
174
+ return new RegExp(source, flags);
175
+ }
176
+ export function tomlBasicString(value) {
177
+ // JSON.stringify produces a valid TOML basic string: escapes \", \\, \n, \r, \t and uses \uXXXX for control chars.
178
+ return JSON.stringify(value);
179
+ }
180
+ export function tomlArray(values) {
181
+ return `[${values.map(value => tomlBasicString(value)).join(', ')}]`;
182
+ }
183
+ export function tomlMultilineBasicString(value) {
184
+ // Triple-quoted basic string: escape backslashes first, then any literal """ sequences.
185
+ const escaped = value.replace(/\\/g, '\\\\').replace(/"""/g, '\\"\\"\\"');
186
+ return `"""\n${escaped}\n"""`;
187
+ }
188
+ // Semicolons removed from [[\]()#;?] to avoid polynomial backtracking
189
+ // when both that group and (?:;...)* can match runs of semicolons.
190
+ // \d{1,4} relaxed to \d{0,4} so empty params (e.g. ESC[;H) still match.
191
+ export const ansiRegex = new RegExp('([\\u001B\\u009B][[\\]()#?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{0,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~])))', 'g');
192
+ export function stripAnsiEscapes(str) {
193
+ return str.replace(ansiRegex, '');
194
+ }
195
+ //# sourceMappingURL=stringUtils.js.map
Binary file
package/package.json CHANGED
@@ -1,6 +1,51 @@
1
1
  {
2
2
  "name": "@xylentis/testgen",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Record a UI test once with the Playwright recorder, then generate it for Playwright (TypeScript, Go) and Selenium (Java, JavaScript).",
5
+ "keywords": [
6
+ "playwright",
7
+ "selenium",
8
+ "codegen",
9
+ "recorder",
10
+ "test-generator",
11
+ "e2e",
12
+ "playwright-go",
13
+ "junit",
14
+ "mocha"
15
+ ],
16
+ "author": "Xylentis (https://portal.xylentis.com)",
17
+ "license": "AGPL-3.0-only",
18
+ "type": "module",
19
+ "bin": {
20
+ "testgen": "dist/cli.js"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "!dist/**/*.map",
25
+ "runtime",
26
+ "ui",
27
+ "docs/testgen-ui.png",
28
+ "NOTICE"
29
+ ],
30
+ "engines": {
31
+ "node": ">=22"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc -p tsconfig.json && node -e \"require('node:fs').copyFileSync('src/vendor/playwright/LICENSE', 'dist/vendor/playwright/LICENSE')\"",
38
+ "prepare": "npm run build",
39
+ "typecheck": "tsc -p tsconfig.json --noEmit",
40
+ "test": "vitest run",
41
+ "e2e": "bash e2e/run.sh"
42
+ },
43
+ "dependencies": {
44
+ "playwright-core": "1.63.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^24.19.1",
48
+ "typescript": "^7.0.2",
49
+ "vitest": "^5.0.3"
50
+ }
51
+ }