mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
Updates the template type checker to check the options of the `viewport` trigger against `IntersectionObserver`. PR Close #64130
1658 lines
47 KiB
TypeScript
1658 lines
47 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC 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.dev/license
|
|
*/
|
|
|
|
import {ErrorCode, ngErrorCode} from '../../src/ngtsc/diagnostics';
|
|
|
|
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
|
|
import {loadStandardTestFiles} from '../../src/ngtsc/testing';
|
|
|
|
import {NgtscTestEnvironment} from './env';
|
|
|
|
const testFiles = loadStandardTestFiles();
|
|
|
|
runInEachFileSystem(() => {
|
|
describe('ngtsc @defer block', () => {
|
|
let env!: NgtscTestEnvironment;
|
|
|
|
beforeEach(() => {
|
|
env = NgtscTestEnvironment.setup(testFiles);
|
|
env.tsconfig();
|
|
});
|
|
|
|
it('should handle deferred blocks', () => {
|
|
env.write(
|
|
'cmp-a.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'cmp-a',
|
|
template: 'CmpA!'
|
|
})
|
|
export class CmpA {}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
import { CmpA } from './cmp-a';
|
|
|
|
@Component({
|
|
selector: 'local-dep',
|
|
template: 'Local dependency',
|
|
})
|
|
export class LocalDep {}
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [CmpA, LocalDep],
|
|
template: \`
|
|
@defer {
|
|
<cmp-a />
|
|
<local-dep />
|
|
}
|
|
\`,
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
|
|
expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)');
|
|
expect(jsContents).toContain('() => [import("./cmp-a").then(m => m.CmpA), LocalDep]');
|
|
|
|
// The `CmpA` symbol wasn't referenced elsewhere, so it can be defer-loaded
|
|
// via dynamic imports and an original import can be removed.
|
|
expect(jsContents).not.toContain('import { CmpA }');
|
|
});
|
|
|
|
it('should include timer scheduler function when `after` or `minimum` parameters are used', () => {
|
|
env.write(
|
|
'cmp-a.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'cmp-a',
|
|
template: 'CmpA!'
|
|
})
|
|
export class CmpA {}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
import { CmpA } from './cmp-a';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [CmpA],
|
|
template: \`
|
|
@defer {
|
|
<cmp-a />
|
|
} @loading (after 500ms; minimum 300ms) {
|
|
Loading...
|
|
}
|
|
\`,
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
expect(jsContents).toContain(
|
|
'ɵɵdefer(2, 0, TestCmp_Defer_2_DepsFn, 1, null, null, 0, null, i0.ɵɵdeferEnableTimerScheduling)',
|
|
);
|
|
});
|
|
|
|
describe('imports', () => {
|
|
it('should retain regular imports when symbol is eagerly referenced', () => {
|
|
env.write(
|
|
'cmp-a.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'cmp-a',
|
|
template: 'CmpA!'
|
|
})
|
|
export class CmpA {}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
import { CmpA } from './cmp-a';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [CmpA],
|
|
template: \`
|
|
@defer {
|
|
<cmp-a />
|
|
}
|
|
\`,
|
|
})
|
|
export class TestCmp {
|
|
constructor() {
|
|
// This line retains the regular import of CmpA,
|
|
// since it's eagerly referenced in the code.
|
|
console.log(CmpA);
|
|
}
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
|
|
expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)');
|
|
|
|
// The dependency function doesn't have a dynamic import, because `CmpA`
|
|
// was eagerly referenced in component's code, thus regular import can not be removed.
|
|
expect(jsContents).toContain('() => [CmpA]');
|
|
expect(jsContents).toContain('import { CmpA }');
|
|
});
|
|
|
|
it('should retain regular imports when one of the symbols is eagerly referenced', () => {
|
|
env.write(
|
|
'cmp-a.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'cmp-a',
|
|
template: 'CmpA!'
|
|
})
|
|
export class CmpA {}
|
|
|
|
@Component({
|
|
selector: 'cmp-b',
|
|
template: 'CmpB!'
|
|
})
|
|
export class CmpB {}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
import { CmpA, CmpB } from './cmp-a';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [CmpA, CmpB],
|
|
template: \`
|
|
@defer {
|
|
<cmp-a />
|
|
<cmp-b />
|
|
}
|
|
\`,
|
|
})
|
|
export class TestCmp {
|
|
constructor() {
|
|
// This line retains the regular import of CmpA,
|
|
// since it's eagerly referenced in the code.
|
|
console.log(CmpA);
|
|
}
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
|
|
expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)');
|
|
|
|
// The dependency function doesn't have a dynamic import, because `CmpA`
|
|
// was eagerly referenced in component's code, thus regular import can not be removed.
|
|
// This also affects `CmpB`, since it was extracted from the same import.
|
|
expect(jsContents).toContain('() => [CmpA, CmpB]');
|
|
expect(jsContents).toContain('import { CmpA, CmpB }');
|
|
});
|
|
|
|
it('should drop regular imports when none of the symbols are eagerly referenced', () => {
|
|
env.write(
|
|
'cmp-a.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'cmp-a',
|
|
template: 'CmpA!'
|
|
})
|
|
export class CmpA {}
|
|
|
|
@Component({
|
|
selector: 'cmp-b',
|
|
template: 'CmpB!'
|
|
})
|
|
export class CmpB {}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
import { CmpA, CmpB } from './cmp-a';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [CmpA, CmpB],
|
|
template: \`
|
|
@defer {
|
|
<cmp-a />
|
|
<cmp-b />
|
|
}
|
|
\`,
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
|
|
expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)');
|
|
|
|
// Both `CmpA` and `CmpB` were used inside the defer block and were not
|
|
// referenced elsewhere, so we generate dynamic imports and drop a regular one.
|
|
expect(jsContents).toContain(
|
|
'() => [' +
|
|
'import("./cmp-a").then(m => m.CmpA), ' +
|
|
'import("./cmp-a").then(m => m.CmpB)]',
|
|
);
|
|
expect(jsContents).not.toContain('import { CmpA, CmpB }');
|
|
});
|
|
|
|
it('should lazy-load dependency referenced with a fowrardRef', () => {
|
|
env.write(
|
|
'cmp-a.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'cmp-a',
|
|
template: 'CmpA!'
|
|
})
|
|
export class CmpA {}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import { Component, forwardRef } from '@angular/core';
|
|
import { CmpA } from './cmp-a';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [forwardRef(() => CmpA)],
|
|
template: \`
|
|
@defer {
|
|
<cmp-a />
|
|
}
|
|
\`,
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
|
|
expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)');
|
|
expect(jsContents).toContain('() => [import("./cmp-a").then(m => m.CmpA)]');
|
|
|
|
// The `CmpA` symbol wasn't referenced elsewhere, so it can be defer-loaded
|
|
// via dynamic imports and an original import can be removed.
|
|
expect(jsContents).not.toContain('import { CmpA }');
|
|
});
|
|
|
|
it('should drop imports when one is deferrable and the rest are type-only imports', () => {
|
|
env.write(
|
|
'cmp-a.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
|
|
export class Foo {}
|
|
|
|
@Component({
|
|
selector: 'cmp-a',
|
|
template: 'CmpA!'
|
|
})
|
|
export class CmpA {}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
import { CmpA, type Foo } from './cmp-a';
|
|
|
|
export const foo: Foo = {};
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [CmpA],
|
|
template: \`
|
|
@defer {
|
|
<cmp-a />
|
|
}
|
|
\`,
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
|
|
expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)');
|
|
expect(jsContents).toContain('() => [import("./cmp-a").then(m => m.CmpA)]');
|
|
expect(jsContents).not.toContain('import { CmpA }');
|
|
});
|
|
|
|
it('should drop multiple imports to the same file when one is deferrable and the other has a single type-only element', () => {
|
|
env.write(
|
|
'cmp-a.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
|
|
export class Foo {}
|
|
|
|
@Component({
|
|
selector: 'cmp-a',
|
|
template: 'CmpA!'
|
|
})
|
|
export class CmpA {}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
import { CmpA } from './cmp-a';
|
|
import { type Foo } from './cmp-a';
|
|
|
|
export const foo: Foo = {};
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [CmpA],
|
|
template: \`
|
|
@defer {
|
|
<cmp-a />
|
|
}
|
|
\`,
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
|
|
expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)');
|
|
expect(jsContents).toContain('() => [import("./cmp-a").then(m => m.CmpA)]');
|
|
expect(jsContents).not.toContain('import { CmpA }');
|
|
});
|
|
|
|
it('should drop multiple imports to the same file when one is deferrable and the other is type-only at the declaration level', () => {
|
|
env.write(
|
|
'cmp-a.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
|
|
export class Foo {}
|
|
|
|
@Component({
|
|
selector: 'cmp-a',
|
|
template: 'CmpA!'
|
|
})
|
|
export class CmpA {}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
import { CmpA } from './cmp-a';
|
|
import type { Foo, CmpA as CmpAlias } from './cmp-a';
|
|
|
|
export const foo: Foo|CmpAlias = {};
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [CmpA],
|
|
template: \`
|
|
@defer {
|
|
<cmp-a />
|
|
}
|
|
\`,
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
|
|
expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)');
|
|
expect(jsContents).toContain('() => [import("./cmp-a").then(m => m.CmpA)]');
|
|
expect(jsContents).not.toContain('import { CmpA }');
|
|
});
|
|
|
|
it('should drop multiple imports to the same file when one is deferrable and the other is a type-only import of all symbols', () => {
|
|
env.write(
|
|
'cmp-a.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
|
|
export class Foo {}
|
|
|
|
@Component({
|
|
selector: 'cmp-a',
|
|
template: 'CmpA!'
|
|
})
|
|
export class CmpA {}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
import { CmpA } from './cmp-a';
|
|
import type * as allCmpA from './cmp-a';
|
|
|
|
export const foo: allCmpA.Foo|allCmpA.CmpA = {};
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [CmpA],
|
|
template: \`
|
|
@defer {
|
|
<cmp-a />
|
|
}
|
|
\`,
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
|
|
expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)');
|
|
expect(jsContents).toContain('() => [import("./cmp-a").then(m => m.CmpA)]');
|
|
expect(jsContents).not.toContain('import { CmpA }');
|
|
});
|
|
|
|
it('should drop multiple imports of deferrable symbols from the same file', () => {
|
|
env.write(
|
|
'cmps.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'cmp-a',
|
|
template: 'CmpA!'
|
|
})
|
|
export class CmpA {}
|
|
|
|
@Component({
|
|
selector: 'cmp-b',
|
|
template: 'CmpB!'
|
|
})
|
|
export class CmpB {}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
import { CmpA } from './cmps';
|
|
import { CmpB } from './cmps';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [CmpA, CmpB],
|
|
template: \`
|
|
@defer {
|
|
<cmp-a />
|
|
<cmp-b />
|
|
}
|
|
\`,
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
|
|
expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)');
|
|
expect(jsContents).toContain(
|
|
'() => [import("./cmps").then(m => m.CmpA), import("./cmps").then(m => m.CmpB)]',
|
|
);
|
|
expect(jsContents).not.toContain('import { CmpA }');
|
|
expect(jsContents).not.toContain('import { CmpB }');
|
|
});
|
|
|
|
it('should handle deferred dependencies imported through a default import', () => {
|
|
env.write(
|
|
'cmp-a.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
@Component({
|
|
selector: 'cmp-a',
|
|
template: 'CmpA!'
|
|
})
|
|
export default class CmpA {}
|
|
`,
|
|
);
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
import CmpA from './cmp-a';
|
|
@Component({
|
|
selector: 'local-dep',
|
|
template: 'Local dependency',
|
|
})
|
|
export class LocalDep {}
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [CmpA, LocalDep],
|
|
template: \`
|
|
@defer {
|
|
<cmp-a />
|
|
<local-dep />
|
|
}
|
|
\`,
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
const jsContents = env.getContents('test.js');
|
|
|
|
expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)');
|
|
expect(jsContents).toContain(
|
|
'const TestCmp_Defer_1_DepsFn = () => [import("./cmp-a").then(m => m.default), LocalDep];',
|
|
);
|
|
expect(jsContents).toContain(
|
|
'i0.ɵsetClassMetadataAsync(TestCmp, () => [import("./cmp-a").then(m => m.default)]',
|
|
);
|
|
// The `CmpA` symbol wasn't referenced elsewhere, so it can be defer-loaded
|
|
// via dynamic imports and an original import can be removed.
|
|
expect(jsContents).not.toContain('import CmpA');
|
|
});
|
|
|
|
it('should defer symbol that is used only in types', () => {
|
|
env.write(
|
|
'cmp.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'cmp',
|
|
template: 'Cmp!'
|
|
})
|
|
export class Cmp {}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import { Component, viewChild } from '@angular/core';
|
|
import { Cmp } from './cmp';
|
|
|
|
const topLevelConst: Cmp = null!;
|
|
|
|
@Component({
|
|
imports: [Cmp],
|
|
template: \`
|
|
@defer {
|
|
<cmp #ref/>
|
|
}
|
|
\`,
|
|
})
|
|
export class TestCmp {
|
|
query = viewChild<Cmp>('ref');
|
|
asType: Cmp;
|
|
inlineType: {foo: Cmp};
|
|
unionType: string | Cmp | number;
|
|
constructor(param: Cmp) {}
|
|
inMethod(param: Cmp): Cmp {
|
|
let localVar: Cmp | null = null;
|
|
return localVar!;
|
|
}
|
|
}
|
|
|
|
function inFunction(param: Cmp): Cmp {
|
|
return null!;
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)');
|
|
expect(jsContents).toContain('() => [import("./cmp").then(m => m.Cmp)]');
|
|
expect(jsContents).not.toContain('import { Cmp }');
|
|
});
|
|
|
|
it('should retain symbols used in types and eagerly', () => {
|
|
env.write(
|
|
'cmp.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'cmp',
|
|
template: 'Cmp!'
|
|
})
|
|
export class Cmp {}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import { Component, viewChild } from '@angular/core';
|
|
import { Cmp } from './cmp';
|
|
|
|
@Component({
|
|
imports: [Cmp],
|
|
template: \`
|
|
@defer {
|
|
<cmp #ref/>
|
|
}
|
|
\`,
|
|
})
|
|
export class TestCmp {
|
|
// Type-only reference
|
|
query = viewChild<Cmp>('ref');
|
|
|
|
// Directy reference
|
|
otherQuery = viewChild(Cmp);
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)');
|
|
expect(jsContents).toContain('() => [Cmp]');
|
|
expect(jsContents).toContain('import { Cmp }');
|
|
});
|
|
});
|
|
|
|
it('should detect pipe used in the `when` trigger as an eager dependency', () => {
|
|
env.write(
|
|
'test-pipe.ts',
|
|
`
|
|
import { Pipe } from '@angular/core';
|
|
|
|
@Pipe({name: 'test'})
|
|
export class TestPipe {
|
|
transform() {
|
|
return 1;
|
|
}
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
import { TestPipe } from './test-pipe';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [TestPipe],
|
|
template: '@defer (when 1 | test) { hello }',
|
|
})
|
|
export class TestCmp {
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
|
|
expect(jsContents).toContain('dependencies: [TestPipe]');
|
|
});
|
|
|
|
it('should detect pipe used in the `prefetch when` trigger as an eager dependency', () => {
|
|
env.write(
|
|
'test-pipe.ts',
|
|
`
|
|
import { Pipe } from '@angular/core';
|
|
|
|
@Pipe({name: 'test'})
|
|
export class TestPipe {
|
|
transform() {
|
|
return 1;
|
|
}
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
import { TestPipe } from './test-pipe';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [TestPipe],
|
|
template: '@defer (when 1 | test) { hello }',
|
|
})
|
|
export class TestCmp {
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
|
|
expect(jsContents).toContain('dependencies: [TestPipe]');
|
|
});
|
|
|
|
it('should detect pipe used both in a trigger and the deferred content as eager', () => {
|
|
env.write(
|
|
'test-pipe.ts',
|
|
`
|
|
import { Pipe } from '@angular/core';
|
|
|
|
@Pipe({name: 'test'})
|
|
export class TestPipe {
|
|
transform() {
|
|
return 1;
|
|
}
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import { Component } from '@angular/core';
|
|
import { TestPipe } from './test-pipe';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [TestPipe],
|
|
template: '@defer (when 1 | test) { {{1 | test}} }',
|
|
})
|
|
export class TestCmp {
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
|
|
expect(jsContents).toContain('dependencies: [TestPipe]');
|
|
});
|
|
|
|
describe('@Component.deferredImports', () => {
|
|
beforeEach(() => {
|
|
env.tsconfig({onlyExplicitDeferDependencyImports: true});
|
|
});
|
|
|
|
it('should handle `@Component.deferredImports` field', () => {
|
|
env.write(
|
|
'deferred-a.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
@Component({
|
|
selector: 'deferred-cmp-a',
|
|
template: 'DeferredCmpA contents',
|
|
})
|
|
export class DeferredCmpA {
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'deferred-b.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
@Component({
|
|
selector: 'deferred-cmp-b',
|
|
template: 'DeferredCmpB contents',
|
|
})
|
|
export class DeferredCmpB {
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'pipe-a.ts',
|
|
`
|
|
import {Pipe} from '@angular/core';
|
|
@Pipe({
|
|
name: 'pipea',
|
|
})
|
|
export class PipeA {
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
import {DeferredCmpA} from './deferred-a';
|
|
import {DeferredCmpB} from './deferred-b';
|
|
import {PipeA} from './pipe-a';
|
|
@Component({
|
|
// @ts-ignore
|
|
deferredImports: [DeferredCmpA, DeferredCmpB, PipeA],
|
|
template: \`
|
|
@for (item of items; track item) {
|
|
@if (true) {
|
|
@defer {
|
|
{{ 'Hi!' | pipea }}
|
|
<deferred-cmp-a />
|
|
}
|
|
@defer {
|
|
<deferred-cmp-b />
|
|
}
|
|
}
|
|
}
|
|
\`,
|
|
})
|
|
export class AppCmp {
|
|
items = [1,2,3];
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
const jsContents = env.getContents('test.js');
|
|
|
|
// Expect that all deferrableImports in local compilation mode
|
|
// are located in a single function (since we can't detect in
|
|
// the local mode which components belong to which block).
|
|
expect(jsContents).toContain(
|
|
'const AppCmp_For_1_Conditional_0_Defer_1_DepsFn = () => [' +
|
|
'import("./deferred-a").then(m => m.DeferredCmpA), ' +
|
|
'import("./pipe-a").then(m => m.PipeA)];',
|
|
);
|
|
expect(jsContents).toContain(
|
|
'const AppCmp_For_1_Conditional_0_Defer_4_DepsFn = () => [' +
|
|
'import("./deferred-b").then(m => m.DeferredCmpB)];',
|
|
);
|
|
|
|
// Make sure there are no eager imports present in the output.
|
|
expect(jsContents).not.toContain(`from './deferred-a'`);
|
|
expect(jsContents).not.toContain(`from './deferred-b'`);
|
|
expect(jsContents).not.toContain(`from './pipe-a'`);
|
|
|
|
// There's 2 separate defer instructions due to the two separate defer blocks
|
|
expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmp_For_1_Conditional_0_Defer_1_DepsFn);');
|
|
expect(jsContents).toContain('ɵɵdefer(4, 3, AppCmp_For_1_Conditional_0_Defer_4_DepsFn);');
|
|
|
|
// Expect `ɵsetClassMetadataAsync` to contain dynamic imports too.
|
|
expect(jsContents).toContain(
|
|
'ɵsetClassMetadataAsync(AppCmp, () => [' +
|
|
'import("./deferred-a").then(m => m.DeferredCmpA), ' +
|
|
'import("./pipe-a").then(m => m.PipeA), ' +
|
|
'import("./deferred-b").then(m => m.DeferredCmpB)], ' +
|
|
'(DeferredCmpA, PipeA, DeferredCmpB) => {',
|
|
);
|
|
});
|
|
|
|
it('should handle defer blocks that rely on deps from `deferredImports` and `imports`', () => {
|
|
env.write(
|
|
'eager-a.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'eager-cmp-a',
|
|
template: 'EagerCmpA contents',
|
|
})
|
|
export class EagerCmpA {
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'deferred-a.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'deferred-cmp-a',
|
|
template: 'DeferredCmpA contents',
|
|
})
|
|
export class DeferredCmpA {
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'deferred-b.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'deferred-cmp-b',
|
|
template: 'DeferredCmpB contents',
|
|
})
|
|
export class DeferredCmpB {
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
import {DeferredCmpA} from './deferred-a';
|
|
import {DeferredCmpB} from './deferred-b';
|
|
import {EagerCmpA} from './eager-a';
|
|
|
|
@Component({
|
|
imports: [EagerCmpA],
|
|
// @ts-ignore
|
|
deferredImports: [DeferredCmpA, DeferredCmpB],
|
|
template: \`
|
|
@defer {
|
|
<eager-cmp-a />
|
|
<deferred-cmp-a />
|
|
}
|
|
@defer {
|
|
<eager-cmp-a />
|
|
<deferred-cmp-b />
|
|
}
|
|
\`,
|
|
})
|
|
export class AppCmp {
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
const jsContents = env.getContents('test.js');
|
|
|
|
// Expect that all deferrableImports to become dynamic imports.
|
|
// Other imported symbols remain eager.
|
|
expect(jsContents).toContain(
|
|
'const AppCmp_Defer_1_DepsFn = () => [' +
|
|
'import("./deferred-a").then(m => m.DeferredCmpA), ' +
|
|
'EagerCmpA];',
|
|
);
|
|
expect(jsContents).toContain(
|
|
'const AppCmp_Defer_4_DepsFn = () => [' +
|
|
'import("./deferred-b").then(m => m.DeferredCmpB), ' +
|
|
'EagerCmpA];',
|
|
);
|
|
|
|
// Make sure there are no eager imports present in the output.
|
|
expect(jsContents).not.toContain(`from './deferred-a'`);
|
|
expect(jsContents).not.toContain(`from './deferred-b'`);
|
|
|
|
// Eager dependencies retain their imports.
|
|
expect(jsContents).toContain(`from './eager-a';`);
|
|
|
|
// Defer blocks would have their own dependency functions in full mode.
|
|
expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmp_Defer_1_DepsFn);');
|
|
expect(jsContents).toContain('ɵɵdefer(4, 3, AppCmp_Defer_4_DepsFn);');
|
|
|
|
// Expect `ɵsetClassMetadataAsync` to contain dynamic imports too.
|
|
expect(jsContents).toContain(
|
|
'ɵsetClassMetadataAsync(AppCmp, () => [' +
|
|
'import("./deferred-a").then(m => m.DeferredCmpA), ' +
|
|
'import("./deferred-b").then(m => m.DeferredCmpB)], ' +
|
|
'(DeferredCmpA, DeferredCmpB) => {',
|
|
);
|
|
});
|
|
|
|
describe('error handling', () => {
|
|
it('should produce an error when unsupported type (@Injectable) is used in `deferredImports`', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, Injectable} from '@angular/core';
|
|
@Injectable()
|
|
class MyInjectable {}
|
|
@Component({
|
|
// @ts-ignore
|
|
deferredImports: [MyInjectable],
|
|
template: '',
|
|
})
|
|
export class AppCmp {
|
|
}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_UNKNOWN_DEFERRED_IMPORT));
|
|
});
|
|
|
|
it('should produce an error when unsupported type (@NgModule) is used in `deferredImports`', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, NgModule} from '@angular/core';
|
|
@NgModule()
|
|
class MyModule {}
|
|
@Component({
|
|
// @ts-ignore
|
|
deferredImports: [MyModule],
|
|
template: '',
|
|
})
|
|
export class AppCmp {
|
|
}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_UNKNOWN_DEFERRED_IMPORT));
|
|
});
|
|
|
|
it('should produce an error when components from `deferredImports` are used outside of defer blocks', () => {
|
|
env.write(
|
|
'deferred-a.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'deferred-cmp-a',
|
|
template: 'DeferredCmpA contents',
|
|
})
|
|
export class DeferredCmpA {
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'deferred-b.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'deferred-cmp-b',
|
|
template: 'DeferredCmpB contents',
|
|
})
|
|
export class DeferredCmpB {
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
import {DeferredCmpA} from './deferred-a';
|
|
import {DeferredCmpB} from './deferred-b';
|
|
@Component({
|
|
// @ts-ignore
|
|
deferredImports: [DeferredCmpA, DeferredCmpB],
|
|
template: \`
|
|
<deferred-cmp-a />
|
|
@defer {
|
|
<deferred-cmp-b />
|
|
}
|
|
\`,
|
|
})
|
|
export class AppCmp {
|
|
}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.DEFERRED_DIRECTIVE_USED_EAGERLY));
|
|
});
|
|
|
|
it('should produce an error the same component is referenced in both `deferredImports` and `imports`', () => {
|
|
env.write(
|
|
'deferred-a.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'deferred-cmp-a',
|
|
template: 'DeferredCmpA contents',
|
|
})
|
|
export class DeferredCmpA {
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
import {DeferredCmpA} from './deferred-a';
|
|
|
|
@Component({
|
|
// @ts-ignore
|
|
deferredImports: [DeferredCmpA],
|
|
imports: [DeferredCmpA],
|
|
template: \`
|
|
@defer {
|
|
<deferred-cmp-a />
|
|
}
|
|
\`,
|
|
})
|
|
export class AppCmp {}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.DEFERRED_DEPENDENCY_IMPORTED_EAGERLY));
|
|
});
|
|
|
|
it('should produce an error when pipes from `deferredImports` are used outside of defer blocks', () => {
|
|
env.write(
|
|
'deferred-pipe-a.ts',
|
|
`
|
|
import {Pipe} from '@angular/core';
|
|
@Pipe({name: 'deferredPipeA'})
|
|
export class DeferredPipeA {
|
|
transform() {}
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'deferred-pipe-b.ts',
|
|
`
|
|
import {Pipe} from '@angular/core';
|
|
@Pipe({name: 'deferredPipeB'})
|
|
export class DeferredPipeB {
|
|
transform() {}
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
import {DeferredPipeA} from './deferred-pipe-a';
|
|
import {DeferredPipeB} from './deferred-pipe-b';
|
|
@Component({
|
|
// @ts-ignore
|
|
deferredImports: [DeferredPipeA, DeferredPipeB],
|
|
template: \`
|
|
{{ 'Eager' | deferredPipeA }}
|
|
@defer {
|
|
{{ 'Deferred' | deferredPipeB }}
|
|
}
|
|
\`,
|
|
})
|
|
export class AppCmp {}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.DEFERRED_PIPE_USED_EAGERLY));
|
|
});
|
|
|
|
it('should not produce an error when a deferred block is wrapped in a conditional', () => {
|
|
env.write(
|
|
'deferred-a.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
@Component({
|
|
selector: 'deferred-cmp-a',
|
|
template: 'DeferredCmpA contents',
|
|
})
|
|
export class DeferredCmpA {
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
import {DeferredCmpA} from './deferred-a';
|
|
@Component({
|
|
// @ts-ignore
|
|
deferredImports: [DeferredCmpA],
|
|
template: \`
|
|
@if (true) {
|
|
@if (true) {
|
|
@if (true) {
|
|
@defer {
|
|
<deferred-cmp-a />
|
|
}
|
|
}
|
|
}
|
|
}
|
|
\`,
|
|
})
|
|
export class AppCmp {
|
|
condition = true;
|
|
}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags).toEqual([]);
|
|
});
|
|
|
|
it('should not produce an error when a dependency is wrapped in a condition inside of a deferred block', () => {
|
|
env.write(
|
|
'deferred-a.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
@Component({
|
|
selector: 'deferred-cmp-a',
|
|
template: 'DeferredCmpA contents',
|
|
})
|
|
export class DeferredCmpA {
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
import {DeferredCmpA} from './deferred-a';
|
|
@Component({
|
|
// @ts-ignore
|
|
deferredImports: [DeferredCmpA],
|
|
template: \`
|
|
@defer {
|
|
@if (true) {
|
|
@if (true) {
|
|
@if (true) {
|
|
<deferred-cmp-a />
|
|
}
|
|
}
|
|
}
|
|
}
|
|
\`,
|
|
})
|
|
export class AppCmp {
|
|
condition = true;
|
|
}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags).toEqual([]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('setClassMetadataAsync', () => {
|
|
it('should generate setClassMetadataAsync for components with defer blocks', () => {
|
|
env.write(
|
|
'cmp-a.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'cmp-a',
|
|
template: 'CmpA!'
|
|
})
|
|
export class CmpA {}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
import {CmpA} from './cmp-a';
|
|
|
|
@Component({
|
|
selector: 'local-dep',
|
|
template: 'Local dependency',
|
|
})
|
|
export class LocalDep {}
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [CmpA, LocalDep],
|
|
template: \`
|
|
@defer {
|
|
<cmp-a />
|
|
<local-dep />
|
|
}
|
|
\`,
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
|
|
expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)');
|
|
expect(jsContents).toContain(
|
|
// ngDevMode check is present
|
|
'(() => { (typeof ngDevMode === "undefined" || ngDevMode) && ' +
|
|
// Main `setClassMetadataAsync` call
|
|
'i0.ɵsetClassMetadataAsync(TestCmp, ' +
|
|
// Dependency loading function (note: no local `LocalDep` here)
|
|
'() => [import("./cmp-a").then(m => m.CmpA)], ' +
|
|
// Callback that invokes `setClassMetadata` at the end
|
|
'CmpA => { i0.ɵsetClassMetadata(TestCmp',
|
|
);
|
|
});
|
|
|
|
it(
|
|
'should *not* generate setClassMetadataAsync for components with defer blocks ' +
|
|
'when dependencies are eagerly referenced as well',
|
|
() => {
|
|
env.write(
|
|
'cmp-a.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'cmp-a',
|
|
template: 'CmpA!'
|
|
})
|
|
export class CmpA {}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
import {CmpA} from './cmp-a';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [CmpA],
|
|
template: \`
|
|
@defer {
|
|
<cmp-a />
|
|
}
|
|
\`,
|
|
})
|
|
export class TestCmp {
|
|
constructor() {
|
|
// This eager reference retains 'CmpA' symbol as eager.
|
|
console.log(CmpA);
|
|
}
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
|
|
// Dependency function eagerly references `CmpA`.
|
|
expect(jsContents).toContain('() => [CmpA]');
|
|
|
|
// The `setClassMetadataAsync` wasn't generated, since there are no deferrable
|
|
// symbols.
|
|
expect(jsContents).not.toContain('setClassMetadataAsync');
|
|
|
|
// But the regular `setClassMetadata` is present.
|
|
expect(jsContents).toContain('setClassMetadata');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should generate setClassMetadataAsync for default imports', () => {
|
|
env.write(
|
|
'cmp-a.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'cmp-a',
|
|
template: 'CmpA!'
|
|
})
|
|
export default class CmpA {}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
import CmpA from './cmp-a';
|
|
|
|
@Component({
|
|
selector: 'local-dep',
|
|
template: 'Local dependency',
|
|
})
|
|
export class LocalDep {}
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [CmpA, LocalDep],
|
|
template: \`
|
|
@defer {
|
|
<cmp-a />
|
|
<local-dep />
|
|
}
|
|
\`,
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
|
|
expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)');
|
|
expect(jsContents).toContain(
|
|
// ngDevMode check is present
|
|
'(() => { (typeof ngDevMode === "undefined" || ngDevMode) && ' +
|
|
// Main `setClassMetadataAsync` call
|
|
'i0.ɵsetClassMetadataAsync(TestCmp, ' +
|
|
// Dependency loading function (note: no local `LocalDep` here)
|
|
'() => [import("./cmp-a").then(m => m.default)], ' +
|
|
// Callback that invokes `setClassMetadata` at the end
|
|
'CmpA => { i0.ɵsetClassMetadata(TestCmp',
|
|
);
|
|
});
|
|
|
|
describe('trigger validation', () => {
|
|
it('should report if reference-based trigger has no reference and there is no placeholder block but a hydrate trigger exists', () => {
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({template: '@defer (on viewport; hydrate on immediate) {hello}'})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].messageText).toBe(
|
|
'Trigger with no target can only be placed on an @defer that has a @placeholder block',
|
|
);
|
|
});
|
|
|
|
it('should report if reference-based trigger has no reference and there is no placeholder block but a hydrate trigger exists and it is also viewport', () => {
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({template: '@defer (on viewport; hydrate on viewport) {hello}'})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].messageText).toBe(
|
|
'Trigger with no target can only be placed on an @defer that has a @placeholder block',
|
|
);
|
|
});
|
|
|
|
it('should report if reference-based trigger has no reference and the placeholder is empty', () => {
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({template: '@defer (on viewport) {hello} @placeholder {}'})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].messageText).toBe(
|
|
'Trigger with no target can only be placed on an @defer that has a @placeholder block with exactly one root element node',
|
|
);
|
|
});
|
|
|
|
it('should report if reference-based trigger has no reference and the placeholder with text at the root', () => {
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({template: '@defer (on viewport) {hello} @placeholder {placeholder}'})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].messageText).toBe(
|
|
'Trigger with no target can only be placed on an @defer that has a @placeholder block with exactly one root element node',
|
|
);
|
|
});
|
|
|
|
it('should report if reference-based trigger has no reference and there is no placeholder block', () => {
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({template: '@defer (on viewport) {hello}'})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].messageText).toBe(
|
|
'Trigger with no target can only be placed on an @defer that has a @placeholder block',
|
|
);
|
|
});
|
|
|
|
it('should report if reference-based trigger has no reference and the placeholder has multiple root elements', () => {
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({template: '@defer (on viewport) {hello} @placeholder {<div></div><span></span>}'})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].messageText).toBe(
|
|
'Trigger with no target can only be placed on an @defer that has a @placeholder block with exactly one root element node',
|
|
);
|
|
});
|
|
|
|
it('should report if reference-based trigger has no reference and the placeholder has one root element and some text', () => {
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({template: '@defer (on viewport) {hello} @placeholder {<div></div> hi}'})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].messageText).toBe(
|
|
'Trigger with no target can only be placed on an @defer that has a @placeholder block with exactly one root element node',
|
|
);
|
|
});
|
|
|
|
it('should count whitespace as a root node when preserveWhitespaces is enabled', () => {
|
|
env.write(
|
|
'/test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
preserveWhitespaces: true,
|
|
template: '@defer (on viewport) {hello} @placeholder {<div></div> }'
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].messageText).toBe(
|
|
'Trigger with no target can only be placed on an @defer that has a @placeholder block with exactly one root element node',
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|