improve pptr dev experience and write dom observer tests

This commit is contained in:
Rene
2021-01-10 17:54:41 +01:00
parent 388b0152b4
commit 9b47e9d76b
10 changed files with 656 additions and 347 deletions
@@ -1,9 +1,19 @@
import { each, indexOf, isString, MutationObserverConstructor, isEmptyArray, liesBetween } from 'support';
import { classNameHost, classNameContent } from 'classnames';
import { each, indexOf, isString, MutationObserverConstructor, isEmptyArray, on, off, attr, is, find } from 'support';
type StringNullUndefined = string | null | undefined;
export type DOMOvserverEventContentChangeResult = Array<[StringNullUndefined, StringNullUndefined] | null | undefined>; // [selector, eventname]
export type DOMOvserverEventContentChange = () => DOMOvserverEventContentChangeResult;
export type DOMObserverIgnoreContentChange = (
mutation: MutationRecord,
domObserverTarget: HTMLElement,
domObserverOptions: DOMObserverOptions | undefined
) => boolean | null | undefined;
export interface DOMObserverOptions {
_observeContent?: boolean;
_attributes?: string[];
_ignoreContentChange?: DOMObserverIgnoreContentChange;
_eventContentChange?: DOMOvserverEventContentChange;
}
export interface DOMObserver {
_disconnect: () => void;
@@ -12,76 +22,115 @@ export interface DOMObserver {
const styleChangingAttributes = ['id', 'class', 'style', 'open'];
const mutationObserverAttrsTextarea = ['wrap', 'cols', 'rows'];
const isUnknownMutation = (
attributeName: string | null,
type: MutationRecordType,
observeContent?: boolean,
target?: Node,
mutationTarget?: Node
) => {
const isAttributesType = type === 'attributes';
const targetIsMutationTarget = target === mutationTarget;
const styleChangingAttrChanged = indexOf(styleChangingAttributes, attributeName) > -1;
const contentChanged = observeContent && !isAttributesType;
const contentAttrChanged =
observeContent &&
isAttributesType &&
styleChangingAttrChanged &&
!targetIsMutationTarget &&
!liesBetween(mutationTarget as Element | undefined, `.${classNameHost}`, `.${classNameContent}`);
const targetAttrChanged = isAttributesType && styleChangingAttrChanged && targetIsMutationTarget && !observeContent;
return contentChanged || contentAttrChanged || targetAttrChanged;
};
const getAttributeChanged = (mutationTarget: Node, attributeName: string, oldValue: string | null): boolean =>
oldValue !== attr(mutationTarget as HTMLElement, attributeName);
export const createDOMObserver = (
target: HTMLElement,
callback: (changedTargetAttrs: string[], styleChanged: boolean, contentChanged: boolean) => any,
callback: (targetChangedAttrs: string[], targetStyleChanged: boolean, contentChanged: boolean) => any,
options?: DOMObserverOptions
): DOMObserver => {
const { _observeContent, _attributes } = options || {};
let isConnected = false;
const { _observeContent, _attributes, _ignoreContentChange, _eventContentChange } = options || {};
const eventContentChangeCallback = () => {
if (isConnected) {
callback([], false, true);
}
};
const refreshEventContentChange = (getElements: (selector: string) => Node[]) => {
if (_eventContentChange) {
const eventContentChanges = _eventContentChange();
const eventElmList = eventContentChanges.reduce<Array<[string, Node[]]>>((arr, item) => {
if (item) {
const selector = item[0];
const eventName = item[1];
const elements = eventName && selector && getElements(selector);
if (elements) {
arr.push([eventName!, elements]);
}
}
return arr;
}, []);
each(eventElmList, (item) => {
const eventName = item[0];
const elements = item[1];
each(elements, (elm) => {
off(elm, eventName, eventContentChangeCallback);
on(elm, eventName, eventContentChangeCallback);
});
});
}
};
// MutationObserver
const observedAttributes = (_attributes || []).concat(_observeContent ? styleChangingAttributes : mutationObserverAttrsTextarea);
const observedAttributes = (_attributes || []).concat(styleChangingAttributes); // TODO: observer textarea attrs if textarea
const observerCallback = (mutations: MutationRecord[]) => {
let styleChanged = false;
const targetChangedAttrs: string[] = [];
const totalAddedNodes: Node[] = [];
let targetStyleChanged = false;
let contentChanged = false;
const changedTargetAttrs: string[] = [];
let childListChanged = false;
each(mutations, (mutation) => {
const { attributeName, target: mutationTarget, type } = mutation;
const { attributeName, target: mutationTarget, type, oldValue, addedNodes } = mutation;
const isAttributesType = type === 'attributes';
const isChildListType = type === 'childList';
const targetIsMutationTarget = target === mutationTarget;
const attributeChanged = isAttributesType && isString(attributeName) && getAttributeChanged(mutationTarget, attributeName!, oldValue);
const targetAttrChanged = attributeChanged && targetIsMutationTarget && !_observeContent;
const styleChangingAttrChanged = indexOf(styleChangingAttributes, attributeName) > -1 && attributeChanged;
styleChanged = styleChanged || isUnknownMutation(attributeName, type);
targetStyleChanged = targetStyleChanged || (targetAttrChanged && styleChangingAttrChanged);
if (_observeContent) {
contentChanged = contentChanged || isUnknownMutation(attributeName, type, true, target, mutationTarget);
if (targetAttrChanged) {
targetChangedAttrs.push(attributeName!);
}
if (isString(attributeName) && target === mutationTarget) {
changedTargetAttrs.push(attributeName);
if (_observeContent) {
const notOnlyAttrChanged = !isAttributesType;
const contentAttrChanged = isAttributesType && styleChangingAttrChanged && !targetIsMutationTarget;
const contentFinalChanged =
(notOnlyAttrChanged || contentAttrChanged) && (_ignoreContentChange ? !_ignoreContentChange(mutation, target, options) : _observeContent);
each(addedNodes, (node) => {
totalAddedNodes.push(node);
});
contentChanged = contentChanged || contentFinalChanged;
childListChanged = childListChanged || isChildListType;
}
});
if (!isEmptyArray(changedTargetAttrs) || styleChanged || contentChanged) {
callback(changedTargetAttrs, styleChanged, contentChanged);
if (childListChanged && !isEmptyArray(totalAddedNodes)) {
refreshEventContentChange((selector) => totalAddedNodes.filter((node) => is(node as Element, selector)));
}
if (!isEmptyArray(targetChangedAttrs) || targetStyleChanged || contentChanged) {
callback(targetChangedAttrs, targetStyleChanged, contentChanged);
}
};
const mutationObserver: MutationObserver = new MutationObserverConstructor!(observerCallback);
const connect = () => {
mutationObserver.observe(target, {
attributes: true,
attributeOldValue: true,
subtree: _observeContent,
childList: _observeContent,
characterData: _observeContent,
attributeFilter: observedAttributes,
});
};
mutationObserver.observe(target, {
attributes: true,
attributeOldValue: true,
attributeFilter: observedAttributes,
subtree: _observeContent,
childList: _observeContent,
characterData: _observeContent,
});
connect();
isConnected = true;
if (_observeContent) {
refreshEventContentChange((selector) => find(selector, target) as Node[]);
}
return {
_disconnect: mutationObserver.disconnect,
_disconnect: () => {
mutationObserver.disconnect();
isConnected = false;
},
_update: () => {
observerCallback(mutationObserver.takeRecords());
},
@@ -10,7 +10,7 @@ const elmPrototype = Element.prototype;
* @param selector The selector which has to be searched by.
* @param elm The element from which the search shall be outgoing.
*/
const find = (selector: string, elm?: InputElementType): ReadonlyArray<Element> => {
const find = (selector: string, elm?: InputElementType): Element[] => {
const arr: Array<Element> = [];
each((elm || document).querySelectorAll(selector), (e: Element) => {
@@ -38,7 +38,7 @@ const is = (elm: InputElementType, selector: string): boolean => {
// eslint-disable-next-line
// @ts-ignore
const fn = elmPrototype.matches || elmPrototype.msMatchesSelector;
return fn.call(elm, selector);
return fn && fn.call(elm, selector);
}
return false;
};
@@ -101,8 +101,8 @@ const closest = (elm: InputElementType, selector: string): OutputElementType =>
* @param deepBoundarySelector The deep boundary selector.
*/
const liesBetween = (elm: InputElementType, highBoundarySelector: string, deepBoundarySelector: string): boolean => {
const closestHighBoundaryElm = closest(elm, highBoundarySelector);
const closestDeepBoundaryElm = findFirst(deepBoundarySelector, closestHighBoundaryElm);
const closestHighBoundaryElm = elm && closest(elm, highBoundarySelector);
const closestDeepBoundaryElm = elm && findFirst(deepBoundarySelector, closestHighBoundaryElm);
return closestHighBoundaryElm && closestDeepBoundaryElm
? closestHighBoundaryElm === elm ||
@@ -1,185 +1,335 @@
import 'overlayscrollbars.scss';
import './index.scss';
import should from 'should';
import { waitFor } from '@testing-library/dom';
import { generateSelectCallback, iterateSelect } from '@/testing-browser/Select';
import { setTestResult } from '@/testing-browser/TestResult';
import { hasDimensions, offsetSize, WH, style } from 'support';
import { timeout } from '@/testing-browser/timeout';
import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult';
import { appendChildren, createDiv, removeElements, children, isArray, isNumber, liesBetween, hasClass } from 'support';
import { createSizeObserver } from 'observers/sizeObserver';
import { createDOMObserver } from 'observers/domObserver';
let sizeIterations = 0;
let directionIterations = 0;
const contentBox = (elm: HTMLElement | null): WH<number> => {
if (elm) {
const computedStyle = window.getComputedStyle(elm);
return {
w: elm.clientWidth - (parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight)),
h: elm.clientHeight - (parseFloat(computedStyle.paddingTop) + parseFloat(computedStyle.paddingBottom)),
};
}
interface DOMObserverResult {
changedTargetAttrs: string[];
styleChanged: boolean;
contentChanged: boolean;
}
interface SeparateChangeThrough {
added?: DOMObserverResult[];
removed?: DOMObserverResult[];
}
return { w: 0, h: 0 };
};
const targetChangesCountSlot: HTMLElement | null = document.querySelector('#targetChanges');
const contentChangesCountSlot: HTMLElement | null = document.querySelector('#contentChanges');
const targetElm: HTMLElement | null = document.querySelector('#target');
const contentElmAttrChange: HTMLElement | null = document.querySelector('#target .content-nest');
const contentBetweenElmAttrChange: HTMLElement | null = document.querySelector('#content-host .padding-nest-item');
const contentHostElmAttrChange: HTMLElement | null = document.querySelector('#content-nest-item-host');
const targetElmsSlot = document.querySelector('#target .host-nest-item');
const targetContentElmsSlot = document.querySelector('#target .content .content-nest');
const targetContentBetweenElmsSlot = document.querySelector('#content-host');
const addRemoveTargetElms: HTMLButtonElement | null = document.querySelector('#addRemoveTargetElms');
const addRemoveTargetContentElms: HTMLButtonElement | null = document.querySelector('#addRemoveTargetContentElms');
const addRemoveTargetContentBetweenElms: HTMLButtonElement | null = document.querySelector('#addRemoveTargetContentBetweenElms');
const setTargetAttr: HTMLSelectElement | null = document.querySelector('#setTargetAttr');
const setFilteredTargetAttr: HTMLSelectElement | null = document.querySelector('#setFilteredTargetAttr');
const setContentAttr: HTMLSelectElement | null = document.querySelector('#setContentAttr');
const setFilteredContentAttr: HTMLSelectElement | null = document.querySelector('#setFilteredContentAttr');
const setContentBetweenAttr: HTMLSelectElement | null = document.querySelector('#setContentBetweenAttr');
const setFilteredContentBetweenAttr: HTMLSelectElement | null = document.querySelector('#setFilteredContentBetweenAttr');
const setContentHostElmAttr: HTMLSelectElement | null = document.querySelector('#setContentHostElmAttr');
const setFilteredContentHostElmAttr: HTMLSelectElement | null = document.querySelector('#setFilteredContentHostElmAttr');
const summaryContent: HTMLElement | null = document.querySelector('#summary-content');
const summaryBetween: HTMLElement | null = document.querySelector('#summary-between');
const targetElm = document.querySelector('#target');
const heightSelect: HTMLSelectElement | null = document.querySelector('#height');
const widthSelect: HTMLSelectElement | null = document.querySelector('#width');
const paddingSelect: HTMLSelectElement | null = document.querySelector('#padding');
const borderSelect: HTMLSelectElement | null = document.querySelector('#border');
const boxSizingSelect: HTMLSelectElement | null = document.querySelector('#boxSizing');
const displaySelect: HTMLSelectElement | null = document.querySelector('#display');
const directionSelect: HTMLSelectElement | null = document.querySelector('#direction');
const startBtn: HTMLButtonElement | null = document.querySelector('#start');
const resizesSlot: HTMLButtonElement | null = document.querySelector('#resizes');
const selectCallback = generateSelectCallback(targetElm as HTMLElement);
const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any) => {
interface IterateSelect {
currSizeIterations: number;
currDirectionIterations: number;
currOffsetSize: WH<number>;
currContentSize: WH<number>;
currDir: string;
const targetElmObservations: DOMObserverResult[] = [];
const targetElmContentElmObservations: DOMObserverResult[] = [];
const getTotalObservations = () => targetElmObservations.length + targetElmContentElmObservations.length;
const getLast = <T>(arr: T[]): T => arr[arr.length - 1] || ({} as T);
const changedThrough = (observationLists?: Array<DOMObserverResult[]> | DOMObserverResult[]) => {
interface Stat {
total: number;
lists: Array<[DOMObserverResult[], number]>;
}
const noObservationLists = observationLists === undefined;
let before: Stat;
let after: Stat;
if (noObservationLists) {
observationLists = [];
}
if (isArray(observationLists) && !isArray(observationLists[0])) {
observationLists = [observationLists] as Array<DOMObserverResult[]>;
}
await iterateSelect<IterateSelect>(select, {
const getStats = (): Stat => {
return {
total: getTotalObservations(),
lists: (observationLists as Array<DOMObserverResult[]>).map((list) => [list, list.length]),
};
};
return {
before: () => {
before = getStats();
},
after: () => {
after = getStats();
},
compare: (comparisonTableOrNumber: number | Map<DOMObserverResult[], number> = 0) => {
let totalDiff = 0;
if (isNumber(comparisonTableOrNumber) || noObservationLists) {
before.lists.forEach((_, index) => {
const [, beforeCount] = before.lists[index];
const [, afterCount] = after.lists[index];
totalDiff += afterCount - beforeCount;
should(afterCount).equal(beforeCount + (noObservationLists ? 0 : (comparisonTableOrNumber as number)));
});
} else {
before.lists.forEach((_, index) => {
const [list, beforeCount] = before.lists[index];
const [, afterCount] = after.lists[index];
totalDiff += afterCount - beforeCount;
should(afterCount).equal(beforeCount + (comparisonTableOrNumber.get(list) || 0));
});
}
should(after.total).equal(before.total + totalDiff);
},
};
};
const attrChangeListener = (attrChangeTarget: HTMLElement | null) =>
generateSelectCallback(attrChangeTarget, (target, possibleValues, selectedValue) => {
const isClass = selectedValue === 'class';
target.classList.remove('something');
possibleValues.forEach((val) => val !== 'class' && target.removeAttribute(val));
isClass && target.classList.add('something');
!isClass && target.setAttribute(selectedValue, 'something');
});
const iterateAttrChange = async (
select: HTMLSelectElement | null,
changeThrough?: DOMObserverResult[],
checkChange?: (observation: DOMObserverResult, selected: string) => any
) => {
const { before, after, compare } = changedThrough(changeThrough);
await iterateSelect<unknown>(select, {
beforeEach() {
const currSizeIterations = sizeIterations;
const currDirectionIterations = directionIterations;
const currOffsetSize = offsetSize(targetElm as HTMLElement);
const currContentSize = contentBox(targetElm as HTMLElement);
const currDir = style(targetElm as HTMLElement, 'direction');
return {
currSizeIterations,
currDirectionIterations,
currOffsetSize,
currContentSize,
currDir,
};
before();
},
async check({ currSizeIterations, currDirectionIterations, currOffsetSize, currContentSize, currDir }) {
const newOffsetSize = offsetSize(targetElm as HTMLElement);
const newContentSize = contentBox(targetElm as HTMLElement);
const newDir = style(targetElm as HTMLElement, 'direction');
const offsetSizeChanged = currOffsetSize.w !== newOffsetSize.w || currOffsetSize.h !== newOffsetSize.h;
const contentSizeChanged = currContentSize.w !== newContentSize.w || currContentSize.h !== newContentSize.h;
const dirChanged = currDir !== newDir;
const dimensions = hasDimensions(targetElm as HTMLElement);
const observerElm = targetElm?.firstElementChild as HTMLElement;
async check(_, selected) {
await waitForOrFailTest(async () => {
after();
// no overflow if not needed
if (targetElm && newContentSize.w > 0) {
should.ok(observerElm.getBoundingClientRect().right <= targetElm.getBoundingClientRect().right);
}
if (targetElm && newContentSize.h > 0) {
should.ok(observerElm.getBoundingClientRect().bottom <= targetElm.getBoundingClientRect().bottom);
}
if (dimensions && (offsetSizeChanged || contentSizeChanged || dirChanged)) {
await waitFor(
() => {
if (offsetSizeChanged || contentSizeChanged) {
should.equal(sizeIterations, currSizeIterations + 1);
}
if (dirChanged) {
should.equal(directionIterations, currDirectionIterations + 1);
}
},
{
onTimeout(error): Error {
setTestResult(false);
return error;
},
}
);
}
if (changeThrough) {
compare(1);
checkChange && checkChange(getLast(changeThrough), selected);
} else {
await timeout(250);
compare(0);
}
});
},
afterEach,
});
};
const addRemoveElementsTest = async (slot: Element | null, changeThrough?: DOMObserverResult[] | SeparateChangeThrough) => {
if (slot) {
let addChangeThrough: DOMObserverResult[] | undefined = changeThrough as DOMObserverResult[] | undefined;
let removeChangeThrough: DOMObserverResult[] | undefined = changeThrough as DOMObserverResult[] | undefined;
if (changeThrough && !isArray(changeThrough)) {
addChangeThrough = (changeThrough as SeparateChangeThrough).added;
removeChangeThrough = (changeThrough as SeparateChangeThrough).removed;
}
heightSelect?.addEventListener('change', selectCallback);
widthSelect?.addEventListener('change', selectCallback);
paddingSelect?.addEventListener('change', selectCallback);
borderSelect?.addEventListener('change', selectCallback);
boxSizingSelect?.addEventListener('change', selectCallback);
displaySelect?.addEventListener('change', selectCallback);
directionSelect?.addEventListener('change', selectCallback);
const addElm = async () => {
const { before, after, compare } = changedThrough(addChangeThrough);
selectCallback(heightSelect);
selectCallback(widthSelect);
selectCallback(paddingSelect);
selectCallback(borderSelect);
selectCallback(boxSizingSelect);
selectCallback(displaySelect);
selectCallback(directionSelect);
before();
appendChildren(slot, createDiv('addedElm'));
await timeout(250);
after();
const iteratePadding = async (afterEach?: () => any) => {
await iterate(paddingSelect, afterEach);
await waitForOrFailTest(() => {
compare(1);
});
if (addChangeThrough) {
const { contentChanged, styleChanged, changedTargetAttrs } = getLast(addChangeThrough);
await waitForOrFailTest(() => {
should(contentChanged).equal(true);
should(styleChanged).equal(false);
should(changedTargetAttrs.length).equal(0);
});
}
};
const removeElm = async () => {
const removeItem = children(slot, '.addedElm')[0];
const { before, after, compare } = changedThrough(removeChangeThrough);
if (removeItem) {
before();
removeElements(removeItem);
await timeout(250);
await waitForOrFailTest(() => {
after();
compare(1);
if (removeChangeThrough) {
const { changedTargetAttrs, styleChanged, contentChanged } = getLast(removeChangeThrough);
should(changedTargetAttrs.length).equal(0);
should(styleChanged).equal(false);
should(contentChanged).equal(true);
}
});
}
};
await addElm();
await addElm();
await addElm();
await removeElm();
await removeElm();
await removeElm();
}
};
const iterateBorder = async (afterEach?: () => any) => {
await iterate(borderSelect, afterEach);
const triggerSummaryElemet = async (summaryElm: HTMLElement | null, changeThrough?: DOMObserverResult[]) => {
// onyl do if summary is working (IE. exception)
if (summaryElm && (summaryElm.nextElementSibling as HTMLElement)?.offsetHeight === 0) {
const click = async () => {
const { before, after, compare } = changedThrough(changeThrough);
before();
summaryElm?.click();
await timeout(250);
after();
await waitForOrFailTest(() => {
compare(1);
});
};
await click();
await click();
}
};
const iterateHeight = async (afterEach?: () => any) => {
await iterate(heightSelect, afterEach);
const addRemoveTargetElmsFn = async () => {
await addRemoveElementsTest(targetElmsSlot);
};
const iterateWidth = async (afterEach?: () => any) => {
await iterate(widthSelect, afterEach);
const addRemoveTargetContentElmsFn = async () => {
await addRemoveElementsTest(targetContentElmsSlot, targetElmContentElmObservations);
};
const iterateBoxSizing = async (afterEach?: () => any) => {
await iterate(boxSizingSelect, afterEach);
const addRemoveTargetContentBetweenElmsFn = async () => {
await addRemoveElementsTest(targetContentBetweenElmsSlot, targetElmContentElmObservations);
};
const iterateDisplay = async (afterEach?: () => any) => {
await iterate(displaySelect, afterEach);
const iterateTargetAttrChange = async () => {
await iterateAttrChange(setTargetAttr, targetElmObservations, (observation, selected) => {
const { changedTargetAttrs, styleChanged, contentChanged } = observation;
should(changedTargetAttrs.includes(selected)).equal(true);
should(styleChanged).equal(true);
should(contentChanged).equal(false);
});
await iterateAttrChange(setFilteredTargetAttr);
};
const iterateDirection = async (afterEach?: () => any) => {
await iterate(directionSelect, afterEach);
const iterateContentAttrChange = async () => {
await iterateAttrChange(setContentAttr, targetElmContentElmObservations, (observation) => {
const { changedTargetAttrs, styleChanged, contentChanged } = observation;
should(changedTargetAttrs.length).equal(0);
should(styleChanged).equal(false);
should(contentChanged).equal(true);
});
await iterateAttrChange(setFilteredContentAttr);
};
const iterateContentBetweenAttrChange = async () => {
await iterateAttrChange(setContentBetweenAttr);
await iterateAttrChange(setFilteredContentBetweenAttr);
};
const iterateContentHostElmAttrChange = async () => {
await iterateAttrChange(setContentHostElmAttr, targetElmContentElmObservations, (observation) => {
const { changedTargetAttrs, styleChanged, contentChanged } = observation;
should(changedTargetAttrs.length).equal(0);
should(styleChanged).equal(false);
should(contentChanged).equal(true);
});
await iterateAttrChange(setFilteredContentHostElmAttr);
};
const triggerContentSummaryChange = async () => {
await triggerSummaryElemet(summaryContent, targetElmContentElmObservations);
};
const triggerBetweenSummaryChange = async () => {
await triggerSummaryElemet(summaryBetween);
};
addRemoveTargetElms?.addEventListener('click', addRemoveTargetElmsFn);
addRemoveTargetContentElms?.addEventListener('click', addRemoveTargetContentElmsFn);
addRemoveTargetContentBetweenElms?.addEventListener('click', addRemoveTargetContentBetweenElmsFn);
setTargetAttr?.addEventListener('change', attrChangeListener(targetElm));
setFilteredTargetAttr?.addEventListener('change', attrChangeListener(targetElm));
setContentAttr?.addEventListener('change', attrChangeListener(contentElmAttrChange));
setFilteredContentAttr?.addEventListener('change', attrChangeListener(contentElmAttrChange));
setContentBetweenAttr?.addEventListener('change', attrChangeListener(contentBetweenElmAttrChange));
setFilteredContentBetweenAttr?.addEventListener('change', attrChangeListener(contentBetweenElmAttrChange));
setContentHostElmAttr?.addEventListener('change', attrChangeListener(contentHostElmAttrChange));
setFilteredContentHostElmAttr?.addEventListener('change', attrChangeListener(contentHostElmAttrChange));
createDOMObserver(
document.querySelector('#target') as HTMLElement,
(changedTargetAttrs: string[], styleChanged: boolean, contentChanged: boolean) => {
targetElmObservations.push({ changedTargetAttrs, styleChanged, contentChanged });
requestAnimationFrame(() => {
if (targetChangesCountSlot) {
targetChangesCountSlot.textContent = `${targetElmObservations.length}`;
}
});
},
{
_attributes: ['data-target'],
}
);
createDOMObserver(
document.querySelector('#target .content') as HTMLElement,
(changedTargetAttrs: string[], styleChanged: boolean, contentChanged: boolean) => {
targetElmContentElmObservations.push({ changedTargetAttrs, styleChanged, contentChanged });
requestAnimationFrame(() => {
if (contentChangesCountSlot) {
contentChangesCountSlot.textContent = `${targetElmContentElmObservations.length}`;
}
});
},
{
_observeContent: true,
_ignoreContentChange: (mutation) => {
const { target, attributeName } = mutation;
return attributeName ? !hasClass(target as Element, 'host') && liesBetween(target as Element, '.host', '.content') : false;
},
}
);
const start = async () => {
setTestResult(null);
console.log('init direction changes:', directionIterations);
console.log('init size changes:', sizeIterations);
should.ok(directionIterations > 0);
should.ok(sizeIterations > 0);
await addRemoveTargetElmsFn();
await addRemoveTargetContentElmsFn();
await addRemoveTargetContentBetweenElmsFn();
targetElm?.removeAttribute('style');
await iterateDisplay();
await iterateDirection();
await iterateBoxSizing(async () => {
await iterateHeight(async () => {
await iterateWidth(async () => {
await iterateBorder(async () => {
await iterateDirection();
await iteratePadding();
});
});
});
});
await iterateTargetAttrChange();
await iterateContentAttrChange();
await iterateContentBetweenAttrChange();
await iterateContentHostElmAttrChange();
await triggerContentSummaryChange();
await triggerBetweenSummaryChange();
setTestResult(true);
};
startBtn?.addEventListener('click', start);
createSizeObserver(
targetElm as HTMLElement,
(directionCache?: any) => {
if (directionCache) {
directionIterations += 1;
} else {
sizeIterations += 1;
}
requestAnimationFrame(() => {
if (resizesSlot) {
resizesSlot.textContent = (directionIterations + sizeIterations).toString();
}
});
},
{ _direction: true, _appear: true }
);
export { start };
@@ -1,49 +1,144 @@
<div id="controls">
<label for="height">height</label>
<select name="height" id="height">
<option value="heightAuto">auto</option>
<option value="heightHundred">100%</option>
<option value="height200">200px</option>
<button id="addRemoveTargetElms">Target Elements</button>
<button id="addRemoveTargetContentElms">Content Elements</button>
<button id="addRemoveTargetContentBetweenElms">Content Between Elements</button>
<label for="setTargetAttr">setTargetAttr</label>
<select name="setTargetAttr" id="setTargetAttr">
<option value="id">id</option>
<option value="class">class</option>
<option value="style">style</option>
<option value="data-target">data-target</option>
</select>
<label for="width">width</label>
<select name="width" id="width">
<option value="widthAuto">auto</option>
<option value="widthHundred">100%</option>
<option value="width200">200px</option>
<label for="setFilteredTargetAttr">setFilteredTargetAttr</label>
<select name="setFilteredTargetAttr" id="setFilteredTargetAttr">
<option value="data-something-a">data-something-a</option>
<option value="data-something-b">data-something-b</option>
<option value="data-something-c">data-something-c</option>
</select>
<label for="padding">padding</label>
<select name="padding" id="padding">
<option value="padding0">0</option>
<option value="padding10">10px</option>
<option value="padding50">50px</option>
<label for="setContentAttr">setContentAttr</label>
<select name="setContentAttr" id="setContentAttr">
<option value="id">id</option>
<option value="class">class</option>
<option value="style">style</option>
<option value="data-target">data-target</option>
</select>
<label for="border">border</label>
<select name="border" id="border">
<option value="border2">2px</option>
<option value="border10">10px</option>
<option value="border0">0</option>
<label for="setFilteredContentAttr">setFilteredContentAttr</label>
<select name="setFilteredContentAttr" id="setFilteredContentAttr">
<option value="data-something-a">data-something-a</option>
<option value="data-something-b">data-something-b</option>
<option value="data-something-c">data-something-c</option>
</select>
<label for="boxSizing">boxSizing</label>
<select name="boxSizing" id="boxSizing">
<option value="boxSizingBorderBox">border-box</option>
<option value="boxSizingContentBox">content-box</option>
<label for="setContentBetweenAttr">setContentBetweenAttr</label>
<select name="setContentBetweenAttr" id="setContentBetweenAttr">
<option value="id">id</option>
<option value="class">class</option>
<option value="style">style</option>
<option value="data-target">data-target</option>
</select>
<label for="display">display</label>
<select name="display" id="display">
<option value="displayBlock">block</option>
<option value="displayNone">none</option>
<label for="setFilteredContentBetweenAttr">setFilteredContentBetweenAttr</label>
<select name="setFilteredContentBetweenAttr" id="setFilteredContentBetweenAttr">
<option value="data-something-a">data-something-a</option>
<option value="data-something-b">data-something-b</option>
<option value="data-something-c">data-something-c</option>
</select>
<label for="direction">direction</label>
<select name="direction" id="direction">
<option value="directionLTR">ltr</option>
<option value="directionRTL">rtl</option>
<label for="setContentHostElmAttr">setContentHostElmAttr</label>
<select name="setContentHostElmAttr" id="setContentHostElmAttr">
<option value="id">id</option>
<option value="class">class</option>
<option value="style">style</option>
<option value="data-target">data-target</option>
</select>
<label for="setFilteredContentHostElmAttr">setFilteredContentHostElmAttr</label>
<select name="setFilteredContentHostElmAttr" id="setFilteredContentHostElmAttr">
<option value="data-something-a">data-something-a</option>
<option value="data-something-b">data-something-b</option>
<option value="data-something-c">data-something-c</option>
</select>
<button id="start">start</button>
<span>Detected resizes: <span id="resizes">0</span></span>
<span>Detected target changes: <span id="targetChanges">0</span></span>
<span>Detected content changes: <span id="contentChanges">0</span></span>
</div>
<div id="stage">
<div>
<div id="target"></div>
<div id="target" class="host">
<div class="host-nest">
<div class="host-nest-item"></div>
</div>
<div class="padding">
<div class="padding-nest">
<div class="padding-nest-item"></div>
</div>
<div class="viewport">
<div class="viewport-nest">
<div class="viewport-nest-item"></div>
</div>
<div class="content">
<div class="content-nest">
<div class="content-nest-item">
<div id="content-nest-item-host" class="host">
<div class="host-nest">
<div class="host-nest-item"></div>
</div>
<div class="padding">
<div class="padding-nest">
<div class="padding-nest-item"></div>
</div>
<div class="viewport">
<div class="viewport-nest">
<div class="viewport-nest-item"></div>
</div>
<div class="content">
<div class="content-nest">
<div class="content-nest-item">
<details>
<summary id="summary-content">Triggers DOM Change</summary>
<p>DOM Content Change should be triggered</p>
</details>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="content-host" class="host">
<div class="host-nest">
<div class="host-nest-item">
<details>
<summary id="summary-between">Won't trigger DOM Change</summary>
<p>DOM Content Change shouldn't be triggered</p>
</details>
</div>
</div>
<div class="padding">
<div class="padding-nest">
<div class="padding-nest-item"></div>
</div>
<div class="viewport">
<div class="viewport-nest">
<div class="viewport-nest-item"></div>
</div>
<div class="content">
<div class="content-nest">
<div class="content-nest-item"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -27,73 +27,51 @@ body {
left: 0;
}
#target {
overflow: hidden;
resize: both;
position: relative;
// prevent container from reaching 0x0 dimensions for testing purposes
min-width: 50px;
min-height: 50px;
.addedElm {
height: 20px;
width: 20px;
background: yellow;
}
.padding0 {
padding: 0;
}
.padding10 {
padding: 10px;
}
.padding50 {
padding: 50px;
}
.border2 {
border: 2px solid red;
}
.border10 {
border: 10px solid red;
}
.border0 {
border: none;
}
.heightAuto {
height: auto;
}
.height200 {
height: 200px;
}
.heightHundred {
height: 100%;
}
.widthAuto {
width: auto;
float: left;
}
.width200 {
width: 200px;
}
.widthHundred {
width: 100%;
}
.boxSizingBorderBox {
box-sizing: border-box;
}
.boxSizingContentBox {
box-sizing: content-box;
}
.displayNone {
display: none;
}
.displayBlock {
display: block;
}
.directionltr {
direction: ltr;
}
.directionRTL {
direction: rtl;
.host {
color: black;
border: 1px solid red;
background: red;
& > .host-nest {
border: 1px solid rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.3);
& > .host-nest-item {
border: 1px solid rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.3);
}
}
& > .padding {
border: 1px solid green;
background: green;
& > .padding-nest {
border: 1px solid rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.3);
& > .padding-nest-item {
border: 1px solid rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.3);
}
}
& > .viewport {
border: 1px solid blue;
background: blue;
& > .viewport-nest {
border: 1px solid rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.3);
& > .viewport-nest-item {
border: 1px solid rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.3);
}
}
& > .content {
border: 1px solid black;
background: black;
color: white;
}
}
}
}
@@ -1,9 +1,8 @@
import 'overlayscrollbars.scss';
import './index.scss';
import should from 'should';
import { waitFor } from '@testing-library/dom';
import { generateSelectCallback, iterateSelect } from '@/testing-browser/Select';
import { setTestResult } from '@/testing-browser/TestResult';
import { generateClassChangeSelectCallback, iterateSelect } from '@/testing-browser/Select';
import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult';
import { hasDimensions, offsetSize, WH, style } from 'support';
import { createSizeObserver } from 'observers/sizeObserver';
@@ -33,7 +32,7 @@ const directionSelect: HTMLSelectElement | null = document.querySelector('#direc
const startBtn: HTMLButtonElement | null = document.querySelector('#start');
const resizesSlot: HTMLButtonElement | null = document.querySelector('#resizes');
const selectCallback = generateSelectCallback(targetElm as HTMLElement);
const selectCallback = generateClassChangeSelectCallback(targetElm as HTMLElement);
const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any) => {
interface IterateSelect {
currSizeIterations: number;
@@ -78,22 +77,14 @@ const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any)
}
if (dimensions && (offsetSizeChanged || contentSizeChanged || dirChanged)) {
await waitFor(
() => {
if (offsetSizeChanged || contentSizeChanged) {
should.equal(sizeIterations, currSizeIterations + 1);
}
if (dirChanged) {
should.equal(directionIterations, currDirectionIterations + 1);
}
},
{
onTimeout(error): Error {
setTestResult(false);
return error;
},
await waitForOrFailTest(() => {
if (offsetSizeChanged || contentSizeChanged) {
should.equal(sizeIterations, currSizeIterations + 1);
}
);
if (dirChanged) {
should.equal(directionIterations, currDirectionIterations + 1);
}
});
}
},
afterEach,
@@ -1,21 +1,13 @@
import 'overlayscrollbars.scss';
import './index.scss';
import should from 'should';
import { waitFor } from '@testing-library/dom';
import { generateSelectCallback, iterateSelect, selectOption } from '@/testing-browser/Select';
import { generateClassChangeSelectCallback, iterateSelect, selectOption } from '@/testing-browser/Select';
import { timeout } from '@/testing-browser/timeout';
import { setTestResult } from '@/testing-browser/TestResult';
import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult';
import { offsetSize } from 'support';
import { createTrinsicObserver } from 'observers/trinsicObserver';
const waitForOptions = {
onTimeout(error: Error): Error {
setTestResult(false);
return error;
},
};
let heightIntrinsic: boolean | undefined;
let heightIterations = 0;
const envElm = document.querySelector('#env');
@@ -27,8 +19,8 @@ const displaySelect: HTMLSelectElement | null = document.querySelector('#display
const startBtn: HTMLButtonElement | null = document.querySelector('#start');
const changesSlot: HTMLButtonElement | null = document.querySelector('#changes');
const envElmSelectCallback = generateSelectCallback(envElm as HTMLElement);
const targetElmSelectCallback = generateSelectCallback(targetElm as HTMLElement);
const envElmSelectCallback = generateClassChangeSelectCallback(envElm as HTMLElement);
const targetElmSelectCallback = generateClassChangeSelectCallback(targetElm as HTMLElement);
envHeightSelect?.addEventListener('change', envElmSelectCallback);
targetHeightSelect?.addEventListener('change', targetElmSelectCallback);
@@ -57,11 +49,11 @@ const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any)
const newHeightIntrinsic = offsetSize(checkElm as HTMLElement).h === 0;
const trinsicHeightChanged = newHeightIntrinsic !== currHeightIntrinsic;
await waitFor(() => {
await waitForOrFailTest(() => {
if (trinsicHeightChanged) {
should.equal(heightIterations, currHeightIterations + 1);
}
}, waitForOptions);
});
},
afterEach,
});
@@ -85,9 +77,9 @@ const changeWhileHidden = async () => {
selectOption(envHeightSelect as HTMLSelectElement, 'envHeightHundred');
selectOption(displaySelect as HTMLSelectElement, 'displayBlock');
await waitFor(() => {
await waitForOrFailTest(() => {
should.equal(heightIntrinsic, false);
}, waitForOptions);
});
};
const hundredToAuto = async () => {
@@ -99,9 +91,9 @@ const changeWhileHidden = async () => {
selectOption(envHeightSelect as HTMLSelectElement, 'envHeightAuto');
selectOption(displaySelect as HTMLSelectElement, 'displayBlock');
await waitFor(() => {
await waitForOrFailTest(() => {
should.equal(heightIntrinsic, true);
}, waitForOptions);
});
};
await autoToHundred();
+13 -5
View File
@@ -9,19 +9,27 @@ const noop = <T>(): T => {
const getSelectOptions = (selectElement: HTMLSelectElement) => Array.from(selectElement.options).map((option) => option.value);
export const generateSelectCallback = (targetElm: HTMLElement | null) => (event: Event | HTMLSelectElement | null) => {
export const generateSelectCallback = (
targetElm: HTMLElement | null,
callback: (targetAffectedElm: HTMLElement, possibleValues: string[], selectedValue: string) => any
) => (event: Event | HTMLSelectElement | null) => {
const target: HTMLSelectElement | null = isEvent(event) ? (event.target as HTMLSelectElement) : event;
if (target) {
const selectedOption = target.value;
const selectOptions = getSelectOptions(target);
if (targetElm) {
selectOptions.forEach((clazz) => targetElm.classList.remove(clazz));
targetElm.classList.add(selectedOption);
callback(targetElm, selectOptions, selectedOption);
}
}
};
export const generateClassChangeSelectCallback = (targetElm: HTMLElement | null) =>
generateSelectCallback(targetElm, (targetAffectedElm, possibleValues, selectedValue) => {
possibleValues.forEach((clazz) => targetAffectedElm.classList.remove(clazz));
targetAffectedElm.classList.add(selectedValue);
});
export const selectOption = (select: HTMLSelectElement | null, selectedOption: string | number): boolean => {
if (!select) {
return false;
@@ -56,7 +64,7 @@ export const iterateSelect = async <T>(
select: HTMLSelectElement | null,
options?: {
beforeEach?: () => T | Promise<T>;
check?: (input: T) => void | Promise<void>;
check?: (input: T, selectedOptions: string) => void | Promise<void>;
afterEach?: () => void | Promise<void>;
}
) => {
@@ -71,7 +79,7 @@ export const iterateSelect = async <T>(
const beforeEachObj: T = await beforeEach();
if (selectOption(select, option)) {
// eslint-disable-next-line
await check(beforeEachObj);
await check(beforeEachObj, option);
// eslint-disable-next-line
await afterEach();
}
@@ -1,3 +1,5 @@
import { waitFor, waitForOptions } from '@testing-library/dom';
const getTestResultElm = () => document.getElementById('testResult');
export const setTestResult = (result: boolean | null) => {
@@ -15,3 +17,12 @@ export const testPassed = (): boolean => {
const elm = getTestResultElm();
return elm ? elm.getAttribute('class') === 'passed' : false;
};
export const waitForOrFailTest = <T>(callback: () => T | Promise<T>, options?: waitForOptions) =>
waitFor(callback, {
...options,
onTimeout(error: Error): Error {
setTestResult(false);
return error;
},
});