test(@angular-devkit/build-angular): move output-hashing test to new test harness

This commit is contained in:
Alan Agius 2021-01-27 09:16:29 +01:00
parent a86ea3f154
commit 54f44bc49a
3 changed files with 182 additions and 195 deletions

View File

@ -1,195 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { Architect } from '@angular-devkit/architect';
import { normalize } from '@angular-devkit/core';
import {
browserBuild,
createArchitect,
host,
lazyModuleFiles,
lazyModuleFnImport,
} from '../../test-utils';
describe('Browser Builder output hashing', () => {
const target = { project: 'app', target: 'build' };
let architect: Architect;
beforeEach(async () => {
await host.initialize().toPromise();
architect = (await createArchitect(host.root())).architect;
});
afterEach(async () => host.restore().toPromise());
it('updates hash as content changes', async () => {
const OUTPUT_RE = /(main|styles|lazy\.module)\.([a-z0-9]+)\.(chunk|bundle)\.(js|css)$/;
function generateFileHashMap(): Map<string, string> {
const hashes = new Map<string, string>();
host
.scopedSync()
.list(normalize('./dist'))
.forEach(name => {
const matches = name.match(OUTPUT_RE);
if (matches) {
const [, module, hash] = matches;
hashes.set(module, hash);
}
});
return hashes;
}
function validateHashes(
oldHashes: Map<string, string>,
newHashes: Map<string, string>,
shouldChange: Array<string>,
): void {
newHashes.forEach((hash, module) => {
if (hash == oldHashes.get(module)) {
if (shouldChange.includes(module)) {
throw new Error(
`Module "${module}" did not change hash (${hash}), but was expected to.`,
);
}
} else if (!shouldChange.includes(module)) {
throw new Error(`Module "${module}" changed hash (${hash}), but was not expected to.`);
}
});
}
let oldHashes: Map<string, string>;
let newHashes: Map<string, string>;
host.writeMultipleFiles(lazyModuleFiles);
host.writeMultipleFiles(lazyModuleFnImport);
const overrides = { outputHashing: 'all', extractCss: true };
// We must do several builds instead of a single one in watch mode, so that the output
// path is deleted on each run and only contains the most recent files.
await browserBuild(architect, host, target, overrides);
// Save the current hashes.
oldHashes = generateFileHashMap();
host.writeMultipleFiles(lazyModuleFiles);
host.writeMultipleFiles(lazyModuleFnImport);
await browserBuild(architect, host, target, overrides);
newHashes = generateFileHashMap();
validateHashes(oldHashes, newHashes, []);
oldHashes = newHashes;
host.writeMultipleFiles({ 'src/styles.css': 'body { background: blue; }' });
// Style hash should change.
await browserBuild(architect, host, target, overrides);
newHashes = generateFileHashMap();
validateHashes(oldHashes, newHashes, ['styles']);
oldHashes = newHashes;
host.writeMultipleFiles({ 'src/app/app.component.css': 'h1 { margin: 10px; }' });
// Main hash should change, since inline styles go in the main bundle.
await browserBuild(architect, host, target, overrides);
newHashes = generateFileHashMap();
validateHashes(oldHashes, newHashes, ['main']);
oldHashes = newHashes;
host.appendToFile('src/app/lazy/lazy.module.ts', `console.log(1);`);
// Lazy loaded bundle should change, and so should inline.
await browserBuild(architect, host, target, overrides);
newHashes = generateFileHashMap();
validateHashes(oldHashes, newHashes, ['lazy.module']);
oldHashes = newHashes;
host.appendToFile('src/main.ts', '');
// Nothing should have changed.
await browserBuild(architect, host, target, overrides);
newHashes = generateFileHashMap();
validateHashes(oldHashes, newHashes, []);
});
it('supports options', async () => {
host.writeMultipleFiles({ 'src/styles.css': `h1 { background: url('./spectrum.png')}` });
host.writeMultipleFiles(lazyModuleFiles);
host.writeMultipleFiles(lazyModuleFnImport);
// We must do several builds instead of a single one in watch mode, so that the output
// path is deleted on each run and only contains the most recent files.
// 'all' should hash everything.
await browserBuild(architect, host, target, { outputHashing: 'all', extractCss: true });
expect(host.fileMatchExists('dist', /runtime\.[0-9a-f]{20}\.js/)).toBeTruthy();
expect(host.fileMatchExists('dist', /main\.[0-9a-f]{20}\.js/)).toBeTruthy();
expect(host.fileMatchExists('dist', /polyfills\.[0-9a-f]{20}\.js/)).toBeTruthy();
expect(host.fileMatchExists('dist', /vendor\.[0-9a-f]{20}\.js/)).toBeTruthy();
expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.css/)).toBeTruthy();
expect(host.fileMatchExists('dist', /spectrum\.[0-9a-f]{20}\.png/)).toBeTruthy();
// 'none' should hash nothing.
await browserBuild(architect, host, target, { outputHashing: 'none', extractCss: true });
expect(host.fileMatchExists('dist', /runtime\.[0-9a-f]{20}\.js/)).toBeFalsy();
expect(host.fileMatchExists('dist', /main\.[0-9a-f]{20}\.js/)).toBeFalsy();
expect(host.fileMatchExists('dist', /polyfills\.[0-9a-f]{20}\.js/)).toBeFalsy();
expect(host.fileMatchExists('dist', /vendor\.[0-9a-f]{20}\.js/)).toBeFalsy();
expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.css/)).toBeFalsy();
expect(host.fileMatchExists('dist', /spectrum\.[0-9a-f]{20}\.png/)).toBeFalsy();
// 'media' should hash css resources only.
await browserBuild(architect, host, target, { outputHashing: 'media', extractCss: true });
expect(host.fileMatchExists('dist', /runtime\.[0-9a-f]{20}\.js/)).toBeFalsy();
expect(host.fileMatchExists('dist', /main\.[0-9a-f]{20}\.js/)).toBeFalsy();
expect(host.fileMatchExists('dist', /polyfills\.[0-9a-f]{20}\.js/)).toBeFalsy();
expect(host.fileMatchExists('dist', /vendor\.[0-9a-f]{20}\.js/)).toBeFalsy();
expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.css/)).toBeFalsy();
expect(host.fileMatchExists('dist', /spectrum\.[0-9a-f]{20}\.png/)).toBeTruthy();
// 'bundles' should hash bundles only.
await browserBuild(architect, host, target, { outputHashing: 'bundles', extractCss: true });
expect(host.fileMatchExists('dist', /runtime\.[0-9a-f]{20}\.js/)).toBeTruthy();
expect(host.fileMatchExists('dist', /main\.[0-9a-f]{20}\.js/)).toBeTruthy();
expect(host.fileMatchExists('dist', /polyfills\.[0-9a-f]{20}\.js/)).toBeTruthy();
expect(host.fileMatchExists('dist', /vendor\.[0-9a-f]{20}\.js/)).toBeTruthy();
expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.css/)).toBeTruthy();
expect(host.fileMatchExists('dist', /spectrum\.[0-9a-f]{20}\.png/)).toBeFalsy();
});
it('does not hash non injected styles', async () => {
const overrides = {
outputHashing: 'all',
extractCss: true,
styles: [{ input: 'src/styles.css', inject: false }],
};
await browserBuild(architect, host, target, overrides);
expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.js/)).toBeFalsy();
expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.js.map/)).toBeFalsy();
expect(host.scopedSync().exists(normalize('dist/styles.css'))).toBe(true);
expect(host.scopedSync().exists(normalize('dist/styles.css.map'))).toBe(true);
});
it('does not hash non injected styles when optimization is enabled', async () => {
host.writeMultipleFiles({ 'src/styles.css': 'body { background: blue; }' });
const overrides = {
outputHashing: 'all',
extractCss: true,
optimization: true,
styles: [{ input: 'src/styles.css', inject: false }],
};
await browserBuild(architect, host, target, overrides);
expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.js/)).toBeFalsy();
expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.js.map/)).toBeFalsy();
expect(host.scopedSync().exists(normalize('dist/styles.css'))).toBe(true);
expect(host.scopedSync().exists(normalize('dist/styles.css.map'))).toBe(true);
});
});

View File

@ -0,0 +1,176 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { buildWebpackBrowser } from '../../index';
import { OutputHashing } from '../../schema';
import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';
describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
describe('Option: "outputHashing"', () => {
beforeEach(async () => {
// Application code is not needed for asset tests
await harness.writeFile('src/main.ts', '');
});
it('hashes all filenames when set to "all"', async () => {
await harness.writeFile(
'src/styles.css',
`h1 { background: url('./spectrum.png')}`,
);
harness.useTarget('build', {
...BASE_OPTIONS,
styles: ['src/styles.css'],
outputHashing: OutputHashing.All,
});
const { result } = await harness.executeOnce();
expect(result?.success).toBe(true);
expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeTrue();
expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeTrue();
expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeTrue();
expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeTrue();
expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeTrue();
});
it(`doesn't hash any filenames when not set`, async () => {
await harness.writeFile(
'src/styles.css',
`h1 { background: url('./spectrum.png')}`,
);
harness.useTarget('build', {
...BASE_OPTIONS,
styles: ['src/styles.css'],
});
const { result } = await harness.executeOnce();
expect(result?.success).toBe(true);
expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeFalse();
expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeFalse();
expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeFalse();
expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeFalse();
expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeFalse();
});
it(`doesn't hash any filenames when set to "none"`, async () => {
await harness.writeFile(
'src/styles.css',
`h1 { background: url('./spectrum.png')}`,
);
harness.useTarget('build', {
...BASE_OPTIONS,
styles: ['src/styles.css'],
outputHashing: OutputHashing.None,
});
const { result } = await harness.executeOnce();
expect(result?.success).toBe(true);
expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeFalse();
expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeFalse();
expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeFalse();
expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeFalse();
expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeFalse();
});
it(`hashes CSS resources filenames only when set to "media"`, async () => {
await harness.writeFile(
'src/styles.css',
`h1 { background: url('./spectrum.png')}`,
);
harness.useTarget('build', {
...BASE_OPTIONS,
styles: ['src/styles.css'],
outputHashing: OutputHashing.Media,
});
const { result } = await harness.executeOnce();
expect(result?.success).toBe(true);
expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeFalse();
expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeFalse();
expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeFalse();
expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeFalse();
expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeTrue();
});
it(`hashes bundles filenames only when set to "bundles"`, async () => {
await harness.writeFile(
'src/styles.css',
`h1 { background: url('./spectrum.png')}`,
);
harness.useTarget('build', {
...BASE_OPTIONS,
styles: ['src/styles.css'],
outputHashing: OutputHashing.Bundles,
});
const { result } = await harness.executeOnce();
expect(result?.success).toBe(true);
expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeTrue();
expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeTrue();
expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeTrue();
expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeTrue();
expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeFalse();
});
it('does not hash non injected styles', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
outputHashing: OutputHashing.All,
styles: [{
input: 'src/styles.css',
inject: false,
}],
});
const { result } = await harness.executeOnce();
expect(result?.success).toBe(true);
expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.js$/)).toBeFalse();
expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.js.map$/)).toBeFalse();
harness.expectFile('dist/styles.css').toExist();
harness.expectFile('dist/styles.css.map').toExist();
});
it('does not override different files which has the same filenames when hashing is "none"', async () => {
await harness.writeFiles({
'src/styles.css': `
h1 { background: url('./test.svg')}
h2 { background: url('./small/test.svg')}
`,
'./src/test.svg': `<svg xmlns="http://www.w3.org/2000/svg">
<text x="20" y="20" font-size="20" fill="red">Hello World</text>
</svg>`,
'./src/small/test.svg': `<svg xmlns="http://www.w3.org/2000/svg">
<text x="10" y="10" font-size="10" fill="red">Hello World</text>
</svg>`,
});
harness.useTarget('build', {
...BASE_OPTIONS,
styles: ['src/styles.css'],
outputHashing: OutputHashing.None,
});
const { result } = await harness.executeOnce();
expect(result?.success).toBe(true);
harness.expectFile('dist/test.svg').toExist();
harness.expectFile('dist/small-test.svg').toExist();
});
});
});

View File

@ -312,6 +312,12 @@ export class BuilderHarness<T> {
return this.host.scopedSync().exists(normalize(path)); return this.host.scopedSync().exists(normalize(path));
} }
hasFileMatch(directory: string, pattern: RegExp): boolean {
return this.host.scopedSync()
.list(normalize(directory))
.some(name => pattern.test(name));
}
readFile(path: string): string { readFile(path: string): string {
const content = this.host.scopedSync().read(normalize(path)); const content = this.host.scopedSync().read(normalize(path));