Skip to content

Commit 74cbce2

Browse files
committed
feat: add precompileForTests()
1 parent a46bc93 commit 74cbce2

4 files changed

Lines changed: 162 additions & 2 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ yarn add -D s-ng-dev-utils
1212

1313
## TSLint Config
1414

15-
This library also comes with a predefined `tslint.json` that extends the angular cli's config to disable rules that conflict with Prettier (via [tslint-config-prettier](https://github.com/prettier/tslint-config-prettier)), with these exceptions that Simonton Software has found useful:
15+
This library also comes with a predefined `tslint.json` that extends the angular cli's config to disable rules that conflict with Prettier (via [tslint-config-prettier](https://github.com/prettier/tslint-config-prettier)), with these exceptions that we have found useful:
1616

1717
- Allows using the `Function` type. Some of our libraries deal a lot with utilities that operate on functions, and using this type is very handy.
18-
- Allows prefixing variables with `_`. This is useful e.g. when overriding a method in a way that does not use all its parameters. Simonton Software uses typescript's "noUnusedParameters" option, which gives an error with unused parameters unless their names are prefixed with `_`.
18+
- Allows prefixing variables with `_`. This is useful e.g. when overriding a method in a way that does not use all its parameters. We uses typescript's "noUnusedParameters" option, which gives an error with unused parameters unless their names are prefixed with `_`.
1919
- Downgrades [no-non-null-assertion](https://palantir.github.io/tslint/rules/no-non-null-assertion/) to a warning. While we believe using `!` should be avoided when reasonable, we find that sometimes it just makes sense.
2020

2121
To use it, change your `tslint.json` to:
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Component, Injectable, NgModule } from "@angular/core";
2+
import { TestBed } from "@angular/core/testing";
3+
import { precompileForTests } from "./precompile-for-tests";
4+
5+
@Injectable({ providedIn: "root" })
6+
class ProvidedInRoot {
7+
counter = 0;
8+
}
9+
10+
@Injectable()
11+
class ProvidedInModule {
12+
counter = 0;
13+
}
14+
15+
@Injectable()
16+
class ProvidedInComponent {
17+
counter = 0;
18+
}
19+
20+
@Component({
21+
template: "I exist! Created {{ providedInComponent.counter }} time(s).",
22+
providers: [ProvidedInComponent],
23+
})
24+
class AComponent {
25+
constructor(public providedInComponent: ProvidedInComponent) {
26+
++providedInComponent.counter;
27+
}
28+
}
29+
30+
@NgModule({ declarations: [AComponent], providers: [ProvidedInModule] })
31+
class AModule {}
32+
33+
describe("precompileForTests()", () => {
34+
precompileForTests([AModule]);
35+
36+
async function initFullModule() {
37+
TestBed.configureTestingModule({ imports: [AModule] });
38+
await TestBed.compileComponents();
39+
}
40+
41+
/////////
42+
43+
it("allows components", async () => {
44+
await initFullModule();
45+
const fixture = TestBed.createComponent(AComponent);
46+
expect(fixture.nativeElement.textContent).toContain("I exist!");
47+
});
48+
49+
it("allows declaring only a component", async () => {
50+
TestBed.configureTestingModule({ declarations: [AComponent] });
51+
await TestBed.compileComponents();
52+
const fixture = TestBed.createComponent(AComponent);
53+
expect(fixture.nativeElement.textContent).toContain("I exist!");
54+
});
55+
56+
for (let i = 0; i < 3; ++i) {
57+
it(`allows clearing service state between tests, iteration ${i}`, async () => {
58+
await initFullModule();
59+
const providedInRoot = TestBed.get(ProvidedInRoot);
60+
const providedInModule = TestBed.get(ProvidedInModule);
61+
const fixture = TestBed.createComponent(AComponent);
62+
fixture.detectChanges();
63+
64+
expect(providedInRoot.counter).toBe(0);
65+
expect(providedInModule.counter).toBe(0);
66+
expect(fixture.nativeElement.textContent).toContain("Created 1 time(s).");
67+
68+
++providedInRoot.counter;
69+
++providedInModule.counter;
70+
});
71+
}
72+
});
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { CompileMetadataResolver } from "@angular/compiler";
2+
import { TestBed } from "@angular/core/testing";
3+
import {
4+
BrowserDynamicTestingModule,
5+
platformBrowserDynamicTesting,
6+
} from "@angular/platform-browser-dynamic/testing";
7+
8+
/**
9+
* Use this to speed up your angular test suite. Normally when you call `TestBed.compileComponents()`, it recompiles all the configured components even if it has compiled them before in a previous test. AoT compilation would be a great fix, but is not well supported/documented for tests yet. This function fills the gap by precompiling the components as if by AoT and setting up `TestBed` to use them.
10+
*
11+
* ```ts
12+
* // let's assume `AppModule` declares or imports a `HelloWorldComponent`
13+
* precompileForTests([AppModule]);
14+
*
15+
* describe("AppComponent", () => {
16+
* it("says hello", async () => {
17+
* TestBed.configureTestingModule({ declarations: [HelloWorldComponent] });
18+
* await TestBed.compileComponents(); // <- this line is faster
19+
* const fixture = TestBed.createComponent(HelloWorldComponent);
20+
* expect(fixture.nativeElement.textContent).toContain("Hello, world!");
21+
* });
22+
* });
23+
* ```
24+
*
25+
* Note that this uses `TestBed.initTestEnvironment` to set up the precompiled components, so you will not be able to use it yourself at the same time.
26+
*/
27+
export function precompileForTests(modules: any[], skipModules: any[] = []) {
28+
beforeAll(async () => {
29+
TestBed.configureTestingModule({ imports: modules });
30+
await TestBed.compileComponents();
31+
32+
// technique modeled from https://github.com/angular/angular/blob/11325bad4ab786a07e52ff380c00622fda11c0b7/packages/core/test/linker/jit_summaries_integration_spec.ts#L83
33+
const metadataResolver = TestBed.get(
34+
CompileMetadataResolver,
35+
) as CompileMetadataResolver;
36+
37+
TestBed.resetTestEnvironment();
38+
TestBed.initTestEnvironment(
39+
BrowserDynamicTestingModule,
40+
platformBrowserDynamicTesting(),
41+
() =>
42+
extractAotSummaries(metadataResolver, modules, new Set(skipModules)),
43+
);
44+
});
45+
}
46+
47+
function extractAotSummaries(
48+
metadataResolver: CompileMetadataResolver,
49+
modules: any[],
50+
skipModules: Set<any>,
51+
): any[] {
52+
return modules.map((module) => () =>
53+
extractAotSummaries0(metadataResolver, module, skipModules),
54+
);
55+
}
56+
57+
function extractAotSummaries0(
58+
metadataResolver: CompileMetadataResolver,
59+
module: any,
60+
skipModules: Set<any>,
61+
) {
62+
if (skipModules.has(module)) {
63+
return [];
64+
}
65+
skipModules.add(module);
66+
67+
const moduleMetadata = metadataResolver.getNgModuleMetadata(module)!;
68+
return [
69+
metadataResolver.getNgModuleSummary(module),
70+
() =>
71+
moduleMetadata.declaredDirectives.map((directive) =>
72+
metadataResolver.getDirectiveSummary(directive.reference),
73+
),
74+
() =>
75+
moduleMetadata.declaredPipes.map((pipe) =>
76+
metadataResolver.getPipeSummary(pipe.reference),
77+
),
78+
() =>
79+
extractAotSummaries(
80+
metadataResolver,
81+
moduleMetadata.importedModules.map(
82+
(importedModule) => importedModule.type.reference,
83+
),
84+
skipModules,
85+
),
86+
];
87+
}

projects/s-ng-dev-utils/src/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export { expectCallsAndReset } from "./lib/expect-calls-and-reset";
66
export { expectSingleCallAndReset } from "./lib/expect-single-call-and-reset";
77
export { expectType } from "./lib/expect-type";
88
export { marbleTest } from "./lib/marble-test";
9+
export { precompileForTests } from "./lib/precompile-for-tests";

0 commit comments

Comments
 (0)