Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export class VitestExecutor implements TestExecutor {
);

const testSetupFiles = this.prepareSetupFiles();
const plugins = createVitestPlugins(this.options, testSetupFiles, browserOptions, {
const plugins = createVitestPlugins({
workspaceRoot,
projectSourceRoot: this.options.projectSourceRoot,
projectName: this.projectName,
Expand All @@ -209,36 +209,54 @@ export class VitestExecutor implements TestExecutor {
: {};

const runnerConfig = this.options.runnerConfig;
const externalConfigPath =
runnerConfig === true
? await findVitestBaseConfig([this.options.projectRoot, this.options.workspaceRoot])
: runnerConfig;
const projectName = this.projectName;

return startVitest(
'test',
undefined,
{
config:
runnerConfig === true
? await findVitestBaseConfig([this.options.projectRoot, this.options.workspaceRoot])
: runnerConfig,
config: externalConfigPath,
root: workspaceRoot,
project: ['base', this.projectName],
name: 'base',
include: [],
project: projectName,
outputFile,
testNamePattern: this.options.filter,
watch,
ui,
...debugOptions,
},
{
test: {
coverage: await generateCoverageOption(coverage, this.projectName),
outputFile,
...debugOptions,
...(reporters ? { reporters } : {}),
projects: [
{
extends: externalConfigPath || true,
test: {
name: projectName,
globals: true,
setupFiles: testSetupFiles,
...(this.options.exclude ? { exclude: this.options.exclude } : {}),
browser: browserOptions.browser,
// Use `jsdom` if no browsers are explicitly configured.
...(browserOptions.browser ? {} : { environment: 'jsdom' }),
...(this.options.include ? { include: this.options.include } : {}),
},
optimizeDeps: {
noDiscovery: true,
},
plugins,
},
],
},
server: {
// Disable the actual file watcher. The boolean watch option above should still
// be enabled as it controls other internal behavior related to rerunning tests.
watch: null,
},
plugins,
},
);
}
Expand Down
213 changes: 89 additions & 124 deletions packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,143 +28,108 @@ interface PluginOptions {
testFileToEntryPoint: ReadonlyMap<string, string>;
}

export function createVitestPlugins(
options: NormalizedUnitTestBuilderOptions,
testSetupFiles: string[],
browserOptions: BrowserConfiguration,
pluginOptions: PluginOptions,
): VitestPlugins {
const { workspaceRoot, projectName, buildResultFiles, testFileToEntryPoint } = pluginOptions;
export function createVitestPlugins(pluginOptions: PluginOptions): VitestPlugins {
const { workspaceRoot, buildResultFiles, testFileToEntryPoint } = pluginOptions;

return [
{
name: 'angular:project-init',
// Type is incorrect. This allows a Promise<void>.
// eslint-disable-next-line @typescript-eslint/no-misused-promises
configureVitest: async (context) => {
// Create a subproject that can be configured with plugins for browser mode.
// Plugins defined directly in the vite overrides will not be present in the
// browser specific Vite instance.
await context.injectTestProjects({
test: {
name: projectName,
root: workspaceRoot,
globals: true,
setupFiles: testSetupFiles,
include: options.include,
...(options.exclude ? { exclude: options.exclude } : {}),
browser: browserOptions.browser,
// Use `jsdom` if no browsers are explicitly configured.
...(browserOptions.browser ? {} : { environment: 'jsdom' }),
},
plugins: [
{
name: 'angular:test-in-memory-provider',
enforce: 'pre',
resolveId: (id, importer) => {
if (importer && (id[0] === '.' || id[0] === '/')) {
let fullPath;
if (testFileToEntryPoint.has(importer)) {
fullPath = toPosixPath(path.join(workspaceRoot, id));
} else {
fullPath = toPosixPath(path.join(path.dirname(importer), id));
}
name: 'angular:test-in-memory-provider',
enforce: 'pre',
resolveId: (id, importer) => {
if (importer && (id[0] === '.' || id[0] === '/')) {
let fullPath;
if (testFileToEntryPoint.has(importer)) {
fullPath = toPosixPath(path.join(workspaceRoot, id));
} else {
fullPath = toPosixPath(path.join(path.dirname(importer), id));
}

const relativePath = path.relative(workspaceRoot, fullPath);
if (buildResultFiles.has(toPosixPath(relativePath))) {
return fullPath;
}
}
const relativePath = path.relative(workspaceRoot, fullPath);
if (buildResultFiles.has(toPosixPath(relativePath))) {
return fullPath;
}
}

if (testFileToEntryPoint.has(id)) {
return id;
}
if (testFileToEntryPoint.has(id)) {
return id;
}

assert(buildResultFiles.size > 0, 'buildResult must be available for resolving.');
const relativePath = path.relative(workspaceRoot, id);
if (buildResultFiles.has(toPosixPath(relativePath))) {
return id;
}
},
load: async (id) => {
assert(
buildResultFiles.size > 0,
'buildResult must be available for in-memory loading.',
);
assert(buildResultFiles.size > 0, 'buildResult must be available for resolving.');
const relativePath = path.relative(workspaceRoot, id);
if (buildResultFiles.has(toPosixPath(relativePath))) {
return id;
}
},
load: async (id) => {
assert(buildResultFiles.size > 0, 'buildResult must be available for in-memory loading.');

// Attempt to load as a source test file.
const entryPoint = testFileToEntryPoint.get(id);
let outputPath;
if (entryPoint) {
outputPath = entryPoint + '.js';
// Attempt to load as a source test file.
const entryPoint = testFileToEntryPoint.get(id);
let outputPath;
if (entryPoint) {
outputPath = entryPoint + '.js';

// To support coverage exclusion of the actual test file, the virtual
// test entry point only references the built and bundled intermediate file.
return {
code: `import "./${outputPath}";`,
};
} else {
// Attempt to load as a built artifact.
const relativePath = path.relative(workspaceRoot, id);
outputPath = toPosixPath(relativePath);
}
// To support coverage exclusion of the actual test file, the virtual
// test entry point only references the built and bundled intermediate file.
return {
code: `import "./${outputPath}";`,
};
} else {
// Attempt to load as a built artifact.
const relativePath = path.relative(workspaceRoot, id);
outputPath = toPosixPath(relativePath);
}

const outputFile = buildResultFiles.get(outputPath);
if (outputFile) {
const sourceMapPath = outputPath + '.map';
const sourceMapFile = buildResultFiles.get(sourceMapPath);
const code =
outputFile.origin === 'memory'
? Buffer.from(outputFile.contents).toString('utf-8')
: await readFile(outputFile.inputPath, 'utf-8');
const sourceMapText = sourceMapFile
? sourceMapFile.origin === 'memory'
? Buffer.from(sourceMapFile.contents).toString('utf-8')
: await readFile(sourceMapFile.inputPath, 'utf-8')
: undefined;
const outputFile = buildResultFiles.get(outputPath);
if (outputFile) {
const sourceMapPath = outputPath + '.map';
const sourceMapFile = buildResultFiles.get(sourceMapPath);
const code =
outputFile.origin === 'memory'
? Buffer.from(outputFile.contents).toString('utf-8')
: await readFile(outputFile.inputPath, 'utf-8');
const sourceMapText = sourceMapFile
? sourceMapFile.origin === 'memory'
? Buffer.from(sourceMapFile.contents).toString('utf-8')
: await readFile(sourceMapFile.inputPath, 'utf-8')
: undefined;

// Vitest will include files in the coverage report if the sourcemap contains no sources.
// For builder-internal generated code chunks, which are typically helper functions,
// a virtual source is added to the sourcemap to prevent them from being incorrectly
// included in the final coverage report.
const map = sourceMapText ? JSON.parse(sourceMapText) : undefined;
if (map) {
if (!map.sources?.length && !map.sourcesContent?.length && !map.mappings) {
map.sources = ['virtual:builder'];
}
}
// Vitest will include files in the coverage report if the sourcemap contains no sources.
// For builder-internal generated code chunks, which are typically helper functions,
// a virtual source is added to the sourcemap to prevent them from being incorrectly
// included in the final coverage report.
const map = sourceMapText ? JSON.parse(sourceMapText) : undefined;
if (map) {
if (!map.sources?.length && !map.sourcesContent?.length && !map.mappings) {
map.sources = ['virtual:builder'];
}
}

return {
code,
map,
};
}
},
configureServer: (server) => {
server.middlewares.use(
createBuildAssetsMiddleware(server.config.base, buildResultFiles),
);
},
},
return {
code,
map,
};
}
},
configureServer: (server) => {
server.middlewares.use(createBuildAssetsMiddleware(server.config.base, buildResultFiles));
},
},
{
name: 'angular:html-index',
transformIndexHtml: () => {
// Add all global stylesheets
if (buildResultFiles.has('styles.css')) {
return [
{
name: 'angular:html-index',
transformIndexHtml: () => {
// Add all global stylesheets
if (buildResultFiles.has('styles.css')) {
return [
{
tag: 'link',
attrs: { href: 'styles.css', rel: 'stylesheet' },
injectTo: 'head',
},
];
}

return [];
},
tag: 'link',
attrs: { href: 'styles.css', rel: 'stylesheet' },
injectTo: 'head',
},
],
});
];
}

return [];
},
},
];
Expand Down