diff --git a/src/cdk-experimental/ui-patterns/listbox/BUILD.bazel b/src/cdk-experimental/ui-patterns/listbox/BUILD.bazel index 7d5ff173e7ed..bd856fecdf4a 100644 --- a/src/cdk-experimental/ui-patterns/listbox/BUILD.bazel +++ b/src/cdk-experimental/ui-patterns/listbox/BUILD.bazel @@ -1,3 +1,4 @@ +load("//tools:defaults.bzl", "ng_web_test_suite") load("//tools:defaults2.bzl", "ts_project") package(default_visibility = ["//visibility:public"]) @@ -18,3 +19,20 @@ ts_project( "//src/cdk-experimental/ui-patterns/behaviors/signal-like", ], ) + +ts_project( + name = "unit_test_sources", + testonly = True, + srcs = glob(["**/*.spec.ts"]), + deps = [ + ":listbox", + "//:node_modules/@angular/core", + "//src/cdk/keycodes", + "//src/cdk/testing/private", + ], +) + +ng_web_test_suite( + name = "unit_tests", + deps = [":unit_test_sources"], +) diff --git a/src/cdk-experimental/ui-patterns/listbox/listbox.spec.ts b/src/cdk-experimental/ui-patterns/listbox/listbox.spec.ts new file mode 100644 index 000000000000..0c4fd44af81f --- /dev/null +++ b/src/cdk-experimental/ui-patterns/listbox/listbox.spec.ts @@ -0,0 +1,154 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {signal} from '@angular/core'; +import {ListboxInputs, ListboxPattern} from './listbox'; +import {OptionPattern} from './option'; +import {createKeyboardEvent} from '@angular/cdk/testing/private'; + +type TestInputs = ListboxInputs; +type TestOption = OptionPattern; +type TestListbox = ListboxPattern; + +describe('Listbox Pattern', () => { + function getListbox(inputs: Partial & Pick) { + return new ListboxPattern({ + items: inputs.items, + value: inputs.value ?? signal([]), + activeIndex: inputs.activeIndex ?? signal(0), + typeaheadDelay: inputs.typeaheadDelay ?? signal(0.5), + wrap: inputs.wrap ?? signal(true), + disabled: inputs.disabled ?? signal(false), + skipDisabled: inputs.skipDisabled ?? signal(true), + multiselectable: inputs.multiselectable ?? signal(false), + focusMode: inputs.focusMode ?? signal('roving'), + textDirection: inputs.textDirection ?? signal('ltr'), + orientation: inputs.orientation ?? signal('vertical'), + selectionMode: inputs.selectionMode ?? signal('explicit'), + }); + } + + function getOptions(listbox: TestListbox, values: string[]): TestOption[] { + return values.map((value, index) => { + return new OptionPattern({ + value: signal(value), + id: signal(`option-${index}`), + disabled: signal(false), + searchTerm: signal(value), + listbox: signal(listbox), + element: signal({focus: () => {}} as HTMLElement), + }); + }); + } + + function getPatterns(values: string[], inputs: Partial = {}) { + const options = signal([]); + const listbox = getListbox({...inputs, items: options}); + options.set(getOptions(listbox, values)); + return {listbox, options}; + } + + function getDefaultPatterns(inputs: Partial = {}) { + return getPatterns( + [ + 'Apple', + 'Apricot', + 'Banana', + 'Blackberry', + 'Blueberry', + 'Cantaloupe', + 'Cherry', + 'Clementine', + 'Cranberry', + ], + inputs, + ); + } + + describe('Navigation', () => { + it('should navigate next on ArrowDown', () => { + const {listbox} = getDefaultPatterns(); + const event = createKeyboardEvent('keydown', 40, 'ArrowDown'); + expect(listbox.inputs.activeIndex()).toBe(0); + listbox.onKeydown(event); + expect(listbox.inputs.activeIndex()).toBe(1); + }); + + it('should navigate prev on ArrowUp', () => { + const event = createKeyboardEvent('keydown', 38, 'ArrowUp'); + const {listbox} = getDefaultPatterns({ + activeIndex: signal(1), + }); + expect(listbox.inputs.activeIndex()).toBe(1); + listbox.onKeydown(event); + expect(listbox.inputs.activeIndex()).toBe(0); + }); + + it('should navigate next on ArrowRight (horizontal)', () => { + const event = createKeyboardEvent('keydown', 39, 'ArrowRight'); + const {listbox} = getDefaultPatterns({ + orientation: signal('horizontal'), + }); + expect(listbox.inputs.activeIndex()).toBe(0); + listbox.onKeydown(event); + expect(listbox.inputs.activeIndex()).toBe(1); + }); + + it('should navigate prev on ArrowLeft (horizontal)', () => { + const event = createKeyboardEvent('keydown', 37, 'ArrowLeft'); + const {listbox} = getDefaultPatterns({ + activeIndex: signal(1), + orientation: signal('horizontal'), + }); + expect(listbox.inputs.activeIndex()).toBe(1); + listbox.onKeydown(event); + expect(listbox.inputs.activeIndex()).toBe(0); + }); + + it('should navigate next on ArrowLeft (horizontal & rtl)', () => { + const event = createKeyboardEvent('keydown', 38, 'ArrowLeft'); + const {listbox} = getDefaultPatterns({ + textDirection: signal('rtl'), + orientation: signal('horizontal'), + }); + expect(listbox.inputs.activeIndex()).toBe(0); + listbox.onKeydown(event); + expect(listbox.inputs.activeIndex()).toBe(1); + }); + + it('should navigate prev on ArrowRight (horizontal & rtl)', () => { + const event = createKeyboardEvent('keydown', 39, 'ArrowRight'); + const {listbox} = getDefaultPatterns({ + activeIndex: signal(1), + textDirection: signal('rtl'), + orientation: signal('horizontal'), + }); + expect(listbox.inputs.activeIndex()).toBe(1); + listbox.onKeydown(event); + expect(listbox.inputs.activeIndex()).toBe(0); + }); + + it('should navigate to the first option on Home', () => { + const event = createKeyboardEvent('keydown', 36, 'Home'); + const {listbox} = getDefaultPatterns({ + activeIndex: signal(8), + }); + expect(listbox.inputs.activeIndex()).toBe(8); + listbox.onKeydown(event); + expect(listbox.inputs.activeIndex()).toBe(0); + }); + + it('should navigate to the last option on End', () => { + const event = createKeyboardEvent('keydown', 35, 'End'); + const {listbox} = getDefaultPatterns(); + expect(listbox.inputs.activeIndex()).toBe(0); + listbox.onKeydown(event); + expect(listbox.inputs.activeIndex()).toBe(8); + }); + }); +});