@@ -15,12 +15,6 @@ import {
1515 expectNoLog ,
1616} from '../setup' ;
1717
18- /**
19- * Maximum time in milliseconds for single build/rebuild
20- * This accounts for CI variability.
21- */
22- export const BUILD_TIMEOUT = 30_000 ;
23-
2418describeBuilder ( buildApplication , APPLICATION_BUILDER_INFO , ( harness ) => {
2519 describe ( 'Behavior: "Rebuild Error Detection"' , ( ) => {
2620 it ( 'detects template errors with no AOT codegen or TS emit differences' , async ( ) => {
@@ -128,6 +122,235 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
128122 ) ;
129123 } ) ;
130124
125+ it ( 'detects template errors across components on watch rebuild when isolatedModules is enabled' , async ( ) => {
126+ harness . useTarget ( 'build' , {
127+ ...BASE_OPTIONS ,
128+ watch : true ,
129+ } ) ;
130+
131+ await harness . modifyFile ( 'tsconfig.json' , ( content ) => {
132+ const tsconfig = JSON . parse ( content ) ;
133+ tsconfig . compilerOptions = {
134+ ...tsconfig . compilerOptions ,
135+ isolatedModules : true ,
136+ } ;
137+
138+ return JSON . stringify ( tsconfig ) ;
139+ } ) ;
140+
141+ const goodChildComponentContents = `
142+ import { Component, Input } from '@angular/core';
143+ @Component({
144+ selector: 'child',
145+ standalone: true,
146+ template: '<p>{{ value }}</p>',
147+ })
148+ export class ChildComponent {
149+ @Input() value!: number;
150+ }
151+ ` ;
152+
153+ const typeErrorText = `Type 'number' is not assignable to type 'string'.` ;
154+
155+ await harness . writeFiles ( {
156+ 'src/app/child.component.ts' : goodChildComponentContents ,
157+ 'src/app/app.module.ts' : `
158+ import { NgModule } from '@angular/core';
159+ import { BrowserModule } from '@angular/platform-browser';
160+ import { AppComponent } from './app.component';
161+ @NgModule({
162+ imports: [
163+ BrowserModule,
164+ AppComponent,
165+ ],
166+ providers: [],
167+ bootstrap: [AppComponent]
168+ })
169+ export class AppModule { }
170+ ` ,
171+ 'src/app/app.component.ts' : `
172+ import { Component } from '@angular/core';
173+ import { ChildComponent } from './child.component';
174+ @Component({
175+ selector: 'app-root',
176+ standalone: true,
177+ imports: [ChildComponent],
178+ template: '<child [value]="123" />',
179+ })
180+ export class AppComponent {}
181+ ` ,
182+ } ) ;
183+
184+ await harness . executeWithCases (
185+ [
186+ async ( { result } ) => {
187+ expect ( result ?. success ) . toBeTrue ( ) ;
188+
189+ // Update child component to change input type from number to string
190+ await harness . writeFile (
191+ 'src/app/child.component.ts' ,
192+ `
193+ import { Component, Input } from '@angular/core';
194+ @Component({
195+ selector: 'child',
196+ standalone: true,
197+ template: '<p>{{ value }}</p>',
198+ })
199+ export class ChildComponent {
200+ @Input() value!: string;
201+ }
202+ ` ,
203+ ) ;
204+ } ,
205+ async ( { result, logs } ) => {
206+ expect ( result ?. success ) . toBeFalse ( ) ;
207+ expectLog ( logs , typeErrorText ) ;
208+
209+ // Make an unrelated change to verify error persists
210+ await harness . modifyFile ( 'src/main.ts' , ( content ) => content + '\n' ) ;
211+ } ,
212+ async ( { result, logs } ) => {
213+ expect ( result ?. success ) . toBeFalse ( ) ;
214+ expectLog ( logs , typeErrorText ) ;
215+
216+ // Revert back to number
217+ await harness . writeFile ( 'src/app/child.component.ts' , goodChildComponentContents ) ;
218+ } ,
219+ async ( { result, logs } ) => {
220+ expect ( result ?. success ) . toBeTrue ( ) ;
221+ expectNoLog ( logs , typeErrorText ) ;
222+
223+ // Make an unrelated change to verify error cache cleared
224+ await harness . modifyFile ( 'src/main.ts' , ( content ) => content + '\n' ) ;
225+ } ,
226+ ( { result, logs } ) => {
227+ expect ( result ?. success ) . toBeTrue ( ) ;
228+ expectNoLog ( logs , typeErrorText ) ;
229+ } ,
230+ ] ,
231+ { outputLogsOnFailure : false } ,
232+ ) ;
233+ } ) ;
234+
235+ it ( 'detects template errors when an input becomes required or a selector is renamed on watch rebuild' , async ( ) => {
236+ harness . useTarget ( 'build' , {
237+ ...BASE_OPTIONS ,
238+ watch : true ,
239+ } ) ;
240+
241+ await harness . modifyFile ( 'tsconfig.json' , ( content ) => {
242+ const tsconfig = JSON . parse ( content ) ;
243+ tsconfig . compilerOptions = {
244+ ...tsconfig . compilerOptions ,
245+ isolatedModules : true ,
246+ } ;
247+
248+ return JSON . stringify ( tsconfig ) ;
249+ } ) ;
250+
251+ const goodChildComponentContents = `
252+ import { Component, input } from '@angular/core';
253+ @Component({
254+ selector: 'app-child',
255+ standalone: true,
256+ template: '<p>{{ label() }}</p>',
257+ })
258+ export class ChildComponent {
259+ label = input('');
260+ }
261+ ` ;
262+
263+ const requiredInputErrorText = `Required input 'label' from component ChildComponent must be specified.` ;
264+ const unknownElementErrorText = `'app-child' is not a known element` ;
265+
266+ await harness . writeFiles ( {
267+ 'src/app/child.component.ts' : goodChildComponentContents ,
268+ 'src/app/app.module.ts' : `
269+ import { NgModule } from '@angular/core';
270+ import { BrowserModule } from '@angular/platform-browser';
271+ import { AppComponent } from './app.component';
272+ @NgModule({
273+ imports: [
274+ BrowserModule,
275+ AppComponent,
276+ ],
277+ providers: [],
278+ bootstrap: [AppComponent]
279+ })
280+ export class AppModule { }
281+ ` ,
282+ 'src/app/app.component.ts' : `
283+ import { Component } from '@angular/core';
284+ import { ChildComponent } from './child.component';
285+ @Component({
286+ selector: 'app-root',
287+ standalone: true,
288+ imports: [ChildComponent],
289+ template: '<app-child />',
290+ })
291+ export class AppComponent {}
292+ ` ,
293+ } ) ;
294+
295+ await harness . executeWithCases (
296+ [
297+ async ( { result } ) => {
298+ expect ( result ?. success ) . toBeTrue ( ) ;
299+
300+ // Make input required using signal input.required() (which has the same TS .d.ts type InputSignal<string>)
301+ await harness . writeFile (
302+ 'src/app/child.component.ts' ,
303+ `
304+ import { Component, input } from '@angular/core';
305+ @Component({
306+ selector: 'app-child',
307+ standalone: true,
308+ template: '<p>{{ label() }}</p>',
309+ })
310+ export class ChildComponent {
311+ label = input.required<string>();
312+ }
313+ ` ,
314+ ) ;
315+ } ,
316+ async ( { result, logs } ) => {
317+ expect ( result ?. success ) . toBeFalse ( ) ;
318+ expectLog ( logs , requiredInputErrorText ) ;
319+
320+ // Revert required input and rename selector instead
321+ await harness . writeFile (
322+ 'src/app/child.component.ts' ,
323+ `
324+ import { Component, input } from '@angular/core';
325+ @Component({
326+ selector: 'app-child-renamed',
327+ standalone: true,
328+ template: '<p>{{ label() }}</p>',
329+ })
330+ export class ChildComponent {
331+ label = input('');
332+ }
333+ ` ,
334+ ) ;
335+ } ,
336+ async ( { result, logs } ) => {
337+ expect ( result ?. success ) . toBeFalse ( ) ;
338+ expectNoLog ( logs , requiredInputErrorText ) ;
339+ expectLog ( logs , unknownElementErrorText ) ;
340+
341+ // Revert back to valid child component
342+ await harness . writeFile ( 'src/app/child.component.ts' , goodChildComponentContents ) ;
343+ } ,
344+ ( { result, logs } ) => {
345+ expect ( result ?. success ) . toBeTrue ( ) ;
346+ expectNoLog ( logs , requiredInputErrorText ) ;
347+ expectNoLog ( logs , unknownElementErrorText ) ;
348+ } ,
349+ ] ,
350+ { outputLogsOnFailure : false } ,
351+ ) ;
352+ } ) ;
353+
131354 it ( 'detects cumulative block syntax errors' , async ( ) => {
132355 harness . useTarget ( 'build' , {
133356 ...BASE_OPTIONS ,
@@ -234,7 +457,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
234457 await harness . appendToFile ( 'src/app/app.component.html' , '<div>Guten Tag</div>' ) ;
235458 } ,
236459 ( { logs } ) => {
237- expectNoLog ( logs , 'invalid-css-content ' ) ;
460+ expectNoLog ( logs , 'Unexpected character "EOF" ' ) ;
238461
239462 harness . expectFile ( 'dist/browser/main.js' ) . content . toContain ( 'Hello, world!' ) ;
240463 harness . expectFile ( 'dist/browser/main.js' ) . content . toContain ( 'Guten Tag' ) ;
0 commit comments