Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
53c0928
Cover experimental layout-pattern internals
fpigeonjr Sep 2, 2026
fe5edaf
Cover the autocomplete component family
fpigeonjr Sep 2, 2026
e5ab643
Cover pipes, dom helpers, and fa-icon shared utils
fpigeonjr Sep 2, 2026
20012c0
Cover phone-entry and international-phone form templates
fpigeonjr Sep 2, 2026
cea12da
Cover components, pagination, and aria primitives
fpigeonjr Sep 2, 2026
31c2cb6
Cover date-range validators and upload-v2 edge paths
fpigeonjr Sep 2, 2026
20a2e5e
Cover label/fieldset wrappers and sam-telephone
fpigeonjr Sep 2, 2026
16c3e81
Cover image, textarea, page service, and fa-icon error helpers
fpigeonjr Sep 2, 2026
e6bfbf2
Cover selected-item model and results message clamp
fpigeonjr Sep 2, 2026
09281a8
Replace 'as any' test casts with 'as never' and typed member access
fpigeonjr Sep 2, 2026
6474613
Ratchet coverage floor to 90%+ QASP target
fpigeonjr Sep 2, 2026
e73083a
Add two fa-icon specs that a global gitignore excluded
fpigeonjr Sep 2, 2026
22e328d
Cover the default-cache short-circuit in autocomplete-cache directly
fpigeonjr Sep 2, 2026
4aadac0
Make key-helper and overlay-ref branch coverage order-independent
fpigeonjr Sep 2, 2026
445276c
Fix three latent defects surfaced by Copilot review
fpigeonjr Sep 2, 2026
e820295
Merge branch 'master' into gh-669-close-remaining-coverage-gap-to-90-…
fpigeonjr Sep 10, 2026
01cee07
Merge remote-tracking branch 'origin/master' into gh-669-close-remain…
fpigeonjr Sep 10, 2026
b2235ae
Merge remote-tracking branch 'origin/master' into gh-669-close-remain…
fpigeonjr Sep 21, 2026
8aeaa07
Fix specs broken by merging origin/master (inject() migration + lint …
fpigeonjr Sep 21, 2026
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
8 changes: 4 additions & 4 deletions coverage-floor.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"statements": 88.73,
"branches": 78.65,
"functions": 85.9,
"lines": 88.72
"statements": 95.6,
"branches": 90.09,
"functions": 92.78,
"lines": 95.67
}
16 changes: 16 additions & 0 deletions src/ui-kit/components/banner/banner.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,20 @@ describe("The Sam Banner component", () => {
component.toggleDetails();
expect(component.showDetail).toBe(true);
});

it("should close the detail when it is open", () => {
component.toggleDetails();
expect(component.showDetail).toBe(true);

component.closeDetail();

expect(component.showDetail).toBe(false);
});

it("should do nothing when the detail is already closed", () => {
expect(component.showDetail).toBe(false);

expect(() => component.closeDetail()).not.toThrow();
expect(component.showDetail).toBe(false);
});
});
156 changes: 156 additions & 0 deletions src/ui-kit/components/data-table/sort-header.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { ChangeDetectorRef } from "@angular/core";
import { Subject } from "rxjs";
import {
SamSortHeaderComponent,
SamSortHeaderIntl,
} from "./sort-header.component";
import { SamSortDirective } from "./sort.directive";
import { CdkColumnDef } from "@angular/cdk/table";
import { constructWithInjector } from "../../../testing/construct-with-injector";

describe("SamSortHeaderIntl", () => {
it("returns the id as the button label", () => {
const intl = new SamSortHeaderIntl();
expect(intl.sortButtonLabel("name")).toBe("name");
});

it("describes ascending sort", () => {
const intl = new SamSortHeaderIntl();
expect(intl.sortDescriptionLabel("name", "asc")).toBe(
"Sorted by name ascending"
);
});

it("describes descending sort", () => {
const intl = new SamSortHeaderIntl();
expect(intl.sortDescriptionLabel("name", "desc")).toBe(
"Sorted by name descending"
);
});
});

describe("SamSortHeaderComponent", () => {
function createSort(overrides: Partial<SamSortDirective> = {}) {
return {
active: "",
direction: "",
samSortChange: new Subject<void>(),
register: vi.fn(),
deregister: vi.fn(),
sort: vi.fn(),
...overrides,
} as unknown as SamSortDirective;
}

function createComponent(
sort: SamSortDirective,
cdkColumnDef: Partial<CdkColumnDef> | null = null
) {
const cdr = { markForCheck: vi.fn() } as unknown as ChangeDetectorRef;
return constructWithInjector(
[
{ provide: SamSortHeaderIntl, useValue: new SamSortHeaderIntl() },
{ provide: ChangeDetectorRef, useValue: cdr },
{ provide: SamSortDirective, useValue: sort },
{ provide: CdkColumnDef, useValue: cdkColumnDef },
],
() => new SamSortHeaderComponent()
);
}

it("subscribes to the sort directive's samSortChange and marks for check", () => {
const sort = createSort();
const header = createComponent(sort);
const cdrSpy = vi.spyOn(header["_changeDetectorRef"], "markForCheck");
(sort.samSortChange as Subject<void>).next();
expect(cdrSpy).toHaveBeenCalled();
});

it("defaults the id from the containing CdkColumnDef when none is provided", () => {
const sort = createSort();
const header = createComponent(sort, { name: "columnName" });
header.ngOnInit();
expect(header.id).toBe("columnName");
expect(sort.register).toHaveBeenCalledWith(header);
});

it("keeps an explicitly-set id rather than defaulting from the column def", () => {
const sort = createSort();
const header = createComponent(sort, { name: "columnName" });
header.id = "explicitId";
header.ngOnInit();
expect(header.id).toBe("explicitId");
});

it("registers with the sort directive on init", () => {
const sort = createSort();
const header = createComponent(sort);
header.id = "name";
header.ngOnInit();
expect(sort.register).toHaveBeenCalledWith(header);
});

it("deregisters and unsubscribes on destroy", () => {
const sort = createSort();
const header = createComponent(sort);
header.id = "name";
header.ngOnInit();
const unsubscribeSpy = vi.spyOn(header.sortSubscription, "unsubscribe");
header.ngOnDestroy();
expect(sort.deregister).toHaveBeenCalledWith(header);
expect(unsubscribeSpy).toHaveBeenCalled();
});

it("reports sorted when this header's id matches the active sort and a direction is set", () => {
const sort = createSort({ active: "name", direction: "asc" });
const header = createComponent(sort);
header.id = "name";
expect(header._isSorted()).toBeTruthy();
});

it("reports not sorted when a different header is active", () => {
const sort = createSort({ active: "other", direction: "asc" });
const header = createComponent(sort);
header.id = "name";
expect(header._isSorted()).toBeFalsy();
});

it("reports not sorted when the direction is empty even if this header is active", () => {
const sort = createSort({ active: "name", direction: "" });
const header = createComponent(sort);
header.id = "name";
expect(header._isSorted()).toBeFalsy();
});

it("samSortHeaderSorted() reflects _isSorted()", () => {
const sort = createSort({ active: "name", direction: "asc" });
const header = createComponent(sort);
header.id = "name";
expect(header.samSortHeaderSorted()).toBeTruthy();
});

it("delegates hostClick to the sort directive when not disabled", () => {
const sort = createSort();
const header = createComponent(sort);
header.disabled = false;
header.hostClick();
expect(sort.sort).toHaveBeenCalledWith(header);
});

it("does nothing on hostClick when disabled", () => {
const sort = createSort();
const header = createComponent(sort);
header.disabled = true;
header.hostClick();
expect(sort.sort).not.toHaveBeenCalled();
});

it("coerces disableClear through coerceBooleanProperty", () => {
const sort = createSort();
const header = createComponent(sort);
header.disableClear = "" as never;
expect(header.disableClear).toBe(true);
header.disableClear = false;
expect(header.disableClear).toBe(false);
});
});
72 changes: 72 additions & 0 deletions src/ui-kit/components/header-next/header.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,76 @@ describe("SamHeaderNextComponent", () => {
"notification icon exists"
);
});

describe("openMobileNav() / closeMobileNav() / navAnimationEnd()", () => {
beforeEach(() => {
fixture.detectChanges();
});

it("activates the mobile nav", () => {
component.openMobileNav();
expect(component.mobileNavActive).toBe(true);
});

it("deactivates the mobile nav and refocuses the open-nav button", () => {
component.mobileNavActive = true;
const focusSpy = vi.spyOn(component.openNavBtn.nativeElement, "focus");

component.closeMobileNav();

expect(component.mobileNavActive).toBe(false);
expect(focusSpy).toHaveBeenCalled();
});

it("focuses the close-nav button once the open animation ends", () => {
const focusSpy = vi.spyOn(component.closeNavBtn.nativeElement, "focus");

component.navAnimationEnd();

expect(focusSpy).toHaveBeenCalled();
});
});

describe("onBrowserResize()", () => {
beforeEach(() => {
fixture.detectChanges();
});

it("deactivates the mobile nav when it's active and the close button is no longer visible", () => {
component.mobileNavActive = true;
vi.spyOn(
component.closeNavBtn.nativeElement,
"getBoundingClientRect"
).mockReturnValue({ width: 0 } as DOMRect);

component.onBrowserResize({} as Event);

expect(component.mobileNavActive).toBe(false);
});

it("leaves the mobile nav active when the close button is still visible", () => {
component.mobileNavActive = true;
vi.spyOn(
component.closeNavBtn.nativeElement,
"getBoundingClientRect"
).mockReturnValue({ width: 40 } as DOMRect);

component.onBrowserResize({} as Event);

expect(component.mobileNavActive).toBe(true);
});

it("does nothing when the mobile nav is not active", () => {
component.mobileNavActive = false;
const getRectSpy = vi.spyOn(
component.closeNavBtn.nativeElement,
"getBoundingClientRect"
);

component.onBrowserResize({} as Event);

expect(getRectSpy).not.toHaveBeenCalled();
expect(component.mobileNavActive).toBe(false);
});
});
});
78 changes: 78 additions & 0 deletions src/ui-kit/components/image/image.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,82 @@ describe("The Sam Image Component", () => {
expect(overEvent.stopPropagation).toHaveBeenCalled();
expect(overEvent.preventDefault).toHaveBeenCalled();
});

it("does not toggle edit mode when the component is not editable", () => {
component.editable = false;
fixture.detectChanges();
const before = component.editMode;

de.query(By.css("button.edit-button")).nativeElement.dispatchEvent(
new Event("click")
);

expect(component.editMode).toBe(before);
});

it("does not emit fileChange on save when no temporary image is staged", () => {
const emitted = vi.fn();
component.fileChange.subscribe(emitted);

de.query(By.css("button.save-button")).nativeElement.dispatchEvent(
new Event("click")
);

expect(emitted).not.toHaveBeenCalled();
});

it("truncates a long file name in the file picker label", () => {
const file = new File(["hello"], "a-very-long-file-name.png", {
type: "image/png",
});
const fileInputEl: HTMLInputElement = de.query(
By.css("input#file")
).nativeElement;
Object.defineProperty(fileInputEl, "files", { value: [file] });
fileInputEl.dispatchEvent(new Event("change"));

expect(component.generateFilePickerLabelText()).toBe("a-very-l...");
});

it("falls back to placeholder label text when no file is staged", () => {
expect(component.generateFilePickerLabelText()).toBe("Select a file");
});

it("labels the done button 'Save' only while an image is staged", () => {
expect(component.generateDoneText()).toBe("Done");

const file = new File(["hello"], "x.png", { type: "image/png" });
const fileInputEl: HTMLInputElement = de.query(
By.css("input#file")
).nativeElement;
Object.defineProperty(fileInputEl, "files", { value: [file] });
fileInputEl.dispatchEvent(new Event("change"));

expect(component.generateDoneText()).toBe("Save");
});

it("prefers the staged image over the committed src", () => {
expect(component.generateSrc()).toBe(washingtonImg);

const file = new File(["hello"], "x.png", { type: "image/png" });
const fileInputEl: HTMLInputElement = de.query(
By.css("input#file")
).nativeElement;
Object.defineProperty(fileInputEl, "files", { value: [file] });
fileInputEl.dispatchEvent(new Event("change"));

expect(component.generateSrc()).toContain("data:fake");
});

it("hides the edit button when not editable or already editing", () => {
component.editable = false;
expect(component.hideEditButton()).toBe(true);

component.editable = true;
component.editMode = true;
expect(component.hideEditButton()).toBe(true);

component.editMode = false;
expect(component.hideEditButton()).toBe(false);
});
});
Loading
Loading