import { ApproxStructure, Assertions, Waiter } from '@ephox/agar'; import { after, before, beforeEach, context, describe, it } from '@ephox/bedrock-client'; import { DomEvent, type EventUnbinder, SugarElement } from '@ephox/sugar'; import * as Behaviour from 'ephox/alloy/api/behaviour/Behaviour'; import { Docking } from 'ephox/alloy/api/behaviour/Docking'; import type { AlloyComponent } from 'ephox/alloy/api/component/ComponentApi'; import * as GuiFactory from 'ephox/alloy/api/component/GuiFactory'; import type { AlloySpec } from 'ephox/alloy/api/component/SpecTypes'; import * as SystemEvents from 'ephox/alloy/api/events/SystemEvents'; import * as GuiSetup from 'ephox/alloy/test/GuiSetup'; describe('browser.alloy.behaviour.docking.DockingTest', () => { context('APIs and scrolling', () => { const hook = GuiSetup.bddSetup( (store, _doc, _body) => { const staticBox: AlloySpec = { dom: { tag: 'div', styles: { width: '100px', height: '100px', background: 'blue' } }, behaviours: Behaviour.derive([ Docking.config({ onDocked: store.adder('static.onDocked'), onUndocked: store.adder('static.onUndocked') }) ]) }; const absoluteBox: AlloySpec = { dom: { tag: 'div', styles: { width: '100px', height: '100px', background: 'red', position: 'absolute', top: '2300px', right: '200px' } }, behaviours: Behaviour.derive([ Docking.config({ onDocked: store.adder('absolute.onDocked'), onUndocked: store.adder('absolute.onUndocked') }) ]) }; return GuiFactory.build( { dom: { tag: 'div', styles: { // We add these margins to allow the window page to scroll 'margin-top': '2000px', 'margin-bottom': '5000px' } }, components: [ staticBox, absoluteBox ] } ); } ); let onWindowScroll: EventUnbinder | null = null; before(() => { const gui = hook.gui(); onWindowScroll = DomEvent.bind(SugarElement.fromDom(window), 'scroll', (evt) => { gui.broadcastEvent(SystemEvents.windowScroll(), evt); }); }); beforeEach(() => { const store = hook.store(); store.clear(); }); after(() => { if (onWindowScroll) { onWindowScroll.unbind(); } }); const boxWithNoPosition = () => ApproxStructure.build((s, str, _arr) => s.element('div', { styles: { position: str.none(), left: str.none(), top: str.none(), right: str.none(), bottom: str.none() } })); const boxWithPosition = (position: string) => ApproxStructure.build((s, str, _arr) => s.element('div', { styles: { position: str.is(position) } })); const assertInitialStructures = ( label: string, boxes: { static: AlloyComponent; absolute: AlloyComponent } ): void => { Assertions.assertStructure( `${label}. Assert initial structure of staticBox. Box should have neither "position: absolute" nor "position: fixed"`, boxWithNoPosition(), boxes.static.element ); Assertions.assertStructure( `${label}. Assert initial structure of absoluteBox`, ApproxStructure.build((s, str, _arr) => s.element('div', { styles: { position: str.is('absolute'), left: str.none(), top: str.is('2300px'), right: str.is('200px'), bottom: str.none() } })), boxes.absolute.element ); }; it('basic test', async () => { const store = hook.store(); const component = hook.component(); const staticBox = component.components()[0]; const absoluteBox = component.components()[1]; store.assertEq('Store should start empty', [ ]); assertInitialStructures('Initial load', { static: staticBox, absolute: absoluteBox }); // Scroll the boxes completely off-screen. One of them is just at the top of the document, // and the other is at 2300px, so scrolling 3000px should do it. window.scrollTo(0, 3000); // Scrolling the static box off-screen should result in it being docked to the top await Waiter.pTryUntil( 'Waiting until position is fixed', () => Assertions.assertStructure( 'Now that static box is offscreen normally, it should switch to fixed coordinates', boxWithPosition('fixed'), staticBox.element ) ); // Scrolling the absolute box off-screen should result it in being docked to the top await Waiter.pTryUntil( 'Waiting until position is fixed', () => Assertions.assertStructure( 'Now that absolute box is offscreen normally, it should switch to fixed coordinates', ApproxStructure.build((s, str, _arr) => s.element('div', { styles: { position: str.is('fixed'), left: str.contains('px'), // essentially checking that there is a left value top: str.contains('0px'), right: str.none(), bottom: str.none() } })), absoluteBox.element ) ); store.assertEq('When docked', [ 'static.onDocked', 'absolute.onDocked' ]); store.clear(); // Now, scroll back so that the absolute box should be able to be restored to // its earlier position. The 2000px also allows the static box to be restored, // because the margin-top on the wrapping container is 2000px. window.scrollTo(0, 2000); // Now, the static box should no longer be docked await Waiter.pTryUntil( 'Waiting until position is static', () => Assertions.assertStructure( 'Now that static box is back on screen, it should switch to having no position again', boxWithNoPosition(), staticBox.element ) ); await Waiter.pTryUntil( 'Waiting until position is absolute', () => Assertions.assertStructure( 'Now that absolute box is back on screen, it should switch back to absolute', boxWithPosition('absolute'), absoluteBox.element ) ); store.assertEq('After undocked', [ 'static.onUndocked', 'absolute.onUndocked' ]); assertInitialStructures('After undocking due to scroll', { static: staticBox, absolute: absoluteBox }); Docking.forceDockToTop(absoluteBox); Assertions.assertStructure( 'After forcing to top, position should be fixed with top: 0', ApproxStructure.build((s, str, _arr) => s.element('div', { styles: { position: str.is('fixed'), top: str.is('0px') } })), absoluteBox.element ); Docking.forceDockToBottom(absoluteBox); Assertions.assertStructure( 'After forcing to bottom, position should be fixed with bottom: 0', ApproxStructure.build((s, str, _arr) => s.element('div', { styles: { position: str.is('fixed'), bottom: str.is('0px') } })), absoluteBox.element ); Docking.refresh(absoluteBox); // After refreshing, should be back at initial position, not the bottom docked position // This is testing that we are only storing the position if we aren't already docked when // using the force APIs. assertInitialStructures('After Docking.refresh', { static: staticBox, absolute: absoluteBox }); }); it('External Scroll event', () => { const store = hook.store(); const component = hook.component(); const staticBox = component.components()[0]; const absoluteBox = component.components()[1]; store.assertEq('Store should start empty', [ ]); // Firstly, check things are where we expect them to be to start the test assertInitialStructures('Before docking', { static: staticBox, absolute: absoluteBox }); window.scrollTo(0, 2000); // Now, dock to the bottom, and check that it worked. Docking.forceDockToBottom(absoluteBox); Assertions.assertStructure( 'After forcing to bottom, position should be fixed with bottom: 0', ApproxStructure.build((s, str, _arr) => s.element('div', { styles: { position: str.is('fixed'), bottom: str.is('0px') } })), absoluteBox.element ); // Now, broadcast an external scroll event to the mothership, and check that it triggers // refresh and puts the box back where it should be. hook.gui().broadcastEvent(SystemEvents.externalElementScroll(), { } as any); assertInitialStructures( 'After broadcasting external scroll event', { static: staticBox, absolute: absoluteBox } ); }); }); });