diff --git a/packages/overlayscrollbars/src/lifecycles/lifecycleHub.ts b/packages/overlayscrollbars/src/lifecycles/lifecycleHub.ts index ac8d537..46e68f5 100644 --- a/packages/overlayscrollbars/src/lifecycles/lifecycleHub.ts +++ b/packages/overlayscrollbars/src/lifecycles/lifecycleHub.ts @@ -231,12 +231,11 @@ export const createLifecycleHub = (options: OSOptions, structureSetup: Structure const trinsicObserver = (_content || !_flexboxGlue) && createTrinsicObserver(_host, onTrinsicChanged); const sizeObserver = createSizeObserver(_host, onSizeChanged, { _appear: true, _direction: !_nativeScrollbarStyling }); - const hostMutationObserver = createDOMObserver(_host, onHostMutation, { + const hostMutationObserver = createDOMObserver(_host, false, onHostMutation, { _styleChangingAttributes: attrs, _attributes: attrs, }); - const contentMutationObserver = createDOMObserver(_content || _viewport, onContentMutation, { - _observeContent: true, + const contentMutationObserver = createDOMObserver(_content || _viewport, true, onContentMutation, { _styleChangingAttributes: attrs, _attributes: attrs, _eventContentChange: options!.updating!.elementEvents as [string, string][], diff --git a/packages/overlayscrollbars/src/observers/domObserver.ts b/packages/overlayscrollbars/src/observers/domObserver.ts index 42a7271..f289a8e 100644 --- a/packages/overlayscrollbars/src/observers/domObserver.ts +++ b/packages/overlayscrollbars/src/observers/domObserver.ts @@ -17,37 +17,65 @@ import { } from 'support'; type StringNullUndefined = string | null | undefined; + +type DOMContentObserverCallback = (contentChanged: boolean) => any; + +type DOMTargetObserverCallback = (targetChangedAttrs: string[], targetStyleChanged: boolean) => any; + +interface DOMObserverOptionsBase { + _attributes?: string[]; + _styleChangingAttributes?: string[]; +} + +interface DOMContentObserverOptions extends DOMObserverOptionsBase { + _eventContentChange?: DOMObserverEventContentChange; // [selector, eventname | function returning eventname] + _nestedTargetSelector?: string; + _ignoreContentChange?: DOMObserverIgnoreContentChange; // function which will prevent marking certain dom changes as content change if it returns true + _ignoreNestedTargetChange?: DOMObserverIgnoreTargetChange; // a function which will prevent marking certain attributes as changed on nested targets if it returns true +} + +interface DOMTargetObserverOptions extends DOMObserverOptionsBase { + _ignoreTargetChange?: DOMObserverIgnoreTargetChange; // a function which will prevent marking certain attributes as changed if it returns true +} + +interface DOMObserverBase { + _destroy: () => void; + _update: () => void; +} + +interface DOMContentObserver extends DOMObserverBase { + _updateEventContentChange: (newEventContentChange?: DOMObserverEventContentChange) => void; +} + +interface DOMTargetObserver extends DOMObserverBase {} + export type DOMObserverEventContentChange = | Array<[StringNullUndefined, ((elms: Node[]) => string) | StringNullUndefined] | null | undefined> | false | null | undefined; + export type DOMObserverIgnoreContentChange = ( mutation: MutationRecord, isNestedTarget: boolean, domObserverTarget: HTMLElement, - domObserverOptions: DOMObserverOptions | undefined + domObserverOptions: DOMContentObserverOptions | undefined ) => boolean; -export type DOMObserverIgnoreTargetAttrChange = ( + +export type DOMObserverIgnoreTargetChange = ( target: Node, attributeName: string, oldAttributeValue: string | null, newAttributeValue: string | null ) => boolean; -export interface DOMObserverOptions { - _observeContent?: boolean; // do observe children and trigger content change - _attributes?: string[]; // observed attributes - _styleChangingAttributes?: string[]; // list of attributes that trigger a contentChange or a targetStyleChange if changed - _eventContentChange?: DOMObserverEventContentChange; // [selector, eventname] - _nestedTargetSelector?: string; - _ignoreTargetAttrChange?: DOMObserverIgnoreTargetAttrChange; - _ignoreContentChange?: DOMObserverIgnoreContentChange; -} -export interface DOMObserver { - _destroy: () => void; - _updateEventContentChange: (newEventContentChange?: DOMObserverEventContentChange) => void; - _update: () => void; -} + +export type DOMObserverCallback = ContentObserver extends true + ? DOMContentObserverCallback + : DOMTargetObserverCallback; + +export type DOMObserverOptions = ContentObserver extends true ? DOMContentObserverOptions : DOMTargetObserverOptions; + +export type DOMObserver = ContentObserver extends true ? DOMContentObserver : DOMTargetObserver; // const styleChangingAttributes = ['id', 'class', 'style', 'open']; // const mutationObserverAttrsTextarea = ['wrap', 'cols', 'rows']; @@ -63,25 +91,26 @@ const createEventContentChange = (target: Element, eventContentChange: DOMObserv let map: Map | undefined; let eventContentChangeRef: DOMObserverEventContentChange; const addEvent = (elm: Node, eventName: string) => { - const entry = map!.get(elm); - const newEntry = isUndefined(entry); - const registerEvent = () => { - map!.set(elm, eventName); - on(elm, eventName, callback); - }; + if (map) { + const entry = map.get(elm); + const newEntry = isUndefined(entry); + const changedExistingEntry = !newEntry && eventName !== entry; + const register = newEntry || changedExistingEntry; - if (!newEntry && eventName !== entry) { - off(elm, entry!, callback); - registerEvent(); - } else if (newEntry) { - registerEvent(); + if (changedExistingEntry) { + off(elm, entry!, callback); + } + if (register) { + map.set(elm, eventName); + on(elm, eventName, callback); + } } }; const _destroy = () => { - map!.forEach((eventName: string, elm: Node) => { - off(elm, eventName, callback); - }); - map!.clear(); + if (map) { + map.forEach((eventName: string, elm: Node) => off(elm, eventName, callback)); + map.clear(); + } }; const _updateElements = (getElements?: (selector: string) => Node[]) => { if (eventContentChangeRef) { @@ -127,37 +156,39 @@ const createEventContentChange = (target: Element, eventContentChange: DOMObserv }; /** - * Creates a DOM observer which observes DOM changes to either the target element or its children. (not only direct children but also nested ones) + * Creates a DOM observer which observes DOM changes to either the target element or its children. * @param target The element which shall be observed. + * @param isContentObserver Whether this observer is just observing the target or just the targets children. (not only direct children but also nested ones) * @param callback The callback which gets called if a change was detected. * @param options The options for DOM change detection. * @returns A object which represents the instance of the DOM observer. */ -export const createDOMObserver = ( +export const createDOMObserver = ( target: HTMLElement, - callback: (targetChangedAttrs: string[], targetStyleChanged: boolean, contentChanged: boolean) => any, - options?: DOMObserverOptions -): DOMObserver => { + isContentObserver: ContentObserver, + callback: DOMObserverCallback, + options?: DOMObserverOptions +): DOMObserver => { let isConnected = false; const { - _observeContent, _attributes, _styleChangingAttributes, _eventContentChange, _nestedTargetSelector, - _ignoreTargetAttrChange: _ignoreTargetChange, + _ignoreTargetChange, + _ignoreNestedTargetChange, _ignoreContentChange, - } = options || {}; + } = (options as DOMContentObserverOptions & DOMTargetObserverOptions) || {}; const { _destroy: destroyEventContentChange, _updateElements: updateEventContentChangeElements, _updateEventContentChange: updateEventContentChange, } = createEventContentChange( target, - _observeContent && _eventContentChange, + isContentObserver && _eventContentChange, debounce(() => { if (isConnected) { - callback([], false, true); + (callback as DOMContentObserverCallback)(true); } }, 84) ); @@ -167,7 +198,7 @@ export const createDOMObserver = ( const finalStyleChangingAttributes = _styleChangingAttributes || []; const observedAttributes = finalAttributes.concat(finalStyleChangingAttributes); const observerCallback = (mutations: MutationRecord[]) => { - const ignoreTargetChange = _ignoreTargetChange || noop; + const ignoreTargetChange = (isContentObserver ? _ignoreNestedTargetChange : _ignoreTargetChange) || noop; const ignoreContentChange = _ignoreContentChange || noop; const targetChangedAttrs: string[] = []; const totalAddedNodes: Node[] = []; @@ -181,19 +212,12 @@ export const createDOMObserver = ( const targetIsMutationTarget = target === mutationTarget; const attributeValue = isAttributesType && isString(attributeName) ? attr(mutationTarget as HTMLElement, attributeName!) : 0; const attributeChanged = attributeValue !== 0 && oldValue !== attributeValue; - const targetAttrChanged = - attributeChanged && - targetIsMutationTarget && - !_observeContent && - !ignoreTargetChange(mutationTarget, attributeName!, oldValue, attributeValue as string | null); const styleChangingAttrChanged = indexOf(finalStyleChangingAttributes, attributeName) > -1 && attributeChanged; - if (targetAttrChanged) { - push(targetChangedAttrs, attributeName!); - } - if (_observeContent) { + // if is content observer and something changed in children + if (isContentObserver && !targetIsMutationTarget) { const notOnlyAttrChanged = !isAttributesType; - const contentAttrChanged = isAttributesType && styleChangingAttrChanged && !targetIsMutationTarget; + const contentAttrChanged = isAttributesType && styleChangingAttrChanged; const isNestedTarget = contentAttrChanged && _nestedTargetSelector && is(mutationTarget, _nestedTargetSelector); const baseAssertion = isNestedTarget ? !ignoreTargetChange(mutationTarget, attributeName!, oldValue, attributeValue as string | null) @@ -205,7 +229,11 @@ export const createDOMObserver = ( contentChanged = contentChanged || contentFinalChanged; childListChanged = childListChanged || isChildListType; } - targetStyleChanged = targetStyleChanged || (targetAttrChanged && styleChangingAttrChanged); + // else if is target observer and target attr changed + else if (attributeChanged && !ignoreTargetChange(mutationTarget, attributeName!, oldValue, attributeValue as string | null)) { + push(targetChangedAttrs, attributeName!); + targetStyleChanged = targetStyleChanged || styleChangingAttrChanged; + } }); if (childListChanged && !isEmptyArray(totalAddedNodes)) { @@ -217,8 +245,11 @@ export const createDOMObserver = ( }, []) ); } - if (!isEmptyArray(targetChangedAttrs) || targetStyleChanged || contentChanged) { - callback(targetChangedAttrs, targetStyleChanged, contentChanged); + + if (isContentObserver && contentChanged) { + (callback as DOMContentObserverCallback)(contentChanged); + } else if (!isEmptyArray(targetChangedAttrs) || targetStyleChanged) { + (callback as DOMTargetObserverCallback)(targetChangedAttrs, targetStyleChanged); } }; const mutationObserver: MutationObserver = new MutationObserverConstructor!(observerCallback); @@ -228,9 +259,9 @@ export const createDOMObserver = ( attributes: true, attributeOldValue: true, attributeFilter: observedAttributes, - subtree: _observeContent, - childList: _observeContent, - characterData: _observeContent, + subtree: isContentObserver, + childList: isContentObserver, + characterData: isContentObserver, }); isConnected = true; @@ -243,12 +274,12 @@ export const createDOMObserver = ( } }, _updateEventContentChange: (newEventContentChange?: DOMObserverEventContentChange) => { - updateEventContentChange(isConnected && _observeContent && newEventContentChange); + updateEventContentChange(isConnected && isContentObserver && newEventContentChange); }, _update: () => { if (isConnected) { observerCallback(mutationObserver.takeRecords()); } }, - }; + } as DOMObserver; }; diff --git a/packages/overlayscrollbars/tests/browser/observers/domObserver/index.browser.ts b/packages/overlayscrollbars/tests/browser/observers/domObserver/index.browser.ts index e7266bf..7ea67e0 100644 --- a/packages/overlayscrollbars/tests/browser/observers/domObserver/index.browser.ts +++ b/packages/overlayscrollbars/tests/browser/observers/domObserver/index.browser.ts @@ -21,14 +21,14 @@ import { import { createDOMObserver } from 'observers/domObserver'; -interface DOMObserverResult { +type DOMContentObserverResult = boolean; +type DOMTargetObserverResult = { changedTargetAttrs: string[]; styleChanged: boolean; - contentChanged: boolean; -} +}; interface SeparateChangeThrough { - added?: DOMObserverResult[]; - removed?: DOMObserverResult[]; + added?: DOMContentObserverResult[]; + removed?: DOMContentObserverResult[]; } const targetChangesCountSlot: HTMLElement | null = document.querySelector('#targetChanges'); @@ -68,23 +68,31 @@ const hostSelector = '.host'; const ignorePrefix = 'ignore'; const attrs = ['id', 'class', 'style', 'open']; const contentChangeArr: Array<[string, string | ((elms: Node[]) => string)]> = [['img', 'load']]; -const targetElmObservations: DOMObserverResult[] = []; -const targetElmContentElmObservations: DOMObserverResult[] = []; +const domTargetObserverObservations: DOMTargetObserverResult[] = []; +const domContentObserverObservations: DOMContentObserverResult[] = []; -createDOMObserver( - document.querySelector('#target') as HTMLElement, - (changedTargetAttrs: string[], styleChanged: boolean, contentChanged: boolean) => { - targetElmObservations.push({ changedTargetAttrs, styleChanged, contentChanged }); +const targetDomObserver = createDOMObserver( + document.querySelector('#target')!, + false, + (changedTargetAttrs: string[], styleChanged: boolean) => { + should.ok(Array.isArray(changedTargetAttrs), 'The changedTargetAttrs parameter in a target dom observer must be a array.'); + should.equal(typeof styleChanged, 'boolean', 'The styleChanged parameter in a target dom observer must be a boolean.'); + + if (styleChanged && changedTargetAttrs.length === 0) { + should.ok(false, 'Style changing properties must always be inside the changedTargetAttrs array.'); + } + + domTargetObserverObservations.push({ changedTargetAttrs, styleChanged }); requestAnimationFrame(() => { if (targetChangesCountSlot) { - targetChangesCountSlot.textContent = `${targetElmObservations.length}`; + targetChangesCountSlot.textContent = `${domTargetObserverObservations.length}`; } }); }, { _styleChangingAttributes: attrs, _attributes: attrs.concat(['data-target']), - _ignoreTargetAttrChange: (target, attrName, oldValue, newValue) => { + _ignoreTargetChange: (target, attrName, oldValue, newValue) => { if (attrName === 'class' && oldValue && newValue) { const diff = diffClass(oldValue, newValue); const ignore = diff.length === 1 && diff[0].startsWith(ignorePrefix); @@ -92,20 +100,35 @@ createDOMObserver( } return false; }, + // @ts-ignore + _ignoreContentChange: () => { + // if param: isContentObserver = false, this function should never be called. + should.ok(false, 'A target dom observer must not call the _ignoreContentChange method.'); + return true; + }, + // @ts-ignore + _ignoreNestedTargetChange: () => { + // if param: isContentObserver = false, this function should never be called. + should.ok(false, 'A target dom observer must not call the _ignoreNestedTargetChange method.'); + return true; + }, } ); -const { _updateEventContentChange } = createDOMObserver( - document.querySelector('#target .content') as HTMLElement, - (changedTargetAttrs: string[], styleChanged: boolean, contentChanged: boolean) => { - targetElmContentElmObservations.push({ changedTargetAttrs, styleChanged, contentChanged }); + +const contentDomObserver = createDOMObserver( + document.querySelector('#target .content')!, + true, + (contentChanged: boolean) => { + should.equal(typeof contentChanged, 'boolean', 'The contentChanged parameter in a content dom observer must be a boolean.'); + + domContentObserverObservations.push(contentChanged); requestAnimationFrame(() => { if (contentChangesCountSlot) { - contentChangesCountSlot.textContent = `${targetElmContentElmObservations.length}`; + contentChangesCountSlot.textContent = `${domContentObserverObservations.length}`; } }); }, { - _observeContent: true, _styleChangingAttributes: attrs, _attributes: attrs, _eventContentChange: contentChangeArr, @@ -114,7 +137,7 @@ const { _updateEventContentChange } = createDOMObserver( const { target, attributeName } = mutation; return isNestedTarget ? false : attributeName ? liesBetween(target as Element, hostSelector, '.content') : false; }, - _ignoreTargetAttrChange: (target, attrName, oldValue, newValue) => { + _ignoreNestedTargetChange: (target, attrName, oldValue, newValue) => { if (attrName === 'class' && oldValue && newValue) { const diff = diffClass(oldValue, newValue); const ignore = diff.length === 1 && diff[0].startsWith(ignorePrefix); @@ -122,15 +145,23 @@ const { _updateEventContentChange } = createDOMObserver( } return false; }, + // @ts-ignore + _ignoreTargetChange: () => { + // if param: isContentObserver = true, this function should never be called. + should.ok(false, 'A content dom observer must not call the _ignoreTargetChange method.'); + return true; + }, } ); -const getTotalObservations = () => targetElmObservations.length + targetElmContentElmObservations.length; +const getTotalObservations = () => domTargetObserverObservations.length + domContentObserverObservations.length; const getLast = (arr: T[], indexFromLast = 0): T => arr[arr.length - 1 - indexFromLast] || ({} as T); -const changedThrough = (observationLists?: Array | DOMObserverResult[]) => { +const changedThrough = ( + observationLists?: Array | ChangeThrough[] +) => { interface Stat { total: number; - lists: Array<[DOMObserverResult[], number]>; + lists: Array<[ChangeThrough[], number]>; } const noObservationLists = observationLists === undefined; let before: Stat; @@ -139,13 +170,13 @@ const changedThrough = (observationLists?: Array | DOMObser observationLists = []; } if (isArray(observationLists) && !isArray(observationLists[0])) { - observationLists = [observationLists] as Array; + observationLists = [observationLists] as Array; } const getStats = (): Stat => { return { total: getTotalObservations(), - lists: (observationLists as Array).map((list) => [list, list.length]), + lists: (observationLists as Array).map((list) => [list, list.length]), }; }; @@ -156,7 +187,7 @@ const changedThrough = (observationLists?: Array | DOMObser after: () => { after = getStats(); }, - compare: (comparisonTableOrNumber: number | Map = 0) => { + compare: (comparisonTableOrNumber: number | Map = 0) => { let totalDiff = 0; if (isNumber(comparisonTableOrNumber) || noObservationLists) { before.lists.forEach((_, index) => { @@ -164,7 +195,11 @@ const changedThrough = (observationLists?: Array | DOMObser const [, afterCount] = after.lists[index]; totalDiff += afterCount - beforeCount; - should(afterCount).equal(beforeCount + (noObservationLists ? 0 : (comparisonTableOrNumber as number))); + should.equal( + afterCount, + beforeCount + (noObservationLists ? 0 : (comparisonTableOrNumber as number)), + 'Before and after changes for a certain observer are correct. (number)' + ); }); } else { before.lists.forEach((_, index) => { @@ -172,10 +207,14 @@ const changedThrough = (observationLists?: Array | DOMObser const [, afterCount] = after.lists[index]; totalDiff += afterCount - beforeCount; - should(afterCount).equal(beforeCount + (comparisonTableOrNumber.get(list) || 0)); + should.equal( + afterCount, + beforeCount + (comparisonTableOrNumber.get(list) || 0), + 'Before and after changes for a certain observer are correct. (Map)' + ); }); } - should(after.total).equal(before.total + totalDiff); + should.equal(after.total, before.total + totalDiff, 'Total changes are correct.'); }, }; }; @@ -188,10 +227,10 @@ const attrChangeListener = (attrChangeTarget: HTMLElement | null) => isClass && target.classList.add('something'); !isClass && target.setAttribute(selectedValue, 'something'); }); -const iterateAttrChange = async ( +const iterateAttrChange = async ( select: HTMLSelectElement | null, - changeThrough?: DOMObserverResult[], - checkChange?: (observation: DOMObserverResult, selected: string) => any + changeThrough?: ChangeThrough[], + checkChange?: (observation: ChangeThrough, selected: string) => any ) => { const { before, after, compare } = changedThrough(changeThrough); @@ -214,10 +253,10 @@ const iterateAttrChange = async ( }, }); }; -const addRemoveElementsTest = async (slot: Element | null, changeThrough?: DOMObserverResult[] | SeparateChangeThrough) => { +const addRemoveElementsTest = async (slot: Element | null, changeThrough?: DOMContentObserverResult[] | SeparateChangeThrough) => { if (slot) { - let addChangeThrough: DOMObserverResult[] | undefined = changeThrough as DOMObserverResult[] | undefined; - let removeChangeThrough: DOMObserverResult[] | undefined = changeThrough as DOMObserverResult[] | undefined; + let addChangeThrough: DOMContentObserverResult[] | undefined = changeThrough as DOMContentObserverResult[] | undefined; + let removeChangeThrough: DOMContentObserverResult[] | undefined = changeThrough as DOMContentObserverResult[] | undefined; if (changeThrough && !isArray(changeThrough)) { addChangeThrough = (changeThrough as SeparateChangeThrough).added; removeChangeThrough = (changeThrough as SeparateChangeThrough).removed; @@ -236,11 +275,9 @@ const addRemoveElementsTest = async (slot: Element | null, changeThrough?: DOMOb }); if (addChangeThrough) { - const { contentChanged, styleChanged, changedTargetAttrs } = getLast(addChangeThrough); + const contentChanged = getLast(addChangeThrough); await waitForOrFailTest(() => { - should(contentChanged).equal(true); - should(styleChanged).equal(false); - should(changedTargetAttrs.length).equal(0); + should.equal(contentChanged, true, 'Adding an content element must result in a content change.'); }); } }; @@ -259,10 +296,8 @@ const addRemoveElementsTest = async (slot: Element | null, changeThrough?: DOMOb compare(1); if (removeChangeThrough) { - const { changedTargetAttrs, styleChanged, contentChanged } = getLast(removeChangeThrough); - should(changedTargetAttrs.length).equal(0); - should(styleChanged).equal(false); - should(contentChanged).equal(true); + const contentChanged = getLast(removeChangeThrough); + should.equal(contentChanged, true, 'Removing an content element must result in a content change.'); } }); } @@ -277,7 +312,7 @@ const addRemoveElementsTest = async (slot: Element | null, changeThrough?: DOMOb await removeElm(); } }; -const triggerSummaryElemet = async (summaryElm: HTMLElement | null, changeThrough?: DOMObserverResult[]) => { +const triggerSummaryElemet = async (summaryElm: HTMLElement | null, changeThrough?: DOMContentObserverResult[]) => { // onyl do if summary is working (IE. exception) if (summaryElm && (summaryElm.nextElementSibling as HTMLElement)?.offsetHeight === 0) { const click = async () => { @@ -302,17 +337,17 @@ const addRemoveTargetElmsFn = async () => { await addRemoveElementsTest(targetElmsSlot); }; const addRemoveTargetContentElmsFn = async () => { - await addRemoveElementsTest(targetContentElmsSlot, targetElmContentElmObservations); + await addRemoveElementsTest(targetContentElmsSlot, domContentObserverObservations); }; const addRemoveTargetContentBetweenElmsFn = async () => { - await addRemoveElementsTest(targetContentBetweenElmsSlot, targetElmContentElmObservations); + await addRemoveElementsTest(targetContentBetweenElmsSlot, domContentObserverObservations); }; const addRemoveImgElmsFn = async () => { const add = async () => { const img = new Image(1, 1); img.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; - const { before, after, compare } = changedThrough(targetElmContentElmObservations); + const { before, after, compare } = changedThrough(domContentObserverObservations); const imgHolder = createDiv('img'); appendChildren(imgHolder, img); @@ -323,15 +358,11 @@ const addRemoveImgElmsFn = async () => { after(); compare(2); - const mutationObserverObservation = getLast(targetElmContentElmObservations, 1); - should(mutationObserverObservation.contentChanged).equal(true); - should(mutationObserverObservation.styleChanged).equal(false); - should(mutationObserverObservation.changedTargetAttrs.length).equal(0); + const previousContentChanged = getLast(domContentObserverObservations, 1); + should.equal(previousContentChanged, true, 'Adding an content image must result in a content change.'); - const eventObservation = getLast(targetElmContentElmObservations); - should(eventObservation.contentChanged).equal(true); - should(eventObservation.styleChanged).equal(false); - should(eventObservation.changedTargetAttrs.length).equal(0); + const lastContentChanged = getLast(domContentObserverObservations); + should.equal(lastContentChanged, true, 'The images load event must result in a content change.'); }); }; @@ -341,7 +372,7 @@ const addRemoveImgElmsFn = async () => { // test event content change debounce const addMultiple = async () => { - const { before, after, compare } = changedThrough(targetElmContentElmObservations); + const { before, after, compare } = changedThrough(domContentObserverObservations); const addMultipleItem = () => { const img = new Image(1, 1); img.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; @@ -362,15 +393,11 @@ const addRemoveImgElmsFn = async () => { after(); compare(2); - const mutationObserverObservation = getLast(targetElmContentElmObservations, 1); - should(mutationObserverObservation.contentChanged).equal(true); - should(mutationObserverObservation.styleChanged).equal(false); - should(mutationObserverObservation.changedTargetAttrs.length).equal(0); + const previousContentChanged = getLast(domContentObserverObservations, 1); + should.equal(previousContentChanged, true, 'Adding mutliple content images must result in a single content change. (debounced)'); - const eventObservation = getLast(targetElmContentElmObservations); - should(eventObservation.contentChanged).equal(true); - should(eventObservation.styleChanged).equal(false); - should(eventObservation.changedTargetAttrs.length).equal(0); + const lastContentChanged = getLast(domContentObserverObservations); + should.equal(lastContentChanged, true, 'Multiple images load events must result in a single cintent change. (debounced)'); }); }; @@ -384,7 +411,7 @@ const addRemoveTransitionElmsFn = async () => { const startTransition = async (elm: Element, expectTransitionEndContentChange: boolean) => { await timeout(50); // time for css to apply class a bit later to trigger transition - const { before: beforeTransition, after: afterTransition, compare: compareTransition } = changedThrough(targetElmContentElmObservations); + const { before: beforeTransition, after: afterTransition, compare: compareTransition } = changedThrough(domContentObserverObservations); beforeTransition(); removeClass(elm, 'resetTransition'); // IE... addClass(elm, 'active'); @@ -398,10 +425,8 @@ const addRemoveTransitionElmsFn = async () => { afterTransition(); compareTransition(expectTransitionEndContentChange ? 2 : 1); // 2 because 1: added class mutation and 2: transition end event - const eventObservation = getLast(targetElmContentElmObservations); - should(eventObservation.contentChanged).equal(true); - should(eventObservation.styleChanged).equal(false); - should(eventObservation.changedTargetAttrs.length).equal(0); + const contentChanged = getLast(domContentObserverObservations); + should.equal(contentChanged, true, 'The transitionend event must trigger a event content change.'); resolve(1); }); }, @@ -414,7 +439,7 @@ const addRemoveTransitionElmsFn = async () => { }; const add = async (expectTransitionEndContentChange: boolean) => { const elm = createDiv(`transition ${expectTransitionEndContentChange ? 'highlight' : ''}`); - const { before, after, compare } = changedThrough(targetElmContentElmObservations); + const { before, after, compare } = changedThrough(domContentObserverObservations); before(); @@ -424,14 +449,12 @@ const addRemoveTransitionElmsFn = async () => { after(); compare(1); - const eventObservation = getLast(targetElmContentElmObservations); - should(eventObservation.contentChanged).equal(true); - should(eventObservation.styleChanged).equal(false); - should(eventObservation.changedTargetAttrs.length).equal(0); + const contentChanged = getLast(domContentObserverObservations); + should.equal(contentChanged, true, 'Adding an content element (transition) must result in a content change.'); }); await startTransition(elm, expectTransitionEndContentChange && true); - _updateEventContentChange(contentChangeArr); + contentDomObserver._updateEventContentChange(contentChangeArr); await startTransition(elm, expectTransitionEndContentChange && false); removeElements(elm); @@ -441,13 +464,13 @@ const addRemoveTransitionElmsFn = async () => { await add(false); - _updateEventContentChange( + contentDomObserver._updateEventContentChange( contentChangeArr.concat([ [ '.transition', (elms) => { elms.forEach((elm) => { - should(hasClass(elm as Element, 'transition')).equal(true); + should.equal(hasClass(elm as Element, 'transition'), true, 'Every checked element must match the correpsonding selector.'); // in this case "".transition" }); return 'transitionend'; }, @@ -458,7 +481,10 @@ const addRemoveTransitionElmsFn = async () => { await add(true); }; const ignoreTargetChangeFn = async () => { - const check = async (target: Element | null, changeThrough: DOMObserverResult[]) => { + const check = async ( + target: Element | null, + changeThrough: ChangeThrough[] + ) => { const { before, after, compare } = changedThrough(changeThrough); before(); @@ -473,24 +499,25 @@ const ignoreTargetChangeFn = async () => { }); }; - await check(targetElm, targetElmObservations); - await check(targetElmContentElm, targetElmContentElmObservations); + await check(targetElm, domTargetObserverObservations); + await check(targetElmContentElm, domContentObserverObservations); }; 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(setTargetAttr, domTargetObserverObservations, (observation, selected) => { + const { changedTargetAttrs, styleChanged } = observation; + should.equal( + changedTargetAttrs.includes(selected), + true, + 'A attribute change on the target element for a DOMTargetObserver must be inside the changedTargetAttrs array.' + ); + should.equal(styleChanged, true, 'A style changing attribute on the target element for a DOMTargetObserver must set styleChanged to true.'); }); await iterateAttrChange(setFilteredTargetAttr); }; 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(setContentAttr, domContentObserverObservations, (observation) => { + const contentChanged = observation; + should.equal(contentChanged, true, 'A attribute change inside the content must trigger a content change for a DOMContentObserver.'); }); await iterateAttrChange(setFilteredContentAttr); }; @@ -499,16 +526,14 @@ const iterateContentBetweenAttrChange = async () => { 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(setContentHostElmAttr, domContentObserverObservations, (observation) => { + const contentChanged = observation; + should.equal(contentChanged, true, 'A attribute change for a nested target must trigger a content change for a DOMContentObserver.'); }); await iterateAttrChange(setFilteredContentHostElmAttr); }; const triggerContentSummaryChange = async () => { - await triggerSummaryElemet(summaryContent, targetElmContentElmObservations); + await triggerSummaryElemet(summaryContent, domContentObserverObservations); }; const triggerBetweenSummaryChange = async () => { await triggerSummaryElemet(summaryBetween); @@ -550,6 +575,16 @@ const start = async () => { await addRemoveImgElmsFn(); setTestResult(true); + + targetDomObserver._update(); + targetDomObserver._destroy(); + targetDomObserver._update(); + + contentDomObserver._updateEventContentChange([]); + contentDomObserver._update(); + contentDomObserver._destroy(); + contentDomObserver._updateEventContentChange([]); + contentDomObserver._update(); }; startBtn?.addEventListener('click', start); diff --git a/packages/overlayscrollbars/tests/browser/observers/sizeObserver/index.browser.ts b/packages/overlayscrollbars/tests/browser/observers/sizeObserver/index.browser.ts index 98ffee7..9c6e73a 100644 --- a/packages/overlayscrollbars/tests/browser/observers/sizeObserver/index.browser.ts +++ b/packages/overlayscrollbars/tests/browser/observers/sizeObserver/index.browser.ts @@ -90,21 +90,27 @@ const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any) // no overflow if not needed if (targetElm && newContentSize.w > 0) { - should.ok(observerElm.getBoundingClientRect().right <= targetElm.getBoundingClientRect().right); + should.ok( + observerElm.getBoundingClientRect().right <= targetElm.getBoundingClientRect().right, + 'Generated observer element inst overflowing target element. (width)' + ); } if (targetElm && newContentSize.h > 0) { - should.ok(observerElm.getBoundingClientRect().bottom <= targetElm.getBoundingClientRect().bottom); + should.ok( + observerElm.getBoundingClientRect().bottom <= targetElm.getBoundingClientRect().bottom, + 'Generated observer element inst overflowing target element. (height)' + ); } if (dimensions && (offsetSizeChanged || contentSizeChanged || dirChanged)) { await waitForOrFailTest(() => { if (offsetSizeChanged || contentSizeChanged) { - should.equal(sizeIterations, currSizeIterations + 1); + should.equal(sizeIterations, currSizeIterations + 1, 'Size change was detected correctly.'); } if (dirChanged) { const expectedCacheValue = newDir === 'rtl'; - should.equal(directionIterations, currDirectionIterations + 1); - should.equal(sizeObserver._getCurrentCacheValues()._directionIsRTL._value, expectedCacheValue); + should.equal(directionIterations, currDirectionIterations + 1, 'Direction change was detected correctly.'); + should.equal(sizeObserver._getCurrentCacheValues()._directionIsRTL._value, expectedCacheValue, 'Direction cache value is correct.'); } }); } @@ -158,8 +164,8 @@ const start = async () => { console.log('init direction changes:', directionIterations); console.log('init size changes:', sizeIterations); - should.ok(directionIterations > 0); - should.ok(sizeIterations > 0); + should.ok(directionIterations > 0, 'Initial direction observations are fired.'); + should.ok(sizeIterations > 0, 'Initial size observations are fired.'); targetElm?.removeAttribute('style'); await iterateDisplay(); @@ -176,7 +182,7 @@ const start = async () => { }); sizeObserver._destroy(); - should.equal(targetElm?.children.length, preInitChildren); + should.equal(targetElm?.children.length, preInitChildren, 'Destruction removes all generated elements.'); setTestResult(true); }; diff --git a/packages/overlayscrollbars/tests/browser/observers/trinsicObserver/index.browser.ts b/packages/overlayscrollbars/tests/browser/observers/trinsicObserver/index.browser.ts index a039e7b..72790d2 100644 --- a/packages/overlayscrollbars/tests/browser/observers/trinsicObserver/index.browser.ts +++ b/packages/overlayscrollbars/tests/browser/observers/trinsicObserver/index.browser.ts @@ -65,9 +65,13 @@ const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any) await waitForOrFailTest(() => { if (trinsicHeightChanged) { - should.equal(heightIterations, currHeightIterations + 1); + should.equal(heightIterations, currHeightIterations + 1, 'Height intrinsic change has been detected correctly.'); } - should.equal(trinsicObserver._getCurrentCacheValues()._heightIntrinsic._value, newHeightIntrinsic); + should.equal( + trinsicObserver._getCurrentCacheValues()._heightIntrinsic._value, + newHeightIntrinsic, + 'Height intrinsic cache value is correct.' + ); }); }, afterEach, @@ -93,7 +97,7 @@ const changeWhileHidden = async () => { selectOption(displaySelect as HTMLSelectElement, 'displayBlock'); await waitForOrFailTest(() => { - should.equal(heightIntrinsic, false); + should.equal(heightIntrinsic, false, 'Trinsic sizing changes while hidden from intrinsic to extrinsic.'); }); }; @@ -107,7 +111,7 @@ const changeWhileHidden = async () => { selectOption(displaySelect as HTMLSelectElement, 'displayBlock'); await waitForOrFailTest(() => { - should.equal(heightIntrinsic, true); + should.equal(heightIntrinsic, true, 'Trinsic sizing changes while hidden from extrinsic to intrinsic.'); }); }; @@ -129,7 +133,7 @@ const start = async () => { await changeWhileHidden(); trinsicObserver._destroy(); - should.equal(targetElm?.children.length, preInitChildren); + should.equal(targetElm?.children.length, preInitChildren, 'After destruction all generated elements are removed.'); setTestResult(true); };