Skip to content

fix(custom-esbuild): drop empty array options in unit-test builder - #2422

Open
myabc wants to merge 2 commits into
just-jeb:masterfrom
myabc:fix/unit-test-empty-coverage-arrays
Open

myabc wants to merge 2 commits into
just-jeb:masterfrom
myabc:fix/unit-test-empty-coverage-arrays

Conversation

@myabc

@myabc myabc commented Sep 21, 2026

Copy link
Copy Markdown

PR Checklist

Please check if your PR fulfills the following requirements:

  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)

PR Type

What kind of change does this PR introduce?

[x] Bugfix
[ ] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build related changes
[ ] CI related changes
[ ] Documentation content changes
[ ] Other... Please describe:

What is the current behavior?

With @angular-builders/custom-esbuild:unit-test, ng test --coverage collects coverage (Coverage enabled with v8 is printed) but prints no coverage table and writes nothing to coverage/<project>/.

Angular CLI only skips its "fill unset array options with []" schema transform (addUndefinedDefaults) for builders named @angular/build:*. For this builder every unset array option arrives as [] and is forwarded unchanged. @angular/build treats an empty array as a value the user set:

  • coverageReporters: [] replaces Vitest's default coverage reporters, so none run.
  • coverageInclude: [] becomes ['spec-*.js', 'chunk-*.js'], which filters out every source file once coverage is remapped to the original sources.
  • reporters: [] makes Angular's Vitest plugin delete testConfig.reporters, so reporters configured in the runnerConfig Vitest file are silently discarded.

Issue Number: #2420

Reproduction: https://github.com/myabc/custom-esbuild-coverage-repro (HEAD~1 is the control case with the stock @angular/build:unit-test builder).

What is the new behavior?

The unit-test builder already deleted an empty browsers array for the same reason. It now deletes every empty array option before delegating, so they reach executeUnitTestBuilder as undefined, exactly as they do for @angular/build:unit-test (whose options go through the CLI's addUndefinedObjectDefaults path, which fills no array defaults at all). ng test --coverage then runs Vitest's default coverage reporters and writes the report to coverage/<project>/, and reporters from the Vitest config file survive. Non-empty arrays are passed through unchanged.

An explicit [] in angular.json is dropped as well. The CLI cannot tell it apart from an unset option for this builder, and none of the array options in the unit-test schema mean anything useful as []: browsers: [] is already normalised to undefined by @angular/build, coverageExclude: [] yields the same config as unset because Angular appends Vitest's default excludes anyway, exclude: [] and setupFiles: [] are already treated as unset, and include/plugins have schema defaults so never arrive as [] unless written explicitly (where [] would mean "run no tests").

Tests: new src/unit-test/index.spec.ts covers the stripped options (browsers, coverageInclude, coverageExclude, coverageReporters, reporters, setupFiles, exclude), the pass-through of non-empty arrays, and the forced vitest runner. Docs: a pitfall row in packages/custom-esbuild/AGENTS.md; the README needs no change because no option or usage changes.

Does this PR introduce a breaking change?

[ ] Yes
[x] No

Other information

Verified by hand: the equivalent change applied to dist/unit-test/index.js of 22.0.1 makes ng test --coverage write a report in the reproduction project and in the project where I first hit this. Locally I ran the custom-esbuild unit tests (yarn test, 24 passing); I did not run the integration tests, so I am relying on CI for those.

Angular CLI fills unset array options with [] for every builder outside
of @angular/build, and @angular/build treats an empty array as a value
the user set. With the unit-test builder, ng test --coverage therefore
ran no coverage reporter and filtered every source file out of the
report. The existing guard for an empty browsers array now also covers
coverageInclude, coverageExclude and coverageReporters.

Fixes just-jeb#2420
Copilot AI lite review requested due to automatic review settings September 21, 2026 15:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The change is narrowly scoped, aligns behavior with the upstream Angular builder, and includes targeted unit tests covering the new normalization logic.

Review effort: Lite
Findings: None

What changed in this PR

Fixes a behavioral mismatch between @angular-builders/custom-esbuild:unit-test and the upstream @angular/build:unit-test builder when running ng test --coverage, where Angular CLI’s defaulting of unset array options to [] causes @angular/build to interpret them as user-specified overrides (disabling Vitest coverage defaults).

Changes:

  • Normalize selected array options (browsers, coverageInclude, coverageExclude, coverageReporters) by deleting them when they are empty, so they delegate as undefined rather than [].
  • Add unit tests validating that empty arrays are stripped, non-empty arrays pass through, and the builder forces the Vitest runner.
  • Document the pitfall/behavior in packages/custom-esbuild/AGENTS.md for future maintenance and debugging.
File Description
packages/​custom-esbuild/​src/​unit-test/​index.ts Deletes specific empty array options before delegating to executeUnitTestBuilder, aligning behavior with @angular/build:unit-test.
packages/​custom-esbuild/​src/​unit-test/​index.spec.ts Adds Jest coverage for the new normalization logic and the forced runner: 'vitest' behavior.
packages/​custom-esbuild/​AGENTS.md Documents the “unset arrays become []” pitfall and the local workaround tied to issue #2420.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@just-jeb

Copy link
Copy Markdown
Owner

Thanks for the bug report and the fix.
One question, what happens if user indeed sets these options to empty array explicitly?

@myabc

myabc commented Sep 22, 2026

Copy link
Copy Markdown
Author

One question, what happens if user indeed sets these options to empty array explicitly?

@just-jeb the short answer is that an explicit [] gets dropped as well – i.e. it's the same as unset.

I think this is OK since the CLI also cannot tell them apart for us. addUndefinedDefaults turns unset into [] for any non-@angular/build:* builder, so both cases reach the builder as []. Only @angular/build:unit-test itself gets the addUndefinedObjectDefaults path that keeps unset as undefined.

The explicit list of coverage options missed reporters: an empty
reporters array makes @angular/build's Vitest plugin delete the
reporters configured in the runner config file. No array option in the
unit-test schema means something different when set to [] than when
unset, so strip all of them, matching what the CLI's
addUndefinedObjectDefaults path gives @angular/build:unit-test itself.
@myabc myabc changed the title fix(custom-esbuild): drop empty coverage arrays in unit-test builder fix(custom-esbuild): drop empty array options in unit-test builder Sep 22, 2026
@myabc

myabc commented Sep 22, 2026

Copy link
Copy Markdown
Author

While tracing the other options I found reporters: [] is a second live instance of the same bug (Angular's Vitest plugin deletes testConfig.reporters whenever the option is defined), so I switched to stripping every empty array in 2472d77 and retitled the PR.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Restrict empty-array cleanup so include: [] retains its intended behavior.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity

Open (1)

Comment on lines +18 to +26
function dropEmptyArrayOptions(options: CustomEsbuildUnitTestSchema) {
for (const option of Object.keys(options) as (keyof CustomEsbuildUnitTestSchema)[]) {
const value = options[option];

if (Array.isArray(value) && !value.length) {
delete options[option];
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants