import { describe, it, expect } from 'vitest'; import { shallowMount } from '@vue/test-utils'; import { ref, nextTick, defineComponent } from 'vue'; import { useLockBodyBackgroundScroll } from '@/presentation/components/Shared/Modal/Hooks/ScrollLock/UseLockBodyBackgroundScroll'; import type { ScrollDomStateAccessor } from '@/presentation/components/Shared/Modal/Hooks/ScrollLock/ScrollDomStateAccessor'; import type { PropertyKeys } from '@/TypeHelpers'; describe('useLockBodyBackgroundScroll', () => { describe('initialization', () => { describe('activates scroll lock when initially active', () => { itEachScrollBlockEffect(async (dom) => { // arrange const isInitiallyActive = true; // act createComponent(isInitiallyActive, dom); await nextTick(); }); }); it('maintains initial styles when initially inactive', async () => { // arrange const isInitiallyActive = false; // act const { initialDomState, actualDomState } = createComponent(isInitiallyActive); await nextTick(); // assert expect(actualDomState).to.deep.equal(initialDomState); }); }); describe('toggling scroll lock', () => { describe('enforces scroll lock when activated', async () => { itEachScrollBlockEffect(async (dom) => { // arrange const isInitiallyActive = false; const { isActive } = createComponent(isInitiallyActive, dom); // act isActive.value = true; await nextTick(); }); }); it('reverts to initial styles when deactivated', async () => { // arrange const isInitiallyActive = true; const { isActive, initialDomState, actualDomState } = createComponent(isInitiallyActive); // act isActive.value = false; await nextTick(); // assert expect(actualDomState).to.deep.equal(initialDomState); }); }); it('restores original styles on unmount', async () => { // arrange const isInitiallyActive = true; const { component, initialDomState, actualDomState } = createComponent(isInitiallyActive); // act component.unmount(); await nextTick(); // assert expect(actualDomState).to.deep.equal(initialDomState); }); }); function createComponent( initialIsActiveValue: boolean, dom?: ScrollDomStateAccessor, ) { const actualDomState = dom ?? createMockDomStateAccessor(); const initialDomState = { ...actualDomState }; const isActive = ref(initialIsActiveValue); const component = shallowMount(defineComponent({ setup() { useLockBodyBackgroundScroll(isActive, actualDomState); }, template: '
', })); return { component, isActive, initialDomState, actualDomState, }; } function itEachScrollBlockEffect(act: (dom: ScrollDomStateAccessor) => Promise) { testScenarios.forEach((m) => { const description = m.description ? ` (${m.description})` : ''; it(`handles '${m.propertyName}'${description}`, async () => { // arrange const dom = createMockDomStateAccessor(); const initialDom = { ...dom }; if (m.prepare) { m.prepare(dom); } // act await act(dom); // assert const expectedValue = m.getExpectedValueOnBlock(initialDom, dom); const actualValue = dom[m.propertyName]; expect(actualValue).to.equal(expectedValue); }); }); } type DomPropertyType = string | number; interface DomStateChange { readonly propertyName: PropertyKeys; readonly description?: string; readonly prepare?: (dom: Writable) => void; getExpectedValueOnBlock( initialDom: Readonly, actualDom: Readonly, ): DomPropertyType; } const testScenarios: ReadonlyArray = [ { propertyName: 'bodyStyleOverflowX', description: 'visible horizontal scrollbar', prepare: (dom) => { dom.htmlClientWidth = 5; dom.htmlScrollWidth = 10; }, getExpectedValueOnBlock: () => 'scroll', }, { propertyName: 'bodyStyleOverflowX', description: 'invisible horizontal scrollbar', prepare: (dom) => { dom.htmlClientWidth = 10; dom.htmlScrollWidth = 5; }, getExpectedValueOnBlock: (initialDom) => initialDom.bodyStyleOverflowX, }, { propertyName: 'bodyStyleOverflowY', description: 'visible vertical scrollbar', prepare: (dom) => { dom.htmlScrollHeight = 10; dom.htmlClientHeight = 5; }, getExpectedValueOnBlock: () => 'scroll', }, { propertyName: 'bodyStyleOverflowY', description: 'invisible vertical scrollbar', prepare: (dom) => { dom.htmlScrollHeight = 5; dom.htmlClientHeight = 10; }, getExpectedValueOnBlock: (initialDom) => initialDom.bodyStyleOverflowY, }, { propertyName: 'htmlScrollLeft', getExpectedValueOnBlock: (initialDom) => initialDom.htmlScrollLeft, }, { propertyName: 'htmlScrollTop', getExpectedValueOnBlock: (initialDom) => initialDom.htmlScrollTop, }, { propertyName: 'bodyStyleLeft', description: 'adjusts for scrolled position', prepare: (dom) => { dom.htmlScrollLeft = 22; }, getExpectedValueOnBlock: () => '-22px', }, { propertyName: 'bodyStyleLeft', description: 'unaffected by no horizontal scroll', prepare: (dom) => { dom.htmlScrollLeft = 0; }, getExpectedValueOnBlock: (initialDom) => initialDom.bodyStyleLeft, }, { propertyName: 'bodyStyleTop', description: 'adjusts for scrolled position', prepare: (dom) => { dom.htmlScrollTop = 12; }, getExpectedValueOnBlock: () => '-12px', }, { propertyName: 'bodyStyleTop', description: 'unaffected by no vertical scroll', prepare: (dom) => { dom.htmlScrollTop = 0; }, getExpectedValueOnBlock: (initialDom) => initialDom.bodyStyleTop, }, { propertyName: 'bodyStylePosition', getExpectedValueOnBlock: () => 'fixed', }, { propertyName: 'bodyStyleWidth', description: 'no margin', getExpectedValueOnBlock: () => '100%', }, { propertyName: 'bodyStyleWidth', description: 'margin on left', prepare: (dom) => { dom.bodyComputedMarginLeft = '3px'; }, getExpectedValueOnBlock: () => 'calc(100% - (3px))', }, { propertyName: 'bodyStyleWidth', description: 'margin on right', prepare: (dom) => { dom.bodyComputedMarginRight = '4px'; }, getExpectedValueOnBlock: () => 'calc(100% - (4px))', }, { propertyName: 'bodyStyleWidth', description: 'margin on left and right', prepare: (dom) => { dom.bodyComputedMarginLeft = '5px'; dom.bodyComputedMarginRight = '5px'; }, getExpectedValueOnBlock: () => 'calc(100% - (5px + 5px))', }, { propertyName: 'bodyStyleHeight', description: 'no margin', getExpectedValueOnBlock: () => '100%', }, { propertyName: 'bodyStyleHeight', description: 'margin on top', prepare: (dom) => { dom.bodyComputedMarginTop = '3px'; }, getExpectedValueOnBlock: () => 'calc(100% - (3px))', }, { propertyName: 'bodyStyleHeight', description: 'margin on bottom', prepare: (dom) => { dom.bodyComputedMarginBottom = '4px'; }, getExpectedValueOnBlock: () => 'calc(100% - (4px))', }, { propertyName: 'bodyStyleHeight', description: 'margin on top and bottom', prepare: (dom) => { dom.bodyComputedMarginTop = '5px'; dom.bodyComputedMarginBottom = '5px'; }, getExpectedValueOnBlock: () => 'calc(100% - (5px + 5px))', }, ]; function createMockDomStateAccessor(): ScrollDomStateAccessor { return { bodyStyleOverflowX: '', bodyStyleOverflowY: '', htmlScrollLeft: 0, htmlScrollTop: 0, bodyStyleLeft: '', bodyStyleTop: '', bodyStylePosition: '', bodyStyleWidth: '', bodyStyleHeight: '', bodyComputedMarginLeft: '', bodyComputedMarginRight: '', bodyComputedMarginTop: '', bodyComputedMarginBottom: '', htmlScrollWidth: 0, htmlScrollHeight: 0, htmlClientWidth: 0, htmlClientHeight: 0, }; } type Writable = { -readonly [P in keyof T]: T[P]; };