Skip to main content

Testing a standalone attribute directive using the Angular testbed

For the purpose of this example, we will create a standalone attribute directive that adds the CSS classes provided by Primer for its Button component:

primer-button.directive.ts
import { Directive, HostBinding, Input } from "@angular/core";

export type PrimerButtonVariant = "default" | "primary" | "danger" | "outline" | "invisible";
type PrimerButtonVariantClass = `btn-${Exclude<PrimerButtonVariant, "default">}`;

export type PrimerButtonSize = "small" | "medium" | "large";
type PrimerButtonSizeClass = "btn-sm" | "btn-large";

@Directive({
exportAs: "primerButton",
selector: "[primerButton]",
standalone: true,
})
export class PrimerButtonDirective {
get #sizeClass(): PrimerButtonSizeClass | null {
switch (this.size) {
case "small":
return "btn-sm";
case "large":
return "btn-large";
case "medium":
// Fall through
default:
return null;
}
}
get #variantClass(): PrimerButtonVariantClass | null {
return this.variant === "default" ? null : `btn-${this.variant}`;
}

@Input()
size: PrimerButtonSize = "medium";
@Input()
variant: PrimerButtonVariant = "default";

@HostBinding("class.btn")
protected get baseClassAdded(): true {
return true;
}
@HostBinding("className")
protected get className(): string {
return [this.#sizeClass, this.#variantClass].filter((className) => className !== null).join("");
}
}

Creating a test host component for a standalone attribute directive

To interact with a standalone component through its component API, we add it to the test host component's imports array:

primer-button.directive.spec.ts
import { Component, Input } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";

import { PrimerButtonDirective, PrimerButtonSize, PrimerButtonVariant } from "./primer-button.directive";

describe(PrimerButtonDirective.name, () => {
@Component({
imports: [PrimerButtonDirective],
standalone: true,
template: `<button primerButton [size]="size" [variant]="variant">Button</button>`,
})
class TestHostComponent {
@Input()
size: PrimerButtonSize = "medium";
@Input()
variant: PrimerButtonVariant = "default";
}

beforeEach(() => {
hostFixture = TestBed.createComponent(TestHostComponent);
host = hostFixture.componentInstance;
hostFixture.autoDetectChanges();
buttonElement = hostFixture.debugElement.query(By.css("button")).nativeElement;
});

let buttonElement: HTMLButtonElement;
let hostFixture: ComponentFixture<TestHostComponent>;
let host: TestHostComponent;
});

Exercising input properties in standalone attribute directive tests

We use the bound properties of our test host component to exercise our attribute directive's input properties:

primer-button.directive.spec.ts
describe(PrimerButtonDirective.name, () => {
// (...)

it("adds the base class", () => {
expect(buttonElement.classList.contains("btn")).toBe(true);
});

it("adds a size class", () => {
host.size = "large";
hostFixture.detectChanges();

expect(buttonElement.classList.contains("btn-large")).toBe(true);
});

it("adds a variant class", () => {
host.variant = "primary";
hostFixture.detectChanges();

expect(buttonElement.classList.contains("btn-primary")).toBe(true);
});
});

Our attribute directive tests verify the attribute directive's side effects by inspecting the DOM of its host element.