diff --git a/packages/.eslintrc.json b/packages/.eslintrc.json index 5e8cf6d595..8e7b667e93 100644 --- a/packages/.eslintrc.json +++ b/packages/.eslintrc.json @@ -60,7 +60,12 @@ }, "overrides": [ { - "files": ["*.test.ts", "*.stories.ts", "**/benchmark/*.ts"], + "files": [ + "*.test.ts", + "*.stories.ts", + "**/benchmark/*.ts", + "**/test/*.ts" + ], "rules": { "spectrum-web-components/document-active-element": ["off"], "lit-a11y/no-autofocus": ["off"], diff --git a/packages/picker/test/index.ts b/packages/picker/test/index.ts new file mode 100644 index 0000000000..2b8558ec73 --- /dev/null +++ b/packages/picker/test/index.ts @@ -0,0 +1,1260 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import type { Picker } from '..'; + +import type { OverlayOpenCloseDetail } from '@spectrum-web-components/overlay'; +import type { MenuItem } from '@spectrum-web-components/menu'; +import { + elementUpdated, + expect, + fixture, + html, + nextFrame, + oneEvent, + waitUntil, +} from '@open-wc/testing'; +import '@spectrum-web-components/shared/src/focus-visible.js'; +import { spy } from 'sinon'; +import { + arrowDownEvent, + arrowLeftEvent, + arrowRightEvent, + arrowUpEvent, + tEvent, +} from '../../../test/testing-helpers.js'; +import { + a11ySnapshot, + findAccessibilityNode, + sendKeys, +} from '@web/test-runner-commands'; +import { iconsOnly } from '../stories/picker.stories.js'; +import { sendMouse } from '../../../test/plugins/browser.js'; +import type { Popover } from '@spectrum-web-components/popover'; + +const isMenuActiveElement = function (): boolean { + return document.activeElement?.localName === 'sp-menu'; +}; + +export function runPickerTests(): void { + let el: Picker; + const pickerFixture = async (): Promise => { + const test = await fixture( + html` +
+ + Where do you live? + + + Deselect + + Select Inverse + + Feather... + Select and Mask... + + Save Selection + Make Work Path + +
+ ` + ); + + return test.querySelector('sp-picker') as Picker; + }; + describe('standard', () => { + beforeEach(async () => { + el = await pickerFixture(); + await elementUpdated(el); + }); + afterEach(async () => { + if (el.open) { + const closed = oneEvent(el, 'sp-closed'); + el.open = false; + await closed; + } + }); + it('loads accessibly', async () => { + await expect(el).to.be.accessible(); + }); + it('accepts new selected item content', async () => { + const option2 = el.querySelector('[value="option-2"') as MenuItem; + el.value = 'option-2'; + await elementUpdated(option2); + await elementUpdated(el); + expect(el.value).to.equal('option-2'); + expect((el.button.textContent || '').trim()).to.equal( + 'Select Inverse' + ); + const itemUpdated = oneEvent(el, 'sp-menu-item-added-or-updated'); + option2.innerHTML = 'Invert Selection'; + await itemUpdated; + await elementUpdated(el); + expect(el.value).to.equal('option-2'); + expect((el.button.textContent || '').trim()).to.equal( + 'Invert Selection' + ); + }); + it('accepts new selected item content when open', async () => { + const option2 = el.querySelector('[value="option-2"') as MenuItem; + el.value = 'option-2'; + await elementUpdated(el); + expect(el.value).to.equal('option-2'); + expect((el.button.textContent || '').trim()).to.equal( + 'Select Inverse' + ); + const opened = oneEvent(el, 'sp-opened'); + el.open = true; + await opened; + const itemUpdated = oneEvent( + option2, + 'sp-menu-item-added-or-updated' + ); + option2.innerHTML = 'Invert Selection'; + await itemUpdated; + await elementUpdated(el); + expect(el.value).to.equal('option-2'); + expect((el.button.textContent || '').trim()).to.equal( + 'Invert Selection' + ); + }); + it('unsets value when children removed', async () => { + el.value = 'option-2'; + + await elementUpdated(el); + expect(el.value).to.equal('option-2'); + expect((el.button.textContent || '').trim()).to.equal( + 'Select Inverse' + ); + + const items = el.querySelectorAll('sp-menu-item'); + const removals: Promise[] = []; + items.forEach((item) => { + const removal = oneEvent(el, 'sp-menu-item-removed'); + item.remove(); + removals.push(removal); + }); + await Promise.all(removals); + await elementUpdated(el); + expect(el.value).to.equal(''); + expect((el.button.textContent || '').trim()).to.equal(''); + }); + it('accepts a new item and value at the same time', async () => { + el.value = 'option-2'; + + await elementUpdated(el); + expect(el.value).to.equal('option-2'); + + const item = document.createElement('sp-menu-item'); + item.value = 'option-new'; + item.textContent = 'New Option'; + + el.append(item); + await elementUpdated(el); + + el.value = 'option-new'; + + await elementUpdated(el); + expect(el.value).to.equal('option-new'); + }); + it('accepts a new item that can be selected', async () => { + el.value = 'option-2'; + + await elementUpdated(el); + expect(el.value).to.equal('option-2'); + const item = document.createElement('sp-menu-item'); + item.value = 'option-new'; + item.textContent = 'New Option'; + + el.append(item); + + await elementUpdated(item); + await elementUpdated(el); + + let opened = oneEvent(el, 'sp-opened'); + el.open = true; + await opened; + // Overlaid content is outside of the context of the Picker element + // and cannot be managed via its updateComplete cycle. + await nextFrame(); + + const close = oneEvent(el, 'sp-closed'); + item.click(); + await close; + // Overlaid content is outside of the context of the Picker element + // and cannot be managed via its updateComplete cycle. + await nextFrame(); + + expect(el.value, 'first time').to.equal('option-new'); + + opened = oneEvent(el, 'sp-opened'); + el.open = true; + await opened; + // Overlaid content is outside of the context of the Picker element + // and cannot be managed via its updateComplete cycle. + await nextFrame(); + + expect(el.value, 'second time').to.equal('option-new'); + }); + it('manages its "name" value in the accessibility tree', async () => { + type NamedNode = { name: string }; + let snapshot = (await a11ySnapshot({})) as unknown as NamedNode & { + children: NamedNode[]; + }; + + expect( + findAccessibilityNode( + snapshot, + (node) => node.name === 'Where do you live?' + ), + '`name` is the label text' + ).to.not.be.null; + + el.value = 'option-2'; + await elementUpdated(el); + snapshot = (await a11ySnapshot({})) as unknown as NamedNode & { + children: NamedNode[]; + }; + + expect( + findAccessibilityNode( + snapshot, + (node) => node.name === 'Where do you live? Select Inverse' + ), + '`name` is the label text plus the selected item text' + ).to.not.be.null; + }); + it('manages `aria-activedescendant`', async () => { + const firstItem = el.querySelector('sp-menu-item:nth-child(1)'); + const secondItem = el.querySelector('sp-menu-item:nth-child(2)'); + const opened = oneEvent(el, 'sp-opened'); + el.open = true; + await opened; + expect( + el.optionsMenu.getAttribute('aria-activedescendant') + ).to.equal(firstItem?.id); + await sendKeys({ press: 'ArrowDown' }); + await elementUpdated(el); + expect( + el.optionsMenu.getAttribute('aria-activedescendant') + ).to.equal(secondItem?.id); + }); + it('renders invalid accessibly', async () => { + el.invalid = true; + await elementUpdated(el); + + expect(el.invalid).to.be.true; + await expect(el).to.be.accessible(); + }); + it('renders selection accessibly', async () => { + el.value = 'option-2'; + await elementUpdated(el); + + await expect(el).to.be.accessible(); + }); + it('opens with visible focus on a menu item on `DownArrow`', async () => { + const firstItem = el.querySelector('sp-menu-item') as MenuItem; + + await elementUpdated(el); + + expect(firstItem.focused, 'should not visually focused').to.be + .false; + + el.focus(); + await elementUpdated(el); + const opened = oneEvent(el, 'sp-opened'); + await sendKeys({ press: 'ArrowRight' }); + await sendKeys({ press: 'ArrowLeft' }); + await sendKeys({ press: 'ArrowDown' }); + await opened; + + expect(el.open).to.be.true; + expect(firstItem.focused, 'should be visually focused').to.be.true; + + const closed = oneEvent(el, 'sp-closed'); + await sendKeys({ + press: 'Escape', + }); + await closed; + + expect(el.open).to.be.false; + await waitUntil(() => !firstItem.focused, 'not visually focused'); + }); + it('opens without visible focus on a menu item on click', async () => { + const firstItem = el.querySelector('sp-menu-item') as MenuItem; + + await elementUpdated(el); + const boundingRect = el.getBoundingClientRect(); + + expect(firstItem.focused, 'not visually focused').to.be.false; + const opened = oneEvent(el, 'sp-opened'); + sendMouse({ + steps: [ + { + type: 'click', + position: [ + boundingRect.x + boundingRect.width / 2, + boundingRect.y + boundingRect.height / 2, + ], + }, + ], + }); + await opened; + await elementUpdated(el); + + expect(el.open).to.be.true; + expect(firstItem.focused, 'still not visually focused').to.be.false; + }); + it('closes when becoming disabled', async () => { + expect(el.open).to.be.false; + el.click(); + await elementUpdated(el); + + expect(el.open).to.be.true; + el.disabled = true; + await elementUpdated(el); + + expect(el.open).to.be.false; + }); + it('closes when clicking away', async () => { + el.id = 'closing'; + const other = document.createElement('div'); + document.body.append(other); + + await elementUpdated(el); + + expect(el.open).to.be.false; + el.click(); + await elementUpdated(el); + + expect(el.open).to.be.true; + other.click(); + await waitUntil(() => !el.open, 'closed'); + + other.remove(); + }); + it('selects', async () => { + const secondItem = el.querySelector( + 'sp-menu-item:nth-of-type(2)' + ) as MenuItem; + const button = el.button as HTMLButtonElement; + + const opened = oneEvent(el, 'sp-opened'); + button.click(); + await opened; + await elementUpdated(el); + + expect(el.open).to.be.true; + expect(el.selectedItem?.itemText).to.be.undefined; + expect(el.value).to.equal(''); + + const closed = oneEvent(el, 'sp-closed'); + secondItem.click(); + await closed; + + expect(el.open).to.be.false; + expect(el.selectedItem?.itemText).to.equal('Select Inverse'); + expect(el.value).to.equal('option-2'); + }); + it('re-selects', async () => { + const firstItem = el.querySelector( + 'sp-menu-item:nth-of-type(1)' + ) as MenuItem; + const secondItem = el.querySelector( + 'sp-menu-item:nth-of-type(2)' + ) as MenuItem; + const button = el.button as HTMLButtonElement; + + const opened = oneEvent(el, 'sp-opened'); + button.click(); + await opened; + await nextFrame(); + + expect(el.open).to.be.true; + expect(el.selectedItem?.itemText).to.be.undefined; + expect(el.value).to.equal(''); + + const closed = oneEvent(el, 'sp-closed'); + secondItem.click(); + await closed; + await nextFrame(); + + expect(el.open).to.be.false; + expect(el.selectedItem?.itemText).to.equal('Select Inverse'); + expect(el.value).to.equal('option-2'); + + const opened2 = oneEvent(el, 'sp-opened'); + button.click(); + await opened2; + await nextFrame(); + + expect(el.open).to.be.true; + expect(el.selectedItem?.itemText).to.equal('Select Inverse'); + expect(el.value).to.equal('option-2'); + + const closed2 = oneEvent(el, 'sp-closed'); + firstItem.click(); + await closed2; + await nextFrame(); + + expect(el.open).to.be.false; + expect(el.selectedItem?.itemText).to.equal('Deselect'); + expect(el.value).to.equal('Deselect'); + }); + it('dispatches bubbling and composed events', async () => { + const changeSpy = spy(); + const parent = el.parentElement as HTMLElement; + parent.attachShadow({ mode: 'open' }); + (parent.shadowRoot as ShadowRoot).append(el); + const secondItem = el.querySelector( + 'sp-menu-item:nth-of-type(2)' + ) as MenuItem; + + parent.addEventListener('change', () => changeSpy()); + + expect(el.value).to.equal(''); + + const opened = oneEvent(el, 'sp-opened'); + el.open = true; + await opened; + await elementUpdated(el); + + const closed = oneEvent(el, 'sp-closed'); + secondItem.click(); + await closed; + await elementUpdated(el); + + expect(el.value).to.equal(secondItem.value); + expect(changeSpy.calledOnce).to.be.true; + }); + it('can have selection prevented', async () => { + const preventChangeSpy = spy(); + const secondItem = el.querySelector( + 'sp-menu-item:nth-of-type(2)' + ) as MenuItem; + const button = el.button as HTMLButtonElement; + + let opened = oneEvent(el, 'sp-opened'); + button.click(); + await opened; + await elementUpdated(el); + + expect(el.open).to.be.true; + expect(el.selectedItem?.itemText).to.be.undefined; + expect(el.value).to.equal(''); + expect(secondItem.selected).to.be.false; + + el.addEventListener('change', (event: Event): void => { + event.preventDefault(); + preventChangeSpy(); + }); + + const closed = oneEvent(el, 'sp-closed'); + opened = oneEvent(el, 'sp-opened'); + secondItem.click(); + await closed; + await opened; + await elementUpdated(el); + expect(preventChangeSpy.calledOnce).to.be.true; + expect(secondItem.selected, 'selection prevented').to.be.false; + }); + it('can throw focus after `change`', async () => { + const input = document.createElement('input'); + document.body.append(input); + + await elementUpdated(el); + + const secondItem = el.querySelector( + 'sp-menu-item:nth-of-type(2)' + ) as MenuItem; + const button = el.button as HTMLButtonElement; + + const opened = oneEvent(el, 'sp-opened'); + button.click(); + await opened; + await elementUpdated(el); + + expect(el.open).to.be.true; + expect(el.selectedItem?.itemText).to.be.undefined; + expect(el.value).to.equal(''); + expect(secondItem.selected).to.be.false; + + el.addEventListener('change', (): void => { + input.focus(); + }); + + const closed = oneEvent(el, 'sp-closed'); + secondItem.click(); + await closed; + await elementUpdated(el); + + expect(el.open).to.be.false; + expect(el.value, 'value changed').to.equal('option-2'); + expect(secondItem.selected, 'selected changed').to.be.true; + await waitUntil( + () => document.activeElement === input, + 'focus throw' + ); + input.remove(); + }); + it('opens on ArrowUp', async () => { + const button = el.button as HTMLButtonElement; + + el.focus(); + await elementUpdated(el); + + expect(el.open, 'inially closed').to.be.false; + + button.dispatchEvent(tEvent()); + await elementUpdated(el); + + expect(el.open, 'still closed').to.be.false; + + button.dispatchEvent(arrowUpEvent()); + await elementUpdated(el); + + expect(el.open, 'open by ArrowUp').to.be.true; + + await waitUntil( + () => document.querySelector('active-overlay') !== null, + 'an active-overlay has been inserted on the page' + ); + + button.dispatchEvent( + new KeyboardEvent('keyup', { + bubbles: true, + composed: true, + cancelable: true, + key: 'Escape', + code: 'Escape', + }) + ); + await elementUpdated(el); + await waitUntil(() => el.open === false, 'closed by Escape'); + await waitUntil( + () => document.querySelector('active-overlay') === null, + 'an active-overlay has been inserted on the page' + ); + }); + it('opens on ArrowDown', async () => { + const firstItem = el.querySelector( + 'sp-menu-item:nth-of-type(1)' + ) as MenuItem; + const button = el.button as HTMLButtonElement; + + el.focus(); + await elementUpdated(el); + + expect(el.open, 'inially closed').to.be.false; + + const opened = oneEvent(el, 'sp-opened'); + button.dispatchEvent(arrowDownEvent()); + await opened; + await elementUpdated(el); + + expect(el.open, 'open by ArrowDown').to.be.true; + expect(el.selectedItem?.itemText).to.be.undefined; + expect(el.value).to.equal(''); + + const closed = oneEvent(el, 'sp-closed'); + firstItem.click(); + await closed; + await elementUpdated(el); + + expect(el.open).to.be.false; + expect(el.selectedItem?.itemText).to.equal('Deselect'); + expect(el.value).to.equal('Deselect'); + }); + it('quick selects on ArrowLeft/Right', async () => { + const selectionSpy = spy(); + el.addEventListener('change', (event: Event) => { + const { value } = event.target as Picker; + selectionSpy(value); + }); + const button = el.button as HTMLButtonElement; + + el.focus(); + button.dispatchEvent(arrowLeftEvent()); + + await elementUpdated(el); + + expect(selectionSpy.callCount).to.equal(1); + expect(selectionSpy.calledWith('Deselected')); + button.dispatchEvent(arrowLeftEvent()); + + await elementUpdated(el); + expect(selectionSpy.callCount).to.equal(1); + button.dispatchEvent(arrowRightEvent()); + + await elementUpdated(el); + expect(selectionSpy.calledWith('option-2')); + + button.dispatchEvent(arrowRightEvent()); + button.dispatchEvent(arrowRightEvent()); + button.dispatchEvent(arrowRightEvent()); + button.dispatchEvent(arrowRightEvent()); + + await elementUpdated(el); + expect(selectionSpy.callCount).to.equal(5); + expect(selectionSpy.calledWith('Save Selection')); + expect(selectionSpy.calledWith('Make Work Path')).to.be.false; + }); + it('quick selects first item on ArrowRight when no value', async () => { + const selectionSpy = spy(); + el.addEventListener('change', (event: Event) => { + const { value } = event.target as Picker; + selectionSpy(value); + }); + const button = el.button as HTMLButtonElement; + + el.focus(); + button.dispatchEvent(arrowRightEvent()); + + await elementUpdated(el); + + expect(selectionSpy.callCount).to.equal(1); + expect(selectionSpy.calledWith('Deselected')); + }); + it('loads', async () => { + expect(el).to.not.be.undefined; + }); + it('refocuses on list when open', async () => { + const firstItem = el.querySelector('sp-menu-item') as MenuItem; + const input = document.createElement('input'); + el.insertAdjacentElement('afterend', input); + + el.focus(); + await sendKeys({ press: 'Tab' }); + expect(document.activeElement === input).to.be.true; + await sendKeys({ press: 'Shift+Tab' }); + expect(document.activeElement === el).to.be.true; + await sendKeys({ press: 'Enter' }); + const opened = oneEvent(el, 'sp-opened'); + el.open = true; + await opened; + await elementUpdated(el); + + await waitUntil( + () => firstItem.focused, + 'The first items should have become focused visually.' + ); + + el.blur(); + await elementUpdated(el); + + expect(el.open).to.be.true; + el.focus(); + await elementUpdated(el); + await waitUntil( + () => isMenuActiveElement(), + 'first item refocused' + ); + expect(el.open).to.be.true; + expect(isMenuActiveElement()).to.be.true; + // Force :focus-visible heuristic + await sendKeys({ press: 'ArrowDown' }); + await sendKeys({ press: 'ArrowUp' }); + expect(firstItem.focused).to.be.true; + }); + it('does not allow tabing to close', async () => { + el.open = true; + await elementUpdated(el); + + expect(el.open).to.be.true; + el.focus(); + await elementUpdated(el); + await waitUntil( + () => isMenuActiveElement(), + 'first item refocused' + ); + expect(el.open).to.be.true; + expect(isMenuActiveElement()).to.be.true; + + await sendKeys({ press: 'Tab' }); + + expect(el.open, 'stays open').to.be.true; + }); + describe('tab order', () => { + let input1: HTMLInputElement; + let input2: HTMLInputElement; + beforeEach(() => { + const surroundingInput = (): HTMLInputElement => { + const input = document.createElement('input'); + input.type = 'text'; + input.tabIndex = 0; + return input; + }; + input1 = surroundingInput(); + input2 = surroundingInput(); + + el.insertAdjacentElement('beforebegin', input1); + el.insertAdjacentElement('afterend', input2); + }); + afterEach(() => { + input1.remove(); + input2.remove(); + }); + it('tabs forward through the element', async () => { + // start at input1 + input1.focus(); + await nextFrame(); + expect(document.activeElement === input1, 'focuses input 1').to + .true; + // tab to the picker + let focused = oneEvent(el, 'focus'); + await sendKeys({ press: 'Tab' }); + await focused; + + expect(el.focused, 'focused').to.be.true; + expect(el.open, 'closed').to.be.false; + expect(document.activeElement === el, 'focuses el').to.be.true; + // tab through the picker to input2 + focused = oneEvent(input2, 'focus'); + await sendKeys({ press: 'Tab' }); + await focused; + expect(document.activeElement === input2, 'focuses input 2').to + .true; + }); + it('shift+tabs backwards through the element', async () => { + // start at input1 + input2.focus(); + await nextFrame(); + expect(document.activeElement, 'focuses input 2').to.equal( + input2 + ); + // tab to the picker + let focused = oneEvent(el, 'focus'); + await sendKeys({ press: 'Shift+Tab' }); + await focused; + + expect(el.focused, 'focused').to.be.true; + expect(el.open, 'closed').to.be.false; + expect(document.activeElement, 'focuses el').to.equal(el); + // tab through the picker to input2 + focused = oneEvent(input1, 'focus'); + await sendKeys({ press: 'Shift+Tab' }); + await focused; + expect(document.activeElement, 'focuses input 1').to.equal( + input1 + ); + }); + it('traps tab in the menu as a `type="modal"` overlay forward', async () => { + el.focus(); + await nextFrame(); + expect(document.activeElement, 'focuses el').to.equal(el); + // press down to open the picker + const opened = oneEvent(el, 'sp-opened'); + await sendKeys({ press: 'ArrowDown' }); + await opened; + + expect(el.open, 'opened').to.be.true; + await waitUntil( + () => isMenuActiveElement(), + 'first item focused' + ); + + const activeElement = document.activeElement as HTMLElement; + const blured = oneEvent(activeElement, 'blur'); + await sendKeys({ press: 'Tab' }); + await blured; + + expect(el.open).to.be.true; + expect(document.activeElement === input1).to.be.false; + expect(document.activeElement === input2).to.be.false; + }); + it('traps tab in the menu as a `type="modal"` overlay backwards', async () => { + el.focus(); + await nextFrame(); + expect(document.activeElement, 'focuses el').to.equal(el); + // press down to open the picker + const opened = oneEvent(el, 'sp-opened'); + await sendKeys({ press: 'ArrowDown' }); + await opened; + + expect(el.open, 'opened').to.be.true; + await waitUntil( + () => isMenuActiveElement(), + 'first item focused' + ); + + const activeElement = document.activeElement as HTMLElement; + const blured = oneEvent(activeElement, 'blur'); + await sendKeys({ press: 'Shift+Tab' }); + await blured; + + expect(el.open).to.be.true; + expect(document.activeElement === input1).to.be.false; + expect(document.activeElement === input2).to.be.false; + }); + it('can close and immediate tab to the next tab stop', async () => { + el.focus(); + await nextFrame(); + expect(document.activeElement, 'focuses el').to.equal(el); + // press down to open the picker + const opened = oneEvent(el, 'sp-opened'); + await sendKeys({ press: 'ArrowUp' }); + await opened; + + expect(el.open, 'opened').to.be.true; + await waitUntil( + () => isMenuActiveElement(), + 'first item focused' + ); + + const closed = oneEvent(el, 'sp-closed'); + el.open = false; + await closed; + + expect(el.open).to.be.false; + expect(document.activeElement === el).to.be.true; + + const focused = oneEvent(input2, 'focus'); + await sendKeys({ press: 'Tab' }); + await focused; + + expect(el.open).to.be.false; + expect(document.activeElement === input2).to.be.true; + }); + it('can close and immediate shift+tab to the previous tab stop', async () => { + el.focus(); + await nextFrame(); + expect(document.activeElement, 'focuses el').to.equal(el); + // press down to open the picker + const opened = oneEvent(el, 'sp-opened'); + await sendKeys({ press: 'ArrowUp' }); + await opened; + + expect(el.open, 'opened').to.be.true; + await waitUntil( + () => isMenuActiveElement(), + 'first item focused' + ); + + const closed = oneEvent(el, 'sp-closed'); + el.open = false; + await closed; + + expect(el.open).to.be.false; + expect(document.activeElement === el).to.be.true; + + const focused = oneEvent(input1, 'focus'); + await sendKeys({ press: 'Shift+Tab' }); + await focused; + + expect(el.open).to.be.false; + expect(document.activeElement === input1).to.be.true; + }); + }); + it('does not open when [readonly]', async () => { + el.readonly = true; + + await elementUpdated(el); + + const button = el.button as HTMLButtonElement; + + button.click(); + await elementUpdated(el); + + expect(el.open).to.be.false; + }); + it('scrolls selected into view on open', async () => { + await ( + el as unknown as { generatePopover(): void } + ).generatePopover(); + (el as unknown as { popover: Popover }).popover.style.height = + '40px'; + + const firstItem = el.querySelector( + 'sp-menu-item:first-child' + ) as MenuItem; + const lastItem = el.querySelector( + 'sp-menu-item:last-child' + ) as MenuItem; + lastItem.disabled = false; + el.value = lastItem.value; + + await elementUpdated(el); + + el.open = true; + + await elementUpdated(el); + await waitUntil(() => isMenuActiveElement(), 'first item focused'); + const getParentOffset = (el: HTMLElement): number => { + const parentScroll = (el.parentElement as HTMLElement) + .scrollTop; + const parentOffset = el.offsetTop - parentScroll; + return parentOffset; + }; + expect(getParentOffset(lastItem)).to.be.lessThan(40); + expect(getParentOffset(firstItem)).to.be.lessThan(-1); + + lastItem.dispatchEvent( + new FocusEvent('focusin', { bubbles: true }) + ); + lastItem.dispatchEvent(arrowDownEvent()); + await elementUpdated(el); + await nextFrame(); + expect(getParentOffset(lastItem)).to.be.greaterThan(40); + expect(getParentOffset(firstItem)).to.be.greaterThan(-1); + }); + }); + describe('slotted label', () => { + const pickerFixture = async (): Promise => { + const test = await fixture( + html` +
+ + Where do you live? + + + + Select a Country with a very long label, too + long in fact + + Deselect + + Select Inverse + + Feather... + Select and Mask... + + Save Selection + Make Work Path + +
+ ` + ); + + return test.querySelector('sp-picker') as Picker; + }; + beforeEach(async () => { + el = await pickerFixture(); + await elementUpdated(el); + }); + afterEach(async () => { + if (el.open) { + const closed = oneEvent(el, 'sp-closed'); + el.open = false; + await closed; + } + }); + + it('loads accessibly w/ slotted label', async () => { + await expect(el).to.be.accessible(); + }); + }); + describe('deprecated', () => { + const pickerFixture = async (): Promise => { + const test = await fixture( + html` +
+ + Where do you live? + + + + Deselect + + Select Inverse + + Feather... + Select and Mask... + + Save Selection + + Make Work Path + + + +
+ ` + ); + + return test.querySelector('sp-picker') as Picker; + }; + beforeEach(async () => { + el = await pickerFixture(); + await elementUpdated(el); + }); + afterEach(async () => { + if (el.open) { + const closed = oneEvent(el, 'sp-closed'); + el.open = false; + await closed; + } + }); + it('selects with deprecated syntax', async () => { + const secondItem = el.querySelector( + 'sp-menu-item:nth-of-type(2)' + ) as MenuItem; + + const opened = oneEvent(el, 'sp-opened'); + el.button.click(); + await opened; + await elementUpdated(el); + + expect(el.open).to.be.true; + expect(el.selectedItem?.itemText).to.be.undefined; + expect(el.value).to.equal(''); + + const closed = oneEvent(el, 'sp-closed'); + secondItem.click(); + await closed; + + expect(el.open).to.be.false; + expect(el.selectedItem?.itemText).to.equal('Select Inverse'); + expect(el.value).to.equal('option-2'); + }); + }); + it('manages its "name" value in the accessibility tree when [icons-only]', async () => { + const test = await fixture(html` +
${iconsOnly({})}
+ `); + const el = test.querySelector('sp-picker') as Picker; + + await elementUpdated(el); + type NamedNode = { name: string }; + let snapshot = (await a11ySnapshot({})) as unknown as NamedNode & { + children: NamedNode[]; + }; + + expect( + findAccessibilityNode( + snapshot, + (node) => node.name === 'Choose an action type... Delete' + ), + '`name` is the label text' + ).to.not.be.null; + + el.value = '2'; + await elementUpdated(el); + snapshot = (await a11ySnapshot({})) as unknown as NamedNode & { + children: NamedNode[]; + }; + + expect( + findAccessibilityNode( + snapshot, + (node) => node.name === 'Choose an action type... Copy' + ), + '`name` is the label text plus the selected item text' + ).to.not.be.null; + }); + it('toggles between pickers', async () => { + const el2 = await pickerFixture(); + const el1 = await pickerFixture(); + + el1.id = 'away'; + el2.id = 'other'; + + await Promise.all([elementUpdated(el1), elementUpdated(el2)]); + + expect(el1.open, 'closed 1').to.be.false; + expect(el2.open, 'closed 1').to.be.false; + let open = oneEvent(el1, 'sp-opened'); + el1.click(); + await open; + expect(el1.open).to.be.true; + expect(el2.open).to.be.false; + + open = oneEvent(el2, 'sp-opened'); + let closed = oneEvent(el1, 'sp-closed'); + el2.click(); + await Promise.all([open, closed]); + expect(el1.open).to.be.false; + expect(el2.open).to.be.true; + + open = oneEvent(el1, 'sp-opened'); + closed = oneEvent(el2, 'sp-closed'); + el1.click(); + await Promise.all([open, closed]); + expect(el1.open).to.be.true; + expect(el2.open).to.be.false; + + closed = oneEvent(el1, 'sp-closed'); + sendKeys({ + press: 'Escape', + }); + await closed; + expect(el1.open).to.be.false; + }); + it('displays selected item text by default', async () => { + const el = await fixture( + html` + + Deselect Text + Select Inverse + Feather... + Select and Mask... + + Save Selection + Make Work Path + + ` + ); + + await elementUpdated(el); + await waitUntil( + () => el.selectedItem?.itemText === 'Select Inverse', + `Selected Item Text: ${el.selectedItem?.itemText}` + ); + + const firstItem = el.querySelector( + 'sp-menu-item:nth-of-type(1)' + ) as MenuItem; + const secondItem = el.querySelector( + 'sp-menu-item:nth-of-type(2)' + ) as MenuItem; + + expect(el.value).to.equal('inverse'); + expect(el.selectedItem?.itemText).to.equal('Select Inverse'); + + el.focus(); + await elementUpdated(el); + expect( + el === document.activeElement, + `activeElement is ${document.activeElement?.localName}` + ).to.be.true; + + const opened = oneEvent(el, 'sp-opened'); + sendKeys({ press: 'Enter' }); + await opened; + await elementUpdated(el.optionsMenu); + + expect( + el.optionsMenu === document.activeElement, + `activeElement is ${document.activeElement?.localName}` + ).to.be.true; + + expect(firstItem.focused, 'firstItem NOT "focused"').to.be.false; + expect(secondItem.focused, 'secondItem "focused"').to.be.true; + expect(el.optionsMenu.getAttribute('aria-activedescendant')).to.equal( + secondItem.id + ); + }); + it('resets value when item not available', async () => { + const el = await fixture( + html` + + Deselect Text + Select Inverse + Feather... + Select and Mask... + + Save Selection + Make Work Path + + ` + ); + + await elementUpdated(el); + await waitUntil(() => el.value === ''); + + expect(el.value).to.equal(''); + expect(el.selectedItem?.itemText).to.be.undefined; + }); + it('allows event listeners on child items', async () => { + const mouseenterSpy = spy(); + const handleMouseenter = (): void => mouseenterSpy(); + const el = await fixture( + html` + + + Deselect Text + + + ` + ); + + await elementUpdated(el); + + const hoverEl = el.querySelector('sp-menu-item') as MenuItem; + + const opened = oneEvent(el, 'sp-opened'); + el.open = true; + await opened; + await elementUpdated(el); + + expect(el.open).to.be.true; + hoverEl.dispatchEvent(new MouseEvent('mouseenter')); + await elementUpdated(el); + + expect(el.open).to.be.true; + + const closed = oneEvent(el, 'sp-closed'); + el.open = false; + await closed; + await elementUpdated(el); + + expect(el.open).to.be.false; + expect(mouseenterSpy.calledOnce).to.be.true; + }); + it('dispatches events on open/close', async () => { + const openedSpy = spy(); + const closedSpy = spy(); + const handleOpenedSpy = (event: Event): void => openedSpy(event); + const handleClosedSpy = (event: Event): void => closedSpy(event); + + const el = await fixture( + html` + + Deselect Text + + ` + ); + + await elementUpdated(el); + const opened = oneEvent(el, 'sp-opened'); + el.open = true; + await opened; + await elementUpdated(el); + + expect(openedSpy.calledOnce).to.be.true; + expect(closedSpy.calledOnce).to.be.false; + + const openedEvent = openedSpy + .args[0][0] as CustomEvent; + expect(openedEvent.detail.interaction).to.equal('modal'); + + const closed = oneEvent(el, 'sp-closed'); + el.open = false; + await closed; + await elementUpdated(el); + + expect(closedSpy.calledOnce).to.be.true; + + const closedEvent = closedSpy + .args[0][0] as CustomEvent; + expect(closedEvent.detail.interaction).to.equal('modal'); + }); +} diff --git a/packages/picker/test/picker-reparenting.test.ts b/packages/picker/test/picker-reparenting.test.ts new file mode 100644 index 0000000000..658b5cd6ef --- /dev/null +++ b/packages/picker/test/picker-reparenting.test.ts @@ -0,0 +1,127 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import '../sp-picker.js'; +import '@spectrum-web-components/menu/sp-menu-item.js'; +import '@spectrum-web-components/menu/sp-menu-divider.js'; +import { Picker } from '../'; +import { MenuItem } from '@spectrum-web-components/menu'; +import { + elementUpdated, + expect, + fixture, + html, + oneEvent, +} from '@open-wc/testing'; + +import '@spectrum-web-components/theme/sp-theme.js'; +import '@spectrum-web-components/theme/src/themes.js'; + +const fixtureElements = async (): Promise<{ + picker: Picker; + before: HTMLDivElement; + after: HTMLDivElement; +}> => { + const test = await fixture(html` + +
+ + Immediately + + I'm already using them + + + Soon + + As part of my next project + + In the future + +
+
+
+ `); + const picker = test.querySelector('sp-picker') as Picker; + await elementUpdated(picker); + return { + picker, + before: test.querySelector('#before') as HTMLDivElement, + after: test.querySelector('#after') as HTMLDivElement, + }; +}; + +describe('Reparented Picker', () => { + it('maintains a `dir` attribute', async () => { + const { picker, before, after } = await fixtureElements(); + + expect(picker.dir).to.equal('ltr'); + expect(picker.getAttribute('dir')).to.equal('ltr'); + + after.append(picker); + await elementUpdated(picker); + + expect(picker.dir).to.equal('ltr'); + expect(picker.getAttribute('dir')).to.equal('ltr'); + + before.append(picker); + await elementUpdated(picker); + + expect(picker.dir).to.equal('ltr'); + expect(picker.getAttribute('dir')).to.equal('ltr'); + }); + it('maintains `value`', async () => { + const { picker, before, after } = await fixtureElements(); + + expect(picker.value).to.equal(''); + + const item2 = picker.querySelector('[value="2"]') as MenuItem; + const item3 = picker.querySelector('[value="3"]') as MenuItem; + let opened = oneEvent(picker, 'sp-opened'); + picker.click(); + await opened; + await elementUpdated(picker); + let closed = oneEvent(picker, 'sp-closed'); + item2.click(); + await closed; + await elementUpdated(picker); + + expect(picker.value).to.equal('2'); + + after.append(picker); + opened = oneEvent(picker, 'sp-opened'); + picker.click(); + await opened; + await elementUpdated(picker); + closed = oneEvent(picker, 'sp-closed'); + await elementUpdated(item3); + item3.click(); + await closed; + await elementUpdated(picker); + + expect(picker.value).to.equal('3'); + + opened = oneEvent(picker, 'sp-opened'); + picker.click(); + await opened; + await elementUpdated(picker); + expect(picker.value).to.equal('3'); + closed = oneEvent(picker, 'sp-closed'); + before.append(picker); + await closed; + await elementUpdated(picker); + + expect(picker.value).to.equal('3'); + }); +}); diff --git a/packages/picker/test/picker-sync.test.ts b/packages/picker/test/picker-sync.test.ts index e851a9a8e6..82e0310d7c 100644 --- a/packages/picker/test/picker-sync.test.ts +++ b/packages/picker/test/picker-sync.test.ts @@ -11,1254 +11,8 @@ governing permissions and limitations under the License. */ import '../sync/sp-picker.js'; -import { Picker } from '..'; - -import '@spectrum-web-components/overlay/active-overlay.js'; -import { OverlayOpenCloseDetail } from '@spectrum-web-components/overlay'; -import '@spectrum-web-components/menu/sp-menu.js'; -import '@spectrum-web-components/menu/sp-menu-item.js'; -import '@spectrum-web-components/menu/sp-menu-divider.js'; -import '@spectrum-web-components/field-label/sp-field-label.js'; -import { Menu, MenuItem } from '@spectrum-web-components/menu'; -import { - elementUpdated, - expect, - fixture, - html, - nextFrame, - oneEvent, - waitUntil, -} from '@open-wc/testing'; -import '@spectrum-web-components/shared/src/focus-visible.js'; -import { spy } from 'sinon'; -import { - arrowDownEvent, - arrowLeftEvent, - arrowRightEvent, - arrowUpEvent, - tEvent, -} from '../../../test/testing-helpers.js'; -import { - a11ySnapshot, - findAccessibilityNode, - sendKeys, -} from '@web/test-runner-commands'; -import { iconsOnly } from '../stories/picker.stories.js'; -import { sendMouse } from '../../../test/plugins/browser.js'; -import type { Popover } from '@spectrum-web-components/popover'; - -const isMenuActiveElement = function (): boolean { - return document.activeElement instanceof Menu; -}; +import { runPickerTests } from './index.js'; describe('Picker, sync', () => { - let el: Picker; - const pickerFixture = async (): Promise => { - const test = await fixture( - html` -
- - Where do you live? - - - Deselect - - Select Inverse - - Feather... - Select and Mask... - - Save Selection - Make Work Path - -
- ` - ); - - return test.querySelector('sp-picker') as Picker; - }; - describe('standard', () => { - beforeEach(async () => { - el = await pickerFixture(); - await elementUpdated(el); - }); - afterEach(async () => { - if (el.open) { - const closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - } - }); - it('loads accessibly', async () => { - await expect(el).to.be.accessible(); - }); - it('accepts new selected item content', async () => { - const option2 = el.querySelector('[value="option-2"') as MenuItem; - el.value = 'option-2'; - await elementUpdated(el); - expect(el.value).to.equal('option-2'); - expect((el.button.textContent || '').trim()).to.equal( - 'Select Inverse' - ); - const itemUpdated = oneEvent(el, 'sp-menu-item-added-or-updated'); - option2.innerHTML = 'Invert Selection'; - await itemUpdated; - await elementUpdated(el); - expect(el.value).to.equal('option-2'); - expect((el.button.textContent || '').trim()).to.equal( - 'Invert Selection' - ); - }); - it('accepts new selected item content when open', async () => { - const option2 = el.querySelector('[value="option-2"') as MenuItem; - el.value = 'option-2'; - await elementUpdated(el); - expect(el.value).to.equal('option-2'); - expect((el.button.textContent || '').trim()).to.equal( - 'Select Inverse' - ); - const opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - const itemUpdated = oneEvent( - option2, - 'sp-menu-item-added-or-updated' - ); - option2.innerHTML = 'Invert Selection'; - await itemUpdated; - await elementUpdated(el); - expect(el.value).to.equal('option-2'); - expect((el.button.textContent || '').trim()).to.equal( - 'Invert Selection' - ); - }); - it('unsets value when children removed', async () => { - el.value = 'option-2'; - - await elementUpdated(el); - expect(el.value).to.equal('option-2'); - expect((el.button.textContent || '').trim()).to.equal( - 'Select Inverse' - ); - - const items = el.querySelectorAll('sp-menu-item'); - const removals: Promise[] = []; - items.forEach((item) => { - const removal = oneEvent(el, 'sp-menu-item-removed'); - item.remove(); - removals.push(removal); - }); - await Promise.all(removals); - await elementUpdated(el); - expect(el.value).to.equal(''); - expect((el.button.textContent || '').trim()).to.equal(''); - }); - it('accepts a new item and value at the same time', async () => { - el.value = 'option-2'; - - await elementUpdated(el); - expect(el.value).to.equal('option-2'); - - const item = document.createElement('sp-menu-item'); - item.value = 'option-new'; - item.textContent = 'New Option'; - - el.append(item); - await elementUpdated(el); - - el.value = 'option-new'; - - await elementUpdated(el); - expect(el.value).to.equal('option-new'); - }); - it('accepts a new item that can be selected', async () => { - el.value = 'option-2'; - - await elementUpdated(el); - expect(el.value).to.equal('option-2'); - const item = document.createElement('sp-menu-item'); - item.value = 'option-new'; - item.textContent = 'New Option'; - - el.append(item); - - await elementUpdated(item); - await elementUpdated(el); - - let opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - // Overlaid content is outside of the context of the Picker element - // and cannot be managed via its updateComplete cycle. - await nextFrame(); - - const close = oneEvent(el, 'sp-closed'); - item.click(); - await close; - // Overlaid content is outside of the context of the Picker element - // and cannot be managed via its updateComplete cycle. - await nextFrame(); - - expect(el.value, 'first time').to.equal('option-new'); - - opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - // Overlaid content is outside of the context of the Picker element - // and cannot be managed via its updateComplete cycle. - await nextFrame(); - - expect(el.value, 'second time').to.equal('option-new'); - }); - it('manages its "name" value in the accessibility tree', async () => { - type NamedNode = { name: string }; - let snapshot = (await a11ySnapshot({})) as unknown as NamedNode & { - children: NamedNode[]; - }; - - expect( - findAccessibilityNode( - snapshot, - (node) => node.name === 'Where do you live?' - ), - '`name` is the label text' - ).to.not.be.null; - - el.value = 'option-2'; - await elementUpdated(el); - snapshot = (await a11ySnapshot({})) as unknown as NamedNode & { - children: NamedNode[]; - }; - - expect( - findAccessibilityNode( - snapshot, - (node) => node.name === 'Where do you live? Select Inverse' - ), - '`name` is the label text plus the selected item text' - ).to.not.be.null; - }); - it('manages `aria-activedescendant`', async () => { - const firstItem = el.querySelector('sp-menu-item:nth-child(1)'); - const secondItem = el.querySelector('sp-menu-item:nth-child(2)'); - const opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - expect( - el.optionsMenu.getAttribute('aria-activedescendant') - ).to.equal(firstItem?.id); - await sendKeys({ press: 'ArrowDown' }); - await elementUpdated(el); - expect( - el.optionsMenu.getAttribute('aria-activedescendant') - ).to.equal(secondItem?.id); - }); - it('renders invalid accessibly', async () => { - el.invalid = true; - await elementUpdated(el); - - expect(el.invalid).to.be.true; - await expect(el).to.be.accessible(); - }); - it('renders selection accessibly', async () => { - el.value = 'option-2'; - await elementUpdated(el); - - await expect(el).to.be.accessible(); - }); - it('opens with visible focus on a menu item on `DownArrow`', async () => { - const firstItem = el.querySelector('sp-menu-item') as MenuItem; - - await elementUpdated(el); - - expect(firstItem.focused, 'should not visually focused').to.be - .false; - - el.focus(); - await elementUpdated(el); - const opened = oneEvent(el, 'sp-opened'); - await sendKeys({ press: 'ArrowRight' }); - await sendKeys({ press: 'ArrowLeft' }); - await sendKeys({ press: 'ArrowDown' }); - await opened; - - expect(el.open).to.be.true; - expect(firstItem.focused, 'should be visually focused').to.be.true; - - const closed = oneEvent(el, 'sp-closed'); - await sendKeys({ - press: 'Escape', - }); - await closed; - - expect(el.open).to.be.false; - await waitUntil(() => !firstItem.focused, 'not visually focused'); - }); - it('opens without visible focus on a menu item on click', async () => { - const firstItem = el.querySelector('sp-menu-item') as MenuItem; - - await elementUpdated(el); - const boundingRect = el.getBoundingClientRect(); - - expect(firstItem.focused, 'not visually focused').to.be.false; - const opened = oneEvent(el, 'sp-opened'); - sendMouse({ - steps: [ - { - type: 'click', - position: [ - boundingRect.x + boundingRect.width / 2, - boundingRect.y + boundingRect.height / 2, - ], - }, - ], - }); - await opened; - - expect(el.open).to.be.true; - expect(firstItem.focused, 'still not visually focused').to.be.false; - }); - it('closes when becoming disabled', async () => { - expect(el.open).to.be.false; - el.click(); - await elementUpdated(el); - - expect(el.open).to.be.true; - el.disabled = true; - await elementUpdated(el); - - expect(el.open).to.be.false; - }); - it('closes when clicking away', async () => { - el.id = 'closing'; - const other = document.createElement('div'); - document.body.append(other); - - await elementUpdated(el); - - expect(el.open).to.be.false; - el.click(); - await elementUpdated(el); - - expect(el.open).to.be.true; - other.click(); - await waitUntil(() => !el.open, 'closed'); - - other.remove(); - }); - it('selects', async () => { - const secondItem = el.querySelector( - 'sp-menu-item:nth-of-type(2)' - ) as MenuItem; - const button = el.button as HTMLButtonElement; - - const opened = oneEvent(el, 'sp-opened'); - button.click(); - await opened; - await elementUpdated(el); - - expect(el.open).to.be.true; - expect(el.selectedItem?.itemText).to.be.undefined; - expect(el.value).to.equal(''); - - const closed = oneEvent(el, 'sp-closed'); - secondItem.click(); - await closed; - - expect(el.open).to.be.false; - expect(el.selectedItem?.itemText).to.equal('Select Inverse'); - expect(el.value).to.equal('option-2'); - }); - it('re-selects', async () => { - const firstItem = el.querySelector( - 'sp-menu-item:nth-of-type(1)' - ) as MenuItem; - const secondItem = el.querySelector( - 'sp-menu-item:nth-of-type(2)' - ) as MenuItem; - const button = el.button as HTMLButtonElement; - - const opened = oneEvent(el, 'sp-opened'); - button.click(); - await opened; - await nextFrame(); - - expect(el.open).to.be.true; - expect(el.selectedItem?.itemText).to.be.undefined; - expect(el.value).to.equal(''); - - const closed = oneEvent(el, 'sp-closed'); - secondItem.click(); - await closed; - await nextFrame(); - - expect(el.open).to.be.false; - expect(el.selectedItem?.itemText).to.equal('Select Inverse'); - expect(el.value).to.equal('option-2'); - - const opened2 = oneEvent(el, 'sp-opened'); - button.click(); - await opened2; - await nextFrame(); - - expect(el.open).to.be.true; - expect(el.selectedItem?.itemText).to.equal('Select Inverse'); - expect(el.value).to.equal('option-2'); - - const closed2 = oneEvent(el, 'sp-closed'); - firstItem.click(); - await closed2; - await nextFrame(); - - expect(el.open).to.be.false; - expect(el.selectedItem?.itemText).to.equal('Deselect'); - expect(el.value).to.equal('Deselect'); - }); - it('dispatches bubbling and composed events', async () => { - const changeSpy = spy(); - const parent = el.parentElement as HTMLElement; - parent.attachShadow({ mode: 'open' }); - (parent.shadowRoot as ShadowRoot).append(el); - const secondItem = el.querySelector( - 'sp-menu-item:nth-of-type(2)' - ) as MenuItem; - - parent.addEventListener('change', () => changeSpy()); - - expect(el.value).to.equal(''); - - const opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - await elementUpdated(el); - - const closed = oneEvent(el, 'sp-closed'); - secondItem.click(); - await closed; - await elementUpdated(el); - - expect(el.value).to.equal(secondItem.value); - expect(changeSpy.calledOnce).to.be.true; - }); - it('can have selection prevented', async () => { - const preventChangeSpy = spy(); - const secondItem = el.querySelector( - 'sp-menu-item:nth-of-type(2)' - ) as MenuItem; - const button = el.button as HTMLButtonElement; - - let opened = oneEvent(el, 'sp-opened'); - button.click(); - await opened; - await elementUpdated(el); - - expect(el.open).to.be.true; - expect(el.selectedItem?.itemText).to.be.undefined; - expect(el.value).to.equal(''); - expect(secondItem.selected).to.be.false; - - el.addEventListener('change', (event: Event): void => { - event.preventDefault(); - preventChangeSpy(); - }); - - const closed = oneEvent(el, 'sp-closed'); - opened = oneEvent(el, 'sp-opened'); - secondItem.click(); - await closed; - await opened; - await elementUpdated(el); - expect(preventChangeSpy.calledOnce).to.be.true; - expect(secondItem.selected, 'selection prevented').to.be.false; - }); - it('can throw focus after `change`', async () => { - const input = document.createElement('input'); - document.body.append(input); - - await elementUpdated(el); - - const secondItem = el.querySelector( - 'sp-menu-item:nth-of-type(2)' - ) as MenuItem; - const button = el.button as HTMLButtonElement; - - const opened = oneEvent(el, 'sp-opened'); - button.click(); - await opened; - await elementUpdated(el); - - expect(el.open).to.be.true; - expect(el.selectedItem?.itemText).to.be.undefined; - expect(el.value).to.equal(''); - expect(secondItem.selected).to.be.false; - - el.addEventListener('change', (): void => { - input.focus(); - }); - - const closed = oneEvent(el, 'sp-closed'); - secondItem.click(); - await closed; - await elementUpdated(el); - - expect(el.open).to.be.false; - expect(el.value, 'value changed').to.equal('option-2'); - expect(secondItem.selected, 'selected changed').to.be.true; - await waitUntil( - () => document.activeElement === input, - 'focus throw' - ); - input.remove(); - }); - it('opens on ArrowUp', async () => { - const button = el.button as HTMLButtonElement; - - el.focus(); - await elementUpdated(el); - - expect(el.open, 'inially closed').to.be.false; - - button.dispatchEvent(tEvent()); - await elementUpdated(el); - - expect(el.open, 'still closed').to.be.false; - - button.dispatchEvent(arrowUpEvent()); - await elementUpdated(el); - - expect(el.open, 'open by ArrowUp').to.be.true; - - await waitUntil( - () => document.querySelector('active-overlay') !== null, - 'an active-overlay has been inserted on the page' - ); - - button.dispatchEvent( - new KeyboardEvent('keyup', { - bubbles: true, - composed: true, - cancelable: true, - key: 'Escape', - code: 'Escape', - }) - ); - await elementUpdated(el); - await waitUntil(() => el.open === false, 'closed by Escape'); - await waitUntil( - () => document.querySelector('active-overlay') === null, - 'an active-overlay has been inserted on the page' - ); - }); - it('opens on ArrowDown', async () => { - const firstItem = el.querySelector( - 'sp-menu-item:nth-of-type(1)' - ) as MenuItem; - const button = el.button as HTMLButtonElement; - - el.focus(); - await elementUpdated(el); - - expect(el.open, 'inially closed').to.be.false; - - const opened = oneEvent(el, 'sp-opened'); - button.dispatchEvent(arrowDownEvent()); - await opened; - await elementUpdated(el); - - expect(el.open, 'open by ArrowDown').to.be.true; - expect(el.selectedItem?.itemText).to.be.undefined; - expect(el.value).to.equal(''); - - const closed = oneEvent(el, 'sp-closed'); - firstItem.click(); - await closed; - await elementUpdated(el); - - expect(el.open).to.be.false; - expect(el.selectedItem?.itemText).to.equal('Deselect'); - expect(el.value).to.equal('Deselect'); - }); - it('quick selects on ArrowLeft/Right', async () => { - const selectionSpy = spy(); - el.addEventListener('change', (event: Event) => { - const { value } = event.target as Picker; - selectionSpy(value); - }); - const button = el.button as HTMLButtonElement; - - el.focus(); - button.dispatchEvent(arrowLeftEvent()); - - await elementUpdated(el); - - expect(selectionSpy.callCount).to.equal(1); - expect(selectionSpy.calledWith('Deselected')); - button.dispatchEvent(arrowLeftEvent()); - - await elementUpdated(el); - expect(selectionSpy.callCount).to.equal(1); - button.dispatchEvent(arrowRightEvent()); - - await elementUpdated(el); - expect(selectionSpy.calledWith('option-2')); - - button.dispatchEvent(arrowRightEvent()); - button.dispatchEvent(arrowRightEvent()); - button.dispatchEvent(arrowRightEvent()); - button.dispatchEvent(arrowRightEvent()); - - await elementUpdated(el); - expect(selectionSpy.callCount).to.equal(5); - expect(selectionSpy.calledWith('Save Selection')); - expect(selectionSpy.calledWith('Make Work Path')).to.be.false; - }); - it('quick selects first item on ArrowRight when no value', async () => { - const selectionSpy = spy(); - el.addEventListener('change', (event: Event) => { - const { value } = event.target as Picker; - selectionSpy(value); - }); - const button = el.button as HTMLButtonElement; - - el.focus(); - button.dispatchEvent(arrowRightEvent()); - - await elementUpdated(el); - - expect(selectionSpy.callCount).to.equal(1); - expect(selectionSpy.calledWith('Deselected')); - }); - it('loads', async () => { - expect(el).to.not.be.undefined; - }); - it('refocuses on list when open', async () => { - const firstItem = el.querySelector('sp-menu-item') as MenuItem; - const input = document.createElement('input'); - el.insertAdjacentElement('afterend', input); - - el.focus(); - await sendKeys({ press: 'Tab' }); - expect(document.activeElement === input).to.be.true; - await sendKeys({ press: 'Shift+Tab' }); - expect(document.activeElement === el).to.be.true; - await sendKeys({ press: 'Enter' }); - const opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - await elementUpdated(el); - - await waitUntil( - () => firstItem.focused, - 'The first items should have become focused visually.' - ); - - el.blur(); - await elementUpdated(el); - - expect(el.open).to.be.true; - el.focus(); - await elementUpdated(el); - await waitUntil( - () => isMenuActiveElement(), - 'first item refocused' - ); - expect(el.open).to.be.true; - expect(isMenuActiveElement()).to.be.true; - // Force :focus-visible heuristic - await sendKeys({ press: 'ArrowDown' }); - await sendKeys({ press: 'ArrowUp' }); - expect(firstItem.focused).to.be.true; - }); - it('does not allow tabing to close', async () => { - el.open = true; - await elementUpdated(el); - - expect(el.open).to.be.true; - el.focus(); - await elementUpdated(el); - await waitUntil( - () => isMenuActiveElement(), - 'first item refocused' - ); - expect(el.open).to.be.true; - expect(isMenuActiveElement()).to.be.true; - - await sendKeys({ press: 'Tab' }); - - expect(el.open, 'stays open').to.be.true; - }); - describe('tab order', () => { - let input1: HTMLInputElement; - let input2: HTMLInputElement; - beforeEach(() => { - const surroundingInput = (): HTMLInputElement => { - const input = document.createElement('input'); - input.type = 'text'; - input.tabIndex = 0; - return input; - }; - input1 = surroundingInput(); - input2 = surroundingInput(); - - el.insertAdjacentElement('beforebegin', input1); - el.insertAdjacentElement('afterend', input2); - }); - afterEach(() => { - input1.remove(); - input2.remove(); - }); - it('tabs forward through the element', async () => { - // start at input1 - input1.focus(); - await nextFrame(); - expect(document.activeElement === input1, 'focuses input 1').to - .true; - // tab to the picker - let focused = oneEvent(el, 'focus'); - await sendKeys({ press: 'Tab' }); - await focused; - - expect(el.focused, 'focused').to.be.true; - expect(el.open, 'closed').to.be.false; - expect(document.activeElement === el, 'focuses el').to.be.true; - // tab through the picker to input2 - focused = oneEvent(input2, 'focus'); - await sendKeys({ press: 'Tab' }); - await focused; - expect(document.activeElement === input2, 'focuses input 2').to - .true; - }); - it('shift+tabs backwards through the element', async () => { - // start at input1 - input2.focus(); - await nextFrame(); - expect(document.activeElement, 'focuses input 2').to.equal( - input2 - ); - // tab to the picker - let focused = oneEvent(el, 'focus'); - await sendKeys({ press: 'Shift+Tab' }); - await focused; - - expect(el.focused, 'focused').to.be.true; - expect(el.open, 'closed').to.be.false; - expect(document.activeElement, 'focuses el').to.equal(el); - // tab through the picker to input2 - focused = oneEvent(input1, 'focus'); - await sendKeys({ press: 'Shift+Tab' }); - await focused; - expect(document.activeElement, 'focuses input 1').to.equal( - input1 - ); - }); - it('traps tab in the menu as a `type="modal"` overlay forward', async () => { - el.focus(); - await nextFrame(); - expect(document.activeElement, 'focuses el').to.equal(el); - // press down to open the picker - const opened = oneEvent(el, 'sp-opened'); - await sendKeys({ press: 'ArrowDown' }); - await opened; - - expect(el.open, 'opened').to.be.true; - await waitUntil( - () => isMenuActiveElement(), - 'first item focused' - ); - - const activeElement = document.activeElement as HTMLElement; - const blured = oneEvent(activeElement, 'blur'); - await sendKeys({ press: 'Tab' }); - await blured; - - expect(el.open).to.be.true; - expect(document.activeElement === input1).to.be.false; - expect(document.activeElement === input2).to.be.false; - }); - it('traps tab in the menu as a `type="modal"` overlay backwards', async () => { - el.focus(); - await nextFrame(); - expect(document.activeElement, 'focuses el').to.equal(el); - // press down to open the picker - const opened = oneEvent(el, 'sp-opened'); - await sendKeys({ press: 'ArrowDown' }); - await opened; - - expect(el.open, 'opened').to.be.true; - await waitUntil( - () => isMenuActiveElement(), - 'first item focused' - ); - - const activeElement = document.activeElement as HTMLElement; - const blured = oneEvent(activeElement, 'blur'); - await sendKeys({ press: 'Shift+Tab' }); - await blured; - - expect(el.open).to.be.true; - expect(document.activeElement === input1).to.be.false; - expect(document.activeElement === input2).to.be.false; - }); - it('can close and immediate tab to the next tab stop', async () => { - el.focus(); - await nextFrame(); - expect(document.activeElement, 'focuses el').to.equal(el); - // press down to open the picker - const opened = oneEvent(el, 'sp-opened'); - await sendKeys({ press: 'ArrowUp' }); - await opened; - - expect(el.open, 'opened').to.be.true; - await waitUntil( - () => isMenuActiveElement(), - 'first item focused' - ); - - const closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - - expect(el.open).to.be.false; - expect(document.activeElement === el).to.be.true; - - const focused = oneEvent(input2, 'focus'); - await sendKeys({ press: 'Tab' }); - await focused; - - expect(el.open).to.be.false; - expect(document.activeElement === input2).to.be.true; - }); - it('can close and immediate shift+tab to the previous tab stop', async () => { - el.focus(); - await nextFrame(); - expect(document.activeElement, 'focuses el').to.equal(el); - // press down to open the picker - const opened = oneEvent(el, 'sp-opened'); - await sendKeys({ press: 'ArrowUp' }); - await opened; - - expect(el.open, 'opened').to.be.true; - await waitUntil( - () => isMenuActiveElement(), - 'first item focused' - ); - - const closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - - expect(el.open).to.be.false; - expect(document.activeElement === el).to.be.true; - - const focused = oneEvent(input1, 'focus'); - await sendKeys({ press: 'Shift+Tab' }); - await focused; - - expect(el.open).to.be.false; - expect(document.activeElement === input1).to.be.true; - }); - }); - it('does not open when [readonly]', async () => { - el.readonly = true; - - await elementUpdated(el); - - const button = el.button as HTMLButtonElement; - - button.click(); - await elementUpdated(el); - - expect(el.open).to.be.false; - }); - it('scrolls selected into view on open', async () => { - await ( - el as unknown as { generatePopover(): void } - ).generatePopover(); - (el as unknown as { popover: Popover }).popover.style.height = - '40px'; - - const firstItem = el.querySelector( - 'sp-menu-item:first-child' - ) as MenuItem; - const lastItem = el.querySelector( - 'sp-menu-item:last-child' - ) as MenuItem; - lastItem.disabled = false; - el.value = lastItem.value; - - await elementUpdated(el); - - el.open = true; - - await elementUpdated(el); - await waitUntil(() => isMenuActiveElement(), 'first item focused'); - const getParentOffset = (el: HTMLElement): number => { - const parentScroll = (el.parentElement as HTMLElement) - .scrollTop; - const parentOffset = el.offsetTop - parentScroll; - return parentOffset; - }; - expect(getParentOffset(lastItem)).to.be.lessThan(40); - expect(getParentOffset(firstItem)).to.be.lessThan(-1); - - lastItem.dispatchEvent( - new FocusEvent('focusin', { bubbles: true }) - ); - lastItem.dispatchEvent(arrowDownEvent()); - await elementUpdated(el); - await nextFrame(); - expect(getParentOffset(lastItem)).to.be.greaterThan(40); - expect(getParentOffset(firstItem)).to.be.greaterThan(-1); - }); - }); - describe('slotted label', () => { - const pickerFixture = async (): Promise => { - const test = await fixture( - html` -
- - Where do you live? - - - - Select a Country with a very long label, too - long in fact - - Deselect - - Select Inverse - - Feather... - Select and Mask... - - Save Selection - Make Work Path - -
- ` - ); - - return test.querySelector('sp-picker') as Picker; - }; - beforeEach(async () => { - el = await pickerFixture(); - await elementUpdated(el); - }); - afterEach(async () => { - if (el.open) { - const closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - } - }); - - it('loads accessibly w/ slotted label', async () => { - await expect(el).to.be.accessible(); - }); - }); - describe('deprecated', () => { - const pickerFixture = async (): Promise => { - const test = await fixture( - html` -
- - Where do you live? - - - - Deselect - - Select Inverse - - Feather... - Select and Mask... - - Save Selection - - Make Work Path - - - -
- ` - ); - - return test.querySelector('sp-picker') as Picker; - }; - beforeEach(async () => { - el = await pickerFixture(); - await elementUpdated(el); - }); - afterEach(async () => { - if (el.open) { - const closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - } - }); - it('selects with deprecated syntax', async () => { - const secondItem = el.querySelector( - 'sp-menu-item:nth-of-type(2)' - ) as MenuItem; - - const opened = oneEvent(el, 'sp-opened'); - el.button.click(); - await opened; - await elementUpdated(el); - - expect(el.open).to.be.true; - expect(el.selectedItem?.itemText).to.be.undefined; - expect(el.value).to.equal(''); - - const closed = oneEvent(el, 'sp-closed'); - secondItem.click(); - await closed; - - expect(el.open).to.be.false; - expect(el.selectedItem?.itemText).to.equal('Select Inverse'); - expect(el.value).to.equal('option-2'); - }); - }); - it('manages its "name" value in the accessibility tree when [icons-only]', async () => { - const test = await fixture(html` -
${iconsOnly({})}
- `); - const el = test.querySelector('sp-picker') as Picker; - - await elementUpdated(el); - type NamedNode = { name: string }; - let snapshot = (await a11ySnapshot({})) as unknown as NamedNode & { - children: NamedNode[]; - }; - - expect( - findAccessibilityNode( - snapshot, - (node) => node.name === 'Choose an action type... Delete' - ), - '`name` is the label text' - ).to.not.be.null; - - el.value = '2'; - await elementUpdated(el); - snapshot = (await a11ySnapshot({})) as unknown as NamedNode & { - children: NamedNode[]; - }; - - expect( - findAccessibilityNode( - snapshot, - (node) => node.name === 'Choose an action type... Copy' - ), - '`name` is the label text plus the selected item text' - ).to.not.be.null; - }); - it('toggles between pickers', async () => { - const el2 = await pickerFixture(); - const el1 = await pickerFixture(); - - el1.id = 'away'; - el2.id = 'other'; - - await Promise.all([elementUpdated(el1), elementUpdated(el2)]); - - expect(el1.open, 'closed 1').to.be.false; - expect(el2.open, 'closed 1').to.be.false; - let open = oneEvent(el1, 'sp-opened'); - el1.click(); - await open; - expect(el1.open).to.be.true; - expect(el2.open).to.be.false; - - open = oneEvent(el2, 'sp-opened'); - let closed = oneEvent(el1, 'sp-closed'); - el2.click(); - await Promise.all([open, closed]); - expect(el1.open).to.be.false; - expect(el2.open).to.be.true; - - open = oneEvent(el1, 'sp-opened'); - closed = oneEvent(el2, 'sp-closed'); - el1.click(); - await Promise.all([open, closed]); - expect(el1.open).to.be.true; - expect(el2.open).to.be.false; - - closed = oneEvent(el1, 'sp-closed'); - sendKeys({ - press: 'Escape', - }); - await closed; - expect(el1.open).to.be.false; - }); - it('displays selected item text by default', async () => { - const el = await fixture( - html` - - Deselect Text - Select Inverse - Feather... - Select and Mask... - - Save Selection - Make Work Path - - ` - ); - - await elementUpdated(el); - await waitUntil( - () => el.selectedItem?.itemText === 'Select Inverse', - `Selected Item Text: ${el.selectedItem?.itemText}` - ); - - const firstItem = el.querySelector( - 'sp-menu-item:nth-of-type(1)' - ) as MenuItem; - const secondItem = el.querySelector( - 'sp-menu-item:nth-of-type(2)' - ) as MenuItem; - - expect(el.value).to.equal('inverse'); - expect(el.selectedItem?.itemText).to.equal('Select Inverse'); - - el.focus(); - await elementUpdated(el); - expect( - el === document.activeElement, - `activeElement is ${document.activeElement?.localName}` - ).to.be.true; - - const opened = oneEvent(el, 'sp-opened'); - sendKeys({ press: 'Enter' }); - await opened; - await elementUpdated(el.optionsMenu); - - expect( - el.optionsMenu === document.activeElement, - `activeElement is ${document.activeElement?.localName}` - ).to.be.true; - - expect(firstItem.focused, 'firstItem NOT "focused"').to.be.false; - expect(secondItem.focused, 'secondItem "focused"').to.be.true; - expect(el.optionsMenu.getAttribute('aria-activedescendant')).to.equal( - secondItem.id - ); - }); - it('resets value when item not available', async () => { - const el = await fixture( - html` - - Deselect Text - Select Inverse - Feather... - Select and Mask... - - Save Selection - Make Work Path - - ` - ); - - await elementUpdated(el); - await waitUntil(() => el.value === ''); - - expect(el.value).to.equal(''); - expect(el.selectedItem?.itemText).to.be.undefined; - }); - it('allows event listeners on child items', async () => { - const mouseenterSpy = spy(); - const handleMouseenter = (): void => mouseenterSpy(); - const el = await fixture( - html` - - - Deselect Text - - - ` - ); - - await elementUpdated(el); - - const hoverEl = el.querySelector('sp-menu-item') as MenuItem; - - const opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - await elementUpdated(el); - - expect(el.open).to.be.true; - hoverEl.dispatchEvent(new MouseEvent('mouseenter')); - await elementUpdated(el); - - expect(el.open).to.be.true; - - const closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - await elementUpdated(el); - - expect(el.open).to.be.false; - expect(mouseenterSpy.calledOnce).to.be.true; - }); - it('dispatches events on open/close', async () => { - const openedSpy = spy(); - const closedSpy = spy(); - const handleOpenedSpy = (event: Event): void => openedSpy(event); - const handleClosedSpy = (event: Event): void => closedSpy(event); - - const el = await fixture( - html` - - Deselect Text - - ` - ); - - await elementUpdated(el); - const opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - await elementUpdated(el); - - expect(openedSpy.calledOnce).to.be.true; - expect(closedSpy.calledOnce).to.be.false; - - const openedEvent = openedSpy - .args[0][0] as CustomEvent; - expect(openedEvent.detail.interaction).to.equal('modal'); - - const closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - await elementUpdated(el); - - expect(closedSpy.calledOnce).to.be.true; - - const closedEvent = closedSpy - .args[0][0] as CustomEvent; - expect(closedEvent.detail.interaction).to.equal('modal'); - }); + runPickerTests(); }); diff --git a/packages/picker/test/picker.test.ts b/packages/picker/test/picker.test.ts index 8995b57544..b9575f7922 100644 --- a/packages/picker/test/picker.test.ts +++ b/packages/picker/test/picker.test.ts @@ -11,1253 +11,8 @@ governing permissions and limitations under the License. */ import '../sp-picker.js'; -import { Picker } from '..'; +import { runPickerTests } from './index.js'; -import '@spectrum-web-components/overlay/active-overlay.js'; -import { OverlayOpenCloseDetail } from '@spectrum-web-components/overlay'; -import '@spectrum-web-components/menu/sp-menu.js'; -import '@spectrum-web-components/menu/sp-menu-item.js'; -import '@spectrum-web-components/menu/sp-menu-divider.js'; -import '@spectrum-web-components/field-label/sp-field-label.js'; -import { Menu, MenuItem } from '@spectrum-web-components/menu'; -import { - elementUpdated, - expect, - fixture, - html, - nextFrame, - oneEvent, - waitUntil, -} from '@open-wc/testing'; -import '@spectrum-web-components/shared/src/focus-visible.js'; -import { spy } from 'sinon'; -import { - arrowDownEvent, - arrowLeftEvent, - arrowRightEvent, - arrowUpEvent, - tEvent, -} from '../../../test/testing-helpers.js'; -import { - a11ySnapshot, - findAccessibilityNode, - sendKeys, -} from '@web/test-runner-commands'; -import { iconsOnly } from '../stories/picker.stories.js'; -import { sendMouse } from '../../../test/plugins/browser.js'; -import type { Popover } from '@spectrum-web-components/popover'; - -const isMenuActiveElement = function (): boolean { - return document.activeElement instanceof Menu; -}; - -describe('Picker, standard', () => { - let el: Picker; - const pickerFixture = async (): Promise => { - const test = await fixture( - html` -
- - Where do you live? - - - Deselect - - Select Inverse - - Feather... - Select and Mask... - - Save Selection - Make Work Path - -
- ` - ); - - return test.querySelector('sp-picker') as Picker; - }; - describe('standard', () => { - beforeEach(async () => { - el = await pickerFixture(); - await elementUpdated(el); - }); - afterEach(async () => { - if (el.open) { - const closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - } - }); - it('loads accessibly', async () => { - await expect(el).to.be.accessible(); - }); - it('accepts new selected item content', async () => { - const option2 = el.querySelector('[value="option-2"') as MenuItem; - el.value = 'option-2'; - await elementUpdated(el); - expect(el.value).to.equal('option-2'); - expect((el.button.textContent || '').trim()).to.equal( - 'Select Inverse' - ); - const itemUpdated = oneEvent(el, 'sp-menu-item-added-or-updated'); - option2.innerHTML = 'Invert Selection'; - await itemUpdated; - await elementUpdated(el); - expect(el.value).to.equal('option-2'); - expect((el.button.textContent || '').trim()).to.equal( - 'Invert Selection' - ); - }); - it('accepts new selected item content when open', async () => { - const option2 = el.querySelector('[value="option-2"') as MenuItem; - el.value = 'option-2'; - await elementUpdated(el); - expect(el.value).to.equal('option-2'); - expect((el.button.textContent || '').trim()).to.equal( - 'Select Inverse' - ); - const opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - const itemUpdated = oneEvent( - option2, - 'sp-menu-item-added-or-updated' - ); - option2.innerHTML = 'Invert Selection'; - await itemUpdated; - await elementUpdated(el); - expect(el.value).to.equal('option-2'); - expect((el.button.textContent || '').trim()).to.equal( - 'Invert Selection' - ); - }); - it('unsets value when children removed', async () => { - el.value = 'option-2'; - - await elementUpdated(el); - expect(el.value).to.equal('option-2'); - expect((el.button.textContent || '').trim()).to.equal( - 'Select Inverse' - ); - - const items = el.querySelectorAll('sp-menu-item'); - const removals: Promise[] = []; - items.forEach((item) => { - const removal = oneEvent(el, 'sp-menu-item-removed'); - item.remove(); - removals.push(removal); - }); - await Promise.all(removals); - await elementUpdated(el); - expect(el.value).to.equal(''); - expect((el.button.textContent || '').trim()).to.equal(''); - }); - it('accepts a new item and value at the same time', async () => { - el.value = 'option-2'; - - await elementUpdated(el); - expect(el.value).to.equal('option-2'); - - const item = document.createElement('sp-menu-item'); - item.value = 'option-new'; - item.textContent = 'New Option'; - - el.append(item); - await elementUpdated(el); - - el.value = 'option-new'; - - await elementUpdated(el); - expect(el.value).to.equal('option-new'); - }); - it('accepts a new item that can be selected', async () => { - el.value = 'option-2'; - - await elementUpdated(el); - expect(el.value).to.equal('option-2'); - const item = document.createElement('sp-menu-item'); - item.value = 'option-new'; - item.textContent = 'New Option'; - - el.append(item); - - await elementUpdated(item); - await elementUpdated(el); - - let opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - // Overlaid content is outside of the context of the Picker element - // and cannot be managed via its updateComplete cycle. - await nextFrame(); - - const close = oneEvent(el, 'sp-closed'); - item.click(); - await close; - // Overlaid content is outside of the context of the Picker element - // and cannot be managed via its updateComplete cycle. - await nextFrame(); - - expect(el.value, 'first time').to.equal('option-new'); - - opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - // Overlaid content is outside of the context of the Picker element - // and cannot be managed via its updateComplete cycle. - await nextFrame(); - - expect(el.value, 'second time').to.equal('option-new'); - }); - it('manages its "name" value in the accessibility tree', async () => { - type NamedNode = { name: string }; - let snapshot = (await a11ySnapshot({})) as unknown as NamedNode & { - children: NamedNode[]; - }; - - expect( - findAccessibilityNode( - snapshot, - (node) => node.name === 'Where do you live?' - ), - '`name` is the label text' - ).to.not.be.null; - - el.value = 'option-2'; - await elementUpdated(el); - snapshot = (await a11ySnapshot({})) as unknown as NamedNode & { - children: NamedNode[]; - }; - - expect( - findAccessibilityNode( - snapshot, - (node) => node.name === 'Where do you live? Select Inverse' - ), - '`name` is the label text plus the selected item text' - ).to.not.be.null; - }); - it('manages `aria-activedescendant`', async () => { - const firstItem = el.querySelector('sp-menu-item:nth-child(1)'); - const secondItem = el.querySelector('sp-menu-item:nth-child(2)'); - const opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - expect( - el.optionsMenu.getAttribute('aria-activedescendant') - ).to.equal(firstItem?.id); - await sendKeys({ press: 'ArrowDown' }); - await elementUpdated(el); - expect( - el.optionsMenu.getAttribute('aria-activedescendant') - ).to.equal(secondItem?.id); - }); - it('renders invalid accessibly', async () => { - el.invalid = true; - await elementUpdated(el); - - expect(el.invalid).to.be.true; - await expect(el).to.be.accessible(); - }); - it('renders selection accessibly', async () => { - el.value = 'option-2'; - await elementUpdated(el); - - await expect(el).to.be.accessible(); - }); - it('opens with visible focus on a menu item on `DownArrow`', async () => { - const firstItem = el.querySelector('sp-menu-item') as MenuItem; - - await elementUpdated(el); - - expect(firstItem.focused, 'should not visually focused').to.be - .false; - - el.focus(); - await elementUpdated(el); - const opened = oneEvent(el, 'sp-opened'); - await sendKeys({ press: 'ArrowRight' }); - await sendKeys({ press: 'ArrowLeft' }); - await sendKeys({ press: 'ArrowDown' }); - await opened; - - expect(el.open).to.be.true; - expect(firstItem.focused, 'should be visually focused').to.be.true; - - const closed = oneEvent(el, 'sp-closed'); - await sendKeys({ - press: 'Escape', - }); - await closed; - - expect(el.open).to.be.false; - await waitUntil(() => !firstItem.focused, 'not visually focused'); - }); - it('opens without visible focus on a menu item on click', async () => { - const firstItem = el.querySelector('sp-menu-item') as MenuItem; - - await elementUpdated(el); - const boundingRect = el.getBoundingClientRect(); - - expect(firstItem.focused, 'not visually focused').to.be.false; - const opened = oneEvent(el, 'sp-opened'); - sendMouse({ - steps: [ - { - type: 'click', - position: [ - boundingRect.x + boundingRect.width / 2, - boundingRect.y + boundingRect.height / 2, - ], - }, - ], - }); - await opened; - - expect(el.open).to.be.true; - expect(firstItem.focused, 'still not visually focused').to.be.false; - }); - it('closes when becoming disabled', async () => { - expect(el.open).to.be.false; - el.click(); - await elementUpdated(el); - - expect(el.open).to.be.true; - el.disabled = true; - await elementUpdated(el); - - expect(el.open).to.be.false; - }); - it('closes when clicking away', async () => { - el.id = 'closing'; - const other = document.createElement('div'); - document.body.append(other); - - await elementUpdated(el); - - expect(el.open).to.be.false; - el.click(); - await elementUpdated(el); - - expect(el.open).to.be.true; - other.click(); - await waitUntil(() => !el.open, 'closed'); - - other.remove(); - }); - it('selects', async () => { - const secondItem = el.querySelector( - 'sp-menu-item:nth-of-type(2)' - ) as MenuItem; - const button = el.button as HTMLButtonElement; - - const opened = oneEvent(el, 'sp-opened'); - button.click(); - await opened; - await elementUpdated(el); - - expect(el.open).to.be.true; - expect(el.selectedItem?.itemText).to.be.undefined; - expect(el.value).to.equal(''); - - const closed = oneEvent(el, 'sp-closed'); - secondItem.click(); - await closed; - - expect(el.open).to.be.false; - expect(el.selectedItem?.itemText).to.equal('Select Inverse'); - expect(el.value).to.equal('option-2'); - }); - it('re-selects', async () => { - const firstItem = el.querySelector( - 'sp-menu-item:nth-of-type(1)' - ) as MenuItem; - const secondItem = el.querySelector( - 'sp-menu-item:nth-of-type(2)' - ) as MenuItem; - const button = el.button as HTMLButtonElement; - - const opened = oneEvent(el, 'sp-opened'); - button.click(); - await opened; - await nextFrame(); - - expect(el.open).to.be.true; - expect(el.selectedItem?.itemText).to.be.undefined; - expect(el.value).to.equal(''); - - const closed = oneEvent(el, 'sp-closed'); - secondItem.click(); - await closed; - await nextFrame(); - - expect(el.open).to.be.false; - expect(el.selectedItem?.itemText).to.equal('Select Inverse'); - expect(el.value).to.equal('option-2'); - - const opened2 = oneEvent(el, 'sp-opened'); - button.click(); - await opened2; - await nextFrame(); - - expect(el.open).to.be.true; - expect(el.selectedItem?.itemText).to.equal('Select Inverse'); - expect(el.value).to.equal('option-2'); - - const closed2 = oneEvent(el, 'sp-closed'); - firstItem.click(); - await closed2; - await nextFrame(); - - expect(el.open).to.be.false; - expect(el.selectedItem?.itemText).to.equal('Deselect'); - expect(el.value).to.equal('Deselect'); - }); - it('dispatches bubbling and composed events', async () => { - const changeSpy = spy(); - const parent = el.parentElement as HTMLElement; - parent.attachShadow({ mode: 'open' }); - (parent.shadowRoot as ShadowRoot).append(el); - const secondItem = el.querySelector( - 'sp-menu-item:nth-of-type(2)' - ) as MenuItem; - - parent.addEventListener('change', () => changeSpy()); - - expect(el.value).to.equal(''); - - const opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - await elementUpdated(el); - - const closed = oneEvent(el, 'sp-closed'); - secondItem.click(); - await closed; - await elementUpdated(el); - - expect(el.value).to.equal(secondItem.value); - expect(changeSpy.calledOnce).to.be.true; - }); - it('can have selection prevented', async () => { - const preventChangeSpy = spy(); - const secondItem = el.querySelector( - 'sp-menu-item:nth-of-type(2)' - ) as MenuItem; - const button = el.button as HTMLButtonElement; - - let opened = oneEvent(el, 'sp-opened'); - button.click(); - await opened; - await elementUpdated(el); - - expect(el.open).to.be.true; - expect(el.selectedItem?.itemText).to.be.undefined; - expect(el.value).to.equal(''); - expect(secondItem.selected).to.be.false; - - el.addEventListener('change', (event: Event): void => { - event.preventDefault(); - preventChangeSpy(); - }); - - const closed = oneEvent(el, 'sp-closed'); - opened = oneEvent(el, 'sp-opened'); - secondItem.click(); - await closed; - await opened; - await elementUpdated(el); - expect(preventChangeSpy.calledOnce).to.be.true; - expect(secondItem.selected, 'selection prevented').to.be.false; - }); - it('can throw focus after `change`', async () => { - const input = document.createElement('input'); - document.body.append(input); - - await elementUpdated(el); - - const secondItem = el.querySelector( - 'sp-menu-item:nth-of-type(2)' - ) as MenuItem; - const button = el.button as HTMLButtonElement; - - const opened = oneEvent(el, 'sp-opened'); - button.click(); - await opened; - await elementUpdated(el); - - expect(el.open).to.be.true; - expect(el.selectedItem?.itemText).to.be.undefined; - expect(el.value).to.equal(''); - expect(secondItem.selected).to.be.false; - - el.addEventListener('change', (): void => { - input.focus(); - }); - - const closed = oneEvent(el, 'sp-closed'); - secondItem.click(); - await closed; - await elementUpdated(el); - - expect(el.open).to.be.false; - expect(el.value, 'value changed').to.equal('option-2'); - expect(secondItem.selected, 'selected changed').to.be.true; - await waitUntil( - () => document.activeElement === input, - 'focus throw' - ); - input.remove(); - }); - it('opens on ArrowUp', async () => { - const button = el.button as HTMLButtonElement; - - el.focus(); - await elementUpdated(el); - - expect(el.open, 'inially closed').to.be.false; - - button.dispatchEvent(tEvent()); - await elementUpdated(el); - - expect(el.open, 'still closed').to.be.false; - - button.dispatchEvent(arrowUpEvent()); - await elementUpdated(el); - - expect(el.open, 'open by ArrowUp').to.be.true; - - await waitUntil( - () => document.querySelector('active-overlay') !== null, - 'an active-overlay has been inserted on the page' - ); - - button.dispatchEvent( - new KeyboardEvent('keyup', { - bubbles: true, - composed: true, - cancelable: true, - key: 'Escape', - code: 'Escape', - }) - ); - await elementUpdated(el); - await waitUntil(() => el.open === false, 'closed by Escape'); - await waitUntil( - () => document.querySelector('active-overlay') === null, - 'an active-overlay has been inserted on the page' - ); - }); - it('opens on ArrowDown', async () => { - const firstItem = el.querySelector( - 'sp-menu-item:nth-of-type(1)' - ) as MenuItem; - const button = el.button as HTMLButtonElement; - - el.focus(); - await elementUpdated(el); - - expect(el.open, 'inially closed').to.be.false; - - const opened = oneEvent(el, 'sp-opened'); - button.dispatchEvent(arrowDownEvent()); - await opened; - await elementUpdated(el); - - expect(el.open, 'open by ArrowDown').to.be.true; - expect(el.selectedItem?.itemText).to.be.undefined; - expect(el.value).to.equal(''); - - const closed = oneEvent(el, 'sp-closed'); - firstItem.click(); - await closed; - await elementUpdated(el); - - expect(el.open).to.be.false; - expect(el.selectedItem?.itemText).to.equal('Deselect'); - expect(el.value).to.equal('Deselect'); - }); - it('quick selects on ArrowLeft/Right', async () => { - const selectionSpy = spy(); - el.addEventListener('change', (event: Event) => { - const { value } = event.target as Picker; - selectionSpy(value); - }); - const button = el.button as HTMLButtonElement; - - el.focus(); - button.dispatchEvent(arrowLeftEvent()); - - await elementUpdated(el); - - expect(selectionSpy.callCount).to.equal(1); - expect(selectionSpy.calledWith('Deselected')); - button.dispatchEvent(arrowLeftEvent()); - - await elementUpdated(el); - expect(selectionSpy.callCount).to.equal(1); - button.dispatchEvent(arrowRightEvent()); - - await elementUpdated(el); - expect(selectionSpy.calledWith('option-2')); - - button.dispatchEvent(arrowRightEvent()); - button.dispatchEvent(arrowRightEvent()); - button.dispatchEvent(arrowRightEvent()); - button.dispatchEvent(arrowRightEvent()); - - await elementUpdated(el); - expect(selectionSpy.callCount).to.equal(5); - expect(selectionSpy.calledWith('Save Selection')); - expect(selectionSpy.calledWith('Make Work Path')).to.be.false; - }); - it('quick selects first item on ArrowRight when no value', async () => { - const selectionSpy = spy(); - el.addEventListener('change', (event: Event) => { - const { value } = event.target as Picker; - selectionSpy(value); - }); - const button = el.button as HTMLButtonElement; - - el.focus(); - button.dispatchEvent(arrowRightEvent()); - - await elementUpdated(el); - - expect(selectionSpy.callCount).to.equal(1); - expect(selectionSpy.calledWith('Deselected')); - }); - it('loads', async () => { - expect(el).to.not.be.undefined; - }); - it('refocuses on list when open', async () => { - const firstItem = el.querySelector('sp-menu-item') as MenuItem; - const input = document.createElement('input'); - el.insertAdjacentElement('afterend', input); - - el.focus(); - await sendKeys({ press: 'Tab' }); - expect(document.activeElement === input).to.be.true; - await sendKeys({ press: 'Shift+Tab' }); - expect(document.activeElement === el).to.be.true; - await sendKeys({ press: 'Enter' }); - const opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - await elementUpdated(el); - - await waitUntil( - () => firstItem.focused, - 'The first items should have become focused visually.' - ); - - el.blur(); - await elementUpdated(el); - - expect(el.open).to.be.true; - el.focus(); - await elementUpdated(el); - await waitUntil( - () => isMenuActiveElement(), - 'first item refocused' - ); - expect(el.open).to.be.true; - expect(isMenuActiveElement()).to.be.true; - // Force :focus-visible heuristic - await sendKeys({ press: 'ArrowDown' }); - await sendKeys({ press: 'ArrowUp' }); - expect(firstItem.focused).to.be.true; - }); - it('does not allow tabing to close', async () => { - el.open = true; - await elementUpdated(el); - - expect(el.open).to.be.true; - el.focus(); - await elementUpdated(el); - await waitUntil( - () => isMenuActiveElement(), - 'first item refocused' - ); - expect(el.open).to.be.true; - expect(isMenuActiveElement()).to.be.true; - - await sendKeys({ press: 'Tab' }); - - expect(el.open, 'stays open').to.be.true; - }); - describe('tab order', () => { - let input1: HTMLInputElement; - let input2: HTMLInputElement; - beforeEach(() => { - const surroundingInput = (): HTMLInputElement => { - const input = document.createElement('input'); - input.type = 'text'; - input.tabIndex = 0; - return input; - }; - input1 = surroundingInput(); - input2 = surroundingInput(); - - el.insertAdjacentElement('beforebegin', input1); - el.insertAdjacentElement('afterend', input2); - }); - afterEach(() => { - input1.remove(); - input2.remove(); - }); - it('tabs forward through the element', async () => { - // start at input1 - input1.focus(); - await nextFrame(); - expect(document.activeElement === input1, 'focuses input 1').to - .true; - // tab to the picker - let focused = oneEvent(el, 'focus'); - await sendKeys({ press: 'Tab' }); - await focused; - - expect(el.focused, 'focused').to.be.true; - expect(el.open, 'closed').to.be.false; - expect(document.activeElement === el, 'focuses el').to.be.true; - // tab through the picker to input2 - focused = oneEvent(input2, 'focus'); - await sendKeys({ press: 'Tab' }); - await focused; - expect(document.activeElement === input2, 'focuses input 2').to - .true; - }); - it('shift+tabs backwards through the element', async () => { - // start at input1 - input2.focus(); - await nextFrame(); - expect(document.activeElement, 'focuses input 2').to.equal( - input2 - ); - // tab to the picker - let focused = oneEvent(el, 'focus'); - await sendKeys({ press: 'Shift+Tab' }); - await focused; - - expect(el.focused, 'focused').to.be.true; - expect(el.open, 'closed').to.be.false; - expect(document.activeElement, 'focuses el').to.equal(el); - // tab through the picker to input2 - focused = oneEvent(input1, 'focus'); - await sendKeys({ press: 'Shift+Tab' }); - await focused; - expect(document.activeElement, 'focuses input 1').to.equal( - input1 - ); - }); - it('traps tab in the menu as a `type="modal"` overlay forward', async () => { - el.focus(); - await nextFrame(); - expect(document.activeElement, 'focuses el').to.equal(el); - // press down to open the picker - const opened = oneEvent(el, 'sp-opened'); - await sendKeys({ press: 'ArrowDown' }); - await opened; - - expect(el.open, 'opened').to.be.true; - await waitUntil( - () => isMenuActiveElement(), - 'first item focused' - ); - - const activeElement = document.activeElement as HTMLElement; - const blured = oneEvent(activeElement, 'blur'); - await sendKeys({ press: 'Tab' }); - await blured; - - expect(el.open).to.be.true; - expect(document.activeElement === input1).to.be.false; - expect(document.activeElement === input2).to.be.false; - }); - it('traps tab in the menu as a `type="modal"` overlay backwards', async () => { - el.focus(); - await nextFrame(); - expect(document.activeElement, 'focuses el').to.equal(el); - // press down to open the picker - const opened = oneEvent(el, 'sp-opened'); - await sendKeys({ press: 'ArrowDown' }); - await opened; - - expect(el.open, 'opened').to.be.true; - await waitUntil( - () => isMenuActiveElement(), - 'first item focused' - ); - - const activeElement = document.activeElement as HTMLElement; - const blured = oneEvent(activeElement, 'blur'); - await sendKeys({ press: 'Shift+Tab' }); - await blured; - - expect(el.open).to.be.true; - expect(document.activeElement === input1).to.be.false; - expect(document.activeElement === input2).to.be.false; - }); - it('can close and immediate tab to the next tab stop', async () => { - el.focus(); - await nextFrame(); - expect(document.activeElement, 'focuses el').to.equal(el); - // press down to open the picker - const opened = oneEvent(el, 'sp-opened'); - await sendKeys({ press: 'ArrowUp' }); - await opened; - - expect(el.open, 'opened').to.be.true; - await waitUntil( - () => isMenuActiveElement(), - 'first item focused' - ); - - const closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - - expect(el.open).to.be.false; - expect(document.activeElement === el).to.be.true; - - const focused = oneEvent(input2, 'focus'); - await sendKeys({ press: 'Tab' }); - await focused; - - expect(el.open).to.be.false; - expect(document.activeElement === input2).to.be.true; - }); - it('can close and immediate shift+tab to the previous tab stop', async () => { - el.focus(); - await nextFrame(); - expect(document.activeElement, 'focuses el').to.equal(el); - // press down to open the picker - const opened = oneEvent(el, 'sp-opened'); - await sendKeys({ press: 'ArrowUp' }); - await opened; - - expect(el.open, 'opened').to.be.true; - await waitUntil( - () => isMenuActiveElement(), - 'first item focused' - ); - - const closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - - expect(el.open).to.be.false; - expect(document.activeElement === el).to.be.true; - - const focused = oneEvent(input1, 'focus'); - await sendKeys({ press: 'Shift+Tab' }); - await focused; - - expect(el.open).to.be.false; - expect(document.activeElement === input1).to.be.true; - }); - }); - it('does not open when [readonly]', async () => { - el.readonly = true; - - await elementUpdated(el); - - const button = el.button as HTMLButtonElement; - - button.click(); - await elementUpdated(el); - - expect(el.open).to.be.false; - }); - it('scrolls selected into view on open', async () => { - await ( - el as unknown as { generatePopover(): void } - ).generatePopover(); - (el as unknown as { popover: Popover }).popover.style.height = - '40px'; - - const firstItem = el.querySelector( - 'sp-menu-item:first-child' - ) as MenuItem; - const lastItem = el.querySelector( - 'sp-menu-item:last-child' - ) as MenuItem; - lastItem.disabled = false; - el.value = lastItem.value; - - await elementUpdated(el); - - el.open = true; - - await elementUpdated(el); - await waitUntil(() => isMenuActiveElement(), 'first item focused'); - const getParentOffset = (el: HTMLElement): number => { - const parentScroll = (el.parentElement as HTMLElement) - .scrollTop; - const parentOffset = el.offsetTop - parentScroll; - return parentOffset; - }; - expect(getParentOffset(lastItem)).to.be.lessThan(40); - expect(getParentOffset(firstItem)).to.be.lessThan(-1); - - lastItem.dispatchEvent( - new FocusEvent('focusin', { bubbles: true }) - ); - lastItem.dispatchEvent(arrowDownEvent()); - await elementUpdated(el); - await nextFrame(); - expect(getParentOffset(lastItem)).to.be.greaterThan(40); - expect(getParentOffset(firstItem)).to.be.greaterThan(-1); - }); - }); - describe('slotted label', () => { - const pickerFixture = async (): Promise => { - const test = await fixture( - html` -
- - Where do you live? - - - - Select a Country with a very long label, too - long in fact - - Deselect - - Select Inverse - - Feather... - Select and Mask... - - Save Selection - Make Work Path - -
- ` - ); - - return test.querySelector('sp-picker') as Picker; - }; - beforeEach(async () => { - el = await pickerFixture(); - await elementUpdated(el); - }); - afterEach(async () => { - if (el.open) { - const closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - } - }); - - it('loads accessibly w/ slotted label', async () => { - await expect(el).to.be.accessible(); - }); - }); - describe('deprecated', () => { - const pickerFixture = async (): Promise => { - const test = await fixture( - html` -
- - Where do you live? - - - - Deselect - - Select Inverse - - Feather... - Select and Mask... - - Save Selection - - Make Work Path - - - -
- ` - ); - - return test.querySelector('sp-picker') as Picker; - }; - beforeEach(async () => { - el = await pickerFixture(); - await elementUpdated(el); - }); - afterEach(async () => { - if (el.open) { - const closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - } - }); - it('selects with deprecated syntax', async () => { - const secondItem = el.querySelector( - 'sp-menu-item:nth-of-type(2)' - ) as MenuItem; - - const opened = oneEvent(el, 'sp-opened'); - el.button.click(); - await opened; - await elementUpdated(el); - - expect(el.open).to.be.true; - expect(el.selectedItem?.itemText).to.be.undefined; - expect(el.value).to.equal(''); - - const closed = oneEvent(el, 'sp-closed'); - secondItem.click(); - await closed; - - expect(el.open).to.be.false; - expect(el.selectedItem?.itemText).to.equal('Select Inverse'); - expect(el.value).to.equal('option-2'); - }); - }); - it('manages its "name" value in the accessibility tree when [icons-only]', async () => { - const test = await fixture(html` -
${iconsOnly({})}
- `); - const el = test.querySelector('sp-picker') as Picker; - - await elementUpdated(el); - type NamedNode = { name: string }; - let snapshot = (await a11ySnapshot({})) as unknown as NamedNode & { - children: NamedNode[]; - }; - - expect( - findAccessibilityNode( - snapshot, - (node) => node.name === 'Choose an action type... Delete' - ), - '`name` is the label text' - ).to.not.be.null; - - el.value = '2'; - await elementUpdated(el); - snapshot = (await a11ySnapshot({})) as unknown as NamedNode & { - children: NamedNode[]; - }; - - expect( - findAccessibilityNode( - snapshot, - (node) => node.name === 'Choose an action type... Copy' - ), - '`name` is the label text plus the selected item text' - ).to.not.be.null; - }); - it('toggles between pickers', async () => { - const el2 = await pickerFixture(); - const el1 = await pickerFixture(); - - el1.id = 'away'; - el2.id = 'other'; - - await Promise.all([elementUpdated(el1), elementUpdated(el2)]); - - expect(el1.open, 'closed 1').to.be.false; - expect(el2.open, 'closed 1').to.be.false; - let open = oneEvent(el1, 'sp-opened'); - el1.click(); - await open; - expect(el1.open).to.be.true; - expect(el2.open).to.be.false; - - open = oneEvent(el2, 'sp-opened'); - let closed = oneEvent(el1, 'sp-closed'); - el2.click(); - await Promise.all([open, closed]); - expect(el1.open).to.be.false; - expect(el2.open).to.be.true; - - open = oneEvent(el1, 'sp-opened'); - closed = oneEvent(el2, 'sp-closed'); - el1.click(); - await Promise.all([open, closed]); - expect(el1.open).to.be.true; - expect(el2.open).to.be.false; - - closed = oneEvent(el1, 'sp-closed'); - sendKeys({ - press: 'Escape', - }); - await closed; - expect(el1.open).to.be.false; - }); - it('displays selected item text by default', async () => { - const el = await fixture( - html` - - Deselect Text - Select Inverse - Feather... - Select and Mask... - - Save Selection - Make Work Path - - ` - ); - - await elementUpdated(el); - await waitUntil( - () => el.selectedItem?.itemText === 'Select Inverse', - `Selected Item Text: ${el.selectedItem?.itemText}` - ); - - const firstItem = el.querySelector( - 'sp-menu-item:nth-of-type(1)' - ) as MenuItem; - const secondItem = el.querySelector( - 'sp-menu-item:nth-of-type(2)' - ) as MenuItem; - - expect(el.value).to.equal('inverse'); - expect(el.selectedItem?.itemText).to.equal('Select Inverse'); - - el.focus(); - await elementUpdated(el); - expect( - el === document.activeElement, - `activeElement is ${document.activeElement?.localName}` - ).to.be.true; - - const opened = oneEvent(el, 'sp-opened'); - sendKeys({ press: 'Enter' }); - await opened; - await elementUpdated(el.optionsMenu); - expect( - el.optionsMenu === document.activeElement, - `activeElement is ${document.activeElement?.localName}` - ).to.be.true; - - expect(firstItem.focused, 'firstItem NOT "focused"').to.be.false; - expect(secondItem.focused, 'secondItem "focused"').to.be.true; - expect(el.optionsMenu.getAttribute('aria-activedescendant')).to.equal( - secondItem.id - ); - }); - it('resets value when item not available', async () => { - const el = await fixture( - html` - - Deselect Text - Select Inverse - Feather... - Select and Mask... - - Save Selection - Make Work Path - - ` - ); - - await elementUpdated(el); - await waitUntil(() => el.value === ''); - - expect(el.value).to.equal(''); - expect(el.selectedItem?.itemText).to.be.undefined; - }); - it('allows event listeners on child items', async () => { - const mouseenterSpy = spy(); - const handleMouseenter = (): void => mouseenterSpy(); - const el = await fixture( - html` - - - Deselect Text - - - ` - ); - - await elementUpdated(el); - - const hoverEl = el.querySelector('sp-menu-item') as MenuItem; - - const opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - await elementUpdated(el); - - expect(el.open).to.be.true; - hoverEl.dispatchEvent(new MouseEvent('mouseenter')); - await elementUpdated(el); - - expect(el.open).to.be.true; - - const closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - await elementUpdated(el); - - expect(el.open).to.be.false; - expect(mouseenterSpy.calledOnce).to.be.true; - }); - it('dispatches events on open/close', async () => { - const openedSpy = spy(); - const closedSpy = spy(); - const handleOpenedSpy = (event: Event): void => openedSpy(event); - const handleClosedSpy = (event: Event): void => closedSpy(event); - - const el = await fixture( - html` - - Deselect Text - - ` - ); - - await elementUpdated(el); - const opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - await elementUpdated(el); - - expect(openedSpy.calledOnce).to.be.true; - expect(closedSpy.calledOnce).to.be.false; - - const openedEvent = openedSpy - .args[0][0] as CustomEvent; - expect(openedEvent.detail.interaction).to.equal('modal'); - - const closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - await elementUpdated(el); - - expect(closedSpy.calledOnce).to.be.true; - - const closedEvent = closedSpy - .args[0][0] as CustomEvent; - expect(closedEvent.detail.interaction).to.equal('modal'); - }); +describe('Picker, sync', () => { + runPickerTests(); }); diff --git a/packages/split-button/test/index.ts b/packages/split-button/test/index.ts new file mode 100644 index 0000000000..304715be87 --- /dev/null +++ b/packages/split-button/test/index.ts @@ -0,0 +1,484 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { elementUpdated, expect, fixture, oneEvent } from '@open-wc/testing'; +import { sendKeys } from '@web/test-runner-commands'; +import { html, TemplateResult } from '@spectrum-web-components/base'; +import { spy } from 'sinon'; + +import fieldDefaults, { + m as field, +} from '../stories/split-button-accent-field.stories.js'; +import moreDefaults, { + m as more, +} from '../stories/split-button-accent-more.stories.js'; + +import type { Button } from '@spectrum-web-components/button'; +import type { MenuItem } from '@spectrum-web-components/menu'; +import type { SplitButton } from '@spectrum-web-components/split-button'; + +// const pickerReady = async (picker: SplitButton): Promise => { +// await elementUpdated(picker); +// if (picker.open) { +// await elementUpdated(picker.optionsMenu); +// } +// } + +export function runSplitButtonTests( + wrapInDiv: (storyArgument: TemplateResult) => TemplateResult, + deprecatedMenu: () => TemplateResult +): void { + it('loads [type="field"] splitbutton accessibly', async () => { + const test = await fixture( + wrapInDiv( + field({ + ...fieldDefaults.args, + ...field.args, + }) + ) + ); + const el1 = test.querySelector('sp-split-button') as SplitButton; + const el2 = test.querySelector('sp-split-button[left]') as SplitButton; + + await elementUpdated(el1); + await elementUpdated(el2); + + await expect(el1).to.be.accessible(); + await expect(el2).to.be.accessible(); + }); + it('loads [type="field"] splitbutton accessibly with deprecated syntax', async () => { + const test = await fixture(html` +
+ ${deprecatedMenu()} + ${deprecatedMenu()} +
+ `); + const el1 = test.querySelector('sp-split-button') as SplitButton; + const el2 = test.querySelector('sp-split-button[left]') as SplitButton; + + await elementUpdated(el1); + await elementUpdated(el2); + + await expect(el1).to.be.accessible(); + await expect(el2).to.be.accessible(); + }); + it('loads [type="more"] splitbutton accessibly', async () => { + const test = await fixture( + wrapInDiv(more({ ...moreDefaults.args, ...more.args })) + ); + const el1 = test.querySelector('sp-split-button') as SplitButton; + const el2 = test.querySelector('sp-split-button[left]') as SplitButton; + + await elementUpdated(el1); + await elementUpdated(el2); + + await expect(el1).to.be.accessible(); + await expect(el2).to.be.accessible(); + }); + it('loads [type="more"] splitbutton accessibly with deprecated syntax', async () => { + const test = await fixture(html` +
+ + ${deprecatedMenu()} + + + ${deprecatedMenu()} + +
+ `); + const el1 = test.querySelector('sp-split-button') as SplitButton; + const el2 = test.querySelector('sp-split-button[left]') as SplitButton; + + await elementUpdated(el1); + await elementUpdated(el2); + + await expect(el1).to.be.accessible(); + await expect(el2).to.be.accessible(); + }); + it('[type="field"] toggles open/close multiple time', async () => { + const test = await fixture( + wrapInDiv( + field({ + ...fieldDefaults.args, + ...field.args, + }) + ) + ); + const el = test.querySelector('sp-split-button') as SplitButton; + + await elementUpdated(el); + let items = el.querySelectorAll('sp-menu-item'); + expect(items.length).to.equal(3); + + let opened = oneEvent(el, 'sp-opened'); + el.open = true; + await opened; + + expect(el.open).to.be.true; + items = el.querySelectorAll('sp-menu-item'); + expect(items.length).to.equal(0); + + let closed = oneEvent(el, 'sp-closed'); + el.open = false; + await closed; + + expect(el.open).to.be.false; + items = el.querySelectorAll('sp-menu-item'); + expect(items.length).to.equal(3); + + opened = oneEvent(el, 'sp-opened'); + el.open = true; + await opened; + + expect(el.open).to.be.true; + items = el.querySelectorAll('sp-menu-item'); + expect(items.length).to.equal(0); + + closed = oneEvent(el, 'sp-closed'); + el.open = false; + await closed; + + expect(el.open).to.be.false; + items = el.querySelectorAll('sp-menu-item'); + expect(items.length).to.equal(3); + }); + it('[type="more"] toggles open/close multiple time', async () => { + const test = await fixture( + wrapInDiv(more({ ...moreDefaults.args, ...more.args })) + ); + const el = test.querySelector('sp-split-button') as SplitButton; + + await elementUpdated(el); + let items = el.querySelectorAll('sp-menu-item'); + expect(items.length).to.equal(3); + + let opened = oneEvent(el, 'sp-opened'); + el.open = true; + await opened; + + expect(el.open).to.be.true; + items = el.querySelectorAll('sp-menu-item'); + expect(items.length).to.equal(0); + + let closed = oneEvent(el, 'sp-closed'); + el.open = false; + await closed; + + expect(el.open).to.be.false; + items = el.querySelectorAll('sp-menu-item'); + expect(items.length).to.equal(3); + + opened = oneEvent(el, 'sp-opened'); + el.open = true; + await opened; + + expect(el.open).to.be.true; + items = el.querySelectorAll('sp-menu-item'); + expect(items.length).to.equal(0); + + closed = oneEvent(el, 'sp-closed'); + el.open = false; + await closed; + + expect(el.open).to.be.false; + items = el.querySelectorAll('sp-menu-item'); + expect(items.length).to.equal(3); + }); + it('receives "focus()"', async () => { + const test = await fixture( + wrapInDiv( + field({ + ...fieldDefaults.args, + ...field.args, + }) + ) + ); + const el1 = test.querySelector('sp-split-button') as SplitButton; + const el2 = test.querySelector('sp-split-button[left]') as SplitButton; + + await elementUpdated(el1); + await elementUpdated(el2); + + el1.focus(); + await elementUpdated(el1); + + expect(document.activeElement === el1).to.be.true; + expect(el1.shadowRoot.activeElement).to.equal(el1.focusElement); + + el2.focus(); + await elementUpdated(el2); + + expect(document.activeElement === el2).to.be.true; + expect(el2.shadowRoot.activeElement === el2.focusElement).to.be.true; + }); + it('[type="field"] manages `selectedItem`', async () => { + const test = await fixture( + wrapInDiv( + field({ + ...fieldDefaults.args, + ...field.args, + }) + ) + ); + const el = test.querySelector('sp-split-button') as SplitButton; + + await elementUpdated(el); + + expect(el.selectedItem?.itemText).to.equal('Option 1'); + expect(el.open).to.be.false; + + const item3 = el.querySelector('sp-menu-item:nth-child(3)') as MenuItem; + const root = el.shadowRoot ? el.shadowRoot : el; + const toggleButton = root.querySelector( + '.trigger' + ) as HTMLButtonElement; + const opened = oneEvent(el, 'sp-opened'); + toggleButton.click(); + await opened; + await elementUpdated(el); + + expect(el.open).to.be.true; + + const closed = oneEvent(el, 'sp-closed'); + item3.click(); + await closed; + + await elementUpdated(el); + + expect(el.selectedItem?.itemText).to.equal('Short'); + expect(el.open).to.be.false; + expect(document.activeElement === el).to.be.true; + expect(el.shadowRoot.activeElement === el.focusElement).to.be.true; + }); + it('[type="more"] manages `selectedItem.itemText`', async () => { + const test = await fixture( + wrapInDiv(more({ ...moreDefaults.args, ...more.args })) + ); + const el = test.querySelector('sp-split-button') as SplitButton; + + await elementUpdated(el); + + expect(el.selectedItem?.itemText).to.equal('Option 1'); + expect(el.open).to.be.false; + + const item3 = el.querySelector('sp-menu-item:nth-child(3)') as MenuItem; + const root = el.shadowRoot ? el.shadowRoot : el; + const toggleButton = root.querySelector( + '.trigger' + ) as HTMLButtonElement; + const opened = oneEvent(el, 'sp-opened'); + toggleButton.click(); + await opened; + + await elementUpdated(el); + + expect(el.open).to.be.true; + + const closed = oneEvent(el, 'sp-closed'); + item3.click(); + await closed; + + await elementUpdated(el); + + expect(el.open).to.be.false; + expect(el.selectedItem?.itemText).to.equal('Option 1'); + }); + + it('passes click events as [type="field"]', async () => { + const firstItemSpy = spy(); + const secondItemSpy = spy(); + const thirdItemSpy = spy(); + const test = await fixture( + wrapInDiv( + field({ + ...fieldDefaults.args, + ...field.args, + firstItemHandler: (): void => firstItemSpy(), + secondItemHandler: (): void => secondItemSpy(), + thirdItemHandler: (): void => thirdItemSpy(), + }) + ) + ); + const el = test.querySelector('sp-split-button') as SplitButton; + await elementUpdated(el); + + expect(el.selectedItem?.itemText).to.equal('Option 1'); + expect(el.open).to.be.false; + + const item1 = el.querySelector('sp-menu-item:nth-child(1)') as MenuItem; + const item2 = el.querySelector('sp-menu-item:nth-child(2)') as MenuItem; + const item3 = el.querySelector('sp-menu-item:nth-child(3)') as MenuItem; + const main = el.button; + + main.click(); + + await elementUpdated(el); + + expect(firstItemSpy.called, 'first called').to.be.true; + expect(firstItemSpy.calledOnce, 'first calledOnce').to.be.true; + + const trigger = (el as unknown as { trigger: Button }).trigger; + let opened = oneEvent(el, 'sp-opened'); + trigger.click(); + await opened; + + await elementUpdated(el); + + expect(el.open, 'open').to.be.true; + + let closed = oneEvent(el, 'sp-closed'); + item3.click(); + await closed; + + await elementUpdated(el); + + expect(el.open, 'not open').to.be.false; + expect(thirdItemSpy.called, 'third called').to.be.true; + expect(thirdItemSpy.calledOnce, 'third calledOnce').to.be.true; + + main.click(); + await elementUpdated(el); + + expect(el.open).to.be.false; + expect(el.selectedItem?.itemText).to.equal('Short'); + expect(thirdItemSpy.called, 'third called, still').to.be.true; + expect(thirdItemSpy.callCount, 'third callCount').to.equal(2); + expect(thirdItemSpy.calledTwice, 'third calledTwice').to.be.true; + + await sendKeys({ + press: 'Tab', + }); + opened = oneEvent(el, 'sp-opened'); + sendKeys({ + press: 'ArrowDown', + }); + await opened; + + await elementUpdated(el); + + expect(el.open, 'reopened').to.be.true; + + closed = oneEvent(el, 'sp-closed'); + item2.click(); + await closed; + + await elementUpdated(el); + + main.click(); + + await elementUpdated(el); + + expect(el.open).to.be.false; + expect(el.selectedItem?.itemText).to.equal('Option Really Extended'); + expect(secondItemSpy.called, 'second called').to.be.true; + expect(secondItemSpy.calledTwice, 'second twice').to.be.true; + + opened = oneEvent(el, 'sp-opened'); + trigger.click(); + await opened; + + await elementUpdated(el); + + expect(el.open, 'opened again').to.be.true; + + closed = oneEvent(el, 'sp-closed'); + item1.click(); + await closed; + await elementUpdated(el); + + main.click(); + + await elementUpdated(el); + + expect(el.selectedItem?.itemText).to.equal('Option 1'); + expect(firstItemSpy.called, 'first called, sill').to.be.true; + expect(firstItemSpy.callCount, 'first callCount').to.equal(3); + }); + it('passes click events as [type="more"]', async () => { + const firstItemSpy = spy(); + const secondItemSpy = spy(); + const thirdItemSpy = spy(); + const test = await fixture( + wrapInDiv( + more({ + ...moreDefaults.args, + ...more.args, + firstItemHandler: (): void => firstItemSpy(), + secondItemHandler: (): void => secondItemSpy(), + thirdItemHandler: (): void => thirdItemSpy(), + }) + ) + ); + const el = test.querySelector('sp-split-button') as SplitButton; + + await elementUpdated(el); + + expect(el.selectedItem?.itemText).to.equal('Option 1'); + expect(el.open).to.be.false; + + const item2 = el.querySelector('sp-menu-item:nth-child(2)') as MenuItem; + const item3 = el.querySelector('sp-menu-item:nth-child(3)') as MenuItem; + const root = el.shadowRoot ? el.shadowRoot : el; + const main = root.querySelector('#button') as HTMLButtonElement; + + main.click(); + + await elementUpdated(el); + + expect(firstItemSpy.called, '1st called').to.be.true; + expect(firstItemSpy.calledOnce, '1st called once').to.be.true; + + const trigger = root.querySelector('.trigger') as HTMLButtonElement; + let opened = oneEvent(el, 'sp-opened'); + trigger.click(); + await opened; + + await elementUpdated(el); + + expect(el.open).to.be.true; + + let closed = oneEvent(el, 'sp-closed'); + item3.click(); + await closed; + await elementUpdated(el); + + expect(el.open, 'not open').to.be.false; + expect(el.selectedItem?.itemText).to.equal('Option 1'); + expect(thirdItemSpy.called, '3rd called').to.be.true; + expect(thirdItemSpy.calledOnce, '3rd called once').to.be.true; + opened = oneEvent(el, 'sp-opened'); + trigger.click(); + await opened; + + await elementUpdated(el); + + expect(el.open).to.be.true; + + closed = oneEvent(el, 'sp-closed'); + item2.click(); + await closed; + + await elementUpdated(el); + + expect(el.open).to.be.false; + expect(el.selectedItem?.itemText).to.equal('Option 1'); + expect(secondItemSpy.called, '2nd called').to.be.true; + expect(secondItemSpy.calledOnce, '2nd called once').to.be.true; + + main.click(); + + await elementUpdated(el); + + expect(firstItemSpy.called).to.be.true; + expect(firstItemSpy.calledTwice, '1st called twice').to.be.true; + }); +} diff --git a/packages/split-button/test/split-button-sync.test.ts b/packages/split-button/test/split-button-sync.test.ts index 9f558a5dd4..d6ca696fa8 100644 --- a/packages/split-button/test/split-button-sync.test.ts +++ b/packages/split-button/test/split-button-sync.test.ts @@ -10,27 +10,9 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import { - elementUpdated, - expect, - fixture, - html, - oneEvent, -} from '@open-wc/testing'; -import { spy } from 'sinon'; - import '../sync/sp-split-button.js'; -import { SplitButton } from '..'; -import fieldDefaults, { - m as field, -} from '../stories/split-button-accent-field.stories.js'; -import moreDefaults, { - m as more, -} from '../stories/split-button-accent-more.stories.js'; -import { MenuItem } from '@spectrum-web-components/menu'; -import { TemplateResult } from '@spectrum-web-components/base'; -import { sendKeys } from '@web/test-runner-commands'; -import type { Button } from '@spectrum-web-components/button'; +import { html, TemplateResult } from '@spectrum-web-components/base'; +import { runSplitButtonTests } from './index.js'; // wrap in div method @@ -49,449 +31,5 @@ const deprecatedMenu = (): TemplateResult => html` `; describe('Splitbutton', () => { - it('loads [type="field"] splitbutton accessibly', async () => { - const test = await fixture( - wrapInDiv( - field({ - ...fieldDefaults.args, - ...field.args, - }) - ) - ); - const el1 = test.querySelector('sp-split-button') as SplitButton; - const el2 = test.querySelector('sp-split-button[left]') as SplitButton; - - await elementUpdated(el1); - await elementUpdated(el2); - - await expect(el1).to.be.accessible(); - await expect(el2).to.be.accessible(); - }); - it('loads [type="field"] splitbutton accessibly with deprecated syntax', async () => { - const test = await fixture(html` -
- ${deprecatedMenu()} - ${deprecatedMenu()} -
- `); - const el1 = test.querySelector('sp-split-button') as SplitButton; - const el2 = test.querySelector('sp-split-button[left]') as SplitButton; - - await elementUpdated(el1); - await elementUpdated(el2); - - await expect(el1).to.be.accessible(); - await expect(el2).to.be.accessible(); - }); - it('loads [type="more"] splitbutton accessibly', async () => { - const test = await fixture( - wrapInDiv(more({ ...moreDefaults.args, ...more.args })) - ); - const el1 = test.querySelector('sp-split-button') as SplitButton; - const el2 = test.querySelector('sp-split-button[left]') as SplitButton; - - await elementUpdated(el1); - await elementUpdated(el2); - - await expect(el1).to.be.accessible(); - await expect(el2).to.be.accessible(); - }); - it('loads [type="more"] splitbutton accessibly with deprecated syntax', async () => { - const test = await fixture(html` -
- - ${deprecatedMenu()} - - - ${deprecatedMenu()} - -
- `); - const el1 = test.querySelector('sp-split-button') as SplitButton; - const el2 = test.querySelector('sp-split-button[left]') as SplitButton; - - await elementUpdated(el1); - await elementUpdated(el2); - - await expect(el1).to.be.accessible(); - await expect(el2).to.be.accessible(); - }); - it('[type="field"] toggles open/close multiple time', async () => { - const test = await fixture( - wrapInDiv( - field({ - ...fieldDefaults.args, - ...field.args, - }) - ) - ); - const el = test.querySelector('sp-split-button') as SplitButton; - - await elementUpdated(el); - let items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(3); - - let opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - - expect(el.open).to.be.true; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(0); - - let closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - - expect(el.open).to.be.false; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(3); - - opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - - expect(el.open).to.be.true; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(0); - - closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - - expect(el.open).to.be.false; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(3); - }); - it('[type="more"] toggles open/close multiple time', async () => { - const test = await fixture( - wrapInDiv(more({ ...moreDefaults.args, ...more.args })) - ); - const el = test.querySelector('sp-split-button') as SplitButton; - - await elementUpdated(el); - let items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(3); - - let opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - - expect(el.open).to.be.true; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(0); - - let closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - - expect(el.open).to.be.false; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(3); - - opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - - expect(el.open).to.be.true; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(0); - - closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - - expect(el.open).to.be.false; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(3); - }); - it('receives "focus()"', async () => { - const test = await fixture( - wrapInDiv( - field({ - ...fieldDefaults.args, - ...field.args, - }) - ) - ); - const el1 = test.querySelector('sp-split-button') as SplitButton; - const el2 = test.querySelector('sp-split-button[left]') as SplitButton; - - await elementUpdated(el1); - await elementUpdated(el2); - - el1.focus(); - await elementUpdated(el1); - - expect(document.activeElement).to.equal(el1); - expect(el1.shadowRoot.activeElement).to.equal(el1.focusElement); - - el2.focus(); - await elementUpdated(el2); - - expect(document.activeElement).to.equal(el2); - expect(el2.shadowRoot.activeElement).to.equal(el2.focusElement); - }); - it('[type="field"] manages `selectedItem`', async () => { - const test = await fixture( - wrapInDiv( - field({ - ...fieldDefaults.args, - ...field.args, - }) - ) - ); - const el = test.querySelector('sp-split-button') as SplitButton; - - await elementUpdated(el); - - expect(el.selectedItem?.itemText).to.equal('Option 1'); - expect(el.open).to.be.false; - - const item3 = el.querySelector('sp-menu-item:nth-child(3)') as MenuItem; - const root = el.shadowRoot ? el.shadowRoot : el; - const toggleButton = root.querySelector( - '.trigger' - ) as HTMLButtonElement; - const opened = oneEvent(el, 'sp-opened'); - toggleButton.click(); - await opened; - - await elementUpdated(el); - - expect(el.open).to.be.true; - - const closed = oneEvent(el, 'sp-closed'); - item3.click(); - await closed; - - await elementUpdated(el); - - expect(el.selectedItem?.itemText).to.equal('Short'); - expect(el.open).to.be.false; - expect(document.activeElement).to.equal(el); - expect(el.shadowRoot.activeElement).to.equal(el.focusElement); - }); - it('[type="more"] manages `selectedItem.itemText`', async () => { - const test = await fixture( - wrapInDiv(more({ ...moreDefaults.args, ...more.args })) - ); - const el = test.querySelector('sp-split-button') as SplitButton; - - await elementUpdated(el); - - expect(el.selectedItem?.itemText).to.equal('Option 1'); - expect(el.open).to.be.false; - - const item3 = el.querySelector('sp-menu-item:nth-child(3)') as MenuItem; - const root = el.shadowRoot ? el.shadowRoot : el; - const toggleButton = root.querySelector( - '.trigger' - ) as HTMLButtonElement; - const opened = oneEvent(el, 'sp-opened'); - toggleButton.click(); - await opened; - - await elementUpdated(el); - - expect(el.open).to.be.true; - - const closed = oneEvent(el, 'sp-closed'); - item3.click(); - await closed; - - await elementUpdated(el); - - expect(el.open).to.be.false; - expect(el.selectedItem?.itemText).to.equal('Option 1'); - }); - - it('passes click events as [type="field"]', async () => { - const firstItemSpy = spy(); - const secondItemSpy = spy(); - const thirdItemSpy = spy(); - const test = await fixture( - wrapInDiv( - field({ - ...fieldDefaults.args, - ...field.args, - firstItemHandler: (): void => firstItemSpy(), - secondItemHandler: (): void => secondItemSpy(), - thirdItemHandler: (): void => thirdItemSpy(), - }) - ) - ); - const el = test.querySelector('sp-split-button') as SplitButton; - await elementUpdated(el); - - expect(el.selectedItem?.itemText).to.equal('Option 1'); - expect(el.open).to.be.false; - - const item1 = el.querySelector('sp-menu-item:nth-child(1)') as MenuItem; - const item2 = el.querySelector('sp-menu-item:nth-child(2)') as MenuItem; - const item3 = el.querySelector('sp-menu-item:nth-child(3)') as MenuItem; - const main = el.button; - - main.click(); - - await elementUpdated(el); - - expect(firstItemSpy.called, 'first called').to.be.true; - expect(firstItemSpy.calledOnce, 'first calledOnce').to.be.true; - - const trigger = (el as unknown as { trigger: Button }).trigger; - let opened = oneEvent(el, 'sp-opened'); - trigger.click(); - await opened; - - await elementUpdated(el); - - expect(el.open, 'open').to.be.true; - - let closed = oneEvent(el, 'sp-closed'); - item3.click(); - await closed; - - await elementUpdated(el); - - expect(el.open, 'not open').to.be.false; - expect(thirdItemSpy.called, 'third called').to.be.true; - expect(thirdItemSpy.calledOnce, 'third calledOnce').to.be.true; - - main.click(); - await elementUpdated(el); - - expect(el.open).to.be.false; - expect(el.selectedItem?.itemText).to.equal('Short'); - expect(thirdItemSpy.called, 'third called, still').to.be.true; - expect(thirdItemSpy.callCount, 'third callCount').to.equal(2); - expect(thirdItemSpy.calledTwice, 'third calledTwice').to.be.true; - - await sendKeys({ - press: 'Tab', - }); - opened = oneEvent(el, 'sp-opened'); - sendKeys({ - press: 'ArrowDown', - }); - await opened; - - await elementUpdated(el); - - expect(el.open, 'reopened').to.be.true; - - closed = oneEvent(el, 'sp-closed'); - item2.click(); - await closed; - - await elementUpdated(el); - - main.click(); - - await elementUpdated(el); - - expect(el.open).to.be.false; - expect(el.selectedItem?.itemText).to.equal('Option Really Extended'); - expect(secondItemSpy.called, 'second called').to.be.true; - expect(secondItemSpy.calledTwice, 'second twice').to.be.true; - - opened = oneEvent(el, 'sp-opened'); - trigger.click(); - await opened; - - await elementUpdated(el); - - expect(el.open, 'opened again').to.be.true; - - closed = oneEvent(el, 'sp-closed'); - item1.click(); - await closed; - await elementUpdated(el); - - main.click(); - - await elementUpdated(el); - - expect(el.selectedItem?.itemText).to.equal('Option 1'); - expect(firstItemSpy.called, 'first called, sill').to.be.true; - expect(firstItemSpy.callCount, 'first callCount').to.equal(3); - }); - it('passes click events as [type="more"]', async () => { - const firstItemSpy = spy(); - const secondItemSpy = spy(); - const thirdItemSpy = spy(); - const test = await fixture( - wrapInDiv( - more({ - ...moreDefaults.args, - ...more.args, - firstItemHandler: (): void => firstItemSpy(), - secondItemHandler: (): void => secondItemSpy(), - thirdItemHandler: (): void => thirdItemSpy(), - }) - ) - ); - const el = test.querySelector('sp-split-button') as SplitButton; - - await elementUpdated(el); - - expect(el.selectedItem?.itemText).to.equal('Option 1'); - expect(el.open).to.be.false; - - const item2 = el.querySelector('sp-menu-item:nth-child(2)') as MenuItem; - const item3 = el.querySelector('sp-menu-item:nth-child(3)') as MenuItem; - const root = el.shadowRoot ? el.shadowRoot : el; - const main = root.querySelector('#button') as HTMLButtonElement; - - main.click(); - - await elementUpdated(el); - - expect(firstItemSpy.called, '1st called').to.be.true; - expect(firstItemSpy.calledOnce, '1st called once').to.be.true; - - const trigger = root.querySelector('.trigger') as HTMLButtonElement; - let opened = oneEvent(el, 'sp-opened'); - trigger.click(); - await opened; - - await elementUpdated(el); - - expect(el.open).to.be.true; - - let closed = oneEvent(el, 'sp-closed'); - item3.click(); - await closed; - await elementUpdated(el); - - expect(el.open, 'not open').to.be.false; - expect(el.selectedItem?.itemText).to.equal('Option 1'); - expect(thirdItemSpy.called, '3rd called').to.be.true; - expect(thirdItemSpy.calledOnce, '3rd called once').to.be.true; - opened = oneEvent(el, 'sp-opened'); - trigger.click(); - await opened; - - await elementUpdated(el); - - expect(el.open).to.be.true; - - closed = oneEvent(el, 'sp-closed'); - item2.click(); - await closed; - - await elementUpdated(el); - - expect(el.open).to.be.false; - expect(el.selectedItem?.itemText).to.equal('Option 1'); - expect(secondItemSpy.called, '2nd called').to.be.true; - expect(secondItemSpy.calledOnce, '2nd called once').to.be.true; - - main.click(); - - await elementUpdated(el); - - expect(firstItemSpy.called).to.be.true; - expect(firstItemSpy.calledTwice, '1st called twice').to.be.true; - }); + runSplitButtonTests(wrapInDiv, deprecatedMenu); }); diff --git a/packages/split-button/test/split-button.test.ts b/packages/split-button/test/split-button.test.ts index f1734df9ef..a5fdb6ef49 100644 --- a/packages/split-button/test/split-button.test.ts +++ b/packages/split-button/test/split-button.test.ts @@ -10,27 +10,9 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import { - elementUpdated, - expect, - fixture, - html, - oneEvent, -} from '@open-wc/testing'; -import { spy } from 'sinon'; - import '../sp-split-button.js'; -import { SplitButton } from '..'; -import fieldDefaults, { - m as field, -} from '../stories/split-button-accent-field.stories.js'; -import moreDefaults, { - m as more, -} from '../stories/split-button-accent-more.stories.js'; -import { MenuItem } from '@spectrum-web-components/menu'; -import { TemplateResult } from '@spectrum-web-components/base'; -import { sendKeys } from '@web/test-runner-commands'; -import type { Button } from '@spectrum-web-components/button'; +import { html, TemplateResult } from '@spectrum-web-components/base'; +import { runSplitButtonTests } from './index.js'; // wrap in div method @@ -49,449 +31,5 @@ const deprecatedMenu = (): TemplateResult => html` `; describe('Splitbutton', () => { - it('loads [type="field"] splitbutton accessibly', async () => { - const test = await fixture( - wrapInDiv( - field({ - ...fieldDefaults.args, - ...field.args, - }) - ) - ); - const el1 = test.querySelector('sp-split-button') as SplitButton; - const el2 = test.querySelector('sp-split-button[left]') as SplitButton; - - await elementUpdated(el1); - await elementUpdated(el2); - - await expect(el1).to.be.accessible(); - await expect(el2).to.be.accessible(); - }); - it('loads [type="field"] splitbutton accessibly with deprecated syntax', async () => { - const test = await fixture(html` -
- ${deprecatedMenu()} - ${deprecatedMenu()} -
- `); - const el1 = test.querySelector('sp-split-button') as SplitButton; - const el2 = test.querySelector('sp-split-button[left]') as SplitButton; - - await elementUpdated(el1); - await elementUpdated(el2); - - await expect(el1).to.be.accessible(); - await expect(el2).to.be.accessible(); - }); - it('loads [type="more"] splitbutton accessibly', async () => { - const test = await fixture( - wrapInDiv(more({ ...moreDefaults.args, ...more.args })) - ); - const el1 = test.querySelector('sp-split-button') as SplitButton; - const el2 = test.querySelector('sp-split-button[left]') as SplitButton; - - await elementUpdated(el1); - await elementUpdated(el2); - - await expect(el1).to.be.accessible(); - await expect(el2).to.be.accessible(); - }); - it('loads [type="more"] splitbutton accessibly with deprecated syntax', async () => { - const test = await fixture(html` -
- - ${deprecatedMenu()} - - - ${deprecatedMenu()} - -
- `); - const el1 = test.querySelector('sp-split-button') as SplitButton; - const el2 = test.querySelector('sp-split-button[left]') as SplitButton; - - await elementUpdated(el1); - await elementUpdated(el2); - - await expect(el1).to.be.accessible(); - await expect(el2).to.be.accessible(); - }); - it('[type="field"] toggles open/close multiple time', async () => { - const test = await fixture( - wrapInDiv( - field({ - ...fieldDefaults.args, - ...field.args, - }) - ) - ); - const el = test.querySelector('sp-split-button') as SplitButton; - - await elementUpdated(el); - let items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(3); - - let opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - - expect(el.open).to.be.true; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(0); - - let closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - - expect(el.open).to.be.false; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(3); - - opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - - expect(el.open).to.be.true; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(0); - - closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - - expect(el.open).to.be.false; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(3); - }); - it('[type="more"] toggles open/close multiple time', async () => { - const test = await fixture( - wrapInDiv(more({ ...moreDefaults.args, ...more.args })) - ); - const el = test.querySelector('sp-split-button') as SplitButton; - - await elementUpdated(el); - let items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(3); - - let opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - - expect(el.open).to.be.true; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(0); - - let closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - - expect(el.open).to.be.false; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(3); - - opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - - expect(el.open).to.be.true; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(0); - - closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - - expect(el.open).to.be.false; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(3); - }); - it('receives "focus()"', async () => { - const test = await fixture( - wrapInDiv( - field({ - ...fieldDefaults.args, - ...field.args, - }) - ) - ); - const el1 = test.querySelector('sp-split-button') as SplitButton; - const el2 = test.querySelector('sp-split-button[left]') as SplitButton; - - await elementUpdated(el1); - await elementUpdated(el2); - - el1.focus(); - await elementUpdated(el1); - - expect(document.activeElement).to.equal(el1); - expect(el1.shadowRoot.activeElement).to.equal(el1.focusElement); - - el2.focus(); - await elementUpdated(el2); - - expect(document.activeElement).to.equal(el2); - expect(el2.shadowRoot.activeElement).to.equal(el2.focusElement); - }); - it('[type="field"] manages `selectedItem`', async () => { - const test = await fixture( - wrapInDiv( - field({ - ...fieldDefaults.args, - ...field.args, - }) - ) - ); - const el = test.querySelector('sp-split-button') as SplitButton; - - await elementUpdated(el); - - expect(el.selectedItem?.itemText).to.equal('Option 1'); - expect(el.open).to.be.false; - - const item3 = el.querySelector('sp-menu-item:nth-child(3)') as MenuItem; - const root = el.shadowRoot ? el.shadowRoot : el; - const toggleButton = root.querySelector( - '.trigger' - ) as HTMLButtonElement; - const opened = oneEvent(el, 'sp-opened'); - toggleButton.click(); - await opened; - - await elementUpdated(el); - - expect(el.open).to.be.true; - - const closed = oneEvent(el, 'sp-closed'); - item3.click(); - await closed; - - await elementUpdated(el); - - expect(el.selectedItem?.itemText).to.equal('Short'); - expect(el.open).to.be.false; - expect(document.activeElement).to.equal(el); - expect(el.shadowRoot.activeElement).to.equal(el.focusElement); - }); - it('[type="more"] manages `selectedItem.itemText`', async () => { - const test = await fixture( - wrapInDiv(more({ ...moreDefaults.args, ...more.args })) - ); - const el = test.querySelector('sp-split-button') as SplitButton; - - await elementUpdated(el); - - expect(el.selectedItem?.itemText).to.equal('Option 1'); - expect(el.open).to.be.false; - - const item3 = el.querySelector('sp-menu-item:nth-child(3)') as MenuItem; - const root = el.shadowRoot ? el.shadowRoot : el; - const toggleButton = root.querySelector( - '.trigger' - ) as HTMLButtonElement; - const opened = oneEvent(el, 'sp-opened'); - toggleButton.click(); - await opened; - - await elementUpdated(el); - - expect(el.open).to.be.true; - - const closed = oneEvent(el, 'sp-closed'); - item3.click(); - await closed; - - await elementUpdated(el); - - expect(el.open).to.be.false; - expect(el.selectedItem?.itemText).to.equal('Option 1'); - }); - - it('passes click events as [type="field"]', async () => { - const firstItemSpy = spy(); - const secondItemSpy = spy(); - const thirdItemSpy = spy(); - const test = await fixture( - wrapInDiv( - field({ - ...fieldDefaults.args, - ...field.args, - firstItemHandler: (): void => firstItemSpy(), - secondItemHandler: (): void => secondItemSpy(), - thirdItemHandler: (): void => thirdItemSpy(), - }) - ) - ); - const el = test.querySelector('sp-split-button') as SplitButton; - await elementUpdated(el); - - expect(el.selectedItem?.itemText).to.equal('Option 1'); - expect(el.open).to.be.false; - - const item1 = el.querySelector('sp-menu-item:nth-child(1)') as MenuItem; - const item2 = el.querySelector('sp-menu-item:nth-child(2)') as MenuItem; - const item3 = el.querySelector('sp-menu-item:nth-child(3)') as MenuItem; - const main = el.button; - - main.click(); - - await elementUpdated(el); - - expect(firstItemSpy.called, 'first called').to.be.true; - expect(firstItemSpy.calledOnce, 'first calledOnce').to.be.true; - - const trigger = (el as unknown as { trigger: Button }).trigger; - let opened = oneEvent(el, 'sp-opened'); - trigger.click(); - await opened; - - await elementUpdated(el); - - expect(el.open, 'open').to.be.true; - - let closed = oneEvent(el, 'sp-closed'); - item3.click(); - await closed; - - await elementUpdated(el); - - expect(el.open, 'not open').to.be.false; - expect(thirdItemSpy.called, 'third called').to.be.true; - expect(thirdItemSpy.calledOnce, 'third calledOnce').to.be.true; - - main.click(); - await elementUpdated(el); - - expect(el.open).to.be.false; - expect(el.selectedItem?.itemText).to.equal('Short'); - expect(thirdItemSpy.called, 'third called, still').to.be.true; - expect(thirdItemSpy.callCount, 'third callCount').to.equal(2); - expect(thirdItemSpy.calledTwice, 'third calledTwice').to.be.true; - - await sendKeys({ - press: 'Tab', - }); - opened = oneEvent(el, 'sp-opened'); - sendKeys({ - press: 'ArrowDown', - }); - await opened; - - await elementUpdated(el); - - expect(el.open, 'reopened').to.be.true; - - closed = oneEvent(el, 'sp-closed'); - item2.click(); - await closed; - - await elementUpdated(el); - - main.click(); - - await elementUpdated(el); - - expect(el.open).to.be.false; - expect(el.selectedItem?.itemText).to.equal('Option Really Extended'); - expect(secondItemSpy.called, 'second called').to.be.true; - expect(secondItemSpy.calledTwice, 'second twice').to.be.true; - - opened = oneEvent(el, 'sp-opened'); - trigger.click(); - await opened; - - await elementUpdated(el); - - expect(el.open, 'opened again').to.be.true; - - closed = oneEvent(el, 'sp-closed'); - item1.click(); - await closed; - await elementUpdated(el); - - main.click(); - - await elementUpdated(el); - - expect(el.selectedItem?.itemText).to.equal('Option 1'); - expect(firstItemSpy.called, 'first called, sill').to.be.true; - expect(firstItemSpy.callCount, 'first callCount').to.equal(3); - }); - it('passes click events as [type="more"]', async () => { - const firstItemSpy = spy(); - const secondItemSpy = spy(); - const thirdItemSpy = spy(); - const test = await fixture( - wrapInDiv( - more({ - ...moreDefaults.args, - ...more.args, - firstItemHandler: (): void => firstItemSpy(), - secondItemHandler: (): void => secondItemSpy(), - thirdItemHandler: (): void => thirdItemSpy(), - }) - ) - ); - const el = test.querySelector('sp-split-button') as SplitButton; - - await elementUpdated(el); - - expect(el.selectedItem?.itemText).to.equal('Option 1'); - expect(el.open).to.be.false; - - const item2 = el.querySelector('sp-menu-item:nth-child(2)') as MenuItem; - const item3 = el.querySelector('sp-menu-item:nth-child(3)') as MenuItem; - const root = el.shadowRoot ? el.shadowRoot : el; - const main = root.querySelector('#button') as HTMLButtonElement; - - main.click(); - - await elementUpdated(el); - - expect(firstItemSpy.called, '1st called').to.be.true; - expect(firstItemSpy.calledOnce, '1st called once').to.be.true; - - const trigger = root.querySelector('.trigger') as HTMLButtonElement; - let opened = oneEvent(el, 'sp-opened'); - trigger.click(); - await opened; - - await elementUpdated(el); - - expect(el.open).to.be.true; - - let closed = oneEvent(el, 'sp-closed'); - item3.click(); - await closed; - await elementUpdated(el); - - expect(el.open, 'not open').to.be.false; - expect(el.selectedItem?.itemText).to.equal('Option 1'); - expect(thirdItemSpy.called, '3rd called').to.be.true; - expect(thirdItemSpy.calledOnce, '3rd called once').to.be.true; - opened = oneEvent(el, 'sp-opened'); - trigger.click(); - await opened; - - await elementUpdated(el); - - expect(el.open).to.be.true; - - closed = oneEvent(el, 'sp-closed'); - item2.click(); - await closed; - - await elementUpdated(el); - - expect(el.open).to.be.false; - expect(el.selectedItem?.itemText).to.equal('Option 1'); - expect(secondItemSpy.called, '2nd called').to.be.true; - expect(secondItemSpy.calledOnce, '2nd called once').to.be.true; - - main.click(); - - await elementUpdated(el); - - expect(firstItemSpy.called).to.be.true; - expect(firstItemSpy.calledTwice, '1st called twice').to.be.true; - }); + runSplitButtonTests(wrapInDiv, deprecatedMenu); }); diff --git a/web-test-runner.config.js b/web-test-runner.config.js index bc5fbe66c5..a6983a269b 100644 --- a/web-test-runner.config.js +++ b/web-test-runner.config.js @@ -136,5 +136,4 @@ export default { ], group: 'unit', browsers: [chromium, firefox, webkit], - browserLogs: false, };