import { ApproxStructure, Assertions, Chain, FocusTools, GeneralSteps, Keyboard, Keys, Logger, Mouse, Step, UiFinder, Waiter } from '@ephox/agar'; import { UnitTest } from '@ephox/bedrock-client'; import { Arr, Cell, Fun, Future, Optional, Result, Strings } from '@ephox/katamari'; import { Compare, SelectorFind } from '@ephox/sugar'; import * as Behaviour from 'ephox/alloy/api/behaviour/Behaviour'; import { Positioning } from 'ephox/alloy/api/behaviour/Positioning'; import { Representing } from 'ephox/alloy/api/behaviour/Representing'; import type { AlloyComponent } from 'ephox/alloy/api/component/ComponentApi'; import * as GuiFactory from 'ephox/alloy/api/component/GuiFactory'; import * as Memento from 'ephox/alloy/api/component/Memento'; import type { SketchSpec } from 'ephox/alloy/api/component/SpecTypes'; 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 type { TestItem } from 'ephox/alloy/test/dropdown/TestDropdownMenu'; import * as TestDropdownMenu from 'ephox/alloy/test/dropdown/TestDropdownMenu'; import * as GuiSetup from 'ephox/alloy/test/GuiSetup'; interface TestDatum { readonly code: string; readonly selector: string; readonly label: string; readonly testItem: TestItem; readonly sAssertHasFocus: Step; } UnitTest.asynctest('DropdownRefetchTest', (success, failure) => { const memSink = Memento.record( Container.sketch({ containerBehaviours: Behaviour.derive([ Positioning.config({ useFixed: Fun.always }) ]) }) ); const resultsStore = Cell([] as TestItem[]); GuiSetup.setup((store, _doc, _body) => { // We want two nearly identical dropdowns, because we need to test the // different branches their Coupling sandbox state. const makeDropdown = (className: string): SketchSpec => { return Dropdown.sketch({ dom: { tag: 'button', classes: [ 'test-dropdown', className ], innerHtml: 'Dropdown+Refetch' }, sandboxClasses: [ 'my-test-sandbox' ], lazySink: (c) => { TestDropdownMenu.assertLazySinkArgs('button', 'test-dropdown', c); return Result.value(memSink.get(c)); }, toggleClass: 'alloy-selected', parts: { menu: TestDropdownMenu.part(store) }, fetch: () => { // We use a cell here, because when testing fetch, we want to be able // to easily change the results. const future = Future.pure( resultsStore.get() ); return future.map((f) => { const menu = TestDropdownMenu.renderMenu({ value: 'v', items: Arr.map(f, TestDropdownMenu.renderItem) }); return Optional.some(TieredMenu.singleData('test', menu)); }); } }); }; const c = GuiFactory.build({ dom: { tag: 'div', classes: [ 'dropdown-mania' ] }, components: [ makeDropdown('dropdown1'), makeDropdown('dropdown2') ] }); return c; }, (doc, _body, gui, container, _store) => { // We use dropdown1 for all the main tests // We use dropdown2 just to test that refetch opens a menu, even if // open has never been called before. const dropdown1: AlloyComponent = container.getSystem().getByDom( SelectorFind.descendant(container.element, '.dropdown1').getOrDie( 'Could not find the first dropdown in the children' ) ).getOrDie(); const dropdown2: AlloyComponent = container.getSystem().getByDom( SelectorFind.descendant(container.element, '.dropdown2').getOrDie( 'Could not find the first dropdown in the children' ) ).getOrDie(); const sink = GuiFactory.build(memSink.asSpec()); gui.add(sink); const testData = Arr.mapToObject([ 'ant', 'bull', 'coyote', 'dingo', 'elephant', 'frog', 'giraffe', 'hippo', 'iguana' ], (animalName): TestDatum => { const text = Strings.capitalize(animalName); const selector = `li:contains(${text})`; return { code: animalName, selector, label: `${animalName}-item`, testItem: { type: 'item', data: { value: animalName, meta: { text } } }, sAssertHasFocus: FocusTools.sTryOnSelector( `Waiting for item (value: ${animalName}, text: ${text}) to receive focus`, doc, selector ) }; }); const focusables = { dropdown1: { label: 'dropdown-button', selector: '.dropdown1' }, dropdown2: { label: 'dropdown-button', selector: '.dropdown2' } }; // Start the initial results with ant, bull, dingo resultsStore.set([ testData.ant.testItem, testData.bull.testItem, testData.dingo.testItem ]); // Change the results, and trigger a refetch and wait until it's done. const sTriggerRefetchWith = (dropdown: AlloyComponent, items: TestItem[]): Step => Step.async( (next) => { resultsStore.set(items); Dropdown.refetch(dropdown).get(next); } ); return [ GuiSetup.mAddStyles(doc, [ ':focus { outline: 2px solid green; }' ]), Logger.t( 'Testing Dropdown1 (opens before refetches)', GeneralSteps.sequence([ Assertions.sAssertStructure( 'Initial structure of dropdown1 button', ApproxStructure.build((s, str, _arr) => s.element('button', { attrs: { 'aria-expanded': str.is('false'), 'aria-haspopup': str.is('true'), 'type': str.is('button') } })), dropdown1.element ), Mouse.sClickOn(gui.element, focusables.dropdown1.selector), // This focus check on ant has the byproduct of waiting until the menu opens. testData.ant.sAssertHasFocus, Assertions.sAssertStructure( 'Initial structure of dropdown1 button', ApproxStructure.build((s, str, _arr) => s.element('button', { attrs: { 'aria-expanded': str.is('true'), 'aria-haspopup': str.is('true'), 'aria-controls': str.contains('aria-controls_') } })), dropdown1.element ), // Now that the menu is open, check that the Representing.getValue of the // sandbox is linked to the dropdown1. Chain.isolate(gui.element, Chain.fromChains([ UiFinder.cFindIn('.my-test-sandbox'), Chain.binder((sandbox) => dropdown1.getSystem().getByDom(sandbox)), Chain.op((sandboxComp) => { Assertions.assertEq('Checking Representing.getValue of sandbox is dropdown1', true, Compare.eq(dropdown1.element, Representing.getValue(sandboxComp).element)); }) ])), // Change the results, and trigger a refetch and wait until it's done. // We don't want to close the menu first. sTriggerRefetchWith(dropdown1, [ testData.bull.testItem, // Even though ant was the previous focus, we don't try to preserve it. testData.ant.testItem, testData.iguana.testItem ]), // Check the menu is still open, but now has focus on bull. Even though ant is still // there. testData.bull.sAssertHasFocus, // Close the menu Keyboard.sKeyup(doc, Keys.escape(), { }), Waiter.sTryUntil( 'Waiting for menu to disappear', FocusTools.sTryOnSelector('Focus should be back on button', doc, 'button') ), UiFinder.sNotExists(gui.element, '.menu'), // Call refetch and check it still opens (even though the menu is closed) // and highlights its first result sTriggerRefetchWith(dropdown1, [ testData.dingo.testItem, // Even though ant was the previous focus, we don't try to preserve it. testData.elephant.testItem, testData.hippo.testItem ]), testData.dingo.sAssertHasFocus ]) ), Logger.t( 'Testing Dropdown2 (refetch before open) will still open on refetch', GeneralSteps.sequence([ // Focus the dropdown first, so that we can check if focus // is transferred to the item after refetch FocusTools.sSetFocus( 'Focus dropdown 2', container.element, focusables.dropdown2.selector ), sTriggerRefetchWith( dropdown2, [ testData.coyote.testItem, testData.elephant.testItem ] ), testData.coyote.sAssertHasFocus ]) ), GuiSetup.mRemoveStyles ]; }, success, failure); });