UNPKG

@v4fire/client

Version:

V4Fire client core library

401 lines (341 loc) • 10.6 kB
/*! * V4Fire Client Core * https://github.com/V4Fire/Client * * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ import type { Page } from 'playwright'; import test from 'tests/config/unit/test'; import { createInitialState as createInitialStateObj } from 'base/b-virtual-scroll-new/modules/state/helpers'; import type { MountedChild, ComponentItem, VirtualScrollState, MountedItem } from 'base/b-virtual-scroll-new/interface'; import { componentEvents, componentObserverLocalEvents } from 'base/b-virtual-scroll-new/const'; import { paginationHandler } from 'tests/network-interceptors/pagination'; import { RequestInterceptor } from 'tests/helpers'; import { VirtualScrollComponentObject } from 'base/b-virtual-scroll-new/test/api/component-object'; import type { DataConveyor, DataItemCtor, MountedItemCtor, StateApi, VirtualScrollTestHelpers, MountedSeparatorCtor, IndexedObj } from 'base/b-virtual-scroll-new/test/api/helpers/interface'; export * from 'base/b-virtual-scroll-new/test/api/component-object'; /** * Creates a helper API for convenient testing of the `b-virtual-scroll-new` component * @param page - the page object representing the testing page. */ export async function createTestHelpers(page: Page): Promise<VirtualScrollTestHelpers> { const component = new VirtualScrollComponentObject(page), initLoadSpy = await component.spyOn('initLoad', {proto: true}), provider = new RequestInterceptor(page, /api/), state = createStateApi({}, createDataConveyor( createIndexedObj, createMountedSeparator, createMountedItem )); provider.response(paginationHandler); return { component, initLoadSpy, provider, state }; } /** * Creates a data conveyor that accumulates added data and can return it. * * For example, the `extractStateFromDataConveyor` function can be used to generate the component's data state based on * the provided data conveyor. * * @param itemsCtor - the constructor function for data items. * @param separatorCtor - the constructor function for mounted separators. * @param mountedCtor - the constructor function for mounted items. */ export function createDataConveyor<DATA>( itemsCtor: DataItemCtor<DATA>, separatorCtor: MountedSeparatorCtor<DATA>, mountedCtor: MountedItemCtor<DATA> ): DataConveyor { let data = <unknown[]>[], items = <MountedItem[]>[], childList = <MountedChild[]>[], dataChunks = <unknown[][]>[]; let dataI = 0, itemsI = 0, childI = 0, page = 0, total: CanUndef<number> = undefined; const obj: DataConveyor = { addData(count: number) { const newData = createChunk(count, itemsCtor, dataI); data.push(...newData); dataChunks.push(newData); dataI = data.length; page++; return newData; }, addItems(count: number) { const newData = createChunk(count, itemsCtor, itemsI), itemsData = createFromData(newData, mountedCtor, itemsI); items.push(...itemsData); childList.push(...itemsData); itemsI = itemsData.length; childI = childList.length; return itemsData; }, addSeparators(count: number) { const newData = createChunk(count, itemsCtor, childI), separatorsData = createFromData(newData, separatorCtor, childI); childList.push(...separatorsData); childI = childList.length; return separatorsData; }, addChild(list: ComponentItem[]) { let itemsCounter = 0; const newChild = <MountedChild[]>list.map((child, i) => { const v = { childIndex: childI + i, node: <any>test.expect.any(String), ...child }; if (child.type === 'item') { Object.assign(v, { itemIndex: items.length + itemsCounter }); itemsCounter++; } return v; }); items.push(...<MountedItem[]>newChild.filter((item) => item.type === 'item')); childList.push(...newChild); childI = childList.length; itemsI = items.length; return childList; }, getDataChunk(index: number) { return dataChunks[index]; }, setTotal(newTotal: number) { total = newTotal; return total; }, reset() { dataI = 0; itemsI = 0; childI = 0; page = 0; total = undefined; childList = []; items = []; data = []; dataChunks = []; }, get items() { return items; }, get total() { return total; }, get childList() { return childList; }, get data() { return data; }, get page() { return page; }, get lastLoadedData() { return dataChunks[dataChunks.length - 1] ?? []; } }; return obj; } /** * Creates an API for convenient manipulation of a component's state fork. * * @param initial - the initial partial state of the component. * @param dataConveyor - the data conveyor used for managing data within the component. */ export function createStateApi( initial: Partial<VirtualScrollState>, dataConveyor: DataConveyor ): StateApi { let state = createInitialState(initial), settled = {}; const obj: StateApi = { compile(override?: Partial<VirtualScrollState>): VirtualScrollState { const compiled = { ...state, ...extractStateFromDataConveyor(dataConveyor) }; Object.keys(settled).forEach((key) => { compiled[key] = settled[key]; }); if (override) { Object.keys(override).forEach((key) => { compiled[key] = override[key]; }); } return compiled; }, set(props: Partial<VirtualScrollState>): StateApi { Object.keys(props).forEach((key) => { settled[key] = props[key]; state[key] = props[key]; }); return obj; }, reset(): void { state = createInitialState(initial); settled = {}; dataConveyor.reset(); }, data: dataConveyor }; return obj; } /** * Creates the "initial" component state and returns it. * Since this state is intended for comparison in tests, some fields use `expect.any` since they are not "stable". * * @param state - the partial component state to override the default values. */ export function createInitialState(state: Partial<VirtualScrollState>): VirtualScrollState { return { ...createInitialStateObj(), maxViewedItem: Object.cast(test.expect.any(Number)), maxViewedChild: Object.cast(test.expect.any(Number)), remainingItems: Object.cast(test.expect.any(Number)), remainingChildren: Object.cast(test.expect.any(Number)), isLoadingInProgress: Object.cast(test.expect.any(Boolean)), dataOffset: Object.cast(test.expect.any(Number)), ...state }; } /** * Extracts state data from the data conveyor and returns it * @param conveyor - the data conveyor to extract state data from. */ export function extractStateFromDataConveyor(conveyor: DataConveyor): Pick<VirtualScrollState, 'data' | 'lastLoadedData' | 'lastLoadedRawData' | 'items' | 'childList'> { return { data: [...conveyor.data], lastLoadedData: [...conveyor.lastLoadedData], lastLoadedRawData: conveyor.page === 0 ? undefined : { data: [...conveyor.lastLoadedData], ...(conveyor.total != null ? {total: conveyor.total} : undefined) }, items: [...conveyor.items], childList: [...conveyor.childList] }; } /** * Calls `objCtor` on each element of the `data` array and returns a new array with the results. * * @param data - the array of data elements. * @param objCtor - the constructor function to create new objects from the data elements. * @param start - the starting index for creating objects (default: 0). */ export function createFromData<DATA, ITEM>( data: DATA[], objCtor: (data: DATA, i: number) => ITEM, start: number = 0 ): ITEM[] { return data.map((item, i) => objCtor(item, start + i)); } /** * Creates a simple object that matches the {@link MountedItem} interface * @param data - the object with index of the mounted item. */ export function createMountedItem(data: IndexedObj): MountedItem { return { itemIndex: data.i, childIndex: data.i, props: { 'data-index': data.i }, key: Object.cast(undefined), item: 'section', type: 'item', node: <any>test.expect.anything(), meta: { data: test.expect.any(Object) } }; } /** * Creates a simple object that matches the {@link MountedChild}` interface * @param data - the object with index of the mounted child. */ export function createMountedSeparator(data: IndexedObj): MountedChild { return { childIndex: data.i, props: { 'data-index': data.i }, key: Object.cast(undefined), item: 'section', type: 'separator', node: <any>test.expect.anything() }; } /** * Creates an array of data with the specified length and uses the `itemCtor` function to build items within the array. * The `start` parameter can be used to specify the starting index that will be passed to the `itemCtor` function. * * @param count - the number of items to create. * @param itemCtor - the constructor function to create items. * @param [start] - the starting index (default: 0). */ export function createChunk<DATA extends unknown = unknown>( count: number, itemCtor: (i: number) => DATA, start: number = 0 ): DATA[] { return Array.from(new Array(count), (_, i) => itemCtor(start + i)); } /** * Creates a simple indexed object * @param i - the index of the object. */ export function createIndexedObj(i: number): IndexedObj { return {i}; } /** * Filters emitter emit calls and removes unnecessary events. * It only keeps component events. * * @param emitCalls - the array of emit calls. * @param [filterObserverEvents] - whether to filter out observer events (default: true). * @param [allowedEvents] */ export function filterEmitterCalls( emitCalls: unknown[][], filterObserverEvents: boolean = true, allowedEvents: string[] = [] ): unknown[][] { return emitCalls.filter(([event]) => Object.isString(event) && (Boolean(componentEvents[event]) || allowedEvents.includes(event)) && (filterObserverEvents ? !(event in componentObserverLocalEvents) : true)); } /** * Filters emitter emit results and removes unnecessary events. * It only keeps component events. * * @param results - the array of emit results. * @param [filterObserverEvents] - whether to filter out observer events (default: true). * @param [allowedEvents] */ export function filterEmitterResults<VAL extends [event: string, ...rest: any[]]>( results: Array<JestMockResult<VAL>>, filterObserverEvents: boolean = true, allowedEvents: string[] = [] ): VAL[] { const filtered = results.filter(({value: [event]}) => Object.isString(event) && (Boolean(componentEvents[event]) || allowedEvents.includes(event)) && (filterObserverEvents ? !(event in componentObserverLocalEvents) : true)); return filtered.map(({value}) => value); }