improve tests

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