@autotests/playwright-impact 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -0
- package/package.json +33 -0
- package/src/analyze-impacted-specs.js +294 -0
- package/src/format-analyze-result.js +29 -0
- package/src/index.d.ts +74 -0
- package/src/index.js +10 -0
- package/src/modules/class-impact-helpers.js +86 -0
- package/src/modules/file-and-git-helpers.js +234 -0
- package/src/modules/fixture-map-helpers.js +167 -0
- package/src/modules/global-watch-helpers.js +278 -0
- package/src/modules/import-impact-helpers.js +236 -0
- package/src/modules/method-filter-helpers.js +338 -0
- package/src/modules/method-impact-helpers.js +757 -0
- package/src/modules/shell.js +24 -0
- package/src/modules/spec-selection-helpers.js +73 -0
- package/tests/_test-helpers.js +45 -0
- package/tests/analyze-impacted-specs.integration.test.js +477 -0
- package/tests/analyze-impacted-specs.test.js +36 -0
- package/tests/class-impact-helpers.test.js +101 -0
- package/tests/file-and-git-helpers.test.js +140 -0
- package/tests/file-status-compat.test.js +55 -0
- package/tests/fixture-map-helpers.test.js +118 -0
- package/tests/format-analyze-result.test.js +26 -0
- package/tests/global-watch-helpers.test.js +92 -0
- package/tests/method-filter-helpers.test.js +316 -0
- package/tests/method-impact-helpers.test.js +195 -0
- package/tests/semantic-coverage-matrix.test.js +381 -0
- package/tests/spec-selection-helpers.test.js +115 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const { collectChangedMethodsByClass, buildImpactedMethodsByClass } = require('../src/modules/method-impact-helpers');
|
|
6
|
+
const { createTempDir, writeFile } = require('./_test-helpers');
|
|
7
|
+
|
|
8
|
+
const changedEntry = { status: 'M', effectivePath: 'src/pages/A.ts', oldPath: 'src/pages/A.ts', newPath: 'src/pages/A.ts' };
|
|
9
|
+
|
|
10
|
+
const collect = (baseContent, headContent) =>
|
|
11
|
+
collectChangedMethodsByClass({
|
|
12
|
+
changedPomEntries: [changedEntry],
|
|
13
|
+
baseRef: 'HEAD',
|
|
14
|
+
readChangeContents: () => ({
|
|
15
|
+
basePath: 'src/pages/A.ts',
|
|
16
|
+
headPath: 'src/pages/A.ts',
|
|
17
|
+
baseContent,
|
|
18
|
+
headContent,
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const hasMethod = (result, className, methodName) => Boolean(result.changedMethodsByClass.get(className)?.has(methodName));
|
|
23
|
+
|
|
24
|
+
test('Type-only changes do not trigger runtime top-level impact', () => {
|
|
25
|
+
const result = collect(
|
|
26
|
+
'import type { X } from "./x"; export class A { run(){ return 1; } }',
|
|
27
|
+
'import type { Y } from "./x"; export class A { run(){ return 1; } }'
|
|
28
|
+
);
|
|
29
|
+
assert.equal(result.stats.topLevelRuntimeChangedFiles, 0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('Interface-only change yields no impacted methods', () => {
|
|
33
|
+
const result = collect(
|
|
34
|
+
'interface A1 { a: string }; export class A { run(){ return 1; } }',
|
|
35
|
+
'interface A1 { a: number }; export class A { run(){ return 1; } }'
|
|
36
|
+
);
|
|
37
|
+
assert.equal(result.stats.semanticChangedMethodsCount, 0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('TypeAlias-only change yields no impacted methods', () => {
|
|
41
|
+
const result = collect(
|
|
42
|
+
'type A1 = { a: string }; export class A { run(){ return 1; } }',
|
|
43
|
+
'type A1 = { a: number }; export class A { run(){ return 1; } }'
|
|
44
|
+
);
|
|
45
|
+
assert.equal(result.stats.semanticChangedMethodsCount, 0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('Type-only import change yields no runtime top-level impact', () => {
|
|
49
|
+
const result = collect(
|
|
50
|
+
'import type { X } from "./x"; export class A { run(){ return 1; } }',
|
|
51
|
+
'import type { Z } from "./z"; export class A { run(){ return 1; } }'
|
|
52
|
+
);
|
|
53
|
+
assert.equal(result.stats.topLevelRuntimeChangedFiles, 0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('Runtime import change triggers top-level runtime impact', () => {
|
|
57
|
+
const result = collect(
|
|
58
|
+
'import { x } from "./x"; export class A { run(){ return 1; } }',
|
|
59
|
+
'import { y } from "./x"; export class A { run(){ return 1; } }'
|
|
60
|
+
);
|
|
61
|
+
assert.equal(result.stats.topLevelRuntimeChangedFiles, 1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('Export statement change triggers top-level runtime impact', () => {
|
|
65
|
+
const result = collect(
|
|
66
|
+
'export const A = 1; export class Page { run(){ return 1; } }',
|
|
67
|
+
'export const A = 2; export class Page { run(){ return 1; } }'
|
|
68
|
+
);
|
|
69
|
+
assert.equal(result.stats.topLevelRuntimeChangedFiles, 1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('Top-level function body change triggers file-wide seed impact', () => {
|
|
73
|
+
const result = collect(
|
|
74
|
+
'function f(){ return 1; } export class Page { run(){return 1;} other(){return 2;} }',
|
|
75
|
+
'function f(){ return 2; } export class Page { run(){return 1;} other(){return 2;} }'
|
|
76
|
+
);
|
|
77
|
+
assert.equal(result.stats.topLevelRuntimeChangedFiles, 1);
|
|
78
|
+
assert.equal(hasMethod(result, 'Page', 'run'), true);
|
|
79
|
+
assert.equal(hasMethod(result, 'Page', 'other'), true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('VariableStatement const value change triggers file-wide seed impact', () => {
|
|
83
|
+
const result = collect(
|
|
84
|
+
'const SELECTOR = ".one"; export class Page { run(){ return 1; } other(){ return 2; } }',
|
|
85
|
+
'const SELECTOR = ".two"; export class Page { run(){ return 1; } other(){ return 2; } }'
|
|
86
|
+
);
|
|
87
|
+
assert.equal(result.stats.topLevelRuntimeChangedFiles, 1);
|
|
88
|
+
assert.equal(hasMethod(result, 'Page', 'run'), true);
|
|
89
|
+
assert.equal(hasMethod(result, 'Page', 'other'), true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('Object literal selector map change triggers file-wide seed impact', () => {
|
|
93
|
+
const result = collect(
|
|
94
|
+
'const selectors = { open: ".open" }; export class Page { run(){ return selectors.open; } other(){ return 2; } }',
|
|
95
|
+
'const selectors = { open: ".start" }; export class Page { run(){ return selectors.open; } other(){ return 2; } }'
|
|
96
|
+
);
|
|
97
|
+
assert.equal(result.stats.topLevelRuntimeChangedFiles, 1);
|
|
98
|
+
assert.equal(hasMethod(result, 'Page', 'run'), true);
|
|
99
|
+
assert.equal(hasMethod(result, 'Page', 'other'), true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('new Map(...) initializer change triggers file-wide seed impact', () => {
|
|
103
|
+
const result = collect(
|
|
104
|
+
'const m = new Map([["k", 1]]); export class Page { run(){ return m.get("k"); } other(){ return 2; } }',
|
|
105
|
+
'const m = new Map([["k", 2]]); export class Page { run(){ return m.get("k"); } other(){ return 2; } }'
|
|
106
|
+
);
|
|
107
|
+
assert.equal(result.stats.topLevelRuntimeChangedFiles, 1);
|
|
108
|
+
assert.equal(hasMethod(result, 'Page', 'run'), true);
|
|
109
|
+
assert.equal(hasMethod(result, 'Page', 'other'), true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('Top-level function signature change triggers file-wide seed impact', () => {
|
|
113
|
+
const result = collect(
|
|
114
|
+
'function f(a:number){ return a; } export class Page { run(){ return f(1); } other(){ return 2; } }',
|
|
115
|
+
'function f(a:string){ return a; } export class Page { run(){ return f("1"); } other(){ return 2; } }'
|
|
116
|
+
);
|
|
117
|
+
assert.equal(result.stats.topLevelRuntimeChangedFiles, 1);
|
|
118
|
+
assert.equal(hasMethod(result, 'Page', 'run'), true);
|
|
119
|
+
assert.equal(hasMethod(result, 'Page', 'other'), true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('Top-level enum value change triggers file-wide seed impact', () => {
|
|
123
|
+
const result = collect(
|
|
124
|
+
'enum K { Open = "open" } export class Page { run(){ return K.Open; } other(){ return 2; } }',
|
|
125
|
+
'enum K { Open = "start" } export class Page { run(){ return K.Open; } other(){ return 2; } }'
|
|
126
|
+
);
|
|
127
|
+
assert.equal(result.stats.topLevelRuntimeChangedFiles, 1);
|
|
128
|
+
assert.equal(hasMethod(result, 'Page', 'run'), true);
|
|
129
|
+
assert.equal(hasMethod(result, 'Page', 'other'), true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('Top-level runtime whitespace-only change yields no impact', () => {
|
|
133
|
+
const result = collect('const A=1; export class Page { run(){ return 1; } }', 'const A = 1; export class Page { run(){ return 1; } }');
|
|
134
|
+
assert.equal(result.stats.topLevelRuntimeChangedFiles, 0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('Top-level runtime comment-only change yields no impact', () => {
|
|
138
|
+
const result = collect('const A=1; export class Page { run(){ return 1; } }', '// comment\nconst A=1; export class Page { run(){ return 1; } }');
|
|
139
|
+
assert.equal(result.stats.topLevelRuntimeChangedFiles, 0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('Method body comment-only change yields no semantic change', () => {
|
|
143
|
+
const result = collect('export class A { run(){ return 1; } }', 'export class A { run(){ /*c*/ return 1; } }');
|
|
144
|
+
assert.equal(result.stats.semanticChangedMethodsCount, 0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('Method body whitespace-only change yields no semantic change', () => {
|
|
148
|
+
const result = collect('export class A { run(){ return 1 + 2; } }', 'export class A { run(){ return 1+2; } }');
|
|
149
|
+
assert.equal(result.stats.semanticChangedMethodsCount, 0);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('Method literal change triggers semantic change', () => {
|
|
153
|
+
const result = collect('export class A { run(){ return 1; } }', 'export class A { run(){ return 2; } }');
|
|
154
|
+
assert.equal(hasMethod(result, 'A', 'run'), true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('Method operator change triggers semantic change', () => {
|
|
158
|
+
const result = collect('export class A { run(a:number,b:number){ return a+b; } }', 'export class A { run(a:number,b:number){ return a-b; } }');
|
|
159
|
+
assert.equal(hasMethod(result, 'A', 'run'), true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('Method call target change triggers semantic change', () => {
|
|
163
|
+
const result = collect(
|
|
164
|
+
'export class A { a(){return 1;} b(){return 2;} run(){ return this.a(); } }',
|
|
165
|
+
'export class A { a(){return 1;} b(){return 2;} run(){ return this.b(); } }'
|
|
166
|
+
);
|
|
167
|
+
assert.equal(hasMethod(result, 'A', 'run'), true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('Method parameter list change triggers semantic change', () => {
|
|
171
|
+
const result = collect('export class A { run(a:number){ return a; } }', 'export class A { run(a:number,b:number){ return a+b; } }');
|
|
172
|
+
assert.equal(hasMethod(result, 'A', 'run'), true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('Method default parameter change triggers semantic change', () => {
|
|
176
|
+
const result = collect('export class A { run(a:number=1){ return a; } }', 'export class A { run(a:number=2){ return a; } }');
|
|
177
|
+
assert.equal(hasMethod(result, 'A', 'run'), true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('Method rest parameter change triggers semantic change', () => {
|
|
181
|
+
const result = collect('export class A { run(...a:number[]){ return a.length; } }', 'export class A { run(...a:string[]){ return a.length; } }');
|
|
182
|
+
assert.equal(hasMethod(result, 'A', 'run'), true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('Method optional parameter change triggers semantic change', () => {
|
|
186
|
+
const result = collect('export class A { run(a?:number){ return a||0; } }', 'export class A { run(a:number){ return a; } }');
|
|
187
|
+
assert.equal(hasMethod(result, 'A', 'run'), true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('Method default/rest/optional parameter changes trigger semantic change', () => {
|
|
191
|
+
const a = collect('export class A { run(a:number=1){ return a; } }', 'export class A { run(a:number=2){ return a; } }');
|
|
192
|
+
const b = collect('export class A { run(...a:number[]){ return a.length; } }', 'export class A { run(...a:string[]){ return a.length; } }');
|
|
193
|
+
const c = collect('export class A { run(a?:number){ return a||0; } }', 'export class A { run(a:number){ return a; } }');
|
|
194
|
+
assert.equal(hasMethod(a, 'A', 'run'), true);
|
|
195
|
+
assert.equal(hasMethod(b, 'A', 'run'), true);
|
|
196
|
+
assert.equal(hasMethod(c, 'A', 'run'), true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('Method return type and async modifier changes trigger semantic change', () => {
|
|
200
|
+
const a = collect('export class A { run():number{ return 1; } }', 'export class A { run():string{ return "1"; } }');
|
|
201
|
+
const b = collect('export class A { run(){ return 1; } }', 'export class A { async run(){ return 1; } }');
|
|
202
|
+
assert.equal(hasMethod(a, 'A', 'run'), true);
|
|
203
|
+
assert.equal(hasMethod(b, 'A', 'run'), true);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('Accessibility modifier change triggers semantic change', () => {
|
|
207
|
+
const result = collect('export class A { public run(){ return 1; } }', 'export class A { protected run(){ return 1; } }');
|
|
208
|
+
assert.equal(hasMethod(result, 'A', 'run'), true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('Method added/removed/renamed are detected', () => {
|
|
212
|
+
const added = collect('export class A { one(){return 1;} }', 'export class A { one(){return 1;} two(){return 2;} }');
|
|
213
|
+
const removed = collect('export class A { one(){return 1;} two(){return 2;} }', 'export class A { one(){return 1;} }');
|
|
214
|
+
const renamed = collect('export class A { old(){return 1;} }', 'export class A { newer(){return 1;} }');
|
|
215
|
+
assert.equal(hasMethod(added, 'A', 'two'), true);
|
|
216
|
+
assert.equal(hasMethod(removed, 'A', 'two'), true);
|
|
217
|
+
assert.equal(hasMethod(renamed, 'A', 'old') || hasMethod(renamed, 'A', 'newer'), true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('Getter and setter add/remove/change are detected', () => {
|
|
221
|
+
const added = collect('export class A { get x(){ return 1; } }', 'export class A { get x(){ return 2; } set x(v:number){} }');
|
|
222
|
+
const removed = collect('export class A { get x(){ return 1; } set x(v:number){} }', 'export class A { get x(){ return 1; } }');
|
|
223
|
+
assert.equal(added.stats.semanticChangedMethodsCount >= 1, true);
|
|
224
|
+
assert.equal(removed.stats.semanticChangedMethodsCount >= 1, true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('Arrow/function-expression property methods are detected', () => {
|
|
228
|
+
const arrow = collect('export class A { run = () => 1; }', 'export class A { run = () => 2; }');
|
|
229
|
+
const fnExpr = collect('export class A { run = function(){ return 1; }; }', 'export class A { run = function(){ return 2; }; }');
|
|
230
|
+
assert.equal(hasMethod(arrow, 'A', 'run'), true);
|
|
231
|
+
assert.equal(hasMethod(fnExpr, 'A', 'run'), true);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('Arrow property whitespace-only change yields no change', () => {
|
|
235
|
+
const result = collect('export class A { run = () => { return 1; }; }', 'export class A { run = ()=>{return 1;}; }');
|
|
236
|
+
assert.equal(result.stats.semanticChangedMethodsCount, 0);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('Overload changes impact method even if implementation unchanged', () => {
|
|
240
|
+
const overloadAdded = collect(
|
|
241
|
+
'export class A { run(a:number):number; run(a:any){ return a; } }',
|
|
242
|
+
'export class A { run(a:number):number; run(a:string):string; run(a:any){ return a; } }'
|
|
243
|
+
);
|
|
244
|
+
const orderChanged = collect(
|
|
245
|
+
'export class A { run(a:number):number; run(a:string):string; run(a:any){ return a; } }',
|
|
246
|
+
'export class A { run(a:string):string; run(a:number):number; run(a:any){ return a; } }'
|
|
247
|
+
);
|
|
248
|
+
assert.equal(hasMethod(overloadAdded, 'A', 'run'), true);
|
|
249
|
+
assert.equal(hasMethod(orderChanged, 'A', 'run'), true);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('Propagation handles cycles and mutual recursion deterministically', () => {
|
|
253
|
+
const dir = createTempDir();
|
|
254
|
+
const file = writeFile(
|
|
255
|
+
dir,
|
|
256
|
+
'A.ts',
|
|
257
|
+
'export class A { a(){ return this.b(); } b(){ return this.a(); } c(){ return this.a(); } }'
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const result = buildImpactedMethodsByClass({
|
|
261
|
+
impactedClasses: new Set(['A']),
|
|
262
|
+
changedMethodsByClass: new Map([['A', new Set(['a'])]]),
|
|
263
|
+
parentsByChild: new Map(),
|
|
264
|
+
pageFiles: [file],
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const methods = Array.from(result.impactedMethodsByClass.get('A') || []).sort();
|
|
268
|
+
assert.deepEqual(methods, ['a', 'b', 'c']);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('Unresolvable this/super calls do not crash and produce warnings', () => {
|
|
272
|
+
const dir = createTempDir();
|
|
273
|
+
const file = writeFile(dir, 'A.ts', 'export class A { run(){ return this.unknown(); } }');
|
|
274
|
+
const file2 = writeFile(dir, 'B.ts', 'export class B extends A { run2(){ return super.unknown(); } }');
|
|
275
|
+
|
|
276
|
+
const result = buildImpactedMethodsByClass({
|
|
277
|
+
impactedClasses: new Set(['A', 'B']),
|
|
278
|
+
changedMethodsByClass: new Map([['A', new Set(['run'])]]),
|
|
279
|
+
parentsByChild: new Map([['B', 'A']]),
|
|
280
|
+
pageFiles: [file, file2],
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
assert.equal(Array.isArray(result.warnings), true);
|
|
284
|
+
assert.equal(result.warnings.length > 0, true);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('Deep this chain treated as uncertain and does not crash', () => {
|
|
288
|
+
const dir = createTempDir();
|
|
289
|
+
const file = writeFile(dir, 'A.ts', 'export class A { run(){ return this.a.b.c(); } c(){ return 1; } }');
|
|
290
|
+
|
|
291
|
+
const result = buildImpactedMethodsByClass({
|
|
292
|
+
impactedClasses: new Set(['A']),
|
|
293
|
+
changedMethodsByClass: new Map([['A', new Set(['c'])]]),
|
|
294
|
+
parentsByChild: new Map(),
|
|
295
|
+
pageFiles: [file],
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
assert.equal(Array.isArray(result.warnings), true);
|
|
299
|
+
assert.equal(result.warnings.some((w) => w.includes('Deep this.* chain')), true);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('Composition call this.child.foo() resolves when child is new Child() in ctor', () => {
|
|
303
|
+
const dir = createTempDir();
|
|
304
|
+
const child = writeFile(dir, 'Child.ts', 'export class Child { foo(){ return 1; } }');
|
|
305
|
+
const page = writeFile(dir, 'Page.ts', 'export class Page { constructor(){ this.child = new Child(); } run(){ return this.child.foo(); } }');
|
|
306
|
+
|
|
307
|
+
const result = buildImpactedMethodsByClass({
|
|
308
|
+
impactedClasses: new Set(['Child']),
|
|
309
|
+
changedMethodsByClass: new Map([['Child', new Set(['foo'])]]),
|
|
310
|
+
parentsByChild: new Map(),
|
|
311
|
+
pageFiles: [child, page],
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const pageMethods = Array.from(result.impactedMethodsByClass.get('Page') || []);
|
|
315
|
+
assert.equal(pageMethods.includes('run'), true);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('Composition call resolves when child is typed as namespace-qualified class', () => {
|
|
319
|
+
const dir = createTempDir();
|
|
320
|
+
const child = writeFile(dir, 'Child.ts', 'export class Child { foo(){ return 1; } }');
|
|
321
|
+
const page = writeFile(dir, 'Page.ts', 'export class Page { child: Pages.Child; run(){ return this.child.foo(); } }');
|
|
322
|
+
|
|
323
|
+
const result = buildImpactedMethodsByClass({
|
|
324
|
+
impactedClasses: new Set(['Child']),
|
|
325
|
+
changedMethodsByClass: new Map([['Child', new Set(['foo'])]]),
|
|
326
|
+
parentsByChild: new Map(),
|
|
327
|
+
pageFiles: [child, page],
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const pageMethods = Array.from(result.impactedMethodsByClass.get('Page') || []);
|
|
331
|
+
assert.equal(pageMethods.includes('run'), true);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('Composition call with unknown child type is marked uncertain', () => {
|
|
335
|
+
const dir = createTempDir();
|
|
336
|
+
const page = writeFile(dir, 'Page.ts', 'export class Page { run(){ return this.child.foo(); } foo(){ return 1; } }');
|
|
337
|
+
|
|
338
|
+
const result = buildImpactedMethodsByClass({
|
|
339
|
+
impactedClasses: new Set(['Page']),
|
|
340
|
+
changedMethodsByClass: new Map([['Page', new Set(['foo'])]]),
|
|
341
|
+
parentsByChild: new Map(),
|
|
342
|
+
pageFiles: [page],
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
assert.equal(result.warnings.some((w) => w.includes('Unknown composed field type')), true);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('Call graph resolves this.foo() to nearest ancestor in lineage', () => {
|
|
349
|
+
const dir = createTempDir();
|
|
350
|
+
const base = writeFile(dir, 'Base.ts', 'export class Base { foo(){ return 1; } }');
|
|
351
|
+
const child = writeFile(dir, 'Child.ts', 'export class Child extends Base { caller(){ return this.foo(); } }');
|
|
352
|
+
|
|
353
|
+
const result = buildImpactedMethodsByClass({
|
|
354
|
+
impactedClasses: new Set(['Base']),
|
|
355
|
+
changedMethodsByClass: new Map([['Base', new Set(['foo'])]]),
|
|
356
|
+
parentsByChild: new Map([['Child', 'Base']]),
|
|
357
|
+
pageFiles: [base, child],
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const childMethods = Array.from(result.impactedMethodsByClass.get('Child') || []);
|
|
361
|
+
assert.equal(childMethods.includes('caller'), true);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test('Call graph resolves super.foo() only via parent chain', () => {
|
|
365
|
+
const dir = createTempDir();
|
|
366
|
+
const base = writeFile(dir, 'Base.ts', 'export class Base { foo(){ return 1; } }');
|
|
367
|
+
const child = writeFile(dir, 'Child.ts', 'export class Child extends Base { caller(){ return super.foo(); } }');
|
|
368
|
+
const grand = writeFile(dir, 'Grand.ts', 'export class Grand extends Child { caller2(){ return super.caller(); } }');
|
|
369
|
+
|
|
370
|
+
const result = buildImpactedMethodsByClass({
|
|
371
|
+
impactedClasses: new Set(['Base']),
|
|
372
|
+
changedMethodsByClass: new Map([['Base', new Set(['foo'])]]),
|
|
373
|
+
parentsByChild: new Map([['Child', 'Base'], ['Grand', 'Child']]),
|
|
374
|
+
pageFiles: [base, child, grand],
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const childMethods = Array.from(result.impactedMethodsByClass.get('Child') || []);
|
|
378
|
+
const grandMethods = Array.from(result.impactedMethodsByClass.get('Grand') || []);
|
|
379
|
+
assert.equal(childMethods.includes('caller'), true);
|
|
380
|
+
assert.equal(grandMethods.includes('caller2'), true);
|
|
381
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const { selectSpecFiles } = require('../src/modules/spec-selection-helpers');
|
|
6
|
+
const { createTempDir, writeFile } = require('./_test-helpers');
|
|
7
|
+
|
|
8
|
+
const listFilesRecursive = (rootDir) => {
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const files = [];
|
|
12
|
+
const walk = (dir) => {
|
|
13
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
14
|
+
const p = path.join(dir, entry.name);
|
|
15
|
+
if (entry.isDirectory()) walk(p);
|
|
16
|
+
else files.push(p);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
walk(rootDir);
|
|
20
|
+
return files;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
test('selectSpecFiles selects spec by fixture key usage in async callback', () => {
|
|
24
|
+
const dir = createTempDir();
|
|
25
|
+
const spec = writeFile(dir, 'tests/a.spec.ts', 'test("x", async ({ appPage, somethingElse }) => { await appPage.open(); });');
|
|
26
|
+
|
|
27
|
+
const selected = selectSpecFiles({ testsRootAbs: dir, fixtureKeys: new Set(['appPage']), listFilesRecursive });
|
|
28
|
+
|
|
29
|
+
assert.deepEqual(selected, [spec]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('selectSpecFiles selects spec by fixture key usage in sync callback', () => {
|
|
33
|
+
const dir = createTempDir();
|
|
34
|
+
const spec = writeFile(dir, 'tests/a.spec.ts', 'test("x", ({ appPage }) => { void appPage; });');
|
|
35
|
+
|
|
36
|
+
const selected = selectSpecFiles({ testsRootAbs: dir, fixtureKeys: new Set(['appPage']), listFilesRecursive });
|
|
37
|
+
|
|
38
|
+
assert.deepEqual(selected, [spec]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('selectSpecFiles supports alias binding in callback params', () => {
|
|
42
|
+
const dir = createTempDir();
|
|
43
|
+
const spec = writeFile(dir, 'tests/a.spec.ts', 'test("x", async ({ appPage: p }) => { await p.open(); });');
|
|
44
|
+
|
|
45
|
+
const selected = selectSpecFiles({ testsRootAbs: dir, fixtureKeys: new Set(['appPage']), listFilesRecursive });
|
|
46
|
+
|
|
47
|
+
assert.deepEqual(selected, [spec]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('selectSpecFiles supports default binding in callback params', () => {
|
|
51
|
+
const dir = createTempDir();
|
|
52
|
+
const spec = writeFile(dir, 'tests/a.spec.ts', 'test("x", async ({ appPage = fallback }) => { await appPage.open(); });');
|
|
53
|
+
|
|
54
|
+
const selected = selectSpecFiles({ testsRootAbs: dir, fixtureKeys: new Set(['appPage']), listFilesRecursive });
|
|
55
|
+
|
|
56
|
+
assert.deepEqual(selected, [spec]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('selectSpecFiles supports rest pattern in callback params', () => {
|
|
60
|
+
const dir = createTempDir();
|
|
61
|
+
const spec = writeFile(dir, 'tests/a.spec.ts', 'test(\"x\", async ({ appPage, ...rest }) => { await appPage.open(); });');
|
|
62
|
+
|
|
63
|
+
const selected = selectSpecFiles({ testsRootAbs: dir, fixtureKeys: new Set(['appPage']), listFilesRecursive });
|
|
64
|
+
|
|
65
|
+
assert.deepEqual(selected, [spec]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('selectSpecFiles supports nested object binding edge form', () => {
|
|
69
|
+
const dir = createTempDir();
|
|
70
|
+
const spec = writeFile(dir, 'tests/a.spec.ts', 'test(\"x\", async ({ appPage: { open } }) => { void open; });');
|
|
71
|
+
|
|
72
|
+
const selected = selectSpecFiles({ testsRootAbs: dir, fixtureKeys: new Set(['appPage']), listFilesRecursive });
|
|
73
|
+
|
|
74
|
+
assert.deepEqual(selected, [spec]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('selectSpecFiles supports multiple fixtures in same callback', () => {
|
|
78
|
+
const dir = createTempDir();
|
|
79
|
+
const spec = writeFile(
|
|
80
|
+
dir,
|
|
81
|
+
'tests/a.spec.ts',
|
|
82
|
+
'test("x", async ({ altPage, appPage }) => { await altPage.open(); await appPage.open(); });'
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const selected = selectSpecFiles({ testsRootAbs: dir, fixtureKeys: new Set(['altPage']), listFilesRecursive });
|
|
86
|
+
|
|
87
|
+
assert.deepEqual(selected, [spec]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('selectSpecFiles scans nested directories', () => {
|
|
91
|
+
const dir = createTempDir();
|
|
92
|
+
const spec = writeFile(dir, 'tests/nested/case.spec.ts', 'test("x", async ({ pageA }) => { await pageA.open(); });');
|
|
93
|
+
|
|
94
|
+
const selected = selectSpecFiles({ testsRootAbs: dir, fixtureKeys: new Set(['pageA']), listFilesRecursive });
|
|
95
|
+
|
|
96
|
+
assert.deepEqual(selected, [spec]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('selectSpecFiles supports tsx spec files', () => {
|
|
100
|
+
const dir = createTempDir();
|
|
101
|
+
const spec = writeFile(dir, 'tests/ui.spec.tsx', 'test("x", async ({ appPage }) => { await appPage.open(); return <div />; });');
|
|
102
|
+
|
|
103
|
+
const selected = selectSpecFiles({ testsRootAbs: dir, fixtureKeys: new Set(['appPage']), listFilesRecursive });
|
|
104
|
+
|
|
105
|
+
assert.deepEqual(selected, [spec]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('selectSpecFiles ignores non-impacted fixtures', () => {
|
|
109
|
+
const dir = createTempDir();
|
|
110
|
+
writeFile(dir, 'tests/a.spec.ts', 'test("x", async ({ abc }) => { await abc.run(); });');
|
|
111
|
+
|
|
112
|
+
const selected = selectSpecFiles({ testsRootAbs: dir, fixtureKeys: new Set(['appPage']), listFilesRecursive });
|
|
113
|
+
|
|
114
|
+
assert.equal(selected.length, 0);
|
|
115
|
+
});
|