# vitest-browser-angular

> **Note:** This repository is a **fork** of the official [`vitest-browser-angular`](https://github.com/vitest-community/vitest-browser-angular) library. The implementations contained here are developed independently and are **not** shared with the official project.

This community package renders Angular components in [Vitest Browser Mode](https://vitest.dev/guide/browser).

```ts
import { Component, input } from '@angular/core';
import { expect, test } from 'vitest';
import { render } from 'vitest-browser-angular';

@Component({
  selector: 'app-hello-world',
  template: '<h1>Hello, {{ name() }}!</h1>',
})
export class HelloWorld {
  name = input.required<string>();
}

test('renders name', async () => {
  const { locator } = await render(HelloWorld, {
    inputs: {
      name: 'World',
    },
  });

  await expect.element(locator).toHaveTextContent('Hello, World!');
});
```

## Setup

There are currently two ways to set up Vitest for Angular:

- Analog's [`vitest-angular` plugin](https://analogjs.org/docs/features/testing/vitest) _(community)_.
- Angular CLI's [`unit-test` builder](https://angular.dev/guide/testing#configuration) _(official)_.

While Angular CLI's `unit-test` builder is the official way to set up Vitest for Angular, it has some [limitations](https://analogjs.org/docs/features/testing/overview#angular-support-for-vitest). Analog's `vitest-angular` plugin provides more Vitest features and greater flexibility.

### Setup with Analog Plugin

1. Set up Vitest

```sh
npm add -D @analogjs/platform vitest-browser-angular

ng g @analogjs/platform:setup-vitest
```

2. Activate browser mode in the generated Vitest configuration by following the [browser mode configuration instructions](https://vitest.dev/guide/browser/#configuration).

### Setup with Angular CLI

1. Configure your Angular project to use the `@angular/build:unit-test` builder, and add the browsers of your choice.

```json
{
  ...,
  "projects": {
    "my-app": {
      ...,
      "architect": {
        "test": {
          "builder": "@angular/build:unit-test",
          "options": {
            "browsers": ["Chromium", "Firefox", "Webkit"]
          }
        }
      }
    }
  }
}
```

_Since Angular v21, Vitest is the default runner so you don't need to set the `runner` option._

2. Install the browser provider of your choice using `ng add`

```sh
# With Playwright
ng add @vitest/browser-playwright

# or with WebdriverIO
ng add @vitest/browser-webdriverio
```

3. Add the `vitest-browser-angular` package to your project.

```sh
npm add -D vitest-browser-angular
```

4. _(Optional)_ By default the browser UI is emptied after every test, so you can't see what a component rendered. To keep the last component mounted and visible after the test, enable the builder's `debug` option in `angular.json`:

```json
{
  ...,
  "architect": {
    "test": {
      "builder": "@angular/build:unit-test",
      "options": {
        "browsers": ["Chromium"],
        "debug": true
      }
    }
  }
}
```

`debug` maps to `teardown: { destroyAfterEach: false }` in the test environment generated by the CLI, which stops Angular from destroying the component at the end of the test. It can also be passed per run with `ng test --debug`.

## Zone.js VS Zoneless Setup

Angular CLI will automatically set up the test environment for you depending on the presence of `zone.js` in your project's polyfills.

When using the Analog plugin, you can control the behavior using the `zoneless` option of `setupTestBed()` in `test-setup.ts`:

```ts
import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed';

setupTestBed({
  zoneless: true,
});
```

For detailed setup instructions for both Zone.js and Zoneless configurations, please refer to the [Analog Vitest documentation](https://analogjs.org/docs/features/testing/vitest).

## Component Preview

To preview, debug and interact with a component in the browser after the test, you can prevent Angular from destroying it.

In Angular CLI, enable this using the `--debug` option (it sets `teardown: { destroyAfterEach: false }` in the CLI-generated test environment).

With the Analog plugin, enable this using the `teardown.destroyAfterEach` option of `setupTestBed()` in `test-setup.ts`:

```ts
import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed';

setupTestBed({
  teardown: { destroyAfterEach: false },
});
```

## Usage

### Basic Example

The `render` function supports two query patterns:

```ts
import { test, expect } from 'vitest';
import { render } from 'vitest-browser-angular';

@Component({
  template: ` <h1>Welcome</h1> `,
})
export class MyComponent {}

test('query elements', async () => {
  // Pattern 1: Use locator to query within the component element
  const { locator } = await render(MyComponent);
  await expect.element(locator.getByText('Welcome')).toBeVisible();

  // Pattern 2: Use screen to query from document.body (useful for portals/overlays)
  const screen = await render(MyComponent);
  await expect.element(screen.getByText('Welcome')).toBeVisible();
  await expect.element(screen.getByText('Some Popover Content')).toBeVisible();
});
```

### Query Methods

Both `locator` and `screen` provide the following query methods:

- `getByRole` - Locate by ARIA role and accessible name
- `getByText` - Locate by text content
- `getByLabelText` - Locate by associated label text
- `getByPlaceholder` - Locate by placeholder text
- `getByAltText` - Locate by alt text (images)
- `getByTitle` - Locate by title attribute
- `getByTestId` - Locate by data-testid attribute

**When to use which pattern:**

- **`locator`**: (full name: "Component Locator") - queries are scoped to the component's host element. Best for most component tests.
- **`screen`**: Queries start from `baseElement` (defaults to `document.body`). Use when testing components that render content outside their host element (modals, tooltips, portals).

### Container Element

Access the component's host element directly via `container` (shortcut for `fixture.nativeElement`):

```ts
const { container, locator } = await render(MyComponent);
expect(container).toBe(locator.element());
```

### Base Element

Customize the root element for screen queries (useful for portal/overlay testing):

```ts
const customContainer = document.querySelector('#modal-root');
const screen = await render(ModalComponent, {
  baseElement: customContainer,
});
// screen queries now start from customContainer instead of document.body
```

## Inputs

Pass input values to components using the `inputs` option:

```ts
import { Component, input } from '@angular/core';

@Component({
  template: '<h2>{{ name() }}</h2><p>Price: ${{ price() }}</p>',
  standalone: true,
})
export class ProductComponent {
  name = input('Unknown Product');
  price = input(0);
}

test('render with inputs', async () => {
  const screen = await render(ProductComponent, {
    inputs: {
      name: 'Laptop',
      price: 1299.99,
    },
  });

  await expect.element(screen.getByText('Laptop')).toBeVisible();
  await expect.element(screen.getByText(/Price: \$1299\.99/)).toBeVisible();
});
```

`render()` is built around Angular's modern signal-based APIs. Signal inputs (`input()`) bound as a `WritableSignal` stay reactive — updating the signal propagates to the component once change detection runs. Passing a `model()` input as a signal enables automatic two-way binding: values written inside the component are written back to your signal, mirroring Angular's `[(x)]` semantics.

Plain `@Input()` decorator inputs are still supported for setting values, but the reactive-binding and write-back behaviors above only apply to signal inputs.

### Rerender

Update component inputs after rendering using `rerender`:

```ts
import { Component, input } from '@angular/core';
import { signal } from '@angular/core';
import { test, expect } from 'vitest';
import { render } from 'vitest-browser-angular';

@Component({
  template: '<h2>{{ name() }}</h2><p>Price: ${{ price() }}</p>',
})
export class ProductComponent {
  name = input('Unknown Product');
  price = input(0);
}

test('rerender with new inputs', async () => {
  const { locator, rerender } = await render(ProductComponent, {
    inputs: { name: 'Laptop', price: 1299.99 },
  });

  await expect.element(locator.getByText('Laptop')).toBeVisible();
  await expect.element(locator.getByText(/Price: \$1299\.99/)).toBeVisible();

  // Partial update — only the specified inputs change
  await rerender({ price: 999.99 });
  await expect.element(locator.getByText(/Price: \$999\.99/)).toBeVisible();
  await expect.element(locator.getByText('Laptop')).toBeVisible();
});
```

You can also pass `WritableSignal` values to keep the binding reactive:

```ts
test('rerender with signals', async () => {
  const { locator, rerender } = await render(ProductComponent, {
    inputs: { name: 'Laptop', price: 1299.99 },
  });

  const price$ = signal(799.99);
  await rerender({ price: price$ });

  await expect.element(locator.getByText(/Price: \$799\.99/)).toBeVisible();

  // Update the signal — the component updates once change detection runs
  price$.set(649.99);
  await expect.element(locator.getByText(/Price: \$649\.99/)).toBeVisible();
});
```

`rerender` is not available when using `withRouting`.

### Model Inputs (Two-way Binding)

[`model()`](https://angular.dev/guide/signals/model) inputs are supported with automatic two-way binding. Pass a `WritableSignal` (e.g. `signal(...)`) as the value — `render()` binds the signal's current value to the component and writes any value the component emits back into the same signal, mirroring Angular's `[(count)]` template syntax.

```ts
import { Component, model } from '@angular/core';
import { signal } from '@angular/core';
import { test, expect } from 'vitest';
import { render } from 'vitest-browser-angular';

@Component({
  selector: 'app-counter',
  template: `
    <p data-testid="count">Count: {{ count() }}</p>
    <button (click)="count.update(v => v + 1)">Increment</button>
  `,
})
export class CounterComponent {
  count = model(0);
}

test('two-way model binding', async () => {
  const count = signal(0);
  const { locator } = await render(CounterComponent, {
    inputs: { count },
  });

  await expect.element(locator.getByTestId('count')).toHaveTextContent('Count: 0');

  // The component updates the model — the value is written back to `count`
  await locator.getByRole('button', { name: 'Increment' }).click();
  expect(count()).toBe(1);
  await expect.element(locator.getByTestId('count')).toHaveTextContent('Count: 1');
});
```

You can also pass a plain value to set the model's initial state; in that case there is no source signal to write back to.

## Outputs

Subscribe to component outputs using the `outputs` option:

```ts
import { Component, output } from '@angular/core';
import { vi } from 'vitest';

@Component({
  template: '<button (click)="send.emit()">Send</button>',
  standalone: true,
})
export class MessageComponent {
  send = output<void>();
}

test('render with outputs', async () => {
  const sendHandler = vi.fn();
  const { locator } = await render(MessageComponent, {
    outputs: {
      send: sendHandler,
    },
  });

  await locator.getByRole('button', { name: 'Send' }).click();
  expect(sendHandler).toHaveBeenCalled();
});
```

Handlers receive the emitted value, so you can assert on the payload:

```ts
@Component({
  template: '<button (click)="save.emit({ id: 1 })">Save</button>',
  standalone: true,
})
export class SaveComponent {
  save = output<{ id: number }>();
}

test('assert on output payload', async () => {
  const saveHandler = vi.fn();
  const { locator } = await render(SaveComponent, {
    outputs: {
      save: saveHandler,
    },
  });

  await locator.getByRole('button', { name: 'Save' }).click();
  expect(saveHandler).toHaveBeenCalledWith({ id: 1 });
});
```

Works with signal-based outputs (`output()`).

When using `withRouting`, outputs cannot be passed directly to `render()`.

## Routing

### Simple Routing

Enable routing with `withRouting: true` for components that use routing features but don't require specific route configuration:

```ts
import { test, expect } from 'vitest';
import { render } from 'vitest-browser-angular';
import { Component } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';

@Component({
  template: `
    <nav>
      <a routerLink="/home">Home</a>
      <a routerLink="/about">About</a>
    </nav>
    <router-outlet></router-outlet>
  `,
  imports: [RouterLink, RouterOutlet],
})
export class RoutedComponent {}

test('render with simple routing', async () => {
  const screen = await render(RoutedComponent, {
    withRouting: true,
  });

  await expect.element(screen.getByText('Home')).toBeVisible();
  await expect.element(screen.getByText('About')).toBeVisible();
});
```

### Routing with Configuration

Configure specific routes and optionally set an initial route:

```ts
import { test, expect } from 'vitest';
import { render } from 'vitest-browser-angular';
import { Component, inject } from '@angular/core';
import { Router, RouterLink, RouterOutlet, Routes } from '@angular/router';

@Component({
  template: '<h1>Home Page</h1>',
})
export class HomeComponent {}

@Component({
  template: '<h1>About Page</h1>',
  standalone: true,
})
export class AboutComponent {}

@Component({
  template: `
    <nav>
      <a routerLink="/home">Home</a>
      <a routerLink="/about">About</a>
    </nav>
    <router-outlet></router-outlet>
  `,
  imports: [RouterLink, RouterOutlet],
  standalone: true,
})
export class AppComponent {
  router = inject(Router);
}

const routes: Routes = [
  { path: 'home', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: '', redirectTo: '/home', pathMatch: 'full' },
];

test('render with route configuration', async () => {
  const { locator, routerHarness, router } = await render(AppComponent, {
    withRouting: {
      routes,
      initialRoute: '/home',
    },
  });

  await expect.element(locator).toHaveTextContent('Home Page');

  // Navigate programmatically (prefer routerHarness over router)
  await routerHarness.navigateByUrl('/about');
  await expect.element(locator).toHaveTextContent('About Page');

  // Use router to inspect state
  expect(router.url).toBe('/about');
});
```

### Route Params

When rendering a routed component, `componentClassInstance` provides access to the actual component instance with full routing context:

```ts
import { Component, inject } from '@angular/core';
import { ActivatedRoute, Routes } from '@angular/router';

@Component({
  template: '<h1>User: {{ userId }}</h1>',
})
export class UserComponent {
  private route = inject(ActivatedRoute);
  userId = this.route.snapshot.params['id'];
}

test('access route params', async () => {
  const routes: Routes = [{ path: 'user/:id', component: UserComponent }];

  const { componentClassInstance } = await render(UserComponent, {
    withRouting: {
      routes,
      initialRoute: '/user/42',
    },
  });

  expect(componentClassInstance.userId).toBe('42');
});
```

### Passing Inputs via Route Data

By default, `withComponentInputBinding()` is enabled, which automatically binds route `data`, route params, and query params to matching component inputs. This works with both signal inputs (`input()`) and `@Input()` decorators:

```ts
import { Component, input } from '@angular/core';
import { Routes } from '@angular/router';

@Component({
  template: `
    <h2>{{ name() }}</h2>
    <p>Age: {{ age() }}</p>
    <p>Role: {{ role() }}</p>
  `,
})
export class ProfileComponent {
  name = input('Guest');
  age = input(0);
  role = input('user');
}

test('pass inputs via route data', async () => {
  const routes: Routes = [
    {
      path: 'profile',
      component: ProfileComponent,
      data: {
        name: 'Jane Doe',
        age: 30,
        role: 'admin',
      },
    },
  ];

  const { locator, componentClassInstance } = await render(ProfileComponent, {
    withRouting: {
      routes,
      initialRoute: '/profile',
    },
  });

  // Inputs are automatically bound from route data
  expect(componentClassInstance.name()).toBe('Jane Doe');
  expect(componentClassInstance.age()).toBe(30);
  expect(componentClassInstance.role()).toBe('admin');

  await expect.element(locator.getByText('Jane Doe')).toBeVisible();
});
```

### Disabling Input Binding

If you need to manually handle route data via `ActivatedRoute` instead of automatic input binding, use `disableInputBinding`:

```ts
test('disable automatic input binding', async () => {
  const routes: Routes = [
    {
      path: 'profile',
      component: ProfileComponent,
      data: { name: 'Jane Doe' },
    },
  ];

  const { componentClassInstance } = await render(ProfileComponent, {
    withRouting: {
      routes,
      initialRoute: '/profile',
      disableInputBinding: true, // Inputs will NOT be bound from route data
    },
  });

  // Inputs retain their default values
  expect(componentClassInstance.name()).toBe('Guest');
});
```

## HTTP Testing

Enable Angular's `HttpClient` testing support with the `withHttp` option. When enabled, the render result exposes an `httpTesting` instance (`HttpTestingController`) you can use to assert on outgoing requests and flush mocked responses — there's no need to manually wire up `provideHttpClient`/`provideHttpClientTesting`.

### Basic HTTP Testing

Enable HTTP testing with `withHttp: true`:

```ts
import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { test, expect } from 'vitest';
import { render } from 'vitest-browser-angular';

@Component({
  template: `
    <h1 data-testid="title">{{ data()?.title ?? '' }}</h1>
    <button data-testid="fetch" (click)="load()">Fetch</button>
  `,
})
export class HttpDemoComponent {
  private http = inject(HttpClient);
  data = signal<{ title: string } | null>(null);

  load() {
    this.http.get<{ title: string }>('/api/data').subscribe(res => this.data.set(res));
  }
}

test('mocks an HTTP response', async () => {
  const { locator, httpTesting } = await render(HttpDemoComponent, {
    withHttp: true,
  });

  await locator.getByTestId('fetch').click();

  const req = httpTesting.expectOne('/api/data');
  expect(req.request.method).toBe('GET');
  req.flush({ title: 'Hello HTTP' });

  await expect.element(locator.getByTestId('title')).toHaveTextContent('Hello HTTP');
});
```

### HTTP with Interceptors

Pass an `HttpConfig` with custom interceptors to register them via Angular's `withInterceptors`:

```ts
import { HttpInterceptorFn } from '@angular/common/http';

const authInterceptor: HttpInterceptorFn = (req, next) =>
  next(req.clone({ setHeaders: { 'X-Custom': 'test-value' } }));

test('applies custom interceptors', async () => {
  const { locator, httpTesting } = await render(HttpDemoComponent, {
    withHttp: { interceptors: [authInterceptor] },
  });

  await locator.getByTestId('fetch').click();

  const req = httpTesting.expectOne('/api/data');
  expect(req.request.headers.get('X-Custom')).toBe('test-value');
  req.flush({ title: 'Intercepted' });

  await expect.element(locator.getByTestId('title')).toHaveTextContent('Intercepted');
});
```

When `withHttp` is omitted, `httpTesting` is `undefined` and `HttpClient` is not configured.

## Defer Blocks

Angular's `@defer` blocks defer part of the template until a trigger fires (idle, interaction, viewport, timer, ...). By default, `render()` keeps defer blocks **paused** (`deferBlockBehavior: DeferBlockBehavior.Manual`), so deferred content is not rendered and triggers don't fire on their own. This gives you deterministic control over when each block transitions.

```html
<div>
  @defer {
    <p>Deferred content</p>
  } @placeholder {
    <p>Placeholder</p>
  } @loading {
    <p>Loading...</p>
  }
</div>
```

### Render a defer block

Use `renderDeferBlock(state, index?)` on the render result to transition one (or all) defer blocks. The available states come from `DeferBlockState` (imported from `@angular/core/testing`): `Placeholder`, `Loading`, `Complete`, and `Error`.

Render **all** blocks in a given state:

```ts
import { DeferBlockState } from '@angular/core/testing';
import { test, expect } from 'vitest';
import { render } from 'vitest-browser-angular';

test('renders deferred content', async () => {
  const { locator, renderDeferBlock } = await render(DeferDemoComponent);

  await expect.element(locator.getByTestId('placeholder')).toBeVisible();

  await renderDeferBlock(DeferBlockState.Complete);

  await expect.element(locator.getByTestId('deferred')).toBeVisible();
});
```

Target a **single** block by its index (matching the order of `fixture.getDeferBlocks()`):

```ts
test('renders only the first defer block', async () => {
  const { locator, renderDeferBlock } = await render(DeferDemoComponent);

  await renderDeferBlock(DeferBlockState.Loading, 0);

  await expect.element(locator.getByTestId('loading')).toBeVisible();
});
```

Passing an index with no matching block throws an error.

### Set initial states at render time

The `deferBlockStates` option applies a state (or several) right after render. Pass a single `DeferBlockState` to apply to every block:

```ts
const { locator } = await render(DeferDemoComponent, {
  deferBlockStates: DeferBlockState.Complete,
});

await expect.element(locator.getByTestId('deferred')).toBeVisible();
```

Or an array of `{ deferBlockState, deferBlockIndex }` to target specific blocks:

```ts
const { locator } = await render(DeferDemoComponent, {
  deferBlockStates: [{ deferBlockState: DeferBlockState.Loading, deferBlockIndex: 0 }],
});

await expect.element(locator.getByTestId('loading')).toBeVisible();
```

### Playthrough mode

Set `deferBlockBehavior: DeferBlockBehavior.Playthrough` to let defer triggers fire naturally, like in a real browser:

```ts
import { Component } from '@angular/core';
import { DeferBlockBehavior } from '@angular/core/testing';
import { test, expect } from 'vitest';
import { render } from 'vitest-browser-angular';

@Component({
  template: `
    <div>
      @defer (on interaction) {
        <p data-testid="deferred">Interaction content</p>
      } @placeholder {
        <button data-testid="show">Show</button>
      }
    </div>
  `,
})
export class DeferInteractionComponent {}

test('plays through interaction triggers', async () => {
  const { locator } = await render(DeferInteractionComponent, {
    deferBlockBehavior: DeferBlockBehavior.Playthrough,
  });

  await locator.getByTestId('show').click();
  await expect
    .element(locator.getByTestId('deferred'))
    .toHaveTextContent('Interaction content');
});
```

`renderDeferBlock` and `deferBlockStates` work in both modes. `renderDeferBlock` is available on every render result: `render()`, routed `render()` (with `withRouting`), and `renderDirective()`.

## Component Providers

If you need to replace a [component provider](https://angular.dev/guide/di/defining-dependency-providers#component-or-directive-providers) declared on the component itself (e.g. to mock a service), use the `overrideProvidersComponent` option.

`replace` must match the exact provider shape declared by the component: the same object shape (same `provide`/`useClass`/`useValue`/`useFactory` keys) or the bare class when the component uses the shorthand `providers: [Foo]`. `with` is a full provider that typically provides the same token as `replace`.

```ts
@Component({
  template: '<h1 data-testid="greeting">{{ greeting }}</h1>',
  providers: [GreetingService],
})
export class HelloWorldComponent {
  private greetingService = inject(GreetingService);
  greeting = this.greetingService.getGreeting('World');
}

test('replaces component service provider with a mock', async () => {
  const screen = await render(HelloWorldComponent, {
    overrideProvidersComponent: [
      { replace: GreetingService, with: { provide: GreetingService, useClass: FakeGreetingService } },
    ],
  });

  await expect.element(screen.getByTestId('greeting')).toHaveTextContent('Fake Greeting');
});
```

For overriding a `providedIn: 'root'` service (provided at root/test level, where the last provider wins), use the `providers` option instead.

## Directives

Use `renderDirective` to test both **attribute** and **structural** directives. It wraps the directive in a generated host component and renders the provided `template`, so you can drive the directive with real DOM events and assert against the host element.

```ts
import { Directive, input, output } from '@angular/core';
import { test, expect } from 'vitest';
import { renderDirective } from 'vitest-browser-angular';

@Directive({
  selector: '[appHighlight]',
  host: { '[style.color]': 'color()' },
})
export class HighlightDirective {
  color = input('black');
  blurred = output<FocusEvent>();
}

test('renders directive', async () => {
  const { directiveInstance, locator } = await renderDirective(HighlightDirective, {
    template: `<button appHighlight>Test</button>`,
  });

  expect(directiveInstance.color()).toBe('black');
  await expect.element(locator.getByText('Test')).toBeVisible();
});
```

The `template` **must** include the directive selector otherwise an error will be thrown.

### Host Props

Pass reactive values and handlers to the template through `hostProps`. Each property is assigned onto the host component instance, so you can reference it directly in the template binding:

```ts
import { signal } from '@angular/core';

test('binds host inputs and outputs', async () => {
  const color = signal('red');
  const onBlur = vi.fn();

  const { getByText } = await renderDirective(HighlightDirective, {
    template: `<button appHighlight [color]="color()" (blurred)="onBlur($event)">Test</button>`,
    hostProps: { color, onBlur },
  });

  expect(getByText('Test')).toHaveStyle({ color: 'rgb(255, 0, 0)' });

  color.set('blue');
  await expect.element(getByText('Test')).toHaveStyle({ color: 'rgb(0, 0, 255)' });
});
```

Signals passed via `hostProps` keep the binding reactive — updating them propagates to the directive once change detection runs.

### Imports and Providers

Pass additional modules (pipes, directives, components used in the template) via `imports`, and register DI providers via `providers`:

```ts
import { JsonPipe } from '@angular/common';

const { getByText } = await renderDirective(HighlightDirective, {
  template: `<button appHighlight>{{ color() | json }}</button>`,
  imports: [JsonPipe],
  providers: [{ provide: SomeService, useValue: fakeService }],
});
```

### Structural directives

Structural directives work out of the box: `renderDirective` finds the directive on its `<ng-template>` anchor even though it never appears as a real element in the DOM.

```ts
import { Directive, effect, input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({ selector: '[appUnless]' })
export class UnlessDirective {
  readonly unless = input(false, { alias: 'appUnless' });
  private templateRef = inject(TemplateRef);
  private vcr = inject(ViewContainerRef);
  constructor() {
    effect(() => {
      this.vcr.clear();
      if (!this.unless()) this.vcr.createEmbeddedView(this.templateRef);
    });
  }
}

test('renders a structural directive', async () => {
  const show = signal(false);
  const { directiveInstance, container, hostFixture } = await renderDirective(UnlessDirective, {
    template: `<div *appUnless="show()">Hidden content</div>`,
    hostProps: { show },
  });

  expect(container.textContent).toContain('Hidden content');

  show.set(true);
  await hostFixture.whenStable();
  expect(container.textContent).not.toContain('Hidden content');
});
```

### Render options

`renderDirective` forwards the same render options as `render` (except routing, `inputs`, `outputs` and `inferTagName`):

- `overrideImportsDirective` / `overrideProvidersDirective` — override the tested directive's `imports`/`providers` metadata to mock its dependencies.

### Result

The render result mirrors `render` and adds directive-specific helpers:

- `directiveInstance` — the instance of the tested directive, resolved from the host element's injector.
- `locator` — Vitest browser locator scoped to the host component's container.
- `fixture` — the host component's `ComponentFixture`.
- `container` / `baseElement` — the rendered elements.
- `debug` — pretty-print the DOM for debugging.
- `inject` — resolve dependencies from the **directive's** injector (also works for providers declared on the directive itself).
- `httpTesting` — the `HttpTestingController` when `withHttp` is enabled.
- `getByRole`, `getByText`, … — the standard `LocatorSelectors` scoped to `baseElement`.

## Dependency Injection

The render result provides an `inject` method to resolve dependencies from the rendered component's injector. Unlike `TestBed.inject()`, which only resolves from the root injector, `result.inject()` starts at the component injector and falls back up the hierarchy — so it works with both global `providers` and component-level providers.

```ts
import { Component, inject, Injectable } from '@angular/core';
import { test, expect } from 'vitest';
import { render } from 'vitest-browser-angular';

@Injectable()
class AnalyticsService {
  track(event: string) {
    return `tracked: ${event}`;
  }
}

@Component({
  template: '<button (click)="click()">Click me</button>',
  providers: [AnalyticsService],
})
class TrackedComponent {
  private analytics = inject(AnalyticsService);
  click() {
    this.analytics.track('click');
  }
}

test('resolves a component-level service', async () => {
  const { inject } = await render(TrackedComponent);

  const analytics = inject(AnalyticsService);
  expect(analytics.track('click')).toBe('tracked: click');
});
```

`inject` also works with `InjectionToken`:

```ts
import { InjectionToken } from '@angular/core';

const API_URL = new InjectionToken<string>('api-url');

test('resolves an InjectionToken', async () => {
  const { inject } = await render(TrackedComponent, {
    providers: [{ provide: API_URL, useValue: 'https://api.example.com' }],
  });

  expect(inject(API_URL)).toBe('https://api.example.com');
});
```

This is particularly useful for asserting on service state after user interactions, without having to expose the service on the component class.

## Schema Options

When your component template references elements that Angular does not recognise — such as web components (`<my-widget>`) or child components you do not want to import in the test — you can pass `NO_ERRORS_SCHEMA` or `CUSTOM_ELEMENTS_SCHEMA` via the `schema` option:

```ts
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { test, expect } from 'vitest';
import { render } from 'vitest-browser-angular';

@Component({
  template: '<my-widget [config]="options"></my-widget>',
})
class ConsumerComponent {
  options = { theme: 'dark' };
}

test('ignores unknown elements with NO_ERRORS_SCHEMA', async () => {
  const { container } = await render(ConsumerComponent, {
    schema: NO_ERRORS_SCHEMA,
  });

  expect(container.querySelector('my-widget')).toBeTruthy();
});
```

Without the schema option, Angular would throw a compile-time error for unknown elements. This is a quick escape hatch when you want to isolate the component under test without mocking every child dependency.

## Clean DOM

By default Angular adds the `ng-version` attribute to the root element of the rendered component. In some cases — such as DOM snapshot assertions or visual regression testing — you may want a cleaner DOM without framework-specific attributes.

Pass `removeAngularAttributes: true` to strip `ng-version` after render:

```ts
import { test, expect } from 'vitest';
import { render } from 'vitest-browser-angular';

@Component({
  template: '<h1>Hello</h1>',
})
class SimpleComponent {}

test('renders a clean DOM', async () => {
  const { container } = await render(SimpleComponent, {
    removeAngularAttributes: true,
  });

  expect(container.hasAttribute('ng-version')).toBe(false);
  expect(container.innerHTML).toContain('<h1>Hello</h1>');
});
```

The attribute is removed after Angular completes its initial change detection, so the component behaves normally — only the DOM output is cleaned up.

## Infer Tag Name

By default, `render()` mounts the component on a `<div>` host element. Pass `inferTagName: true` to use the component's selector as the host tag name instead:

```ts
import { Component } from '@angular/core';
import { test, expect } from 'vitest';
import { render } from 'vitest-browser-angular';

@Component({
  selector: 'app-hello-world',
  template: '<h1>Hello World</h1>',
})
export class HelloWorld {}

test('uses the component selector as the host tag', async () => {
  const { container } = await render(HelloWorld, {
    inferTagName: true,
  });

  expect(container.tagName).toBe('APP-HELLO-WORLD');
});
```

This can be useful when the host element's tag name matters — for example when testing styles that target a specific element selector or when asserting on the DOM structure. `inferTagName` only works when `withRouting` is not enabled.


## Code Of Conduct

Be kind to each other and please read our [code of conduct](CODE_OF_CONDUCT.md).

<br/>

## Credits

This project is inspired by the following projects:

[vitest-browser-vue](https://github.com/vitest-dev/vitest-browser-vue)
[angular-testing-library](https://github.com/testing-library/angular-testing-library)

## License

MIT
