@camunda/e2e-test-suite 0.0.162 → 0.0.164
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/gremlin/bin/gremlin +3 -0
- package/dist/gremlin/dist/cli.js +30 -0
- package/dist/gremlin/dist/format/fixWithEslint.js +43 -0
- package/dist/gremlin/dist/format/formatWithPrettier.js +43 -0
- package/dist/gremlin/dist/generate/generateNegativeTestsFile.js +98 -0
- package/dist/gremlin/dist/generate/renderGeneratedSpecFile.js +94 -0
- package/dist/gremlin/dist/strategies/invalidInput/collectInvalidInputTestCases.js +243 -0
- package/dist/gremlin/dist/strategies/invalidInput/extractValidInputSchemaForField.js +132 -0
- package/dist/gremlin/dist/strategies/invalidInput/renderInvalidInputTest.js +92 -0
- package/dist/gremlin/dist/strategies/invalidInput/types.js +2 -0
- package/dist/pages/SM-8.9/FormJsPage.js +3 -1
- package/dist/pages/SM-8.9/OperateProcessInstancePage.d.ts +1 -0
- package/dist/pages/SM-8.9/OperateProcessInstancePage.js +3 -0
- package/dist/pages/SM-8.9/OperateProcessesPage.d.ts +4 -0
- package/dist/pages/SM-8.9/OperateProcessesPage.js +20 -0
- package/dist/pages/SM-8.9/TaskDetailsPage.d.ts +1 -0
- package/dist/pages/SM-8.9/TaskDetailsPage.js +19 -0
- package/dist/tests/8.8/agentic-ai-user-flows.spec.js +6 -6
- package/dist/tests/8.9/agentic-ai-user-flows.spec.js +8 -8
- package/dist/tests/8.9/cluster-variables.spec.js +4 -4
- package/dist/tests/SM-8.9/cluster-variables.spec.d.ts +1 -0
- package/dist/tests/SM-8.9/cluster-variables.spec.js +60 -0
- package/dist/utils/apiHelpers.d.ts +12 -7
- package/dist/utils/apiHelpers.js +181 -99
- package/package.json +8 -2
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const commander_1 = require("commander");
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const generateNegativeTestsFile_1 = require("./generate/generateNegativeTestsFile");
|
|
9
|
+
const program = new commander_1.Command();
|
|
10
|
+
program
|
|
11
|
+
.name('gremlin')
|
|
12
|
+
.description('Generate additional (negative) Playwright tests using pluggable strategies')
|
|
13
|
+
.argument('<testFile>', 'Path to an input Playwright .spec.ts file')
|
|
14
|
+
.option('-o, --output <dir>', 'Output directory (relative to input test file directory when relative)')
|
|
15
|
+
.option('--project <tsconfig>', 'Path to tsconfig.json to use for type-aware analysis (defaults to nearest tsconfig.json above the input file)')
|
|
16
|
+
.action(async (testFile, options) => {
|
|
17
|
+
const inputPath = node_path_1.default.resolve(process.cwd(), testFile);
|
|
18
|
+
const result = await (0, generateNegativeTestsFile_1.generateNegativeTestsFile)({
|
|
19
|
+
inputTestFilePath: inputPath,
|
|
20
|
+
outputDir: options.output,
|
|
21
|
+
tsConfigPath: options.project,
|
|
22
|
+
});
|
|
23
|
+
// eslint-disable-next-line no-console
|
|
24
|
+
console.log(result.outputFilePath);
|
|
25
|
+
});
|
|
26
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
27
|
+
// eslint-disable-next-line no-console
|
|
28
|
+
console.error(error);
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.fixWithEslint = void 0;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
10
|
+
async function fixWithEslint(args) {
|
|
11
|
+
const cwd = findNearestPackageJsonDir(args.startSearchFrom);
|
|
12
|
+
if (!cwd) {
|
|
13
|
+
throw new Error(`Unable to find a package.json to run eslint from (started at: ${args.startSearchFrom})`);
|
|
14
|
+
}
|
|
15
|
+
const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
16
|
+
await new Promise((resolve, reject) => {
|
|
17
|
+
(0, node_child_process_1.execFile)(npxCmd, ['eslint', '--fix', args.filePath], { cwd, env: process.env }, (error, stdout, stderr) => {
|
|
18
|
+
if (error) {
|
|
19
|
+
reject(new Error(`eslint --fix failed for ${args.filePath}\n\n${stdout}\n${stderr}`));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
resolve();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
exports.fixWithEslint = fixWithEslint;
|
|
27
|
+
function findNearestPackageJsonDir(start) {
|
|
28
|
+
let current = node_path_1.default.resolve(start);
|
|
29
|
+
// If start is a file, search from its directory.
|
|
30
|
+
if (node_path_1.default.extname(current)) {
|
|
31
|
+
current = node_path_1.default.dirname(current);
|
|
32
|
+
}
|
|
33
|
+
// eslint-disable-next-line no-constant-condition
|
|
34
|
+
while (true) {
|
|
35
|
+
const candidate = node_path_1.default.join(current, 'package.json');
|
|
36
|
+
if ((0, node_fs_1.existsSync)(candidate))
|
|
37
|
+
return current;
|
|
38
|
+
const parent = node_path_1.default.dirname(current);
|
|
39
|
+
if (parent === current)
|
|
40
|
+
return null;
|
|
41
|
+
current = parent;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.formatWithPrettier = void 0;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
10
|
+
async function formatWithPrettier(args) {
|
|
11
|
+
const cwd = findNearestPackageJsonDir(args.startSearchFrom);
|
|
12
|
+
if (!cwd) {
|
|
13
|
+
throw new Error(`Unable to find a package.json to run prettier from (started at: ${args.startSearchFrom})`);
|
|
14
|
+
}
|
|
15
|
+
const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
16
|
+
await new Promise((resolve, reject) => {
|
|
17
|
+
(0, node_child_process_1.execFile)(npxCmd, ['prettier', '--write', args.filePath], { cwd, env: process.env }, (error, stdout, stderr) => {
|
|
18
|
+
if (error) {
|
|
19
|
+
reject(new Error(`prettier failed for ${args.filePath}\n\n${stdout}\n${stderr}`));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
resolve();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
exports.formatWithPrettier = formatWithPrettier;
|
|
27
|
+
function findNearestPackageJsonDir(start) {
|
|
28
|
+
let current = node_path_1.default.resolve(start);
|
|
29
|
+
// If start is a file, search from its directory.
|
|
30
|
+
if (node_path_1.default.extname(current)) {
|
|
31
|
+
current = node_path_1.default.dirname(current);
|
|
32
|
+
}
|
|
33
|
+
// eslint-disable-next-line no-constant-condition
|
|
34
|
+
while (true) {
|
|
35
|
+
const candidate = node_path_1.default.join(current, 'package.json');
|
|
36
|
+
if ((0, node_fs_1.existsSync)(candidate))
|
|
37
|
+
return current;
|
|
38
|
+
const parent = node_path_1.default.dirname(current);
|
|
39
|
+
if (parent === current)
|
|
40
|
+
return null;
|
|
41
|
+
current = parent;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateNegativeTestsFile = void 0;
|
|
7
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const node_fs_1 = require("node:fs");
|
|
10
|
+
const ts_morph_1 = require("ts-morph");
|
|
11
|
+
const fixWithEslint_1 = require("../format/fixWithEslint");
|
|
12
|
+
const collectInvalidInputTestCases_1 = require("../strategies/invalidInput/collectInvalidInputTestCases");
|
|
13
|
+
const formatWithPrettier_1 = require("../format/formatWithPrettier");
|
|
14
|
+
const renderGeneratedSpecFile_1 = require("./renderGeneratedSpecFile");
|
|
15
|
+
const INVALID_INPUT_STRATEGY_SLUG = 'invalid-input';
|
|
16
|
+
async function generateNegativeTestsFile(args) {
|
|
17
|
+
const inputDir = node_path_1.default.dirname(args.inputTestFilePath);
|
|
18
|
+
const resolvedOutputDir = args.outputDir
|
|
19
|
+
? node_path_1.default.isAbsolute(args.outputDir)
|
|
20
|
+
? args.outputDir
|
|
21
|
+
: node_path_1.default.resolve(inputDir, args.outputDir)
|
|
22
|
+
: inputDir;
|
|
23
|
+
await promises_1.default.mkdir(resolvedOutputDir, { recursive: true });
|
|
24
|
+
const baseName = deriveBaseName(args.inputTestFilePath);
|
|
25
|
+
const outputFilePath = await resolveIncrementingOutputPath(resolvedOutputDir, `${baseName}-${INVALID_INPUT_STRATEGY_SLUG}-generated.spec.ts`);
|
|
26
|
+
const tsConfigPath = args.tsConfigPath ?? (await findNearestTsConfig(args.inputTestFilePath));
|
|
27
|
+
if (!tsConfigPath) {
|
|
28
|
+
throw new Error(`Unable to locate tsconfig.json (use --project). Started from: ${args.inputTestFilePath}`);
|
|
29
|
+
}
|
|
30
|
+
const project = new ts_morph_1.Project({ tsConfigFilePath: tsConfigPath });
|
|
31
|
+
const sourceFile = project.addSourceFileAtPathIfExists(args.inputTestFilePath);
|
|
32
|
+
if (!sourceFile) {
|
|
33
|
+
throw new Error(`Input test file not found: ${args.inputTestFilePath}`);
|
|
34
|
+
}
|
|
35
|
+
const testCases = await (0, collectInvalidInputTestCases_1.collectInvalidInputTestCases)({ project, sourceFile });
|
|
36
|
+
const rendered = (0, renderGeneratedSpecFile_1.renderGeneratedSpecFile)({
|
|
37
|
+
sourceFile,
|
|
38
|
+
testCases,
|
|
39
|
+
strategySlug: INVALID_INPUT_STRATEGY_SLUG,
|
|
40
|
+
});
|
|
41
|
+
await promises_1.default.writeFile(outputFilePath, rendered, 'utf8');
|
|
42
|
+
// Apply autofixes first (eslint), then normalize formatting (prettier).
|
|
43
|
+
await (0, fixWithEslint_1.fixWithEslint)({
|
|
44
|
+
filePath: outputFilePath,
|
|
45
|
+
startSearchFrom: args.inputTestFilePath,
|
|
46
|
+
});
|
|
47
|
+
await (0, formatWithPrettier_1.formatWithPrettier)({
|
|
48
|
+
filePath: outputFilePath,
|
|
49
|
+
startSearchFrom: args.inputTestFilePath,
|
|
50
|
+
});
|
|
51
|
+
return { outputFilePath };
|
|
52
|
+
}
|
|
53
|
+
exports.generateNegativeTestsFile = generateNegativeTestsFile;
|
|
54
|
+
function deriveBaseName(inputPath) {
|
|
55
|
+
const fileName = node_path_1.default.basename(inputPath);
|
|
56
|
+
if (fileName.endsWith('.spec.ts')) {
|
|
57
|
+
return fileName.slice(0, -'.spec.ts'.length);
|
|
58
|
+
}
|
|
59
|
+
const parsed = node_path_1.default.parse(fileName);
|
|
60
|
+
return parsed.name;
|
|
61
|
+
}
|
|
62
|
+
async function resolveIncrementingOutputPath(dir, fileName) {
|
|
63
|
+
const initial = node_path_1.default.join(dir, fileName);
|
|
64
|
+
if (!(0, node_fs_1.existsSync)(initial))
|
|
65
|
+
return initial;
|
|
66
|
+
const specSuffix = '.spec.ts';
|
|
67
|
+
if (fileName.endsWith(specSuffix)) {
|
|
68
|
+
const stem = fileName.slice(0, -specSuffix.length);
|
|
69
|
+
for (let i = 1; i < 10000; i++) {
|
|
70
|
+
const candidate = node_path_1.default.join(dir, `${stem}-${i}${specSuffix}`);
|
|
71
|
+
if (!(0, node_fs_1.existsSync)(candidate))
|
|
72
|
+
return candidate;
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`Unable to find an available output filename in: ${dir}`);
|
|
75
|
+
}
|
|
76
|
+
const ext = node_path_1.default.extname(fileName);
|
|
77
|
+
const withoutExt = fileName.slice(0, -ext.length);
|
|
78
|
+
for (let i = 1; i < 10000; i++) {
|
|
79
|
+
const candidate = node_path_1.default.join(dir, `${withoutExt}-${i}${ext}`);
|
|
80
|
+
if (!(0, node_fs_1.existsSync)(candidate))
|
|
81
|
+
return candidate;
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`Unable to find an available output filename in: ${dir}`);
|
|
84
|
+
}
|
|
85
|
+
async function findNearestTsConfig(startFile) {
|
|
86
|
+
let current = node_path_1.default.dirname(startFile);
|
|
87
|
+
// Walk upward until filesystem root.
|
|
88
|
+
// eslint-disable-next-line no-constant-condition
|
|
89
|
+
while (true) {
|
|
90
|
+
const candidate = node_path_1.default.join(current, 'tsconfig.json');
|
|
91
|
+
if ((0, node_fs_1.existsSync)(candidate))
|
|
92
|
+
return candidate;
|
|
93
|
+
const parent = node_path_1.default.dirname(current);
|
|
94
|
+
if (parent === current)
|
|
95
|
+
return null;
|
|
96
|
+
current = parent;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderGeneratedSpecFile = void 0;
|
|
4
|
+
const ts_morph_1 = require("ts-morph");
|
|
5
|
+
function renderGeneratedSpecFile(args) {
|
|
6
|
+
const preamble = renderPreamble(args.sourceFile);
|
|
7
|
+
if (args.testCases.length === 0) {
|
|
8
|
+
// Still emit a file to make behavior predictable.
|
|
9
|
+
return `${preamble}\n\n// gremlin: no ${args.strategySlug} test cases generated\n`;
|
|
10
|
+
}
|
|
11
|
+
const describeTitle = args.testCases[0]?.originDescribeTitle ??
|
|
12
|
+
'Generated - (negative) generated';
|
|
13
|
+
const describeBlock = renderDescribeBlock({
|
|
14
|
+
describeTitle,
|
|
15
|
+
strategySlug: args.strategySlug,
|
|
16
|
+
beforeEachText: args.testCases[0]?.beforeEachText,
|
|
17
|
+
afterEachText: args.testCases[0]?.afterEachText,
|
|
18
|
+
tests: args.testCases.map((t) => t.renderedTestText),
|
|
19
|
+
});
|
|
20
|
+
return `${preamble}\n\n${describeBlock}\n`;
|
|
21
|
+
}
|
|
22
|
+
exports.renderGeneratedSpecFile = renderGeneratedSpecFile;
|
|
23
|
+
function renderPreamble(sourceFile) {
|
|
24
|
+
const keptStatements = sourceFile.getStatements().filter((stmt) => {
|
|
25
|
+
if (ts_morph_1.Node.isImportDeclaration(stmt))
|
|
26
|
+
return true;
|
|
27
|
+
// Drop the original test.describe(...) block(s)
|
|
28
|
+
if (ts_morph_1.Node.isExpressionStatement(stmt)) {
|
|
29
|
+
const expr = stmt.getExpression();
|
|
30
|
+
const call = expr.asKind(ts_morph_1.SyntaxKind.CallExpression);
|
|
31
|
+
if (!call)
|
|
32
|
+
return true;
|
|
33
|
+
const callee = call.getExpression();
|
|
34
|
+
// Drop top-level test('...', ...) (rare, but possible)
|
|
35
|
+
if (ts_morph_1.Node.isIdentifier(callee) && callee.getText() === 'test') {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
// Drop top-level test.describe('...', ...)
|
|
39
|
+
if (ts_morph_1.Node.isPropertyAccessExpression(callee)) {
|
|
40
|
+
const isTestDescribe = callee.getName() === 'describe' &&
|
|
41
|
+
ts_morph_1.Node.isIdentifier(callee.getExpression()) &&
|
|
42
|
+
callee.getExpression().getText() === 'test';
|
|
43
|
+
if (isTestDescribe)
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
// Keep other top-level declarations (constants, helper funcs, etc.)
|
|
49
|
+
return true;
|
|
50
|
+
});
|
|
51
|
+
return keptStatements
|
|
52
|
+
.map((s) => s.getText())
|
|
53
|
+
.join('\n\n')
|
|
54
|
+
.trim();
|
|
55
|
+
}
|
|
56
|
+
function renderDescribeBlock(args) {
|
|
57
|
+
const lines = [];
|
|
58
|
+
lines.push(`test.describe('${escapeSingleQuotes(args.describeTitle)} - ${escapeSingleQuotes(args.strategySlug)} - (negative) generated', () => {`);
|
|
59
|
+
if (args.beforeEachText) {
|
|
60
|
+
lines.push(indentBlock(dedent(args.beforeEachText), 2));
|
|
61
|
+
}
|
|
62
|
+
if (args.afterEachText) {
|
|
63
|
+
lines.push(indentBlock(dedent(args.afterEachText), 2));
|
|
64
|
+
}
|
|
65
|
+
for (const testText of args.tests) {
|
|
66
|
+
lines.push(indentBlock(testText, 2));
|
|
67
|
+
}
|
|
68
|
+
lines.push('});');
|
|
69
|
+
return lines.join('\n\n');
|
|
70
|
+
}
|
|
71
|
+
function indentBlock(text, spaces) {
|
|
72
|
+
const prefix = ' '.repeat(spaces);
|
|
73
|
+
return text
|
|
74
|
+
.split('\n')
|
|
75
|
+
.map((line) => (line.trim().length === 0 ? line : prefix + line))
|
|
76
|
+
.join('\n');
|
|
77
|
+
}
|
|
78
|
+
function dedent(text) {
|
|
79
|
+
const lines = text.replace(/\r\n/g, '\n').split('\n');
|
|
80
|
+
const nonEmpty = lines.filter((l) => l.trim().length > 0);
|
|
81
|
+
if (nonEmpty.length === 0)
|
|
82
|
+
return text.trim();
|
|
83
|
+
const minIndent = Math.min(...nonEmpty.map((l) => {
|
|
84
|
+
const match = l.match(/^\s*/);
|
|
85
|
+
return match ? match[0].length : 0;
|
|
86
|
+
}));
|
|
87
|
+
return lines
|
|
88
|
+
.map((l) => l.slice(minIndent))
|
|
89
|
+
.join('\n')
|
|
90
|
+
.trim();
|
|
91
|
+
}
|
|
92
|
+
function escapeSingleQuotes(value) {
|
|
93
|
+
return value.replaceAll("'", "\\'");
|
|
94
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.collectInvalidInputTestCases = void 0;
|
|
4
|
+
const ts_morph_1 = require("ts-morph");
|
|
5
|
+
const extractValidInputSchemaForField_1 = require("./extractValidInputSchemaForField");
|
|
6
|
+
const renderInvalidInputTest_1 = require("./renderInvalidInputTest");
|
|
7
|
+
async function collectInvalidInputTestCases(args) {
|
|
8
|
+
const describeInfo = findPrimaryDescribeInfo(args.sourceFile);
|
|
9
|
+
const testCalls = findTestCalls(args.sourceFile);
|
|
10
|
+
const generated = [];
|
|
11
|
+
for (const testCall of testCalls) {
|
|
12
|
+
const originTitle = getStringLiteralArg(testCall, 0) ?? 'Unnamed test';
|
|
13
|
+
const callback = testCall.getArguments()[1];
|
|
14
|
+
if (!callback || !ts_morph_1.Node.isArrowFunction(callback))
|
|
15
|
+
continue;
|
|
16
|
+
const fillCalls = callback
|
|
17
|
+
.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)
|
|
18
|
+
.filter((call) => isFillMethodCall(call));
|
|
19
|
+
for (const fillCall of fillCalls) {
|
|
20
|
+
const propAccess = fillCall.getExpressionIfKindOrThrow(ts_morph_1.SyntaxKind.PropertyAccessExpression);
|
|
21
|
+
const pageObjectExpr = propAccess.getExpression();
|
|
22
|
+
const pageObjectName = pageObjectExpr.getText();
|
|
23
|
+
const fillMethodName = propAccess.getName();
|
|
24
|
+
const inputFieldName = inferInputFieldNameFromFillMethod(fillMethodName);
|
|
25
|
+
if (!inputFieldName)
|
|
26
|
+
continue;
|
|
27
|
+
const schema = (0, extractValidInputSchemaForField_1.extractValidInputSchemaForField)({
|
|
28
|
+
project: args.project,
|
|
29
|
+
typeAtNode: pageObjectExpr,
|
|
30
|
+
fieldName: inputFieldName,
|
|
31
|
+
});
|
|
32
|
+
if (!schema)
|
|
33
|
+
continue;
|
|
34
|
+
const invalidMin = makeInvalidString(schema._schema.minlength - 1);
|
|
35
|
+
const invalidMax = makeInvalidString(schema._schema.maxlength + 1);
|
|
36
|
+
const prefixStatements = computePrefixStatements({
|
|
37
|
+
testCall,
|
|
38
|
+
callback,
|
|
39
|
+
targetFillCall: fillCall,
|
|
40
|
+
replacementArgLiteral: toSingleQuotedStringLiteral(invalidMin),
|
|
41
|
+
});
|
|
42
|
+
const minParamsText = buildMinimalTestParams({
|
|
43
|
+
callback,
|
|
44
|
+
pageObjectName,
|
|
45
|
+
referencedText: prefixStatements,
|
|
46
|
+
});
|
|
47
|
+
const minTest = (0, renderInvalidInputTest_1.renderInvalidInputTest)({
|
|
48
|
+
testTitle: `${originTitle} - invalid ${inputFieldName} minlength`,
|
|
49
|
+
testParams: minParamsText,
|
|
50
|
+
prefixStatements,
|
|
51
|
+
pageObjectName,
|
|
52
|
+
inputFieldName,
|
|
53
|
+
violationEffect: schema._schema.violationEffect,
|
|
54
|
+
schema,
|
|
55
|
+
kind: 'minlength',
|
|
56
|
+
invalidValue: invalidMin,
|
|
57
|
+
originalCallback: callback,
|
|
58
|
+
targetFillCall: fillCall,
|
|
59
|
+
});
|
|
60
|
+
const prefixStatementsMax = computePrefixStatements({
|
|
61
|
+
testCall,
|
|
62
|
+
callback,
|
|
63
|
+
targetFillCall: fillCall,
|
|
64
|
+
replacementArgLiteral: toSingleQuotedStringLiteral(invalidMax),
|
|
65
|
+
});
|
|
66
|
+
const maxParamsText = buildMinimalTestParams({
|
|
67
|
+
callback,
|
|
68
|
+
pageObjectName,
|
|
69
|
+
referencedText: prefixStatementsMax,
|
|
70
|
+
});
|
|
71
|
+
const maxTest = (0, renderInvalidInputTest_1.renderInvalidInputTest)({
|
|
72
|
+
testTitle: `${originTitle} - invalid ${inputFieldName} maxlength`,
|
|
73
|
+
testParams: maxParamsText,
|
|
74
|
+
prefixStatements: prefixStatementsMax,
|
|
75
|
+
pageObjectName,
|
|
76
|
+
inputFieldName,
|
|
77
|
+
violationEffect: schema._schema.violationEffect,
|
|
78
|
+
schema,
|
|
79
|
+
kind: 'maxlength',
|
|
80
|
+
invalidValue: invalidMax,
|
|
81
|
+
originalCallback: callback,
|
|
82
|
+
targetFillCall: fillCall,
|
|
83
|
+
});
|
|
84
|
+
generated.push({
|
|
85
|
+
originTestTitle: originTitle,
|
|
86
|
+
originDescribeTitle: describeInfo?.title,
|
|
87
|
+
beforeEachText: describeInfo?.beforeEachText,
|
|
88
|
+
afterEachText: describeInfo?.afterEachText,
|
|
89
|
+
renderedTestText: minTest,
|
|
90
|
+
});
|
|
91
|
+
generated.push({
|
|
92
|
+
originTestTitle: originTitle,
|
|
93
|
+
originDescribeTitle: describeInfo?.title,
|
|
94
|
+
beforeEachText: describeInfo?.beforeEachText,
|
|
95
|
+
afterEachText: describeInfo?.afterEachText,
|
|
96
|
+
renderedTestText: maxTest,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return generated;
|
|
101
|
+
}
|
|
102
|
+
exports.collectInvalidInputTestCases = collectInvalidInputTestCases;
|
|
103
|
+
function findTestCalls(sourceFile) {
|
|
104
|
+
return sourceFile
|
|
105
|
+
.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)
|
|
106
|
+
.filter((call) => {
|
|
107
|
+
const expr = call.getExpression();
|
|
108
|
+
return ts_morph_1.Node.isIdentifier(expr) && expr.getText() === 'test';
|
|
109
|
+
})
|
|
110
|
+
.filter((call) => call.getArguments().length >= 2);
|
|
111
|
+
}
|
|
112
|
+
function isFillMethodCall(call) {
|
|
113
|
+
const expr = call.getExpression();
|
|
114
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
115
|
+
return false;
|
|
116
|
+
const name = expr.getName();
|
|
117
|
+
return name.startsWith('fill') && name.length > 'fill'.length;
|
|
118
|
+
}
|
|
119
|
+
function inferInputFieldNameFromFillMethod(fillMethodName) {
|
|
120
|
+
if (!fillMethodName.startsWith('fill'))
|
|
121
|
+
return null;
|
|
122
|
+
const suffix = fillMethodName.slice('fill'.length);
|
|
123
|
+
if (suffix.length === 0)
|
|
124
|
+
return null;
|
|
125
|
+
const camel = suffix[0].toLowerCase() + suffix.slice(1);
|
|
126
|
+
return `${camel}Input`;
|
|
127
|
+
}
|
|
128
|
+
function getStringLiteralArg(call, index) {
|
|
129
|
+
const arg = call.getArguments()[index];
|
|
130
|
+
if (!arg)
|
|
131
|
+
return null;
|
|
132
|
+
const literal = arg.asKind(ts_morph_1.SyntaxKind.StringLiteral);
|
|
133
|
+
return literal ? literal.getLiteralText() : null;
|
|
134
|
+
}
|
|
135
|
+
function findPrimaryDescribeInfo(sourceFile) {
|
|
136
|
+
const describeCalls = sourceFile
|
|
137
|
+
.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)
|
|
138
|
+
.filter((call) => {
|
|
139
|
+
const expr = call.getExpression();
|
|
140
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
141
|
+
return false;
|
|
142
|
+
return (expr.getExpression().getText() === 'test' &&
|
|
143
|
+
expr.getName() === 'describe');
|
|
144
|
+
});
|
|
145
|
+
const primary = describeCalls[0];
|
|
146
|
+
if (!primary)
|
|
147
|
+
return null;
|
|
148
|
+
const title = getStringLiteralArg(primary, 0) ?? 'Generated';
|
|
149
|
+
const callback = primary.getArguments()[1];
|
|
150
|
+
if (!callback || !ts_morph_1.Node.isArrowFunction(callback)) {
|
|
151
|
+
return { title };
|
|
152
|
+
}
|
|
153
|
+
const block = callback.getBody().asKind(ts_morph_1.SyntaxKind.Block);
|
|
154
|
+
if (!block)
|
|
155
|
+
return { title };
|
|
156
|
+
const statements = block.getStatements();
|
|
157
|
+
const beforeEach = statements.find((s) => s.getText().trimStart().startsWith('test.beforeEach'));
|
|
158
|
+
const afterEach = statements.find((s) => s.getText().trimStart().startsWith('test.afterEach'));
|
|
159
|
+
return {
|
|
160
|
+
title,
|
|
161
|
+
beforeEachText: beforeEach?.getText(),
|
|
162
|
+
afterEachText: afterEach?.getText(),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function computePrefixStatements(args) {
|
|
166
|
+
if (!ts_morph_1.Node.isArrowFunction(args.callback))
|
|
167
|
+
return '';
|
|
168
|
+
const body = args.callback.getBody();
|
|
169
|
+
const block = body.asKind(ts_morph_1.SyntaxKind.Block);
|
|
170
|
+
if (!block)
|
|
171
|
+
return '';
|
|
172
|
+
const statements = block.getStatements();
|
|
173
|
+
const targetStatement = statements.find((stmt) => stmt
|
|
174
|
+
.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)
|
|
175
|
+
.some((c) => c === args.targetFillCall));
|
|
176
|
+
if (!targetStatement)
|
|
177
|
+
return '';
|
|
178
|
+
const idx = statements.indexOf(targetStatement);
|
|
179
|
+
const prefix = statements.slice(0, idx + 1);
|
|
180
|
+
const rendered = prefix.map((stmt) => {
|
|
181
|
+
if (stmt !== targetStatement)
|
|
182
|
+
return stmt.getText();
|
|
183
|
+
// Replace only the argument list for the targeted fill call.
|
|
184
|
+
return replaceCallArgInStatement({
|
|
185
|
+
statementText: stmt.getText(),
|
|
186
|
+
call: args.targetFillCall,
|
|
187
|
+
newArg: args.replacementArgLiteral,
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
return rendered.join('\n');
|
|
191
|
+
}
|
|
192
|
+
function replaceCallArgInStatement(args) {
|
|
193
|
+
const expr = args.call.getExpression();
|
|
194
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
195
|
+
return args.statementText;
|
|
196
|
+
const methodName = expr.getName();
|
|
197
|
+
// Naive but stable enough for our conventions.
|
|
198
|
+
const pattern = new RegExp(`${escapeRegExp(methodName)}\\s*\\(([^)]*)\\)`, 'm');
|
|
199
|
+
return args.statementText.replace(pattern, `${methodName}(${args.newArg})`);
|
|
200
|
+
}
|
|
201
|
+
function escapeRegExp(value) {
|
|
202
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
203
|
+
}
|
|
204
|
+
function buildMinimalTestParams(args) {
|
|
205
|
+
const params = args.callback.getParameters();
|
|
206
|
+
if (params.length === 0)
|
|
207
|
+
return '{}';
|
|
208
|
+
const first = params[0];
|
|
209
|
+
const nameNode = first.getNameNode();
|
|
210
|
+
// Common convention: async ({loginPage, homePage}, testInfo) => ...
|
|
211
|
+
if (nameNode && ts_morph_1.Node.isObjectBindingPattern(nameNode)) {
|
|
212
|
+
const available = nameNode.getElements().map((el) => el.getName());
|
|
213
|
+
const required = new Set([args.pageObjectName]);
|
|
214
|
+
for (const candidate of available) {
|
|
215
|
+
if (candidate &&
|
|
216
|
+
isIdentifierReferenced({
|
|
217
|
+
text: args.referencedText,
|
|
218
|
+
identifier: candidate,
|
|
219
|
+
})) {
|
|
220
|
+
required.add(candidate);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const firstParamText = `{${Array.from(required).sort().join(', ')}}`;
|
|
224
|
+
const rest = params.slice(1).map((p) => p.getText());
|
|
225
|
+
return [firstParamText, ...rest].join(', ');
|
|
226
|
+
}
|
|
227
|
+
// Fallback: keep original parameters.
|
|
228
|
+
return params.map((p) => p.getText()).join(', ');
|
|
229
|
+
}
|
|
230
|
+
function isIdentifierReferenced(args) {
|
|
231
|
+
const escaped = escapeRegExp(args.identifier);
|
|
232
|
+
const re = new RegExp(`\\b${escaped}\\b`, 'm');
|
|
233
|
+
return re.test(args.text);
|
|
234
|
+
}
|
|
235
|
+
function makeInvalidString(length) {
|
|
236
|
+
if (length <= 0)
|
|
237
|
+
return '';
|
|
238
|
+
return 'a'.repeat(length);
|
|
239
|
+
}
|
|
240
|
+
function toSingleQuotedStringLiteral(value) {
|
|
241
|
+
// Keep generated output consistent with the repo's single-quote style.
|
|
242
|
+
return `'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`;
|
|
243
|
+
}
|