angular-cn/packages/compiler-cli/test/ngtsc/incremental_error_spec.ts

293 lines
8.5 KiB
TypeScript

/**
* @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 {absoluteFrom as _} from '../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
import {loadStandardTestFiles} from '../helpers/src/mock_file_loading';
import {NgtscTestEnvironment} from './env';
const testFiles = loadStandardTestFiles();
runInEachFileSystem(() => {
describe('ngtsc incremental compilation with errors', () => {
let env !: NgtscTestEnvironment;
beforeEach(() => {
env = NgtscTestEnvironment.setup(testFiles);
env.enableMultipleCompilations();
env.tsconfig();
// This file is part of the program, but not referenced by anything else. It can be used by
// each test to verify that it isn't re-emitted after incremental builds.
env.write('unrelated.ts', `
export class Unrelated {}
`);
});
function expectToHaveWritten(files: string[]): void {
const set = env.getFilesWrittenSinceLastFlush();
for (const file of files) {
expect(set).toContain(file);
expect(set).toContain(file.replace(/\.js$/, '.d.ts'));
}
// Validate that 2x the size of `files` have been written (one .d.ts, one .js) and no more.
expect(set.size).toBe(2 * files.length);
}
it('should handle an error in an unrelated file', () => {
env.write('cmp.ts', `
import {Component} from '@angular/core';
@Component({selector: 'test-cmp', template: '...'})
export class TestCmp {}
`);
env.write('other.ts', `
export class Other {}
`);
// Start with a clean compilation.
env.driveMain();
// Introduce the error.
env.write('other.ts', `
export class Other // missing braces
`);
env.flushWrittenFileTracking();
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].file !.fileName).toBe(_('/other.ts'));
expectToHaveWritten([]);
// Remove the error. /other.js should now be emitted again.
env.write('other.ts', `
export class Other {}
`);
env.flushWrittenFileTracking();
env.driveMain();
expectToHaveWritten(['/other.js']);
});
it('should recover from an error in a component\'s metadata', () => {
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({selector: 'test-cmp', template: '...'})
export class TestCmp {}
`);
// Start with a clean compilation.
env.driveMain();
// Introduce the error.
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({selector: 'test-cmp', template: ...})
export class TestCmp {}
`);
env.flushWrittenFileTracking();
const diags = env.driveDiagnostics();
expect(diags.length).toBeGreaterThan(0);
expect(env.getFilesWrittenSinceLastFlush()).not.toContain(_('/test.js'));
// Clear the error and verify that the compiler now emits test.js again.
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({selector: 'test-cmp', template: '...'})
export class TestCmp {}
`);
env.flushWrittenFileTracking();
env.driveMain();
expectToHaveWritten(['/test.js']);
});
it('should recover from an error in a component that is part of a module', () => {
// In this test, there are two components, TestCmp and TargetCmp, that are part of the same
// NgModule. TestCmp is broken in an incremental build and then fixed, and the test verifies
// that TargetCmp is re-emitted.
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({selector: 'test-cmp', template: '...'})
export class TestCmp {}
`);
env.write('target.ts', `
import {Component} from '@angular/core';
@Component({selector: 'target-cmp', template: '<test-cmp></test-cmp>'})
export class TargetCmp {}
`);
env.write('module.ts', `
import {NgModule} from '@angular/core';
import {TargetCmp} from './target';
import {TestCmp} from './test';
@NgModule({
declarations: [TestCmp, TargetCmp],
})
export class Module {}
`);
// Start with a clean compilation.
env.driveMain();
// Introduce the syntactic error.
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({selector: ..., template: '...'}) // ... is not valid syntax
export class TestCmp {}
`);
env.flushWrittenFileTracking();
const diags = env.driveDiagnostics();
expect(diags.length).toBeGreaterThan(0);
expectToHaveWritten([]);
// Clear the error and trigger the rebuild.
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({selector: 'test-cmp', template: '...'})
export class TestCmp {}
`);
env.flushWrittenFileTracking();
env.driveMain();
expectToHaveWritten([
// The file which had the error should have been emitted, of course.
'/test.js',
// Because TestCmp belongs to a module, the module's file should also have been
// re-emitted.
'/module.js',
// Because TargetCmp also belongs to the same module, it should be re-emitted since
// TestCmp's elector may have changed.
'/target.js',
]);
});
it('should recover from an error even across multiple NgModules', () => {
// This test is a variation on the above. Two components (CmpA and CmpB) exist in an NgModule,
// which indirectly imports a LibModule (via another NgModule in the middle). The test is
// designed to verify that CmpA and CmpB are re-emitted if somewhere upstream in the NgModule
// graph, an error is fixed. To check this, LibModule is broken and then fixed in incremental
// build steps.
env.write('a.ts', `
import {Component} from '@angular/core';
@Component({selector: 'test-cmp', template: '...'})
export class CmpA {}
`);
env.write('b.ts', `
import {Component} from '@angular/core';
@Component({selector: 'target-cmp', template: '...'})
export class CmpB {}
`);
env.write('module.ts', `
import {NgModule} from '@angular/core';
import {LibModule} from './lib';
import {CmpA} from './a';
import {CmpB} from './b';
@NgModule({
imports: [LibModule],
exports: [LibModule],
})
export class IndirectModule {}
@NgModule({
declarations: [CmpA, CmpB],
imports: [IndirectModule],
})
export class Module {}
`);
env.write('lib.ts', `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'lib-cmp',
template: '...',
})
export class LibCmp {}
@NgModule({
declarations: [LibCmp],
exports: [LibCmp],
})
export class LibModule {}
`);
// Start with a clean compilation.
env.driveMain();
// Introduce the error in LibModule
env.write('lib.ts', `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'lib-cmp',
template: '...',
})
export class LibCmp {}
@NgModule({
declarations: [LibCmp],
exports: [LibCmp],
})
export class LibModule // missing braces
`);
env.flushWrittenFileTracking();
// env.driveMain();
const diags = env.driveDiagnostics();
expect(diags.length).toBeGreaterThan(0);
expectToHaveWritten([]);
// Clear the error and recompile.
env.write('lib.ts', `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'lib-cmp',
template: '...',
})
export class LibCmp {}
@NgModule({
declarations: [LibCmp],
exports: [LibCmp],
})
export class LibModule {}
`);
env.flushWrittenFileTracking();
env.driveMain();
expectToHaveWritten([
// Both CmpA and CmpB should be re-emitted.
'/a.js',
'/b.js',
// So should the module itself.
'/module.js',
// And of course, the file with the error.
'/lib.js',
]);
});
});
});