diff --git a/config/jest-puppeteer.rollup.js b/config/jest-puppeteer.rollup.js index d0b1ebd..cd7e313 100644 --- a/config/jest-puppeteer.rollup.js +++ b/config/jest-puppeteer.rollup.js @@ -15,6 +15,18 @@ const cacheFilePrefix = 'jest-puppeteer-overlayscrollbars-cache-'; const cacheEncoding = 'utf8'; const cacheHash = 'md5'; +const rollupAdditionalWatchFiles = (files) => ({ + buildStart() { + if (files) { + files.forEach((file) => { + if (fs.existsSync(file)) { + this.addWatchFile(file); + } + }); + } + }, +}); + const makeHtmlAttributes = (attributes) => { if (!attributes) { return ''; @@ -26,7 +38,7 @@ const makeHtmlAttributes = (attributes) => { return keys.reduce((result, key) => (result += ` ${key}="${attributes[key]}"`), ''); }; -const genHtmlTemplateFunc = (content) => ({ attributes, files, meta, publicPath, title }) => { +const genHtmlTemplateFunc = (contentOrContentFn) => ({ attributes, files, meta, publicPath, title }) => { const scripts = (files.js || []) .map(({ fileName }) => ``) .join('\n'); @@ -87,7 +99,7 @@ const genHtmlTemplateFunc = (content) => ({ attributes, files, meta, publicPath, ${links} - ${content || ''} + ${(typeof contentOrContentFn === 'function' ? contentOrContentFn() : contentOrContentFn) || ''} ${scripts}
@@ -152,8 +164,19 @@ const setupRollupTest = async (rootDir, testPath, cacheDir, watch) => { if (typeof rollupConfig === 'function') { try { const htmlFilePath = path.resolve(testDir, deploymentConfig.html.input); - const htmlFileContent = fs.existsSync(htmlFilePath) ? fs.readFileSync(htmlFilePath, 'utf8') : null; const dist = path.resolve(testDir, deploymentConfig.build); + const getHtmlFileContent = () => (fs.existsSync(htmlFilePath) ? fs.readFileSync(htmlFilePath, 'utf8') : null); + const logBuilding = (re) => { + const text = re ? ' RE-BUILDING ' : ' BUILDING '; + console.log(`\x1b[1m\x1b[44m${text}\x1b[0m \x1b[90m${testPath}\x1b[0m`); // eslint-disable-line + }; + const logBundleFinish = (duration) => { + if (duration) { + console.log(`Bundle finished after ${Math.round(duration / 1000)} seconds.`); // eslint-disable-line + } else { + console.log(`Bundle finished.`); // eslint-disable-line + } + }; let rollupConfigObj = rollupConfig(undefined, { project: rootDir, @@ -173,11 +196,12 @@ const setupRollupTest = async (rootDir, testPath, cacheDir, watch) => { rollupPluginHtml({ title: `Jest-Puppeteer: ${testName}`, fileName: deploymentConfig.html.output, - template: genHtmlTemplateFunc(htmlFileContent), + template: genHtmlTemplateFunc(getHtmlFileContent), meta: [{ charset: 'utf-8' }, { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge' }], }), ...(watch ? [ + rollupAdditionalWatchFiles([htmlFilePath]), rollupPluginServe({ contentBase: dist, historyApiFallback: `/${deploymentConfig.html.output}`, @@ -225,10 +249,13 @@ const setupRollupTest = async (rootDir, testPath, cacheDir, watch) => { console.log('Error:', error); // eslint-disable-line } if (code === 'START') { - console.log(firstWatch ? 'Building...' : 'Rebuilding...'); // eslint-disable-line + if (firstWatch) { + console.log(''); // eslint-disable-line + } + logBuilding(!firstWatch); } if (code === 'BUNDLE_END') { - console.log(`Bundle finished after ${Math.round(duration / 1000)} seconds.`); // eslint-disable-line + logBundleFinish(duration); if (result && result.close) { result.close(); } @@ -246,6 +273,9 @@ const setupRollupTest = async (rootDir, testPath, cacheDir, watch) => { rollupWatchers.push(rollupWatcher); } else { + console.log(''); // eslint-disable-line + logBuilding(); + const startTime = Date.now(); // eslint-disable-next-line no-await-in-loop const bundle = await rollup.rollup(inputConfig); @@ -253,7 +283,12 @@ const setupRollupTest = async (rootDir, testPath, cacheDir, watch) => { const outputConfig = output[i]; // eslint-disable-next-line no-await-in-loop await bundle.write(outputConfig); + + const endTime = Date.now(); + logBundleFinish(endTime - startTime); } + + console.log(''); // eslint-disable-line } } diff --git a/packages/overlayscrollbars/src/observers/domObserver.ts b/packages/overlayscrollbars/src/observers/domObserver.ts index 748de7b..5f971de 100644 --- a/packages/overlayscrollbars/src/observers/domObserver.ts +++ b/packages/overlayscrollbars/src/observers/domObserver.ts @@ -1,9 +1,19 @@ -import { each, indexOf, isString, MutationObserverConstructor, isEmptyArray, liesBetween } from 'support'; -import { classNameHost, classNameContent } from 'classnames'; +import { each, indexOf, isString, MutationObserverConstructor, isEmptyArray, on, off, attr, is, find } from 'support'; +type StringNullUndefined = string | null | undefined; + +export type DOMOvserverEventContentChangeResult = Array<[StringNullUndefined, StringNullUndefined] | null | undefined>; // [selector, eventname] +export type DOMOvserverEventContentChange = () => DOMOvserverEventContentChangeResult; +export type DOMObserverIgnoreContentChange = ( + mutation: MutationRecord, + domObserverTarget: HTMLElement, + domObserverOptions: DOMObserverOptions | undefined +) => boolean | null | undefined; export interface DOMObserverOptions { _observeContent?: boolean; _attributes?: string[]; + _ignoreContentChange?: DOMObserverIgnoreContentChange; + _eventContentChange?: DOMOvserverEventContentChange; } export interface DOMObserver { _disconnect: () => void; @@ -12,76 +22,115 @@ export interface DOMObserver { const styleChangingAttributes = ['id', 'class', 'style', 'open']; const mutationObserverAttrsTextarea = ['wrap', 'cols', 'rows']; - -const isUnknownMutation = ( - attributeName: string | null, - type: MutationRecordType, - observeContent?: boolean, - target?: Node, - mutationTarget?: Node -) => { - const isAttributesType = type === 'attributes'; - const targetIsMutationTarget = target === mutationTarget; - const styleChangingAttrChanged = indexOf(styleChangingAttributes, attributeName) > -1; - const contentChanged = observeContent && !isAttributesType; - const contentAttrChanged = - observeContent && - isAttributesType && - styleChangingAttrChanged && - !targetIsMutationTarget && - !liesBetween(mutationTarget as Element | undefined, `.${classNameHost}`, `.${classNameContent}`); - const targetAttrChanged = isAttributesType && styleChangingAttrChanged && targetIsMutationTarget && !observeContent; - - return contentChanged || contentAttrChanged || targetAttrChanged; -}; +const getAttributeChanged = (mutationTarget: Node, attributeName: string, oldValue: string | null): boolean => + oldValue !== attr(mutationTarget as HTMLElement, attributeName); export const createDOMObserver = ( target: HTMLElement, - callback: (changedTargetAttrs: string[], styleChanged: boolean, contentChanged: boolean) => any, + callback: (targetChangedAttrs: string[], targetStyleChanged: boolean, contentChanged: boolean) => any, options?: DOMObserverOptions ): DOMObserver => { - const { _observeContent, _attributes } = options || {}; + let isConnected = false; + const { _observeContent, _attributes, _ignoreContentChange, _eventContentChange } = options || {}; + const eventContentChangeCallback = () => { + if (isConnected) { + callback([], false, true); + } + }; + const refreshEventContentChange = (getElements: (selector: string) => Node[]) => { + if (_eventContentChange) { + const eventContentChanges = _eventContentChange(); + const eventElmList = eventContentChanges.reduce>((arr, item) => { + if (item) { + const selector = item[0]; + const eventName = item[1]; + const elements = eventName && selector && getElements(selector); + + if (elements) { + arr.push([eventName!, elements]); + } + } + return arr; + }, []); + + each(eventElmList, (item) => { + const eventName = item[0]; + const elements = item[1]; + + each(elements, (elm) => { + off(elm, eventName, eventContentChangeCallback); + on(elm, eventName, eventContentChangeCallback); + }); + }); + } + }; // MutationObserver - const observedAttributes = (_attributes || []).concat(_observeContent ? styleChangingAttributes : mutationObserverAttrsTextarea); + const observedAttributes = (_attributes || []).concat(styleChangingAttributes); // TODO: observer textarea attrs if textarea const observerCallback = (mutations: MutationRecord[]) => { - let styleChanged = false; + const targetChangedAttrs: string[] = []; + const totalAddedNodes: Node[] = []; + let targetStyleChanged = false; let contentChanged = false; - const changedTargetAttrs: string[] = []; + let childListChanged = false; each(mutations, (mutation) => { - const { attributeName, target: mutationTarget, type } = mutation; + const { attributeName, target: mutationTarget, type, oldValue, addedNodes } = mutation; + const isAttributesType = type === 'attributes'; + const isChildListType = type === 'childList'; + const targetIsMutationTarget = target === mutationTarget; + const attributeChanged = isAttributesType && isString(attributeName) && getAttributeChanged(mutationTarget, attributeName!, oldValue); + const targetAttrChanged = attributeChanged && targetIsMutationTarget && !_observeContent; + const styleChangingAttrChanged = indexOf(styleChangingAttributes, attributeName) > -1 && attributeChanged; - styleChanged = styleChanged || isUnknownMutation(attributeName, type); + targetStyleChanged = targetStyleChanged || (targetAttrChanged && styleChangingAttrChanged); - if (_observeContent) { - contentChanged = contentChanged || isUnknownMutation(attributeName, type, true, target, mutationTarget); + if (targetAttrChanged) { + targetChangedAttrs.push(attributeName!); } - if (isString(attributeName) && target === mutationTarget) { - changedTargetAttrs.push(attributeName); + if (_observeContent) { + const notOnlyAttrChanged = !isAttributesType; + const contentAttrChanged = isAttributesType && styleChangingAttrChanged && !targetIsMutationTarget; + const contentFinalChanged = + (notOnlyAttrChanged || contentAttrChanged) && (_ignoreContentChange ? !_ignoreContentChange(mutation, target, options) : _observeContent); + + each(addedNodes, (node) => { + totalAddedNodes.push(node); + }); + + contentChanged = contentChanged || contentFinalChanged; + childListChanged = childListChanged || isChildListType; } }); - if (!isEmptyArray(changedTargetAttrs) || styleChanged || contentChanged) { - callback(changedTargetAttrs, styleChanged, contentChanged); + if (childListChanged && !isEmptyArray(totalAddedNodes)) { + refreshEventContentChange((selector) => totalAddedNodes.filter((node) => is(node as Element, selector))); + } + if (!isEmptyArray(targetChangedAttrs) || targetStyleChanged || contentChanged) { + callback(targetChangedAttrs, targetStyleChanged, contentChanged); } }; const mutationObserver: MutationObserver = new MutationObserverConstructor!(observerCallback); - const connect = () => { - mutationObserver.observe(target, { - attributes: true, - attributeOldValue: true, - subtree: _observeContent, - childList: _observeContent, - characterData: _observeContent, - attributeFilter: observedAttributes, - }); - }; + mutationObserver.observe(target, { + attributes: true, + attributeOldValue: true, + attributeFilter: observedAttributes, + subtree: _observeContent, + childList: _observeContent, + characterData: _observeContent, + }); - connect(); + isConnected = true; + + if (_observeContent) { + refreshEventContentChange((selector) => find(selector, target) as Node[]); + } return { - _disconnect: mutationObserver.disconnect, + _disconnect: () => { + mutationObserver.disconnect(); + isConnected = false; + }, _update: () => { observerCallback(mutationObserver.takeRecords()); }, diff --git a/packages/overlayscrollbars/src/support/dom/traversal.ts b/packages/overlayscrollbars/src/support/dom/traversal.ts index a6f4fe0..6a52161 100644 --- a/packages/overlayscrollbars/src/support/dom/traversal.ts +++ b/packages/overlayscrollbars/src/support/dom/traversal.ts @@ -10,7 +10,7 @@ const elmPrototype = Element.prototype; * @param selector The selector which has to be searched by. * @param elm The element from which the search shall be outgoing. */ -const find = (selector: string, elm?: InputElementType): ReadonlyArray => { +const find = (selector: string, elm?: InputElementType): Element[] => { const arr: Array = []; each((elm || document).querySelectorAll(selector), (e: Element) => { @@ -38,7 +38,7 @@ const is = (elm: InputElementType, selector: string): boolean => { // eslint-disable-next-line // @ts-ignore const fn = elmPrototype.matches || elmPrototype.msMatchesSelector; - return fn.call(elm, selector); + return fn && fn.call(elm, selector); } return false; }; @@ -101,8 +101,8 @@ const closest = (elm: InputElementType, selector: string): OutputElementType => * @param deepBoundarySelector The deep boundary selector. */ const liesBetween = (elm: InputElementType, highBoundarySelector: string, deepBoundarySelector: string): boolean => { - const closestHighBoundaryElm = closest(elm, highBoundarySelector); - const closestDeepBoundaryElm = findFirst(deepBoundarySelector, closestHighBoundaryElm); + const closestHighBoundaryElm = elm && closest(elm, highBoundarySelector); + const closestDeepBoundaryElm = elm && findFirst(deepBoundarySelector, closestHighBoundaryElm); return closestHighBoundaryElm && closestDeepBoundaryElm ? closestHighBoundaryElm === elm || diff --git a/packages/overlayscrollbars/tests/puppeteer/observers/domObserver/index.browser.ts b/packages/overlayscrollbars/tests/puppeteer/observers/domObserver/index.browser.ts index fcd16ab..0852535 100644 --- a/packages/overlayscrollbars/tests/puppeteer/observers/domObserver/index.browser.ts +++ b/packages/overlayscrollbars/tests/puppeteer/observers/domObserver/index.browser.ts @@ -1,185 +1,335 @@ import 'overlayscrollbars.scss'; import './index.scss'; import should from 'should'; -import { waitFor } from '@testing-library/dom'; import { generateSelectCallback, iterateSelect } from '@/testing-browser/Select'; -import { setTestResult } from '@/testing-browser/TestResult'; -import { hasDimensions, offsetSize, WH, style } from 'support'; +import { timeout } from '@/testing-browser/timeout'; +import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult'; +import { appendChildren, createDiv, removeElements, children, isArray, isNumber, liesBetween, hasClass } from 'support'; -import { createSizeObserver } from 'observers/sizeObserver'; +import { createDOMObserver } from 'observers/domObserver'; -let sizeIterations = 0; -let directionIterations = 0; -const contentBox = (elm: HTMLElement | null): WH => { - if (elm) { - const computedStyle = window.getComputedStyle(elm); - return { - w: elm.clientWidth - (parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight)), - h: elm.clientHeight - (parseFloat(computedStyle.paddingTop) + parseFloat(computedStyle.paddingBottom)), - }; - } +interface DOMObserverResult { + changedTargetAttrs: string[]; + styleChanged: boolean; + contentChanged: boolean; +} +interface SeparateChangeThrough { + added?: DOMObserverResult[]; + removed?: DOMObserverResult[]; +} - return { w: 0, h: 0 }; -}; +const targetChangesCountSlot: HTMLElement | null = document.querySelector('#targetChanges'); +const contentChangesCountSlot: HTMLElement | null = document.querySelector('#contentChanges'); +const targetElm: HTMLElement | null = document.querySelector('#target'); +const contentElmAttrChange: HTMLElement | null = document.querySelector('#target .content-nest'); +const contentBetweenElmAttrChange: HTMLElement | null = document.querySelector('#content-host .padding-nest-item'); +const contentHostElmAttrChange: HTMLElement | null = document.querySelector('#content-nest-item-host'); + +const targetElmsSlot = document.querySelector('#target .host-nest-item'); +const targetContentElmsSlot = document.querySelector('#target .content .content-nest'); +const targetContentBetweenElmsSlot = document.querySelector('#content-host'); + +const addRemoveTargetElms: HTMLButtonElement | null = document.querySelector('#addRemoveTargetElms'); +const addRemoveTargetContentElms: HTMLButtonElement | null = document.querySelector('#addRemoveTargetContentElms'); +const addRemoveTargetContentBetweenElms: HTMLButtonElement | null = document.querySelector('#addRemoveTargetContentBetweenElms'); +const setTargetAttr: HTMLSelectElement | null = document.querySelector('#setTargetAttr'); +const setFilteredTargetAttr: HTMLSelectElement | null = document.querySelector('#setFilteredTargetAttr'); +const setContentAttr: HTMLSelectElement | null = document.querySelector('#setContentAttr'); +const setFilteredContentAttr: HTMLSelectElement | null = document.querySelector('#setFilteredContentAttr'); +const setContentBetweenAttr: HTMLSelectElement | null = document.querySelector('#setContentBetweenAttr'); +const setFilteredContentBetweenAttr: HTMLSelectElement | null = document.querySelector('#setFilteredContentBetweenAttr'); +const setContentHostElmAttr: HTMLSelectElement | null = document.querySelector('#setContentHostElmAttr'); +const setFilteredContentHostElmAttr: HTMLSelectElement | null = document.querySelector('#setFilteredContentHostElmAttr'); +const summaryContent: HTMLElement | null = document.querySelector('#summary-content'); +const summaryBetween: HTMLElement | null = document.querySelector('#summary-between'); -const targetElm = document.querySelector('#target'); -const heightSelect: HTMLSelectElement | null = document.querySelector('#height'); -const widthSelect: HTMLSelectElement | null = document.querySelector('#width'); -const paddingSelect: HTMLSelectElement | null = document.querySelector('#padding'); -const borderSelect: HTMLSelectElement | null = document.querySelector('#border'); -const boxSizingSelect: HTMLSelectElement | null = document.querySelector('#boxSizing'); -const displaySelect: HTMLSelectElement | null = document.querySelector('#display'); -const directionSelect: HTMLSelectElement | null = document.querySelector('#direction'); const startBtn: HTMLButtonElement | null = document.querySelector('#start'); -const resizesSlot: HTMLButtonElement | null = document.querySelector('#resizes'); -const selectCallback = generateSelectCallback(targetElm as HTMLElement); -const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any) => { - interface IterateSelect { - currSizeIterations: number; - currDirectionIterations: number; - currOffsetSize: WH; - currContentSize: WH; - currDir: string; +const targetElmObservations: DOMObserverResult[] = []; +const targetElmContentElmObservations: DOMObserverResult[] = []; +const getTotalObservations = () => targetElmObservations.length + targetElmContentElmObservations.length; +const getLast = (arr: T[]): T => arr[arr.length - 1] || ({} as T); +const changedThrough = (observationLists?: Array | DOMObserverResult[]) => { + interface Stat { + total: number; + lists: Array<[DOMObserverResult[], number]>; + } + const noObservationLists = observationLists === undefined; + let before: Stat; + let after: Stat; + if (noObservationLists) { + observationLists = []; + } + if (isArray(observationLists) && !isArray(observationLists[0])) { + observationLists = [observationLists] as Array; } - await iterateSelect(select, { + const getStats = (): Stat => { + return { + total: getTotalObservations(), + lists: (observationLists as Array).map((list) => [list, list.length]), + }; + }; + + return { + before: () => { + before = getStats(); + }, + after: () => { + after = getStats(); + }, + compare: (comparisonTableOrNumber: number | Map = 0) => { + let totalDiff = 0; + if (isNumber(comparisonTableOrNumber) || noObservationLists) { + before.lists.forEach((_, index) => { + const [, beforeCount] = before.lists[index]; + const [, afterCount] = after.lists[index]; + + totalDiff += afterCount - beforeCount; + should(afterCount).equal(beforeCount + (noObservationLists ? 0 : (comparisonTableOrNumber as number))); + }); + } else { + before.lists.forEach((_, index) => { + const [list, beforeCount] = before.lists[index]; + const [, afterCount] = after.lists[index]; + + totalDiff += afterCount - beforeCount; + should(afterCount).equal(beforeCount + (comparisonTableOrNumber.get(list) || 0)); + }); + } + should(after.total).equal(before.total + totalDiff); + }, + }; +}; +const attrChangeListener = (attrChangeTarget: HTMLElement | null) => + generateSelectCallback(attrChangeTarget, (target, possibleValues, selectedValue) => { + const isClass = selectedValue === 'class'; + + target.classList.remove('something'); + possibleValues.forEach((val) => val !== 'class' && target.removeAttribute(val)); + isClass && target.classList.add('something'); + !isClass && target.setAttribute(selectedValue, 'something'); + }); +const iterateAttrChange = async ( + select: HTMLSelectElement | null, + changeThrough?: DOMObserverResult[], + checkChange?: (observation: DOMObserverResult, selected: string) => any +) => { + const { before, after, compare } = changedThrough(changeThrough); + + await iterateSelect(select, { beforeEach() { - const currSizeIterations = sizeIterations; - const currDirectionIterations = directionIterations; - const currOffsetSize = offsetSize(targetElm as HTMLElement); - const currContentSize = contentBox(targetElm as HTMLElement); - const currDir = style(targetElm as HTMLElement, 'direction'); - - return { - currSizeIterations, - currDirectionIterations, - currOffsetSize, - currContentSize, - currDir, - }; + before(); }, - async check({ currSizeIterations, currDirectionIterations, currOffsetSize, currContentSize, currDir }) { - const newOffsetSize = offsetSize(targetElm as HTMLElement); - const newContentSize = contentBox(targetElm as HTMLElement); - const newDir = style(targetElm as HTMLElement, 'direction'); - const offsetSizeChanged = currOffsetSize.w !== newOffsetSize.w || currOffsetSize.h !== newOffsetSize.h; - const contentSizeChanged = currContentSize.w !== newContentSize.w || currContentSize.h !== newContentSize.h; - const dirChanged = currDir !== newDir; - const dimensions = hasDimensions(targetElm as HTMLElement); - const observerElm = targetElm?.firstElementChild as HTMLElement; + async check(_, selected) { + await waitForOrFailTest(async () => { + after(); - // no overflow if not needed - if (targetElm && newContentSize.w > 0) { - should.ok(observerElm.getBoundingClientRect().right <= targetElm.getBoundingClientRect().right); - } - if (targetElm && newContentSize.h > 0) { - should.ok(observerElm.getBoundingClientRect().bottom <= targetElm.getBoundingClientRect().bottom); - } - - if (dimensions && (offsetSizeChanged || contentSizeChanged || dirChanged)) { - await waitFor( - () => { - if (offsetSizeChanged || contentSizeChanged) { - should.equal(sizeIterations, currSizeIterations + 1); - } - if (dirChanged) { - should.equal(directionIterations, currDirectionIterations + 1); - } - }, - { - onTimeout(error): Error { - setTestResult(false); - return error; - }, - } - ); - } + if (changeThrough) { + compare(1); + checkChange && checkChange(getLast(changeThrough), selected); + } else { + await timeout(250); + compare(0); + } + }); }, - afterEach, }); }; +const addRemoveElementsTest = async (slot: Element | null, changeThrough?: DOMObserverResult[] | SeparateChangeThrough) => { + if (slot) { + let addChangeThrough: DOMObserverResult[] | undefined = changeThrough as DOMObserverResult[] | undefined; + let removeChangeThrough: DOMObserverResult[] | undefined = changeThrough as DOMObserverResult[] | undefined; + if (changeThrough && !isArray(changeThrough)) { + addChangeThrough = (changeThrough as SeparateChangeThrough).added; + removeChangeThrough = (changeThrough as SeparateChangeThrough).removed; + } -heightSelect?.addEventListener('change', selectCallback); -widthSelect?.addEventListener('change', selectCallback); -paddingSelect?.addEventListener('change', selectCallback); -borderSelect?.addEventListener('change', selectCallback); -boxSizingSelect?.addEventListener('change', selectCallback); -displaySelect?.addEventListener('change', selectCallback); -directionSelect?.addEventListener('change', selectCallback); + const addElm = async () => { + const { before, after, compare } = changedThrough(addChangeThrough); -selectCallback(heightSelect); -selectCallback(widthSelect); -selectCallback(paddingSelect); -selectCallback(borderSelect); -selectCallback(boxSizingSelect); -selectCallback(displaySelect); -selectCallback(directionSelect); + before(); + appendChildren(slot, createDiv('addedElm')); + await timeout(250); + after(); -const iteratePadding = async (afterEach?: () => any) => { - await iterate(paddingSelect, afterEach); + await waitForOrFailTest(() => { + compare(1); + }); + + if (addChangeThrough) { + const { contentChanged, styleChanged, changedTargetAttrs } = getLast(addChangeThrough); + await waitForOrFailTest(() => { + should(contentChanged).equal(true); + should(styleChanged).equal(false); + should(changedTargetAttrs.length).equal(0); + }); + } + }; + + const removeElm = async () => { + const removeItem = children(slot, '.addedElm')[0]; + const { before, after, compare } = changedThrough(removeChangeThrough); + + if (removeItem) { + before(); + removeElements(removeItem); + await timeout(250); + + await waitForOrFailTest(() => { + after(); + compare(1); + + if (removeChangeThrough) { + const { changedTargetAttrs, styleChanged, contentChanged } = getLast(removeChangeThrough); + should(changedTargetAttrs.length).equal(0); + should(styleChanged).equal(false); + should(contentChanged).equal(true); + } + }); + } + }; + + await addElm(); + await addElm(); + await addElm(); + + await removeElm(); + await removeElm(); + await removeElm(); + } }; -const iterateBorder = async (afterEach?: () => any) => { - await iterate(borderSelect, afterEach); +const triggerSummaryElemet = async (summaryElm: HTMLElement | null, changeThrough?: DOMObserverResult[]) => { + // onyl do if summary is working (IE. exception) + if (summaryElm && (summaryElm.nextElementSibling as HTMLElement)?.offsetHeight === 0) { + const click = async () => { + const { before, after, compare } = changedThrough(changeThrough); + + before(); + summaryElm?.click(); + await timeout(250); + after(); + + await waitForOrFailTest(() => { + compare(1); + }); + }; + + await click(); + await click(); + } }; -const iterateHeight = async (afterEach?: () => any) => { - await iterate(heightSelect, afterEach); + +const addRemoveTargetElmsFn = async () => { + await addRemoveElementsTest(targetElmsSlot); }; -const iterateWidth = async (afterEach?: () => any) => { - await iterate(widthSelect, afterEach); +const addRemoveTargetContentElmsFn = async () => { + await addRemoveElementsTest(targetContentElmsSlot, targetElmContentElmObservations); }; -const iterateBoxSizing = async (afterEach?: () => any) => { - await iterate(boxSizingSelect, afterEach); +const addRemoveTargetContentBetweenElmsFn = async () => { + await addRemoveElementsTest(targetContentBetweenElmsSlot, targetElmContentElmObservations); }; -const iterateDisplay = async (afterEach?: () => any) => { - await iterate(displaySelect, afterEach); +const iterateTargetAttrChange = async () => { + await iterateAttrChange(setTargetAttr, targetElmObservations, (observation, selected) => { + const { changedTargetAttrs, styleChanged, contentChanged } = observation; + should(changedTargetAttrs.includes(selected)).equal(true); + should(styleChanged).equal(true); + should(contentChanged).equal(false); + }); + await iterateAttrChange(setFilteredTargetAttr); }; -const iterateDirection = async (afterEach?: () => any) => { - await iterate(directionSelect, afterEach); +const iterateContentAttrChange = async () => { + await iterateAttrChange(setContentAttr, targetElmContentElmObservations, (observation) => { + const { changedTargetAttrs, styleChanged, contentChanged } = observation; + should(changedTargetAttrs.length).equal(0); + should(styleChanged).equal(false); + should(contentChanged).equal(true); + }); + await iterateAttrChange(setFilteredContentAttr); }; +const iterateContentBetweenAttrChange = async () => { + await iterateAttrChange(setContentBetweenAttr); + await iterateAttrChange(setFilteredContentBetweenAttr); +}; +const iterateContentHostElmAttrChange = async () => { + await iterateAttrChange(setContentHostElmAttr, targetElmContentElmObservations, (observation) => { + const { changedTargetAttrs, styleChanged, contentChanged } = observation; + should(changedTargetAttrs.length).equal(0); + should(styleChanged).equal(false); + should(contentChanged).equal(true); + }); + await iterateAttrChange(setFilteredContentHostElmAttr); +}; +const triggerContentSummaryChange = async () => { + await triggerSummaryElemet(summaryContent, targetElmContentElmObservations); +}; +const triggerBetweenSummaryChange = async () => { + await triggerSummaryElemet(summaryBetween); +}; + +addRemoveTargetElms?.addEventListener('click', addRemoveTargetElmsFn); +addRemoveTargetContentElms?.addEventListener('click', addRemoveTargetContentElmsFn); +addRemoveTargetContentBetweenElms?.addEventListener('click', addRemoveTargetContentBetweenElmsFn); +setTargetAttr?.addEventListener('change', attrChangeListener(targetElm)); +setFilteredTargetAttr?.addEventListener('change', attrChangeListener(targetElm)); +setContentAttr?.addEventListener('change', attrChangeListener(contentElmAttrChange)); +setFilteredContentAttr?.addEventListener('change', attrChangeListener(contentElmAttrChange)); +setContentBetweenAttr?.addEventListener('change', attrChangeListener(contentBetweenElmAttrChange)); +setFilteredContentBetweenAttr?.addEventListener('change', attrChangeListener(contentBetweenElmAttrChange)); +setContentHostElmAttr?.addEventListener('change', attrChangeListener(contentHostElmAttrChange)); +setFilteredContentHostElmAttr?.addEventListener('change', attrChangeListener(contentHostElmAttrChange)); + +createDOMObserver( + document.querySelector('#target') as HTMLElement, + (changedTargetAttrs: string[], styleChanged: boolean, contentChanged: boolean) => { + targetElmObservations.push({ changedTargetAttrs, styleChanged, contentChanged }); + requestAnimationFrame(() => { + if (targetChangesCountSlot) { + targetChangesCountSlot.textContent = `${targetElmObservations.length}`; + } + }); + }, + { + _attributes: ['data-target'], + } +); +createDOMObserver( + document.querySelector('#target .content') as HTMLElement, + (changedTargetAttrs: string[], styleChanged: boolean, contentChanged: boolean) => { + targetElmContentElmObservations.push({ changedTargetAttrs, styleChanged, contentChanged }); + requestAnimationFrame(() => { + if (contentChangesCountSlot) { + contentChangesCountSlot.textContent = `${targetElmContentElmObservations.length}`; + } + }); + }, + { + _observeContent: true, + _ignoreContentChange: (mutation) => { + const { target, attributeName } = mutation; + return attributeName ? !hasClass(target as Element, 'host') && liesBetween(target as Element, '.host', '.content') : false; + }, + } +); const start = async () => { setTestResult(null); - console.log('init direction changes:', directionIterations); - console.log('init size changes:', sizeIterations); - should.ok(directionIterations > 0); - should.ok(sizeIterations > 0); + await addRemoveTargetElmsFn(); + await addRemoveTargetContentElmsFn(); + await addRemoveTargetContentBetweenElmsFn(); - targetElm?.removeAttribute('style'); - await iterateDisplay(); - await iterateDirection(); - await iterateBoxSizing(async () => { - await iterateHeight(async () => { - await iterateWidth(async () => { - await iterateBorder(async () => { - await iterateDirection(); - await iteratePadding(); - }); - }); - }); - }); + await iterateTargetAttrChange(); + await iterateContentAttrChange(); + await iterateContentBetweenAttrChange(); + await iterateContentHostElmAttrChange(); + + await triggerContentSummaryChange(); + await triggerBetweenSummaryChange(); setTestResult(true); }; startBtn?.addEventListener('click', start); -createSizeObserver( - targetElm as HTMLElement, - (directionCache?: any) => { - if (directionCache) { - directionIterations += 1; - } else { - sizeIterations += 1; - } - requestAnimationFrame(() => { - if (resizesSlot) { - resizesSlot.textContent = (directionIterations + sizeIterations).toString(); - } - }); - }, - { _direction: true, _appear: true } -); - export { start }; diff --git a/packages/overlayscrollbars/tests/puppeteer/observers/domObserver/index.html b/packages/overlayscrollbars/tests/puppeteer/observers/domObserver/index.html index 05167a7..c398ebe 100644 --- a/packages/overlayscrollbars/tests/puppeteer/observers/domObserver/index.html +++ b/packages/overlayscrollbars/tests/puppeteer/observers/domObserver/index.html @@ -1,49 +1,144 @@
- - + + + + - - + + + - - + + + + - - + + + - - + + + + - - + + + - - + + + + + + + + - Detected resizes: 0 + Detected target changes: 0 + Detected content changes: 0
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Triggers DOM Change +

DOM Content Change should be triggered

+
+
+
+
+
+
+
+
+
+
+
+
+
+ Won't trigger DOM Change +

DOM Content Change shouldn't be triggered

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/overlayscrollbars/tests/puppeteer/observers/domObserver/index.scss b/packages/overlayscrollbars/tests/puppeteer/observers/domObserver/index.scss index 8e9c890..5347c89 100644 --- a/packages/overlayscrollbars/tests/puppeteer/observers/domObserver/index.scss +++ b/packages/overlayscrollbars/tests/puppeteer/observers/domObserver/index.scss @@ -27,73 +27,51 @@ body { left: 0; } -#target { - overflow: hidden; - resize: both; - position: relative; - // prevent container from reaching 0x0 dimensions for testing purposes - min-width: 50px; - min-height: 50px; +.addedElm { + height: 20px; + width: 20px; + background: yellow; } -.padding0 { - padding: 0; -} -.padding10 { - padding: 10px; -} -.padding50 { - padding: 50px; -} - -.border2 { - border: 2px solid red; -} -.border10 { - border: 10px solid red; -} -.border0 { - border: none; -} - -.heightAuto { - height: auto; -} -.height200 { - height: 200px; -} -.heightHundred { - height: 100%; -} - -.widthAuto { - width: auto; - float: left; -} -.width200 { - width: 200px; -} -.widthHundred { - width: 100%; -} - -.boxSizingBorderBox { - box-sizing: border-box; -} -.boxSizingContentBox { - box-sizing: content-box; -} - -.displayNone { - display: none; -} -.displayBlock { - display: block; -} - -.directionltr { - direction: ltr; -} -.directionRTL { - direction: rtl; +.host { + color: black; + border: 1px solid red; + background: red; + & > .host-nest { + border: 1px solid rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 0.3); + & > .host-nest-item { + border: 1px solid rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 0.3); + } + } + & > .padding { + border: 1px solid green; + background: green; + & > .padding-nest { + border: 1px solid rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 0.3); + & > .padding-nest-item { + border: 1px solid rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 0.3); + } + } + & > .viewport { + border: 1px solid blue; + background: blue; + & > .viewport-nest { + border: 1px solid rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 0.3); + & > .viewport-nest-item { + border: 1px solid rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 0.3); + } + } + & > .content { + border: 1px solid black; + background: black; + color: white; + } + } + } } diff --git a/packages/overlayscrollbars/tests/puppeteer/observers/sizeObserver/index.browser.ts b/packages/overlayscrollbars/tests/puppeteer/observers/sizeObserver/index.browser.ts index fcd16ab..2e3a5b4 100644 --- a/packages/overlayscrollbars/tests/puppeteer/observers/sizeObserver/index.browser.ts +++ b/packages/overlayscrollbars/tests/puppeteer/observers/sizeObserver/index.browser.ts @@ -1,9 +1,8 @@ import 'overlayscrollbars.scss'; import './index.scss'; import should from 'should'; -import { waitFor } from '@testing-library/dom'; -import { generateSelectCallback, iterateSelect } from '@/testing-browser/Select'; -import { setTestResult } from '@/testing-browser/TestResult'; +import { generateClassChangeSelectCallback, iterateSelect } from '@/testing-browser/Select'; +import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult'; import { hasDimensions, offsetSize, WH, style } from 'support'; import { createSizeObserver } from 'observers/sizeObserver'; @@ -33,7 +32,7 @@ const directionSelect: HTMLSelectElement | null = document.querySelector('#direc const startBtn: HTMLButtonElement | null = document.querySelector('#start'); const resizesSlot: HTMLButtonElement | null = document.querySelector('#resizes'); -const selectCallback = generateSelectCallback(targetElm as HTMLElement); +const selectCallback = generateClassChangeSelectCallback(targetElm as HTMLElement); const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any) => { interface IterateSelect { currSizeIterations: number; @@ -78,22 +77,14 @@ const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any) } if (dimensions && (offsetSizeChanged || contentSizeChanged || dirChanged)) { - await waitFor( - () => { - if (offsetSizeChanged || contentSizeChanged) { - should.equal(sizeIterations, currSizeIterations + 1); - } - if (dirChanged) { - should.equal(directionIterations, currDirectionIterations + 1); - } - }, - { - onTimeout(error): Error { - setTestResult(false); - return error; - }, + await waitForOrFailTest(() => { + if (offsetSizeChanged || contentSizeChanged) { + should.equal(sizeIterations, currSizeIterations + 1); } - ); + if (dirChanged) { + should.equal(directionIterations, currDirectionIterations + 1); + } + }); } }, afterEach, diff --git a/packages/overlayscrollbars/tests/puppeteer/observers/trinsicObserver/index.browser.ts b/packages/overlayscrollbars/tests/puppeteer/observers/trinsicObserver/index.browser.ts index 18688b0..8da85d2 100644 --- a/packages/overlayscrollbars/tests/puppeteer/observers/trinsicObserver/index.browser.ts +++ b/packages/overlayscrollbars/tests/puppeteer/observers/trinsicObserver/index.browser.ts @@ -1,21 +1,13 @@ import 'overlayscrollbars.scss'; import './index.scss'; import should from 'should'; -import { waitFor } from '@testing-library/dom'; -import { generateSelectCallback, iterateSelect, selectOption } from '@/testing-browser/Select'; +import { generateClassChangeSelectCallback, iterateSelect, selectOption } from '@/testing-browser/Select'; import { timeout } from '@/testing-browser/timeout'; -import { setTestResult } from '@/testing-browser/TestResult'; +import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult'; import { offsetSize } from 'support'; import { createTrinsicObserver } from 'observers/trinsicObserver'; -const waitForOptions = { - onTimeout(error: Error): Error { - setTestResult(false); - return error; - }, -}; - let heightIntrinsic: boolean | undefined; let heightIterations = 0; const envElm = document.querySelector('#env'); @@ -27,8 +19,8 @@ const displaySelect: HTMLSelectElement | null = document.querySelector('#display const startBtn: HTMLButtonElement | null = document.querySelector('#start'); const changesSlot: HTMLButtonElement | null = document.querySelector('#changes'); -const envElmSelectCallback = generateSelectCallback(envElm as HTMLElement); -const targetElmSelectCallback = generateSelectCallback(targetElm as HTMLElement); +const envElmSelectCallback = generateClassChangeSelectCallback(envElm as HTMLElement); +const targetElmSelectCallback = generateClassChangeSelectCallback(targetElm as HTMLElement); envHeightSelect?.addEventListener('change', envElmSelectCallback); targetHeightSelect?.addEventListener('change', targetElmSelectCallback); @@ -57,11 +49,11 @@ const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any) const newHeightIntrinsic = offsetSize(checkElm as HTMLElement).h === 0; const trinsicHeightChanged = newHeightIntrinsic !== currHeightIntrinsic; - await waitFor(() => { + await waitForOrFailTest(() => { if (trinsicHeightChanged) { should.equal(heightIterations, currHeightIterations + 1); } - }, waitForOptions); + }); }, afterEach, }); @@ -85,9 +77,9 @@ const changeWhileHidden = async () => { selectOption(envHeightSelect as HTMLSelectElement, 'envHeightHundred'); selectOption(displaySelect as HTMLSelectElement, 'displayBlock'); - await waitFor(() => { + await waitForOrFailTest(() => { should.equal(heightIntrinsic, false); - }, waitForOptions); + }); }; const hundredToAuto = async () => { @@ -99,9 +91,9 @@ const changeWhileHidden = async () => { selectOption(envHeightSelect as HTMLSelectElement, 'envHeightAuto'); selectOption(displaySelect as HTMLSelectElement, 'displayBlock'); - await waitFor(() => { + await waitForOrFailTest(() => { should.equal(heightIntrinsic, true); - }, waitForOptions); + }); }; await autoToHundred(); diff --git a/packages/testing-browser/src/Select.ts b/packages/testing-browser/src/Select.ts index 076007b..1696ac3 100644 --- a/packages/testing-browser/src/Select.ts +++ b/packages/testing-browser/src/Select.ts @@ -9,19 +9,27 @@ const noop = (): T => { const getSelectOptions = (selectElement: HTMLSelectElement) => Array.from(selectElement.options).map((option) => option.value); -export const generateSelectCallback = (targetElm: HTMLElement | null) => (event: Event | HTMLSelectElement | null) => { +export const generateSelectCallback = ( + targetElm: HTMLElement | null, + callback: (targetAffectedElm: HTMLElement, possibleValues: string[], selectedValue: string) => any +) => (event: Event | HTMLSelectElement | null) => { const target: HTMLSelectElement | null = isEvent(event) ? (event.target as HTMLSelectElement) : event; if (target) { const selectedOption = target.value; const selectOptions = getSelectOptions(target); if (targetElm) { - selectOptions.forEach((clazz) => targetElm.classList.remove(clazz)); - targetElm.classList.add(selectedOption); + callback(targetElm, selectOptions, selectedOption); } } }; +export const generateClassChangeSelectCallback = (targetElm: HTMLElement | null) => + generateSelectCallback(targetElm, (targetAffectedElm, possibleValues, selectedValue) => { + possibleValues.forEach((clazz) => targetAffectedElm.classList.remove(clazz)); + targetAffectedElm.classList.add(selectedValue); + }); + export const selectOption = (select: HTMLSelectElement | null, selectedOption: string | number): boolean => { if (!select) { return false; @@ -56,7 +64,7 @@ export const iterateSelect = async ( select: HTMLSelectElement | null, options?: { beforeEach?: () => T | Promise; - check?: (input: T) => void | Promise; + check?: (input: T, selectedOptions: string) => void | Promise; afterEach?: () => void | Promise; } ) => { @@ -71,7 +79,7 @@ export const iterateSelect = async ( const beforeEachObj: T = await beforeEach(); if (selectOption(select, option)) { // eslint-disable-next-line - await check(beforeEachObj); + await check(beforeEachObj, option); // eslint-disable-next-line await afterEach(); } diff --git a/packages/testing-browser/src/TestResult.ts b/packages/testing-browser/src/TestResult.ts index df0a772..fb40720 100644 --- a/packages/testing-browser/src/TestResult.ts +++ b/packages/testing-browser/src/TestResult.ts @@ -1,3 +1,5 @@ +import { waitFor, waitForOptions } from '@testing-library/dom'; + const getTestResultElm = () => document.getElementById('testResult'); export const setTestResult = (result: boolean | null) => { @@ -15,3 +17,12 @@ export const testPassed = (): boolean => { const elm = getTestResultElm(); return elm ? elm.getAttribute('class') === 'passed' : false; }; + +export const waitForOrFailTest = (callback: () => T | Promise, options?: waitForOptions) => + waitFor(callback, { + ...options, + onTimeout(error: Error): Error { + setTestResult(false); + return error; + }, + });