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
+41 -6
View File
@@ -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 ||
@@ -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;
} }
@@ -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,
@@ -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();
+13 -5
View File
@@ -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;
},
});