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 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][],
@@ -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 boolean> = ContentObserver extends true
? DOMContentObserverCallback
: DOMTargetObserverCallback;
export type DOMObserverOptions<ContentObserver extends boolean> = ContentObserver extends true ? DOMContentObserverOptions : DOMTargetObserverOptions;
export type DOMObserver<ContentObserver extends boolean> = 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<Node, string> | 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 = <ContentObserver extends boolean>(
target: HTMLElement,
callback: (targetChangedAttrs: string[], targetStyleChanged: boolean, contentChanged: boolean) => any,
options?: DOMObserverOptions
): DOMObserver => {
isContentObserver: ContentObserver,
callback: DOMObserverCallback<ContentObserver>,
options?: DOMObserverOptions<ContentObserver>
): DOMObserver<ContentObserver> => {
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<ContentObserver>;
};
@@ -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 = <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 {
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<DOMObserverResult[]> | DOMObser
observationLists = [];
}
if (isArray(observationLists) && !isArray(observationLists[0])) {
observationLists = [observationLists] as Array<DOMObserverResult[]>;
observationLists = [observationLists] as Array<ChangeThrough[]>;
}
const getStats = (): Stat => {
return {
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 = getStats();
},
compare: (comparisonTableOrNumber: number | Map<DOMObserverResult[], number> = 0) => {
compare: (comparisonTableOrNumber: number | Map<ChangeThrough[], number> = 0) => {
let totalDiff = 0;
if (isNumber(comparisonTableOrNumber) || noObservationLists) {
before.lists.forEach((_, index) => {
@@ -164,7 +195,11 @@ const changedThrough = (observationLists?: Array<DOMObserverResult[]> | 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<DOMObserverResult[]> | 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 <ChangeThrough extends DOMContentObserverResult | DOMTargetObserverResult>(
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 <ChangeThrough extends DOMContentObserverResult | DOMTargetObserverResult>(
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);
@@ -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);
};
@@ -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);
};