mirror of
https://github.com/tenrok/OverlayScrollbars.git
synced 2026-06-22 11:30:35 +03:00
improve pptr dev experience and write dom observer tests
This commit is contained in:
@@ -15,6 +15,18 @@ const cacheFilePrefix = 'jest-puppeteer-overlayscrollbars-cache-';
|
|||||||
const cacheEncoding = 'utf8';
|
const cacheEncoding = 'utf8';
|
||||||
const cacheHash = 'md5';
|
const cacheHash = 'md5';
|
||||||
|
|
||||||
|
const rollupAdditionalWatchFiles = (files) => ({
|
||||||
|
buildStart() {
|
||||||
|
if (files) {
|
||||||
|
files.forEach((file) => {
|
||||||
|
if (fs.existsSync(file)) {
|
||||||
|
this.addWatchFile(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const makeHtmlAttributes = (attributes) => {
|
const makeHtmlAttributes = (attributes) => {
|
||||||
if (!attributes) {
|
if (!attributes) {
|
||||||
return '';
|
return '';
|
||||||
@@ -26,7 +38,7 @@ const makeHtmlAttributes = (attributes) => {
|
|||||||
return keys.reduce((result, key) => (result += ` ${key}="${attributes[key]}"`), '');
|
return keys.reduce((result, key) => (result += ` ${key}="${attributes[key]}"`), '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const genHtmlTemplateFunc = (content) => ({ attributes, files, meta, publicPath, title }) => {
|
const genHtmlTemplateFunc = (contentOrContentFn) => ({ attributes, files, meta, publicPath, title }) => {
|
||||||
const scripts = (files.js || [])
|
const scripts = (files.js || [])
|
||||||
.map(({ fileName }) => `<script src="${publicPath}${fileName}"${makeHtmlAttributes(attributes.script)}></script>`)
|
.map(({ fileName }) => `<script src="${publicPath}${fileName}"${makeHtmlAttributes(attributes.script)}></script>`)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
@@ -87,7 +99,7 @@ const genHtmlTemplateFunc = (content) => ({ attributes, files, meta, publicPath,
|
|||||||
${links}
|
${links}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
${content || ''}
|
${(typeof contentOrContentFn === 'function' ? contentOrContentFn() : contentOrContentFn) || ''}
|
||||||
${scripts}
|
${scripts}
|
||||||
<div id="testResult"></div>
|
<div id="testResult"></div>
|
||||||
</body>
|
</body>
|
||||||
@@ -152,8 +164,19 @@ const setupRollupTest = async (rootDir, testPath, cacheDir, watch) => {
|
|||||||
if (typeof rollupConfig === 'function') {
|
if (typeof rollupConfig === 'function') {
|
||||||
try {
|
try {
|
||||||
const htmlFilePath = path.resolve(testDir, deploymentConfig.html.input);
|
const htmlFilePath = path.resolve(testDir, deploymentConfig.html.input);
|
||||||
const htmlFileContent = fs.existsSync(htmlFilePath) ? fs.readFileSync(htmlFilePath, 'utf8') : null;
|
|
||||||
const dist = path.resolve(testDir, deploymentConfig.build);
|
const dist = path.resolve(testDir, deploymentConfig.build);
|
||||||
|
const getHtmlFileContent = () => (fs.existsSync(htmlFilePath) ? fs.readFileSync(htmlFilePath, 'utf8') : null);
|
||||||
|
const logBuilding = (re) => {
|
||||||
|
const text = re ? ' RE-BUILDING ' : ' BUILDING ';
|
||||||
|
console.log(`\x1b[1m\x1b[44m${text}\x1b[0m \x1b[90m${testPath}\x1b[0m`); // eslint-disable-line
|
||||||
|
};
|
||||||
|
const logBundleFinish = (duration) => {
|
||||||
|
if (duration) {
|
||||||
|
console.log(`Bundle finished after ${Math.round(duration / 1000)} seconds.`); // eslint-disable-line
|
||||||
|
} else {
|
||||||
|
console.log(`Bundle finished.`); // eslint-disable-line
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let rollupConfigObj = rollupConfig(undefined, {
|
let rollupConfigObj = rollupConfig(undefined, {
|
||||||
project: rootDir,
|
project: rootDir,
|
||||||
@@ -173,11 +196,12 @@ const setupRollupTest = async (rootDir, testPath, cacheDir, watch) => {
|
|||||||
rollupPluginHtml({
|
rollupPluginHtml({
|
||||||
title: `Jest-Puppeteer: ${testName}`,
|
title: `Jest-Puppeteer: ${testName}`,
|
||||||
fileName: deploymentConfig.html.output,
|
fileName: deploymentConfig.html.output,
|
||||||
template: genHtmlTemplateFunc(htmlFileContent),
|
template: genHtmlTemplateFunc(getHtmlFileContent),
|
||||||
meta: [{ charset: 'utf-8' }, { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge' }],
|
meta: [{ charset: 'utf-8' }, { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge' }],
|
||||||
}),
|
}),
|
||||||
...(watch
|
...(watch
|
||||||
? [
|
? [
|
||||||
|
rollupAdditionalWatchFiles([htmlFilePath]),
|
||||||
rollupPluginServe({
|
rollupPluginServe({
|
||||||
contentBase: dist,
|
contentBase: dist,
|
||||||
historyApiFallback: `/${deploymentConfig.html.output}`,
|
historyApiFallback: `/${deploymentConfig.html.output}`,
|
||||||
@@ -225,10 +249,13 @@ const setupRollupTest = async (rootDir, testPath, cacheDir, watch) => {
|
|||||||
console.log('Error:', error); // eslint-disable-line
|
console.log('Error:', error); // eslint-disable-line
|
||||||
}
|
}
|
||||||
if (code === 'START') {
|
if (code === 'START') {
|
||||||
console.log(firstWatch ? 'Building...' : 'Rebuilding...'); // eslint-disable-line
|
if (firstWatch) {
|
||||||
|
console.log(''); // eslint-disable-line
|
||||||
|
}
|
||||||
|
logBuilding(!firstWatch);
|
||||||
}
|
}
|
||||||
if (code === 'BUNDLE_END') {
|
if (code === 'BUNDLE_END') {
|
||||||
console.log(`Bundle finished after ${Math.round(duration / 1000)} seconds.`); // eslint-disable-line
|
logBundleFinish(duration);
|
||||||
if (result && result.close) {
|
if (result && result.close) {
|
||||||
result.close();
|
result.close();
|
||||||
}
|
}
|
||||||
@@ -246,6 +273,9 @@ const setupRollupTest = async (rootDir, testPath, cacheDir, watch) => {
|
|||||||
|
|
||||||
rollupWatchers.push(rollupWatcher);
|
rollupWatchers.push(rollupWatcher);
|
||||||
} else {
|
} else {
|
||||||
|
console.log(''); // eslint-disable-line
|
||||||
|
logBuilding();
|
||||||
|
const startTime = Date.now();
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const bundle = await rollup.rollup(inputConfig);
|
const bundle = await rollup.rollup(inputConfig);
|
||||||
|
|
||||||
@@ -253,7 +283,12 @@ const setupRollupTest = async (rootDir, testPath, cacheDir, watch) => {
|
|||||||
const outputConfig = output[i];
|
const outputConfig = output[i];
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await bundle.write(outputConfig);
|
await bundle.write(outputConfig);
|
||||||
|
|
||||||
|
const endTime = Date.now();
|
||||||
|
logBundleFinish(endTime - startTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(''); // eslint-disable-line
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
import { each, indexOf, isString, MutationObserverConstructor, isEmptyArray, liesBetween } from 'support';
|
import { each, indexOf, isString, MutationObserverConstructor, isEmptyArray, on, off, attr, is, find } from 'support';
|
||||||
import { classNameHost, classNameContent } from 'classnames';
|
|
||||||
|
|
||||||
|
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 {
|
export interface DOMObserverOptions {
|
||||||
_observeContent?: boolean;
|
_observeContent?: boolean;
|
||||||
_attributes?: string[];
|
_attributes?: string[];
|
||||||
|
_ignoreContentChange?: DOMObserverIgnoreContentChange;
|
||||||
|
_eventContentChange?: DOMOvserverEventContentChange;
|
||||||
}
|
}
|
||||||
export interface DOMObserver {
|
export interface DOMObserver {
|
||||||
_disconnect: () => void;
|
_disconnect: () => void;
|
||||||
@@ -12,76 +22,115 @@ export interface DOMObserver {
|
|||||||
|
|
||||||
const styleChangingAttributes = ['id', 'class', 'style', 'open'];
|
const styleChangingAttributes = ['id', 'class', 'style', 'open'];
|
||||||
const mutationObserverAttrsTextarea = ['wrap', 'cols', 'rows'];
|
const mutationObserverAttrsTextarea = ['wrap', 'cols', 'rows'];
|
||||||
|
const getAttributeChanged = (mutationTarget: Node, attributeName: string, oldValue: string | null): boolean =>
|
||||||
const isUnknownMutation = (
|
oldValue !== attr(mutationTarget as HTMLElement, attributeName);
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createDOMObserver = (
|
export const createDOMObserver = (
|
||||||
target: HTMLElement,
|
target: HTMLElement,
|
||||||
callback: (changedTargetAttrs: string[], styleChanged: boolean, contentChanged: boolean) => any,
|
callback: (targetChangedAttrs: string[], targetStyleChanged: boolean, contentChanged: boolean) => any,
|
||||||
options?: DOMObserverOptions
|
options?: DOMObserverOptions
|
||||||
): DOMObserver => {
|
): 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
|
// MutationObserver
|
||||||
const observedAttributes = (_attributes || []).concat(_observeContent ? styleChangingAttributes : mutationObserverAttrsTextarea);
|
const observedAttributes = (_attributes || []).concat(styleChangingAttributes); // TODO: observer textarea attrs if textarea
|
||||||
const observerCallback = (mutations: MutationRecord[]) => {
|
const observerCallback = (mutations: MutationRecord[]) => {
|
||||||
let styleChanged = false;
|
const targetChangedAttrs: string[] = [];
|
||||||
|
const totalAddedNodes: Node[] = [];
|
||||||
|
let targetStyleChanged = false;
|
||||||
let contentChanged = false;
|
let contentChanged = false;
|
||||||
const changedTargetAttrs: string[] = [];
|
let childListChanged = false;
|
||||||
each(mutations, (mutation) => {
|
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) {
|
if (targetAttrChanged) {
|
||||||
contentChanged = contentChanged || isUnknownMutation(attributeName, type, true, target, mutationTarget);
|
targetChangedAttrs.push(attributeName!);
|
||||||
}
|
}
|
||||||
if (isString(attributeName) && target === mutationTarget) {
|
if (_observeContent) {
|
||||||
changedTargetAttrs.push(attributeName);
|
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) {
|
if (childListChanged && !isEmptyArray(totalAddedNodes)) {
|
||||||
callback(changedTargetAttrs, styleChanged, contentChanged);
|
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 mutationObserver: MutationObserver = new MutationObserverConstructor!(observerCallback);
|
||||||
|
|
||||||
const connect = () => {
|
mutationObserver.observe(target, {
|
||||||
mutationObserver.observe(target, {
|
attributes: true,
|
||||||
attributes: true,
|
attributeOldValue: true,
|
||||||
attributeOldValue: true,
|
attributeFilter: observedAttributes,
|
||||||
subtree: _observeContent,
|
subtree: _observeContent,
|
||||||
childList: _observeContent,
|
childList: _observeContent,
|
||||||
characterData: _observeContent,
|
characterData: _observeContent,
|
||||||
attributeFilter: observedAttributes,
|
});
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
connect();
|
isConnected = true;
|
||||||
|
|
||||||
|
if (_observeContent) {
|
||||||
|
refreshEventContentChange((selector) => find(selector, target) as Node[]);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_disconnect: mutationObserver.disconnect,
|
_disconnect: () => {
|
||||||
|
mutationObserver.disconnect();
|
||||||
|
isConnected = false;
|
||||||
|
},
|
||||||
_update: () => {
|
_update: () => {
|
||||||
observerCallback(mutationObserver.takeRecords());
|
observerCallback(mutationObserver.takeRecords());
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const elmPrototype = Element.prototype;
|
|||||||
* @param selector The selector which has to be searched by.
|
* @param selector The selector which has to be searched by.
|
||||||
* @param elm The element from which the search shall be outgoing.
|
* @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> = [];
|
const arr: Array<Element> = [];
|
||||||
|
|
||||||
each((elm || document).querySelectorAll(selector), (e: Element) => {
|
each((elm || document).querySelectorAll(selector), (e: Element) => {
|
||||||
@@ -38,7 +38,7 @@ const is = (elm: InputElementType, selector: string): boolean => {
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const fn = elmPrototype.matches || elmPrototype.msMatchesSelector;
|
const fn = elmPrototype.matches || elmPrototype.msMatchesSelector;
|
||||||
return fn.call(elm, selector);
|
return fn && fn.call(elm, selector);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -101,8 +101,8 @@ const closest = (elm: InputElementType, selector: string): OutputElementType =>
|
|||||||
* @param deepBoundarySelector The deep boundary selector.
|
* @param deepBoundarySelector The deep boundary selector.
|
||||||
*/
|
*/
|
||||||
const liesBetween = (elm: InputElementType, highBoundarySelector: string, deepBoundarySelector: string): boolean => {
|
const liesBetween = (elm: InputElementType, highBoundarySelector: string, deepBoundarySelector: string): boolean => {
|
||||||
const closestHighBoundaryElm = closest(elm, highBoundarySelector);
|
const closestHighBoundaryElm = elm && closest(elm, highBoundarySelector);
|
||||||
const closestDeepBoundaryElm = findFirst(deepBoundarySelector, closestHighBoundaryElm);
|
const closestDeepBoundaryElm = elm && findFirst(deepBoundarySelector, closestHighBoundaryElm);
|
||||||
|
|
||||||
return closestHighBoundaryElm && closestDeepBoundaryElm
|
return closestHighBoundaryElm && closestDeepBoundaryElm
|
||||||
? closestHighBoundaryElm === elm ||
|
? closestHighBoundaryElm === elm ||
|
||||||
|
|||||||
+295
-145
@@ -1,185 +1,335 @@
|
|||||||
import 'overlayscrollbars.scss';
|
import 'overlayscrollbars.scss';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
import should from 'should';
|
import should from 'should';
|
||||||
import { waitFor } from '@testing-library/dom';
|
|
||||||
import { generateSelectCallback, iterateSelect } from '@/testing-browser/Select';
|
import { generateSelectCallback, iterateSelect } from '@/testing-browser/Select';
|
||||||
import { setTestResult } from '@/testing-browser/TestResult';
|
import { timeout } from '@/testing-browser/timeout';
|
||||||
import { hasDimensions, offsetSize, WH, style } from 'support';
|
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;
|
interface DOMObserverResult {
|
||||||
let directionIterations = 0;
|
changedTargetAttrs: string[];
|
||||||
const contentBox = (elm: HTMLElement | null): WH<number> => {
|
styleChanged: boolean;
|
||||||
if (elm) {
|
contentChanged: boolean;
|
||||||
const computedStyle = window.getComputedStyle(elm);
|
}
|
||||||
return {
|
interface SeparateChangeThrough {
|
||||||
w: elm.clientWidth - (parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight)),
|
added?: DOMObserverResult[];
|
||||||
h: elm.clientHeight - (parseFloat(computedStyle.paddingTop) + parseFloat(computedStyle.paddingBottom)),
|
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 startBtn: HTMLButtonElement | null = document.querySelector('#start');
|
||||||
const resizesSlot: HTMLButtonElement | null = document.querySelector('#resizes');
|
|
||||||
|
|
||||||
const selectCallback = generateSelectCallback(targetElm as HTMLElement);
|
const targetElmObservations: DOMObserverResult[] = [];
|
||||||
const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any) => {
|
const targetElmContentElmObservations: DOMObserverResult[] = [];
|
||||||
interface IterateSelect {
|
const getTotalObservations = () => targetElmObservations.length + targetElmContentElmObservations.length;
|
||||||
currSizeIterations: number;
|
const getLast = <T>(arr: T[]): T => arr[arr.length - 1] || ({} as T);
|
||||||
currDirectionIterations: number;
|
const changedThrough = (observationLists?: Array<DOMObserverResult[]> | DOMObserverResult[]) => {
|
||||||
currOffsetSize: WH<number>;
|
interface Stat {
|
||||||
currContentSize: WH<number>;
|
total: number;
|
||||||
currDir: string;
|
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() {
|
beforeEach() {
|
||||||
const currSizeIterations = sizeIterations;
|
before();
|
||||||
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,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
async check({ currSizeIterations, currDirectionIterations, currOffsetSize, currContentSize, currDir }) {
|
async check(_, selected) {
|
||||||
const newOffsetSize = offsetSize(targetElm as HTMLElement);
|
await waitForOrFailTest(async () => {
|
||||||
const newContentSize = contentBox(targetElm as HTMLElement);
|
after();
|
||||||
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;
|
|
||||||
|
|
||||||
// no overflow if not needed
|
if (changeThrough) {
|
||||||
if (targetElm && newContentSize.w > 0) {
|
compare(1);
|
||||||
should.ok(observerElm.getBoundingClientRect().right <= targetElm.getBoundingClientRect().right);
|
checkChange && checkChange(getLast(changeThrough), selected);
|
||||||
}
|
} else {
|
||||||
if (targetElm && newContentSize.h > 0) {
|
await timeout(250);
|
||||||
should.ok(observerElm.getBoundingClientRect().bottom <= targetElm.getBoundingClientRect().bottom);
|
compare(0);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
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;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
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);
|
const addElm = async () => {
|
||||||
widthSelect?.addEventListener('change', selectCallback);
|
const { before, after, compare } = changedThrough(addChangeThrough);
|
||||||
paddingSelect?.addEventListener('change', selectCallback);
|
|
||||||
borderSelect?.addEventListener('change', selectCallback);
|
|
||||||
boxSizingSelect?.addEventListener('change', selectCallback);
|
|
||||||
displaySelect?.addEventListener('change', selectCallback);
|
|
||||||
directionSelect?.addEventListener('change', selectCallback);
|
|
||||||
|
|
||||||
selectCallback(heightSelect);
|
before();
|
||||||
selectCallback(widthSelect);
|
appendChildren(slot, createDiv('addedElm'));
|
||||||
selectCallback(paddingSelect);
|
await timeout(250);
|
||||||
selectCallback(borderSelect);
|
after();
|
||||||
selectCallback(boxSizingSelect);
|
|
||||||
selectCallback(displaySelect);
|
|
||||||
selectCallback(directionSelect);
|
|
||||||
|
|
||||||
const iteratePadding = async (afterEach?: () => any) => {
|
await waitForOrFailTest(() => {
|
||||||
await iterate(paddingSelect, afterEach);
|
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) => {
|
const triggerSummaryElemet = async (summaryElm: HTMLElement | null, changeThrough?: DOMObserverResult[]) => {
|
||||||
await iterate(borderSelect, afterEach);
|
// 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) => {
|
const addRemoveTargetContentElmsFn = async () => {
|
||||||
await iterate(widthSelect, afterEach);
|
await addRemoveElementsTest(targetContentElmsSlot, targetElmContentElmObservations);
|
||||||
};
|
};
|
||||||
const iterateBoxSizing = async (afterEach?: () => any) => {
|
const addRemoveTargetContentBetweenElmsFn = async () => {
|
||||||
await iterate(boxSizingSelect, afterEach);
|
await addRemoveElementsTest(targetContentBetweenElmsSlot, targetElmContentElmObservations);
|
||||||
};
|
};
|
||||||
const iterateDisplay = async (afterEach?: () => any) => {
|
const iterateTargetAttrChange = async () => {
|
||||||
await iterate(displaySelect, afterEach);
|
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) => {
|
const iterateContentAttrChange = async () => {
|
||||||
await iterate(directionSelect, afterEach);
|
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 () => {
|
const start = async () => {
|
||||||
setTestResult(null);
|
setTestResult(null);
|
||||||
|
|
||||||
console.log('init direction changes:', directionIterations);
|
await addRemoveTargetElmsFn();
|
||||||
console.log('init size changes:', sizeIterations);
|
await addRemoveTargetContentElmsFn();
|
||||||
should.ok(directionIterations > 0);
|
await addRemoveTargetContentBetweenElmsFn();
|
||||||
should.ok(sizeIterations > 0);
|
|
||||||
|
|
||||||
targetElm?.removeAttribute('style');
|
await iterateTargetAttrChange();
|
||||||
await iterateDisplay();
|
await iterateContentAttrChange();
|
||||||
await iterateDirection();
|
await iterateContentBetweenAttrChange();
|
||||||
await iterateBoxSizing(async () => {
|
await iterateContentHostElmAttrChange();
|
||||||
await iterateHeight(async () => {
|
|
||||||
await iterateWidth(async () => {
|
await triggerContentSummaryChange();
|
||||||
await iterateBorder(async () => {
|
await triggerBetweenSummaryChange();
|
||||||
await iterateDirection();
|
|
||||||
await iteratePadding();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
setTestResult(true);
|
setTestResult(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
startBtn?.addEventListener('click', start);
|
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 };
|
export { start };
|
||||||
|
|||||||
@@ -1,49 +1,144 @@
|
|||||||
<div id="controls">
|
<div id="controls">
|
||||||
<label for="height">height</label>
|
<button id="addRemoveTargetElms">Target Elements</button>
|
||||||
<select name="height" id="height">
|
<button id="addRemoveTargetContentElms">Content Elements</button>
|
||||||
<option value="heightAuto">auto</option>
|
<button id="addRemoveTargetContentBetweenElms">Content Between Elements</button>
|
||||||
<option value="heightHundred">100%</option>
|
|
||||||
<option value="height200">200px</option>
|
<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>
|
</select>
|
||||||
<label for="width">width</label>
|
|
||||||
<select name="width" id="width">
|
<label for="setFilteredTargetAttr">setFilteredTargetAttr</label>
|
||||||
<option value="widthAuto">auto</option>
|
<select name="setFilteredTargetAttr" id="setFilteredTargetAttr">
|
||||||
<option value="widthHundred">100%</option>
|
<option value="data-something-a">data-something-a</option>
|
||||||
<option value="width200">200px</option>
|
<option value="data-something-b">data-something-b</option>
|
||||||
|
<option value="data-something-c">data-something-c</option>
|
||||||
</select>
|
</select>
|
||||||
<label for="padding">padding</label>
|
|
||||||
<select name="padding" id="padding">
|
<label for="setContentAttr">setContentAttr</label>
|
||||||
<option value="padding0">0</option>
|
<select name="setContentAttr" id="setContentAttr">
|
||||||
<option value="padding10">10px</option>
|
<option value="id">id</option>
|
||||||
<option value="padding50">50px</option>
|
<option value="class">class</option>
|
||||||
|
<option value="style">style</option>
|
||||||
|
<option value="data-target">data-target</option>
|
||||||
</select>
|
</select>
|
||||||
<label for="border">border</label>
|
|
||||||
<select name="border" id="border">
|
<label for="setFilteredContentAttr">setFilteredContentAttr</label>
|
||||||
<option value="border2">2px</option>
|
<select name="setFilteredContentAttr" id="setFilteredContentAttr">
|
||||||
<option value="border10">10px</option>
|
<option value="data-something-a">data-something-a</option>
|
||||||
<option value="border0">0</option>
|
<option value="data-something-b">data-something-b</option>
|
||||||
|
<option value="data-something-c">data-something-c</option>
|
||||||
</select>
|
</select>
|
||||||
<label for="boxSizing">boxSizing</label>
|
|
||||||
<select name="boxSizing" id="boxSizing">
|
<label for="setContentBetweenAttr">setContentBetweenAttr</label>
|
||||||
<option value="boxSizingBorderBox">border-box</option>
|
<select name="setContentBetweenAttr" id="setContentBetweenAttr">
|
||||||
<option value="boxSizingContentBox">content-box</option>
|
<option value="id">id</option>
|
||||||
|
<option value="class">class</option>
|
||||||
|
<option value="style">style</option>
|
||||||
|
<option value="data-target">data-target</option>
|
||||||
</select>
|
</select>
|
||||||
<label for="display">display</label>
|
|
||||||
<select name="display" id="display">
|
<label for="setFilteredContentBetweenAttr">setFilteredContentBetweenAttr</label>
|
||||||
<option value="displayBlock">block</option>
|
<select name="setFilteredContentBetweenAttr" id="setFilteredContentBetweenAttr">
|
||||||
<option value="displayNone">none</option>
|
<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>
|
</select>
|
||||||
<label for="direction">direction</label>
|
|
||||||
<select name="direction" id="direction">
|
<label for="setContentHostElmAttr">setContentHostElmAttr</label>
|
||||||
<option value="directionLTR">ltr</option>
|
<select name="setContentHostElmAttr" id="setContentHostElmAttr">
|
||||||
<option value="directionRTL">rtl</option>
|
<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>
|
</select>
|
||||||
|
|
||||||
<button id="start">start</button>
|
<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>
|
||||||
<div id="stage">
|
<div id="stage">
|
||||||
<div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,73 +27,51 @@ body {
|
|||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#target {
|
.addedElm {
|
||||||
overflow: hidden;
|
height: 20px;
|
||||||
resize: both;
|
width: 20px;
|
||||||
position: relative;
|
background: yellow;
|
||||||
// prevent container from reaching 0x0 dimensions for testing purposes
|
|
||||||
min-width: 50px;
|
|
||||||
min-height: 50px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.padding0 {
|
.host {
|
||||||
padding: 0;
|
color: black;
|
||||||
}
|
border: 1px solid red;
|
||||||
.padding10 {
|
background: red;
|
||||||
padding: 10px;
|
& > .host-nest {
|
||||||
}
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
.padding50 {
|
background: rgba(255, 255, 255, 0.3);
|
||||||
padding: 50px;
|
& > .host-nest-item {
|
||||||
}
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
.border2 {
|
}
|
||||||
border: 2px solid red;
|
}
|
||||||
}
|
& > .padding {
|
||||||
.border10 {
|
border: 1px solid green;
|
||||||
border: 10px solid red;
|
background: green;
|
||||||
}
|
& > .padding-nest {
|
||||||
.border0 {
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
border: none;
|
background: rgba(255, 255, 255, 0.3);
|
||||||
}
|
& > .padding-nest-item {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
.heightAuto {
|
background: rgba(255, 255, 255, 0.3);
|
||||||
height: auto;
|
}
|
||||||
}
|
}
|
||||||
.height200 {
|
& > .viewport {
|
||||||
height: 200px;
|
border: 1px solid blue;
|
||||||
}
|
background: blue;
|
||||||
.heightHundred {
|
& > .viewport-nest {
|
||||||
height: 100%;
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
}
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
& > .viewport-nest-item {
|
||||||
.widthAuto {
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
width: auto;
|
background: rgba(255, 255, 255, 0.3);
|
||||||
float: left;
|
}
|
||||||
}
|
}
|
||||||
.width200 {
|
& > .content {
|
||||||
width: 200px;
|
border: 1px solid black;
|
||||||
}
|
background: black;
|
||||||
.widthHundred {
|
color: white;
|
||||||
width: 100%;
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.boxSizingBorderBox {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.boxSizingContentBox {
|
|
||||||
box-sizing: content-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.displayNone {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.displayBlock {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.directionltr {
|
|
||||||
direction: ltr;
|
|
||||||
}
|
|
||||||
.directionRTL {
|
|
||||||
direction: rtl;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-19
@@ -1,9 +1,8 @@
|
|||||||
import 'overlayscrollbars.scss';
|
import 'overlayscrollbars.scss';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
import should from 'should';
|
import should from 'should';
|
||||||
import { waitFor } from '@testing-library/dom';
|
import { generateClassChangeSelectCallback, iterateSelect } from '@/testing-browser/Select';
|
||||||
import { generateSelectCallback, iterateSelect } from '@/testing-browser/Select';
|
import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult';
|
||||||
import { setTestResult } from '@/testing-browser/TestResult';
|
|
||||||
import { hasDimensions, offsetSize, WH, style } from 'support';
|
import { hasDimensions, offsetSize, WH, style } from 'support';
|
||||||
|
|
||||||
import { createSizeObserver } from 'observers/sizeObserver';
|
import { createSizeObserver } from 'observers/sizeObserver';
|
||||||
@@ -33,7 +32,7 @@ const directionSelect: HTMLSelectElement | null = document.querySelector('#direc
|
|||||||
const startBtn: HTMLButtonElement | null = document.querySelector('#start');
|
const startBtn: HTMLButtonElement | null = document.querySelector('#start');
|
||||||
const resizesSlot: HTMLButtonElement | null = document.querySelector('#resizes');
|
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) => {
|
const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any) => {
|
||||||
interface IterateSelect {
|
interface IterateSelect {
|
||||||
currSizeIterations: number;
|
currSizeIterations: number;
|
||||||
@@ -78,22 +77,14 @@ const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dimensions && (offsetSizeChanged || contentSizeChanged || dirChanged)) {
|
if (dimensions && (offsetSizeChanged || contentSizeChanged || dirChanged)) {
|
||||||
await waitFor(
|
await waitForOrFailTest(() => {
|
||||||
() => {
|
if (offsetSizeChanged || contentSizeChanged) {
|
||||||
if (offsetSizeChanged || contentSizeChanged) {
|
should.equal(sizeIterations, currSizeIterations + 1);
|
||||||
should.equal(sizeIterations, currSizeIterations + 1);
|
|
||||||
}
|
|
||||||
if (dirChanged) {
|
|
||||||
should.equal(directionIterations, currDirectionIterations + 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onTimeout(error): Error {
|
|
||||||
setTestResult(false);
|
|
||||||
return error;
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
if (dirChanged) {
|
||||||
|
should.equal(directionIterations, currDirectionIterations + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
afterEach,
|
afterEach,
|
||||||
|
|||||||
+10
-18
@@ -1,21 +1,13 @@
|
|||||||
import 'overlayscrollbars.scss';
|
import 'overlayscrollbars.scss';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
import should from 'should';
|
import should from 'should';
|
||||||
import { waitFor } from '@testing-library/dom';
|
import { generateClassChangeSelectCallback, iterateSelect, selectOption } from '@/testing-browser/Select';
|
||||||
import { generateSelectCallback, iterateSelect, selectOption } from '@/testing-browser/Select';
|
|
||||||
import { timeout } from '@/testing-browser/timeout';
|
import { timeout } from '@/testing-browser/timeout';
|
||||||
import { setTestResult } from '@/testing-browser/TestResult';
|
import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult';
|
||||||
import { offsetSize } from 'support';
|
import { offsetSize } from 'support';
|
||||||
|
|
||||||
import { createTrinsicObserver } from 'observers/trinsicObserver';
|
import { createTrinsicObserver } from 'observers/trinsicObserver';
|
||||||
|
|
||||||
const waitForOptions = {
|
|
||||||
onTimeout(error: Error): Error {
|
|
||||||
setTestResult(false);
|
|
||||||
return error;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let heightIntrinsic: boolean | undefined;
|
let heightIntrinsic: boolean | undefined;
|
||||||
let heightIterations = 0;
|
let heightIterations = 0;
|
||||||
const envElm = document.querySelector('#env');
|
const envElm = document.querySelector('#env');
|
||||||
@@ -27,8 +19,8 @@ const displaySelect: HTMLSelectElement | null = document.querySelector('#display
|
|||||||
const startBtn: HTMLButtonElement | null = document.querySelector('#start');
|
const startBtn: HTMLButtonElement | null = document.querySelector('#start');
|
||||||
const changesSlot: HTMLButtonElement | null = document.querySelector('#changes');
|
const changesSlot: HTMLButtonElement | null = document.querySelector('#changes');
|
||||||
|
|
||||||
const envElmSelectCallback = generateSelectCallback(envElm as HTMLElement);
|
const envElmSelectCallback = generateClassChangeSelectCallback(envElm as HTMLElement);
|
||||||
const targetElmSelectCallback = generateSelectCallback(targetElm as HTMLElement);
|
const targetElmSelectCallback = generateClassChangeSelectCallback(targetElm as HTMLElement);
|
||||||
|
|
||||||
envHeightSelect?.addEventListener('change', envElmSelectCallback);
|
envHeightSelect?.addEventListener('change', envElmSelectCallback);
|
||||||
targetHeightSelect?.addEventListener('change', targetElmSelectCallback);
|
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 newHeightIntrinsic = offsetSize(checkElm as HTMLElement).h === 0;
|
||||||
const trinsicHeightChanged = newHeightIntrinsic !== currHeightIntrinsic;
|
const trinsicHeightChanged = newHeightIntrinsic !== currHeightIntrinsic;
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitForOrFailTest(() => {
|
||||||
if (trinsicHeightChanged) {
|
if (trinsicHeightChanged) {
|
||||||
should.equal(heightIterations, currHeightIterations + 1);
|
should.equal(heightIterations, currHeightIterations + 1);
|
||||||
}
|
}
|
||||||
}, waitForOptions);
|
});
|
||||||
},
|
},
|
||||||
afterEach,
|
afterEach,
|
||||||
});
|
});
|
||||||
@@ -85,9 +77,9 @@ const changeWhileHidden = async () => {
|
|||||||
selectOption(envHeightSelect as HTMLSelectElement, 'envHeightHundred');
|
selectOption(envHeightSelect as HTMLSelectElement, 'envHeightHundred');
|
||||||
selectOption(displaySelect as HTMLSelectElement, 'displayBlock');
|
selectOption(displaySelect as HTMLSelectElement, 'displayBlock');
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitForOrFailTest(() => {
|
||||||
should.equal(heightIntrinsic, false);
|
should.equal(heightIntrinsic, false);
|
||||||
}, waitForOptions);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const hundredToAuto = async () => {
|
const hundredToAuto = async () => {
|
||||||
@@ -99,9 +91,9 @@ const changeWhileHidden = async () => {
|
|||||||
selectOption(envHeightSelect as HTMLSelectElement, 'envHeightAuto');
|
selectOption(envHeightSelect as HTMLSelectElement, 'envHeightAuto');
|
||||||
selectOption(displaySelect as HTMLSelectElement, 'displayBlock');
|
selectOption(displaySelect as HTMLSelectElement, 'displayBlock');
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitForOrFailTest(() => {
|
||||||
should.equal(heightIntrinsic, true);
|
should.equal(heightIntrinsic, true);
|
||||||
}, waitForOptions);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
await autoToHundred();
|
await autoToHundred();
|
||||||
|
|||||||
@@ -9,19 +9,27 @@ const noop = <T>(): T => {
|
|||||||
|
|
||||||
const getSelectOptions = (selectElement: HTMLSelectElement) => Array.from(selectElement.options).map((option) => option.value);
|
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;
|
const target: HTMLSelectElement | null = isEvent(event) ? (event.target as HTMLSelectElement) : event;
|
||||||
if (target) {
|
if (target) {
|
||||||
const selectedOption = target.value;
|
const selectedOption = target.value;
|
||||||
const selectOptions = getSelectOptions(target);
|
const selectOptions = getSelectOptions(target);
|
||||||
|
|
||||||
if (targetElm) {
|
if (targetElm) {
|
||||||
selectOptions.forEach((clazz) => targetElm.classList.remove(clazz));
|
callback(targetElm, selectOptions, selectedOption);
|
||||||
targetElm.classList.add(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 => {
|
export const selectOption = (select: HTMLSelectElement | null, selectedOption: string | number): boolean => {
|
||||||
if (!select) {
|
if (!select) {
|
||||||
return false;
|
return false;
|
||||||
@@ -56,7 +64,7 @@ export const iterateSelect = async <T>(
|
|||||||
select: HTMLSelectElement | null,
|
select: HTMLSelectElement | null,
|
||||||
options?: {
|
options?: {
|
||||||
beforeEach?: () => T | Promise<T>;
|
beforeEach?: () => T | Promise<T>;
|
||||||
check?: (input: T) => void | Promise<void>;
|
check?: (input: T, selectedOptions: string) => void | Promise<void>;
|
||||||
afterEach?: () => void | Promise<void>;
|
afterEach?: () => void | Promise<void>;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
@@ -71,7 +79,7 @@ export const iterateSelect = async <T>(
|
|||||||
const beforeEachObj: T = await beforeEach();
|
const beforeEachObj: T = await beforeEach();
|
||||||
if (selectOption(select, option)) {
|
if (selectOption(select, option)) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
await check(beforeEachObj);
|
await check(beforeEachObj, option);
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
await afterEach();
|
await afterEach();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { waitFor, waitForOptions } from '@testing-library/dom';
|
||||||
|
|
||||||
const getTestResultElm = () => document.getElementById('testResult');
|
const getTestResultElm = () => document.getElementById('testResult');
|
||||||
|
|
||||||
export const setTestResult = (result: boolean | null) => {
|
export const setTestResult = (result: boolean | null) => {
|
||||||
@@ -15,3 +17,12 @@ export const testPassed = (): boolean => {
|
|||||||
const elm = getTestResultElm();
|
const elm = getTestResultElm();
|
||||||
return elm ? elm.getAttribute('class') === 'passed' : false;
|
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;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user