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.