mirror of
https://github.com/tenrok/OverlayScrollbars.git
synced 2026-06-21 00:20:36 +03:00
improve domObserver and add domObserver unit tests
This commit is contained in:
@@ -11,6 +11,8 @@ import {
|
|||||||
is,
|
is,
|
||||||
find,
|
find,
|
||||||
push,
|
push,
|
||||||
|
from,
|
||||||
|
runEachAndClear,
|
||||||
} from 'support';
|
} from 'support';
|
||||||
|
|
||||||
type DOMContentObserverCallback = (contentChangedTroughEvent: boolean) => any;
|
type DOMContentObserverCallback = (contentChangedTroughEvent: boolean) => any;
|
||||||
@@ -19,7 +21,6 @@ type DOMTargetObserverCallback = (targetChangedAttrs: string[], targetStyleChang
|
|||||||
|
|
||||||
interface DOMObserverOptionsBase {
|
interface DOMObserverOptionsBase {
|
||||||
_attributes?: string[];
|
_attributes?: string[];
|
||||||
_styleChangingAttributes?: string[];
|
|
||||||
/**
|
/**
|
||||||
* A function which can ignore a changed attribute if it returns true.
|
* A function which can ignore a changed attribute if it returns true.
|
||||||
* for DOMTargetObserver this applies to the changes to the observed target
|
* for DOMTargetObserver this applies to the changes to the observed target
|
||||||
@@ -34,7 +35,13 @@ interface DOMContentObserverOptions extends DOMObserverOptionsBase {
|
|||||||
_ignoreContentChange?: DOMObserverIgnoreContentChange; // function which will prevent marking certain dom changes as content change if it returns true
|
_ignoreContentChange?: DOMObserverIgnoreContentChange; // function which will prevent marking certain dom changes as content change if it returns true
|
||||||
}
|
}
|
||||||
|
|
||||||
type DOMTargetObserverOptions = DOMObserverOptionsBase;
|
interface DOMTargetObserverOptions extends DOMObserverOptionsBase {
|
||||||
|
/**
|
||||||
|
* Marks certain attributes as style changing, should be a subset of the _attributes prop.
|
||||||
|
* Used to set the "targetStyleChanged" param in the DOMTargetObserverCallback.
|
||||||
|
*/
|
||||||
|
_styleChangingAttributes?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
type ContentChangeArrayItem = [selector?: string, eventNames?: string] | null | undefined;
|
type ContentChangeArrayItem = [selector?: string, eventNames?: string] | null | undefined;
|
||||||
|
|
||||||
@@ -71,7 +78,10 @@ export type DOMObserver<ContentObserver extends boolean> = [
|
|||||||
update: () => void | false | Parameters<DOMObserverCallback<ContentObserver>>
|
update: () => void | false | Parameters<DOMObserverCallback<ContentObserver>>
|
||||||
];
|
];
|
||||||
|
|
||||||
type EventContentChangeUpdateElement = (getElements?: (selector: string) => Node[]) => void;
|
type EventContentChangeUpdateElement = (
|
||||||
|
getElements?: (selector: string) => Node[],
|
||||||
|
removed?: boolean
|
||||||
|
) => void;
|
||||||
type EventContentChange = [destroy: () => void, updateElements: EventContentChangeUpdateElement];
|
type EventContentChange = [destroy: () => void, updateElements: EventContentChangeUpdateElement];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,21 +92,20 @@ type EventContentChange = [destroy: () => void, updateElements: EventContentChan
|
|||||||
* @returns A object which contains a set of helper functions to destroy and update the observation of elements.
|
* @returns A object which contains a set of helper functions to destroy and update the observation of elements.
|
||||||
*/
|
*/
|
||||||
const createEventContentChange = (
|
const createEventContentChange = (
|
||||||
target: Element,
|
target: HTMLElement,
|
||||||
callback: (...args: any) => any,
|
callback: (...args: any) => any,
|
||||||
eventContentChange?: DOMObserverEventContentChange
|
eventContentChange?: DOMObserverEventContentChange
|
||||||
): EventContentChange => {
|
): EventContentChange => {
|
||||||
let map: WeakMap<Node, [string, () => any]> | undefined; // weak map to prevent memory leak for detached elements
|
let map: WeakMap<Node, (() => any)[]> | undefined; // weak map to prevent memory leak for detached elements
|
||||||
let destroyed = false;
|
let destroyed = false;
|
||||||
const destroy = () => {
|
const destroy = () => {
|
||||||
destroyed = true;
|
destroyed = true;
|
||||||
};
|
};
|
||||||
const updateElements: EventContentChangeUpdateElement = (getElements?) => {
|
const updateElements: EventContentChangeUpdateElement = (getElements) => {
|
||||||
if (eventContentChange) {
|
if (eventContentChange) {
|
||||||
const eventElmList = eventContentChange.reduce<Array<[Node[], string]>>((arr, item) => {
|
const eventElmList = eventContentChange.reduce<Array<[Node[], string]>>((arr, item) => {
|
||||||
if (item) {
|
if (item) {
|
||||||
const selector = item[0];
|
const [selector, eventNames] = item;
|
||||||
const eventNames = item[1];
|
|
||||||
const elements =
|
const elements =
|
||||||
eventNames &&
|
eventNames &&
|
||||||
selector &&
|
selector &&
|
||||||
@@ -112,27 +121,23 @@ const createEventContentChange = (
|
|||||||
each(eventElmList, (item) =>
|
each(eventElmList, (item) =>
|
||||||
each(item[0], (elm) => {
|
each(item[0], (elm) => {
|
||||||
const eventNames = item[1];
|
const eventNames = item[1];
|
||||||
const entry = map!.get(elm);
|
const entries = map!.get(elm) || [];
|
||||||
|
const isTargetChild = target.contains(elm);
|
||||||
|
|
||||||
if (entry) {
|
if (isTargetChild) {
|
||||||
const entryEventNames = entry[0];
|
const off = on(elm, eventNames, (event: Event) => {
|
||||||
const entryOff = entry[1];
|
if (destroyed) {
|
||||||
|
off();
|
||||||
// in case an already registered element is registered again, unregister the previous events
|
map!.delete(elm);
|
||||||
if (entryEventNames === eventNames) {
|
} else {
|
||||||
entryOff();
|
callback(event);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
map!.set(elm, push(entries, off));
|
||||||
|
} else {
|
||||||
|
runEachAndClear(entries);
|
||||||
|
map!.delete(elm);
|
||||||
}
|
}
|
||||||
|
|
||||||
const off = on(elm, eventNames, (event: Event) => {
|
|
||||||
if (destroyed) {
|
|
||||||
off();
|
|
||||||
map!.delete(elm);
|
|
||||||
} else {
|
|
||||||
callback(event);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
map!.set(elm, [eventNames, off]);
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -193,13 +198,21 @@ export const createDOMObserver = <ContentObserver extends boolean>(
|
|||||||
): void | Parameters<DOMObserverCallback<ContentObserver>> => {
|
): void | Parameters<DOMObserverCallback<ContentObserver>> => {
|
||||||
const ignoreTargetChange = _ignoreTargetChange || noop;
|
const ignoreTargetChange = _ignoreTargetChange || noop;
|
||||||
const ignoreContentChange = _ignoreContentChange || noop;
|
const ignoreContentChange = _ignoreContentChange || noop;
|
||||||
const targetChangedAttrs: string[] = [];
|
const totalChangedNodes: Set<Node> = new Set();
|
||||||
const totalAddedNodes: Node[] = [];
|
const targetChangedAttrs: Set<string> = new Set();
|
||||||
let targetStyleChanged = false;
|
let targetStyleChanged = false;
|
||||||
let contentChanged = false;
|
let contentChanged = false;
|
||||||
let childListChanged = false;
|
let childListChanged = false;
|
||||||
|
|
||||||
each(mutations, (mutation) => {
|
each(mutations, (mutation) => {
|
||||||
const { attributeName, target: mutationTarget, type, oldValue, addedNodes } = mutation;
|
const {
|
||||||
|
attributeName,
|
||||||
|
target: mutationTarget,
|
||||||
|
type,
|
||||||
|
oldValue,
|
||||||
|
addedNodes,
|
||||||
|
removedNodes,
|
||||||
|
} = mutation;
|
||||||
const isAttributesType = type === 'attributes';
|
const isAttributesType = type === 'attributes';
|
||||||
const isChildListType = type === 'childList';
|
const isChildListType = type === 'childList';
|
||||||
const targetIsMutationTarget = target === mutationTarget;
|
const targetIsMutationTarget = target === mutationTarget;
|
||||||
@@ -212,9 +225,9 @@ export const createDOMObserver = <ContentObserver extends boolean>(
|
|||||||
indexOf(finalStyleChangingAttributes, attributeName) > -1 && attributeChanged;
|
indexOf(finalStyleChangingAttributes, attributeName) > -1 && attributeChanged;
|
||||||
|
|
||||||
// if is content observer and something changed in children
|
// if is content observer and something changed in children
|
||||||
if (isContentObserver && !targetIsMutationTarget) {
|
if (isContentObserver && (isChildListType || !targetIsMutationTarget)) {
|
||||||
const notOnlyAttrChanged = !isAttributesType;
|
const notOnlyAttrChanged = !isAttributesType;
|
||||||
const contentAttrChanged = isAttributesType && styleChangingAttrChanged;
|
const contentAttrChanged = isAttributesType && attributeChanged;
|
||||||
const isNestedTarget =
|
const isNestedTarget =
|
||||||
contentAttrChanged && _nestedTargetSelector && is(mutationTarget, _nestedTargetSelector);
|
contentAttrChanged && _nestedTargetSelector && is(mutationTarget, _nestedTargetSelector);
|
||||||
const baseAssertion = isNestedTarget
|
const baseAssertion = isNestedTarget
|
||||||
@@ -223,7 +236,8 @@ export const createDOMObserver = <ContentObserver extends boolean>(
|
|||||||
const contentFinalChanged =
|
const contentFinalChanged =
|
||||||
baseAssertion && !ignoreContentChange(mutation, !!isNestedTarget, target, options);
|
baseAssertion && !ignoreContentChange(mutation, !!isNestedTarget, target, options);
|
||||||
|
|
||||||
push(totalAddedNodes, addedNodes);
|
each(addedNodes, (node) => totalChangedNodes.add(node));
|
||||||
|
each(removedNodes, (node) => totalChangedNodes.add(node));
|
||||||
|
|
||||||
contentChanged = contentChanged || contentFinalChanged;
|
contentChanged = contentChanged || contentFinalChanged;
|
||||||
childListChanged = childListChanged || isChildListType;
|
childListChanged = childListChanged || isChildListType;
|
||||||
@@ -235,15 +249,15 @@ export const createDOMObserver = <ContentObserver extends boolean>(
|
|||||||
attributeChanged &&
|
attributeChanged &&
|
||||||
!ignoreTargetChange(mutationTarget, attributeName!, oldValue, attributeValue)
|
!ignoreTargetChange(mutationTarget, attributeName!, oldValue, attributeValue)
|
||||||
) {
|
) {
|
||||||
push(targetChangedAttrs, attributeName!);
|
targetChangedAttrs.add(attributeName!);
|
||||||
targetStyleChanged = targetStyleChanged || styleChangingAttrChanged;
|
targetStyleChanged = targetStyleChanged || styleChangingAttrChanged;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (childListChanged && !isEmptyArray(totalAddedNodes)) {
|
// adds / removes the new elements from the event content change
|
||||||
// adds / removes the new elements from the event content change
|
if (totalChangedNodes.size > 0) {
|
||||||
updateEventContentChangeElements((selector) =>
|
updateEventContentChangeElements((selector: string) =>
|
||||||
totalAddedNodes.reduce<Node[]>((arr, node) => {
|
from(totalChangedNodes).reduce<Node[]>((arr, node) => {
|
||||||
push(arr, find(selector, node));
|
push(arr, find(selector, node));
|
||||||
return is(node, selector) ? push(arr, node) : arr;
|
return is(node, selector) ? push(arr, node) : arr;
|
||||||
}, [])
|
}, [])
|
||||||
@@ -254,12 +268,15 @@ export const createDOMObserver = <ContentObserver extends boolean>(
|
|||||||
!fromRecords && contentChanged && (callback as DOMContentObserverCallback)(false);
|
!fromRecords && contentChanged && (callback as DOMContentObserverCallback)(false);
|
||||||
return [false] as Parameters<DOMObserverCallback<ContentObserver>>;
|
return [false] as Parameters<DOMObserverCallback<ContentObserver>>;
|
||||||
}
|
}
|
||||||
if (!isEmptyArray(targetChangedAttrs) || targetStyleChanged) {
|
|
||||||
!fromRecords &&
|
if (targetChangedAttrs.size > 0 || targetStyleChanged) {
|
||||||
(callback as DOMTargetObserverCallback)(targetChangedAttrs, targetStyleChanged);
|
const args: Parameters<DOMTargetObserverCallback> = [
|
||||||
return [targetChangedAttrs, targetStyleChanged] as Parameters<
|
from(targetChangedAttrs),
|
||||||
DOMObserverCallback<ContentObserver>
|
targetStyleChanged,
|
||||||
>;
|
];
|
||||||
|
!fromRecords && (callback as DOMTargetObserverCallback).apply(0, args);
|
||||||
|
|
||||||
|
return args as Parameters<DOMObserverCallback<ContentObserver>>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const mutationObserver: MutationObserver = new MutationObserverConstructor!((mutations) =>
|
const mutationObserver: MutationObserver = new MutationObserverConstructor!((mutations) =>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from 'observers/domObserver';
|
||||||
|
export * from 'observers/sizeObserver';
|
||||||
|
export * from 'observers/trinsicObserver';
|
||||||
@@ -34,9 +34,13 @@ import {
|
|||||||
classNameScrollbar,
|
classNameScrollbar,
|
||||||
classNameViewportArrange,
|
classNameViewportArrange,
|
||||||
} from 'classnames';
|
} from 'classnames';
|
||||||
import { createSizeObserver, SizeObserverCallbackParams } from 'observers/sizeObserver';
|
import {
|
||||||
import { createTrinsicObserver } from 'observers/trinsicObserver';
|
createSizeObserver,
|
||||||
import { createDOMObserver, DOMObserver } from 'observers/domObserver';
|
createTrinsicObserver,
|
||||||
|
createDOMObserver,
|
||||||
|
DOMObserver,
|
||||||
|
SizeObserverCallbackParams,
|
||||||
|
} from 'observers';
|
||||||
import type { SetupState, SetupUpdateCheckOption } from 'setups';
|
import type { SetupState, SetupUpdateCheckOption } from 'setups';
|
||||||
import type { StructureSetupState } from 'setups/structureSetup';
|
import type { StructureSetupState } from 'setups/structureSetup';
|
||||||
import type { StructureSetupElementsObj } from 'setups/structureSetup/structureSetup.elements';
|
import type { StructureSetupElementsObj } from 'setups/structureSetup/structureSetup.elements';
|
||||||
@@ -318,7 +322,6 @@ export const createStructureSetupObservers = (
|
|||||||
true,
|
true,
|
||||||
onContentMutation,
|
onContentMutation,
|
||||||
{
|
{
|
||||||
_styleChangingAttributes: contentMutationObserverAttr.concat(attributes || []),
|
|
||||||
_attributes: contentMutationObserverAttr.concat(attributes || []),
|
_attributes: contentMutationObserverAttr.concat(attributes || []),
|
||||||
_eventContentChange: elementEvents,
|
_eventContentChange: elementEvents,
|
||||||
_nestedTargetSelector: hostSelector,
|
_nestedTargetSelector: hostSelector,
|
||||||
|
|||||||
@@ -0,0 +1,478 @@
|
|||||||
|
import { createDOMObserver } from 'observers';
|
||||||
|
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
jest.mock('support/compatibility/apis', () => {
|
||||||
|
const originalModule = jest.requireActual('support/compatibility/apis');
|
||||||
|
const mockRAF = (arg: any) => setTimeout(arg, 0);
|
||||||
|
return {
|
||||||
|
...originalModule,
|
||||||
|
// @ts-ignore
|
||||||
|
rAF: jest.fn().mockImplementation((...args) => mockRAF(...args)),
|
||||||
|
cAF: jest.fn().mockImplementation((...args) => clearTimeout(...args)),
|
||||||
|
// @ts-ignore
|
||||||
|
setT: jest.fn().mockImplementation((...args) => setTimeout(...args)),
|
||||||
|
clearT: jest.fn().mockImplementation((...args) => clearTimeout(...args)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createDOMObserver', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.outerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('target observer', () => {
|
||||||
|
test('basic functionality', async () => {
|
||||||
|
document.body.innerHTML = '<div></div>';
|
||||||
|
const callback = jest.fn();
|
||||||
|
const div = document.body.firstElementChild as HTMLElement;
|
||||||
|
const [destroy, update] = createDOMObserver(document.body, false, callback, {
|
||||||
|
_attributes: ['style', 'class', 'id'],
|
||||||
|
_styleChangingAttributes: ['id'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(destroy).toEqual(expect.any(Function));
|
||||||
|
expect(update).toEqual(expect.any(Function));
|
||||||
|
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
document.body.style.width = '100px';
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(callback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(['style'], false);
|
||||||
|
|
||||||
|
document.body.classList.add('test');
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(callback).toHaveBeenCalledTimes(2);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(['class'], false);
|
||||||
|
|
||||||
|
document.body.id = 'test';
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(callback).toHaveBeenCalledTimes(3);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(['id'], true);
|
||||||
|
|
||||||
|
document.body.style.width = '';
|
||||||
|
document.body.classList.remove('test');
|
||||||
|
document.body.id = '';
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(callback).toHaveBeenCalledTimes(4);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(['style', 'class', 'id'], true);
|
||||||
|
|
||||||
|
document.body.setAttribute('data-something', 'hi');
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(callback).toHaveBeenCalledTimes(4);
|
||||||
|
|
||||||
|
div.style.width = '100px';
|
||||||
|
div.classList.add('test');
|
||||||
|
div.id = 'test';
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(callback).toHaveBeenCalledTimes(4);
|
||||||
|
|
||||||
|
div.append(document.createElement('div'));
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(callback).toHaveBeenCalledTimes(4);
|
||||||
|
|
||||||
|
div.remove();
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(callback).toHaveBeenCalledTimes(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('update', async () => {
|
||||||
|
document.body.innerHTML = '<div></div>';
|
||||||
|
const callback = jest.fn();
|
||||||
|
const [destroy, update] = createDOMObserver(document.body, false, callback, {
|
||||||
|
_attributes: ['style', 'class', 'id'],
|
||||||
|
_styleChangingAttributes: ['data-stylechanged'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(destroy).toEqual(expect.any(Function));
|
||||||
|
expect(update).toEqual(expect.any(Function));
|
||||||
|
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
document.body.style.width = '100px';
|
||||||
|
document.body.classList.add('test');
|
||||||
|
document.body.id = 'test';
|
||||||
|
document.body.classList.add('test2');
|
||||||
|
const [changedAttrs, styleChanged] = update() as any;
|
||||||
|
|
||||||
|
expect(changedAttrs).toEqual(['style', 'class', 'id']);
|
||||||
|
expect(styleChanged).toEqual(false);
|
||||||
|
|
||||||
|
document.body.setAttribute('data-stylechanged', 'true');
|
||||||
|
document.body.id = '';
|
||||||
|
|
||||||
|
const [changedAttrs2, styleChanged2] = update() as any;
|
||||||
|
|
||||||
|
expect(changedAttrs2).toEqual(['data-stylechanged', 'id']);
|
||||||
|
expect(styleChanged2).toEqual(true);
|
||||||
|
|
||||||
|
document.body.removeAttribute('data-stylechanged');
|
||||||
|
document.body.id = 'something';
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(1);
|
||||||
|
const changed = update();
|
||||||
|
expect(changed).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy', async () => {
|
||||||
|
document.body.innerHTML = '<div></div>';
|
||||||
|
const callback = jest.fn();
|
||||||
|
const div = document.body.firstElementChild as HTMLElement;
|
||||||
|
const [destroy, update] = createDOMObserver(document.body, false, callback, {
|
||||||
|
_attributes: ['style', 'class', 'id'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(destroy).toEqual(expect.any(Function));
|
||||||
|
expect(update).toEqual(expect.any(Function));
|
||||||
|
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
document.body.style.width = '100px';
|
||||||
|
document.body.classList.add('test');
|
||||||
|
document.body.id = 'test';
|
||||||
|
document.body.classList.add('test2');
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(['style', 'class', 'id'], false);
|
||||||
|
|
||||||
|
destroy();
|
||||||
|
|
||||||
|
document.body.style.width = '';
|
||||||
|
document.body.classList.remove('test');
|
||||||
|
document.body.id = '';
|
||||||
|
document.body.setAttribute('data-something', 'hi');
|
||||||
|
div.append(document.createElement('div'));
|
||||||
|
div.remove();
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(update()).toBeFalsy();
|
||||||
|
expect(destroy()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('content observer', () => {
|
||||||
|
test('basic functionality', async () => {
|
||||||
|
document.body.innerHTML = '<div></div>';
|
||||||
|
const callback = jest.fn();
|
||||||
|
const div = document.body.firstElementChild as HTMLElement;
|
||||||
|
const [destroy, update] = createDOMObserver(document.body, true, callback, {
|
||||||
|
_attributes: ['style', 'class', 'id'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(destroy).toEqual(expect.any(Function));
|
||||||
|
expect(update).toEqual(expect.any(Function));
|
||||||
|
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
document.body.style.width = '100px';
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
document.body.classList.add('test');
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
document.body.id = 'test';
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
document.body.setAttribute('data-something', 'hi');
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
div.style.width = '100px';
|
||||||
|
div.classList.add('test');
|
||||||
|
div.id = 'test';
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(callback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(callback).toHaveBeenCalledWith(false);
|
||||||
|
|
||||||
|
div.append(document.createElement('div'));
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(callback).toHaveBeenCalledTimes(2);
|
||||||
|
expect(callback).toHaveBeenCalledWith(false);
|
||||||
|
|
||||||
|
document.body.append(document.createElement('div'));
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(callback).toHaveBeenCalledTimes(3);
|
||||||
|
expect(callback).toHaveBeenCalledWith(false);
|
||||||
|
|
||||||
|
div.remove();
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(callback).toHaveBeenCalledTimes(4);
|
||||||
|
expect(callback).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ignoreContentChange', async () => {
|
||||||
|
document.body.innerHTML = '<div></div>';
|
||||||
|
const callback = jest.fn();
|
||||||
|
const ignoreContentChange = jest.fn(() => true);
|
||||||
|
const div = document.body.firstElementChild as HTMLElement;
|
||||||
|
const [destroy, update] = createDOMObserver(document.body, true, callback, {
|
||||||
|
_attributes: ['style', 'class', 'id'],
|
||||||
|
_ignoreContentChange: ignoreContentChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(destroy).toEqual(expect.any(Function));
|
||||||
|
expect(update).toEqual(expect.any(Function));
|
||||||
|
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
expect(ignoreContentChange).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
div.style.width = '100px';
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(ignoreContentChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
div.classList.add('test');
|
||||||
|
div.id = 'test';
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(ignoreContentChange).toHaveBeenCalledTimes(3);
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
div.append(document.createElement('div'));
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(ignoreContentChange).toHaveBeenCalledTimes(4);
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
document.body.append(document.createElement('div'));
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(ignoreContentChange).toHaveBeenCalledTimes(5);
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
div.remove();
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(ignoreContentChange).toHaveBeenCalledTimes(6);
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('eventContentChange', async () => {
|
||||||
|
document.body.innerHTML = '<div></div><p></p>';
|
||||||
|
const callback = jest.fn();
|
||||||
|
const div = document.body.firstElementChild as HTMLElement;
|
||||||
|
const [destroy, update] = createDOMObserver(document.body, true, callback, {
|
||||||
|
_attributes: ['style', 'class', 'id'],
|
||||||
|
_eventContentChange: [
|
||||||
|
['*', 'click'],
|
||||||
|
['*', 'keydown'],
|
||||||
|
['span', 'transitionend animationend'],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(destroy).toEqual(expect.any(Function));
|
||||||
|
expect(update).toEqual(expect.any(Function));
|
||||||
|
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
const paragraph = document.createElement('p');
|
||||||
|
const span = document.createElement('span');
|
||||||
|
const appendedDiv = document.createElement('div');
|
||||||
|
appendedDiv.append(span);
|
||||||
|
div.append(appendedDiv);
|
||||||
|
div.append(paragraph);
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(false);
|
||||||
|
|
||||||
|
div.dispatchEvent(new Event('click'));
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(2);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(true);
|
||||||
|
|
||||||
|
div.dispatchEvent(new Event('keydown'));
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(3);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(true);
|
||||||
|
|
||||||
|
paragraph.dispatchEvent(new Event('click'));
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(4);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(true);
|
||||||
|
|
||||||
|
paragraph.dispatchEvent(new Event('keydown'));
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(5);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(true);
|
||||||
|
|
||||||
|
// debounced to one update
|
||||||
|
div.dispatchEvent(new Event('click'));
|
||||||
|
div.dispatchEvent(new Event('keydown'));
|
||||||
|
paragraph.dispatchEvent(new Event('click'));
|
||||||
|
paragraph.dispatchEvent(new Event('keydown'));
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(6);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(true);
|
||||||
|
|
||||||
|
span.dispatchEvent(new Event('click'));
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(7);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(true);
|
||||||
|
|
||||||
|
span.dispatchEvent(new Event('transitionend'));
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(8);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(true);
|
||||||
|
|
||||||
|
span.dispatchEvent(new Event('animationend'));
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(9);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(true);
|
||||||
|
|
||||||
|
span.dispatchEvent(new Event('transitionend'));
|
||||||
|
span.dispatchEvent(new Event('animationend'));
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(10);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(true);
|
||||||
|
|
||||||
|
appendedDiv.dispatchEvent(new Event('click'));
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(11);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(true);
|
||||||
|
|
||||||
|
// remove from target and trigger events from new location
|
||||||
|
document.body.parentElement!.append(appendedDiv);
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(12);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(false);
|
||||||
|
|
||||||
|
span.dispatchEvent(new Event('transitionend'));
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(12);
|
||||||
|
|
||||||
|
appendedDiv.dispatchEvent(new Event('click'));
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(12);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('update', async () => {
|
||||||
|
document.body.innerHTML = '<div></div>';
|
||||||
|
const callback = jest.fn();
|
||||||
|
const div = document.body.firstElementChild as HTMLElement;
|
||||||
|
const [destroy, update] = createDOMObserver(document.body, true, callback, {
|
||||||
|
_attributes: ['style', 'class', 'id'],
|
||||||
|
_eventContentChange: [
|
||||||
|
['*', 'click'],
|
||||||
|
['*', 'keydown'],
|
||||||
|
['span', 'transitionend animationend'],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(destroy).toEqual(expect.any(Function));
|
||||||
|
expect(update).toEqual(expect.any(Function));
|
||||||
|
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
div.style.width = '100px';
|
||||||
|
div.classList.add('test');
|
||||||
|
div.id = 'test';
|
||||||
|
|
||||||
|
const [contentChangedThroughEvent] = update() as any;
|
||||||
|
expect(contentChangedThroughEvent).toBe(false);
|
||||||
|
|
||||||
|
const paragraph = document.createElement('p');
|
||||||
|
const span = document.createElement('span');
|
||||||
|
const appendedDiv = document.createElement('div');
|
||||||
|
appendedDiv.append(span);
|
||||||
|
div.append(appendedDiv);
|
||||||
|
div.append(paragraph);
|
||||||
|
|
||||||
|
const [contentChangedThroughEvent2] = update() as any;
|
||||||
|
expect(contentChangedThroughEvent2).toBe(false);
|
||||||
|
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
div.dispatchEvent(new Event('click'));
|
||||||
|
div.dispatchEvent(new Event('keydown'));
|
||||||
|
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
const change = update();
|
||||||
|
expect(change).toBeFalsy();
|
||||||
|
expect(callback).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy', async () => {
|
||||||
|
document.body.innerHTML = '<div></div><p></p>';
|
||||||
|
const callback = jest.fn();
|
||||||
|
const div = document.body.firstElementChild as HTMLElement;
|
||||||
|
const [destroy, update] = createDOMObserver(document.body, true, callback, {
|
||||||
|
_attributes: ['style', 'class', 'id'],
|
||||||
|
_eventContentChange: [
|
||||||
|
['*', 'click'],
|
||||||
|
['*', 'keydown'],
|
||||||
|
['span', 'transitionend animationend'],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(destroy).toEqual(expect.any(Function));
|
||||||
|
expect(update).toEqual(expect.any(Function));
|
||||||
|
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
div.style.width = '100px';
|
||||||
|
div.classList.add('test');
|
||||||
|
div.id = 'test';
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(callback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(callback).toHaveBeenCalledWith(false);
|
||||||
|
|
||||||
|
const paragraph = document.createElement('p');
|
||||||
|
const span = document.createElement('span');
|
||||||
|
const appendedDiv = document.createElement('div');
|
||||||
|
appendedDiv.append(span);
|
||||||
|
div.append(appendedDiv);
|
||||||
|
div.append(paragraph);
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(2);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(false);
|
||||||
|
|
||||||
|
destroy();
|
||||||
|
|
||||||
|
div.dispatchEvent(new Event('click'));
|
||||||
|
div.dispatchEvent(new Event('keydown'));
|
||||||
|
paragraph.dispatchEvent(new Event('click'));
|
||||||
|
paragraph.dispatchEvent(new Event('keydown'));
|
||||||
|
span.dispatchEvent(new Event('click'));
|
||||||
|
span.dispatchEvent(new Event('transitionend'));
|
||||||
|
span.dispatchEvent(new Event('animationend'));
|
||||||
|
appendedDiv.dispatchEvent(new Event('click'));
|
||||||
|
document.body.parentElement!.append(appendedDiv);
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
span.dispatchEvent(new Event('transitionend'));
|
||||||
|
appendedDiv.dispatchEvent(new Event('click'));
|
||||||
|
div.style.width = '100px';
|
||||||
|
div.classList.add('test');
|
||||||
|
div.id = 'test';
|
||||||
|
await Promise.resolve();
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(2);
|
||||||
|
expect(update()).toBeFalsy();
|
||||||
|
expect(destroy()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+1
-1
@@ -80,7 +80,7 @@ const fillBody = (
|
|||||||
return getSnapshot();
|
return getSnapshot();
|
||||||
};
|
};
|
||||||
const clearBody = () => {
|
const clearBody = () => {
|
||||||
document.body.innerHTML = '';
|
document.body.outerHTML = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getElements = (targetType: TargetType) => {
|
const getElements = (targetType: TargetType) => {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
on,
|
on,
|
||||||
} from 'support';
|
} from 'support';
|
||||||
|
|
||||||
import { createDOMObserver } from 'observers/domObserver';
|
import { createDOMObserver } from 'observers';
|
||||||
|
|
||||||
type DOMContentObserverResult = {
|
type DOMContentObserverResult = {
|
||||||
contentChange: boolean;
|
contentChange: boolean;
|
||||||
@@ -168,7 +168,6 @@ const createContentDomOserver = (
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
_styleChangingAttributes: attrs,
|
|
||||||
_attributes: attrs,
|
_attributes: attrs,
|
||||||
_eventContentChange: eventContentChange,
|
_eventContentChange: eventContentChange,
|
||||||
_nestedTargetSelector: hostSelector,
|
_nestedTargetSelector: hostSelector,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from '@~local/browser-testing';
|
} from '@~local/browser-testing';
|
||||||
import { hasDimensions, offsetSize, WH, style } from 'support';
|
import { hasDimensions, offsetSize, WH, style } from 'support';
|
||||||
import { addPlugin, sizeObserverPlugin } from 'plugins';
|
import { addPlugin, sizeObserverPlugin } from 'plugins';
|
||||||
import { createSizeObserver } from 'observers/sizeObserver';
|
import { createSizeObserver } from 'observers';
|
||||||
|
|
||||||
if (!window.ResizeObserver) {
|
if (!window.ResizeObserver) {
|
||||||
addPlugin(sizeObserverPlugin);
|
addPlugin(sizeObserverPlugin);
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ import {
|
|||||||
waitForOrFailTest,
|
waitForOrFailTest,
|
||||||
} from '@~local/browser-testing';
|
} from '@~local/browser-testing';
|
||||||
import { offsetSize } from 'support';
|
import { offsetSize } from 'support';
|
||||||
import { createTrinsicObserver } from 'observers/trinsicObserver';
|
import { createTrinsicObserver } from 'observers';
|
||||||
import { addPlugin, sizeObserverPlugin } from 'plugins';
|
import { addPlugin, sizeObserverPlugin } from 'plugins';
|
||||||
|
|
||||||
if (!window.ResizeObserver) {
|
if (!window.ResizeObserver) {
|
||||||
|
|||||||
Reference in New Issue
Block a user