import { ComponentFixture, TestBed, fakeAsync, tick, } from "@angular/core/testing"; import { SamListBoxComponent } from "./listbox.component"; import { By } from "@angular/platform-browser"; import { CommonModule } from "@angular/common"; import { SamWrapperModule } from "../../../ui-kit/wrappers"; function fakeCheckboxEvent(checked: boolean): Event { return { target: { checked } } as unknown as Event; } function fakeKeyboardEvent(key: string): KeyboardEvent { return { key, target: { value: "id" }, preventDefault: () => {}, } as unknown as KeyboardEvent; } const options = [ { name: "id1", value: "test1", label: "test-id1", required: false, checked: false, disabled: false, }, { name: "id2", value: "test2", label: "test-id2", required: true, checked: true, disabled: false, }, { name: "id3", value: "test3", label: "test-id3", required: false, checked: false, disabled: false, }, { name: "id4", value: "test4", label: "test-id4", required: false, checked: false, disabled: false, }, { name: "id5", value: "test5", label: "test-id5", required: false, checked: true, disabled: false, }, { name: "id6", value: "test6", label: "test-id6", required: false, checked: false, disabled: false, }, { name: "id7", value: "test7", label: "test-id7", required: false, checked: false, disabled: false, }, { name: "id8", value: "test8", label: "test-id8", required: false, checked: false, disabled: false, }, ]; // Component mutates option objects in place (e.g. setting `highlighted`), // so each test needs its own deep copy of `options` to avoid leaking // highlighted/selection state between tests. function cloneOptions() { return options.map((option) => ({ ...option })); } describe("SamListBoxComponent", () => { let component: SamListBoxComponent; let fixture: ComponentFixture; beforeEach(() => { TestBed.configureTestingModule({ imports: [SamWrapperModule, CommonModule], declarations: [SamListBoxComponent], }); fixture = TestBed.createComponent(SamListBoxComponent); component = fixture.componentInstance; component.options = cloneOptions(); }); it("on init with singlemode", () => { component.options = cloneOptions(); component.isSingleMode = true; component.ngOnInit(); fixture.detectChanges(); expect(component.model.length).toBe(0); }); it("onCheck with single mode", () => { const ev = fakeCheckboxEvent(true); component.isSingleMode = true; component.options = cloneOptions(); const row = options[6].value; component.onChecked(ev, row); fixture.detectChanges(); expect(component.model.length).toBe(1); }); it("Should have reuslts on focus", fakeAsync(() => { component.options = cloneOptions(); fixture.detectChanges(); tick(); fixture.detectChanges(); const container = fixture.debugElement.query(By.css(".checkbox-container")); expect(container.nativeElement.children.length).toBe(8); })); it("should show a hint message", function () { const hint = "Life pro tip: eat vegetables"; component.hint = hint; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toContain(hint); }); it("should show an error message", function () { const errorMessage = "Uh-oh, something went wrong"; component.errorMessage = errorMessage; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toContain(errorMessage); }); it("should show a label", function () { const labelText = "Pick from the following options"; component.label = labelText; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toContain(labelText); }); it("should disable", function () { component.options[0].disabled = true; fixture.detectChanges(); const value = component.checkboxListElement.nativeElement.getElementsByTagName( "input" )[0]; expect(value.disabled).toBe(true); component.options[0].disabled = false; fixture.detectChanges(); expect(value.disabled).toBe(false); }); it("should implement controlvalueaccessor", () => { component.onChange([]); component.onTouched(); component.registerOnChange(() => undefined); component.registerOnTouched(() => undefined); component.writeValue(["test"]); expect(component.model[0]).toBe("test"); component.writeValue(undefined); expect(component.model.length).toBe(0); }); it("Checking option mode", () => { component.isSingleMode = true; component.ngOnInit(); fixture.detectChanges(); expect(component.optionsMode).toBe("radio"); component.isSingleMode = false; component.ngOnInit(); fixture.detectChanges(); expect(component.optionsMode).toBe("checkbox"); }); it("onChecked checked/unchecked", () => { const ev = fakeCheckboxEvent(true); component.options = cloneOptions(); const row = options[6].value; component.onChecked(ev, row); fixture.detectChanges(); (ev.target as unknown as { checked: boolean }).checked = false; component.onChecked(ev, row); fixture.detectChanges(); }); it("onChecked with the full option object, as the template passes it", () => { // The template's (change) binding calls onChecked($event, option) with // the whole OptionModel, not just its value — exercise that path // directly so insertion order/removal via the object-valued shape stays // covered (isChecked treats both shapes as equivalent). const ev = fakeCheckboxEvent(true); component.options = cloneOptions(); const row = component.options[6]; component.onChecked(ev, row); fixture.detectChanges(); expect(component.model).toContain(row); expect(component.isChecked(row.value)).toBe(true); (ev.target as unknown as { checked: boolean }).checked = false; component.onChecked(ev, row); fixture.detectChanges(); expect(component.model).not.toContain(row); expect(component.isChecked(row.value)).toBe(false); }); it("should process arrow up and down keypresses", fakeAsync(() => { component.options = cloneOptions(); fixture.detectChanges(); tick(); fixture.detectChanges(); const container = fixture.debugElement.query(By.css(".checkbox-container")); expect(container.nativeElement.children.length).toBe(8); const downEvent = fakeKeyboardEvent("Down"); component.onKeyDown(downEvent); tick(); fixture.detectChanges(); expect(component.options[1]["highlighted"]).toBeTruthy(); const upEvent = fakeKeyboardEvent("Up"); component.onKeyDown(upEvent); tick(); fixture.detectChanges(); expect(component.options[0]["highlighted"]).toBeTruthy(); })); it("Up arrow when on first item", fakeAsync(() => { component.options = cloneOptions(); fixture.detectChanges(); component.onHover(0); tick(); fixture.detectChanges(); const list = fixture.debugElement.query(By.css(".checkbox-container")); expect(list.nativeElement.children.length).toBe(8); expect(component.options[0]["highlighted"]).toBeTruthy(); const upEvent = fakeKeyboardEvent("Up"); component.onKeyDown(upEvent); tick(); fixture.detectChanges(); expect(component.options[0]["highlighted"]).toBeTruthy(); })); it("Down arrow when on over lists item", fakeAsync(() => { component.options = cloneOptions(); fixture.detectChanges(); component.onHover(0); tick(); fixture.detectChanges(); expect(component.options[0]["highlighted"]).toBeTruthy(); component.onHover(component.options.length - 1); fixture.detectChanges(); tick(); expect( component.options[component.options.length - 1]["highlighted"] ).toBeTruthy(); const upEvent = fakeKeyboardEvent("Down"); component.onKeyDown(upEvent); tick(); fixture.detectChanges(); expect(component.options[7]["highlighted"]).toBeTruthy(); })); it("Should remove item from selected results", fakeAsync(() => { const ev = fakeCheckboxEvent(false); component.model.push(options[1].value); vi.spyOn(component.modelChange, "emit"); component.onChecked(ev, options[1].value); fixture.detectChanges(); expect(component.model.length).toBe(0); expect(component.modelChange.emit).toHaveBeenCalledWith(component.model); })); it("should not select disabled options when writing a value via setSelectedItem", () => { const optionsWithDisabled = options.map((o, i) => i === 1 ? { ...o, disabled: true } : o ); component.options = optionsWithDisabled; fixture.detectChanges(); component.writeValue([ optionsWithDisabled[6].value, optionsWithDisabled[1].value, ]); expect(component.model).toEqual([optionsWithDisabled[6].value]); }); it("should default to an empty model when writeValue is called without an array", () => { component.options = cloneOptions(); fixture.detectChanges(); component.writeValue(undefined); expect(component.model).toEqual([]); }); it("should report whether a value is currently checked", () => { component.options = cloneOptions(); component.model = [options[2].value]; fixture.detectChanges(); expect(component.isChecked(options[2].value)).toBe(true); expect(component.isChecked(options[3].value)).toBe(false); }); it("marks each listbox option's aria-selected/aria-checked from the canonical model selection, kept in sync as options are (un)checked", () => { component.options = options; fixture.detectChanges(); const listItems = fixture.debugElement.queryAll( By.css('li[role="option"]') ); expect(listItems[1].nativeElement.getAttribute("aria-selected")).toBe( "false" ); expect(listItems[1].nativeElement.getAttribute("aria-checked")).toBe( "false" ); const input1: HTMLInputElement = listItems[1].nativeElement.querySelector("input"); input1.checked = true; input1.dispatchEvent(new Event("change")); fixture.detectChanges(); expect(listItems[1].nativeElement.getAttribute("aria-selected")).toBe( "true" ); expect(listItems[1].nativeElement.getAttribute("aria-checked")).toBe( "true" ); input1.checked = false; input1.dispatchEvent(new Event("change")); fixture.detectChanges(); expect(listItems[1].nativeElement.getAttribute("aria-selected")).toBe( "false" ); expect(listItems[1].nativeElement.getAttribute("aria-checked")).toBe( "false" ); }); });