Skip to content

refactor: use executeWithCases instead of RXJS boillerplate #30672

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions modules/testing/builder/src/jasmine-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,19 @@
import { BuilderHandlerFn } from '@angular-devkit/architect';
import { json } from '@angular-devkit/core';
import { readFileSync } from 'node:fs';
import { concatMap, count, firstValueFrom, take, timeout } from 'rxjs';
import { BuilderHarness, BuilderHarnessExecutionResult } from './builder-harness';
import { concatMap, count, debounceTime, firstValueFrom, take, timeout } from 'rxjs';
import {
BuilderHarness,
BuilderHarnessExecutionOptions,
BuilderHarnessExecutionResult,
} from './builder-harness';
import { host } from './test-utils';

/**
* Maximum time for single build/rebuild
* This accounts for CI variability.
*/
export const BUILD_TIMEOUT = 25_000;
export const BUILD_TIMEOUT = 30_000;

const optionSchemaCache = new Map<string, json.schema.JsonSchema>();

Expand Down Expand Up @@ -62,10 +66,12 @@ export class JasmineBuilderHarness<T> extends BuilderHarness<T> {
executionResult: BuilderHarnessExecutionResult,
index: number,
) => void | Promise<void>)[],
options?: Partial<BuilderHarnessExecutionOptions> & { timeout?: number },
): Promise<void> {
const executionCount = await firstValueFrom(
this.execute().pipe(
timeout(BUILD_TIMEOUT),
this.execute(options).pipe(
timeout(options?.timeout ?? BUILD_TIMEOUT),
debounceTime(100), // This is needed as sometimes 2 events for the same change fire with webpack.
concatMap(async (result, index) => await cases[index](result, index)),
take(cases.length),
count(),
Expand Down Expand Up @@ -118,13 +124,17 @@ export function expectFile<T>(path: string, harness: BuilderHarness<T>): Harness
return {
toExist() {
const exists = harness.hasFile(path);
expect(exists).toBe(true, 'Expected file to exist: ' + path);
expect(exists)
.withContext('Expected file to exist: ' + path)
.toBeTrue();

return exists;
},
toNotExist() {
const exists = harness.hasFile(path);
expect(exists).toBe(false, 'Expected file to not exist: ' + path);
expect(exists)
.withContext('Expected file to exist: ' + path)
.toBeFalse();

return !exists;
},
Expand Down Expand Up @@ -170,13 +180,17 @@ export function expectDirectory<T>(
return {
toExist() {
const exists = harness.hasDirectory(path);
expect(exists).toBe(true, 'Expected directory to exist: ' + path);
expect(exists)
.withContext('Expected directory to exist: ' + path)
.toBeTrue();

return exists;
},
toNotExist() {
const exists = harness.hasDirectory(path);
expect(exists).toBe(false, 'Expected directory to not exist: ' + path);
expect(exists)
.withContext('Expected directory to not exist: ' + path)
.toBeFalse();

return !exists;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,9 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { concatMap, count, take, timeout } from 'rxjs';
import { buildApplication } from '../../index';
import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup';

/**
* Maximum time in milliseconds for single build/rebuild
* This accounts for CI variability.
*/
const BUILD_TIMEOUT = 10_000;

describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
describe('Behavior: "Rebuilds when input asset changes"', () => {
beforeEach(async () => {
Expand All @@ -36,30 +29,18 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
watch: true,
});

const buildCount = await harness
.execute({ outputLogsOnFailure: false })
.pipe(
timeout(BUILD_TIMEOUT),
concatMap(async ({ result }, index) => {
switch (index) {
case 0:
expect(result?.success).toBeTrue();
harness.expectFile('dist/browser/asset.txt').content.toContain('foo');
await harness.executeWithCases([
async ({ result }) => {
expect(result?.success).toBeTrue();
harness.expectFile('dist/browser/asset.txt').content.toContain('foo');

await harness.writeFile('public/asset.txt', 'bar');
break;
case 1:
expect(result?.success).toBeTrue();
harness.expectFile('dist/browser/asset.txt').content.toContain('bar');
break;
}
}),
take(2),
count(),
)
.toPromise();

expect(buildCount).toBe(2);
await harness.writeFile('public/asset.txt', 'bar');
},
({ result }) => {
expect(result?.success).toBeTrue();
harness.expectFile('dist/browser/asset.txt').content.toContain('bar');
},
]);
});

it('remove deleted asset from output', async () => {
Expand All @@ -79,32 +60,21 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
watch: true,
});

const buildCount = await harness
.execute({ outputLogsOnFailure: false })
.pipe(
timeout(BUILD_TIMEOUT),
concatMap(async ({ result }, index) => {
switch (index) {
case 0:
expect(result?.success).toBeTrue();
harness.expectFile('dist/browser/asset-one.txt').toExist();
harness.expectFile('dist/browser/asset-two.txt').toExist();
await harness.executeWithCases([
async ({ result }) => {
expect(result?.success).toBeTrue();
harness.expectFile('dist/browser/asset-one.txt').toExist();
harness.expectFile('dist/browser/asset-two.txt').toExist();

await harness.removeFile('public/asset-two.txt');
break;
case 1:
expect(result?.success).toBeTrue();
harness.expectFile('dist/browser/asset-one.txt').toExist();
harness.expectFile('dist/browser/asset-two.txt').toNotExist();
break;
}
}),
take(2),
count(),
)
.toPromise();
await harness.removeFile('public/asset-two.txt');
},

expect(buildCount).toBe(2);
({ result }) => {
expect(result?.success).toBeTrue();
harness.expectFile('dist/browser/asset-one.txt').toExist();
harness.expectFile('dist/browser/asset-two.txt').toNotExist();
},
]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { concatMap, count, take, timeout } from 'rxjs';
import { buildApplication } from '../../index';
import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup';

Expand All @@ -32,46 +31,31 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
await harness.writeFile('src/app/app.component.scss', "@import './a';");
await harness.writeFile('src/app/a.scss', '$primary: aqua;\\nh1 { color: $primary; }');

const buildCount = await harness
.execute()
.pipe(
timeout(30000),
concatMap(async ({ result }, index) => {
expect(result?.success).toBe(true);
await harness.executeWithCases([
async ({ result }) => {
expect(result?.success).toBe(true);

switch (index) {
case 0:
harness.expectFile('dist/browser/main.js').content.toContain('color: aqua');
harness.expectFile('dist/browser/main.js').content.not.toContain('color: blue');
harness.expectFile('dist/browser/main.js').content.toContain('color: aqua');
harness.expectFile('dist/browser/main.js').content.not.toContain('color: blue');

await harness.writeFile(
'src/app/a.scss',
'$primary: blue;\\nh1 { color: $primary; }',
);
break;
case 1:
harness.expectFile('dist/browser/main.js').content.not.toContain('color: aqua');
harness.expectFile('dist/browser/main.js').content.toContain('color: blue');
await harness.writeFile('src/app/a.scss', '$primary: blue;\\nh1 { color: $primary; }');
},
async ({ result }) => {
expect(result?.success).toBe(true);

await harness.writeFile(
'src/app/a.scss',
'$primary: green;\\nh1 { color: $primary; }',
);
break;
case 2:
harness.expectFile('dist/browser/main.js').content.not.toContain('color: aqua');
harness.expectFile('dist/browser/main.js').content.not.toContain('color: blue');
harness.expectFile('dist/browser/main.js').content.toContain('color: green');
harness.expectFile('dist/browser/main.js').content.not.toContain('color: aqua');
harness.expectFile('dist/browser/main.js').content.toContain('color: blue');

break;
}
}),
take(3),
count(),
)
.toPromise();
await harness.writeFile('src/app/a.scss', '$primary: green;\\nh1 { color: $primary; }');
},
({ result }) => {
expect(result?.success).toBe(true);

expect(buildCount).toBe(3);
harness.expectFile('dist/browser/main.js').content.not.toContain('color: aqua');
harness.expectFile('dist/browser/main.js').content.not.toContain('color: blue');
harness.expectFile('dist/browser/main.js').content.toContain('color: green');
},
]);
});
}
});
Expand Down
Loading