@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,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { buildInheritanceGraph, collectImpactedClasses, getFixtureKeysForClasses } = require('../src/modules/class-impact-helpers');
|
|
8
|
+
const { createTempDir, writeFile } = require('./_test-helpers');
|
|
9
|
+
|
|
10
|
+
test('buildInheritanceGraph detects direct inheritance', () => {
|
|
11
|
+
const dir = createTempDir();
|
|
12
|
+
const basePath = writeFile(dir, 'Base.ts', 'export class Base {}');
|
|
13
|
+
const childPath = writeFile(dir, 'Child.ts', 'export class Child extends Base {}');
|
|
14
|
+
|
|
15
|
+
const graph = buildInheritanceGraph([basePath, childPath], fs.readFileSync);
|
|
16
|
+
|
|
17
|
+
assert.equal(graph.parentsByChild.get('Child'), 'Base');
|
|
18
|
+
assert.deepEqual(Array.from(graph.childrenByParent.get('Base') || []), ['Child']);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('buildInheritanceGraph tracks multi-level inheritance', () => {
|
|
22
|
+
const dir = createTempDir();
|
|
23
|
+
const p1 = writeFile(dir, 'A.ts', 'export class A {}');
|
|
24
|
+
const p2 = writeFile(dir, 'B.ts', 'export class B extends A {}');
|
|
25
|
+
const p3 = writeFile(dir, 'C.ts', 'export class C extends B {}');
|
|
26
|
+
|
|
27
|
+
const graph = buildInheritanceGraph([p1, p2, p3], fs.readFileSync);
|
|
28
|
+
|
|
29
|
+
assert.equal(graph.parentsByChild.get('B'), 'A');
|
|
30
|
+
assert.equal(graph.parentsByChild.get('C'), 'B');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('collectImpactedClasses includes base and head class names', () => {
|
|
34
|
+
const changedPomEntries = [{ status: 'M', effectivePath: 'src/pages/A.ts', oldPath: 'src/pages/A.ts', newPath: 'src/pages/A.ts' }];
|
|
35
|
+
const childrenByParent = new Map();
|
|
36
|
+
const readChangeContents = () => ({
|
|
37
|
+
baseContent: 'export class OldA {}',
|
|
38
|
+
headContent: 'export class NewA {}',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const impacted = collectImpactedClasses({ changedPomEntries, childrenByParent, baseRef: null, readChangeContents });
|
|
42
|
+
|
|
43
|
+
assert.equal(impacted.has('OldA'), true);
|
|
44
|
+
assert.equal(impacted.has('NewA'), true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('collectImpactedClasses includes descendants from graph', () => {
|
|
48
|
+
const changedPomEntries = [{ status: 'M', effectivePath: 'src/pages/A.ts', oldPath: 'src/pages/A.ts', newPath: 'src/pages/A.ts' }];
|
|
49
|
+
const childrenByParent = new Map([
|
|
50
|
+
['Base', new Set(['Child'])],
|
|
51
|
+
['Child', new Set(['GrandChild'])],
|
|
52
|
+
]);
|
|
53
|
+
const readChangeContents = () => ({ baseContent: 'export class Base {}', headContent: 'export class Base {}' });
|
|
54
|
+
|
|
55
|
+
const impacted = collectImpactedClasses({ changedPomEntries, childrenByParent, baseRef: null, readChangeContents });
|
|
56
|
+
|
|
57
|
+
assert.equal(impacted.has('Base'), true);
|
|
58
|
+
assert.equal(impacted.has('Child'), true);
|
|
59
|
+
assert.equal(impacted.has('GrandChild'), true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('collectImpactedClasses handles empty entries', () => {
|
|
63
|
+
const impacted = collectImpactedClasses({
|
|
64
|
+
changedPomEntries: [],
|
|
65
|
+
childrenByParent: new Map(),
|
|
66
|
+
baseRef: null,
|
|
67
|
+
readChangeContents: () => ({ baseContent: '', headContent: '' }),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
assert.equal(impacted.size, 0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('getFixtureKeysForClasses returns merged keys without duplicates', () => {
|
|
74
|
+
const impactedClasses = new Set(['A', 'B']);
|
|
75
|
+
const classToFixtureKeys = new Map([
|
|
76
|
+
['A', new Set(['aPage', 'aExtra'])],
|
|
77
|
+
['B', new Set(['bPage', 'aPage'])],
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const keys = getFixtureKeysForClasses(impactedClasses, classToFixtureKeys);
|
|
81
|
+
|
|
82
|
+
assert.deepEqual(Array.from(keys).sort(), ['aExtra', 'aPage', 'bPage']);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('getFixtureKeysForClasses ignores classes without mapping', () => {
|
|
86
|
+
const impactedClasses = new Set(['A', 'Unknown']);
|
|
87
|
+
const classToFixtureKeys = new Map([['A', new Set(['aPage'])]]);
|
|
88
|
+
|
|
89
|
+
const keys = getFixtureKeysForClasses(impactedClasses, classToFixtureKeys);
|
|
90
|
+
|
|
91
|
+
assert.deepEqual(Array.from(keys), ['aPage']);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('buildInheritanceGraph ignores files without extends', () => {
|
|
95
|
+
const dir = createTempDir();
|
|
96
|
+
const a = writeFile(dir, 'Plain.ts', 'export class Plain {}');
|
|
97
|
+
const graph = buildInheritanceGraph([a], fs.readFileSync);
|
|
98
|
+
|
|
99
|
+
assert.equal(graph.parentsByChild.size, 0);
|
|
100
|
+
assert.equal(graph.childrenByParent.size, 0);
|
|
101
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const { getChangedEntries } = require('../src/modules/file-and-git-helpers');
|
|
6
|
+
const { createTempDir, writeFile, initGitRepo, commitAll, run } = require('./_test-helpers');
|
|
7
|
+
|
|
8
|
+
const profile = {
|
|
9
|
+
isRelevantPomPath: (filePath) => filePath.startsWith('src/pages/') && (filePath.endsWith('.ts') || filePath.endsWith('.tsx')),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const setupRepo = () => {
|
|
13
|
+
const dir = createTempDir();
|
|
14
|
+
initGitRepo(dir);
|
|
15
|
+
writeFile(dir, 'src/pages/Page.ts', 'export class Page { open(){ return 1; } }\n');
|
|
16
|
+
writeFile(dir, 'README.md', '# test\n');
|
|
17
|
+
commitAll(dir, 'base');
|
|
18
|
+
return dir;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
test('getChangedEntries includes unstaged working tree changes when baseRef is provided', () => {
|
|
22
|
+
const dir = setupRepo();
|
|
23
|
+
writeFile(dir, 'README.md', '# test2\n');
|
|
24
|
+
commitAll(dir, 'commit2');
|
|
25
|
+
|
|
26
|
+
writeFile(dir, 'src/pages/Page.ts', 'export class Page { open(){ return 2; } }\n');
|
|
27
|
+
|
|
28
|
+
const noWorkingTree = getChangedEntries({
|
|
29
|
+
repoRoot: dir,
|
|
30
|
+
baseRef: 'HEAD~1',
|
|
31
|
+
includeWorkingTreeWithBase: false,
|
|
32
|
+
profile,
|
|
33
|
+
fileExtensions: ['.ts', '.tsx'],
|
|
34
|
+
});
|
|
35
|
+
const withWorkingTree = getChangedEntries({
|
|
36
|
+
repoRoot: dir,
|
|
37
|
+
baseRef: 'HEAD~1',
|
|
38
|
+
includeWorkingTreeWithBase: true,
|
|
39
|
+
profile,
|
|
40
|
+
fileExtensions: ['.ts', '.tsx'],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
assert.equal(noWorkingTree.entries.some((entry) => entry.effectivePath === 'src/pages/Page.ts'), false);
|
|
44
|
+
assert.equal(withWorkingTree.entries.some((entry) => entry.effectivePath === 'src/pages/Page.ts'), true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('getChangedEntries includes staged working tree changes when baseRef is provided', () => {
|
|
48
|
+
const dir = setupRepo();
|
|
49
|
+
writeFile(dir, 'README.md', '# test2\n');
|
|
50
|
+
commitAll(dir, 'commit2');
|
|
51
|
+
|
|
52
|
+
writeFile(dir, 'src/pages/Page.ts', 'export class Page { open(){ return 3; } }\n');
|
|
53
|
+
run(dir, 'git', ['add', 'src/pages/Page.ts']);
|
|
54
|
+
|
|
55
|
+
const result = getChangedEntries({ repoRoot: dir, baseRef: 'HEAD~1', includeWorkingTreeWithBase: true, profile, fileExtensions: ['.ts', '.tsx'] });
|
|
56
|
+
assert.equal(result.entries.some((entry) => entry.effectivePath === 'src/pages/Page.ts'), true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('includeWorkingTreeWithBase disabled ignores working tree changes', () => {
|
|
60
|
+
const dir = setupRepo();
|
|
61
|
+
writeFile(dir, 'README.md', '# test2\n');
|
|
62
|
+
commitAll(dir, 'commit2');
|
|
63
|
+
|
|
64
|
+
writeFile(dir, 'src/pages/Page.ts', 'export class Page { open(){ return 10; } }\n');
|
|
65
|
+
|
|
66
|
+
const result = getChangedEntries({
|
|
67
|
+
repoRoot: dir,
|
|
68
|
+
baseRef: 'HEAD~1',
|
|
69
|
+
includeWorkingTreeWithBase: false,
|
|
70
|
+
profile,
|
|
71
|
+
fileExtensions: ['.ts', '.tsx'],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
assert.equal(result.entries.some((entry) => entry.effectivePath === 'src/pages/Page.ts'), false);
|
|
75
|
+
assert.equal(result.changedEntriesBySource.fromWorkingTree, 0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('getChangedEntries includes untracked ts source entries', () => {
|
|
79
|
+
const dir = setupRepo();
|
|
80
|
+
writeFile(dir, 'src/pages/NewPage.ts', 'export class NewPage {}\n');
|
|
81
|
+
|
|
82
|
+
const result = getChangedEntries({ repoRoot: dir, baseRef: null, includeWorkingTreeWithBase: true, profile, fileExtensions: ['.ts', '.tsx'] });
|
|
83
|
+
assert.equal(result.entries.some((entry) => entry.effectivePath === 'src/pages/NewPage.ts' && entry.status === 'A'), true);
|
|
84
|
+
assert.equal(result.changedEntriesBySource.fromUntracked >= 1, true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('getChangedEntries includes untracked tsx source entries', () => {
|
|
88
|
+
const dir = setupRepo();
|
|
89
|
+
writeFile(dir, 'src/pages/NewPage.tsx', 'export class NewPage {}\n');
|
|
90
|
+
|
|
91
|
+
const result = getChangedEntries({ repoRoot: dir, baseRef: null, includeWorkingTreeWithBase: true, profile, fileExtensions: ['.ts', '.tsx'] });
|
|
92
|
+
assert.equal(result.entries.some((entry) => entry.effectivePath === 'src/pages/NewPage.tsx' && entry.status === 'A'), true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('getChangedEntries detects rename status with -M', () => {
|
|
96
|
+
const dir = setupRepo();
|
|
97
|
+
run(dir, 'git', ['mv', 'src/pages/Page.ts', 'src/pages/PageRenamed.ts']);
|
|
98
|
+
|
|
99
|
+
const result = getChangedEntries({ repoRoot: dir, baseRef: null, includeWorkingTreeWithBase: true, profile, fileExtensions: ['.ts', '.tsx'] });
|
|
100
|
+
const renameEntry = result.entries.find((entry) => entry.effectivePath === 'src/pages/PageRenamed.ts');
|
|
101
|
+
|
|
102
|
+
assert.ok(renameEntry);
|
|
103
|
+
assert.equal(renameEntry.status, 'R');
|
|
104
|
+
assert.equal(renameEntry.oldPath, 'src/pages/Page.ts');
|
|
105
|
+
assert.equal(renameEntry.newPath, 'src/pages/PageRenamed.ts');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('getChangedEntries keeps higher-priority status when path appears in both diff sources', () => {
|
|
109
|
+
const dir = setupRepo();
|
|
110
|
+
run(dir, 'git', ['mv', 'src/pages/Page.ts', 'src/pages/PageRenamed.ts']);
|
|
111
|
+
commitAll(dir, 'rename page');
|
|
112
|
+
|
|
113
|
+
writeFile(dir, 'src/pages/PageRenamed.ts', 'export class Page { open(){ return 4; } }\n');
|
|
114
|
+
|
|
115
|
+
const result = getChangedEntries({ repoRoot: dir, baseRef: 'HEAD~1', includeWorkingTreeWithBase: true, profile, fileExtensions: ['.ts', '.tsx'] });
|
|
116
|
+
const entry = result.entries.find((item) => item.effectivePath === 'src/pages/PageRenamed.ts');
|
|
117
|
+
|
|
118
|
+
assert.ok(entry);
|
|
119
|
+
assert.equal(entry.status, 'R');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('Combined base...HEAD and working tree diffs are unioned', () => {
|
|
123
|
+
const dir = setupRepo();
|
|
124
|
+
writeFile(dir, 'src/pages/Committed.ts', 'export class Committed { open(){ return 1; } }\n');
|
|
125
|
+
commitAll(dir, 'add committed page');
|
|
126
|
+
writeFile(dir, 'src/pages/Working.ts', 'export class Working { open(){ return 1; } }\n');
|
|
127
|
+
|
|
128
|
+
const result = getChangedEntries({
|
|
129
|
+
repoRoot: dir,
|
|
130
|
+
baseRef: 'HEAD~1',
|
|
131
|
+
includeWorkingTreeWithBase: true,
|
|
132
|
+
profile,
|
|
133
|
+
fileExtensions: ['.ts', '.tsx'],
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
assert.equal(result.entries.some((entry) => entry.effectivePath === 'src/pages/Committed.ts'), true);
|
|
137
|
+
assert.equal(result.entries.some((entry) => entry.effectivePath === 'src/pages/Working.ts'), true);
|
|
138
|
+
assert.equal(result.changedEntriesBySource.fromBaseHead >= 1, true);
|
|
139
|
+
assert.equal(result.changedEntriesBySource.fromWorkingTree + result.changedEntriesBySource.fromUntracked >= 1, true);
|
|
140
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const { __testOnly } = require('../src/modules/file-and-git-helpers');
|
|
6
|
+
|
|
7
|
+
const { parseChangedEntryLine, normalizeEntryStatus, mergeByPriority } = __testOnly;
|
|
8
|
+
|
|
9
|
+
test('Copy status (C) treated safely as add with warning', () => {
|
|
10
|
+
const parsed = parseChangedEntryLine('C100\tsrc/a.ts\tsrc/b.ts');
|
|
11
|
+
const warnings = [];
|
|
12
|
+
const normalized = normalizeEntryStatus(parsed, warnings);
|
|
13
|
+
|
|
14
|
+
assert.equal(normalized.status, 'A');
|
|
15
|
+
assert.equal(warnings.length > 0, true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('Type change status (T) treated safely as modify with warning', () => {
|
|
19
|
+
const parsed = parseChangedEntryLine('T\tsrc/a.ts');
|
|
20
|
+
const warnings = [];
|
|
21
|
+
const normalized = normalizeEntryStatus(parsed, warnings);
|
|
22
|
+
|
|
23
|
+
assert.equal(normalized.status, 'M');
|
|
24
|
+
assert.equal(warnings.length > 0, true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('Unmerged status (U) treated safely as modify with warning', () => {
|
|
28
|
+
const parsed = parseChangedEntryLine('U\tsrc/a.ts');
|
|
29
|
+
const warnings = [];
|
|
30
|
+
const normalized = normalizeEntryStatus(parsed, warnings);
|
|
31
|
+
|
|
32
|
+
assert.equal(normalized.status, 'M');
|
|
33
|
+
assert.equal(warnings.length > 0, true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('Unknown git status triggers compat fallback with warning', () => {
|
|
37
|
+
const parsed = parseChangedEntryLine('Z\tsrc/a.ts');
|
|
38
|
+
const warnings = [];
|
|
39
|
+
const normalized = normalizeEntryStatus(parsed, warnings);
|
|
40
|
+
|
|
41
|
+
assert.equal(normalized.status, 'M');
|
|
42
|
+
assert.equal(warnings.length > 0, true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('Status merge precedence D > R > M > A is deterministic', () => {
|
|
46
|
+
const a = { status: 'A', effectivePath: 'x.ts' };
|
|
47
|
+
const m = { status: 'M', effectivePath: 'x.ts' };
|
|
48
|
+
const r = { status: 'R', effectivePath: 'x.ts', oldPath: 'old.ts', newPath: 'x.ts' };
|
|
49
|
+
const d = { status: 'D', effectivePath: 'x.ts' };
|
|
50
|
+
|
|
51
|
+
assert.equal(mergeByPriority(a, m).status, 'M');
|
|
52
|
+
assert.equal(mergeByPriority(m, r).status, 'R');
|
|
53
|
+
assert.equal(mergeByPriority(r, d).status, 'D');
|
|
54
|
+
assert.equal(mergeByPriority(d, a).status, 'D');
|
|
55
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { parseFixtureMappings } = require('../src/modules/fixture-map-helpers');
|
|
7
|
+
const { createTempDir, writeFile } = require('./_test-helpers');
|
|
8
|
+
|
|
9
|
+
test('parseFixtureMappings parses fixture to class mapping', () => {
|
|
10
|
+
const dir = createTempDir();
|
|
11
|
+
const typesPath = writeFile(
|
|
12
|
+
dir,
|
|
13
|
+
'src/fixtures/types.ts',
|
|
14
|
+
'type X = {\n appPage: Pages.AppPage;\n altPage: Pages.AltPage;\n};\n'
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const mappings = parseFixtureMappings({ typesPath });
|
|
18
|
+
|
|
19
|
+
assert.deepEqual(Array.from(mappings.fixtureKeyToClass.entries()).sort(), [
|
|
20
|
+
['altPage', 'AltPage'],
|
|
21
|
+
['appPage', 'AppPage'],
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('parseFixtureMappings supports many fixture keys per class', () => {
|
|
26
|
+
const dir = createTempDir();
|
|
27
|
+
const typesPath = writeFile(
|
|
28
|
+
dir,
|
|
29
|
+
'src/fixtures/types.ts',
|
|
30
|
+
'type X = {\n pageA: Pages.SharedPage;\n pageB: Pages.SharedPage;\n};\n'
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const mappings = parseFixtureMappings({ typesPath });
|
|
34
|
+
|
|
35
|
+
assert.deepEqual(Array.from(mappings.classToFixtureKeys.get('SharedPage') || []).sort(), ['pageA', 'pageB']);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('parseFixtureMappings supports direct helper type mapping', () => {
|
|
39
|
+
const dir = createTempDir();
|
|
40
|
+
const typesPath = writeFile(
|
|
41
|
+
dir,
|
|
42
|
+
'src/fixtures/types.ts',
|
|
43
|
+
'type X = {\n userManagementHelper: UserManagementHelper;\n rosterHelper: RosterHelper;\n};\n'
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const mappings = parseFixtureMappings({ typesPath });
|
|
47
|
+
|
|
48
|
+
assert.deepEqual(Array.from(mappings.fixtureKeyToClass.entries()).sort(), [
|
|
49
|
+
['rosterHelper', 'RosterHelper'],
|
|
50
|
+
['userManagementHelper', 'UserManagementHelper'],
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('parseFixtureMappings supports multi-namespace type mapping', () => {
|
|
55
|
+
const dir = createTempDir();
|
|
56
|
+
const typesPath = writeFile(
|
|
57
|
+
dir,
|
|
58
|
+
'src/fixtures/types.ts',
|
|
59
|
+
'type X = {\n graphClient: Api.Services.GraphClient;\n};\n'
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const mappings = parseFixtureMappings({ typesPath });
|
|
63
|
+
assert.equal(mappings.fixtureKeyToClass.get('graphClient'), 'GraphClient');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('parseFixtureMappings supports interface inheritance and type intersections', () => {
|
|
67
|
+
const dir = createTempDir();
|
|
68
|
+
const typesPath = writeFile(
|
|
69
|
+
dir,
|
|
70
|
+
'src/fixtures/types.ts',
|
|
71
|
+
[
|
|
72
|
+
'interface BaseFixtures {',
|
|
73
|
+
' userManagementHelper: UserManagementHelper;',
|
|
74
|
+
'}',
|
|
75
|
+
'type ServiceHelpersFixture = BaseFixtures & {',
|
|
76
|
+
' rosterHelper: RosterHelper;',
|
|
77
|
+
'};',
|
|
78
|
+
'',
|
|
79
|
+
].join('\n')
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const mappings = parseFixtureMappings({ typesPath });
|
|
83
|
+
|
|
84
|
+
assert.equal(mappings.fixtureKeyToClass.get('userManagementHelper'), 'UserManagementHelper');
|
|
85
|
+
assert.equal(mappings.fixtureKeyToClass.get('rosterHelper'), 'RosterHelper');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('parseFixtureMappings ignores non-class-like property types', () => {
|
|
89
|
+
const dir = createTempDir();
|
|
90
|
+
const typesPath = writeFile(
|
|
91
|
+
dir,
|
|
92
|
+
'src/fixtures/types.ts',
|
|
93
|
+
[
|
|
94
|
+
'type Misc = {',
|
|
95
|
+
' retries: number;',
|
|
96
|
+
' labels: string[];',
|
|
97
|
+
' status: "ok" | "fail";',
|
|
98
|
+
' page: Pages.AppPage;',
|
|
99
|
+
'};',
|
|
100
|
+
'',
|
|
101
|
+
].join('\n')
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const mappings = parseFixtureMappings({ typesPath });
|
|
105
|
+
assert.equal(mappings.fixtureKeyToClass.get('page'), 'AppPage');
|
|
106
|
+
assert.equal(mappings.fixtureKeyToClass.has('retries'), false);
|
|
107
|
+
assert.equal(mappings.fixtureKeyToClass.has('labels'), false);
|
|
108
|
+
assert.equal(mappings.fixtureKeyToClass.has('status'), false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('parseFixtureMappings returns empty maps for missing file', () => {
|
|
112
|
+
const dir = createTempDir();
|
|
113
|
+
const typesPath = path.join(dir, 'missing.ts');
|
|
114
|
+
const mappings = parseFixtureMappings({ typesPath });
|
|
115
|
+
|
|
116
|
+
assert.equal(mappings.classToFixtureKeys.size, 0);
|
|
117
|
+
assert.equal(mappings.fixtureKeyToClass.size, 0);
|
|
118
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const { formatSelectionReasonsForLog } = require('../src/format-analyze-result');
|
|
6
|
+
|
|
7
|
+
test('formatSelectionReasonsForLog returns empty string for empty input', () => {
|
|
8
|
+
const result = formatSelectionReasonsForLog({ selectedSpecs: [], selectionReasons: new Map(), repoRoot: process.cwd() });
|
|
9
|
+
assert.equal(result, '');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('formatSelectionReasonsForLog truncates output by maxLines', () => {
|
|
13
|
+
const repoRoot = '/repo';
|
|
14
|
+
const selectedSpecs = ['/repo/tests/a.spec.ts', '/repo/tests/b.spec.ts', '/repo/tests/c.spec.ts'];
|
|
15
|
+
const selectionReasons = new Map([
|
|
16
|
+
['/repo/tests/a.spec.ts', 'reason A'],
|
|
17
|
+
['/repo/tests/b.spec.ts', 'reason B'],
|
|
18
|
+
['/repo/tests/c.spec.ts', 'reason C'],
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const result = formatSelectionReasonsForLog({ selectedSpecs, selectionReasons, repoRoot, maxLines: 2 });
|
|
22
|
+
|
|
23
|
+
assert.match(result, /a\.spec\.ts: reason A/);
|
|
24
|
+
assert.match(result, /b\.spec\.ts: reason B/);
|
|
25
|
+
assert.match(result, /\.\.\. 1 more selected specs with reasons/);
|
|
26
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const { evaluateGlobalWatch, __testOnly } = require('../src/modules/global-watch-helpers');
|
|
6
|
+
const { createTempDir, writeFile } = require('./_test-helpers');
|
|
7
|
+
const { listFilesRecursive } = require('../src/modules/file-and-git-helpers');
|
|
8
|
+
|
|
9
|
+
test('global watch glob matching works for changed entry path', () => {
|
|
10
|
+
const dir = createTempDir();
|
|
11
|
+
writeFile(dir, 'src/fixtures/types.ts', 'type T = {};\n');
|
|
12
|
+
|
|
13
|
+
const result = evaluateGlobalWatch({
|
|
14
|
+
repoRoot: dir,
|
|
15
|
+
changedEntries: [{ status: 'M', effectivePath: 'src/fixtures/types.ts', oldPath: 'src/fixtures/types.ts', newPath: 'src/fixtures/types.ts' }],
|
|
16
|
+
patterns: ['src/fixtures/**'],
|
|
17
|
+
listFilesRecursive,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
assert.equal(result.matchedPaths.includes('src/fixtures/types.ts'), true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('global watch reads tsconfig JSONC aliases and resolves closure transitively', () => {
|
|
24
|
+
const dir = createTempDir();
|
|
25
|
+
writeFile(
|
|
26
|
+
dir,
|
|
27
|
+
'tsconfig.json',
|
|
28
|
+
[
|
|
29
|
+
'{',
|
|
30
|
+
' "compilerOptions": {',
|
|
31
|
+
' "baseUrl": "./",',
|
|
32
|
+
' "paths": {',
|
|
33
|
+
' "@lib/*": ["./src/lib/*"],',
|
|
34
|
+
' },',
|
|
35
|
+
' },',
|
|
36
|
+
'}',
|
|
37
|
+
'',
|
|
38
|
+
].join('\n')
|
|
39
|
+
);
|
|
40
|
+
writeFile(dir, 'src/watch.ts', 'import { a } from "@lib/a"; export const w = a;\n');
|
|
41
|
+
writeFile(dir, 'src/lib/a.ts', 'export { b } from "./b"; export const a = 1;\n');
|
|
42
|
+
writeFile(dir, 'src/lib/b.ts', 'export const b = 2;\n');
|
|
43
|
+
|
|
44
|
+
const closure = __testOnly.resolveGlobalWatchClosure({
|
|
45
|
+
repoRoot: dir,
|
|
46
|
+
patterns: ['src/watch.ts'],
|
|
47
|
+
listFilesRecursive,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
assert.equal(closure.resolvedFilesRelative.includes('src/watch.ts'), true);
|
|
51
|
+
assert.equal(closure.resolvedFilesRelative.includes('src/lib/a.ts'), true);
|
|
52
|
+
assert.equal(closure.resolvedFilesRelative.includes('src/lib/b.ts'), true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('global watch resolves asset string dependency with parent fallback', () => {
|
|
56
|
+
const dir = createTempDir();
|
|
57
|
+
writeFile(
|
|
58
|
+
dir,
|
|
59
|
+
'src/api/mocks/helpers/setup.ts',
|
|
60
|
+
'export const x = async (m) => m.mockOperations("k", "data.json");\n'
|
|
61
|
+
);
|
|
62
|
+
writeFile(dir, 'src/api/mocks/data.json', '{"ok":true}\n');
|
|
63
|
+
|
|
64
|
+
const closure = __testOnly.resolveGlobalWatchClosure({
|
|
65
|
+
repoRoot: dir,
|
|
66
|
+
patterns: ['src/api/mocks/helpers/setup.ts'],
|
|
67
|
+
listFilesRecursive,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
assert.equal(closure.resolvedFilesRelative.includes('src/api/mocks/data.json'), true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('global watch matches changed rename entries by old/new/effective path', () => {
|
|
74
|
+
const dir = createTempDir();
|
|
75
|
+
writeFile(dir, 'src/global-setup-stem.ts', 'export default async () => {};\n');
|
|
76
|
+
|
|
77
|
+
const result = evaluateGlobalWatch({
|
|
78
|
+
repoRoot: dir,
|
|
79
|
+
changedEntries: [
|
|
80
|
+
{
|
|
81
|
+
status: 'R',
|
|
82
|
+
effectivePath: 'src/global-setup-stem-renamed.ts',
|
|
83
|
+
oldPath: 'src/global-setup-stem.ts',
|
|
84
|
+
newPath: 'src/global-setup-stem-renamed.ts',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
patterns: ['src/global-setup-stem.ts'],
|
|
88
|
+
listFilesRecursive,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
assert.equal(result.matchedPaths.includes('src/global-setup-stem.ts'), true);
|
|
92
|
+
});
|