import { FocusTools, GeneralSteps, Keyboard, Keys, Logger, Mouse, Step, Touch, UiFinder, Waiter } from '@ephox/agar'; import { UnitTest } from '@ephox/bedrock-client'; import { Arr, Fun, Future, Obj, Optional, Result } from '@ephox/katamari'; import * as Behaviour from 'ephox/alloy/api/behaviour/Behaviour'; import { Focusing } from 'ephox/alloy/api/behaviour/Focusing'; import { Keying } from 'ephox/alloy/api/behaviour/Keying'; import { Positioning } from 'ephox/alloy/api/behaviour/Positioning'; import * as GuiFactory from 'ephox/alloy/api/component/GuiFactory'; import * as Memento from 'ephox/alloy/api/component/Memento'; import { Container } from 'ephox/alloy/api/ui/Container'; import { Dropdown } from 'ephox/alloy/api/ui/Dropdown'; import { tieredMenu as TieredMenu } from 'ephox/alloy/api/ui/TieredMenu'; import * as TestDropdownMenu from 'ephox/alloy/test/dropdown/TestDropdownMenu'; import * as GuiSetup from 'ephox/alloy/test/GuiSetup'; import * as NavigationUtils from 'ephox/alloy/test/NavigationUtils'; interface TestFocusable { label: string; selector: string; } UnitTest.asynctest('DropdownMenuTest', (success, failure) => { const sink = Memento.record( Container.sketch({ containerBehaviours: Behaviour.derive([ Positioning.config({ useFixed: Fun.always }) ]) }) ); GuiSetup.setup((store, _doc, _body) => { const makeFlow = (v: string) => Container.sketch({ dom: { tag: 'span', innerHtml: ' ' + v + ' ', classes: [ v ] }, containerBehaviours: Behaviour.derive([ Focusing.config({ }) ]) }); const widget = Container.sketch({ containerBehaviours: Behaviour.derive([ Keying.config({ mode: 'flow', selector: 'span' }) ]), components: Arr.map([ 'one', 'two', 'three' ], makeFlow) }); const testData = { primary: 'tools-menu', menus: Obj.map({ 'tools-menu': { value: 'tools-menu-value', text: 'Tools Menu', items: Arr.map([ { type: 'item', data: { value: 'packages', meta: { text: 'Packages' }}, hasSubmenu: true }, { type: 'item', data: { value: 'about', meta: { text: 'About' }}}, { type: 'widget', widget, data: { value: 'widget', meta: { }}} ], TestDropdownMenu.renderItem) }, 'packages': { // menu name should be triggering parent item so TieredMenuSpec path works value: 'packages-menu-value', text: 'Packages Menu', items: Arr.map([ { type: 'item', data: { value: 'sortby', meta: { text: 'SortBy' }}, hasSubmenu: true } ], TestDropdownMenu.renderItem) }, 'sortby': { value: 'sortby-menu-value', text: 'Sortby Menu', items: Arr.map([ { type: 'item', data: { value: 'strings', meta: { text: 'Strings' }}, hasSubmenu: true }, { type: 'item', data: { value: 'numbers', meta: { text: 'Numbers' }}, hasSubmenu: true } ], TestDropdownMenu.renderItem) }, 'strings': { value: 'strings-menu-value', text: 'Strings Menu', items: Arr.map([ { type: 'item', data: { value: 'versions', meta: { text: 'Versions', html: 'Versions' }}}, { type: 'item', data: { value: 'alphabetic', meta: { text: 'Alphabetic' }}} ], TestDropdownMenu.renderItem) }, 'numbers': { value: 'numbers-menu-value', text: 'Numbers Menu', items: Arr.map([ { type: 'item', data: { value: 'doubled', meta: { text: 'Doubled digits' }}, hasSubmenu: false } ], TestDropdownMenu.renderItem) } }, TestDropdownMenu.renderMenu), expansions: { packages: 'packages', sortby: 'sortby', strings: 'strings', numbers: 'numbers' } }; const c = GuiFactory.build( Dropdown.sketch({ uid: 'test-dropdown', dom: { tag: 'div', innerHtml: '+', classes: [ 'dropdown-button' ] }, toggleClass: 'alloy-selected', components: [ ], lazySink: (c) => { TestDropdownMenu.assertLazySinkArgs('div', 'dropdown-button', c); return Result.value(sink.get(c)); }, parts: { menu: TestDropdownMenu.part(store) }, fetch: () => { return Future.pure(testData).map((d) => Optional.from(TieredMenu.tieredData(d.primary, d.menus, d.expansions))); } }) ); return c; }, (doc, _body, gui, dropdown, store) => { gui.add( GuiFactory.build(sink.asSpec()) ); const focusables: Record = { toolsMenu: { label: 'tools-menu', selector: '.menu[aria-label="Tools Menu"]' }, packagesMenu: { label: 'packages-menu', selector: '.menu[aria-label="Packages Menu"]' }, sortbyMenu: { label: 'sortby-menu', selector: '.menu[aria-label="Sortby Menu"]' }, stringsMenu: { label: 'strings-menu', selector: '.menu[aria-label="Strings Menu"]' }, numbersMenu: { label: 'numbers-menu', selector: '.menu[aria-label="Numbers Menu"]' }, button: { label: 'dropdown-button', selector: '.dropdown-button' }, packages: { label: 'packages-item', selector: 'li:contains("Packages")' }, about: { label: 'about-item', selector: 'li:contains("About")' }, sortby: { label: 'sortby-item', selector: 'li:contains("SortBy")' }, strings: { label: 'strings-item', selector: 'li:contains("Strings")' }, numbers: { label: 'numbers-item', selector: 'li:contains("Numbers")' }, doubled: { label: 'doubled-item', selector: 'li:contains("Doubled")' }, versions: { label: 'versions-item', selector: 'li:contains("Versions")' }, widget: { label: 'widget-item', selector: '.item-widget' }, widgetOne: { label: 'widget-item:1', selector: '.item-widget .one' }, widgetTwo: { label: 'widget-item:2', selector: '.item-widget .two' }, widgetThree: { label: 'widget-item:3', selector: '.item-widget .three' } }; const sTestMenus = (label: string, stored: string[], focused: TestFocusable, active: TestFocusable[], background: TestFocusable[], others: TestFocusable[]) => { const sCheckBackground = GeneralSteps.sequence( Arr.bind(background, (bg) => [ UiFinder.sExists(gui.element, bg.selector), UiFinder.sNotExists(gui.element, bg.selector + '.selected-menu') ]) ); const sCheckActive = GeneralSteps.sequence( Arr.map(active, (o) => UiFinder.sExists(gui.element, o.selector + '.selected-menu')) ); const sCheckOthers = GeneralSteps.sequence( Arr.map(others, (o) => UiFinder.sNotExists(gui.element, o.selector)) ); return Logger.t( label, GeneralSteps.sequence([ store.sAssertEq('checking store', stored), FocusTools.sTryOnSelector('Searching for focus on: ' + focused.label, doc, focused.selector), sCheckActive, sCheckBackground, sCheckOthers, store.sClear, Step.wait(0) ]) ); }; // A bit of dupe with DropdownButtonSpecTest return [ Step.sync(() => { Focusing.focus(dropdown); }), sTestMenus( 'Initially', [ ], focusables.button, [ ], [ ], [ focusables.toolsMenu, focusables.packagesMenu, focusables.sortbyMenu, focusables.stringsMenu, focusables.numbersMenu ] ), Keyboard.sKeydown(doc, Keys.enter(), { }), Waiter.sTryUntil( 'Wait until dropdown content loads', UiFinder.sExists(gui.element, '.menu') ), sTestMenus( 'After open', [ ], focusables.packages, [ focusables.toolsMenu ], [ ], [ focusables.packagesMenu, focusables.sortbyMenu, focusables.stringsMenu, focusables.numbersMenu ] ), // Press enter should expand Keyboard.sKeydown(doc, Keys.enter(), {}), sTestMenus( 'After expand packages menu', [ ], focusables.sortby, [ focusables.packagesMenu ], [ focusables.toolsMenu ], [ focusables.sortbyMenu, focusables.stringsMenu, focusables.numbersMenu ] ), // Press left should collapse Keyboard.sKeydown(doc, Keys.left(), {}), sTestMenus( 'After collapse packages menu', [ ], focusables.packages, [ focusables.toolsMenu ], [ ], [ focusables.packagesMenu, focusables.sortbyMenu, focusables.stringsMenu, focusables.numbersMenu ] ), // Press right should expand again Keyboard.sKeydown(doc, Keys.right(), {}), sTestMenus( 'After expanding packages menu with right arrow', [ ], focusables.sortby, [ focusables.packagesMenu ], [ focusables.toolsMenu ], [ focusables.sortbyMenu, focusables.stringsMenu, focusables.numbersMenu ] ), // Press space should expand again Keyboard.sKeydown(doc, Keys.space(), {}), sTestMenus( 'After expanding sortby menu with space arrow', [ ], focusables.strings, [ focusables.sortbyMenu ], [ focusables.toolsMenu, focusables.packagesMenu ], [ focusables.stringsMenu, focusables.numbersMenu ] ), // Pressing down should focus on numbers Keyboard.sKeydown(doc, Keys.down(), { }), sTestMenus( 'After pressing down in sortby menu', [ ], focusables.numbers, [ focusables.sortbyMenu ], [ focusables.toolsMenu, focusables.packagesMenu ], [ focusables.stringsMenu, focusables.numbersMenu ] ), // Pressing escape should focus sortby Keyboard.sKeyup(doc, Keys.escape(), { }), sTestMenus( 'After pressing down in sortby menu', [ ], focusables.sortby, [ focusables.packagesMenu ], [ focusables.toolsMenu ], [ focusables.sortbyMenu, focusables.stringsMenu, focusables.numbersMenu ] ), // Pressing right should open up sortby menu Keyboard.sKeydown(doc, Keys.right(), { }), sTestMenus( 'After pressing right again after Escape', [ ], focusables.strings, [ focusables.sortbyMenu ], [ focusables.toolsMenu, focusables.packagesMenu ], [ focusables.stringsMenu, focusables.numbersMenu ] ), // Pressing down, then enter should open up numbers menu Keyboard.sKeydown(doc, Keys.down(), { }), Keyboard.sKeydown(doc, Keys.enter(), { }), sTestMenus( 'After pressing right again after Escape', [ ], focusables.doubled, [ focusables.numbersMenu ], [ focusables.sortbyMenu, focusables.toolsMenu, focusables.packagesMenu ], [ focusables.stringsMenu ] ), // Pressing enter should trigger doubled Keyboard.sKeydown(doc, Keys.enter(), { }), sTestMenus( 'After pressing enter on last level', [ 'dropdown.menu.execute: doubled' ], focusables.doubled, [ focusables.numbersMenu ], [ focusables.sortbyMenu, focusables.toolsMenu, focusables.packagesMenu ], [ focusables.stringsMenu ] ), // Hover on "strings" Mouse.sHoverOn(gui.element, focusables.strings.selector), sTestMenus( 'After hovering on "strings" (should only expand)', [ ], focusables.strings, [ focusables.sortbyMenu ], [ focusables.toolsMenu, focusables.packagesMenu ], [ focusables.numbersMenu ] ), // Click on "about" Mouse.sClickOn(gui.element, focusables.about.selector), // Menus are somewhat irrelevant here, because the hover would have changed them, // not the click store.sAssertEq('Checking about fired', [ 'dropdown.menu.execute: about' ]), store.sClear, // Tap on "about" Touch.sTapOn(gui.element, focusables.about.selector), store.sAssertEq('Checking about fired', [ 'dropdown.menu.execute: about' ]), store.sClear, // Hover on "about" Mouse.sHoverOn(gui.element, focusables.about.selector), sTestMenus( 'After hovering on "strings"', [ ], focusables.about, [ focusables.toolsMenu ], [ ], [ focusables.packagesMenu, focusables.sortbyMenu, focusables.stringsMenu, focusables.numbersMenu ] ), // Pressing right on "about" does nothing Keyboard.sKeydown(doc, Keys.right(), { }), sTestMenus( 'Pressing on "about" does nothing', [ ], focusables.about, [ focusables.toolsMenu ], [ ], [ focusables.packagesMenu, focusables.sortbyMenu, focusables.stringsMenu, focusables.numbersMenu ] ), // Now, let's play with the inline widget Keyboard.sKeydown(doc, Keys.tab(), { }), sTestMenus( 'After pressing from about', [ ], focusables.widget, [ focusables.toolsMenu ], [ ], [ focusables.packagesMenu, focusables.sortbyMenu, focusables.stringsMenu, focusables.numbersMenu ] ), // Press enter to go into the inline widget Keyboard.sKeydown(doc, Keys.enter(), { }), sTestMenus( 'After pressing on inline widget', [ ], focusables.widgetOne, [ focusables.toolsMenu ], [ ], [ focusables.packagesMenu, focusables.sortbyMenu, focusables.stringsMenu, focusables.numbersMenu ] ), NavigationUtils.sequence( doc, Keys.right(), {}, [ focusables.widgetTwo, focusables.widgetThree, focusables.widgetOne ] ), // Press escape to exit the inline widget Keyboard.sKeyup(doc, Keys.escape(), { }), sTestMenus( 'After pressing inside inline widget', [ ], focusables.widget, [ focusables.toolsMenu ], [ ], [ focusables.packagesMenu, focusables.sortbyMenu, focusables.stringsMenu, focusables.numbersMenu ] ) ]; }, success, failure); });