Skip to content
Open
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
22 changes: 18 additions & 4 deletions packages/angular/cli/src/commands/mcp/tools/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,16 @@ const testToolOutputSchema = z.object({

export type TestToolOutput = z.infer<typeof testToolOutputSchema>;

function shouldUseHeadlessOption(
testTarget: import('@angular-devkit/core').workspaces.TargetDefinition | undefined,
): boolean {
return (
testTarget?.builder === '@angular/build:unit-test' && testTarget.options?.['runner'] !== 'karma'
);
}

export async function runTest(input: TestToolInput, context: McpToolContext) {
const { workspacePath, projectName } = await resolveWorkspaceAndProject({
const { workspace, workspacePath, projectName } = await resolveWorkspaceAndProject({
host: context.host,
workspacePathInput: input.workspace,
projectNameInput: input.project,
Expand All @@ -40,8 +48,13 @@ export async function runTest(input: TestToolInput, context: McpToolContext) {
// Build "ng"'s command line.
const args = ['test', projectName];

// This is ran by the agent so we want a non-watched, headless test.
args.push('--browsers', 'ChromeHeadless');
if (shouldUseHeadlessOption(workspace.projects.get(projectName)?.targets.get('test'))) {
args.push('--headless', 'true');
} else {
// Karma-based projects need an explicit headless browser for non-interactive MCP execution.
args.push('--browsers', 'ChromeHeadless');
}

args.push('--watch', 'false');

if (input.filter) {
Expand Down Expand Up @@ -83,7 +96,8 @@ Perform a one-off, non-watched unit test execution with ng test.
<Operational Notes>
* This tool uses "ng test".
* It supports filtering by spec name if the underlying builder supports it (e.g., 'unit-test' builder).
* This runs a headless Chrome as a browser, so requires Chrome to be installed.
* For the "@angular/build:unit-test" builder with Vitest, this tool requests headless execution via "--headless true".
* For Karma-based projects, this tool forces headless Chrome with "--browsers ChromeHeadless", so Chrome must be installed.
</Operational Notes>
`,
isReadOnly: false,
Expand Down
36 changes: 36 additions & 0 deletions packages/angular/cli/src/commands/mcp/tools/test_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,40 @@ describe('Test Tool', () => {
expect(structuredContent.status).toBe('failure');
expect(structuredContent.logs).toEqual([...testLogs, 'Test failed']);
});

it('should use the headless option for the unit-test builder when using Vitest', async () => {
addProjectToWorkspace(mockContext.workspace.projects, 'my-vitest-app', {
test: {
builder: '@angular/build:unit-test',
options: {
runner: 'vitest',
},
},
});

await runTest({ project: 'my-vitest-app' }, mockContext);

expect(mockHost.runCommand).toHaveBeenCalledWith(
'ng',
['test', 'my-vitest-app', '--headless', 'true', '--watch', 'false'],
{ cwd: '/test' },
);
});

it('should use the headless option for the unit-test builder when the runner is omitted', async () => {
addProjectToWorkspace(mockContext.workspace.projects, 'my-default-vitest-app', {
test: {
builder: '@angular/build:unit-test',
options: {},
},
});

await runTest({ project: 'my-default-vitest-app' }, mockContext);

expect(mockHost.runCommand).toHaveBeenCalledWith(
'ng',
['test', 'my-default-vitest-app', '--headless', 'true', '--watch', 'false'],
{ cwd: '/test' },
);
});
Comment on lines +97 to +131
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

These two new test cases are very similar and contain duplicated logic. To improve maintainability and reduce redundancy, you could consider parameterizing them. This can be done by defining the test cases in an array and iterating over them to create the it blocks dynamically.

  const testCases = [
    {
      description: 'when using Vitest',
      projectName: 'my-vitest-app',
      options: { runner: 'vitest' },
    },
    {
      description: 'when the runner is omitted',
      projectName: 'my-default-vitest-app',
      options: {},
    },
  ];

  for (const { description, projectName, options } of testCases) {
    it('should use the headless option for the unit-test builder ' + description, async () => {
      addProjectToWorkspace(mockContext.workspace.projects, projectName, {
        test: {
          builder: '@angular/build:unit-test',
          options,
        },
      });

      await runTest({ project: projectName }, mockContext);

      expect(mockHost.runCommand).toHaveBeenCalledWith(
        'ng',
        ['test', projectName, '--headless', 'true', '--watch', 'false'],
        { cwd: '/test' },
      );
    });
  }

});
Loading