Skip to content

Commit 41219f3

Browse files
Adding unit tests
1 parent b62ea5d commit 41219f3

File tree

2 files changed

+319
-0
lines changed

2 files changed

+319
-0
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { buildApplication } from '../../index';
10+
import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup';
11+
12+
/** Minimal subset of an esbuild metafile used by stats assertions. */
13+
interface StatsMetafile {
14+
inputs: Record<string, unknown>;
15+
outputs: Record<string, { inputs: Record<string, unknown> }>;
16+
}
17+
18+
describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
19+
describe('Option: "statsJson"', () => {
20+
describe('browser-only build', () => {
21+
beforeEach(() => {
22+
harness.useTarget('build', {
23+
...BASE_OPTIONS,
24+
statsJson: true,
25+
});
26+
});
27+
28+
it('generates browser-stats.json and browser-initial-stats.json', async () => {
29+
const { result } = await harness.executeOnce();
30+
31+
expect(result?.success).toBeTrue();
32+
harness.expectFile('dist/browser-stats.json').toExist();
33+
harness.expectFile('dist/browser-initial-stats.json').toExist();
34+
});
35+
36+
it('does not generate server stats files when SSR is disabled', async () => {
37+
const { result } = await harness.executeOnce();
38+
39+
expect(result?.success).toBeTrue();
40+
harness.expectFile('dist/server-stats.json').toNotExist();
41+
harness.expectFile('dist/server-initial-stats.json').toNotExist();
42+
});
43+
44+
it('does not generate the legacy stats.json file', async () => {
45+
const { result } = await harness.executeOnce();
46+
47+
expect(result?.success).toBeTrue();
48+
harness.expectFile('dist/stats.json').toNotExist();
49+
});
50+
51+
it('stats files contain valid esbuild metafile structure', async () => {
52+
const { result } = await harness.executeOnce();
53+
54+
expect(result?.success).toBeTrue();
55+
56+
for (const filename of ['dist/browser-stats.json', 'dist/browser-initial-stats.json']) {
57+
const stats = JSON.parse(harness.readFile(filename)) as StatsMetafile;
58+
expect(stats.inputs).withContext(`${filename} must have an inputs field`).toBeDefined();
59+
expect(stats.outputs).withContext(`${filename} must have an outputs field`).toBeDefined();
60+
}
61+
});
62+
63+
it('output paths do not overlap between browser-stats.json and browser-initial-stats.json', async () => {
64+
const { result } = await harness.executeOnce();
65+
66+
expect(result?.success).toBeTrue();
67+
68+
const nonInitialPaths = new Set(
69+
Object.keys(
70+
(JSON.parse(harness.readFile('dist/browser-stats.json')) as StatsMetafile).outputs,
71+
),
72+
);
73+
const initialPaths = Object.keys(
74+
(JSON.parse(harness.readFile('dist/browser-initial-stats.json')) as StatsMetafile)
75+
.outputs,
76+
);
77+
78+
for (const outputPath of initialPaths) {
79+
expect(nonInitialPaths.has(outputPath))
80+
.withContext(`Output '${outputPath}' must not appear in both stats files`)
81+
.toBeFalse();
82+
}
83+
});
84+
85+
it('inputs in each stats file are only those referenced by included outputs', async () => {
86+
const { result } = await harness.executeOnce();
87+
88+
expect(result?.success).toBeTrue();
89+
90+
for (const filename of ['dist/browser-stats.json', 'dist/browser-initial-stats.json']) {
91+
const stats = JSON.parse(harness.readFile(filename)) as StatsMetafile;
92+
const referencedInputs = new Set(
93+
Object.values(stats.outputs).flatMap((output) => Object.keys(output.inputs)),
94+
);
95+
96+
for (const inputPath of Object.keys(stats.inputs)) {
97+
expect(referencedInputs.has(inputPath))
98+
.withContext(
99+
`Input '${inputPath}' in '${filename}' is not referenced by any included output`,
100+
)
101+
.toBeTrue();
102+
}
103+
}
104+
});
105+
});
106+
107+
describe('when statsJson is false', () => {
108+
it('does not generate any stats files', async () => {
109+
harness.useTarget('build', {
110+
...BASE_OPTIONS,
111+
statsJson: false,
112+
});
113+
114+
const { result } = await harness.executeOnce();
115+
116+
expect(result?.success).toBeTrue();
117+
harness.expectFile('dist/browser-stats.json').toNotExist();
118+
harness.expectFile('dist/browser-initial-stats.json').toNotExist();
119+
harness.expectFile('dist/stats.json').toNotExist();
120+
});
121+
});
122+
123+
describe('SSR build', () => {
124+
beforeEach(async () => {
125+
await harness.modifyFile('src/tsconfig.app.json', (content) => {
126+
const tsConfig = JSON.parse(content) as { files?: string[] };
127+
tsConfig.files ??= [];
128+
tsConfig.files.push('main.server.ts');
129+
130+
return JSON.stringify(tsConfig);
131+
});
132+
133+
harness.useTarget('build', {
134+
...BASE_OPTIONS,
135+
statsJson: true,
136+
server: 'src/main.server.ts',
137+
ssr: true,
138+
});
139+
});
140+
141+
it('generates all four stats files', async () => {
142+
const { result } = await harness.executeOnce();
143+
144+
expect(result?.success).toBeTrue();
145+
harness.expectFile('dist/browser-stats.json').toExist();
146+
harness.expectFile('dist/browser-initial-stats.json').toExist();
147+
harness.expectFile('dist/server-stats.json').toExist();
148+
harness.expectFile('dist/server-initial-stats.json').toExist();
149+
});
150+
151+
it('server stats files contain valid esbuild metafile structure', async () => {
152+
const { result } = await harness.executeOnce();
153+
154+
expect(result?.success).toBeTrue();
155+
156+
for (const filename of ['dist/server-stats.json', 'dist/server-initial-stats.json']) {
157+
const stats = JSON.parse(harness.readFile(filename)) as StatsMetafile;
158+
expect(stats.inputs).withContext(`${filename} must have an inputs field`).toBeDefined();
159+
expect(stats.outputs).withContext(`${filename} must have an outputs field`).toBeDefined();
160+
}
161+
});
162+
163+
it('server output paths do not overlap between server-stats.json and server-initial-stats.json', async () => {
164+
const { result } = await harness.executeOnce();
165+
166+
expect(result?.success).toBeTrue();
167+
168+
const nonInitialPaths = new Set(
169+
Object.keys(
170+
(JSON.parse(harness.readFile('dist/server-stats.json')) as StatsMetafile).outputs,
171+
),
172+
);
173+
const initialPaths = Object.keys(
174+
(JSON.parse(harness.readFile('dist/server-initial-stats.json')) as StatsMetafile).outputs,
175+
);
176+
177+
for (const outputPath of initialPaths) {
178+
expect(nonInitialPaths.has(outputPath))
179+
.withContext(`Output '${outputPath}' must not appear in both server stats files`)
180+
.toBeFalse();
181+
}
182+
});
183+
});
184+
});
185+
});
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { filterMetafile } from './utils';
10+
11+
// Derive the Metafile type from filterMetafile's own signature to avoid a direct esbuild import.
12+
type TestMetafile = Parameters<typeof filterMetafile>[0];
13+
14+
/**
15+
* Builds a minimal Metafile-shaped object for testing filterMetafile.
16+
* @param outputsWithInputs Maps each output path to the input paths it references.
17+
* @param unreferencedInputs Additional input paths that exist in the metafile but
18+
* are not referenced by any output.
19+
*/
20+
function createMetafile(
21+
outputsWithInputs: Record<string, string[]>,
22+
unreferencedInputs: string[] = [],
23+
): TestMetafile {
24+
const inputs: TestMetafile['inputs'] = {};
25+
const outputs: TestMetafile['outputs'] = {};
26+
27+
for (const path of unreferencedInputs) {
28+
inputs[path] = { bytes: 0, imports: [] };
29+
}
30+
31+
for (const [outputPath, inputPaths] of Object.entries(outputsWithInputs)) {
32+
const outputInputs: TestMetafile['outputs'][string]['inputs'] = {};
33+
for (const inputPath of inputPaths) {
34+
outputInputs[inputPath] = { bytesInOutput: 0 };
35+
inputs[inputPath] ??= { bytes: 0, imports: [] };
36+
}
37+
outputs[outputPath] = { bytes: 0, inputs: outputInputs, imports: [], exports: [] };
38+
}
39+
40+
return { inputs, outputs };
41+
}
42+
43+
describe('filterMetafile', () => {
44+
it('returns only outputs matching the predicate', () => {
45+
const metafile = createMetafile({
46+
'browser/main.js': ['src/main.ts'],
47+
'browser/polyfills.js': ['src/polyfills.ts'],
48+
'server/server.mjs': ['src/server.ts'],
49+
});
50+
51+
const result = filterMetafile(metafile, (path) => path.startsWith('browser/'));
52+
53+
expect(Object.keys(result.outputs)).toEqual(
54+
jasmine.arrayContaining(['browser/main.js', 'browser/polyfills.js']),
55+
);
56+
expect(Object.keys(result.outputs)).not.toContain('server/server.mjs');
57+
});
58+
59+
it('includes only inputs referenced by outputs that match the predicate', () => {
60+
const metafile = createMetafile({
61+
'browser/main.js': ['src/main.ts', 'src/app.ts'],
62+
'server/server.mjs': ['src/server.ts'],
63+
});
64+
65+
const result = filterMetafile(metafile, (path) => path.startsWith('browser/'));
66+
67+
expect(Object.keys(result.inputs)).toContain('src/main.ts');
68+
expect(Object.keys(result.inputs)).toContain('src/app.ts');
69+
expect(Object.keys(result.inputs)).not.toContain('src/server.ts');
70+
});
71+
72+
it('excludes unreferenced inputs even when they exist in the original metafile', () => {
73+
const metafile = createMetafile({ 'browser/main.js': ['src/main.ts'] }, [
74+
'src/unreferenced.ts',
75+
]);
76+
77+
const result = filterMetafile(metafile, () => true);
78+
79+
expect(Object.keys(result.inputs)).not.toContain('src/unreferenced.ts');
80+
});
81+
82+
it('returns empty outputs and inputs when predicate never matches', () => {
83+
const metafile = createMetafile({
84+
'browser/main.js': ['src/main.ts'],
85+
'browser/polyfills.js': ['src/polyfills.ts'],
86+
});
87+
88+
const result = filterMetafile(metafile, () => false);
89+
90+
expect(Object.keys(result.outputs)).toEqual([]);
91+
expect(Object.keys(result.inputs)).toEqual([]);
92+
});
93+
94+
it('returns all outputs and their referenced inputs when predicate always matches', () => {
95+
const metafile = createMetafile({
96+
'browser/main.js': ['src/main.ts'],
97+
'browser/polyfills.js': ['src/polyfills.ts'],
98+
});
99+
100+
const result = filterMetafile(metafile, () => true);
101+
102+
expect(Object.keys(result.outputs).length).toBe(2);
103+
expect(Object.keys(result.inputs)).toEqual(
104+
jasmine.arrayContaining(['src/main.ts', 'src/polyfills.ts']),
105+
);
106+
});
107+
108+
it('deduplicates inputs referenced by multiple matching outputs', () => {
109+
const metafile = createMetafile({
110+
'browser/main.js': ['src/shared.ts', 'src/main.ts'],
111+
'browser/polyfills.js': ['src/shared.ts', 'src/polyfills.ts'],
112+
});
113+
114+
const result = filterMetafile(metafile, () => true);
115+
116+
const inputKeys = Object.keys(result.inputs);
117+
const sharedOccurrences = inputKeys.filter((k) => k === 'src/shared.ts').length;
118+
expect(sharedOccurrences).toBe(1);
119+
});
120+
121+
it('does not mutate the original metafile', () => {
122+
const metafile = createMetafile({
123+
'browser/main.js': ['src/main.ts'],
124+
'server/server.mjs': ['src/server.ts'],
125+
});
126+
const originalOutputCount = Object.keys(metafile.outputs).length;
127+
const originalInputCount = Object.keys(metafile.inputs).length;
128+
129+
filterMetafile(metafile, (path) => path.startsWith('browser/'));
130+
131+
expect(Object.keys(metafile.outputs).length).toBe(originalOutputCount);
132+
expect(Object.keys(metafile.inputs).length).toBe(originalInputCount);
133+
});
134+
});

0 commit comments

Comments
 (0)