import { Waiter } from '@ephox/agar'; import { afterEach, beforeEach, describe, it } from '@ephox/bedrock-client'; import { Fun } from '@ephox/katamari'; import { Attribute, Classes, SugarDocument } from '@ephox/sugar'; import { assert } from 'chai'; import { Positioning } from 'ephox/alloy/api/behaviour/Positioning'; 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 * as AlloyEvents from 'ephox/alloy/api/events/AlloyEvents'; import * as NativeEvents from 'ephox/alloy/api/events/NativeEvents'; import { Button } from 'ephox/alloy/api/ui/Button'; import { Container } from 'ephox/alloy/api/ui/Container'; import type { PlacementSpec } from 'ephox/alloy/behaviour/positioning/PositioningTypes'; import * as Layout from 'ephox/alloy/positioning/layout/Layout'; import * as LayoutInset from 'ephox/alloy/positioning/layout/LayoutInset'; import type { Layouts } from 'ephox/alloy/positioning/mode/Anchoring'; import type { TransitionMode } from 'ephox/alloy/positioning/view/Transitions'; import * as GuiSetup from 'ephox/alloy/test/GuiSetup'; import * as PositionTestUtils from 'ephox/alloy/test/PositionTestUtils'; import * as Sinks from 'ephox/alloy/test/Sinks'; interface Scenario { readonly spec: PlacementSpec; readonly expectTransition: boolean; readonly waitForCompletion?: boolean; } describe.skip('browser.alloy.position.TransitionsTest', () => { const transitionTime = 75; const northLayout = { onLtr: () => [ Layout.north ], onRtl: () => [ Layout.north ] }; const northInsetLayout = { onLtr: () => [ LayoutInset.north ], onRtl: () => [ LayoutInset.north ] }; const memButton1 = Memento.record(Button.sketch({ action: Fun.noop, dom: { styles: { position: 'absolute', left: '100px', top: '150px' }, innerHtml: 'Button 1', tag: 'button' }, uid: 'button1' })); const memButton2 = Memento.record(Button.sketch({ action: Fun.noop, dom: { styles: { position: 'absolute', left: '500px', top: '250px' }, innerHtml: 'Button 2', tag: 'button' }, uid: 'button2' })); const sinks = Sinks.bddSetup(); const gui = GuiSetup.bddSetup((store, _doc, _body) => { return GuiFactory.build( Container.sketch({ components: [ GuiFactory.premade(sinks.fixed()), GuiFactory.premade(sinks.relative()), GuiFactory.premade(sinks.popup()), GuiFactory.premade(GuiFactory.build(memButton1.asSpec())), GuiFactory.premade(GuiFactory.build(memButton2.asSpec())) ], events: AlloyEvents.derive([ AlloyEvents.run(NativeEvents.transitionend(), store.adder('transitionend')), AlloyEvents.run(NativeEvents.transitioncancel(), store.adder('transitioncancel')) ]) }) ); }); GuiSetup.bddAddStyles(SugarDocument.getDocument(), [ `.transition { transition: ${transitionTime}ms ease; transition-property: left, right, top, bottom; }` ]); beforeEach(() => { gui.store().clear(); }); afterEach(() => { Positioning.reset(sinks.relative(), sinks.popup()); Positioning.reset(sinks.fixed(), sinks.popup()); }); const getHotspotPlacementSpec = (hotspot: AlloyComponent, transitionMode: TransitionMode = 'all', layouts?: Layouts): PlacementSpec => ({ anchor: { type: 'hotspot', hotspot, layouts }, transition: { classes: [ 'transition' ], mode: transitionMode } }); const getMakeshiftPlacementSpec = (x: number, y: number, transitionMode: TransitionMode = 'all', layouts?: Layouts): PlacementSpec => ({ anchor: { type: 'makeshift', x, y, layouts }, transition: { classes: [ 'transition' ], mode: transitionMode } }); const assertClasses = (label: string, classes: string[]) => { assert.deepEqual(Classes.get(sinks.popup().element), classes, label); }; const pTestNoTransition = async (sinkName: string, sink: AlloyComponent, spec: PlacementSpec) => { await PositionTestUtils.pTestSink(sinkName, sink, sinks.popup(), spec); assertClasses('No classes added for transition', []); gui.store().assertEq('No transition events', []); }; const pTestTransition = async (sinkName: string, sink: AlloyComponent, scenarios: Scenario[], events: string[]) => { // Wait a while between tests, sometimes there are cancel events that pop in await Waiter.pWait(transitionTime * 2); gui.store().clear(); // Position initially at the first button const button1 = memButton1.get(gui.component()); await PositionTestUtils.pTestSink(sinkName, sink, sinks.popup(), getHotspotPlacementSpec(button1)); assertClasses(`No classes added for first transition in ${sinkName} sink`, []); // Run through placing each spec for (let i = 0; i < scenarios.length; i++) { const scenario = scenarios[i]; await PositionTestUtils.pTestSink(sinkName, sink, sinks.popup(), scenario.spec); if (scenario.expectTransition) { assertClasses(`Has transition class for transition ${i + 1} in ${sinkName} sink`, [ 'transition' ]); if (scenario.waitForCompletion === false) { await Waiter.pWait(transitionTime / 2); } else { await Waiter.pTryUntilPredicate('Transition ends within expected timeframe', () => Classes.get(sinks.popup().element).length === 0, 10, transitionTime * 2); } } else { assertClasses(`No classes added for transition ${i + 1} in ${sinkName} sink`, []); } } // Wait for the transitions to complete await Waiter.pTryUntil('Wait for transition to complete', () => { assertClasses(`Transition classes removed for ${sinkName} sink`, []); assert.isFalse(Attribute.has(sinks.popup().element, 'data-alloy-transition-timer'), 'Transition timer attribute should not be set'); gui.store().assertEq(`Transition events for ${sinkName} sink`, events); }, 10, transitionTime * 3); }; it('TINY-7740: should not add the transition class when first positioning', async () => { const hotspot = memButton1.get(gui.component()); const spec = getHotspotPlacementSpec(hotspot); await pTestNoTransition('relative', sinks.relative(), spec); await pTestNoTransition('fixed', sinks.fixed(), spec); }); it('TINY-7740: should not transition when not configured', async () => { const hotspot = memButton1.get(gui.component()); const scenarios: Scenario[] = [ { spec: { anchor: { type: 'hotspot', hotspot }}, expectTransition: false } ]; await pTestTransition('relative', sinks.relative(), scenarios, []); await pTestTransition('fixed', sinks.fixed(), scenarios, []); }); it('TINY-7740: should not transition when positioning in the same location', async () => { const button1 = memButton1.get(gui.component()); const scenarios: Scenario[] = [ { spec: getHotspotPlacementSpec(button1), expectTransition: false } ]; await pTestTransition('relative', sinks.relative(), scenarios, []); await pTestTransition('fixed', sinks.fixed(), scenarios, []); }); it('TINY-7740: should add the transition class when moving to the next element', async () => { const button2 = memButton2.get(gui.component()); const scenarios: Scenario[] = [ { spec: getHotspotPlacementSpec(button2), expectTransition: true } ]; // We should have 2 events, one for each direction (left/top) const expectedEvents = [ 'transitionend', 'transitionend' ]; await pTestTransition('relative', sinks.relative(), scenarios, expectedEvents); await pTestTransition('fixed', sinks.fixed(), scenarios, expectedEvents); }); it('TINY-7740: should keep transitioning when moved during a transition', async () => { const button1 = memButton1.get(gui.component()); const button2 = memButton2.get(gui.component()); const scenarios: Scenario[] = [ { spec: getHotspotPlacementSpec(button2), expectTransition: true, waitForCompletion: false }, { spec: getHotspotPlacementSpec(button1), expectTransition: true, waitForCompletion: true } ]; const expectedEvents = [ 'transitioncancel', 'transitioncancel', 'transitionend', 'transitionend' ]; await pTestTransition('relative', sinks.relative(), scenarios, expectedEvents); await pTestTransition('fixed', sinks.fixed(), scenarios, expectedEvents); }); it('TINY-7740: all transition mode should transition on any change', async () => { const button2 = memButton2.get(gui.component()); const scenarios: Scenario[] = [ { spec: getHotspotPlacementSpec(button2, 'all'), expectTransition: true }, { spec: getMakeshiftPlacementSpec(100, 200, 'all'), expectTransition: true } ]; // Note: We have 2 of each event, one for each direction (left/top) const expectedEvents = [ 'transitionend', 'transitionend', 'transitionend', 'transitionend' ]; await pTestTransition('relative', sinks.relative(), scenarios, expectedEvents); await pTestTransition('fixed', sinks.fixed(), scenarios, expectedEvents); }); it('TINY-7740: layout transition mode should only transition when the layout changes', async () => { const button2 = memButton2.get(gui.component()); const scenarios: Scenario[] = [ { spec: getMakeshiftPlacementSpec(500, 300, 'layout'), expectTransition: false }, { spec: getHotspotPlacementSpec(button2, 'layout', northLayout), expectTransition: true } ]; const expectedEvents = [ 'transitionend', 'transitionend' ]; await pTestTransition('relative', sinks.relative(), scenarios, expectedEvents); await pTestTransition('fixed', sinks.fixed(), scenarios, expectedEvents); }); it('TINY-7740: placement transition mode should only transition when the placement changes', async () => { const button1 = memButton1.get(gui.component()); const scenarios: Scenario[] = [ { spec: getMakeshiftPlacementSpec(100, 300, 'placement'), expectTransition: false }, { spec: getHotspotPlacementSpec(button1, 'placement', northLayout), expectTransition: true }, { spec: getHotspotPlacementSpec(button1, 'placement', northInsetLayout), expectTransition: false } ]; const expectedEvents = [ 'transitionend', 'transitionend' ]; await pTestTransition('relative', sinks.relative(), scenarios, expectedEvents); await pTestTransition('fixed', sinks.fixed(), scenarios, expectedEvents); }); it('TINY-7740: should stop transitioning once the duration passes if repositioned and the mode doesn\'t trigger a new transition', async () => { // Position initially at the first button const button1 = memButton1.get(gui.component()); await PositionTestUtils.pTestSink('relative', sinks.relative(), sinks.popup(), getHotspotPlacementSpec(button1, 'placement')); await PositionTestUtils.pTestSink('relative', sinks.relative(), sinks.popup(), getMakeshiftPlacementSpec(100, 300, 'placement', northLayout)); assertClasses('Has started to transition', [ 'transition' ]); // Wait half the transition time await Waiter.pWait(transitionTime / 2); await PositionTestUtils.pTestSink('relative', sinks.relative(), sinks.popup(), getMakeshiftPlacementSpec(100, 350, 'placement', northLayout)); assertClasses('Still transitioning', [ 'transition' ]); // Wait the rest of the transition time, plus ~2 frames and make sure the transition is complete await Waiter.pTryUntil('The transition should have completed', () => assertClasses('Transition Complete', [ ]), 10, transitionTime * 2); const expectedEvents = [ 'transitioncancel', 'transitioncancel' ]; await Waiter.pTryUntil('Transition events', () => gui.store().assertEq('Transition events', expectedEvents), 10, 100); }); });