From d172f8d6f7361f96205cba787366460b8aca9907 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 20 Jan 2023 14:46:15 +0300 Subject: [PATCH 001/159] Working on b-scrolly --- src/components/base/b-scrolly/README.md | 3 +++ src/components/base/b-scrolly/b-scrolly.ss | 17 +++++++++++++++++ src/components/base/b-scrolly/b-scrolly.styl | 0 src/components/base/b-scrolly/b-scrolly.ts | 17 +++++++++++++++++ src/components/base/b-scrolly/index.js | 10 ++++++++++ src/components/base/b-scrolly/interface.ts | 3 +++ src/components/base/b-scrolly/modules/map.ts | 6 ++++++ .../base/b-scrolly/modules/observers.ts | 6 ++++++ .../b-virtual-scroll/modules/chunk-render.ts | 1 + 9 files changed, 63 insertions(+) create mode 100644 src/components/base/b-scrolly/README.md create mode 100644 src/components/base/b-scrolly/b-scrolly.ss create mode 100644 src/components/base/b-scrolly/b-scrolly.styl create mode 100644 src/components/base/b-scrolly/b-scrolly.ts create mode 100644 src/components/base/b-scrolly/index.js create mode 100644 src/components/base/b-scrolly/interface.ts create mode 100644 src/components/base/b-scrolly/modules/map.ts create mode 100644 src/components/base/b-scrolly/modules/observers.ts diff --git a/src/components/base/b-scrolly/README.md b/src/components/base/b-scrolly/README.md new file mode 100644 index 0000000000..397f787423 --- /dev/null +++ b/src/components/base/b-scrolly/README.md @@ -0,0 +1,3 @@ +## Загрузка и хранение данных + +## Отрисовка элементов diff --git a/src/components/base/b-scrolly/b-scrolly.ss b/src/components/base/b-scrolly/b-scrolly.ss new file mode 100644 index 0000000000..230c335066 --- /dev/null +++ b/src/components/base/b-scrolly/b-scrolly.ss @@ -0,0 +1,17 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +- namespace [%fileName%] + +- include 'components/super/i-data'|b as placeholder + +- template index() extends ['i-data'].index + - block body + < .&__wrapper + < .&__item v-for = item in items + += self.slot('item', {':item': 'item'}) diff --git a/src/components/base/b-scrolly/b-scrolly.styl b/src/components/base/b-scrolly/b-scrolly.styl new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts new file mode 100644 index 0000000000..cf86aba227 --- /dev/null +++ b/src/components/base/b-scrolly/b-scrolly.ts @@ -0,0 +1,17 @@ +import iData, { component, field, RequestParams } from 'components/super/i-data/i-data'; + +@component() +export default class bScrolly extends iData { + @field({forceUpdate: false}) + readonly data!: unknown[]; + + // @ts-ignore (getter instead readonly) + override get requestParams(): RequestParams { + return { + get: {} + }; + } +} + +// Модель дозапросов - остается такой же, +// изменение request влечет за собой перерендер, а requestQuery возвращает постранично параметры diff --git a/src/components/base/b-scrolly/index.js b/src/components/base/b-scrolly/index.js new file mode 100644 index 0000000000..dc7adca297 --- /dev/null +++ b/src/components/base/b-scrolly/index.js @@ -0,0 +1,10 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +package('b-scrolly') + .extends('i-data'); diff --git a/src/components/base/b-scrolly/interface.ts b/src/components/base/b-scrolly/interface.ts new file mode 100644 index 0000000000..0386778934 --- /dev/null +++ b/src/components/base/b-scrolly/interface.ts @@ -0,0 +1,3 @@ +export interface Item { + // ... +} diff --git a/src/components/base/b-scrolly/modules/map.ts b/src/components/base/b-scrolly/modules/map.ts new file mode 100644 index 0000000000..bffe6059a9 --- /dev/null +++ b/src/components/base/b-scrolly/modules/map.ts @@ -0,0 +1,6 @@ +/** + * Создает карту высот компонентов. + */ +export function createMap(): void { + // .... +} diff --git a/src/components/base/b-scrolly/modules/observers.ts b/src/components/base/b-scrolly/modules/observers.ts new file mode 100644 index 0000000000..8ea1bbab35 --- /dev/null +++ b/src/components/base/b-scrolly/modules/observers.ts @@ -0,0 +1,6 @@ +/** + * Создает наблюдателя за изменением высоты компонентов. + */ +export function createResizeObserver(): void { + // ... +} diff --git a/src/components/base/b-virtual-scroll/modules/chunk-render.ts b/src/components/base/b-virtual-scroll/modules/chunk-render.ts index 62bde7f358..95cff0817d 100644 --- a/src/components/base/b-virtual-scroll/modules/chunk-render.ts +++ b/src/components/base/b-virtual-scroll/modules/chunk-render.ts @@ -379,6 +379,7 @@ export default class ChunkRender extends Friend { this.render(); } } + } /** From 2eda52d4035981a092201f65be68ae5e840275a4 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 22 Mar 2023 17:39:21 +0300 Subject: [PATCH 002/159] Initial commit --- components-lock.json | 36 ++++++- src/components/base/b-scrolly/README.md | 3 + src/components/base/b-scrolly/b-scrolly.ss | 18 ++++ src/components/base/b-scrolly/b-scrolly.styl | 16 +++ src/components/base/b-scrolly/b-scrolly.ts | 99 +++++++++++++++++++ src/components/base/b-scrolly/index.js | 10 ++ .../base/b-scrolly/test/unit/render.ts | 46 +++++++++ .../pages/p-v4-components-demo/index.js | 1 + 8 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 src/components/base/b-scrolly/README.md create mode 100644 src/components/base/b-scrolly/b-scrolly.ss create mode 100644 src/components/base/b-scrolly/b-scrolly.styl create mode 100644 src/components/base/b-scrolly/b-scrolly.ts create mode 100644 src/components/base/b-scrolly/index.js create mode 100644 src/components/base/b-scrolly/test/unit/render.ts diff --git a/components-lock.json b/components-lock.json index b44eacd2a7..ac4be81f68 100644 --- a/components-lock.json +++ b/components-lock.json @@ -1,5 +1,5 @@ { - "hash": "616c42c562d59e36a0531ca09a0838c78ef5cfe70c388d0a0695753efd8a31da", + "hash": "119d10ed0c1fea13cd3496c4968c7537a00374ccb00828b1afacfb36380ae7ba", "data": { "%data": "%data:Map", "%data:Map": [ @@ -845,6 +845,38 @@ "etpl": null } ], + [ + "b-scrolly", + { + "index": "src/components/base/b-scrolly/index.js", + "declaration": { + "name": "b-scrolly", + "parent": "i-data", + "dependencies": [], + "libs": [] + }, + "name": "b-scrolly", + "parent": "i-data", + "dependencies": [], + "libs": [], + "resolvedLibs": { + "%data": "%data:Set", + "%data:Set": [] + }, + "resolvedOwnLibs": { + "%data": "%data:Set", + "%data:Set": [] + }, + "type": "block", + "mixin": false, + "logic": "src/components/base/b-scrolly/b-scrolly.ts", + "styles": [ + "src/components/base/b-scrolly/b-scrolly.styl" + ], + "tpl": "src/components/base/b-scrolly/b-scrolly.ss", + "etpl": null + } + ], [ "b-select", { @@ -1880,6 +1912,7 @@ "b-list", "b-tree", "b-window", + "b-scrolly", "b-form", "b-button", "b-icon-button", @@ -1904,6 +1937,7 @@ "b-list", "b-tree", "b-window", + "b-scrolly", "b-form", "b-button", "b-icon-button", diff --git a/src/components/base/b-scrolly/README.md b/src/components/base/b-scrolly/README.md new file mode 100644 index 0000000000..9620d98db0 --- /dev/null +++ b/src/components/base/b-scrolly/README.md @@ -0,0 +1,3 @@ +## TODO: + +1. Бенчмарк подход \ No newline at end of file diff --git a/src/components/base/b-scrolly/b-scrolly.ss b/src/components/base/b-scrolly/b-scrolly.ss new file mode 100644 index 0000000000..3f731c6cca --- /dev/null +++ b/src/components/base/b-scrolly/b-scrolly.ss @@ -0,0 +1,18 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +- namespace [%fileName%] + +- include 'components/super/i-data'|b as placeholder + +- template index() extends ['i-data'].index + - block body + < .&__wrapper + < .&__container ref = container | -test-ref = container + < .&__item v-for = el in list + += self.slot('default', {':item': 'el'}) diff --git a/src/components/base/b-scrolly/b-scrolly.styl b/src/components/base/b-scrolly/b-scrolly.styl new file mode 100644 index 0000000000..8e2d61e439 --- /dev/null +++ b/src/components/base/b-scrolly/b-scrolly.styl @@ -0,0 +1,16 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +@import "components/super/i-data/i-data.styl" + +$p = { + +} + +b-scrolly extends i-data + // ... diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts new file mode 100644 index 0000000000..ff55f59833 --- /dev/null +++ b/src/components/base/b-scrolly/b-scrolly.ts @@ -0,0 +1,99 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:components/base/b-scrolly/README.md]] + * @packageDocumentation + */ + +import iData, { component, prop, field } from 'components/super/i-data/i-data'; +import VDOM, { create, render } from 'components/friends/vdom'; + +VDOM.addToPrototype(create); +VDOM.addToPrototype(render); + +export * from 'components/super/i-data/i-data'; + +@component() +export default class bScrolly extends iData { + /** + * Если установлено в `true` то компонент будет удалять невидимые во `viewport` DOM узлы. + * + * Опция может быть полезной в случае если необходимо отрисовывать контент который состоит из 1000+ элементов. + * Для работы этой опции необходима поддержка {@link ResizeObserver}. + */ + @prop(Boolean) + readonly dropNodes: boolean = false; + + @field() + list: unknown[] = []; + + pushToList(count: number): void { + const arr = Array.from(new Array(count), () => ({id: Math.random().toString(), count})); + + performance.measure('list render', {start: performance.now()}); + this.field.set('list', this.list.concat(arr)); + + void this.nextTick().then(() => { + requestAnimationFrame(() => { + performance.measure('list render', {end: performance.now()}); + }); + }); + } + + updateList(count: number): void { + const arr = Array.from(new Array(count), () => ({id: Math.random().toString(), count})); + + performance.measure('list render', {start: performance.now()}); + this.field.set('list', arr); + + void this.nextTick().then(() => { + requestAnimationFrame(() => { + performance.measure('list render', {end: performance.now()}); + }); + }); + } + + updateListElements(count: number): void { + const arr = Array.from(new Array(count), () => ({id: Math.random().toString(), count})); + + performance.measure('list render', {start: performance.now()}); + this.field.set('list', arr); + + void this.nextTick().then(() => { + requestAnimationFrame(() => { + performance.measure('list render', {end: performance.now()}); + }); + }); + } + + pushToListCreateElement(count: number): void { + const arr = Array.from(new Array(count), () => ({id: Math.random().toString(), count})); + + performance.measure('list render', {start: performance.now()}); + const + el = this.block?.element('container'); + + const renderedNodes = arr.map(({id}) => ({ + type: 'div', + children: { + default: id.toString() + } + })); + + const nodes = this.vdom.render(this.vdom.create(renderedNodes)); + + el?.append(...nodes); + + void this.nextTick().then(() => { + requestAnimationFrame(() => { + performance.measure('list render', {end: performance.now()}); + }); + }); + } +} diff --git a/src/components/base/b-scrolly/index.js b/src/components/base/b-scrolly/index.js new file mode 100644 index 0000000000..dc7adca297 --- /dev/null +++ b/src/components/base/b-scrolly/index.js @@ -0,0 +1,10 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +package('b-scrolly') + .extends('i-data'); diff --git a/src/components/base/b-scrolly/test/unit/render.ts b/src/components/base/b-scrolly/test/unit/render.ts new file mode 100644 index 0000000000..8dd62c5894 --- /dev/null +++ b/src/components/base/b-scrolly/test/unit/render.ts @@ -0,0 +1,46 @@ +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import BOM from 'tests/helpers/bom'; + +test.use({ + actionTimeout: 0 +}); + +test.describe('b-scrolly rendering', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + ['pushToListCreateElement', 'pushToList', 'updateList'].forEach((method) => { + test(`b-scrolly perf ${method}`, async ({page, browser, context}) => { + const scrolly = await Component.createComponent(page, 'b-scrolly', { + children: { + default: ({item}) => item.id + } + }); + + await page.pause(); + + const client = await context.newCDPSession(page); + await client.send('Emulation.setCPUThrottlingRate', {rate: 8}); + + await browser.startTracing(page, {path: `${method}.json`}); + + await page.evaluate(() => performance.mark('YOLO')); + await page.evaluate(([method]) => globalThis.method = method, [method]); + + await scrolly.evaluate((c) => c[globalThis.method](100)); + await BOM.waitForIdleCallback(page); + await scrolly.evaluate((c) => c[globalThis.method](100)); + await BOM.waitForIdleCallback(page); + await scrolly.evaluate((c) => c[globalThis.method](500)); + await BOM.waitForIdleCallback(page); + await scrolly.evaluate((c) => c[globalThis.method](500)); + await BOM.waitForIdleCallback(page); + await scrolly.evaluate((c) => c[globalThis.method](500)); + + await browser.stopTracing(); + }); + }); +}); diff --git a/src/components/pages/p-v4-components-demo/index.js b/src/components/pages/p-v4-components-demo/index.js index f636dfd00d..f0343144cd 100644 --- a/src/components/pages/p-v4-components-demo/index.js +++ b/src/components/pages/p-v4-components-demo/index.js @@ -18,6 +18,7 @@ package('p-v4-components-demo') 'b-list', 'b-tree', 'b-window', + 'b-scrolly', 'b-form', 'b-button', From 4e9ccd83318f01da442ee206856bfdfc1368a17e Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 7 Apr 2023 14:30:54 +0300 Subject: [PATCH 003/159] WIP b-scrolly --- src/components/base/b-scrolly/README.md | 101 ++++++++++++++++++++- src/components/base/b-scrolly/b-scrolly.ss | 43 ++++++++- src/components/base/b-scrolly/b-scrolly.ts | 85 ++--------------- src/components/base/b-scrolly/const.ts | 16 ++++ src/components/base/b-scrolly/interface.ts | 3 + 5 files changed, 169 insertions(+), 79 deletions(-) create mode 100644 src/components/base/b-scrolly/const.ts create mode 100644 src/components/base/b-scrolly/interface.ts diff --git a/src/components/base/b-scrolly/README.md b/src/components/base/b-scrolly/README.md index 9620d98db0..d26505800e 100644 --- a/src/components/base/b-scrolly/README.md +++ b/src/components/base/b-scrolly/README.md @@ -1,3 +1,102 @@ ## TODO: -1. Бенчмарк подход \ No newline at end of file +1. Бенчмарк подходов + +## Идеи + +1. При скролле брать оффсет скролла и бинарным поиском искать элементы которые сейчас на экране должны быть и отображать их + +```typescript + const vdomCreate: typeof this['vdom']['create'] = this.vdom.create.bind(this.vdom); + const self = this; + + setNodes.forEach((node) => node.remove()); + + if (!vueInstance) { + vueInstance = new Vue({ + render: function () { + const nodes = getArray(count).map((data: DummyUser) => vdomCreate('b-dummy-user', { + attrs: { + dummyData: data, + key: data.userId, + } + })); + + // const nodes = vdomCreate('keep-alive', { + // attrs: {}, + // children: { + // default: () => getArray(count).map((data) => ({ + // type: 'b-dummy-user', + // attrs: { + // dummyData: data, + // key: data.userId, + // } + // })) + // } + // }) + + return nodes; + }, + + beforeCreate() { + let parent = self; + if (parent != null) { + const + root = Object.create(parent.$root); + + Object.defineProperty(root, '$remoteParent', { + configurable: true, + enumerable: true, + writable: true, + value: parent + }); + + Object.defineProperty(this, 'unsafe', { + configurable: true, + enumerable: true, + writable: true, + value: root + }); + } + } + }); + + const + container = document.createElement('div'); + + mountResult = vueInstance.mount(container); + + const + el = this.block?.element('container'); + el?.append(container); + + } else { + mountResult.$forceUpdate(); + } + + debugger; + + this.nextTick(() => { + Array.from(document.querySelectorAll('.b-dummy-user')).forEach((el) => { + if (setNodes.has(el)) { + return; + } + + setNodes.add(el); + el.component._forkDestroy = el.component.$destroy; + + Object.defineProperty(el.component, '$destroy', { + configurable: true, + enumerable: false, + writable: true, + value: () => { + debugger; + return false; + } + }); + + }); + }); + + console.log(setNodes); +``` \ No newline at end of file diff --git a/src/components/base/b-scrolly/b-scrolly.ss b/src/components/base/b-scrolly/b-scrolly.ss index 3f731c6cca..3c2f2d4590 100644 --- a/src/components/base/b-scrolly/b-scrolly.ss +++ b/src/components/base/b-scrolly/b-scrolly.ss @@ -14,5 +14,44 @@ - block body < .&__wrapper < .&__container ref = container | -test-ref = container - < .&__item v-for = el in list - += self.slot('default', {':item': 'el'}) + + < .&__tombstones & + ref = tombstones | + v-if = $slots['tombstone'] + . + < .&__tombstone v-for = i in tombstonesSize || chunkSize + += self.slot('tombstone') + + < .&__loader & + ref = loader | + v-if = $slots['loader'] + . + += self.slot('loader') + + < .&__retry & + ref = retry | + v-if = $slots['retry'] | + :style = {display: 'none'} + . + += self.slot('retry') + + < .&__empty & + ref = empty | + v-if = $slots['empty'] | + :style = {display: 'none'} + . + += self.slot('empty') + + < .&__done & + ref = done | + v-if = $slots['done'] | + :style = {display: 'none'} + . + += self.slot('done') + + < .&__render-next & + ref = renderNext | + v-if = $slots['renderNext'] | + :style = {display: 'none'} + . + += self.slot('renderNext') diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts index ff55f59833..68cc0c8597 100644 --- a/src/components/base/b-scrolly/b-scrolly.ts +++ b/src/components/base/b-scrolly/b-scrolly.ts @@ -11,89 +11,22 @@ * @packageDocumentation */ -import iData, { component, prop, field } from 'components/super/i-data/i-data'; +import iData, { component, prop } from 'components/super/i-data/i-data'; import VDOM, { create, render } from 'components/friends/vdom'; +import { RenderStrategy } from 'components/base/b-scrolly/const'; +import type { RenderStrategyKeys } from 'components/base/b-scrolly/interface'; + +export * from 'components/base/b-scrolly/interface'; +export * from 'components/base/b-scrolly/const'; VDOM.addToPrototype(create); VDOM.addToPrototype(render); -export * from 'components/super/i-data/i-data'; - @component() export default class bScrolly extends iData { /** - * Если установлено в `true` то компонент будет удалять невидимые во `viewport` DOM узлы. - * - * Опция может быть полезной в случае если необходимо отрисовывать контент который состоит из 1000+ элементов. - * Для работы этой опции необходима поддержка {@link ResizeObserver}. + * {@link RenderStrategy} */ - @prop(Boolean) - readonly dropNodes: boolean = false; - - @field() - list: unknown[] = []; - - pushToList(count: number): void { - const arr = Array.from(new Array(count), () => ({id: Math.random().toString(), count})); - - performance.measure('list render', {start: performance.now()}); - this.field.set('list', this.list.concat(arr)); - - void this.nextTick().then(() => { - requestAnimationFrame(() => { - performance.measure('list render', {end: performance.now()}); - }); - }); - } - - updateList(count: number): void { - const arr = Array.from(new Array(count), () => ({id: Math.random().toString(), count})); - - performance.measure('list render', {start: performance.now()}); - this.field.set('list', arr); - - void this.nextTick().then(() => { - requestAnimationFrame(() => { - performance.measure('list render', {end: performance.now()}); - }); - }); - } - - updateListElements(count: number): void { - const arr = Array.from(new Array(count), () => ({id: Math.random().toString(), count})); - - performance.measure('list render', {start: performance.now()}); - this.field.set('list', arr); - - void this.nextTick().then(() => { - requestAnimationFrame(() => { - performance.measure('list render', {end: performance.now()}); - }); - }); - } - - pushToListCreateElement(count: number): void { - const arr = Array.from(new Array(count), () => ({id: Math.random().toString(), count})); - - performance.measure('list render', {start: performance.now()}); - const - el = this.block?.element('container'); - - const renderedNodes = arr.map(({id}) => ({ - type: 'div', - children: { - default: id.toString() - } - })); - - const nodes = this.vdom.render(this.vdom.create(renderedNodes)); - - el?.append(...nodes); - - void this.nextTick().then(() => { - requestAnimationFrame(() => { - performance.measure('list render', {end: performance.now()}); - }); - }); - } + @prop({type: String, validator: (v) => Object.isString(v) && RenderStrategy.hasOwnProperty(v)}) + readonly renderStrategy: RenderStrategyKeys = RenderStrategy.default; } diff --git a/src/components/base/b-scrolly/const.ts b/src/components/base/b-scrolly/const.ts new file mode 100644 index 0000000000..c4f5ae21fc --- /dev/null +++ b/src/components/base/b-scrolly/const.ts @@ -0,0 +1,16 @@ +/** + * Стратегия отрисовки компонентов. + */ +export const RenderStrategy = { + /** + * Данная стратегия реализует отрисовку с помощью создания инстанса `Vue` и в дальнейшем переиспользует + * его для отрисовки компонент через `forceRender`. + */ + forceRenderChunk: 'forceRenderChunk', + + /** + * Данная стратегия реализует отрисовку с помощью `vdom.create` и `vdom.render`. + */ + default: 'default' +}; + diff --git a/src/components/base/b-scrolly/interface.ts b/src/components/base/b-scrolly/interface.ts new file mode 100644 index 0000000000..b7b5bbdc78 --- /dev/null +++ b/src/components/base/b-scrolly/interface.ts @@ -0,0 +1,3 @@ +import type { RenderStrategy } from 'components/base/b-scrolly/const'; + +export type RenderStrategyKeys = keyof typeof RenderStrategy; From 10ab99d574cda1b8ba885b87dd48588b036e1bad Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 20 Apr 2023 16:12:50 +0300 Subject: [PATCH 004/159] WIP --- src/components/base/b-scrolly/README.md | 7 + src/components/base/b-scrolly/b-scrolly.ts | 321 +++++++++++++++++- src/components/base/b-scrolly/const.ts | 164 ++++++++- src/components/base/b-scrolly/interface.ts | 115 ++++++- .../base/b-scrolly/modules/local-events.ts | 50 +++ .../base/b-scrolly/modules/mediator/index.ts | 172 ++++++++++ .../modules/observer/intersection-observer.ts | 0 .../base/b-scrolly/modules/observer/scroll.ts | 0 .../modules/render/engines/force-update.ts | 16 + .../b-scrolly/modules/render/engines/vdom.ts | 25 ++ .../base/b-scrolly/modules/render/index.ts | 67 ++++ .../base/b-scrolly/modules/slots.ts | 157 +++++++++ .../base/b-scrolly/modules/state.ts | 116 +++++++ .../base/b-scrolly/test/unit/render.ts | 46 --- src/components/super/i-data/i-data.ts | 2 +- 15 files changed, 1201 insertions(+), 57 deletions(-) create mode 100644 src/components/base/b-scrolly/modules/local-events.ts create mode 100644 src/components/base/b-scrolly/modules/mediator/index.ts create mode 100644 src/components/base/b-scrolly/modules/observer/intersection-observer.ts create mode 100644 src/components/base/b-scrolly/modules/observer/scroll.ts create mode 100644 src/components/base/b-scrolly/modules/render/engines/force-update.ts create mode 100644 src/components/base/b-scrolly/modules/render/engines/vdom.ts create mode 100644 src/components/base/b-scrolly/modules/render/index.ts create mode 100644 src/components/base/b-scrolly/modules/slots.ts create mode 100644 src/components/base/b-scrolly/modules/state.ts delete mode 100644 src/components/base/b-scrolly/test/unit/render.ts diff --git a/src/components/base/b-scrolly/README.md b/src/components/base/b-scrolly/README.md index d26505800e..d80ff9eb30 100644 --- a/src/components/base/b-scrolly/README.md +++ b/src/components/base/b-scrolly/README.md @@ -1,3 +1,10 @@ +- Загруженных данных может не хватит на отрисовку поэтому прятать лоадер можно только когда набралось нужное кол-во данных +- PartialRender ? Если указано к отрисовке 10 компонентов а загружено за раз данных 5 - рисовать ли эти 5 и потом еще 5 +- Подумать над форматом данных к отрисовке, раньше клиент получал 10 элементов данных и должен был вернуть 10 элементов к отрисовке +- Preload нескольких страниц + + + ## TODO: 1. Бенчмарк подходов diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts index 68cc0c8597..62ff8fcee1 100644 --- a/src/components/base/b-scrolly/b-scrolly.ts +++ b/src/components/base/b-scrolly/b-scrolly.ts @@ -11,10 +11,43 @@ * @packageDocumentation */ -import iData, { component, prop } from 'components/super/i-data/i-data'; +import type { AsyncOptions } from 'core/async'; + import VDOM, { create, render } from 'components/friends/vdom'; -import { RenderStrategy } from 'components/base/b-scrolly/const'; -import type { RenderStrategyKeys } from 'components/base/b-scrolly/interface'; +import type iItems from 'components/traits/i-items/i-items'; +import type { CreateFromItemFn } from 'components/traits/i-items/i-items'; + +import type { + + ComponentState, + ComponentDb, + ComponentRenderStrategyKeys as ComponentRenderStrategyKeys, + RequestParams, + RequestQueryFn, + ShouldRequestFn, + ComponentRefs, + ComponentItemFactory, + ComponentItemType + +} from 'components/base/b-scrolly/interface'; + +import { + + componentRenderStrategy, + componentDataLocalEvents, + defaultProps, + componentLocalEvents, + componentItemType + +} from 'components/base/b-scrolly/const'; + +import { Mediator } from 'components/base/b-scrolly/modules/mediator'; +import { ComponentFactory } from 'components/base/b-scrolly/modules/render'; +import { SlotsStateController } from 'components/base/b-scrolly/modules/slots'; +import { ComponentInternalState } from 'components/base/b-scrolly/modules/state'; +import { typedLocalEmitterFactory } from 'components/base/b-scrolly/modules/local-events'; + +import iData, { component, prop, system, $$ } from 'components/super/i-data/i-data'; export * from 'components/base/b-scrolly/interface'; export * from 'components/base/b-scrolly/const'; @@ -23,10 +56,286 @@ VDOM.addToPrototype(create); VDOM.addToPrototype(render); @component() -export default class bScrolly extends iData { +export default class bScrolly extends iData implements iItems { + /** {@link iItems.item} */ + readonly Item!: object; + + /** {@link iItems.Items} */ + readonly Items!: Array; + + /** {@link iItems.item} */ + @prop({type: [String, Function]}) + readonly item?: iItems['item']; + + /** {@link iItems.itemKey} */ + @prop({type: [String, Function]}) + readonly itemKey?: CreateFromItemFn; + + /** {@link ComponentItemType} */ + @prop({type: [String, Function]}) + readonly itemType: ComponentItemType | CreateFromItemFn = componentItemType.item; + + /** {@link iItems.itemProps} */ + @prop({type: [Function, Object], default: () => ({})}) + readonly itemProps!: iItems['itemProps']; + + /** {@link ComponentItemFactory} */ + @prop({ + type: Function, + default: (ctx: bScrolly, items: object[]) => { + const descriptors = items.map((data, i) => ({ + key: ctx.itemKey?.(data, i), + + item: Object.isFunction(ctx.item) ? ctx.item(data, i) : ctx.item, + type: Object.isFunction(ctx.itemType) ? ctx.itemType(data, i) : ctx.itemType, + + props: Object.isFunction(ctx.itemProps) ? + ctx.itemProps(data, i, { + key: ctx.itemKey?.(data, i), + ctx + }) : + ctx.itemProps + })); + + return descriptors; + } + }) + + readonly itemsFactory!: ComponentItemFactory; + + override readonly DB!: ComponentDb; + /** * {@link RenderStrategy} */ - @prop({type: String, validator: (v) => Object.isString(v) && RenderStrategy.hasOwnProperty(v)}) - readonly renderStrategy: RenderStrategyKeys = RenderStrategy.default; + @prop({type: String, validator: (v) => Object.isString(v) && componentRenderStrategy.hasOwnProperty(v)}) + readonly componentRenderStrategy: ComponentRenderStrategyKeys = componentRenderStrategy.default; + + /** + * {@link bScrollyRequestQueryFn} + */ + @prop({type: Function}) + readonly requestQuery?: RequestQueryFn; + + /** + * Number of elements per one render chunk + */ + // eslint-disable-next-line @typescript-eslint/unbound-method + @prop({type: Number, validator: Number.isNatural}) + readonly chunkSize: number = 10; + + /** + * When this function returns `true` the component will stop to request new data. + * This function will be called on each data loading cycle. + */ + @prop({ + type: Function, + default: defaultProps.shouldStopRequestingData + }) + + readonly shouldStopRequestingData!: ShouldRequestFn; + + /** + * When this function returns `true` the component will be able to request additional data. + * This function will be called on each new element enters the viewport. + */ + @prop({ + type: Function, + default: defaultProps.shouldPerformDataRequest + }) + + readonly shouldPerformRequest!: ShouldRequestFn; + + /** + * When this function returns `true` the component will be able to render additional data. + * This function will be called on each new element enters the viewport. + */ + @prop({ + type: Function, + default: defaultProps.shouldPerformDataRender + }) + + readonly shouldPerformRender!: ShouldRequestFn; + + /** {@link typedLocalEmitterFactory} */ + @system((ctx) => typedLocalEmitterFactory(ctx)) + readonly typedLocalEmitter!: ReturnType; + + /** {@link slotsStateController} */ + @system((ctx) => new SlotsStateController(ctx)) + readonly slotsStateController!: SlotsStateController; + + /** {@link ComponentInternalState} */ + @system((ctx) => new ComponentInternalState(ctx)) + readonly componentInternalState!: ComponentInternalState; + + /** {@link ComponentFactory} */ + @system((ctx) => new ComponentFactory(ctx)) + readonly componentFactory!: ComponentFactory; + + /** {@link Mediator} */ + @system((ctx) => new Mediator(ctx)) + readonly mediator!: Mediator; + + /** + * Cached result of evoking `shouldStopRequestingData` + */ + @system() + protected shouldStopRequestingDataValue?: boolean; + + // @ts-ignore (getter instead readonly) + override get requestParams(): iData['requestParams'] { + return { + get: { + ...this.requestQuery?.(this.getComponentState())?.get, + ...Object.isDictionary(this.request?.get) ? this.request?.get : undefined + } + }; + } + + protected override readonly $refs!: iData['$refs'] & ComponentRefs; + + override reload(...args: Parameters): ReturnType { + this.componentStatus = 'loading'; + return super.reload(...args); + } + + override initLoad(...args: Parameters): ReturnType { + const callSuperAndStateReset = () => { + this.reset(); + return super.initLoad(...args); + }; + + const + isInitialLoading = !this.isReady; + + this.typedLocalEmitter.emit(componentDataLocalEvents.dataLoadStart, isInitialLoading); + + const initLoadResult = isInitialLoading ? + callSuperAndStateReset() : + this.initLoadNext(); + + if (Object.isPromise(initLoadResult)) { + initLoadResult + .then((res) => { + this.onInitLoadSuccess(isInitialLoading, isInitialLoading ? this.db : this.convertDataToDB(res)); + }) + .catch((err) => { + this.onInitLoadError(isInitialLoading); + throw err; + }) + .finally(() => { + this.onInitLoadFinish(isInitialLoading); + }); + } + + return >initLoadResult; + } + + /** + * Initializes the load of the next data chunk + * @param args + */ + initLoadNext(): Promise { + if (!this.dataProvider) { + throw ReferenceError('Missing dataProvider'); + } + + const params = this.getRequestParams(); + return this.dataProvider.get(params[0], params[1]); + } + + /** + * Renders the next data chunk to the page (ignores `client` check for render posibility) + */ + renderNext(): void { + // ... + } + + /** + * Returns an internal component state + */ + getComponentState(): ComponentState { + return this.componentInternalState.compile(); + } + + /** + * Collets all of the request params all over the component (ig `requestProp`, `requestQuery`) + * {@link bScrollyRequestParams} + */ + getRequestParams(): RequestParams { + const label: AsyncOptions = { + label: $$.initLoad, + join: 'replace' + }; + + const + defParams = this.dataProvider?.getDefaultRequestParams('get'); + + if (Object.isArray(defParams)) { + Object.assign(defParams[1], label); + } + + return defParams; + } + + /** + * Resets a component state and the state of the component modules + */ + protected reset(): void { + this.typedLocalEmitter.emit(componentLocalEvents.resetState); + this.shouldStopRequestingDataValue = undefined; + } + + /** + * Wrapper for `shouldStopRequestingData` + */ + protected shouldStopRequestingDataWrapper(): boolean { + return this.shouldStopRequestingData(this.getComponentState(), this); + } + + protected override convertDataToDB(data: unknown): O | this['DB'] { + this.typedLocalEmitter.emit(componentLocalEvents.convertDataToDB, data); + return super.convertDataToDB(data); + } + + /** + * Handler: data load successfully finished + * + * @param isInitialLoading - `true` if this load was an initial component loading + * @param data + */ + protected onInitLoadSuccess(isInitialLoading: boolean, data: unknown): void { + if (!Object.isPlainObject(data) || !Object.isArray(data.data)) { + throw new ReferenceError('Missing data field in the loaded data'); + } + + this.typedLocalEmitter.emit(componentDataLocalEvents.dataLoadSuccess, data.data, isInitialLoading); + + if ( + isInitialLoading && + Object.size(data.data) === 0 + ) { + if (this.shouldStopRequestingDataValue) { + this.typedLocalEmitter.emit(componentDataLocalEvents.dataEmpty, isInitialLoading); + } + } + } + + /** + * Handler: failed to load data + * + * @param isInitialLoading - `true` if this load was an initial component loading + */ + protected onInitLoadError(isInitialLoading: boolean): void { + this.typedLocalEmitter.emit(componentDataLocalEvents.dataLoadError, isInitialLoading); + } + + /** + * Handler: data loading is finished + * @param isInitialLoading - `true` if this load was an initial component loading + */ + protected onInitLoadFinish(isInitialLoading: boolean): void { + this.typedLocalEmitter.emit(componentDataLocalEvents.dataLoadFinish, isInitialLoading); + } } diff --git a/src/components/base/b-scrolly/const.ts b/src/components/base/b-scrolly/const.ts index c4f5ae21fc..689356b96f 100644 --- a/src/components/base/b-scrolly/const.ts +++ b/src/components/base/b-scrolly/const.ts @@ -1,7 +1,18 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import type { ComponentState } from 'components/base/b-scrolly/interface'; + /** - * Стратегия отрисовки компонентов. + * Render strategy for producing the components. */ -export const RenderStrategy = { +export const componentRenderStrategy = { /** * Данная стратегия реализует отрисовку с помощью создания инстанса `Vue` и в дальнейшем переиспользует * его для отрисовки компонент через `forceRender`. @@ -14,3 +25,152 @@ export const RenderStrategy = { default: 'default' }; +/** + * Стратегии возможных вариантов работы компонента. + */ +export const componentStrategy = { + /** + * Стратегия, при которой определение вхождение элемента + * в область видимости, будет происходить с помощью `intersectionObserver`. + * + * При это узлы из DOM дерева удаляться не будут + */ + intersectionObserver: 'intersectionObserver', + + /** + * Стратегия, при которой определение вхождение элемента + * в область видимости, будет происходить с помощью прослушивания события `scroll`. + * + * При это узлы из DOM дерева удаляться не будут + */ + scroll: 'scroll', + + /** + * Стратегия, при которой определение вхождение элемента + * в область видимости, будет происходить с помощью прослушивания события `scroll`. + * + * При это узлы из DOM дерева буду удаляться и возвращаться + */ + scrollWithDropNodes: 'scrollWithDropNodes', + + /** + * Стратегия, при которой определение вхождение элемента + * в область видимости, будет происходить с помощью прослушивания события `scroll`. + * + * При это узлы из DOM дерева будут переиспользоваться. + */ + scrollWithRecycleNodes: 'scrollWithRecycleNodes' +}; + +/** + * События компонента связанные с данными. (эмитятся в `localEmitter`) + */ +export const componentDataLocalEvents = { + /** + * Загрузка данных началась. + */ + dataLoadStart: 'dataLoadStart', + + /** + * Загрузка данных завершена. + */ + dataLoadFinish: 'dataLoadFinish', + + /** + * Возникла ошибка при загрузки данных. + */ + dataLoadError: 'dataLoadError', + + /** + * Данные успешно загружены. + */ + dataLoadSuccess: 'dataLoadSuccess', + + /** + * Успешная загрузка в которых нет данных. + */ + dataEmpty: 'dataEmpty' +}; + +/** + * События компонента. + */ +export const componentLocalEvents = { + /** + * Сброс состояние компонента. + */ + resetState: 'resetState', + + /** + * Вызов конвертации данных в `DB`. + */ + convertDataToDB: 'convertDataToDB' +}; + +/** + * События отрисовки компонента. + */ +export const componentRenderLocalEvents = { + /** + * Начался рендеринг элементов. + */ + renderStart: 'renderStart', + + /** + * Закончился рендеринг элементов. + */ + renderDone: 'renderDone', + + /** + * Начался рендеринг элементов движком отрисовки. + */ + renderEngineStart: 'renderEngineStart', + + /** + * Закончился рендеринг элементов движком отрисовки. + */ + renderEngineDone: 'renderEngineDone', + + /** + * Началась вставка элементов `DOM`. + */ + domInsertStart: 'domInsertStart', + + /** + * Завершилась вставка элементов в `DOM`. + */ + domInsertDone: 'domInsertDone' +}; + +export const canPerformRenderRejectionReason = { + notEnoughData: 'notEnoughData', + clientRejection: 'clientRejection' +}; + +/** + * События наблюдателя за элементами. + */ +export const componentObserverLocalEvents = { + elementEnter: 'elementEnter', + elementOut: 'elementOut' +}; + +export const componentItemType = { + item: 'item', + separator: 'separator' +}; + +/** + * `should` свойства компонента по умолчанию. + */ +export const defaultProps = { + /** {@link bScrolly.shouldStopRequestingData} */ + shouldStopRequestingData: (_state: ComponentState, _ctx: bScrolly): boolean => false, + + /** {@link bScrolly.shouldPerformRequest} */ + shouldPerformDataRequest: (_state: ComponentState, _ctx: bScrolly): boolean => false, + + /** {@link bScrolly.shouldPerformRender} */ + shouldPerformDataRender: (_state: ComponentState, _ctx: bScrolly): boolean => false +}; + diff --git a/src/components/base/b-scrolly/interface.ts b/src/components/base/b-scrolly/interface.ts index b7b5bbdc78..904cfca58b 100644 --- a/src/components/base/b-scrolly/interface.ts +++ b/src/components/base/b-scrolly/interface.ts @@ -1,3 +1,114 @@ -import type { RenderStrategy } from 'components/base/b-scrolly/const'; +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ -export type RenderStrategyKeys = keyof typeof RenderStrategy; +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import type { componentItemType, componentDataLocalEvents, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents, componentRenderStrategy, canPerformRenderRejectionReason } from 'components/base/b-scrolly/const'; +import type { CreateRequestOptions, RequestQuery } from 'components/super/i-data/i-data'; + +/** + * {@link componentRenderStrategy} + */ +export type ComponentRenderStrategyKeys = keyof typeof componentRenderStrategy; + +/** + * Состояние компонента. + */ +export interface ComponentState { + loadPage: number; + renderPage: number; + data: object[]; + isLastEmpty: boolean; + isInitialLoading: boolean; + lastLoaded: object[]; + lastLoadedRawData: unknown; +} + +/** + * Тип данных которые хранит компонент. + */ +export interface ComponentDb { + data: unknown[]; + total?: number; +} + +/** + * Функция которая возвращает GET параметры для запроса. + */ +export interface RequestQueryFn { + (params: ComponentState): Dictionary; +} + +export interface ComponentItemFactory { + (ctx: bScrolly, items: unknown[]): ComponentItem[]; +} + +export interface ComponentItem { + type: ComponentItemType; + item: string; + props?: Dictionary; + key: string; + children?: ComponentItem[]; +} + +export type ComponentItemType = keyof typeof componentItemType; + +/** + * Параметры запроса. + */ +export type RequestParams = [RequestQuery, CreateRequestOptions]; + +export type CanPerformRenderRejectionReason = keyof typeof canPerformRenderRejectionReason; + +/** + * Функция для опроса клиента о необходимости выполнить то или иное действие. + */ +export interface ShouldRequestFn { + (params: ComponentState, ctx: bScrolly): boolean; +} + +export type ComponentLocalEvents = + keyof typeof componentDataLocalEvents | + keyof typeof componentLocalEvents | + keyof typeof componentRenderLocalEvents | + keyof typeof componentObserverLocalEvents; + +/** + * Имя события: аргументы события + */ +export interface LocalEventPayloadMap { + dataLoadSuccess: [data: object[], isInitialLoading: boolean]; + dataLoadFinish: [isInitialLoading: boolean]; + dataLoadStart: [isInitialLoading: boolean]; + dataLoadError: [isInitialLoading: boolean]; + dataEmpty: [isInitialLoading: boolean]; + + resetState: []; + convertDataToDB: [data: unknown]; + + elementEnter: [element: HTMLElement, index: number, data: unknown]; + elementOut: [element: HTMLElement, index: number, data: unknown]; + + renderStart: []; + renderDone: []; + renderEngineStart: []; + renderEngineDone: []; + domInsertStart: []; + domInsertDone: []; +} + +export interface ComponentRefs { + container: HTMLElement; + loader?: HTMLElement; + tombstones?: HTMLElement; + empty?: HTMLElement; + retry?: HTMLElement; + done?: HTMLElement; + renderNext?: HTMLElement; +} + +export type LocalEventPayload = LocalEventPayloadMap[T]; diff --git a/src/components/base/b-scrolly/modules/local-events.ts b/src/components/base/b-scrolly/modules/local-events.ts new file mode 100644 index 0000000000..07a7b1f5d5 --- /dev/null +++ b/src/components/base/b-scrolly/modules/local-events.ts @@ -0,0 +1,50 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { AsyncOptions } from 'core/async'; + +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import type { ComponentLocalEvents, LocalEventPayload } from 'components/base/b-scrolly/interface'; + +/** + * Factory for producing typed `localEmitter` methods. + * Provides a methods of the `localEmitter` with types + * + * @param ctx + */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function typedLocalEmitterFactory(ctx: bScrolly) { + const once = ( + event: EVENT, + handler: (...args: LocalEventPayload) => void, + asyncOpts?: AsyncOptions + ) => { + ctx.unsafe.localEmitter.once(event, handler, asyncOpts); + }; + + const on = ( + event: EVENT, + handler: (...args: LocalEventPayload) => void, + asyncOpts?: AsyncOptions + ) => { + ctx.unsafe.localEmitter.on(event, handler, asyncOpts); + }; + + const emit = ( + event: EVENT, + ...payload: LocalEventPayload + ) => { + ctx.unsafe.localEmitter.emit(event, ...payload); + }; + + return { + once, + on, + emit + }; +} diff --git a/src/components/base/b-scrolly/modules/mediator/index.ts b/src/components/base/b-scrolly/modules/mediator/index.ts new file mode 100644 index 0000000000..9acb430e58 --- /dev/null +++ b/src/components/base/b-scrolly/modules/mediator/index.ts @@ -0,0 +1,172 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import symbolGenerator from 'core/symbol'; + +import Friend from 'components/friends/friend'; + +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import type { CanPerformRenderRejectionReason } from 'components/base/b-scrolly/b-scrolly'; +import { canPerformRenderRejectionReason, componentDataLocalEvents, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; + +export const + $$ = symbolGenerator(), + mediatorAsyncGroup = 'mediator'; + +/** + * Friendly to the `bScrolly` class. + * Provides an API for composing and managing `bScrolly` modules + */ +export class Mediator extends Friend { + + /** + * {@link bScrolly} + */ + override readonly C!: bScrolly; + + /** + * `True` if the next rendering process will be initial + */ + protected isInitialRender: boolean = true; + + /** + * @param ctx + */ + constructor(ctx: bScrolly) { + super(ctx); + + const + {typedLocalEmitter} = ctx; + + typedLocalEmitter.on(componentDataLocalEvents.dataLoadSuccess, () => this.onDataLoaded()); + typedLocalEmitter.on(componentObserverLocalEvents.elementEnter, () => this.onElementEnters()); + typedLocalEmitter.on(componentObserverLocalEvents.elementOut, () => this.onElementOut()); + typedLocalEmitter.on(componentLocalEvents.resetState, () => this.reset()); + } + + /** + * Resets the module state + */ + protected reset(): void { + const + {ctx} = this; + + this.isInitialRender = true; + + ctx.async.clearAll({group: new RegExp(mediatorAsyncGroup)}); + } + + /** + * Returns status of the possibility to render a components. + * Also returns reason of the rejection if the is no possibility to render components + */ + protected canPerformRender(): {result: boolean; reason?: CanPerformRenderRejectionReason} { + if (this.isInitialRender) { + return { + result: true + }; + } + + const + {ctx} = this, + {chunkSize} = ctx, + dataSlice = this.getNextDataSlice(); + + if (dataSlice.length < chunkSize) { + return { + result: false, + reason: canPerformRenderRejectionReason.notEnoughData + }; + } + + return { + result: true + }; + } + + /** + * Renders the next chunk of the elements + */ + protected performRender(): void { + const + {ctx, refs} = this, + dataSlice = this.getNextDataSlice(); + + ctx.typedLocalEmitter.emit(componentRenderLocalEvents.renderStart); + + const + nodes = ctx.componentFactory.produceComponents(dataSlice); + + if (nodes.length === 0) { + return; + } + + ctx.typedLocalEmitter.emit(componentRenderLocalEvents.domInsertStart); + + const + fragment = document.createDocumentFragment(); + + for (let i = 0; i < nodes.length; i++) { + this.dom.appendChild(fragment, nodes[i], { + group: mediatorAsyncGroup, + destroyIfComponent: true + }); + } + + ctx.async.requestAnimationFrame(() => { + refs.container.appendChild(fragment); + + ctx.typedLocalEmitter.emit(componentRenderLocalEvents.domInsertDone); + ctx.typedLocalEmitter.emit(componentRenderLocalEvents.renderDone); + + }, {label: $$.insertDomRaf, group: mediatorAsyncGroup}); + } + + /** + * Returns a data slice that should be rendered next + */ + protected getNextDataSlice(): object[] { + const + {ctx} = this, + {chunkSize} = ctx, + {data, renderPage} = ctx.getComponentState(); + + return data.slice(renderPage * chunkSize, (renderPage + 1) * chunkSize); + } + + /** + * Handler: element enters the viewport + */ + protected onElementEnters(): void { + // ... + } + + /** + * Handler: element leaves the viewport + */ + protected onElementOut(): void { + // ... + } + + /** + * Handler: data was loaded + */ + protected onDataLoaded(): void { + const + {ctx} = this, + {result, reason} = this.canPerformRender(); + + if (result) { + return this.performRender(); + } + + if (reason === canPerformRenderRejectionReason.notEnoughData) { + void ctx.initLoad(); + } + } +} diff --git a/src/components/base/b-scrolly/modules/observer/intersection-observer.ts b/src/components/base/b-scrolly/modules/observer/intersection-observer.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/base/b-scrolly/modules/observer/scroll.ts b/src/components/base/b-scrolly/modules/observer/scroll.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/base/b-scrolly/modules/render/engines/force-update.ts b/src/components/base/b-scrolly/modules/render/engines/force-update.ts new file mode 100644 index 0000000000..ccef8b2f2b --- /dev/null +++ b/src/components/base/b-scrolly/modules/render/engines/force-update.ts @@ -0,0 +1,16 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * Renders the provided `VNodes` to the `HTMLElements` via `$forceUpdate` and single render engine instance. + * + * @param args + */ +export function render(..._args: any[]): HTMLElement[] { + return []; +} diff --git a/src/components/base/b-scrolly/modules/render/engines/vdom.ts b/src/components/base/b-scrolly/modules/render/engines/vdom.ts new file mode 100644 index 0000000000..e78978d31e --- /dev/null +++ b/src/components/base/b-scrolly/modules/render/engines/vdom.ts @@ -0,0 +1,25 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { VNodeDescriptor } from 'components/friends/vdom'; +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; + +/** + * Renders the provided `VNodes` to the `HTMLElements` via `vdom.render` API. + * + * @param ctx + * @param data + */ +export function render(ctx: bScrolly, items: VNodeDescriptor[]): HTMLElement[] { + const + vnodes = ctx.vdom.create(...items), + // https://github.com/vuejs/core/issues/6061 + nodes = ctx.vdom.render(vnodes).filter((node) => node.nodeType !== node.TEXT_NODE); + + return nodes; +} diff --git a/src/components/base/b-scrolly/modules/render/index.ts b/src/components/base/b-scrolly/modules/render/index.ts new file mode 100644 index 0000000000..d50e85d411 --- /dev/null +++ b/src/components/base/b-scrolly/modules/render/index.ts @@ -0,0 +1,67 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import Friend from 'components/friends/friend'; +import type { VNodeDescriptor } from 'components/friends/vdom'; + +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import type { ComponentItem } from 'components/base/b-scrolly/interface'; +import { componentRenderLocalEvents, componentRenderStrategy } from 'components/base/b-scrolly/const'; + +import * as forceUpdate from 'components/base/b-scrolly/modules/render/engines/force-update'; +import * as vdomRender from 'components/base/b-scrolly/modules/render/engines/vdom'; + +/** + * Friendly to the `bScrolly` class. + * Provides an API for component producing + */ +export class ComponentFactory extends Friend { + /** + * {@link bScrolly} + */ + override readonly C!: bScrolly; + + /** + * @param data + */ + produceComponents(data: object[]): HTMLElement[] { + const createDescriptor = (item: ComponentItem): VNodeDescriptor => ({ + type: item.item, + attrs: item.props, + children: Object.size(item.children) > 0 ? item.children?.map(createDescriptor) : [] + }); + + const + {ctx} = this, + items = ctx.itemsFactory(ctx, data), + descriptors = items.map(createDescriptor); + + return this.callRenderEngine(descriptors); + } + + /** + * @param descriptors + */ + protected callRenderEngine(descriptors: VNodeDescriptor[]): HTMLElement[] { + const + {ctx} = this; + + let res; + ctx.typedLocalEmitter.emit(componentRenderLocalEvents.renderEngineStart); + + if (ctx.componentRenderStrategy === componentRenderStrategy.forceRenderChunk) { + res = forceUpdate.render(ctx, descriptors); + + } else { + res = vdomRender.render(ctx, descriptors); + } + + ctx.typedLocalEmitter.emit(componentRenderLocalEvents.renderEngineDone); + return res; + } +} diff --git a/src/components/base/b-scrolly/modules/slots.ts b/src/components/base/b-scrolly/modules/slots.ts new file mode 100644 index 0000000000..d0e19c8946 --- /dev/null +++ b/src/components/base/b-scrolly/modules/slots.ts @@ -0,0 +1,157 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import symbolGenerator from 'core/symbol'; +import type { AsyncOptions } from 'core/async'; + +import Friend from 'components/friends/friend'; +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import { componentDataLocalEvents, componentLocalEvents } from 'components/base/b-scrolly/const'; +import type { ComponentRefs } from 'components/base/b-scrolly/b-scrolly'; + +export const + $$ = symbolGenerator(), + slotsStateControllerAsyncGroup = 'slotsStateController'; + +/** + * Класс реализующий показ нужных слотов в нужный момент времени. + */ +export class SlotsStateController extends Friend { + + /** + * {@link bScrolly} + */ + override readonly C!: bScrolly; + + /** + * Опции для асинхронной функции обновление состояния отображения слотов. + */ + protected readonly asyncUpdateLabel: AsyncOptions = { + label: $$.updateSlotsVisibility, + group: slotsStateControllerAsyncGroup + }; + + /** + * @param ctx + */ + constructor(ctx: bScrolly) { + super(ctx); + + const + {typedLocalEmitter} = ctx; + + typedLocalEmitter.on(componentDataLocalEvents.dataLoadError, () => this.loadingFailedState()); + typedLocalEmitter.on(componentDataLocalEvents.dataLoadStart, () => this.loadingProgressState()); + typedLocalEmitter.on(componentDataLocalEvents.dataLoadSuccess, () => this.loadingSuccessState()); + typedLocalEmitter.on(componentDataLocalEvents.dataEmpty, () => this.emptyState()); + typedLocalEmitter.on(componentLocalEvents.resetState, () => this.reset()); + } + + /** + * Отображает слоты которые должны отображаться при пустом состоянии. + */ + emptyState(): void { + this.setSlotsVisibility({ + container: true, + done: true, + empty: true, + loader: false, + renderNext: false, + retry: false, + tombstones: false + }); + } + + /** + * Отображает слоты которые должны отображаться в момент загрузки данных. + */ + loadingProgressState(): void { + this.setSlotsVisibility({ + container: true, + loader: true, + tombstones: true, + done: false, + empty: false, + renderNext: false, + retry: false + }); + } + + /** + * Отображает слоты которые должны отображаться в момент неудачной загрузки. + */ + loadingFailedState(): void { + this.setSlotsVisibility({ + container: true, + retry: true, + done: false, + empty: false, + loader: false, + renderNext: false, + tombstones: false + }); + } + + /** + * Отображает слоты которые должны отображаться в момент успешной загрузки данных. + */ + loadingSuccessState(): void { + // Здесь нужно не loadingSuccessState а какое-то другое событие так как LoadingSuccess может происходить много раз + this.setSlotsVisibility({ + container: true, + done: false, + empty: false, + loader: false, + renderNext: false, + retry: false, + tombstones: false + }); + } + + /** + * Очищает состояние модуля. + */ + reset(): void { + this.async.clearAll({group: new RegExp(slotsStateControllerAsyncGroup)}); + } + + /** + * Устанавливает состояние слотов. + * + * @param stateObj + */ + protected setSlotsVisibility(stateObj: Required): void { + this.async.cancelAnimationFrame(this.asyncUpdateLabel); + + this.async.requestAnimationFrame(() => { + for (const [name, state] of Object.entries(stateObj)) { + this.setState(name, state); + } + + }, this.asyncUpdateLabel); + } + + /** + * Устанавливает состояние слота. + * + * @param name + * @param state + */ + protected setState(name: keyof SlotsStateObj, state: boolean): void { + const + ref = this.ctx.$refs[name]; + + if (ref) { + ref.style.display = state ? '' : 'none'; + } + } +} + +type SlotsStateObj = { + [key in keyof ComponentRefs]: boolean; +}; diff --git a/src/components/base/b-scrolly/modules/state.ts b/src/components/base/b-scrolly/modules/state.ts new file mode 100644 index 0000000000..53282d508c --- /dev/null +++ b/src/components/base/b-scrolly/modules/state.ts @@ -0,0 +1,116 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import { componentDataLocalEvents, componentLocalEvents } from 'components/base/b-scrolly/const'; +import type { ComponentState } from 'components/base/b-scrolly/interface'; +import Friend from 'components/friends/friend'; + +export class ComponentInternalState extends Friend { + + /** + * {@link bScrolly} + */ + override readonly C!: bScrolly; + + protected currentLoadPage: number = 0; + + protected currentRenderPage: number = 0; + + protected data: object[] = []; + + protected lastLoadedData: object[] = []; + + protected lastLoadedRawData: CanUndef; + + protected isLastEmpty: boolean = false; + + protected isInitialLoading: boolean = true; + + /** + * @param ctx + */ + constructor(ctx: bScrolly) { + super(ctx); + + const + {typedLocalEmitter} = ctx; + + typedLocalEmitter.on(componentDataLocalEvents.dataLoadStart, () => this.incrementLoadPage()); + typedLocalEmitter.on(componentDataLocalEvents.dataLoadSuccess, (...args) => this.updateData(...args)); + typedLocalEmitter.on(componentLocalEvents.convertDataToDB, (...args) => this.updateRawLastLoaded(...args)); + typedLocalEmitter.on(componentLocalEvents.resetState, (...args) => this.reset(...args)); + } + + /** + * Собирает состояние компонента в один объект. + */ + compile(): ComponentState { + return { + loadPage: this.currentLoadPage, + renderPage: this.currentRenderPage, + data: this.data, + isLastEmpty: this.isLastEmpty, + isInitialLoading: this.isInitialLoading, + lastLoaded: this.lastLoadedData, + lastLoadedRawData: this.lastLoadedRawData + }; + } + + /** + * Обнуляет состояние модуля. + */ + reset(): void { + this.currentLoadPage = -1; + this.currentRenderPage = -1; + this.data = []; + this.isLastEmpty = false; + this.isInitialLoading = true; + } + + /** + * Обновляет указатель последней загруженной страницы. + */ + incrementLoadPage(): this { + this.currentLoadPage++; + return this; + } + + /** + * Обновляет указать последней отрисованной страницы. + */ + incrementRenderPage(): this { + this.currentRenderPage++; + return this; + } + + /** + * Обновляет состояние последних сырых загруженных данных. + * + * @param data + */ + updateRawLastLoaded(data: unknown): this { + this.lastLoadedRawData = data; + return this; + } + + /** + * Обновляет состояние загруженных данных. + * + * @param data + * @param isInitialLoading + */ + updateData(data: object[], isInitialLoading: boolean): this { + this.data = this.data.concat(data); + this.isLastEmpty = data.length === 0; + this.isInitialLoading = isInitialLoading; + this.lastLoadedData = data; + + return this; + } +} diff --git a/src/components/base/b-scrolly/test/unit/render.ts b/src/components/base/b-scrolly/test/unit/render.ts deleted file mode 100644 index 8dd62c5894..0000000000 --- a/src/components/base/b-scrolly/test/unit/render.ts +++ /dev/null @@ -1,46 +0,0 @@ -import test from 'tests/config/unit/test'; -import Component from 'tests/helpers/component'; -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import BOM from 'tests/helpers/bom'; - -test.use({ - actionTimeout: 0 -}); - -test.describe('b-scrolly rendering', () => { - test.beforeEach(async ({demoPage}) => { - await demoPage.goto(); - }); - - ['pushToListCreateElement', 'pushToList', 'updateList'].forEach((method) => { - test(`b-scrolly perf ${method}`, async ({page, browser, context}) => { - const scrolly = await Component.createComponent(page, 'b-scrolly', { - children: { - default: ({item}) => item.id - } - }); - - await page.pause(); - - const client = await context.newCDPSession(page); - await client.send('Emulation.setCPUThrottlingRate', {rate: 8}); - - await browser.startTracing(page, {path: `${method}.json`}); - - await page.evaluate(() => performance.mark('YOLO')); - await page.evaluate(([method]) => globalThis.method = method, [method]); - - await scrolly.evaluate((c) => c[globalThis.method](100)); - await BOM.waitForIdleCallback(page); - await scrolly.evaluate((c) => c[globalThis.method](100)); - await BOM.waitForIdleCallback(page); - await scrolly.evaluate((c) => c[globalThis.method](500)); - await BOM.waitForIdleCallback(page); - await scrolly.evaluate((c) => c[globalThis.method](500)); - await BOM.waitForIdleCallback(page); - await scrolly.evaluate((c) => c[globalThis.method](500)); - - await browser.stopTracing(); - }); - }); -}); diff --git a/src/components/super/i-data/i-data.ts b/src/components/super/i-data/i-data.ts index b282f946bf..7caf82f089 100644 --- a/src/components/super/i-data/i-data.ts +++ b/src/components/super/i-data/i-data.ts @@ -53,7 +53,7 @@ export { export * from 'components/super/i-block/i-block'; export * from 'components/super/i-data/interface'; -const +export const $$ = symbolGenerator(); @component({functional: null}) From a2b3334a6851658de0c434ea276d5b2b37f13be8 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 1 Jun 2023 17:07:18 +0300 Subject: [PATCH 005/159] WIP --- index.d.ts | 23 +- package.json | 1 + src/components/base/b-scrolly/b-scrolly.ts | 78 ++++-- src/components/base/b-scrolly/const.ts | 17 +- src/components/base/b-scrolly/interface.ts | 18 +- .../engines/force-update.ts | 0 .../{render => factory}/engines/vdom.ts | 0 .../modules/{render => factory}/index.ts | 20 +- .../base/b-scrolly/modules/juggler/index.ts | 258 ++++++++++++++++++ .../index.ts} | 6 + .../base/b-scrolly/modules/mediator/index.ts | 172 ------------ .../base/b-scrolly/modules/observer/const.ts | 12 + .../observer/engines/intersection-observer.ts | 55 ++++ .../modules/observer/engines/scroll.ts | 35 +++ .../base/b-scrolly/modules/observer/index.ts | 53 ++++ .../b-scrolly/modules/observer/interface.ts | 23 ++ .../modules/observer/intersection-observer.ts | 0 .../base/b-scrolly/modules/observer/scroll.ts | 0 .../modules/{slots.ts => slots/index.ts} | 25 +- .../base/b-scrolly/modules/slots/interface.ts | 5 + .../modules/{state.ts => state/index.ts} | 101 ++++++- .../test/api/component-object/index.ts | 196 +++++++++++++ .../base/b-scrolly/test/api/helpers/index.ts | 178 ++++++++++++ .../test/unit/lifecycle/initialization.ts | 166 +++++++++++ .../b-scrolly/test/unit/lifecycle/slots.ts | 149 ++++++++++ src/core/prelude/test-env/components/json.ts | 39 ++- src/core/prelude/test-env/index.ts | 1 + src/core/prelude/test-env/mock/index.ts | 47 ++++ tests/helpers/component-object/builder.ts | 207 ++++++++++++++ tests/helpers/component-object/index.ts | 14 + tests/helpers/component-object/initializer.ts | 46 ++++ tests/helpers/component-object/interface.ts | 22 ++ tests/helpers/component-object/mock.ts | 126 +++++++++ tests/helpers/index.ts | 2 + tests/helpers/mock/index.ts | 75 +++++ tests/helpers/providers/interceptor/index.ts | 180 ++++++++++++ tests/helpers/providers/pagination/index.ts | 107 ++++---- yarn.lock | 68 ++++- 38 files changed, 2240 insertions(+), 285 deletions(-) rename src/components/base/b-scrolly/modules/{render => factory}/engines/force-update.ts (100%) rename src/components/base/b-scrolly/modules/{render => factory}/engines/vdom.ts (100%) rename src/components/base/b-scrolly/modules/{render => factory}/index.ts (80%) create mode 100644 src/components/base/b-scrolly/modules/juggler/index.ts rename src/components/base/b-scrolly/modules/{local-events.ts => local-events/index.ts} (87%) delete mode 100644 src/components/base/b-scrolly/modules/mediator/index.ts create mode 100644 src/components/base/b-scrolly/modules/observer/const.ts create mode 100644 src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts create mode 100644 src/components/base/b-scrolly/modules/observer/engines/scroll.ts create mode 100644 src/components/base/b-scrolly/modules/observer/index.ts create mode 100644 src/components/base/b-scrolly/modules/observer/interface.ts delete mode 100644 src/components/base/b-scrolly/modules/observer/intersection-observer.ts delete mode 100644 src/components/base/b-scrolly/modules/observer/scroll.ts rename src/components/base/b-scrolly/modules/{slots.ts => slots/index.ts} (87%) create mode 100644 src/components/base/b-scrolly/modules/slots/interface.ts rename src/components/base/b-scrolly/modules/{state.ts => state/index.ts} (54%) create mode 100644 src/components/base/b-scrolly/test/api/component-object/index.ts create mode 100644 src/components/base/b-scrolly/test/api/helpers/index.ts create mode 100644 src/components/base/b-scrolly/test/unit/lifecycle/initialization.ts create mode 100644 src/components/base/b-scrolly/test/unit/lifecycle/slots.ts create mode 100644 src/core/prelude/test-env/mock/index.ts create mode 100644 tests/helpers/component-object/builder.ts create mode 100644 tests/helpers/component-object/index.ts create mode 100644 tests/helpers/component-object/initializer.ts create mode 100644 tests/helpers/component-object/interface.ts create mode 100644 tests/helpers/component-object/mock.ts create mode 100644 tests/helpers/mock/index.ts create mode 100644 tests/helpers/providers/interceptor/index.ts diff --git a/index.d.ts b/index.d.ts index 6a6d4b4339..a4fc4e0c77 100644 --- a/index.d.ts +++ b/index.d.ts @@ -116,7 +116,28 @@ declare var * Requires a module by the specified path. * This function should only be used when writing tests. */ - importModule: (path: string) => any; + importModule: (path: string) => any, + + /** + * Jest mock API for testing env + */ + jest: { + /** + * Wrapper for jest `spyOn` function + * + * {@link ModuleMocker.spyOn} + * @see https://jestjs.io/docs/mock-functions + */ + spy: import('jest-mock').ModuleMocker['spyOn']; + + /** + * Wrapper for jest `fn` function + * + * {@link ModuleMocker.fn} + * @see https://jestjs.io/docs/mock-functions + */ + mock: import('jest-mock').ModuleMocker['fn']; + }; interface TouchGesturesCreateOptions { /** diff --git a/package.json b/package.json index bb9eddab21..3c01f4ca92 100644 --- a/package.json +++ b/package.json @@ -154,6 +154,7 @@ "fast-glob": "3.2.12", "husky": "7.0.4", "jasmine": "3.99.0", + "jest-mock": "28.1.3", "nyc": "15.1.0", "playwright": "1.32.1", "webpack": "5.79.0" diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts index 62ff8fcee1..ae93b847cf 100644 --- a/src/components/base/b-scrolly/b-scrolly.ts +++ b/src/components/base/b-scrolly/b-scrolly.ts @@ -27,7 +27,8 @@ import type { ShouldRequestFn, ComponentRefs, ComponentItemFactory, - ComponentItemType + ComponentItemType, + ComponentStrategyKeys } from 'components/base/b-scrolly/interface'; @@ -37,12 +38,14 @@ import { componentDataLocalEvents, defaultProps, componentLocalEvents, - componentItemType + componentItemType, + componentStrategy } from 'components/base/b-scrolly/const'; -import { Mediator } from 'components/base/b-scrolly/modules/mediator'; -import { ComponentFactory } from 'components/base/b-scrolly/modules/render'; +import { Juggler } from 'components/base/b-scrolly/modules/juggler'; +import { Observer } from 'components/base/b-scrolly/modules/observer'; +import { ComponentFactory } from 'components/base/b-scrolly/modules/factory'; import { SlotsStateController } from 'components/base/b-scrolly/modules/slots'; import { ComponentInternalState } from 'components/base/b-scrolly/modules/state'; import { typedLocalEmitterFactory } from 'components/base/b-scrolly/modules/local-events'; @@ -111,6 +114,12 @@ export default class bScrolly extends iData implements iItems { @prop({type: String, validator: (v) => Object.isString(v) && componentRenderStrategy.hasOwnProperty(v)}) readonly componentRenderStrategy: ComponentRenderStrategyKeys = componentRenderStrategy.default; + /** + * {@link ComponentStrategyKeys} + */ + @prop({type: String, validator: (v) => Object.isString(v) && componentStrategy.hasOwnProperty(v)}) + readonly componentStrategy: ComponentStrategyKeys = componentStrategy.intersectionObserver; + /** * {@link bScrollyRequestQueryFn} */ @@ -144,7 +153,7 @@ export default class bScrolly extends iData implements iItems { default: defaultProps.shouldPerformDataRequest }) - readonly shouldPerformRequest!: ShouldRequestFn; + readonly shouldPerformDataRequest!: ShouldRequestFn; /** * When this function returns `true` the component will be able to render additional data. @@ -155,7 +164,17 @@ export default class bScrolly extends iData implements iItems { default: defaultProps.shouldPerformDataRender }) - readonly shouldPerformRender!: ShouldRequestFn; + readonly shouldPerformDataRender!: ShouldRequestFn; + + /** + * If true then the elements observer will not be initialized. + * That may be useful if you wanna implement a lazy loading via client interaction + */ + @prop({ + type: Boolean + }) + + readonly disableObserver: boolean = false; /** {@link typedLocalEmitterFactory} */ @system((ctx) => typedLocalEmitterFactory(ctx)) @@ -173,15 +192,12 @@ export default class bScrolly extends iData implements iItems { @system((ctx) => new ComponentFactory(ctx)) readonly componentFactory!: ComponentFactory; - /** {@link Mediator} */ - @system((ctx) => new Mediator(ctx)) - readonly mediator!: Mediator; + /** {@link Juggler} */ + @system((ctx) => new Juggler(ctx)) + readonly juggler!: Juggler; - /** - * Cached result of evoking `shouldStopRequestingData` - */ - @system() - protected shouldStopRequestingDataValue?: boolean; + @system((ctx) => new Observer(ctx)) + readonly observer!: Observer; // @ts-ignore (getter instead readonly) override get requestParams(): iData['requestParams'] { @@ -255,7 +271,7 @@ export default class bScrolly extends iData implements iItems { /** * Returns an internal component state */ - getComponentState(): ComponentState { + getComponentState(): Readonly { return this.componentInternalState.compile(); } @@ -280,18 +296,34 @@ export default class bScrolly extends iData implements iItems { } /** - * Resets a component state and the state of the component modules + * Wrapper for `shouldStopRequestingData` */ - protected reset(): void { - this.typedLocalEmitter.emit(componentLocalEvents.resetState); - this.shouldStopRequestingDataValue = undefined; + shouldStopRequestingDataWrapper(): boolean { + const + state = this.getComponentState(); + + return state.isDone || this.shouldStopRequestingData(this.getComponentState(), this); } /** - * Wrapper for `shouldStopRequestingData` + * Wrapper for `shouldPerformDataRender` + */ + shouldPerformDataRenderWrapper(): boolean { + return this.shouldPerformDataRender(this.getComponentState(), this); + } + + /** + * Wrapper from `shouldPerformDataRequest` */ - protected shouldStopRequestingDataWrapper(): boolean { - return this.shouldStopRequestingData(this.getComponentState(), this); + shouldPerformDataRequestWrapper(): boolean { + return this.shouldPerformDataRequest(this.getComponentState(), this); + } + + /** + * Resets a component state and the state of the component modules + */ + protected reset(): void { + this.typedLocalEmitter.emit(componentLocalEvents.resetState); } protected override convertDataToDB(data: unknown): O | this['DB'] { @@ -316,7 +348,7 @@ export default class bScrolly extends iData implements iItems { isInitialLoading && Object.size(data.data) === 0 ) { - if (this.shouldStopRequestingDataValue) { + if (this.shouldStopRequestingDataWrapper()) { this.typedLocalEmitter.emit(componentDataLocalEvents.dataEmpty, isInitialLoading); } } diff --git a/src/components/base/b-scrolly/const.ts b/src/components/base/b-scrolly/const.ts index 689356b96f..81aab14ff1 100644 --- a/src/components/base/b-scrolly/const.ts +++ b/src/components/base/b-scrolly/const.ts @@ -104,7 +104,12 @@ export const componentLocalEvents = { /** * Вызов конвертации данных в `DB`. */ - convertDataToDB: 'convertDataToDB' + convertDataToDB: 'convertDataToDB', + + /** + * This event will be emitted then all of the component data is rendered and all of the component data was loaded + */ + done: 'done' }; /** @@ -168,7 +173,15 @@ export const defaultProps = { shouldStopRequestingData: (_state: ComponentState, _ctx: bScrolly): boolean => false, /** {@link bScrolly.shouldPerformRequest} */ - shouldPerformDataRequest: (_state: ComponentState, _ctx: bScrolly): boolean => false, + shouldPerformDataRequest: (state: ComponentState, _ctx: bScrolly): boolean => { + const isLastRequestNotEmpty = () => state.lastLoaded.length > 0; + + if (state.isInitialRender) { + return isLastRequestNotEmpty(); + } + + return false; + }, /** {@link bScrolly.shouldPerformRender} */ shouldPerformDataRender: (_state: ComponentState, _ctx: bScrolly): boolean => false diff --git a/src/components/base/b-scrolly/interface.ts b/src/components/base/b-scrolly/interface.ts index 904cfca58b..2c72be3935 100644 --- a/src/components/base/b-scrolly/interface.ts +++ b/src/components/base/b-scrolly/interface.ts @@ -7,13 +7,14 @@ */ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { componentItemType, componentDataLocalEvents, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents, componentRenderStrategy, canPerformRenderRejectionReason } from 'components/base/b-scrolly/const'; +import type { componentItemType, componentDataLocalEvents, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents, componentRenderStrategy, canPerformRenderRejectionReason, componentStrategy } from 'components/base/b-scrolly/const'; import type { CreateRequestOptions, RequestQuery } from 'components/super/i-data/i-data'; /** * {@link componentRenderStrategy} */ export type ComponentRenderStrategyKeys = keyof typeof componentRenderStrategy; +export type ComponentStrategyKeys = keyof typeof componentStrategy; /** * Состояние компонента. @@ -25,6 +26,11 @@ export interface ComponentState { isLastEmpty: boolean; isInitialLoading: boolean; lastLoaded: object[]; + isInitialRender: boolean; + isDone: boolean; + maxViewedIndex: CanUndef; + itemsTillEnd: CanUndef; + mountedItems: Readonly; lastLoadedRawData: unknown; } @@ -55,6 +61,11 @@ export interface ComponentItem { children?: ComponentItem[]; } +export interface MountedComponentItem extends ComponentItem { + node: HTMLElement; + index: number; +} + export type ComponentItemType = keyof typeof componentItemType; /** @@ -88,10 +99,11 @@ export interface LocalEventPayloadMap { dataEmpty: [isInitialLoading: boolean]; resetState: []; + done: []; convertDataToDB: [data: unknown]; - elementEnter: [element: HTMLElement, index: number, data: unknown]; - elementOut: [element: HTMLElement, index: number, data: unknown]; + elementEnter: [componentItem: MountedComponentItem]; + elementOut: [componentItem: MountedComponentItem]; renderStart: []; renderDone: []; diff --git a/src/components/base/b-scrolly/modules/render/engines/force-update.ts b/src/components/base/b-scrolly/modules/factory/engines/force-update.ts similarity index 100% rename from src/components/base/b-scrolly/modules/render/engines/force-update.ts rename to src/components/base/b-scrolly/modules/factory/engines/force-update.ts diff --git a/src/components/base/b-scrolly/modules/render/engines/vdom.ts b/src/components/base/b-scrolly/modules/factory/engines/vdom.ts similarity index 100% rename from src/components/base/b-scrolly/modules/render/engines/vdom.ts rename to src/components/base/b-scrolly/modules/factory/engines/vdom.ts diff --git a/src/components/base/b-scrolly/modules/render/index.ts b/src/components/base/b-scrolly/modules/factory/index.ts similarity index 80% rename from src/components/base/b-scrolly/modules/render/index.ts rename to src/components/base/b-scrolly/modules/factory/index.ts index d50e85d411..6f4e58f957 100644 --- a/src/components/base/b-scrolly/modules/render/index.ts +++ b/src/components/base/b-scrolly/modules/factory/index.ts @@ -13,8 +13,8 @@ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import type { ComponentItem } from 'components/base/b-scrolly/interface'; import { componentRenderLocalEvents, componentRenderStrategy } from 'components/base/b-scrolly/const'; -import * as forceUpdate from 'components/base/b-scrolly/modules/render/engines/force-update'; -import * as vdomRender from 'components/base/b-scrolly/modules/render/engines/vdom'; +import * as forceUpdate from 'components/base/b-scrolly/modules/factory/engines/force-update'; +import * as vdomRender from 'components/base/b-scrolly/modules/factory/engines/vdom'; /** * Friendly to the `bScrolly` class. @@ -29,7 +29,17 @@ export class ComponentFactory extends Friend { /** * @param data */ - produceComponents(data: object[]): HTMLElement[] { + produceComponentItems(data: object[]): ComponentItem[] { + const + {ctx} = this; + + return ctx.itemsFactory(ctx, data); + } + + /** + * @param data + */ + produceNodes(componentItems: ComponentItem[]): HTMLElement[] { const createDescriptor = (item: ComponentItem): VNodeDescriptor => ({ type: item.item, attrs: item.props, @@ -37,9 +47,7 @@ export class ComponentFactory extends Friend { }); const - {ctx} = this, - items = ctx.itemsFactory(ctx, data), - descriptors = items.map(createDescriptor); + descriptors = componentItems.map(createDescriptor); return this.callRenderEngine(descriptors); } diff --git a/src/components/base/b-scrolly/modules/juggler/index.ts b/src/components/base/b-scrolly/modules/juggler/index.ts new file mode 100644 index 0000000000..1aa2829762 --- /dev/null +++ b/src/components/base/b-scrolly/modules/juggler/index.ts @@ -0,0 +1,258 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import symbolGenerator from 'core/symbol'; + +import Friend from 'components/friends/friend'; + +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import type { CanPerformRenderRejectionReason, ComponentItem } from 'components/base/b-scrolly/b-scrolly'; +import { canPerformRenderRejectionReason, componentDataLocalEvents, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; +import type { MountedComponentItem } from 'components/base/b-scrolly/interface'; + +export const + $$ = symbolGenerator(), + jugglerAsyncGroup = '[[JUGGLER]]'; + +/** + * Friendly to the `bScrolly` class. + * Provides an API for managing DOM insertion of the components + */ +export class Juggler extends Friend { + + /** + * {@link bScrolly} + */ + override readonly C!: bScrolly; + + protected get nextDataSliceStartIndex(): number { + const + {ctx, ctx: {chunkSize}} = this, + {renderPage} = ctx.getComponentState(); + + return renderPage * chunkSize; + } + + protected get nextDataSliceEndIndex(): number { + const + {ctx, ctx: {chunkSize}} = this, + {renderPage} = ctx.getComponentState(); + + return (renderPage + 1) * chunkSize; + } + + /** + * @param ctx + */ + constructor(ctx: bScrolly) { + super(ctx); + + const + {typedLocalEmitter} = ctx; + + typedLocalEmitter.on(componentObserverLocalEvents.elementEnter, (component) => this.onElementEnters(component)); + typedLocalEmitter.on(componentObserverLocalEvents.elementOut, (component) => this.onElementOut(component)); + typedLocalEmitter.on(componentLocalEvents.resetState, () => this.reset()); + typedLocalEmitter.on(componentRenderLocalEvents.renderDone, () => this.checkIsDone()); + + typedLocalEmitter.on(componentDataLocalEvents.dataLoadSuccess, () => { + this.onDataLoaded(); + this.checkIsDone(); + }); + + } + + /** + * Resets the module state + */ + protected reset(): void { + const + {ctx} = this; + + ctx.async.clearAll({group: new RegExp(jugglerAsyncGroup)}); + } + + /** + * Returns status of the possibility to render a components. + * Also returns reason of the rejection if the is no possibility to render components + */ + protected canPerformRender(): {result: boolean; reason?: CanPerformRenderRejectionReason} { + const + {ctx} = this, + {chunkSize} = ctx, + state = this.ctx.getComponentState(), + dataSlice = this.getNextDataSlice(); + + if (dataSlice.length < chunkSize) { + return { + result: false, + reason: canPerformRenderRejectionReason.notEnoughData + }; + } + + if (state.isInitialRender) { + return { + result: true + }; + } + + const + clientResponse = ctx.shouldPerformDataRenderWrapper(); + + return { + result: clientResponse, + reason: clientResponse === false ? canPerformRenderRejectionReason.clientRejection : undefined + }; + } + + /** + * Renders the next chunk of the elements + */ + protected performRender(): void { + const + {ctx, refs} = this, + dataSlice = this.getNextDataSlice(); + + ctx.typedLocalEmitter.emit(componentRenderLocalEvents.renderStart); + + const + items = ctx.componentFactory.produceComponentItems(dataSlice), + nodes = ctx.componentFactory.produceNodes(items), + mountedItems = this.mountedComponentItems(items, nodes); + + ctx.componentInternalState.updateMountedComponents(mountedItems); + ctx.observer.observe(mountedItems); + + ctx.typedLocalEmitter.emit(componentRenderLocalEvents.domInsertStart); + + const + fragment = document.createDocumentFragment(); + + for (let i = 0; i < nodes.length; i++) { + this.dom.appendChild(fragment, nodes[i], { + group: jugglerAsyncGroup, + destroyIfComponent: true + }); + } + + ctx.async.requestAnimationFrame(() => { + refs.container.appendChild(fragment); + + ctx.typedLocalEmitter.emit(componentRenderLocalEvents.domInsertDone); + ctx.typedLocalEmitter.emit(componentRenderLocalEvents.renderDone); + + }, {label: $$.insertDomRaf, group: jugglerAsyncGroup}); + } + + /** + * Returns a data slice that should be rendered next + */ + protected getNextDataSlice(): object[] { + const + {ctx} = this, + {data} = ctx.getComponentState(); + + return data.slice(this.nextDataSliceStartIndex, this.nextDataSliceEndIndex); + } + + /** + * Stores the component items + * + * @param items + * @param nodes + */ + protected mountedComponentItems(items: ComponentItem[], nodes: HTMLElement[]): MountedComponentItem[] { + const + {ctx} = this, + {mountedItems} = ctx.getComponentState(); + + return items.map((item, i) => ({ + ...item, + node: nodes[i], + index: mountedItems.length + i + })); + } + + /** + * Performs render if it is possible + */ + protected loadDataOrPerformRender(): void { + const + {ctx} = this, + state = ctx.getComponentState(), + {result, reason} = this.canPerformRender(); + + if (result) { + return this.performRender(); + } + + if (reason === canPerformRenderRejectionReason.notEnoughData) { + if (ctx.shouldStopRequestingDataWrapper()) { + this.performRender(); + + } else if (ctx.shouldPerformDataRequestWrapper()) { + void ctx.initLoad(); + + } else if (state.isInitialRender) { + this.performRender(); + } + } + + if (reason === canPerformRenderRejectionReason.clientRejection) { + // ... + } + } + + /** + * Checks if all data are rendered and all requests are made + */ + protected checkIsDone(): void { + const + {ctx} = this, + {isDone} = ctx.getComponentState(), + slice = this.getNextDataSlice(); + + if ( + slice.length === 0 && + ctx.shouldStopRequestingDataWrapper() && + !isDone + ) { + ctx.typedLocalEmitter.emit(componentLocalEvents.done); + } + } + + /** + * Handler: data was loaded + */ + protected onDataLoaded(): void { + this.loadDataOrPerformRender(); + } + + /** + * Handler: element enters the viewport + */ + protected onElementEnters(component: MountedComponentItem): void { + const + {ctx} = this, + state = ctx.getComponentState(), + {index} = component; + + if (state.maxViewedIndex == null || state.maxViewedIndex < index) { + ctx.componentInternalState.setMaxViewedIndex(index); + } + + this.loadDataOrPerformRender(); + } + + /** + * Handler: element leaves the viewport + */ + protected onElementOut(_component: MountedComponentItem): void { + // ... + } +} diff --git a/src/components/base/b-scrolly/modules/local-events.ts b/src/components/base/b-scrolly/modules/local-events/index.ts similarity index 87% rename from src/components/base/b-scrolly/modules/local-events.ts rename to src/components/base/b-scrolly/modules/local-events/index.ts index 07a7b1f5d5..ff275e33db 100644 --- a/src/components/base/b-scrolly/modules/local-events.ts +++ b/src/components/base/b-scrolly/modules/local-events/index.ts @@ -35,6 +35,11 @@ export function typedLocalEmitterFactory(ctx: bScrolly) { ctx.unsafe.localEmitter.on(event, handler, asyncOpts); }; + const promisifyOnce = ( + event: EVENT, + asyncOpts?: AsyncOptions + ) => ctx.unsafe.localEmitter.promisifyOnce(event, asyncOpts); + const emit = ( event: EVENT, ...payload: LocalEventPayload @@ -45,6 +50,7 @@ export function typedLocalEmitterFactory(ctx: bScrolly) { return { once, on, + promisifyOnce, emit }; } diff --git a/src/components/base/b-scrolly/modules/mediator/index.ts b/src/components/base/b-scrolly/modules/mediator/index.ts deleted file mode 100644 index 9acb430e58..0000000000 --- a/src/components/base/b-scrolly/modules/mediator/index.ts +++ /dev/null @@ -1,172 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import symbolGenerator from 'core/symbol'; - -import Friend from 'components/friends/friend'; - -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { CanPerformRenderRejectionReason } from 'components/base/b-scrolly/b-scrolly'; -import { canPerformRenderRejectionReason, componentDataLocalEvents, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; - -export const - $$ = symbolGenerator(), - mediatorAsyncGroup = 'mediator'; - -/** - * Friendly to the `bScrolly` class. - * Provides an API for composing and managing `bScrolly` modules - */ -export class Mediator extends Friend { - - /** - * {@link bScrolly} - */ - override readonly C!: bScrolly; - - /** - * `True` if the next rendering process will be initial - */ - protected isInitialRender: boolean = true; - - /** - * @param ctx - */ - constructor(ctx: bScrolly) { - super(ctx); - - const - {typedLocalEmitter} = ctx; - - typedLocalEmitter.on(componentDataLocalEvents.dataLoadSuccess, () => this.onDataLoaded()); - typedLocalEmitter.on(componentObserverLocalEvents.elementEnter, () => this.onElementEnters()); - typedLocalEmitter.on(componentObserverLocalEvents.elementOut, () => this.onElementOut()); - typedLocalEmitter.on(componentLocalEvents.resetState, () => this.reset()); - } - - /** - * Resets the module state - */ - protected reset(): void { - const - {ctx} = this; - - this.isInitialRender = true; - - ctx.async.clearAll({group: new RegExp(mediatorAsyncGroup)}); - } - - /** - * Returns status of the possibility to render a components. - * Also returns reason of the rejection if the is no possibility to render components - */ - protected canPerformRender(): {result: boolean; reason?: CanPerformRenderRejectionReason} { - if (this.isInitialRender) { - return { - result: true - }; - } - - const - {ctx} = this, - {chunkSize} = ctx, - dataSlice = this.getNextDataSlice(); - - if (dataSlice.length < chunkSize) { - return { - result: false, - reason: canPerformRenderRejectionReason.notEnoughData - }; - } - - return { - result: true - }; - } - - /** - * Renders the next chunk of the elements - */ - protected performRender(): void { - const - {ctx, refs} = this, - dataSlice = this.getNextDataSlice(); - - ctx.typedLocalEmitter.emit(componentRenderLocalEvents.renderStart); - - const - nodes = ctx.componentFactory.produceComponents(dataSlice); - - if (nodes.length === 0) { - return; - } - - ctx.typedLocalEmitter.emit(componentRenderLocalEvents.domInsertStart); - - const - fragment = document.createDocumentFragment(); - - for (let i = 0; i < nodes.length; i++) { - this.dom.appendChild(fragment, nodes[i], { - group: mediatorAsyncGroup, - destroyIfComponent: true - }); - } - - ctx.async.requestAnimationFrame(() => { - refs.container.appendChild(fragment); - - ctx.typedLocalEmitter.emit(componentRenderLocalEvents.domInsertDone); - ctx.typedLocalEmitter.emit(componentRenderLocalEvents.renderDone); - - }, {label: $$.insertDomRaf, group: mediatorAsyncGroup}); - } - - /** - * Returns a data slice that should be rendered next - */ - protected getNextDataSlice(): object[] { - const - {ctx} = this, - {chunkSize} = ctx, - {data, renderPage} = ctx.getComponentState(); - - return data.slice(renderPage * chunkSize, (renderPage + 1) * chunkSize); - } - - /** - * Handler: element enters the viewport - */ - protected onElementEnters(): void { - // ... - } - - /** - * Handler: element leaves the viewport - */ - protected onElementOut(): void { - // ... - } - - /** - * Handler: data was loaded - */ - protected onDataLoaded(): void { - const - {ctx} = this, - {result, reason} = this.canPerformRender(); - - if (result) { - return this.performRender(); - } - - if (reason === canPerformRenderRejectionReason.notEnoughData) { - void ctx.initLoad(); - } - } -} diff --git a/src/components/base/b-scrolly/modules/observer/const.ts b/src/components/base/b-scrolly/modules/observer/const.ts new file mode 100644 index 0000000000..1b4d12682a --- /dev/null +++ b/src/components/base/b-scrolly/modules/observer/const.ts @@ -0,0 +1,12 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * Group for async operations of the observer module + */ +export const observerAsyncGroup = '[[OBSERVER]]'; diff --git a/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts b/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts new file mode 100644 index 0000000000..bf669a90fa --- /dev/null +++ b/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts @@ -0,0 +1,55 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import { componentLocalEvents, componentObserverLocalEvents } from 'components/base/b-scrolly/const'; +import type { MountedComponentItem } from 'components/base/b-scrolly/interface'; +import { observerAsyncGroup } from 'components/base/b-scrolly/modules/observer/const'; +import type { ObserverEngine } from 'components/base/b-scrolly/modules/observer/interface'; +import Friend from 'components/friends/friend'; + +export default class IoObserver extends Friend implements ObserverEngine { + + /** + * {@link bScrolly} + */ + override readonly C!: bScrolly; + + /** + * @param ctx + */ + constructor(ctx: bScrolly) { + super(ctx); + + ctx.typedLocalEmitter.on(componentLocalEvents.resetState, () => this.reset()); + } + + /** + * @inheritdoc + */ + watchForIntersection(components: MountedComponentItem[]): void { + const + {ctx} = this; + + for (const component of components) { + ctx.dom.watchForIntersection(component.node, { + group: observerAsyncGroup, + label: component.key, + once: true, + delay: 0 + }, () => ctx.typedLocalEmitter.emit(componentObserverLocalEvents.elementEnter, component)); + } + } + + /** + * @inheritdoc + */ + reset(): void { + this.async.clearAll({group: new RegExp(observerAsyncGroup)}); + } +} diff --git a/src/components/base/b-scrolly/modules/observer/engines/scroll.ts b/src/components/base/b-scrolly/modules/observer/engines/scroll.ts new file mode 100644 index 0000000000..5efce8a712 --- /dev/null +++ b/src/components/base/b-scrolly/modules/observer/engines/scroll.ts @@ -0,0 +1,35 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import type { MountedComponentItem } from 'components/base/b-scrolly/interface'; +import { observerAsyncGroup } from 'components/base/b-scrolly/modules/observer/const'; +import type { ObserverEngine } from 'components/base/b-scrolly/modules/observer/interface'; +import Friend from 'components/friends/friend'; + +export default class ScrollObserver extends Friend implements ObserverEngine { + + /** + * {@link bScrolly} + */ + override readonly C!: bScrolly; + + /** + * @inheritdoc + */ + watchForIntersection(_components: MountedComponentItem[]): void { + // ... + } + + /** + * @inheritdoc + */ + reset(): void { + this.async.clearAll({group: new RegExp(observerAsyncGroup)}); + } +} diff --git a/src/components/base/b-scrolly/modules/observer/index.ts b/src/components/base/b-scrolly/modules/observer/index.ts new file mode 100644 index 0000000000..a82f9be736 --- /dev/null +++ b/src/components/base/b-scrolly/modules/observer/index.ts @@ -0,0 +1,53 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import type { MountedComponentItem } from 'components/base/b-scrolly/interface'; +import ScrollObserver from 'components/base/b-scrolly/modules/observer/engines/scroll'; +import IoObserver from 'components/base/b-scrolly/modules/observer/engines/intersection-observer'; +import Friend from 'components/friends/friend'; + +export { default as IoObserver } from 'components/base/b-scrolly/modules/observer/engines/intersection-observer'; +export { default as ScrollObserver } from 'components/base/b-scrolly/modules/observer/engines/scroll'; + +export class Observer extends Friend { + /** + * {@link bScrolly} + */ + override readonly C!: bScrolly; + + /** + * Observe engine + */ + protected engine: IoObserver | ScrollObserver; + + /** + * @param ctx + */ + constructor(ctx: bScrolly) { + super(ctx); + + this.engine = ctx.componentStrategy === 'intersectionObserver' ? + new IoObserver(ctx) : + new ScrollObserver(ctx); + } + + /** + * @param mountedItems + */ + observe(mountedItems: MountedComponentItem[]): void { + const + {ctx} = this; + + if (ctx.disableObserver) { + return; + } + + this.engine.watchForIntersection(mountedItems); + } +} diff --git a/src/components/base/b-scrolly/modules/observer/interface.ts b/src/components/base/b-scrolly/modules/observer/interface.ts new file mode 100644 index 0000000000..d531d27356 --- /dev/null +++ b/src/components/base/b-scrolly/modules/observer/interface.ts @@ -0,0 +1,23 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { MountedComponentItem } from 'components/base/b-scrolly/interface'; + +export interface ObserverEngine { + /** + * Initializes a watcher to watch component enters the viewport + * @param components + */ + watchForIntersection(components: MountedComponentItem[]): void; + + /** + * Resets the module state + */ + reset(): void; +} + diff --git a/src/components/base/b-scrolly/modules/observer/intersection-observer.ts b/src/components/base/b-scrolly/modules/observer/intersection-observer.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/components/base/b-scrolly/modules/observer/scroll.ts b/src/components/base/b-scrolly/modules/observer/scroll.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/components/base/b-scrolly/modules/slots.ts b/src/components/base/b-scrolly/modules/slots/index.ts similarity index 87% rename from src/components/base/b-scrolly/modules/slots.ts rename to src/components/base/b-scrolly/modules/slots/index.ts index d0e19c8946..957162ab3a 100644 --- a/src/components/base/b-scrolly/modules/slots.ts +++ b/src/components/base/b-scrolly/modules/slots/index.ts @@ -12,7 +12,9 @@ import type { AsyncOptions } from 'core/async'; import Friend from 'components/friends/friend'; import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import { componentDataLocalEvents, componentLocalEvents } from 'components/base/b-scrolly/const'; -import type { ComponentRefs } from 'components/base/b-scrolly/b-scrolly'; +import type { SlotsStateObj } from 'components/base/b-scrolly/modules/slots/interface'; + +export * from 'components/base/b-scrolly/modules/slots/interface'; export const $$ = symbolGenerator(), @@ -49,6 +51,7 @@ export class SlotsStateController extends Friend { typedLocalEmitter.on(componentDataLocalEvents.dataLoadStart, () => this.loadingProgressState()); typedLocalEmitter.on(componentDataLocalEvents.dataLoadSuccess, () => this.loadingSuccessState()); typedLocalEmitter.on(componentDataLocalEvents.dataEmpty, () => this.emptyState()); + typedLocalEmitter.on(componentLocalEvents.done, () => this.doneState()); typedLocalEmitter.on(componentLocalEvents.resetState, () => this.reset()); } @@ -67,6 +70,18 @@ export class SlotsStateController extends Friend { }); } + doneState(): void { + this.setSlotsVisibility({ + container: true, + done: true, + empty: false, + loader: false, + renderNext: false, + retry: false, + tombstones: false + }); + } + /** * Отображает слоты которые должны отображаться в момент загрузки данных. */ @@ -130,7 +145,7 @@ export class SlotsStateController extends Friend { this.async.requestAnimationFrame(() => { for (const [name, state] of Object.entries(stateObj)) { - this.setState(name, state); + this.setDisplayState(name, state); } }, this.asyncUpdateLabel); @@ -142,7 +157,7 @@ export class SlotsStateController extends Friend { * @param name * @param state */ - protected setState(name: keyof SlotsStateObj, state: boolean): void { + protected setDisplayState(name: keyof SlotsStateObj, state: boolean): void { const ref = this.ctx.$refs[name]; @@ -151,7 +166,3 @@ export class SlotsStateController extends Friend { } } } - -type SlotsStateObj = { - [key in keyof ComponentRefs]: boolean; -}; diff --git a/src/components/base/b-scrolly/modules/slots/interface.ts b/src/components/base/b-scrolly/modules/slots/interface.ts new file mode 100644 index 0000000000..a61d6f46c7 --- /dev/null +++ b/src/components/base/b-scrolly/modules/slots/interface.ts @@ -0,0 +1,5 @@ +import type { ComponentRefs } from 'components/base/b-scrolly/interface'; + +export type SlotsStateObj = { + [key in keyof ComponentRefs]: boolean; +}; diff --git a/src/components/base/b-scrolly/modules/state.ts b/src/components/base/b-scrolly/modules/state/index.ts similarity index 54% rename from src/components/base/b-scrolly/modules/state.ts rename to src/components/base/b-scrolly/modules/state/index.ts index 53282d508c..f2f66387ff 100644 --- a/src/components/base/b-scrolly/modules/state.ts +++ b/src/components/base/b-scrolly/modules/state/index.ts @@ -7,8 +7,8 @@ */ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import { componentDataLocalEvents, componentLocalEvents } from 'components/base/b-scrolly/const'; -import type { ComponentState } from 'components/base/b-scrolly/interface'; +import { componentDataLocalEvents, componentLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; +import type { ComponentState, MountedComponentItem } from 'components/base/b-scrolly/interface'; import Friend from 'components/friends/friend'; export class ComponentInternalState extends Friend { @@ -22,6 +22,10 @@ export class ComponentInternalState extends Friend { protected currentRenderPage: number = 0; + protected itemsTillEnd: CanUndef = undefined; + + protected maxViewedIndex: CanUndef = undefined; + protected data: object[] = []; protected lastLoadedData: object[] = []; @@ -32,6 +36,18 @@ export class ComponentInternalState extends Friend { protected isInitialLoading: boolean = true; + protected isDone: boolean = false; + + /** + * Component items that was rendered + */ + protected mountedItems: MountedComponentItem[] = []; + + /** + * `True` if the next rendering process will be initial + */ + protected isInitialRender: boolean = true; + /** * @param ctx */ @@ -43,22 +59,33 @@ export class ComponentInternalState extends Friend { typedLocalEmitter.on(componentDataLocalEvents.dataLoadStart, () => this.incrementLoadPage()); typedLocalEmitter.on(componentDataLocalEvents.dataLoadSuccess, (...args) => this.updateData(...args)); - typedLocalEmitter.on(componentLocalEvents.convertDataToDB, (...args) => this.updateRawLastLoaded(...args)); + typedLocalEmitter.on(componentLocalEvents.convertDataToDB, (...args) => this.setRawLastLoaded(...args)); typedLocalEmitter.on(componentLocalEvents.resetState, (...args) => this.reset(...args)); + typedLocalEmitter.on(componentLocalEvents.done, () => this.setIsDone(true)); + + typedLocalEmitter.on(componentRenderLocalEvents.renderStart, () => { + this.setIsInitialRender(false); + this.incrementRenderPage(); + }); } /** * Собирает состояние компонента в один объект. */ - compile(): ComponentState { + compile(): Readonly { return { loadPage: this.currentLoadPage, renderPage: this.currentRenderPage, data: this.data, isLastEmpty: this.isLastEmpty, isInitialLoading: this.isInitialLoading, + isInitialRender: this.isInitialRender, + isDone: this.isDone, lastLoaded: this.lastLoadedData, - lastLoadedRawData: this.lastLoadedRawData + lastLoadedRawData: this.lastLoadedRawData, + maxViewedIndex: this.maxViewedIndex, + mountedItems: this.mountedItems, + itemsTillEnd: this.itemsTillEnd }; } @@ -66,11 +93,16 @@ export class ComponentInternalState extends Friend { * Обнуляет состояние модуля. */ reset(): void { - this.currentLoadPage = -1; - this.currentRenderPage = -1; + this.currentLoadPage = 0; + this.currentRenderPage = 0; + this.maxViewedIndex = 0; + this.itemsTillEnd = 0; this.data = []; + this.mountedItems = []; this.isLastEmpty = false; + this.isDone = false; this.isInitialLoading = true; + this.isInitialRender = true; } /** @@ -89,13 +121,8 @@ export class ComponentInternalState extends Friend { return this; } - /** - * Обновляет состояние последних сырых загруженных данных. - * - * @param data - */ - updateRawLastLoaded(data: unknown): this { - this.lastLoadedRawData = data; + storeComponentItems(items: MountedComponentItem[]): this { + this.mountedItems.push(...items); return this; } @@ -113,4 +140,50 @@ export class ComponentInternalState extends Friend { return this; } + + updateMountedComponents(mountedItems: MountedComponentItem[]): this { + this.mountedItems.push(...mountedItems); + return this; + } + + updateItemsTillEnd(): this { + if (this.maxViewedIndex == null) { + throw new Error('Missing max viewed index'); + } + + this.itemsTillEnd = this.mountedItems.length - 1 - this.maxViewedIndex; + return this; + } + + /** + * Обновляет состояние последних сырых загруженных данных. + * + * @param data + */ + setRawLastLoaded(data: unknown): this { + this.lastLoadedRawData = data; + return this; + } + + setIsDone(v: boolean): this { + this.isDone = v; + return this; + } + + /** + * Sets an initial render state + * + * @param state + */ + setIsInitialRender(state: boolean): this { + this.isInitialRender = state; + return this; + } + + setMaxViewedIndex(index: number): this { + this.maxViewedIndex = index; + this.updateItemsTillEnd(); + + return this; + } } diff --git a/src/components/base/b-scrolly/test/api/component-object/index.ts b/src/components/base/b-scrolly/test/api/component-object/index.ts new file mode 100644 index 0000000000..c8f450f7c6 --- /dev/null +++ b/src/components/base/b-scrolly/test/api/component-object/index.ts @@ -0,0 +1,196 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { JSHandle, Locator, Page } from 'playwright'; + +import { ComponentObject, Scroll, Utils } from 'tests/helpers'; + +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import type { ComponentRefs, ComponentState } from 'components/base/b-scrolly/b-scrolly'; +import type { SlotsStateObj } from 'components/base/b-scrolly/modules/slots'; + +export class ScrollyComponentObject extends ComponentObject { + + /** + * Container ref + */ + readonly container: Locator; + + /** + * @param page + */ + constructor(page: Page) { + super(page, 'b-scrolly'); + this.container = this.node.locator(this.elSelector('container')); + } + + override async build(...args: Parameters['build']>): Promise> { + await this.page.addStyleTag({content: ` + [data-index] { + width: 200px; + height: 200px; + margin: 16px; + background-color: red; + } + + [data-index]:after { + content: attr(data-index); + } + + #done { + width: 200px; + height: 200px; + display: flex; + justify-content: center; + align-items: center; + background-color: green; + } + + #done:after { + content: "done"; + } + `}); + + return super.build(...args); + } + + /** + * Returns an internal component state + */ + getComponentState(): Promise { + return this.component.evaluate((ctx) => ctx.getComponentState()); + } + + /** + * Returns a container child count + */ + async getContainerChildCount(): Promise { + return this.container.evaluate((ctx) => ctx.childNodes.length); + } + + /** + * Waits for container child count equals to N + */ + async waitForContainerChildCountEqualsTo(n: number): Promise { + return Utils.waitForFunction((await this.container.elementHandle())!, (ctx, n) => ctx.childNodes.length === n, n); + } + + /** + * Waits for container child count more or equals to N + */ + async waitForContainerChildCountMoreThen(n: number): Promise { + return Utils.waitForFunction((await this.container.elementHandle())!, (ctx, n) => ctx.childNodes.length >= n, n); + } + + /** + * Returns a promise that will be resolved after the component emits `domInsertDone` + */ + async waitForDomInsertDoneEvent(): Promise { + await this.component.evaluate((ctx) => ctx.typedLocalEmitter.promisifyOnce('domInsertDone')); + return this; + } + + /** + * Returns promise that will be resolved then the provided slot will hit `isVisible` state + * + * @param slotName + * @param isVisible + */ + async waitForSlotState(slotName: keyof ComponentRefs, isVisible: boolean): Promise { + const + root = await this.node.elementHandle(); + + await root?.waitForSelector(this.elSelector(slotName), {state: isVisible ? 'visible' : 'hidden'}); + } + + async getSlotsState(): Promise> { + const + root = await this.node.elementHandle(); + + const + container = await root?.$(this.elSelector('container')), + loader = await root?.$(this.elSelector('loader')), + tombstones = await root?.$(this.elSelector('tombstones')), + empty = await root?.$(this.elSelector('empty')), + retry = await root?.$(this.elSelector('retry')), + done = await root?.$(this.elSelector('done')), + renderNext = await root?.$(this.elSelector('renderNext')); + + return { + container: Boolean(await container?.isVisible()), + loader: Boolean(await loader?.isVisible()), + tombstones: Boolean(await tombstones?.isVisible()), + empty: Boolean(await empty?.isVisible()), + retry: Boolean(await retry?.isVisible()), + done: Boolean(await done?.isVisible()), + renderNext: Boolean(await renderNext?.isVisible()) + }; + } + + /** + * Scrolls page to the bottom + */ + async scrollToBottom(): Promise { + await Scroll.scrollToBottom(this.page); + return this; + } + + /** + * Adds default `iItems` props + */ + withPaginationItemProps(): this { + this.setProps({ + item: 'section', + itemProps: (item) => ({'data-index': item.i}) + }); + + return this; + } + + /** + * Adds a `requestProp` + * @param requestParams + */ + withRequestProp(requestParams: Dictionary = {}): this { + this.setProps({ + request: { + get: { + chunkSize: 10, + id: Math.random(), + ...requestParams + } + } + }); + + return this; + } + + /** + * Adds a `Provider` into provider prop + */ + withPaginationProvider(): this { + this.setProps({dataProvider: 'Provider'}); + return this; + } + + /** + * Calls every `pagination-like` default props setters: + * + * - `withPaginationProvider` + * - `withPaginationItemProps` + * - `withRequestProp` + * + * @param requestParams + */ + withDefaultPaginationProviderProps(requestParams: Dictionary = {}): this { + return this + .withPaginationProvider() + .withPaginationItemProps() + .withRequestProp(requestParams); + } +} diff --git a/src/components/base/b-scrolly/test/api/helpers/index.ts b/src/components/base/b-scrolly/test/api/helpers/index.ts new file mode 100644 index 0000000000..6413928763 --- /dev/null +++ b/src/components/base/b-scrolly/test/api/helpers/index.ts @@ -0,0 +1,178 @@ +/*! + * 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 type { ComponentState, MountedComponentItem } from 'components/base/b-scrolly/interface'; +import { paginationHandler } from 'tests/helpers/providers/pagination'; +import { ScrollyComponentObject } from 'components/base/b-scrolly/test/api/component-object'; +import { RequestInterceptor } from 'tests/helpers/providers/interceptor'; + +export * from 'components/base/b-scrolly/test/api/component-object'; + +type DataItemCtor = (i: number) => DATA; +type MountedItemCtor = (data: DATA, i: number) => MountedComponentItem; + +/** + * Creates a test helpers for `b-scrolly` component + * @param page + */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export async function createTestHelpers(page: Page) { + const + component = new ScrollyComponentObject(page), + initLoadSpy = await component.spyOn('initLoad', {proto: true}), + provider = new RequestInterceptor(page, /api/), + state = createState({}, createDataConveyor( + indexDataCtor, + sectionMountedItemCtor + )); + + provider.response(paginationHandler); + + return { + component, + initLoadSpy, + provider, + state + }; +} + +export interface DataConveyor { + addData(count: number): this; + addMounted(count: number): this; + get data(): DATA[]; + get mounted(): MountedComponentItem[]; +} + +export function createDataConveyor( + itemsCtor: DataItemCtor, + mountedCtor: MountedItemCtor +): DataConveyor { + const + data = [], + mounted = []; + + let + dataI = 0, + mountedI = 0; + + const obj: DataConveyor = { + addData(count: number) { + const + newData = createData(count, itemsCtor, dataI); + + data.push(...newData); + + dataI = data.length; + return this; + }, + + addMounted(count: number) { + const + newData = createData(count, itemsCtor, mountedI), + mountedData = createMountedDataFrom(newData, mountedCtor, mountedI); + + mounted.push(...mountedData); + + mountedI = mountedData.length; + return this; + }, + + get mounted() { + return mounted; + }, + + get data() { + return data; + } + }; + + return obj; +} + +export function createMountedDataFrom( + data: DATA[], + ctor: MountedItemCtor, + start: number = 0 +): MountedComponentItem[] { + return data.map((item, i) => ctor(item, start + i)); +} + +export function sectionMountedItemCtor(data: DATA, i: number): MountedComponentItem { + return { + index: i, + props: { + 'data-index': i + }, + item: 'section', + type: 'item', + key: Object.cast(undefined), + node: test.expect.any(String) + }; +} + +export function indexDataCtor(i: number): {i: number} { + return {i}; +} + +/** + * TODO: Docs + * @param count + * @param start + * @param itemCtor + */ +export function createData( + count: number, + itemCtor: (i: number) => DATA, + start: number = 0 +): DATA[] { + return Array.from(new Array(count), (_, i) => itemCtor(start + i)); +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function createState( + initial: Partial, + dataConveyor: DataConveyor +) { + const state = fromInitialState(initial); + + return { + compile() { + return { + ...state, + ...stateFromDataConveyor(dataConveyor) + }; + }, + + data: dataConveyor + }; +} + +export function fromInitialState(state: Partial): ComponentState { + return { + renderPage: 0, + loadPage: 0, + maxViewedIndex: test.expect.any(Number), + itemsTillEnd: test.expect.any(Number), + isInitialRender: true, + isInitialLoading: true, + isLastEmpty: false, + ...state + }; +} + +export function stateFromDataConveyor(conveyor: DataConveyor): Pick { + return { + data: conveyor.data, + lastLoaded: conveyor.data, + lastLoadedRawData: {data: conveyor.data}, + mountedItems: conveyor.mounted + }; +} diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/initialization.ts b/src/components/base/b-scrolly/test/unit/lifecycle/initialization.ts new file mode 100644 index 0000000000..63fc7dee46 --- /dev/null +++ b/src/components/base/b-scrolly/test/unit/lifecycle/initialization.ts @@ -0,0 +1,166 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * @file Test cases of the component lifecycle + */ + +import test from 'tests/config/unit/test'; + +import { defaultProps } from 'components/base/b-scrolly/const'; +import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; + +test.describe('', () => { + let + component: Awaited>['component'], + initLoadSpy: Awaited>['initLoadSpy'], + provider:Awaited>['provider'], + state: Awaited>['state']; + + test.beforeEach(async ({demoPage, page}) => { + await demoPage.goto(); + + ({component, initLoadSpy, provider, state} = await createTestHelpers(page)); + await provider.start(); + }); + + test('1', async () => { + const chunkSize = 12; + + await component.setProps({ + chunkSize, + disableObserver: true + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForDomInsertDoneEvent(); + + await test.expect(component.getContainerChildCount()).resolves.toBe(chunkSize); + }); + + test('2', async () => { + const + chunkSize = 12, + providerChunkSize = chunkSize / 2; + + const + shouldStopRequestingData = await component.mockFn(() => false), + shouldPerformDataRequest = await component.mockFn(defaultProps.shouldPerformDataRequest); + + state.data + .addData(providerChunkSize) + .addMounted(chunkSize); + + await component.setProps({ + chunkSize, + shouldStopRequestingData, + shouldPerformDataRequest, + disableObserver: true + }); + + await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); + await component.build(); + await component.waitForDomInsertDoneEvent(); + + await test.expect(shouldStopRequestingData.calls).resolves.toEqual([ + [ + state.compile(), + test.expect.any(Object) + ] + ]); + + await test.expect(shouldPerformDataRequest.calls).resolves.toEqual([ + [ + state.compile(), + test.expect.any(Object) + ] + ]); + + await test.expect(initLoadSpy.calls).resolves.toEqual([[], []]); + await test.expect(component.getContainerChildCount()).resolves.toBe(chunkSize); + }); + + test('3', async () => { + const + chunkSize = 12, + providerChunkSize = chunkSize / 2; + + const + shouldStopRequestingData = await component.mockFn(() => false), + shouldPerformDataRequest = await component.mockFn(() => false); + + state.data + .addData(providerChunkSize) + .addMounted(providerChunkSize); + + await component.setProps({ + chunkSize, + shouldStopRequestingData, + shouldPerformDataRequest, + disableObserver: true + }); + + await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); + await component.build(); + await component.waitForContainerChildCountEqualsTo(providerChunkSize); + + await test.expect(shouldStopRequestingData.calls).resolves.toEqual([ + [ + state.compile(), + test.expect.any(Object) + ] + ]); + + await test.expect(shouldPerformDataRequest.calls).resolves.toEqual([ + [ + state.compile(), + test.expect.any(Object) + ] + ]); + + await test.expect(initLoadSpy.calls).resolves.toEqual([[]]); + await test.expect(component.getContainerChildCount()).resolves.toBe(providerChunkSize); + }); + + test('4', async () => { + const + chunkSize = 12, + providerChunkSize = chunkSize / 2; + + const + shouldStopRequestingData = await component.mockFn(() => true), + shouldPerformDataRequest = await component.mockFn(() => false); + + state.data + .addData(providerChunkSize) + .addMounted(providerChunkSize); + + await component.setProps({ + chunkSize, + shouldStopRequestingData, + shouldPerformDataRequest, + disableObserver: true + }); + + await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); + await component.build(); + await component.waitForDomInsertDoneEvent(); + + await test.expect(shouldStopRequestingData.calls).resolves.toEqual([ + [ + state.compile(), + test.expect.any(Object) + ] + ]); + + await test.expect(initLoadSpy.calls).resolves.toEqual([[]]); + await test.expect(shouldPerformDataRequest.calls).resolves.toEqual([]); + await test.expect(component.getContainerChildCount()).resolves.toBe(providerChunkSize); + }); +}); diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts b/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts new file mode 100644 index 0000000000..8b629d5dd1 --- /dev/null +++ b/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts @@ -0,0 +1,149 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * @file Test cases of the component lifecycle + */ + +import test from 'tests/config/unit/test'; + +import { createData, createTestHelpers, indexDataCtor } from 'components/base/b-scrolly/test/api/helpers'; +import type { SlotsStateObj } from 'components/base/b-scrolly/modules/slots'; + +test.describe(' slots', () => { + let + component: Awaited>['component'], + provider: Awaited>['provider']; + + test.beforeEach(async ({demoPage, page}) => { + await demoPage.goto(); + + ({component, provider} = await createTestHelpers(page)); + await provider.start(); + + await component.setChildren({ + done: { + type: 'div', + attrs: { + id: 'done' + } + } + }); + }); + + test.describe('`done`', () => { + test('Activates when all data has been loaded after the initial load', async () => { + const chunkSize = 12; + + provider + .responseOnce(200, {data: createData(chunkSize, indexDataCtor)}) + .response(200, {data: []}); + + await component.setProps({ + chunkSize, + shouldStopRequestingData: () => true + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForSlotState('done', true); + + const + slots = await component.getSlotsState(); + + test.expect(slots).toEqual(>{ + container: true, + done: true, + empty: false, + loader: false, + renderNext: false, + retry: false, + tombstones: false + }); + }); + + test.only('Activates when all data has been loaded after the second load', async () => { + const chunkSize = 12; + + provider + .responseOnce(200, {data: createData(chunkSize, indexDataCtor)}) + .responseOnce(200, {data: createData(chunkSize, indexDataCtor, chunkSize)}) + .response(200, {data: []}); + + await component.setProps({ + chunkSize, + shouldStopRequestingData: ({lastLoadedRawData}) => { + debugger; + return lastLoadedRawData.data.length < 12; + }, + shouldPerformDataRequest: ({lastLoadedRawData}) => lastLoadedRawData.data.length >= 12, + shouldPerformDataRender: () => true + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.scrollToBottom(); + await component.waitForSlotState('done', true); + + const + slots = await component.getSlotsState(); + + test.expect(slots).toEqual(>{ + container: true, + done: true, + empty: false, + loader: false, + renderNext: false, + retry: false, + tombstones: false + }); + }); + + test('Activates when data loading is completed but data is less than chunkSize', async () => { + const chunkSize = 12; + + provider + .responseOnce(200, {data: createData(chunkSize / 2, indexDataCtor)}) + .response(200, {data: []}); + + await component.setProps({ + chunkSize, + shouldStopRequestingData: () => true + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForSlotState('done', true); + + const + slots = await component.getSlotsState(); + + test.expect(slots).toEqual(>{ + container: true, + done: true, + empty: false, + loader: false, + renderNext: false, + retry: false, + tombstones: false + }); + }); + }); + + test.describe('empty', async () => { + // ... + }); + + test.describe('tombstone & loader', async () => { + // ... + }); + + test.describe('retry', async () => { + // ... + }); +}); diff --git a/src/core/prelude/test-env/components/json.ts b/src/core/prelude/test-env/components/json.ts index 2d70a55d32..e987c98eb5 100644 --- a/src/core/prelude/test-env/components/json.ts +++ b/src/core/prelude/test-env/components/json.ts @@ -11,6 +11,7 @@ const fnEvalSymbol = Symbol('Function for eval'); export const fnAlias = 'FN__', fnEvalAlias = 'FNEVAL__', + fnMockAlias = 'FNMOCK__', regExpAlias = 'REGEX__'; export function evalFn(func: T): T { @@ -18,6 +19,31 @@ export function evalFn(func: T): T { return func; } +/** + * TODO: DOCS + * @param obj + * @param id + */ +export function setSerializerAsMockFn(obj: T, id: string): T { + Object.assign(obj, { + toJSON: () => `${fnMockAlias}${id}` + }); + + return obj; +} + +export function stringifyFunction(val: Function): string { + if (val[fnEvalSymbol] != null) { + return `${fnEvalAlias}${val.toString()}`; + } + + return `${fnAlias}${val.toString()}`; +} + +export function stringifyRegExp(regExp: RegExp): string { + return `${regExpAlias}${JSON.stringify({source: regExp.source, flags: regExp.flags})}`; +} + /** * Stringifies the passed object to a JSON string and returns it. * The function also supports serialization of functions and regular expressions. @@ -27,15 +53,11 @@ export function evalFn(func: T): T { export function expandedStringify(obj: object): string { return JSON.stringify(obj, (_, val) => { if (Object.isFunction(val)) { - if (val[fnEvalSymbol] != null) { - return `${fnEvalAlias}${val.toString()}`; - } - - return `${fnAlias}${val.toString()}`; + return stringifyFunction(val); } if (Object.isRegExp(val)) { - return `${regExpAlias}${JSON.stringify({source: val.source, flags: val.flags})}`; + return stringifyRegExp(val); } return val; @@ -61,6 +83,11 @@ export function expandedParse(str: string): T { return Function(`return ${val.replace(fnEvalAlias, '')}`)()(); } + if (val.startsWith(fnMockAlias)) { + const mockId = val.replace(fnMockAlias, ''); + return globalThis[mockId]; + } + if (val.startsWith(regExpAlias)) { const obj = JSON.parse(val.replace(regExpAlias, '')); return new RegExp(obj.source, obj.flags); diff --git a/src/core/prelude/test-env/index.ts b/src/core/prelude/test-env/index.ts index e14cf6ceca..8800be4829 100644 --- a/src/core/prelude/test-env/index.ts +++ b/src/core/prelude/test-env/index.ts @@ -14,3 +14,4 @@ import 'core/prelude/test-env/import'; import 'core/prelude/test-env/components'; import 'core/prelude/test-env/gestures'; +import 'core/prelude/test-env/mock'; diff --git a/src/core/prelude/test-env/mock/index.ts b/src/core/prelude/test-env/mock/index.ts new file mode 100644 index 0000000000..7029b6a334 --- /dev/null +++ b/src/core/prelude/test-env/mock/index.ts @@ -0,0 +1,47 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * @file Provides an API to Work with `jest-mock` package + */ + +import { ModuleMocker } from 'jest-mock'; + +let + globalApi: ModuleMocker; + +globalThis.jest = { + /** + * {@link ModuleMocker.spyOn} + * @see https://jestjs.io/docs/mock-functions + * + * @param args + */ + spy: (...args: Parameters): any => { + globalApi ??= mockerFactory(); + return globalApi.spyOn(...args); + }, + + /** + * {@link ModuleMocker.fn} + * @see https://jestjs.io/docs/mock-functions + * + * @param args + */ + mock: (...args: any[]): any => { + globalApi ??= mockerFactory(); + return globalApi.fn(...args); + } +}; + +/** + * {@link ModuleMocker} + */ +function mockerFactory(): ModuleMocker { + return new ModuleMocker(globalThis); +} diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts new file mode 100644 index 0000000000..2a92371f0c --- /dev/null +++ b/tests/helpers/component-object/builder.ts @@ -0,0 +1,207 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { JSHandle, Locator, Page } from 'playwright'; +import path from 'upath'; +import { resolve } from '@pzlr/build-core'; + +import type iBlock from 'components/super/i-block/i-block'; +import { Component, DOM, Utils } from 'tests/helpers'; + +export default class ComponentObjectBuilder { + + /** + * Name of the component that will be created + */ + readonly componentName: string; + + /** + * Component props + */ + readonly props: Dictionary = {}; + + /** + * Component children + */ + readonly children: VNodeChildren = {}; + + /** + * Component root element locator + */ + readonly node: Locator; + + /** + * Component path to import via webpack require. + * By default plzr.resolve.blockSync will be used + */ + readonly componentClassImportPath: Nullable; + + /** + * {@link Page} + */ + protected page: Page; + + /** + * Uniq component node id + */ + protected id: string; + + /** + * Stores component instance + */ + protected componentStore?: JSHandle; + + /** + * Short hand for generating element selectors + * {@link DOM.elNameSelectorGenerator} + * + * @example + * ```typescript + * this.elSelector('element') // .${componentName}__element + * ``` + */ + get elSelector(): (elName: string) => string { + return DOM.elNameSelectorGenerator(this.componentName); + } + + /** + * Component link + */ + get component(): JSHandle { + if (!this.componentStore) { + throw new Error('Bad access to the component without `build` or `pick` call'); + } + + return this.componentStore; + } + + /** + * Returns if the `component` property are available. (`ComponentObject` are builded). + */ + get isBuilded(): boolean { + return Boolean(this.componentStore); + } + + /** + * @param page + * @param componentName + */ + constructor(page: Page, componentName: string) { + this.page = page; + this.componentName = componentName; + this.id = `${this.componentName}_${Math.random().toString()}`; + this.props = {'data-testid': this.id}; + this.node = page.getByTestId(this.id); + this.componentClassImportPath = path.join(path.relative(`${process.cwd()}/src`, resolve.blockSync(this.componentName)!), `/${this.componentName}.ts`); + } + + /** + * Returns a component class + */ + async getComponentClass(): Promise COMPONENT>> { + const + {componentClassImportPath} = this; + + if (componentClassImportPath == null) { + throw new Error('Missing component path'); + } + + const + classModule = await Utils.import<{default: new () => COMPONENT}>(this.page, componentClassImportPath), + classInstance = await classModule.evaluateHandle((ctx) => ctx.default); + + return classInstance; + } + + /** + * Creates a `component` instance with the provided + * in constructor `componentName` and settled via `setProps` properties + */ + async build(): Promise> { + this.componentStore = await Component.createComponent(this.page, this.componentName, { + attrs: { + ...this.props + }, + children: this.children + }); + + return this.componentStore; + } + + /** + * Picks the `Node` with the provided selector and extracts 'component' property + * that will be settled as `component` property of the `ComponentObject`. + * + * After this operation `ComponentObject` will be marked as builded and the `ComponentObject.component` + * property will be accessible. + * + * @param selectorOrLocator + */ + async pick(selector: string): Promise; + + /** + * Extracts 'component' property from the provided locator + * that will be settled as `component` property of the `ComponentObject`. + * + * After this operation `ComponentObject` will be marked as builded and the `ComponentObject.component` + * property will be accessible. + * + * @param locator + */ + async pick(locator: Locator): Promise; + + /** + * @inheritdoc + */ + async pick(selectorOrLocator: string | Locator): Promise { + const + locator = Object.isString(selectorOrLocator) ? this.page.locator(selectorOrLocator) : selectorOrLocator; + + this.componentStore = await locator.elementHandle().then(async (el) => { + await el?.evaluate((ctx, [id]) => ctx.setAttribute('data-test-id', id), [this.id]); + return el?.getProperty('component'); + }); + + await this.applyProps(); + + return this; + } + + /** + * Saves the provided props to store. + * After component will be created or picked the stored props will be settled + * + * @param props + */ + setProps(props: Dictionary): this { + Object.assign(this.props, props); + return this; + } + + /** + * Saves the provided child to store. + * After component will be created the stored children will be settled + * + * @param children + */ + setChildren(children: VNodeChildren): this { + Object.assign(this.children, children); + return this; + } + + /** + * Applies the settled via `setProps` props to the component instance + */ + async applyProps(): Promise { + const + {component, props} = this; + + await component.evaluate((ctx, [props]) => Object.assign(ctx, props), [props]); + return this; + } +} diff --git a/tests/helpers/component-object/index.ts b/tests/helpers/component-object/index.ts new file mode 100644 index 0000000000..e8017e5e24 --- /dev/null +++ b/tests/helpers/component-object/index.ts @@ -0,0 +1,14 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type iBlock from 'components/super/i-block/i-block'; +import ComponentObjectMock from 'tests/helpers/component-object/mock'; + +export default class ComponentObject extends ComponentObjectMock { + // ... +} diff --git a/tests/helpers/component-object/initializer.ts b/tests/helpers/component-object/initializer.ts new file mode 100644 index 0000000000..23ded78850 --- /dev/null +++ b/tests/helpers/component-object/initializer.ts @@ -0,0 +1,46 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type iBlock from 'components/super/i-block/i-block'; +import ComponentObjectBuilder from 'tests/helpers/component-object/builder'; + +interface _InitializerFunction { + (ctx: C, ...args: ARGS): unknown; +} + +export default class ComponentObjectInitializer extends ComponentObjectBuilder { + // TODO: Implement or destroy +} + +/** + * Почему такая странная схема с инициализацией: + * + * Все проблемы из-за за замыканий, + * допустим у нас метод который на вход принимает путь на который надо установить spy и хук на который это сделать + * + * ```typescript + * async spyOn(path: string, spyOptions: {hook: string}): Promise + * ``` + * + * Пытаемся сделать реализацию: + * + * ```typescript + * async spyOn(path: string, spyOptions: {hook: string}): Promise { + * this.setProps({ + * [`@componentHook:${hook}`]: (ctx) => jest.spy(ctx, path) + * }) + * } + * ``` + * + * Ииии падаем с ошибкой во время выполнения Reference error path is not defined так как функция + * будет передана в браузера + */ + + // Expose function for pushing messages to the Node.js script. +// const log = []; +// await page.exposeFunction('logCall', msg => log.push(msg)); diff --git a/tests/helpers/component-object/interface.ts b/tests/helpers/component-object/interface.ts new file mode 100644 index 0000000000..f6cd51e7f8 --- /dev/null +++ b/tests/helpers/component-object/interface.ts @@ -0,0 +1,22 @@ +export interface SpyOptions { + /** + * If `true` then the spy will be settled to the prototype method. + * The spy will be settled before component creating. + */ + proto?: boolean; +} + +export interface CompileSpyObject { + /** + * Returns a snapshot of the current spy object state. + */ + compile(): Promise; +} + +export interface SpyObject extends CompileSpyObject { + get calls(): Promise; + get callsLength(): Promise; + get lastCall(): Promise; +} + +export type SyncSpyObject = {[P in keyof SpyObject]: Awaited} & CompileSpyObject; diff --git a/tests/helpers/component-object/mock.ts b/tests/helpers/component-object/mock.ts new file mode 100644 index 0000000000..a57b67df3d --- /dev/null +++ b/tests/helpers/component-object/mock.ts @@ -0,0 +1,126 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type iBlock from 'components/super/i-block/i-block'; + +import type { SpyObject, SpyOptions } from 'tests/helpers/component-object/interface'; +import { createAndDisposeMock, spy } from 'tests/helpers/mock'; +import { setSerializerAsMockFn } from 'core/prelude/test-env/components/json'; +import ComponentObjectInitializer from 'tests/helpers/component-object/initializer'; + +export default class ComponentObjectMock extends ComponentObjectInitializer { + + /** + * Creates a spy for the specified path + * + * @param path + * @param spyOptions + * + * Sets a spy to the `component instance`: + * + * @example + * ```typescript + * const builder = new ComponentBuilder(page, 'b-component'); + * builder.spyOn('initLoad'); + * await builder.build(); + * + * const + * calls = await builder.spies.initLoad.calls, + * lastCall = await builder.spies.initLoad.lastCall; + * ``` + * + * Sets a spy to the `prototype`: + * + * @example + * ```typescript + * const builder = new ComponentBuilder(page, 'b-component'); + * builder.spyOn('initLoad', {proto: true}); + * await builder.build(); + * + * const + * calls = await builder.spies.initLoad.calls, + * lastCall = await builder.spies.initLoad.lastCall; + * ``` + */ + async spyOn(path: string, spyOptions?: SpyOptions): Promise { + const + evaluateArgs = [path, spyOptions], + ctx = spyOptions?.proto ? await this.getComponentClass() : this.component; + + const instance = await spy(ctx, (ctx, [path, spyOptions]) => { + if (spyOptions?.proto === true) { + path = `prototype.${path}`; + } + + const + pathArray = path.split('.'), + method = pathArray.pop(); + + const + obj = pathArray.length >= 1 ? Object.get(ctx, pathArray.join('.')) : ctx; + + if (!obj) { + throw new ReferenceError(`Cannot find object by the provided path: ${path}`); + } + + return jest.spy( + obj, + method + ); + }, evaluateArgs); + + return instance; + } + + /** + * Creates a mock function + * @param paths + * + * @example + * ```typescript + * const builder = new ComponentBuilder(page, 'b-component'); + * builder.mock({initLoad: builder.mockFn()}); + * await builder.build(); + * + * const + * calls = await builder.mocks.initLoad.calls, + * lastCall = await builder.mocks.initLoad.lastCall; + * + * await builder.mocks.initLoad.implementation(() => 123); + * const result = await builder.component.evaluate((ctx) => ctx.initLoad()); + * console.log(result) // 123; + * ``` + * + * Mock the prototype function + * + * @example + * ```typescript + * const builder = new ComponentBuilder(page, 'b-component'); + * + * builder.mock({ + * initLoad: { + * fn: builder.mockFn(), + * proto: true + * } + * }); + * + * await builder.build(); + * ``` + * + * > Notice that the implementation will be provided into browser, + * this imposes some restrictions, such as not being able to use a closure + */ + async mockFn(fn?: (...args: any[]) => any): Promise { + fn ??= () => undefined; + + const + {agent, id} = await createAndDisposeMock(this.page, fn); + + return setSerializerAsMockFn(agent, id); + } +} diff --git a/tests/helpers/index.ts b/tests/helpers/index.ts index ad74005cb4..6bc1160d70 100644 --- a/tests/helpers/index.ts +++ b/tests/helpers/index.ts @@ -10,6 +10,7 @@ import DOM from 'tests/helpers/dom'; import BOM from 'tests/helpers/bom'; import Utils from 'tests/helpers/utils'; import Component from 'tests/helpers/component'; +import ComponentObject from 'tests/helpers/component-object'; import Scroll from 'tests/helpers/scroll'; import Router from 'tests/helpers/router'; import Gestures from 'tests/helpers/gestures'; @@ -20,6 +21,7 @@ export { DOM, Utils, Component, + ComponentObject, Scroll, Router, Gestures diff --git a/tests/helpers/mock/index.ts b/tests/helpers/mock/index.ts new file mode 100644 index 0000000000..5d95df1fb3 --- /dev/null +++ b/tests/helpers/mock/index.ts @@ -0,0 +1,75 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { ModuleMocker } from 'jest-mock'; +import type { JSHandle, Page } from 'playwright'; +import type { SpyObject, SyncSpyObject } from 'tests/helpers/component-object/interface'; + +function wrapAsSpy(agent: JSHandle | ReturnType>, obj: T): T & SpyObject { + Object.defineProperties(obj, { + calls: { + get: () => agent.evaluate((ctx) => ctx.mock.calls) + }, + + callsLength: { + get: () => agent.evaluate((ctx) => ctx.mock.calls.length) + }, + + lastCall: { + get: () => agent.evaluate((ctx) => ctx.mock.calls[ctx.mock.calls.length - 1]) + }, + + compile: { + value: async () => { + const [ + calls, + lastCall, + callsLength + ] = await agent.evaluate((ctx) => [ + ctx.mock.calls, + ctx.mock.lastCall, + ctx.mock.calls.length + ]); + + return { + calls, + lastCall, + callsLength, + compile: (obj).compile.bind(obj) + }; + } + } + }); + + return obj; +} + +export async function spy( + ctx: T, + spyCtor: (ctx: T, ...args: ARGS) => ReturnType, + ...argsToCtor: ARGS +): Promise { + const + agent = await ctx.evaluateHandle>(spyCtor, ...argsToCtor); + + return wrapAsSpy(agent, {}); +} + +export async function createAndDisposeMock( + page: Page, + fn: (...args: any[]) => any +): Promise<{agent: SpyObject; id: string}> { + const + tmpFn = `tmp_${Math.random().toString()}`; + + const agent = await page.evaluateHandle(([tmpFn, fnString]) => + // eslint-disable-next-line no-new-func + globalThis[tmpFn] = jest.mock(Object.cast(new Function(`return ${fnString}`)())), [tmpFn, fn.toString()]); + + return {agent: wrapAsSpy(agent, {}), id: tmpFn}; +} diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts new file mode 100644 index 0000000000..44b5dffcfc --- /dev/null +++ b/tests/helpers/providers/interceptor/index.ts @@ -0,0 +1,180 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { BrowserContext, Page, Request, Route } from 'playwright'; +import { ModuleMocker } from 'jest-mock'; + +type ResponseHandler = (route: Route, request: Request) => CanPromise; + +/** + * API that provides simple way to intercept and response to any request + */ +export class RequestInterceptor { + /** + * Route context + */ + readonly routeCtx: Page | BrowserContext; + + /** + * Route patter + */ + readonly routePattern: string | RegExp; + + /** + * Route listener + */ + readonly routeListener: ResponseHandler; + + /** + * Default response that will be used to response every request if there is not responses in `responseQueue` + */ + readonly mock: ReturnType; + + /** + * @param ctx + * @param pattern + */ + constructor(ctx: Page | BrowserContext, pattern: string | RegExp) { + this.routeCtx = ctx; + this.routePattern = pattern; + + this.routeListener = async (route: Route, request: Request) => { + await this.mock(route, request); + }; + + const mocker = new ModuleMocker(globalThis); + this.mock = mocker.fn(); + } + + /** + * Sets a response for one request + * + * @example + * ```typescript + * const interceptor = new RequestInterceptor(page, /api/); + * + * interceptor + * .response((r: Route) => r.fulfill({status: 200})); + * .response((r: Route) => r.fulfill({status: 500})); + * ``` + */ + responseOnce(handler: ResponseHandler): this; + + /** + * Sets a response for one request + * + * @example + * ```typescript + * const interceptor = new RequestInterceptor(page, /api/); + * + * interceptor + * .responseOnce(200, {content: 1}); + * .responseOnce(500) + * ``` + */ + responseOnce(status: number, payload: object | string | number): this; + + /** + * @inheritdoc + */ + responseOnce(handlerOrStatus: number | ResponseHandler, payload?: object | string | number): this { + let fn; + + if (Object.isFunction(handlerOrStatus)) { + fn = handlerOrStatus; + + } else { + const status = handlerOrStatus; + fn = this.cookResponseFn(status, payload); + } + + this.mock.mockImplementationOnce(fn); + return this; + } + + /** + * Sets a response for every request. + * If there is not responses settled via `responseOnce` (ie `responseQueue` is empty) that response will be used + * + * @example + * ```typescript + * const interceptor = new RequestInterceptor(page, /api/); + * interceptor.response((r: Route) => r.fulfill({status: 200})); + * ``` + * + * @param handler + */ + response(handler: ResponseHandler): this; + + /** + * Sets a response for every request. + * If there is not responses settled via `responseOnce` (ie `responseQueue` is empty) that response will be used + * + * @example + * ```typescript + * const interceptor = new RequestInterceptor(page, /api/); + * interceptor.response(200, {}); + * ``` + * + * @param status + * @param payload + */ + response(status: number, payload: object | string | number): this; + + /** + * @inheritdoc + */ + response(handlerOrStatus: number | ResponseHandler, payload?: object | string | number): this { + let fn; + + if (Object.isFunction(handlerOrStatus)) { + fn = handlerOrStatus; + + } else { + const status = handlerOrStatus; + fn = this.cookResponseFn(status, payload); + } + + this.mock.mockImplementation(fn); + return this; + } + + /** + * Clears the responses that was created via `responseOnce` + */ + clearResponseQueue(): this { + this.mock.mockReset(); + return this; + } + + /** + * Stops the request interception + */ + async stop(): Promise { + await this.routeCtx.unroute(this.routePattern, this.routeListener); + return this; + } + + /** + * Starts the request interception + */ + async start(): Promise { + await this.routeCtx.route(this.routePattern, this.routeListener); + return this; + } + + /** + * Cooks a response handler + * + * @param status + * @param payload + */ + protected cookResponseFn(status: number, payload?: string | object | number): ResponseHandler { + return (route) => route.fulfill({status, body: JSON.stringify(payload), contentType: 'application/json'}); + } +} diff --git a/tests/helpers/providers/pagination/index.ts b/tests/helpers/providers/pagination/index.ts index 71582e03e6..f959363f3e 100644 --- a/tests/helpers/providers/pagination/index.ts +++ b/tests/helpers/providers/pagination/index.ts @@ -7,7 +7,7 @@ */ import { fromQueryString } from 'core/url'; -import type { BrowserContext, Page } from 'playwright'; +import type { BrowserContext, Page, Route } from 'playwright'; import type { RequestState, RequestQuery } from 'tests/helpers/providers/pagination/interface'; @@ -27,60 +27,67 @@ const requestStates: Dictionary = { export async function interceptPaginationRequest( pageOrContext: Page | BrowserContext ): Promise { - return pageOrContext.route(/api/, async (route) => { - const routeQuery = fromQueryString(new URL(route.request().url()).search); - - const query = { - chunkSize: 12, - id: String(Math.random()), - sleep: 100, - ...routeQuery - }; - - const res = { - status: 200 - }; - - await sleep(query.sleep); - - // eslint-disable-next-line no-multi-assign - const state = requestStates[query.id] = requestStates[query.id] ?? { - i: 0, - requestNumber: 0, - totalSent: 0, - failCount: 0, - ...query - }; - - const - isFailCountNotReached = query.failCount != null ? state.failCount <= query.failCount : true; - - if (Object.isNumber(query.failOn) && query.failOn === state.requestNumber && isFailCountNotReached) { - state.failCount++; - res.status = 500; - return undefined; - } - - state.requestNumber++; - - if (state.totalSent === state.total) { - return { - ...query.additionalData, - data: [] - }; - } - - const dataToSend = Array.from(Array(query.chunkSize), () => ({i: state.i++})); - state.totalSent += dataToSend.length; + return pageOrContext.route(/api/, paginationHandler); +} +/** + * Handler for intercepted pagination requests + * @param route + */ +export async function paginationHandler(route: Route): Promise { + const routeQuery = fromQueryString(new URL(route.request().url()).search); + + const query = { + chunkSize: 12, + id: String(Math.random()), + sleep: 100, + ...routeQuery + }; + + const res = { + status: 200 + }; + + await sleep(query.sleep); + + // eslint-disable-next-line no-multi-assign + const state = requestStates[query.id] = requestStates[query.id] ?? { + i: 0, + requestNumber: 0, + totalSent: 0, + failCount: 0, + ...query + }; + + const + isFailCountNotReached = query.failCount != null ? state.failCount <= query.failCount : true; + + if (Object.isNumber(query.failOn) && query.failOn === state.requestNumber && isFailCountNotReached) { + state.failCount++; + res.status = 500; + return undefined; + } + + state.requestNumber++; + + if (state.totalSent === state.total) { return route.fulfill({ status: res.status, contentType: 'application/json', - body: JSON.stringify({ - ...query.additionalData, - data: dataToSend - }) + body: JSON.stringify([]) }); + } + + const dataToSend = Array.from(Array(query.chunkSize), () => ({i: state.i++})); + state.totalSent += dataToSend.length; + + return route.fulfill({ + status: res.status, + contentType: 'application/json', + body: JSON.stringify({ + ...query.additionalData, + data: dataToSend + }) }); } diff --git a/yarn.lock b/yarn.lock index 70a61337e4..bc3806f7d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1613,6 +1613,29 @@ __metadata: languageName: node linkType: hard +"@jest/schemas@npm:^28.1.3": + version: 28.1.3 + resolution: "@jest/schemas@npm:28.1.3" + dependencies: + "@sinclair/typebox": ^0.24.1 + checksum: 3cf1d4b66c9c4ffda58b246de1ddcba8e6ad085af63dccdf07922511f13b68c0cc480a7bc620cb4f3099a6f134801c747e1df7bfc7a4ef4dceefbdea3e31e1de + languageName: node + linkType: hard + +"@jest/types@npm:^28.1.3": + version: 28.1.3 + resolution: "@jest/types@npm:28.1.3" + dependencies: + "@jest/schemas": ^28.1.3 + "@types/istanbul-lib-coverage": ^2.0.0 + "@types/istanbul-reports": ^3.0.0 + "@types/node": "*" + "@types/yargs": ^17.0.8 + chalk: ^4.0.0 + checksum: 1e258d9c063fcf59ebc91e46d5ea5984674ac7ae6cae3e50aa780d22b4405bf2c925f40350bf30013839eb5d4b5e521d956ddf8f3b7c78debef0e75a07f57350 + languageName: node + linkType: hard + "@jridgewell/gen-mapping@npm:^0.1.0": version: 0.1.1 resolution: "@jridgewell/gen-mapping@npm:0.1.1" @@ -1779,6 +1802,13 @@ __metadata: languageName: node linkType: hard +"@sinclair/typebox@npm:^0.24.1": + version: 0.24.51 + resolution: "@sinclair/typebox@npm:0.24.51" + checksum: fd0d855e748ef767eb19da1a60ed0ab928e91e0f358c1dd198d600762c0015440b15755e96d1176e2a0db7e09c6a64ed487828ee10dd0c3e22f61eb09c478cd0 + languageName: node + linkType: hard + "@sindresorhus/is@npm:^0.7.0": version: 0.7.0 resolution: "@sindresorhus/is@npm:0.7.0" @@ -2376,6 +2406,31 @@ __metadata: languageName: node linkType: hard +"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0": + version: 2.0.4 + resolution: "@types/istanbul-lib-coverage@npm:2.0.4" + checksum: a25d7589ee65c94d31464c16b72a9dc81dfa0bea9d3e105ae03882d616e2a0712a9c101a599ec482d297c3591e16336962878cb3eb1a0a62d5b76d277a890ce7 + languageName: node + linkType: hard + +"@types/istanbul-lib-report@npm:*": + version: 3.0.0 + resolution: "@types/istanbul-lib-report@npm:3.0.0" + dependencies: + "@types/istanbul-lib-coverage": "*" + checksum: 656398b62dc288e1b5226f8880af98087233cdb90100655c989a09f3052b5775bf98ba58a16c5ae642fb66c61aba402e07a9f2bff1d1569e3b306026c59f3f36 + languageName: node + linkType: hard + +"@types/istanbul-reports@npm:^3.0.0": + version: 3.0.1 + resolution: "@types/istanbul-reports@npm:3.0.1" + dependencies: + "@types/istanbul-lib-report": "*" + checksum: f1ad54bc68f37f60b30c7915886b92f86b847033e597f9b34f2415acdbe5ed742fa559a0a40050d74cdba3b6a63c342cac1f3a64dba5b68b66a6941f4abd7903 + languageName: node + linkType: hard + "@types/jasmine@npm:3.10.3": version: 3.10.3 resolution: "@types/jasmine@npm:3.10.3" @@ -2515,7 +2570,7 @@ __metadata: languageName: node linkType: hard -"@types/yargs@npm:^17.0.10": +"@types/yargs@npm:^17.0.10, @types/yargs@npm:^17.0.8": version: 17.0.24 resolution: "@types/yargs@npm:17.0.24" dependencies: @@ -2694,6 +2749,7 @@ __metadata: imagemin-webp: 6.0.0 is-path-inside: 3.0.3 jasmine: 3.99.0 + jest-mock: 28.1.3 jsdom: 16.7.0 merge2: 1.4.1 mini-css-extract-plugin: 2.5.3 @@ -11096,6 +11152,16 @@ __metadata: languageName: node linkType: hard +"jest-mock@npm:28.1.3": + version: 28.1.3 + resolution: "jest-mock@npm:28.1.3" + dependencies: + "@jest/types": ^28.1.3 + "@types/node": "*" + checksum: a573bf8e5f12f4c29c661266c31b5c6b69a28d3195b83049983bce025b2b1a0152351567e89e63b102ef817034c2a3aa97eda4e776f3bae2aee54c5765573aa7 + languageName: node + linkType: hard + "jest-worker@npm:^27.0.2, jest-worker@npm:^27.4.5": version: 27.5.1 resolution: "jest-worker@npm:27.5.1" From bfc4fb194a8ef415050c7332a8efcad525e2fcad Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 7 Jun 2023 20:27:49 +0300 Subject: [PATCH 006/159] WIP --- src/components/base/b-scrolly/README.md | 8 +- src/components/base/b-scrolly/b-scrolly.ts | 69 +++--- src/components/base/b-scrolly/const.ts | 39 ++-- src/components/base/b-scrolly/interface.ts | 52 ++--- .../{local-events => emitter}/index.ts | 2 +- .../base/b-scrolly/modules/factory/index.ts | 4 +- .../base/b-scrolly/modules/juggler/index.ts | 59 +++-- .../observer/engines/intersection-observer.ts | 4 +- .../base/b-scrolly/modules/slots/index.ts | 16 +- .../base/b-scrolly/modules/state/helpers.ts | 49 ++++ .../base/b-scrolly/modules/state/index.ts | 177 ++++++++------- .../test/api/component-object/index.ts | 35 ++- .../base/b-scrolly/test/api/helpers/index.ts | 65 ++++-- .../b-scrolly/test/unit/functional/emitter.ts | 209 ++++++++++++++++++ .../b-scrolly/test/unit/functional/state.ts | 139 ++++++++++++ .../test/unit/lifecycle/initialization.ts | 71 ++++-- .../b-scrolly/test/unit/lifecycle/slots.ts | 9 +- .../base/b-virtual-scroll/b-virtual-scroll.ss | 7 - src/components/super/i-block/interface.ts | 3 + tests/helpers/component-object/interface.ts | 1 + tests/helpers/component-object/mock.ts | 11 +- tests/helpers/mock/index.ts | 9 +- tests/helpers/mock/interface.ts | 8 + tests/helpers/providers/interceptor/index.ts | 31 ++- 24 files changed, 818 insertions(+), 259 deletions(-) rename src/components/base/b-scrolly/modules/{local-events => emitter}/index.ts (96%) create mode 100644 src/components/base/b-scrolly/modules/state/helpers.ts create mode 100644 src/components/base/b-scrolly/test/unit/functional/emitter.ts create mode 100644 src/components/base/b-scrolly/test/unit/functional/state.ts create mode 100644 tests/helpers/mock/interface.ts diff --git a/src/components/base/b-scrolly/README.md b/src/components/base/b-scrolly/README.md index d80ff9eb30..c152072b00 100644 --- a/src/components/base/b-scrolly/README.md +++ b/src/components/base/b-scrolly/README.md @@ -2,8 +2,12 @@ - PartialRender ? Если указано к отрисовке 10 компонентов а загружено за раз данных 5 - рисовать ли эти 5 и потом еще 5 - Подумать над форматом данных к отрисовке, раньше клиент получал 10 элементов данных и должен был вернуть 10 элементов к отрисовке - Preload нескольких страниц - - +- Обработка ошибок тесты +- Проверка стейта во время ошибок +- typedLocalEmitter можно ли избавиться и как-то нормально типизировать события компонента +- кейс: загрузили чанк отрисовали -> загружаем следующий (он грузится 20 сек) -> пока грузится полный скролл внизу -> запрещаем загрузку и переходим в isDone состояние -> данные загрузились -> что произойдет??? +- componentInternalState.setLoadingPage(val) -> componentInternalState.set('loadingPage', val); +- стоит ли для state использовать builder like подход ## TODO: diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts index ae93b847cf..5d13d1fa30 100644 --- a/src/components/base/b-scrolly/b-scrolly.ts +++ b/src/components/base/b-scrolly/b-scrolly.ts @@ -24,7 +24,7 @@ import type { ComponentRenderStrategyKeys as ComponentRenderStrategyKeys, RequestParams, RequestQueryFn, - ShouldRequestFn, + ShouldFn, ComponentRefs, ComponentItemFactory, ComponentItemType, @@ -48,7 +48,7 @@ import { Observer } from 'components/base/b-scrolly/modules/observer'; import { ComponentFactory } from 'components/base/b-scrolly/modules/factory'; import { SlotsStateController } from 'components/base/b-scrolly/modules/slots'; import { ComponentInternalState } from 'components/base/b-scrolly/modules/state'; -import { typedLocalEmitterFactory } from 'components/base/b-scrolly/modules/local-events'; +import { typedEmitterFactory } from 'components/base/b-scrolly/modules/emitter'; import iData, { component, prop, system, $$ } from 'components/super/i-data/i-data'; @@ -142,7 +142,7 @@ export default class bScrolly extends iData implements iItems { default: defaultProps.shouldStopRequestingData }) - readonly shouldStopRequestingData!: ShouldRequestFn; + readonly shouldStopRequestingData!: ShouldFn; /** * When this function returns `true` the component will be able to request additional data. @@ -153,7 +153,7 @@ export default class bScrolly extends iData implements iItems { default: defaultProps.shouldPerformDataRequest }) - readonly shouldPerformDataRequest!: ShouldRequestFn; + readonly shouldPerformDataRequest!: ShouldFn; /** * When this function returns `true` the component will be able to render additional data. @@ -164,7 +164,7 @@ export default class bScrolly extends iData implements iItems { default: defaultProps.shouldPerformDataRender }) - readonly shouldPerformDataRender!: ShouldRequestFn; + readonly shouldPerformDataRender!: ShouldFn; /** * If true then the elements observer will not be initialized. @@ -176,9 +176,9 @@ export default class bScrolly extends iData implements iItems { readonly disableObserver: boolean = false; - /** {@link typedLocalEmitterFactory} */ - @system((ctx) => typedLocalEmitterFactory(ctx)) - readonly typedLocalEmitter!: ReturnType; + /** {@link typedEmitterFactory} */ + @system((ctx) => typedEmitterFactory(ctx)) + readonly componentEmitter!: ReturnType; /** {@link slotsStateController} */ @system((ctx) => new SlotsStateController(ctx)) @@ -217,31 +217,43 @@ export default class bScrolly extends iData implements iItems { } override initLoad(...args: Parameters): ReturnType { + const + state = this.getComponentState(); + + if (state.isLoadingInProgress) { + return; + } + + this.componentInternalState.setIsLoadingInProgress(true); + const callSuperAndStateReset = () => { - this.reset(); + if (this.isReadyOnce) { + this.reset(); + } + return super.initLoad(...args); }; const isInitialLoading = !this.isReady; - this.typedLocalEmitter.emit(componentDataLocalEvents.dataLoadStart, isInitialLoading); - const initLoadResult = isInitialLoading ? callSuperAndStateReset() : this.initLoadNext(); + this.componentEmitter.emit(componentDataLocalEvents.dataLoadStart, isInitialLoading); + if (Object.isPromise(initLoadResult)) { initLoadResult .then((res) => { + this.componentInternalState.setIsLoadingInProgress(false); this.onInitLoadSuccess(isInitialLoading, isInitialLoading ? this.db : this.convertDataToDB(res)); }) .catch((err) => { + this.componentInternalState.setIsLoadingInProgress(false); this.onInitLoadError(isInitialLoading); + throw err; - }) - .finally(() => { - this.onInitLoadFinish(isInitialLoading); }); } @@ -302,7 +314,15 @@ export default class bScrolly extends iData implements iItems { const state = this.getComponentState(); - return state.isDone || this.shouldStopRequestingData(this.getComponentState(), this); + if (state.isRequestsStopped) { + return state.isRequestsStopped; + } + + const + newVal = this.shouldStopRequestingData(state, this); + + this.componentInternalState.setIsRequestsStopped(newVal); + return newVal; } /** @@ -323,11 +343,11 @@ export default class bScrolly extends iData implements iItems { * Resets a component state and the state of the component modules */ protected reset(): void { - this.typedLocalEmitter.emit(componentLocalEvents.resetState); + this.componentEmitter.emit(componentLocalEvents.resetState); } protected override convertDataToDB(data: unknown): O | this['DB'] { - this.typedLocalEmitter.emit(componentLocalEvents.convertDataToDB, data); + this.componentEmitter.emit(componentLocalEvents.convertDataToDB, data); return super.convertDataToDB(data); } @@ -342,14 +362,15 @@ export default class bScrolly extends iData implements iItems { throw new ReferenceError('Missing data field in the loaded data'); } - this.typedLocalEmitter.emit(componentDataLocalEvents.dataLoadSuccess, data.data, isInitialLoading); + this.componentInternalState.updateData(data.data, isInitialLoading); + this.componentEmitter.emit(componentDataLocalEvents.dataLoadSuccess, data.data, isInitialLoading); if ( isInitialLoading && Object.size(data.data) === 0 ) { if (this.shouldStopRequestingDataWrapper()) { - this.typedLocalEmitter.emit(componentDataLocalEvents.dataEmpty, isInitialLoading); + this.componentEmitter.emit(componentDataLocalEvents.dataEmpty, isInitialLoading); } } } @@ -360,14 +381,6 @@ export default class bScrolly extends iData implements iItems { * @param isInitialLoading - `true` if this load was an initial component loading */ protected onInitLoadError(isInitialLoading: boolean): void { - this.typedLocalEmitter.emit(componentDataLocalEvents.dataLoadError, isInitialLoading); - } - - /** - * Handler: data loading is finished - * @param isInitialLoading - `true` if this load was an initial component loading - */ - protected onInitLoadFinish(isInitialLoading: boolean): void { - this.typedLocalEmitter.emit(componentDataLocalEvents.dataLoadFinish, isInitialLoading); + this.componentEmitter.emit(componentDataLocalEvents.dataLoadError, isInitialLoading); } } diff --git a/src/components/base/b-scrolly/const.ts b/src/components/base/b-scrolly/const.ts index 81aab14ff1..82e45e1c8d 100644 --- a/src/components/base/b-scrolly/const.ts +++ b/src/components/base/b-scrolly/const.ts @@ -71,11 +71,6 @@ export const componentDataLocalEvents = { */ dataLoadStart: 'dataLoadStart', - /** - * Загрузка данных завершена. - */ - dataLoadFinish: 'dataLoadFinish', - /** * Возникла ошибка при загрузки данных. */ @@ -109,7 +104,7 @@ export const componentLocalEvents = { /** * This event will be emitted then all of the component data is rendered and all of the component data was loaded */ - done: 'done' + lifecycleDone: 'lifecycleDone' }; /** @@ -147,8 +142,26 @@ export const componentRenderLocalEvents = { domInsertDone: 'domInsertDone' }; +export const componentEvents = { + ...componentDataLocalEvents, + ...componentRenderLocalEvents, + ...componentLocalEvents +}; + export const canPerformRenderRejectionReason = { + /** + * Not enough data to perform a render (ie data.length is 5 and chunkSize is 12) + */ notEnoughData: 'notEnoughData', + + /** + * No data at all to perform render (ie data.length is 0) + */ + noData: 'noData', + + /** + * Client returns `false` in `shouldPerformDataRender` + */ clientRejection: 'clientRejection' }; @@ -170,17 +183,15 @@ export const componentItemType = { */ export const defaultProps = { /** {@link bScrolly.shouldStopRequestingData} */ - shouldStopRequestingData: (_state: ComponentState, _ctx: bScrolly): boolean => false, + shouldStopRequestingData: (state: ComponentState, _ctx: bScrolly): boolean => { + const isLastRequestNotEmpty = () => state.lastLoadedData.length > 0; + return !isLastRequestNotEmpty(); + }, /** {@link bScrolly.shouldPerformRequest} */ shouldPerformDataRequest: (state: ComponentState, _ctx: bScrolly): boolean => { - const isLastRequestNotEmpty = () => state.lastLoaded.length > 0; - - if (state.isInitialRender) { - return isLastRequestNotEmpty(); - } - - return false; + const isLastRequestNotEmpty = () => state.lastLoadedData.length > 0; + return isLastRequestNotEmpty(); }, /** {@link bScrolly.shouldPerformRender} */ diff --git a/src/components/base/b-scrolly/interface.ts b/src/components/base/b-scrolly/interface.ts index 2c72be3935..9a42a077cc 100644 --- a/src/components/base/b-scrolly/interface.ts +++ b/src/components/base/b-scrolly/interface.ts @@ -20,16 +20,19 @@ export type ComponentStrategyKeys = keyof typeof componentStrategy; * Состояние компонента. */ export interface ComponentState { + maxViewedIndex: CanUndef; + itemsTillEnd: CanUndef; loadPage: number; renderPage: number; - data: object[]; isLastEmpty: boolean; isInitialLoading: boolean; - lastLoaded: object[]; isInitialRender: boolean; - isDone: boolean; - maxViewedIndex: CanUndef; - itemsTillEnd: CanUndef; + isRequestsStopped: boolean; + isRenderingDone: boolean; + isLoadingInProgress: boolean; + isLifecycleDone: boolean; + lastLoadedData: Readonly; + data: Readonly; mountedItems: Readonly; lastLoadedRawData: unknown; } @@ -78,7 +81,7 @@ export type CanPerformRenderRejectionReason = keyof typeof canPerformRenderRejec /** * Функция для опроса клиента о необходимости выполнить то или иное действие. */ -export interface ShouldRequestFn { +export interface ShouldFn { (params: ComponentState, ctx: bScrolly): boolean; } @@ -92,25 +95,24 @@ export type ComponentLocalEvents = * Имя события: аргументы события */ export interface LocalEventPayloadMap { - dataLoadSuccess: [data: object[], isInitialLoading: boolean]; - dataLoadFinish: [isInitialLoading: boolean]; - dataLoadStart: [isInitialLoading: boolean]; - dataLoadError: [isInitialLoading: boolean]; - dataEmpty: [isInitialLoading: boolean]; - - resetState: []; - done: []; - convertDataToDB: [data: unknown]; - - elementEnter: [componentItem: MountedComponentItem]; - elementOut: [componentItem: MountedComponentItem]; - - renderStart: []; - renderDone: []; - renderEngineStart: []; - renderEngineDone: []; - domInsertStart: []; - domInsertDone: []; + [componentDataLocalEvents.dataLoadSuccess]: [data: object[], isInitialLoading: boolean]; + [componentDataLocalEvents.dataLoadStart]: [isInitialLoading: boolean]; + [componentDataLocalEvents.dataLoadError]: [isInitialLoading: boolean]; + [componentDataLocalEvents.dataEmpty]: [isInitialLoading: boolean]; + + [componentLocalEvents.resetState]: []; + [componentLocalEvents.lifecycleDone]: []; + [componentLocalEvents.convertDataToDB]: [data: unknown]; + + [componentObserverLocalEvents.elementEnter]: [componentItem: MountedComponentItem]; + [componentObserverLocalEvents.elementOut]: [componentItem: MountedComponentItem]; + + [componentRenderLocalEvents.renderStart]: []; + [componentRenderLocalEvents.renderDone]: []; + [componentRenderLocalEvents.renderEngineStart]: []; + [componentRenderLocalEvents.renderEngineDone]: []; + [componentRenderLocalEvents.domInsertStart]: []; + [componentRenderLocalEvents.domInsertDone]: []; } export interface ComponentRefs { diff --git a/src/components/base/b-scrolly/modules/local-events/index.ts b/src/components/base/b-scrolly/modules/emitter/index.ts similarity index 96% rename from src/components/base/b-scrolly/modules/local-events/index.ts rename to src/components/base/b-scrolly/modules/emitter/index.ts index ff275e33db..797222eae6 100644 --- a/src/components/base/b-scrolly/modules/local-events/index.ts +++ b/src/components/base/b-scrolly/modules/emitter/index.ts @@ -18,7 +18,7 @@ import type { ComponentLocalEvents, LocalEventPayload } from 'components/base/b- * @param ctx */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function typedLocalEmitterFactory(ctx: bScrolly) { +export function typedEmitterFactory(ctx: bScrolly) { const once = ( event: EVENT, handler: (...args: LocalEventPayload) => void, diff --git a/src/components/base/b-scrolly/modules/factory/index.ts b/src/components/base/b-scrolly/modules/factory/index.ts index 6f4e58f957..05f68b97f9 100644 --- a/src/components/base/b-scrolly/modules/factory/index.ts +++ b/src/components/base/b-scrolly/modules/factory/index.ts @@ -60,7 +60,7 @@ export class ComponentFactory extends Friend { {ctx} = this; let res; - ctx.typedLocalEmitter.emit(componentRenderLocalEvents.renderEngineStart); + ctx.componentEmitter.emit(componentRenderLocalEvents.renderEngineStart); if (ctx.componentRenderStrategy === componentRenderStrategy.forceRenderChunk) { res = forceUpdate.render(ctx, descriptors); @@ -69,7 +69,7 @@ export class ComponentFactory extends Friend { res = vdomRender.render(ctx, descriptors); } - ctx.typedLocalEmitter.emit(componentRenderLocalEvents.renderEngineDone); + ctx.componentEmitter.emit(componentRenderLocalEvents.renderEngineDone); return res; } } diff --git a/src/components/base/b-scrolly/modules/juggler/index.ts b/src/components/base/b-scrolly/modules/juggler/index.ts index 1aa2829762..94127c031e 100644 --- a/src/components/base/b-scrolly/modules/juggler/index.ts +++ b/src/components/base/b-scrolly/modules/juggler/index.ts @@ -53,18 +53,12 @@ export class Juggler extends Friend { super(ctx); const - {typedLocalEmitter} = ctx; - - typedLocalEmitter.on(componentObserverLocalEvents.elementEnter, (component) => this.onElementEnters(component)); - typedLocalEmitter.on(componentObserverLocalEvents.elementOut, (component) => this.onElementOut(component)); - typedLocalEmitter.on(componentLocalEvents.resetState, () => this.reset()); - typedLocalEmitter.on(componentRenderLocalEvents.renderDone, () => this.checkIsDone()); - - typedLocalEmitter.on(componentDataLocalEvents.dataLoadSuccess, () => { - this.onDataLoaded(); - this.checkIsDone(); - }); + {componentEmitter} = ctx; + componentEmitter.on(componentObserverLocalEvents.elementEnter, (component) => this.onElementEnters(component)); + componentEmitter.on(componentObserverLocalEvents.elementOut, (component) => this.onElementOut(component)); + componentEmitter.on(componentLocalEvents.resetState, () => this.reset()); + componentEmitter.on(componentDataLocalEvents.dataLoadSuccess, () => this.onDataLoaded()); } /** @@ -88,6 +82,13 @@ export class Juggler extends Friend { state = this.ctx.getComponentState(), dataSlice = this.getNextDataSlice(); + if (dataSlice.length === 0) { + return { + result: false, + reason: canPerformRenderRejectionReason.noData + }; + } + if (dataSlice.length < chunkSize) { return { result: false, @@ -118,7 +119,7 @@ export class Juggler extends Friend { {ctx, refs} = this, dataSlice = this.getNextDataSlice(); - ctx.typedLocalEmitter.emit(componentRenderLocalEvents.renderStart); + ctx.componentEmitter.emit(componentRenderLocalEvents.renderStart); const items = ctx.componentFactory.produceComponentItems(dataSlice), @@ -128,7 +129,7 @@ export class Juggler extends Friend { ctx.componentInternalState.updateMountedComponents(mountedItems); ctx.observer.observe(mountedItems); - ctx.typedLocalEmitter.emit(componentRenderLocalEvents.domInsertStart); + ctx.componentEmitter.emit(componentRenderLocalEvents.domInsertStart); const fragment = document.createDocumentFragment(); @@ -143,8 +144,8 @@ export class Juggler extends Friend { ctx.async.requestAnimationFrame(() => { refs.container.appendChild(fragment); - ctx.typedLocalEmitter.emit(componentRenderLocalEvents.domInsertDone); - ctx.typedLocalEmitter.emit(componentRenderLocalEvents.renderDone); + ctx.componentEmitter.emit(componentRenderLocalEvents.domInsertDone); + ctx.componentEmitter.emit(componentRenderLocalEvents.renderDone); }, {label: $$.insertDomRaf, group: jugglerAsyncGroup}); } @@ -191,6 +192,16 @@ export class Juggler extends Friend { return this.performRender(); } + if (reason === canPerformRenderRejectionReason.noData) { + if (ctx.shouldStopRequestingDataWrapper()) { + return; + } + + if (ctx.shouldPerformDataRequestWrapper()) { + void ctx.initLoad(); + } + } + if (reason === canPerformRenderRejectionReason.notEnoughData) { if (ctx.shouldStopRequestingDataWrapper()) { this.performRender(); @@ -208,24 +219,6 @@ export class Juggler extends Friend { } } - /** - * Checks if all data are rendered and all requests are made - */ - protected checkIsDone(): void { - const - {ctx} = this, - {isDone} = ctx.getComponentState(), - slice = this.getNextDataSlice(); - - if ( - slice.length === 0 && - ctx.shouldStopRequestingDataWrapper() && - !isDone - ) { - ctx.typedLocalEmitter.emit(componentLocalEvents.done); - } - } - /** * Handler: data was loaded */ diff --git a/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts b/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts index bf669a90fa..e51cd4c579 100644 --- a/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts +++ b/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts @@ -26,7 +26,7 @@ export default class IoObserver extends Friend implements ObserverEngine { constructor(ctx: bScrolly) { super(ctx); - ctx.typedLocalEmitter.on(componentLocalEvents.resetState, () => this.reset()); + ctx.componentEmitter.on(componentLocalEvents.resetState, () => this.reset()); } /** @@ -42,7 +42,7 @@ export default class IoObserver extends Friend implements ObserverEngine { label: component.key, once: true, delay: 0 - }, () => ctx.typedLocalEmitter.emit(componentObserverLocalEvents.elementEnter, component)); + }, () => ctx.componentEmitter.emit(componentObserverLocalEvents.elementEnter, component)); } } diff --git a/src/components/base/b-scrolly/modules/slots/index.ts b/src/components/base/b-scrolly/modules/slots/index.ts index 957162ab3a..e7453877e9 100644 --- a/src/components/base/b-scrolly/modules/slots/index.ts +++ b/src/components/base/b-scrolly/modules/slots/index.ts @@ -45,14 +45,14 @@ export class SlotsStateController extends Friend { super(ctx); const - {typedLocalEmitter} = ctx; - - typedLocalEmitter.on(componentDataLocalEvents.dataLoadError, () => this.loadingFailedState()); - typedLocalEmitter.on(componentDataLocalEvents.dataLoadStart, () => this.loadingProgressState()); - typedLocalEmitter.on(componentDataLocalEvents.dataLoadSuccess, () => this.loadingSuccessState()); - typedLocalEmitter.on(componentDataLocalEvents.dataEmpty, () => this.emptyState()); - typedLocalEmitter.on(componentLocalEvents.done, () => this.doneState()); - typedLocalEmitter.on(componentLocalEvents.resetState, () => this.reset()); + {componentEmitter} = ctx; + + componentEmitter.on(componentDataLocalEvents.dataLoadError, () => this.loadingFailedState()); + componentEmitter.on(componentDataLocalEvents.dataLoadStart, () => this.loadingProgressState()); + componentEmitter.on(componentDataLocalEvents.dataLoadSuccess, () => this.loadingSuccessState()); + componentEmitter.on(componentDataLocalEvents.dataEmpty, () => this.emptyState()); + componentEmitter.on(componentLocalEvents.lifecycleDone, () => this.doneState()); + componentEmitter.on(componentLocalEvents.resetState, () => this.reset()); } /** diff --git a/src/components/base/b-scrolly/modules/state/helpers.ts b/src/components/base/b-scrolly/modules/state/helpers.ts new file mode 100644 index 0000000000..15cb965a84 --- /dev/null +++ b/src/components/base/b-scrolly/modules/state/helpers.ts @@ -0,0 +1,49 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { ComponentState } from 'components/base/b-scrolly/b-scrolly'; + +export function createInitialState(): ComponentState { + return { + loadPage: 0, + + renderPage: 0, + + itemsTillEnd: undefined, + + maxViewedIndex: undefined, + + data: [], + + lastLoadedData: [], + + lastLoadedRawData: undefined, + + isLastEmpty: false, + + isInitialLoading: true, + + /** + * Component items that was rendered + */ + mountedItems: [], + + /** + * `True` if the next rendering process will be initial + */ + isInitialRender: true, + + isRequestsStopped: false, + + isRenderingDone: false, + + isLoadingInProgress: false, + + isLifecycleDone: false + }; +} diff --git a/src/components/base/b-scrolly/modules/state/index.ts b/src/components/base/b-scrolly/modules/state/index.ts index f2f66387ff..86d924b34b 100644 --- a/src/components/base/b-scrolly/modules/state/index.ts +++ b/src/components/base/b-scrolly/modules/state/index.ts @@ -9,6 +9,7 @@ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import { componentDataLocalEvents, componentLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; import type { ComponentState, MountedComponentItem } from 'components/base/b-scrolly/interface'; +import { createInitialState } from 'components/base/b-scrolly/modules/state/helpers'; import Friend from 'components/friends/friend'; export class ComponentInternalState extends Friend { @@ -18,35 +19,7 @@ export class ComponentInternalState extends Friend { */ override readonly C!: bScrolly; - protected currentLoadPage: number = 0; - - protected currentRenderPage: number = 0; - - protected itemsTillEnd: CanUndef = undefined; - - protected maxViewedIndex: CanUndef = undefined; - - protected data: object[] = []; - - protected lastLoadedData: object[] = []; - - protected lastLoadedRawData: CanUndef; - - protected isLastEmpty: boolean = false; - - protected isInitialLoading: boolean = true; - - protected isDone: boolean = false; - - /** - * Component items that was rendered - */ - protected mountedItems: MountedComponentItem[] = []; - - /** - * `True` if the next rendering process will be initial - */ - protected isInitialRender: boolean = true; + protected state: ComponentState = createInitialState(); /** * @param ctx @@ -55,18 +28,24 @@ export class ComponentInternalState extends Friend { super(ctx); const - {typedLocalEmitter} = ctx; + {componentEmitter} = ctx; - typedLocalEmitter.on(componentDataLocalEvents.dataLoadStart, () => this.incrementLoadPage()); - typedLocalEmitter.on(componentDataLocalEvents.dataLoadSuccess, (...args) => this.updateData(...args)); - typedLocalEmitter.on(componentLocalEvents.convertDataToDB, (...args) => this.setRawLastLoaded(...args)); - typedLocalEmitter.on(componentLocalEvents.resetState, (...args) => this.reset(...args)); - typedLocalEmitter.on(componentLocalEvents.done, () => this.setIsDone(true)); + componentEmitter.on(componentDataLocalEvents.dataLoadStart, () => this.incrementLoadPage()); + componentEmitter.on(componentLocalEvents.convertDataToDB, (...args) => this.setRawLastLoaded(...args)); + componentEmitter.on(componentLocalEvents.resetState, (...args) => this.reset(...args)); - typedLocalEmitter.on(componentRenderLocalEvents.renderStart, () => { + componentEmitter.on(componentRenderLocalEvents.renderStart, () => { this.setIsInitialRender(false); this.incrementRenderPage(); }); + + componentEmitter.on(componentRenderLocalEvents.renderDone, () => { + this.updateIsRenderDone(); + }); + + componentEmitter.on(componentDataLocalEvents.dataLoadSuccess, () => { + this.updateIsRenderDone(); + }); } /** @@ -74,18 +53,7 @@ export class ComponentInternalState extends Friend { */ compile(): Readonly { return { - loadPage: this.currentLoadPage, - renderPage: this.currentRenderPage, - data: this.data, - isLastEmpty: this.isLastEmpty, - isInitialLoading: this.isInitialLoading, - isInitialRender: this.isInitialRender, - isDone: this.isDone, - lastLoaded: this.lastLoadedData, - lastLoadedRawData: this.lastLoadedRawData, - maxViewedIndex: this.maxViewedIndex, - mountedItems: this.mountedItems, - itemsTillEnd: this.itemsTillEnd + ...this.state }; } @@ -93,23 +61,14 @@ export class ComponentInternalState extends Friend { * Обнуляет состояние модуля. */ reset(): void { - this.currentLoadPage = 0; - this.currentRenderPage = 0; - this.maxViewedIndex = 0; - this.itemsTillEnd = 0; - this.data = []; - this.mountedItems = []; - this.isLastEmpty = false; - this.isDone = false; - this.isInitialLoading = true; - this.isInitialRender = true; + this.state = createInitialState(); } /** * Обновляет указатель последней загруженной страницы. */ incrementLoadPage(): this { - this.currentLoadPage++; + this.state.loadPage++; return this; } @@ -117,12 +76,12 @@ export class ComponentInternalState extends Friend { * Обновляет указать последней отрисованной страницы. */ incrementRenderPage(): this { - this.currentRenderPage++; + this.state.renderPage++; return this; } storeComponentItems(items: MountedComponentItem[]): this { - this.mountedItems.push(...items); + (this.state.mountedItems).push(...items); return this; } @@ -133,25 +92,63 @@ export class ComponentInternalState extends Friend { * @param isInitialLoading */ updateData(data: object[], isInitialLoading: boolean): this { - this.data = this.data.concat(data); - this.isLastEmpty = data.length === 0; - this.isInitialLoading = isInitialLoading; - this.lastLoadedData = data; + this.state.data = this.state.data.concat(data); + this.state.isLastEmpty = data.length === 0; + this.state.isInitialLoading = isInitialLoading; + this.state.lastLoadedData = data; return this; } updateMountedComponents(mountedItems: MountedComponentItem[]): this { - this.mountedItems.push(...mountedItems); + (this.state.mountedItems).push(...mountedItems); return this; } updateItemsTillEnd(): this { - if (this.maxViewedIndex == null) { + if (this.state.maxViewedIndex == null) { throw new Error('Missing max viewed index'); } - this.itemsTillEnd = this.mountedItems.length - 1 - this.maxViewedIndex; + this.state.itemsTillEnd = this.state.mountedItems.length - 1 - this.state.maxViewedIndex; + return this; + } + + updateIsRenderDone(): this { + const + {ctx} = this, + state = ctx.getComponentState(); + + if ( + !state.isLoadingInProgress && + state.isRequestsStopped && + state.data.length === state.mountedItems.length + ) { + ctx.componentInternalState.setIsRenderingDone(true); + + } else { + ctx.componentInternalState.setIsRenderingDone(false); + } + + return this; + } + + updateIsLifecycleDone(): this { + const + {ctx} = this, + state = ctx.getComponentState(); + + if (state.isLifecycleDone) { + return this; + } + + if ( + state.isRequestsStopped && + state.isRenderingDone + ) { + ctx.componentInternalState.setIsLifecycleDone(true); + } + return this; } @@ -161,12 +158,7 @@ export class ComponentInternalState extends Friend { * @param data */ setRawLastLoaded(data: unknown): this { - this.lastLoadedRawData = data; - return this; - } - - setIsDone(v: boolean): this { - this.isDone = v; + this.state.lastLoadedRawData = data; return this; } @@ -176,12 +168,45 @@ export class ComponentInternalState extends Friend { * @param state */ setIsInitialRender(state: boolean): this { - this.isInitialRender = state; + this.state.isInitialRender = state; + return this; + } + + setIsRequestsStopped(state: boolean): this { + this.state.isRequestsStopped = state; + this.updateIsRenderDone(); + this.updateIsLifecycleDone(); + + return this; + } + + setIsRenderingDone(state: boolean): this { + this.state.isRenderingDone = state; + this.updateIsLifecycleDone(); + + return this; + } + + setIsLifecycleDone(state: boolean): this { + const + {ctx} = this; + + this.state.isLifecycleDone = state; + + if (state) { + ctx.componentEmitter.emit(componentLocalEvents.lifecycleDone); + } + + return this; + } + + setIsLoadingInProgress(state: boolean): this { + this.state.isLoadingInProgress = state; return this; } setMaxViewedIndex(index: number): this { - this.maxViewedIndex = index; + this.state.maxViewedIndex = index; this.updateItemsTillEnd(); return this; diff --git a/src/components/base/b-scrolly/test/api/component-object/index.ts b/src/components/base/b-scrolly/test/api/component-object/index.ts index c8f450f7c6..d46843a1ae 100644 --- a/src/components/base/b-scrolly/test/api/component-object/index.ts +++ b/src/components/base/b-scrolly/test/api/component-object/index.ts @@ -8,7 +8,7 @@ import type { JSHandle, Locator, Page } from 'playwright'; -import { ComponentObject, Scroll, Utils } from 'tests/helpers'; +import { ComponentObject, Scroll } from 'tests/helpers'; import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import type { ComponentRefs, ComponentState } from 'components/base/b-scrolly/b-scrolly'; @@ -59,6 +59,13 @@ export class ScrollyComponentObject extends ComponentObject { return super.build(...args); } + /** + * Calls a reload method of the component + */ + reload(): Promise { + return this.component.evaluate((ctx) => ctx.reload()); + } + /** * Returns an internal component state */ @@ -70,31 +77,37 @@ export class ScrollyComponentObject extends ComponentObject { * Returns a container child count */ async getContainerChildCount(): Promise { - return this.container.evaluate((ctx) => ctx.childNodes.length); + return this.container.locator('*').count(); } /** * Waits for container child count equals to N */ async waitForContainerChildCountEqualsTo(n: number): Promise { - return Utils.waitForFunction((await this.container.elementHandle())!, (ctx, n) => ctx.childNodes.length === n, n); - } - - /** - * Waits for container child count more or equals to N - */ - async waitForContainerChildCountMoreThen(n: number): Promise { - return Utils.waitForFunction((await this.container.elementHandle())!, (ctx, n) => ctx.childNodes.length >= n, n); + await this.container.locator('*').nth(n - 1).waitFor({state: 'attached'}); } /** * Returns a promise that will be resolved after the component emits `domInsertDone` */ async waitForDomInsertDoneEvent(): Promise { - await this.component.evaluate((ctx) => ctx.typedLocalEmitter.promisifyOnce('domInsertDone')); + await this.component.evaluate((ctx) => ctx.componentEmitter.promisifyOnce('domInsertDone')); return this; } + async waitForLifecycleDone(): Promise { + await this.component.evaluate((ctx) => { + const + state = ctx.getComponentState(); + + if (state.isLifecycleDone) { + return; + } + + return ctx.componentEmitter.promisifyOnce('lifecycleDone'); + }); + } + /** * Returns promise that will be resolved then the provided slot will hit `isVisible` state * diff --git a/src/components/base/b-scrolly/test/api/helpers/index.ts b/src/components/base/b-scrolly/test/api/helpers/index.ts index 6413928763..f6e25fae31 100644 --- a/src/components/base/b-scrolly/test/api/helpers/index.ts +++ b/src/components/base/b-scrolly/test/api/helpers/index.ts @@ -13,12 +13,17 @@ import type { ComponentState, MountedComponentItem } from 'components/base/b-scr import { paginationHandler } from 'tests/helpers/providers/pagination'; import { ScrollyComponentObject } from 'components/base/b-scrolly/test/api/component-object'; import { RequestInterceptor } from 'tests/helpers/providers/interceptor'; +import { componentEvents } from 'components/base/b-scrolly/const'; export * from 'components/base/b-scrolly/test/api/component-object'; type DataItemCtor = (i: number) => DATA; type MountedItemCtor = (data: DATA, i: number) => MountedComponentItem; +export function filterEmitterCalls(calls: unknown[][]): unknown[][] { + return calls.filter(([event]) => Object.isString(event) && Boolean(componentEvents[event])); +} + /** * Creates a test helpers for `b-scrolly` component * @param page @@ -45,9 +50,12 @@ export async function createTestHelpers(page: Page) { } export interface DataConveyor { - addData(count: number): this; - addMounted(count: number): this; + addData(count: number): DATA[]; + addMounted(count: number): MountedComponentItem[]; + getDataChunk(index: number): DATA[]; + reset(): void; get data(): DATA[]; + get lastLoadedData(): DATA[]; get mounted(): MountedComponentItem[]; } @@ -55,9 +63,10 @@ export function createDataConveyor( itemsCtor: DataItemCtor, mountedCtor: MountedItemCtor ): DataConveyor { - const + let data = [], - mounted = []; + mounted = [], + dataChunks = []; let dataI = 0, @@ -69,9 +78,10 @@ export function createDataConveyor( newData = createData(count, itemsCtor, dataI); data.push(...newData); + dataChunks.push(newData); dataI = data.length; - return this; + return newData; }, addMounted(count: number) { @@ -82,7 +92,19 @@ export function createDataConveyor( mounted.push(...mountedData); mountedI = mountedData.length; - return this; + return mountedData; + }, + + reset() { + dataI = 0; + mountedI = 0; + mounted = []; + data = []; + dataChunks = []; + }, + + getDataChunk(i: number) { + return dataChunks[i]; }, get mounted() { @@ -91,6 +113,10 @@ export function createDataConveyor( get data() { return data; + }, + + get lastLoadedData() { + return dataChunks[dataChunks.length - 1]; } }; @@ -111,9 +137,9 @@ export function sectionMountedItemCtor(data: DATA, i: number): Mount props: { 'data-index': i }, + key: Object.cast(undefined), item: 'section', type: 'item', - key: Object.cast(undefined), node: test.expect.any(String) }; } @@ -141,16 +167,27 @@ export function createState( initial: Partial, dataConveyor: DataConveyor ) { - const state = fromInitialState(initial); + let + state = fromInitialState(initial); return { - compile() { + setLoadPage(val: number) { + state.loadPage = val; + }, + + compile(override?: Partial): ComponentState { return { ...state, - ...stateFromDataConveyor(dataConveyor) + ...stateFromDataConveyor(dataConveyor), + ...override }; }, + reset() { + state = fromInitialState(initial); + dataConveyor.reset(); + }, + data: dataConveyor }; } @@ -163,16 +200,18 @@ export function fromInitialState(state: Partial): ComponentState itemsTillEnd: test.expect.any(Number), isInitialRender: true, isInitialLoading: true, + isLoadingInProgress: test.expect.any(Boolean), isLastEmpty: false, + isLifecycleDone: false, ...state }; } -export function stateFromDataConveyor(conveyor: DataConveyor): Pick { +export function stateFromDataConveyor(conveyor: DataConveyor): Pick { return { data: conveyor.data, - lastLoaded: conveyor.data, - lastLoadedRawData: {data: conveyor.data}, + lastLoadedData: conveyor.lastLoadedData, + lastLoadedRawData: {data: conveyor.lastLoadedData}, mountedItems: conveyor.mounted }; } diff --git a/src/components/base/b-scrolly/test/unit/functional/emitter.ts b/src/components/base/b-scrolly/test/unit/functional/emitter.ts new file mode 100644 index 0000000000..347e2b873e --- /dev/null +++ b/src/components/base/b-scrolly/test/unit/functional/emitter.ts @@ -0,0 +1,209 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * @file Test cases of the component lifecycle + */ + +import test from 'tests/config/unit/test'; + +import { createTestHelpers, filterEmitterCalls } from 'components/base/b-scrolly/test/api/helpers'; + +test.describe(' emitter', () => { + let + component: Awaited>['component'], + provider:Awaited>['provider'], + state: Awaited>['state']; + + test.beforeEach(async ({demoPage, page}) => { + await demoPage.goto(); + + ({component, provider, state} = await createTestHelpers(page)); + await provider.start(); + }); + + test('All data has been loaded after the initial load', async () => { + const chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: []}); + + await component.setProps({ + chunkSize, + shouldStopRequestingData: () => true, + '@hook:beforeDataCreate': (ctx) => jest.spy(ctx.localEmitter, 'emit') + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + + const + spy = await component.getSpy((ctx) => ctx.unsafe.localEmitter.emit), + calls = filterEmitterCalls(await spy.calls); + + test.expect(calls).toEqual([ + ['dataLoadStart', true], + ['convertDataToDB', {data: state.data.data}], + ['dataLoadSuccess', state.data.data, true], + ['renderStart'], + ['renderEngineStart'], + ['renderEngineDone'], + ['domInsertStart'], + ['domInsertDone'], + ['renderDone'], + ['lifecycleDone'] + ]); + }); + + test('All data has been loaded after the second load', async () => { + const + chunkSize = 12, + providerChunkSize = chunkSize / 2; + + const + firstDataChunk = state.data.addData(providerChunkSize), + secondDataChunk = state.data.addData(providerChunkSize); + + provider + .responseOnce(200, {data: firstDataChunk}) + .responseOnce(200, {data: secondDataChunk}) + .response(200, {data: []}); + + await component.setProps({ + chunkSize, + shouldPerformDataRequest: () => true, + shouldStopRequestingData: ({lastLoadedData}) => lastLoadedData.length === 0, + '@hook:beforeDataCreate': (ctx) => jest.spy(ctx.localEmitter, 'emit') + }); + + await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); + await component.build(); + await component.waitForContainerChildCountEqualsTo(chunkSize); + + const + spy = await component.getSpy((ctx) => ctx.unsafe.localEmitter.emit), + calls = filterEmitterCalls(await spy.calls); + + test.expect(calls).toEqual([ + ['dataLoadStart', true], + ['convertDataToDB', {data: firstDataChunk}], + ['dataLoadSuccess', firstDataChunk, true], + ['dataLoadStart', false], + ['convertDataToDB', {data: secondDataChunk}], + ['dataLoadSuccess', secondDataChunk, false], + ['renderStart'], + ['renderEngineStart'], + ['renderEngineDone'], + ['domInsertStart'], + ['domInsertDone'], + ['renderDone'], + ['dataLoadStart', false], + ['convertDataToDB', {data: []}], + ['dataLoadSuccess', [], false], + ['lifecycleDone'] + ]); + }); + + test('Data loading is completed but data is less than chunkSize', async () => { + const + chunkSize = 12, + providerChunkSize = chunkSize / 2; + + const + firstDataChunk = state.data.addData(providerChunkSize); + + provider + .responseOnce(200, {data: firstDataChunk}) + .response(200, {data: []}); + + await component.setProps({ + chunkSize, + shouldPerformDataRequest: () => true, + shouldStopRequestingData: ({lastLoadedData}) => lastLoadedData.length === 0, + '@hook:beforeDataCreate': (ctx) => jest.spy(ctx.localEmitter, 'emit') + }); + + await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); + await component.build(); + await component.waitForContainerChildCountEqualsTo(providerChunkSize); + await component.waitForLifecycleDone(); + + const + spy = await component.getSpy((ctx) => ctx.unsafe.localEmitter.emit), + calls = filterEmitterCalls(await spy.calls); + + test.expect(calls).toEqual([ + ['dataLoadStart', true], + ['convertDataToDB', {data: firstDataChunk}], + ['dataLoadSuccess', firstDataChunk, true], + ['dataLoadStart', false], + ['convertDataToDB', {data: []}], + ['dataLoadSuccess', [], false], + ['renderStart'], + ['renderEngineStart'], + ['renderEngineDone'], + ['domInsertStart'], + ['domInsertDone'], + ['renderDone'], + ['lifecycleDone'] + ]); + }); + + test('Reload was called after data was rendered', async () => { + const chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: []}); + + await component.setProps({ + chunkSize, + shouldStopRequestingData: () => true, + '@hook:beforeDataCreate': (ctx) => jest.spy(ctx.localEmitter, 'emit') + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForContainerChildCountEqualsTo(chunkSize); + + state.reset(); + provider.responseOnce(200, {data: state.data.addData(chunkSize)}); + + await component.reload(); + await component.waitForContainerChildCountEqualsTo(chunkSize); + + const + spy = await component.getSpy((ctx) => ctx.unsafe.localEmitter.emit), + calls = filterEmitterCalls(await spy.calls); + + test.expect(calls).toEqual([ + ['dataLoadStart', true], + ['convertDataToDB', {data: state.data.data}], + ['dataLoadSuccess', state.data.data, true], + ['renderStart'], + ['renderEngineStart'], + ['renderEngineDone'], + ['domInsertStart'], + ['domInsertDone'], + ['renderDone'], + ['lifecycleDone'], + ['resetState'], + ['dataLoadStart', true], + ['convertDataToDB', {data: state.data.data}], + ['dataLoadSuccess', state.data.data, true], + ['renderStart'], + ['renderEngineStart'], + ['renderEngineDone'], + ['domInsertStart'], + ['domInsertDone'], + ['renderDone'], + ['lifecycleDone'] + ]); + }); +}); diff --git a/src/components/base/b-scrolly/test/unit/functional/state.ts b/src/components/base/b-scrolly/test/unit/functional/state.ts new file mode 100644 index 0000000000..e5ec7222d4 --- /dev/null +++ b/src/components/base/b-scrolly/test/unit/functional/state.ts @@ -0,0 +1,139 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * @file Test cases of the component lifecycle + */ + +import test from 'tests/config/unit/test'; + +import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import { defaultProps } from 'components/base/b-scrolly/const'; +import type { ShouldFn } from 'components/base/b-scrolly/b-scrolly'; + +test.describe(' state', () => { + let + component: Awaited>['component'], + provider:Awaited>['provider'], + state: Awaited>['state']; + + test.beforeEach(async ({demoPage, page}) => { + await demoPage.goto(); + + ({component, provider, state} = await createTestHelpers(page)); + await provider.start(); + }); + + test('Initial state', async () => { + const + chunkSize = 12, + mockFn = await component.mockFn((ctx: bScrolly) => ctx.getComponentState()); + + provider.response(200, {data: []}, {delay: (10).seconds()}); + + const expectedState = state.compile({ + lastLoadedRawData: undefined, + lastLoadedData: [], + itemsTillEnd: undefined, + isRequestsStopped: false, + isRenderingDone: false, + isLoadingInProgress: true, + maxViewedIndex: undefined, + loadPage: 1 + }); + + await component.setProps({ + '@hook:created': mockFn + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + + await test.expect(mockFn.results).resolves.toEqual([{type: 'return', value: expectedState}]); + }); + + test('State after loading first and second data chunks', async () => { + const + chunkSize = 12, + providerChunkSize = chunkSize / 2; + + const + shouldStopRequestingData = await component.mockFn(defaultProps.shouldStopRequestingData), + shouldPerformDataRequest = await component.mockFn(({isInitialLoading, itemsTillEnd, isLastEmpty}) => + isInitialLoading || (itemsTillEnd === 0 && !isLastEmpty)), + shouldPerformDataRender = await component.mockFn(({isInitialRender, itemsTillEnd}) => + isInitialRender || itemsTillEnd === 0); + + await test.step('After rendering first data chunk', async () => { + provider + .responseOnce(200, {data: state.data.addData(providerChunkSize)}) + .responseOnce(200, {data: state.data.addData(providerChunkSize)}); + + state.data.addMounted(chunkSize); + + await component.setProps({ + chunkSize, + shouldStopRequestingData, + shouldPerformDataRequest, + shouldPerformDataRender + }); + + await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); + await component.build(); + await component.waitForContainerChildCountEqualsTo(chunkSize); + + const + currentState = await component.getComponentState(); + + test.expect(currentState).toEqual(state.compile({ + isInitialLoading: false, + isInitialRender: false, + isRenderingDone: false, + isRequestsStopped: false, + isLoadingInProgress: false, + loadPage: 2, + renderPage: 1 + })); + }); + + await test.step('After rendering second data chunk', async () => { + provider + .responseOnce(200, {data: state.data.addData(providerChunkSize)}) + .responseOnce(200, {data: state.data.addData(providerChunkSize)}) + .response(200, {data: state.data.addData(0)}); + + state.data.addMounted(chunkSize); + + await component.scrollToBottom(); + await component.waitForContainerChildCountEqualsTo(chunkSize * 2); + await component.scrollToBottom(); + await component.waitForLifecycleDone(); + + const + currentState = await component.getComponentState(); + + test.expect(currentState).toEqual(state.compile({ + isInitialLoading: false, + isInitialRender: false, + isRenderingDone: true, + isRequestsStopped: true, + isLoadingInProgress: false, + isLastEmpty: true, + isLifecycleDone: true, + loadPage: 5, + renderPage: 2 + })); + }); + }); + + test('Events state', async () => { + // ... + }); + +}); diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/initialization.ts b/src/components/base/b-scrolly/test/unit/lifecycle/initialization.ts index 63fc7dee46..5cd788b89f 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/initialization.ts +++ b/src/components/base/b-scrolly/test/unit/lifecycle/initialization.ts @@ -39,9 +39,8 @@ test.describe('', () => { await component.withDefaultPaginationProviderProps({chunkSize}); await component.build(); - await component.waitForDomInsertDoneEvent(); - await test.expect(component.getContainerChildCount()).resolves.toBe(chunkSize); + await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); }); test('2', async () => { @@ -53,9 +52,9 @@ test.describe('', () => { shouldStopRequestingData = await component.mockFn(() => false), shouldPerformDataRequest = await component.mockFn(defaultProps.shouldPerformDataRequest); - state.data - .addData(providerChunkSize) - .addMounted(chunkSize); + const data = state.data.addData(providerChunkSize); + state.data.addData(providerChunkSize); + state.data.addMounted(chunkSize); await component.setProps({ chunkSize, @@ -66,24 +65,41 @@ test.describe('', () => { await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); await component.build(); - await component.waitForDomInsertDoneEvent(); + await component.waitForContainerChildCountEqualsTo(chunkSize); await test.expect(shouldStopRequestingData.calls).resolves.toEqual([ [ - state.compile(), + state.compile({ + itemsTillEnd: undefined, + maxViewedIndex: undefined, + isRenderingDone: false, + isRequestsStopped: false, + lastLoadedData: data, + lastLoadedRawData: {data}, + data, + loadPage: 1 + }), test.expect.any(Object) ] ]); await test.expect(shouldPerformDataRequest.calls).resolves.toEqual([ [ - state.compile(), + state.compile({ + itemsTillEnd: undefined, + maxViewedIndex: undefined, + isRenderingDone: false, + isRequestsStopped: false, + lastLoadedData: data, + lastLoadedRawData: {data}, + data, + loadPage: 1 + }), test.expect.any(Object) ] ]); await test.expect(initLoadSpy.calls).resolves.toEqual([[], []]); - await test.expect(component.getContainerChildCount()).resolves.toBe(chunkSize); }); test('3', async () => { @@ -95,9 +111,9 @@ test.describe('', () => { shouldStopRequestingData = await component.mockFn(() => false), shouldPerformDataRequest = await component.mockFn(() => false); - state.data - .addData(providerChunkSize) - .addMounted(providerChunkSize); + state.setLoadPage(1); + state.data.addData(providerChunkSize); + state.data.addMounted(providerChunkSize); await component.setProps({ chunkSize, @@ -112,20 +128,29 @@ test.describe('', () => { await test.expect(shouldStopRequestingData.calls).resolves.toEqual([ [ - state.compile(), + state.compile({ + itemsTillEnd: undefined, + maxViewedIndex: undefined, + isRenderingDone: false, + isRequestsStopped: false + }), test.expect.any(Object) ] ]); await test.expect(shouldPerformDataRequest.calls).resolves.toEqual([ [ - state.compile(), + state.compile({ + itemsTillEnd: undefined, + maxViewedIndex: undefined, + isRenderingDone: false, + isRequestsStopped: false + }), test.expect.any(Object) ] ]); await test.expect(initLoadSpy.calls).resolves.toEqual([[]]); - await test.expect(component.getContainerChildCount()).resolves.toBe(providerChunkSize); }); test('4', async () => { @@ -137,9 +162,9 @@ test.describe('', () => { shouldStopRequestingData = await component.mockFn(() => true), shouldPerformDataRequest = await component.mockFn(() => false); - state.data - .addData(providerChunkSize) - .addMounted(providerChunkSize); + state.setLoadPage(1); + state.data.addData(providerChunkSize); + state.data.addMounted(providerChunkSize); await component.setProps({ chunkSize, @@ -150,17 +175,21 @@ test.describe('', () => { await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); await component.build(); - await component.waitForDomInsertDoneEvent(); + await component.waitForContainerChildCountEqualsTo(providerChunkSize); await test.expect(shouldStopRequestingData.calls).resolves.toEqual([ [ - state.compile(), + state.compile({ + itemsTillEnd: undefined, + maxViewedIndex: undefined, + isRenderingDone: false, + isRequestsStopped: false + }), test.expect.any(Object) ] ]); await test.expect(initLoadSpy.calls).resolves.toEqual([[]]); await test.expect(shouldPerformDataRequest.calls).resolves.toEqual([]); - await test.expect(component.getContainerChildCount()).resolves.toBe(providerChunkSize); }); }); diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts b/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts index 8b629d5dd1..28cde93996 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts +++ b/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts @@ -15,7 +15,7 @@ import test from 'tests/config/unit/test'; import { createData, createTestHelpers, indexDataCtor } from 'components/base/b-scrolly/test/api/helpers'; import type { SlotsStateObj } from 'components/base/b-scrolly/modules/slots'; -test.describe(' slots', () => { +test.skip(' slots', () => { let component: Awaited>['component'], provider: Awaited>['provider']; @@ -67,7 +67,7 @@ test.describe(' slots', () => { }); }); - test.only('Activates when all data has been loaded after the second load', async () => { + test('Activates when all data has been loaded after the second load', async () => { const chunkSize = 12; provider @@ -77,10 +77,7 @@ test.describe(' slots', () => { await component.setProps({ chunkSize, - shouldStopRequestingData: ({lastLoadedRawData}) => { - debugger; - return lastLoadedRawData.data.length < 12; - }, + shouldStopRequestingData: ({lastLoadedRawData}) => lastLoadedRawData.data.length < 12, shouldPerformDataRequest: ({lastLoadedRawData}) => lastLoadedRawData.data.length >= 12, shouldPerformDataRender: () => true }); diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ss b/src/components/base/b-virtual-scroll/b-virtual-scroll.ss index 3c2f2d4590..ad0d390d0a 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ss +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ss @@ -42,13 +42,6 @@ . += self.slot('empty') - < .&__done & - ref = done | - v-if = $slots['done'] | - :style = {display: 'none'} - . - += self.slot('done') - < .&__render-next & ref = renderNext | v-if = $slots['renderNext'] | diff --git a/src/components/super/i-block/interface.ts b/src/components/super/i-block/interface.ts index b6e2886764..8fe25a41cb 100644 --- a/src/components/super/i-block/interface.ts +++ b/src/components/super/i-block/interface.ts @@ -92,6 +92,9 @@ export interface UnsafeIBlock extends UnsafeCompone // @ts-ignore (access) localEmitter: CTX['localEmitter']; + // @ts-ignore (access) + selfEmitter: CTX['selfEmitter']; + // @ts-ignore (access) parentEmitter: CTX['parentEmitter']; diff --git a/tests/helpers/component-object/interface.ts b/tests/helpers/component-object/interface.ts index f6cd51e7f8..7bc26178c6 100644 --- a/tests/helpers/component-object/interface.ts +++ b/tests/helpers/component-object/interface.ts @@ -16,6 +16,7 @@ export interface CompileSpyObject { export interface SpyObject extends CompileSpyObject { get calls(): Promise; get callsLength(): Promise; + get results(): Promise; get lastCall(): Promise; } diff --git a/tests/helpers/component-object/mock.ts b/tests/helpers/component-object/mock.ts index a57b67df3d..8343380390 100644 --- a/tests/helpers/component-object/mock.ts +++ b/tests/helpers/component-object/mock.ts @@ -12,6 +12,7 @@ import type { SpyObject, SpyOptions } from 'tests/helpers/component-object/inter import { createAndDisposeMock, spy } from 'tests/helpers/mock'; import { setSerializerAsMockFn } from 'core/prelude/test-env/components/json'; import ComponentObjectInitializer from 'tests/helpers/component-object/initializer'; +import type { SpyCtor } from 'tests/helpers/mock/interface'; export default class ComponentObjectMock extends ComponentObjectInitializer { @@ -77,6 +78,10 @@ export default class ComponentObjectMock extends Compo return instance; } + async getSpy(spyFinder: SpyCtor): Promise { + return spy(this.component, spyFinder); + } + /** * Creates a mock function * @param paths @@ -115,11 +120,11 @@ export default class ComponentObjectMock extends Compo * > Notice that the implementation will be provided into browser, * this imposes some restrictions, such as not being able to use a closure */ - async mockFn(fn?: (...args: any[]) => any): Promise { - fn ??= () => undefined; + async mockFn any = (...args: any[]) => any>(fn?: FN): Promise { + fn ??= Object.cast(() => undefined); const - {agent, id} = await createAndDisposeMock(this.page, fn); + {agent, id} = await createAndDisposeMock(this.page, fn!); return setSerializerAsMockFn(agent, id); } diff --git a/tests/helpers/mock/index.ts b/tests/helpers/mock/index.ts index 5d95df1fb3..b92433aff6 100644 --- a/tests/helpers/mock/index.ts +++ b/tests/helpers/mock/index.ts @@ -9,8 +9,9 @@ import type { ModuleMocker } from 'jest-mock'; import type { JSHandle, Page } from 'playwright'; import type { SpyObject, SyncSpyObject } from 'tests/helpers/component-object/interface'; +import type { ExtractFromJSHandle } from 'tests/helpers/mock/interface'; -function wrapAsSpy(agent: JSHandle | ReturnType>, obj: T): T & SpyObject { +export function wrapAsSpy(agent: JSHandle | ReturnType>, obj: T): T & SpyObject { Object.defineProperties(obj, { calls: { get: () => agent.evaluate((ctx) => ctx.mock.calls) @@ -24,6 +25,10 @@ function wrapAsSpy(agent: JSHandle agent.evaluate((ctx) => ctx.mock.calls[ctx.mock.calls.length - 1]) }, + results: { + get: () => agent.evaluate((ctx) => ctx.mock.results) + }, + compile: { value: async () => { const [ @@ -51,7 +56,7 @@ function wrapAsSpy(agent: JSHandle( ctx: T, - spyCtor: (ctx: T, ...args: ARGS) => ReturnType, + spyCtor: (ctx: ExtractFromJSHandle, ...args: ARGS) => ReturnType, ...argsToCtor: ARGS ): Promise { const diff --git a/tests/helpers/mock/interface.ts b/tests/helpers/mock/interface.ts new file mode 100644 index 0000000000..91f18ce5b5 --- /dev/null +++ b/tests/helpers/mock/interface.ts @@ -0,0 +1,8 @@ +import type { JSHandle } from '@playwright/test'; +import type { ModuleMocker } from 'jest-mock'; + +export interface SpyCtor { + (ctx: CTX, ...args: ARGS): ReturnType; +} + +export type ExtractFromJSHandle = T extends JSHandle ? V : never; diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index 44b5dffcfc..e3a6be66b2 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -6,11 +6,16 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +import delay from 'delay'; import type { BrowserContext, Page, Request, Route } from 'playwright'; import { ModuleMocker } from 'jest-mock'; type ResponseHandler = (route: Route, request: Request) => CanPromise; +interface ResponseOptions { + delay?: number; +} + /** * API that provides simple way to intercept and response to any request */ @@ -123,13 +128,18 @@ export class RequestInterceptor { * * @param status * @param payload + * @param opts */ - response(status: number, payload: object | string | number): this; + response(status: number, payload: object | string | number, opts?: ResponseOptions): this; /** * @inheritdoc */ - response(handlerOrStatus: number | ResponseHandler, payload?: object | string | number): this { + response( + handlerOrStatus: number | ResponseHandler, + payload?: object | string | number, + opts?: ResponseOptions + ): this { let fn; if (Object.isFunction(handlerOrStatus)) { @@ -137,7 +147,7 @@ export class RequestInterceptor { } else { const status = handlerOrStatus; - fn = this.cookResponseFn(status, payload); + fn = this.cookResponseFn(status, payload, opts); } this.mock.mockImplementation(fn); @@ -173,8 +183,19 @@ export class RequestInterceptor { * * @param status * @param payload + * @param opts */ - protected cookResponseFn(status: number, payload?: string | object | number): ResponseHandler { - return (route) => route.fulfill({status, body: JSON.stringify(payload), contentType: 'application/json'}); + protected cookResponseFn( + status: number, + payload?: string | object | number, + opts?: ResponseOptions + ): ResponseHandler { + return async (route) => { + if (opts?.delay != null) { + await delay(opts.delay); + } + + return route.fulfill({status, body: JSON.stringify(payload), contentType: 'application/json'}); + }; } } From b23f46992d23573e025056a8b63958831d9798c3 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 7 Jun 2023 20:45:09 +0300 Subject: [PATCH 007/159] WIP --- .../test/api/component-object/index.ts | 35 ++++++++--------- .../test/unit/functional/rendering.ts | 0 .../b-scrolly/test/unit/lifecycle/slots.ts | 38 +++++++++---------- 3 files changed, 35 insertions(+), 38 deletions(-) create mode 100644 src/components/base/b-scrolly/test/unit/functional/rendering.ts diff --git a/src/components/base/b-scrolly/test/api/component-object/index.ts b/src/components/base/b-scrolly/test/api/component-object/index.ts index d46843a1ae..43020c8952 100644 --- a/src/components/base/b-scrolly/test/api/component-object/index.ts +++ b/src/components/base/b-scrolly/test/api/component-object/index.ts @@ -116,32 +116,29 @@ export class ScrollyComponentObject extends ComponentObject { */ async waitForSlotState(slotName: keyof ComponentRefs, isVisible: boolean): Promise { const - root = await this.node.elementHandle(); + slot = await this.node.locator(this.elSelector(slotName)); - await root?.waitForSelector(this.elSelector(slotName), {state: isVisible ? 'visible' : 'hidden'}); + await slot.waitFor({state: isVisible ? 'visible' : 'hidden'}); } async getSlotsState(): Promise> { const - root = await this.node.elementHandle(); - - const - container = await root?.$(this.elSelector('container')), - loader = await root?.$(this.elSelector('loader')), - tombstones = await root?.$(this.elSelector('tombstones')), - empty = await root?.$(this.elSelector('empty')), - retry = await root?.$(this.elSelector('retry')), - done = await root?.$(this.elSelector('done')), - renderNext = await root?.$(this.elSelector('renderNext')); + container = await this.node.locator(this.elSelector('container')), + loader = await this.node.locator(this.elSelector('loader')), + tombstones = await this.node.locator(this.elSelector('tombstones')), + empty = await this.node.locator(this.elSelector('empty')), + retry = await this.node.locator(this.elSelector('retry')), + done = await this.node.locator(this.elSelector('done')), + renderNext = await this.node.locator(this.elSelector('renderNext')); return { - container: Boolean(await container?.isVisible()), - loader: Boolean(await loader?.isVisible()), - tombstones: Boolean(await tombstones?.isVisible()), - empty: Boolean(await empty?.isVisible()), - retry: Boolean(await retry?.isVisible()), - done: Boolean(await done?.isVisible()), - renderNext: Boolean(await renderNext?.isVisible()) + container: await container.isVisible(), + loader: await loader.isVisible(), + tombstones: await tombstones.isVisible(), + empty: await empty.isVisible(), + retry: await retry.isVisible(), + done: await done.isVisible(), + renderNext: await renderNext.isVisible() }; } diff --git a/src/components/base/b-scrolly/test/unit/functional/rendering.ts b/src/components/base/b-scrolly/test/unit/functional/rendering.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts b/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts index 28cde93996..59f2ba3383 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts +++ b/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts @@ -15,38 +15,40 @@ import test from 'tests/config/unit/test'; import { createData, createTestHelpers, indexDataCtor } from 'components/base/b-scrolly/test/api/helpers'; import type { SlotsStateObj } from 'components/base/b-scrolly/modules/slots'; -test.skip(' slots', () => { +test.describe(' slots', () => { let component: Awaited>['component'], - provider: Awaited>['provider']; + provider: Awaited>['provider'], + state: Awaited>['state']; test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); - ({component, provider} = await createTestHelpers(page)); + ({component, provider, state} = await createTestHelpers(page)); await provider.start(); + }); - await component.setChildren({ - done: { - type: 'div', - attrs: { - id: 'done' + test.describe('`done`', () => { + test.beforeEach(async () => { + await component.setChildren({ + done: { + type: 'div', + attrs: { + id: 'done' + } } - } + }); }); - }); - test.describe('`done`', () => { test('Activates when all data has been loaded after the initial load', async () => { const chunkSize = 12; provider - .responseOnce(200, {data: createData(chunkSize, indexDataCtor)}) + .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); await component.setProps({ - chunkSize, - shouldStopRequestingData: () => true + chunkSize }); await component.withDefaultPaginationProviderProps({chunkSize}); @@ -71,14 +73,12 @@ test.skip(' slots', () => { const chunkSize = 12; provider - .responseOnce(200, {data: createData(chunkSize, indexDataCtor)}) - .responseOnce(200, {data: createData(chunkSize, indexDataCtor, chunkSize)}) + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); await component.setProps({ chunkSize, - shouldStopRequestingData: ({lastLoadedRawData}) => lastLoadedRawData.data.length < 12, - shouldPerformDataRequest: ({lastLoadedRawData}) => lastLoadedRawData.data.length >= 12, shouldPerformDataRender: () => true }); @@ -110,7 +110,7 @@ test.skip(' slots', () => { await component.setProps({ chunkSize, - shouldStopRequestingData: () => true + shouldPerformDataRender: () => true }); await component.withDefaultPaginationProviderProps({chunkSize}); From 44bf468399de8a2273e0b8fc16d63c47fccfadc8 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 9 Jun 2023 20:35:04 +0300 Subject: [PATCH 008/159] WIP --- src/components/base/b-scrolly/README.md | 1 + src/components/base/b-scrolly/b-scrolly.ts | 3 + .../test/api/component-object/index.ts | 33 +-- .../test/api/component-object/styles.ts | 54 ++++ .../b-scrolly/test/unit/lifecycle/slots.ts | 237 ++++++++++++++++-- tests/helpers/providers/interceptor/index.ts | 12 +- 6 files changed, 291 insertions(+), 49 deletions(-) create mode 100644 src/components/base/b-scrolly/test/api/component-object/styles.ts diff --git a/src/components/base/b-scrolly/README.md b/src/components/base/b-scrolly/README.md index c152072b00..6737f636bb 100644 --- a/src/components/base/b-scrolly/README.md +++ b/src/components/base/b-scrolly/README.md @@ -8,6 +8,7 @@ - кейс: загрузили чанк отрисовали -> загружаем следующий (он грузится 20 сек) -> пока грузится полный скролл внизу -> запрещаем загрузку и переходим в isDone состояние -> данные загрузились -> что произойдет??? - componentInternalState.setLoadingPage(val) -> componentInternalState.set('loadingPage', val); - стоит ли для state использовать builder like подход +- негативные тест кейсы ## TODO: diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts index 5d13d1fa30..fed4806cef 100644 --- a/src/components/base/b-scrolly/b-scrolly.ts +++ b/src/components/base/b-scrolly/b-scrolly.ts @@ -82,6 +82,9 @@ export default class bScrolly extends iData implements iItems { @prop({type: [Function, Object], default: () => ({})}) readonly itemProps!: iItems['itemProps']; + @prop(Number) + readonly tombstonesSize?: number; + /** {@link ComponentItemFactory} */ @prop({ type: Function, diff --git a/src/components/base/b-scrolly/test/api/component-object/index.ts b/src/components/base/b-scrolly/test/api/component-object/index.ts index 43020c8952..058a63c313 100644 --- a/src/components/base/b-scrolly/test/api/component-object/index.ts +++ b/src/components/base/b-scrolly/test/api/component-object/index.ts @@ -13,6 +13,7 @@ import { ComponentObject, Scroll } from 'tests/helpers'; import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import type { ComponentRefs, ComponentState } from 'components/base/b-scrolly/b-scrolly'; import type { SlotsStateObj } from 'components/base/b-scrolly/modules/slots'; +import { testStyles } from 'components/base/b-scrolly/test/api/component-object/styles'; export class ScrollyComponentObject extends ComponentObject { @@ -30,32 +31,7 @@ export class ScrollyComponentObject extends ComponentObject { } override async build(...args: Parameters['build']>): Promise> { - await this.page.addStyleTag({content: ` - [data-index] { - width: 200px; - height: 200px; - margin: 16px; - background-color: red; - } - - [data-index]:after { - content: attr(data-index); - } - - #done { - width: 200px; - height: 200px; - display: flex; - justify-content: center; - align-items: center; - background-color: green; - } - - #done:after { - content: "done"; - } - `}); - + await this.page.addStyleTag({content: testStyles}); return super.build(...args); } @@ -113,12 +89,13 @@ export class ScrollyComponentObject extends ComponentObject { * * @param slotName * @param isVisible + * @param timeout */ - async waitForSlotState(slotName: keyof ComponentRefs, isVisible: boolean): Promise { + async waitForSlotState(slotName: keyof ComponentRefs, isVisible: boolean, timeout?: number): Promise { const slot = await this.node.locator(this.elSelector(slotName)); - await slot.waitFor({state: isVisible ? 'visible' : 'hidden'}); + await slot.waitFor({state: isVisible ? 'visible' : 'hidden', timeout}); } async getSlotsState(): Promise> { diff --git a/src/components/base/b-scrolly/test/api/component-object/styles.ts b/src/components/base/b-scrolly/test/api/component-object/styles.ts new file mode 100644 index 0000000000..af9534ee51 --- /dev/null +++ b/src/components/base/b-scrolly/test/api/component-object/styles.ts @@ -0,0 +1,54 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export const testStyles = ` +[data-index] { + width: 200px; + height: 200px; + margin: 16px; + background-color: red; +} + +[data-index]:after { + content: attr(data-index); +} + +.b-scrolly__container { + min-width: 20px; + min-height: 20px; +} + +#done { + width: 200px; + height: 200px; + display: flex; + justify-content: center; + align-items: center; + background-color: green; +} + +#done:after { + content: "done"; +} + +#empty:after { + content: "empty"; +} + +#retry:after { + content: "retry" +} + +#loader, +#tombstone { + display: block; + height: 120px; + width: 200px; + background-color: grey; +} +`; diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts b/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts index 59f2ba3383..4fdd1f9de5 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts +++ b/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts @@ -12,8 +12,9 @@ import test from 'tests/config/unit/test'; -import { createData, createTestHelpers, indexDataCtor } from 'components/base/b-scrolly/test/api/helpers'; +import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; import type { SlotsStateObj } from 'components/base/b-scrolly/modules/slots'; +import type { ShouldFn } from 'components/base/b-scrolly/b-scrolly'; test.describe(' slots', () => { let @@ -26,20 +27,50 @@ test.describe(' slots', () => { ({component, provider, state} = await createTestHelpers(page)); await provider.start(); - }); - test.describe('`done`', () => { - test.beforeEach(async () => { - await component.setChildren({ - done: { - type: 'div', - attrs: { - id: 'done' - } + await component.setChildren({ + done: { + type: 'div', + attrs: { + id: 'done' } - }); + }, + + empty: { + type: 'div', + attrs: { + id: 'empty' + } + }, + + retry: { + type: 'div', + attrs: { + id: 'retry' + } + }, + + tombstone: { + type: 'div', + attrs: { + id: 'tombstone' + } + }, + + loader: { + type: 'div', + attrs: { + id: 'loader' + } + } }); + await component.setProps({ + tombstonesSize: 1 + }); + }); + + test.describe('`done`', () => { test('Activates when all data has been loaded after the initial load', async () => { const chunkSize = 12; @@ -105,7 +136,7 @@ test.describe(' slots', () => { const chunkSize = 12; provider - .responseOnce(200, {data: createData(chunkSize / 2, indexDataCtor)}) + .responseOnce(200, {data: state.data.addData(chunkSize / 2)}) .response(200, {data: []}); await component.setProps({ @@ -130,17 +161,189 @@ test.describe(' slots', () => { tombstones: false }); }); + + test('Does not activates if there is more data to download', async () => { + const chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: []}); + + await component.setProps({ + chunkSize, + shouldPerformDataRender: (({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0) + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForContainerChildCountEqualsTo(chunkSize); + + const + slots = await component.getSlotsState(); + + test.expect(slots).toEqual(>{ + container: true, + done: false, + empty: false, + loader: false, + renderNext: false, + retry: false, + tombstones: false + }); + }); }); - test.describe('empty', async () => { - // ... + test.describe('empty', () => { + test('Activates when no data has been loaded after the initial load', async () => { + const chunkSize = 12; + + provider.response(200, {data: []}); + + await component.setProps({ + chunkSize, + shouldPerformDataRender: () => true + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForSlotState('empty', true); + + const + slots = await component.getSlotsState(); + + test.expect(slots).toEqual(>{ + container: true, + done: true, + empty: true, + loader: false, + renderNext: false, + retry: false, + tombstones: false + }); + }); }); - test.describe('tombstone & loader', async () => { - // ... + test.describe('tombstone & loader', () => { + test('Activates while initial data loading in progress', async () => { + const chunkSize = 12; + + provider + .response(200, {data: state.data.addData(chunkSize)}, {delay: (10).seconds()}); + + await component.setProps({ + chunkSize + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForSlotState('loader', true); + + const + slots = await component.getSlotsState(); + + test.expect(slots).toEqual(>{ + container: true, + done: false, + empty: false, + loader: true, + renderNext: false, + retry: false, + tombstones: true + }); + }); + + test('Active while initial load loads all data', async () => { + const + chunkSize = 12, + providerChunkSize = chunkSize / 2; + + provider + .response(200, {data: state.data.addData(providerChunkSize)}, {delay: (3).seconds()}); + + await component.setProps({ + chunkSize + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + + let error; + + try { + await component.waitForSlotState('loader', false, (5).seconds()); + + } catch (err) { + error = err; + } + + test.expect(error).not.toBe(undefined); + }); + }); + + test.describe('retry', () => { + test('Activates when a data load error occurred during initial loading', async () => { + const chunkSize = 12; + + provider.response(500, {}); + + await component.setProps({ + chunkSize, + shouldPerformDataRender: () => true + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForSlotState('retry', true); + + const + slots = await component.getSlotsState(); + + test.expect(slots).toEqual(>{ + container: true, + done: false, + empty: false, + loader: false, + renderNext: false, + retry: true, + tombstones: false + }); + }); + + test('Activates when a data load error occurred during initial loading of the second data chunk', async () => { + const + chunkSize = 12, + providerChunkSize = chunkSize / 2; + + provider + .responseOnce(200, {data: state.data.addData(providerChunkSize)}) + .response(500, {}); + + await component.setProps({ + chunkSize, + shouldPerformDataRender: () => true + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForSlotState('retry', true); + + const + slots = await component.getSlotsState(); + + test.expect(slots).toEqual(>{ + container: true, + done: false, + empty: false, + loader: false, + renderNext: false, + retry: true, + tombstones: false + }); + }); }); - test.describe('retry', async () => { + test.describe('renderNext', async () => { // ... }); }); diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index e3a6be66b2..89adcf0c03 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -68,7 +68,7 @@ export class RequestInterceptor { * .response((r: Route) => r.fulfill({status: 500})); * ``` */ - responseOnce(handler: ResponseHandler): this; + responseOnce(handler: ResponseHandler, opts?: ResponseOptions): this; /** * Sets a response for one request @@ -82,12 +82,16 @@ export class RequestInterceptor { * .responseOnce(500) * ``` */ - responseOnce(status: number, payload: object | string | number): this; + responseOnce(status: number, payload: object | string | number, opts?: ResponseOptions): this; /** * @inheritdoc */ - responseOnce(handlerOrStatus: number | ResponseHandler, payload?: object | string | number): this { + responseOnce( + handlerOrStatus: number | ResponseHandler, + payload?: object | string | number, + opts?: ResponseOptions + ): this { let fn; if (Object.isFunction(handlerOrStatus)) { @@ -95,7 +99,7 @@ export class RequestInterceptor { } else { const status = handlerOrStatus; - fn = this.cookResponseFn(status, payload); + fn = this.cookResponseFn(status, payload, opts); } this.mock.mockImplementationOnce(fn); From c6d88af6e4629dcd9bde7eeab1fa1f7aa65c23b2 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sun, 11 Jun 2023 13:52:52 +0300 Subject: [PATCH 009/159] WIP --- .../b-scrolly/test/unit/lifecycle/slots.ts | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts b/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts index 4fdd1f9de5..40e0cb5701 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts +++ b/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts @@ -10,6 +10,8 @@ * @file Test cases of the component lifecycle */ +import delay from 'delay'; + import test from 'tests/config/unit/test'; import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; @@ -253,13 +255,13 @@ test.describe(' slots', () => { }); }); - test('Active while initial load loads all data', async () => { + test.only('Active while initial load loads all data', async () => { const chunkSize = 12, providerChunkSize = chunkSize / 2; provider - .response(200, {data: state.data.addData(providerChunkSize)}, {delay: (3).seconds()}); + .response(200, {data: state.data.addData(providerChunkSize)}, {delay: (4).seconds()}); await component.setProps({ chunkSize @@ -268,16 +270,27 @@ test.describe(' slots', () => { await component.withDefaultPaginationProviderProps({chunkSize}); await component.build(); - let error; + let i = 0; - try { - await component.waitForSlotState('loader', false, (5).seconds()); + while (i < 4) { + await component.waitForSlotState('loader', true); - } catch (err) { - error = err; - } + const + slots = await component.getSlotsState(); - test.expect(error).not.toBe(undefined); + test.expect(slots).toEqual(>{ + container: true, + done: false, + empty: false, + loader: true, + renderNext: false, + retry: false, + tombstones: true + }); + + await delay(700); + i++; + } }); }); From 705ae8f4513becd99c85981bd93777594e7cb0d7 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sun, 11 Jun 2023 14:40:37 +0300 Subject: [PATCH 010/159] :art: --- src/components/base/b-scrolly/interface.ts | 2 +- src/components/base/b-scrolly/modules/juggler/index.ts | 2 +- src/components/base/b-scrolly/modules/state/helpers.ts | 2 +- src/components/base/b-scrolly/modules/state/index.ts | 8 ++++---- src/components/base/b-scrolly/test/api/helpers/index.ts | 4 ++-- .../base/b-scrolly/test/unit/lifecycle/slots.ts | 3 ++- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/components/base/b-scrolly/interface.ts b/src/components/base/b-scrolly/interface.ts index 9a42a077cc..bba67a9168 100644 --- a/src/components/base/b-scrolly/interface.ts +++ b/src/components/base/b-scrolly/interface.ts @@ -33,7 +33,7 @@ export interface ComponentState { isLifecycleDone: boolean; lastLoadedData: Readonly; data: Readonly; - mountedItems: Readonly; + items: Readonly; lastLoadedRawData: unknown; } diff --git a/src/components/base/b-scrolly/modules/juggler/index.ts b/src/components/base/b-scrolly/modules/juggler/index.ts index 94127c031e..d9b58e4d99 100644 --- a/src/components/base/b-scrolly/modules/juggler/index.ts +++ b/src/components/base/b-scrolly/modules/juggler/index.ts @@ -170,7 +170,7 @@ export class Juggler extends Friend { protected mountedComponentItems(items: ComponentItem[], nodes: HTMLElement[]): MountedComponentItem[] { const {ctx} = this, - {mountedItems} = ctx.getComponentState(); + {items: mountedItems} = ctx.getComponentState(); return items.map((item, i) => ({ ...item, diff --git a/src/components/base/b-scrolly/modules/state/helpers.ts b/src/components/base/b-scrolly/modules/state/helpers.ts index 15cb965a84..ad61af116b 100644 --- a/src/components/base/b-scrolly/modules/state/helpers.ts +++ b/src/components/base/b-scrolly/modules/state/helpers.ts @@ -31,7 +31,7 @@ export function createInitialState(): ComponentState { /** * Component items that was rendered */ - mountedItems: [], + items: [], /** * `True` if the next rendering process will be initial diff --git a/src/components/base/b-scrolly/modules/state/index.ts b/src/components/base/b-scrolly/modules/state/index.ts index 86d924b34b..3c69912eb2 100644 --- a/src/components/base/b-scrolly/modules/state/index.ts +++ b/src/components/base/b-scrolly/modules/state/index.ts @@ -81,7 +81,7 @@ export class ComponentInternalState extends Friend { } storeComponentItems(items: MountedComponentItem[]): this { - (this.state.mountedItems).push(...items); + (this.state.items).push(...items); return this; } @@ -101,7 +101,7 @@ export class ComponentInternalState extends Friend { } updateMountedComponents(mountedItems: MountedComponentItem[]): this { - (this.state.mountedItems).push(...mountedItems); + (this.state.items).push(...mountedItems); return this; } @@ -110,7 +110,7 @@ export class ComponentInternalState extends Friend { throw new Error('Missing max viewed index'); } - this.state.itemsTillEnd = this.state.mountedItems.length - 1 - this.state.maxViewedIndex; + this.state.itemsTillEnd = this.state.items.length - 1 - this.state.maxViewedIndex; return this; } @@ -122,7 +122,7 @@ export class ComponentInternalState extends Friend { if ( !state.isLoadingInProgress && state.isRequestsStopped && - state.data.length === state.mountedItems.length + state.data.length === state.items.length ) { ctx.componentInternalState.setIsRenderingDone(true); diff --git a/src/components/base/b-scrolly/test/api/helpers/index.ts b/src/components/base/b-scrolly/test/api/helpers/index.ts index f6e25fae31..7f09a1e581 100644 --- a/src/components/base/b-scrolly/test/api/helpers/index.ts +++ b/src/components/base/b-scrolly/test/api/helpers/index.ts @@ -207,11 +207,11 @@ export function fromInitialState(state: Partial): ComponentState }; } -export function stateFromDataConveyor(conveyor: DataConveyor): Pick { +export function stateFromDataConveyor(conveyor: DataConveyor): Pick { return { data: conveyor.data, lastLoadedData: conveyor.lastLoadedData, lastLoadedRawData: {data: conveyor.lastLoadedData}, - mountedItems: conveyor.mounted + items: conveyor.mounted }; } diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts b/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts index 40e0cb5701..b7ed05bb03 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts +++ b/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts @@ -168,6 +168,7 @@ test.describe(' slots', () => { const chunkSize = 12; provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) .responseOnce(200, {data: state.data.addData(chunkSize)}) .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); @@ -255,7 +256,7 @@ test.describe(' slots', () => { }); }); - test.only('Active while initial load loads all data', async () => { + test('Active while initial load loads all data', async () => { const chunkSize = 12, providerChunkSize = chunkSize / 2; From 070bb19da01d8ad5bbecc0ca9b30d01235552e63 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sun, 11 Jun 2023 15:49:28 +0300 Subject: [PATCH 011/159] WIP --- src/components/base/b-scrolly/README.md | 1 + src/components/base/b-scrolly/interface.ts | 19 ++++--- .../base/b-scrolly/modules/helpers/index.ts | 19 +++++++ .../base/b-scrolly/modules/juggler/index.ts | 52 +++++++++++++------ .../observer/engines/intersection-observer.ts | 4 +- .../modules/observer/engines/scroll.ts | 4 +- .../base/b-scrolly/modules/observer/index.ts | 8 +-- .../b-scrolly/modules/observer/interface.ts | 4 +- .../base/b-scrolly/modules/state/helpers.ts | 8 ++- .../base/b-scrolly/modules/state/index.ts | 41 +++++++++++---- .../base/b-scrolly/test/api/helpers/index.ts | 39 +++++++++----- .../b-scrolly/test/unit/functional/state.ts | 6 ++- .../test/unit/lifecycle/initialization.ts | 20 +++++-- 13 files changed, 160 insertions(+), 65 deletions(-) create mode 100644 src/components/base/b-scrolly/modules/helpers/index.ts diff --git a/src/components/base/b-scrolly/README.md b/src/components/base/b-scrolly/README.md index 6737f636bb..8de961a41e 100644 --- a/src/components/base/b-scrolly/README.md +++ b/src/components/base/b-scrolly/README.md @@ -9,6 +9,7 @@ - componentInternalState.setLoadingPage(val) -> componentInternalState.set('loadingPage', val); - стоит ли для state использовать builder like подход - негативные тест кейсы +- улучшить имена интерфейсов (MountedItem, MountedSeparator, ревью имен в ComponentState) ## TODO: diff --git a/src/components/base/b-scrolly/interface.ts b/src/components/base/b-scrolly/interface.ts index bba67a9168..94cdd3cfec 100644 --- a/src/components/base/b-scrolly/interface.ts +++ b/src/components/base/b-scrolly/interface.ts @@ -20,8 +20,10 @@ export type ComponentStrategyKeys = keyof typeof componentStrategy; * Состояние компонента. */ export interface ComponentState { - maxViewedIndex: CanUndef; + maxViewedItem: CanUndef; + maxViewedChild: CanUndef; itemsTillEnd: CanUndef; + childTillEnd: CanUndef; loadPage: number; renderPage: number; isLastEmpty: boolean; @@ -33,7 +35,8 @@ export interface ComponentState { isLifecycleDone: boolean; lastLoadedData: Readonly; data: Readonly; - items: Readonly; + items: Readonly; + childList: Readonly; lastLoadedRawData: unknown; } @@ -64,9 +67,13 @@ export interface ComponentItem { children?: ComponentItem[]; } -export interface MountedComponentItem extends ComponentItem { +export interface AnyMounted extends ComponentItem { node: HTMLElement; - index: number; + childIndex: number; +} + +export interface MountedItem extends AnyMounted { + itemIndex: number; } export type ComponentItemType = keyof typeof componentItemType; @@ -104,8 +111,8 @@ export interface LocalEventPayloadMap { [componentLocalEvents.lifecycleDone]: []; [componentLocalEvents.convertDataToDB]: [data: unknown]; - [componentObserverLocalEvents.elementEnter]: [componentItem: MountedComponentItem]; - [componentObserverLocalEvents.elementOut]: [componentItem: MountedComponentItem]; + [componentObserverLocalEvents.elementEnter]: [componentItem: AnyMounted]; + [componentObserverLocalEvents.elementOut]: [componentItem: AnyMounted]; [componentRenderLocalEvents.renderStart]: []; [componentRenderLocalEvents.renderDone]: []; diff --git a/src/components/base/b-scrolly/modules/helpers/index.ts b/src/components/base/b-scrolly/modules/helpers/index.ts new file mode 100644 index 0000000000..2cc4075fb3 --- /dev/null +++ b/src/components/base/b-scrolly/modules/helpers/index.ts @@ -0,0 +1,19 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { componentItemType } from 'components/base/b-scrolly/const'; +import type { MountedItem } from 'components/base/b-scrolly/interface'; + +/** + * Возвращает `true` если переданное значение является типом `MountedItem` + * + * @param val + */ +export function isItem(val: any): val is MountedItem { + return Object.isPlainObject(val) && val.type === componentItemType.item; +} diff --git a/src/components/base/b-scrolly/modules/juggler/index.ts b/src/components/base/b-scrolly/modules/juggler/index.ts index d9b58e4d99..3e3ad895f4 100644 --- a/src/components/base/b-scrolly/modules/juggler/index.ts +++ b/src/components/base/b-scrolly/modules/juggler/index.ts @@ -12,8 +12,9 @@ import Friend from 'components/friends/friend'; import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import type { CanPerformRenderRejectionReason, ComponentItem } from 'components/base/b-scrolly/b-scrolly'; -import { canPerformRenderRejectionReason, componentDataLocalEvents, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; -import type { MountedComponentItem } from 'components/base/b-scrolly/interface'; +import { canPerformRenderRejectionReason, componentDataLocalEvents, componentItemType, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; +import type { AnyMounted, MountedItem } from 'components/base/b-scrolly/interface'; +import { isItem } from 'components/base/b-scrolly/modules/helpers'; export const $$ = symbolGenerator(), @@ -124,10 +125,12 @@ export class Juggler extends Friend { const items = ctx.componentFactory.produceComponentItems(dataSlice), nodes = ctx.componentFactory.produceNodes(items), - mountedItems = this.mountedComponentItems(items, nodes); + anyMounted = this.produceMounted(items, nodes), + mountedItems = anyMounted.filter((mounted) => mounted.type === componentItemType.item); - ctx.componentInternalState.updateMountedComponents(mountedItems); - ctx.observer.observe(mountedItems); + ctx.componentInternalState.updateMountedItems(mountedItems); + ctx.componentInternalState.updateChildList(anyMounted); + ctx.observer.observe(anyMounted); ctx.componentEmitter.emit(componentRenderLocalEvents.domInsertStart); @@ -167,16 +170,27 @@ export class Juggler extends Friend { * @param items * @param nodes */ - protected mountedComponentItems(items: ComponentItem[], nodes: HTMLElement[]): MountedComponentItem[] { + protected produceMounted(items: ComponentItem[], nodes: HTMLElement[]): Array { const {ctx} = this, - {items: mountedItems} = ctx.getComponentState(); + {items: mountedItems, childList} = ctx.getComponentState(); + + return items.map((item, i) => { + if (item.type === componentItemType.item) { + return { + ...item, + node: nodes[i], + itemIndex: mountedItems.length + i, + childIndex: childList.length + i + }; + } - return items.map((item, i) => ({ - ...item, - node: nodes[i], - index: mountedItems.length + i - })); + return { + ...item, + node: nodes[i], + childIndex: mountedItems.length + i + }; + }); } /** @@ -229,14 +243,18 @@ export class Juggler extends Friend { /** * Handler: element enters the viewport */ - protected onElementEnters(component: MountedComponentItem): void { + protected onElementEnters(component: AnyMounted): void { const {ctx} = this, state = ctx.getComponentState(), - {index} = component; + {childIndex} = component; + + if (isItem(component) && (state.maxViewedItem == null || state.maxViewedItem < component.itemIndex)) { + ctx.componentInternalState.setMaxViewedItemIndex(component.itemIndex); + } - if (state.maxViewedIndex == null || state.maxViewedIndex < index) { - ctx.componentInternalState.setMaxViewedIndex(index); + if (state.maxViewedChild == null || state.maxViewedChild < childIndex) { + ctx.componentInternalState.setMaxViewedChildIndex(childIndex); } this.loadDataOrPerformRender(); @@ -245,7 +263,7 @@ export class Juggler extends Friend { /** * Handler: element leaves the viewport */ - protected onElementOut(_component: MountedComponentItem): void { + protected onElementOut(_component: AnyMounted): void { // ... } } diff --git a/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts b/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts index e51cd4c579..fbb5f20bf6 100644 --- a/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts +++ b/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts @@ -8,7 +8,7 @@ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import { componentLocalEvents, componentObserverLocalEvents } from 'components/base/b-scrolly/const'; -import type { MountedComponentItem } from 'components/base/b-scrolly/interface'; +import type { AnyMounted } from 'components/base/b-scrolly/interface'; import { observerAsyncGroup } from 'components/base/b-scrolly/modules/observer/const'; import type { ObserverEngine } from 'components/base/b-scrolly/modules/observer/interface'; import Friend from 'components/friends/friend'; @@ -32,7 +32,7 @@ export default class IoObserver extends Friend implements ObserverEngine { /** * @inheritdoc */ - watchForIntersection(components: MountedComponentItem[]): void { + watchForIntersection(components: AnyMounted[]): void { const {ctx} = this; diff --git a/src/components/base/b-scrolly/modules/observer/engines/scroll.ts b/src/components/base/b-scrolly/modules/observer/engines/scroll.ts index 5efce8a712..8a37477748 100644 --- a/src/components/base/b-scrolly/modules/observer/engines/scroll.ts +++ b/src/components/base/b-scrolly/modules/observer/engines/scroll.ts @@ -7,7 +7,7 @@ */ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { MountedComponentItem } from 'components/base/b-scrolly/interface'; +import type { AnyMounted } from 'components/base/b-scrolly/interface'; import { observerAsyncGroup } from 'components/base/b-scrolly/modules/observer/const'; import type { ObserverEngine } from 'components/base/b-scrolly/modules/observer/interface'; import Friend from 'components/friends/friend'; @@ -22,7 +22,7 @@ export default class ScrollObserver extends Friend implements ObserverEngine { /** * @inheritdoc */ - watchForIntersection(_components: MountedComponentItem[]): void { + watchForIntersection(_components: AnyMounted[]): void { // ... } diff --git a/src/components/base/b-scrolly/modules/observer/index.ts b/src/components/base/b-scrolly/modules/observer/index.ts index a82f9be736..53d8073913 100644 --- a/src/components/base/b-scrolly/modules/observer/index.ts +++ b/src/components/base/b-scrolly/modules/observer/index.ts @@ -7,7 +7,7 @@ */ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { MountedComponentItem } from 'components/base/b-scrolly/interface'; +import type { AnyMounted } from 'components/base/b-scrolly/interface'; import ScrollObserver from 'components/base/b-scrolly/modules/observer/engines/scroll'; import IoObserver from 'components/base/b-scrolly/modules/observer/engines/intersection-observer'; import Friend from 'components/friends/friend'; @@ -38,9 +38,9 @@ export class Observer extends Friend { } /** - * @param mountedItems + * @param mounted */ - observe(mountedItems: MountedComponentItem[]): void { + observe(mounted: AnyMounted[]): void { const {ctx} = this; @@ -48,6 +48,6 @@ export class Observer extends Friend { return; } - this.engine.watchForIntersection(mountedItems); + this.engine.watchForIntersection(mounted); } } diff --git a/src/components/base/b-scrolly/modules/observer/interface.ts b/src/components/base/b-scrolly/modules/observer/interface.ts index d531d27356..435753a241 100644 --- a/src/components/base/b-scrolly/modules/observer/interface.ts +++ b/src/components/base/b-scrolly/modules/observer/interface.ts @@ -6,14 +6,14 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { MountedComponentItem } from 'components/base/b-scrolly/interface'; +import type { MountedItem } from 'components/base/b-scrolly/interface'; export interface ObserverEngine { /** * Initializes a watcher to watch component enters the viewport * @param components */ - watchForIntersection(components: MountedComponentItem[]): void; + watchForIntersection(components: MountedItem[]): void; /** * Resets the module state diff --git a/src/components/base/b-scrolly/modules/state/helpers.ts b/src/components/base/b-scrolly/modules/state/helpers.ts index ad61af116b..e92d01ad04 100644 --- a/src/components/base/b-scrolly/modules/state/helpers.ts +++ b/src/components/base/b-scrolly/modules/state/helpers.ts @@ -16,7 +16,11 @@ export function createInitialState(): ComponentState { itemsTillEnd: undefined, - maxViewedIndex: undefined, + childTillEnd: undefined, + + maxViewedItem: undefined, + + maxViewedChild: undefined, data: [], @@ -33,6 +37,8 @@ export function createInitialState(): ComponentState { */ items: [], + childList: [], + /** * `True` if the next rendering process will be initial */ diff --git a/src/components/base/b-scrolly/modules/state/index.ts b/src/components/base/b-scrolly/modules/state/index.ts index 3c69912eb2..a31c87a8d5 100644 --- a/src/components/base/b-scrolly/modules/state/index.ts +++ b/src/components/base/b-scrolly/modules/state/index.ts @@ -8,7 +8,7 @@ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import { componentDataLocalEvents, componentLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; -import type { ComponentState, MountedComponentItem } from 'components/base/b-scrolly/interface'; +import type { AnyMounted, ComponentState, MountedItem } from 'components/base/b-scrolly/interface'; import { createInitialState } from 'components/base/b-scrolly/modules/state/helpers'; import Friend from 'components/friends/friend'; @@ -80,8 +80,8 @@ export class ComponentInternalState extends Friend { return this; } - storeComponentItems(items: MountedComponentItem[]): this { - (this.state.items).push(...items); + storeComponentItems(items: MountedItem[]): this { + (this.state.items).push(...items); return this; } @@ -100,17 +100,31 @@ export class ComponentInternalState extends Friend { return this; } - updateMountedComponents(mountedItems: MountedComponentItem[]): this { - (this.state.items).push(...mountedItems); + updateMountedItems(mounted: MountedItem[]): this { + (this.state.items).push(...mounted); + return this; + } + + updateChildList(mounted: AnyMounted[]): this { + (this.state.childList).push(...mounted); return this; } updateItemsTillEnd(): this { - if (this.state.maxViewedIndex == null) { - throw new Error('Missing max viewed index'); + if (this.state.maxViewedItem == null) { + throw new Error('Missing max viewed item index'); + } + + this.state.itemsTillEnd = this.state.items.length - 1 - this.state.maxViewedItem; + return this; + } + + updateChildTillEnd(): this { + if (this.state.maxViewedChild == null) { + throw new Error('Missing max viewed child index'); } - this.state.itemsTillEnd = this.state.items.length - 1 - this.state.maxViewedIndex; + this.state.childTillEnd = this.state.childList.length - 1 - this.state.maxViewedChild; return this; } @@ -205,10 +219,17 @@ export class ComponentInternalState extends Friend { return this; } - setMaxViewedIndex(index: number): this { - this.state.maxViewedIndex = index; + setMaxViewedItemIndex(itemIndex: number): this { + this.state.maxViewedItem = itemIndex; this.updateItemsTillEnd(); return this; } + + setMaxViewedChildIndex(childIndex: number): this { + this.state.maxViewedChild = childIndex; + this.updateChildTillEnd(); + + return this; + } } diff --git a/src/components/base/b-scrolly/test/api/helpers/index.ts b/src/components/base/b-scrolly/test/api/helpers/index.ts index 7f09a1e581..8aedff9eb1 100644 --- a/src/components/base/b-scrolly/test/api/helpers/index.ts +++ b/src/components/base/b-scrolly/test/api/helpers/index.ts @@ -9,7 +9,7 @@ import type { Page } from 'playwright'; import test from 'tests/config/unit/test'; -import type { ComponentState, MountedComponentItem } from 'components/base/b-scrolly/interface'; +import type { ComponentState, MountedItem } from 'components/base/b-scrolly/interface'; import { paginationHandler } from 'tests/helpers/providers/pagination'; import { ScrollyComponentObject } from 'components/base/b-scrolly/test/api/component-object'; import { RequestInterceptor } from 'tests/helpers/providers/interceptor'; @@ -18,7 +18,7 @@ import { componentEvents } from 'components/base/b-scrolly/const'; export * from 'components/base/b-scrolly/test/api/component-object'; type DataItemCtor = (i: number) => DATA; -type MountedItemCtor = (data: DATA, i: number) => MountedComponentItem; +type MountedItemCtor = (data: DATA, i: number) => MountedItem; export function filterEmitterCalls(calls: unknown[][]): unknown[][] { return calls.filter(([event]) => Object.isString(event) && Boolean(componentEvents[event])); @@ -51,12 +51,12 @@ export async function createTestHelpers(page: Page) { export interface DataConveyor { addData(count: number): DATA[]; - addMounted(count: number): MountedComponentItem[]; + addMounted(count: number): MountedItem[]; getDataChunk(index: number): DATA[]; reset(): void; get data(): DATA[]; get lastLoadedData(): DATA[]; - get mounted(): MountedComponentItem[]; + get mounted(): MountedItem[]; } export function createDataConveyor( @@ -65,7 +65,7 @@ export function createDataConveyor( ): DataConveyor { let data = [], - mounted = [], + mounted = [], dataChunks = []; let @@ -127,13 +127,14 @@ export function createMountedDataFrom( data: DATA[], ctor: MountedItemCtor, start: number = 0 -): MountedComponentItem[] { +): MountedItem[] { return data.map((item, i) => ctor(item, start + i)); } -export function sectionMountedItemCtor(data: DATA, i: number): MountedComponentItem { +export function sectionMountedItemCtor(data: DATA, i: number): MountedItem { return { - index: i, + itemIndex: i, + childIndex: i, props: { 'data-index': i }, @@ -193,25 +194,35 @@ export function createState( } export function fromInitialState(state: Partial): ComponentState { - return { + return { renderPage: 0, loadPage: 0, - maxViewedIndex: test.expect.any(Number), - itemsTillEnd: test.expect.any(Number), + maxViewedItem: Object.cast(test.expect.any(Number)), + maxViewedChild: Object.cast(test.expect.any(Number)), + itemsTillEnd: Object.cast(test.expect.any(Number)), + childTillEnd: Object.cast(test.expect.any(Number)), isInitialRender: true, isInitialLoading: true, - isLoadingInProgress: test.expect.any(Boolean), + isLoadingInProgress: Object.cast(test.expect.any(Boolean)), isLastEmpty: false, isLifecycleDone: false, + isRequestsStopped: false, + isRenderingDone: false, + lastLoadedData: [], + data: [], + items: [], + childList: [], + lastLoadedRawData: undefined, ...state }; } -export function stateFromDataConveyor(conveyor: DataConveyor): Pick { +export function stateFromDataConveyor(conveyor: DataConveyor): Pick { return { data: conveyor.data, lastLoadedData: conveyor.lastLoadedData, lastLoadedRawData: {data: conveyor.lastLoadedData}, - items: conveyor.mounted + items: conveyor.mounted, + childList: conveyor.mounted }; } diff --git a/src/components/base/b-scrolly/test/unit/functional/state.ts b/src/components/base/b-scrolly/test/unit/functional/state.ts index e5ec7222d4..cf12e64de8 100644 --- a/src/components/base/b-scrolly/test/unit/functional/state.ts +++ b/src/components/base/b-scrolly/test/unit/functional/state.ts @@ -39,12 +39,14 @@ test.describe(' state', () => { const expectedState = state.compile({ lastLoadedRawData: undefined, - lastLoadedData: [], itemsTillEnd: undefined, + childTillEnd: undefined, + maxViewedItem: undefined, + maxViewedChild: undefined, isRequestsStopped: false, isRenderingDone: false, isLoadingInProgress: true, - maxViewedIndex: undefined, + lastLoadedData: [], loadPage: 1 }); diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/initialization.ts b/src/components/base/b-scrolly/test/unit/lifecycle/initialization.ts index 5cd788b89f..94321b18e0 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/initialization.ts +++ b/src/components/base/b-scrolly/test/unit/lifecycle/initialization.ts @@ -71,7 +71,9 @@ test.describe('', () => { [ state.compile({ itemsTillEnd: undefined, - maxViewedIndex: undefined, + childTillEnd: undefined, + maxViewedItem: undefined, + maxViewedChild: undefined, isRenderingDone: false, isRequestsStopped: false, lastLoadedData: data, @@ -87,7 +89,9 @@ test.describe('', () => { [ state.compile({ itemsTillEnd: undefined, - maxViewedIndex: undefined, + childTillEnd: undefined, + maxViewedItem: undefined, + maxViewedChild: undefined, isRenderingDone: false, isRequestsStopped: false, lastLoadedData: data, @@ -130,7 +134,9 @@ test.describe('', () => { [ state.compile({ itemsTillEnd: undefined, - maxViewedIndex: undefined, + childTillEnd: undefined, + maxViewedItem: undefined, + maxViewedChild: undefined, isRenderingDone: false, isRequestsStopped: false }), @@ -142,7 +148,9 @@ test.describe('', () => { [ state.compile({ itemsTillEnd: undefined, - maxViewedIndex: undefined, + childTillEnd: undefined, + maxViewedItem: undefined, + maxViewedChild: undefined, isRenderingDone: false, isRequestsStopped: false }), @@ -181,7 +189,9 @@ test.describe('', () => { [ state.compile({ itemsTillEnd: undefined, - maxViewedIndex: undefined, + childTillEnd: undefined, + maxViewedItem: undefined, + maxViewedChild: undefined, isRenderingDone: false, isRequestsStopped: false }), From f529c2bd0e2c007f2e402f2e5cdddd7f642087f5 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sun, 11 Jun 2023 16:10:03 +0300 Subject: [PATCH 012/159] WIP --- src/components/base/b-scrolly/test/unit/functional/state.ts | 2 +- src/components/base/b-scrolly/test/unit/lifecycle/slots.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/base/b-scrolly/test/unit/functional/state.ts b/src/components/base/b-scrolly/test/unit/functional/state.ts index cf12e64de8..c940f709b8 100644 --- a/src/components/base/b-scrolly/test/unit/functional/state.ts +++ b/src/components/base/b-scrolly/test/unit/functional/state.ts @@ -134,7 +134,7 @@ test.describe(' state', () => { }); }); - test('Events state', async () => { + test.skip('Events state', async () => { // ... }); diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts b/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts index b7ed05bb03..86d7847a20 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts +++ b/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts @@ -357,7 +357,7 @@ test.describe(' slots', () => { }); }); - test.describe('renderNext', async () => { + test.skip('renderNext', async () => { // ... }); }); From 2ba63f9ccaaf8d65a2e56acfa515986ef291059c Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sun, 11 Jun 2023 20:07:56 +0300 Subject: [PATCH 013/159] :art: --- src/components/base/b-scrolly/interface.ts | 6 +- .../base/b-scrolly/modules/factory/index.ts | 2 +- .../{emitter.ts => emitter/index.ts} | 0 .../{rendering.ts => observer/index.ts} | 0 .../functional/rendering/component-factory.ts | 123 ++++++++++++++++++ .../test/unit/functional/rendering/default.ts | 0 .../functional/{state.ts => state/index.ts} | 4 + .../{ => initialization}/initialization.ts | 0 .../test/unit/lifecycle/{ => slots}/slots.ts | 2 + 9 files changed, 133 insertions(+), 4 deletions(-) rename src/components/base/b-scrolly/test/unit/functional/{emitter.ts => emitter/index.ts} (100%) rename src/components/base/b-scrolly/test/unit/functional/{rendering.ts => observer/index.ts} (100%) create mode 100644 src/components/base/b-scrolly/test/unit/functional/rendering/component-factory.ts create mode 100644 src/components/base/b-scrolly/test/unit/functional/rendering/default.ts rename src/components/base/b-scrolly/test/unit/functional/{state.ts => state/index.ts} (98%) rename src/components/base/b-scrolly/test/unit/lifecycle/{ => initialization}/initialization.ts (100%) rename src/components/base/b-scrolly/test/unit/lifecycle/{ => slots}/slots.ts (98%) diff --git a/src/components/base/b-scrolly/interface.ts b/src/components/base/b-scrolly/interface.ts index 94cdd3cfec..b8053182df 100644 --- a/src/components/base/b-scrolly/interface.ts +++ b/src/components/base/b-scrolly/interface.ts @@ -55,8 +55,8 @@ export interface RequestQueryFn { (params: ComponentState): Dictionary; } -export interface ComponentItemFactory { - (ctx: bScrolly, items: unknown[]): ComponentItem[]; +export interface ComponentItemFactory { + (ctx: bScrolly, items: DATA[]): ComponentItem[]; } export interface ComponentItem { @@ -64,7 +64,7 @@ export interface ComponentItem { item: string; props?: Dictionary; key: string; - children?: ComponentItem[]; + children?: VNodeChildren; } export interface AnyMounted extends ComponentItem { diff --git a/src/components/base/b-scrolly/modules/factory/index.ts b/src/components/base/b-scrolly/modules/factory/index.ts index 05f68b97f9..a36f65095d 100644 --- a/src/components/base/b-scrolly/modules/factory/index.ts +++ b/src/components/base/b-scrolly/modules/factory/index.ts @@ -43,7 +43,7 @@ export class ComponentFactory extends Friend { const createDescriptor = (item: ComponentItem): VNodeDescriptor => ({ type: item.item, attrs: item.props, - children: Object.size(item.children) > 0 ? item.children?.map(createDescriptor) : [] + children: item.children }); const diff --git a/src/components/base/b-scrolly/test/unit/functional/emitter.ts b/src/components/base/b-scrolly/test/unit/functional/emitter/index.ts similarity index 100% rename from src/components/base/b-scrolly/test/unit/functional/emitter.ts rename to src/components/base/b-scrolly/test/unit/functional/emitter/index.ts diff --git a/src/components/base/b-scrolly/test/unit/functional/rendering.ts b/src/components/base/b-scrolly/test/unit/functional/observer/index.ts similarity index 100% rename from src/components/base/b-scrolly/test/unit/functional/rendering.ts rename to src/components/base/b-scrolly/test/unit/functional/observer/index.ts diff --git a/src/components/base/b-scrolly/test/unit/functional/rendering/component-factory.ts b/src/components/base/b-scrolly/test/unit/functional/rendering/component-factory.ts new file mode 100644 index 0000000000..74e3f747de --- /dev/null +++ b/src/components/base/b-scrolly/test/unit/functional/rendering/component-factory.ts @@ -0,0 +1,123 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * @file Test cases of the component lifecycle + */ + +import test from 'tests/config/unit/test'; + +import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; +import type { ComponentItemFactory } from 'components/base/b-scrolly/b-scrolly'; +import type { ComponentItem } from 'components/base/b-scrolly/interface'; + +test.describe(' rendering via component factory', () => { + let + component: Awaited>['component'], + provider:Awaited>['provider'], + state: Awaited>['state']; + + test.beforeEach(async ({demoPage, page}) => { + await demoPage.goto(); + + ({component, provider, state} = await createTestHelpers(page)); + await provider.start(); + }); + + test('Returned items with type `item` is equal to the provided data', async () => { + const + chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: state.data.addData(0)}); + + const itemsFactory = await component.mockFn>((ctx, data) => + data.map((value) => ({ + item: 'section', + key: '', + type: 'item', + children: [], + props: { + 'data-index': value.i + } + }))); + + await component.setProps({ + itemsFactory, + shouldPerformDataRender: () => true, + chunkSize + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + + await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); + + }); + + test('In additional `item`, `separator` was also returned', async () => { + const + chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: state.data.addData(0)}); + + const itemsFactory = await component.mockFn>((ctx, data) => { + const result = data.map((value) => ({ + item: 'section', + key: '', + type: 'item', + children: [], + props: { + 'data-index': value.i + } + })); + + result.push({ + item: 'b-button', + key: '', + children: { + default: { + type: 'div', + attrs: { + id: 'button' + } + } + }, + type: 'separator' + }); + + return result; + }); + + await component.setProps({ + itemsFactory, + shouldPerformDataRender: () => true, + chunkSize + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + + await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize + 1)).resolves.toBeUndefined(); + }); + + test('Returned items with type `item` is less than the provided data', async () => { + // Ошибка + }); + + test('Returned item with type `item` is more than the provided data', async () => { + // Выкидывает ошибку + }); + + test('`item` was not returned, but equal to the number of data, the number of `separator` was returned', async () => { + // Выкидывает ошибку + }); +}); diff --git a/src/components/base/b-scrolly/test/unit/functional/rendering/default.ts b/src/components/base/b-scrolly/test/unit/functional/rendering/default.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/base/b-scrolly/test/unit/functional/state.ts b/src/components/base/b-scrolly/test/unit/functional/state/index.ts similarity index 98% rename from src/components/base/b-scrolly/test/unit/functional/state.ts rename to src/components/base/b-scrolly/test/unit/functional/state/index.ts index c940f709b8..0ac1000e6b 100644 --- a/src/components/base/b-scrolly/test/unit/functional/state.ts +++ b/src/components/base/b-scrolly/test/unit/functional/state/index.ts @@ -134,6 +134,10 @@ test.describe(' state', () => { }); }); + test.skip('State after rendering via `itemsFactory`', async () => { + // ... + }); + test.skip('Events state', async () => { // ... }); diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/initialization.ts b/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts similarity index 100% rename from src/components/base/b-scrolly/test/unit/lifecycle/initialization.ts rename to src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts b/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts similarity index 98% rename from src/components/base/b-scrolly/test/unit/lifecycle/slots.ts rename to src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts index 86d7847a20..05b2644a50 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/slots.ts +++ b/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts @@ -175,6 +175,8 @@ test.describe(' slots', () => { await component.setProps({ chunkSize, + // eslint-disable-next-line max-len + shouldPerformDataRequest: (({isInitialLoading, itemsTillEnd}) => isInitialLoading || itemsTillEnd === 0), shouldPerformDataRender: (({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0) }); From 03bb5c1b7d9bbce679a7d9a8abbbfe7cc2eb0378 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 12 Jun 2023 17:23:51 +0300 Subject: [PATCH 014/159] Rework rendering convoyer --- src/components/base/b-scrolly/README.md | 3 - src/components/base/b-scrolly/b-scrolly.ts | 37 +++++-- src/components/base/b-scrolly/const.ts | 7 +- src/components/base/b-scrolly/interface.ts | 14 ++- .../base/b-scrolly/modules/factory/index.ts | 4 +- .../base/b-scrolly/modules/juggler/index.ts | 82 +++------------- .../b-scrolly/modules/presets/chunk-size.ts | 71 ++++++++++++++ .../base/b-scrolly/modules/state/helpers.ts | 2 - .../base/b-scrolly/modules/state/index.ts | 96 ++++++++++--------- .../base/b-scrolly/test/api/helpers/index.ts | 1 - .../test/unit/functional/emitter/index.ts | 2 +- .../unit/functional/presets/chunk-size.ts | 35 +++++++ .../functional/rendering/component-factory.ts | 10 +- .../test/unit/functional/state/index.ts | 3 - .../initialization/initialization.ts | 38 +++++--- .../test/unit/lifecycle/slots/slots.ts | 1 + 16 files changed, 247 insertions(+), 159 deletions(-) create mode 100644 src/components/base/b-scrolly/modules/presets/chunk-size.ts create mode 100644 src/components/base/b-scrolly/test/unit/functional/presets/chunk-size.ts diff --git a/src/components/base/b-scrolly/README.md b/src/components/base/b-scrolly/README.md index 8de961a41e..7c5faf3c48 100644 --- a/src/components/base/b-scrolly/README.md +++ b/src/components/base/b-scrolly/README.md @@ -87,8 +87,6 @@ mountResult.$forceUpdate(); } - debugger; - this.nextTick(() => { Array.from(document.querySelectorAll('.b-dummy-user')).forEach((el) => { if (setNodes.has(el)) { @@ -103,7 +101,6 @@ enumerable: false, writable: true, value: () => { - debugger; return false; } }); diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts index fed4806cef..932d5bf44e 100644 --- a/src/components/base/b-scrolly/b-scrolly.ts +++ b/src/components/base/b-scrolly/b-scrolly.ts @@ -28,7 +28,8 @@ import type { ComponentRefs, ComponentItemFactory, ComponentItemType, - ComponentStrategyKeys + ComponentStrategyKeys, + CanPerformRenderResult } from 'components/base/b-scrolly/interface'; @@ -51,6 +52,7 @@ import { ComponentInternalState } from 'components/base/b-scrolly/modules/state' import { typedEmitterFactory } from 'components/base/b-scrolly/modules/emitter'; import iData, { component, prop, system, $$ } from 'components/super/i-data/i-data'; +import { chunkSizePreset } from 'components/base/b-scrolly/modules/presets/chunk-size'; export * from 'components/base/b-scrolly/interface'; export * from 'components/base/b-scrolly/const'; @@ -88,8 +90,12 @@ export default class bScrolly extends iData implements iItems { /** {@link ComponentItemFactory} */ @prop({ type: Function, - default: (ctx: bScrolly, items: object[]) => { - const descriptors = items.map((data, i) => ({ + default: (state: ComponentState, ctx: bScrolly) => { + if (ctx.chunkSize == null) { + throw new Error('chunkSize.getNextDataSlice is used but chunkSize prop is not settled'); + } + + const descriptors = chunkSizePreset.getNextDataSlice(state, ctx.chunkSize).map((data, i) => ({ key: ctx.itemKey?.(data, i), item: Object.isFunction(ctx.item) ? ctx.item(data, i) : ctx.item, @@ -134,7 +140,7 @@ export default class bScrolly extends iData implements iItems { */ // eslint-disable-next-line @typescript-eslint/unbound-method @prop({type: Number, validator: Number.isNatural}) - readonly chunkSize: number = 10; + readonly chunkSize?: number = 10; /** * When this function returns `true` the component will stop to request new data. @@ -159,15 +165,24 @@ export default class bScrolly extends iData implements iItems { readonly shouldPerformDataRequest!: ShouldFn; /** - * When this function returns `true` the component will be able to render additional data. - * This function will be called on each new element enters the viewport. + * TODO: docs */ @prop({ type: Function, - default: defaultProps.shouldPerformDataRender + default: (state: ComponentState, ctx: bScrolly) => { + if (ctx.chunkSize == null) { + throw new Error('ChunkSize.renderGuard preset is active but chunkSize prop is not settled'); + } + + return chunkSizePreset.renderGuard(state, ctx, ctx.chunkSize); + } }) - readonly shouldPerformDataRender!: ShouldFn; + readonly renderGuard!: ShouldFn; + + // TODO: подумать над названием + @prop(Function) + readonly shouldPerformDataRender?: ShouldFn; /** * If true then the elements observer will not be initialized. @@ -331,8 +346,8 @@ export default class bScrolly extends iData implements iItems { /** * Wrapper for `shouldPerformDataRender` */ - shouldPerformDataRenderWrapper(): boolean { - return this.shouldPerformDataRender(this.getComponentState(), this); + shouldPerformDataRenderWrapper(): ReturnType { + return this.renderGuard(this.getComponentState(), this); } /** @@ -366,6 +381,8 @@ export default class bScrolly extends iData implements iItems { } this.componentInternalState.updateData(data.data, isInitialLoading); + this.shouldStopRequestingDataWrapper(); + this.componentEmitter.emit(componentDataLocalEvents.dataLoadSuccess, data.data, isInitialLoading); if ( diff --git a/src/components/base/b-scrolly/const.ts b/src/components/base/b-scrolly/const.ts index 82e45e1c8d..f38f627eb9 100644 --- a/src/components/base/b-scrolly/const.ts +++ b/src/components/base/b-scrolly/const.ts @@ -159,10 +159,15 @@ export const canPerformRenderRejectionReason = { */ noData: 'noData', + /** + * All rendering are done + */ + done: 'done', + /** * Client returns `false` in `shouldPerformDataRender` */ - clientRejection: 'clientRejection' + noPermission: 'noPermission' }; /** diff --git a/src/components/base/b-scrolly/interface.ts b/src/components/base/b-scrolly/interface.ts index b8053182df..891e79d86d 100644 --- a/src/components/base/b-scrolly/interface.ts +++ b/src/components/base/b-scrolly/interface.ts @@ -30,7 +30,6 @@ export interface ComponentState { isInitialLoading: boolean; isInitialRender: boolean; isRequestsStopped: boolean; - isRenderingDone: boolean; isLoadingInProgress: boolean; isLifecycleDone: boolean; lastLoadedData: Readonly; @@ -55,8 +54,8 @@ export interface RequestQueryFn { (params: ComponentState): Dictionary; } -export interface ComponentItemFactory { - (ctx: bScrolly, items: DATA[]): ComponentItem[]; +export interface ComponentItemFactory { + (state: ComponentState, ctx: bScrolly): ComponentItem[]; } export interface ComponentItem { @@ -88,8 +87,8 @@ export type CanPerformRenderRejectionReason = keyof typeof canPerformRenderRejec /** * Функция для опроса клиента о необходимости выполнить то или иное действие. */ -export interface ShouldFn { - (params: ComponentState, ctx: bScrolly): boolean; +export interface ShouldFn { + (params: ComponentState, ctx: bScrolly): RES; } export type ComponentLocalEvents = @@ -98,6 +97,11 @@ export type ComponentLocalEvents = keyof typeof componentRenderLocalEvents | keyof typeof componentObserverLocalEvents; +export interface CanPerformRenderResult { + result: boolean; + reason?: CanPerformRenderRejectionReason; +} + /** * Имя события: аргументы события */ diff --git a/src/components/base/b-scrolly/modules/factory/index.ts b/src/components/base/b-scrolly/modules/factory/index.ts index a36f65095d..249d5c7ed6 100644 --- a/src/components/base/b-scrolly/modules/factory/index.ts +++ b/src/components/base/b-scrolly/modules/factory/index.ts @@ -29,11 +29,11 @@ export class ComponentFactory extends Friend { /** * @param data */ - produceComponentItems(data: object[]): ComponentItem[] { + produceComponentItems(): ComponentItem[] { const {ctx} = this; - return ctx.itemsFactory(ctx, data); + return ctx.itemsFactory(ctx.getComponentState(), ctx); } /** diff --git a/src/components/base/b-scrolly/modules/juggler/index.ts b/src/components/base/b-scrolly/modules/juggler/index.ts index 3e3ad895f4..6250ce68f6 100644 --- a/src/components/base/b-scrolly/modules/juggler/index.ts +++ b/src/components/base/b-scrolly/modules/juggler/index.ts @@ -11,9 +11,9 @@ import symbolGenerator from 'core/symbol'; import Friend from 'components/friends/friend'; import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { CanPerformRenderRejectionReason, ComponentItem } from 'components/base/b-scrolly/b-scrolly'; +import type { ComponentItem } from 'components/base/b-scrolly/b-scrolly'; import { canPerformRenderRejectionReason, componentDataLocalEvents, componentItemType, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; -import type { AnyMounted, MountedItem } from 'components/base/b-scrolly/interface'; +import type { AnyMounted, CanPerformRenderResult, MountedItem } from 'components/base/b-scrolly/interface'; import { isItem } from 'components/base/b-scrolly/modules/helpers'; export const @@ -31,22 +31,6 @@ export class Juggler extends Friend { */ override readonly C!: bScrolly; - protected get nextDataSliceStartIndex(): number { - const - {ctx, ctx: {chunkSize}} = this, - {renderPage} = ctx.getComponentState(); - - return renderPage * chunkSize; - } - - protected get nextDataSliceEndIndex(): number { - const - {ctx, ctx: {chunkSize}} = this, - {renderPage} = ctx.getComponentState(); - - return (renderPage + 1) * chunkSize; - } - /** * @param ctx */ @@ -76,40 +60,11 @@ export class Juggler extends Friend { * Returns status of the possibility to render a components. * Also returns reason of the rejection if the is no possibility to render components */ - protected canPerformRender(): {result: boolean; reason?: CanPerformRenderRejectionReason} { + protected canPerformRender(): CanPerformRenderResult { const - {ctx} = this, - {chunkSize} = ctx, - state = this.ctx.getComponentState(), - dataSlice = this.getNextDataSlice(); - - if (dataSlice.length === 0) { - return { - result: false, - reason: canPerformRenderRejectionReason.noData - }; - } - - if (dataSlice.length < chunkSize) { - return { - result: false, - reason: canPerformRenderRejectionReason.notEnoughData - }; - } - - if (state.isInitialRender) { - return { - result: true - }; - } - - const - clientResponse = ctx.shouldPerformDataRenderWrapper(); + {ctx} = this; - return { - result: clientResponse, - reason: clientResponse === false ? canPerformRenderRejectionReason.clientRejection : undefined - }; + return ctx.shouldPerformDataRenderWrapper(); } /** @@ -117,13 +72,12 @@ export class Juggler extends Friend { */ protected performRender(): void { const - {ctx, refs} = this, - dataSlice = this.getNextDataSlice(); + {ctx, refs} = this; ctx.componentEmitter.emit(componentRenderLocalEvents.renderStart); const - items = ctx.componentFactory.produceComponentItems(dataSlice), + items = ctx.componentFactory.produceComponentItems(), nodes = ctx.componentFactory.produceNodes(items), anyMounted = this.produceMounted(items, nodes), mountedItems = anyMounted.filter((mounted) => mounted.type === componentItemType.item); @@ -153,17 +107,6 @@ export class Juggler extends Friend { }, {label: $$.insertDomRaf, group: jugglerAsyncGroup}); } - /** - * Returns a data slice that should be rendered next - */ - protected getNextDataSlice(): object[] { - const - {ctx} = this, - {data} = ctx.getComponentState(); - - return data.slice(this.nextDataSliceStartIndex, this.nextDataSliceEndIndex); - } - /** * Stores the component items * @@ -206,8 +149,13 @@ export class Juggler extends Friend { return this.performRender(); } + if (reason === canPerformRenderRejectionReason.done) { + ctx.componentInternalState.setIsLifecycleDone(true); + return; + } + if (reason === canPerformRenderRejectionReason.noData) { - if (ctx.shouldStopRequestingDataWrapper()) { + if (state.isRequestsStopped) { return; } @@ -217,7 +165,7 @@ export class Juggler extends Friend { } if (reason === canPerformRenderRejectionReason.notEnoughData) { - if (ctx.shouldStopRequestingDataWrapper()) { + if (state.isRequestsStopped) { this.performRender(); } else if (ctx.shouldPerformDataRequestWrapper()) { @@ -228,7 +176,7 @@ export class Juggler extends Friend { } } - if (reason === canPerformRenderRejectionReason.clientRejection) { + if (reason === canPerformRenderRejectionReason.noPermission) { // ... } } diff --git a/src/components/base/b-scrolly/modules/presets/chunk-size.ts b/src/components/base/b-scrolly/modules/presets/chunk-size.ts new file mode 100644 index 0000000000..b092928eb1 --- /dev/null +++ b/src/components/base/b-scrolly/modules/presets/chunk-size.ts @@ -0,0 +1,71 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import { canPerformRenderRejectionReason, CanPerformRenderResult, ComponentState } from 'components/base/b-scrolly/b-scrolly'; + +/** + * Returns a data slice that should be rendered next + */ +export function getNextDataSlice(state: ComponentState, chunkSize: number): object[] { + const + {data, renderPage} = state, + nextDataSliceStartIndex = renderPage * chunkSize, + nextDataSliceEndIndex = (renderPage + 1) * chunkSize; + + return data.slice(nextDataSliceStartIndex, nextDataSliceEndIndex); +} + +export const chunkSizePreset = { + renderGuard( + state: ComponentState, + ctx: bScrolly, + chunkSize: number + ): CanPerformRenderResult { + const + dataSlice = getNextDataSlice(state, chunkSize); + + if (dataSlice.length === 0) { + if (state.isRequestsStopped) { + return { + result: false, + reason: canPerformRenderRejectionReason.done + }; + } + + return { + result: false, + reason: canPerformRenderRejectionReason.noData + }; + } + + if (dataSlice.length < chunkSize) { + return { + result: false, + reason: canPerformRenderRejectionReason.notEnoughData + }; + } + + if (state.isInitialRender) { + return { + result: true + }; + } + + const + clientResponse = ctx.shouldPerformDataRender?.(state, ctx); + + return { + result: clientResponse == null ? true : clientResponse, + reason: clientResponse === false ? canPerformRenderRejectionReason.noPermission : undefined + }; + }, + + getNextDataSlice +}; + diff --git a/src/components/base/b-scrolly/modules/state/helpers.ts b/src/components/base/b-scrolly/modules/state/helpers.ts index e92d01ad04..7cf2c8fcc3 100644 --- a/src/components/base/b-scrolly/modules/state/helpers.ts +++ b/src/components/base/b-scrolly/modules/state/helpers.ts @@ -46,8 +46,6 @@ export function createInitialState(): ComponentState { isRequestsStopped: false, - isRenderingDone: false, - isLoadingInProgress: false, isLifecycleDone: false diff --git a/src/components/base/b-scrolly/modules/state/index.ts b/src/components/base/b-scrolly/modules/state/index.ts index a31c87a8d5..efb166b197 100644 --- a/src/components/base/b-scrolly/modules/state/index.ts +++ b/src/components/base/b-scrolly/modules/state/index.ts @@ -34,18 +34,18 @@ export class ComponentInternalState extends Friend { componentEmitter.on(componentLocalEvents.convertDataToDB, (...args) => this.setRawLastLoaded(...args)); componentEmitter.on(componentLocalEvents.resetState, (...args) => this.reset(...args)); - componentEmitter.on(componentRenderLocalEvents.renderStart, () => { + componentEmitter.on(componentRenderLocalEvents.domInsertStart, () => { this.setIsInitialRender(false); this.incrementRenderPage(); }); - componentEmitter.on(componentRenderLocalEvents.renderDone, () => { - this.updateIsRenderDone(); - }); + // componentEmitter.on(componentRenderLocalEvents.renderDone, () => { + // this.updateIsRenderDone(); + // }); - componentEmitter.on(componentDataLocalEvents.dataLoadSuccess, () => { - this.updateIsRenderDone(); - }); + // componentEmitter.on(componentDataLocalEvents.dataLoadSuccess, () => { + // this.updateIsRenderDone(); + // }); } /** @@ -128,43 +128,43 @@ export class ComponentInternalState extends Friend { return this; } - updateIsRenderDone(): this { - const - {ctx} = this, - state = ctx.getComponentState(); - - if ( - !state.isLoadingInProgress && - state.isRequestsStopped && - state.data.length === state.items.length - ) { - ctx.componentInternalState.setIsRenderingDone(true); - - } else { - ctx.componentInternalState.setIsRenderingDone(false); - } + // updateIsRenderDone(): this { + // const + // {ctx} = this, + // state = ctx.getComponentState(); - return this; - } + // if ( + // !state.isLoadingInProgress && + // state.isRequestsStopped && + // state.data.length === state.items.length + // ) { + // ctx.componentInternalState.setIsRenderingDone(true); - updateIsLifecycleDone(): this { - const - {ctx} = this, - state = ctx.getComponentState(); + // } else { + // ctx.componentInternalState.setIsRenderingDone(false); + // } - if (state.isLifecycleDone) { - return this; - } + // return this; + // } - if ( - state.isRequestsStopped && - state.isRenderingDone - ) { - ctx.componentInternalState.setIsLifecycleDone(true); - } + // updateIsLifecycleDone(): this { + // const + // {ctx} = this, + // state = ctx.getComponentState(); - return this; - } + // if (state.isLifecycleDone) { + // return this; + // } + + // if ( + // state.isRequestsStopped && + // state.isRenderingDone + // ) { + // ctx.componentInternalState.setIsLifecycleDone(true); + // } + + // return this; + // } /** * Обновляет состояние последних сырых загруженных данных. @@ -188,20 +188,24 @@ export class ComponentInternalState extends Friend { setIsRequestsStopped(state: boolean): this { this.state.isRequestsStopped = state; - this.updateIsRenderDone(); - this.updateIsLifecycleDone(); + // this.updateIsRenderDone(); + // this.updateIsLifecycleDone(); return this; } - setIsRenderingDone(state: boolean): this { - this.state.isRenderingDone = state; - this.updateIsLifecycleDone(); + // setIsRenderingDone(state: boolean): this { + // this.state.isRenderingDone = state; + // // this.updateIsLifecycleDone(); - return this; - } + // return this; + // } setIsLifecycleDone(state: boolean): this { + if (this.state.isLifecycleDone === state) { + return this; + } + const {ctx} = this; diff --git a/src/components/base/b-scrolly/test/api/helpers/index.ts b/src/components/base/b-scrolly/test/api/helpers/index.ts index 8aedff9eb1..cae5ec9d67 100644 --- a/src/components/base/b-scrolly/test/api/helpers/index.ts +++ b/src/components/base/b-scrolly/test/api/helpers/index.ts @@ -207,7 +207,6 @@ export function fromInitialState(state: Partial): ComponentState isLastEmpty: false, isLifecycleDone: false, isRequestsStopped: false, - isRenderingDone: false, lastLoadedData: [], data: [], items: [], diff --git a/src/components/base/b-scrolly/test/unit/functional/emitter/index.ts b/src/components/base/b-scrolly/test/unit/functional/emitter/index.ts index 347e2b873e..4112339d7b 100644 --- a/src/components/base/b-scrolly/test/unit/functional/emitter/index.ts +++ b/src/components/base/b-scrolly/test/unit/functional/emitter/index.ts @@ -61,7 +61,7 @@ test.describe(' emitter', () => { ]); }); - test('All data has been loaded after the second load', async () => { + test('All data has been loaded after the second load', async ({page}) => { const chunkSize = 12, providerChunkSize = chunkSize / 2; diff --git a/src/components/base/b-scrolly/test/unit/functional/presets/chunk-size.ts b/src/components/base/b-scrolly/test/unit/functional/presets/chunk-size.ts new file mode 100644 index 0000000000..900c397b8a --- /dev/null +++ b/src/components/base/b-scrolly/test/unit/functional/presets/chunk-size.ts @@ -0,0 +1,35 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * @file Test cases of the component lifecycle + */ + +import test from 'tests/config/unit/test'; + +import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; +import type { ComponentItemFactory } from 'components/base/b-scrolly/b-scrolly'; +import type { ComponentItem } from 'components/base/b-scrolly/interface'; + +test.describe(' with chunkSize preset', () => { + let + component: Awaited>['component'], + provider:Awaited>['provider'], + state: Awaited>['state']; + + test.beforeEach(async ({demoPage, page}) => { + await demoPage.goto(); + + ({component, provider, state} = await createTestHelpers(page)); + await provider.start(); + }); + + test.skip('Should render components', async () => { + // ... + }); +}); diff --git a/src/components/base/b-scrolly/test/unit/functional/rendering/component-factory.ts b/src/components/base/b-scrolly/test/unit/functional/rendering/component-factory.ts index 74e3f747de..b6e3d8da0e 100644 --- a/src/components/base/b-scrolly/test/unit/functional/rendering/component-factory.ts +++ b/src/components/base/b-scrolly/test/unit/functional/rendering/component-factory.ts @@ -29,7 +29,7 @@ test.describe(' rendering via component factory', () => { await provider.start(); }); - test('Returned items with type `item` is equal to the provided data', async () => { + test.skip('Returned items with type `item` is equal to the provided data', async () => { const chunkSize = 12; @@ -61,7 +61,7 @@ test.describe(' rendering via component factory', () => { }); - test('In additional `item`, `separator` was also returned', async () => { + test.skip('In additional `item`, `separator` was also returned', async () => { const chunkSize = 12; @@ -109,15 +109,15 @@ test.describe(' rendering via component factory', () => { await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize + 1)).resolves.toBeUndefined(); }); - test('Returned items with type `item` is less than the provided data', async () => { + test.skip('Returned items with type `item` is less than the provided data', async () => { // Ошибка }); - test('Returned item with type `item` is more than the provided data', async () => { + test.skip('Returned item with type `item` is more than the provided data', async () => { // Выкидывает ошибку }); - test('`item` was not returned, but equal to the number of data, the number of `separator` was returned', async () => { + test.skip('`item` was not returned, but equal to the number of data, the number of `separator` was returned', async () => { // Выкидывает ошибку }); }); diff --git a/src/components/base/b-scrolly/test/unit/functional/state/index.ts b/src/components/base/b-scrolly/test/unit/functional/state/index.ts index 0ac1000e6b..5d92c01176 100644 --- a/src/components/base/b-scrolly/test/unit/functional/state/index.ts +++ b/src/components/base/b-scrolly/test/unit/functional/state/index.ts @@ -44,7 +44,6 @@ test.describe(' state', () => { maxViewedItem: undefined, maxViewedChild: undefined, isRequestsStopped: false, - isRenderingDone: false, isLoadingInProgress: true, lastLoadedData: [], loadPage: 1 @@ -96,7 +95,6 @@ test.describe(' state', () => { test.expect(currentState).toEqual(state.compile({ isInitialLoading: false, isInitialRender: false, - isRenderingDone: false, isRequestsStopped: false, isLoadingInProgress: false, loadPage: 2, @@ -123,7 +121,6 @@ test.describe(' state', () => { test.expect(currentState).toEqual(state.compile({ isInitialLoading: false, isInitialRender: false, - isRenderingDone: true, isRequestsStopped: true, isLoadingInProgress: false, isLastEmpty: true, diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts b/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts index 94321b18e0..89894fe2dd 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts +++ b/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts @@ -52,8 +52,10 @@ test.describe('', () => { shouldStopRequestingData = await component.mockFn(() => false), shouldPerformDataRequest = await component.mockFn(defaultProps.shouldPerformDataRequest); - const data = state.data.addData(providerChunkSize); - state.data.addData(providerChunkSize); + const + firstDataChunk = state.data.addData(providerChunkSize), + secondDataChunk = state.data.addData(providerChunkSize); + state.data.addMounted(chunkSize); await component.setProps({ @@ -74,14 +76,28 @@ test.describe('', () => { childTillEnd: undefined, maxViewedItem: undefined, maxViewedChild: undefined, - isRenderingDone: false, isRequestsStopped: false, - lastLoadedData: data, - lastLoadedRawData: {data}, - data, + lastLoadedData: firstDataChunk, + lastLoadedRawData: {data: firstDataChunk}, + data: firstDataChunk, loadPage: 1 }), test.expect.any(Object) + ], + [ + state.compile({ + itemsTillEnd: undefined, + childTillEnd: undefined, + maxViewedItem: undefined, + maxViewedChild: undefined, + isRequestsStopped: false, + isInitialLoading: false, + lastLoadedData: secondDataChunk, + lastLoadedRawData: {data: secondDataChunk}, + data: state.data.data, + loadPage: 2 + }), + test.expect.any(Object) ] ]); @@ -92,11 +108,10 @@ test.describe('', () => { childTillEnd: undefined, maxViewedItem: undefined, maxViewedChild: undefined, - isRenderingDone: false, isRequestsStopped: false, - lastLoadedData: data, - lastLoadedRawData: {data}, - data, + lastLoadedData: firstDataChunk, + lastLoadedRawData: {data: firstDataChunk}, + data: firstDataChunk, loadPage: 1 }), test.expect.any(Object) @@ -137,7 +152,6 @@ test.describe('', () => { childTillEnd: undefined, maxViewedItem: undefined, maxViewedChild: undefined, - isRenderingDone: false, isRequestsStopped: false }), test.expect.any(Object) @@ -151,7 +165,6 @@ test.describe('', () => { childTillEnd: undefined, maxViewedItem: undefined, maxViewedChild: undefined, - isRenderingDone: false, isRequestsStopped: false }), test.expect.any(Object) @@ -192,7 +205,6 @@ test.describe('', () => { childTillEnd: undefined, maxViewedItem: undefined, maxViewedChild: undefined, - isRenderingDone: false, isRequestsStopped: false }), test.expect.any(Object) diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts b/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts index 05b2644a50..188b7ce652 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts +++ b/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts @@ -183,6 +183,7 @@ test.describe(' slots', () => { await component.withDefaultPaginationProviderProps({chunkSize}); await component.build(); await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.waitForSlotState('loader', false); const slots = await component.getSlotsState(); From 3c8587ba136cd2ad4096ababa6ab7f988c15fde5 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 12 Jun 2023 17:47:01 +0300 Subject: [PATCH 015/159] Rework rendering convoyer --- src/components/base/b-scrolly/interface.ts | 10 +++--- .../functional/rendering/component-factory.ts | 32 +++++++++++-------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/components/base/b-scrolly/interface.ts b/src/components/base/b-scrolly/interface.ts index 891e79d86d..7642134c54 100644 --- a/src/components/base/b-scrolly/interface.ts +++ b/src/components/base/b-scrolly/interface.ts @@ -19,7 +19,7 @@ export type ComponentStrategyKeys = keyof typeof componentStrategy; /** * Состояние компонента. */ -export interface ComponentState { +export interface ComponentState { maxViewedItem: CanUndef; maxViewedChild: CanUndef; itemsTillEnd: CanUndef; @@ -32,8 +32,8 @@ export interface ComponentState { isRequestsStopped: boolean; isLoadingInProgress: boolean; isLifecycleDone: boolean; - lastLoadedData: Readonly; - data: Readonly; + lastLoadedData: Readonly; + data: Readonly; items: Readonly; childList: Readonly; lastLoadedRawData: unknown; @@ -54,8 +54,8 @@ export interface RequestQueryFn { (params: ComponentState): Dictionary; } -export interface ComponentItemFactory { - (state: ComponentState, ctx: bScrolly): ComponentItem[]; +export interface ComponentItemFactory { + (state: ComponentState, ctx: bScrolly): ComponentItem[]; } export interface ComponentItem { diff --git a/src/components/base/b-scrolly/test/unit/functional/rendering/component-factory.ts b/src/components/base/b-scrolly/test/unit/functional/rendering/component-factory.ts index b6e3d8da0e..004fa04a97 100644 --- a/src/components/base/b-scrolly/test/unit/functional/rendering/component-factory.ts +++ b/src/components/base/b-scrolly/test/unit/functional/rendering/component-factory.ts @@ -29,7 +29,7 @@ test.describe(' rendering via component factory', () => { await provider.start(); }); - test.skip('Returned items with type `item` is equal to the provided data', async () => { + test.only('Returned items with type `item` is equal to the provided data', async () => { const chunkSize = 12; @@ -37,16 +37,19 @@ test.describe(' rendering via component factory', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: state.data.addData(0)}); - const itemsFactory = await component.mockFn>((ctx, data) => - data.map((value) => ({ + const itemsFactory = await component.mockFn>((state) => { + const data = state.lastLoadedData; + + return data.map((item) => ({ item: 'section', key: '', type: 'item', children: [], props: { - 'data-index': value.i + 'data-index': item.i } - }))); + })); + }); await component.setProps({ itemsFactory, @@ -56,12 +59,12 @@ test.describe(' rendering via component factory', () => { await component.withDefaultPaginationProviderProps({chunkSize}); await component.build(); + await component.waitForContainerChildCountEqualsTo(chunkSize); - await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); - + await test.expect(component.getContainerChildCount()).resolves.toBe(chunkSize); }); - test.skip('In additional `item`, `separator` was also returned', async () => { + test.only('In additional `item`, `separator` was also returned', async () => { const chunkSize = 12; @@ -69,18 +72,21 @@ test.describe(' rendering via component factory', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: state.data.addData(0)}); - const itemsFactory = await component.mockFn>((ctx, data) => { - const result = data.map((value) => ({ + const itemsFactory = await component.mockFn>((state) => { + const + data = state.lastLoadedData; + + const items = data.map((item) => ({ item: 'section', key: '', type: 'item', children: [], props: { - 'data-index': value.i + 'data-index': item.i } })); - result.push({ + items.push({ item: 'b-button', key: '', children: { @@ -94,7 +100,7 @@ test.describe(' rendering via component factory', () => { type: 'separator' }); - return result; + return items; }); await component.setProps({ From 4304851963391207f51f804db90346dad19dc7fe Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 12 Jun 2023 18:21:44 +0300 Subject: [PATCH 016/159] :art: itemsFactory --- src/components/base/b-scrolly/README.md | 2 + .../test/api/component-object/index.ts | 17 ++- .../test/unit/functional/emitter/index.ts | 2 +- .../functional/rendering/component-factory.ts | 132 +++++++++++++++--- 4 files changed, 127 insertions(+), 26 deletions(-) diff --git a/src/components/base/b-scrolly/README.md b/src/components/base/b-scrolly/README.md index 7c5faf3c48..1cbf1ffaa5 100644 --- a/src/components/base/b-scrolly/README.md +++ b/src/components/base/b-scrolly/README.md @@ -10,6 +10,8 @@ - стоит ли для state использовать builder like подход - негативные тест кейсы - улучшить имена интерфейсов (MountedItem, MountedSeparator, ревью имен в ComponentState) +- протыкать все метода на использование (удалить неиспользуемые) +- улучшить имена тест кейсов ## TODO: diff --git a/src/components/base/b-scrolly/test/api/component-object/index.ts b/src/components/base/b-scrolly/test/api/component-object/index.ts index 058a63c313..c9f9bb326b 100644 --- a/src/components/base/b-scrolly/test/api/component-object/index.ts +++ b/src/components/base/b-scrolly/test/api/component-object/index.ts @@ -22,12 +22,15 @@ export class ScrollyComponentObject extends ComponentObject { */ readonly container: Locator; + readonly childList: Locator; + /** * @param page */ constructor(page: Page) { super(page, 'b-scrolly'); this.container = this.node.locator(this.elSelector('container')); + this.childList = this.container.locator('> *'); } override async build(...args: Parameters['build']>): Promise> { @@ -53,22 +56,18 @@ export class ScrollyComponentObject extends ComponentObject { * Returns a container child count */ async getContainerChildCount(): Promise { - return this.container.locator('*').count(); + return this.childList.count(); } /** * Waits for container child count equals to N */ async waitForContainerChildCountEqualsTo(n: number): Promise { - await this.container.locator('*').nth(n - 1).waitFor({state: 'attached'}); - } + await this.childList.nth(n - 1).waitFor({state: 'attached'}); - /** - * Returns a promise that will be resolved after the component emits `domInsertDone` - */ - async waitForDomInsertDoneEvent(): Promise { - await this.component.evaluate((ctx) => ctx.componentEmitter.promisifyOnce('domInsertDone')); - return this; + if (await this.childList.count() > n) { + throw new Error('More than expected items'); + } } async waitForLifecycleDone(): Promise { diff --git a/src/components/base/b-scrolly/test/unit/functional/emitter/index.ts b/src/components/base/b-scrolly/test/unit/functional/emitter/index.ts index 4112339d7b..347e2b873e 100644 --- a/src/components/base/b-scrolly/test/unit/functional/emitter/index.ts +++ b/src/components/base/b-scrolly/test/unit/functional/emitter/index.ts @@ -61,7 +61,7 @@ test.describe(' emitter', () => { ]); }); - test('All data has been loaded after the second load', async ({page}) => { + test('All data has been loaded after the second load', async () => { const chunkSize = 12, providerChunkSize = chunkSize / 2; diff --git a/src/components/base/b-scrolly/test/unit/functional/rendering/component-factory.ts b/src/components/base/b-scrolly/test/unit/functional/rendering/component-factory.ts index 004fa04a97..3c9cc42d36 100644 --- a/src/components/base/b-scrolly/test/unit/functional/rendering/component-factory.ts +++ b/src/components/base/b-scrolly/test/unit/functional/rendering/component-factory.ts @@ -29,7 +29,7 @@ test.describe(' rendering via component factory', () => { await provider.start(); }); - test.only('Returned items with type `item` is equal to the provided data', async () => { + test('Returned items with type `item` is equal to the provided data', async () => { const chunkSize = 12; @@ -61,10 +61,10 @@ test.describe(' rendering via component factory', () => { await component.build(); await component.waitForContainerChildCountEqualsTo(chunkSize); - await test.expect(component.getContainerChildCount()).resolves.toBe(chunkSize); + await test.expect(component.childList).toHaveCount(chunkSize); }); - test.only('In additional `item`, `separator` was also returned', async () => { + test('In additional `item`, `separator` was also returned', async () => { const chunkSize = 12; @@ -90,12 +90,10 @@ test.describe(' rendering via component factory', () => { item: 'b-button', key: '', children: { - default: { - type: 'div', - attrs: { - id: 'button' - } - } + default: 'ima button' + }, + props: { + id: 'button' }, type: 'separator' }); @@ -111,19 +109,121 @@ test.describe(' rendering via component factory', () => { await component.withDefaultPaginationProviderProps({chunkSize}); await component.build(); + await component.waitForContainerChildCountEqualsTo(chunkSize + 1); - await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize + 1)).resolves.toBeUndefined(); + await test.expect(component.container.locator('#button')).toBeVisible(); + await test.expect(component.childList).toHaveCount(chunkSize + 1); }); - test.skip('Returned items with type `item` is less than the provided data', async () => { - // Ошибка + test('Returned items with type `item` is less than the provided data', async () => { + const + chunkSize = 12, + renderedChunkSize = chunkSize - 2; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: state.data.addData(0)}); + + const itemsFactory = await component.mockFn>((state) => { + const data = state.lastLoadedData; + + const items = data.map((item) => ({ + item: 'section', + key: '', + type: 'item', + children: [], + props: { + 'data-index': item.i + } + })); + + items.length -= 2; + return items; + }); + + await component.setProps({ + itemsFactory, + shouldPerformDataRender: () => true, + chunkSize + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForContainerChildCountEqualsTo(renderedChunkSize); + + await test.expect(component.childList).toHaveCount(renderedChunkSize); }); - test.skip('Returned item with type `item` is more than the provided data', async () => { - // Выкидывает ошибку + test('Returned item with type `item` is more than the provided data', async () => { + const + chunkSize = 12, + renderedChunkSize = chunkSize * 2; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: state.data.addData(0)}); + + const itemsFactory = await component.mockFn>((state) => { + const data = state.lastLoadedData; + + const items = data.map((item) => ({ + item: 'section', + key: '', + type: 'item', + children: [], + props: { + 'data-index': item.i + } + })); + + return [...items, ...items]; + }); + + await component.setProps({ + itemsFactory, + shouldPerformDataRender: () => true, + chunkSize + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForContainerChildCountEqualsTo(renderedChunkSize); + + await test.expect(component.childList).toHaveCount(renderedChunkSize); }); - test.skip('`item` was not returned, but equal to the number of data, the number of `separator` was returned', async () => { - // Выкидывает ошибку + test('`item` was not returned, but equal to the number of data, the number of `separator` was returned', async () => { + const + chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: state.data.addData(0)}); + + const itemsFactory = await component.mockFn>((state) => { + const data = state.lastLoadedData; + + return data.map((item) => ({ + item: 'section', + key: '', + type: 'separator', + children: [], + props: { + 'data-index': item.i + } + })); + }); + + await component.setProps({ + itemsFactory, + shouldPerformDataRender: () => true, + chunkSize + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForContainerChildCountEqualsTo(chunkSize); + + await test.expect(component.childList).toHaveCount(chunkSize); }); }); From 822e69753b1d0a074fc48899cecf5336aeaa809a Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 12 Jun 2023 18:28:08 +0300 Subject: [PATCH 017/159] WIP --- .../test/unit/functional/rendering/default.ts | 62 +++++++++++++++++++ ...{component-factory.ts => items-factory.ts} | 0 2 files changed, 62 insertions(+) rename src/components/base/b-scrolly/test/unit/functional/rendering/{component-factory.ts => items-factory.ts} (100%) diff --git a/src/components/base/b-scrolly/test/unit/functional/rendering/default.ts b/src/components/base/b-scrolly/test/unit/functional/rendering/default.ts index e69de29bb2..cb020d3dff 100644 --- a/src/components/base/b-scrolly/test/unit/functional/rendering/default.ts +++ b/src/components/base/b-scrolly/test/unit/functional/rendering/default.ts @@ -0,0 +1,62 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * @file Test cases of the component lifecycle + */ + +import test from 'tests/config/unit/test'; + +import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; +import type { ShouldFn } from 'components/base/b-scrolly/interface'; + +test.describe(' rendering via component factory', () => { + let + component: Awaited>['component'], + provider:Awaited>['provider'], + state: Awaited>['state']; + + test.beforeEach(async ({demoPage, page}) => { + await demoPage.goto(); + + ({component, provider, state} = await createTestHelpers(page)); + await provider.start(); + }); + + test('Should render all loaded data', async () => { + const + chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: state.data.addData(0)}); + + const shouldPerformDataRender = await component.mockFn( + ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 + ); + + await component.setProps({ + shouldPerformDataRender, + chunkSize + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.scrollToBottom(); + await component.waitForContainerChildCountEqualsTo(chunkSize * 2); + await component.scrollToBottom(); + await component.waitForContainerChildCountEqualsTo(chunkSize * 3); + await component.scrollToBottom(); + await component.waitForContainerChildCountEqualsTo(chunkSize * 3); + + await test.expect(component.childList).toHaveCount(chunkSize * 3); + }); +}); diff --git a/src/components/base/b-scrolly/test/unit/functional/rendering/component-factory.ts b/src/components/base/b-scrolly/test/unit/functional/rendering/items-factory.ts similarity index 100% rename from src/components/base/b-scrolly/test/unit/functional/rendering/component-factory.ts rename to src/components/base/b-scrolly/test/unit/functional/rendering/items-factory.ts From 7a0ebaae6a21e250cb9f1b0184a58b83747ca043 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 12 Jun 2023 20:25:28 +0300 Subject: [PATCH 018/159] WIP --- src/components/base/b-scrolly/README.md | 1 + .../base/b-scrolly/test/api/helpers/index.ts | 84 +++++++++--- .../functional/rendering/items-factory.ts | 91 +++++++++--- .../test/unit/functional/state/index.ts | 129 +++++++++++++++++- .../initialization/initialization.ts | 6 +- tests/helpers/component-object/mock.ts | 6 +- tests/helpers/mock/index.ts | 7 +- 7 files changed, 274 insertions(+), 50 deletions(-) diff --git a/src/components/base/b-scrolly/README.md b/src/components/base/b-scrolly/README.md index 1cbf1ffaa5..04234792f8 100644 --- a/src/components/base/b-scrolly/README.md +++ b/src/components/base/b-scrolly/README.md @@ -12,6 +12,7 @@ - улучшить имена интерфейсов (MountedItem, MountedSeparator, ревью имен в ComponentState) - протыкать все метода на использование (удалить неиспользуемые) - улучшить имена тест кейсов +- ревью и рефакторинг src\components\base\b-scrolly\test\api\helpers\index.ts ## TODO: diff --git a/src/components/base/b-scrolly/test/api/helpers/index.ts b/src/components/base/b-scrolly/test/api/helpers/index.ts index cae5ec9d67..4bb5bf2778 100644 --- a/src/components/base/b-scrolly/test/api/helpers/index.ts +++ b/src/components/base/b-scrolly/test/api/helpers/index.ts @@ -9,7 +9,7 @@ import type { Page } from 'playwright'; import test from 'tests/config/unit/test'; -import type { ComponentState, MountedItem } from 'components/base/b-scrolly/interface'; +import type { AnyMounted, ComponentItem, ComponentState, MountedItem } from 'components/base/b-scrolly/interface'; import { paginationHandler } from 'tests/helpers/providers/pagination'; import { ScrollyComponentObject } from 'components/base/b-scrolly/test/api/component-object'; import { RequestInterceptor } from 'tests/helpers/providers/interceptor'; @@ -51,12 +51,15 @@ export async function createTestHelpers(page: Page) { export interface DataConveyor { addData(count: number): DATA[]; - addMounted(count: number): MountedItem[]; + addItems(count: number): MountedItem[]; + addSeparators(count: number): AnyMounted[]; + addChild(child: ComponentItem[]): AnyMounted[]; getDataChunk(index: number): DATA[]; reset(): void; get data(): DATA[]; + get childList(): AnyMounted[]; get lastLoadedData(): DATA[]; - get mounted(): MountedItem[]; + get items(): MountedItem[]; } export function createDataConveyor( @@ -65,12 +68,14 @@ export function createDataConveyor( ): DataConveyor { let data = [], - mounted = [], + items = [], + childList = [], dataChunks = []; let dataI = 0, - mountedI = 0; + itemsI = 0, + childI = 0; const obj: DataConveyor = { addData(count: number) { @@ -84,21 +89,62 @@ export function createDataConveyor( return newData; }, - addMounted(count: number) { + addItems(count: number) { const - newData = createData(count, itemsCtor, mountedI), - mountedData = createMountedDataFrom(newData, mountedCtor, mountedI); + newData = createData(count, itemsCtor, itemsI), + itemsData = createMountedDataFrom(newData, mountedCtor, itemsI); - mounted.push(...mountedData); + items.push(...itemsData); + childList.push(...itemsData); - mountedI = mountedData.length; - return mountedData; + itemsI = itemsData.length; + childI = childList.length; + + return itemsData; + }, + + addSeparators(count: number) { + const + newData = createData(count, itemsCtor, childI), + separatorsData = createMountedDataFrom(newData, (data, i) => { + const base = Object.reject(mountedCtor(data, i), 'itemIndex'); + + // TODO: рефактор нужны нормальные конструктор + return Object.cast({ + ...base, + type: 'separator' + }); + }, childI); + + childList.push(...separatorsData); + childI = childList.length; + + return separatorsData; + }, + + addChild(list: ComponentItem[]) { + const newChild = list.map((child, i) => { + const v = { + childIndex: childI + i, + node: test.expect.any(String), + ...child + }; + + return v; + }); + + childList.push(...newChild); + childI = childList.length; + + return childList; }, reset() { dataI = 0; - mountedI = 0; - mounted = []; + itemsI = 0; + childI = 0; + childList = []; + items = []; data = []; dataChunks = []; }, @@ -107,8 +153,12 @@ export function createDataConveyor( return dataChunks[i]; }, - get mounted() { - return mounted; + get items() { + return items; + }, + + get childList() { + return childList; }, get data() { @@ -221,7 +271,7 @@ export function stateFromDataConveyor(conveyor: DataConveyor): Pick rendering via component factory', () => { let @@ -42,7 +42,7 @@ test.describe(' rendering via component factory', () => { return data.map((item) => ({ item: 'section', - key: '', + key: Object.cast(undefined), type: 'item', children: [], props: { @@ -72,13 +72,25 @@ test.describe(' rendering via component factory', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: state.data.addData(0)}); - const itemsFactory = await component.mockFn>((state) => { + const separator = { + item: 'b-button', + key: '', + children: { + default: 'ima button' + }, + props: { + id: 'button' + }, + type: 'separator' + }; + + const itemsFactory = await component.mockFn((state, ctx, separator) => { const data = state.lastLoadedData; - const items = data.map((item) => ({ + const items = data.map((item) => ({ item: 'section', - key: '', + key: Object.cast(undefined), type: 'item', children: [], props: { @@ -86,20 +98,10 @@ test.describe(' rendering via component factory', () => { } })); - items.push({ - item: 'b-button', - key: '', - children: { - default: 'ima button' - }, - props: { - id: 'button' - }, - type: 'separator' - }); + items.push(separator); return items; - }); + }, separator); await component.setProps({ itemsFactory, @@ -129,7 +131,7 @@ test.describe(' rendering via component factory', () => { const items = data.map((item) => ({ item: 'section', - key: '', + key: Object.cast(undefined), type: 'item', children: [], props: { @@ -168,7 +170,7 @@ test.describe(' rendering via component factory', () => { const items = data.map((item) => ({ item: 'section', - key: '', + key: Object.cast(undefined), type: 'item', children: [], props: { @@ -205,7 +207,7 @@ test.describe(' rendering via component factory', () => { return data.map((item) => ({ item: 'section', - key: '', + key: Object.cast(undefined), type: 'separator', children: [], props: { @@ -226,4 +228,53 @@ test.describe(' rendering via component factory', () => { await test.expect(component.childList).toHaveCount(chunkSize); }); + + test('`ItemsFactory` returns twice as much data as `chunkSize`', async () => { + const + chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: state.data.addData(0)}); + + const shouldPerformDataRender = await component.mockFn( + ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 + ); + + const itemsFactory = await component.mockFn>((state) => { + const data = state.lastLoadedData; + + const items = data.map((item) => ({ + item: 'section', + key: Object.cast(undefined), + type: 'item', + children: [], + props: { + 'data-index': item.i + } + })); + + return [...items, ...items]; + }); + + await component.setProps({ + itemsFactory, + shouldPerformDataRender, + chunkSize + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForContainerChildCountEqualsTo(chunkSize * 2); + await component.scrollToBottom(); + await component.waitForContainerChildCountEqualsTo(chunkSize * 2 * 2); + await component.scrollToBottom(); + await component.waitForContainerChildCountEqualsTo(chunkSize * 3 * 2); + await component.scrollToBottom(); + await component.waitForContainerChildCountEqualsTo(chunkSize * 3 * 2); + + await test.expect(component.childList).toHaveCount(chunkSize * 3 * 2); + }); }); diff --git a/src/components/base/b-scrolly/test/unit/functional/state/index.ts b/src/components/base/b-scrolly/test/unit/functional/state/index.ts index 5d92c01176..c16b9838c0 100644 --- a/src/components/base/b-scrolly/test/unit/functional/state/index.ts +++ b/src/components/base/b-scrolly/test/unit/functional/state/index.ts @@ -15,7 +15,7 @@ import test from 'tests/config/unit/test'; import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import { defaultProps } from 'components/base/b-scrolly/const'; -import type { ShouldFn } from 'components/base/b-scrolly/b-scrolly'; +import type { ComponentItem, ShouldFn } from 'components/base/b-scrolly/b-scrolly'; test.describe(' state', () => { let @@ -76,7 +76,7 @@ test.describe(' state', () => { .responseOnce(200, {data: state.data.addData(providerChunkSize)}) .responseOnce(200, {data: state.data.addData(providerChunkSize)}); - state.data.addMounted(chunkSize); + state.data.addItems(chunkSize); await component.setProps({ chunkSize, @@ -108,7 +108,7 @@ test.describe(' state', () => { .responseOnce(200, {data: state.data.addData(providerChunkSize)}) .response(200, {data: state.data.addData(0)}); - state.data.addMounted(chunkSize); + state.data.addItems(chunkSize); await component.scrollToBottom(); await component.waitForContainerChildCountEqualsTo(chunkSize * 2); @@ -131,8 +131,127 @@ test.describe(' state', () => { }); }); - test.skip('State after rendering via `itemsFactory`', async () => { - // ... + test.describe('State after rendering via `itemsFactory`', () => { + test('`itemsFactory` returns items with `item` and `separator` type', async () => { + const chunkSize = 12; + + const separator: ComponentItem = { + item: 'b-button', + key: Object.cast(undefined), + children: { + default: 'ima button' + }, + props: { + id: 'button' + }, + type: 'separator' + }; + + const itemsFactory = await component.mockFn((state, ctx, separator) => { + const + data = state.lastLoadedData; + + const items = data.map((item) => ({ + item: 'section', + key: Object.cast(undefined), + type: 'item', + props: { + 'data-index': item.i + } + })); + + if (data.length > 0) { + items.push(separator); + } + + return items; + }, separator); + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: state.data.addData(0)}); + + state.data.addItems(chunkSize); + state.data.addChild([separator]); + + await component.setProps({ + itemsFactory, + shouldPerformDataRender: () => true, + chunkSize + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForContainerChildCountEqualsTo(chunkSize + 1); + await component.waitForLifecycleDone(); + + const + currentState = await component.getComponentState(); + + test.expect(currentState).toEqual(state.compile({ + isInitialLoading: false, + isInitialRender: false, + isRequestsStopped: true, + isLoadingInProgress: false, + isLastEmpty: true, + isLifecycleDone: true, + loadPage: 2, + renderPage: 1 + })); + }); + + test('`itemsFactory` does not returns items with `item` type', async () => { + const chunkSize = 12; + + const itemsFactory = await component.mockFn((state) => { + const + data = state.lastLoadedData; + + const items = data.map((item) => ({ + item: 'section', + key: Object.cast(undefined), + type: 'separator', + props: { + 'data-index': item.i + } + })); + + return items; + }); + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: state.data.addData(0)}); + + state.data.addSeparators(chunkSize); + + await component.setProps({ + itemsFactory, + shouldPerformDataRender: () => true, + chunkSize + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.waitForLifecycleDone(); + + const + currentState = await component.getComponentState(); + + test.expect(currentState).toEqual(state.compile({ + isInitialLoading: false, + isInitialRender: false, + isRequestsStopped: true, + isLoadingInProgress: false, + isLastEmpty: true, + isLifecycleDone: true, + maxViewedItem: undefined, + itemsTillEnd: undefined, + loadPage: 2, + renderPage: 1 + })); + }); }); test.skip('Events state', async () => { diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts b/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts index 89894fe2dd..a183a67baf 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts +++ b/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts @@ -56,7 +56,7 @@ test.describe('', () => { firstDataChunk = state.data.addData(providerChunkSize), secondDataChunk = state.data.addData(providerChunkSize); - state.data.addMounted(chunkSize); + state.data.addItems(chunkSize); await component.setProps({ chunkSize, @@ -132,7 +132,7 @@ test.describe('', () => { state.setLoadPage(1); state.data.addData(providerChunkSize); - state.data.addMounted(providerChunkSize); + state.data.addItems(providerChunkSize); await component.setProps({ chunkSize, @@ -185,7 +185,7 @@ test.describe('', () => { state.setLoadPage(1); state.data.addData(providerChunkSize); - state.data.addMounted(providerChunkSize); + state.data.addItems(providerChunkSize); await component.setProps({ chunkSize, diff --git a/tests/helpers/component-object/mock.ts b/tests/helpers/component-object/mock.ts index 8343380390..a36096c41f 100644 --- a/tests/helpers/component-object/mock.ts +++ b/tests/helpers/component-object/mock.ts @@ -120,11 +120,13 @@ export default class ComponentObjectMock extends Compo * > Notice that the implementation will be provided into browser, * this imposes some restrictions, such as not being able to use a closure */ - async mockFn any = (...args: any[]) => any>(fn?: FN): Promise { + async mockFn< + FN extends (...args: any[]) => any = (...args: any[]) => any + >(fn?: FN, ...args: any[]): Promise { fn ??= Object.cast(() => undefined); const - {agent, id} = await createAndDisposeMock(this.page, fn!); + {agent, id} = await createAndDisposeMock(this.page, fn!, ...args); return setSerializerAsMockFn(agent, id); } diff --git a/tests/helpers/mock/index.ts b/tests/helpers/mock/index.ts index b92433aff6..916ebccc91 100644 --- a/tests/helpers/mock/index.ts +++ b/tests/helpers/mock/index.ts @@ -67,14 +67,15 @@ export async function spy( export async function createAndDisposeMock( page: Page, - fn: (...args: any[]) => any + fn: (...args: any[]) => any, + ...args: any[] ): Promise<{agent: SpyObject; id: string}> { const tmpFn = `tmp_${Math.random().toString()}`; - const agent = await page.evaluateHandle(([tmpFn, fnString]) => + const agent = await page.evaluateHandle(([tmpFn, fnString, args]) => // eslint-disable-next-line no-new-func - globalThis[tmpFn] = jest.mock(Object.cast(new Function(`return ${fnString}`)())), [tmpFn, fn.toString()]); + globalThis[tmpFn] = jest.mock((...fnArgs) => Object.cast(new Function(`return ${fnString}`)()(...fnArgs, ...args))), [tmpFn, fn.toString(), args]); return {agent: wrapAsSpy(agent, {}), id: tmpFn}; } From 77944ef5a33f1d907ebc975c4a4d41924db5d397 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 14 Jun 2023 18:56:23 +0300 Subject: [PATCH 019/159] Documentation improvements --- src/components/base/b-scrolly/b-scrolly.ts | 190 +++++++---- src/components/base/b-scrolly/const.ts | 139 +++----- src/components/base/b-scrolly/interface.ts | 139 -------- .../base/b-scrolly/interface/common.ts | 74 +++++ .../base/b-scrolly/interface/component.ts | 301 ++++++++++++++++++ .../base/b-scrolly/interface/events.ts | 144 +++++++++ .../base/b-scrolly/interface/index.ts | 12 + .../base/b-scrolly/interface/requests.ts | 27 ++ .../base/b-scrolly/modules/emitter/index.ts | 23 +- .../b-scrolly/modules/emitter/interface.ts | 55 ++++ .../base/b-scrolly/modules/factory/index.ts | 26 +- .../base/b-scrolly/modules/helpers/index.ts | 5 +- .../base/b-scrolly/modules/juggler/index.ts | 41 +-- .../base/b-scrolly/modules/observer/const.ts | 2 +- .../base/b-scrolly/modules/observer/index.ts | 67 ++-- .../b-scrolly/modules/observer/interface.ts | 12 +- .../b-scrolly/modules/presets/chunk-size.ts | 86 ++--- .../base/b-scrolly/modules/slots/index.ts | 35 +- .../base/b-scrolly/modules/slots/interface.ts | 12 + .../base/b-scrolly/modules/state/index.ts | 56 ---- .../test/unit/functional/rendering/default.ts | 8 +- .../functional/rendering/items-factory.ts | 4 +- .../test/unit/functional/state/index.ts | 8 +- 23 files changed, 963 insertions(+), 503 deletions(-) delete mode 100644 src/components/base/b-scrolly/interface.ts create mode 100644 src/components/base/b-scrolly/interface/common.ts create mode 100644 src/components/base/b-scrolly/interface/component.ts create mode 100644 src/components/base/b-scrolly/interface/events.ts create mode 100644 src/components/base/b-scrolly/interface/index.ts create mode 100644 src/components/base/b-scrolly/interface/requests.ts create mode 100644 src/components/base/b-scrolly/modules/emitter/interface.ts diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts index 932d5bf44e..89eb86b10e 100644 --- a/src/components/base/b-scrolly/b-scrolly.ts +++ b/src/components/base/b-scrolly/b-scrolly.ts @@ -21,14 +21,14 @@ import type { ComponentState, ComponentDb, - ComponentRenderStrategyKeys as ComponentRenderStrategyKeys, + ComponentRenderStrategy as ComponentRenderStrategy, RequestParams, RequestQueryFn, - ShouldFn, + ShouldPerform, ComponentRefs, ComponentItemFactory, ComponentItemType, - ComponentStrategyKeys, + ComponentStrategy, CanPerformRenderResult } from 'components/base/b-scrolly/interface'; @@ -49,7 +49,7 @@ import { Observer } from 'components/base/b-scrolly/modules/observer'; import { ComponentFactory } from 'components/base/b-scrolly/modules/factory'; import { SlotsStateController } from 'components/base/b-scrolly/modules/slots'; import { ComponentInternalState } from 'components/base/b-scrolly/modules/state'; -import { typedEmitterFactory } from 'components/base/b-scrolly/modules/emitter'; +import { componentTypedEmitter } from 'components/base/b-scrolly/modules/emitter'; import iData, { component, prop, system, $$ } from 'components/super/i-data/i-data'; import { chunkSizePreset } from 'components/base/b-scrolly/modules/presets/chunk-size'; @@ -60,6 +60,9 @@ export * from 'components/base/b-scrolly/const'; VDOM.addToPrototype(create); VDOM.addToPrototype(render); +/** + * Компонент реализующий загрузку и отрисовку больших массивов данных чанками. + */ @component() export default class bScrolly extends iData implements iItems { /** {@link iItems.item} */ @@ -78,16 +81,55 @@ export default class bScrolly extends iData implements iItems { /** {@link ComponentItemType} */ @prop({type: [String, Function]}) - readonly itemType: ComponentItemType | CreateFromItemFn = componentItemType.item; + readonly itemType: keyof ComponentItemType | CreateFromItemFn = componentItemType.item; /** {@link iItems.itemProps} */ @prop({type: [Function, Object], default: () => ({})}) readonly itemProps!: iItems['itemProps']; + /** + * Specifies the number of times the `tombstone` component will be rendered. + * + * This prop can be useful if you want to render multiple `tombstone` components + * using a single specified element. For example, if you set `tombstonesSize` to 3, + * then three `tombstone` components will be rendered on your page. + */ @prop(Number) readonly tombstonesSize?: number; - /** {@link ComponentItemFactory} */ + /** + * This factory function is used to pass information about the components that need to be rendered. + * The function should return an array of arbitrary length consisting of objects that satisfy the + * {@link ComponentItem} interface. + * + * By default, the rendering strategy is based on the `chunkSize` and `iItems` trait. + * In other words, the default implementation takes a data slice of length `chunkSize` + * and calls the `iItems` functions to generate a {@link ComponentItem} object. + * + * However, nothing prevents the client from implementing any strategy by overriding this function. + * + * For example, it is possible to define a function + * that takes the last loaded data and draws twice as many components: + * + * @example + * ```typescript + * const itemsFactory = (state) => { + * const data = state.lastLoadedData; + * + * const items = data.map((item) => ({ + * item: 'section', + * key: Object.cast(undefined), + * type: 'item', + * children: [], + * props: { + * 'data-index': item.i + * } + * })); + * + * return [...items, ...items]; + * } + * ``` + */ @prop({ type: Function, default: (state: ComponentState, ctx: bScrolly) => { @@ -118,27 +160,38 @@ export default class bScrolly extends iData implements iItems { override readonly DB!: ComponentDb; /** - * {@link RenderStrategy} + * The rendering strategy of components. + * Determines which approach will be taken for rendering components within the rendering engine. + * + * * `default` - The default approach, + * which creates a new instance of the rendering engine each time a new rendering is performed. + * + * * `reuse` - An approach + * that reuses the current instance of the rendering engine whenever a new rendering is performed. + * + * {@link ComponentRenderStrategy} */ @prop({type: String, validator: (v) => Object.isString(v) && componentRenderStrategy.hasOwnProperty(v)}) - readonly componentRenderStrategy: ComponentRenderStrategyKeys = componentRenderStrategy.default; + readonly componentRenderStrategy: keyof ComponentRenderStrategy = componentRenderStrategy.default; /** - * {@link ComponentStrategyKeys} + * Strategies for component operation modes. + * {@link ComponentStrategy} */ @prop({type: String, validator: (v) => Object.isString(v) && componentStrategy.hasOwnProperty(v)}) - readonly componentStrategy: ComponentStrategyKeys = componentStrategy.intersectionObserver; + readonly componentStrategy: keyof ComponentStrategy = componentStrategy.intersectionObserver; /** - * {@link bScrollyRequestQueryFn} + * Function that returns the GET parameters for a request. + * {@link RequestQueryFn} */ @prop({type: Function}) readonly requestQuery?: RequestQueryFn; /** - * Number of elements per one render chunk + * The number of elements to render at once. + * This prop is used in conjunction with `renderGuard` and `chunkSize` preset. */ - // eslint-disable-next-line @typescript-eslint/unbound-method @prop({type: Number, validator: Number.isNatural}) readonly chunkSize?: number = 10; @@ -151,7 +204,7 @@ export default class bScrolly extends iData implements iItems { default: defaultProps.shouldStopRequestingData }) - readonly shouldStopRequestingData!: ShouldFn; + readonly shouldStopRequestingData!: ShouldPerform; /** * When this function returns `true` the component will be able to request additional data. @@ -162,43 +215,67 @@ export default class bScrolly extends iData implements iItems { default: defaultProps.shouldPerformDataRequest }) - readonly shouldPerformDataRequest!: ShouldFn; + readonly shouldPerformDataRequest!: ShouldPerform; /** - * TODO: docs + * This function is called after successful data loading or when the component enters the visible area. + * + * This function asks the client whether rendering can be performed. The client responds with an object + * indicating whether rendering is allowed or the reason for denial. The client's response should be an object + * of type {@link CanPerformRenderResult}. + * + * Based on the result of this function, the component takes appropriate actions. For example, + * it may load data if it is not sufficient for rendering, or perform rendering if all conditions are met. + * + * By default, the {@link chunkSizePreset.renderGuard} strategy is used, + * which already implements the mechanism for communication with the component. */ @prop({ type: Function, default: (state: ComponentState, ctx: bScrolly) => { if (ctx.chunkSize == null) { - throw new Error('ChunkSize.renderGuard preset is active but chunkSize prop is not settled'); + throw new Error('The "ChunkSize.renderGuard" preset is active, but the "chunkSize" prop is not set.'); } return chunkSizePreset.renderGuard(state, ctx, ctx.chunkSize); } }) + readonly renderGuard!: ShouldPerform; - readonly renderGuard!: ShouldFn; - - // TODO: подумать над названием + /** + * This function is called in the `renderGuard` after other checks are completed. + * + * This function receives the component state as input, based on which the client + * should determine whether the component should render the next chunk of components. + * + * For example, if we want to render the next data chunk only when the client + * has seen all the main components, we can implement the following function: + * + * ```typescript + * const shouldPerformDataRender = (state) => { + * return state.isInitialRender || state.itemsTillEnd === 0; + * } + * ``` + */ @prop(Function) - readonly shouldPerformDataRender?: ShouldFn; + readonly shouldPerformDataRender?: ShouldPerform; /** - * If true then the elements observer will not be initialized. - * That may be useful if you wanna implement a lazy loading via client interaction + * If `true`, the element observation module will not be initialized. + * + * Setting this prop to `true` can be useful if you want to implement lazy rendering + * and control it using the `renderNext` method. */ @prop({ type: Boolean }) - readonly disableObserver: boolean = false; - /** {@link typedEmitterFactory} */ - @system((ctx) => typedEmitterFactory(ctx)) - readonly componentEmitter!: ReturnType; + /** {@link componentTypedEmitter} */ + @system((ctx) => componentTypedEmitter(ctx)) + readonly componentEmitter!: ReturnType; - /** {@link slotsStateController} */ + /** {@link SlotsStateController} */ @system((ctx) => new SlotsStateController(ctx)) readonly slotsStateController!: SlotsStateController; @@ -214,6 +291,7 @@ export default class bScrolly extends iData implements iItems { @system((ctx) => new Juggler(ctx)) readonly juggler!: Juggler; + /** {@link Observer} */ @system((ctx) => new Observer(ctx)) readonly observer!: Observer; @@ -277,9 +355,8 @@ export default class bScrolly extends iData implements iItems { return >initLoadResult; } - /** - * Initializes the load of the next data chunk + * Initializes the loading of the next data chunk. * @param args */ initLoadNext(): Promise { @@ -292,22 +369,23 @@ export default class bScrolly extends iData implements iItems { } /** - * Renders the next data chunk to the page (ignores `client` check for render posibility) + * Renders the next data chunk to the page (ignores the `client` check for render possibility). */ renderNext(): void { // ... } /** - * Returns an internal component state + * Returns the component state. + * {@link ComponentState} */ getComponentState(): Readonly { return this.componentInternalState.compile(); } /** - * Collets all of the request params all over the component (ig `requestProp`, `requestQuery`) - * {@link bScrollyRequestParams} + * Gathers all request parameters from the component fields `requestProp` and `requestQuery`. + * {@link RequestParams} */ getRequestParams(): RequestParams { const label: AsyncOptions = { @@ -315,10 +393,9 @@ export default class bScrolly extends iData implements iItems { join: 'replace' }; - const - defParams = this.dataProvider?.getDefaultRequestParams('get'); + const defParams = this.dataProvider?.getDefaultRequestParams('get'); - if (Object.isArray(defParams)) { + if (Array.isArray(defParams)) { Object.assign(defParams[1], label); } @@ -326,39 +403,32 @@ export default class bScrolly extends iData implements iItems { } /** - * Wrapper for `shouldStopRequestingData` + * Wrapper for `shouldStopRequestingData`. + * {@link shouldStopRequestingDataWrapper} */ shouldStopRequestingDataWrapper(): boolean { - const - state = this.getComponentState(); + const state = this.getComponentState(); if (state.isRequestsStopped) { return state.isRequestsStopped; } - const - newVal = this.shouldStopRequestingData(state, this); + const newVal = this.shouldStopRequestingData(state, this); this.componentInternalState.setIsRequestsStopped(newVal); return newVal; } /** - * Wrapper for `shouldPerformDataRender` - */ - shouldPerformDataRenderWrapper(): ReturnType { - return this.renderGuard(this.getComponentState(), this); - } - - /** - * Wrapper from `shouldPerformDataRequest` + * Wrapper for `shouldPerformDataRequest`. + * {@link shouldPerformDataRequest} */ shouldPerformDataRequestWrapper(): boolean { return this.shouldPerformDataRequest(this.getComponentState(), this); } /** - * Resets a component state and the state of the component modules + * Resets the component state and the state of the component modules. */ protected reset(): void { this.componentEmitter.emit(componentLocalEvents.resetState); @@ -370,14 +440,14 @@ export default class bScrolly extends iData implements iItems { } /** - * Handler: data load successfully finished + * Handler: data load successfully finished. * - * @param isInitialLoading - `true` if this load was an initial component loading + * @param isInitialLoading - `true` if this load was an initial component loading. * @param data */ protected onInitLoadSuccess(isInitialLoading: boolean, data: unknown): void { - if (!Object.isPlainObject(data) || !Object.isArray(data.data)) { - throw new ReferenceError('Missing data field in the loaded data'); + if (!Object.isPlainObject(data) || !Array.isArray(data.data)) { + throw new ReferenceError('Missing "data" field in the loaded data'); } this.componentInternalState.updateData(data.data, isInitialLoading); @@ -385,10 +455,7 @@ export default class bScrolly extends iData implements iItems { this.componentEmitter.emit(componentDataLocalEvents.dataLoadSuccess, data.data, isInitialLoading); - if ( - isInitialLoading && - Object.size(data.data) === 0 - ) { + if (isInitialLoading && Object.size(data.data) === 0) { if (this.shouldStopRequestingDataWrapper()) { this.componentEmitter.emit(componentDataLocalEvents.dataEmpty, isInitialLoading); } @@ -396,11 +463,12 @@ export default class bScrolly extends iData implements iItems { } /** - * Handler: failed to load data + * Handler: failed to load data. * - * @param isInitialLoading - `true` if this load was an initial component loading + * @param isInitialLoading - `true` if this load was an initial component loading. */ protected onInitLoadError(isInitialLoading: boolean): void { this.componentEmitter.emit(componentDataLocalEvents.dataLoadError, isInitialLoading); } + } diff --git a/src/components/base/b-scrolly/const.ts b/src/components/base/b-scrolly/const.ts index f38f627eb9..8593af32af 100644 --- a/src/components/base/b-scrolly/const.ts +++ b/src/components/base/b-scrolly/const.ts @@ -7,184 +7,137 @@ */ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { ComponentState } from 'components/base/b-scrolly/interface'; +import type { ComponentDataLocalEvents, ComponentItemType, ComponentLifecycleEvents, ComponentObserverLocalEvents, ComponentRenderLocalEvents, ComponentRenderStrategy, ComponentState, ComponentStrategy } from 'components/base/b-scrolly/interface'; /** - * Render strategy for producing the components. + * {@link ComponentRenderStrategy} */ -export const componentRenderStrategy = { - /** - * Данная стратегия реализует отрисовку с помощью создания инстанса `Vue` и в дальнейшем переиспользует - * его для отрисовки компонент через `forceRender`. - */ - forceRenderChunk: 'forceRenderChunk', - - /** - * Данная стратегия реализует отрисовку с помощью `vdom.create` и `vdom.render`. - */ - default: 'default' +export const componentRenderStrategy: ComponentRenderStrategy = { + default: 'default', + reuse: 'reuse' }; /** - * Стратегии возможных вариантов работы компонента. + * {@link ComponentStrategy} */ -export const componentStrategy = { - /** - * Стратегия, при которой определение вхождение элемента - * в область видимости, будет происходить с помощью `intersectionObserver`. - * - * При это узлы из DOM дерева удаляться не будут - */ +export const componentStrategy: ComponentStrategy = { intersectionObserver: 'intersectionObserver', - - /** - * Стратегия, при которой определение вхождение элемента - * в область видимости, будет происходить с помощью прослушивания события `scroll`. - * - * При это узлы из DOM дерева удаляться не будут - */ scroll: 'scroll', - - /** - * Стратегия, при которой определение вхождение элемента - * в область видимости, будет происходить с помощью прослушивания события `scroll`. - * - * При это узлы из DOM дерева буду удаляться и возвращаться - */ scrollWithDropNodes: 'scrollWithDropNodes', - - /** - * Стратегия, при которой определение вхождение элемента - * в область видимости, будет происходить с помощью прослушивания события `scroll`. - * - * При это узлы из DOM дерева будут переиспользоваться. - */ scrollWithRecycleNodes: 'scrollWithRecycleNodes' }; /** - * События компонента связанные с данными. (эмитятся в `localEmitter`) + * {@link ComponentDataLocalEvents} */ -export const componentDataLocalEvents = { - /** - * Загрузка данных началась. - */ +export const componentDataLocalEvents: ComponentDataLocalEvents = { dataLoadStart: 'dataLoadStart', - - /** - * Возникла ошибка при загрузки данных. - */ dataLoadError: 'dataLoadError', - - /** - * Данные успешно загружены. - */ dataLoadSuccess: 'dataLoadSuccess', - - /** - * Успешная загрузка в которых нет данных. - */ dataEmpty: 'dataEmpty' }; /** - * События компонента. + * {@link ComponentLifecycleEvents} */ -export const componentLocalEvents = { - /** - * Сброс состояние компонента. - */ +export const componentLocalEvents: ComponentLifecycleEvents = { resetState: 'resetState', - - /** - * Вызов конвертации данных в `DB`. - */ convertDataToDB: 'convertDataToDB', - - /** - * This event will be emitted then all of the component data is rendered and all of the component data was loaded - */ lifecycleDone: 'lifecycleDone' }; /** - * События отрисовки компонента. + * Component rendering events. */ -export const componentRenderLocalEvents = { +export const componentRenderLocalEvents: ComponentRenderLocalEvents = { /** - * Начался рендеринг элементов. + * Rendering of items has started. */ renderStart: 'renderStart', /** - * Закончился рендеринг элементов. + * Rendering of items has finished. */ renderDone: 'renderDone', /** - * Начался рендеринг элементов движком отрисовки. + * Rendering of items has started with the render engine. */ renderEngineStart: 'renderEngineStart', /** - * Закончился рендеринг элементов движком отрисовки. + * Rendering of items has finished with the render engine. */ renderEngineDone: 'renderEngineDone', /** - * Началась вставка элементов `DOM`. + * DOM node insertion has started. */ domInsertStart: 'domInsertStart', /** - * Завершилась вставка элементов в `DOM`. + * DOM node insertion has finished. */ domInsertDone: 'domInsertDone' }; +/** + * Events of the element observer. + */ +export const componentObserverLocalEvents: ComponentObserverLocalEvents = { + /** + * The element has entered the viewport. + */ + elementEnter: 'elementEnter', + + /** + * The element has exited the viewport. + */ + elementOut: 'elementOut' +}; + export const componentEvents = { ...componentDataLocalEvents, ...componentRenderLocalEvents, - ...componentLocalEvents + ...componentLocalEvents, + ...componentObserverLocalEvents }; +/** + * Reasons for rejecting a render operation. + */ export const canPerformRenderRejectionReason = { /** - * Not enough data to perform a render (ie data.length is 5 and chunkSize is 12) + * Insufficient data to perform a render (e.g., `data.length` is 5 and `chunkSize` is 12). */ notEnoughData: 'notEnoughData', /** - * No data at all to perform render (ie data.length is 0) + * No data available to perform a render (e.g., `data.length` is 0). */ noData: 'noData', /** - * All rendering are done + * All rendering operations have been completed. */ done: 'done', /** - * Client returns `false` in `shouldPerformDataRender` + * The client returns `false` in `shouldPerformDataRender`. */ noPermission: 'noPermission' }; /** - * События наблюдателя за элементами. + * {@link ComponentItemType} */ -export const componentObserverLocalEvents = { - elementEnter: 'elementEnter', - elementOut: 'elementOut' -}; - -export const componentItemType = { +export const componentItemType: ComponentItemType = { item: 'item', separator: 'separator' }; /** - * `should` свойства компонента по умолчанию. + * `should-like` свойства компонента по умолчанию. */ export const defaultProps = { /** {@link bScrolly.shouldStopRequestingData} */ diff --git a/src/components/base/b-scrolly/interface.ts b/src/components/base/b-scrolly/interface.ts deleted file mode 100644 index 7642134c54..0000000000 --- a/src/components/base/b-scrolly/interface.ts +++ /dev/null @@ -1,139 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { componentItemType, componentDataLocalEvents, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents, componentRenderStrategy, canPerformRenderRejectionReason, componentStrategy } from 'components/base/b-scrolly/const'; -import type { CreateRequestOptions, RequestQuery } from 'components/super/i-data/i-data'; - -/** - * {@link componentRenderStrategy} - */ -export type ComponentRenderStrategyKeys = keyof typeof componentRenderStrategy; -export type ComponentStrategyKeys = keyof typeof componentStrategy; - -/** - * Состояние компонента. - */ -export interface ComponentState { - maxViewedItem: CanUndef; - maxViewedChild: CanUndef; - itemsTillEnd: CanUndef; - childTillEnd: CanUndef; - loadPage: number; - renderPage: number; - isLastEmpty: boolean; - isInitialLoading: boolean; - isInitialRender: boolean; - isRequestsStopped: boolean; - isLoadingInProgress: boolean; - isLifecycleDone: boolean; - lastLoadedData: Readonly; - data: Readonly; - items: Readonly; - childList: Readonly; - lastLoadedRawData: unknown; -} - -/** - * Тип данных которые хранит компонент. - */ -export interface ComponentDb { - data: unknown[]; - total?: number; -} - -/** - * Функция которая возвращает GET параметры для запроса. - */ -export interface RequestQueryFn { - (params: ComponentState): Dictionary; -} - -export interface ComponentItemFactory { - (state: ComponentState, ctx: bScrolly): ComponentItem[]; -} - -export interface ComponentItem { - type: ComponentItemType; - item: string; - props?: Dictionary; - key: string; - children?: VNodeChildren; -} - -export interface AnyMounted extends ComponentItem { - node: HTMLElement; - childIndex: number; -} - -export interface MountedItem extends AnyMounted { - itemIndex: number; -} - -export type ComponentItemType = keyof typeof componentItemType; - -/** - * Параметры запроса. - */ -export type RequestParams = [RequestQuery, CreateRequestOptions]; - -export type CanPerformRenderRejectionReason = keyof typeof canPerformRenderRejectionReason; - -/** - * Функция для опроса клиента о необходимости выполнить то или иное действие. - */ -export interface ShouldFn { - (params: ComponentState, ctx: bScrolly): RES; -} - -export type ComponentLocalEvents = - keyof typeof componentDataLocalEvents | - keyof typeof componentLocalEvents | - keyof typeof componentRenderLocalEvents | - keyof typeof componentObserverLocalEvents; - -export interface CanPerformRenderResult { - result: boolean; - reason?: CanPerformRenderRejectionReason; -} - -/** - * Имя события: аргументы события - */ -export interface LocalEventPayloadMap { - [componentDataLocalEvents.dataLoadSuccess]: [data: object[], isInitialLoading: boolean]; - [componentDataLocalEvents.dataLoadStart]: [isInitialLoading: boolean]; - [componentDataLocalEvents.dataLoadError]: [isInitialLoading: boolean]; - [componentDataLocalEvents.dataEmpty]: [isInitialLoading: boolean]; - - [componentLocalEvents.resetState]: []; - [componentLocalEvents.lifecycleDone]: []; - [componentLocalEvents.convertDataToDB]: [data: unknown]; - - [componentObserverLocalEvents.elementEnter]: [componentItem: AnyMounted]; - [componentObserverLocalEvents.elementOut]: [componentItem: AnyMounted]; - - [componentRenderLocalEvents.renderStart]: []; - [componentRenderLocalEvents.renderDone]: []; - [componentRenderLocalEvents.renderEngineStart]: []; - [componentRenderLocalEvents.renderEngineDone]: []; - [componentRenderLocalEvents.domInsertStart]: []; - [componentRenderLocalEvents.domInsertDone]: []; -} - -export interface ComponentRefs { - container: HTMLElement; - loader?: HTMLElement; - tombstones?: HTMLElement; - empty?: HTMLElement; - retry?: HTMLElement; - done?: HTMLElement; - renderNext?: HTMLElement; -} - -export type LocalEventPayload = LocalEventPayloadMap[T]; diff --git a/src/components/base/b-scrolly/interface/common.ts b/src/components/base/b-scrolly/interface/common.ts new file mode 100644 index 0000000000..ae8452f056 --- /dev/null +++ b/src/components/base/b-scrolly/interface/common.ts @@ -0,0 +1,74 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import type { ComponentState } from 'components/base/b-scrolly/interface/component'; + +/** + * Interface representing the response of the client to the `renderGuard` method for rendering requests. + * + * To grant permission for rendering, the response object should have the following structure: + * ```typescript + * const canPerform: CanPerformRenderResult = { + * result: true + * } + * ``` + * + * To deny rendering, the response object should have the following structure: + * ```typescript + * const canPerform: CanPerformRenderResult = { + * result: false, + * reason: 'notEnoughData' + * } + * ``` + * + * Depending on the reason, specific actions will be taken based on the implementation of the `renderGuard`. + */ +export interface CanPerformRenderResult { + /** + * If `true`, rendering is permitted; if `false`, rendering is denied. + */ + result: boolean; + + /** + * The reason for rejecting the rendering request. + */ + reason?: keyof CanPerformRenderRejectionReason; +} + +/** + * Reasons for rejecting a render operation. + */ +export interface CanPerformRenderRejectionReason { + /** + * Insufficient data to perform a render (e.g., `data.length` is 5 and `chunkSize` is 12). + */ + notEnoughData: 'notEnoughData'; + + /** + * No data available to perform a render (e.g., `data.length` is 0). + */ + noData: 'noData'; + + /** + * All rendering operations have been completed. + */ + done: 'done'; + + /** + * The client returns `false` in `shouldPerformDataRender`. + */ + noPermission: 'noPermission'; +} + +/** + * A function used to query the client about whether to perform a specific action or not. + */ +export interface ShouldPerform { + (state: ComponentState, ctx: bScrolly): RES; +} diff --git a/src/components/base/b-scrolly/interface/component.ts b/src/components/base/b-scrolly/interface/component.ts new file mode 100644 index 0000000000..69065ee660 --- /dev/null +++ b/src/components/base/b-scrolly/interface/component.ts @@ -0,0 +1,301 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; + +/** + * Render strategy for producing the components. + */ +export interface ComponentRenderStrategy { + /** + * An approach that reuses the current instance of the rendering engine whenever a new rendering is performed. + */ + reuse: 'reuse'; + + /** + * The default approach, which creates a new instance of the rendering engine each time a new rendering is performed. + */ + default: 'default'; +} + +/** + * Strategies for component operation modes. + */ +export interface ComponentStrategy { + /** + * Strategy where element visibility is determined using `intersectionObserver`. + * Nodes will not be removed from the DOM tree. + */ + intersectionObserver: 'intersectionObserver'; + + /** + * Strategy where element visibility is determined by listening to the `scroll` event. + * Nodes will not be removed from the DOM tree. + */ + scroll: 'scroll'; + + /** + * Strategy where element visibility is determined by listening to the `scroll` event. + * Nodes will be removed from and returned to the DOM tree. + */ + scrollWithDropNodes: 'scrollWithDropNodes'; + + /** + * Strategy where element visibility is determined by listening to the `scroll` event. + * Nodes from the DOM tree will be recycled. + */ + scrollWithRecycleNodes: 'scrollWithRecycleNodes'; +} + +/** + * Component state. + */ +export interface ComponentState { + /** + * The largest component index of type `item` that appeared in the viewport. + */ + maxViewedItem: CanUndef; + + /** + * The largest component index of any type that appeared in the viewport. + */ + maxViewedChild: CanUndef; + + /** + * The number of components of type `item` that have not yet been visible to the user. + */ + itemsTillEnd: CanUndef; + + /** + * The number of components of any type that have not yet been visible to the user. + */ + childTillEnd: CanUndef; + + /** + * The current page number for loading data. + * It changes after each successful data load. + */ + loadPage: number; + + /** + * The current page number for rendering data. + * It changes after each successful rendering. + */ + renderPage: number; + + /** + * Indicates if the last loaded data is empty. + */ + isLastEmpty: boolean; + + /** + * Indicates if the component is in the initial loading state. + */ + isInitialLoading: boolean; + + /** + * Indicates if the component is in the initial rendering state. + */ + isInitialRender: boolean; + + /** + * Indicates if the component has stopped making requests. + */ + isRequestsStopped: boolean; + + /** + * Indicates if there is an ongoing loading process. + */ + isLoadingInProgress: boolean; + + /** + * Indicates if the component's lifecycle is done, i.e., all data is rendered and loaded. + */ + isLifecycleDone: boolean; + + /** + * The last loaded data. + */ + lastLoadedData: Readonly; + + /** + * The component data. + */ + data: Readonly; + + /** + * List of all components of type `item` that have been rendered. + */ + items: Readonly; + + /** + * List of all components that have been rendered. + */ + childList: Readonly; + + /** + * The last loaded raw data. + */ + lastLoadedRawData: unknown; +} + +/** + * Types of rendered components. + */ +export interface ComponentItemType { + /** + * This type indicates that the component is the "main" component to render. + * + * For example, in the {@link ComponentState} interface, you can notice that + * there are specific fields for the `item` type, such as `itemsTillEnd`. + * + * Components with this type are stored both in the `items` array and the `childList` array in {@link ComponentState}. + */ + item: 'item'; + + /** + * This type indicates that the component is "secondary". + * + * Components with this type are stored in the `childList` array in {@link ComponentState}. + */ + separator: 'separator'; +} + +/** + * Abstract representation of a component to be rendered. + * + * To render a `b-button` component with the default slot, the following set of parameters needs to be passed: + * + * @example + * ```typescript + * const bButton = { + * type: 'item', + * item: 'b-button', + * props: { + * id: 'button' + * }, + * key: 'unique id', + * children: { + * default: 'Hello world' + * } + * } + * ``` + */ +export interface ComponentItem { + /** + * The type of the component (item or separator). + */ + type: keyof ComponentItemType; + + /** + * The name of the component, e.g., `b-button` or `section`. + */ + item: string; + + /** + * The component's properties. + */ + props?: Dictionary; + + /** + * Unique key for this component (data set). + */ + key: string; + + /** + * Children nodes of the component. + */ + children?: VNodeChildren; +} + +/** + * Represents any mounted component (item or separator) within the DOM tree. + */ +export interface AnyMounted extends ComponentItem { + /** + * The DOM node associated with the component. + */ + node: HTMLElement; + + /** + * The index of the component within the list of children. + */ + childIndex: number; +} + +/** + * Represents a mounted item component within the DOM tree. + */ +export interface MountedItem extends AnyMounted { + /** + * The index of the item within the list of items. + */ + itemIndex: number; +} + +/** + * Represents the nodes of a component. + */ +export interface ComponentRefs { + /** + * The container element in which components are rendered. + */ + container: HTMLElement; + + /** + * The slot that is displayed while data is being loaded. + */ + loader?: HTMLElement; + + /** + * The slot that is displayed for tombstones. + */ + tombstones?: HTMLElement; + + /** + * The slot that is displayed when data loading is complete and there is no data. + */ + empty?: HTMLElement; + + /** + * The slot that is displayed when a data loading error occurs. + */ + retry?: HTMLElement; + + /** + * The slot that is displayed when all data is loaded and rendered. + */ + done?: HTMLElement; + + /** + * The slot that is displayed when there is no active loading. + */ + renderNext?: HTMLElement; +} + +/** + * The type of data stored by the component. + */ +export interface ComponentDb { + /** + * The component data. + */ + data: unknown[]; + + /** + * The total number of data items. + */ + total?: number; +} + +/** + * Typeof {@link bScrolly.itemsFactory}. + */ +export interface ComponentItemFactory { + (state: ComponentState, ctx: bScrolly): ComponentItem[]; +} diff --git a/src/components/base/b-scrolly/interface/events.ts b/src/components/base/b-scrolly/interface/events.ts new file mode 100644 index 0000000000..54784cecf2 --- /dev/null +++ b/src/components/base/b-scrolly/interface/events.ts @@ -0,0 +1,144 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { AnyMounted } from 'components/base/b-scrolly/interface/component'; +import { componentDataLocalEvents, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; + +/** + * Component data-related events (emitted in `localEmitter`). + */ +export interface ComponentDataLocalEvents { + /** + * Data loading has started. + */ + dataLoadStart: 'dataLoadStart'; + + /** + * An error occurred while loading data. + */ + dataLoadError: 'dataLoadError'; + + /** + * Data has been successfully loaded. + */ + dataLoadSuccess: 'dataLoadSuccess'; + + /** + * Successful load with no data. + */ + dataEmpty: 'dataEmpty'; +} + +/** + * Component events. + */ +export interface ComponentLifecycleEvents { + /** + * Reset component state. + */ + resetState: 'resetState'; + + /** + * Trigger data conversion to the `DB`. + */ + convertDataToDB: 'convertDataToDB'; + + /** + * This event is emitted when all component data is rendered and loaded. + */ + lifecycleDone: 'lifecycleDone'; +} + +/** + * Component rendering events. + */ +export interface ComponentRenderLocalEvents { + /** + * Rendering of items has started. + */ + renderStart: 'renderStart'; + + /** + * Rendering of items has finished. + */ + renderDone: 'renderDone'; + + /** + * Rendering of items has started with the render engine. + */ + renderEngineStart: 'renderEngineStart'; + + /** + * Rendering of items has finished with the render engine. + */ + renderEngineDone: 'renderEngineDone'; + + /** + * DOM node insertion has started. + */ + domInsertStart: 'domInsertStart'; + + /** + * DOM node insertion has finished. + */ + domInsertDone: 'domInsertDone'; +} + +/** + * Events of the element observer. + */ +export interface ComponentObserverLocalEvents { + /** + * The element has entered the viewport. + */ + elementEnter: 'elementEnter'; + + /** + * The element has exited the viewport. + */ + elementOut: 'elementOut'; +} + +/** + * Possible component events. + */ +export type ComponentEvents = + keyof ComponentDataLocalEvents | + keyof ComponentLifecycleEvents | + keyof ComponentRenderLocalEvents | + keyof ComponentObserverLocalEvents; + +/** + * Mapping of event names and their event arguments. + * [Event Name: Event Arguments] + */ +export interface LocalEventPayloadMap { + [componentDataLocalEvents.dataLoadSuccess]: [data: object[], isInitialLoading: boolean]; + [componentDataLocalEvents.dataLoadStart]: [isInitialLoading: boolean]; + [componentDataLocalEvents.dataLoadError]: [isInitialLoading: boolean]; + [componentDataLocalEvents.dataEmpty]: [isInitialLoading: boolean]; + + [componentLocalEvents.resetState]: []; + [componentLocalEvents.lifecycleDone]: []; + [componentLocalEvents.convertDataToDB]: [data: unknown]; + + [componentObserverLocalEvents.elementEnter]: [componentItem: AnyMounted]; + [componentObserverLocalEvents.elementOut]: [componentItem: AnyMounted]; + + [componentRenderLocalEvents.renderStart]: []; + [componentRenderLocalEvents.renderDone]: []; + [componentRenderLocalEvents.renderEngineStart]: []; + [componentRenderLocalEvents.renderEngineDone]: []; + [componentRenderLocalEvents.domInsertStart]: []; + [componentRenderLocalEvents.domInsertDone]: []; +} + +/** + * Returns the type of event arguments. + */ +export type LocalEventPayload = LocalEventPayloadMap[T]; diff --git a/src/components/base/b-scrolly/interface/index.ts b/src/components/base/b-scrolly/interface/index.ts new file mode 100644 index 0000000000..6257135740 --- /dev/null +++ b/src/components/base/b-scrolly/interface/index.ts @@ -0,0 +1,12 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export * from 'components/base/b-scrolly/interface/events'; +export * from 'components/base/b-scrolly/interface/component'; +export * from 'components/base/b-scrolly/interface/requests'; +export * from 'components/base/b-scrolly/interface/common'; diff --git a/src/components/base/b-scrolly/interface/requests.ts b/src/components/base/b-scrolly/interface/requests.ts new file mode 100644 index 0000000000..ef4cadeff4 --- /dev/null +++ b/src/components/base/b-scrolly/interface/requests.ts @@ -0,0 +1,27 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { CreateRequestOptions, RequestQuery } from 'components/super/i-data/i-data'; +import type { ComponentState } from 'components/base/b-scrolly/interface/component'; + +/** + * Function that returns the GET parameters for a request. + */ +export interface RequestQueryFn { + /** + * Returns the GET parameters for a request. + * + * @param params - The component state. + */ + (params: ComponentState): Dictionary; +} + +/** + * Requests parameters. + */ +export type RequestParams = [RequestQuery, CreateRequestOptions]; diff --git a/src/components/base/b-scrolly/modules/emitter/index.ts b/src/components/base/b-scrolly/modules/emitter/index.ts index 797222eae6..c39d5ae5b0 100644 --- a/src/components/base/b-scrolly/modules/emitter/index.ts +++ b/src/components/base/b-scrolly/modules/emitter/index.ts @@ -9,17 +9,17 @@ import type { AsyncOptions } from 'core/async'; import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { ComponentLocalEvents, LocalEventPayload } from 'components/base/b-scrolly/interface'; +import type { ComponentEvents, LocalEventPayload } from 'components/base/b-scrolly/interface'; +import type { ComponentTypedEmitter } from 'components/base/b-scrolly/modules/emitter/interface'; + +export * from 'components/base/b-scrolly/modules/emitter/interface'; /** - * Factory for producing typed `localEmitter` methods. - * Provides a methods of the `localEmitter` with types - * + * Provides methods for interacting with the `localEmitter` using typed events. * @param ctx */ -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function typedEmitterFactory(ctx: bScrolly) { - const once = ( +export function componentTypedEmitter(ctx: bScrolly): ComponentTypedEmitter { + const once = ( event: EVENT, handler: (...args: LocalEventPayload) => void, asyncOpts?: AsyncOptions @@ -27,7 +27,7 @@ export function typedEmitterFactory(ctx: bScrolly) { ctx.unsafe.localEmitter.once(event, handler, asyncOpts); }; - const on = ( + const on = ( event: EVENT, handler: (...args: LocalEventPayload) => void, asyncOpts?: AsyncOptions @@ -35,22 +35,23 @@ export function typedEmitterFactory(ctx: bScrolly) { ctx.unsafe.localEmitter.on(event, handler, asyncOpts); }; - const promisifyOnce = ( + const promisifyOnce = ( event: EVENT, asyncOpts?: AsyncOptions ) => ctx.unsafe.localEmitter.promisifyOnce(event, asyncOpts); - const emit = ( + const emit = ( event: EVENT, ...payload: LocalEventPayload ) => { ctx.unsafe.localEmitter.emit(event, ...payload); }; - return { + return { once, on, promisifyOnce, emit }; } + diff --git a/src/components/base/b-scrolly/modules/emitter/interface.ts b/src/components/base/b-scrolly/modules/emitter/interface.ts new file mode 100644 index 0000000000..3b8ef38c8b --- /dev/null +++ b/src/components/base/b-scrolly/modules/emitter/interface.ts @@ -0,0 +1,55 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { AsyncOptions } from 'core/async'; +import type { ComponentEvents, LocalEventPayload } from 'components/base/b-scrolly/interface'; + +/** + * An interface representing the typed `localEmitter` methods. + */ +export interface ComponentTypedEmitter { + /** + * @param event - The event name. + * @param handler - The event handler function. + * @param [asyncOpts] - Optional async options. + */ + once( + event: EVENT, + handler: (...args: LocalEventPayload) => void, + asyncOpts?: AsyncOptions + ): void; + + /** + * @param event - The event name. + * @param handler - The event handler function. + * @param [asyncOpts] - Optional async options. + */ + on( + event: EVENT, + handler: (...args: LocalEventPayload) => void, + asyncOpts?: AsyncOptions + ): void; + + /** + * @param event - The event name. + * @param [asyncOpts] - Optional async options. + */ + promisifyOnce( + event: EVENT, + asyncOpts?: AsyncOptions + ): Promise>; + + /** + * @param event - The event name. + * @param payload - The event payload. + */ + emit( + event: EVENT, + ...payload: LocalEventPayload + ): void; +} diff --git a/src/components/base/b-scrolly/modules/factory/index.ts b/src/components/base/b-scrolly/modules/factory/index.ts index 249d5c7ed6..e88c3aed90 100644 --- a/src/components/base/b-scrolly/modules/factory/index.ts +++ b/src/components/base/b-scrolly/modules/factory/index.ts @@ -17,17 +17,14 @@ import * as forceUpdate from 'components/base/b-scrolly/modules/factory/engines/ import * as vdomRender from 'components/base/b-scrolly/modules/factory/engines/vdom'; /** - * Friendly to the `bScrolly` class. - * Provides an API for component producing + * A friendly class that provides an API for component production, specifically tailored for the `bScrolly` class. */ export class ComponentFactory extends Friend { - /** - * {@link bScrolly} - */ override readonly C!: bScrolly; /** - * @param data + * Produces component items based on the current state and context. + * Returns an array of component items. */ produceComponentItems(): ComponentItem[] { const @@ -37,7 +34,10 @@ export class ComponentFactory extends Friend { } /** - * @param data + * Produces DOM nodes from an array of component items. + * Returns an array of DOM nodes representing the component items. + * + * @param componentItems - An array of component items. */ produceNodes(componentItems: ComponentItem[]): HTMLElement[] { const createDescriptor = (item: ComponentItem): VNodeDescriptor => ({ @@ -46,14 +46,15 @@ export class ComponentFactory extends Friend { children: item.children }); - const - descriptors = componentItems.map(createDescriptor); - + const descriptors = componentItems.map(createDescriptor); return this.callRenderEngine(descriptors); } /** - * @param descriptors + * Calls the render engine to render the components based on the provided descriptors. + * Returns an array of rendered DOM nodes. + * + * @param descriptors - An array of VNode descriptors. */ protected callRenderEngine(descriptors: VNodeDescriptor[]): HTMLElement[] { const @@ -62,9 +63,8 @@ export class ComponentFactory extends Friend { let res; ctx.componentEmitter.emit(componentRenderLocalEvents.renderEngineStart); - if (ctx.componentRenderStrategy === componentRenderStrategy.forceRenderChunk) { + if (ctx.componentRenderStrategy === componentRenderStrategy.reuse) { res = forceUpdate.render(ctx, descriptors); - } else { res = vdomRender.render(ctx, descriptors); } diff --git a/src/components/base/b-scrolly/modules/helpers/index.ts b/src/components/base/b-scrolly/modules/helpers/index.ts index 2cc4075fb3..5822d79271 100644 --- a/src/components/base/b-scrolly/modules/helpers/index.ts +++ b/src/components/base/b-scrolly/modules/helpers/index.ts @@ -10,9 +10,8 @@ import { componentItemType } from 'components/base/b-scrolly/const'; import type { MountedItem } from 'components/base/b-scrolly/interface'; /** - * Возвращает `true` если переданное значение является типом `MountedItem` - * - * @param val + * Returns `true` if the value is of type `MountedItem`, otherwise `false`. + * @param val - The value to check. */ export function isItem(val: any): val is MountedItem { return Object.isPlainObject(val) && val.type === componentItemType.item; diff --git a/src/components/base/b-scrolly/modules/juggler/index.ts b/src/components/base/b-scrolly/modules/juggler/index.ts index 6250ce68f6..34dfb1e62b 100644 --- a/src/components/base/b-scrolly/modules/juggler/index.ts +++ b/src/components/base/b-scrolly/modules/juggler/index.ts @@ -13,7 +13,7 @@ import Friend from 'components/friends/friend'; import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import type { ComponentItem } from 'components/base/b-scrolly/b-scrolly'; import { canPerformRenderRejectionReason, componentDataLocalEvents, componentItemType, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; -import type { AnyMounted, CanPerformRenderResult, MountedItem } from 'components/base/b-scrolly/interface'; +import type { AnyMounted, MountedItem } from 'components/base/b-scrolly/interface'; import { isItem } from 'components/base/b-scrolly/modules/helpers'; export const @@ -21,8 +21,8 @@ export const jugglerAsyncGroup = '[[JUGGLER]]'; /** - * Friendly to the `bScrolly` class. - * Provides an API for managing DOM insertion of the components + * A class that is friendly to `bScrolly`. + * Provides an API for initializing various component modules and inserting components into the DOM tree. */ export class Juggler extends Friend { @@ -47,7 +47,7 @@ export class Juggler extends Friend { } /** - * Resets the module state + * Resets the module's state to its initial state. */ protected reset(): void { const @@ -57,18 +57,8 @@ export class Juggler extends Friend { } /** - * Returns status of the possibility to render a components. - * Also returns reason of the rejection if the is no possibility to render components - */ - protected canPerformRender(): CanPerformRenderResult { - const - {ctx} = this; - - return ctx.shouldPerformDataRenderWrapper(); - } - - /** - * Renders the next chunk of the elements + * Renders components using `componentFactory` and inserts them into the DOM tree. + * `componentFactory`, in turn, calls `itemsFactory` to obtain the set of components to render. */ protected performRender(): void { const @@ -108,7 +98,7 @@ export class Juggler extends Friend { } /** - * Stores the component items + * Augments `ComponentItem` with various properties such as the component node, item index, and child index. * * @param items * @param nodes @@ -137,13 +127,16 @@ export class Juggler extends Friend { } /** - * Performs render if it is possible + * A function that performs actions (data loading/rendering) depending on the result of the `renderGuard` method. + * + * This function is the "starting point" for rendering components and is called after successful data loading + * or when rendered items enter the viewport. */ protected loadDataOrPerformRender(): void { const {ctx} = this, state = ctx.getComponentState(), - {result, reason} = this.canPerformRender(); + {result, reason} = ctx.renderGuard(state, ctx); if (result) { return this.performRender(); @@ -175,21 +168,17 @@ export class Juggler extends Friend { this.performRender(); } } - - if (reason === canPerformRenderRejectionReason.noPermission) { - // ... - } } /** - * Handler: data was loaded + * Handler: successful data loading. */ protected onDataLoaded(): void { this.loadDataOrPerformRender(); } /** - * Handler: element enters the viewport + * Handler: component enters the viewport. */ protected onElementEnters(component: AnyMounted): void { const @@ -209,7 +198,7 @@ export class Juggler extends Friend { } /** - * Handler: element leaves the viewport + * Handler: component leaves the viewport. */ protected onElementOut(_component: AnyMounted): void { // ... diff --git a/src/components/base/b-scrolly/modules/observer/const.ts b/src/components/base/b-scrolly/modules/observer/const.ts index 1b4d12682a..9f8a9be196 100644 --- a/src/components/base/b-scrolly/modules/observer/const.ts +++ b/src/components/base/b-scrolly/modules/observer/const.ts @@ -7,6 +7,6 @@ */ /** - * Group for async operations of the observer module + * Group for async operations of the observer module. */ export const observerAsyncGroup = '[[OBSERVER]]'; diff --git a/src/components/base/b-scrolly/modules/observer/index.ts b/src/components/base/b-scrolly/modules/observer/index.ts index 53d8073913..a98e89d4d3 100644 --- a/src/components/base/b-scrolly/modules/observer/index.ts +++ b/src/components/base/b-scrolly/modules/observer/index.ts @@ -15,39 +15,42 @@ import Friend from 'components/friends/friend'; export { default as IoObserver } from 'components/base/b-scrolly/modules/observer/engines/intersection-observer'; export { default as ScrollObserver } from 'components/base/b-scrolly/modules/observer/engines/scroll'; +/** + * Observer class for `bScrolly` component. + * It provides observation capabilities using different engines such as IoObserver and ScrollObserver. + */ export class Observer extends Friend { - /** - * {@link bScrolly} - */ - override readonly C!: bScrolly; - - /** - * Observe engine - */ - protected engine: IoObserver | ScrollObserver; - - /** - * @param ctx - */ - constructor(ctx: bScrolly) { - super(ctx); - - this.engine = ctx.componentStrategy === 'intersectionObserver' ? - new IoObserver(ctx) : - new ScrollObserver(ctx); + override readonly C!: bScrolly; + + /** + * The observation engine used by the Observer. + * It can be either an {@link IoObserver} or {@link ScrollObserver} instance. + */ + protected engine: IoObserver | ScrollObserver; + + /** + * @param ctx - The `bScrolly` component instance. + */ + constructor(ctx: bScrolly) { + super(ctx); + + this.engine = ctx.componentStrategy === 'intersectionObserver' ? + new IoObserver(ctx) : + new ScrollObserver(ctx); + } + + /** + * Starts observing the specified mounted elements. + * @param mounted - An array of elements to be observed. + */ + observe(mounted: AnyMounted[]): void { + const + {ctx} = this; + + if (ctx.disableObserver) { + return; } - /** - * @param mounted - */ - observe(mounted: AnyMounted[]): void { - const - {ctx} = this; - - if (ctx.disableObserver) { - return; - } - - this.engine.watchForIntersection(mounted); - } + this.engine.watchForIntersection(mounted); + } } diff --git a/src/components/base/b-scrolly/modules/observer/interface.ts b/src/components/base/b-scrolly/modules/observer/interface.ts index 435753a241..0e7b06f5b1 100644 --- a/src/components/base/b-scrolly/modules/observer/interface.ts +++ b/src/components/base/b-scrolly/modules/observer/interface.ts @@ -8,16 +8,20 @@ import type { MountedItem } from 'components/base/b-scrolly/interface'; +/** + * Interface representing an observer engine for watching components entering the viewport. + */ export interface ObserverEngine { /** - * Initializes a watcher to watch component enters the viewport - * @param components + * Initializes a watcher to track when components enter the viewport. + * + * @param components - An array of mounted items to be watched. */ watchForIntersection(components: MountedItem[]): void; /** - * Resets the module state + * Resets the state of the observer engine. + * This can be used to clear any existing observers and reset the module to its initial state. */ reset(): void; } - diff --git a/src/components/base/b-scrolly/modules/presets/chunk-size.ts b/src/components/base/b-scrolly/modules/presets/chunk-size.ts index b092928eb1..789b9f175f 100644 --- a/src/components/base/b-scrolly/modules/presets/chunk-size.ts +++ b/src/components/base/b-scrolly/modules/presets/chunk-size.ts @@ -7,10 +7,15 @@ */ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import { canPerformRenderRejectionReason, CanPerformRenderResult, ComponentState } from 'components/base/b-scrolly/b-scrolly'; + +import { canPerformRenderRejectionReason } from 'components/base/b-scrolly/const'; +import type { CanPerformRenderResult, ComponentState } from 'components/base/b-scrolly/interface'; /** - * Returns a data slice that should be rendered next + * Returns the next slice of data that should be rendered. + * + * @param state + * @param chunkSize */ export function getNextDataSlice(state: ComponentState, chunkSize: number): object[] { const @@ -21,51 +26,58 @@ export function getNextDataSlice(state: ComponentState, chunkSize: number): obje return data.slice(nextDataSliceStartIndex, nextDataSliceEndIndex); } +/** + * A preset configuration for the chunk size. + */ export const chunkSizePreset = { - renderGuard( - state: ComponentState, - ctx: bScrolly, - chunkSize: number - ): CanPerformRenderResult { - const - dataSlice = getNextDataSlice(state, chunkSize); - - if (dataSlice.length === 0) { - if (state.isRequestsStopped) { - return { - result: false, - reason: canPerformRenderRejectionReason.done - }; - } - - return { - result: false, - reason: canPerformRenderRejectionReason.noData - }; - } + /** + * A guard function that determines if the render can be performed based on the current state and chunk size. + * + * @param state + * @param ctx + * @param chunkSize + */ + renderGuard( + state: ComponentState, + ctx: bScrolly, + chunkSize: number + ): CanPerformRenderResult { + const dataSlice = getNextDataSlice(state, chunkSize); - if (dataSlice.length < chunkSize) { + if (dataSlice.length === 0) { + if (state.isRequestsStopped) { return { result: false, - reason: canPerformRenderRejectionReason.notEnoughData + reason: canPerformRenderRejectionReason.done }; } - if (state.isInitialRender) { - return { - result: true - }; - } + return { + result: false, + reason: canPerformRenderRejectionReason.noData + }; + } - const - clientResponse = ctx.shouldPerformDataRender?.(state, ctx); + if (dataSlice.length < chunkSize) { + return { + result: false, + reason: canPerformRenderRejectionReason.notEnoughData + }; + } + if (state.isInitialRender) { return { - result: clientResponse == null ? true : clientResponse, - reason: clientResponse === false ? canPerformRenderRejectionReason.noPermission : undefined + result: true }; - }, + } - getNextDataSlice -}; + const clientResponse = ctx.shouldPerformDataRender?.(state, ctx); + return { + result: clientResponse == null ? true : clientResponse, + reason: clientResponse === false ? canPerformRenderRejectionReason.noPermission : undefined + }; + }, + + getNextDataSlice +}; diff --git a/src/components/base/b-scrolly/modules/slots/index.ts b/src/components/base/b-scrolly/modules/slots/index.ts index e7453877e9..bb500e061e 100644 --- a/src/components/base/b-scrolly/modules/slots/index.ts +++ b/src/components/base/b-scrolly/modules/slots/index.ts @@ -21,17 +21,14 @@ export const slotsStateControllerAsyncGroup = 'slotsStateController'; /** - * Класс реализующий показ нужных слотов в нужный момент времени. + * A class that manages the visibility of slots based on different states. */ export class SlotsStateController extends Friend { - /** - * {@link bScrolly} - */ override readonly C!: bScrolly; /** - * Опции для асинхронной функции обновление состояния отображения слотов. + * Options for the asynchronous operations. */ protected readonly asyncUpdateLabel: AsyncOptions = { label: $$.updateSlotsVisibility, @@ -56,7 +53,7 @@ export class SlotsStateController extends Friend { } /** - * Отображает слоты которые должны отображаться при пустом состоянии. + * Displays the slots that should be shown when the data state is empty. */ emptyState(): void { this.setSlotsVisibility({ @@ -70,6 +67,9 @@ export class SlotsStateController extends Friend { }); } + /** + * Displays the slots that should be shown when the lifecycle is done. + */ doneState(): void { this.setSlotsVisibility({ container: true, @@ -83,7 +83,7 @@ export class SlotsStateController extends Friend { } /** - * Отображает слоты которые должны отображаться в момент загрузки данных. + * Displays the slots that should be shown during data loading progress. */ loadingProgressState(): void { this.setSlotsVisibility({ @@ -98,7 +98,7 @@ export class SlotsStateController extends Friend { } /** - * Отображает слоты которые должны отображаться в момент неудачной загрузки. + * Displays the slots that should be shown when data loading fails. */ loadingFailedState(): void { this.setSlotsVisibility({ @@ -113,10 +113,9 @@ export class SlotsStateController extends Friend { } /** - * Отображает слоты которые должны отображаться в момент успешной загрузки данных. + * Displays the slots that should be shown when data loading is successful. */ loadingSuccessState(): void { - // Здесь нужно не loadingSuccessState а какое-то другое событие так как LoadingSuccess может происходить много раз this.setSlotsVisibility({ container: true, done: false, @@ -129,16 +128,16 @@ export class SlotsStateController extends Friend { } /** - * Очищает состояние модуля. + * Resets the state of the module. */ reset(): void { this.async.clearAll({group: new RegExp(slotsStateControllerAsyncGroup)}); } /** - * Устанавливает состояние слотов. + * Sets the visibility state of the slots. * - * @param stateObj + * @param stateObj - An object specifying the visibility state of each slot. */ protected setSlotsVisibility(stateObj: Required): void { this.async.cancelAnimationFrame(this.asyncUpdateLabel); @@ -147,19 +146,17 @@ export class SlotsStateController extends Friend { for (const [name, state] of Object.entries(stateObj)) { this.setDisplayState(name, state); } - }, this.asyncUpdateLabel); } /** - * Устанавливает состояние слота. + * Sets the display state of a slot. * - * @param name - * @param state + * @param name - The name of the slot. + * @param state - The visibility state of the slot. */ protected setDisplayState(name: keyof SlotsStateObj, state: boolean): void { - const - ref = this.ctx.$refs[name]; + const ref = this.ctx.$refs[name]; if (ref) { ref.style.display = state ? '' : 'none'; diff --git a/src/components/base/b-scrolly/modules/slots/interface.ts b/src/components/base/b-scrolly/modules/slots/interface.ts index a61d6f46c7..9b47834373 100644 --- a/src/components/base/b-scrolly/modules/slots/interface.ts +++ b/src/components/base/b-scrolly/modules/slots/interface.ts @@ -1,5 +1,17 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + import type { ComponentRefs } from 'components/base/b-scrolly/interface'; +/** + * Represents the state of slots. + * [slotName: slotVisibility] + */ export type SlotsStateObj = { [key in keyof ComponentRefs]: boolean; }; diff --git a/src/components/base/b-scrolly/modules/state/index.ts b/src/components/base/b-scrolly/modules/state/index.ts index efb166b197..931a0fa3f7 100644 --- a/src/components/base/b-scrolly/modules/state/index.ts +++ b/src/components/base/b-scrolly/modules/state/index.ts @@ -38,14 +38,6 @@ export class ComponentInternalState extends Friend { this.setIsInitialRender(false); this.incrementRenderPage(); }); - - // componentEmitter.on(componentRenderLocalEvents.renderDone, () => { - // this.updateIsRenderDone(); - // }); - - // componentEmitter.on(componentDataLocalEvents.dataLoadSuccess, () => { - // this.updateIsRenderDone(); - // }); } /** @@ -128,44 +120,6 @@ export class ComponentInternalState extends Friend { return this; } - // updateIsRenderDone(): this { - // const - // {ctx} = this, - // state = ctx.getComponentState(); - - // if ( - // !state.isLoadingInProgress && - // state.isRequestsStopped && - // state.data.length === state.items.length - // ) { - // ctx.componentInternalState.setIsRenderingDone(true); - - // } else { - // ctx.componentInternalState.setIsRenderingDone(false); - // } - - // return this; - // } - - // updateIsLifecycleDone(): this { - // const - // {ctx} = this, - // state = ctx.getComponentState(); - - // if (state.isLifecycleDone) { - // return this; - // } - - // if ( - // state.isRequestsStopped && - // state.isRenderingDone - // ) { - // ctx.componentInternalState.setIsLifecycleDone(true); - // } - - // return this; - // } - /** * Обновляет состояние последних сырых загруженных данных. * @@ -188,19 +142,9 @@ export class ComponentInternalState extends Friend { setIsRequestsStopped(state: boolean): this { this.state.isRequestsStopped = state; - // this.updateIsRenderDone(); - // this.updateIsLifecycleDone(); - return this; } - // setIsRenderingDone(state: boolean): this { - // this.state.isRenderingDone = state; - // // this.updateIsLifecycleDone(); - - // return this; - // } - setIsLifecycleDone(state: boolean): this { if (this.state.isLifecycleDone === state) { return this; diff --git a/src/components/base/b-scrolly/test/unit/functional/rendering/default.ts b/src/components/base/b-scrolly/test/unit/functional/rendering/default.ts index cb020d3dff..5ec84395fb 100644 --- a/src/components/base/b-scrolly/test/unit/functional/rendering/default.ts +++ b/src/components/base/b-scrolly/test/unit/functional/rendering/default.ts @@ -13,7 +13,7 @@ import test from 'tests/config/unit/test'; import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; -import type { ShouldFn } from 'components/base/b-scrolly/interface'; +import type { ShouldPerform } from 'components/base/b-scrolly/interface'; test.describe(' rendering via component factory', () => { let @@ -38,7 +38,7 @@ test.describe(' rendering via component factory', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: state.data.addData(0)}); - const shouldPerformDataRender = await component.mockFn( + const shouldPerformDataRender = await component.mockFn( ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 ); @@ -59,4 +59,8 @@ test.describe(' rendering via component factory', () => { await test.expect(component.childList).toHaveCount(chunkSize * 3); }); + + test.skip('Rendering components with children', async () => { + // .. + }); }); diff --git a/src/components/base/b-scrolly/test/unit/functional/rendering/items-factory.ts b/src/components/base/b-scrolly/test/unit/functional/rendering/items-factory.ts index 4c00b55d99..ffc6336f52 100644 --- a/src/components/base/b-scrolly/test/unit/functional/rendering/items-factory.ts +++ b/src/components/base/b-scrolly/test/unit/functional/rendering/items-factory.ts @@ -14,7 +14,7 @@ import test from 'tests/config/unit/test'; import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; import type { ComponentItemFactory } from 'components/base/b-scrolly/b-scrolly'; -import type { ComponentItem, ShouldFn } from 'components/base/b-scrolly/interface'; +import type { ComponentItem, ShouldPerform } from 'components/base/b-scrolly/interface'; test.describe(' rendering via component factory', () => { let @@ -239,7 +239,7 @@ test.describe(' rendering via component factory', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: state.data.addData(0)}); - const shouldPerformDataRender = await component.mockFn( + const shouldPerformDataRender = await component.mockFn( ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 ); diff --git a/src/components/base/b-scrolly/test/unit/functional/state/index.ts b/src/components/base/b-scrolly/test/unit/functional/state/index.ts index c16b9838c0..52584162a5 100644 --- a/src/components/base/b-scrolly/test/unit/functional/state/index.ts +++ b/src/components/base/b-scrolly/test/unit/functional/state/index.ts @@ -15,7 +15,7 @@ import test from 'tests/config/unit/test'; import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import { defaultProps } from 'components/base/b-scrolly/const'; -import type { ComponentItem, ShouldFn } from 'components/base/b-scrolly/b-scrolly'; +import type { ComponentItem, ShouldPerform } from 'components/base/b-scrolly/b-scrolly'; test.describe(' state', () => { let @@ -65,10 +65,10 @@ test.describe(' state', () => { providerChunkSize = chunkSize / 2; const - shouldStopRequestingData = await component.mockFn(defaultProps.shouldStopRequestingData), - shouldPerformDataRequest = await component.mockFn(({isInitialLoading, itemsTillEnd, isLastEmpty}) => + shouldStopRequestingData = (defaultProps.shouldStopRequestingData), + shouldPerformDataRequest = (({isInitialLoading, itemsTillEnd, isLastEmpty}) => isInitialLoading || (itemsTillEnd === 0 && !isLastEmpty)), - shouldPerformDataRender = await component.mockFn(({isInitialRender, itemsTillEnd}) => + shouldPerformDataRender = (({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0); await test.step('After rendering first data chunk', async () => { From bf019a8d63ebcb7baf9b42541eb62367656ea5cd Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 14 Jun 2023 22:32:04 +0300 Subject: [PATCH 020/159] :art: --- src/components/base/b-scrolly/README.md | 21 +++++++++++++++++++ .../base/b-scrolly/interface/component.ts | 6 +++--- .../base/b-scrolly/interface/events.ts | 6 +++--- .../base/b-scrolly/modules/juggler/index.ts | 8 +++---- .../observer/engines/intersection-observer.ts | 4 ++-- .../modules/observer/engines/scroll.ts | 4 ++-- .../base/b-scrolly/modules/observer/index.ts | 4 ++-- .../base/b-scrolly/modules/state/index.ts | 6 +++--- .../base/b-scrolly/test/api/helpers/index.ts | 20 ++++++++++-------- .../test/unit/lifecycle/slots/slots.ts | 13 ++++++++---- 10 files changed, 60 insertions(+), 32 deletions(-) diff --git a/src/components/base/b-scrolly/README.md b/src/components/base/b-scrolly/README.md index 04234792f8..77256ad2de 100644 --- a/src/components/base/b-scrolly/README.md +++ b/src/components/base/b-scrolly/README.md @@ -14,6 +14,27 @@ - улучшить имена тест кейсов - ревью и рефакторинг src\components\base\b-scrolly\test\api\helpers\index.ts +```mermaid +graph TD; + A[loadDataOrPerformRender] -->|state, ctx| B[renderGuard] + B -->|result = true| C[performRender] + B -->|result = false| E[reason] + E -->|done| F[setIsLifecycleDone=true] + E -->|noData| G[isRequestsStopped] + G -->|false| H[shouldPerformDataRequestWrapper] + H -->|true| I[initLoad] + G -->|true| J[return] + E -->|notEnoughData| K[isRequestsStopped] + K -->|true| L[performRender] + K -->|false| M[shouldPerformDataRequestWrapper] + M -->|true| N[initLoad] + M -->|false| O[isInitialRender] + O -->|true| P[performRender] + O -->|false| Q[return] + + +``` + ## TODO: 1. Бенчмарк подходов diff --git a/src/components/base/b-scrolly/interface/component.ts b/src/components/base/b-scrolly/interface/component.ts index 69065ee660..b089b23088 100644 --- a/src/components/base/b-scrolly/interface/component.ts +++ b/src/components/base/b-scrolly/interface/component.ts @@ -136,7 +136,7 @@ export interface ComponentState { /** * List of all components that have been rendered. */ - childList: Readonly; + childList: Readonly; /** * The last loaded raw data. @@ -216,7 +216,7 @@ export interface ComponentItem { /** * Represents any mounted component (item or separator) within the DOM tree. */ -export interface AnyMounted extends ComponentItem { +export interface MountedChild extends ComponentItem { /** * The DOM node associated with the component. */ @@ -231,7 +231,7 @@ export interface AnyMounted extends ComponentItem { /** * Represents a mounted item component within the DOM tree. */ -export interface MountedItem extends AnyMounted { +export interface MountedItem extends MountedChild { /** * The index of the item within the list of items. */ diff --git a/src/components/base/b-scrolly/interface/events.ts b/src/components/base/b-scrolly/interface/events.ts index 54784cecf2..f7a744a4d5 100644 --- a/src/components/base/b-scrolly/interface/events.ts +++ b/src/components/base/b-scrolly/interface/events.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { AnyMounted } from 'components/base/b-scrolly/interface/component'; +import type { MountedChild } from 'components/base/b-scrolly/interface/component'; import { componentDataLocalEvents, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; /** @@ -127,8 +127,8 @@ export interface LocalEventPayloadMap { [componentLocalEvents.lifecycleDone]: []; [componentLocalEvents.convertDataToDB]: [data: unknown]; - [componentObserverLocalEvents.elementEnter]: [componentItem: AnyMounted]; - [componentObserverLocalEvents.elementOut]: [componentItem: AnyMounted]; + [componentObserverLocalEvents.elementEnter]: [componentItem: MountedChild]; + [componentObserverLocalEvents.elementOut]: [componentItem: MountedChild]; [componentRenderLocalEvents.renderStart]: []; [componentRenderLocalEvents.renderDone]: []; diff --git a/src/components/base/b-scrolly/modules/juggler/index.ts b/src/components/base/b-scrolly/modules/juggler/index.ts index 34dfb1e62b..7ff71c7678 100644 --- a/src/components/base/b-scrolly/modules/juggler/index.ts +++ b/src/components/base/b-scrolly/modules/juggler/index.ts @@ -13,7 +13,7 @@ import Friend from 'components/friends/friend'; import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import type { ComponentItem } from 'components/base/b-scrolly/b-scrolly'; import { canPerformRenderRejectionReason, componentDataLocalEvents, componentItemType, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; -import type { AnyMounted, MountedItem } from 'components/base/b-scrolly/interface'; +import type { MountedChild, MountedItem } from 'components/base/b-scrolly/interface'; import { isItem } from 'components/base/b-scrolly/modules/helpers'; export const @@ -103,7 +103,7 @@ export class Juggler extends Friend { * @param items * @param nodes */ - protected produceMounted(items: ComponentItem[], nodes: HTMLElement[]): Array { + protected produceMounted(items: ComponentItem[], nodes: HTMLElement[]): Array { const {ctx} = this, {items: mountedItems, childList} = ctx.getComponentState(); @@ -180,7 +180,7 @@ export class Juggler extends Friend { /** * Handler: component enters the viewport. */ - protected onElementEnters(component: AnyMounted): void { + protected onElementEnters(component: MountedChild): void { const {ctx} = this, state = ctx.getComponentState(), @@ -200,7 +200,7 @@ export class Juggler extends Friend { /** * Handler: component leaves the viewport. */ - protected onElementOut(_component: AnyMounted): void { + protected onElementOut(_component: MountedChild): void { // ... } } diff --git a/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts b/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts index fbb5f20bf6..b30169127f 100644 --- a/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts +++ b/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts @@ -8,7 +8,7 @@ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import { componentLocalEvents, componentObserverLocalEvents } from 'components/base/b-scrolly/const'; -import type { AnyMounted } from 'components/base/b-scrolly/interface'; +import type { MountedChild } from 'components/base/b-scrolly/interface'; import { observerAsyncGroup } from 'components/base/b-scrolly/modules/observer/const'; import type { ObserverEngine } from 'components/base/b-scrolly/modules/observer/interface'; import Friend from 'components/friends/friend'; @@ -32,7 +32,7 @@ export default class IoObserver extends Friend implements ObserverEngine { /** * @inheritdoc */ - watchForIntersection(components: AnyMounted[]): void { + watchForIntersection(components: MountedChild[]): void { const {ctx} = this; diff --git a/src/components/base/b-scrolly/modules/observer/engines/scroll.ts b/src/components/base/b-scrolly/modules/observer/engines/scroll.ts index 8a37477748..06ca908d2f 100644 --- a/src/components/base/b-scrolly/modules/observer/engines/scroll.ts +++ b/src/components/base/b-scrolly/modules/observer/engines/scroll.ts @@ -7,7 +7,7 @@ */ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { AnyMounted } from 'components/base/b-scrolly/interface'; +import type { MountedChild } from 'components/base/b-scrolly/interface'; import { observerAsyncGroup } from 'components/base/b-scrolly/modules/observer/const'; import type { ObserverEngine } from 'components/base/b-scrolly/modules/observer/interface'; import Friend from 'components/friends/friend'; @@ -22,7 +22,7 @@ export default class ScrollObserver extends Friend implements ObserverEngine { /** * @inheritdoc */ - watchForIntersection(_components: AnyMounted[]): void { + watchForIntersection(_components: MountedChild[]): void { // ... } diff --git a/src/components/base/b-scrolly/modules/observer/index.ts b/src/components/base/b-scrolly/modules/observer/index.ts index a98e89d4d3..4bf5abe031 100644 --- a/src/components/base/b-scrolly/modules/observer/index.ts +++ b/src/components/base/b-scrolly/modules/observer/index.ts @@ -7,7 +7,7 @@ */ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { AnyMounted } from 'components/base/b-scrolly/interface'; +import type { MountedChild } from 'components/base/b-scrolly/interface'; import ScrollObserver from 'components/base/b-scrolly/modules/observer/engines/scroll'; import IoObserver from 'components/base/b-scrolly/modules/observer/engines/intersection-observer'; import Friend from 'components/friends/friend'; @@ -43,7 +43,7 @@ export class Observer extends Friend { * Starts observing the specified mounted elements. * @param mounted - An array of elements to be observed. */ - observe(mounted: AnyMounted[]): void { + observe(mounted: MountedChild[]): void { const {ctx} = this; diff --git a/src/components/base/b-scrolly/modules/state/index.ts b/src/components/base/b-scrolly/modules/state/index.ts index 931a0fa3f7..ee5d4a5c21 100644 --- a/src/components/base/b-scrolly/modules/state/index.ts +++ b/src/components/base/b-scrolly/modules/state/index.ts @@ -8,7 +8,7 @@ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import { componentDataLocalEvents, componentLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; -import type { AnyMounted, ComponentState, MountedItem } from 'components/base/b-scrolly/interface'; +import type { MountedChild, ComponentState, MountedItem } from 'components/base/b-scrolly/interface'; import { createInitialState } from 'components/base/b-scrolly/modules/state/helpers'; import Friend from 'components/friends/friend'; @@ -97,8 +97,8 @@ export class ComponentInternalState extends Friend { return this; } - updateChildList(mounted: AnyMounted[]): this { - (this.state.childList).push(...mounted); + updateChildList(mounted: MountedChild[]): this { + (this.state.childList).push(...mounted); return this; } diff --git a/src/components/base/b-scrolly/test/api/helpers/index.ts b/src/components/base/b-scrolly/test/api/helpers/index.ts index 4bb5bf2778..256afcd5f7 100644 --- a/src/components/base/b-scrolly/test/api/helpers/index.ts +++ b/src/components/base/b-scrolly/test/api/helpers/index.ts @@ -9,19 +9,21 @@ import type { Page } from 'playwright'; import test from 'tests/config/unit/test'; -import type { AnyMounted, ComponentItem, ComponentState, MountedItem } from 'components/base/b-scrolly/interface'; +import type { MountedChild, ComponentItem, ComponentState, MountedItem } from 'components/base/b-scrolly/interface'; import { paginationHandler } from 'tests/helpers/providers/pagination'; import { ScrollyComponentObject } from 'components/base/b-scrolly/test/api/component-object'; import { RequestInterceptor } from 'tests/helpers/providers/interceptor'; -import { componentEvents } from 'components/base/b-scrolly/const'; +import { componentEvents, componentObserverLocalEvents } from 'components/base/b-scrolly/const'; export * from 'components/base/b-scrolly/test/api/component-object'; type DataItemCtor = (i: number) => DATA; type MountedItemCtor = (data: DATA, i: number) => MountedItem; -export function filterEmitterCalls(calls: unknown[][]): unknown[][] { - return calls.filter(([event]) => Object.isString(event) && Boolean(componentEvents[event])); +export function filterEmitterCalls(calls: unknown[][], filterObserverEvents: boolean = true): unknown[][] { + return calls.filter(([event]) => Object.isString(event) && + Boolean(componentEvents[event]) && + (filterObserverEvents ? !(event in componentObserverLocalEvents) : true)); } /** @@ -52,12 +54,12 @@ export async function createTestHelpers(page: Page) { export interface DataConveyor { addData(count: number): DATA[]; addItems(count: number): MountedItem[]; - addSeparators(count: number): AnyMounted[]; - addChild(child: ComponentItem[]): AnyMounted[]; + addSeparators(count: number): MountedChild[]; + addChild(child: ComponentItem[]): MountedChild[]; getDataChunk(index: number): DATA[]; reset(): void; get data(): DATA[]; - get childList(): AnyMounted[]; + get childList(): MountedChild[]; get lastLoadedData(): DATA[]; get items(): MountedItem[]; } @@ -69,7 +71,7 @@ export function createDataConveyor( let data = [], items = [], - childList = [], + childList = [], dataChunks = []; let @@ -123,7 +125,7 @@ export function createDataConveyor( }, addChild(list: ComponentItem[]) { - const newChild = list.map((child, i) => { + const newChild = list.map((child, i) => { const v = { childIndex: childI + i, node: test.expect.any(String), diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts b/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts index 188b7ce652..f6a14d059d 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts +++ b/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts @@ -16,7 +16,7 @@ import test from 'tests/config/unit/test'; import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; import type { SlotsStateObj } from 'components/base/b-scrolly/modules/slots'; -import type { ShouldFn } from 'components/base/b-scrolly/b-scrolly'; +import type { ShouldPerform } from 'components/base/b-scrolly/b-scrolly'; test.describe(' slots', () => { let @@ -173,11 +173,16 @@ test.describe(' slots', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); + const shouldPerformDataRequest = + (({isInitialLoading, itemsTillEnd}) => isInitialLoading || itemsTillEnd === 0); + + const shouldPerformDataRender = + (({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0); + await component.setProps({ chunkSize, - // eslint-disable-next-line max-len - shouldPerformDataRequest: (({isInitialLoading, itemsTillEnd}) => isInitialLoading || itemsTillEnd === 0), - shouldPerformDataRender: (({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0) + shouldPerformDataRequest, + shouldPerformDataRender }); await component.withDefaultPaginationProviderProps({chunkSize}); From 21ee4a32a03dfa07f2cc6e1d0aeaa2dd37f4166b Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 15 Jun 2023 13:50:13 +0300 Subject: [PATCH 021/159] WIP --- .../base/b-scrolly/test/api/helpers/index.ts | 241 ++++++++++-------- .../b-scrolly/test/api/helpers/interface.ts | 127 +++++++++ .../initialization/initialization.ts | 11 +- 3 files changed, 271 insertions(+), 108 deletions(-) create mode 100644 src/components/base/b-scrolly/test/api/helpers/interface.ts diff --git a/src/components/base/b-scrolly/test/api/helpers/index.ts b/src/components/base/b-scrolly/test/api/helpers/index.ts index 256afcd5f7..9a7a5d323e 100644 --- a/src/components/base/b-scrolly/test/api/helpers/index.ts +++ b/src/components/base/b-scrolly/test/api/helpers/index.ts @@ -14,31 +14,23 @@ import { paginationHandler } from 'tests/helpers/providers/pagination'; import { ScrollyComponentObject } from 'components/base/b-scrolly/test/api/component-object'; import { RequestInterceptor } from 'tests/helpers/providers/interceptor'; import { componentEvents, componentObserverLocalEvents } from 'components/base/b-scrolly/const'; +import type { DataConveyor, DataItemCtor, MountedItemCtor, StateApi, ScrollyTestHelpers, MountedSeparatorCtor } from 'components/base/b-scrolly/test/api/helpers/interface'; export * from 'components/base/b-scrolly/test/api/component-object'; -type DataItemCtor = (i: number) => DATA; -type MountedItemCtor = (data: DATA, i: number) => MountedItem; - -export function filterEmitterCalls(calls: unknown[][], filterObserverEvents: boolean = true): unknown[][] { - return calls.filter(([event]) => Object.isString(event) && - Boolean(componentEvents[event]) && - (filterObserverEvents ? !(event in componentObserverLocalEvents) : true)); -} - /** - * Creates a test helpers for `b-scrolly` component - * @param page + * Creates a helper API for convenient testing of the `b-scrolly` component. + * @param page The page object representing the testing page. */ -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export async function createTestHelpers(page: Page) { +export async function createTestHelpers(page: Page): Promise { const component = new ScrollyComponentObject(page), initLoadSpy = await component.spyOn('initLoad', {proto: true}), provider = new RequestInterceptor(page, /api/), - state = createState({}, createDataConveyor( - indexDataCtor, - sectionMountedItemCtor + state = createStateApi({}, createDataConveyor( + createIndexedObj, + createMountedSeparator, + createMountedItem )); provider.response(paginationHandler); @@ -51,28 +43,26 @@ export async function createTestHelpers(page: Page) { }; } -export interface DataConveyor { - addData(count: number): DATA[]; - addItems(count: number): MountedItem[]; - addSeparators(count: number): MountedChild[]; - addChild(child: ComponentItem[]): MountedChild[]; - getDataChunk(index: number): DATA[]; - reset(): void; - get data(): DATA[]; - get childList(): MountedChild[]; - get lastLoadedData(): DATA[]; - get items(): MountedItem[]; -} - -export function createDataConveyor( - itemsCtor: DataItemCtor, - mountedCtor: MountedItemCtor +/** + * 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( + itemsCtor: DataItemCtor, + separatorCtor: MountedSeparatorCtor, + mountedCtor: MountedItemCtor ): DataConveyor { let - data = [], + data = [], items = [], childList = [], - dataChunks = []; + dataChunks = []; let dataI = 0, @@ -81,8 +71,7 @@ export function createDataConveyor( const obj: DataConveyor = { addData(count: number) { - const - newData = createData(count, itemsCtor, dataI); + const newData = createChunk(count, itemsCtor, dataI); data.push(...newData); dataChunks.push(newData); @@ -93,8 +82,8 @@ export function createDataConveyor( addItems(count: number) { const - newData = createData(count, itemsCtor, itemsI), - itemsData = createMountedDataFrom(newData, mountedCtor, itemsI); + newData = createChunk(count, itemsCtor, itemsI), + itemsData = createFromData(newData, mountedCtor, itemsI); items.push(...itemsData); childList.push(...itemsData); @@ -107,16 +96,8 @@ export function createDataConveyor( addSeparators(count: number) { const - newData = createData(count, itemsCtor, childI), - separatorsData = createMountedDataFrom(newData, (data, i) => { - const base = Object.reject(mountedCtor(data, i), 'itemIndex'); - - // TODO: рефактор нужны нормальные конструктор - return Object.cast({ - ...base, - type: 'separator' - }); - }, childI); + newData = createChunk(count, itemsCtor, childI), + separatorsData = createFromData(newData, separatorCtor, childI); childList.push(...separatorsData); childI = childList.length; @@ -151,10 +132,6 @@ export function createDataConveyor( dataChunks = []; }, - getDataChunk(i: number) { - return dataChunks[i]; - }, - get items() { return items; }, @@ -175,69 +152,30 @@ export function createDataConveyor( return obj; } -export function createMountedDataFrom( - data: DATA[], - ctor: MountedItemCtor, - start: number = 0 -): MountedItem[] { - return data.map((item, i) => ctor(item, start + i)); -} - -export function sectionMountedItemCtor(data: DATA, i: number): MountedItem { - return { - itemIndex: i, - childIndex: i, - props: { - 'data-index': i - }, - key: Object.cast(undefined), - item: 'section', - type: 'item', - node: test.expect.any(String) - }; -} - -export function indexDataCtor(i: number): {i: number} { - return {i}; -} - /** - * TODO: Docs - * @param count - * @param start - * @param itemCtor + * 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 createData( - count: number, - itemCtor: (i: number) => DATA, - start: number = 0 -): DATA[] { - return Array.from(new Array(count), (_, i) => itemCtor(start + i)); -} - -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function createState( +export function createStateApi( initial: Partial, dataConveyor: DataConveyor -) { +): StateApi { let - state = fromInitialState(initial); + state = createInitialState(initial); return { - setLoadPage(val: number) { - state.loadPage = val; - }, - compile(override?: Partial): ComponentState { return { ...state, - ...stateFromDataConveyor(dataConveyor), + ...extractStateFromDataConveyor(dataConveyor), ...override }; }, - reset() { - state = fromInitialState(initial); + reset(): void { + state = createInitialState(initial); dataConveyor.reset(); }, @@ -245,7 +183,13 @@ export function createState( }; } -export function fromInitialState(state: Partial): ComponentState { +/** + * 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): ComponentState { return { renderPage: 0, loadPage: 0, @@ -268,7 +212,11 @@ export function fromInitialState(state: Partial): ComponentState }; } -export function stateFromDataConveyor(conveyor: DataConveyor): Pick { +/** + * 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 { return { data: conveyor.data, lastLoadedData: conveyor.lastLoadedData, @@ -277,3 +225,90 @@ export function stateFromDataConveyor(conveyor: DataConveyor): Pick( + 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 i The index of the mounted item. + */ +export function createMountedItem(i: number): MountedItem { + return { + itemIndex: i, + childIndex: i, + props: { + 'data-index': i + }, + key: Object.cast(undefined), + item: 'section', + type: 'item', + node: test.expect.any(String) + }; +} + +/** + * Creates a simple object that matches the {@link MountedChild}` interface. + * @param i The index of the mounted child. + */ +export function createMountedSeparator(i: number): MountedChild { + return { + childIndex: i, + props: { + 'data-index': i + }, + key: Object.cast(undefined), + item: 'section', + type: 'separator', + node: test.expect.any(String) + }; +} + +/** + * 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( + 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): {i: number} { + return {i}; +} + +/** + * Filters emitter emit calls and removes unnecessary events. + * It only keeps component events, excluding observer-like events. + * + * @param emitCalls The array of emit calls. + * @param filterObserverEvents Whether to filter out observer events (default: true). + */ +export function filterEmitterCalls(emitCalls: unknown[][], filterObserverEvents: boolean = true): unknown[][] { + return emitCalls.filter(([event]) => Object.isString(event) && + Boolean(componentEvents[event]) && + (filterObserverEvents ? !(event in componentObserverLocalEvents) : true)); +} diff --git a/src/components/base/b-scrolly/test/api/helpers/interface.ts b/src/components/base/b-scrolly/test/api/helpers/interface.ts new file mode 100644 index 0000000000..45f4266d17 --- /dev/null +++ b/src/components/base/b-scrolly/test/api/helpers/interface.ts @@ -0,0 +1,127 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { ComponentItem, ComponentState, MountedChild, MountedItem } from 'components/base/b-scrolly/interface'; +import type { ScrollyComponentObject } from 'components/base/b-scrolly/test/api/component-object'; +import type { SpyObject } from 'tests/helpers/component-object/interface'; +import type { RequestInterceptor } from 'tests/helpers/providers/interceptor'; + +/** + * The interface defining the data conveyor for convenient data manipulation. + */ +export interface DataConveyor { + /** + * Adds a specified number of data items to the conveyor. + * + * @param count The number of data items to add. + * @returns An array containing the newly added data items. + */ + addData(count: number): DATA[]; + + /** + * Adds a specified number of mounted items to the conveyor. + * + * @param count The number of mounted items to add. + * @returns An array containing the newly added mounted items. + */ + addItems(count: number): MountedItem[]; + + /** + * Adds a specified number of mounted child items (separators) to the conveyor. + * + * @param count The number of mounted child items to add. + * @returns An array containing the newly added mounted child items. + */ + addSeparators(count: number): MountedChild[]; + + /** + * Adds an array of component items as mounted child items to the conveyor. + * + * @param child The array of component items to add as mounted child items. + * @returns The updated array of mounted child items. + */ + addChild(child: ComponentItem[]): MountedChild[]; + + /** + * Resets the data conveyor, clearing all data and items. + */ + reset(): void; + + /** + * Retrieves the array of data items in the conveyor. + */ + get data(): DATA[]; + + /** + * Retrieves the array of mounted child items in the conveyor. + */ + get childList(): MountedChild[]; + + /** + * Retrieves the array of last loaded data items in the conveyor. + */ + get lastLoadedData(): DATA[]; + + /** + * Retrieves the array of mounted items in the conveyor. + */ + get items(): MountedItem[]; +} + +/** + * The interface defining the API for manipulating the component state. + */ +export interface StateApi { + /** + * Compiles and returns the assembled component state object. + * + * @param override An object for overriding the current fields of the component state. + * @returns The compiled component state. + */ + compile(override?: Partial): ComponentState; + + /** + * Resets the component state to its initial values. + */ + reset(): void; + + /** + * The data conveyor used for managing data within the component state. + */ + data: DataConveyor; +} + +/** + * Helpers returned by the `createTestHelpers` function. + */ +export interface ScrollyTestHelpers { + /** + * The component object representing the `bScrolly` component. + */ + component: ScrollyComponentObject; + + /** + * The spy object for the `initLoad` function. + */ + initLoadSpy: SpyObject; + + /** + * The request interceptor provider. + */ + provider: RequestInterceptor; + + /** + * The state API object for convenient manipulation of the component's state fork. + */ + state: StateApi; +} + +export type DataItemCtor = (i: number) => COMPILED; +export type MountedItemCtor = (data: DATA, i: number) => MountedItem; +export type MountedSeparatorCtor = (data: DATA, i: number) => MountedChild; + diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts b/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts index a183a67baf..709558d49a 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts +++ b/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts @@ -130,7 +130,6 @@ test.describe('', () => { shouldStopRequestingData = await component.mockFn(() => false), shouldPerformDataRequest = await component.mockFn(() => false); - state.setLoadPage(1); state.data.addData(providerChunkSize); state.data.addItems(providerChunkSize); @@ -152,7 +151,8 @@ test.describe('', () => { childTillEnd: undefined, maxViewedItem: undefined, maxViewedChild: undefined, - isRequestsStopped: false + isRequestsStopped: false, + loadPage: 1 }), test.expect.any(Object) ] @@ -165,7 +165,8 @@ test.describe('', () => { childTillEnd: undefined, maxViewedItem: undefined, maxViewedChild: undefined, - isRequestsStopped: false + isRequestsStopped: false, + loadPage: 1 }), test.expect.any(Object) ] @@ -183,7 +184,6 @@ test.describe('', () => { shouldStopRequestingData = await component.mockFn(() => true), shouldPerformDataRequest = await component.mockFn(() => false); - state.setLoadPage(1); state.data.addData(providerChunkSize); state.data.addItems(providerChunkSize); @@ -205,7 +205,8 @@ test.describe('', () => { childTillEnd: undefined, maxViewedItem: undefined, maxViewedChild: undefined, - isRequestsStopped: false + isRequestsStopped: false, + loadPage: 1 }), test.expect.any(Object) ] From a1e6266e44dfddfb660a671b70653302de48d91d Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 15 Jun 2023 14:08:09 +0300 Subject: [PATCH 022/159] :art: --- .../test/api/component-object/index.ts | 110 +++++++++++------- tests/helpers/component-object/builder.ts | 16 ++- 2 files changed, 77 insertions(+), 49 deletions(-) diff --git a/src/components/base/b-scrolly/test/api/component-object/index.ts b/src/components/base/b-scrolly/test/api/component-object/index.ts index c9f9bb326b..7036e79f61 100644 --- a/src/components/base/b-scrolly/test/api/component-object/index.ts +++ b/src/components/base/b-scrolly/test/api/component-object/index.ts @@ -13,67 +13,86 @@ import { ComponentObject, Scroll } from 'tests/helpers'; import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import type { ComponentRefs, ComponentState } from 'components/base/b-scrolly/b-scrolly'; import type { SlotsStateObj } from 'components/base/b-scrolly/modules/slots'; + import { testStyles } from 'components/base/b-scrolly/test/api/component-object/styles'; +/** + * The component object API for testing the {@link bScrolly} component. + */ export class ScrollyComponentObject extends ComponentObject { - /** - * Container ref + * The locator for the container ref. */ readonly container: Locator; + /** + * The locator to select all children in the container ref. + */ readonly childList: Locator; /** - * @param page + * @param page The Playwright page instance. */ constructor(page: Page) { super(page, 'b-scrolly'); + this.container = this.node.locator(this.elSelector('container')); this.childList = this.container.locator('> *'); } + /** + * Overrides the build method to add test styles before building the component. + * + * @param args The arguments for the build method. + */ override async build(...args: Parameters['build']>): Promise> { await this.page.addStyleTag({content: testStyles}); return super.build(...args); } /** - * Calls a reload method of the component + * Calls the reload method of the component. */ reload(): Promise { return this.component.evaluate((ctx) => ctx.reload()); } /** - * Returns an internal component state + * Returns the internal component state. */ getComponentState(): Promise { return this.component.evaluate((ctx) => ctx.getComponentState()); } /** - * Returns a container child count + * Returns the count of children in the container. */ async getContainerChildCount(): Promise { return this.childList.count(); } /** - * Waits for container child count equals to N + * Waits for the container child count to be equal to N. + * Throws an error if there are more items in the child list than expected. + * + * @param n The expected child count. */ async waitForContainerChildCountEqualsTo(n: number): Promise { await this.childList.nth(n - 1).waitFor({state: 'attached'}); - if (await this.childList.count() > n) { - throw new Error('More than expected items'); + const count = await this.childList.count(); + + if (count > n) { + throw new Error(`Expected container to have exactly ${n} items, but got ${count}`); } } + /** + * Waits for the component lifecycle to be done. + */ async waitForLifecycleDone(): Promise { await this.component.evaluate((ctx) => { - const - state = ctx.getComponentState(); + const state = ctx.getComponentState(); if (state.isLifecycleDone) { return; @@ -84,28 +103,30 @@ export class ScrollyComponentObject extends ComponentObject { } /** - * Returns promise that will be resolved then the provided slot will hit `isVisible` state + * Waits for the provided slot to reach the specified visibility state. * - * @param slotName - * @param isVisible - * @param timeout + * @param slotName The name of the slot. + * @param isVisible The expected visibility state. + * @param timeout The timeout for waiting (optional). */ async waitForSlotState(slotName: keyof ComponentRefs, isVisible: boolean, timeout?: number): Promise { - const - slot = await this.node.locator(this.elSelector(slotName)); - + const slot = this.node.locator(this.elSelector(slotName)); await slot.waitFor({state: isVisible ? 'visible' : 'hidden', timeout}); } + /** + * Returns an object representing the state of all slots. + * Each slot is represented as [slotName: slotState], where `slotState=true` means the slot is visible. + */ async getSlotsState(): Promise> { const - container = await this.node.locator(this.elSelector('container')), - loader = await this.node.locator(this.elSelector('loader')), - tombstones = await this.node.locator(this.elSelector('tombstones')), - empty = await this.node.locator(this.elSelector('empty')), - retry = await this.node.locator(this.elSelector('retry')), - done = await this.node.locator(this.elSelector('done')), - renderNext = await this.node.locator(this.elSelector('renderNext')); + container = this.node.locator(this.elSelector('container')), + loader = this.node.locator(this.elSelector('loader')), + tombstones = this.node.locator(this.elSelector('tombstones')), + empty = this.node.locator(this.elSelector('empty')), + retry = this.node.locator(this.elSelector('retry')), + done = this.node.locator(this.elSelector('done')), + renderNext = this.node.locator(this.elSelector('renderNext')); return { container: await container.isVisible(), @@ -119,7 +140,7 @@ export class ScrollyComponentObject extends ComponentObject { } /** - * Scrolls page to the bottom + * Scrolls the page to the bottom. */ async scrollToBottom(): Promise { await Scroll.scrollToBottom(this.page); @@ -127,10 +148,10 @@ export class ScrollyComponentObject extends ComponentObject { } /** - * Adds default `iItems` props + * Adds default `itemProps` for pagination. */ - withPaginationItemProps(): this { - this.setProps({ + async withPaginationItemProps(): Promise { + await this.setProps({ item: 'section', itemProps: (item) => ({'data-index': item.i}) }); @@ -139,11 +160,12 @@ export class ScrollyComponentObject extends ComponentObject { } /** - * Adds a `requestProp` - * @param requestParams + * Adds a `requestProp` for pagination. + * + * @param requestParams The request parameters. */ - withRequestProp(requestParams: Dictionary = {}): this { - this.setProps({ + async withRequestPaginationProps(requestParams: Dictionary = {}): Promise { + await this.setProps({ request: { get: { chunkSize: 10, @@ -157,26 +179,26 @@ export class ScrollyComponentObject extends ComponentObject { } /** - * Adds a `Provider` into provider prop + * Adds a `Provider` into the provider prop for pagination. */ - withPaginationProvider(): this { - this.setProps({dataProvider: 'Provider'}); + async withPaginationProvider(): Promise { + await this.setProps({dataProvider: 'Provider'}); return this; } /** - * Calls every `pagination-like` default props setters: - * + * Calls all default pagination prop setters: * - `withPaginationProvider` * - `withPaginationItemProps` * - `withRequestProp` * - * @param requestParams + * @param requestParams The request parameters. */ - withDefaultPaginationProviderProps(requestParams: Dictionary = {}): this { - return this - .withPaginationProvider() - .withPaginationItemProps() - .withRequestProp(requestParams); + async withDefaultPaginationProviderProps(requestParams: Dictionary = {}): Promise { + await this.withPaginationProvider(); + await this.withPaginationItemProps(); + await this.withRequestPaginationProps(requestParams); + + return this; } } diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index 2a92371f0c..ae0c4779f3 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -167,7 +167,7 @@ export default class ComponentObjectBuilder { return el?.getProperty('component'); }); - await this.applyProps(); + await this.applyProps(this.props); return this; } @@ -178,8 +178,14 @@ export default class ComponentObjectBuilder { * * @param props */ - setProps(props: Dictionary): this { - Object.assign(this.props, props); + async setProps(props: Dictionary): Promise { + if (!this.isBuilded) { + Object.assign(this.props, props); + + } else { + await this.applyProps(props); + } + return this; } @@ -197,9 +203,9 @@ export default class ComponentObjectBuilder { /** * Applies the settled via `setProps` props to the component instance */ - async applyProps(): Promise { + async applyProps(props: Dictionary): Promise { const - {component, props} = this; + {component} = this; await component.evaluate((ctx, [props]) => Object.assign(ctx, props), [props]); return this; From 361c382848afae8d589e0e177f39a3e8b8983ff6 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 16 Jun 2023 11:38:12 +0300 Subject: [PATCH 023/159] :art: docs --- tests/helpers/component-object/builder.ts | 116 ++++++++++------- tests/helpers/mock/index.ts | 146 ++++++++++++++++++---- tests/helpers/mock/interface.ts | 48 ++++++- 3 files changed, 236 insertions(+), 74 deletions(-) diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index ae0c4779f3..123111a6b5 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -6,58 +6,70 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { JSHandle, Locator, Page } from 'playwright'; import path from 'upath'; +import type { JSHandle, Locator, Page } from 'playwright'; import { resolve } from '@pzlr/build-core'; -import type iBlock from 'components/super/i-block/i-block'; import { Component, DOM, Utils } from 'tests/helpers'; -export default class ComponentObjectBuilder { +import type iBlock from 'components/super/i-block/i-block'; +/** + * A class implementing the `ComponentObject` approach that encapsulates different + * interactions with a component from the client. + * + * This class provides a basic API for creating or selecting any component and interacting with it during tests. + * + * However, the recommended usage is to inherit from this class and implement a specific `ComponentObject` + * that encapsulates and enhances the client's interaction with component during the test. + */ +export default class ComponentObjectBuilder { /** - * Name of the component that will be created + * The name of the component to be rendered. */ readonly componentName: string; /** - * Component props + * The props of the component. */ readonly props: Dictionary = {}; /** - * Component children + * The children of the component. */ readonly children: VNodeChildren = {}; /** - * Component root element locator + * The locator for the root node of the component. */ readonly node: Locator; /** - * Component path to import via webpack require. - * By default plzr.resolve.blockSync will be used + * The path to the class used to build the component. + * By default, it generates the path using `plzr.resolve.blockSync(componentName)`. + * + * This field is used for setting up various mocks and spies. + * Setting the path is optional if you're not using the `spy` API. */ readonly componentClassImportPath: Nullable; /** - * {@link Page} + * The page on which the component is located. */ - protected page: Page; + readonly page: Page; /** - * Uniq component node id + * The unique ID of the component generated when the constructor is called. */ protected id: string; /** - * Stores component instance + * Stores a reference to the component's `JSHandle`. */ protected componentStore?: JSHandle; /** - * Short hand for generating element selectors + * A shorthand for generating selectors for component elements. * {@link DOM.elNameSelectorGenerator} * * @example @@ -70,7 +82,8 @@ export default class ComponentObjectBuilder { } /** - * Component link + * Public access to the reference of the component's `JSHandle`. + * @throws {@link Error} if trying to access a component that has not been built or picked */ get component(): JSHandle { if (!this.componentStore) { @@ -81,15 +94,15 @@ export default class ComponentObjectBuilder { } /** - * Returns if the `component` property are available. (`ComponentObject` are builded). + * Returns `true` if the component is built or picked. */ get isBuilded(): boolean { return Boolean(this.componentStore); } /** - * @param page - * @param componentName + * @param page - The page on which the component is located + * @param componentName - The name of the component to be rendered */ constructor(page: Page, componentName: string) { this.page = page; @@ -97,30 +110,34 @@ export default class ComponentObjectBuilder { this.id = `${this.componentName}_${Math.random().toString()}`; this.props = {'data-testid': this.id}; this.node = page.getByTestId(this.id); - this.componentClassImportPath = path.join(path.relative(`${process.cwd()}/src`, resolve.blockSync(this.componentName)!), `/${this.componentName}.ts`); + this.componentClassImportPath = path.join( + path.relative(`${process.cwd()}/src`, resolve.blockSync(this.componentName)!), + `/${this.componentName}.ts` + ); } /** - * Returns a component class + * Returns the base class of the component. */ async getComponentClass(): Promise COMPONENT>> { - const - {componentClassImportPath} = this; + const {componentClassImportPath} = this; if (componentClassImportPath == null) { throw new Error('Missing component path'); } - const - classModule = await Utils.import<{default: new () => COMPONENT}>(this.page, componentClassImportPath), - classInstance = await classModule.evaluateHandle((ctx) => ctx.default); + const classModule = await Utils.import<{default: new () => COMPONENT}>( + this.page, + componentClassImportPath); + + const classInstance = await classModule.evaluateHandle((ctx) => ctx.default); return classInstance; } /** - * Creates a `component` instance with the provided - * in constructor `componentName` and settled via `setProps` properties + * Renders the component with the previously set props and children + * using the `setProps` and `setChildren` methods. */ async build(): Promise> { this.componentStore = await Component.createComponent(this.page, this.componentName, { @@ -134,24 +151,24 @@ export default class ComponentObjectBuilder { } /** - * Picks the `Node` with the provided selector and extracts 'component' property - * that will be settled as `component` property of the `ComponentObject`. + * Picks the `Node` with the provided selector and extracts the `component` property, + * which will be assigned to the `component` property of the `ComponentObject`. * - * After this operation `ComponentObject` will be marked as builded and the `ComponentObject.component` - * property will be accessible. + * After this operation, the `ComponentObject` will be marked as built and the `ComponentObject.component` property + * will be accessible. * - * @param selectorOrLocator + * @param selectorOrLocator - The selector or locator for the component node */ async pick(selector: string): Promise; /** - * Extracts 'component' property from the provided locator - * that will be settled as `component` property of the `ComponentObject`. + * Extracts the `component` property from the provided locator, + * which will be assigned to the `component` property of the `ComponentObject`. * - * After this operation `ComponentObject` will be marked as builded and the `ComponentObject.component` - * property will be accessible. + * After this operation, the `ComponentObject` will be marked as built and the `ComponentObject.component` property + * will be accessible. * - * @param locator + * @param locator - The locator for the component node */ async pick(locator: Locator): Promise; @@ -159,8 +176,9 @@ export default class ComponentObjectBuilder { * @inheritdoc */ async pick(selectorOrLocator: string | Locator): Promise { - const - locator = Object.isString(selectorOrLocator) ? this.page.locator(selectorOrLocator) : selectorOrLocator; + const locator = Object.isString(selectorOrLocator) ? + this.page.locator(selectorOrLocator) : + selectorOrLocator; this.componentStore = await locator.elementHandle().then(async (el) => { await el?.evaluate((ctx, [id]) => ctx.setAttribute('data-test-id', id), [this.id]); @@ -173,15 +191,14 @@ export default class ComponentObjectBuilder { } /** - * Saves the provided props to store. - * After component will be created or picked the stored props will be settled + * Stores the provided props. + * The stored props will be assigned when the component is created or picked. * - * @param props + * @param props - The props to set */ async setProps(props: Dictionary): Promise { if (!this.isBuilded) { Object.assign(this.props, props); - } else { await this.applyProps(props); } @@ -190,10 +207,10 @@ export default class ComponentObjectBuilder { } /** - * Saves the provided child to store. - * After component will be created the stored children will be settled + * Stores the provided children. + * The stored children will be assigned when the component is created. * - * @param children + * @param children - The children to set */ setChildren(children: VNodeChildren): this { Object.assign(this.children, children); @@ -201,12 +218,17 @@ export default class ComponentObjectBuilder { } /** - * Applies the settled via `setProps` props to the component instance + * Applies the stored props to the component instance. + * @param props */ async applyProps(props: Dictionary): Promise { const {component} = this; + if (!this.isBuilded) { + return this.setProps(props); + } + await component.evaluate((ctx, [props]) => Object.assign(ctx, props), [props]); return this; } diff --git a/tests/helpers/mock/index.ts b/tests/helpers/mock/index.ts index 916ebccc91..59f1b69fdb 100644 --- a/tests/helpers/mock/index.ts +++ b/tests/helpers/mock/index.ts @@ -8,9 +8,17 @@ import type { ModuleMocker } from 'jest-mock'; import type { JSHandle, Page } from 'playwright'; -import type { SpyObject, SyncSpyObject } from 'tests/helpers/component-object/interface'; -import type { ExtractFromJSHandle } from 'tests/helpers/mock/interface'; +import { setSerializerAsMockFn } from 'core/prelude/test-env/components/json'; +import type { ExtractFromJSHandle, SpyExtractor, SpyObject } from 'tests/helpers/mock/interface'; + +/** + * Wraps an object as a spy object by adding additional properties for accessing spy information. + * + * @param agent The JSHandle representing the spy or mock function. + * @param obj The object to wrap as a spy object. + * @returns The wrapped object with spy properties. + */ export function wrapAsSpy(agent: JSHandle | ReturnType>, obj: T): T & SpyObject { Object.defineProperties(obj, { calls: { @@ -27,34 +35,34 @@ export function wrapAsSpy(agent: JSHandle agent.evaluate((ctx) => ctx.mock.results) - }, - - compile: { - value: async () => { - const [ - calls, - lastCall, - callsLength - ] = await agent.evaluate((ctx) => [ - ctx.mock.calls, - ctx.mock.lastCall, - ctx.mock.calls.length - ]); - - return { - calls, - lastCall, - callsLength, - compile: (obj).compile.bind(obj) - }; - } } }); return obj; } -export async function spy( +/** + * Creates a spy object. + * + * @param ctx The `JSHandle` to spy on. + * @param spyCtor The function that creates the spy. + * @param argsToCtor The arguments to pass to the spy constructor function. + * @returns A promise that resolves to the created spy object. + * + * @example + * ```typescript + * const ctx = ...; // JSHandle to spy on + * const spyCtor = (ctx) => jest.spy(ctx, 'prop'); // Spy constructor function + * const spy = await createSpy(ctx, spyCtor); + * + * // Access spy properties + * console.log(await spy.calls); + * console.log(await spy.callsLength); + * console.log(await spy.lastCall); + * console.log(await spy.results); + * ``` + */ +export async function createSpy( ctx: T, spyCtor: (ctx: ExtractFromJSHandle, ...args: ARGS) => ReturnType, ...argsToCtor: ARGS @@ -65,7 +73,95 @@ export async function spy( return wrapAsSpy(agent, {}); } -export async function createAndDisposeMock( +/** + * Retrieves an existing spy object from a `JSHandle`. + * + * @param ctx The `JSHandle` containing the spy object. + * @param spyExtractor The function to extract the spy object. + * @returns A promise that resolves to the spy object. + * + * @example + * ```typescript + * const component = await Component.createComponent(page, 'b-button', { + * attrs: { + * '@hook:beforeDataCreate': (ctx) => jest.spy(ctx.localEmitter, 'emit') + * } + * }); + * + * const spyExtractor = (ctx) => ctx.unsafe.localEmitter.emit; // Spy extractor function + * const spy = await getSpy(ctx, spyExtractor); + * + * // Access spy properties + * console.log(await spy.calls); + * console.log(await spy.callsLength); + * console.log(await spy.lastCall); + * console.log(await spy.results); + * ``` + */ +export async function getSpy( + ctx: T, + spyExtractor: SpyExtractor, []> +): Promise { + return createSpy(ctx, spyExtractor); +} + +/** + * Creates a mock function and injects it into a Page object. + * + * @param page The Page object to inject the mock function into. + * @param fn The mock function. + * @param args The arguments to pass to the function. + * @returns A promise that resolves to the mock function as a spy object. + * + * @example + * ```typescript + * const page = ...; // Page object + * const fn = () => {}; // The mock function + * const mockFn = await createMockFn(page, fn); + * + * // Access spy properties + * console.log(await mockFn.calls); + * console.log(await mockFn.callsLength); + * console.log(await mockFn.lastCall); + * console.log(await mockFn.results); + * ``` + */ +export async function createMockFn( + page: Page, + fn: (...args: any[]) => any, + ...args: any[] +): Promise { + const + {agent, id} = await injectMockIntoPage(page, fn, ...args); + + return setSerializerAsMockFn(agent, id); +} + +/** + * Injects a mock function into a Page object and returns the spy object. + * + * This function also returns the ID of the injected mock function, which is stored in `globalThis`. + * This binding allows the function to be found during object serialization within the page context. + * + * @param page The Page object to inject the mock function into. + * @param fn The mock function. + * @param args The arguments to pass to the function. + * @returns A promise that resolves to an object containing the spy object and the ID of the injected mock function. + * + * @example + * ```typescript + * const page = ...; // Page object + * const fn = () => {}; // The mock function + * const { agent, id } = await injectMockIntoPage(page, fn); + * + * // Access spy properties + * console.log(await agent.calls); + * console.log(await agent.callsLength); + * console.log(await agent.lastCall); + * console.log(await agent.results); + * ``` + */ +export async function injectMockIntoPage( page: Page, fn: (...args: any[]) => any, ...args: any[] diff --git a/tests/helpers/mock/interface.ts b/tests/helpers/mock/interface.ts index 91f18ce5b5..54464b6ff8 100644 --- a/tests/helpers/mock/interface.ts +++ b/tests/helpers/mock/interface.ts @@ -1,8 +1,52 @@ -import type { JSHandle } from '@playwright/test'; +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { JSHandle } from 'playwright'; import type { ModuleMocker } from 'jest-mock'; -export interface SpyCtor { +/** + * Represents a spy object with properties for accessing spy information. + */ +export interface SpyObject { + /** + * The array of arguments passed to the spy function on each call. + */ + readonly calls: Promise; + + /** + * The number of times the spy function has been called. + */ + readonly callsLength: Promise; + + /** + * The arguments of the most recent call to the spy function. + */ + readonly lastCall: Promise; + + /** + * The results of each call to the spy function. + */ + readonly results: Promise; +} + +/** + * Represents a function that extracts or creates a spy object from a `JSHandle`. + */ +export interface SpyExtractor { + /** + * Extracts or creates a spy object from a `JSHandle`. + * + * @param ctx The `JSHandle` containing the spy object. + */ (ctx: CTX, ...args: ARGS): ReturnType; } +/** + * Extracts the type from a `JSHandle`. + */ export type ExtractFromJSHandle = T extends JSHandle ? V : never; From d66909de1b2a1ecbbe6ceaa8edad8d82fa43bacc Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 16 Jun 2023 11:38:20 +0300 Subject: [PATCH 024/159] WIP --- tests/helpers/component-object/initializer.ts | 46 ------------------- 1 file changed, 46 deletions(-) delete mode 100644 tests/helpers/component-object/initializer.ts diff --git a/tests/helpers/component-object/initializer.ts b/tests/helpers/component-object/initializer.ts deleted file mode 100644 index 23ded78850..0000000000 --- a/tests/helpers/component-object/initializer.ts +++ /dev/null @@ -1,46 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type iBlock from 'components/super/i-block/i-block'; -import ComponentObjectBuilder from 'tests/helpers/component-object/builder'; - -interface _InitializerFunction { - (ctx: C, ...args: ARGS): unknown; -} - -export default class ComponentObjectInitializer extends ComponentObjectBuilder { - // TODO: Implement or destroy -} - -/** - * Почему такая странная схема с инициализацией: - * - * Все проблемы из-за за замыканий, - * допустим у нас метод который на вход принимает путь на который надо установить spy и хук на который это сделать - * - * ```typescript - * async spyOn(path: string, spyOptions: {hook: string}): Promise - * ``` - * - * Пытаемся сделать реализацию: - * - * ```typescript - * async spyOn(path: string, spyOptions: {hook: string}): Promise { - * this.setProps({ - * [`@componentHook:${hook}`]: (ctx) => jest.spy(ctx, path) - * }) - * } - * ``` - * - * Ииии падаем с ошибкой во время выполнения Reference error path is not defined так как функция - * будет передана в браузера - */ - - // Expose function for pushing messages to the Node.js script. -// const log = []; -// await page.exposeFunction('logCall', msg => log.push(msg)); From 85501833b65fdb947dba6d142c61ea2bfbf8500d Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 16 Jun 2023 12:22:35 +0300 Subject: [PATCH 025/159] :art: --- tests/helpers/component-object/interface.ts | 32 ++--- tests/helpers/component-object/mock.ts | 132 ++++++++++---------- 2 files changed, 78 insertions(+), 86 deletions(-) diff --git a/tests/helpers/component-object/interface.ts b/tests/helpers/component-object/interface.ts index 7bc26178c6..817c058c12 100644 --- a/tests/helpers/component-object/interface.ts +++ b/tests/helpers/component-object/interface.ts @@ -1,23 +1,17 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + export interface SpyOptions { /** - * If `true` then the spy will be settled to the prototype method. - * The spy will be settled before component creating. - */ + * If set to true, the spy will be installed on the prototype of the component class. + * + * Setting this option is useful for methods such as `initLoad` because they + * are called not from an instance of the component, but using the `call` method from the class prototype. + */ proto?: boolean; } - -export interface CompileSpyObject { - /** - * Returns a snapshot of the current spy object state. - */ - compile(): Promise; -} - -export interface SpyObject extends CompileSpyObject { - get calls(): Promise; - get callsLength(): Promise; - get results(): Promise; - get lastCall(): Promise; -} - -export type SyncSpyObject = {[P in keyof SpyObject]: Awaited} & CompileSpyObject; diff --git a/tests/helpers/component-object/mock.ts b/tests/helpers/component-object/mock.ts index a36096c41f..561dae36ca 100644 --- a/tests/helpers/component-object/mock.ts +++ b/tests/helpers/component-object/mock.ts @@ -5,129 +5,127 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ - import type iBlock from 'components/super/i-block/i-block'; -import type { SpyObject, SpyOptions } from 'tests/helpers/component-object/interface'; -import { createAndDisposeMock, spy } from 'tests/helpers/mock'; -import { setSerializerAsMockFn } from 'core/prelude/test-env/components/json'; -import ComponentObjectInitializer from 'tests/helpers/component-object/initializer'; -import type { SpyCtor } from 'tests/helpers/mock/interface'; +import ComponentObjectBuilder from 'tests/helpers/component-object/builder'; +import { createSpy, createMockFn, getSpy } from 'tests/helpers/mock'; -export default class ComponentObjectMock extends ComponentObjectInitializer { +import type { SpyOptions } from 'tests/helpers/component-object/interface'; +import type { SpyExtractor, SpyObject } from 'tests/helpers/mock/interface'; +/** + * The `ComponentObjectMock` class extends the `ComponentObjectBuilder` class + * and provides additional methods for creating spies and mock functions. + * + * It is used for testing components in a mock environment. + */ +export default class ComponentObjectMock extends ComponentObjectBuilder { /** - * Creates a spy for the specified path + * Creates a spy to observe calls to the specified method. * - * @param path - * @param spyOptions + * @param path - The path to the method relative to the context (component). + * The {@link Object.get} method is used for searching, so you can use a complex path with separators. * - * Sets a spy to the `component instance`: + * @param spyOptions - Options for setting up the spy. + * @param spyOptions.proto - If set to `true`, the spy will be installed on the prototype of the component class. + * In this case, you don't need to add `prototype` to the `path`; it will be added automatically. + * + * @returns A promise that resolves to the spy object. * * @example * ```typescript - * const builder = new ComponentBuilder(page, 'b-component'); - * builder.spyOn('initLoad'); - * await builder.build(); - * * const - * calls = await builder.spies.initLoad.calls, - * lastCall = await builder.spies.initLoad.lastCall; - * ``` + * component = new ComponentObject(page, 'b-scrolly'), + * spy = await component.spyOn('initLoad', {proto: true}); // Installs a spy on the prototype of the component class * - * Sets a spy to the `prototype`: + * await component.build(); + * console.log(await spy.calls); + * ``` * * @example * ```typescript - * const builder = new ComponentBuilder(page, 'b-component'); - * builder.spyOn('initLoad', {proto: true}); - * await builder.build(); + * const component = new ComponentObject(page, 'b-scrolly'); + * const spy = await component.spyOn('someModule.someMethod'); * - * const - * calls = await builder.spies.initLoad.calls, - * lastCall = await builder.spies.initLoad.lastCall; + * await component.build(); + * console.log(await spy.calls); * ``` */ async spyOn(path: string, spyOptions?: SpyOptions): Promise { - const - evaluateArgs = [path, spyOptions], - ctx = spyOptions?.proto ? await this.getComponentClass() : this.component; + const evaluateArgs = [path, spyOptions]; + const ctx = spyOptions?.proto ? await this.getComponentClass() : this.component; - const instance = await spy(ctx, (ctx, [path, spyOptions]) => { + const instance = await createSpy(ctx, (ctx, [path, spyOptions]) => { if (spyOptions?.proto === true) { path = `prototype.${path}`; } - const - pathArray = path.split('.'), - method = pathArray.pop(); + const pathArray = path.split('.'); + const method = pathArray.pop(); - const - obj = pathArray.length >= 1 ? Object.get(ctx, pathArray.join('.')) : ctx; + const obj = pathArray.length >= 1 ? Object.get(ctx, pathArray.join('.')) : ctx; if (!obj) { throw new ReferenceError(`Cannot find object by the provided path: ${path}`); } - return jest.spy( - obj, - method - ); + return jest.spy(obj, method); }, evaluateArgs); return instance; } - async getSpy(spyFinder: SpyCtor): Promise { - return spy(this.component, spyFinder); - } - /** - * Creates a mock function - * @param paths + * Extracts the spy using the provided function. The provided function should return a reference to the spy. + * + * @param spyExtractor - The function that extracts the spy. + * @returns A promise that resolves to the spy object. * * @example * ```typescript - * const builder = new ComponentBuilder(page, 'b-component'); - * builder.mock({initLoad: builder.mockFn()}); - * await builder.build(); + * await component.setProps({ + * '@hook:beforeDataCreate': (ctx) => jest.spy(ctx.localEmitter, 'emit') + * }); + * + * await component.build(); * * const - * calls = await builder.mocks.initLoad.calls, - * lastCall = await builder.mocks.initLoad.lastCall; + * spy = await component.getSpy((ctx) => ctx.localEmitter.emit); * - * await builder.mocks.initLoad.implementation(() => 123); - * const result = await builder.component.evaluate((ctx) => ctx.initLoad()); - * console.log(result) // 123; + * console.log(await spy.calls); * ``` + */ + async getSpy(spyExtractor: SpyExtractor): Promise { + return getSpy(this.component, spyExtractor); + } + + /** + * Creates a mock function. + * + * @param fn - The mock function. + * @param args - Arguments to pass to the mock function. * - * Mock the prototype function + * @returns A promise that resolves to the mock function object. * * @example * ```typescript - * const builder = new ComponentBuilder(page, 'b-component'); + * const + * component = new ComponentObject(page, 'b-scrolly'), + * shouldStopRequestingData = await component.mockFn(() => false); * - * builder.mock({ - * initLoad: { - * fn: builder.mockFn(), - * proto: true - * } + * await component.setProps({ + * shouldStopRequestingData * }); * - * await builder.build(); + * await component.build(); + * console.log(await shouldStopRequestingData.calls); * ``` - * - * > Notice that the implementation will be provided into browser, - * this imposes some restrictions, such as not being able to use a closure */ async mockFn< FN extends (...args: any[]) => any = (...args: any[]) => any >(fn?: FN, ...args: any[]): Promise { fn ??= Object.cast(() => undefined); - const - {agent, id} = await createAndDisposeMock(this.page, fn!, ...args); - - return setSerializerAsMockFn(agent, id); + return createMockFn(this.page, fn!, ...args); } } From 49679fc7fdad7bd06577e9fe094599acdd8513be Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 16 Jun 2023 12:31:51 +0300 Subject: [PATCH 026/159] :art: --- .../base/b-scrolly/test/api/helpers/index.ts | 30 +++++++++---------- .../b-scrolly/test/api/helpers/interface.ts | 6 +++- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/components/base/b-scrolly/test/api/helpers/index.ts b/src/components/base/b-scrolly/test/api/helpers/index.ts index 9a7a5d323e..88d00fc8fa 100644 --- a/src/components/base/b-scrolly/test/api/helpers/index.ts +++ b/src/components/base/b-scrolly/test/api/helpers/index.ts @@ -14,7 +14,7 @@ import { paginationHandler } from 'tests/helpers/providers/pagination'; import { ScrollyComponentObject } from 'components/base/b-scrolly/test/api/component-object'; import { RequestInterceptor } from 'tests/helpers/providers/interceptor'; import { componentEvents, componentObserverLocalEvents } from 'components/base/b-scrolly/const'; -import type { DataConveyor, DataItemCtor, MountedItemCtor, StateApi, ScrollyTestHelpers, MountedSeparatorCtor } from 'components/base/b-scrolly/test/api/helpers/interface'; +import type { DataConveyor, DataItemCtor, MountedItemCtor, StateApi, ScrollyTestHelpers, MountedSeparatorCtor, IndexedObj } from 'components/base/b-scrolly/test/api/helpers/interface'; export * from 'components/base/b-scrolly/test/api/component-object'; @@ -53,10 +53,10 @@ export async function createTestHelpers(page: Page): Promise * @param separatorCtor The constructor function for mounted separators. * @param mountedCtor The constructor function for mounted items. */ -export function createDataConveyor( - itemsCtor: DataItemCtor, - separatorCtor: MountedSeparatorCtor, - mountedCtor: MountedItemCtor +export function createDataConveyor( + itemsCtor: DataItemCtor, + separatorCtor: MountedSeparatorCtor, + mountedCtor: MountedItemCtor ): DataConveyor { let data = [], @@ -243,14 +243,14 @@ export function createFromData( /** * Creates a simple object that matches the {@link MountedItem} interface. - * @param i The index of the mounted item. + * @param data The object with index of the mounted item. */ -export function createMountedItem(i: number): MountedItem { +export function createMountedItem(data: IndexedObj): MountedItem { return { - itemIndex: i, - childIndex: i, + itemIndex: data.i, + childIndex: data.i, props: { - 'data-index': i + 'data-index': data.i }, key: Object.cast(undefined), item: 'section', @@ -261,13 +261,13 @@ export function createMountedItem(i: number): MountedItem { /** * Creates a simple object that matches the {@link MountedChild}` interface. - * @param i The index of the mounted child. + * @param data The object with index of the mounted child. */ -export function createMountedSeparator(i: number): MountedChild { +export function createMountedSeparator(data: IndexedObj): MountedChild { return { - childIndex: i, + childIndex: data.i, props: { - 'data-index': i + 'data-index': data.i }, key: Object.cast(undefined), item: 'section', @@ -296,7 +296,7 @@ export function createChunk( * Creates a simple indexed object. * @param i The index of the object. */ -export function createIndexedObj(i: number): {i: number} { +export function createIndexedObj(i: number): IndexedObj { return {i}; } diff --git a/src/components/base/b-scrolly/test/api/helpers/interface.ts b/src/components/base/b-scrolly/test/api/helpers/interface.ts index 45f4266d17..34795da959 100644 --- a/src/components/base/b-scrolly/test/api/helpers/interface.ts +++ b/src/components/base/b-scrolly/test/api/helpers/interface.ts @@ -8,7 +8,7 @@ import type { ComponentItem, ComponentState, MountedChild, MountedItem } from 'components/base/b-scrolly/interface'; import type { ScrollyComponentObject } from 'components/base/b-scrolly/test/api/component-object'; -import type { SpyObject } from 'tests/helpers/component-object/interface'; +import type { SpyObject } from 'tests/helpers/mock/interface'; import type { RequestInterceptor } from 'tests/helpers/providers/interceptor'; /** @@ -121,6 +121,10 @@ export interface ScrollyTestHelpers { state: StateApi; } +export interface IndexedObj { + i: number; +} + export type DataItemCtor = (i: number) => COMPILED; export type MountedItemCtor = (data: DATA, i: number) => MountedItem; export type MountedSeparatorCtor = (data: DATA, i: number) => MountedChild; From 6db51aef90690f9f9cff8577e82a516403f81420 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 16 Jun 2023 13:06:40 +0300 Subject: [PATCH 027/159] :art: --- src/components/base/b-scrolly/b-scrolly.ts | 12 ++-- src/components/base/b-scrolly/const.ts | 64 +++---------------- .../base/b-scrolly/interface/common.ts | 15 +++-- .../base/b-scrolly/modules/juggler/index.ts | 8 +-- .../b-scrolly/modules/presets/chunk-size.ts | 14 ++-- .../test/unit/functional/state/index.ts | 4 +- .../initialization/initialization.ts | 4 +- 7 files changed, 38 insertions(+), 83 deletions(-) diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts index 89eb86b10e..f8a1fcc618 100644 --- a/src/components/base/b-scrolly/b-scrolly.ts +++ b/src/components/base/b-scrolly/b-scrolly.ts @@ -29,7 +29,7 @@ import type { ComponentItemFactory, ComponentItemType, ComponentStrategy, - CanPerformRenderResult + RenderGuardResult } from 'components/base/b-scrolly/interface'; @@ -37,7 +37,7 @@ import { componentRenderStrategy, componentDataLocalEvents, - defaultProps, + defaultShouldProps, componentLocalEvents, componentItemType, componentStrategy @@ -201,7 +201,7 @@ export default class bScrolly extends iData implements iItems { */ @prop({ type: Function, - default: defaultProps.shouldStopRequestingData + default: defaultShouldProps.shouldStopRequestingData }) readonly shouldStopRequestingData!: ShouldPerform; @@ -212,7 +212,7 @@ export default class bScrolly extends iData implements iItems { */ @prop({ type: Function, - default: defaultProps.shouldPerformDataRequest + default: defaultShouldProps.shouldPerformDataRequest }) readonly shouldPerformDataRequest!: ShouldPerform; @@ -222,7 +222,7 @@ export default class bScrolly extends iData implements iItems { * * This function asks the client whether rendering can be performed. The client responds with an object * indicating whether rendering is allowed or the reason for denial. The client's response should be an object - * of type {@link CanPerformRenderResult}. + * of type {@link RenderGuardResult}. * * Based on the result of this function, the component takes appropriate actions. For example, * it may load data if it is not sufficient for rendering, or perform rendering if all conditions are met. @@ -240,7 +240,7 @@ export default class bScrolly extends iData implements iItems { return chunkSizePreset.renderGuard(state, ctx, ctx.chunkSize); } }) - readonly renderGuard!: ShouldPerform; + readonly renderGuard!: ShouldPerform; /** * This function is called in the `renderGuard` after other checks are completed. diff --git a/src/components/base/b-scrolly/const.ts b/src/components/base/b-scrolly/const.ts index 8593af32af..a522dcabad 100644 --- a/src/components/base/b-scrolly/const.ts +++ b/src/components/base/b-scrolly/const.ts @@ -7,7 +7,7 @@ */ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { ComponentDataLocalEvents, ComponentItemType, ComponentLifecycleEvents, ComponentObserverLocalEvents, ComponentRenderLocalEvents, ComponentRenderStrategy, ComponentState, ComponentStrategy } from 'components/base/b-scrolly/interface'; +import type { ComponentDataLocalEvents, ComponentEvents, ComponentItemType, ComponentLifecycleEvents, ComponentObserverLocalEvents, ComponentRenderLocalEvents, ComponentRenderStrategy, ComponentState, ComponentStrategy, RenderGuardRejectionReason } from 'components/base/b-scrolly/interface'; /** * {@link ComponentRenderStrategy} @@ -47,52 +47,22 @@ export const componentLocalEvents: ComponentLifecycleEvents = { }; /** - * Component rendering events. + * {@link ComponentRenderLocalEvents} */ export const componentRenderLocalEvents: ComponentRenderLocalEvents = { - /** - * Rendering of items has started. - */ renderStart: 'renderStart', - - /** - * Rendering of items has finished. - */ renderDone: 'renderDone', - - /** - * Rendering of items has started with the render engine. - */ renderEngineStart: 'renderEngineStart', - - /** - * Rendering of items has finished with the render engine. - */ renderEngineDone: 'renderEngineDone', - - /** - * DOM node insertion has started. - */ domInsertStart: 'domInsertStart', - - /** - * DOM node insertion has finished. - */ domInsertDone: 'domInsertDone' }; /** - * Events of the element observer. + * {@link ComponentObserverLocalEvents} */ export const componentObserverLocalEvents: ComponentObserverLocalEvents = { - /** - * The element has entered the viewport. - */ elementEnter: 'elementEnter', - - /** - * The element has exited the viewport. - */ elementOut: 'elementOut' }; @@ -104,27 +74,12 @@ export const componentEvents = { }; /** - * Reasons for rejecting a render operation. + * {@link RenderGuardRejectionReason} */ -export const canPerformRenderRejectionReason = { - /** - * Insufficient data to perform a render (e.g., `data.length` is 5 and `chunkSize` is 12). - */ +export const renderGuardRejectionReason: RenderGuardRejectionReason = { notEnoughData: 'notEnoughData', - - /** - * No data available to perform a render (e.g., `data.length` is 0). - */ noData: 'noData', - - /** - * All rendering operations have been completed. - */ done: 'done', - - /** - * The client returns `false` in `shouldPerformDataRender`. - */ noPermission: 'noPermission' }; @@ -136,23 +91,20 @@ export const componentItemType: ComponentItemType = { separator: 'separator' }; -/** - * `should-like` свойства компонента по умолчанию. - */ -export const defaultProps = { +export const defaultShouldProps = { /** {@link bScrolly.shouldStopRequestingData} */ shouldStopRequestingData: (state: ComponentState, _ctx: bScrolly): boolean => { const isLastRequestNotEmpty = () => state.lastLoadedData.length > 0; return !isLastRequestNotEmpty(); }, - /** {@link bScrolly.shouldPerformRequest} */ + /** {@link bScrolly.shouldPerformDataRequest} */ shouldPerformDataRequest: (state: ComponentState, _ctx: bScrolly): boolean => { const isLastRequestNotEmpty = () => state.lastLoadedData.length > 0; return isLastRequestNotEmpty(); }, - /** {@link bScrolly.shouldPerformRender} */ + /** {@link bScrolly.shouldPerformDataRender} */ shouldPerformDataRender: (_state: ComponentState, _ctx: bScrolly): boolean => false }; diff --git a/src/components/base/b-scrolly/interface/common.ts b/src/components/base/b-scrolly/interface/common.ts index ae8452f056..00ae710da3 100644 --- a/src/components/base/b-scrolly/interface/common.ts +++ b/src/components/base/b-scrolly/interface/common.ts @@ -13,23 +13,26 @@ import type { ComponentState } from 'components/base/b-scrolly/interface/compone * Interface representing the response of the client to the `renderGuard` method for rendering requests. * * To grant permission for rendering, the response object should have the following structure: + * * ```typescript - * const canPerform: CanPerformRenderResult = { + * const canPerform: RenderGuardResult = { * result: true * } * ``` * * To deny rendering, the response object should have the following structure: + * * ```typescript - * const canPerform: CanPerformRenderResult = { + * const canPerform: RenderGuardResult = { * result: false, * reason: 'notEnoughData' * } * ``` * - * Depending on the reason, specific actions will be taken based on the implementation of the `renderGuard`. + * Based on the result of this function, the component takes appropriate actions. For example, + * it may load data if it is not sufficient for rendering, or perform rendering if all conditions are met. */ -export interface CanPerformRenderResult { +export interface RenderGuardResult { /** * If `true`, rendering is permitted; if `false`, rendering is denied. */ @@ -38,13 +41,13 @@ export interface CanPerformRenderResult { /** * The reason for rejecting the rendering request. */ - reason?: keyof CanPerformRenderRejectionReason; + reason?: keyof RenderGuardRejectionReason; } /** * Reasons for rejecting a render operation. */ -export interface CanPerformRenderRejectionReason { +export interface RenderGuardRejectionReason { /** * Insufficient data to perform a render (e.g., `data.length` is 5 and `chunkSize` is 12). */ diff --git a/src/components/base/b-scrolly/modules/juggler/index.ts b/src/components/base/b-scrolly/modules/juggler/index.ts index 7ff71c7678..57cb25cd74 100644 --- a/src/components/base/b-scrolly/modules/juggler/index.ts +++ b/src/components/base/b-scrolly/modules/juggler/index.ts @@ -12,7 +12,7 @@ import Friend from 'components/friends/friend'; import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import type { ComponentItem } from 'components/base/b-scrolly/b-scrolly'; -import { canPerformRenderRejectionReason, componentDataLocalEvents, componentItemType, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; +import { renderGuardRejectionReason, componentDataLocalEvents, componentItemType, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; import type { MountedChild, MountedItem } from 'components/base/b-scrolly/interface'; import { isItem } from 'components/base/b-scrolly/modules/helpers'; @@ -142,12 +142,12 @@ export class Juggler extends Friend { return this.performRender(); } - if (reason === canPerformRenderRejectionReason.done) { + if (reason === renderGuardRejectionReason.done) { ctx.componentInternalState.setIsLifecycleDone(true); return; } - if (reason === canPerformRenderRejectionReason.noData) { + if (reason === renderGuardRejectionReason.noData) { if (state.isRequestsStopped) { return; } @@ -157,7 +157,7 @@ export class Juggler extends Friend { } } - if (reason === canPerformRenderRejectionReason.notEnoughData) { + if (reason === renderGuardRejectionReason.notEnoughData) { if (state.isRequestsStopped) { this.performRender(); diff --git a/src/components/base/b-scrolly/modules/presets/chunk-size.ts b/src/components/base/b-scrolly/modules/presets/chunk-size.ts index 789b9f175f..21a52b138b 100644 --- a/src/components/base/b-scrolly/modules/presets/chunk-size.ts +++ b/src/components/base/b-scrolly/modules/presets/chunk-size.ts @@ -8,8 +8,8 @@ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import { canPerformRenderRejectionReason } from 'components/base/b-scrolly/const'; -import type { CanPerformRenderResult, ComponentState } from 'components/base/b-scrolly/interface'; +import { renderGuardRejectionReason } from 'components/base/b-scrolly/const'; +import type { RenderGuardResult, ComponentState } from 'components/base/b-scrolly/interface'; /** * Returns the next slice of data that should be rendered. @@ -41,27 +41,27 @@ export const chunkSizePreset = { state: ComponentState, ctx: bScrolly, chunkSize: number - ): CanPerformRenderResult { + ): RenderGuardResult { const dataSlice = getNextDataSlice(state, chunkSize); if (dataSlice.length === 0) { if (state.isRequestsStopped) { return { result: false, - reason: canPerformRenderRejectionReason.done + reason: renderGuardRejectionReason.done }; } return { result: false, - reason: canPerformRenderRejectionReason.noData + reason: renderGuardRejectionReason.noData }; } if (dataSlice.length < chunkSize) { return { result: false, - reason: canPerformRenderRejectionReason.notEnoughData + reason: renderGuardRejectionReason.notEnoughData }; } @@ -75,7 +75,7 @@ export const chunkSizePreset = { return { result: clientResponse == null ? true : clientResponse, - reason: clientResponse === false ? canPerformRenderRejectionReason.noPermission : undefined + reason: clientResponse === false ? renderGuardRejectionReason.noPermission : undefined }; }, diff --git a/src/components/base/b-scrolly/test/unit/functional/state/index.ts b/src/components/base/b-scrolly/test/unit/functional/state/index.ts index 52584162a5..efaa2a3a3f 100644 --- a/src/components/base/b-scrolly/test/unit/functional/state/index.ts +++ b/src/components/base/b-scrolly/test/unit/functional/state/index.ts @@ -14,7 +14,7 @@ import test from 'tests/config/unit/test'; import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import { defaultProps } from 'components/base/b-scrolly/const'; +import { defaultShouldProps } from 'components/base/b-scrolly/const'; import type { ComponentItem, ShouldPerform } from 'components/base/b-scrolly/b-scrolly'; test.describe(' state', () => { @@ -65,7 +65,7 @@ test.describe(' state', () => { providerChunkSize = chunkSize / 2; const - shouldStopRequestingData = (defaultProps.shouldStopRequestingData), + shouldStopRequestingData = (defaultShouldProps.shouldStopRequestingData), shouldPerformDataRequest = (({isInitialLoading, itemsTillEnd, isLastEmpty}) => isInitialLoading || (itemsTillEnd === 0 && !isLastEmpty)), shouldPerformDataRender = (({isInitialRender, itemsTillEnd}) => diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts b/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts index 709558d49a..89ea709c87 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts +++ b/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts @@ -12,7 +12,7 @@ import test from 'tests/config/unit/test'; -import { defaultProps } from 'components/base/b-scrolly/const'; +import { defaultShouldProps } from 'components/base/b-scrolly/const'; import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; test.describe('', () => { @@ -50,7 +50,7 @@ test.describe('', () => { const shouldStopRequestingData = await component.mockFn(() => false), - shouldPerformDataRequest = await component.mockFn(defaultProps.shouldPerformDataRequest); + shouldPerformDataRequest = await component.mockFn(defaultShouldProps.shouldPerformDataRequest); const firstDataChunk = state.data.addData(providerChunkSize), From a34263ec0986f6def081ae7814adfef1f000eadc Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 16 Jun 2023 13:37:20 +0300 Subject: [PATCH 028/159] WIP --- index.d.ts | 8 ++++---- src/components/base/b-scrolly/b-scrolly.ts | 1 + .../base/b-scrolly/test/unit/functional/emitter/index.ts | 8 ++++---- src/core/prelude/test-env/mock/index.ts | 2 +- tests/helpers/component-object/mock.ts | 4 ++-- tests/helpers/mock/index.ts | 6 +++--- 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/index.d.ts b/index.d.ts index 3ceaf306e8..ad11abd203 100644 --- a/index.d.ts +++ b/index.d.ts @@ -119,11 +119,11 @@ declare var importModule: (path: string) => any, /** - * Jest mock API for testing env + * Jest mock API for test environment. */ - jest: { + jestMock: { /** - * Wrapper for jest `spyOn` function + * Wrapper for jest `spyOn` function. * * {@link ModuleMocker.spyOn} * @see https://jestjs.io/docs/mock-functions @@ -131,7 +131,7 @@ declare var spy: import('jest-mock').ModuleMocker['spyOn']; /** - * Wrapper for jest `fn` function + * Wrapper for jest `fn` function. * * {@link ModuleMocker.fn} * @see https://jestjs.io/docs/mock-functions diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts index f8a1fcc618..dec468e4d6 100644 --- a/src/components/base/b-scrolly/b-scrolly.ts +++ b/src/components/base/b-scrolly/b-scrolly.ts @@ -251,6 +251,7 @@ export default class bScrolly extends iData implements iItems { * For example, if we want to render the next data chunk only when the client * has seen all the main components, we can implement the following function: * + * @example * ```typescript * const shouldPerformDataRender = (state) => { * return state.isInitialRender || state.itemsTillEnd === 0; diff --git a/src/components/base/b-scrolly/test/unit/functional/emitter/index.ts b/src/components/base/b-scrolly/test/unit/functional/emitter/index.ts index 347e2b873e..b019468531 100644 --- a/src/components/base/b-scrolly/test/unit/functional/emitter/index.ts +++ b/src/components/base/b-scrolly/test/unit/functional/emitter/index.ts @@ -37,7 +37,7 @@ test.describe(' emitter', () => { await component.setProps({ chunkSize, shouldStopRequestingData: () => true, - '@hook:beforeDataCreate': (ctx) => jest.spy(ctx.localEmitter, 'emit') + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx.localEmitter, 'emit') }); await component.withDefaultPaginationProviderProps({chunkSize}); @@ -79,7 +79,7 @@ test.describe(' emitter', () => { chunkSize, shouldPerformDataRequest: () => true, shouldStopRequestingData: ({lastLoadedData}) => lastLoadedData.length === 0, - '@hook:beforeDataCreate': (ctx) => jest.spy(ctx.localEmitter, 'emit') + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx.localEmitter, 'emit') }); await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); @@ -126,7 +126,7 @@ test.describe(' emitter', () => { chunkSize, shouldPerformDataRequest: () => true, shouldStopRequestingData: ({lastLoadedData}) => lastLoadedData.length === 0, - '@hook:beforeDataCreate': (ctx) => jest.spy(ctx.localEmitter, 'emit') + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx.localEmitter, 'emit') }); await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); @@ -165,7 +165,7 @@ test.describe(' emitter', () => { await component.setProps({ chunkSize, shouldStopRequestingData: () => true, - '@hook:beforeDataCreate': (ctx) => jest.spy(ctx.localEmitter, 'emit') + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx.localEmitter, 'emit') }); await component.withDefaultPaginationProviderProps({chunkSize}); diff --git a/src/core/prelude/test-env/mock/index.ts b/src/core/prelude/test-env/mock/index.ts index 7029b6a334..36d8a576e5 100644 --- a/src/core/prelude/test-env/mock/index.ts +++ b/src/core/prelude/test-env/mock/index.ts @@ -15,7 +15,7 @@ import { ModuleMocker } from 'jest-mock'; let globalApi: ModuleMocker; -globalThis.jest = { +globalThis.jestMock = { /** * {@link ModuleMocker.spyOn} * @see https://jestjs.io/docs/mock-functions diff --git a/tests/helpers/component-object/mock.ts b/tests/helpers/component-object/mock.ts index 561dae36ca..dd152d2de7 100644 --- a/tests/helpers/component-object/mock.ts +++ b/tests/helpers/component-object/mock.ts @@ -69,7 +69,7 @@ export default class ComponentObjectMock extends Compo throw new ReferenceError(`Cannot find object by the provided path: ${path}`); } - return jest.spy(obj, method); + return jestMock.spy(obj, method); }, evaluateArgs); return instance; @@ -84,7 +84,7 @@ export default class ComponentObjectMock extends Compo * @example * ```typescript * await component.setProps({ - * '@hook:beforeDataCreate': (ctx) => jest.spy(ctx.localEmitter, 'emit') + * '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx.localEmitter, 'emit') * }); * * await component.build(); diff --git a/tests/helpers/mock/index.ts b/tests/helpers/mock/index.ts index 59f1b69fdb..60310659ea 100644 --- a/tests/helpers/mock/index.ts +++ b/tests/helpers/mock/index.ts @@ -52,7 +52,7 @@ export function wrapAsSpy(agent: JSHandle jest.spy(ctx, 'prop'); // Spy constructor function + * const spyCtor = (ctx) => jestMock.spy(ctx, 'prop'); // Spy constructor function * const spy = await createSpy(ctx, spyCtor); * * // Access spy properties @@ -84,7 +84,7 @@ export async function createSpy( * ```typescript * const component = await Component.createComponent(page, 'b-button', { * attrs: { - * '@hook:beforeDataCreate': (ctx) => jest.spy(ctx.localEmitter, 'emit') + * '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx.localEmitter, 'emit') * } * }); * @@ -171,7 +171,7 @@ export async function injectMockIntoPage( const agent = await page.evaluateHandle(([tmpFn, fnString, args]) => // eslint-disable-next-line no-new-func - globalThis[tmpFn] = jest.mock((...fnArgs) => Object.cast(new Function(`return ${fnString}`)()(...fnArgs, ...args))), [tmpFn, fn.toString(), args]); + globalThis[tmpFn] = jestMock.mock((...fnArgs) => Object.cast(new Function(`return ${fnString}`)()(...fnArgs, ...args))), [tmpFn, fn.toString(), args]); return {agent: wrapAsSpy(agent, {}), id: tmpFn}; } From 169ce84754527b86c44ff87fd6d0661953e4859e Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 19 Jun 2023 13:58:40 +0300 Subject: [PATCH 029/159] WIP --- index.d.ts | 4 - src/components/base/b-scrolly/b-scrolly.ts | 18 ++- src/components/base/b-scrolly/const.ts | 15 ++- .../base/b-scrolly/interface/common.ts | 2 +- .../base/b-scrolly/modules/state/helpers.ts | 61 ++++------ .../test/unit/functional/observer/index.ts | 7 ++ .../unit/functional/presets/chunk-size.ts | 12 +- src/core/prelude/test-env/components/json.ts | 9 +- src/core/prelude/test-env/mock/index.ts | 8 +- tests/helpers/component-object/builder.ts | 13 +-- tests/helpers/component-object/interface.ts | 10 +- tests/helpers/providers/interceptor/index.ts | 104 +++++++++++------- 12 files changed, 139 insertions(+), 124 deletions(-) diff --git a/index.d.ts b/index.d.ts index ad11abd203..8150638e16 100644 --- a/index.d.ts +++ b/index.d.ts @@ -124,16 +124,12 @@ declare var jestMock: { /** * Wrapper for jest `spyOn` function. - * - * {@link ModuleMocker.spyOn} * @see https://jestjs.io/docs/mock-functions */ spy: import('jest-mock').ModuleMocker['spyOn']; /** * Wrapper for jest `fn` function. - * - * {@link ModuleMocker.fn} * @see https://jestjs.io/docs/mock-functions */ mock: import('jest-mock').ModuleMocker['fn']; diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts index dec468e4d6..b1d5e485dc 100644 --- a/src/components/base/b-scrolly/b-scrolly.ts +++ b/src/components/base/b-scrolly/b-scrolly.ts @@ -21,7 +21,7 @@ import type { ComponentState, ComponentDb, - ComponentRenderStrategy as ComponentRenderStrategy, + ComponentRenderStrategy, RequestParams, RequestQueryFn, ShouldPerform, @@ -61,7 +61,11 @@ VDOM.addToPrototype(create); VDOM.addToPrototype(render); /** - * Компонент реализующий загрузку и отрисовку больших массивов данных чанками. + * Component that implements loading and rendering of large data arrays in chunks. + * + * The `bScrolly` component extends the `iData` class and implements the `iItems` interface. + * It provides functionality for efficiently loading and displaying large amounts of data + * by dynamically rendering chunks of data as the user scrolls. */ @component() export default class bScrolly extends iData implements iItems { @@ -100,11 +104,11 @@ export default class bScrolly extends iData implements iItems { /** * This factory function is used to pass information about the components that need to be rendered. * The function should return an array of arbitrary length consisting of objects that satisfy the - * {@link ComponentItem} interface. + * `ComponentItem` interface. * * By default, the rendering strategy is based on the `chunkSize` and `iItems` trait. * In other words, the default implementation takes a data slice of length `chunkSize` - * and calls the `iItems` functions to generate a {@link ComponentItem} object. + * and calls the `iItems` functions to generate a `ComponentItem` object. * * However, nothing prevents the client from implementing any strategy by overriding this function. * @@ -405,7 +409,7 @@ export default class bScrolly extends iData implements iItems { /** * Wrapper for `shouldStopRequestingData`. - * {@link shouldStopRequestingDataWrapper} + * {@link bScrolly.shouldStopRequestingDataWrapper} */ shouldStopRequestingDataWrapper(): boolean { const state = this.getComponentState(); @@ -422,7 +426,7 @@ export default class bScrolly extends iData implements iItems { /** * Wrapper for `shouldPerformDataRequest`. - * {@link shouldPerformDataRequest} + * {@link bScrolly.shouldPerformDataRequest} */ shouldPerformDataRequestWrapper(): boolean { return this.shouldPerformDataRequest(this.getComponentState(), this); @@ -445,6 +449,8 @@ export default class bScrolly extends iData implements iItems { * * @param isInitialLoading - `true` if this load was an initial component loading. * @param data + * + * @throws {@link ReferenceError} if there is not `data` field in the loaded data. */ protected onInitLoadSuccess(isInitialLoading: boolean, data: unknown): void { if (!Object.isPlainObject(data) || !Array.isArray(data.data)) { diff --git a/src/components/base/b-scrolly/const.ts b/src/components/base/b-scrolly/const.ts index a522dcabad..0a90065580 100644 --- a/src/components/base/b-scrolly/const.ts +++ b/src/components/base/b-scrolly/const.ts @@ -7,7 +7,20 @@ */ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { ComponentDataLocalEvents, ComponentEvents, ComponentItemType, ComponentLifecycleEvents, ComponentObserverLocalEvents, ComponentRenderLocalEvents, ComponentRenderStrategy, ComponentState, ComponentStrategy, RenderGuardRejectionReason } from 'components/base/b-scrolly/interface'; + +import type { + + ComponentDataLocalEvents, + ComponentItemType, + ComponentLifecycleEvents, + ComponentObserverLocalEvents, + ComponentRenderLocalEvents, + ComponentRenderStrategy, + ComponentState, + ComponentStrategy, + RenderGuardRejectionReason + +} from 'components/base/b-scrolly/interface'; /** * {@link ComponentRenderStrategy} diff --git a/src/components/base/b-scrolly/interface/common.ts b/src/components/base/b-scrolly/interface/common.ts index 00ae710da3..137eef6be0 100644 --- a/src/components/base/b-scrolly/interface/common.ts +++ b/src/components/base/b-scrolly/interface/common.ts @@ -13,7 +13,7 @@ import type { ComponentState } from 'components/base/b-scrolly/interface/compone * Interface representing the response of the client to the `renderGuard` method for rendering requests. * * To grant permission for rendering, the response object should have the following structure: - * + * * ```typescript * const canPerform: RenderGuardResult = { * result: true diff --git a/src/components/base/b-scrolly/modules/state/helpers.ts b/src/components/base/b-scrolly/modules/state/helpers.ts index 7cf2c8fcc3..1167717a01 100644 --- a/src/components/base/b-scrolly/modules/state/helpers.ts +++ b/src/components/base/b-scrolly/modules/state/helpers.ts @@ -8,46 +8,29 @@ import type { ComponentState } from 'components/base/b-scrolly/b-scrolly'; +/** + * Creates an initial state object for a component. + * + * @returns An object representing the initial state of a component. + */ export function createInitialState(): ComponentState { return { - loadPage: 0, - - renderPage: 0, - - itemsTillEnd: undefined, - - childTillEnd: undefined, - - maxViewedItem: undefined, - - maxViewedChild: undefined, - - data: [], - - lastLoadedData: [], - - lastLoadedRawData: undefined, - - isLastEmpty: false, - - isInitialLoading: true, - - /** - * Component items that was rendered - */ - items: [], - - childList: [], - - /** - * `True` if the next rendering process will be initial - */ - isInitialRender: true, - - isRequestsStopped: false, - - isLoadingInProgress: false, - - isLifecycleDone: false + loadPage: 0, + renderPage: 0, + itemsTillEnd: undefined, + childTillEnd: undefined, + maxViewedItem: undefined, + maxViewedChild: undefined, + data: [], + lastLoadedData: [], + lastLoadedRawData: undefined, + isLastEmpty: false, + isInitialLoading: true, + items: [], + childList: [], + isInitialRender: true, + isRequestsStopped: false, + isLoadingInProgress: false, + isLifecycleDone: false }; } diff --git a/src/components/base/b-scrolly/test/unit/functional/observer/index.ts b/src/components/base/b-scrolly/test/unit/functional/observer/index.ts index e69de29bb2..e94b2ed4cb 100644 --- a/src/components/base/b-scrolly/test/unit/functional/observer/index.ts +++ b/src/components/base/b-scrolly/test/unit/functional/observer/index.ts @@ -0,0 +1,7 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ diff --git a/src/components/base/b-scrolly/test/unit/functional/presets/chunk-size.ts b/src/components/base/b-scrolly/test/unit/functional/presets/chunk-size.ts index 900c397b8a..ae4662aef9 100644 --- a/src/components/base/b-scrolly/test/unit/functional/presets/chunk-size.ts +++ b/src/components/base/b-scrolly/test/unit/functional/presets/chunk-size.ts @@ -6,26 +6,18 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -/** - * @file Test cases of the component lifecycle - */ - import test from 'tests/config/unit/test'; import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; -import type { ComponentItemFactory } from 'components/base/b-scrolly/b-scrolly'; -import type { ComponentItem } from 'components/base/b-scrolly/interface'; test.describe(' with chunkSize preset', () => { let - component: Awaited>['component'], - provider:Awaited>['provider'], - state: Awaited>['state']; + provider:Awaited>['provider']; test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); - ({component, provider, state} = await createTestHelpers(page)); + ({provider} = await createTestHelpers(page)); await provider.start(); }); diff --git a/src/core/prelude/test-env/components/json.ts b/src/core/prelude/test-env/components/json.ts index e987c98eb5..790ae84b39 100644 --- a/src/core/prelude/test-env/components/json.ts +++ b/src/core/prelude/test-env/components/json.ts @@ -20,9 +20,12 @@ export function evalFn(func: T): T { } /** - * TODO: DOCS - * @param obj - * @param id + * Overrides the `toJSON` method of the provided object to return the identifier of a mock function + * within the page context. + * + * @param obj - The object to override the `toJSON` method for. + * @param id - The identifier of the mock function. + * @returns The modified object with the overridden `toJSON` method. */ export function setSerializerAsMockFn(obj: T, id: string): T { Object.assign(obj, { diff --git a/src/core/prelude/test-env/mock/index.ts b/src/core/prelude/test-env/mock/index.ts index 36d8a576e5..30223acdd7 100644 --- a/src/core/prelude/test-env/mock/index.ts +++ b/src/core/prelude/test-env/mock/index.ts @@ -6,18 +6,15 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -/** - * @file Provides an API to Work with `jest-mock` package - */ - import { ModuleMocker } from 'jest-mock'; let - globalApi: ModuleMocker; + globalApi: CanUndef; globalThis.jestMock = { /** * {@link ModuleMocker.spyOn} + * * @see https://jestjs.io/docs/mock-functions * * @param args @@ -29,6 +26,7 @@ globalThis.jestMock = { /** * {@link ModuleMocker.fn} + * * @see https://jestjs.io/docs/mock-functions * * @param args diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index 123111a6b5..ff6ddecdac 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -126,11 +126,9 @@ export default class ComponentObjectBuilder { throw new Error('Missing component path'); } - const classModule = await Utils.import<{default: new () => COMPONENT}>( - this.page, - componentClassImportPath); - - const classInstance = await classModule.evaluateHandle((ctx) => ctx.default); + const + classModule = await Utils.import<{default: new () => COMPONENT}>(this.page, componentClassImportPath), + classInstance = await classModule.evaluateHandle((ctx) => ctx.default); return classInstance; } @@ -157,7 +155,7 @@ export default class ComponentObjectBuilder { * After this operation, the `ComponentObject` will be marked as built and the `ComponentObject.component` property * will be accessible. * - * @param selectorOrLocator - The selector or locator for the component node + * @param selector - The selector or locator for the component node */ async pick(selector: string): Promise; @@ -172,9 +170,6 @@ export default class ComponentObjectBuilder { */ async pick(locator: Locator): Promise; - /** - * @inheritdoc - */ async pick(selectorOrLocator: string | Locator): Promise { const locator = Object.isString(selectorOrLocator) ? this.page.locator(selectorOrLocator) : diff --git a/tests/helpers/component-object/interface.ts b/tests/helpers/component-object/interface.ts index 817c058c12..fca44c0160 100644 --- a/tests/helpers/component-object/interface.ts +++ b/tests/helpers/component-object/interface.ts @@ -8,10 +8,10 @@ export interface SpyOptions { /** - * If set to true, the spy will be installed on the prototype of the component class. - * - * Setting this option is useful for methods such as `initLoad` because they - * are called not from an instance of the component, but using the `call` method from the class prototype. - */ + * If set to true, the spy will be installed on the prototype of the component class. + * + * Setting this option is useful for methods such as `initLoad` because they + * are called not from an instance of the component, but using the `call` method from the class prototype. + */ proto?: boolean; } diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index 89adcf0c03..dcff8cf2cc 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -10,39 +10,47 @@ import delay from 'delay'; import type { BrowserContext, Page, Request, Route } from 'playwright'; import { ModuleMocker } from 'jest-mock'; +/** + * Type definition for the response handler function. + */ type ResponseHandler = (route: Route, request: Request) => CanPromise; +/** + * Interface for response options. + */ interface ResponseOptions { delay?: number; } /** - * API that provides simple way to intercept and response to any request + * API that provides a simple way to intercept and respond to any request. */ export class RequestInterceptor { /** - * Route context + * The route context. */ readonly routeCtx: Page | BrowserContext; /** - * Route patter + * The route pattern. */ readonly routePattern: string | RegExp; /** - * Route listener + * The route listener. */ readonly routeListener: ResponseHandler; /** - * Default response that will be used to response every request if there is not responses in `responseQueue` + * The default response that will be used to respond to every request if there are no responses in `responseQueue`. */ readonly mock: ReturnType; /** - * @param ctx - * @param pattern + * Creates a new instance of RequestInterceptor. + * + * @param ctx - The page or browser context. + * @param pattern - The route pattern to match against requests. */ constructor(ctx: Page | BrowserContext, pattern: string | RegExp) { this.routeCtx = ctx; @@ -57,36 +65,42 @@ export class RequestInterceptor { } /** - * Sets a response for one request + * Sets a response for one request. * * @example * ```typescript * const interceptor = new RequestInterceptor(page, /api/); * * interceptor - * .response((r: Route) => r.fulfill({status: 200})); - * .response((r: Route) => r.fulfill({status: 500})); + * .responseOnce((r: Route) => r.fulfill({status: 200})) + * .responseOnce((r: Route) => r.fulfill({status: 500})); * ``` + * + * @param handler - The response handler function. + * @param opts - The response options. + * @returns The current instance of RequestInterceptor. */ responseOnce(handler: ResponseHandler, opts?: ResponseOptions): this; /** - * Sets a response for one request + * Sets a response for one request. * * @example * ```typescript * const interceptor = new RequestInterceptor(page, /api/); * * interceptor - * .responseOnce(200, {content: 1}); - * .responseOnce(500) + * .responseOnce(200, {content: 1}) + * .responseOnce(500); * ``` + * + * @param status - The response status. + * @param payload - The response payload. + * @param opts - The response options. + * @returns The current instance of RequestInterceptor. */ responseOnce(status: number, payload: object | string | number, opts?: ResponseOptions): this; - /** - * @inheritdoc - */ responseOnce( handlerOrStatus: number | ResponseHandler, payload?: object | string | number, @@ -96,7 +110,6 @@ export class RequestInterceptor { if (Object.isFunction(handlerOrStatus)) { fn = handlerOrStatus; - } else { const status = handlerOrStatus; fn = this.cookResponseFn(status, payload, opts); @@ -108,7 +121,7 @@ export class RequestInterceptor { /** * Sets a response for every request. - * If there is not responses settled via `responseOnce` (ie `responseQueue` is empty) that response will be used + * If there are no responses set via `responseOnce` (i.e., `responseQueue` is empty), that response will be used. * * @example * ```typescript @@ -116,13 +129,14 @@ export class RequestInterceptor { * interceptor.response((r: Route) => r.fulfill({status: 200})); * ``` * - * @param handler + * @param handler - The response handler function. + * @returns The current instance of RequestInterceptor. */ response(handler: ResponseHandler): this; /** * Sets a response for every request. - * If there is not responses settled via `responseOnce` (ie `responseQueue` is empty) that response will be used + * If there are no responses set via `responseOnce` (i.e., `responseQueue` is empty), that response will be used. * * @example * ```typescript @@ -130,15 +144,13 @@ export class RequestInterceptor { * interceptor.response(200, {}); * ``` * - * @param status - * @param payload - * @param opts + * @param status - The response status. + * @param payload - The response payload. + * @param opts - The response options. + * @returns The current instance of RequestInterceptor. */ response(status: number, payload: object | string | number, opts?: ResponseOptions): this; - /** - * @inheritdoc - */ response( handlerOrStatus: number | ResponseHandler, payload?: object | string | number, @@ -148,7 +160,6 @@ export class RequestInterceptor { if (Object.isFunction(handlerOrStatus)) { fn = handlerOrStatus; - } else { const status = handlerOrStatus; fn = this.cookResponseFn(status, payload, opts); @@ -159,7 +170,9 @@ export class RequestInterceptor { } /** - * Clears the responses that was created via `responseOnce` + * Clears the responses that were created via `responseOnce`. + * + * @returns The current instance of RequestInterceptor. */ clearResponseQueue(): this { this.mock.mockReset(); @@ -167,7 +180,9 @@ export class RequestInterceptor { } /** - * Stops the request interception + * Stops the request interception. + * + * @returns A promise that resolves with the current instance of RequestInterceptor. */ async stop(): Promise { await this.routeCtx.unroute(this.routePattern, this.routeListener); @@ -175,7 +190,9 @@ export class RequestInterceptor { } /** - * Starts the request interception + * Starts the request interception. + * + * @returns A promise that resolves with the current instance of RequestInterceptor. */ async start(): Promise { await this.routeCtx.route(this.routePattern, this.routeListener); @@ -183,23 +200,28 @@ export class RequestInterceptor { } /** - * Cooks a response handler + * Cooks a response handler. * - * @param status - * @param payload - * @param opts + * @param status - The response status. + * @param payload - The response payload. + * @param opts - The response options. + * @returns The response handler function. */ protected cookResponseFn( status: number, payload?: string | object | number, opts?: ResponseOptions ): ResponseHandler { - return async (route) => { - if (opts?.delay != null) { - await delay(opts.delay); - } - - return route.fulfill({status, body: JSON.stringify(payload), contentType: 'application/json'}); - }; + return async (route) => { + if (opts?.delay != null) { + await delay(opts.delay); + } + + return route.fulfill({ + status, + body: JSON.stringify(payload), + contentType: 'application/json' + }); + }; } } From 8d4702516ea1ce7a3a119246a398eace0e6a75c0 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Tue, 20 Jun 2023 11:02:56 +0300 Subject: [PATCH 030/159] WIP --- .../base/b-scrolly/test/api/helpers/index.ts | 38 +++++++++---------- tests/helpers/providers/interceptor/index.ts | 25 ++++-------- .../providers/interceptor/interface.ts | 18 +++++++++ 3 files changed, 45 insertions(+), 36 deletions(-) create mode 100644 tests/helpers/providers/interceptor/interface.ts diff --git a/src/components/base/b-scrolly/test/api/helpers/index.ts b/src/components/base/b-scrolly/test/api/helpers/index.ts index 88d00fc8fa..b7cb266ec7 100644 --- a/src/components/base/b-scrolly/test/api/helpers/index.ts +++ b/src/components/base/b-scrolly/test/api/helpers/index.ts @@ -20,7 +20,7 @@ export * from 'components/base/b-scrolly/test/api/component-object'; /** * Creates a helper API for convenient testing of the `b-scrolly` component. - * @param page The page object representing the testing page. + * @param page - The page object representing the testing page. */ export async function createTestHelpers(page: Page): Promise { const @@ -49,9 +49,9 @@ export async function createTestHelpers(page: Page): Promise * 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. + * @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( itemsCtor: DataItemCtor, @@ -155,8 +155,8 @@ export function createDataConveyor( /** * 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. + * @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, @@ -187,7 +187,7 @@ export function createStateApi( * 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. + * @param state - The partial component state to override the default values. */ export function createInitialState(state: Partial): ComponentState { return { @@ -214,7 +214,7 @@ export function createInitialState(state: Partial): ComponentSta /** * Extracts state data from the data conveyor and returns it. - * @param conveyor The data conveyor to extract state data from. + * @param conveyor - The data conveyor to extract state data from. */ export function extractStateFromDataConveyor(conveyor: DataConveyor): Pick { return { @@ -229,9 +229,9 @@ export function extractStateFromDataConveyor(conveyor: DataConveyor): Pick( data: DATA[], @@ -243,7 +243,7 @@ export function createFromData( /** * Creates a simple object that matches the {@link MountedItem} interface. - * @param data The object with index of the mounted item. + * @param data - The object with index of the mounted item. */ export function createMountedItem(data: IndexedObj): MountedItem { return { @@ -261,7 +261,7 @@ export function createMountedItem(data: IndexedObj): MountedItem { /** * Creates a simple object that matches the {@link MountedChild}` interface. - * @param data The object with index of the mounted child. + * @param data - The object with index of the mounted child. */ export function createMountedSeparator(data: IndexedObj): MountedChild { return { @@ -280,9 +280,9 @@ export function createMountedSeparator(data: IndexedObj): MountedChild { * 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). + * @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( count: number, @@ -294,7 +294,7 @@ export function createChunk( /** * Creates a simple indexed object. - * @param i The index of the object. + * @param i - The index of the object. */ export function createIndexedObj(i: number): IndexedObj { return {i}; @@ -304,8 +304,8 @@ export function createIndexedObj(i: number): IndexedObj { * Filters emitter emit calls and removes unnecessary events. * It only keeps component events, excluding observer-like events. * - * @param emitCalls The array of emit calls. - * @param filterObserverEvents Whether to filter out observer events (default: true). + * @param emitCalls - The array of emit calls. + * @param filterObserverEvents - Whether to filter out observer events (default: true). */ export function filterEmitterCalls(emitCalls: unknown[][], filterObserverEvents: boolean = true): unknown[][] { return emitCalls.filter(([event]) => Object.isString(event) && diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index dcff8cf2cc..781298d75c 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -6,21 +6,11 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import delay from 'delay'; import type { BrowserContext, Page, Request, Route } from 'playwright'; +import delay from 'delay'; import { ModuleMocker } from 'jest-mock'; -/** - * Type definition for the response handler function. - */ -type ResponseHandler = (route: Route, request: Request) => CanPromise; - -/** - * Interface for response options. - */ -interface ResponseOptions { - delay?: number; -} +import type { ResponseHandler, ResponseOptions } from 'tests/helpers/providers/interceptor/interface'; /** * API that provides a simple way to intercept and respond to any request. @@ -42,7 +32,7 @@ export class RequestInterceptor { readonly routeListener: ResponseHandler; /** - * The default response that will be used to respond to every request if there are no responses in `responseQueue`. + * Экземпляр jest-mock который берет на себя логику имплементации ответов */ readonly mock: ReturnType; @@ -121,7 +111,7 @@ export class RequestInterceptor { /** * Sets a response for every request. - * If there are no responses set via `responseOnce` (i.e., `responseQueue` is empty), that response will be used. + * If there are no responses set via {@link RequestInterceptor.responseOnce}, that response will be used. * * @example * ```typescript @@ -136,7 +126,7 @@ export class RequestInterceptor { /** * Sets a response for every request. - * If there are no responses set via `responseOnce` (i.e., `responseQueue` is empty), that response will be used. + * If there are no responses set via {@link RequestInterceptor.responseOnce}, that response will be used. * * @example * ```typescript @@ -170,11 +160,12 @@ export class RequestInterceptor { } /** - * Clears the responses that were created via `responseOnce`. + * Clears the responses that were created via {@link RequestInterceptor.responseOnce} or + * {@link RequestInterceptor.response}. * * @returns The current instance of RequestInterceptor. */ - clearResponseQueue(): this { + clearResponseOnce(): this { this.mock.mockReset(); return this; } diff --git a/tests/helpers/providers/interceptor/interface.ts b/tests/helpers/providers/interceptor/interface.ts new file mode 100644 index 0000000000..f7846ee0da --- /dev/null +++ b/tests/helpers/providers/interceptor/interface.ts @@ -0,0 +1,18 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { Route, Request } from 'playwright'; + +export type ResponseHandler = (route: Route, request: Request) => CanPromise; + +/** + * Interface for response options. + */ +export interface ResponseOptions { + delay?: number; +} From 1dab519bc72f7072b1034e08b27ee45e70fc53ef Mon Sep 17 00:00:00 2001 From: bonkalol Date: Tue, 20 Jun 2023 11:06:59 +0300 Subject: [PATCH 031/159] WIP --- src/components/base/b-virtual-scroll/b-virtual-scroll.ss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ss b/src/components/base/b-virtual-scroll/b-virtual-scroll.ss index ad0d390d0a..3c2f2d4590 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ss +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ss @@ -42,6 +42,13 @@ . += self.slot('empty') + < .&__done & + ref = done | + v-if = $slots['done'] | + :style = {display: 'none'} + . + += self.slot('done') + < .&__render-next & ref = renderNext | v-if = $slots['renderNext'] | From 540e9afc733218707a5ef17c667bfaaf661f9544 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Tue, 20 Jun 2023 11:40:32 +0300 Subject: [PATCH 032/159] :art: --- src/components/base/b-scrolly/b-scrolly.ts | 12 +- .../modules/factory/engines/force-update.ts | 2 +- .../b-scrolly/modules/factory/engines/vdom.ts | 2 +- .../base/b-scrolly/modules/juggler/index.ts | 26 +-- .../base/b-scrolly/modules/state/index.ts | 164 +++++++++--------- .../test/unit/functional/state/index.ts | 4 - 6 files changed, 99 insertions(+), 111 deletions(-) diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts index b1d5e485dc..10bd4adcbf 100644 --- a/src/components/base/b-scrolly/b-scrolly.ts +++ b/src/components/base/b-scrolly/b-scrolly.ts @@ -62,8 +62,8 @@ VDOM.addToPrototype(render); /** * Component that implements loading and rendering of large data arrays in chunks. - * * The `bScrolly` component extends the `iData` class and implements the `iItems` interface. + * * It provides functionality for efficiently loading and displaying large amounts of data * by dynamically rendering chunks of data as the user scrolls. */ @@ -408,8 +408,7 @@ export default class bScrolly extends iData implements iItems { } /** - * Wrapper for `shouldStopRequestingData`. - * {@link bScrolly.shouldStopRequestingDataWrapper} + * Wrapper for {@link bScrolly.shouldStopRequestingData}. */ shouldStopRequestingDataWrapper(): boolean { const state = this.getComponentState(); @@ -425,8 +424,7 @@ export default class bScrolly extends iData implements iItems { } /** - * Wrapper for `shouldPerformDataRequest`. - * {@link bScrolly.shouldPerformDataRequest} + * Wrapper for {@link bScrolly.shouldPerformDataRequest}. */ shouldPerformDataRequestWrapper(): boolean { return this.shouldPerformDataRequest(this.getComponentState(), this); @@ -457,10 +455,10 @@ export default class bScrolly extends iData implements iItems { throw new ReferenceError('Missing "data" field in the loaded data'); } - this.componentInternalState.updateData(data.data, isInitialLoading); + this.componentInternalState.updateData(data.data, isInitialLoading); this.shouldStopRequestingDataWrapper(); - this.componentEmitter.emit(componentDataLocalEvents.dataLoadSuccess, data.data, isInitialLoading); + this.componentEmitter.emit(componentDataLocalEvents.dataLoadSuccess, data.data, isInitialLoading); if (isInitialLoading && Object.size(data.data) === 0) { if (this.shouldStopRequestingDataWrapper()) { diff --git a/src/components/base/b-scrolly/modules/factory/engines/force-update.ts b/src/components/base/b-scrolly/modules/factory/engines/force-update.ts index ccef8b2f2b..473c648e53 100644 --- a/src/components/base/b-scrolly/modules/factory/engines/force-update.ts +++ b/src/components/base/b-scrolly/modules/factory/engines/force-update.ts @@ -9,7 +9,7 @@ /** * Renders the provided `VNodes` to the `HTMLElements` via `$forceUpdate` and single render engine instance. * - * @param args + * @param _args */ export function render(..._args: any[]): HTMLElement[] { return []; diff --git a/src/components/base/b-scrolly/modules/factory/engines/vdom.ts b/src/components/base/b-scrolly/modules/factory/engines/vdom.ts index e78978d31e..1365166737 100644 --- a/src/components/base/b-scrolly/modules/factory/engines/vdom.ts +++ b/src/components/base/b-scrolly/modules/factory/engines/vdom.ts @@ -13,7 +13,7 @@ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; * Renders the provided `VNodes` to the `HTMLElements` via `vdom.render` API. * * @param ctx - * @param data + * @param items */ export function render(ctx: bScrolly, items: VNodeDescriptor[]): HTMLElement[] { const diff --git a/src/components/base/b-scrolly/modules/juggler/index.ts b/src/components/base/b-scrolly/modules/juggler/index.ts index 57cb25cd74..f91fa12a2a 100644 --- a/src/components/base/b-scrolly/modules/juggler/index.ts +++ b/src/components/base/b-scrolly/modules/juggler/index.ts @@ -14,7 +14,6 @@ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import type { ComponentItem } from 'components/base/b-scrolly/b-scrolly'; import { renderGuardRejectionReason, componentDataLocalEvents, componentItemType, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; import type { MountedChild, MountedItem } from 'components/base/b-scrolly/interface'; -import { isItem } from 'components/base/b-scrolly/modules/helpers'; export const $$ = symbolGenerator(), @@ -69,12 +68,10 @@ export class Juggler extends Friend { const items = ctx.componentFactory.produceComponentItems(), nodes = ctx.componentFactory.produceNodes(items), - anyMounted = this.produceMounted(items, nodes), - mountedItems = anyMounted.filter((mounted) => mounted.type === componentItemType.item); + mounted = this.produceMounted(items, nodes); - ctx.componentInternalState.updateMountedItems(mountedItems); - ctx.componentInternalState.updateChildList(anyMounted); - ctx.observer.observe(anyMounted); + ctx.componentInternalState.updateMounted(mounted); + ctx.observer.observe(mounted); ctx.componentEmitter.emit(componentRenderLocalEvents.domInsertStart); @@ -103,7 +100,7 @@ export class Juggler extends Friend { * @param items * @param nodes */ - protected produceMounted(items: ComponentItem[], nodes: HTMLElement[]): Array { + protected produceMounted(items: ComponentItem[], nodes: HTMLElement[]): Array { const {ctx} = this, {items: mountedItems, childList} = ctx.getComponentState(); @@ -179,26 +176,19 @@ export class Juggler extends Friend { /** * Handler: component enters the viewport. + * @param component */ protected onElementEnters(component: MountedChild): void { const - {ctx} = this, - state = ctx.getComponentState(), - {childIndex} = component; - - if (isItem(component) && (state.maxViewedItem == null || state.maxViewedItem < component.itemIndex)) { - ctx.componentInternalState.setMaxViewedItemIndex(component.itemIndex); - } - - if (state.maxViewedChild == null || state.maxViewedChild < childIndex) { - ctx.componentInternalState.setMaxViewedChildIndex(childIndex); - } + {ctx} = this; + ctx.componentInternalState.setMaxViewedIndex(component); this.loadDataOrPerformRender(); } /** * Handler: component leaves the viewport. + * @param _component */ protected onElementOut(_component: MountedChild): void { // ... diff --git a/src/components/base/b-scrolly/modules/state/index.ts b/src/components/base/b-scrolly/modules/state/index.ts index ee5d4a5c21..027f5ac461 100644 --- a/src/components/base/b-scrolly/modules/state/index.ts +++ b/src/components/base/b-scrolly/modules/state/index.ts @@ -9,16 +9,19 @@ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import { componentDataLocalEvents, componentLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; import type { MountedChild, ComponentState, MountedItem } from 'components/base/b-scrolly/interface'; +import { isItem } from 'components/base/b-scrolly/modules/helpers'; import { createInitialState } from 'components/base/b-scrolly/modules/state/helpers'; import Friend from 'components/friends/friend'; +/** + * Friendly to the `bScrolly` class that represents the internal state of a component. + */ export class ComponentInternalState extends Friend { + override readonly C!: bScrolly; /** - * {@link bScrolly} + * Current state of the component. */ - override readonly C!: bScrolly; - protected state: ComponentState = createInitialState(); /** @@ -41,7 +44,9 @@ export class ComponentInternalState extends Friend { } /** - * Собирает состояние компонента в один объект. + * Compiles and returns the current state of the component. + * + * @returns The current state of the component. */ compile(): Readonly { return { @@ -50,134 +55,133 @@ export class ComponentInternalState extends Friend { } /** - * Обнуляет состояние модуля. + * Resets the state of the component. */ reset(): void { this.state = createInitialState(); } /** - * Обновляет указатель последней загруженной страницы. + * Increments the load page pointer. */ - incrementLoadPage(): this { + incrementLoadPage(): void { this.state.loadPage++; - return this; } - /** - * Обновляет указать последней отрисованной страницы. + * Increments the render page pointer. */ - incrementRenderPage(): this { + incrementRenderPage(): void { this.state.renderPage++; - return this; - } - - storeComponentItems(items: MountedItem[]): this { - (this.state.items).push(...items); - return this; } /** - * Обновляет состояние загруженных данных. + * Updates the loaded data state. * - * @param data - * @param isInitialLoading + * @param data - The new data to update the state. + * @param isInitialLoading - Indicates if it's the initial loading. */ - updateData(data: object[], isInitialLoading: boolean): this { + updateData(data: object[], isInitialLoading: boolean): void { this.state.data = this.state.data.concat(data); this.state.isLastEmpty = data.length === 0; this.state.isInitialLoading = isInitialLoading; this.state.lastLoadedData = data; - - return this; - } - - updateMountedItems(mounted: MountedItem[]): this { - (this.state.items).push(...mounted); - return this; } - updateChildList(mounted: MountedChild[]): this { - (this.state.childList).push(...mounted); - return this; - } - - updateItemsTillEnd(): this { - if (this.state.maxViewedItem == null) { - throw new Error('Missing max viewed item index'); - } - - this.state.itemsTillEnd = this.state.items.length - 1 - this.state.maxViewedItem; - return this; - } - - updateChildTillEnd(): this { - if (this.state.maxViewedChild == null) { - throw new Error('Missing max viewed child index'); - } + /** + * Updates the arrays with mounted child elements of the component. + * + * @param mounted - The mounted child elements. + */ + updateMounted(mounted: MountedChild[]): void { + const + {state} = this, + childList = state.childList, + itemsList = state.items, + newItems = mounted.filter((child) => child.type === 'item'); - this.state.childTillEnd = this.state.childList.length - 1 - this.state.maxViewedChild; - return this; + childList.push(...mounted); + itemsList.push(...newItems); } /** - * Обновляет состояние последних сырых загруженных данных. + * Updates the state of the last raw loaded data. * - * @param data + * @param data - The last raw loaded data. */ - setRawLastLoaded(data: unknown): this { + setRawLastLoaded(data: unknown): void { this.state.lastLoadedRawData = data; - return this; } /** - * Sets an initial render state + * Sets the flag indicating if it's the initial render cycle. * - * @param state + * @param value - The value of the flag. */ - setIsInitialRender(state: boolean): this { - this.state.isInitialRender = state; - return this; + setIsInitialRender(value: boolean): void { + this.state.isInitialRender = value; } - setIsRequestsStopped(state: boolean): this { - this.state.isRequestsStopped = state; - return this; + /** + * Sets the flag indicating if requests are stopped and the component won't make any more requests + * until the lifecycle is refreshed. + * + * @param value - The value of the flag. + */ + setIsRequestsStopped(value: boolean): void { + this.state.isRequestsStopped = value; } - setIsLifecycleDone(state: boolean): this { - if (this.state.isLifecycleDone === state) { - return this; + /** + * Sets the flag indicating if the component's lifecycle is done. + * + * @param value - The value of the flag. + */ + setIsLifecycleDone(value: boolean): void { + const + {state} = this; + + if (state.isLifecycleDone === value) { + return; } const {ctx} = this; - this.state.isLifecycleDone = state; + state.isLifecycleDone = value; - if (state) { + if (value) { ctx.componentEmitter.emit(componentLocalEvents.lifecycleDone); } - - return this; } - setIsLoadingInProgress(state: boolean): this { - this.state.isLoadingInProgress = state; - return this; + /** + * Sets the flag indicating if the component is currently loading data. + * + * @param value - The value of the flag. + */ + setIsLoadingInProgress(value: boolean): void { + this.state.isLoadingInProgress = value; } - setMaxViewedItemIndex(itemIndex: number): this { - this.state.maxViewedItem = itemIndex; - this.updateItemsTillEnd(); - - return this; - } + /** + * Sets the maximum viewed index based on the passed component's index. + * + * @param component - The component to compare and update the maximum viewed index. + */ + setMaxViewedIndex(component: MountedChild): void { + const + {state} = this, + {childIndex} = component; - setMaxViewedChildIndex(childIndex: number): this { - this.state.maxViewedChild = childIndex; - this.updateChildTillEnd(); + if (isItem(component) && (state.maxViewedItem == null || state.maxViewedItem < component.itemIndex)) { + state.maxViewedItem = component.itemIndex; + state.itemsTillEnd = state.items.length - 1 - state.maxViewedItem; + } - return this; + if (state.maxViewedChild == null || state.maxViewedChild < childIndex) { + state.maxViewedChild = component.childIndex; + state.childTillEnd = state.childList.length - 1 - state.maxViewedChild; + } } } + diff --git a/src/components/base/b-scrolly/test/unit/functional/state/index.ts b/src/components/base/b-scrolly/test/unit/functional/state/index.ts index efaa2a3a3f..fbc1bf088a 100644 --- a/src/components/base/b-scrolly/test/unit/functional/state/index.ts +++ b/src/components/base/b-scrolly/test/unit/functional/state/index.ts @@ -6,10 +6,6 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -/** - * @file Test cases of the component lifecycle - */ - import test from 'tests/config/unit/test'; import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; From 0886904859c891984990f0fa9a2051d433a7557c Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 21 Jun 2023 18:01:43 +0300 Subject: [PATCH 033/159] WIP --- index.d.ts | 5 + src/components/base/b-scrolly/b-scrolly.ts | 400 ++++-------------- src/components/base/b-scrolly/const.ts | 10 + src/components/base/b-scrolly/handlers.ts | 198 +++++++++ .../base/b-scrolly/modules/emitter/index.ts | 10 +- .../b-scrolly/modules/emitter/interface.ts | 2 +- .../base/b-scrolly/modules/factory/index.ts | 38 +- .../base/b-scrolly/modules/juggler/index.ts | 196 --------- .../observer/engines/intersection-observer.ts | 12 +- .../base/b-scrolly/modules/observer/index.ts | 7 + .../base/b-scrolly/modules/slots/index.ts | 18 - .../base/b-scrolly/modules/state/index.ts | 38 +- src/components/base/b-scrolly/props.ts | 278 ++++++++++++ .../test/api/component-object/index.ts | 16 +- .../base/b-scrolly/test/api/helpers/index.ts | 46 +- .../b-scrolly/test/api/helpers/interface.ts | 15 +- .../functional/emitter/{index.ts => order.ts} | 26 +- tests/helpers/component-object/builder.ts | 1 + tests/helpers/mock/interface.ts | 5 +- 19 files changed, 705 insertions(+), 616 deletions(-) create mode 100644 src/components/base/b-scrolly/handlers.ts delete mode 100644 src/components/base/b-scrolly/modules/juggler/index.ts create mode 100644 src/components/base/b-scrolly/props.ts rename src/components/base/b-scrolly/test/unit/functional/emitter/{index.ts => order.ts} (85%) diff --git a/index.d.ts b/index.d.ts index 8150638e16..62af36adc9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -135,6 +135,11 @@ declare var mock: import('jest-mock').ModuleMocker['fn']; }; +interface JestMockResult { + type: 'throw' | 'return'; + value: VAL; +} + interface TouchGesturesCreateOptions { /** * An element to dispatch the event diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts index 10bd4adcbf..7a1908ceb1 100644 --- a/src/components/base/b-scrolly/b-scrolly.ts +++ b/src/components/base/b-scrolly/b-scrolly.ts @@ -11,48 +11,15 @@ * @packageDocumentation */ -import type { AsyncOptions } from 'core/async'; - import VDOM, { create, render } from 'components/friends/vdom'; import type iItems from 'components/traits/i-items/i-items'; -import type { CreateFromItemFn } from 'components/traits/i-items/i-items'; - -import type { - - ComponentState, - ComponentDb, - ComponentRenderStrategy, - RequestParams, - RequestQueryFn, - ShouldPerform, - ComponentRefs, - ComponentItemFactory, - ComponentItemType, - ComponentStrategy, - RenderGuardResult - -} from 'components/base/b-scrolly/interface'; - -import { - - componentRenderStrategy, - componentDataLocalEvents, - defaultShouldProps, - componentLocalEvents, - componentItemType, - componentStrategy - -} from 'components/base/b-scrolly/const'; -import { Juggler } from 'components/base/b-scrolly/modules/juggler'; -import { Observer } from 'components/base/b-scrolly/modules/observer'; -import { ComponentFactory } from 'components/base/b-scrolly/modules/factory'; -import { SlotsStateController } from 'components/base/b-scrolly/modules/slots'; -import { ComponentInternalState } from 'components/base/b-scrolly/modules/state'; -import { componentTypedEmitter } from 'components/base/b-scrolly/modules/emitter'; +import { bScrollyDomInsertAsyncGroup, renderGuardRejectionReason } from 'components/base/b-scrolly/const'; -import iData, { component, prop, system, $$ } from 'components/super/i-data/i-data'; -import { chunkSizePreset } from 'components/base/b-scrolly/modules/presets/chunk-size'; +import iData, { $$, component, RequestParams } from 'components/super/i-data/i-data'; +import { bScrollyHandlers } from 'components/base/b-scrolly/handlers'; +import type { AsyncOptions } from 'core/async'; +import type { ComponentState } from 'components/base/b-scrolly/interface'; export * from 'components/base/b-scrolly/interface'; export * from 'components/base/b-scrolly/const'; @@ -68,238 +35,7 @@ VDOM.addToPrototype(render); * by dynamically rendering chunks of data as the user scrolls. */ @component() -export default class bScrolly extends iData implements iItems { - /** {@link iItems.item} */ - readonly Item!: object; - - /** {@link iItems.Items} */ - readonly Items!: Array; - - /** {@link iItems.item} */ - @prop({type: [String, Function]}) - readonly item?: iItems['item']; - - /** {@link iItems.itemKey} */ - @prop({type: [String, Function]}) - readonly itemKey?: CreateFromItemFn; - - /** {@link ComponentItemType} */ - @prop({type: [String, Function]}) - readonly itemType: keyof ComponentItemType | CreateFromItemFn = componentItemType.item; - - /** {@link iItems.itemProps} */ - @prop({type: [Function, Object], default: () => ({})}) - readonly itemProps!: iItems['itemProps']; - - /** - * Specifies the number of times the `tombstone` component will be rendered. - * - * This prop can be useful if you want to render multiple `tombstone` components - * using a single specified element. For example, if you set `tombstonesSize` to 3, - * then three `tombstone` components will be rendered on your page. - */ - @prop(Number) - readonly tombstonesSize?: number; - - /** - * This factory function is used to pass information about the components that need to be rendered. - * The function should return an array of arbitrary length consisting of objects that satisfy the - * `ComponentItem` interface. - * - * By default, the rendering strategy is based on the `chunkSize` and `iItems` trait. - * In other words, the default implementation takes a data slice of length `chunkSize` - * and calls the `iItems` functions to generate a `ComponentItem` object. - * - * However, nothing prevents the client from implementing any strategy by overriding this function. - * - * For example, it is possible to define a function - * that takes the last loaded data and draws twice as many components: - * - * @example - * ```typescript - * const itemsFactory = (state) => { - * const data = state.lastLoadedData; - * - * const items = data.map((item) => ({ - * item: 'section', - * key: Object.cast(undefined), - * type: 'item', - * children: [], - * props: { - * 'data-index': item.i - * } - * })); - * - * return [...items, ...items]; - * } - * ``` - */ - @prop({ - type: Function, - default: (state: ComponentState, ctx: bScrolly) => { - if (ctx.chunkSize == null) { - throw new Error('chunkSize.getNextDataSlice is used but chunkSize prop is not settled'); - } - - const descriptors = chunkSizePreset.getNextDataSlice(state, ctx.chunkSize).map((data, i) => ({ - key: ctx.itemKey?.(data, i), - - item: Object.isFunction(ctx.item) ? ctx.item(data, i) : ctx.item, - type: Object.isFunction(ctx.itemType) ? ctx.itemType(data, i) : ctx.itemType, - - props: Object.isFunction(ctx.itemProps) ? - ctx.itemProps(data, i, { - key: ctx.itemKey?.(data, i), - ctx - }) : - ctx.itemProps - })); - - return descriptors; - } - }) - - readonly itemsFactory!: ComponentItemFactory; - - override readonly DB!: ComponentDb; - - /** - * The rendering strategy of components. - * Determines which approach will be taken for rendering components within the rendering engine. - * - * * `default` - The default approach, - * which creates a new instance of the rendering engine each time a new rendering is performed. - * - * * `reuse` - An approach - * that reuses the current instance of the rendering engine whenever a new rendering is performed. - * - * {@link ComponentRenderStrategy} - */ - @prop({type: String, validator: (v) => Object.isString(v) && componentRenderStrategy.hasOwnProperty(v)}) - readonly componentRenderStrategy: keyof ComponentRenderStrategy = componentRenderStrategy.default; - - /** - * Strategies for component operation modes. - * {@link ComponentStrategy} - */ - @prop({type: String, validator: (v) => Object.isString(v) && componentStrategy.hasOwnProperty(v)}) - readonly componentStrategy: keyof ComponentStrategy = componentStrategy.intersectionObserver; - - /** - * Function that returns the GET parameters for a request. - * {@link RequestQueryFn} - */ - @prop({type: Function}) - readonly requestQuery?: RequestQueryFn; - - /** - * The number of elements to render at once. - * This prop is used in conjunction with `renderGuard` and `chunkSize` preset. - */ - @prop({type: Number, validator: Number.isNatural}) - readonly chunkSize?: number = 10; - - /** - * When this function returns `true` the component will stop to request new data. - * This function will be called on each data loading cycle. - */ - @prop({ - type: Function, - default: defaultShouldProps.shouldStopRequestingData - }) - - readonly shouldStopRequestingData!: ShouldPerform; - - /** - * When this function returns `true` the component will be able to request additional data. - * This function will be called on each new element enters the viewport. - */ - @prop({ - type: Function, - default: defaultShouldProps.shouldPerformDataRequest - }) - - readonly shouldPerformDataRequest!: ShouldPerform; - - /** - * This function is called after successful data loading or when the component enters the visible area. - * - * This function asks the client whether rendering can be performed. The client responds with an object - * indicating whether rendering is allowed or the reason for denial. The client's response should be an object - * of type {@link RenderGuardResult}. - * - * Based on the result of this function, the component takes appropriate actions. For example, - * it may load data if it is not sufficient for rendering, or perform rendering if all conditions are met. - * - * By default, the {@link chunkSizePreset.renderGuard} strategy is used, - * which already implements the mechanism for communication with the component. - */ - @prop({ - type: Function, - default: (state: ComponentState, ctx: bScrolly) => { - if (ctx.chunkSize == null) { - throw new Error('The "ChunkSize.renderGuard" preset is active, but the "chunkSize" prop is not set.'); - } - - return chunkSizePreset.renderGuard(state, ctx, ctx.chunkSize); - } - }) - readonly renderGuard!: ShouldPerform; - - /** - * This function is called in the `renderGuard` after other checks are completed. - * - * This function receives the component state as input, based on which the client - * should determine whether the component should render the next chunk of components. - * - * For example, if we want to render the next data chunk only when the client - * has seen all the main components, we can implement the following function: - * - * @example - * ```typescript - * const shouldPerformDataRender = (state) => { - * return state.isInitialRender || state.itemsTillEnd === 0; - * } - * ``` - */ - @prop(Function) - readonly shouldPerformDataRender?: ShouldPerform; - - /** - * If `true`, the element observation module will not be initialized. - * - * Setting this prop to `true` can be useful if you want to implement lazy rendering - * and control it using the `renderNext` method. - */ - @prop({ - type: Boolean - }) - readonly disableObserver: boolean = false; - - /** {@link componentTypedEmitter} */ - @system((ctx) => componentTypedEmitter(ctx)) - readonly componentEmitter!: ReturnType; - - /** {@link SlotsStateController} */ - @system((ctx) => new SlotsStateController(ctx)) - readonly slotsStateController!: SlotsStateController; - - /** {@link ComponentInternalState} */ - @system((ctx) => new ComponentInternalState(ctx)) - readonly componentInternalState!: ComponentInternalState; - - /** {@link ComponentFactory} */ - @system((ctx) => new ComponentFactory(ctx)) - readonly componentFactory!: ComponentFactory; - - /** {@link Juggler} */ - @system((ctx) => new Juggler(ctx)) - readonly juggler!: Juggler; - - /** {@link Observer} */ - @system((ctx) => new Observer(ctx)) - readonly observer!: Observer; - +export default class bScrolly extends bScrollyHandlers implements iItems { // @ts-ignore (getter instead readonly) override get requestParams(): iData['requestParams'] { return { @@ -310,8 +46,6 @@ export default class bScrolly extends iData implements iItems { }; } - protected override readonly $refs!: iData['$refs'] & ComponentRefs; - override reload(...args: Parameters): ReturnType { this.componentStatus = 'loading'; return super.reload(...args); @@ -342,17 +76,16 @@ export default class bScrolly extends iData implements iItems { callSuperAndStateReset() : this.initLoadNext(); - this.componentEmitter.emit(componentDataLocalEvents.dataLoadStart, isInitialLoading); + this.onDataLoadStart(isInitialLoading); if (Object.isPromise(initLoadResult)) { initLoadResult .then((res) => { - this.componentInternalState.setIsLoadingInProgress(false); - this.onInitLoadSuccess(isInitialLoading, isInitialLoading ? this.db : this.convertDataToDB(res)); + this.onDataLoadSuccess(isInitialLoading, isInitialLoading ? this.db : this.convertDataToDB(res)); }) .catch((err) => { this.componentInternalState.setIsLoadingInProgress(false); - this.onInitLoadError(isInitialLoading); + this.onDataLoadError(isInitialLoading); throw err; }); @@ -360,9 +93,10 @@ export default class bScrolly extends iData implements iItems { return >initLoadResult; } + /** * Initializes the loading of the next data chunk. - * @param args + * @throws {@link ReferenceError} if there is no `dataProvider` set. */ initLoadNext(): Promise { if (!this.dataProvider) { @@ -373,6 +107,13 @@ export default class bScrolly extends iData implements iItems { return this.dataProvider.get(params[0], params[1]); } + /** + * Resets the component state to its initial state. + */ + reset(): void { + this.onReset(); + } + /** * Renders the next data chunk to the page (ignores the `client` check for render possibility). */ @@ -410,7 +151,7 @@ export default class bScrolly extends iData implements iItems { /** * Wrapper for {@link bScrolly.shouldStopRequestingData}. */ - shouldStopRequestingDataWrapper(): boolean { + shouldStopRequestingDataWrapper(this: bScrolly): boolean { const state = this.getComponentState(); if (state.isRequestsStopped) { @@ -426,54 +167,95 @@ export default class bScrolly extends iData implements iItems { /** * Wrapper for {@link bScrolly.shouldPerformDataRequest}. */ - shouldPerformDataRequestWrapper(): boolean { + shouldPerformDataRequestWrapper(this: bScrolly): boolean { return this.shouldPerformDataRequest(this.getComponentState(), this); } + protected override convertDataToDB(data: unknown): O | this['DB'] { + const result = super.convertDataToDB(data); + this.onConvertDataToDB(data); + + return result; + } + /** - * Resets the component state and the state of the component modules. + * Renders components using {@link bScrolly.componentFactory} and inserts them into the DOM tree. + * {@link bScrolly.componentFactory}, in turn, calls {@link bScrolly.itemsFactory} to obtain + * the set of components to render. */ - protected reset(): void { - this.componentEmitter.emit(componentLocalEvents.resetState); - } + protected performRender(): void { + this.onRenderStart(); - protected override convertDataToDB(data: unknown): O | this['DB'] { - this.componentEmitter.emit(componentLocalEvents.convertDataToDB, data); - return super.convertDataToDB(data); + const + items = this.componentFactory.produceComponentItems(), + nodes = this.componentFactory.produceNodes(items), + mounted = this.componentFactory.produceMounted(items, nodes); + + this.componentInternalState.updateMounted(mounted); + this.observer.observe(mounted); + + this.onDomInsertStart(); + + const + fragment = document.createDocumentFragment(); + + for (let i = 0; i < nodes.length; i++) { + this.dom.appendChild(fragment, nodes[i], { + group: bScrollyDomInsertAsyncGroup, + destroyIfComponent: true + }); + } + + this.async.requestAnimationFrame(() => { + this.$refs.container.appendChild(fragment); + + this.onDomInsertDone(); + this.onRenderDone(); + + }, {label: $$.insertDomRaf, group: bScrollyDomInsertAsyncGroup}); } /** - * Handler: data load successfully finished. + * A function that performs actions (data loading/rendering) depending + * on the result of the {@link bScrolly.renderGuard} method. * - * @param isInitialLoading - `true` if this load was an initial component loading. - * @param data - * - * @throws {@link ReferenceError} if there is not `data` field in the loaded data. + * This function is the "starting point" for rendering components and is called after successful data loading + * or when rendered items enter the viewport. */ - protected onInitLoadSuccess(isInitialLoading: boolean, data: unknown): void { - if (!Object.isPlainObject(data) || !Array.isArray(data.data)) { - throw new ReferenceError('Missing "data" field in the loaded data'); + protected loadDataOrPerformRender(): void { + const + state = this.getComponentState(), + {result, reason} = this.renderGuard(state, this); + + if (result) { + return this.performRender(); } - this.componentInternalState.updateData(data.data, isInitialLoading); - this.shouldStopRequestingDataWrapper(); + if (reason === renderGuardRejectionReason.done) { + this.onLifecycleDone(); + return; + } - this.componentEmitter.emit(componentDataLocalEvents.dataLoadSuccess, data.data, isInitialLoading); + if (reason === renderGuardRejectionReason.noData) { + if (state.isRequestsStopped) { + return; + } - if (isInitialLoading && Object.size(data.data) === 0) { - if (this.shouldStopRequestingDataWrapper()) { - this.componentEmitter.emit(componentDataLocalEvents.dataEmpty, isInitialLoading); + if (this.shouldPerformDataRequestWrapper()) { + void this.initLoad(); } } - } - /** - * Handler: failed to load data. - * - * @param isInitialLoading - `true` if this load was an initial component loading. - */ - protected onInitLoadError(isInitialLoading: boolean): void { - this.componentEmitter.emit(componentDataLocalEvents.dataLoadError, isInitialLoading); - } + if (reason === renderGuardRejectionReason.notEnoughData) { + if (state.isRequestsStopped) { + this.performRender(); + + } else if (this.shouldPerformDataRequestWrapper()) { + void this.initLoad(); + } else if (state.isInitialRender) { + this.performRender(); + } + } + } } diff --git a/src/components/base/b-scrolly/const.ts b/src/components/base/b-scrolly/const.ts index 0a90065580..96c47fdc8b 100644 --- a/src/components/base/b-scrolly/const.ts +++ b/src/components/base/b-scrolly/const.ts @@ -22,6 +22,16 @@ import type { } from 'components/base/b-scrolly/interface'; +/** + * Base group for performing asynchronous operations of the component. + */ +export const bScrollyAsyncGroup = 'b-scrolly'; + +/** + * Group for asynchronous operations related to inserting nodes into the DOM tree. + */ +export const bScrollyDomInsertAsyncGroup = `${bScrollyAsyncGroup}:dom-insert`; + /** * {@link ComponentRenderStrategy} */ diff --git a/src/components/base/b-scrolly/handlers.ts b/src/components/base/b-scrolly/handlers.ts new file mode 100644 index 0000000000..ea9efda8fd --- /dev/null +++ b/src/components/base/b-scrolly/handlers.ts @@ -0,0 +1,198 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import bScrollyProps from 'components/base/b-scrolly/props'; +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import { bScrollyAsyncGroup, componentEvents } from 'components/base/b-scrolly/const'; +import { component } from 'components/super/i-data/i-data'; +import type { MountedChild } from 'components/base/b-scrolly/interface'; + +/** + * A class that provides an API to handle events emitted by the {@link bScrolly} component. + * This class is designed to work in conjunction with {@link bScrolly}. + */ +@component() +export abstract class bScrollyHandlers extends bScrollyProps { + + /** + * Handler: component reset event. + * Resets the component state to its initial state. + */ + onReset(): void { + this.componentInternalState.reset(); + this.observer.reset(); + + this.async.clearAll({group: new RegExp(bScrollyAsyncGroup)}); + + this.componentEmitter.emit(componentEvents.resetState); + } + + /** + * Handler: render start event. + * Triggered when the component rendering starts. + */ + onRenderStart(): void { + this.componentEmitter.emit(componentEvents.renderStart); + } + + /** + * Handler: render engine start event. + * Triggered when the component rendering using the rendering engine starts. + */ + onRenderEngineStart(): void { + this.componentEmitter.emit(componentEvents.renderEngineStart); + } + + /** + * Handler: render engine done event. + * Triggered when the component rendering using the rendering engine is completed. + */ + onRenderEngineDone(): void { + this.componentEmitter.emit(componentEvents.renderEngineDone); + } + + /** + * Handler: DOM insert start event. + * Triggered when the insertion of rendered components into the DOM tree starts. + */ + onDomInsertStart(): void { + this.componentInternalState.setIsInitialRender(false); + this.componentInternalState.incrementRenderPage(); + + this.componentEmitter.emit(componentEvents.domInsertStart); + } + + /** + * Handler: DOM insert done event. + * Triggered when the insertion of rendered components into the DOM tree is completed. + */ + onDomInsertDone(): void { + this.componentEmitter.emit(componentEvents.domInsertDone); + } + + /** + * Handler: render done event. + * Triggered when rendering is completed. + */ + onRenderDone(): void { + this.componentEmitter.emit(componentEvents.renderDone); + } + + /** + * Handler: lifecycle done event. + * Triggered when the internal lifecycle of the component is completed. + */ + onLifecycleDone(this: bScrolly): void { + const + state = this.getComponentState(); + + if (state.isLifecycleDone === true) { + return; + } + + this.slotsStateController.doneState(); + this.componentInternalState.setIsLifecycleDone(true); + this.componentEmitter.emit(componentEvents.lifecycleDone); + } + + /** + * Handler: convert data to database event. + * Triggered when the loaded data is converted. + * + * @param data - The converted data. + */ + onConvertDataToDB(data: unknown): void { + this.componentInternalState.setRawLastLoaded(data); + this.componentEmitter.emit(componentEvents.convertDataToDB, data); + } + + /** + * Handler: data load start event. + * Triggered when data loading starts. + * + * @param isInitialLoading - Indicates whether it is an initial component loading. + */ + onDataLoadStart(isInitialLoading: boolean): void { + this.slotsStateController.loadingProgressState(); + this.componentInternalState.incrementLoadPage(); + + this.componentEmitter.emit(componentEvents.dataLoadStart, isInitialLoading); + } + + /** + * Handler: data load success event. + * Triggered when data loading is successfully completed. + * + * @param isInitialLoading - Indicates whether it is an initial component loading. + * @param data - The loaded data. + * @throws {@link ReferenceError} if the loaded data does not have a "data" field. + */ + onDataLoadSuccess(this: bScrolly, isInitialLoading: boolean, data: unknown): void { + this.componentInternalState.setIsLoadingInProgress(false); + + if (!Object.isPlainObject(data) || !Array.isArray(data.data)) { + throw new ReferenceError('Missing "data" field in the loaded data'); + } + + this.componentInternalState.updateData(data.data, isInitialLoading); + this.shouldStopRequestingDataWrapper(); + this.componentEmitter.emit(componentEvents.dataLoadSuccess, data.data, isInitialLoading); + + this.slotsStateController.loadingSuccessState(); + this.loadDataOrPerformRender(); + + if (isInitialLoading && Object.size(data.data) === 0) { + if (this.shouldStopRequestingDataWrapper()) { + this.onDataEmpty(isInitialLoading); + } + } + } + + /** + * Handler: data load error event. + * Triggered when data loading fails. + * + * @param isInitialLoading - Indicates whether it is an initial component loading. + */ + onDataLoadError(isInitialLoading: boolean): void { + this.slotsStateController.loadingFailedState(); + + this.componentEmitter.emit(componentEvents.dataLoadError, isInitialLoading); + } + + /** + * Handler: data empty event. + * Triggered when the loaded data is empty. + * + * @param isInitialLoading - Indicates whether it is an initial component loading. + */ + onDataEmpty(isInitialLoading: boolean): void { + this.slotsStateController.emptyState(); + + this.componentEmitter.emit(componentEvents.dataEmpty, isInitialLoading); + } + + /** + * Handler: component enters the viewport. + * @param component - The component that enters the viewport. + */ + onElementEnters(this: bScrolly, component: MountedChild): void { + this.componentInternalState.setMaxViewedIndex(component); + this.loadDataOrPerformRender(); + + this.componentEmitter.emit(componentEvents.elementEnter, component); + } + + /** + * Handler: component leaves the viewport. + * @param _component - The component that leaves the viewport. + */ + onElementOut(_component: MountedChild): void { + // ... + } +} diff --git a/src/components/base/b-scrolly/modules/emitter/index.ts b/src/components/base/b-scrolly/modules/emitter/index.ts index c39d5ae5b0..aafe1bb0ff 100644 --- a/src/components/base/b-scrolly/modules/emitter/index.ts +++ b/src/components/base/b-scrolly/modules/emitter/index.ts @@ -15,7 +15,7 @@ import type { ComponentTypedEmitter } from 'components/base/b-scrolly/modules/em export * from 'components/base/b-scrolly/modules/emitter/interface'; /** - * Provides methods for interacting with the `localEmitter` using typed events. + * Provides methods for interacting with the `selfEmitter` using typed events. * @param ctx */ export function componentTypedEmitter(ctx: bScrolly): ComponentTypedEmitter { @@ -24,7 +24,7 @@ export function componentTypedEmitter(ctx: bScrolly): ComponentTypedEmitter { handler: (...args: LocalEventPayload) => void, asyncOpts?: AsyncOptions ) => { - ctx.unsafe.localEmitter.once(event, handler, asyncOpts); + ctx.once(event, handler, asyncOpts); }; const on = ( @@ -32,19 +32,19 @@ export function componentTypedEmitter(ctx: bScrolly): ComponentTypedEmitter { handler: (...args: LocalEventPayload) => void, asyncOpts?: AsyncOptions ) => { - ctx.unsafe.localEmitter.on(event, handler, asyncOpts); + ctx.on(event, handler, asyncOpts); }; const promisifyOnce = ( event: EVENT, asyncOpts?: AsyncOptions - ) => ctx.unsafe.localEmitter.promisifyOnce(event, asyncOpts); + ) => ctx.promisifyOnce(event, asyncOpts); const emit = ( event: EVENT, ...payload: LocalEventPayload ) => { - ctx.unsafe.localEmitter.emit(event, ...payload); + ctx.emit(event, ...payload); }; return { diff --git a/src/components/base/b-scrolly/modules/emitter/interface.ts b/src/components/base/b-scrolly/modules/emitter/interface.ts index 3b8ef38c8b..96e6c39099 100644 --- a/src/components/base/b-scrolly/modules/emitter/interface.ts +++ b/src/components/base/b-scrolly/modules/emitter/interface.ts @@ -10,7 +10,7 @@ import type { AsyncOptions } from 'core/async'; import type { ComponentEvents, LocalEventPayload } from 'components/base/b-scrolly/interface'; /** - * An interface representing the typed `localEmitter` methods. + * An interface representing the typed `selfEmitter` methods. */ export interface ComponentTypedEmitter { /** diff --git a/src/components/base/b-scrolly/modules/factory/index.ts b/src/components/base/b-scrolly/modules/factory/index.ts index e88c3aed90..a7f944ca3e 100644 --- a/src/components/base/b-scrolly/modules/factory/index.ts +++ b/src/components/base/b-scrolly/modules/factory/index.ts @@ -10,8 +10,8 @@ import Friend from 'components/friends/friend'; import type { VNodeDescriptor } from 'components/friends/vdom'; import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { ComponentItem } from 'components/base/b-scrolly/interface'; -import { componentRenderLocalEvents, componentRenderStrategy } from 'components/base/b-scrolly/const'; +import type { ComponentItem, MountedChild, MountedItem } from 'components/base/b-scrolly/interface'; +import { componentItemType, componentRenderStrategy } from 'components/base/b-scrolly/const'; import * as forceUpdate from 'components/base/b-scrolly/modules/factory/engines/force-update'; import * as vdomRender from 'components/base/b-scrolly/modules/factory/engines/vdom'; @@ -50,6 +50,35 @@ export class ComponentFactory extends Friend { return this.callRenderEngine(descriptors); } + /** + * Augments `ComponentItem` with various properties such as the component node, item index, and child index. + * + * @param items + * @param nodes + */ + produceMounted(items: ComponentItem[], nodes: HTMLElement[]): Array { + const + {ctx} = this, + {items: mountedItems, childList} = ctx.getComponentState(); + + return items.map((item, i) => { + if (item.type === componentItemType.item) { + return { + ...item, + node: nodes[i], + itemIndex: mountedItems.length + i, + childIndex: childList.length + i + }; + } + + return { + ...item, + node: nodes[i], + childIndex: mountedItems.length + i + }; + }); + } + /** * Calls the render engine to render the components based on the provided descriptors. * Returns an array of rendered DOM nodes. @@ -61,15 +90,16 @@ export class ComponentFactory extends Friend { {ctx} = this; let res; - ctx.componentEmitter.emit(componentRenderLocalEvents.renderEngineStart); + ctx.onRenderEngineStart(); if (ctx.componentRenderStrategy === componentRenderStrategy.reuse) { res = forceUpdate.render(ctx, descriptors); + } else { res = vdomRender.render(ctx, descriptors); } - ctx.componentEmitter.emit(componentRenderLocalEvents.renderEngineDone); + ctx.onRenderEngineDone(); return res; } } diff --git a/src/components/base/b-scrolly/modules/juggler/index.ts b/src/components/base/b-scrolly/modules/juggler/index.ts deleted file mode 100644 index f91fa12a2a..0000000000 --- a/src/components/base/b-scrolly/modules/juggler/index.ts +++ /dev/null @@ -1,196 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import symbolGenerator from 'core/symbol'; - -import Friend from 'components/friends/friend'; - -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { ComponentItem } from 'components/base/b-scrolly/b-scrolly'; -import { renderGuardRejectionReason, componentDataLocalEvents, componentItemType, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; -import type { MountedChild, MountedItem } from 'components/base/b-scrolly/interface'; - -export const - $$ = symbolGenerator(), - jugglerAsyncGroup = '[[JUGGLER]]'; - -/** - * A class that is friendly to `bScrolly`. - * Provides an API for initializing various component modules and inserting components into the DOM tree. - */ -export class Juggler extends Friend { - - /** - * {@link bScrolly} - */ - override readonly C!: bScrolly; - - /** - * @param ctx - */ - constructor(ctx: bScrolly) { - super(ctx); - - const - {componentEmitter} = ctx; - - componentEmitter.on(componentObserverLocalEvents.elementEnter, (component) => this.onElementEnters(component)); - componentEmitter.on(componentObserverLocalEvents.elementOut, (component) => this.onElementOut(component)); - componentEmitter.on(componentLocalEvents.resetState, () => this.reset()); - componentEmitter.on(componentDataLocalEvents.dataLoadSuccess, () => this.onDataLoaded()); - } - - /** - * Resets the module's state to its initial state. - */ - protected reset(): void { - const - {ctx} = this; - - ctx.async.clearAll({group: new RegExp(jugglerAsyncGroup)}); - } - - /** - * Renders components using `componentFactory` and inserts them into the DOM tree. - * `componentFactory`, in turn, calls `itemsFactory` to obtain the set of components to render. - */ - protected performRender(): void { - const - {ctx, refs} = this; - - ctx.componentEmitter.emit(componentRenderLocalEvents.renderStart); - - const - items = ctx.componentFactory.produceComponentItems(), - nodes = ctx.componentFactory.produceNodes(items), - mounted = this.produceMounted(items, nodes); - - ctx.componentInternalState.updateMounted(mounted); - ctx.observer.observe(mounted); - - ctx.componentEmitter.emit(componentRenderLocalEvents.domInsertStart); - - const - fragment = document.createDocumentFragment(); - - for (let i = 0; i < nodes.length; i++) { - this.dom.appendChild(fragment, nodes[i], { - group: jugglerAsyncGroup, - destroyIfComponent: true - }); - } - - ctx.async.requestAnimationFrame(() => { - refs.container.appendChild(fragment); - - ctx.componentEmitter.emit(componentRenderLocalEvents.domInsertDone); - ctx.componentEmitter.emit(componentRenderLocalEvents.renderDone); - - }, {label: $$.insertDomRaf, group: jugglerAsyncGroup}); - } - - /** - * Augments `ComponentItem` with various properties such as the component node, item index, and child index. - * - * @param items - * @param nodes - */ - protected produceMounted(items: ComponentItem[], nodes: HTMLElement[]): Array { - const - {ctx} = this, - {items: mountedItems, childList} = ctx.getComponentState(); - - return items.map((item, i) => { - if (item.type === componentItemType.item) { - return { - ...item, - node: nodes[i], - itemIndex: mountedItems.length + i, - childIndex: childList.length + i - }; - } - - return { - ...item, - node: nodes[i], - childIndex: mountedItems.length + i - }; - }); - } - - /** - * A function that performs actions (data loading/rendering) depending on the result of the `renderGuard` method. - * - * This function is the "starting point" for rendering components and is called after successful data loading - * or when rendered items enter the viewport. - */ - protected loadDataOrPerformRender(): void { - const - {ctx} = this, - state = ctx.getComponentState(), - {result, reason} = ctx.renderGuard(state, ctx); - - if (result) { - return this.performRender(); - } - - if (reason === renderGuardRejectionReason.done) { - ctx.componentInternalState.setIsLifecycleDone(true); - return; - } - - if (reason === renderGuardRejectionReason.noData) { - if (state.isRequestsStopped) { - return; - } - - if (ctx.shouldPerformDataRequestWrapper()) { - void ctx.initLoad(); - } - } - - if (reason === renderGuardRejectionReason.notEnoughData) { - if (state.isRequestsStopped) { - this.performRender(); - - } else if (ctx.shouldPerformDataRequestWrapper()) { - void ctx.initLoad(); - - } else if (state.isInitialRender) { - this.performRender(); - } - } - } - - /** - * Handler: successful data loading. - */ - protected onDataLoaded(): void { - this.loadDataOrPerformRender(); - } - - /** - * Handler: component enters the viewport. - * @param component - */ - protected onElementEnters(component: MountedChild): void { - const - {ctx} = this; - - ctx.componentInternalState.setMaxViewedIndex(component); - this.loadDataOrPerformRender(); - } - - /** - * Handler: component leaves the viewport. - * @param _component - */ - protected onElementOut(_component: MountedChild): void { - // ... - } -} diff --git a/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts b/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts index b30169127f..b05014abf0 100644 --- a/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts +++ b/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts @@ -7,7 +7,6 @@ */ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import { componentLocalEvents, componentObserverLocalEvents } from 'components/base/b-scrolly/const'; import type { MountedChild } from 'components/base/b-scrolly/interface'; import { observerAsyncGroup } from 'components/base/b-scrolly/modules/observer/const'; import type { ObserverEngine } from 'components/base/b-scrolly/modules/observer/interface'; @@ -20,15 +19,6 @@ export default class IoObserver extends Friend implements ObserverEngine { */ override readonly C!: bScrolly; - /** - * @param ctx - */ - constructor(ctx: bScrolly) { - super(ctx); - - ctx.componentEmitter.on(componentLocalEvents.resetState, () => this.reset()); - } - /** * @inheritdoc */ @@ -42,7 +32,7 @@ export default class IoObserver extends Friend implements ObserverEngine { label: component.key, once: true, delay: 0 - }, () => ctx.componentEmitter.emit(componentObserverLocalEvents.elementEnter, component)); + }, () => ctx.onElementEnters(component)); } } diff --git a/src/components/base/b-scrolly/modules/observer/index.ts b/src/components/base/b-scrolly/modules/observer/index.ts index 4bf5abe031..58b267e0f6 100644 --- a/src/components/base/b-scrolly/modules/observer/index.ts +++ b/src/components/base/b-scrolly/modules/observer/index.ts @@ -53,4 +53,11 @@ export class Observer extends Friend { this.engine.watchForIntersection(mounted); } + + /** + * Resets the module state. + */ + reset(): void { + this.engine.reset(); + } } diff --git a/src/components/base/b-scrolly/modules/slots/index.ts b/src/components/base/b-scrolly/modules/slots/index.ts index bb500e061e..364eb049e0 100644 --- a/src/components/base/b-scrolly/modules/slots/index.ts +++ b/src/components/base/b-scrolly/modules/slots/index.ts @@ -11,7 +11,6 @@ import type { AsyncOptions } from 'core/async'; import Friend from 'components/friends/friend'; import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import { componentDataLocalEvents, componentLocalEvents } from 'components/base/b-scrolly/const'; import type { SlotsStateObj } from 'components/base/b-scrolly/modules/slots/interface'; export * from 'components/base/b-scrolly/modules/slots/interface'; @@ -35,23 +34,6 @@ export class SlotsStateController extends Friend { group: slotsStateControllerAsyncGroup }; - /** - * @param ctx - */ - constructor(ctx: bScrolly) { - super(ctx); - - const - {componentEmitter} = ctx; - - componentEmitter.on(componentDataLocalEvents.dataLoadError, () => this.loadingFailedState()); - componentEmitter.on(componentDataLocalEvents.dataLoadStart, () => this.loadingProgressState()); - componentEmitter.on(componentDataLocalEvents.dataLoadSuccess, () => this.loadingSuccessState()); - componentEmitter.on(componentDataLocalEvents.dataEmpty, () => this.emptyState()); - componentEmitter.on(componentLocalEvents.lifecycleDone, () => this.doneState()); - componentEmitter.on(componentLocalEvents.resetState, () => this.reset()); - } - /** * Displays the slots that should be shown when the data state is empty. */ diff --git a/src/components/base/b-scrolly/modules/state/index.ts b/src/components/base/b-scrolly/modules/state/index.ts index 027f5ac461..7792855ddf 100644 --- a/src/components/base/b-scrolly/modules/state/index.ts +++ b/src/components/base/b-scrolly/modules/state/index.ts @@ -7,7 +7,6 @@ */ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import { componentDataLocalEvents, componentLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; import type { MountedChild, ComponentState, MountedItem } from 'components/base/b-scrolly/interface'; import { isItem } from 'components/base/b-scrolly/modules/helpers'; import { createInitialState } from 'components/base/b-scrolly/modules/state/helpers'; @@ -22,26 +21,7 @@ export class ComponentInternalState extends Friend { /** * Current state of the component. */ - protected state: ComponentState = createInitialState(); - - /** - * @param ctx - */ - constructor(ctx: bScrolly) { - super(ctx); - - const - {componentEmitter} = ctx; - - componentEmitter.on(componentDataLocalEvents.dataLoadStart, () => this.incrementLoadPage()); - componentEmitter.on(componentLocalEvents.convertDataToDB, (...args) => this.setRawLastLoaded(...args)); - componentEmitter.on(componentLocalEvents.resetState, (...args) => this.reset(...args)); - - componentEmitter.on(componentRenderLocalEvents.domInsertStart, () => { - this.setIsInitialRender(false); - this.incrementRenderPage(); - }); - } + state: ComponentState = createInitialState(); /** * Compiles and returns the current state of the component. @@ -137,21 +117,7 @@ export class ComponentInternalState extends Friend { * @param value - The value of the flag. */ setIsLifecycleDone(value: boolean): void { - const - {state} = this; - - if (state.isLifecycleDone === value) { - return; - } - - const - {ctx} = this; - - state.isLifecycleDone = value; - - if (value) { - ctx.componentEmitter.emit(componentLocalEvents.lifecycleDone); - } + this.state.isLifecycleDone = value; } /** diff --git a/src/components/base/b-scrolly/props.ts b/src/components/base/b-scrolly/props.ts new file mode 100644 index 0000000000..17ce753368 --- /dev/null +++ b/src/components/base/b-scrolly/props.ts @@ -0,0 +1,278 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type iItems from 'components/traits/i-items/i-items'; +import type { CreateFromItemFn } from 'components/traits/i-items/i-items'; + +import type { + + ComponentState, + ComponentDb, + ComponentRenderStrategy, + RequestQueryFn, + ShouldPerform, + ComponentItemFactory, + ComponentItemType, + ComponentStrategy, + RenderGuardResult, + ComponentRefs + +} from 'components/base/b-scrolly/interface'; + +import { + + componentRenderStrategy, + defaultShouldProps, + componentItemType, + componentStrategy + +} from 'components/base/b-scrolly/const'; + +import iData, { component, prop, system } from 'components/super/i-data/i-data'; +import { chunkSizePreset } from 'components/base/b-scrolly/modules/presets/chunk-size'; +import { ComponentTypedEmitter, componentTypedEmitter } from 'components/base/b-scrolly/modules/emitter'; +import { SlotsStateController } from 'components/base/b-scrolly/modules/slots'; +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import { ComponentFactory } from 'components/base/b-scrolly/modules/factory'; +import { Observer } from 'components/base/b-scrolly/modules/observer'; +import { ComponentInternalState } from 'components/base/b-scrolly/modules/state'; + +/** + * A class that is friendly to {@link bScrolly}. It contains the properties of the {@link bScrolly} component. + */ +@component() +export default abstract class bScrollyProps extends iData implements iItems { + /** {@link iItems.item} */ + readonly Item!: object; + + /** {@link iItems.Items} */ + readonly Items!: Array; + + /** {@link iItems.item} */ + @prop({type: [String, Function]}) + readonly item?: iItems['item']; + + /** {@link iItems.itemKey} */ + @prop({type: [String, Function]}) + readonly itemKey?: CreateFromItemFn; + + /** {@link ComponentItemType} */ + @prop({type: [String, Function]}) + readonly itemType: keyof ComponentItemType | CreateFromItemFn = componentItemType.item; + + /** {@link iItems.itemProps} */ + @prop({type: [Function, Object], default: () => ({})}) + readonly itemProps!: iItems['itemProps']; + + /** + * Specifies the number of times the `tombstone` component will be rendered. + * + * This prop can be useful if you want to render multiple `tombstone` components + * using a single specified element. For example, if you set `tombstonesSize` to 3, + * then three `tombstone` components will be rendered on your page. + */ + @prop(Number) + readonly tombstonesSize?: number; + + /** + * This factory function is used to pass information about the components that need to be rendered. + * The function should return an array of arbitrary length consisting of objects that satisfy the + * `ComponentItem` interface. + * + * By default, the rendering strategy is based on the `chunkSize` and `iItems` trait. + * In other words, the default implementation takes a data slice of length `chunkSize` + * and calls the `iItems` functions to generate a `ComponentItem` object. + * + * However, nothing prevents the client from implementing any strategy by overriding this function. + * + * For example, it is possible to define a function + * that takes the last loaded data and draws twice as many components: + * + * @example + * ```typescript + * const itemsFactory = (state) => { + * const data = state.lastLoadedData; + * + * const items = data.map((item) => ({ + * item: 'section', + * key: Object.cast(undefined), + * type: 'item', + * children: [], + * props: { + * 'data-index': item.i + * } + * })); + * + * return [...items, ...items]; + * } + * ``` + */ + @prop({ + type: Function, + default: (state: ComponentState, ctx: bScrollyProps) => { + if (ctx.chunkSize == null) { + throw new Error('chunkSize.getNextDataSlice is used but chunkSize prop is not settled'); + } + + const descriptors = chunkSizePreset.getNextDataSlice(state, ctx.chunkSize).map((data, i) => ({ + key: ctx.itemKey?.(data, i), + + item: Object.isFunction(ctx.item) ? ctx.item(data, i) : ctx.item, + type: Object.isFunction(ctx.itemType) ? ctx.itemType(data, i) : ctx.itemType, + + props: Object.isFunction(ctx.itemProps) ? + ctx.itemProps(data, i, { + key: ctx.itemKey?.(data, i), + ctx + }) : + ctx.itemProps + })); + + return descriptors; + } + }) + + readonly itemsFactory!: ComponentItemFactory; + + override readonly DB!: ComponentDb; + + /** + * The rendering strategy of components. + * Determines which approach will be taken for rendering components within the rendering engine. + * + * * `default` - The default approach, + * which creates a new instance of the rendering engine each time a new rendering is performed. + * + * * `reuse` - An approach + * that reuses the current instance of the rendering engine whenever a new rendering is performed. + * + * {@link ComponentRenderStrategy} + */ + @prop({type: String, validator: (v) => Object.isString(v) && componentRenderStrategy.hasOwnProperty(v)}) + readonly componentRenderStrategy: keyof ComponentRenderStrategy = componentRenderStrategy.default; + + /** + * Strategies for component operation modes. + * {@link ComponentStrategy} + */ + @prop({type: String, validator: (v) => Object.isString(v) && componentStrategy.hasOwnProperty(v)}) + readonly componentStrategy: keyof ComponentStrategy = componentStrategy.intersectionObserver; + + /** + * Function that returns the GET parameters for a request. + * {@link RequestQueryFn} + */ + @prop({type: Function}) + readonly requestQuery?: RequestQueryFn; + + /** + * The number of elements to render at once. + * This prop is used in conjunction with `renderGuard` and `chunkSize` preset. + */ + @prop({type: Number, validator: Number.isNatural}) + readonly chunkSize?: number = 10; + + /** + * When this function returns `true` the component will stop to request new data. + * This function will be called on each data loading cycle. + */ + @prop({ + type: Function, + default: defaultShouldProps.shouldStopRequestingData + }) + + readonly shouldStopRequestingData!: ShouldPerform; + + /** + * When this function returns `true` the component will be able to request additional data. + * This function will be called on each new element enters the viewport. + */ + @prop({ + type: Function, + default: defaultShouldProps.shouldPerformDataRequest + }) + + readonly shouldPerformDataRequest!: ShouldPerform; + + /** + * This function is called after successful data loading or when the component enters the visible area. + * + * This function asks the client whether rendering can be performed. The client responds with an object + * indicating whether rendering is allowed or the reason for denial. The client's response should be an object + * of type {@link RenderGuardResult}. + * + * Based on the result of this function, the component takes appropriate actions. For example, + * it may load data if it is not sufficient for rendering, or perform rendering if all conditions are met. + * + * By default, the {@link chunkSizePreset.renderGuard} strategy is used, + * which already implements the mechanism for communication with the component. + */ + @prop({ + type: Function, + default: (state: ComponentState, ctx: bScrolly) => { + if (ctx.chunkSize == null) { + throw new Error('The "ChunkSize.renderGuard" preset is active, but the "chunkSize" prop is not set.'); + } + + return chunkSizePreset.renderGuard(state, ctx, ctx.chunkSize); + } + }) + + readonly renderGuard!: ShouldPerform; + + /** + * This function is called in the `renderGuard` after other checks are completed. + * + * This function receives the component state as input, based on which the client + * should determine whether the component should render the next chunk of components. + * + * For example, if we want to render the next data chunk only when the client + * has seen all the main components, we can implement the following function: + * + * @example + * ```typescript + * const shouldPerformDataRender = (state) => { + * return state.isInitialRender || state.itemsTillEnd === 0; + * } + * ``` + */ + @prop(Function) + readonly shouldPerformDataRender?: ShouldPerform; + + /** + * If `true`, the element observation module will not be initialized. + * + * Setting this prop to `true` can be useful if you want to implement lazy rendering + * and control it using the `renderNext` method. + */ + @prop(Boolean) + readonly disableObserver: boolean = false; + + /** {@link componentTypedEmitter} */ + @system((ctx) => componentTypedEmitter(ctx)) + readonly componentEmitter!: ComponentTypedEmitter; + + /** {@link SlotsStateController} */ + @system((ctx) => new SlotsStateController(ctx)) + readonly slotsStateController!: SlotsStateController; + + /** {@link ComponentInternalState} */ + @system((ctx) => new ComponentInternalState(ctx)) + readonly componentInternalState!: ComponentInternalState; + + /** {@link ComponentFactory} */ + @system((ctx) => new ComponentFactory(ctx)) + readonly componentFactory!: ComponentFactory; + + /** {@link Observer} */ + @system((ctx) => new Observer(ctx)) + readonly observer!: Observer; + + protected override readonly $refs!: iData['$refs'] & ComponentRefs; +} + diff --git a/src/components/base/b-scrolly/test/api/component-object/index.ts b/src/components/base/b-scrolly/test/api/component-object/index.ts index 7036e79f61..a1e016da13 100644 --- a/src/components/base/b-scrolly/test/api/component-object/index.ts +++ b/src/components/base/b-scrolly/test/api/component-object/index.ts @@ -31,7 +31,7 @@ export class ScrollyComponentObject extends ComponentObject { readonly childList: Locator; /** - * @param page The Playwright page instance. + * @param page - The Playwright page instance. */ constructor(page: Page) { super(page, 'b-scrolly'); @@ -43,7 +43,7 @@ export class ScrollyComponentObject extends ComponentObject { /** * Overrides the build method to add test styles before building the component. * - * @param args The arguments for the build method. + * @param args - The arguments for the build method. */ override async build(...args: Parameters['build']>): Promise> { await this.page.addStyleTag({content: testStyles}); @@ -75,7 +75,7 @@ export class ScrollyComponentObject extends ComponentObject { * Waits for the container child count to be equal to N. * Throws an error if there are more items in the child list than expected. * - * @param n The expected child count. + * @param n - The expected child count. */ async waitForContainerChildCountEqualsTo(n: number): Promise { await this.childList.nth(n - 1).waitFor({state: 'attached'}); @@ -105,9 +105,9 @@ export class ScrollyComponentObject extends ComponentObject { /** * Waits for the provided slot to reach the specified visibility state. * - * @param slotName The name of the slot. - * @param isVisible The expected visibility state. - * @param timeout The timeout for waiting (optional). + * @param slotName - The name of the slot. + * @param isVisible - The expected visibility state. + * @param timeout - The timeout for waiting (optional). */ async waitForSlotState(slotName: keyof ComponentRefs, isVisible: boolean, timeout?: number): Promise { const slot = this.node.locator(this.elSelector(slotName)); @@ -162,7 +162,7 @@ export class ScrollyComponentObject extends ComponentObject { /** * Adds a `requestProp` for pagination. * - * @param requestParams The request parameters. + * @param requestParams - The request parameters. */ async withRequestPaginationProps(requestParams: Dictionary = {}): Promise { await this.setProps({ @@ -192,7 +192,7 @@ export class ScrollyComponentObject extends ComponentObject { * - `withPaginationItemProps` * - `withRequestProp` * - * @param requestParams The request parameters. + * @param requestParams - The request parameters. */ async withDefaultPaginationProviderProps(requestParams: Dictionary = {}): Promise { await this.withPaginationProvider(); diff --git a/src/components/base/b-scrolly/test/api/helpers/index.ts b/src/components/base/b-scrolly/test/api/helpers/index.ts index b7cb266ec7..8ab45aa615 100644 --- a/src/components/base/b-scrolly/test/api/helpers/index.ts +++ b/src/components/base/b-scrolly/test/api/helpers/index.ts @@ -67,7 +67,8 @@ export function createDataConveyor( let dataI = 0, itemsI = 0, - childI = 0; + childI = 0, + page = 0; const obj: DataConveyor = { addData(count: number) { @@ -77,6 +78,8 @@ export function createDataConveyor( dataChunks.push(newData); dataI = data.length; + page++; + return newData; }, @@ -144,8 +147,12 @@ export function createDataConveyor( return data; }, + get page() { + return page; + }, + get lastLoadedData() { - return dataChunks[dataChunks.length - 1]; + return dataChunks[dataChunks.length - 1] ?? []; } }; @@ -218,11 +225,11 @@ export function createInitialState(state: Partial): ComponentSta */ export function extractStateFromDataConveyor(conveyor: DataConveyor): Pick { return { - data: conveyor.data, - lastLoadedData: conveyor.lastLoadedData, - lastLoadedRawData: {data: conveyor.lastLoadedData}, - items: conveyor.items, - childList: conveyor.childList + data: [...conveyor.data], + lastLoadedData: [...conveyor.lastLoadedData], + lastLoadedRawData: conveyor.page === 0 ? undefined : {data: [...conveyor.lastLoadedData]}, + items: [...conveyor.items], + childList: [...conveyor.childList] }; } @@ -305,10 +312,31 @@ export function createIndexedObj(i: number): IndexedObj { * It only keeps component events, excluding observer-like events. * * @param emitCalls - The array of emit calls. - * @param filterObserverEvents - Whether to filter out observer events (default: true). + * @param [filterObserverEvents] - Whether to filter out observer events (default: true). */ -export function filterEmitterCalls(emitCalls: unknown[][], filterObserverEvents: boolean = true): unknown[][] { +export function filterEmitterCalls( + emitCalls: unknown[][], + filterObserverEvents: boolean = true +): unknown[][] { return emitCalls.filter(([event]) => Object.isString(event) && Boolean(componentEvents[event]) && (filterObserverEvents ? !(event in componentObserverLocalEvents) : true)); } + +/** + * Filters emitter emit results and removes unnecessary events. + * It only keeps component events, excluding observer-like events. + * + * @param results - The array of emit results. + * @param [filterObserverEvents] - Whether to filter out observer events (default: true). + */ +export function filterEmitterResults( + results: Array>, + filterObserverEvents: boolean = true +): VAL[] { + const filtered = results.filter(({value: [event]}) => Object.isString(event) && + Boolean(componentEvents[event]) && + (filterObserverEvents ? !(event in componentObserverLocalEvents) : true)); + + return filtered.map(({value}) => value); +} diff --git a/src/components/base/b-scrolly/test/api/helpers/interface.ts b/src/components/base/b-scrolly/test/api/helpers/interface.ts index 34795da959..f4ed3ad6ad 100644 --- a/src/components/base/b-scrolly/test/api/helpers/interface.ts +++ b/src/components/base/b-scrolly/test/api/helpers/interface.ts @@ -18,7 +18,7 @@ export interface DataConveyor { /** * Adds a specified number of data items to the conveyor. * - * @param count The number of data items to add. + * @param count - The number of data items to add. * @returns An array containing the newly added data items. */ addData(count: number): DATA[]; @@ -26,7 +26,7 @@ export interface DataConveyor { /** * Adds a specified number of mounted items to the conveyor. * - * @param count The number of mounted items to add. + * @param count - The number of mounted items to add. * @returns An array containing the newly added mounted items. */ addItems(count: number): MountedItem[]; @@ -34,7 +34,7 @@ export interface DataConveyor { /** * Adds a specified number of mounted child items (separators) to the conveyor. * - * @param count The number of mounted child items to add. + * @param count - The number of mounted child items to add. * @returns An array containing the newly added mounted child items. */ addSeparators(count: number): MountedChild[]; @@ -42,7 +42,7 @@ export interface DataConveyor { /** * Adds an array of component items as mounted child items to the conveyor. * - * @param child The array of component items to add as mounted child items. + * @param child - The array of component items to add as mounted child items. * @returns The updated array of mounted child items. */ addChild(child: ComponentItem[]): MountedChild[]; @@ -57,6 +57,11 @@ export interface DataConveyor { */ get data(): DATA[]; + /** + * Returns a data page. + */ + get page(): number; + /** * Retrieves the array of mounted child items in the conveyor. */ @@ -80,7 +85,7 @@ export interface StateApi { /** * Compiles and returns the assembled component state object. * - * @param override An object for overriding the current fields of the component state. + * @param override - An object for overriding the current fields of the component state. * @returns The compiled component state. */ compile(override?: Partial): ComponentState; diff --git a/src/components/base/b-scrolly/test/unit/functional/emitter/index.ts b/src/components/base/b-scrolly/test/unit/functional/emitter/order.ts similarity index 85% rename from src/components/base/b-scrolly/test/unit/functional/emitter/index.ts rename to src/components/base/b-scrolly/test/unit/functional/emitter/order.ts index b019468531..e4b33c23af 100644 --- a/src/components/base/b-scrolly/test/unit/functional/emitter/index.ts +++ b/src/components/base/b-scrolly/test/unit/functional/emitter/order.ts @@ -7,18 +7,19 @@ */ /** - * @file Test cases of the component lifecycle + * @file This file contains test cases to verify the functionality of events emitter in the `b-scrolly` component. */ import test from 'tests/config/unit/test'; import { createTestHelpers, filterEmitterCalls } from 'components/base/b-scrolly/test/api/helpers'; +import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; test.describe(' emitter', () => { let - component: Awaited>['component'], - provider:Awaited>['provider'], - state: Awaited>['state']; + component: ScrollyTestHelpers['component'], + provider: ScrollyTestHelpers['provider'], + state: ScrollyTestHelpers['state']; test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); @@ -37,14 +38,15 @@ test.describe(' emitter', () => { await component.setProps({ chunkSize, shouldStopRequestingData: () => true, - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx.localEmitter, 'emit') + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') }); await component.withDefaultPaginationProviderProps({chunkSize}); await component.build(); + await component.waitForLifecycleDone(); const - spy = await component.getSpy((ctx) => ctx.unsafe.localEmitter.emit), + spy = await component.getSpy((ctx) => ctx.emit), calls = filterEmitterCalls(await spy.calls); test.expect(calls).toEqual([ @@ -79,7 +81,7 @@ test.describe(' emitter', () => { chunkSize, shouldPerformDataRequest: () => true, shouldStopRequestingData: ({lastLoadedData}) => lastLoadedData.length === 0, - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx.localEmitter, 'emit') + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') }); await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); @@ -87,7 +89,7 @@ test.describe(' emitter', () => { await component.waitForContainerChildCountEqualsTo(chunkSize); const - spy = await component.getSpy((ctx) => ctx.unsafe.localEmitter.emit), + spy = await component.getSpy((ctx) => ctx.emit), calls = filterEmitterCalls(await spy.calls); test.expect(calls).toEqual([ @@ -126,7 +128,7 @@ test.describe(' emitter', () => { chunkSize, shouldPerformDataRequest: () => true, shouldStopRequestingData: ({lastLoadedData}) => lastLoadedData.length === 0, - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx.localEmitter, 'emit') + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') }); await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); @@ -135,7 +137,7 @@ test.describe(' emitter', () => { await component.waitForLifecycleDone(); const - spy = await component.getSpy((ctx) => ctx.unsafe.localEmitter.emit), + spy = await component.getSpy((ctx) => ctx.emit), calls = filterEmitterCalls(await spy.calls); test.expect(calls).toEqual([ @@ -165,7 +167,7 @@ test.describe(' emitter', () => { await component.setProps({ chunkSize, shouldStopRequestingData: () => true, - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx.localEmitter, 'emit') + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') }); await component.withDefaultPaginationProviderProps({chunkSize}); @@ -179,7 +181,7 @@ test.describe(' emitter', () => { await component.waitForContainerChildCountEqualsTo(chunkSize); const - spy = await component.getSpy((ctx) => ctx.unsafe.localEmitter.emit), + spy = await component.getSpy((ctx) => ctx.emit), calls = filterEmitterCalls(await spy.calls); test.expect(calls).toEqual([ diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index ff6ddecdac..bbc65a4ce5 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -194,6 +194,7 @@ export default class ComponentObjectBuilder { async setProps(props: Dictionary): Promise { if (!this.isBuilded) { Object.assign(this.props, props); + } else { await this.applyProps(props); } diff --git a/tests/helpers/mock/interface.ts b/tests/helpers/mock/interface.ts index 54464b6ff8..a81616267e 100644 --- a/tests/helpers/mock/interface.ts +++ b/tests/helpers/mock/interface.ts @@ -31,7 +31,7 @@ export interface SpyObject { /** * The results of each call to the spy function. */ - readonly results: Promise; + readonly results: Promise; } /** @@ -41,7 +41,8 @@ export interface SpyExtractor { /** * Extracts or creates a spy object from a `JSHandle`. * - * @param ctx The `JSHandle` containing the spy object. + * @param ctx - The `JSHandle` containing the spy object. + * @param args */ (ctx: CTX, ...args: ARGS): ReturnType; } From bf9a4ad088658dc00296d68229c4f7b73087fff1 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 22 Jun 2023 13:41:18 +0300 Subject: [PATCH 034/159] WIP --- index.d.ts | 2 +- src/components/base/b-scrolly/README.md | 2 + src/components/base/b-scrolly/handlers.ts | 3 +- .../base/b-scrolly/test/api/helpers/index.ts | 10 +- .../test/unit/functional/emitter/state.ts | 163 ++++++++++++++++++ .../test/unit/functional/state/index.ts | 2 +- tests/helpers/mock/interface.ts | 4 +- 7 files changed, 177 insertions(+), 9 deletions(-) create mode 100644 src/components/base/b-scrolly/test/unit/functional/emitter/state.ts diff --git a/index.d.ts b/index.d.ts index 62af36adc9..0ed18e864c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -135,7 +135,7 @@ declare var mock: import('jest-mock').ModuleMocker['fn']; }; -interface JestMockResult { +interface JestMockResult { type: 'throw' | 'return'; value: VAL; } diff --git a/src/components/base/b-scrolly/README.md b/src/components/base/b-scrolly/README.md index 77256ad2de..f97f26829a 100644 --- a/src/components/base/b-scrolly/README.md +++ b/src/components/base/b-scrolly/README.md @@ -13,6 +13,8 @@ - протыкать все метода на использование (удалить неиспользуемые) - улучшить имена тест кейсов - ревью и рефакторинг src\components\base\b-scrolly\test\api\helpers\index.ts +- dbChange +- описать что состояние типа loadPage, renderPage обновляются постфактум (после того как загрузка данных случилась, после того как рендеринг случился) ```mermaid graph TD; diff --git a/src/components/base/b-scrolly/handlers.ts b/src/components/base/b-scrolly/handlers.ts index ea9efda8fd..3663e8fc10 100644 --- a/src/components/base/b-scrolly/handlers.ts +++ b/src/components/base/b-scrolly/handlers.ts @@ -119,7 +119,6 @@ export abstract class bScrollyHandlers extends bScrollyProps { */ onDataLoadStart(isInitialLoading: boolean): void { this.slotsStateController.loadingProgressState(); - this.componentInternalState.incrementLoadPage(); this.componentEmitter.emit(componentEvents.dataLoadStart, isInitialLoading); } @@ -140,6 +139,8 @@ export abstract class bScrollyHandlers extends bScrollyProps { } this.componentInternalState.updateData(data.data, isInitialLoading); + this.componentInternalState.incrementLoadPage(); + this.shouldStopRequestingDataWrapper(); this.componentEmitter.emit(componentEvents.dataLoadSuccess, data.data, isInitialLoading); diff --git a/src/components/base/b-scrolly/test/api/helpers/index.ts b/src/components/base/b-scrolly/test/api/helpers/index.ts index 8ab45aa615..5f92607605 100644 --- a/src/components/base/b-scrolly/test/api/helpers/index.ts +++ b/src/components/base/b-scrolly/test/api/helpers/index.ts @@ -262,7 +262,7 @@ export function createMountedItem(data: IndexedObj): MountedItem { key: Object.cast(undefined), item: 'section', type: 'item', - node: test.expect.any(String) + node: test.expect.anything() }; } @@ -279,7 +279,7 @@ export function createMountedSeparator(data: IndexedObj): MountedChild { key: Object.cast(undefined), item: 'section', type: 'separator', - node: test.expect.any(String) + node: test.expect.anything() }; } @@ -329,13 +329,15 @@ export function filterEmitterCalls( * * @param results - The array of emit results. * @param [filterObserverEvents] - Whether to filter out observer events (default: true). + * @param [allowedEvents] */ export function filterEmitterResults( results: Array>, - filterObserverEvents: boolean = true + filterObserverEvents: boolean = true, + allowedEvents: string[] = [] ): VAL[] { const filtered = results.filter(({value: [event]}) => Object.isString(event) && - Boolean(componentEvents[event]) && + (Boolean(componentEvents[event]) || allowedEvents.includes(event)) && (filterObserverEvents ? !(event in componentObserverLocalEvents) : true)); return filtered.map(({value}) => value); diff --git a/src/components/base/b-scrolly/test/unit/functional/emitter/state.ts b/src/components/base/b-scrolly/test/unit/functional/emitter/state.ts new file mode 100644 index 0000000000..cb3be8fdee --- /dev/null +++ b/src/components/base/b-scrolly/test/unit/functional/emitter/state.ts @@ -0,0 +1,163 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * @file This file contains test cases that verify the correctness of the component state during event emission. + */ + +import test from 'tests/config/unit/test'; + +import { createTestHelpers, filterEmitterResults } from 'components/base/b-scrolly/test/api/helpers'; +import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; + +test.describe(' emitter state', () => { + let + component: ScrollyTestHelpers['component'], + provider: ScrollyTestHelpers['provider'], + state: ScrollyTestHelpers['state']; + + const initialStateFields = { + itemsTillEnd: undefined, + childTillEnd: undefined, + maxViewedChild: undefined, + maxViewedItem: undefined + }; + + test.beforeEach(async ({demoPage, page}) => { + await demoPage.goto(); + + ({component, provider, state} = await createTestHelpers(page)); + await provider.start(); + }); + + test('All data has been loaded after the initial load', async () => { + const + chunkSize = 12, + initialState = state.compile(initialStateFields), + data = state.data.addData(chunkSize); + + const loadedState = state.compile({ + ...initialStateFields, + loadPage: 1, + isRequestsStopped: true + }); + + state.data.addItems(chunkSize); + + const renderedUnmountedState = state.compile({ + ...initialStateFields, + loadPage: 1, + renderPage: 1, + isRequestsStopped: true, + isInitialRender: false + }); + + const renderedMountedState = state.compile({ + ...initialStateFields, + loadPage: 1, + renderPage: 1, + isRequestsStopped: true, + isInitialRender: false + }); + + provider + .responseOnce(200, {data}) + .response(200, {data: []}); + + await component.setProps({ + chunkSize, + shouldStopRequestingData: () => true, + '@hook:beforeDataCreate': (ctx) => { + const original = ctx.emit; + + ctx.emit = jestMock.mock((...args) => { + original(...args); + return [args[0], Object.fastClone(ctx.getComponentState())]; + }); + } + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForLifecycleDone(); + + const + spy = await component.getSpy((ctx) => ctx.emit), + results = filterEmitterResults(await spy.results, true, ['initLoadStart', 'initLoad']); + + test.expect(results).toEqual([ + [ + 'initLoadStart', + { + ...initialState, + isLoadingInProgress: true + } + ], + [ + 'dataLoadStart', + { + ...initialState, + isLoadingInProgress: true + } + ], + [ + 'convertDataToDB', + { + ...initialState, + ...Object.select(loadedState, ['lastLoadedRawData']) + } + ], + [ + 'initLoad', + { + ...initialState, + ...Object.select(loadedState, ['lastLoadedRawData']) + } + ], + [ + 'dataLoadSuccess', + loadedState + ], + [ + 'renderStart', + loadedState + ], + [ + 'renderEngineStart', + loadedState + ], + [ + 'renderEngineDone', + loadedState + ], + [ + 'domInsertStart', + renderedUnmountedState + ], + [ + 'domInsertDone', + renderedMountedState + ], + [ + 'renderDone', + renderedMountedState + ], + [ + 'lifecycleDone', + { + ...renderedMountedState, + itemsTillEnd: test.expect.any(Number), + childTillEnd: test.expect.any(Number), + maxViewedChild: test.expect.any(Number), + maxViewedItem: test.expect.any(Number), + isLifecycleDone: true + } + ] + ]); + }); +}); diff --git a/src/components/base/b-scrolly/test/unit/functional/state/index.ts b/src/components/base/b-scrolly/test/unit/functional/state/index.ts index fbc1bf088a..9cb67dd879 100644 --- a/src/components/base/b-scrolly/test/unit/functional/state/index.ts +++ b/src/components/base/b-scrolly/test/unit/functional/state/index.ts @@ -42,7 +42,7 @@ test.describe(' state', () => { isRequestsStopped: false, isLoadingInProgress: true, lastLoadedData: [], - loadPage: 1 + loadPage: 0 }); await component.setProps({ diff --git a/tests/helpers/mock/interface.ts b/tests/helpers/mock/interface.ts index a81616267e..fdbd5ea6da 100644 --- a/tests/helpers/mock/interface.ts +++ b/tests/helpers/mock/interface.ts @@ -16,7 +16,7 @@ export interface SpyObject { /** * The array of arguments passed to the spy function on each call. */ - readonly calls: Promise; + readonly calls: Promise; /** * The number of times the spy function has been called. @@ -26,7 +26,7 @@ export interface SpyObject { /** * The arguments of the most recent call to the spy function. */ - readonly lastCall: Promise; + readonly lastCall: Promise; /** * The results of each call to the spy function. From fb4d522a3c24d8fee3dc58601c64c5dc966179e7 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 23 Jun 2023 19:58:10 +0300 Subject: [PATCH 035/159] WIP --- src/components/base/b-scrolly/README.md | 1 + src/components/base/b-scrolly/b-scrolly.ts | 14 +- src/components/base/b-scrolly/props.ts | 6 +- .../base/b-scrolly/test/api/helpers/index.ts | 17 +- .../b-scrolly/test/api/helpers/interface.ts | 17 ++ .../emitter/{order.ts => payload.ts} | 0 .../test/unit/functional/emitter/state.ts | 287 ++++++++++-------- 7 files changed, 208 insertions(+), 134 deletions(-) rename src/components/base/b-scrolly/test/unit/functional/emitter/{order.ts => payload.ts} (100%) diff --git a/src/components/base/b-scrolly/README.md b/src/components/base/b-scrolly/README.md index f97f26829a..2ad793c2ed 100644 --- a/src/components/base/b-scrolly/README.md +++ b/src/components/base/b-scrolly/README.md @@ -15,6 +15,7 @@ - ревью и рефакторинг src\components\base\b-scrolly\test\api\helpers\index.ts - dbChange - описать что состояние типа loadPage, renderPage обновляются постфактум (после того как загрузка данных случилась, после того как рендеринг случился) +- лишний вызов shouldStopRequestingData в onDataLoadSuccess ```mermaid graph TD; diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts index 7a1908ceb1..1e251f8a29 100644 --- a/src/components/base/b-scrolly/b-scrolly.ts +++ b/src/components/base/b-scrolly/b-scrolly.ts @@ -55,25 +55,21 @@ export default class bScrolly extends bScrollyHandlers implements iItems { const state = this.getComponentState(); + if (!this.isReady && this.isReadyOnce) { + this.reset(); + } + if (state.isLoadingInProgress) { return; } this.componentInternalState.setIsLoadingInProgress(true); - const callSuperAndStateReset = () => { - if (this.isReadyOnce) { - this.reset(); - } - - return super.initLoad(...args); - }; - const isInitialLoading = !this.isReady; const initLoadResult = isInitialLoading ? - callSuperAndStateReset() : + super.initLoad(...args) : this.initLoadNext(); this.onDataLoadStart(isInitialLoading); diff --git a/src/components/base/b-scrolly/props.ts b/src/components/base/b-scrolly/props.ts index 17ce753368..030e71ca22 100644 --- a/src/components/base/b-scrolly/props.ts +++ b/src/components/base/b-scrolly/props.ts @@ -200,7 +200,7 @@ export default abstract class bScrollyProps extends iData implements iItems { readonly shouldPerformDataRequest!: ShouldPerform; /** - * This function is called after successful data loading or when the component enters the visible area. + * This function is called after successful data loading or when the child components enters the visible area. * * This function asks the client whether rendering can be performed. The client responds with an object * indicating whether rendering is allowed or the reason for denial. The client's response should be an object @@ -226,13 +226,13 @@ export default abstract class bScrollyProps extends iData implements iItems { readonly renderGuard!: ShouldPerform; /** - * This function is called in the `renderGuard` after other checks are completed. + * This function is called in the {@link bScrolly.renderGuard} after other checks are completed. * * This function receives the component state as input, based on which the client * should determine whether the component should render the next chunk of components. * * For example, if we want to render the next data chunk only when the client - * has seen all the main components, we can implement the following function: + * has seen all the main (type=item) components, we can implement the following function: * * @example * ```typescript diff --git a/src/components/base/b-scrolly/test/api/helpers/index.ts b/src/components/base/b-scrolly/test/api/helpers/index.ts index 5f92607605..549bd98849 100644 --- a/src/components/base/b-scrolly/test/api/helpers/index.ts +++ b/src/components/base/b-scrolly/test/api/helpers/index.ts @@ -125,6 +125,10 @@ export function createDataConveyor( return childList; }, + getDataChunk(index: number) { + return dataChunks[index]; + }, + reset() { dataI = 0; itemsI = 0; @@ -172,7 +176,7 @@ export function createStateApi( let state = createInitialState(initial); - return { + const obj: StateApi = { compile(override?: Partial): ComponentState { return { ...state, @@ -181,6 +185,15 @@ export function createStateApi( }; }, + set(props: Partial): StateApi { + state = { + ...state, + ...props + }; + + return obj; + }, + reset(): void { state = createInitialState(initial); dataConveyor.reset(); @@ -188,6 +201,8 @@ export function createStateApi( data: dataConveyor }; + + return obj; } /** diff --git a/src/components/base/b-scrolly/test/api/helpers/interface.ts b/src/components/base/b-scrolly/test/api/helpers/interface.ts index f4ed3ad6ad..2b49e012c3 100644 --- a/src/components/base/b-scrolly/test/api/helpers/interface.ts +++ b/src/components/base/b-scrolly/test/api/helpers/interface.ts @@ -47,6 +47,14 @@ export interface DataConveyor { */ addChild(child: ComponentItem[]): MountedChild[]; + /** + * Returns an array of data for the given index added using the `addData` method. + * + * @param index - The index of the data chunk. + * @returns An array of data. + */ + getDataChunk(index: number): DATA[]; + /** * Resets the data conveyor, clearing all data and items. */ @@ -95,6 +103,15 @@ export interface StateApi { */ reset(): void; + /** + * Sets the values from an object as the current state. Fields set using this method are not automatically reset, + * and they can only be reset by overriding them or using the `reset` method. + * + * @param props - An object containing the new state values. + * @returns The updated StateApi object. + */ + set(props: Partial): StateApi; + /** * The data conveyor used for managing data within the component state. */ diff --git a/src/components/base/b-scrolly/test/unit/functional/emitter/order.ts b/src/components/base/b-scrolly/test/unit/functional/emitter/payload.ts similarity index 100% rename from src/components/base/b-scrolly/test/unit/functional/emitter/order.ts rename to src/components/base/b-scrolly/test/unit/functional/emitter/payload.ts diff --git a/src/components/base/b-scrolly/test/unit/functional/emitter/state.ts b/src/components/base/b-scrolly/test/unit/functional/emitter/state.ts index cb3be8fdee..a4498df425 100644 --- a/src/components/base/b-scrolly/test/unit/functional/emitter/state.ts +++ b/src/components/base/b-scrolly/test/unit/functional/emitter/state.ts @@ -35,129 +35,174 @@ test.describe(' emitter state', () => { await provider.start(); }); - test('All data has been loaded after the initial load', async () => { - const - chunkSize = 12, - initialState = state.compile(initialStateFields), - data = state.data.addData(chunkSize); - - const loadedState = state.compile({ - ...initialStateFields, - loadPage: 1, - isRequestsStopped: true - }); - - state.data.addItems(chunkSize); - - const renderedUnmountedState = state.compile({ - ...initialStateFields, - loadPage: 1, - renderPage: 1, - isRequestsStopped: true, - isInitialRender: false - }); - - const renderedMountedState = state.compile({ - ...initialStateFields, - loadPage: 1, - renderPage: 1, - isRequestsStopped: true, - isInitialRender: false - }); - - provider - .responseOnce(200, {data}) - .response(200, {data: []}); - - await component.setProps({ - chunkSize, - shouldStopRequestingData: () => true, - '@hook:beforeDataCreate': (ctx) => { - const original = ctx.emit; - - ctx.emit = jestMock.mock((...args) => { - original(...args); - return [args[0], Object.fastClone(ctx.getComponentState())]; - }); - } + test.describe('All data has been loaded after the initial load', () => { + test('State at the time of emitting events must be correct', async () => { + const chunkSize = 12; + + const states = [ + state.compile(initialStateFields), + ( + state.data.addData(chunkSize), + state.set({loadPage: 1, isRequestsStopped: true}).compile(initialStateFields) + ), + ( + state.data.addItems(chunkSize), + state.set({isInitialRender: false, renderPage: 1}).compile(initialStateFields) + ), + ( + state.compile(initialStateFields) + ), + ( + state.set({isLifecycleDone: true}).compile() + ) + ]; + + provider + .responseOnce(200, {data: state.data.getDataChunk(0)}) + .response(200, {data: []}); + + await component.setProps({ + chunkSize, + shouldStopRequestingData: () => true, + '@hook:beforeDataCreate': (ctx) => { + const original = ctx.emit; + + ctx.emit = jestMock.mock((...args) => { + original(...args); + return [args[0], Object.fastClone(ctx.getComponentState())]; + }); + } + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForLifecycleDone(); + + const + spy = await component.getSpy((ctx) => ctx.emit), + results = filterEmitterResults(await spy.results, true, ['initLoadStart', 'initLoad']); + + test.expect(results).toEqual([ + ['initLoadStart', {...states[0], isLoadingInProgress: true}], + ['dataLoadStart', {...states[0], isLoadingInProgress: true}], + ['convertDataToDB', {...states[0], lastLoadedRawData: states[1].lastLoadedRawData}], + ['initLoad', {...states[0], lastLoadedRawData: states[1].lastLoadedRawData}], + ['dataLoadSuccess', states[1]], + ['renderStart', states[1]], + ['renderEngineStart', states[1]], + ['renderEngineDone', states[1]], + ['domInsertStart', states[2]], + ['domInsertDone', states[3]], + ['renderDone', states[3]], + ['lifecycleDone', states[4]] + ]); }); + }); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); - await component.waitForLifecycleDone(); - - const - spy = await component.getSpy((ctx) => ctx.emit), - results = filterEmitterResults(await spy.results, true, ['initLoadStart', 'initLoad']); - - test.expect(results).toEqual([ - [ - 'initLoadStart', - { - ...initialState, - isLoadingInProgress: true - } - ], - [ - 'dataLoadStart', - { - ...initialState, - isLoadingInProgress: true + test.describe('All data has been loaded after the second load and reload was called', () => { + test('State at the time of emitting events must be correct', async () => { + const + chunkSize = 12, + providerChunkSize = chunkSize / 2; + + const states = [ + state.compile(initialStateFields), + ( + state.data.addData(providerChunkSize), + state.set({loadPage: 1}).compile(initialStateFields) + ), + ( + state.data.addData(providerChunkSize), + state.set({loadPage: 2, isInitialLoading: false}).compile(initialStateFields) + ), + ( + state.data.addItems(chunkSize), + state.set({renderPage: 1, isInitialRender: false}).compile(initialStateFields) + ), + ( + state.compile(initialStateFields) + ), + ( + state.compile() + ), + ( + state.data.addData(0), + state.set({loadPage: 3, isRequestsStopped: true, isLastEmpty: true}).compile() + ), + ( + state.set({isLifecycleDone: true}).compile() + ) + ]; + + provider + .responseOnce(200, {data: state.data.getDataChunk(0)}) + .responseOnce(200, {data: state.data.getDataChunk(1)}) + .responseOnce(200, {data: state.data.getDataChunk(2)}) + .responseOnce(200, {data: state.data.getDataChunk(0)}) + .responseOnce(200, {data: state.data.getDataChunk(1)}) + .response(200, {data: state.data.getDataChunk(2)}); + + await component.setProps({ + chunkSize, + '@hook:beforeDataCreate': (ctx) => { + const original = ctx.emit; + + ctx.emit = jestMock.mock((...args) => { + original(...args); + return [args[0], Object.fastClone(ctx.getComponentState())]; + }); } - ], - [ - 'convertDataToDB', - { - ...initialState, - ...Object.select(loadedState, ['lastLoadedRawData']) - } - ], - [ - 'initLoad', - { - ...initialState, - ...Object.select(loadedState, ['lastLoadedRawData']) - } - ], - [ - 'dataLoadSuccess', - loadedState - ], - [ - 'renderStart', - loadedState - ], - [ - 'renderEngineStart', - loadedState - ], - [ - 'renderEngineDone', - loadedState - ], - [ - 'domInsertStart', - renderedUnmountedState - ], - [ - 'domInsertDone', - renderedMountedState - ], - [ - 'renderDone', - renderedMountedState - ], - [ - 'lifecycleDone', - { - ...renderedMountedState, - itemsTillEnd: test.expect.any(Number), - childTillEnd: test.expect.any(Number), - maxViewedChild: test.expect.any(Number), - maxViewedItem: test.expect.any(Number), - isLifecycleDone: true - } - ] - ]); + }); + + await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); + await component.build(); + await component.waitForLifecycleDone(); + await component.reload(); + await component.waitForLifecycleDone(); + + const + spy = await component.getSpy((ctx) => ctx.emit), + results = filterEmitterResults(await spy.results, true, ['initLoadStart', 'initLoad']); + + test.expect(results).toEqual([ + ['initLoadStart', {...states[0], isLoadingInProgress: true}], + ['dataLoadStart', {...states[0], isLoadingInProgress: true}], + ['convertDataToDB', {...states[0], lastLoadedRawData: states[1].lastLoadedRawData}], + ['initLoad', {...states[0], lastLoadedRawData: states[1].lastLoadedRawData}], + ['dataLoadSuccess', states[1]], + ['dataLoadStart', states[1]], + ['convertDataToDB', {...states[1], lastLoadedRawData: states[2].lastLoadedRawData}], + ['dataLoadSuccess', states[2]], + ['renderStart', states[2]], + ['renderEngineStart', states[2]], + ['renderEngineDone', states[2]], + ['domInsertStart', states[3]], + ['domInsertDone', states[4]], + ['renderDone', states[4]], + ['dataLoadStart', states[5]], + ['convertDataToDB', {...states[5], lastLoadedRawData: states[6].lastLoadedRawData}], + ['dataLoadSuccess', states[6]], + ['lifecycleDone', states[7]], + ['resetState', states[0]], + ['initLoadStart', {...states[0], isLoadingInProgress: true}], + ['dataLoadStart', {...states[0], isLoadingInProgress: true}], + ['convertDataToDB', {...states[0], lastLoadedRawData: states[1].lastLoadedRawData}], + ['initLoad', {...states[0], lastLoadedRawData: states[1].lastLoadedRawData}], + ['dataLoadSuccess', states[1]], + ['dataLoadStart', states[1]], + ['convertDataToDB', {...states[1], lastLoadedRawData: states[2].lastLoadedRawData}], + ['dataLoadSuccess', states[2]], + ['renderStart', states[2]], + ['renderEngineStart', states[2]], + ['renderEngineDone', states[2]], + ['domInsertStart', states[3]], + ['domInsertDone', states[4]], + ['renderDone', states[4]], + ['dataLoadStart', states[5]], + ['convertDataToDB', {...states[5], lastLoadedRawData: states[6].lastLoadedRawData}], + ['dataLoadSuccess', states[6]], + ['lifecycleDone', states[7]] + ]); + }); }); }); From 916ce09757184ee67f1acc76f6e6efc5e2e05365 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sun, 25 Jun 2023 20:42:47 +0300 Subject: [PATCH 036/159] WIP --- src/components/base/b-scrolly/b-scrolly.ts | 91 ++++++++++++++++++- src/components/base/b-scrolly/handlers.ts | 6 +- .../base/b-scrolly/interface/component.ts | 13 +++ .../b-scrolly/modules/presets/chunk-size.ts | 83 ----------------- .../base/b-scrolly/modules/state/helpers.ts | 12 +++ .../base/b-scrolly/modules/state/index.ts | 34 ++++++- src/components/base/b-scrolly/props.ts | 55 +++++------ .../test/api/component-object/index.ts | 32 +++++-- .../unit/functional/presets/chunk-size.ts | 70 +++++++++++++- 9 files changed, 260 insertions(+), 136 deletions(-) delete mode 100644 src/components/base/b-scrolly/modules/presets/chunk-size.ts diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts index 1e251f8a29..47e1a67b2b 100644 --- a/src/components/base/b-scrolly/b-scrolly.ts +++ b/src/components/base/b-scrolly/b-scrolly.ts @@ -19,7 +19,7 @@ import { bScrollyDomInsertAsyncGroup, renderGuardRejectionReason } from 'compone import iData, { $$, component, RequestParams } from 'components/super/i-data/i-data'; import { bScrollyHandlers } from 'components/base/b-scrolly/handlers'; import type { AsyncOptions } from 'core/async'; -import type { ComponentState } from 'components/base/b-scrolly/interface'; +import type { ComponentState, RenderGuardResult } from 'components/base/b-scrolly/interface'; export * from 'components/base/b-scrolly/interface'; export * from 'components/base/b-scrolly/const'; @@ -167,6 +167,38 @@ export default class bScrolly extends bScrollyHandlers implements iItems { return this.shouldPerformDataRequest(this.getComponentState(), this); } + /** + * Returns the chunk size that should be rendered. + * + * @param state + * @returns The chunk size. + * @throws Error if the `chunkSize` size is not defined. + */ + getChunkSize(state: ComponentState): number { + if (this.chunkSize == null) { + throw new Error('`chunkSize` prop is not defined'); + } + + return Object.isFunction(this.chunkSize) ? + this.chunkSize(state, this) : + this.chunkSize; + } + + /** + * Returns the next slice of data that should be rendered. + * + * @param state + * @param chunkSize + */ + getNextDataSlice(state: ComponentState, chunkSize: number): object[] { + const + {data} = state, + nextDataSliceStartIndex = this.componentInternalState.getRenderCursor(), + nextDataSliceEndIndex = nextDataSliceStartIndex + chunkSize; + + return data.slice(nextDataSliceStartIndex, nextDataSliceEndIndex); + } + protected override convertDataToDB(data: unknown): O | this['DB'] { const result = super.convertDataToDB(data); this.onConvertDataToDB(data); @@ -174,6 +206,57 @@ export default class bScrolly extends bScrollyHandlers implements iItems { return result; } + /** + * This function is called after successful data loading or when the child components enters the visible area. + * + * This function asks the client whether rendering can be performed. The client responds with an object + * indicating whether rendering is allowed or the reason for denial. The client's response should be an object + * of type {@link RenderGuardResult}. + * + * Based on the result of this function, the component takes appropriate actions. For example, + * it may load data if it is not sufficient for rendering, or perform rendering if all conditions are met. + */ + protected renderGuard(state: ComponentState): RenderGuardResult { + const + chunkSize = this.getChunkSize(state), + dataSlice = this.getNextDataSlice(state, chunkSize); + + if (dataSlice.length === 0) { + if (state.isRequestsStopped) { + return { + result: false, + reason: renderGuardRejectionReason.done + }; + } + + return { + result: false, + reason: renderGuardRejectionReason.noData + }; + } + + if (dataSlice.length < chunkSize) { + return { + result: false, + reason: renderGuardRejectionReason.notEnoughData + }; + } + + if (state.isInitialRender) { + return { + result: true + }; + } + + const + clientResponse = this.shouldPerformDataRender?.(state, this) ?? true; + + return { + result: clientResponse, + reason: clientResponse === false ? renderGuardRejectionReason.noPermission : undefined + }; + } + /** * Renders components using {@link bScrolly.componentFactory} and inserts them into the DOM tree. * {@link bScrolly.componentFactory}, in turn, calls {@link bScrolly.itemsFactory} to obtain @@ -187,10 +270,8 @@ export default class bScrolly extends bScrollyHandlers implements iItems { nodes = this.componentFactory.produceNodes(items), mounted = this.componentFactory.produceMounted(items, nodes); - this.componentInternalState.updateMounted(mounted); this.observer.observe(mounted); - - this.onDomInsertStart(); + this.onDomInsertStart(mounted); const fragment = document.createDocumentFragment(); @@ -221,7 +302,7 @@ export default class bScrolly extends bScrollyHandlers implements iItems { protected loadDataOrPerformRender(): void { const state = this.getComponentState(), - {result, reason} = this.renderGuard(state, this); + {result, reason} = this.renderGuard(state); if (result) { return this.performRender(); diff --git a/src/components/base/b-scrolly/handlers.ts b/src/components/base/b-scrolly/handlers.ts index 3663e8fc10..3298aa7728 100644 --- a/src/components/base/b-scrolly/handlers.ts +++ b/src/components/base/b-scrolly/handlers.ts @@ -59,8 +59,12 @@ export abstract class bScrollyHandlers extends bScrollyProps { /** * Handler: DOM insert start event. * Triggered when the insertion of rendered components into the DOM tree starts. + * + * @param childList */ - onDomInsertStart(): void { + onDomInsertStart(this: bScrolly, childList: MountedChild[]): void { + this.componentInternalState.updateRenderCursor(); + this.componentInternalState.updateMounted(childList); this.componentInternalState.setIsInitialRender(false); this.componentInternalState.incrementRenderPage(); diff --git a/src/components/base/b-scrolly/interface/component.ts b/src/components/base/b-scrolly/interface/component.ts index b089b23088..99eac7437d 100644 --- a/src/components/base/b-scrolly/interface/component.ts +++ b/src/components/base/b-scrolly/interface/component.ts @@ -144,6 +144,19 @@ export interface ComponentState { lastLoadedRawData: unknown; } +/** + * Private (not accessible to the client) component state. + * + * This state stores all the internal component state that should not be + * accessible to the client. + */ +export interface PrivateComponentState { + /** + * Pointer to the index of the data element that was last rendered. + */ + renderCursor: number; +} + /** * Types of rendered components. */ diff --git a/src/components/base/b-scrolly/modules/presets/chunk-size.ts b/src/components/base/b-scrolly/modules/presets/chunk-size.ts deleted file mode 100644 index 21a52b138b..0000000000 --- a/src/components/base/b-scrolly/modules/presets/chunk-size.ts +++ /dev/null @@ -1,83 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; - -import { renderGuardRejectionReason } from 'components/base/b-scrolly/const'; -import type { RenderGuardResult, ComponentState } from 'components/base/b-scrolly/interface'; - -/** - * Returns the next slice of data that should be rendered. - * - * @param state - * @param chunkSize - */ -export function getNextDataSlice(state: ComponentState, chunkSize: number): object[] { - const - {data, renderPage} = state, - nextDataSliceStartIndex = renderPage * chunkSize, - nextDataSliceEndIndex = (renderPage + 1) * chunkSize; - - return data.slice(nextDataSliceStartIndex, nextDataSliceEndIndex); -} - -/** - * A preset configuration for the chunk size. - */ -export const chunkSizePreset = { - /** - * A guard function that determines if the render can be performed based on the current state and chunk size. - * - * @param state - * @param ctx - * @param chunkSize - */ - renderGuard( - state: ComponentState, - ctx: bScrolly, - chunkSize: number - ): RenderGuardResult { - const dataSlice = getNextDataSlice(state, chunkSize); - - if (dataSlice.length === 0) { - if (state.isRequestsStopped) { - return { - result: false, - reason: renderGuardRejectionReason.done - }; - } - - return { - result: false, - reason: renderGuardRejectionReason.noData - }; - } - - if (dataSlice.length < chunkSize) { - return { - result: false, - reason: renderGuardRejectionReason.notEnoughData - }; - } - - if (state.isInitialRender) { - return { - result: true - }; - } - - const clientResponse = ctx.shouldPerformDataRender?.(state, ctx); - - return { - result: clientResponse == null ? true : clientResponse, - reason: clientResponse === false ? renderGuardRejectionReason.noPermission : undefined - }; - }, - - getNextDataSlice -}; diff --git a/src/components/base/b-scrolly/modules/state/helpers.ts b/src/components/base/b-scrolly/modules/state/helpers.ts index 1167717a01..7b04ce7b30 100644 --- a/src/components/base/b-scrolly/modules/state/helpers.ts +++ b/src/components/base/b-scrolly/modules/state/helpers.ts @@ -7,6 +7,7 @@ */ import type { ComponentState } from 'components/base/b-scrolly/b-scrolly'; +import type { PrivateComponentState } from 'components/base/b-scrolly/interface'; /** * Creates an initial state object for a component. @@ -34,3 +35,14 @@ export function createInitialState(): ComponentState { isLifecycleDone: false }; } + +/** + * Creates an initial private state object for a component. + * + * @returns An object representing the initial private state of a component. + */ +export function createPrivateInitialState(): PrivateComponentState { + return { + renderCursor: 0 + }; +} diff --git a/src/components/base/b-scrolly/modules/state/index.ts b/src/components/base/b-scrolly/modules/state/index.ts index 7792855ddf..3b968a7376 100644 --- a/src/components/base/b-scrolly/modules/state/index.ts +++ b/src/components/base/b-scrolly/modules/state/index.ts @@ -7,9 +7,9 @@ */ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { MountedChild, ComponentState, MountedItem } from 'components/base/b-scrolly/interface'; +import type { MountedChild, ComponentState, MountedItem, PrivateComponentState } from 'components/base/b-scrolly/interface'; import { isItem } from 'components/base/b-scrolly/modules/helpers'; -import { createInitialState } from 'components/base/b-scrolly/modules/state/helpers'; +import { createInitialState, createPrivateInitialState } from 'components/base/b-scrolly/modules/state/helpers'; import Friend from 'components/friends/friend'; /** @@ -23,6 +23,11 @@ export class ComponentInternalState extends Friend { */ state: ComponentState = createInitialState(); + /** + * Current private state of the component. + */ + protected privateState: PrivateComponentState = createPrivateInitialState(); + /** * Compiles and returns the current state of the component. * @@ -39,6 +44,7 @@ export class ComponentInternalState extends Friend { */ reset(): void { this.state = createInitialState(); + this.privateState = createPrivateInitialState(); } /** @@ -149,5 +155,29 @@ export class ComponentInternalState extends Friend { state.childTillEnd = state.childList.length - 1 - state.maxViewedChild; } } + + /** + * Returns the cursor indicating the last index of the last rendered data element. + */ + getRenderCursor(): number { + return this.privateState.renderCursor; + } + + /** + * Updates the cursor indicating the last index of the last rendered data element. + */ + updateRenderCursor(): void { + const + {ctx} = this; + + if (ctx.chunkSize != null) { + const + {state} = this, + current = this.getRenderCursor(), + chunkSize = ctx.getChunkSize(state); + + this.privateState.renderCursor = current + chunkSize; + } + } } diff --git a/src/components/base/b-scrolly/props.ts b/src/components/base/b-scrolly/props.ts index 030e71ca22..2ffb44005f 100644 --- a/src/components/base/b-scrolly/props.ts +++ b/src/components/base/b-scrolly/props.ts @@ -19,7 +19,6 @@ import type { ComponentItemFactory, ComponentItemType, ComponentStrategy, - RenderGuardResult, ComponentRefs } from 'components/base/b-scrolly/interface'; @@ -34,7 +33,6 @@ import { } from 'components/base/b-scrolly/const'; import iData, { component, prop, system } from 'components/super/i-data/i-data'; -import { chunkSizePreset } from 'components/base/b-scrolly/modules/presets/chunk-size'; import { ComponentTypedEmitter, componentTypedEmitter } from 'components/base/b-scrolly/modules/emitter'; import { SlotsStateController } from 'components/base/b-scrolly/modules/slots'; import type bScrolly from 'components/base/b-scrolly/b-scrolly'; @@ -114,12 +112,12 @@ export default abstract class bScrollyProps extends iData implements iItems { */ @prop({ type: Function, - default: (state: ComponentState, ctx: bScrollyProps) => { + default: (state: ComponentState, ctx: bScrolly) => { if (ctx.chunkSize == null) { - throw new Error('chunkSize.getNextDataSlice is used but chunkSize prop is not settled'); + throw new Error('"chunkSize.getNextDataSlice" is used but "chunkSize" prop is not set.'); } - const descriptors = chunkSizePreset.getNextDataSlice(state, ctx.chunkSize).map((data, i) => ({ + const descriptors = ctx.getNextDataSlice(state, ctx.getChunkSize(state)).map((data, i) => ({ key: ctx.itemKey?.(data, i), item: Object.isFunction(ctx.item) ? ctx.item(data, i) : ctx.item, @@ -172,10 +170,25 @@ export default abstract class bScrollyProps extends iData implements iItems { /** * The number of elements to render at once. - * This prop is used in conjunction with `renderGuard` and `chunkSize` preset. + * + * This prop can also be a function that returns the chunk size to render based on certain criteria + * (e.g., rendering page). + * + * For example, you can render 6 elements initially, then 12, and then 18 elements based on the + * rendering page. + * + * @example + * ```typescript + * const chunkSize = (state: ComponentState) => { + * return [6, 12, 18][state.renderPage] ?? 18; + * } + * ``` + * + * It's important to note that this prop is used by default, but it can be ignored and you can return + * any amount of data to render by setting a custom {@link bScrolly.itemsFactory} for the component. */ - @prop({type: Number, validator: Number.isNatural}) - readonly chunkSize?: number = 10; + @prop({type: [Number, Function]}) + readonly chunkSize?: number | ShouldPerform = 10; /** * When this function returns `true` the component will stop to request new data. @@ -199,32 +212,6 @@ export default abstract class bScrollyProps extends iData implements iItems { readonly shouldPerformDataRequest!: ShouldPerform; - /** - * This function is called after successful data loading or when the child components enters the visible area. - * - * This function asks the client whether rendering can be performed. The client responds with an object - * indicating whether rendering is allowed or the reason for denial. The client's response should be an object - * of type {@link RenderGuardResult}. - * - * Based on the result of this function, the component takes appropriate actions. For example, - * it may load data if it is not sufficient for rendering, or perform rendering if all conditions are met. - * - * By default, the {@link chunkSizePreset.renderGuard} strategy is used, - * which already implements the mechanism for communication with the component. - */ - @prop({ - type: Function, - default: (state: ComponentState, ctx: bScrolly) => { - if (ctx.chunkSize == null) { - throw new Error('The "ChunkSize.renderGuard" preset is active, but the "chunkSize" prop is not set.'); - } - - return chunkSizePreset.renderGuard(state, ctx, ctx.chunkSize); - } - }) - - readonly renderGuard!: ShouldPerform; - /** * This function is called in the {@link bScrolly.renderGuard} after other checks are completed. * diff --git a/src/components/base/b-scrolly/test/api/component-object/index.ts b/src/components/base/b-scrolly/test/api/component-object/index.ts index a1e016da13..f56520f4a6 100644 --- a/src/components/base/b-scrolly/test/api/component-object/index.ts +++ b/src/components/base/b-scrolly/test/api/component-object/index.ts @@ -75,18 +75,38 @@ export class ScrollyComponentObject extends ComponentObject { * Waits for the container child count to be equal to N. * Throws an error if there are more items in the child list than expected. * - * @param n - The expected child count. + * @param count - The expected child count. */ - async waitForContainerChildCountEqualsTo(n: number): Promise { - await this.childList.nth(n - 1).waitFor({state: 'attached'}); + async waitForContainerChildCountEqualsTo(count: number): Promise { + await this.childList.nth(count - 1).waitFor({state: 'attached'}); - const count = await this.childList.count(); + const realCount = await this.childList.count(); - if (count > n) { - throw new Error(`Expected container to have exactly ${n} items, but got ${count}`); + if (realCount > count) { + throw new Error(`Expected container to have exactly ${count} items, but got ${realCount}`); } } + /** + * Returns a promise that resolves when an element matching the given selector is inserted into the container. + * + * @param selector - The selector to match the element. + * @returns A promise that resolves when the element is attached. + */ + async waitForChild(selector: string): Promise { + await this.container.locator(selector).waitFor({state: 'attached'}); + } + + /** + * Returns a promise that resolves when an element with the attribute data-index="n" is inserted into the container. + * + * @param index - The index value for the data-index attribute. + * @returns A promise that resolves when the element is attached. + */ + async waitForDataIndexChild(index: number): Promise { + return this.waitForChild(`[data-index="${index}"]`); + } + /** * Waits for the component lifecycle to be done. */ diff --git a/src/components/base/b-scrolly/test/unit/functional/presets/chunk-size.ts b/src/components/base/b-scrolly/test/unit/functional/presets/chunk-size.ts index ae4662aef9..eeed7bd6ec 100644 --- a/src/components/base/b-scrolly/test/unit/functional/presets/chunk-size.ts +++ b/src/components/base/b-scrolly/test/unit/functional/presets/chunk-size.ts @@ -6,22 +6,82 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +/** + * @file This file contains test cases for the `chunkSize` preset. + */ + import test from 'tests/config/unit/test'; import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; +import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; +import type { ComponentState } from 'components/base/b-scrolly/interface'; -test.describe(' with chunkSize preset', () => { +test.describe(' `chunkSize` preset', () => { let - provider:Awaited>['provider']; + component: ScrollyTestHelpers['component'], + provider: ScrollyTestHelpers['provider'], + state: ScrollyTestHelpers['state']; test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); - ({provider} = await createTestHelpers(page)); + ({component, provider, state} = await createTestHelpers(page)); await provider.start(); + + await page.setViewportSize({height: 640, width: 360}); }); - test.skip('Should render components', async () => { - // ... + test.describe('With a different chunk size for each render cycle', () => { + test('Should render 6 components first, then 12, then 18', async () => { + const chunkSize = [6, 12, 18]; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize[0])}) + .responseOnce(200, {data: state.data.addData(chunkSize[1])}) + .responseOnce(200, {data: state.data.addData(chunkSize[2])}) + .response(200, {data: []}); + + await component.setProps({ + chunkSize: (state: ComponentState) => [6, 12, 18][state.renderPage] ?? 18, + shouldPerformDataRender: ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 + }); + + await component.withDefaultPaginationProviderProps(); + await component.build(); + + await test.step('First chunk', async () => { + const + expectedIndex = chunkSize[0]; + + await test.expect(component.waitForContainerChildCountEqualsTo(expectedIndex)).resolves.toBeUndefined(); + await test.expect(component.waitForDataIndexChild(expectedIndex - 1)).resolves.toBeUndefined(); + }); + + await test.step('Second chunk', async () => { + const + expectedIndex = chunkSize[0] + chunkSize[1]; + + await component.scrollToBottom(); + + await test.expect(component.waitForContainerChildCountEqualsTo(expectedIndex)).resolves.toBeUndefined(); + await test.expect(component.waitForDataIndexChild(expectedIndex - 1)).resolves.toBeUndefined(); + }); + + await test.step('Third chunk', async () => { + const + expectedIndex = chunkSize[0] + chunkSize[1] + chunkSize[2]; + + await component.scrollToBottom(); + + await test.expect(component.waitForContainerChildCountEqualsTo(expectedIndex)).resolves.toBeUndefined(); + await test.expect(component.waitForDataIndexChild(expectedIndex - 1)).resolves.toBeUndefined(); + }); + + await test.step('Lifecycle is done', async () => { + await component.scrollToBottom(); + + await test.expect(component.waitForLifecycleDone()).resolves.toBeUndefined(); + }); + }); }); }); From 3fdacffacebe8aea1c24f182f07e32d5d43c02ee Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sun, 25 Jun 2023 21:37:16 +0300 Subject: [PATCH 037/159] WIP --- .../unit/functional/presets/chunk-size.ts | 87 ------------------- .../test/unit/functional/rendering/default.ts | 71 +++++++++++++-- .../functional/rendering/items-factory.ts | 9 +- .../functional/state/{index.ts => base.ts} | 16 ++-- .../{emitter/state.ts => state/emitter.ts} | 0 .../initialization/initialization.ts | 25 ++---- .../test/unit/lifecycle/slots/slots.ts | 2 +- 7 files changed, 84 insertions(+), 126 deletions(-) delete mode 100644 src/components/base/b-scrolly/test/unit/functional/presets/chunk-size.ts rename src/components/base/b-scrolly/test/unit/functional/state/{index.ts => base.ts} (95%) rename src/components/base/b-scrolly/test/unit/functional/{emitter/state.ts => state/emitter.ts} (100%) diff --git a/src/components/base/b-scrolly/test/unit/functional/presets/chunk-size.ts b/src/components/base/b-scrolly/test/unit/functional/presets/chunk-size.ts deleted file mode 100644 index eeed7bd6ec..0000000000 --- a/src/components/base/b-scrolly/test/unit/functional/presets/chunk-size.ts +++ /dev/null @@ -1,87 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -/** - * @file This file contains test cases for the `chunkSize` preset. - */ - -import test from 'tests/config/unit/test'; - -import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; -import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; -import type { ComponentState } from 'components/base/b-scrolly/interface'; - -test.describe(' `chunkSize` preset', () => { - let - component: ScrollyTestHelpers['component'], - provider: ScrollyTestHelpers['provider'], - state: ScrollyTestHelpers['state']; - - test.beforeEach(async ({demoPage, page}) => { - await demoPage.goto(); - - ({component, provider, state} = await createTestHelpers(page)); - await provider.start(); - - await page.setViewportSize({height: 640, width: 360}); - }); - - test.describe('With a different chunk size for each render cycle', () => { - test('Should render 6 components first, then 12, then 18', async () => { - const chunkSize = [6, 12, 18]; - - provider - .responseOnce(200, {data: state.data.addData(chunkSize[0])}) - .responseOnce(200, {data: state.data.addData(chunkSize[1])}) - .responseOnce(200, {data: state.data.addData(chunkSize[2])}) - .response(200, {data: []}); - - await component.setProps({ - chunkSize: (state: ComponentState) => [6, 12, 18][state.renderPage] ?? 18, - shouldPerformDataRender: ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 - }); - - await component.withDefaultPaginationProviderProps(); - await component.build(); - - await test.step('First chunk', async () => { - const - expectedIndex = chunkSize[0]; - - await test.expect(component.waitForContainerChildCountEqualsTo(expectedIndex)).resolves.toBeUndefined(); - await test.expect(component.waitForDataIndexChild(expectedIndex - 1)).resolves.toBeUndefined(); - }); - - await test.step('Second chunk', async () => { - const - expectedIndex = chunkSize[0] + chunkSize[1]; - - await component.scrollToBottom(); - - await test.expect(component.waitForContainerChildCountEqualsTo(expectedIndex)).resolves.toBeUndefined(); - await test.expect(component.waitForDataIndexChild(expectedIndex - 1)).resolves.toBeUndefined(); - }); - - await test.step('Third chunk', async () => { - const - expectedIndex = chunkSize[0] + chunkSize[1] + chunkSize[2]; - - await component.scrollToBottom(); - - await test.expect(component.waitForContainerChildCountEqualsTo(expectedIndex)).resolves.toBeUndefined(); - await test.expect(component.waitForDataIndexChild(expectedIndex - 1)).resolves.toBeUndefined(); - }); - - await test.step('Lifecycle is done', async () => { - await component.scrollToBottom(); - - await test.expect(component.waitForLifecycleDone()).resolves.toBeUndefined(); - }); - }); - }); -}); diff --git a/src/components/base/b-scrolly/test/unit/functional/rendering/default.ts b/src/components/base/b-scrolly/test/unit/functional/rendering/default.ts index 5ec84395fb..b7f7477ea7 100644 --- a/src/components/base/b-scrolly/test/unit/functional/rendering/default.ts +++ b/src/components/base/b-scrolly/test/unit/functional/rendering/default.ts @@ -7,25 +7,28 @@ */ /** - * @file Test cases of the component lifecycle + * @file Basic test cases for component rendering functionality. */ import test from 'tests/config/unit/test'; import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; -import type { ShouldPerform } from 'components/base/b-scrolly/interface'; +import type { ComponentState, ShouldPerform } from 'components/base/b-scrolly/interface'; +import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; -test.describe(' rendering via component factory', () => { +test.describe('', () => { let - component: Awaited>['component'], - provider:Awaited>['provider'], - state: Awaited>['state']; + component: ScrollyTestHelpers['component'], + provider: ScrollyTestHelpers['provider'], + state: ScrollyTestHelpers['state']; test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); ({component, provider, state} = await createTestHelpers(page)); await provider.start(); + + await page.setViewportSize({height: 640, width: 360}); }); test('Should render all loaded data', async () => { @@ -60,7 +63,61 @@ test.describe(' rendering via component factory', () => { await test.expect(component.childList).toHaveCount(chunkSize * 3); }); - test.skip('Rendering components with children', async () => { + test('Should render components with child', async () => { // .. }); + + test.describe('With a different chunk size for each render cycle', () => { + test('Should render 6 components first, then 12, then 18', async () => { + const chunkSize = [6, 12, 18]; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize[0])}) + .responseOnce(200, {data: state.data.addData(chunkSize[1])}) + .responseOnce(200, {data: state.data.addData(chunkSize[2])}) + .response(200, {data: []}); + + await component.setProps({ + chunkSize: (state: ComponentState) => [6, 12, 18][state.renderPage] ?? 18, + shouldPerformDataRender: ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 + }); + + await component.withDefaultPaginationProviderProps(); + await component.build(); + + await test.step('First chunk', async () => { + const + expectedIndex = chunkSize[0]; + + await test.expect(component.waitForContainerChildCountEqualsTo(expectedIndex)).resolves.toBeUndefined(); + await test.expect(component.waitForDataIndexChild(expectedIndex - 1)).resolves.toBeUndefined(); + }); + + await test.step('Second chunk', async () => { + const + expectedIndex = chunkSize[0] + chunkSize[1]; + + await component.scrollToBottom(); + + await test.expect(component.waitForContainerChildCountEqualsTo(expectedIndex)).resolves.toBeUndefined(); + await test.expect(component.waitForDataIndexChild(expectedIndex - 1)).resolves.toBeUndefined(); + }); + + await test.step('Third chunk', async () => { + const + expectedIndex = chunkSize[0] + chunkSize[1] + chunkSize[2]; + + await component.scrollToBottom(); + + await test.expect(component.waitForContainerChildCountEqualsTo(expectedIndex)).resolves.toBeUndefined(); + await test.expect(component.waitForDataIndexChild(expectedIndex - 1)).resolves.toBeUndefined(); + }); + + await test.step('Lifecycle is done', async () => { + await component.scrollToBottom(); + + await test.expect(component.waitForLifecycleDone()).resolves.toBeUndefined(); + }); + }); + }); }); diff --git a/src/components/base/b-scrolly/test/unit/functional/rendering/items-factory.ts b/src/components/base/b-scrolly/test/unit/functional/rendering/items-factory.ts index ffc6336f52..b8a24dffea 100644 --- a/src/components/base/b-scrolly/test/unit/functional/rendering/items-factory.ts +++ b/src/components/base/b-scrolly/test/unit/functional/rendering/items-factory.ts @@ -7,7 +7,7 @@ */ /** - * @file Test cases of the component lifecycle + * @file Test cases of the component `itemsFactory` prop. */ import test from 'tests/config/unit/test'; @@ -15,12 +15,13 @@ import test from 'tests/config/unit/test'; import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; import type { ComponentItemFactory } from 'components/base/b-scrolly/b-scrolly'; import type { ComponentItem, ShouldPerform } from 'components/base/b-scrolly/interface'; +import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; test.describe(' rendering via component factory', () => { let - component: Awaited>['component'], - provider:Awaited>['provider'], - state: Awaited>['state']; + component: ScrollyTestHelpers['component'], + provider: ScrollyTestHelpers['provider'], + state: ScrollyTestHelpers['state']; test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); diff --git a/src/components/base/b-scrolly/test/unit/functional/state/index.ts b/src/components/base/b-scrolly/test/unit/functional/state/base.ts similarity index 95% rename from src/components/base/b-scrolly/test/unit/functional/state/index.ts rename to src/components/base/b-scrolly/test/unit/functional/state/base.ts index 9cb67dd879..63086f223e 100644 --- a/src/components/base/b-scrolly/test/unit/functional/state/index.ts +++ b/src/components/base/b-scrolly/test/unit/functional/state/base.ts @@ -6,18 +6,23 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +/** + * @file This file contains test cases that verify the correctness of the internal component state module. + */ + import test from 'tests/config/unit/test'; import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import { defaultShouldProps } from 'components/base/b-scrolly/const'; import type { ComponentItem, ShouldPerform } from 'components/base/b-scrolly/b-scrolly'; +import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; test.describe(' state', () => { let - component: Awaited>['component'], - provider:Awaited>['provider'], - state: Awaited>['state']; + component: ScrollyTestHelpers['component'], + provider: ScrollyTestHelpers['provider'], + state: ScrollyTestHelpers['state']; test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); @@ -249,9 +254,4 @@ test.describe(' state', () => { })); }); }); - - test.skip('Events state', async () => { - // ... - }); - }); diff --git a/src/components/base/b-scrolly/test/unit/functional/emitter/state.ts b/src/components/base/b-scrolly/test/unit/functional/state/emitter.ts similarity index 100% rename from src/components/base/b-scrolly/test/unit/functional/emitter/state.ts rename to src/components/base/b-scrolly/test/unit/functional/state/emitter.ts diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts b/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts index 89ea709c87..092bb0f92a 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts +++ b/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts @@ -7,20 +7,21 @@ */ /** - * @file Test cases of the component lifecycle + * @file Test cases of the component lifecycle. */ import test from 'tests/config/unit/test'; import { defaultShouldProps } from 'components/base/b-scrolly/const'; import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; +import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; test.describe('', () => { let - component: Awaited>['component'], - initLoadSpy: Awaited>['initLoadSpy'], - provider:Awaited>['provider'], - state: Awaited>['state']; + component: ScrollyTestHelpers['component'], + initLoadSpy: ScrollyTestHelpers['initLoadSpy'], + provider: ScrollyTestHelpers['provider'], + state: ScrollyTestHelpers['state']; test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); @@ -29,20 +30,6 @@ test.describe('', () => { await provider.start(); }); - test('1', async () => { - const chunkSize = 12; - - await component.setProps({ - chunkSize, - disableObserver: true - }); - - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); - - await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); - }); - test('2', async () => { const chunkSize = 12, diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts b/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts index f6a14d059d..95b4bd9e01 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts +++ b/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts @@ -18,7 +18,7 @@ import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; import type { SlotsStateObj } from 'components/base/b-scrolly/modules/slots'; import type { ShouldPerform } from 'components/base/b-scrolly/b-scrolly'; -test.describe(' slots', () => { +test.describe('', () => { let component: Awaited>['component'], provider: Awaited>['provider'], From 05ac277b2f34c7b45b11f0d05fb9475a69d8fcd3 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sun, 25 Jun 2023 22:23:09 +0300 Subject: [PATCH 038/159] WIP --- src/components/base/b-scrolly/handlers.ts | 25 +++++++++++-------- .../base/b-scrolly/interface/events.ts | 2 +- .../base/b-scrolly/modules/slots/index.ts | 10 +++++++- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/components/base/b-scrolly/handlers.ts b/src/components/base/b-scrolly/handlers.ts index 3298aa7728..dc779cdd8d 100644 --- a/src/components/base/b-scrolly/handlers.ts +++ b/src/components/base/b-scrolly/handlers.ts @@ -145,16 +145,23 @@ export abstract class bScrollyHandlers extends bScrollyProps { this.componentInternalState.updateData(data.data, isInitialLoading); this.componentInternalState.incrementLoadPage(); - this.shouldStopRequestingDataWrapper(); + const + isRequestsStopped = this.shouldStopRequestingDataWrapper(); + this.componentEmitter.emit(componentEvents.dataLoadSuccess, data.data, isInitialLoading); this.slotsStateController.loadingSuccessState(); - this.loadDataOrPerformRender(); - if (isInitialLoading && Object.size(data.data) === 0) { - if (this.shouldStopRequestingDataWrapper()) { - this.onDataEmpty(isInitialLoading); - } + if ( + isInitialLoading && + isRequestsStopped && + Object.size(data.data) === 0 + ) { + this.onDataEmpty(); + this.onLifecycleDone(); + + } else { + this.loadDataOrPerformRender(); } } @@ -173,13 +180,11 @@ export abstract class bScrollyHandlers extends bScrollyProps { /** * Handler: data empty event. * Triggered when the loaded data is empty. - * - * @param isInitialLoading - Indicates whether it is an initial component loading. */ - onDataEmpty(isInitialLoading: boolean): void { + onDataEmpty(): void { this.slotsStateController.emptyState(); - this.componentEmitter.emit(componentEvents.dataEmpty, isInitialLoading); + this.componentEmitter.emit(componentEvents.dataEmpty); } /** diff --git a/src/components/base/b-scrolly/interface/events.ts b/src/components/base/b-scrolly/interface/events.ts index f7a744a4d5..71b6a52929 100644 --- a/src/components/base/b-scrolly/interface/events.ts +++ b/src/components/base/b-scrolly/interface/events.ts @@ -121,7 +121,7 @@ export interface LocalEventPayloadMap { [componentDataLocalEvents.dataLoadSuccess]: [data: object[], isInitialLoading: boolean]; [componentDataLocalEvents.dataLoadStart]: [isInitialLoading: boolean]; [componentDataLocalEvents.dataLoadError]: [isInitialLoading: boolean]; - [componentDataLocalEvents.dataEmpty]: [isInitialLoading: boolean]; + [componentDataLocalEvents.dataEmpty]: []; [componentLocalEvents.resetState]: []; [componentLocalEvents.lifecycleDone]: []; diff --git a/src/components/base/b-scrolly/modules/slots/index.ts b/src/components/base/b-scrolly/modules/slots/index.ts index 364eb049e0..09d82680d3 100644 --- a/src/components/base/b-scrolly/modules/slots/index.ts +++ b/src/components/base/b-scrolly/modules/slots/index.ts @@ -34,6 +34,11 @@ export class SlotsStateController extends Friend { group: slotsStateControllerAsyncGroup }; + /** + * The last state of the slots. + */ + protected lastState?: SlotsStateObj; + /** * Displays the slots that should be shown when the data state is empty. */ @@ -56,7 +61,7 @@ export class SlotsStateController extends Friend { this.setSlotsVisibility({ container: true, done: true, - empty: false, + empty: this.lastState?.empty ?? false, loader: false, renderNext: false, retry: false, @@ -114,6 +119,7 @@ export class SlotsStateController extends Friend { */ reset(): void { this.async.clearAll({group: new RegExp(slotsStateControllerAsyncGroup)}); + this.lastState = undefined; } /** @@ -122,6 +128,8 @@ export class SlotsStateController extends Friend { * @param stateObj - An object specifying the visibility state of each slot. */ protected setSlotsVisibility(stateObj: Required): void { + this.lastState = stateObj; + this.async.cancelAnimationFrame(this.asyncUpdateLabel); this.async.requestAnimationFrame(() => { From c828a497353e50beae6c07ea6b0a858bc0617a64 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sun, 25 Jun 2023 22:36:53 +0300 Subject: [PATCH 039/159] :art: --- src/components/base/b-scrolly/b-scrolly.ts | 70 +++++++++++----------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts index 47e1a67b2b..0bd54dc777 100644 --- a/src/components/base/b-scrolly/b-scrolly.ts +++ b/src/components/base/b-scrolly/b-scrolly.ts @@ -257,41 +257,6 @@ export default class bScrolly extends bScrollyHandlers implements iItems { }; } - /** - * Renders components using {@link bScrolly.componentFactory} and inserts them into the DOM tree. - * {@link bScrolly.componentFactory}, in turn, calls {@link bScrolly.itemsFactory} to obtain - * the set of components to render. - */ - protected performRender(): void { - this.onRenderStart(); - - const - items = this.componentFactory.produceComponentItems(), - nodes = this.componentFactory.produceNodes(items), - mounted = this.componentFactory.produceMounted(items, nodes); - - this.observer.observe(mounted); - this.onDomInsertStart(mounted); - - const - fragment = document.createDocumentFragment(); - - for (let i = 0; i < nodes.length; i++) { - this.dom.appendChild(fragment, nodes[i], { - group: bScrollyDomInsertAsyncGroup, - destroyIfComponent: true - }); - } - - this.async.requestAnimationFrame(() => { - this.$refs.container.appendChild(fragment); - - this.onDomInsertDone(); - this.onRenderDone(); - - }, {label: $$.insertDomRaf, group: bScrollyDomInsertAsyncGroup}); - } - /** * A function that performs actions (data loading/rendering) depending * on the result of the {@link bScrolly.renderGuard} method. @@ -335,4 +300,39 @@ export default class bScrolly extends bScrollyHandlers implements iItems { } } } + + /** + * Renders components using {@link bScrolly.componentFactory} and inserts them into the DOM tree. + * {@link bScrolly.componentFactory}, in turn, calls {@link bScrolly.itemsFactory} to obtain + * the set of components to render. + */ + protected performRender(): void { + this.onRenderStart(); + + const + items = this.componentFactory.produceComponentItems(), + nodes = this.componentFactory.produceNodes(items), + mounted = this.componentFactory.produceMounted(items, nodes); + + this.observer.observe(mounted); + this.onDomInsertStart(mounted); + + const + fragment = document.createDocumentFragment(); + + for (let i = 0; i < nodes.length; i++) { + this.dom.appendChild(fragment, nodes[i], { + group: bScrollyDomInsertAsyncGroup, + destroyIfComponent: true + }); + } + + this.async.requestAnimationFrame(() => { + this.$refs.container.appendChild(fragment); + + this.onDomInsertDone(); + this.onRenderDone(); + + }, {label: $$.insertDomRaf, group: bScrollyDomInsertAsyncGroup}); + } } From 07796fa8317f6f13f8b0ce0e69ead86004eed31d Mon Sep 17 00:00:00 2001 From: bonkalol Date: Tue, 27 Jun 2023 16:24:12 +0300 Subject: [PATCH 040/159] Add test cases for retry --- src/components/base/b-scrolly/README.md | 3 + src/components/base/b-scrolly/b-scrolly.ts | 48 +++-- src/components/base/b-scrolly/handlers.ts | 2 + .../base/b-scrolly/interface/component.ts | 5 + .../base/b-scrolly/modules/state/helpers.ts | 3 +- .../base/b-scrolly/modules/state/index.ts | 9 + .../test/api/component-object/index.ts | 10 ++ .../base/b-scrolly/test/api/helpers/index.ts | 14 +- .../test/unit/functional/state/base.ts | 2 +- .../test/unit/functional/state/emitter.ts | 2 +- .../b-scrolly/test/unit/scenario/retry.ts | 166 ++++++++++++++++++ tests/helpers/providers/interceptor/index.ts | 1 + 12 files changed, 232 insertions(+), 33 deletions(-) create mode 100644 src/components/base/b-scrolly/test/unit/scenario/retry.ts diff --git a/src/components/base/b-scrolly/README.md b/src/components/base/b-scrolly/README.md index 2ad793c2ed..87d12ad9ed 100644 --- a/src/components/base/b-scrolly/README.md +++ b/src/components/base/b-scrolly/README.md @@ -16,6 +16,9 @@ - dbChange - описать что состояние типа loadPage, renderPage обновляются постфактум (после того как загрузка данных случилась, после того как рендеринг случился) - лишний вызов shouldStopRequestingData в onDataLoadSuccess +- поддержка onRequestError? +- обработка async replace error в initLoad +- не нравится проверка .then((res) => if (res == null) {this.onDataLoadError()}) ```mermaid graph TD; diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts index 0bd54dc777..bb26983e9b 100644 --- a/src/components/base/b-scrolly/b-scrolly.ts +++ b/src/components/base/b-scrolly/b-scrolly.ts @@ -55,6 +55,8 @@ export default class bScrolly extends bScrollyHandlers implements iItems { const state = this.getComponentState(); + this.componentInternalState.setIsLastErrored(false); + if (!this.isReady && this.isReadyOnce) { this.reset(); } @@ -77,32 +79,23 @@ export default class bScrolly extends bScrollyHandlers implements iItems { if (Object.isPromise(initLoadResult)) { initLoadResult .then((res) => { + if ( + (isInitialLoading && this.db == null) || + (!isInitialLoading && res == null) + ) { + return this.onDataLoadError(isInitialLoading); + } + this.onDataLoadSuccess(isInitialLoading, isInitialLoading ? this.db : this.convertDataToDB(res)); }) - .catch((err) => { - this.componentInternalState.setIsLoadingInProgress(false); + .catch(() => { this.onDataLoadError(isInitialLoading); - - throw err; }); } return >initLoadResult; } - /** - * Initializes the loading of the next data chunk. - * @throws {@link ReferenceError} if there is no `dataProvider` set. - */ - initLoadNext(): Promise { - if (!this.dataProvider) { - throw ReferenceError('Missing dataProvider'); - } - - const params = this.getRequestParams(); - return this.dataProvider.get(params[0], params[1]); - } - /** * Resets the component state to its initial state. */ @@ -199,6 +192,19 @@ export default class bScrolly extends bScrollyHandlers implements iItems { return data.slice(nextDataSliceStartIndex, nextDataSliceEndIndex); } + /** + * Initializes the loading of the next data chunk. + * @throws {@link ReferenceError} if there is no `dataProvider` set. + */ + protected initLoadNext(): Promise { + if (!this.dataProvider) { + throw ReferenceError('Missing dataProvider'); + } + + const params = this.getRequestParams(); + return this.dataProvider.get(params[0], params[1]); + } + protected override convertDataToDB(data: unknown): O | this['DB'] { const result = super.convertDataToDB(data); this.onConvertDataToDB(data); @@ -266,7 +272,13 @@ export default class bScrolly extends bScrollyHandlers implements iItems { */ protected loadDataOrPerformRender(): void { const - state = this.getComponentState(), + state = this.getComponentState(); + + if (state.isLastErrored) { + return; + } + + const {result, reason} = this.renderGuard(state); if (result) { diff --git a/src/components/base/b-scrolly/handlers.ts b/src/components/base/b-scrolly/handlers.ts index dc779cdd8d..b1ce8d067d 100644 --- a/src/components/base/b-scrolly/handlers.ts +++ b/src/components/base/b-scrolly/handlers.ts @@ -172,6 +172,8 @@ export abstract class bScrollyHandlers extends bScrollyProps { * @param isInitialLoading - Indicates whether it is an initial component loading. */ onDataLoadError(isInitialLoading: boolean): void { + this.componentInternalState.setIsLoadingInProgress(false); + this.componentInternalState.setIsLastErrored(true); this.slotsStateController.loadingFailedState(); this.componentEmitter.emit(componentEvents.dataLoadError, isInitialLoading); diff --git a/src/components/base/b-scrolly/interface/component.ts b/src/components/base/b-scrolly/interface/component.ts index 99eac7437d..172d4e92b1 100644 --- a/src/components/base/b-scrolly/interface/component.ts +++ b/src/components/base/b-scrolly/interface/component.ts @@ -93,6 +93,11 @@ export interface ComponentState { */ isLastEmpty: boolean; + /** + * Indicates if the last data load ended with an error. + */ + isLastErrored: boolean; + /** * Indicates if the component is in the initial loading state. */ diff --git a/src/components/base/b-scrolly/modules/state/helpers.ts b/src/components/base/b-scrolly/modules/state/helpers.ts index 7b04ce7b30..746d515927 100644 --- a/src/components/base/b-scrolly/modules/state/helpers.ts +++ b/src/components/base/b-scrolly/modules/state/helpers.ts @@ -32,7 +32,8 @@ export function createInitialState(): ComponentState { isInitialRender: true, isRequestsStopped: false, isLoadingInProgress: false, - isLifecycleDone: false + isLifecycleDone: false, + isLastErrored: false }; } diff --git a/src/components/base/b-scrolly/modules/state/index.ts b/src/components/base/b-scrolly/modules/state/index.ts index 3b968a7376..e8ee55facc 100644 --- a/src/components/base/b-scrolly/modules/state/index.ts +++ b/src/components/base/b-scrolly/modules/state/index.ts @@ -135,6 +135,15 @@ export class ComponentInternalState extends Friend { this.state.isLoadingInProgress = value; } + /** + * Устанавливает флаг который указывает на то, что последняя загрузка завершилась с ошибкой. + * + * @param value + */ + setIsLastErrored(value: boolean): void { + this.state.isLastErrored = value; + } + /** * Sets the maximum viewed index based on the passed component's index. * diff --git a/src/components/base/b-scrolly/test/api/component-object/index.ts b/src/components/base/b-scrolly/test/api/component-object/index.ts index f56520f4a6..cfc29b9e0c 100644 --- a/src/components/base/b-scrolly/test/api/component-object/index.ts +++ b/src/components/base/b-scrolly/test/api/component-object/index.ts @@ -107,6 +107,16 @@ export class ScrollyComponentObject extends ComponentObject { return this.waitForChild(`[data-index="${index}"]`); } + /** + * Returns a promise that resolves when the specified event occurs. + * + * @param eventName - The name of the event to wait for. + * @returns A promise that resolves to the payload of the event, or `undefined`. + */ + async waitForEvent(eventName: string): Promise> { + return this.component.evaluate((ctx, [eventName]) => ctx.promisifyOnce(eventName), [eventName]); + } + /** * Waits for the component lifecycle to be done. */ diff --git a/src/components/base/b-scrolly/test/api/helpers/index.ts b/src/components/base/b-scrolly/test/api/helpers/index.ts index 549bd98849..4c8571aa09 100644 --- a/src/components/base/b-scrolly/test/api/helpers/index.ts +++ b/src/components/base/b-scrolly/test/api/helpers/index.ts @@ -15,6 +15,7 @@ import { ScrollyComponentObject } from 'components/base/b-scrolly/test/api/compo import { RequestInterceptor } from 'tests/helpers/providers/interceptor'; import { componentEvents, componentObserverLocalEvents } from 'components/base/b-scrolly/const'; import type { DataConveyor, DataItemCtor, MountedItemCtor, StateApi, ScrollyTestHelpers, MountedSeparatorCtor, IndexedObj } from 'components/base/b-scrolly/test/api/helpers/interface'; +import { createInitialState as createInitialStateObj } from 'components/base/b-scrolly/modules/state/helpers'; export * from 'components/base/b-scrolly/test/api/component-object'; @@ -213,23 +214,12 @@ export function createStateApi( */ export function createInitialState(state: Partial): ComponentState { return { - renderPage: 0, - loadPage: 0, + ...createInitialStateObj(), maxViewedItem: Object.cast(test.expect.any(Number)), maxViewedChild: Object.cast(test.expect.any(Number)), itemsTillEnd: Object.cast(test.expect.any(Number)), childTillEnd: Object.cast(test.expect.any(Number)), - isInitialRender: true, - isInitialLoading: true, isLoadingInProgress: Object.cast(test.expect.any(Boolean)), - isLastEmpty: false, - isLifecycleDone: false, - isRequestsStopped: false, - lastLoadedData: [], - data: [], - items: [], - childList: [], - lastLoadedRawData: undefined, ...state }; } diff --git a/src/components/base/b-scrolly/test/unit/functional/state/base.ts b/src/components/base/b-scrolly/test/unit/functional/state/base.ts index 63086f223e..e8d4a51268 100644 --- a/src/components/base/b-scrolly/test/unit/functional/state/base.ts +++ b/src/components/base/b-scrolly/test/unit/functional/state/base.ts @@ -18,7 +18,7 @@ import { defaultShouldProps } from 'components/base/b-scrolly/const'; import type { ComponentItem, ShouldPerform } from 'components/base/b-scrolly/b-scrolly'; import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; -test.describe(' state', () => { +test.describe('', () => { let component: ScrollyTestHelpers['component'], provider: ScrollyTestHelpers['provider'], diff --git a/src/components/base/b-scrolly/test/unit/functional/state/emitter.ts b/src/components/base/b-scrolly/test/unit/functional/state/emitter.ts index a4498df425..14f2c4e086 100644 --- a/src/components/base/b-scrolly/test/unit/functional/state/emitter.ts +++ b/src/components/base/b-scrolly/test/unit/functional/state/emitter.ts @@ -15,7 +15,7 @@ import test from 'tests/config/unit/test'; import { createTestHelpers, filterEmitterResults } from 'components/base/b-scrolly/test/api/helpers'; import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; -test.describe(' emitter state', () => { +test.describe('', () => { let component: ScrollyTestHelpers['component'], provider: ScrollyTestHelpers['provider'], diff --git a/src/components/base/b-scrolly/test/unit/scenario/retry.ts b/src/components/base/b-scrolly/test/unit/scenario/retry.ts new file mode 100644 index 0000000000..cbcbfec89a --- /dev/null +++ b/src/components/base/b-scrolly/test/unit/scenario/retry.ts @@ -0,0 +1,166 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * @file This file contains test cases that verify the correctness of the internal component state module. + */ + +import test from 'tests/config/unit/test'; + +import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; +import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; +import type { ComponentElement } from 'core/component'; +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; + +test.describe('', () => { + let + component: ScrollyTestHelpers['component'], + provider: ScrollyTestHelpers['provider'], + state: ScrollyTestHelpers['state']; + + test.beforeEach(async ({demoPage, page}) => { + await demoPage.goto(); + + ({component, provider, state} = await createTestHelpers(page)); + await provider.start(); + + await component.setChildren({ + retry: { + type: 'div', + attrs: { + id: 'retry', + '@click': () => (>document.querySelector('.b-scrolly')).component?.initLoad() + } + } + }); + }); + + test.describe('Data loading error ocurred on initial loading', () => { + test('Should reload data after initLoad call', async () => { + const chunkSize = 12; + + provider + .responseOnce(500, {}) + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: []}); + + await component.setProps({chunkSize}); + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + + await component.waitForSlotState('retry', true); + await component.node.locator('#retry').click(); + + await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); + }); + + test('Should goes to retry state after failing to load data twice', async () => { + const chunkSize = 12; + + provider + .responseOnce(500, {}) + .responseOnce(500, {}) + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: []}); + + await component.setProps({chunkSize}); + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + + await component.waitForSlotState('retry', true); + const event = component.waitForEvent('dataLoadError'); + await component.node.locator('#retry').click(); + await event; + await component.node.locator('#retry').click(); + + await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); + }); + }); + + test.describe('Data loading error ocurred on second data chunk loading', () => { + test('Should reload second data chunk after initLoad call', async () => { + const chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .responseOnce(500, {}) + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: []}); + + await component.withDefaultPaginationProviderProps({chunkSize}); + + await component.setProps({ + chunkSize, + shouldPerformDataRender: ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 + }); + + await component.build(); + await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.scrollToBottom(); + + await component.waitForSlotState('retry', true); + await component.node.locator('#retry').click(); + await component.waitForDataIndexChild(chunkSize * 2 - 1); + + await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize * 2)).resolves.toBeUndefined(); + }); + }); + + test.describe('An error occurred while loading the second chunk of data for rendering the first chunk of elements', () => { + test('Should reload second data chunk and perform a render', async () => { + const + chunkSize = 12, + providerChunkSize = chunkSize / 2; + + provider + .responseOnce(200, {data: state.data.addData(providerChunkSize)}) + .responseOnce(500, {}) + .responseOnce(200, {data: state.data.addData(providerChunkSize)}) + .response(200, {data: []}); + + await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); + + await component.setProps({ + chunkSize, + shouldPerformDataRender: ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 + }); + + await component.build(); + await component.waitForSlotState('retry', true); + await component.node.locator('#retry').click(); + + await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.waitForDataIndexChild(chunkSize - 1); + await component.waitForLifecycleDone(); + + await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); + }); + }); + + test.describe('An error occurred while loading the last chunk of data', () => { + test('After a successful load, the component lifecycle should complete', async () => { + const chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .responseOnce(500, {}) + .response(200, {data: []}); + + await component.setProps({chunkSize}); + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + + await component.waitForSlotState('retry', true); + await component.node.locator('#retry').click(); + + test.expect(provider.mock.mock.calls.length).toBe(3); + await test.expect(component.waitForLifecycleDone()).resolves.toBeUndefined(); + await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); + }); + }); +}); diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index 781298d75c..9a79441045 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -100,6 +100,7 @@ export class RequestInterceptor { if (Object.isFunction(handlerOrStatus)) { fn = handlerOrStatus; + } else { const status = handlerOrStatus; fn = this.cookResponseFn(status, payload, opts); From 13fa1e366727a9b56b7052e7a1dba0072f60016e Mon Sep 17 00:00:00 2001 From: bonkalol Date: Tue, 27 Jun 2023 18:11:05 +0300 Subject: [PATCH 041/159] Test cases for manual rendering --- .../base/b-scrolly/modules/slots/index.ts | 2 +- .../test/api/component-object/index.ts | 4 +- .../test/api/component-object/styles.ts | 4 + .../functional/rendering/items-factory.ts | 2 +- .../test/unit/scenario/manual-rendering.ts | 129 ++++++++++++++++++ .../b-scrolly/test/unit/scenario/retry.ts | 2 +- tests/helpers/providers/interceptor/index.ts | 17 +-- .../providers/interceptor/interface.ts | 2 + 8 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 src/components/base/b-scrolly/test/unit/scenario/manual-rendering.ts diff --git a/src/components/base/b-scrolly/modules/slots/index.ts b/src/components/base/b-scrolly/modules/slots/index.ts index 09d82680d3..700d6e337a 100644 --- a/src/components/base/b-scrolly/modules/slots/index.ts +++ b/src/components/base/b-scrolly/modules/slots/index.ts @@ -108,7 +108,7 @@ export class SlotsStateController extends Friend { done: false, empty: false, loader: false, - renderNext: false, + renderNext: true, retry: false, tombstones: false }); diff --git a/src/components/base/b-scrolly/test/api/component-object/index.ts b/src/components/base/b-scrolly/test/api/component-object/index.ts index cfc29b9e0c..1f01111303 100644 --- a/src/components/base/b-scrolly/test/api/component-object/index.ts +++ b/src/components/base/b-scrolly/test/api/component-object/index.ts @@ -140,7 +140,7 @@ export class ScrollyComponentObject extends ComponentObject { * @param timeout - The timeout for waiting (optional). */ async waitForSlotState(slotName: keyof ComponentRefs, isVisible: boolean, timeout?: number): Promise { - const slot = this.node.locator(this.elSelector(slotName)); + const slot = this.node.locator(this.elSelector(slotName.dasherize())); await slot.waitFor({state: isVisible ? 'visible' : 'hidden', timeout}); } @@ -156,7 +156,7 @@ export class ScrollyComponentObject extends ComponentObject { empty = this.node.locator(this.elSelector('empty')), retry = this.node.locator(this.elSelector('retry')), done = this.node.locator(this.elSelector('done')), - renderNext = this.node.locator(this.elSelector('renderNext')); + renderNext = this.node.locator(this.elSelector('render-next')); return { container: await container.isVisible(), diff --git a/src/components/base/b-scrolly/test/api/component-object/styles.ts b/src/components/base/b-scrolly/test/api/component-object/styles.ts index af9534ee51..5f861d58a6 100644 --- a/src/components/base/b-scrolly/test/api/component-object/styles.ts +++ b/src/components/base/b-scrolly/test/api/component-object/styles.ts @@ -44,6 +44,10 @@ export const testStyles = ` content: "retry" } +#renderNext:after { + content: "render next" +} + #loader, #tombstone { display: block; diff --git a/src/components/base/b-scrolly/test/unit/functional/rendering/items-factory.ts b/src/components/base/b-scrolly/test/unit/functional/rendering/items-factory.ts index b8a24dffea..676f8447d2 100644 --- a/src/components/base/b-scrolly/test/unit/functional/rendering/items-factory.ts +++ b/src/components/base/b-scrolly/test/unit/functional/rendering/items-factory.ts @@ -230,7 +230,7 @@ test.describe(' rendering via component factory', () => { await test.expect(component.childList).toHaveCount(chunkSize); }); - test('`ItemsFactory` returns twice as much data as `chunkSize`', async () => { + test('`itemsFactory` returns twice as much data as `chunkSize`', async () => { const chunkSize = 12; diff --git a/src/components/base/b-scrolly/test/unit/scenario/manual-rendering.ts b/src/components/base/b-scrolly/test/unit/scenario/manual-rendering.ts new file mode 100644 index 0000000000..918112582a --- /dev/null +++ b/src/components/base/b-scrolly/test/unit/scenario/manual-rendering.ts @@ -0,0 +1,129 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * @file This file contains test cases for verifying the functionality of loading data + * using methods instead of observers. + */ + +import test from 'tests/config/unit/test'; + +import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; +import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; +import type { ComponentElement } from 'core/component'; +import type bScrolly from 'components/base/b-scrolly/b-scrolly'; + +test.describe('', () => { + let + component: ScrollyTestHelpers['component'], + provider: ScrollyTestHelpers['provider'], + state: ScrollyTestHelpers['state']; + + test.beforeEach(async ({demoPage, page}) => { + await demoPage.goto(); + + ({component, provider, state} = await createTestHelpers(page)); + await provider.start(); + + await component.setChildren({ + renderNext: { + type: 'div', + attrs: { + id: 'renderNext', + '@click': () => (>document.querySelector('.b-scrolly')).component?.initLoad() + } + }, + + retry: { + type: 'div', + attrs: { + id: 'retry', + '@click': () => (>document.querySelector('.b-scrolly')).component?.initLoad() + } + } + }); + }); + + test.describe('Загрузился и отрисовался первый чанк данных', () => { + const chunkSize = 12; + + test.beforeEach(async () => { + provider.response(200, () => ({data: state.data.addData(chunkSize)})); + + await component.setProps({ + chunkSize, + disableObserver: true, + shouldPerformDataRender: () => true + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForContainerChildCountEqualsTo(chunkSize); + }); + + test('Должен загрузить и отрисовать следующий после вызова initLoad', async () => { + await component.node.locator('#renderNext').click(); + + test.expect(provider.mock.mock.calls.length).toBe(2); + await test.expect(component.waitForDataIndexChild(chunkSize * 2 - 1)).resolves.toBeUndefined(); + await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize * 2)).resolves.toBeUndefined(); + }); + + test('Должен завершиться жизненный цикл компонента после того как все данные загружены', async () => { + provider.response(200, {data: []}); + + await component.node.locator('#renderNext').click(); + + await test.expect(component.waitForLifecycleDone()).resolves.toBeUndefined(); + await test.expect(component.waitForSlotState('renderNext', false)).resolves.toBeUndefined(); + await test.expect(component.waitForDataIndexChild(chunkSize - 1)).resolves.toBeUndefined(); + await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); + }); + + test.describe('Произошла ошибка загрузки второго чанка данных', () => { + test.beforeEach(async () => { + provider.responseOnce(500, {data: []}); + await component.node.locator('#renderNext').click(); + }); + + test('Не должен отображать renderNext слот', async () => { + await test.expect(component.waitForSlotState('renderNext', false)).resolves.toBeUndefined(); + }); + + test('Должен отображать retry слот', async () => { + await test.expect(component.waitForSlotState('retry', true)).resolves.toBeUndefined(); + }); + + test.describe('Произошла перезагрузка данных', () => { + test.beforeEach(async () => { + await component.node.locator('#retry').click(); + }); + + test('Должен отобразить загруженные данные', async () => { + await test.expect(component.waitForDataIndexChild(chunkSize * 2 - 1)).resolves.toBeUndefined(); + await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize * 2)).resolves.toBeUndefined(); + }); + + test.describe('Закончились данные к отображению', () => { + test.beforeEach(async () => { + provider.response(200, {data: []}); + await component.node.locator('#renderNext').click(); + }); + + test('Должен завершиться жизненный цикл компонента после того как все данные загружены', async () => { + await test.expect(component.waitForLifecycleDone()).resolves.toBeUndefined(); + await test.expect(component.waitForSlotState('renderNext', false)).resolves.toBeUndefined(); + await test.expect(component.waitForDataIndexChild(chunkSize * 2 - 1)).resolves.toBeUndefined(); + await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize * 2)).resolves.toBeUndefined(); + }); + }); + }); + }); + }); + +}); diff --git a/src/components/base/b-scrolly/test/unit/scenario/retry.ts b/src/components/base/b-scrolly/test/unit/scenario/retry.ts index cbcbfec89a..4704c0216b 100644 --- a/src/components/base/b-scrolly/test/unit/scenario/retry.ts +++ b/src/components/base/b-scrolly/test/unit/scenario/retry.ts @@ -7,7 +7,7 @@ */ /** - * @file This file contains test cases that verify the correctness of the internal component state module. + * @file This file contains test cases to verify the functionality of reloading data after an error. */ import test from 'tests/config/unit/test'; diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index 9a79441045..f0022cadc9 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -10,7 +10,7 @@ import type { BrowserContext, Page, Request, Route } from 'playwright'; import delay from 'delay'; import { ModuleMocker } from 'jest-mock'; -import type { ResponseHandler, ResponseOptions } from 'tests/helpers/providers/interceptor/interface'; +import type { ResponseHandler, ResponseOptions, ResponsePayload } from 'tests/helpers/providers/interceptor/interface'; /** * API that provides a simple way to intercept and respond to any request. @@ -89,11 +89,11 @@ export class RequestInterceptor { * @param opts - The response options. * @returns The current instance of RequestInterceptor. */ - responseOnce(status: number, payload: object | string | number, opts?: ResponseOptions): this; + responseOnce(status: number, payload: ResponsePayload | ResponseHandler, opts?: ResponseOptions): this; responseOnce( handlerOrStatus: number | ResponseHandler, - payload?: object | string | number, + payload?: ResponsePayload | ResponseHandler, opts?: ResponseOptions ): this { let fn; @@ -140,17 +140,18 @@ export class RequestInterceptor { * @param opts - The response options. * @returns The current instance of RequestInterceptor. */ - response(status: number, payload: object | string | number, opts?: ResponseOptions): this; + response(status: number, payload: ResponsePayload | ResponseHandler, opts?: ResponseOptions): this; response( handlerOrStatus: number | ResponseHandler, - payload?: object | string | number, + payload?: ResponsePayload | ResponseHandler, opts?: ResponseOptions ): this { let fn; if (Object.isFunction(handlerOrStatus)) { fn = handlerOrStatus; + } else { const status = handlerOrStatus; fn = this.cookResponseFn(status, payload, opts); @@ -201,17 +202,17 @@ export class RequestInterceptor { */ protected cookResponseFn( status: number, - payload?: string | object | number, + payload?: ResponsePayload | ResponseHandler, opts?: ResponseOptions ): ResponseHandler { - return async (route) => { + return async (route, request) => { if (opts?.delay != null) { await delay(opts.delay); } return route.fulfill({ status, - body: JSON.stringify(payload), + body: JSON.stringify(Object.isFunction(payload) ? await payload(route, request) : payload), contentType: 'application/json' }); }; diff --git a/tests/helpers/providers/interceptor/interface.ts b/tests/helpers/providers/interceptor/interface.ts index f7846ee0da..f59e1b03de 100644 --- a/tests/helpers/providers/interceptor/interface.ts +++ b/tests/helpers/providers/interceptor/interface.ts @@ -16,3 +16,5 @@ export type ResponseHandler = (route: Route, request: Request) => CanPromise Date: Tue, 27 Jun 2023 19:25:11 +0300 Subject: [PATCH 042/159] :art: --- .../test/unit/lifecycle/slots/slots.ts | 109 +++++++++++++++++- .../test/unit/scenario/manual-rendering.ts | 21 ++-- 2 files changed, 116 insertions(+), 14 deletions(-) diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts b/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts index 95b4bd9e01..7cd29ee493 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts +++ b/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts @@ -7,7 +7,7 @@ */ /** - * @file Test cases of the component lifecycle + * @file This file describes test cases for checking the correctness of displaying component slots in different states. */ import delay from 'delay'; @@ -17,7 +17,9 @@ import test from 'tests/config/unit/test'; import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; import type { SlotsStateObj } from 'components/base/b-scrolly/modules/slots'; import type { ShouldPerform } from 'components/base/b-scrolly/b-scrolly'; +import { BOM } from 'tests/helpers'; +// eslint-disable-next-line max-lines-per-function test.describe('', () => { let component: Awaited>['component'], @@ -365,7 +367,108 @@ test.describe('', () => { }); }); - test.skip('renderNext', async () => { - // ... + test.describe('renderNext', () => { + test.beforeEach(async () => { + await component.setChildren({ + renderNext: { + type: 'div', + attrs: { + id: 'renderNext' + } + } + }); + }); + + test('Activates when data is loaded', async () => { + const chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: []}); + + await component.setProps({ + chunkSize, + disableObserver: true, + shouldPerformDataRender: () => true + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForSlotState('renderNext', true); + + const + slots = await component.getSlotsState(); + + test.expect(slots).toEqual(>{ + container: true, + done: false, + empty: false, + loader: false, + renderNext: true, + retry: false, + tombstones: false + }); + }); + + test('Doesn\'t activates while data is loading', async ({page}) => { + const chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}, {delay: (10).seconds()}) + .response(200, {data: []}); + + await component.setProps({ + chunkSize, + disableObserver: true, + shouldPerformDataRender: () => true + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await BOM.waitForIdleCallback(page); + await component.waitForSlotState('renderNext', false); + + const + slots = await component.getSlotsState(); + + test.expect(slots).toEqual(>{ + container: true, + done: false, + empty: false, + loader: true, + renderNext: false, + retry: false, + tombstones: true + }); + }); + + test('Doesn\'t activates if there\'s a data loading error', async () => { + const chunkSize = 12; + + provider.response(500, {data: []}); + + await component.setProps({ + chunkSize, + disableObserver: true, + shouldPerformDataRender: () => true + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForSlotState('renderNext', false); + + const + slots = await component.getSlotsState(); + + test.expect(slots).toEqual(>{ + container: true, + done: false, + empty: false, + loader: false, + renderNext: false, + retry: true, + tombstones: false + }); + }); }); }); diff --git a/src/components/base/b-scrolly/test/unit/scenario/manual-rendering.ts b/src/components/base/b-scrolly/test/unit/scenario/manual-rendering.ts index 918112582a..5f1ea41baa 100644 --- a/src/components/base/b-scrolly/test/unit/scenario/manual-rendering.ts +++ b/src/components/base/b-scrolly/test/unit/scenario/manual-rendering.ts @@ -49,7 +49,7 @@ test.describe('', () => { }); }); - test.describe('Загрузился и отрисовался первый чанк данных', () => { + test.describe('The first chunk of data is loaded and rendered', () => { const chunkSize = 12; test.beforeEach(async () => { @@ -66,7 +66,7 @@ test.describe('', () => { await component.waitForContainerChildCountEqualsTo(chunkSize); }); - test('Должен загрузить и отрисовать следующий после вызова initLoad', async () => { + test('Should load and render the next chunk after calling initLoad', async () => { await component.node.locator('#renderNext').click(); test.expect(provider.mock.mock.calls.length).toBe(2); @@ -74,7 +74,7 @@ test.describe('', () => { await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize * 2)).resolves.toBeUndefined(); }); - test('Должен завершиться жизненный цикл компонента после того как все данные загружены', async () => { + test('Should complete the component lifecycle after all data is loaded', async () => { provider.response(200, {data: []}); await component.node.locator('#renderNext').click(); @@ -85,37 +85,37 @@ test.describe('', () => { await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); }); - test.describe('Произошла ошибка загрузки второго чанка данных', () => { + test.describe('An error occurred while loading the second chunk of data', () => { test.beforeEach(async () => { provider.responseOnce(500, {data: []}); await component.node.locator('#renderNext').click(); }); - test('Не должен отображать renderNext слот', async () => { + test('Should not display the renderNext slot', async () => { await test.expect(component.waitForSlotState('renderNext', false)).resolves.toBeUndefined(); }); - test('Должен отображать retry слот', async () => { + test('Should display the retry slot', async () => { await test.expect(component.waitForSlotState('retry', true)).resolves.toBeUndefined(); }); - test.describe('Произошла перезагрузка данных', () => { + test.describe('Data reload occurred', () => { test.beforeEach(async () => { await component.node.locator('#retry').click(); }); - test('Должен отобразить загруженные данные', async () => { + test('Should display the loaded data', async () => { await test.expect(component.waitForDataIndexChild(chunkSize * 2 - 1)).resolves.toBeUndefined(); await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize * 2)).resolves.toBeUndefined(); }); - test.describe('Закончились данные к отображению', () => { + test.describe('No more data to display', () => { test.beforeEach(async () => { provider.response(200, {data: []}); await component.node.locator('#renderNext').click(); }); - test('Должен завершиться жизненный цикл компонента после того как все данные загружены', async () => { + test('Should complete the component lifecycle after all data is loaded', async () => { await test.expect(component.waitForLifecycleDone()).resolves.toBeUndefined(); await test.expect(component.waitForSlotState('renderNext', false)).resolves.toBeUndefined(); await test.expect(component.waitForDataIndexChild(chunkSize * 2 - 1)).resolves.toBeUndefined(); @@ -125,5 +125,4 @@ test.describe('', () => { }); }); }); - }); From 4de77833a359a077ec0594be2001389c4a564772 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Tue, 27 Jun 2023 19:26:13 +0300 Subject: [PATCH 043/159] :art: --- .../base/b-scrolly/test/unit/functional/observer/index.ts | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 src/components/base/b-scrolly/test/unit/functional/observer/index.ts diff --git a/src/components/base/b-scrolly/test/unit/functional/observer/index.ts b/src/components/base/b-scrolly/test/unit/functional/observer/index.ts deleted file mode 100644 index e94b2ed4cb..0000000000 --- a/src/components/base/b-scrolly/test/unit/functional/observer/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ From ea2bdfab66eb4849bf62bec2c5fd2262c92a4e48 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 28 Jun 2023 15:18:36 +0300 Subject: [PATCH 044/159] onRequestError support --- src/components/base/b-scrolly/README.md | 18 +++++++------- src/components/base/b-scrolly/b-scrolly.ts | 16 +++---------- src/components/base/b-scrolly/handlers.ts | 18 +++++++++++++- .../base/b-scrolly/modules/helpers/index.ts | 8 +++++++ .../b-scrolly/test/unit/scenario/retry.ts | 24 +++++++++++++++---- 5 files changed, 56 insertions(+), 28 deletions(-) diff --git a/src/components/base/b-scrolly/README.md b/src/components/base/b-scrolly/README.md index 87d12ad9ed..daf710c43f 100644 --- a/src/components/base/b-scrolly/README.md +++ b/src/components/base/b-scrolly/README.md @@ -1,21 +1,21 @@ -- Загруженных данных может не хватит на отрисовку поэтому прятать лоадер можно только когда набралось нужное кол-во данных -- PartialRender ? Если указано к отрисовке 10 компонентов а загружено за раз данных 5 - рисовать ли эти 5 и потом еще 5 -- Подумать над форматом данных к отрисовке, раньше клиент получал 10 элементов данных и должен был вернуть 10 элементов к отрисовке -- Preload нескольких страниц +- ~~Загруженных данных может не хватит на отрисовку поэтому прятать лоадер можно только когда набралось нужное кол-во данных~~ -> неактуально +- PartialRender ? Если указано к отрисовке 10 компонентов а загружено за раз данных 5 - рисовать ли эти 5 и потом еще 5 -> для версии 1.1.0 +- ~~Подумать над форматом данных к отрисовке, раньше клиент получал 10 элементов данных и должен был вернуть 10 элементов к отрисовке~~ -> Реализовано +- Preload нескольких страниц -> есть ли нужда? - Обработка ошибок тесты - Проверка стейта во время ошибок - typedLocalEmitter можно ли избавиться и как-то нормально типизировать события компонента -- кейс: загрузили чанк отрисовали -> загружаем следующий (он грузится 20 сек) -> пока грузится полный скролл внизу -> запрещаем загрузку и переходим в isDone состояние -> данные загрузились -> что произойдет??? -- componentInternalState.setLoadingPage(val) -> componentInternalState.set('loadingPage', val); -- стоит ли для state использовать builder like подход -- негативные тест кейсы +- ~~кейс: загрузили чанк отрисовали -> загружаем следующий (он грузится 20 сек) -> пока грузится полный скролл внизу -> запрещаем загрузку и переходим в isDone состояние -> данные загрузились -> что произойдет???~~ -> неактуально +- ~~componentInternalState.setLoadingPage(val) -> componentInternalState.set('loadingPage', val);~~ -> неактуально +- ~~стоит ли для state использовать builder like подход~~ -> неактуально +- ~~негативные тест кейсы~~ -> реализовано - улучшить имена интерфейсов (MountedItem, MountedSeparator, ревью имен в ComponentState) - протыкать все метода на использование (удалить неиспользуемые) - улучшить имена тест кейсов - ревью и рефакторинг src\components\base\b-scrolly\test\api\helpers\index.ts - dbChange - описать что состояние типа loadPage, renderPage обновляются постфактум (после того как загрузка данных случилась, после того как рендеринг случился) -- лишний вызов shouldStopRequestingData в onDataLoadSuccess +- ~~лишний вызов shouldStopRequestingData в onDataLoadSuccess~~ -> исправлено - поддержка onRequestError? - обработка async replace error в initLoad - не нравится проверка .then((res) => if (res == null) {this.onDataLoadError()}) diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts index bb26983e9b..a4513c5e9f 100644 --- a/src/components/base/b-scrolly/b-scrolly.ts +++ b/src/components/base/b-scrolly/b-scrolly.ts @@ -12,7 +12,6 @@ */ import VDOM, { create, render } from 'components/friends/vdom'; -import type iItems from 'components/traits/i-items/i-items'; import { bScrollyDomInsertAsyncGroup, renderGuardRejectionReason } from 'components/base/b-scrolly/const'; @@ -35,7 +34,7 @@ VDOM.addToPrototype(render); * by dynamically rendering chunks of data as the user scrolls. */ @component() -export default class bScrolly extends bScrollyHandlers implements iItems { +export default class bScrolly extends bScrollyHandlers { // @ts-ignore (getter instead readonly) override get requestParams(): iData['requestParams'] { return { @@ -83,14 +82,12 @@ export default class bScrolly extends bScrollyHandlers implements iItems { (isInitialLoading && this.db == null) || (!isInitialLoading && res == null) ) { - return this.onDataLoadError(isInitialLoading); + return; } this.onDataLoadSuccess(isInitialLoading, isInitialLoading ? this.db : this.convertDataToDB(res)); }) - .catch(() => { - this.onDataLoadError(isInitialLoading); - }); + .catch(stderr); } return >initLoadResult; @@ -103,13 +100,6 @@ export default class bScrolly extends bScrollyHandlers implements iItems { this.onReset(); } - /** - * Renders the next data chunk to the page (ignores the `client` check for render possibility). - */ - renderNext(): void { - // ... - } - /** * Returns the component state. * {@link ComponentState} diff --git a/src/components/base/b-scrolly/handlers.ts b/src/components/base/b-scrolly/handlers.ts index b1ce8d067d..d8e4333176 100644 --- a/src/components/base/b-scrolly/handlers.ts +++ b/src/components/base/b-scrolly/handlers.ts @@ -9,8 +9,9 @@ import bScrollyProps from 'components/base/b-scrolly/props'; import type bScrolly from 'components/base/b-scrolly/b-scrolly'; import { bScrollyAsyncGroup, componentEvents } from 'components/base/b-scrolly/const'; -import { component } from 'components/super/i-data/i-data'; +import iData, { component } from 'components/super/i-data/i-data'; import type { MountedChild } from 'components/base/b-scrolly/interface'; +import { isAsyncReplaceError } from 'components/base/b-scrolly/modules/helpers'; /** * A class that provides an API to handle events emitted by the {@link bScrolly} component. @@ -179,6 +180,21 @@ export abstract class bScrollyHandlers extends bScrollyProps { this.componentEmitter.emit(componentEvents.dataLoadError, isInitialLoading); } + override onRequestError(this: bScrolly, ...args: Parameters): ReturnType { + const + err = args[0]; + + if (isAsyncReplaceError(err)) { + return; + } + + const + state = this.getComponentState(); + + this.onDataLoadError(state.isInitialLoading); + return super.onRequestError(err, this.initLoad.bind(this)); + } + /** * Handler: data empty event. * Triggered when the loaded data is empty. diff --git a/src/components/base/b-scrolly/modules/helpers/index.ts b/src/components/base/b-scrolly/modules/helpers/index.ts index 5822d79271..ab9575bb7c 100644 --- a/src/components/base/b-scrolly/modules/helpers/index.ts +++ b/src/components/base/b-scrolly/modules/helpers/index.ts @@ -16,3 +16,11 @@ import type { MountedItem } from 'components/base/b-scrolly/interface'; export function isItem(val: any): val is MountedItem { return Object.isPlainObject(val) && val.type === componentItemType.item; } + +/** + * Returns `true` if the specified value is an `async replace` error. + * @param val + */ +export function isAsyncReplaceError(val: unknown): boolean { + return Object.isPlainObject(val) && val.join === 'replace'; +} diff --git a/src/components/base/b-scrolly/test/unit/scenario/retry.ts b/src/components/base/b-scrolly/test/unit/scenario/retry.ts index 4704c0216b..b5a0b46a55 100644 --- a/src/components/base/b-scrolly/test/unit/scenario/retry.ts +++ b/src/components/base/b-scrolly/test/unit/scenario/retry.ts @@ -53,12 +53,30 @@ test.describe('', () => { await component.withDefaultPaginationProviderProps({chunkSize}); await component.build(); - await component.waitForSlotState('retry', true); await component.node.locator('#retry').click(); await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); }); + test('Should reload data after invoking retry function from `onRequestError` handler', async () => { + const chunkSize = 12; + + provider + .responseOnce(500, {}) + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: []}); + + await component.setProps({ + chunkSize, + '@onRequestError': (_, retryFn) => setTimeout(retryFn, 0) + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + + await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); + }); + test('Should goes to retry state after failing to load data twice', async () => { const chunkSize = 12; @@ -72,7 +90,6 @@ test.describe('', () => { await component.withDefaultPaginationProviderProps({chunkSize}); await component.build(); - await component.waitForSlotState('retry', true); const event = component.waitForEvent('dataLoadError'); await component.node.locator('#retry').click(); await event; @@ -103,7 +120,6 @@ test.describe('', () => { await component.waitForContainerChildCountEqualsTo(chunkSize); await component.scrollToBottom(); - await component.waitForSlotState('retry', true); await component.node.locator('#retry').click(); await component.waitForDataIndexChild(chunkSize * 2 - 1); @@ -131,7 +147,6 @@ test.describe('', () => { }); await component.build(); - await component.waitForSlotState('retry', true); await component.node.locator('#retry').click(); await component.waitForContainerChildCountEqualsTo(chunkSize); @@ -155,7 +170,6 @@ test.describe('', () => { await component.withDefaultPaginationProviderProps({chunkSize}); await component.build(); - await component.waitForSlotState('retry', true); await component.node.locator('#retry').click(); test.expect(provider.mock.mock.calls.length).toBe(3); From e23a8aed4c0a7a08f040205e6f078592aec105c8 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 28 Jun 2023 15:33:45 +0300 Subject: [PATCH 045/159] Immediate transition to the loading state --- src/components/base/b-scrolly/handlers.ts | 2 +- .../base/b-scrolly/modules/slots/index.ts | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/components/base/b-scrolly/handlers.ts b/src/components/base/b-scrolly/handlers.ts index d8e4333176..3eb666f445 100644 --- a/src/components/base/b-scrolly/handlers.ts +++ b/src/components/base/b-scrolly/handlers.ts @@ -123,7 +123,7 @@ export abstract class bScrollyHandlers extends bScrollyProps { * @param isInitialLoading - Indicates whether it is an initial component loading. */ onDataLoadStart(isInitialLoading: boolean): void { - this.slotsStateController.loadingProgressState(); + this.slotsStateController.loadingProgressState(isInitialLoading); this.componentEmitter.emit(componentEvents.dataLoadStart, isInitialLoading); } diff --git a/src/components/base/b-scrolly/modules/slots/index.ts b/src/components/base/b-scrolly/modules/slots/index.ts index 700d6e337a..56c017b965 100644 --- a/src/components/base/b-scrolly/modules/slots/index.ts +++ b/src/components/base/b-scrolly/modules/slots/index.ts @@ -71,8 +71,9 @@ export class SlotsStateController extends Friend { /** * Displays the slots that should be shown during data loading progress. + * @param [immediate] - if set to true, {@link requestAnimationFrame} will not be used to switch the state. */ - loadingProgressState(): void { + loadingProgressState(immediate: boolean = false): void { this.setSlotsVisibility({ container: true, loader: true, @@ -81,7 +82,7 @@ export class SlotsStateController extends Friend { empty: false, renderNext: false, retry: false - }); + }, immediate); } /** @@ -126,17 +127,24 @@ export class SlotsStateController extends Friend { * Sets the visibility state of the slots. * * @param stateObj - An object specifying the visibility state of each slot. + * @param [immediate] - if set to true, {@link requestAnimationFrame} will not be used to switch the state. */ - protected setSlotsVisibility(stateObj: Required): void { + protected setSlotsVisibility(stateObj: Required, immediate: boolean = false): void { this.lastState = stateObj; this.async.cancelAnimationFrame(this.asyncUpdateLabel); - this.async.requestAnimationFrame(() => { + const update = () => { for (const [name, state] of Object.entries(stateObj)) { this.setDisplayState(name, state); } - }, this.asyncUpdateLabel); + }; + + if (immediate) { + return update(); + } + + this.async.requestAnimationFrame(update, this.asyncUpdateLabel); } /** From 881460c18ff125d58ad80321fe3f7a9ec7ed3f39 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 28 Jun 2023 18:41:07 +0300 Subject: [PATCH 046/159] Test cases for set --- .../base/b-scrolly/test/api/helpers/index.ts | 6 +- .../b-scrolly/test/unit/scenario/props.ts | 130 ++++++++++++++++++ 2 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 src/components/base/b-scrolly/test/unit/scenario/props.ts diff --git a/src/components/base/b-scrolly/test/api/helpers/index.ts b/src/components/base/b-scrolly/test/api/helpers/index.ts index 4c8571aa09..2678b088f6 100644 --- a/src/components/base/b-scrolly/test/api/helpers/index.ts +++ b/src/components/base/b-scrolly/test/api/helpers/index.ts @@ -318,13 +318,15 @@ export function createIndexedObj(i: number): IndexedObj { * * @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 + filterObserverEvents: boolean = true, + allowedEvents: string[] = [] ): unknown[][] { return emitCalls.filter(([event]) => Object.isString(event) && - Boolean(componentEvents[event]) && + (Boolean(componentEvents[event]) || allowedEvents.includes(event)) && (filterObserverEvents ? !(event in componentObserverLocalEvents) : true)); } diff --git a/src/components/base/b-scrolly/test/unit/scenario/props.ts b/src/components/base/b-scrolly/test/unit/scenario/props.ts new file mode 100644 index 0000000000..3a4dabf02f --- /dev/null +++ b/src/components/base/b-scrolly/test/unit/scenario/props.ts @@ -0,0 +1,130 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * @file Этот файл содержит тест кейсы для проверки функциональности изменения пропов компонентов. + */ + +import test from 'tests/config/unit/test'; + +import { createTestHelpers, filterEmitterCalls } from 'components/base/b-scrolly/test/api/helpers'; +import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; + +test.describe('', () => { + let + component: ScrollyTestHelpers['component'], + provider: ScrollyTestHelpers['provider'], + state: ScrollyTestHelpers['state'], + initLoadSpy: ScrollyTestHelpers['initLoadSpy']; + + test.beforeEach(async ({demoPage, page}) => { + await demoPage.goto(); + + ({component, provider, state, initLoadSpy} = await createTestHelpers(page)); + await provider.start(); + }); + + test.skip('`chunkSize` prop changes after the first chunk has been rendered', () => { + test('Should render the second chunk with the new chunk size', async () => { + const + chunkSize = 12; + + provider.response(200, () => ({data: state.data.addData(chunkSize)})); + + await component.setProps({ + chunkSize, + shouldPerformDataRender: ({isInitialRender, itemsTIllEnd}) => isInitialRender || itemsTIllEnd === 0 + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForContainerChildCountEqualsTo(chunkSize); + + await component.setProps({ + chunkSize: chunkSize * 2 + }); + + await component.scrollToBottom(); + + test.expect(provider.mock.mock.calls.length).toBe(3); + await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize * 3)).resolves.toBeUndefined(); + }); + }); + + test.skip('`request` prop was changed', () => { + test('Should reload the component data', async () => { + const + chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: []}); + + await component.setProps({ + chunkSize, + shouldPerformDataRender: ({isInitialRender, itemsTIllEnd}) => isInitialRender || itemsTIllEnd === 0, + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') + }); + + await component.withDefaultPaginationProviderProps({chunkSize}); + await component.build(); + await component.waitForContainerChildCountEqualsTo(chunkSize); + + await component.setProps({ + get: { + chunkSize: 20 + } + }); + + await component.waitForDataIndexChild(chunkSize * 2 - 1); + + const + spy = await component.getSpy((ctx) => ctx.emit), + calls = filterEmitterCalls(await spy.calls, true, ['initLoadStart', 'initLoad']).map(([event]) => event); + + test.expect(calls).toEqual([ + 'initLoadStart', + 'dataLoadStart', + 'convertDataToDB', + 'initLoad', + 'dataLoadSuccess', + 'renderStart', + 'renderEngineStart', + 'renderEngineDone', + 'domInsertStart', + 'domInsertDone', + 'renderDone', + 'initLoadStart', + 'dataLoadStart', + 'convertDataToDB', + 'initLoad', + 'dataLoadSuccess', + 'renderStart', + 'renderEngineStart', + 'renderEngineDone', + 'domInsertStart', + 'domInsertDone', + 'renderDone' + ]); + + await test.expect(initLoadSpy.calls).resolves.toBe([[], []]); + await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeTruthy(); + }); + }); + + test.skip('`requestQuery` prop was changed', () => { + test('Should not reload the entire component', async () => { + // ... + }); + + test('Should request the second chunk with the new parameters', async () => { + // ... + }); + }); +}); From 103c971ec46ed1d7d53b81dd6e549e091959f4d3 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 28 Jun 2023 18:43:27 +0300 Subject: [PATCH 047/159] Remove b-virtual-scroll legacy --- .../base/b-virtual-scroll/CHANGELOG.md | 172 ------ .../base/b-virtual-scroll/README.md | 218 -------- .../base/b-virtual-scroll/b-virtual-scroll.ss | 57 -- .../b-virtual-scroll/b-virtual-scroll.styl | 21 - .../base/b-virtual-scroll/b-virtual-scroll.ts | 488 ----------------- .../b-virtual-scroll_theme_demo.styl | 30 -- src/components/base/b-virtual-scroll/demo.js | 107 ---- src/components/base/b-virtual-scroll/index.js | 10 - .../base/b-virtual-scroll/interface.ts | 222 -------- .../b-virtual-scroll/modules/chunk-render.ts | 403 -------------- .../b-virtual-scroll/modules/chunk-request.ts | 418 --------------- .../modules/component-render.ts | 218 -------- .../base/b-virtual-scroll/modules/helpers.ts | 106 ---- .../base/b-virtual-scroll/test/index.js | 32 -- .../test/runners/events/chunk-loaded.js | 230 -------- .../test/runners/events/chunk-loading.js | 106 ---- .../test/runners/events/data-change.js | 216 -------- .../test/runners/events/db-change.js | 184 ------- .../test/runners/functional/items.js | 102 ---- .../test/runners/functional/render-next.js | 102 ---- .../test/runners/functional/state.js | 263 --------- .../test/runners/render/render.js | 205 ------- .../test/runners/slots/empty.js | 123 ----- .../test/runners/slots/render-next.js | 498 ------------------ .../base/b-virtual-scroll/test/unit/render.ts | 139 ----- .../pages/p-v4-components-demo/index.js | 1 - 26 files changed, 4671 deletions(-) delete mode 100644 src/components/base/b-virtual-scroll/CHANGELOG.md delete mode 100644 src/components/base/b-virtual-scroll/README.md delete mode 100644 src/components/base/b-virtual-scroll/b-virtual-scroll.ss delete mode 100644 src/components/base/b-virtual-scroll/b-virtual-scroll.styl delete mode 100644 src/components/base/b-virtual-scroll/b-virtual-scroll.ts delete mode 100644 src/components/base/b-virtual-scroll/b-virtual-scroll_theme_demo.styl delete mode 100644 src/components/base/b-virtual-scroll/demo.js delete mode 100644 src/components/base/b-virtual-scroll/index.js delete mode 100644 src/components/base/b-virtual-scroll/interface.ts delete mode 100644 src/components/base/b-virtual-scroll/modules/chunk-render.ts delete mode 100644 src/components/base/b-virtual-scroll/modules/chunk-request.ts delete mode 100644 src/components/base/b-virtual-scroll/modules/component-render.ts delete mode 100644 src/components/base/b-virtual-scroll/modules/helpers.ts delete mode 100644 src/components/base/b-virtual-scroll/test/index.js delete mode 100644 src/components/base/b-virtual-scroll/test/runners/events/chunk-loaded.js delete mode 100644 src/components/base/b-virtual-scroll/test/runners/events/chunk-loading.js delete mode 100644 src/components/base/b-virtual-scroll/test/runners/events/data-change.js delete mode 100644 src/components/base/b-virtual-scroll/test/runners/events/db-change.js delete mode 100644 src/components/base/b-virtual-scroll/test/runners/functional/items.js delete mode 100644 src/components/base/b-virtual-scroll/test/runners/functional/render-next.js delete mode 100644 src/components/base/b-virtual-scroll/test/runners/functional/state.js delete mode 100644 src/components/base/b-virtual-scroll/test/runners/render/render.js delete mode 100644 src/components/base/b-virtual-scroll/test/runners/slots/empty.js delete mode 100644 src/components/base/b-virtual-scroll/test/runners/slots/render-next.js delete mode 100644 src/components/base/b-virtual-scroll/test/unit/render.ts diff --git a/src/components/base/b-virtual-scroll/CHANGELOG.md b/src/components/base/b-virtual-scroll/CHANGELOG.md deleted file mode 100644 index d4917b773c..0000000000 --- a/src/components/base/b-virtual-scroll/CHANGELOG.md +++ /dev/null @@ -1,172 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## 4.??.?? (2023-??-??) - -#### :house: Internal - -* API migration - -## v3.30.1 (2022-10-25) - -#### :bug: Bug Fix - -* Fixed an issue with wrong arguments was provided into `getItemKey` - -## v3.18.2 (2022-03-22) - -#### :bug: Bug Fix - -* Fixed an issue with `initLoad` race condition - -## v3.15.4 (2022-01-24) - -#### :boom: Breaking Change - -* The event`chunkRenderStart` is renamed to `chunkRender:renderStart` and now it emits before a component driver renders components - -#### :rocket: New Feature - -* Added new events `chunkRender:*` - -## v3.9.0 (2021-11-08) - -#### :rocket: New Feature - -* [Added a new event `chunkRenderStart`](https://github.com/V4Fire/Client/issues/651) -* [Added `pageNumber` in `chunkLoaded` event](https://github.com/V4Fire/Client/issues/651) - -## v3.0.0-rc.182 (2021-04-28) - -#### :bug: Bug Fix - -* Fixed an issue with `optionKey` being ignored - -## v3.0.0-rc.181 (2021-04-20) - -#### :bug: Bug Fix - -* [Fixed an issue with `itemProps` not being provided to child components](https://github.com/V4Fire/Client/issues/512) - -## v3.0.0-rc.170 (2021-03-26) - -#### :rocket: New Feature - -* Added a new event `chunkRender` - -## v3.0.0-rc.164 (2021-03-22) - -#### :bug: Bug Fix - -* Now `bVirtualScroll` will throw an error if the rendering of components returns an empty array - -## v3.0.0-rc.153 (2021-03-04) - -#### :house: Internal - -* [`bVirtualScroll` is now implements `iItems` trait](https://github.com/V4Fire/Client/issues/471) - -## v3.0.0-rc.151 (2021-03-04) - -#### :house: Internal - -* Downgraded the delay before initializing to `15ms` -* Some optimizations - -## v3.0.0-rc.126 (2021-01-26) - -#### :bug: Bug Fix - -* Added handling of the empty request - -## v3.0.0-rc.122 (2021-01-13) - -#### :house: Internal - -* Removed iItems implementation. [Issue to move back](https://github.com/V4Fire/Client/issues/471) - -## v3.0.0-rc.102 (2020-11-26) - -#### :bug: Bug Fix - -* Fixed an issue with layout shifts after `reInit` - -## v3.0.0-rc.81 (2020-10-08) - -#### :bug: Bug Fix - -* Fixed an issue with `renderNext`: hasn't been data rendering after a loading error - -## v3.0.0-rc.74 (2020-10-06) - -#### :bug: Bug Fix - -* Fixed an issue with removing the progress modifier - -## v3.0.0-rc.68 (2020-09-23) - -#### :bug: Bug Fix - -* [Fixed an issue with the second data batch load affects initial rendering after reInit](https://github.com/V4Fire/Client/issues/346) - -## v3.0.0-rc.60 (2020-09-01) - -#### :bug: Bug Fix - -* [Fixed a possible memory leak](https://github.com/V4Fire/Client/pull/321) - -## v3.0.0-rc.59 (2020-08-10) - -#### :rocket: New Feature - -* [Added ability to render data manually](https://github.com/V4Fire/Client/issues/202) - -#### :nail_care: Polish - -* Improved documentation - -## v3.0.0-rc.39 (2020-07-22) - -#### :rocket: New Feature - -* [Added life cycle events](https://github.com/V4Fire/Client/issues/205) - -#### :bug: Bug Fix - -* [Fixed an issue when data from `lastLoadedData` and `lastLoadedChunk.normalized` aren't synchronized](https://github.com/V4Fire/Client/issues/281) -* [Fixed `lastLoadedChunk.raw` returns undefined](https://github.com/V4Fire/Client/issues/267) - -#### :house: Internal - -* [Refactoring of tests](https://github.com/V4Fire/Client/pull/293) -* [Fixed ESLint warnings](https://github.com/V4Fire/Client/pull/293) - -## v3.0.0-rc.31 (2020-06-17) - -#### :bug: Bug Fix - -* Fixed a problem with the disappearance of loaders before the content was rendered - -## v3.0.0-rc.25 (2020-06-03) - -#### :bug: Bug Fix - -* [Fixed an issue where skeletons disappeared](https://github.com/V4Fire/Client/issues/230) -* [Fixed an issue with a race condition `chunk-request/init`](https://github.com/V4Fire/Client/issues/203) -* [Fixed an issue where an `empty` slot appeared when there was data](https://github.com/V4Fire/Client/issues/259) - -## v3.0.0-rc.19 (2020-05-26) - -#### :bug: Bug Fix - -* [Fixed rendering of truncated data](https://github.com/V4Fire/Client/issues/231) -* [Fixed rendering of empty slot](https://github.com/V4Fire/Client/issues/241) -* [Fixed clear of `in-view`](https://github.com/V4Fire/Client/pull/201) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md deleted file mode 100644 index d201b391f3..0000000000 --- a/src/components/base/b-virtual-scroll/README.md +++ /dev/null @@ -1,218 +0,0 @@ -# components/base/b-virtual-scroll - -This module provides a component to render component sequences with the support of lazy loading and dynamically updating. -This component can be very efficient if you need to render a good amount of elements. - -## Synopsis - -* The component extends [[iData]]. - -* The component implements the [[iItems]] trait. - -* By default, the root tag of the component is `
`. - -## Events - -| EventName | Description | Payload description | Payload | -|------------------|--------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|-----------------------------| -| dbChange | The event is fired after receiving data from a data provider. The event won't be fired if the data is empty. | Cumulative data of all tied requests | `RemoteData` | -| dataChange | The event is fired after changing a data batch | Data batch value | `unknown[]` | -| chunkLoading | The event is fired before start to load data from a data provider | Current page | `number` | -| chunkLoaded | The event is fired after every successful response from a data provider | A structure with raw and normalized data that takes from a data provider | `LastLoadedChunk`, `number` | -| chunkRenderStart | The event is fired before components are rendered | chunk number | `number` | -| chunkRender | The event is fired after rendered nodes inserted into DOM | Render items, chunk number | `RenderItem[]`, `number` | - -Also, you can see the parent component and the component traits. - -## Usage - -### Basic - -``` -< b-virtual-scroll & - :dataProvider = 'demo.Pagination' | - :request = {get: {chunkSize: 12}} | - :item = 'b-card' | - :itemKey = (el, i) => resolveKey(el) | - :itemProps = getPropsForOption | - :dbConverter = convertDataToVirtual -. -``` - -The component expects that loaded data will have the structure that matches with the `RemoteData` interface. - -```typescript -export interface RemoteData extends Dictionary { - /** - * Data to render - */ - data?: object[]; - - /** - * Total number of elements - */ - total?: number; -} -``` - -You can use `dbConverter` to convert data to match this interface. - -To specify what kind of component to render, you have to use the `option` property. -Mind, the property can be defined as a string or function. - -### Manual data display control - -By default, data is requested and rendered automatically (when scrolling the page), you can override this behavior to load and render data manually. - -To set loading and rendering data in manual mode, set the `loadStrategy` prop to `manual`. - -``` -< b-virtual-scroll & - :dataProvider = 'demo.Pagination' | - :dbConverter = convertDataToVirtual | - :request = {get: {chunkSize: 12}} | - :loadStrategy = 'manual' | - - :item = 'b-card' | - :itemKey = (el, i) => resolveKey(el) | - :itemProps = getPropsForItem | -. - < template #renderNext = o - < .&__render-next @click = o.ctx.renderNext - Render or load next -``` - -Initial loading and request will be made automatically, but after that `renderNext` method will need to be used to request and render data. - -## Slots - -The component supports a bunch of slots to provide. - -1. `tombstone` This slot is displayed only during data loading, it will be duplicated `chunkSize` number of times. -This slot is great if you want to display skeletons while the component is loading data. - -``` -< b-virtual-scroll - < template #tombstone - < .&__skeleton -``` - -2. `loader` This slot is displayed only during data loading. -This slot is great if you want to display something while the component is loading data. - -``` -< b-virtual-scroll - < template #loader - < b-loader -``` - -3. `empty` This slot is displayed if the component has no data at all to render after completing data requests. - -``` -< b-virtual-scroll - < template #empty - < .&__empty - There is no data to render -``` - -4. `retry` This slot is displayed if the component data request error occurs. - -``` -< b-virtual-scroll - < template #retry = o - < .&__retry @click = o.ctx.reloadLast - Retry last request -``` - -5. `renderNext` This slot is displayed if the component has data to render or requests are not stopped. -This slot can be useful if you want to provide the ability to manually request additional data. - -``` -< b-virtual-scroll - < template #retry = o - < .&__retry @click = o.ctx.renderNext - Render next -``` - -6. `done` This slot is displayed if the component rendered and requested all data. - -``` -< b-virtual-scroll - < template - < .&__done - All data are rendered and requested -``` - -## API - -Also, you can see the parent component and the component traits. - -### Props - -#### [cacheSize = `400`] - -The maximum number of elements to cache. - -#### [renderGap = `10`] - -Number of elements till the page bottom that should initialize a new render iteration. - -#### [chunkSize = `10`] - -Number of elements per one render chunk. - -#### [tombstonesSize] - -Number of tombstones to render. - -#### [clearNodes = `false`] - -If true, then elements are dropped from a DOM tree after scrolling. -This method is recommended to use if you need to display a huge number of elements and prevent an OOM error. - -#### [cacheNodes = `true`] - -If true, then created nodes are cached. - -#### [requestQuery] - -A function that returns parameters to make a request. - -#### [getData] - -A function to request a new data chunk to render. - -#### [shouldMakeRequest] - -When this function returns true the component will be able to request additional data. - -#### [shouldStopRequest] - -When this function returns true the component will stop to request new data. - -### Methods - -#### reInit - -Re-initializes the component. - -#### reloadLast - -Reloads the last request (if there is no `db` or `options` the method calls reload). - -#### renderNext - -Tries to render the next data chunk. -The method emits a new request for data if necessary. - -#### getCurrentDataState - -Returns an object with the current data state of the component. - -#### getItemAttrs - -Returns additional props to pass to an item component. - -#### getItemComponentName - -Returns a component name to render an item. diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ss b/src/components/base/b-virtual-scroll/b-virtual-scroll.ss deleted file mode 100644 index 3c2f2d4590..0000000000 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ss +++ /dev/null @@ -1,57 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -- namespace [%fileName%] - -- include 'components/super/i-data'|b as placeholder - -- template index() extends ['i-data'].index - - block body - < .&__wrapper - < .&__container ref = container | -test-ref = container - - < .&__tombstones & - ref = tombstones | - v-if = $slots['tombstone'] - . - < .&__tombstone v-for = i in tombstonesSize || chunkSize - += self.slot('tombstone') - - < .&__loader & - ref = loader | - v-if = $slots['loader'] - . - += self.slot('loader') - - < .&__retry & - ref = retry | - v-if = $slots['retry'] | - :style = {display: 'none'} - . - += self.slot('retry') - - < .&__empty & - ref = empty | - v-if = $slots['empty'] | - :style = {display: 'none'} - . - += self.slot('empty') - - < .&__done & - ref = done | - v-if = $slots['done'] | - :style = {display: 'none'} - . - += self.slot('done') - - < .&__render-next & - ref = renderNext | - v-if = $slots['renderNext'] | - :style = {display: 'none'} - . - += self.slot('renderNext') diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.styl b/src/components/base/b-virtual-scroll/b-virtual-scroll.styl deleted file mode 100644 index 3cc1d3a5ea..0000000000 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.styl +++ /dev/null @@ -1,21 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -@import "components/super/i-data/i-data.styl" - -$p = { - -} - -b-virtual-scroll extends i-data - width 100% - - &__container - position relative - box-sizing border-box - width 100% diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts deleted file mode 100644 index 813a83f208..0000000000 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts +++ /dev/null @@ -1,488 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -/** - * [[include:components/base/b-virtual-scroll/README.md]] - * @packageDocumentation - */ - -import symbolGenerator from 'core/symbol'; - -import DOM, { watchForIntersection, appendChild } from 'components/friends/dom'; -import VDOM, { render, create } from 'components/friends/vdom'; -import Block, { getFullElementName } from 'components/friends/block'; - -import iItems, { IterationKey } from 'components/traits/i-items/i-items'; - -import iData, { - - component, - computed, - prop, - system, - field, - watch, - wait, - hook, - - RequestParams, - RequestError, - - InitLoadOptions, - RetryRequestFn, - CheckDBEquality, - - UnsafeGetter - -} from 'components/super/i-data/i-data'; - -import ComponentRender from 'components/base/b-virtual-scroll/modules/component-render'; -import ChunkRender from 'components/base/b-virtual-scroll/modules/chunk-render'; -import ChunkRequest from 'components/base/b-virtual-scroll/modules/chunk-request'; - -import { getRequestParams, isAsyncReplaceError } from 'components/base/b-virtual-scroll/modules/helpers'; - -import type { - - GetData, - RemoteData, - - RequestFn, - RequestQueryFn, - - LocalState, - LoadStrategy, - - DataState, - MergeDataStateParams, - - UnsafeBVirtualScroll - -} from 'components/base/b-virtual-scroll/interface'; - -export * from 'components/super/i-data/i-data'; -export * from 'components/base/b-virtual-scroll/modules/helpers'; -export * from 'components/base/b-virtual-scroll/interface'; - -export { RequestFn, RemoteData, RequestQueryFn, GetData }; - -DOM.addToPrototype(watchForIntersection, appendChild); -VDOM.addToPrototype(render, create); -Block.addToPrototype(getFullElementName); - -const - $$ = symbolGenerator(); - -@component() -export default class bVirtualScroll extends iData implements iItems { - /** {@link iItems.Item} */ - readonly Item!: object; - - /** {@link iItems.Items} */ - readonly Items!: Array; - - override readonly DB!: RemoteData; - - override readonly checkDBEquality: CheckDBEquality = false; - - /** {@link LoadStrategy} */ - @prop({type: String, watch: 'syncPropsWatcher'}) - readonly loadStrategy: LoadStrategy = 'scroll'; - - /** {@link iItems.item} */ - @prop({type: [String, Function], required: false}) - readonly item?: iItems['item']; - - /** {@link iItems.itemKey} */ - @prop({type: [String, Function], required: false}) - readonly itemKey?: iItems['itemKey']; - - /** {@link iItems.itemProps} */ - @prop({type: [Function, Object], default: () => ({})}) - readonly itemProps!: iItems['itemProps']; - - /** - * The maximum number of elements to cache - */ - @prop({type: Number, watch: 'syncPropsWatcher', validator: Number.isNatural}) - readonly cacheSize: number = 400; - - /** - * Number of elements till the page bottom that should initialize a new render iteration - */ - @prop({type: Number, validator: Number.isNatural}) - readonly renderGap: number = 10; - - /** - * Number of elements per one render chunk - */ - @prop({type: Number, validator: Number.isNatural}) - readonly chunkSize: number = 10; - - /** - * Number of tombstones to render - */ - @prop({type: Number, required: false, validator: Number.isNatural}) - readonly tombstonesSize?: number; - - /** - * If true, then elements are dropped from a DOM tree after scrolling. - * This method is recommended to use if you need to display a huge number of elements and prevent an OOM error. - */ - @prop(Boolean) - readonly clearNodes: boolean = false; - - /** - * If true, then created nodes are cached - */ - @prop({type: Boolean, watch: 'syncPropsWatcher'}) - readonly cacheNodes: boolean = true; - - /** - * Function that returns parameters to make a request - */ - @prop({type: Function, required: false}) - readonly requestQuery?: RequestQueryFn; - - @prop({type: [Object, Array], required: false}) - override readonly request?: RequestParams; - - /** - * Function to request a new data chunk to render - */ - @prop({type: Function, default: (ctx: bVirtualScroll, query) => ctx.dataProvider?.get(query), required: false}) - readonly getData!: GetData; - - /** - * When this function returns true the component will be able to request additional data - */ - @prop({type: Function, default: (v: DataState) => v.itemsTillBottom <= 10 && !v.isLastEmpty}) - readonly shouldMakeRequest!: RequestFn; - - /** - * When this function returns true the component will stop to request new data - */ - @prop({type: Function, default: (v) => v.isLastEmpty}) - readonly shouldStopRequest!: RequestFn; - - /** {@link iItems.items} */ - @computed({dependencies: ['itemsStore']}) - get items(): this['Items'] { - return this.itemsStore ?? []; - } - - /** {@link iItems.items} */ - set items(value: this['Items']) { - this.field.set('itemsStore', value); - } - - override get unsafe(): UnsafeGetter> { - return Object.cast(this); - } - - /** {@link iItems.items} */ - @field((o) => o.sync.link()) - protected itemsStore!: iItems['items']; - - /** - * Total amount of items that can be loaded - */ - @system() - protected total?: number; - - /** - * Local component state - */ - protected get localState(): LocalState { - return this.localStateStore; - } - - /** - * @param state - * @emits `localEmitter:localState.loading()` - * @emits `localEmitter:localState.ready()` - * @emits `localEmitter:localState.error()` - */ - protected set localState(state: LocalState) { - this.localStateStore = state; - this.localEmitter.emit(`localState.${state}`); - } - - /** - * Local component state store - */ - @system() - protected localStateStore: LocalState = 'init'; - - // @ts-ignore (getter instead readonly) - override get requestParams(): RequestParams { - return { - get: { - ...this.requestQuery?.(this.getDataStateSnapshot())?.get, - ...Object.isDictionary(this.request?.get) ? this.request?.get : undefined - } - }; - } - - /** - * API for scroll rendering - */ - @system((o) => new ChunkRender(o)) - protected chunkRender!: ChunkRender; - - /** - * API for scroll data requests - */ - @system((o) => new ChunkRequest(o)) - protected chunkRequest!: ChunkRequest; - - /** - * API for dynamic component rendering - */ - @system((o) => new ComponentRender(o)) - protected componentRender!: ComponentRender; - - protected override readonly $refs!: iData['$refs'] & { - container: HTMLElement; - loader?: HTMLElement; - tombstones?: HTMLElement; - empty?: HTMLElement; - retry?: HTMLElement; - done?: HTMLElement; - renderNext?: HTMLElement; - }; - - /** - * @param [data] - * @param [opts] - * - * @emits `chunkLoading(page: number)` - * */ - override initLoad(data?: unknown, opts?: InitLoadOptions): CanPromise { - this.async.clearAll({label: 'chunkRequest.waitForInitCalls'}); - - if (!this.lfc.isBeforeCreate()) { - this.reInit(); - } - - if (this.isActivated) { - this.emit('chunkLoading', 0); - } - - return super.initLoad(data, opts); - } - - /** - * Re-initializes the component - */ - reInit(): void { - this.componentRender.reInit(); - this.chunkRequest.reset(); - this.chunkRender.reInit(); - } - - /** - * Reloads the last request (if there is no `db` or `options` the method calls reload) - */ - reloadLast(): void { - if (!this.db || this.chunkRequest.data.length === 0) { - this.reload().catch(stderr); - - } else { - this.chunkRequest.reloadLast(); - } - } - - /** - * Tries to render the next data chunk. - * The method emits a new request for data if necessary. - */ - renderNext(): void { - const - {localState, chunkRequest, dataProvider, items} = this; - - if (localState !== 'ready' || dataProvider == null && items.length === 0) { - return; - } - - chunkRequest.try().catch(stderr); - } - - /** - * Returns an object with the current data state of the component - * - * @typeParam ITEM - data item to render - * @typeParam RAW - raw provider data - */ - getCurrentDataState< - ITEM extends object = object, - RAW extends object = object - >(): DataState { - let overrideParams: MergeDataStateParams = {}; - - if (this.componentStatus !== 'ready' || !Object.isTruly(this.dataProvider)) { - overrideParams = { - currentPage: 0, - ...overrideParams - }; - } - - return this.getDataStateSnapshot(overrideParams, this.chunkRequest, this.chunkRender); - } - - /** - * Returns additional props to pass to an item component - * - * @param el - * @param i - */ - getItemAttrs(el: this['Item'], i: number): CanUndef { - const - {itemProps} = this; - - return Object.isFunction(itemProps) ? - itemProps(el, i, { - key: this.getItemKey(el, i), - ctx: this - }) : - itemProps; - } - - /** - * Returns a component name to render an item - * - * @param el - * @param i - */ - getItemComponentName(el: this['Item'], i: number): string { - const {item} = this; - return Object.isFunction(item) ? item(el, i) : item; - } - - /** {@link iItems.getItemKey} */ - getItemKey(el: this['Item'], i: number): CanUndef { - return iItems.getItemKey(this, el, i); - } - - /** - * Takes a snapshot of the current data state and returns it - * - * @param [overrideParams] - * @param [chunkRequest] - * @param [chunkRender] - * - * @typeParam ITEM - data item to render - * @typeParam RAW - raw provider data - */ - protected getDataStateSnapshot< - ITEM extends object = object, - RAW extends unknown = unknown - >( - overrideParams?: MergeDataStateParams, - chunkRequest?: ChunkRequest, - chunkRender?: ChunkRender - ): DataState { - return getRequestParams(chunkRequest, chunkRender, overrideParams); - } - - /** @emits `chunkLoaded(lastLoadedChunk:` [[LastLoadedChunk]]`)` */ - protected override initRemoteData(): void { - if (!this.db) { - return; - } - - this.localState = 'init'; - - const - {data, total} = this.db; - - if (data && data.length > 0) { - const lastLoadedChunk = { - normalized: data, - raw: this.chunkRequest.lastLoadedChunk.raw - }; - - const params = this.getDataStateSnapshot({ - data, - total, - lastLoadedData: data, - lastLoadedChunk - }); - - this.chunkRequest.lastLoadedChunk = lastLoadedChunk; - this.chunkRequest.shouldStopRequest(params); - this.chunkRequest.data = data; - this.total = total; - - } else { - this.chunkRequest.isLastEmpty = true; - - const - params = this.getDataStateSnapshot({isLastEmpty: true}); - - this.chunkRequest.shouldStopRequest(params); - } - - this.emit('chunkLoaded', this.chunkRequest.lastLoadedChunk); - this.chunkRequest.init().catch(stderr); - } - - protected override convertDataToDB(data: object): O | this['DB'] { - this.chunkRequest.lastLoadedChunk.raw = data; - return super.convertDataToDB(data); - } - - /** - * Initializes rendering on the items passed to the component - */ - @hook('mounted') - @watch(['itemsStore']) - @wait('ready', {defer: true, label: $$.initOptions}) - protected initItems(): CanPromise { - if (this.dataProvider !== undefined) { - return; - } - - if (this.localState === 'ready') { - this.reInit(); - } - - this.chunkRequest.lastLoadedChunk.normalized = Object.isArray(this.items) ? [...this.items] : []; - this.chunkRequest.init().catch(stderr); - } - - /** - * Synchronization of the component props - */ - @wait('ready', {defer: true, label: $$.syncPropsWatcher}) - protected syncPropsWatcher(): CanPromise { - return this.reInit(); - } - - protected override syncDataProviderWatcher(initLoad?: boolean): void { - const - provider = this.dataProviderProp; - - if (provider === undefined) { - this.reInit(); - - } else { - super.syncDataProviderWatcher(initLoad); - } - } - - protected override onRequestError(err: Error | RequestError, retry: RetryRequestFn): void { - super.onRequestError(err, retry); - - if (isAsyncReplaceError(err)) { - return; - } - - this.localState = 'error'; - } -} diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll_theme_demo.styl b/src/components/base/b-virtual-scroll/b-virtual-scroll_theme_demo.styl deleted file mode 100644 index ce7c80bb2d..0000000000 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll_theme_demo.styl +++ /dev/null @@ -1,30 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -b-virtual-scroll_theme_demo extends b-virtual-scroll - /theme &__option-el - position relative - - display flex - justify-content center - align-items center - - size 200px - margin 20px - - background-color red - - &:after - content attr(data-index) - font-size 20px - color #FFF - - /theme &__skeleton - size 200px - margin 20px - background-color gray diff --git a/src/components/base/b-virtual-scroll/demo.js b/src/components/base/b-virtual-scroll/demo.js deleted file mode 100644 index 90dd3a7d1b..0000000000 --- a/src/components/base/b-virtual-scroll/demo.js +++ /dev/null @@ -1,107 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -// @ts-check - -const - s = JSON.stringify; - -const baseAttrs = { - ':theme': s('demo'), - ':option': s('section'), - ':optionProps': '({current}, i) => ({"data-index": current.i})' -}; - -const slots = { - tombstone: '
Skeleton
', - retry: '', - empty: '
Empty
', - done: '
Done
', - loader: '
Loader
', - renderNext: '
Load next
' -}; - -const suits = { - /* - * Slots - */ - slots: [ - /** - * Slot empty - */ - { - attrs: { - ...baseAttrs, - ':dataProvider': s('demo.Pagination'), - ':dbConverter': '({data}) => ({data: data.splice(0, 4)})', - id: 'emptyNoSlot' - } - }, - { - attrs: { - ...baseAttrs, - ':dataProvider': s('demo.Pagination'), - ':dbConverter': '({data}) => ({data: data.splice(0, 4)})', - ':request': '{get: {chunkSize: 8, total: 8}}', - id: 'emptyWithData' - }, - - content: { - empty: slots.empty - } - }, - { - attrs: { - ...baseAttrs, - ':dataProvider': s('demo.Pagination'), - ':dbConverter': '({data}) => ({data: []})', - id: 'emptyWithSlot' - }, - - content: { - empty: slots.empty - } - }, - - /** - * Slot loadNext - */ - { - attrs: { - ...baseAttrs, - ':dataProvider': s('demo.Pagination'), - ':loadStrategy': s('manual'), - id: 'renderNextNoSlot' - } - }, - - { - attrs: { - ...baseAttrs, - ':dataProvider': s('demo.Pagination'), - ':loadStrategy': s('manual'), - id: 'renderNextWithSlot' - }, - - content: { - renderNext: slots.renderNext - } - } - ], - - render: [ - { - attrs: { - ...baseAttrs, - id: 'target' - } - } - ] -}; - -module.exports = suits; diff --git a/src/components/base/b-virtual-scroll/index.js b/src/components/base/b-virtual-scroll/index.js deleted file mode 100644 index 302b437616..0000000000 --- a/src/components/base/b-virtual-scroll/index.js +++ /dev/null @@ -1,10 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -package('b-virtual-scroll') - .extends('i-data'); diff --git a/src/components/base/b-virtual-scroll/interface.ts b/src/components/base/b-virtual-scroll/interface.ts deleted file mode 100644 index 03498a508f..0000000000 --- a/src/components/base/b-virtual-scroll/interface.ts +++ /dev/null @@ -1,222 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { UnsafeIData } from 'components/super/i-data/i-data'; - -export interface RequestQueryFn { - (params: DataState): Dictionary; -} -export interface RequestFn { - (params: DataState): boolean; -} - -export interface GetData { - (ctx: bVirtualScroll, query: CanUndef): Promise; -} - -export interface VirtualItemEl { - /** - * Current render data - */ - current: T; - - /** - * Previous render data - */ - prev: CanUndef; - - /** - * Next render data - */ - next: CanUndef; -} - -/** - * @deprecated - * {@link VirtualItemEl} - */ -export type OptionEl = VirtualItemEl; - -/** - * @typeParam ITEM - data item to render - * @typeParam RAW - raw provider data - */ -export interface DataState { - /** - * Number of the last loaded page - */ - currentPage: number; - - /** - * Number of a page to upload - */ - nextPage: number; - - /** - * All loaded data - */ - data: object[]; - - /** - * Number of items to show till the page bottom is reached - */ - itemsTillBottom: number; - - /** - * Items to render - */ - items: Array>; - - /** - * Data that pending to be rendered - */ - pendingData: object[]; - - /** - * True if the last requested data response was empty - */ - isLastEmpty: boolean; - - /** - * Last loaded data chunk - */ - lastLoadedChunk: { - /** - * Normalized data (processed with `dbConverter`) - */ - normalized: ITEM[]; - - /** - * Raw provider data - */ - raw: CanUndef; - }; - - /** - * @deprecated - * {@link DataState.lastLoadedChunk} - */ - lastLoadedData: ITEM[]; - - /** - * `total` property from the loaded data - */ - total: CanUndef; -} - -export interface RemoteData extends Dictionary { - /** - * Data to render components - */ - data?: object[]; - - /** - * Total number of elements - */ - total?: number; -} - -export interface RenderItem { - /** - * Component data - */ - data: T; - - /** - * Component DOM element - */ - node: CanUndef; - - /** - * Component destructor - */ - destructor: CanUndef; - - /** - * Component position in a DOM tree - */ - index: number; -} - -/** - * Attributes of items to render - */ -export type ItemAttrs = Dictionary; - -/** - * Last loaded data chunk - * - * @typeParam DATA - data to render - * @typeParam RAW - raw provider data - */ -export interface LastLoadedChunk { - normalized: DATA; - raw: CanUndef; -} - -export interface DataToRender { - itemAttrs: Dictionary; - itemParams: VirtualItemEl; - index: number; -} - -/** - * Local state of a component: - * - * * `error` - indicates the component loading error appear - * * `init` - indicates the component now loading the first chunk of data - * * `ready` - indicates the component now is ready to render data - */ -export type LocalState = 'init' | 'ready' | 'error'; - -/** - * The loading strategy: - * - * * `scroll` - will prompt the client to load data every time a new element appears in the viewport - * * `manual` - there is only one way to load data: by using `renderNext` method (except the initial load) - */ -export type LoadStrategy = 'scroll' | 'manual'; - -/** - * Display state of the ref - */ -export type RefDisplayState = '' | 'none'; - -/** - * `bVirtualScroll` `$refs` - */ -export type bVirtualScrollRefs = bVirtualScroll['$refs']; - -// @ts-ignore (unsafe) -export interface UnsafeBVirtualScroll extends UnsafeIData { - // @ts-ignore (access) - total: CTX['total']; - - // @ts-ignore (access) - localState: CTX['localState']; - - // @ts-ignore (access) - chunkRender: CTX['chunkRender']; - - // @ts-ignore (access) - chunkRequest: CTX['chunkRequest']; - - // @ts-ignore (access) - componentRender: CTX['componentRender']; - - // @ts-ignore (access) - getDataStateSnapshot: CTX['getDataStateSnapshot']; - - // @ts-ignore (access) - onRequestError: CTX['onRequestError']; -} - -export type MergeDataStateParams = { - [key in keyof DataState]?: DataState[key]; -}; diff --git a/src/components/base/b-virtual-scroll/modules/chunk-render.ts b/src/components/base/b-virtual-scroll/modules/chunk-render.ts deleted file mode 100644 index 934697009a..0000000000 --- a/src/components/base/b-virtual-scroll/modules/chunk-render.ts +++ /dev/null @@ -1,403 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import symbolGenerator from 'core/symbol'; - -import type { WatchOptions } from 'core/dom/intersection-watcher'; - -import Friend from 'components/friends/friend'; -import DOM, { watchForIntersection, appendChild } from 'components/friends/dom'; - -import type iBlock from 'components/super/i-block/i-block'; -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; - -import type ComponentRender from 'components/base/b-virtual-scroll/modules/component-render'; -import type ChunkRequest from 'components/base/b-virtual-scroll/modules/chunk-request'; - -import type { RenderItem, VirtualItemEl } from 'components/base/b-virtual-scroll/interface'; - -DOM.addToPrototype({appendChild, watchForIntersection}); - -const - $$ = symbolGenerator(); - -export default class ChunkRender extends Friend { - override readonly C!: bVirtualScroll; - - /** - * Render items - */ - items: RenderItem[] = []; - - /** - * Index of the last element that intersects the viewport - */ - lastIntersectsItem: number = 0; - - /** - * Chunk number of the current render - */ - chunk: number = 0; - - /** - * Last rendered range - */ - lastRenderRange: number[] = [0, 0]; - - /** - * Async group - */ - readonly asyncGroup: string = 'scroll-render:'; - - /** - * Number of items - */ - get itemsCount(): number { - return this.items.length; - } - - /** - * Async in-view label prefix - */ - protected readonly asyncInViewPrefix: string = 'in-view:'; - - /** - * Refs state update map - */ - protected refsUpdateMap: Map = new Map(); - - /** - * API for dynamic component rendering - */ - protected get componentRender(): ComponentRender { - return this.ctx.componentRender; - } - - /** - * API for scroll data requests - */ - protected get chunkRequest(): ChunkRequest { - return this.ctx.chunkRequest; - } - - /** - * Returns a random threshold number - */ - protected get randomThreshold(): number { - return Math.floor((Math.random() * (0.06 - 0.01) + 0.01) * 100) / 100; - } - - constructor(component: iBlock) { - super(component); - this.component.on('hook:mounted', this.initEventHandlers.bind(this)); - } - - /** - * Re-initializes the rendering process - */ - reInit(): void { - this.lastIntersectsItem = 0; - this.lastRenderRange = [0, 0]; - this.chunk = 0; - this.items = []; - this.refsUpdateMap = new Map(); - - this.async.clearAll({group: new RegExp(this.asyncGroup)}); - - this.setLoadersVisibility(true, true); - this.setRefVisibility('retry', false, true); - this.setRefVisibility('done', false, true); - this.setRefVisibility('empty', false, true); - this.setRefVisibility('renderNext', false, true); - - this.initEventHandlers(); - } - - /** - * Initializes render items - * @param data - */ - initItems(data: unknown[]): void { - this.items = this.items.concat(data.map(this.createRenderItem.bind(this))); - } - - /** - * Renders the component content - * - * @emits `chunkRender:renderStart(chunkNumber: number)` - * @emits `chunkRender:renderComplete(chunkNumber: number)` - * @emits `chunkRender:beforeMount(chunkNumber: number)` - * @emits `chunkRender:mounted(renderItems:` [[RenderItem]]`[], chunkNumber: number)` - */ - render(): void { - if (this.ctx.localState !== 'ready') { - return; - } - - const - {ctx, chunk, items} = this; - - const - renderFrom = (chunk - 1) * ctx.chunkSize, - renderTo = chunk * ctx.chunkSize, - renderItems = items.slice(renderFrom, renderTo); - - if ( - renderFrom === this.lastRenderRange[0] && - renderTo === this.lastRenderRange[1] || - renderItems.length === 0 - ) { - return; - } - - const - currentChunk = this.chunk; - - this.chunk++; - this.lastRenderRange = [renderFrom, renderTo]; - - ctx.emit('chunkRender:renderStart', currentChunk); - - const - nodes = this.renderItems(renderItems); - - ctx.emit('chunkRender:renderComplete', currentChunk); - ctx.emit('chunkRender:beforeMount', currentChunk); - - if (nodes.length === 0) { - return; - } - - const - fragment = document.createDocumentFragment(); - - for (let i = 0; i < nodes.length; i++) { - this.dom.appendChild(fragment, nodes[i], { - group: this.asyncGroup, - destroyIfComponent: true - }); - } - - this.async.requestAnimationFrame(() => { - this.refs.container.appendChild(fragment); - ctx.emit('chunkRender:mounted', renderItems, currentChunk); - }, {group: this.asyncGroup}); - } - - /** - * Hides or shows the specified ref - * - * @param ref - * @param show - * @param [immediate] - if settled as `true` will immediately update a DOM tree - */ - setRefVisibility(ref: keyof bVirtualScroll['$refs'], show: boolean, immediate: boolean = false): void { - const - refEl = >this.refs[ref]; - - if (!refEl) { - return; - } - - if (immediate) { - refEl.style.display = show ? '' : 'none'; - return; - } - - this.refsUpdateMap.set(ref, show); - this.performRefsVisibilityUpdate(); - } - - /** - * Hides or shows refs of the loader and tombstones - * - * @param show - * @param [immediate] - if settled as `true` will immediately update a DOM tree - */ - setLoadersVisibility(show: boolean, immediate: boolean = false): void { - this.setRefVisibility('tombstones', show, immediate); - this.setRefVisibility('loader', show, immediate); - } - - /** - * Tries to show the `renderNext` slot - */ - tryShowRenderNextSlot(): void { - const - {ctx, chunkRequest} = this; - - if (ctx.dataProvider == null && ctx.items.length === 0) { - return; - } - - if (chunkRequest.isDone) { - return; - } - - this.setRefVisibility('renderNext', true); - } - - /** - * Updates visibility of refs by using `requestAnimationFrame` - */ - protected performRefsVisibilityUpdate(): void { - this.async.requestAnimationFrame(() => { - this.refsUpdateMap.forEach((show, ref) => { - const - state = show ? '' : 'none', - refEl = >this.refs[ref]; - - if (!refEl) { - return; - } - - refEl.style.display = state; - }); - - this.refsUpdateMap.clear(); - - }, {label: $$.updateRefsVisibility, group: this.asyncGroup, join: true}); - } - - /** - * Event handlers initialization - */ - protected initEventHandlers(): void { - this.ctx.localEmitter.once('localState.ready', this.onReady.bind(this), {label: $$.reInitReady}); - this.ctx.localEmitter.once('localState.error', this.onError.bind(this), {label: $$.reInitError}); - } - - /** - * Renders the specified items - * @param items - */ - protected renderItems(items: RenderItem[]): HTMLElement[] { - const - nodes = this.componentRender.render(items); - - for (let i = 0; i < nodes.length; i++) { - const - node = nodes[i], - item = items[i]; - - item.node = node; - - const itemsData = { - current: item.data, - prev: items[i - 1]?.data, - next: items[i + 1]?.data - }; - - if (!Object.isFunction(node[$$.inView])) { - this.wrapInView(item, itemsData); - } - } - - return nodes; - } - - /** - * Wraps the specified item node with the `in-view` directive - * - * @param item - * @param itemData - */ - protected wrapInView(item: RenderItem, itemData: VirtualItemEl): void { - const - {ctx} = this, - {node} = item; - - if (ctx.loadStrategy === 'manual') { - return; - } - - const - label = `${this.asyncGroup}:${this.asyncInViewPrefix}${ctx.getItemKey(itemData, item.index)}`; - - if (!node) { - return; - } - - const inViewOpts = { - ...this.getInViewOptions(), - group: this.asyncGroup, - label - }; - - this.dom.watchForIntersection(node, inViewOpts, () => this.onNodeIntersect(item.index)); - } - - /** - * Returns a render item by the specified parameters - * - * @param data - data to render in item - * @param index - index of the item - */ - protected createRenderItem(data: object, index: number): RenderItem { - return { - data, - index: this.itemsCount + index, - node: undefined, - destructor: undefined - }; - } - - /** - * Returns options to initialize the `in-view` directive - */ - protected getInViewOptions(): WatchOptions { - return { - delay: 0, - threshold: this.randomThreshold, - once: !this.ctx.clearNodes - }; - } - - /** - * Handler: element becomes visible in the viewport - * @param index - */ - protected onNodeIntersect(index: number): void { - const - {ctx, items, lastIntersectsItem} = this, - {chunkSize, renderGap} = ctx; - - const - currentRender = (this.chunk - 1) * chunkSize; - - this.lastIntersectsItem = index; - - if (index + renderGap + chunkSize >= items.length) { - this.chunkRequest.try().catch(stderr); - } - - if (index >= lastIntersectsItem) { - if (currentRender - index <= renderGap) { - this.render(); - } - } - } - - /** - * Handler: component ready - */ - protected onReady(): void { - this.setLoadersVisibility(false); - this.chunk++; - this.render(); - } - - /** - * Handler: error occurred - */ - protected onError(): void { - this.setLoadersVisibility(false); - this.setRefVisibility('renderNext', false); - this.setRefVisibility('retry', true); - } -} diff --git a/src/components/base/b-virtual-scroll/modules/chunk-request.ts b/src/components/base/b-virtual-scroll/modules/chunk-request.ts deleted file mode 100644 index 49d332b696..0000000000 --- a/src/components/base/b-virtual-scroll/modules/chunk-request.ts +++ /dev/null @@ -1,418 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import symbolGenerator from 'core/symbol'; -import Friend from 'components/friends/friend'; - -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type ChunkRender from 'components/base/b-virtual-scroll/modules/chunk-render'; - -import { isAsyncClearError } from 'components/base/b-virtual-scroll/modules/helpers'; -import type { RemoteData, DataState, LastLoadedChunk } from 'components/base/b-virtual-scroll/interface'; - -const - $$ = symbolGenerator(); - -export default class ChunkRequest extends Friend { - override readonly C!: bVirtualScroll; - - /** - * Current page - */ - page: number = 1; - - /** - * Total amount of elements being loaded - */ - total: number = 0; - - /** - * All loaded data - */ - data: unknown[] = []; - - /** - * Last loaded data chunk that was processed with `dbConverter` - * - * @deprecated - * {@link ChunkRequest.lastLoadedChunk} - */ - lastLoadedData: unknown[] = []; - - /** - * Last loaded data chunk - */ - lastLoadedChunk: LastLoadedChunk = { - normalized: [], - raw: undefined - }; - - /** - * True if all requests for additional data has been requested - */ - isDone: boolean = false; - - /** - * True if the last request returned an empty array or undefined - */ - isLastEmpty: boolean = false; - - /** - * Contains data that pending to be rendered - */ - pendingData: object[] = []; - - /** - * A buffer to accumulate data from the main request and all additional requests. - * Sometimes a data provider can't provide the whole batch of data in one request, - * so you need to emit some extra requests till the data batch is filled. - */ - currentAccumulatedData: CanUndef = undefined; - - /** - * Contains `currentAccumulatedData` from previous requests cycle - */ - previousDataStore: CanUndef = undefined; - - /** {@link ChunkRequest.previousDataStore} */ - get previousData(): CanUndef { - return this.previousDataStore; - } - - /** - * @emits dataChange(v: unknown) - * {@link ChunkRequest.previousDataStore} - */ - set previousData(v: unknown) { - this.previousDataStore = v; - this.ctx.emit('dataChange', v); - } - - /** - * API for scroll rendering - */ - protected get chunkRender(): ChunkRender { - return this.ctx.chunkRender; - } - - /** - * Resets the current state - */ - reset(): void { - this.total = 0; - this.page = 1; - - this.lastLoadedData = []; - this.data = []; - this.lastLoadedChunk = {raw: undefined, normalized: []}; - this.pendingData = []; - - this.isDone = false; - this.isLastEmpty = false; - this.currentAccumulatedData = undefined; - this.previousDataStore = undefined; - - this.async.clearTimeout({label: 'chunkRequest.waitForInitCalls'}); - this.async.cancelRequest({label: $$.request}); - } - - /** - * Reloads the last request - */ - reloadLast(): void { - this.isDone = false; - this.isLastEmpty = false; - - this.chunkRender.setRefVisibility('retry', false); - this.try().catch(stderr); - } - - /** - * Initializes the request module - */ - async init(): Promise { - await this.async.sleep(15, {label: 'chunkRequest.waitForInitCalls'}); - - const - {chunkSize, dataProvider} = this.ctx; - - this.pendingData = [...this.lastLoadedChunk.normalized]; - - if (this.pendingData.length < chunkSize && dataProvider != null && !this.isDone) { - this.currentAccumulatedData = this.ctx.db?.data; - } - - await this.try(false); - - if ( - this.ctx.localState !== 'error' && - this.pendingData.length === 0 && - this.chunkRender.itemsCount === 0 && - this.isDone - ) { - this.chunkRender.setRefVisibility('empty', true); - } - - this.chunkRender.tryShowRenderNextSlot(); - - if (this.previousData === undefined && Array.isArray(this.ctx.db?.data)) { - this.previousData = this.ctx.db!.data; - } - - this.ctx.localState = 'ready'; - } - - /** - * Tries to request additional data - * - * @param [initialCall] - * - * @emits `dbChange(data:` [[RemoteData]]`)` - * @emits `chunkLoading(page: number)` - */ - try(initialCall: boolean = true): Promise> { - const - {ctx, chunkRender} = this, - {chunkSize, dataProvider} = ctx; - - const - resolved = Promise.resolve(undefined); - - const additionParams = { - lastLoadedChunk: { - ...this.lastLoadedChunk, - normalized: this.lastLoadedChunk.normalized - } - }; - - if (this.pendingData.length > 0) { - if (dataProvider == null) { - chunkRender.initItems(this.pendingData.splice(0, chunkSize)); - chunkRender.render(); - - if (this.pendingData.length === 0) { - this.emitDone(); - } - - return resolved; - } - - if (this.pendingData.length >= chunkSize) { - chunkRender.initItems(this.pendingData.splice(0, chunkSize)); - chunkRender.render(); - - if (this.isDone && this.pendingData.length === 0) { - this.emitDone(); - } - - return resolved; - } - } - - const updateCurrentData = () => { - if (this.currentAccumulatedData != null) { - this.previousData = this.currentAccumulatedData; - this.currentAccumulatedData = undefined; - } - }; - - const shouldRequest = ctx.loadStrategy === 'scroll' ? - ctx.shouldMakeRequest(ctx.getDataStateSnapshot(additionParams, this, chunkRender)) : - true; - - if (this.isDone) { - updateCurrentData(); - this.onRequestsDone(); - return resolved; - } - - const cantRequest = () => this.isDone || - !shouldRequest || - ctx.dataProvider == null || - ctx.mods.progress === 'true'; - - if (cantRequest()) { - return resolved; - } - - if (initialCall) { - this.currentAccumulatedData = undefined; - } - - chunkRender.setLoadersVisibility(true); - chunkRender.setRefVisibility('renderNext', false); - - ctx.emit('chunkLoading', this.page); - - return this.load() - .then((v) => { - if (Object.size(v?.data) === 0) { - this.isLastEmpty = true; - - this.shouldStopRequest(this.ctx.getDataStateSnapshot({ - lastLoadedData: [], - lastLoadedChunk: { - raw: undefined, - normalized: [] - } - }, this, chunkRender)); - - chunkRender.setLoadersVisibility(false); - updateCurrentData(); - - return; - } - - const - data = (v).data!; - - this.page++; - this.isLastEmpty = false; - - this.data = this.data.concat(data); - this.pendingData = this.pendingData.concat(data); - this.currentAccumulatedData = Array.concat(this.currentAccumulatedData ?? [], data); - - ctx.emit('dbChange', {...v, data: this.data}); - this.shouldStopRequest(this.ctx.getCurrentDataState()); - - if (this.pendingData.length < ctx.chunkSize) { - return this.try(false); - } - - this.previousData = this.currentAccumulatedData; - this.currentAccumulatedData = undefined; - - chunkRender.setLoadersVisibility(false); - - if (!this.isDone) { - chunkRender.initItems(this.pendingData.splice(0, chunkSize)); - chunkRender.render(); - } - - if (!this.isDone || this.pendingData.length > 0) { - chunkRender.setRefVisibility('renderNext', true); - } - - }).catch((err) => { - if (isAsyncClearError(err)) { - return; - } - - stderr(err); - return undefined; - }); - } - - /** - * Checks for the possibility of stopping data requests - * @param params - */ - shouldStopRequest(params: DataState): boolean { - const {ctx} = this; - this.isDone = ctx.shouldStopRequest(params); - - if (this.isDone) { - this.onRequestsDone(); - } - - return this.isDone; - } - - /** - * Sets `isDone` to `true` and fires `onRequestDone` handler - */ - protected emitDone(): void { - this.isDone = true; - this.onRequestsDone(); - } - - /** - * Loads additional data - * @emits `chunkLoaded(lastLoadedChunk:` [[LastLoadedChunk]]`)` - */ - protected load(): Promise> { - const { - ctx, - chunkRender - } = this; - - void ctx.setMod('progress', true); - - const - defaultRequestParams = ctx.dataProvider?.getDefaultRequestParams('get'), - params = >(defaultRequestParams ?? [])[0]; - - Object.assign(params, ctx.requestQuery?.(ctx.getCurrentDataState())?.get); - - return ctx.async.request(ctx.getData(this.component, params), {label: $$.request}) - .then((data) => { - this.ctx.localState = 'ready'; - void ctx.removeMod('progress', true); - this.lastLoadedChunk.raw = data; - - const - converted = data != null ? ctx.convertDataToDB(data) : undefined; - - this.lastLoadedChunk.normalized = Object.size(converted?.data) <= 0 ? - this.lastLoadedChunk.normalized = [] : - this.lastLoadedChunk.normalized = converted!.data!; - - ctx.emit('chunkLoaded', this.lastLoadedChunk, this.page); - return converted; - }) - - .catch((err) => { - void ctx.removeMod('progress', true); - - if (isAsyncClearError(err)) { - return Promise.reject(err); - } - - chunkRender.setRefVisibility('retry', true); - chunkRender.setRefVisibility('renderNext', false); - - this.ctx.onRequestError(err, this.ctx.reloadLast.bind(this.ctx)); - stderr(err); - - this.lastLoadedChunk.raw = []; - this.lastLoadedChunk.normalized = []; - - return undefined; - }); - } - - /** - * Handler: all requests are done - */ - protected onRequestsDone(): void { - const - {ctx, chunkRender, async: $a} = this, - {chunkSize} = ctx; - - if (this.pendingData.length > 0) { - chunkRender.initItems(this.pendingData.splice(0, chunkSize)); - chunkRender.render(); - } - - if (this.pendingData.length === 0) { - chunkRender.setRefVisibility('done', true); - chunkRender.setRefVisibility('renderNext', false); - } - - $a.wait(() => ctx.localState === 'ready', {label: $$.requestDoneWaitForReady}) - .then(() => { - if (this.pendingData.length === 0) { - chunkRender.setRefVisibility('done', true); - } - - chunkRender.setLoadersVisibility(false); - }) - .catch(stderr); - } -} diff --git a/src/components/base/b-virtual-scroll/modules/component-render.ts b/src/components/base/b-virtual-scroll/modules/component-render.ts deleted file mode 100644 index 11ee3e4e62..0000000000 --- a/src/components/base/b-virtual-scroll/modules/component-render.ts +++ /dev/null @@ -1,218 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import Friend from 'components/friends/friend'; -import { mergeProps } from 'core/component/render'; - -import type ScrollRender from 'components/base/b-virtual-scroll/modules/chunk-render'; -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; - -import type { RenderItem, DataToRender, ItemAttrs, VirtualItemEl } from 'components/base/b-virtual-scroll/interface'; - -export default class ComponentRender extends Friend { - override readonly C!: bVirtualScroll; - - /** - * Async group - */ - readonly asyncGroup: string = 'component-render'; - - /** - * If false, the cache flushing process is not currently running - */ - protected canDropCache: boolean = false; - - /** - * Rendered items cache - */ - protected nodesCache: Dictionary = Object.createDict(); - - /** - * True if rendered nodes can be cached - */ - protected get canCache(): boolean { - return this.ctx.cacheNodes && this.ctx.clearNodes; - } - - /** - * API for scroll rendering - */ - protected get scrollRender(): ScrollRender { - return this.ctx.chunkRender; - } - - /** - * Classname for options - */ - get optionClass(): CanUndef { - return this.ctx.block?.getFullElementName('option-el'); - } - - /** - * Re-initializes component render - */ - reInit(): void { - Object.keys(this.nodesCache).forEach((key) => { - const el = this.nodesCache[key]; - el?.remove(); - }); - - this.nodesCache = Object.createDict(); - } - - /** - * Returns a node from the cache by the specified key - * @param key - */ - getCachedComponent(key: string): CanUndef { - return this.nodesCache[key]; - } - - /** - * Saves a node to the cache by the specified key - * - * @param key - * @param node - */ - cacheNode(key: string, node: HTMLElement): HTMLElement { - if (!this.ctx.cacheNodes) { - return node; - } - - this.nodesCache[key] = node; - - const - {nodesCache, ctx: {cacheSize}} = this; - - if (Object.keys(nodesCache).length > cacheSize) { - this.canDropCache = true; - } - - return node; - } - - /** {@link bVirtualScroll.getOptionKey} */ - getItemKey(data: VirtualItemEl, index: number): string { - return String(this.ctx.getItemKey(data, index)); - } - - /** - * Renders the specified chunk of items - * @param items - */ - render(items: RenderItem[]): HTMLElement[] { - const - {canCache} = this; - - const - res: HTMLElement[] = [], - needRender: Array<[RenderItem, number, VirtualItemEl]> = []; - - for (let i = 0; i < items.length; i++) { - const - item = items[i]; - - if (item.node) { - res[i] = item.node; - continue; - } - - const getItemKeyData = { - current: item.data, - prev: items[i - 1]?.data, - next: items[i + 1]?.data - }; - - if (canCache) { - const - key = this.getItemKey(getItemKeyData, item.index), - node = this.getCachedComponent(key); - - if (node) { - res[i] = node; - item.node = node; - continue; - } - } - - needRender.push([item, i, getItemKeyData]); - } - - if (needRender.length > 0) { - const - nodes = this.createComponents(needRender.map(([item]) => item)); - - for (let i = 0; i < needRender.length; i++) { - const - [item, indexesToAssign, getItemKeyData] = needRender[i], - node = nodes[i]; - - const - key = this.getItemKey(getItemKeyData, item.index); - - if (canCache) { - this.cacheNode(key, item.node = node); - } - - res[indexesToAssign] = node; - } - } - - return res; - } - - /** - * Creates and renders components by the specified parameters - * @param items - */ - protected createComponents(items: RenderItem[]): HTMLElement[] { - const - {ctx: c, scrollRender: {items: totalItems}} = this; - - const render = (children: DataToRender[]) => { - const map = ({itemAttrs, itemParams, index}) => - this.ctx.vdom.create(c.getItemComponentName(itemParams, index), itemAttrs); - - return c.vdom.render(children.map(map)); - }; - - const getChildrenAttrs = (props: ItemAttrs) => ({ - attrs: mergeProps(props, {class: this.optionClass}) - }); - - const getItemEl = (data, i: number) => ({ - current: data, - prev: totalItems[i - 1]?.data, - next: totalItems[i + 1]?.data - }); - - const - children: DataToRender[] = []; - - for (let i = 0; i < items.length; i++) { - const - item = items[i], - itemParams = getItemEl(item.data, item.index), - itemIndex = item.index; - - const attrs = c.getItemAttrs(getItemEl(item.data, item.index), item.index); - - children.push({itemParams, itemAttrs: getChildrenAttrs(attrs!), index: itemIndex}); - } - - const - // https://github.com/vuejs/core/issues/6061 - res = render(children).filter((node) => node.nodeType !== node.TEXT_NODE); - - if (res.length === 0) { - throw new Error('Failed to render components. Possibly an error occurred while creating the components.'); - } - - return res; - } -} diff --git a/src/components/base/b-virtual-scroll/modules/helpers.ts b/src/components/base/b-virtual-scroll/modules/helpers.ts deleted file mode 100644 index 32738cf489..0000000000 --- a/src/components/base/b-virtual-scroll/modules/helpers.ts +++ /dev/null @@ -1,106 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type ChunkRender from 'components/base/b-virtual-scroll/modules/chunk-render'; -import type ChunkRequest from 'components/base/b-virtual-scroll/modules/chunk-request'; -import type { DataState } from 'components/base/b-virtual-scroll/interface'; - -/** - * Returns accumulated data among `b-virtual-scroll`,` chunk-render`, `chunk-request` and passes it to the client - * to make any decisions. For instance, one more chunk of data needs to be loaded. - * - * @param [chunkRequestCtx] - * @param [chunkRenderCtx] - * @param [merge] - * - * @typeParam ITEM - data item to render - * @typeParam RAW - raw provider data - */ -export function getRequestParams( - chunkRequestCtx?: ChunkRequest, - chunkRenderCtx?: ChunkRender, - merge?: Dictionary -): DataState { - const - component = chunkRenderCtx?.component ?? chunkRequestCtx?.component, - pendingData = chunkRequestCtx?.pendingData ?? []; - - const lastLoadedData = >chunkRequestCtx?.lastLoadedChunk.normalized; - - const base: DataState = { - currentPage: 0, - nextPage: 1, - - data: [], - items: [], - isLastEmpty: false, - itemsTillBottom: 0, - total: undefined, - - pendingData, - - lastLoadedData: lastLoadedData ?? [], - lastLoadedChunk: { - raw: undefined, - normalized: lastLoadedData ?? [] - } - }; - - const params = chunkRequestCtx && chunkRenderCtx ? - { - items: chunkRenderCtx.items, - itemsTillBottom: chunkRenderCtx.items.length - chunkRenderCtx.lastIntersectsItem, - - currentPage: chunkRequestCtx.page, - isLastEmpty: chunkRequestCtx.isLastEmpty, - total: component?.unsafe.total, - - pendingData, - data: chunkRequestCtx.data, - - lastLoadedData: lastLoadedData ?? [], - lastLoadedChunk: { - raw: chunkRequestCtx.lastLoadedChunk.raw, - normalized: lastLoadedData ?? [] - } - } : - base; - - const - mergeLastLoadedChunk = merge?.lastLoadedChunk; - - const merged = { - ...params, - ...merge, - lastLoadedChunk: { - ...params.lastLoadedChunk, - ...mergeLastLoadedChunk - } - }; - - return >{ - ...merged, - nextPage: merged.currentPage + 1 - }; -} - -/** - * True if the specified value is an `async replace` error - * @param val - */ -export function isAsyncReplaceError(val: unknown): boolean { - return Object.isPlainObject(val) && val.join === 'replace'; -} - -/** - * True if the specified value is an `async clear` error - * @param val - */ -export function isAsyncClearError(val: unknown): boolean { - return Object.isPlainObject(val) && val.type === 'clearAsync'; -} diff --git a/src/components/base/b-virtual-scroll/test/index.js b/src/components/base/b-virtual-scroll/test/index.js deleted file mode 100644 index bfc1bf2e89..0000000000 --- a/src/components/base/b-virtual-scroll/test/index.js +++ /dev/null @@ -1,32 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -// @ts-check - -/** - * @typedef {import('playwright').Page} Page - */ - -const - h = include('tests/helpers').default, - u = include('tests/utils').default; - -/** - * Starts a test - * - * @param {Page} page - * @param {object} params - * @returns {Promise} - */ -module.exports = async (page, params) => { - const - test = u.getCurrentTest(); - - await h.utils.setup(page, params.context); - return test(page); -}; diff --git a/src/components/base/b-virtual-scroll/test/runners/events/chunk-loaded.js b/src/components/base/b-virtual-scroll/test/runners/events/chunk-loaded.js deleted file mode 100644 index 20edbc8bae..0000000000 --- a/src/components/base/b-virtual-scroll/test/runners/events/chunk-loaded.js +++ /dev/null @@ -1,230 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -// @ts-check - -/** - * @typedef {import('playwright').Page} Page - */ - -const - h = include('tests/helpers').default; - -/** @param {Page} page */ -module.exports = (page) => { - let - component, - node, - container; - - beforeEach(async () => { - await page.evaluate(() => { - globalThis.removeCreatedComponents(); - - const baseAttrs = { - theme: 'demo', - option: 'section', - optionProps: ({current}) => ({'data-index': current.i}) - }; - - const scheme = [ - { - attrs: { - ...baseAttrs, - id: 'target' - } - } - ]; - - globalThis.renderComponents('b-virtual-scroll', scheme); - }); - - await h.bom.waitForIdleCallback(page); - await h.component.waitForComponentStatus(page, '.b-virtual-scroll', 'ready'); - - component = await h.component.waitForComponent(page, '#target'); - node = await h.dom.waitForEl(page, '#target'); - container = await h.dom.waitForRef(node, 'container'); - }); - - const - getArray = (offset = 0, length = 12) => ({data: Array.from(Array(length), (v, i) => ({i: i + offset}))}), - firstChunkExpected = getArray(); - - const subscribe = () => component.evaluate((ctx) => new Promise((res) => ctx.watch(':onChunkLoaded', res))); - - const setProps = (requestProps = {}) => component.evaluate((ctx, requestProps) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.chunkSize = 10; - ctx.request = {get: {chunkSize: 12, id: Math.random(), ...requestProps}}; - }, requestProps); - - describe('b-virtual-scroll `chunkLoaded` event', () => { - describe('emitted', () => { - it('after loading the first chunk', async () => { - const subscribePromise = subscribe(); - await setProps(); - - await expectAsync(subscribePromise).toBeResolved(); - }); - - it('after loading the second chunk', async () => { - await setProps(); - await h.dom.waitForEl(container, 'section'); - - const subscribePromise = subscribe(); - - await h.scroll.scrollToBottom(page); - await expectAsync(subscribePromise).toBeResolved(); - }); - - it('when loading the first chunk after re-initialization', async () => { - await setProps(); - await h.dom.waitForEl(container, 'section'); - - const subscribePromise = subscribe(); - - await component.evaluate((ctx) => ctx.request = {get: {chunkSize: 20}}); - await expectAsync(subscribePromise).toBeResolved(); - }); - - it('three times to get the full data batch', async () => { - await component.evaluate((ctx) => { - ctx.watch(':onChunkLoaded', () => { - ctx.tmp.called = (ctx.tmp.called ?? 0) + 1; - }); - }); - - await setProps({chunkSize: 4}); - await h.dom.waitForEl(container, 'section'); - - expect(await component.evaluate((ctx) => ctx.tmp.called)).toBe(3); - }); - - it('after successful loading of the first chunk without payload', async () => { - const subscribePromise = subscribe(); - - await setProps({chunkSize: 0, total: 0}); - - await expectAsync(subscribePromise).toBeResolved(); - }); - - it('after successful loading of the second chunk without payload', async () => { - await setProps({chunkSize: 12, total: 12}); - await h.dom.waitForEl(container, 'section'); - - const subscribePromise = subscribe(); - - await h.scroll.scrollToBottom(page); - await expectAsync(subscribePromise).toBeResolved(); - }); - }); - - describe('not emitted', () => { - it('if there was a request error', async () => { - await component.evaluate((ctx) => ctx.watch(':onChunkLoaded', () => ctx.tmp.change = true)); - - await setProps({failOn: 0}); - await h.bom.waitForIdleCallback(page, {sleepAfterIdles: 1000}); - - expect(await component.evaluate((ctx) => ctx.tmp.change)).toBeUndefined(); - }); - }); - - describe('has correct payload', () => { - it('after loading the first chunk', async () => { - const subscribePromise = subscribe(); - - await component.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.chunkSize = 10; - ctx.request = {get: {chunkSize: 12, additionalData: {size: 12}}}; - }); - - await expectAsync(subscribePromise).toBeResolvedTo({ - normalized: firstChunkExpected.data, - raw: {data: firstChunkExpected.data, size: 12} - }); - }); - - it('after loading the second chunk', async () => { - await component.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.chunkSize = 10; - ctx.request = {get: {chunkSize: 12, additionalData: {size: 12}}}; - }); - - await h.dom.waitForEl(container, 'section'); - - const subscribePromise = subscribe(); - - await h.scroll.scrollToBottom(page); - await expectAsync(subscribePromise).toBeResolved({ - normalized: firstChunkExpected.data, - raw: {data: firstChunkExpected.data, size: 12} - }); - }); - - it('after loading the first chunk with an empty payload', async () => { - const subscribePromise = subscribe(); - - await component.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.chunkSize = 10; - ctx.request = {get: {id: Math.random(), chunkSize: 0, total: 0, additionalData: {size: 12}}}; - }); - - await expectAsync(subscribePromise).toBeResolved({ - normalized: [], - raw: {data: [], size: 12} - }); - }); - - it('after loading the second chunk with an empty payload', async () => { - await component.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.chunkSize = 10; - ctx.request = {get: {id: Math.random(), chunkSize: 12, total: 12, additionalData: {size: 12}}}; - }); - - await h.dom.waitForEl(container, 'section'); - - const subscribePromise = subscribe(); - - await h.scroll.scrollToBottom(page); - await expectAsync(subscribePromise).toBeResolved({ - normalized: [], - raw: {data: [], size: 12} - }); - }); - - it('when loading the first chunk in parts', async () => { - await component.evaluate((ctx) => { - ctx.tmp.eventAccumulator = {}; - - ctx.watch(':onChunkLoaded', (val) => { - ctx.tmp.called = (ctx.tmp.called ?? 0) + 1; - ctx.tmp.eventAccumulator[ctx.tmp.called] = Object.fastClone(val); - }); - - ctx.dataProvider = 'demo.Pagination'; - ctx.chunkSize = 10; - ctx.request = {get: {chunkSize: 4, id: Math.random(), additionalData: {size: 12}}}; - }); - - await h.dom.waitForEl(container, 'section'); - - expect(await component.evaluate((ctx) => ctx.tmp.eventAccumulator)).toEqual({ - 1: {normalized: getArray(0, 4).data, raw: {data: getArray(0, 4).data, size: 12}}, - 2: {normalized: getArray(4, 4).data, raw: {data: getArray(4, 4).data, size: 12}}, - 3: {normalized: getArray(8, 4).data, raw: {data: getArray(8, 4).data, size: 12}} - }); - }); - }); - }); -}; diff --git a/src/components/base/b-virtual-scroll/test/runners/events/chunk-loading.js b/src/components/base/b-virtual-scroll/test/runners/events/chunk-loading.js deleted file mode 100644 index 286f8e8597..0000000000 --- a/src/components/base/b-virtual-scroll/test/runners/events/chunk-loading.js +++ /dev/null @@ -1,106 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -// @ts-check - -/** - * @typedef {import('playwright').Page} Page - */ - -const - h = include('tests/helpers').default; - -/** @param {Page} page */ -module.exports = (page) => { - let - component, - node, - container; - - beforeEach(async () => { - await page.evaluate(() => { - globalThis.removeCreatedComponents(); - - const baseAttrs = { - theme: 'demo', - option: 'section', - optionProps: ({current}) => ({'data-index': current.i}) - }; - - const scheme = [ - { - attrs: { - ...baseAttrs, - id: 'target' - } - } - ]; - - globalThis.renderComponents('b-virtual-scroll', scheme); - }); - - await h.bom.waitForIdleCallback(page); - await h.component.waitForComponentStatus(page, '.b-virtual-scroll', 'ready'); - - component = await h.component.waitForComponent(page, '#target'); - node = await h.dom.waitForEl(page, '#target'); - container = await h.dom.waitForRef(node, 'container'); - }); - - const subscribe = () => component.evaluate((ctx) => new Promise((res) => ctx.watch(':onChunkLoading', res))); - - const setProps = (requestProps = {}) => component.evaluate((ctx, requestProps) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.chunkSize = 10; - ctx.request = {get: {chunkSize: 12, id: Math.random(), ...requestProps}}; - }, requestProps); - - describe('b-virtual-scroll `chunkLoading` event', () => { - describe('emitted', () => { - it('when loading the first chunk', async () => { - const subscribePromise = subscribe(); - - await setProps(); - await expectAsync(subscribePromise).toBeResolvedTo(0); - }); - - it('when loading the second chunk', async () => { - await setProps(); - await h.dom.waitForEl(container, 'section'); - - const subscribePromise = subscribe(); - - await h.scroll.scrollToBottom(page); - await expectAsync(subscribePromise).toBeResolvedTo(1); - }); - - it('when loading the first chunk after re-initialization', async () => { - await setProps(); - await h.dom.waitForEl(container, 'section'); - - const subscribePromise = subscribe(); - await setProps({id: Math.random()}); - - await expectAsync(subscribePromise).toBeResolvedTo(0); - }); - - it('three times when loading a full chunk', async () => { - await component.evaluate((ctx) => ctx.watch(':onChunkLoading', (val) => { - ctx.tmp.currentCall = (ctx.tmp.currentCall ?? 0) + 1; - ctx.tmp[ctx.tmp.currentCall] = val; - })); - - await setProps({chunkSize: 4}); - await h.dom.waitForEl(container, 'section'); - - expect(await component.evaluate((ctx) => ctx.tmp.currentCall)).toBe(3); - expect(await component.evaluate((ctx) => [ctx.tmp[1], ctx.tmp[2], ctx.tmp[3]])).toEqual([0, 1, 2]); - }); - }); - }); -}; diff --git a/src/components/base/b-virtual-scroll/test/runners/events/data-change.js b/src/components/base/b-virtual-scroll/test/runners/events/data-change.js deleted file mode 100644 index d05883d4c5..0000000000 --- a/src/components/base/b-virtual-scroll/test/runners/events/data-change.js +++ /dev/null @@ -1,216 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -// @ts-check - -/** - * @typedef {import('playwright').Page} Page - */ - -const - h = include('tests/helpers').default; - -/** @param {Page} page */ -module.exports = (page) => { - let - component, - node, - container; - - const - getArray = (offset = 0, length = 12) => Array.from(Array(length), (v, i) => ({i: i + offset})), - firstChunkExpected = getArray(), - secondChunkExpected = getArray(12); - - const setProps = (requestProps = {}) => component.evaluate((ctx, requestProps) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.chunkSize = 10; - ctx.request = {get: {chunkSize: 12, id: Math.random(), ...requestProps}}; - }, requestProps); - - const subscribe = () => component.evaluate((ctx) => new Promise((res) => ctx.watch(':onDataChange', res))); - - beforeEach(async () => { - await page.evaluate(() => { - globalThis.removeCreatedComponents(); - - const baseAttrs = { - theme: 'demo', - option: 'section', - optionProps: ({current}) => ({'data-index': current.i}) - }; - - const scheme = [ - { - attrs: { - ...baseAttrs, - id: 'target' - } - } - ]; - - globalThis.renderComponents('b-virtual-scroll', scheme); - }); - - await h.bom.waitForIdleCallback(page); - await h.component.waitForComponentStatus(page, '.b-virtual-scroll', 'ready'); - - component = await h.component.waitForComponent(page, '#target'); - node = await h.dom.waitForEl(page, '#target'); - container = await h.dom.waitForRef(node, 'container'); - }); - - describe('b-virtual-scroll `dataChange` event', () => { - describe('emitted', () => { - it('after loading the first chunk', async () => { - const subscribePromise = subscribe(); - - await setProps(); - await expectAsync(subscribePromise).toBeResolved(); - }); - - it('after loading the second chunk', async () => { - await setProps(); - await h.dom.waitForEl(container, 'section'); - - const subscribePromise = subscribe(); - - await h.scroll.scrollToBottom(page); - await expectAsync(subscribePromise).toBeResolved(); - }); - - it('after loading the first part of the batch and stopping further loading because `shouldStopRequest` have returned `true`', async () => { - const subscribePromise = subscribe(); - - await component.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.request = {get: {chunkSize: 4, id: Math.random()}}; - ctx.shouldStopRequest = () => true; - }); - - await expectAsync(subscribePromise).toBeResolvedTo(getArray(0, 4)); - }); - - it('after loading the second part of the batch and stopping further loading because `shouldStopRequest` have returned `true`', async () => { - const subscribePromise = subscribe(); - - await component.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.request = {get: {chunkSize: 4, id: Math.random()}}; - ctx.shouldStopRequest = (v) => v.pendingData.length === 8; - }); - - await expectAsync(subscribePromise).toBeResolvedTo(getArray(0, 8)); - }); - - it('after loading the first part of the second batch and stopping further loading because `shouldStopRequest` have returned `true`', async () => { - await component.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.request = {get: {chunkSize: 4, id: Math.random()}}; - ctx.shouldStopRequest = (v) => { - const {lastLoadedChunk: {normalized}} = v; - return normalized[normalized.length - 1].i === 15; - }; - }); - - await h.dom.waitForEl(container, 'section'); - - const subscribePromise = subscribe(); - await h.scroll.scrollToBottom(page); - - await expectAsync(subscribePromise).toBeResolvedTo(getArray(12, 4)); - }); - - }); - - describe('not emitted', () => { - it('if there was a request error', async () => { - await component.evaluate((ctx) => ctx.watch(':onDataChange', () => ctx.tmp.change = true)); - - await setProps({failOn: 0}); - await h.bom.waitForIdleCallback(page, {sleepAfterIdles: 1000}); - - expect(await component.evaluate((ctx) => ctx.tmp.change)).toBeUndefined(); - }); - - it('if there was a request error on the second chunk', async () => { - await setProps({failOn: 1}); - await h.dom.waitForEl(container, 'section'); - - await component.evaluate((ctx) => ctx.watch(':onDataChange', () => ctx.tmp.change = true)); - - await h.scroll.scrollToBottom(page); - await h.bom.waitForIdleCallback(page, {sleepAfterIdles: 500}); - - expect(await component.evaluate((ctx) => ctx.tmp.change)).toBeUndefined(); - }); - }); - - describe('has correct payload', () => { - it('if nothing was loaded', async () => { - const subscribePromise = subscribe(); - - await setProps({total: 0, chunkSize: 0}); - await expectAsync(subscribePromise).toBeResolvedTo([]); - }); - - describe('after loading', () => { - it('first chunk', async () => { - const subscribePromise = subscribe(); - - await setProps({chunkSize: 12}); - await expectAsync(subscribePromise).toBeResolvedTo(firstChunkExpected); - }); - - it('second chunk', async () => { - await setProps({chunkSize: 12}); - await h.dom.waitForEl(container, 'section'); - - const subscribePromise = subscribe(); - - await h.scroll.scrollToBottom(page); - await expectAsync(subscribePromise).toBeResolvedTo(secondChunkExpected); - }); - }); - - describe('after re-initialization', () => { - it('and loading the first chunk with 2 requests', async () => { - await setProps({id: undefined}); - await h.dom.waitForEl(container, 'section'); - - const subscribePromise = subscribe(); - - await component.evaluate((ctx) => ctx.request = {get: {chunkSize: 6, id: Math.random()}}); - - await expectAsync(subscribePromise).toBeResolvedTo(firstChunkExpected); - }); - - it('and loading the second chunk with 2 requests', async () => { - await setProps({id: undefined}); - await h.dom.waitForEl(container, 'section'); - - await component.evaluate((ctx) => ctx.watch(':onDataChange', (val) => { - ctx.tmp.currentCall = ctx.tmp.currentCall ?? 0; - ctx.tmp[ctx.tmp.currentCall] = val; - ctx.tmp.currentCall++; - })); - - await component.evaluate((ctx) => ctx.request = {get: {chunkSize: 6, id: Math.random()}}); - await h.bom.waitForIdleCallback(page, {sleepAfterIdles: 1000}); - - expect(await component.evaluate((ctx) => ctx.tmp[0])).toEqual(firstChunkExpected); - - await h.scroll.scrollToBottom(page); - await h.bom.waitForIdleCallback(page, {sleepAfterIdles: 1000}); - - expect(await component.evaluate((ctx) => ctx.tmp[1])).toEqual(secondChunkExpected); - }); - }); - }); - }); -}; diff --git a/src/components/base/b-virtual-scroll/test/runners/events/db-change.js b/src/components/base/b-virtual-scroll/test/runners/events/db-change.js deleted file mode 100644 index b0780bc9a7..0000000000 --- a/src/components/base/b-virtual-scroll/test/runners/events/db-change.js +++ /dev/null @@ -1,184 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -// @ts-check - -/** - * @typedef {import('playwright').Page} Page - */ - -const - h = include('tests/helpers').default; - -/** @param {Page} page */ -module.exports = (page) => { - let - component, - node, - container; - - const - getArray = (offset = 0, length = 12) => ({data: Array.from(Array(length), (v, i) => ({i: i + offset}))}), - firstChunkExpected = getArray(); - - const setProps = (requestProps = {}) => component.evaluate((ctx, requestProps) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.chunkSize = 10; - ctx.request = {get: {chunkSize: 12, id: Math.random(), ...requestProps}}; - }, requestProps); - - const subscribe = () => component.evaluate((ctx) => new Promise((res) => ctx.watch(':onDBChange', res))); - - beforeEach(async () => { - await page.evaluate(() => { - globalThis.removeCreatedComponents(); - - const baseAttrs = { - theme: 'demo', - option: 'section', - optionProps: ({current}) => ({'data-index': current.i}) - }; - - const scheme = [ - { - attrs: { - ...baseAttrs, - id: 'target' - } - } - ]; - - globalThis.renderComponents('b-virtual-scroll', scheme); - }); - - await h.bom.waitForIdleCallback(page); - await h.component.waitForComponentStatus(page, '.b-virtual-scroll', 'ready'); - - component = await h.component.waitForComponent(page, '#target'); - node = await h.dom.waitForEl(page, '#target'); - container = await h.dom.waitForRef(node, 'container'); - }); - - describe('b-virtual-scroll `dbChange` event', () => { - describe('emitted', () => { - it('after loading the first chunk', async () => { - const subscribePromise = subscribe(); - await setProps(); - - await expectAsync(subscribePromise).toBeResolved(); - }); - - it('after loading the second chunk', async () => { - await setProps(); - await h.dom.waitForEl(container, 'section'); - - const subscribePromise = subscribe(); - - await h.scroll.scrollToBottom(page); - await expectAsync(subscribePromise).toBeResolved(); - }); - - it('after loading the first chunk after re-initialization', async () => { - await setProps(); - await h.dom.waitForEl(container, 'section'); - - const subscribePromise = subscribe(); - - await component.evaluate((ctx) => ctx.request = {get: {chunkSize: 20}}); - await expectAsync(subscribePromise).toBeResolved(); - }); - - it('three times to get the full data batch', async () => { - await component.evaluate((ctx) => { - ctx.watch(':onDBChange', () => { - ctx.tmp.called = (ctx.tmp.called ?? 0) + 1; - }); - }); - - await setProps({chunkSize: 4}); - await h.dom.waitForEl(container, 'section'); - - expect(await component.evaluate((ctx) => ctx.tmp.called)).toBe(3); - }); - - it('after successful loading of the first chunk without payload', async () => { - const subscribePromise = subscribe(); - - await setProps({chunkSize: 0, total: 0}); - - await expectAsync(subscribePromise).toBeResolved(); - }); - }); - - describe('not emitted', () => { - it('if there was a request error', async () => { - await component.evaluate((ctx) => { - ctx.watch(':onDBChange', () => { - ctx.tmp.called = (ctx.tmp.called ?? 0) + 1; - }); - }); - - await setProps({failOn: 0}); - await h.bom.waitForIdleCallback(page, {sleepAfterIdles: 500}); - - expect(await component.evaluate((ctx) => ctx.tmp.called)).toBeUndefined(); - }); - - it('after successful loading of the second chunk without payload', async () => { - await setProps({chunkSize: 12, total: 12}); - await h.dom.waitForEl(container, 'section'); - - await component.evaluate((ctx) => { - ctx.watch(':onDBChange', () => { - ctx.tmp.called = (ctx.tmp.called ?? 0) + 1; - }); - }); - - await h.scroll.scrollToBottom(page); - expect(await component.evaluate((ctx) => ctx.tmp.called)).toBeUndefined(); - }); - }); - - describe('has correct payload', () => { - it('after loading the first chunk', async () => { - const subscribePromise = subscribe(); - - await setProps({chunkSize: 4}); - await h.dom.waitForEl(container, 'section'); - - await expectAsync(subscribePromise).toBeResolvedTo(getArray(0, 4)); - }); - - it('after loading two chunks', async () => { - await component.evaluate((ctx) => { - ctx.watch(':onDBChange', (val) => { - ctx.tmp.called = (ctx.tmp.called ?? 0) + 1; - ctx.tmp[ctx.tmp.called] = Object.fastClone(val); - }); - }); - - await setProps({chunkSize: 6}); - await h.dom.waitForEl(container, 'section'); - - expect(await component.evaluate((ctx) => ctx.tmp[1])).toEqual(getArray(0, 6)); - expect(await component.evaluate((ctx) => ctx.tmp[2])).toEqual(getArray(0, 12)); - - }); - - it('after re-initialization and loading the first chunk', async () => { - await setProps({chunkSize: 6}); - await h.dom.waitForEl(container, 'section'); - - const subscribePromise = subscribe(); - - await setProps({chunkSize: 12, id: Math.random()}); - await expectAsync(subscribePromise).toBeResolvedTo(firstChunkExpected); - }); - }); - }); -}; diff --git a/src/components/base/b-virtual-scroll/test/runners/functional/items.js b/src/components/base/b-virtual-scroll/test/runners/functional/items.js deleted file mode 100644 index 970b40be35..0000000000 --- a/src/components/base/b-virtual-scroll/test/runners/functional/items.js +++ /dev/null @@ -1,102 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -// @ts-check - -/** - * @typedef {import('playwright').Page} Page - */ - -const - h = include('tests/helpers').default; - -/** @param {Page} page */ -module.exports = (page) => { - let - node, - container, - component; - - const renderComponent = async (attrs = {}) => { - await page.evaluate(([attrs]) => { - globalThis.removeCreatedComponents(); - - const baseAttrs = { - theme: 'demo', - dataProvider: 'demo.Pagination', - chunkSize: 10, - request: {get: {chunkSize: 10, id: Math.random()}}, - item: 'section', - itemProps: ({current}) => ({'data-index': current.i}), - itemKey: (data) => data.current.i, - optionKey: (data) => data.current.i - }; - - const scheme = [ - { - attrs: { - ...baseAttrs, - id: 'target', - ...attrs - } - } - ]; - - globalThis.renderComponents('b-virtual-scroll', scheme); - }, [attrs]); - - node = await h.dom.waitForEl(page, '#target'); - component = await h.component.waitForComponent(page, '#target'); - container = await h.dom.waitForRef(node, 'container'); - }; - - beforeEach(async () => { - await h.component.waitForComponent(page, '#root-component'); - await page.evaluate(() => globalThis.removeCreatedComponents()); - }); - - describe('b-virtual-scroll with the `iItems` trait', () => { - it('renders a correct item', async () => { - await renderComponent(); - await h.dom.waitForEl(container, 'section'); - expect(await container.$('section')).toBeTruthy(); - }); - - it('renders an item with provided props', async () => { - await renderComponent(); - await h.dom.waitForEl(container, 'section'); - const attrVal = await (await container.$('section')).evaluate((el) => el.getAttribute('data-index')); - expect(parseInt(attrVal, 10)).toBe(0); - }); - - it('uses the deprecated `optionKey` property', async () => { - await renderComponent({ - itemKey: undefined - }); - - const optionKey1 = await component.evaluate((ctx) => ctx.getItemKey({current: {i: 0}})); - expect(optionKey1).toBe(0); - - const optionKey2 = await component.evaluate((ctx) => ctx.getItemKey({current: {i: 1}})); - expect(optionKey2).toBe(1); - }); - - it('uses the `itemKey` property', async () => { - await renderComponent({ - optionKey: undefined - }); - - const itemKey1 = await component.evaluate((ctx) => ctx.getItemKey({current: {i: 0}})); - expect(itemKey1).toBe(0); - - const itemKey2 = await component.evaluate((ctx) => ctx.getItemKey({current: {i: 1}})); - expect(itemKey2).toBe(1); - }); - }); - -}; diff --git a/src/components/base/b-virtual-scroll/test/runners/functional/render-next.js b/src/components/base/b-virtual-scroll/test/runners/functional/render-next.js deleted file mode 100644 index 826fcb7725..0000000000 --- a/src/components/base/b-virtual-scroll/test/runners/functional/render-next.js +++ /dev/null @@ -1,102 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -// @ts-check - -/** - * @typedef {import('playwright').Page} Page - */ - -const - h = include('tests/helpers').default; - -/** @param {Page} page */ -module.exports = (page) => { - let - component, - node, - container; - - const - getContainerChildCount = () => component.evaluate((ctx) => ctx.$refs.container.childElementCount); - - beforeEach(async () => { - await page.evaluate(() => { - globalThis.removeCreatedComponents(); - - const baseAttrs = { - theme: 'demo', - option: 'section', - optionProps: ({current}) => ({'data-index': current.i}) - }; - - const scheme = [ - { - attrs: { - ...baseAttrs, - id: 'target' - } - } - ]; - - globalThis.renderComponents('b-virtual-scroll', scheme); - }); - - await h.bom.waitForIdleCallback(page); - await h.component.waitForComponentStatus(page, '.b-virtual-scroll', 'ready'); - - node = await h.dom.waitForEl(page, '#target'); - component = await h.component.waitForComponent(page, '#target'); - container = await h.dom.waitForRef(node, 'container'); - - await component.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.chunkSize = 10; - ctx.request = {get: {chunkSize: 10, id: Math.random()}}; - }); - }); - - describe('b-virtual-scroll', () => { - ['manual', 'scroll'].forEach((strategy) => { - describe(`renderNext with loadStrategy: ${strategy}`, () => { - it('renders the next data batch', async () => { - await component.evaluate((ctx, strategy) => { - ctx.loadStrategy = strategy; - ctx.request = {get: {chunkSize: 20, id: Math.random()}}; - }, strategy); - - await h.dom.waitForEl(container, 'section'); - - expect(await component.evaluate((ctx) => ctx.chunkRequest.pendingData.length)).toBe(10); - await component.evaluate((ctx) => ctx.renderNext()); - - expect(await component.evaluate((ctx) => ctx.chunkRequest.pendingData.length)).toBe(0); - await h.dom.waitForEl(container, 'section:nth-child(11)'); - - expect(await getContainerChildCount()).toBe(20); - }); - - it('requests and renders the next data batch', async () => { - await component.evaluate((ctx, strategy) => { - ctx.loadStrategy = strategy; - ctx.request = {get: {chunkSize: 10, id: Math.random()}}; - }, strategy); - - await h.dom.waitForEl(container, 'section'); - expect(await component.evaluate((ctx) => ctx.chunkRequest.pendingData.length)).toBe(0); - - await component.evaluate((ctx) => ctx.renderNext()); - await h.dom.waitForEl(container, 'section:nth-child(11)'); - - expect(await component.evaluate((ctx) => ctx.chunkRequest.pendingData.length)).toBe(0); - expect(await getContainerChildCount()).toBe(20); - }); - }); - }); - }); -}; diff --git a/src/components/base/b-virtual-scroll/test/runners/functional/state.js b/src/components/base/b-virtual-scroll/test/runners/functional/state.js deleted file mode 100644 index 85d5e1ef9d..0000000000 --- a/src/components/base/b-virtual-scroll/test/runners/functional/state.js +++ /dev/null @@ -1,263 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -// @ts-check - -/** - * @typedef {import('playwright').Page} Page - */ - -const - h = include('tests/helpers').default; - -/** @param {Page} page */ -module.exports = (page) => { - let - component, - node, - container; - - const - getArray = (offset = 0, length = 12) => ({data: Array.from(Array(length), (v, i) => ({i: i + offset}))}), - firstChunkExpected = getArray(), - secondChunkExpected = getArray(12); - - const getExpected = (params = {}) => ({ - items: undefined, - - itemsTillBottom: undefined, - currentPage: 0, - nextPage: 1, - isLastEmpty: false, - total: undefined, - - data: [], - pendingData: [], - - lastLoadedData: [], - lastLoadedChunk: { - raw: undefined, - normalized: [] - }, - ...params - }); - - const getCurrentComponentState = () => component.evaluate((ctx) => ({ - ...ctx.getCurrentDataState(), - itemsTillBottom: undefined, - items: undefined - })); - - const setProps = (requestProps = {}) => component.evaluate((ctx, requestProps) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.chunkSize = 10; - ctx.request = {get: {chunkSize: 12, id: Math.random(), ...requestProps}}; - }, requestProps); - - const - initialTimeout = globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL; - - beforeAll(() => { - globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL = (20).seconds(); - }); - - afterAll(() => { - globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL = initialTimeout; - }); - - beforeEach(async () => { - await h.utils.reloadAndWaitForIdle(page); - await h.component.waitForComponent(page, '#root-component'); - - await page.evaluate(() => { - globalThis.removeCreatedComponents(); - - const baseAttrs = { - theme: 'demo', - option: 'section', - optionProps: ({current}) => ({'data-index': current.i}) - }; - - const scheme = [ - { - attrs: { - ...baseAttrs, - id: 'target' - } - } - ]; - - globalThis.renderComponents('b-virtual-scroll', scheme); - }); - - await h.bom.waitForIdleCallback(page); - await h.component.waitForComponentStatus(page, '.b-virtual-scroll', 'ready'); - - component = await h.component.waitForComponent(page, '#target'); - node = await h.dom.waitForEl(page, '#target'); - container = await h.dom.waitForRef(node, 'container'); - }); - - describe('b-virtual-scroll `getCurrentDataState`', () => { - describe('returns the correct value', () => { - it('if there is no `dataProvider`', async () => { - const - expected = getExpected(), - current = await getCurrentComponentState(); - - expect(current).toEqual(expected); - }); - - it('after loading the first chunk', async () => { - const expected = getExpected({ - currentPage: 1, - nextPage: 2, - data: firstChunkExpected.data, - pendingData: getArray(10, 2).data, - lastLoadedData: firstChunkExpected.data, - lastLoadedChunk: { - raw: firstChunkExpected, - normalized: firstChunkExpected.data - } - }); - - await setProps(); - await h.dom.waitForEl(container, 'section'); - - const current = await getCurrentComponentState(); - expect(current).toEqual(expected); - }); - - it('after loading the second chunk', async () => { - const expected = getExpected({ - currentPage: 2, - nextPage: 3, - data: getArray(0, 24).data, - pendingData: getArray(20, 4).data, - lastLoadedData: secondChunkExpected.data, - lastLoadedChunk: { - raw: secondChunkExpected, - normalized: secondChunkExpected.data - } - }); - - await setProps(); - await h.dom.waitForEl(container, 'section'); - - await h.scroll.scrollToBottom(page); - await h.dom.waitForEl(container, 'section:nth-child(11)'); - - const current = await getCurrentComponentState(); - expect(current).toEqual(expected); - }); - - it('after re-initialization and without `dataProvider`', async () => { - const expected = getExpected({currentPage: 0, nextPage: 1}); - - await setProps(); - await h.dom.waitForEl(container, 'section'); - - await component.evaluate((ctx) => { - ctx.dataProvider = ''; - ctx.request = undefined; - ctx.reInit(); - }); - - await h.dom.waitForEl(container, 'section', {to: 'unmount'}); - await h.bom.waitForIdleCallback(page, {sleepAfterIdles: 1000}); - - const current = await getCurrentComponentState(); - expect(current).toEqual(expected); - }); - - it('after re-initialization and with `dataProvider`', async () => { - const expected = getExpected({ - currentPage: 1, - nextPage: 2, - data: firstChunkExpected.data, - pendingData: getArray(10, 2).data, - lastLoadedData: firstChunkExpected.data, - lastLoadedChunk: { - raw: firstChunkExpected, - normalized: firstChunkExpected.data - } - }); - - await setProps(); - await h.dom.waitForEl(container, 'section'); - - await setProps({id: Math.random()}); - await h.bom.waitForIdleCallback(page); - await h.dom.waitForEl(container, 'section', {to: 'mount'}); - - const current = await getCurrentComponentState(); - expect(current).toEqual(expected); - }); - - it('if for the full loading it was necessary to go several times to `dataProvider`', async () => { - const expected = getExpected({ - currentPage: 3, - nextPage: 4, - data: firstChunkExpected.data, - pendingData: getArray(10, 2).data, - lastLoadedData: getArray(8, 4).data, - lastLoadedChunk: { - raw: getArray(8, 4), - normalized: getArray(8, 4).data - } - }); - - await setProps({chunkSize: 4}); - await h.dom.waitForEl(container, 'section'); - - const current = await getCurrentComponentState(); - expect(current).toEqual(expected); - }); - }); - }); - - describe('b-virtual-scroll `getDataStateSnapshot`', () => { - describe('returns the correct value', () => { - it('with `chunkRequest` and `chunkRender`', async () => { - const - expected = getExpected(), - current = await component.evaluate((ctx) => ctx.getDataStateSnapshot({ - items: undefined, - itemsTillBottom: undefined - })); - - expect(current).toEqual(expected); - }); - - it('with `chunkRequest`', async () => { - const - expected = getExpected(), - current = await component.evaluate((ctx) => ctx.getDataStateSnapshot({ - items: undefined, - itemsTillBottom: undefined - }, ctx.chunkRequest)); - - expect(current).toEqual(expected); - }); - - it('with override params, `chunkRequest` and `chunkRender`', async () => { - const expected = getExpected({ - currentPage: 1, - nextPage: 2 - }); - - const current = await component.evaluate((ctx) => ctx.getDataStateSnapshot({ - items: undefined, - itemsTillBottom: undefined - }, ctx.chunkRequest, ctx.chunkRender)); - - expect(current).toEqual(expected); - }); - }); - }); -}; diff --git a/src/components/base/b-virtual-scroll/test/runners/render/render.js b/src/components/base/b-virtual-scroll/test/runners/render/render.js deleted file mode 100644 index b2bc6574fd..0000000000 --- a/src/components/base/b-virtual-scroll/test/runners/render/render.js +++ /dev/null @@ -1,205 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -// @ts-check - -/** - * @typedef {import('playwright').Page} Page - */ - -const - h = include('tests/helpers').default; - -/** @param {Page} page */ -module.exports = (page) => { - let - component, - node, - container; - - const - getContainerChildCount = () => component.evaluate((ctx) => ctx.$refs.container.childElementCount); - - const setProps = async (reqParams) => { - await component.evaluate((ctx, reqParams) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.chunkSize = 10; - ctx.request = {get: {chunkSize: 10, id: Math.random(), ...reqParams}}; - }, reqParams); - - await h.dom.waitForEl(container, 'section'); - }; - - const - initialTimeout = globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL; - - beforeAll(() => { - globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL = (20).seconds(); - }); - - afterAll(() => { - globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL = initialTimeout; - }); - - beforeEach(async () => { - await page.evaluate(() => { - globalThis.removeCreatedComponents(); - - const baseAttrs = { - theme: 'demo', - option: 'section', - optionProps: ({current}) => ({'data-index': current.i}) - }; - - const scheme = [ - { - attrs: { - ...baseAttrs, - id: 'target' - } - } - ]; - - globalThis.renderComponents('b-virtual-scroll', scheme); - globalThis.componentNode = document.querySelector('.b-virtual-scroll'); - }); - - await h.bom.waitForIdleCallback(page); - await h.component.waitForComponentStatus(page, '.b-virtual-scroll', 'ready'); - - node = await h.dom.waitForEl(page, '#target'); - component = await h.component.waitForComponent(page, '#target'); - container = await h.dom.waitForRef(node, 'container'); - }); - - describe('b-virtual-scroll rendering', () => { - describe('after re-initialization', () => { - describe('by changing the `request` prop', () => { - it('removes old elements', async () => { - await setProps(); - - await component.evaluate((ctx) => ctx.request = {get: {chunkSize: 10, total: 0}}); - await h.dom.waitForEl(container, 'section', {to: 'unmount'}); - - expect(await component.evaluate((ctx) => ctx.$refs.container.childElementCount === 0)).toBeTrue(); - }); - - it('renders new', async () => { - await setProps(); - - const - chunkSize = await component.evaluate((ctx) => ctx.requestParams.get.chunkSize); - - await h.dom.waitForEl(container, `section:nth-child(${chunkSize - 1})`); - expect(await getContainerChildCount()).toBe(chunkSize); - - await component.evaluate((ctx) => ctx.request = {get: {chunkSize: 4, total: 4, id: 'uniq-options'}}); - - await h.dom.waitForEl(container, 'section', {to: 'unmount'}); - await h.dom.waitForEl(container, 'section'); - - const - newChunkSize = await component.evaluate((ctx) => ctx.requestParams.get.chunkSize); - - await h.dom.waitForEl(container, `section:nth-child(${newChunkSize - 1})`); - expect(await getContainerChildCount()).toBe(newChunkSize); - }); - }); - - describe('by changing the `request` prop while second data batch loading is in progress', () => { - it('should render first chunk with correct data', async () => { - await component.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.chunkSize = 2; - ctx.request = {get: {chunkSize: 2, delay: 1500, id: Math.random()}}; - }); - - await h.dom.waitForEl(container, 'section'); - - await component.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.chunkSize = 2; - ctx.request = {get: {chunkSize: 2, i: 10, total: 2, delay: 1500, id: Math.random()}}; - }); - - expect(await h.dom.waitForEl(container, '[data-index="10"]')); - expect(await getContainerChildCount()).toBe(2); - }); - }); - - describe('by changing the `dataProvider` prop', () => { - it('removes old elements', async () => { - await setProps(); - - await component.evaluate((ctx) => ctx.dataProvider = undefined); - await h.dom.waitForEl(container, 'section', {to: 'unmount'}); - - expect(await component.evaluate((ctx) => ctx.$refs.container.childElementCount === 0)).toBeTrue(); - }); - - it('renders new', async () => { - await h.dom.waitForEl(container, 'section', {to: 'unmount'}); - expect(await component.evaluate((ctx) => ctx.$refs.container.childElementCount === 0)).toBeTrue(); - - await component.evaluate((ctx) => ctx.dataProvider = 'demo.Pagination'); - await h.dom.waitForEl(container, 'section'); - - expect(await getContainerChildCount()).toBeGreaterThan(0); - }); - }); - }); - - describe('with `items`', () => { - it('renders the first chunk', async () => { - const - chunkSize = await component.evaluate((ctx) => ctx.chunkSize); - - await component.evaluate((ctx) => ctx.items = Array.from(Array(40), (v, i) => ({i}))); - await h.dom.waitForEl(container, 'section'); - - expect(await getContainerChildCount()).toBe(chunkSize); - }); - - it('renders all available `items`', async () => { - await component.evaluate((ctx) => ctx.items = Array.from(Array(40), (v, i) => ({i}))); - await h.dom.waitForEl(container, 'section'); - - const - total = await component.evaluate((ctx) => ctx.items.length), - checkFn = async () => await getContainerChildCount() === total; - - await h.scroll.scrollToBottomWhile(page, checkFn, {timeout: 1e5}); - expect(await getContainerChildCount()).toBe(total); - }); - - it('does not render more than received data', async () => { - await component.evaluate((ctx) => ctx.items = Array.from(Array(40), (v, i) => ({i}))); - await h.dom.waitForEl(container, 'section'); - - const - total = await component.evaluate((ctx) => ctx.items.length), - checkFn = async () => await getContainerChildCount() === total; - - await h.scroll.scrollToBottomWhile(page, checkFn, {timeout: 1e5}); - expect(await getContainerChildCount()).toBe(total); - - await h.bom.waitForIdleCallback(page); - await h.scroll.scrollToBottom(page); - expect(await getContainerChildCount()).toBe(total); - }); - }); - - describe('without `items` and` dataProvider` specified', () => { - it('does not render anything', async () => { - expect(await component.evaluate((ctx) => ctx.items.length === 0)).toBeTrue(); - expect(await component.evaluate((ctx) => ctx.dataProvider === undefined)).toBeTrue(); - expect(await component.evaluate((ctx) => ctx.$refs.container.childElementCount === 0)).toBeTrue(); - }); - }); - }); -}; diff --git a/src/components/base/b-virtual-scroll/test/runners/slots/empty.js b/src/components/base/b-virtual-scroll/test/runners/slots/empty.js deleted file mode 100644 index 7cc73ee19a..0000000000 --- a/src/components/base/b-virtual-scroll/test/runners/slots/empty.js +++ /dev/null @@ -1,123 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -// @ts-check - -/** - * @typedef {import('playwright').Page} Page - */ - -const - h = include('tests/helpers').default; - -/** @param {Page} page */ -module.exports = (page) => { - const components = { - emptyWithSlot: undefined, - emptyNoSlot: undefined, - emptyWithData: undefined - }; - - const nodes = { - emptyWithSlot: undefined, - emptyNoSlot: undefined, - emptyWithData: undefined - }; - - beforeAll(async () => { - await page.evaluate(() => { - const baseAttrs = { - theme: 'demo', - option: 'section', - optionProps: ({current}) => ({'data-index': current.i}) - }; - - const slots = { - empty: { - tag: 'div', - attrs: { - id: 'empty', - 'data-test-ref': 'empty' - }, - content: 'Empty' - } - }; - - const scheme = [ - { - attrs: { - ...baseAttrs, - dataProvider: 'demo.Pagination', - dbConverter: ({data}) => ({data: data.splice(0, 4)}), - id: 'emptyNoSlot' - } - }, - { - attrs: { - ...baseAttrs, - dataProvider: 'demo.Pagination', - dbConverter: ({data}) => ({data: data.splice(0, 4)}), - request: {get: {chunkSize: 8, total: 8}}, - id: 'emptyWithData' - }, - - content: { - empty: slots.empty - } - }, - { - attrs: { - ...baseAttrs, - dataProvider: 'demo.Pagination', - dbConverter: () => ({data: []}), - id: 'emptyWithSlot' - }, - - content: { - empty: slots.empty - } - } - ]; - - globalThis.renderComponents('b-virtual-scroll', scheme); - }); - - await h.bom.waitForIdleCallback(page); - - for (let keys = Object.keys(components), i = 0; i < keys.length; i++) { - const key = keys[i]; - - nodes[key] = await h.dom.waitForEl(page, `#${key}`); - await nodes[key].evaluate((ctx) => ctx.style.display = ''); - - // eslint-disable-next-line require-atomic-updates - components[key] = await h.component.getComponentById(page, key); - } - }); - - describe('b-virtual-scroll slot empty', () => { - describe('does not render `empty slot`', () => { - it('if it is not set', async () => { - expect(await components.emptyNoSlot.evaluate((ctx) => Boolean(ctx.$slots['empty']))).toBe(false); - expect(await h.dom.getRef(nodes.emptyNoSlot, 'empty')).toBeFalsy(); - }); - - it('if there is data', async () => { - expect(await components.emptyWithData.evaluate((ctx) => Boolean(ctx.$slots['empty']))).toBe(true); - expect(await nodes.emptyWithData.waitForSelector('#empty', {state: 'hidden'})).toBeFalsy(); - }); - }); - - describe('render `empty slot`', () => { - it('if it is set and there is no data', async () => { - expect(await components.emptyWithSlot.evaluate((ctx) => Boolean(ctx.$slots['empty']))).toBe(true); - expect(await h.dom.getRef(nodes.emptyWithSlot, 'empty')).toBeTruthy(); - }); - }); - }); -}; diff --git a/src/components/base/b-virtual-scroll/test/runners/slots/render-next.js b/src/components/base/b-virtual-scroll/test/runners/slots/render-next.js deleted file mode 100644 index 8316b3ec40..0000000000 --- a/src/components/base/b-virtual-scroll/test/runners/slots/render-next.js +++ /dev/null @@ -1,498 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -/* eslint-disable max-lines-per-function */ -// @ts-check - -/** - * @typedef {import('playwright').Page} Page - */ - -const - h = include('tests/helpers').default; - -/** @param {Page} page */ -module.exports = (page) => { - const components = { - renderNextWithSlot: undefined, - renderNextNoSlot: undefined - }; - - const nodes = { - renderNextWithSlot: undefined, - renderNextNoSlot: undefined - }; - - const containers = { - renderNextWithSlot: undefined, - renderNextNoSlot: undefined - }; - - const isNotHidden = async (selector, ctx) => { - const - el = await ctx.$(selector), - state = await el.evaluate((ctx) => ctx.parentNode.style.display); - - return state === ''; - }; - - const - initialTimeout = globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL; - - beforeAll(() => { - globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL = (20).seconds(); - }); - - beforeEach(async () => { - await h.utils.reloadAndWaitForIdle(page); - await h.component.waitForComponent(page, '#root-component'); - - await page.evaluate(() => { - const dummy = document.querySelector('#dummy-component'); - - if (dummy) { - document.querySelector('#dummy-component').remove(); - } - - globalThis.removeCreatedComponents(); - - const baseAttrs = { - theme: 'demo', - option: 'section', - optionProps: ({current}) => ({'data-index': current.i}) - }; - - const slots = { - renderNext: { - tag: 'div', - attrs: { - id: 'renderNext', - 'data-test-ref': 'renderNext' - } - } - }; - - const scheme = [ - { - attrs: { - ...baseAttrs, - dataProvider: 'demo.Pagination', - loadStrategy: 'manual', - id: 'renderNextNoSlot' - } - }, - - { - attrs: { - ...baseAttrs, - dataProvider: 'demo.Pagination', - loadStrategy: 'manual', - id: 'renderNextWithSlot' - }, - - content: { - renderNext: slots.renderNext - } - } - ]; - - globalThis.renderComponents('b-virtual-scroll', scheme, '.p-v4-components-demo'); - }); - - await h.bom.waitForIdleCallback(page); - await h.component.waitForComponentStatus(page, '.b-virtual-scroll', 'ready'); - - const - allComponents = await page.$$('.b-virtual-scroll'); - - for (let i = 0; i < allComponents.length; i++) { - await allComponents[i].evaluate((ctx) => ctx.style.display = 'none'); - } - - for (let keys = Object.keys(components), i = 0; i < keys.length; i++) { - const key = keys[i]; - - nodes[key] = await h.dom.waitForEl(page, `#${key}`); - await nodes[key].evaluate((ctx) => ctx.style.display = ''); - containers[key] = await h.dom.waitForRef(nodes[key], 'container'); - - // eslint-disable-next-line require-atomic-updates - components[key] = await h.component.getComponentById(page, key); - } - - await components.renderNextWithSlot.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.request = {get: {}}; - ctx.shouldStopRequest = (v) => v.isLastEmpty; - - return new Promise((res) => { - if (ctx.isReady) { - return res(); - } - - ctx.localEmitter.on('localState.ready', res); - }); - }); - - await h.bom.waitForIdleCallback(page); - }); - - afterAll(() => { - globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL = initialTimeout; - }); - - describe('b-virtual-scroll `renderNext` slot', () => { - describe('not render', () => { - it('if it is not set', async () => { - expect(await components.renderNextNoSlot.evaluate((ctx) => Boolean(ctx.$slots['empty']))).toBe(false); - expect(await h.dom.getRef(nodes.renderNextNoSlot, 'empty')).toBeFalsy(); - }); - - it('there are no loaded data', async () => { - await components.renderNextWithSlot.evaluate((ctx) => { - ctx.dbConverter = () => ({data: []}); - ctx.request = {get: {total: 0, chunkSize: 0, id: Math.random()}}; - - return new Promise((res) => { - ctx.localEmitter.on('localState.ready', res); - }); - }); - - await h.bom.waitForIdleCallback(page); - await h.bom.waitForRAF(page); - - expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeFalse(); - }); - - it('there are no data', async () => { - await components.renderNextWithSlot.evaluate((ctx) => { - ctx.dataProvider = undefined; - ctx.items = []; - - return new Promise((res) => { - ctx.localEmitter.on('localState.ready', res); - }); - }); - - await h.bom.waitForRAF(page); - expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeFalse(); - }); - - it('if initial loading in progress', async () => { - await components.renderNextWithSlot.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.request = {get: {chunkSize: 10, sleep: 1000}}; - }); - - await h.bom.waitForIdleCallback(page); - await h.bom.waitForRAF(page); - - await expectAsync(page.waitForFunction(() => { - const - node = document.querySelector('#renderNext'), - parent = node.parentElement; - - return parent.style.display === 'none'; - })).toBeResolved(); - }); - - it('if the second batch of data loading in progress', async () => { - await components.renderNextWithSlot.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.request = {get: {total: 20, chunkSize: 10, id: Math.random(), sleep: 500}}; - ctx.chunkSize = 10; - }); - - await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); - await h.scroll.scrollToBottom(page); - await h.dom.waitForRef(nodes.renderNextWithSlot, 'renderNext'); - await components.renderNextWithSlot.evaluate((ctx) => ctx.renderNext()); - await h.bom.waitForRAF(page); - - expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeFalse(); - }); - - it('if all data were loaded after the initial request', async () => { - await components.renderNextWithSlot.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.request = {get: {total: 10, chunkSize: 10, id: Math.random()}}; - ctx.shouldStopRequest = () => true; - ctx.chunkSize = 10; - }); - - await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); - await h.bom.waitForIdleCallback(page); - - expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeFalse(); - }); - - it('if all data were loaded after the second batch load', async () => { - await components.renderNextWithSlot.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.request = {get: {total: 20, chunkSize: 10, id: Math.random(), sleep: 50}}; - ctx.shouldStopRequest = ({data}) => data.length === 20; - ctx.chunkSize = 10; - }); - - await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); - await h.dom.waitForRef(nodes.renderNextWithSlot, 'renderNext'); - await components.renderNextWithSlot.evaluate((ctx) => ctx.renderNext()); - - await h.dom.waitForEl(containers.renderNextWithSlot, 'section:nth-child(11)'); - await h.bom.waitForIdleCallback(page); - await h.bom.waitForRAF(page); - - expect(await (await nodes.renderNextWithSlot.$('#renderNext')).evaluate((ctx) => ctx.parentNode.style.display)).toBe('none'); - }); - - it('if all items were rendered', async () => { - await components.renderNextWithSlot.evaluate((ctx) => { - ctx.dataProvider = undefined; - ctx.chunkSize = 10; - // @ts-ignore - ctx.items = Array.from(Array(10), (v, i) => ({i})); - }); - - await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); - await h.bom.waitForRAF(page); - expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeFalse(); - }); - - it('if all items were rendered after second render', async () => { - await components.renderNextWithSlot.evaluate((ctx) => { - ctx.dataProvider = undefined; - ctx.chunkSize = 10; - ctx.items = Array.from(Array(20), (v, i) => ({i})); - }); - - await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); - await h.dom.waitForRef(nodes.renderNextWithSlot, 'renderNext'); - await components.renderNextWithSlot.evaluate((ctx) => ctx.renderNext()); - await h.dom.waitForEl(containers.renderNextWithSlot, 'section:nth-child(11)'); - await h.bom.waitForIdleCallback(page); - await h.bom.waitForRAF(page); - - expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeFalse(); - }); - - it('if all data were rendered and loaded', async () => { - await components.renderNextWithSlot.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.shouldStopRequest = ({data}) => data.length === 80; - ctx.request = {get: {total: 80, chunkSize: 40, id: Math.random()}}; - ctx.chunkSize = 20; - - return new Promise((res) => { - ctx.localEmitter.once('localState.ready', res); - }); - }); - - await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); - - let - renders = 1; - - const - totalRenders = 4; - - while (renders < totalRenders) { - await h.dom.waitForRef(nodes.renderNextWithSlot, 'renderNext'); - await components.renderNextWithSlot.evaluate((ctx) => ctx.renderNext()); - await h.dom.waitForEl(containers.renderNextWithSlot, `section:nth-child(${(renders * 20) + 1})`); - await h.bom.waitForIdleCallback(page); - await h.bom.waitForRAF(page); - - renders++; - } - - expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeFalse(); - }); - - it('if an error appears on the initial loading', async () => { - await components.renderNextWithSlot.evaluate((ctx) => { - const p = new Promise((res) => { - ctx.watch(':onRequestError', res); - }); - - ctx.dataProvider = 'demo.Pagination'; - ctx.request = {get: {total: 20, chunkSize: 10, id: Math.random(), failOn: 0, sleep: 500}}; - ctx.chunkSize = 10; - - return p; - }); - - await h.bom.waitForIdleCallback(page); - expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeFalse(); - }); - - it('if an error appears on the second data batch loading', async () => { - const requestErrorPromise = components.renderNextWithSlot.evaluate((ctx) => new Promise((res) => { - ctx.watch(':requestError', res); - })); - - await components.renderNextWithSlot.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.request = {get: {total: 40, chunkSize: 10, id: Math.random(), failOn: 1, sleep: 50}}; - ctx.chunkSize = 10; - }); - - await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); - await h.dom.waitForRef(nodes.renderNextWithSlot, 'renderNext'); - - await components.renderNextWithSlot.evaluate((ctx) => ctx.renderNext()); - await requestErrorPromise; - - await h.bom.waitForIdleCallback(page); - await h.bom.waitForRAF(page); - - expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeFalse(); - }); - }); - - describe('render', () => { - it('after initial loading', async () => { - await components.renderNextWithSlot.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.request = {get: {total: 20, chunkSize: 10, id: Math.random()}}; - ctx.chunkSize = 10; - - return new Promise((res) => { - ctx.localEmitter.on('localState.ready', res); - }); - }); - - await h.bom.waitForIdleCallback(page); - expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeTrue(); - }); - - it('after loading of the second data batch', async () => { - await components.renderNextWithSlot.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.request = {get: {total: 40, chunkSize: 10, id: Math.random()}}; - ctx.chunkSize = 10; - - return new Promise((res) => { - ctx.localEmitter.on('localState.ready', res); - }); - }); - - await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); - await h.dom.waitForRef(nodes.renderNextWithSlot, 'renderNext'); - await components.renderNextWithSlot.evaluate((ctx) => ctx.renderNext()); - await h.dom.waitForEl(containers.renderNextWithSlot, 'section:nth-child(11)'); - await h.bom.waitForIdleCallback(page); - await h.bom.waitForRAF(page); - - expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeTrue(); - }); - - it('after the initial rendering with items provided', async () => { - await components.renderNextWithSlot.evaluate((ctx) => { - ctx.dataProvider = undefined; - // @ts-ignore - ctx.items = Array.from(Array(20), (v, i) => ({i})); - }); - - await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); - await h.bom.waitForIdleCallback(page); - - expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeTrue(); - }); - - it('after the second rendering with items provided', async () => { - await components.renderNextWithSlot.evaluate((ctx) => { - ctx.dataProvider = undefined; - // @ts-ignore - ctx.items = Array.from(Array(40), (v, i) => ({i})); - }); - - await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); - await h.dom.waitForRef(nodes.renderNextWithSlot, 'renderNext'); - await components.renderNextWithSlot.evaluate((ctx) => ctx.renderNext()); - await h.dom.waitForEl(containers.renderNextWithSlot, 'section:nth-child(11)'); - await h.bom.waitForRAF(page); - - expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeTrue(); - }); - - it('until all data are rendered', async () => { - await components.renderNextWithSlot.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.shouldStopRequest = ({data}) => data.length === 60; - ctx.request = {get: {total: 60, chunkSize: 30, id: Math.random()}}; - ctx.chunkSize = 10; - - return new Promise((res) => { - ctx.localEmitter.on('localState.ready', res); - }); - }); - - await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); - - let - renders = 1; - - const - totalRenders = 5; - - while (renders < totalRenders) { - await h.dom.waitForRef(nodes.renderNextWithSlot, 'renderNext'); - await components.renderNextWithSlot.evaluate((ctx) => ctx.renderNext()); - await h.dom.waitForEl(containers.renderNextWithSlot, `section:nth-child(${(renders * 10) + 1})`); - await h.bom.waitForRAF(page); - - expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeTrue(); - renders++; - } - }); - - it('if there was an error on the initial loading, but after retrying all fine', async () => { - const requestErrorPromise = components.renderNextWithSlot.evaluate((ctx) => new Promise((res) => { - ctx.watch(':requestError', res); - })); - - await components.renderNextWithSlot.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.request = {get: {total: 40, chunkSize: 10, id: Math.random(), failOn: 0, failCount: 1, sleep: 50}}; - ctx.chunkSize = 10; - }); - - await requestErrorPromise; - await components.renderNextWithSlot.evaluate((ctx) => ctx.reloadLast()); - - await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); - await h.bom.waitForRAF(page); - - expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeTrue(); - }); - - it('if there was an error on the second data batch loading, but after retrying all fine', async () => { - const requestErrorPromise = components.renderNextWithSlot.evaluate((ctx) => new Promise((res) => { - ctx.watch(':requestError', res); - })); - - await components.renderNextWithSlot.evaluate((ctx) => { - ctx.dataProvider = 'demo.Pagination'; - ctx.request = {get: {total: 40, chunkSize: 10, id: Math.random(), failOn: 1, failCount: 1, sleep: 50}}; - ctx.chunkSize = 10; - }); - - await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); - - await components.renderNextWithSlot.evaluate((ctx) => ctx.renderNext()); - await requestErrorPromise; - await h.bom.waitForIdleCallback(page); - await components.renderNextWithSlot.evaluate((ctx) => ctx.reloadLast()); - await h.dom.waitForEl(containers.renderNextWithSlot, 'section:nth-child(11)'); - await h.bom.waitForRAF(page); - - expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeTrue(); - }); - }); - }); -}; diff --git a/src/components/base/b-virtual-scroll/test/unit/render.ts b/src/components/base/b-virtual-scroll/test/unit/render.ts deleted file mode 100644 index 86f2eb133c..0000000000 --- a/src/components/base/b-virtual-scroll/test/unit/render.ts +++ /dev/null @@ -1,139 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import test from 'tests/config/unit/test'; - -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; - -import Component from 'tests/helpers/component'; -import Scroll from 'tests/helpers/scroll'; -import BOM from 'tests/helpers/bom'; -import { interceptPaginationRequest } from 'tests/helpers/providers/pagination'; - -test.describe('b-virtual-scroll render', () => { - - const baseAttrs = { - theme: 'demo', - item: 'section', - id: 'target', - itemProps: ({current}) => ({'data-index': current.i}) - }; - - const providerProps = (reqParams = {}) => ({ - dataProvider: 'Provider', - chunkSize: 10, - request: {get: {chunkSize: 10, id: Math.random(), ...reqParams}} - }); - - const attrs = (attrs = {}) => ({attrs: { - ...baseAttrs, - ...attrs - }}); - - const - sectionSelector = '.b-virtual-scroll__container section', - buttonSelector = '.b-virtual-scroll__container button', - getContainerChildCount = (c) => c.evaluate((ctx) => ctx.$refs.container.childElementCount); - - test.beforeEach(async ({context, demoPage}) => { - await interceptPaginationRequest(context); - await demoPage.goto(); - }); - - test.describe('with `dataProvider`', () => { - test('renders the first chunk', async ({page}) => { - const - component = await Component.createComponent(page, 'b-virtual-scroll', attrs(providerProps())), - chunkSize = await component.evaluate((ctx) => ctx.chunkSize); - - await page.waitForSelector(sectionSelector, {state: 'attached'}); - test.expect(await getContainerChildCount(component)).toBe(chunkSize); - }); - - test('renders b-button', async ({page}) => { - const - component = await Component.createComponent(page, 'b-virtual-scroll', attrs({...providerProps(), item: 'b-button'})), - chunkSize = await component.evaluate((ctx) => ctx.chunkSize); - - await page.waitForSelector(buttonSelector, {state: 'attached'}); - test.expect(await getContainerChildCount(component)).toBe(chunkSize); - }); - - test('renders all available items', async ({page}) => { - const - component = await Component.createComponent(page, 'b-virtual-scroll', attrs(providerProps({total: 40}))); - - const - total = await component.evaluate((ctx) => ctx.field.get('requestParams.get.total')), - checkFn = async () => await getContainerChildCount(component) === total; - - await Scroll.scrollToBottomWhile(page, checkFn, {timeout: 1e5}); - - test.expect(await getContainerChildCount(component)).toBe(total); - }); - - test('does not render more than received data', async ({page}) => { - const - component = await Component.createComponent(page, 'b-virtual-scroll', attrs(providerProps({total: 40}))); - - const - total = await component.evaluate((ctx) => ctx.field.get('requestParams.get.total')), - checkFn = async () => await getContainerChildCount(component) === total; - - await Scroll.scrollToBottomWhile(page, checkFn, {timeout: 1e5}); - test.expect(await getContainerChildCount(component)).toBe(total); - - await BOM.waitForIdleCallback(page); - await Scroll.scrollToBottom(page); - test.expect(await getContainerChildCount(component)).toBe(total); - }); - - test('renders the first chunk with 3 requests to get the full chunk', async ({page}) => { - const - component = await Component.createComponent(page, 'b-virtual-scroll', attrs(providerProps({chunkSize: 4}))); - - const - chunkSize = await component.evaluate((ctx) => ctx.chunkSize); - - await page.waitForSelector(sectionSelector, {state: 'attached'}); - - test.expect(await getContainerChildCount(component)).toBe(chunkSize); - }); - - test('renders the first chunk with truncated data in all loaded chunks', async ({page}) => { - const component = await Component.createComponent(page, 'b-virtual-scroll', attrs({ - dataProvider: 'Provider', - chunkSize: 4, - request: {get: {chunkSize: 8, total: 32, id: 'uniq'}}, - dbConverter: ({data}) => ({data: data.splice(0, 1)}) - })); - - const - chunkSize = await component.evaluate((ctx) => ctx.chunkSize); - - await page.waitForSelector(sectionSelector, {state: 'attached'}); - - test.expect(await getContainerChildCount(component)).toBe(chunkSize); - }); - - test('renders all data if `shouldStopRequest` returns true', async ({page}) => { - const component = await Component.createComponent(page, 'b-virtual-scroll', attrs({ - dataProvider: 'Provider', - chunkSize: 10, - request: {get: {chunkSize: 40, total: 80, id: Math.random(), delay: 100}}, - shouldStopRequest: ({data}) => data.length === 80 - })); - - const - checkFn = async () => await getContainerChildCount(component) === 80; - - await Scroll.scrollToBottomWhile(page, checkFn, {timeout: 1e5}); - test.expect(await getContainerChildCount(component)).toBe(80); - }); - }); -}); diff --git a/src/components/pages/p-v4-components-demo/index.js b/src/components/pages/p-v4-components-demo/index.js index a83d8e692e..6fd7e315cd 100644 --- a/src/components/pages/p-v4-components-demo/index.js +++ b/src/components/pages/p-v4-components-demo/index.js @@ -17,7 +17,6 @@ package('p-v4-components-demo') 'b-list', 'b-tree', - 'b-virtual-scroll', 'b-window', 'b-scrolly', 'b-bottom-slide', From b0dff92bf2e3e8a1eb4fb601e4069c9c562c8036 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 28 Jun 2023 18:51:20 +0300 Subject: [PATCH 048/159] renamed b-scrolly into b-virtual-scroll --- components-lock.json | 40 ++----------------- .../base/b-scrolly/interface/index.ts | 12 ------ .../{b-scrolly => b-virtual-scroll}/README.md | 2 +- .../b-virtual-scroll.ss} | 0 .../b-virtual-scroll.styl} | 2 +- .../b-virtual-scroll.ts} | 34 ++++++++-------- .../{b-scrolly => b-virtual-scroll}/const.ts | 20 +++++----- .../handlers.ts | 28 ++++++------- .../{b-scrolly => b-virtual-scroll}/index.js | 2 +- .../interface/common.ts | 6 +-- .../interface/component.ts | 6 +-- .../interface/events.ts | 4 +- .../base/b-virtual-scroll/interface/index.ts | 12 ++++++ .../interface/requests.ts | 2 +- .../modules/emitter/index.ts | 10 ++--- .../modules/emitter/interface.ts | 2 +- .../modules/factory/engines/force-update.ts | 0 .../modules/factory/engines/vdom.ts | 4 +- .../modules/factory/index.ts | 14 +++---- .../modules/helpers/index.ts | 4 +- .../modules/observer/const.ts | 0 .../observer/engines/intersection-observer.ts | 12 +++--- .../modules/observer/engines/scroll.ts | 12 +++--- .../modules/observer/index.ts | 20 +++++----- .../modules/observer/interface.ts | 2 +- .../modules/slots/index.ts | 8 ++-- .../modules/slots/interface.ts | 2 +- .../modules/state/helpers.ts | 4 +- .../modules/state/index.ts | 12 +++--- .../{b-scrolly => b-virtual-scroll}/props.ts | 36 ++++++++--------- .../test/api/component-object/index.ts | 16 ++++---- .../test/api/component-object/styles.ts | 2 +- .../test/api/helpers/index.ts | 18 ++++----- .../test/api/helpers/interface.ts | 10 ++--- .../test/unit/functional/emitter/payload.ts | 15 +++---- .../test/unit/functional/rendering/default.ts | 14 +++---- .../functional/rendering/items-factory.ts | 16 ++++---- .../test/unit/functional/state/base.ts | 20 +++++----- .../test/unit/functional/state/emitter.ts | 12 +++--- .../initialization/initialization.ts | 16 ++++---- .../test/unit/lifecycle/slots/slots.ts | 9 +++-- .../test/unit/scenario/manual-rendering.ts | 18 ++++----- .../test/unit/scenario/props.ts | 14 +++---- .../test/unit/scenario/retry.ts | 16 ++++---- .../pages/p-v4-components-demo/index.js | 2 +- .../i-lock-page-scroll/test/unit/desktop.ts | 8 ++-- tests/helpers/component-object/mock.ts | 6 +-- 47 files changed, 246 insertions(+), 278 deletions(-) delete mode 100644 src/components/base/b-scrolly/interface/index.ts rename src/components/base/{b-scrolly => b-virtual-scroll}/README.md (98%) rename src/components/base/{b-scrolly/b-scrolly.ss => b-virtual-scroll/b-virtual-scroll.ss} (100%) rename src/components/base/{b-scrolly/b-scrolly.styl => b-virtual-scroll/b-virtual-scroll.styl} (87%) rename src/components/base/{b-scrolly/b-scrolly.ts => b-virtual-scroll/b-virtual-scroll.ts} (86%) rename src/components/base/{b-scrolly => b-virtual-scroll}/const.ts (79%) rename src/components/base/{b-scrolly => b-virtual-scroll}/handlers.ts (83%) rename src/components/base/{b-scrolly => b-virtual-scroll}/index.js (86%) rename src/components/base/{b-scrolly => b-virtual-scroll}/interface/common.ts (88%) rename src/components/base/{b-scrolly => b-virtual-scroll}/interface/component.ts (97%) rename src/components/base/{b-scrolly => b-virtual-scroll}/interface/events.ts (96%) create mode 100644 src/components/base/b-virtual-scroll/interface/index.ts rename src/components/base/{b-scrolly => b-virtual-scroll}/interface/requests.ts (86%) rename src/components/base/{b-scrolly => b-virtual-scroll}/modules/emitter/index.ts (74%) rename src/components/base/{b-scrolly => b-virtual-scroll}/modules/emitter/interface.ts (97%) rename src/components/base/{b-scrolly => b-virtual-scroll}/modules/factory/engines/force-update.ts (100%) rename src/components/base/{b-scrolly => b-virtual-scroll}/modules/factory/engines/vdom.ts (76%) rename src/components/base/{b-scrolly => b-virtual-scroll}/modules/factory/index.ts (84%) rename src/components/base/{b-scrolly => b-virtual-scroll}/modules/helpers/index.ts (80%) rename src/components/base/{b-scrolly => b-virtual-scroll}/modules/observer/const.ts (100%) rename src/components/base/{b-scrolly => b-virtual-scroll}/modules/observer/engines/intersection-observer.ts (63%) rename src/components/base/{b-scrolly => b-virtual-scroll}/modules/observer/engines/scroll.ts (54%) rename src/components/base/{b-scrolly => b-virtual-scroll}/modules/observer/index.ts (57%) rename src/components/base/{b-scrolly => b-virtual-scroll}/modules/observer/interface.ts (89%) rename src/components/base/{b-scrolly => b-virtual-scroll}/modules/slots/index.ts (92%) rename src/components/base/{b-scrolly => b-virtual-scroll}/modules/slots/interface.ts (79%) rename src/components/base/{b-scrolly => b-virtual-scroll}/modules/state/helpers.ts (85%) rename src/components/base/{b-scrolly => b-virtual-scroll}/modules/state/index.ts (91%) rename src/components/base/{b-scrolly => b-virtual-scroll}/props.ts (84%) rename src/components/base/{b-scrolly => b-virtual-scroll}/test/api/component-object/index.ts (90%) rename src/components/base/{b-scrolly => b-virtual-scroll}/test/api/component-object/styles.ts (96%) rename src/components/base/{b-scrolly => b-virtual-scroll}/test/api/helpers/index.ts (92%) rename src/components/base/{b-scrolly => b-virtual-scroll}/test/api/helpers/interface.ts (92%) rename src/components/base/{b-scrolly => b-virtual-scroll}/test/unit/functional/emitter/payload.ts (93%) rename src/components/base/{b-scrolly => b-virtual-scroll}/test/unit/functional/rendering/default.ts (89%) rename src/components/base/{b-scrolly => b-virtual-scroll}/test/unit/functional/rendering/items-factory.ts (92%) rename src/components/base/{b-scrolly => b-virtual-scroll}/test/unit/functional/state/base.ts (90%) rename src/components/base/{b-scrolly => b-virtual-scroll}/test/unit/functional/state/emitter.ts (95%) rename src/components/base/{b-scrolly => b-virtual-scroll}/test/unit/lifecycle/initialization/initialization.ts (90%) rename src/components/base/{b-scrolly => b-virtual-scroll}/test/unit/lifecycle/slots/slots.ts (96%) rename src/components/base/{b-scrolly => b-virtual-scroll}/test/unit/scenario/manual-rendering.ts (84%) rename src/components/base/{b-scrolly => b-virtual-scroll}/test/unit/scenario/props.ts (89%) rename src/components/base/{b-scrolly => b-virtual-scroll}/test/unit/scenario/retry.ts (90%) diff --git a/components-lock.json b/components-lock.json index cdfb691aa3..ea8415471d 100644 --- a/components-lock.json +++ b/components-lock.json @@ -1,5 +1,5 @@ { - "hash": "98b9e76f9769986d41b4840a4a7b9627fee82101970d254040a01748361df5d9", + "hash": "f276ed6ff1bb9ba1d1af98bc3a06e4c12b73c653879637c37d2936ad8f3d4de9", "data": { "%data": "%data:Map", "%data:Map": [ @@ -905,38 +905,6 @@ "etpl": null } ], - [ - "b-scrolly", - { - "index": "src/components/base/b-scrolly/index.js", - "declaration": { - "name": "b-scrolly", - "parent": "i-data", - "dependencies": [], - "libs": [] - }, - "name": "b-scrolly", - "parent": "i-data", - "dependencies": [], - "libs": [], - "resolvedLibs": { - "%data": "%data:Set", - "%data:Set": [] - }, - "resolvedOwnLibs": { - "%data": "%data:Set", - "%data:Set": [] - }, - "type": "block", - "mixin": false, - "logic": "src/components/base/b-scrolly/b-scrolly.ts", - "styles": [ - "src/components/base/b-scrolly/b-scrolly.styl" - ], - "tpl": "src/components/base/b-scrolly/b-scrolly.ss", - "etpl": null - } - ], [ "b-select", { @@ -2229,9 +2197,8 @@ "b-remote-provider", "b-list", "b-tree", - "b-virtual-scroll", "b-window", - "b-scrolly", + "b-virtual-scroll", "b-bottom-slide", "b-slider", "b-sidebar", @@ -2266,9 +2233,8 @@ "b-remote-provider", "b-list", "b-tree", - "b-virtual-scroll", "b-window", - "b-scrolly", + "b-virtual-scroll", "b-bottom-slide", "b-slider", "b-sidebar", diff --git a/src/components/base/b-scrolly/interface/index.ts b/src/components/base/b-scrolly/interface/index.ts deleted file mode 100644 index 6257135740..0000000000 --- a/src/components/base/b-scrolly/interface/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -export * from 'components/base/b-scrolly/interface/events'; -export * from 'components/base/b-scrolly/interface/component'; -export * from 'components/base/b-scrolly/interface/requests'; -export * from 'components/base/b-scrolly/interface/common'; diff --git a/src/components/base/b-scrolly/README.md b/src/components/base/b-virtual-scroll/README.md similarity index 98% rename from src/components/base/b-scrolly/README.md rename to src/components/base/b-virtual-scroll/README.md index daf710c43f..a03014ed89 100644 --- a/src/components/base/b-scrolly/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -12,7 +12,7 @@ - улучшить имена интерфейсов (MountedItem, MountedSeparator, ревью имен в ComponentState) - протыкать все метода на использование (удалить неиспользуемые) - улучшить имена тест кейсов -- ревью и рефакторинг src\components\base\b-scrolly\test\api\helpers\index.ts +- ревью и рефакторинг src\components\base\b-virtual-scroll\test\api\helpers\index.ts - dbChange - описать что состояние типа loadPage, renderPage обновляются постфактум (после того как загрузка данных случилась, после того как рендеринг случился) - ~~лишний вызов shouldStopRequestingData в onDataLoadSuccess~~ -> исправлено diff --git a/src/components/base/b-scrolly/b-scrolly.ss b/src/components/base/b-virtual-scroll/b-virtual-scroll.ss similarity index 100% rename from src/components/base/b-scrolly/b-scrolly.ss rename to src/components/base/b-virtual-scroll/b-virtual-scroll.ss diff --git a/src/components/base/b-scrolly/b-scrolly.styl b/src/components/base/b-virtual-scroll/b-virtual-scroll.styl similarity index 87% rename from src/components/base/b-scrolly/b-scrolly.styl rename to src/components/base/b-virtual-scroll/b-virtual-scroll.styl index 8e2d61e439..2f6b64db48 100644 --- a/src/components/base/b-scrolly/b-scrolly.styl +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.styl @@ -12,5 +12,5 @@ $p = { } -b-scrolly extends i-data +b-virtual-scroll extends i-data // ... diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts similarity index 86% rename from src/components/base/b-scrolly/b-scrolly.ts rename to src/components/base/b-virtual-scroll/b-virtual-scroll.ts index a4513c5e9f..dcea1cf388 100644 --- a/src/components/base/b-scrolly/b-scrolly.ts +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts @@ -7,34 +7,34 @@ */ /** - * [[include:components/base/b-scrolly/README.md]] + * [[include:components/base/b-virtual-scroll/README.md]] * @packageDocumentation */ import VDOM, { create, render } from 'components/friends/vdom'; -import { bScrollyDomInsertAsyncGroup, renderGuardRejectionReason } from 'components/base/b-scrolly/const'; +import { bVirtualScrollDomInsertAsyncGroup, renderGuardRejectionReason } from 'components/base/b-virtual-scroll/const'; import iData, { $$, component, RequestParams } from 'components/super/i-data/i-data'; -import { bScrollyHandlers } from 'components/base/b-scrolly/handlers'; +import { bVirtualScrollHandlers } from 'components/base/b-virtual-scroll/handlers'; import type { AsyncOptions } from 'core/async'; -import type { ComponentState, RenderGuardResult } from 'components/base/b-scrolly/interface'; +import type { ComponentState, RenderGuardResult } from 'components/base/b-virtual-scroll/interface'; -export * from 'components/base/b-scrolly/interface'; -export * from 'components/base/b-scrolly/const'; +export * from 'components/base/b-virtual-scroll/interface'; +export * from 'components/base/b-virtual-scroll/const'; VDOM.addToPrototype(create); VDOM.addToPrototype(render); /** * Component that implements loading and rendering of large data arrays in chunks. - * The `bScrolly` component extends the `iData` class and implements the `iItems` interface. + * The `bVirtualScroll` component extends the `iData` class and implements the `iItems` interface. * * It provides functionality for efficiently loading and displaying large amounts of data * by dynamically rendering chunks of data as the user scrolls. */ @component() -export default class bScrolly extends bScrollyHandlers { +export default class bVirtualScroll extends bVirtualScrollHandlers { // @ts-ignore (getter instead readonly) override get requestParams(): iData['requestParams'] { return { @@ -128,9 +128,9 @@ export default class bScrolly extends bScrollyHandlers { } /** - * Wrapper for {@link bScrolly.shouldStopRequestingData}. + * Wrapper for {@link bVirtualScroll.shouldStopRequestingData}. */ - shouldStopRequestingDataWrapper(this: bScrolly): boolean { + shouldStopRequestingDataWrapper(this: bVirtualScroll): boolean { const state = this.getComponentState(); if (state.isRequestsStopped) { @@ -144,9 +144,9 @@ export default class bScrolly extends bScrollyHandlers { } /** - * Wrapper for {@link bScrolly.shouldPerformDataRequest}. + * Wrapper for {@link bVirtualScroll.shouldPerformDataRequest}. */ - shouldPerformDataRequestWrapper(this: bScrolly): boolean { + shouldPerformDataRequestWrapper(this: bVirtualScroll): boolean { return this.shouldPerformDataRequest(this.getComponentState(), this); } @@ -255,7 +255,7 @@ export default class bScrolly extends bScrollyHandlers { /** * A function that performs actions (data loading/rendering) depending - * on the result of the {@link bScrolly.renderGuard} method. + * on the result of the {@link bVirtualScroll.renderGuard} method. * * This function is the "starting point" for rendering components and is called after successful data loading * or when rendered items enter the viewport. @@ -304,8 +304,8 @@ export default class bScrolly extends bScrollyHandlers { } /** - * Renders components using {@link bScrolly.componentFactory} and inserts them into the DOM tree. - * {@link bScrolly.componentFactory}, in turn, calls {@link bScrolly.itemsFactory} to obtain + * Renders components using {@link bVirtualScroll.componentFactory} and inserts them into the DOM tree. + * {@link bVirtualScroll.componentFactory}, in turn, calls {@link bVirtualScroll.itemsFactory} to obtain * the set of components to render. */ protected performRender(): void { @@ -324,7 +324,7 @@ export default class bScrolly extends bScrollyHandlers { for (let i = 0; i < nodes.length; i++) { this.dom.appendChild(fragment, nodes[i], { - group: bScrollyDomInsertAsyncGroup, + group: bVirtualScrollDomInsertAsyncGroup, destroyIfComponent: true }); } @@ -335,6 +335,6 @@ export default class bScrolly extends bScrollyHandlers { this.onDomInsertDone(); this.onRenderDone(); - }, {label: $$.insertDomRaf, group: bScrollyDomInsertAsyncGroup}); + }, {label: $$.insertDomRaf, group: bVirtualScrollDomInsertAsyncGroup}); } } diff --git a/src/components/base/b-scrolly/const.ts b/src/components/base/b-virtual-scroll/const.ts similarity index 79% rename from src/components/base/b-scrolly/const.ts rename to src/components/base/b-virtual-scroll/const.ts index 96c47fdc8b..1eec86084e 100644 --- a/src/components/base/b-scrolly/const.ts +++ b/src/components/base/b-virtual-scroll/const.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; import type { @@ -20,17 +20,17 @@ import type { ComponentStrategy, RenderGuardRejectionReason -} from 'components/base/b-scrolly/interface'; +} from 'components/base/b-virtual-scroll/interface'; /** * Base group for performing asynchronous operations of the component. */ -export const bScrollyAsyncGroup = 'b-scrolly'; +export const bVirtualScrollAsyncGroup = 'b-virtual-scroll'; /** * Group for asynchronous operations related to inserting nodes into the DOM tree. */ -export const bScrollyDomInsertAsyncGroup = `${bScrollyAsyncGroup}:dom-insert`; +export const bVirtualScrollDomInsertAsyncGroup = `${bVirtualScrollAsyncGroup}:dom-insert`; /** * {@link ComponentRenderStrategy} @@ -115,19 +115,19 @@ export const componentItemType: ComponentItemType = { }; export const defaultShouldProps = { - /** {@link bScrolly.shouldStopRequestingData} */ - shouldStopRequestingData: (state: ComponentState, _ctx: bScrolly): boolean => { + /** {@link bVirtualScroll.shouldStopRequestingData} */ + shouldStopRequestingData: (state: ComponentState, _ctx: bVirtualScroll): boolean => { const isLastRequestNotEmpty = () => state.lastLoadedData.length > 0; return !isLastRequestNotEmpty(); }, - /** {@link bScrolly.shouldPerformDataRequest} */ - shouldPerformDataRequest: (state: ComponentState, _ctx: bScrolly): boolean => { + /** {@link bVirtualScroll.shouldPerformDataRequest} */ + shouldPerformDataRequest: (state: ComponentState, _ctx: bVirtualScroll): boolean => { const isLastRequestNotEmpty = () => state.lastLoadedData.length > 0; return isLastRequestNotEmpty(); }, - /** {@link bScrolly.shouldPerformDataRender} */ - shouldPerformDataRender: (_state: ComponentState, _ctx: bScrolly): boolean => false + /** {@link bVirtualScroll.shouldPerformDataRender} */ + shouldPerformDataRender: (_state: ComponentState, _ctx: bVirtualScroll): boolean => false }; diff --git a/src/components/base/b-scrolly/handlers.ts b/src/components/base/b-virtual-scroll/handlers.ts similarity index 83% rename from src/components/base/b-scrolly/handlers.ts rename to src/components/base/b-virtual-scroll/handlers.ts index 3eb666f445..201e4dd62a 100644 --- a/src/components/base/b-scrolly/handlers.ts +++ b/src/components/base/b-virtual-scroll/handlers.ts @@ -6,19 +6,19 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import bScrollyProps from 'components/base/b-scrolly/props'; -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import { bScrollyAsyncGroup, componentEvents } from 'components/base/b-scrolly/const'; +import bVirtualScrollProps from 'components/base/b-virtual-scroll/props'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import { bVirtualScrollAsyncGroup, componentEvents } from 'components/base/b-virtual-scroll/const'; import iData, { component } from 'components/super/i-data/i-data'; -import type { MountedChild } from 'components/base/b-scrolly/interface'; -import { isAsyncReplaceError } from 'components/base/b-scrolly/modules/helpers'; +import type { MountedChild } from 'components/base/b-virtual-scroll/interface'; +import { isAsyncReplaceError } from 'components/base/b-virtual-scroll/modules/helpers'; /** - * A class that provides an API to handle events emitted by the {@link bScrolly} component. - * This class is designed to work in conjunction with {@link bScrolly}. + * A class that provides an API to handle events emitted by the {@link bVirtualScroll} component. + * This class is designed to work in conjunction with {@link bVirtualScroll}. */ @component() -export abstract class bScrollyHandlers extends bScrollyProps { +export abstract class bVirtualScrollHandlers extends bVirtualScrollProps { /** * Handler: component reset event. @@ -28,7 +28,7 @@ export abstract class bScrollyHandlers extends bScrollyProps { this.componentInternalState.reset(); this.observer.reset(); - this.async.clearAll({group: new RegExp(bScrollyAsyncGroup)}); + this.async.clearAll({group: new RegExp(bVirtualScrollAsyncGroup)}); this.componentEmitter.emit(componentEvents.resetState); } @@ -63,7 +63,7 @@ export abstract class bScrollyHandlers extends bScrollyProps { * * @param childList */ - onDomInsertStart(this: bScrolly, childList: MountedChild[]): void { + onDomInsertStart(this: bVirtualScroll, childList: MountedChild[]): void { this.componentInternalState.updateRenderCursor(); this.componentInternalState.updateMounted(childList); this.componentInternalState.setIsInitialRender(false); @@ -92,7 +92,7 @@ export abstract class bScrollyHandlers extends bScrollyProps { * Handler: lifecycle done event. * Triggered when the internal lifecycle of the component is completed. */ - onLifecycleDone(this: bScrolly): void { + onLifecycleDone(this: bVirtualScroll): void { const state = this.getComponentState(); @@ -136,7 +136,7 @@ export abstract class bScrollyHandlers extends bScrollyProps { * @param data - The loaded data. * @throws {@link ReferenceError} if the loaded data does not have a "data" field. */ - onDataLoadSuccess(this: bScrolly, isInitialLoading: boolean, data: unknown): void { + onDataLoadSuccess(this: bVirtualScroll, isInitialLoading: boolean, data: unknown): void { this.componentInternalState.setIsLoadingInProgress(false); if (!Object.isPlainObject(data) || !Array.isArray(data.data)) { @@ -180,7 +180,7 @@ export abstract class bScrollyHandlers extends bScrollyProps { this.componentEmitter.emit(componentEvents.dataLoadError, isInitialLoading); } - override onRequestError(this: bScrolly, ...args: Parameters): ReturnType { + override onRequestError(this: bVirtualScroll, ...args: Parameters): ReturnType { const err = args[0]; @@ -209,7 +209,7 @@ export abstract class bScrollyHandlers extends bScrollyProps { * Handler: component enters the viewport. * @param component - The component that enters the viewport. */ - onElementEnters(this: bScrolly, component: MountedChild): void { + onElementEnters(this: bVirtualScroll, component: MountedChild): void { this.componentInternalState.setMaxViewedIndex(component); this.loadDataOrPerformRender(); diff --git a/src/components/base/b-scrolly/index.js b/src/components/base/b-virtual-scroll/index.js similarity index 86% rename from src/components/base/b-scrolly/index.js rename to src/components/base/b-virtual-scroll/index.js index dc7adca297..302b437616 100644 --- a/src/components/base/b-scrolly/index.js +++ b/src/components/base/b-virtual-scroll/index.js @@ -6,5 +6,5 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -package('b-scrolly') +package('b-virtual-scroll') .extends('i-data'); diff --git a/src/components/base/b-scrolly/interface/common.ts b/src/components/base/b-virtual-scroll/interface/common.ts similarity index 88% rename from src/components/base/b-scrolly/interface/common.ts rename to src/components/base/b-virtual-scroll/interface/common.ts index 137eef6be0..cb762cca92 100644 --- a/src/components/base/b-scrolly/interface/common.ts +++ b/src/components/base/b-virtual-scroll/interface/common.ts @@ -6,8 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { ComponentState } from 'components/base/b-scrolly/interface/component'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { ComponentState } from 'components/base/b-virtual-scroll/interface/component'; /** * Interface representing the response of the client to the `renderGuard` method for rendering requests. @@ -73,5 +73,5 @@ export interface RenderGuardRejectionReason { * A function used to query the client about whether to perform a specific action or not. */ export interface ShouldPerform { - (state: ComponentState, ctx: bScrolly): RES; + (state: ComponentState, ctx: bVirtualScroll): RES; } diff --git a/src/components/base/b-scrolly/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts similarity index 97% rename from src/components/base/b-scrolly/interface/component.ts rename to src/components/base/b-virtual-scroll/interface/component.ts index 172d4e92b1..dbbe9f4788 100644 --- a/src/components/base/b-scrolly/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; /** * Render strategy for producing the components. @@ -312,8 +312,8 @@ export interface ComponentDb { } /** - * Typeof {@link bScrolly.itemsFactory}. + * Typeof {@link bVirtualScroll.itemsFactory}. */ export interface ComponentItemFactory { - (state: ComponentState, ctx: bScrolly): ComponentItem[]; + (state: ComponentState, ctx: bVirtualScroll): ComponentItem[]; } diff --git a/src/components/base/b-scrolly/interface/events.ts b/src/components/base/b-virtual-scroll/interface/events.ts similarity index 96% rename from src/components/base/b-scrolly/interface/events.ts rename to src/components/base/b-virtual-scroll/interface/events.ts index 71b6a52929..73c17698c4 100644 --- a/src/components/base/b-scrolly/interface/events.ts +++ b/src/components/base/b-virtual-scroll/interface/events.ts @@ -6,8 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { MountedChild } from 'components/base/b-scrolly/interface/component'; -import { componentDataLocalEvents, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents } from 'components/base/b-scrolly/const'; +import type { MountedChild } from 'components/base/b-virtual-scroll/interface/component'; +import { componentDataLocalEvents, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents } from 'components/base/b-virtual-scroll/const'; /** * Component data-related events (emitted in `localEmitter`). diff --git a/src/components/base/b-virtual-scroll/interface/index.ts b/src/components/base/b-virtual-scroll/interface/index.ts new file mode 100644 index 0000000000..9ea53371bd --- /dev/null +++ b/src/components/base/b-virtual-scroll/interface/index.ts @@ -0,0 +1,12 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export * from 'components/base/b-virtual-scroll/interface/events'; +export * from 'components/base/b-virtual-scroll/interface/component'; +export * from 'components/base/b-virtual-scroll/interface/requests'; +export * from 'components/base/b-virtual-scroll/interface/common'; diff --git a/src/components/base/b-scrolly/interface/requests.ts b/src/components/base/b-virtual-scroll/interface/requests.ts similarity index 86% rename from src/components/base/b-scrolly/interface/requests.ts rename to src/components/base/b-virtual-scroll/interface/requests.ts index ef4cadeff4..fdcf26ae66 100644 --- a/src/components/base/b-scrolly/interface/requests.ts +++ b/src/components/base/b-virtual-scroll/interface/requests.ts @@ -7,7 +7,7 @@ */ import type { CreateRequestOptions, RequestQuery } from 'components/super/i-data/i-data'; -import type { ComponentState } from 'components/base/b-scrolly/interface/component'; +import type { ComponentState } from 'components/base/b-virtual-scroll/interface/component'; /** * Function that returns the GET parameters for a request. diff --git a/src/components/base/b-scrolly/modules/emitter/index.ts b/src/components/base/b-virtual-scroll/modules/emitter/index.ts similarity index 74% rename from src/components/base/b-scrolly/modules/emitter/index.ts rename to src/components/base/b-virtual-scroll/modules/emitter/index.ts index aafe1bb0ff..56cec3a17f 100644 --- a/src/components/base/b-scrolly/modules/emitter/index.ts +++ b/src/components/base/b-virtual-scroll/modules/emitter/index.ts @@ -8,17 +8,17 @@ import type { AsyncOptions } from 'core/async'; -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { ComponentEvents, LocalEventPayload } from 'components/base/b-scrolly/interface'; -import type { ComponentTypedEmitter } from 'components/base/b-scrolly/modules/emitter/interface'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { ComponentEvents, LocalEventPayload } from 'components/base/b-virtual-scroll/interface'; +import type { ComponentTypedEmitter } from 'components/base/b-virtual-scroll/modules/emitter/interface'; -export * from 'components/base/b-scrolly/modules/emitter/interface'; +export * from 'components/base/b-virtual-scroll/modules/emitter/interface'; /** * Provides methods for interacting with the `selfEmitter` using typed events. * @param ctx */ -export function componentTypedEmitter(ctx: bScrolly): ComponentTypedEmitter { +export function componentTypedEmitter(ctx: bVirtualScroll): ComponentTypedEmitter { const once = ( event: EVENT, handler: (...args: LocalEventPayload) => void, diff --git a/src/components/base/b-scrolly/modules/emitter/interface.ts b/src/components/base/b-virtual-scroll/modules/emitter/interface.ts similarity index 97% rename from src/components/base/b-scrolly/modules/emitter/interface.ts rename to src/components/base/b-virtual-scroll/modules/emitter/interface.ts index 96e6c39099..45a3536974 100644 --- a/src/components/base/b-scrolly/modules/emitter/interface.ts +++ b/src/components/base/b-virtual-scroll/modules/emitter/interface.ts @@ -7,7 +7,7 @@ */ import type { AsyncOptions } from 'core/async'; -import type { ComponentEvents, LocalEventPayload } from 'components/base/b-scrolly/interface'; +import type { ComponentEvents, LocalEventPayload } from 'components/base/b-virtual-scroll/interface'; /** * An interface representing the typed `selfEmitter` methods. diff --git a/src/components/base/b-scrolly/modules/factory/engines/force-update.ts b/src/components/base/b-virtual-scroll/modules/factory/engines/force-update.ts similarity index 100% rename from src/components/base/b-scrolly/modules/factory/engines/force-update.ts rename to src/components/base/b-virtual-scroll/modules/factory/engines/force-update.ts diff --git a/src/components/base/b-scrolly/modules/factory/engines/vdom.ts b/src/components/base/b-virtual-scroll/modules/factory/engines/vdom.ts similarity index 76% rename from src/components/base/b-scrolly/modules/factory/engines/vdom.ts rename to src/components/base/b-virtual-scroll/modules/factory/engines/vdom.ts index 1365166737..0e8f3921a2 100644 --- a/src/components/base/b-scrolly/modules/factory/engines/vdom.ts +++ b/src/components/base/b-virtual-scroll/modules/factory/engines/vdom.ts @@ -7,7 +7,7 @@ */ import type { VNodeDescriptor } from 'components/friends/vdom'; -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; /** * Renders the provided `VNodes` to the `HTMLElements` via `vdom.render` API. @@ -15,7 +15,7 @@ import type bScrolly from 'components/base/b-scrolly/b-scrolly'; * @param ctx * @param items */ -export function render(ctx: bScrolly, items: VNodeDescriptor[]): HTMLElement[] { +export function render(ctx: bVirtualScroll, items: VNodeDescriptor[]): HTMLElement[] { const vnodes = ctx.vdom.create(...items), // https://github.com/vuejs/core/issues/6061 diff --git a/src/components/base/b-scrolly/modules/factory/index.ts b/src/components/base/b-virtual-scroll/modules/factory/index.ts similarity index 84% rename from src/components/base/b-scrolly/modules/factory/index.ts rename to src/components/base/b-virtual-scroll/modules/factory/index.ts index a7f944ca3e..2e32c3d911 100644 --- a/src/components/base/b-scrolly/modules/factory/index.ts +++ b/src/components/base/b-virtual-scroll/modules/factory/index.ts @@ -9,18 +9,18 @@ import Friend from 'components/friends/friend'; import type { VNodeDescriptor } from 'components/friends/vdom'; -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { ComponentItem, MountedChild, MountedItem } from 'components/base/b-scrolly/interface'; -import { componentItemType, componentRenderStrategy } from 'components/base/b-scrolly/const'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { ComponentItem, MountedChild, MountedItem } from 'components/base/b-virtual-scroll/interface'; +import { componentItemType, componentRenderStrategy } from 'components/base/b-virtual-scroll/const'; -import * as forceUpdate from 'components/base/b-scrolly/modules/factory/engines/force-update'; -import * as vdomRender from 'components/base/b-scrolly/modules/factory/engines/vdom'; +import * as forceUpdate from 'components/base/b-virtual-scroll/modules/factory/engines/force-update'; +import * as vdomRender from 'components/base/b-virtual-scroll/modules/factory/engines/vdom'; /** - * A friendly class that provides an API for component production, specifically tailored for the `bScrolly` class. + * A friendly class that provides an API for component production, specifically tailored for the `bVirtualScroll` class. */ export class ComponentFactory extends Friend { - override readonly C!: bScrolly; + override readonly C!: bVirtualScroll; /** * Produces component items based on the current state and context. diff --git a/src/components/base/b-scrolly/modules/helpers/index.ts b/src/components/base/b-virtual-scroll/modules/helpers/index.ts similarity index 80% rename from src/components/base/b-scrolly/modules/helpers/index.ts rename to src/components/base/b-virtual-scroll/modules/helpers/index.ts index ab9575bb7c..2ab2756b07 100644 --- a/src/components/base/b-scrolly/modules/helpers/index.ts +++ b/src/components/base/b-virtual-scroll/modules/helpers/index.ts @@ -6,8 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { componentItemType } from 'components/base/b-scrolly/const'; -import type { MountedItem } from 'components/base/b-scrolly/interface'; +import { componentItemType } from 'components/base/b-virtual-scroll/const'; +import type { MountedItem } from 'components/base/b-virtual-scroll/interface'; /** * Returns `true` if the value is of type `MountedItem`, otherwise `false`. diff --git a/src/components/base/b-scrolly/modules/observer/const.ts b/src/components/base/b-virtual-scroll/modules/observer/const.ts similarity index 100% rename from src/components/base/b-scrolly/modules/observer/const.ts rename to src/components/base/b-virtual-scroll/modules/observer/const.ts diff --git a/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts b/src/components/base/b-virtual-scroll/modules/observer/engines/intersection-observer.ts similarity index 63% rename from src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts rename to src/components/base/b-virtual-scroll/modules/observer/engines/intersection-observer.ts index b05014abf0..7937255afa 100644 --- a/src/components/base/b-scrolly/modules/observer/engines/intersection-observer.ts +++ b/src/components/base/b-virtual-scroll/modules/observer/engines/intersection-observer.ts @@ -6,18 +6,18 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { MountedChild } from 'components/base/b-scrolly/interface'; -import { observerAsyncGroup } from 'components/base/b-scrolly/modules/observer/const'; -import type { ObserverEngine } from 'components/base/b-scrolly/modules/observer/interface'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { MountedChild } from 'components/base/b-virtual-scroll/interface'; +import { observerAsyncGroup } from 'components/base/b-virtual-scroll/modules/observer/const'; +import type { ObserverEngine } from 'components/base/b-virtual-scroll/modules/observer/interface'; import Friend from 'components/friends/friend'; export default class IoObserver extends Friend implements ObserverEngine { /** - * {@link bScrolly} + * {@link bVirtualScroll} */ - override readonly C!: bScrolly; + override readonly C!: bVirtualScroll; /** * @inheritdoc diff --git a/src/components/base/b-scrolly/modules/observer/engines/scroll.ts b/src/components/base/b-virtual-scroll/modules/observer/engines/scroll.ts similarity index 54% rename from src/components/base/b-scrolly/modules/observer/engines/scroll.ts rename to src/components/base/b-virtual-scroll/modules/observer/engines/scroll.ts index 06ca908d2f..9307af79a0 100644 --- a/src/components/base/b-scrolly/modules/observer/engines/scroll.ts +++ b/src/components/base/b-virtual-scroll/modules/observer/engines/scroll.ts @@ -6,18 +6,18 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { MountedChild } from 'components/base/b-scrolly/interface'; -import { observerAsyncGroup } from 'components/base/b-scrolly/modules/observer/const'; -import type { ObserverEngine } from 'components/base/b-scrolly/modules/observer/interface'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { MountedChild } from 'components/base/b-virtual-scroll/interface'; +import { observerAsyncGroup } from 'components/base/b-virtual-scroll/modules/observer/const'; +import type { ObserverEngine } from 'components/base/b-virtual-scroll/modules/observer/interface'; import Friend from 'components/friends/friend'; export default class ScrollObserver extends Friend implements ObserverEngine { /** - * {@link bScrolly} + * {@link bVirtualScroll} */ - override readonly C!: bScrolly; + override readonly C!: bVirtualScroll; /** * @inheritdoc diff --git a/src/components/base/b-scrolly/modules/observer/index.ts b/src/components/base/b-virtual-scroll/modules/observer/index.ts similarity index 57% rename from src/components/base/b-scrolly/modules/observer/index.ts rename to src/components/base/b-virtual-scroll/modules/observer/index.ts index 58b267e0f6..585019fed1 100644 --- a/src/components/base/b-scrolly/modules/observer/index.ts +++ b/src/components/base/b-virtual-scroll/modules/observer/index.ts @@ -6,21 +6,21 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { MountedChild } from 'components/base/b-scrolly/interface'; -import ScrollObserver from 'components/base/b-scrolly/modules/observer/engines/scroll'; -import IoObserver from 'components/base/b-scrolly/modules/observer/engines/intersection-observer'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { MountedChild } from 'components/base/b-virtual-scroll/interface'; +import ScrollObserver from 'components/base/b-virtual-scroll/modules/observer/engines/scroll'; +import IoObserver from 'components/base/b-virtual-scroll/modules/observer/engines/intersection-observer'; import Friend from 'components/friends/friend'; -export { default as IoObserver } from 'components/base/b-scrolly/modules/observer/engines/intersection-observer'; -export { default as ScrollObserver } from 'components/base/b-scrolly/modules/observer/engines/scroll'; +export { default as IoObserver } from 'components/base/b-virtual-scroll/modules/observer/engines/intersection-observer'; +export { default as ScrollObserver } from 'components/base/b-virtual-scroll/modules/observer/engines/scroll'; /** - * Observer class for `bScrolly` component. + * Observer class for `bVirtualScroll` component. * It provides observation capabilities using different engines such as IoObserver and ScrollObserver. */ export class Observer extends Friend { - override readonly C!: bScrolly; + override readonly C!: bVirtualScroll; /** * The observation engine used by the Observer. @@ -29,9 +29,9 @@ export class Observer extends Friend { protected engine: IoObserver | ScrollObserver; /** - * @param ctx - The `bScrolly` component instance. + * @param ctx - The `bVirtualScroll` component instance. */ - constructor(ctx: bScrolly) { + constructor(ctx: bVirtualScroll) { super(ctx); this.engine = ctx.componentStrategy === 'intersectionObserver' ? diff --git a/src/components/base/b-scrolly/modules/observer/interface.ts b/src/components/base/b-virtual-scroll/modules/observer/interface.ts similarity index 89% rename from src/components/base/b-scrolly/modules/observer/interface.ts rename to src/components/base/b-virtual-scroll/modules/observer/interface.ts index 0e7b06f5b1..f5f3a248dc 100644 --- a/src/components/base/b-scrolly/modules/observer/interface.ts +++ b/src/components/base/b-virtual-scroll/modules/observer/interface.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { MountedItem } from 'components/base/b-scrolly/interface'; +import type { MountedItem } from 'components/base/b-virtual-scroll/interface'; /** * Interface representing an observer engine for watching components entering the viewport. diff --git a/src/components/base/b-scrolly/modules/slots/index.ts b/src/components/base/b-virtual-scroll/modules/slots/index.ts similarity index 92% rename from src/components/base/b-scrolly/modules/slots/index.ts rename to src/components/base/b-virtual-scroll/modules/slots/index.ts index 56c017b965..126f9c7bfc 100644 --- a/src/components/base/b-scrolly/modules/slots/index.ts +++ b/src/components/base/b-virtual-scroll/modules/slots/index.ts @@ -10,10 +10,10 @@ import symbolGenerator from 'core/symbol'; import type { AsyncOptions } from 'core/async'; import Friend from 'components/friends/friend'; -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { SlotsStateObj } from 'components/base/b-scrolly/modules/slots/interface'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { SlotsStateObj } from 'components/base/b-virtual-scroll/modules/slots/interface'; -export * from 'components/base/b-scrolly/modules/slots/interface'; +export * from 'components/base/b-virtual-scroll/modules/slots/interface'; export const $$ = symbolGenerator(), @@ -24,7 +24,7 @@ export const */ export class SlotsStateController extends Friend { - override readonly C!: bScrolly; + override readonly C!: bVirtualScroll; /** * Options for the asynchronous operations. diff --git a/src/components/base/b-scrolly/modules/slots/interface.ts b/src/components/base/b-virtual-scroll/modules/slots/interface.ts similarity index 79% rename from src/components/base/b-scrolly/modules/slots/interface.ts rename to src/components/base/b-virtual-scroll/modules/slots/interface.ts index 9b47834373..b5599dd4a2 100644 --- a/src/components/base/b-scrolly/modules/slots/interface.ts +++ b/src/components/base/b-virtual-scroll/modules/slots/interface.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { ComponentRefs } from 'components/base/b-scrolly/interface'; +import type { ComponentRefs } from 'components/base/b-virtual-scroll/interface'; /** * Represents the state of slots. diff --git a/src/components/base/b-scrolly/modules/state/helpers.ts b/src/components/base/b-virtual-scroll/modules/state/helpers.ts similarity index 85% rename from src/components/base/b-scrolly/modules/state/helpers.ts rename to src/components/base/b-virtual-scroll/modules/state/helpers.ts index 746d515927..11c53a0469 100644 --- a/src/components/base/b-scrolly/modules/state/helpers.ts +++ b/src/components/base/b-virtual-scroll/modules/state/helpers.ts @@ -6,8 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { ComponentState } from 'components/base/b-scrolly/b-scrolly'; -import type { PrivateComponentState } from 'components/base/b-scrolly/interface'; +import type { ComponentState } from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { PrivateComponentState } from 'components/base/b-virtual-scroll/interface'; /** * Creates an initial state object for a component. diff --git a/src/components/base/b-scrolly/modules/state/index.ts b/src/components/base/b-virtual-scroll/modules/state/index.ts similarity index 91% rename from src/components/base/b-scrolly/modules/state/index.ts rename to src/components/base/b-virtual-scroll/modules/state/index.ts index e8ee55facc..8941f77d0d 100644 --- a/src/components/base/b-scrolly/modules/state/index.ts +++ b/src/components/base/b-virtual-scroll/modules/state/index.ts @@ -6,17 +6,17 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { MountedChild, ComponentState, MountedItem, PrivateComponentState } from 'components/base/b-scrolly/interface'; -import { isItem } from 'components/base/b-scrolly/modules/helpers'; -import { createInitialState, createPrivateInitialState } from 'components/base/b-scrolly/modules/state/helpers'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { MountedChild, ComponentState, MountedItem, PrivateComponentState } from 'components/base/b-virtual-scroll/interface'; +import { isItem } from 'components/base/b-virtual-scroll/modules/helpers'; +import { createInitialState, createPrivateInitialState } from 'components/base/b-virtual-scroll/modules/state/helpers'; import Friend from 'components/friends/friend'; /** - * Friendly to the `bScrolly` class that represents the internal state of a component. + * Friendly to the `bVirtualScroll` class that represents the internal state of a component. */ export class ComponentInternalState extends Friend { - override readonly C!: bScrolly; + override readonly C!: bVirtualScroll; /** * Current state of the component. diff --git a/src/components/base/b-scrolly/props.ts b/src/components/base/b-virtual-scroll/props.ts similarity index 84% rename from src/components/base/b-scrolly/props.ts rename to src/components/base/b-virtual-scroll/props.ts index 2ffb44005f..572a145be2 100644 --- a/src/components/base/b-scrolly/props.ts +++ b/src/components/base/b-virtual-scroll/props.ts @@ -21,7 +21,7 @@ import type { ComponentStrategy, ComponentRefs -} from 'components/base/b-scrolly/interface'; +} from 'components/base/b-virtual-scroll/interface'; import { @@ -30,21 +30,21 @@ import { componentItemType, componentStrategy -} from 'components/base/b-scrolly/const'; +} from 'components/base/b-virtual-scroll/const'; import iData, { component, prop, system } from 'components/super/i-data/i-data'; -import { ComponentTypedEmitter, componentTypedEmitter } from 'components/base/b-scrolly/modules/emitter'; -import { SlotsStateController } from 'components/base/b-scrolly/modules/slots'; -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import { ComponentFactory } from 'components/base/b-scrolly/modules/factory'; -import { Observer } from 'components/base/b-scrolly/modules/observer'; -import { ComponentInternalState } from 'components/base/b-scrolly/modules/state'; +import { ComponentTypedEmitter, componentTypedEmitter } from 'components/base/b-virtual-scroll/modules/emitter'; +import { SlotsStateController } from 'components/base/b-virtual-scroll/modules/slots'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import { ComponentFactory } from 'components/base/b-virtual-scroll/modules/factory'; +import { Observer } from 'components/base/b-virtual-scroll/modules/observer'; +import { ComponentInternalState } from 'components/base/b-virtual-scroll/modules/state'; /** - * A class that is friendly to {@link bScrolly}. It contains the properties of the {@link bScrolly} component. + * A class that is friendly to {@link bVirtualScroll}. It contains the properties of the {@link bVirtualScroll} component. */ @component() -export default abstract class bScrollyProps extends iData implements iItems { +export default abstract class bVirtualScrollProps extends iData implements iItems { /** {@link iItems.item} */ readonly Item!: object; @@ -112,7 +112,7 @@ export default abstract class bScrollyProps extends iData implements iItems { */ @prop({ type: Function, - default: (state: ComponentState, ctx: bScrolly) => { + default: (state: ComponentState, ctx: bVirtualScroll) => { if (ctx.chunkSize == null) { throw new Error('"chunkSize.getNextDataSlice" is used but "chunkSize" prop is not set.'); } @@ -185,7 +185,7 @@ export default abstract class bScrollyProps extends iData implements iItems { * ``` * * It's important to note that this prop is used by default, but it can be ignored and you can return - * any amount of data to render by setting a custom {@link bScrolly.itemsFactory} for the component. + * any amount of data to render by setting a custom {@link bVirtualScroll.itemsFactory} for the component. */ @prop({type: [Number, Function]}) readonly chunkSize?: number | ShouldPerform = 10; @@ -213,7 +213,7 @@ export default abstract class bScrollyProps extends iData implements iItems { readonly shouldPerformDataRequest!: ShouldPerform; /** - * This function is called in the {@link bScrolly.renderGuard} after other checks are completed. + * This function is called in the {@link bVirtualScroll.renderGuard} after other checks are completed. * * This function receives the component state as input, based on which the client * should determine whether the component should render the next chunk of components. @@ -241,23 +241,23 @@ export default abstract class bScrollyProps extends iData implements iItems { readonly disableObserver: boolean = false; /** {@link componentTypedEmitter} */ - @system((ctx) => componentTypedEmitter(ctx)) + @system((ctx) => componentTypedEmitter(ctx)) readonly componentEmitter!: ComponentTypedEmitter; /** {@link SlotsStateController} */ - @system((ctx) => new SlotsStateController(ctx)) + @system((ctx) => new SlotsStateController(ctx)) readonly slotsStateController!: SlotsStateController; /** {@link ComponentInternalState} */ - @system((ctx) => new ComponentInternalState(ctx)) + @system((ctx) => new ComponentInternalState(ctx)) readonly componentInternalState!: ComponentInternalState; /** {@link ComponentFactory} */ - @system((ctx) => new ComponentFactory(ctx)) + @system((ctx) => new ComponentFactory(ctx)) readonly componentFactory!: ComponentFactory; /** {@link Observer} */ - @system((ctx) => new Observer(ctx)) + @system((ctx) => new Observer(ctx)) readonly observer!: Observer; protected override readonly $refs!: iData['$refs'] & ComponentRefs; diff --git a/src/components/base/b-scrolly/test/api/component-object/index.ts b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts similarity index 90% rename from src/components/base/b-scrolly/test/api/component-object/index.ts rename to src/components/base/b-virtual-scroll/test/api/component-object/index.ts index 1f01111303..fc15c0c30f 100644 --- a/src/components/base/b-scrolly/test/api/component-object/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts @@ -10,16 +10,16 @@ import type { JSHandle, Locator, Page } from 'playwright'; import { ComponentObject, Scroll } from 'tests/helpers'; -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import type { ComponentRefs, ComponentState } from 'components/base/b-scrolly/b-scrolly'; -import type { SlotsStateObj } from 'components/base/b-scrolly/modules/slots'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { ComponentRefs, ComponentState } from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { SlotsStateObj } from 'components/base/b-virtual-scroll/modules/slots'; -import { testStyles } from 'components/base/b-scrolly/test/api/component-object/styles'; +import { testStyles } from 'components/base/b-virtual-scroll/test/api/component-object/styles'; /** - * The component object API for testing the {@link bScrolly} component. + * The component object API for testing the {@link bVirtualScroll} component. */ -export class ScrollyComponentObject extends ComponentObject { +export class VirtualScrollComponentObject extends ComponentObject { /** * The locator for the container ref. */ @@ -34,7 +34,7 @@ export class ScrollyComponentObject extends ComponentObject { * @param page - The Playwright page instance. */ constructor(page: Page) { - super(page, 'b-scrolly'); + super(page, 'b-virtual-scroll'); this.container = this.node.locator(this.elSelector('container')); this.childList = this.container.locator('> *'); @@ -45,7 +45,7 @@ export class ScrollyComponentObject extends ComponentObject { * * @param args - The arguments for the build method. */ - override async build(...args: Parameters['build']>): Promise> { + override async build(...args: Parameters['build']>): Promise> { await this.page.addStyleTag({content: testStyles}); return super.build(...args); } diff --git a/src/components/base/b-scrolly/test/api/component-object/styles.ts b/src/components/base/b-virtual-scroll/test/api/component-object/styles.ts similarity index 96% rename from src/components/base/b-scrolly/test/api/component-object/styles.ts rename to src/components/base/b-virtual-scroll/test/api/component-object/styles.ts index 5f861d58a6..7a7da255dc 100644 --- a/src/components/base/b-scrolly/test/api/component-object/styles.ts +++ b/src/components/base/b-virtual-scroll/test/api/component-object/styles.ts @@ -18,7 +18,7 @@ export const testStyles = ` content: attr(data-index); } -.b-scrolly__container { +.b-virtual-scroll__container { min-width: 20px; min-height: 20px; } diff --git a/src/components/base/b-scrolly/test/api/helpers/index.ts b/src/components/base/b-virtual-scroll/test/api/helpers/index.ts similarity index 92% rename from src/components/base/b-scrolly/test/api/helpers/index.ts rename to src/components/base/b-virtual-scroll/test/api/helpers/index.ts index 2678b088f6..3868260953 100644 --- a/src/components/base/b-scrolly/test/api/helpers/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/helpers/index.ts @@ -9,23 +9,23 @@ import type { Page } from 'playwright'; import test from 'tests/config/unit/test'; -import type { MountedChild, ComponentItem, ComponentState, MountedItem } from 'components/base/b-scrolly/interface'; +import type { MountedChild, ComponentItem, ComponentState, MountedItem } from 'components/base/b-virtual-scroll/interface'; import { paginationHandler } from 'tests/helpers/providers/pagination'; -import { ScrollyComponentObject } from 'components/base/b-scrolly/test/api/component-object'; +import { VirtualScrollComponentObject } from 'components/base/b-virtual-scroll/test/api/component-object'; import { RequestInterceptor } from 'tests/helpers/providers/interceptor'; -import { componentEvents, componentObserverLocalEvents } from 'components/base/b-scrolly/const'; -import type { DataConveyor, DataItemCtor, MountedItemCtor, StateApi, ScrollyTestHelpers, MountedSeparatorCtor, IndexedObj } from 'components/base/b-scrolly/test/api/helpers/interface'; -import { createInitialState as createInitialStateObj } from 'components/base/b-scrolly/modules/state/helpers'; +import { componentEvents, componentObserverLocalEvents } from 'components/base/b-virtual-scroll/const'; +import type { DataConveyor, DataItemCtor, MountedItemCtor, StateApi, VirtualScrollTestHelpers, MountedSeparatorCtor, IndexedObj } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; +import { createInitialState as createInitialStateObj } from 'components/base/b-virtual-scroll/modules/state/helpers'; -export * from 'components/base/b-scrolly/test/api/component-object'; +export * from 'components/base/b-virtual-scroll/test/api/component-object'; /** - * Creates a helper API for convenient testing of the `b-scrolly` component. + * Creates a helper API for convenient testing of the `b-virtual-scroll` component. * @param page - The page object representing the testing page. */ -export async function createTestHelpers(page: Page): Promise { +export async function createTestHelpers(page: Page): Promise { const - component = new ScrollyComponentObject(page), + component = new VirtualScrollComponentObject(page), initLoadSpy = await component.spyOn('initLoad', {proto: true}), provider = new RequestInterceptor(page, /api/), state = createStateApi({}, createDataConveyor( diff --git a/src/components/base/b-scrolly/test/api/helpers/interface.ts b/src/components/base/b-virtual-scroll/test/api/helpers/interface.ts similarity index 92% rename from src/components/base/b-scrolly/test/api/helpers/interface.ts rename to src/components/base/b-virtual-scroll/test/api/helpers/interface.ts index 2b49e012c3..904ad481be 100644 --- a/src/components/base/b-scrolly/test/api/helpers/interface.ts +++ b/src/components/base/b-virtual-scroll/test/api/helpers/interface.ts @@ -6,8 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { ComponentItem, ComponentState, MountedChild, MountedItem } from 'components/base/b-scrolly/interface'; -import type { ScrollyComponentObject } from 'components/base/b-scrolly/test/api/component-object'; +import type { ComponentItem, ComponentState, MountedChild, MountedItem } from 'components/base/b-virtual-scroll/interface'; +import type { VirtualScrollComponentObject } from 'components/base/b-virtual-scroll/test/api/component-object'; import type { SpyObject } from 'tests/helpers/mock/interface'; import type { RequestInterceptor } from 'tests/helpers/providers/interceptor'; @@ -121,11 +121,11 @@ export interface StateApi { /** * Helpers returned by the `createTestHelpers` function. */ -export interface ScrollyTestHelpers { +export interface VirtualScrollTestHelpers { /** - * The component object representing the `bScrolly` component. + * The component object representing the `bVirtualScroll` component. */ - component: ScrollyComponentObject; + component: VirtualScrollComponentObject; /** * The spy object for the `initLoad` function. diff --git a/src/components/base/b-scrolly/test/unit/functional/emitter/payload.ts b/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts similarity index 93% rename from src/components/base/b-scrolly/test/unit/functional/emitter/payload.ts rename to src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts index e4b33c23af..9343a181c3 100644 --- a/src/components/base/b-scrolly/test/unit/functional/emitter/payload.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts @@ -7,19 +7,20 @@ */ /** - * @file This file contains test cases to verify the functionality of events emitter in the `b-scrolly` component. + * @file This file contains test cases to verify the functionality of events emitter in the + * `b-virtual-scroll` component. */ import test from 'tests/config/unit/test'; -import { createTestHelpers, filterEmitterCalls } from 'components/base/b-scrolly/test/api/helpers'; -import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; +import { createTestHelpers, filterEmitterCalls } from 'components/base/b-virtual-scroll/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; -test.describe(' emitter', () => { +test.describe(' emitter', () => { let - component: ScrollyTestHelpers['component'], - provider: ScrollyTestHelpers['provider'], - state: ScrollyTestHelpers['state']; + component: VirtualScrollTestHelpers['component'], + provider: VirtualScrollTestHelpers['provider'], + state: VirtualScrollTestHelpers['state']; test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); diff --git a/src/components/base/b-scrolly/test/unit/functional/rendering/default.ts b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts similarity index 89% rename from src/components/base/b-scrolly/test/unit/functional/rendering/default.ts rename to src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts index b7f7477ea7..d87c7bbdf1 100644 --- a/src/components/base/b-scrolly/test/unit/functional/rendering/default.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts @@ -12,15 +12,15 @@ import test from 'tests/config/unit/test'; -import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; -import type { ComponentState, ShouldPerform } from 'components/base/b-scrolly/interface'; -import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; +import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; +import type { ComponentState, ShouldPerform } from 'components/base/b-virtual-scroll/interface'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; -test.describe('', () => { +test.describe('', () => { let - component: ScrollyTestHelpers['component'], - provider: ScrollyTestHelpers['provider'], - state: ScrollyTestHelpers['state']; + component: VirtualScrollTestHelpers['component'], + provider: VirtualScrollTestHelpers['provider'], + state: VirtualScrollTestHelpers['state']; test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); diff --git a/src/components/base/b-scrolly/test/unit/functional/rendering/items-factory.ts b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts similarity index 92% rename from src/components/base/b-scrolly/test/unit/functional/rendering/items-factory.ts rename to src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts index 676f8447d2..a17c04e014 100644 --- a/src/components/base/b-scrolly/test/unit/functional/rendering/items-factory.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts @@ -12,16 +12,16 @@ import test from 'tests/config/unit/test'; -import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; -import type { ComponentItemFactory } from 'components/base/b-scrolly/b-scrolly'; -import type { ComponentItem, ShouldPerform } from 'components/base/b-scrolly/interface'; -import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; +import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; +import type { ComponentItemFactory } from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { ComponentItem, ShouldPerform } from 'components/base/b-virtual-scroll/interface'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; -test.describe(' rendering via component factory', () => { +test.describe(' rendering via component factory', () => { let - component: ScrollyTestHelpers['component'], - provider: ScrollyTestHelpers['provider'], - state: ScrollyTestHelpers['state']; + component: VirtualScrollTestHelpers['component'], + provider: VirtualScrollTestHelpers['provider'], + state: VirtualScrollTestHelpers['state']; test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); diff --git a/src/components/base/b-scrolly/test/unit/functional/state/base.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/base.ts similarity index 90% rename from src/components/base/b-scrolly/test/unit/functional/state/base.ts rename to src/components/base/b-virtual-scroll/test/unit/functional/state/base.ts index e8d4a51268..b2ddf87a02 100644 --- a/src/components/base/b-scrolly/test/unit/functional/state/base.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/base.ts @@ -12,17 +12,17 @@ import test from 'tests/config/unit/test'; -import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; -import { defaultShouldProps } from 'components/base/b-scrolly/const'; -import type { ComponentItem, ShouldPerform } from 'components/base/b-scrolly/b-scrolly'; -import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; +import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import { defaultShouldProps } from 'components/base/b-virtual-scroll/const'; +import type { ComponentItem, ShouldPerform } from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; -test.describe('', () => { +test.describe('', () => { let - component: ScrollyTestHelpers['component'], - provider: ScrollyTestHelpers['provider'], - state: ScrollyTestHelpers['state']; + component: VirtualScrollTestHelpers['component'], + provider: VirtualScrollTestHelpers['provider'], + state: VirtualScrollTestHelpers['state']; test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); @@ -34,7 +34,7 @@ test.describe('', () => { test('Initial state', async () => { const chunkSize = 12, - mockFn = await component.mockFn((ctx: bScrolly) => ctx.getComponentState()); + mockFn = await component.mockFn((ctx: bVirtualScroll) => ctx.getComponentState()); provider.response(200, {data: []}, {delay: (10).seconds()}); diff --git a/src/components/base/b-scrolly/test/unit/functional/state/emitter.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts similarity index 95% rename from src/components/base/b-scrolly/test/unit/functional/state/emitter.ts rename to src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts index 14f2c4e086..e71f6a056e 100644 --- a/src/components/base/b-scrolly/test/unit/functional/state/emitter.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts @@ -12,14 +12,14 @@ import test from 'tests/config/unit/test'; -import { createTestHelpers, filterEmitterResults } from 'components/base/b-scrolly/test/api/helpers'; -import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; +import { createTestHelpers, filterEmitterResults } from 'components/base/b-virtual-scroll/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; -test.describe('', () => { +test.describe('', () => { let - component: ScrollyTestHelpers['component'], - provider: ScrollyTestHelpers['provider'], - state: ScrollyTestHelpers['state']; + component: VirtualScrollTestHelpers['component'], + provider: VirtualScrollTestHelpers['provider'], + state: VirtualScrollTestHelpers['state']; const initialStateFields = { itemsTillEnd: undefined, diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts b/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts similarity index 90% rename from src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts rename to src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts index 092bb0f92a..2a7d0a8879 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/initialization/initialization.ts +++ b/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts @@ -12,16 +12,16 @@ import test from 'tests/config/unit/test'; -import { defaultShouldProps } from 'components/base/b-scrolly/const'; -import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; -import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; +import { defaultShouldProps } from 'components/base/b-virtual-scroll/const'; +import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; -test.describe('', () => { +test.describe('', () => { let - component: ScrollyTestHelpers['component'], - initLoadSpy: ScrollyTestHelpers['initLoadSpy'], - provider: ScrollyTestHelpers['provider'], - state: ScrollyTestHelpers['state']; + component: VirtualScrollTestHelpers['component'], + initLoadSpy: VirtualScrollTestHelpers['initLoadSpy'], + provider: VirtualScrollTestHelpers['provider'], + state: VirtualScrollTestHelpers['state']; test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); diff --git a/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts b/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts similarity index 96% rename from src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts rename to src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts index 7cd29ee493..a410ec7a89 100644 --- a/src/components/base/b-scrolly/test/unit/lifecycle/slots/slots.ts +++ b/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts @@ -14,13 +14,13 @@ import delay from 'delay'; import test from 'tests/config/unit/test'; -import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; -import type { SlotsStateObj } from 'components/base/b-scrolly/modules/slots'; -import type { ShouldPerform } from 'components/base/b-scrolly/b-scrolly'; +import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; +import type { SlotsStateObj } from 'components/base/b-virtual-scroll/modules/slots'; +import type { ShouldPerform } from 'components/base/b-virtual-scroll/b-virtual-scroll'; import { BOM } from 'tests/helpers'; // eslint-disable-next-line max-lines-per-function -test.describe('', () => { +test.describe('', () => { let component: Awaited>['component'], provider: Awaited>['provider'], @@ -456,6 +456,7 @@ test.describe('', () => { await component.withDefaultPaginationProviderProps({chunkSize}); await component.build(); await component.waitForSlotState('renderNext', false); + await component.waitForSlotState('tombstones', false); const slots = await component.getSlotsState(); diff --git a/src/components/base/b-scrolly/test/unit/scenario/manual-rendering.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts similarity index 84% rename from src/components/base/b-scrolly/test/unit/scenario/manual-rendering.ts rename to src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts index 5f1ea41baa..995b8c4a9d 100644 --- a/src/components/base/b-scrolly/test/unit/scenario/manual-rendering.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts @@ -13,16 +13,16 @@ import test from 'tests/config/unit/test'; -import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; -import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; +import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; import type { ComponentElement } from 'core/component'; -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -test.describe('', () => { +test.describe('', () => { let - component: ScrollyTestHelpers['component'], - provider: ScrollyTestHelpers['provider'], - state: ScrollyTestHelpers['state']; + component: VirtualScrollTestHelpers['component'], + provider: VirtualScrollTestHelpers['provider'], + state: VirtualScrollTestHelpers['state']; test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); @@ -35,7 +35,7 @@ test.describe('', () => { type: 'div', attrs: { id: 'renderNext', - '@click': () => (>document.querySelector('.b-scrolly')).component?.initLoad() + '@click': () => (>document.querySelector('.b-virtual-scroll')).component?.initLoad() } }, @@ -43,7 +43,7 @@ test.describe('', () => { type: 'div', attrs: { id: 'retry', - '@click': () => (>document.querySelector('.b-scrolly')).component?.initLoad() + '@click': () => (>document.querySelector('.b-virtual-scroll')).component?.initLoad() } } }); diff --git a/src/components/base/b-scrolly/test/unit/scenario/props.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts similarity index 89% rename from src/components/base/b-scrolly/test/unit/scenario/props.ts rename to src/components/base/b-virtual-scroll/test/unit/scenario/props.ts index 3a4dabf02f..720cb3680f 100644 --- a/src/components/base/b-scrolly/test/unit/scenario/props.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts @@ -12,15 +12,15 @@ import test from 'tests/config/unit/test'; -import { createTestHelpers, filterEmitterCalls } from 'components/base/b-scrolly/test/api/helpers'; -import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; +import { createTestHelpers, filterEmitterCalls } from 'components/base/b-virtual-scroll/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; -test.describe('', () => { +test.describe('', () => { let - component: ScrollyTestHelpers['component'], - provider: ScrollyTestHelpers['provider'], - state: ScrollyTestHelpers['state'], - initLoadSpy: ScrollyTestHelpers['initLoadSpy']; + component: VirtualScrollTestHelpers['component'], + provider: VirtualScrollTestHelpers['provider'], + state: VirtualScrollTestHelpers['state'], + initLoadSpy: VirtualScrollTestHelpers['initLoadSpy']; test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); diff --git a/src/components/base/b-scrolly/test/unit/scenario/retry.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts similarity index 90% rename from src/components/base/b-scrolly/test/unit/scenario/retry.ts rename to src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts index b5a0b46a55..5d46242220 100644 --- a/src/components/base/b-scrolly/test/unit/scenario/retry.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts @@ -12,16 +12,16 @@ import test from 'tests/config/unit/test'; -import { createTestHelpers } from 'components/base/b-scrolly/test/api/helpers'; -import type { ScrollyTestHelpers } from 'components/base/b-scrolly/test/api/helpers/interface'; +import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; import type { ComponentElement } from 'core/component'; -import type bScrolly from 'components/base/b-scrolly/b-scrolly'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -test.describe('', () => { +test.describe('', () => { let - component: ScrollyTestHelpers['component'], - provider: ScrollyTestHelpers['provider'], - state: ScrollyTestHelpers['state']; + component: VirtualScrollTestHelpers['component'], + provider: VirtualScrollTestHelpers['provider'], + state: VirtualScrollTestHelpers['state']; test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); @@ -34,7 +34,7 @@ test.describe('', () => { type: 'div', attrs: { id: 'retry', - '@click': () => (>document.querySelector('.b-scrolly')).component?.initLoad() + '@click': () => (>document.querySelector('.b-virtual-scroll')).component?.initLoad() } } }); diff --git a/src/components/pages/p-v4-components-demo/index.js b/src/components/pages/p-v4-components-demo/index.js index 6fd7e315cd..1b75e17bbd 100644 --- a/src/components/pages/p-v4-components-demo/index.js +++ b/src/components/pages/p-v4-components-demo/index.js @@ -18,7 +18,7 @@ package('p-v4-components-demo') 'b-list', 'b-tree', 'b-window', - 'b-scrolly', + 'b-virtual-scroll', 'b-bottom-slide', 'b-slider', 'b-sidebar', diff --git a/src/components/traits/i-lock-page-scroll/test/unit/desktop.ts b/src/components/traits/i-lock-page-scroll/test/unit/desktop.ts index cce185aac4..4fffa77743 100644 --- a/src/components/traits/i-lock-page-scroll/test/unit/desktop.ts +++ b/src/components/traits/i-lock-page-scroll/test/unit/desktop.ts @@ -106,21 +106,21 @@ test.describe('components/traits/i-lock-page-scroll - desktop', () => { const getScrollTop = () => page.evaluate(() => document.documentElement.scrollTop); - const scrollYPosition = 500; + const VirtualScrollPosition = 500; await page.evaluate( (yPos) => { document.querySelector('body')!.style.setProperty('height', '5000px'); globalThis.scrollTo(0, yPos); }, - scrollYPosition + VirtualScrollPosition ); - await test.expect(getScrollTop()).resolves.toEqual(scrollYPosition); + await test.expect(getScrollTop()).resolves.toEqual(VirtualScrollPosition); await lock(); await unlock(); - await test.expect(getScrollTop()).resolves.toEqual(scrollYPosition); + await test.expect(getScrollTop()).resolves.toEqual(VirtualScrollPosition); }); }); diff --git a/tests/helpers/component-object/mock.ts b/tests/helpers/component-object/mock.ts index dd152d2de7..8de570bf31 100644 --- a/tests/helpers/component-object/mock.ts +++ b/tests/helpers/component-object/mock.ts @@ -35,7 +35,7 @@ export default class ComponentObjectMock extends Compo * @example * ```typescript * const - * component = new ComponentObject(page, 'b-scrolly'), + * component = new ComponentObject(page, 'b-virtual-scroll'), * spy = await component.spyOn('initLoad', {proto: true}); // Installs a spy on the prototype of the component class * * await component.build(); @@ -44,7 +44,7 @@ export default class ComponentObjectMock extends Compo * * @example * ```typescript - * const component = new ComponentObject(page, 'b-scrolly'); + * const component = new ComponentObject(page, 'b-virtual-scroll'); * const spy = await component.spyOn('someModule.someMethod'); * * await component.build(); @@ -110,7 +110,7 @@ export default class ComponentObjectMock extends Compo * @example * ```typescript * const - * component = new ComponentObject(page, 'b-scrolly'), + * component = new ComponentObject(page, 'b-virtual-scroll'), * shouldStopRequestingData = await component.mockFn(() => false); * * await component.setProps({ From 524266a16ff48c9e662c49b89cb19af371a7696c Mon Sep 17 00:00:00 2001 From: bonkalol Date: Tue, 4 Jul 2023 20:50:23 +0300 Subject: [PATCH 049/159] :wrench: --- .../base/b-virtual-scroll/README.md | 5 +++++ .../base/b-virtual-scroll/b-virtual-scroll.ts | 19 ++++++++++--------- src/components/base/b-virtual-scroll/const.ts | 8 ++++---- .../base/b-virtual-scroll/handlers.ts | 4 ++-- .../base/b-virtual-scroll/interface/common.ts | 4 ++-- .../b-virtual-scroll/interface/component.ts | 17 +++++++++++------ .../b-virtual-scroll/interface/requests.ts | 6 +++--- .../b-virtual-scroll/modules/state/helpers.ts | 4 ++-- .../b-virtual-scroll/modules/state/index.ts | 6 +++--- src/components/base/b-virtual-scroll/props.ts | 9 +++++---- .../test/api/component-object/index.ts | 4 ++-- .../test/api/helpers/index.ts | 12 ++++++------ .../test/api/helpers/interface.ts | 6 +++--- .../test/unit/functional/rendering/default.ts | 6 +++--- 14 files changed, 61 insertions(+), 49 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index a03014ed89..d061f2d4da 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -19,6 +19,11 @@ - поддержка onRequestError? - обработка async replace error в initLoad - не нравится проверка .then((res) => if (res == null) {this.onDataLoadError()}) +- не спредить стейт компонента а возвращать ссылку +- миграция с prev/current/next описать +- тесты на dbConverter +- debug модуль овверайд в edadeal/core +- initLoad ВСЕГДА должен эмититься на nextTick чтобы сначала отрабатывал промис чейн и только потом всплывало событие (обсудить с Андреем) ```mermaid graph TD; diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts index dcea1cf388..c9cf47b859 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts @@ -16,12 +16,13 @@ import VDOM, { create, render } from 'components/friends/vdom'; import { bVirtualScrollDomInsertAsyncGroup, renderGuardRejectionReason } from 'components/base/b-virtual-scroll/const'; import iData, { $$, component, RequestParams } from 'components/super/i-data/i-data'; -import { bVirtualScrollHandlers } from 'components/base/b-virtual-scroll/handlers'; +import { iVirtualScrollHandlers } from 'components/base/b-virtual-scroll/handlers'; import type { AsyncOptions } from 'core/async'; -import type { ComponentState, RenderGuardResult } from 'components/base/b-virtual-scroll/interface'; +import type { VirtualScrollState, RenderGuardResult } from 'components/base/b-virtual-scroll/interface'; export * from 'components/base/b-virtual-scroll/interface'; export * from 'components/base/b-virtual-scroll/const'; +export * from 'components/super/i-data/i-data'; VDOM.addToPrototype(create); VDOM.addToPrototype(render); @@ -34,7 +35,7 @@ VDOM.addToPrototype(render); * by dynamically rendering chunks of data as the user scrolls. */ @component() -export default class bVirtualScroll extends bVirtualScrollHandlers { +export default class bVirtualScroll extends iVirtualScrollHandlers { // @ts-ignore (getter instead readonly) override get requestParams(): iData['requestParams'] { return { @@ -102,9 +103,9 @@ export default class bVirtualScroll extends bVirtualScrollHandlers { /** * Returns the component state. - * {@link ComponentState} + * {@link VirtualScrollState} */ - getComponentState(): Readonly { + getComponentState(): Readonly { return this.componentInternalState.compile(); } @@ -157,7 +158,7 @@ export default class bVirtualScroll extends bVirtualScrollHandlers { * @returns The chunk size. * @throws Error if the `chunkSize` size is not defined. */ - getChunkSize(state: ComponentState): number { + getChunkSize(state: VirtualScrollState): number { if (this.chunkSize == null) { throw new Error('`chunkSize` prop is not defined'); } @@ -173,7 +174,7 @@ export default class bVirtualScroll extends bVirtualScrollHandlers { * @param state * @param chunkSize */ - getNextDataSlice(state: ComponentState, chunkSize: number): object[] { + getNextDataSlice(state: VirtualScrollState, chunkSize: number): object[] { const {data} = state, nextDataSliceStartIndex = this.componentInternalState.getRenderCursor(), @@ -196,8 +197,8 @@ export default class bVirtualScroll extends bVirtualScrollHandlers { } protected override convertDataToDB(data: unknown): O | this['DB'] { - const result = super.convertDataToDB(data); this.onConvertDataToDB(data); + const result = super.convertDataToDB(data); return result; } @@ -212,7 +213,7 @@ export default class bVirtualScroll extends bVirtualScrollHandlers { * Based on the result of this function, the component takes appropriate actions. For example, * it may load data if it is not sufficient for rendering, or perform rendering if all conditions are met. */ - protected renderGuard(state: ComponentState): RenderGuardResult { + protected renderGuard(state: VirtualScrollState): RenderGuardResult { const chunkSize = this.getChunkSize(state), dataSlice = this.getNextDataSlice(state, chunkSize); diff --git a/src/components/base/b-virtual-scroll/const.ts b/src/components/base/b-virtual-scroll/const.ts index 1eec86084e..01f6baba9d 100644 --- a/src/components/base/b-virtual-scroll/const.ts +++ b/src/components/base/b-virtual-scroll/const.ts @@ -16,7 +16,7 @@ import type { ComponentObserverLocalEvents, ComponentRenderLocalEvents, ComponentRenderStrategy, - ComponentState, + VirtualScrollState, ComponentStrategy, RenderGuardRejectionReason @@ -116,18 +116,18 @@ export const componentItemType: ComponentItemType = { export const defaultShouldProps = { /** {@link bVirtualScroll.shouldStopRequestingData} */ - shouldStopRequestingData: (state: ComponentState, _ctx: bVirtualScroll): boolean => { + shouldStopRequestingData: (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { const isLastRequestNotEmpty = () => state.lastLoadedData.length > 0; return !isLastRequestNotEmpty(); }, /** {@link bVirtualScroll.shouldPerformDataRequest} */ - shouldPerformDataRequest: (state: ComponentState, _ctx: bVirtualScroll): boolean => { + shouldPerformDataRequest: (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { const isLastRequestNotEmpty = () => state.lastLoadedData.length > 0; return isLastRequestNotEmpty(); }, /** {@link bVirtualScroll.shouldPerformDataRender} */ - shouldPerformDataRender: (_state: ComponentState, _ctx: bVirtualScroll): boolean => false + shouldPerformDataRender: (_state: VirtualScrollState, _ctx: bVirtualScroll): boolean => false }; diff --git a/src/components/base/b-virtual-scroll/handlers.ts b/src/components/base/b-virtual-scroll/handlers.ts index 201e4dd62a..7d6206c86f 100644 --- a/src/components/base/b-virtual-scroll/handlers.ts +++ b/src/components/base/b-virtual-scroll/handlers.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import bVirtualScrollProps from 'components/base/b-virtual-scroll/props'; +import iVirtualScrollProps from 'components/base/b-virtual-scroll/props'; import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; import { bVirtualScrollAsyncGroup, componentEvents } from 'components/base/b-virtual-scroll/const'; import iData, { component } from 'components/super/i-data/i-data'; @@ -18,7 +18,7 @@ import { isAsyncReplaceError } from 'components/base/b-virtual-scroll/modules/he * This class is designed to work in conjunction with {@link bVirtualScroll}. */ @component() -export abstract class bVirtualScrollHandlers extends bVirtualScrollProps { +export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { /** * Handler: component reset event. diff --git a/src/components/base/b-virtual-scroll/interface/common.ts b/src/components/base/b-virtual-scroll/interface/common.ts index cb762cca92..50283b0517 100644 --- a/src/components/base/b-virtual-scroll/interface/common.ts +++ b/src/components/base/b-virtual-scroll/interface/common.ts @@ -7,7 +7,7 @@ */ import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { ComponentState } from 'components/base/b-virtual-scroll/interface/component'; +import type { VirtualScrollState } from 'components/base/b-virtual-scroll/interface/component'; /** * Interface representing the response of the client to the `renderGuard` method for rendering requests. @@ -73,5 +73,5 @@ export interface RenderGuardRejectionReason { * A function used to query the client about whether to perform a specific action or not. */ export interface ShouldPerform { - (state: ComponentState, ctx: bVirtualScroll): RES; + (state: VirtualScrollState, ctx: bVirtualScroll): RES; } diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts index dbbe9f4788..0640a316c4 100644 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -54,8 +54,12 @@ export interface ComponentStrategy { /** * Component state. + * + * @typeParam DATA - Instance of the data element. + * @typeParam RAW_DATA - The data loaded from the server but not yet processed. + * This type parameter determines the type of the lastLoadedRawData property */ -export interface ComponentState { +export interface VirtualScrollState { /** * The largest component index of type `item` that appeared in the viewport. */ @@ -146,7 +150,7 @@ export interface ComponentState { /** * The last loaded raw data. */ - lastLoadedRawData: unknown; + lastLoadedRawData: CanUndef; } /** @@ -169,17 +173,18 @@ export interface ComponentItemType { /** * This type indicates that the component is the "main" component to render. * - * For example, in the {@link ComponentState} interface, you can notice that + * For example, in the {@link VirtualScrollState} interface, you can notice that * there are specific fields for the `item` type, such as `itemsTillEnd`. * - * Components with this type are stored both in the `items` array and the `childList` array in {@link ComponentState}. + * Components with this type are stored both in the `items` array and the `childList` array in + * {@link VirtualScrollState}. */ item: 'item'; /** * This type indicates that the component is "secondary". * - * Components with this type are stored in the `childList` array in {@link ComponentState}. + * Components with this type are stored in the `childList` array in {@link VirtualScrollState}. */ separator: 'separator'; } @@ -315,5 +320,5 @@ export interface ComponentDb { * Typeof {@link bVirtualScroll.itemsFactory}. */ export interface ComponentItemFactory { - (state: ComponentState, ctx: bVirtualScroll): ComponentItem[]; + (state: VirtualScrollState, ctx: bVirtualScroll): ComponentItem[]; } diff --git a/src/components/base/b-virtual-scroll/interface/requests.ts b/src/components/base/b-virtual-scroll/interface/requests.ts index fdcf26ae66..661349eaa4 100644 --- a/src/components/base/b-virtual-scroll/interface/requests.ts +++ b/src/components/base/b-virtual-scroll/interface/requests.ts @@ -7,7 +7,7 @@ */ import type { CreateRequestOptions, RequestQuery } from 'components/super/i-data/i-data'; -import type { ComponentState } from 'components/base/b-virtual-scroll/interface/component'; +import type { VirtualScrollState } from 'components/base/b-virtual-scroll/interface/component'; /** * Function that returns the GET parameters for a request. @@ -18,10 +18,10 @@ export interface RequestQueryFn { * * @param params - The component state. */ - (params: ComponentState): Dictionary; + (params: VirtualScrollState): Dictionary; } /** * Requests parameters. */ -export type RequestParams = [RequestQuery, CreateRequestOptions]; +export type VirtualScrollRequestParams = [RequestQuery, CreateRequestOptions]; diff --git a/src/components/base/b-virtual-scroll/modules/state/helpers.ts b/src/components/base/b-virtual-scroll/modules/state/helpers.ts index 11c53a0469..70dfe995bb 100644 --- a/src/components/base/b-virtual-scroll/modules/state/helpers.ts +++ b/src/components/base/b-virtual-scroll/modules/state/helpers.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { ComponentState } from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { VirtualScrollState } from 'components/base/b-virtual-scroll/b-virtual-scroll'; import type { PrivateComponentState } from 'components/base/b-virtual-scroll/interface'; /** @@ -14,7 +14,7 @@ import type { PrivateComponentState } from 'components/base/b-virtual-scroll/int * * @returns An object representing the initial state of a component. */ -export function createInitialState(): ComponentState { +export function createInitialState(): VirtualScrollState { return { loadPage: 0, renderPage: 0, diff --git a/src/components/base/b-virtual-scroll/modules/state/index.ts b/src/components/base/b-virtual-scroll/modules/state/index.ts index 8941f77d0d..a099e0557d 100644 --- a/src/components/base/b-virtual-scroll/modules/state/index.ts +++ b/src/components/base/b-virtual-scroll/modules/state/index.ts @@ -7,7 +7,7 @@ */ import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { MountedChild, ComponentState, MountedItem, PrivateComponentState } from 'components/base/b-virtual-scroll/interface'; +import type { MountedChild, VirtualScrollState, MountedItem, PrivateComponentState } from 'components/base/b-virtual-scroll/interface'; import { isItem } from 'components/base/b-virtual-scroll/modules/helpers'; import { createInitialState, createPrivateInitialState } from 'components/base/b-virtual-scroll/modules/state/helpers'; import Friend from 'components/friends/friend'; @@ -21,7 +21,7 @@ export class ComponentInternalState extends Friend { /** * Current state of the component. */ - state: ComponentState = createInitialState(); + state: VirtualScrollState = createInitialState(); /** * Current private state of the component. @@ -33,7 +33,7 @@ export class ComponentInternalState extends Friend { * * @returns The current state of the component. */ - compile(): Readonly { + compile(): Readonly { return { ...this.state }; diff --git a/src/components/base/b-virtual-scroll/props.ts b/src/components/base/b-virtual-scroll/props.ts index 572a145be2..bd15af59d8 100644 --- a/src/components/base/b-virtual-scroll/props.ts +++ b/src/components/base/b-virtual-scroll/props.ts @@ -11,7 +11,7 @@ import type { CreateFromItemFn } from 'components/traits/i-items/i-items'; import type { - ComponentState, + VirtualScrollState, ComponentDb, ComponentRenderStrategy, RequestQueryFn, @@ -41,10 +41,11 @@ import { Observer } from 'components/base/b-virtual-scroll/modules/observer'; import { ComponentInternalState } from 'components/base/b-virtual-scroll/modules/state'; /** - * A class that is friendly to {@link bVirtualScroll}. It contains the properties of the {@link bVirtualScroll} component. + * A class that is friendly to {@link bVirtualScroll}. + * It contains the properties of the {@link bVirtualScroll} component. */ @component() -export default abstract class bVirtualScrollProps extends iData implements iItems { +export default abstract class iVirtualScrollProps extends iData implements iItems { /** {@link iItems.item} */ readonly Item!: object; @@ -112,7 +113,7 @@ export default abstract class bVirtualScrollProps extends iData implements iItem */ @prop({ type: Function, - default: (state: ComponentState, ctx: bVirtualScroll) => { + default: (state: VirtualScrollState, ctx: bVirtualScroll) => { if (ctx.chunkSize == null) { throw new Error('"chunkSize.getNextDataSlice" is used but "chunkSize" prop is not set.'); } diff --git a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts index fc15c0c30f..afaf613206 100644 --- a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts @@ -11,7 +11,7 @@ import type { JSHandle, Locator, Page } from 'playwright'; import { ComponentObject, Scroll } from 'tests/helpers'; import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { ComponentRefs, ComponentState } from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { ComponentRefs, VirtualScrollState } from 'components/base/b-virtual-scroll/b-virtual-scroll'; import type { SlotsStateObj } from 'components/base/b-virtual-scroll/modules/slots'; import { testStyles } from 'components/base/b-virtual-scroll/test/api/component-object/styles'; @@ -60,7 +60,7 @@ export class VirtualScrollComponentObject extends ComponentObject { + getComponentState(): Promise { return this.component.evaluate((ctx) => ctx.getComponentState()); } diff --git a/src/components/base/b-virtual-scroll/test/api/helpers/index.ts b/src/components/base/b-virtual-scroll/test/api/helpers/index.ts index 3868260953..2a8c9056a9 100644 --- a/src/components/base/b-virtual-scroll/test/api/helpers/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/helpers/index.ts @@ -9,7 +9,7 @@ import type { Page } from 'playwright'; import test from 'tests/config/unit/test'; -import type { MountedChild, ComponentItem, ComponentState, MountedItem } from 'components/base/b-virtual-scroll/interface'; +import type { MountedChild, ComponentItem, VirtualScrollState, MountedItem } from 'components/base/b-virtual-scroll/interface'; import { paginationHandler } from 'tests/helpers/providers/pagination'; import { VirtualScrollComponentObject } from 'components/base/b-virtual-scroll/test/api/component-object'; import { RequestInterceptor } from 'tests/helpers/providers/interceptor'; @@ -171,14 +171,14 @@ export function createDataConveyor( * @param dataConveyor - The data conveyor used for managing data within the component. */ export function createStateApi( - initial: Partial, + initial: Partial, dataConveyor: DataConveyor ): StateApi { let state = createInitialState(initial); const obj: StateApi = { - compile(override?: Partial): ComponentState { + compile(override?: Partial): VirtualScrollState { return { ...state, ...extractStateFromDataConveyor(dataConveyor), @@ -186,7 +186,7 @@ export function createStateApi( }; }, - set(props: Partial): StateApi { + set(props: Partial): StateApi { state = { ...state, ...props @@ -212,7 +212,7 @@ export function createStateApi( * * @param state - The partial component state to override the default values. */ -export function createInitialState(state: Partial): ComponentState { +export function createInitialState(state: Partial): VirtualScrollState { return { ...createInitialStateObj(), maxViewedItem: Object.cast(test.expect.any(Number)), @@ -228,7 +228,7 @@ export function createInitialState(state: Partial): ComponentSta * 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 { +export function extractStateFromDataConveyor(conveyor: DataConveyor): Pick { return { data: [...conveyor.data], lastLoadedData: [...conveyor.lastLoadedData], diff --git a/src/components/base/b-virtual-scroll/test/api/helpers/interface.ts b/src/components/base/b-virtual-scroll/test/api/helpers/interface.ts index 904ad481be..9366d74fd3 100644 --- a/src/components/base/b-virtual-scroll/test/api/helpers/interface.ts +++ b/src/components/base/b-virtual-scroll/test/api/helpers/interface.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { ComponentItem, ComponentState, MountedChild, MountedItem } from 'components/base/b-virtual-scroll/interface'; +import type { ComponentItem, VirtualScrollState, MountedChild, MountedItem } from 'components/base/b-virtual-scroll/interface'; import type { VirtualScrollComponentObject } from 'components/base/b-virtual-scroll/test/api/component-object'; import type { SpyObject } from 'tests/helpers/mock/interface'; import type { RequestInterceptor } from 'tests/helpers/providers/interceptor'; @@ -96,7 +96,7 @@ export interface StateApi { * @param override - An object for overriding the current fields of the component state. * @returns The compiled component state. */ - compile(override?: Partial): ComponentState; + compile(override?: Partial): VirtualScrollState; /** * Resets the component state to its initial values. @@ -110,7 +110,7 @@ export interface StateApi { * @param props - An object containing the new state values. * @returns The updated StateApi object. */ - set(props: Partial): StateApi; + set(props: Partial): StateApi; /** * The data conveyor used for managing data within the component state. diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts index d87c7bbdf1..0f0f5b265e 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts @@ -13,7 +13,7 @@ import test from 'tests/config/unit/test'; import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; -import type { ComponentState, ShouldPerform } from 'components/base/b-virtual-scroll/interface'; +import type { VirtualScrollState, ShouldPerform } from 'components/base/b-virtual-scroll/interface'; import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; test.describe('', () => { @@ -63,7 +63,7 @@ test.describe('', () => { await test.expect(component.childList).toHaveCount(chunkSize * 3); }); - test('Should render components with child', async () => { + test.skip('Should render components with child', async () => { // .. }); @@ -78,7 +78,7 @@ test.describe('', () => { .response(200, {data: []}); await component.setProps({ - chunkSize: (state: ComponentState) => [6, 12, 18][state.renderPage] ?? 18, + chunkSize: (state: VirtualScrollState) => [6, 12, 18][state.renderPage] ?? 18, shouldPerformDataRender: ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 }); From 3b0f5fed258c138e765e2f1480d4043613aa941c Mon Sep 17 00:00:00 2001 From: bonkalol Date: Tue, 4 Jul 2023 23:09:41 +0300 Subject: [PATCH 050/159] :wrench: --- .../base/b-virtual-scroll/README.md | 2 +- .../b-virtual-scroll/modules/state/index.ts | 4 +-- .../initialization/initialization.ts | 19 +++++++++++--- tests/helpers/component-object/builder.ts | 2 +- tests/helpers/component-object/interface.ts | 7 ++++- tests/helpers/component-object/mock.ts | 2 +- tests/helpers/mock/index.ts | 26 +++++++++---------- 7 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index d061f2d4da..8a7d67911a 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -23,7 +23,7 @@ - миграция с prev/current/next описать - тесты на dbConverter - debug модуль овверайд в edadeal/core -- initLoad ВСЕГДА должен эмититься на nextTick чтобы сначала отрабатывал промис чейн и только потом всплывало событие (обсудить с Андреем) +- описа про мок модуль, calls и ссылки и как это побеждать ```mermaid graph TD; diff --git a/src/components/base/b-virtual-scroll/modules/state/index.ts b/src/components/base/b-virtual-scroll/modules/state/index.ts index a099e0557d..33b1634a61 100644 --- a/src/components/base/b-virtual-scroll/modules/state/index.ts +++ b/src/components/base/b-virtual-scroll/modules/state/index.ts @@ -34,9 +34,7 @@ export class ComponentInternalState extends Friend { * @returns The current state of the component. */ compile(): Readonly { - return { - ...this.state - }; + return this.state; } /** diff --git a/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts b/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts index 2a7d0a8879..987f1c6c4e 100644 --- a/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts +++ b/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts @@ -15,6 +15,7 @@ import test from 'tests/config/unit/test'; import { defaultShouldProps } from 'components/base/b-virtual-scroll/const'; import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; test.describe('', () => { let @@ -23,6 +24,15 @@ test.describe('', () => { provider: VirtualScrollTestHelpers['provider'], state: VirtualScrollTestHelpers['state']; + const hookProp = { + '@hook:beforeDataCreate': (ctx: bVirtualScroll) => { + const + original = ctx.componentInternalState.compile.bind(ctx.componentInternalState); + + ctx.componentInternalState.compile = () => ({...original()}); + } + }; + test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); @@ -49,7 +59,8 @@ test.describe('', () => { chunkSize, shouldStopRequestingData, shouldPerformDataRequest, - disableObserver: true + disableObserver: true, + ...hookProp }); await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); @@ -124,7 +135,8 @@ test.describe('', () => { chunkSize, shouldStopRequestingData, shouldPerformDataRequest, - disableObserver: true + disableObserver: true, + ...hookProp }); await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); @@ -178,7 +190,8 @@ test.describe('', () => { chunkSize, shouldStopRequestingData, shouldPerformDataRequest, - disableObserver: true + disableObserver: true, + ...hookProp }); await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index bbc65a4ce5..06a6be58de 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -23,7 +23,7 @@ import type iBlock from 'components/super/i-block/i-block'; * However, the recommended usage is to inherit from this class and implement a specific `ComponentObject` * that encapsulates and enhances the client's interaction with component during the test. */ -export default class ComponentObjectBuilder { +export default abstract class ComponentObjectBuilder { /** * The name of the component to be rendered. */ diff --git a/tests/helpers/component-object/interface.ts b/tests/helpers/component-object/interface.ts index fca44c0160..62f1ec073e 100644 --- a/tests/helpers/component-object/interface.ts +++ b/tests/helpers/component-object/interface.ts @@ -6,11 +6,16 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +import type iData from 'components/super/i-data/i-data'; + +/** + * Options for configuring a spy. + */ export interface SpyOptions { /** * If set to true, the spy will be installed on the prototype of the component class. * - * Setting this option is useful for methods such as `initLoad` because they + * Setting this option is useful for methods such as {@link iData.initLoad} because they * are called not from an instance of the component, but using the `call` method from the class prototype. */ proto?: boolean; diff --git a/tests/helpers/component-object/mock.ts b/tests/helpers/component-object/mock.ts index 8de570bf31..2d88ac8d84 100644 --- a/tests/helpers/component-object/mock.ts +++ b/tests/helpers/component-object/mock.ts @@ -19,7 +19,7 @@ import type { SpyExtractor, SpyObject } from 'tests/helpers/mock/interface'; * * It is used for testing components in a mock environment. */ -export default class ComponentObjectMock extends ComponentObjectBuilder { +export default abstract class ComponentObjectMock extends ComponentObjectBuilder { /** * Creates a spy to observe calls to the specified method. * diff --git a/tests/helpers/mock/index.ts b/tests/helpers/mock/index.ts index 60310659ea..96594ff0a8 100644 --- a/tests/helpers/mock/index.ts +++ b/tests/helpers/mock/index.ts @@ -15,8 +15,8 @@ import type { ExtractFromJSHandle, SpyExtractor, SpyObject } from 'tests/helpers /** * Wraps an object as a spy object by adding additional properties for accessing spy information. * - * @param agent The JSHandle representing the spy or mock function. - * @param obj The object to wrap as a spy object. + * @param agent - The JSHandle representing the spy or mock function. + * @param obj - The object to wrap as a spy object. * @returns The wrapped object with spy properties. */ export function wrapAsSpy(agent: JSHandle | ReturnType>, obj: T): T & SpyObject { @@ -44,9 +44,9 @@ export function wrapAsSpy(agent: JSHandle( /** * Retrieves an existing spy object from a `JSHandle`. * - * @param ctx The `JSHandle` containing the spy object. - * @param spyExtractor The function to extract the spy object. + * @param ctx - The `JSHandle` containing the spy object. + * @param spyExtractor - The function to extract the spy object. * @returns A promise that resolves to the spy object. * * @example @@ -108,9 +108,9 @@ export async function getSpy( /** * Creates a mock function and injects it into a Page object. * - * @param page The Page object to inject the mock function into. - * @param fn The mock function. - * @param args The arguments to pass to the function. + * @param page - The Page object to inject the mock function into. + * @param fn - The mock function. + * @param args - The arguments to pass to the function. * @returns A promise that resolves to the mock function as a spy object. * * @example @@ -143,9 +143,9 @@ export async function createMockFn( * This function also returns the ID of the injected mock function, which is stored in `globalThis`. * This binding allows the function to be found during object serialization within the page context. * - * @param page The Page object to inject the mock function into. - * @param fn The mock function. - * @param args The arguments to pass to the function. + * @param page - The Page object to inject the mock function into. + * @param fn - The mock function. + * @param args - The arguments to pass to the function. * @returns A promise that resolves to an object containing the spy object and the ID of the injected mock function. * * @example From fc5e7b50a20c1fcba265dad0e639edf1e06c1580 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 5 Jul 2023 00:36:34 +0300 Subject: [PATCH 051/159] Refactoring --- .../base/b-virtual-scroll/b-virtual-scroll.ts | 1 + .../b-virtual-scroll/modules/state/index.ts | 2 +- .../test/unit/functional/emitter/payload.ts | 4 +- .../initialization/initialization.ts | 400 ++++++++++-------- 4 files changed, 237 insertions(+), 170 deletions(-) diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts index c9cf47b859..680b4cbec3 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts @@ -294,6 +294,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers { if (reason === renderGuardRejectionReason.notEnoughData) { if (state.isRequestsStopped) { this.performRender(); + this.onLifecycleDone(); } else if (this.shouldPerformDataRequestWrapper()) { void this.initLoad(); diff --git a/src/components/base/b-virtual-scroll/modules/state/index.ts b/src/components/base/b-virtual-scroll/modules/state/index.ts index 33b1634a61..c27a38f736 100644 --- a/src/components/base/b-virtual-scroll/modules/state/index.ts +++ b/src/components/base/b-virtual-scroll/modules/state/index.ts @@ -21,7 +21,7 @@ export class ComponentInternalState extends Friend { /** * Current state of the component. */ - state: VirtualScrollState = createInitialState(); + protected state: VirtualScrollState = createInitialState(); /** * Current private state of the component. diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts b/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts index 9343a181c3..e6ccc6142e 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts @@ -152,9 +152,9 @@ test.describe(' emitter', () => { ['renderEngineStart'], ['renderEngineDone'], ['domInsertStart'], + ['lifecycleDone'], ['domInsertDone'], - ['renderDone'], - ['lifecycleDone'] + ['renderDone'] ]); }); diff --git a/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts b/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts index 987f1c6c4e..d20ffefcdf 100644 --- a/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts +++ b/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts @@ -40,179 +40,245 @@ test.describe('', () => { await provider.start(); }); - test('2', async () => { - const - chunkSize = 12, - providerChunkSize = chunkSize / 2; - - const - shouldStopRequestingData = await component.mockFn(() => false), - shouldPerformDataRequest = await component.mockFn(defaultShouldProps.shouldPerformDataRequest); - - const - firstDataChunk = state.data.addData(providerChunkSize), - secondDataChunk = state.data.addData(providerChunkSize); - - state.data.addItems(chunkSize); - - await component.setProps({ - chunkSize, - shouldStopRequestingData, - shouldPerformDataRequest, - disableObserver: true, - ...hookProp + test.describe('Property `chunkSize` is set to 12', () => { + test.describe('Loaded data array is half length of the `chunkSize` prop', () => { + test.describe('`shouldPerformDataRequest` returns true after the initial loading', () => { + let + shouldStopRequestingData, + shouldPerformDataRequest; + + let + firstDataChunk, + secondDataChunk; + + const + chunkSize = 12; + + test.beforeEach(async () => { + const providerChunkSize = chunkSize / 2; + + shouldStopRequestingData = await component.mockFn(() => false); + shouldPerformDataRequest = await component.mockFn(defaultShouldProps.shouldPerformDataRequest); + + firstDataChunk = state.data.addData(providerChunkSize); + secondDataChunk = state.data.addData(providerChunkSize); + + state.data.addItems(chunkSize); + + await component.setProps({ + chunkSize, + shouldStopRequestingData, + shouldPerformDataRequest, + disableObserver: true, + ...hookProp + }); + + await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); + await component.build(); + await component.waitForContainerChildCountEqualsTo(chunkSize); + }); + + test('Should render 12 items', async () => { + await test.expect(component.getContainerChildCount()).resolves.toBe(chunkSize); + }); + + test('Should call `shouldStopRequestingData` twice', async () => { + await test.expect(shouldStopRequestingData.calls).resolves.toEqual([ + [ + state.compile({ + itemsTillEnd: undefined, + childTillEnd: undefined, + maxViewedItem: undefined, + maxViewedChild: undefined, + isRequestsStopped: false, + lastLoadedData: firstDataChunk, + lastLoadedRawData: {data: firstDataChunk}, + data: firstDataChunk, + loadPage: 1 + }), + test.expect.any(Object) + ], + [ + state.compile({ + itemsTillEnd: undefined, + childTillEnd: undefined, + maxViewedItem: undefined, + maxViewedChild: undefined, + isRequestsStopped: false, + isInitialLoading: false, + lastLoadedData: secondDataChunk, + lastLoadedRawData: {data: secondDataChunk}, + data: state.data.data, + loadPage: 2 + }), + test.expect.any(Object) + ] + ]); + }); + + test('Should call `shouldPerformDataRequest` once', async () => { + await test.expect(shouldPerformDataRequest.calls).resolves.toEqual([ + [ + state.compile({ + itemsTillEnd: undefined, + childTillEnd: undefined, + maxViewedItem: undefined, + maxViewedChild: undefined, + isRequestsStopped: false, + lastLoadedData: firstDataChunk, + lastLoadedRawData: {data: firstDataChunk}, + data: firstDataChunk, + loadPage: 1 + }), + test.expect.any(Object) + ] + ]); + }); + + test('Should call `initLoad` twice', async () => { + await test.expect(initLoadSpy.calls).resolves.toEqual([[], []]); + }); + }); }); - - await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); - await component.build(); - await component.waitForContainerChildCountEqualsTo(chunkSize); - - await test.expect(shouldStopRequestingData.calls).resolves.toEqual([ - [ - state.compile({ - itemsTillEnd: undefined, - childTillEnd: undefined, - maxViewedItem: undefined, - maxViewedChild: undefined, - isRequestsStopped: false, - lastLoadedData: firstDataChunk, - lastLoadedRawData: {data: firstDataChunk}, - data: firstDataChunk, - loadPage: 1 - }), - test.expect.any(Object) - ], - [ - state.compile({ - itemsTillEnd: undefined, - childTillEnd: undefined, - maxViewedItem: undefined, - maxViewedChild: undefined, - isRequestsStopped: false, - isInitialLoading: false, - lastLoadedData: secondDataChunk, - lastLoadedRawData: {data: secondDataChunk}, - data: state.data.data, - loadPage: 2 - }), - test.expect.any(Object) - ] - ]); - - await test.expect(shouldPerformDataRequest.calls).resolves.toEqual([ - [ - state.compile({ - itemsTillEnd: undefined, - childTillEnd: undefined, - maxViewedItem: undefined, - maxViewedChild: undefined, - isRequestsStopped: false, - lastLoadedData: firstDataChunk, - lastLoadedRawData: {data: firstDataChunk}, - data: firstDataChunk, - loadPage: 1 - }), - test.expect.any(Object) - ] - ]); - - await test.expect(initLoadSpy.calls).resolves.toEqual([[], []]); }); - test('3', async () => { - const - chunkSize = 12, - providerChunkSize = chunkSize / 2; - - const - shouldStopRequestingData = await component.mockFn(() => false), - shouldPerformDataRequest = await component.mockFn(() => false); - - state.data.addData(providerChunkSize); - state.data.addItems(providerChunkSize); - - await component.setProps({ - chunkSize, - shouldStopRequestingData, - shouldPerformDataRequest, - disableObserver: true, - ...hookProp + test.describe('Property `chunkSize` is set to 12', () => { + test.describe('Loaded data array is half length of the `chunkSize` prop', () => { + test.describe('`shouldPerformDataRequest` returns false after the initial loading', () => { + let + shouldStopRequestingData, + shouldPerformDataRequest; + + const + chunkSize = 12, + providerChunkSize = chunkSize / 2; + + test.beforeEach(async () => { + shouldStopRequestingData = await component.mockFn(() => false); + shouldPerformDataRequest = await component.mockFn(() => false); + + state.data.addData(providerChunkSize); + state.data.addItems(providerChunkSize); + + await component.setProps({ + chunkSize, + shouldStopRequestingData, + shouldPerformDataRequest, + disableObserver: true, + ...hookProp + }); + + await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); + await component.build(); + await component.waitForContainerChildCountEqualsTo(providerChunkSize); + }); + + test('Should render 6 items', async () => { + await test.expect(component.getContainerChildCount()).resolves.toBe(providerChunkSize); + }); + + test('Should call `shouldStopRequestingData` once', async () => { + await test.expect(shouldStopRequestingData.calls).resolves.toEqual([ + [ + state.compile({ + itemsTillEnd: undefined, + childTillEnd: undefined, + maxViewedItem: undefined, + maxViewedChild: undefined, + isRequestsStopped: false, + loadPage: 1 + }), + test.expect.any(Object) + ] + ]); + }); + + test('Should call `shouldPerformDataRequest` once', async () => { + await test.expect(shouldPerformDataRequest.calls).resolves.toEqual([ + [ + state.compile({ + itemsTillEnd: undefined, + childTillEnd: undefined, + maxViewedItem: undefined, + maxViewedChild: undefined, + isRequestsStopped: false, + loadPage: 1 + }), + test.expect.any(Object) + ] + ]); + }); + + test('Should call `initLoad` once', async () => { + await test.expect(initLoadSpy.calls).resolves.toEqual([[]]); + }); + }); }); - - await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); - await component.build(); - await component.waitForContainerChildCountEqualsTo(providerChunkSize); - - await test.expect(shouldStopRequestingData.calls).resolves.toEqual([ - [ - state.compile({ - itemsTillEnd: undefined, - childTillEnd: undefined, - maxViewedItem: undefined, - maxViewedChild: undefined, - isRequestsStopped: false, - loadPage: 1 - }), - test.expect.any(Object) - ] - ]); - - await test.expect(shouldPerformDataRequest.calls).resolves.toEqual([ - [ - state.compile({ - itemsTillEnd: undefined, - childTillEnd: undefined, - maxViewedItem: undefined, - maxViewedChild: undefined, - isRequestsStopped: false, - loadPage: 1 - }), - test.expect.any(Object) - ] - ]); - - await test.expect(initLoadSpy.calls).resolves.toEqual([[]]); }); - test('4', async () => { - const - chunkSize = 12, - providerChunkSize = chunkSize / 2; - - const - shouldStopRequestingData = await component.mockFn(() => true), - shouldPerformDataRequest = await component.mockFn(() => false); - - state.data.addData(providerChunkSize); - state.data.addItems(providerChunkSize); - - await component.setProps({ - chunkSize, - shouldStopRequestingData, - shouldPerformDataRequest, - disableObserver: true, - ...hookProp + test.describe('Property `chunkSize` is set to 12', () => { + test.describe('Loaded data array is half length of the `chunkSize` prop', () => { + test.describe('`shouldStopRequestingData` returns true after the initial loading', () => { + const + chunkSize = 12, + providerChunkSize = chunkSize / 2; + + let + shouldStopRequestingData, + shouldPerformDataRequest; + + test.beforeEach(async () => { + shouldStopRequestingData = await component.mockFn(() => true); + shouldPerformDataRequest = await component.mockFn(() => false); + + state.data.addData(providerChunkSize); + state.data.addItems(providerChunkSize); + + await component.setProps({ + chunkSize, + shouldStopRequestingData, + shouldPerformDataRequest, + disableObserver: true, + ...hookProp + }); + + await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); + await component.build(); + await component.waitForContainerChildCountEqualsTo(providerChunkSize); + }); + + test('Should render 6 items', async () => { + await test.expect(component.getContainerChildCount()).resolves.toBe(providerChunkSize); + }); + + test('Should call `shouldStopRequestingData` once', async () => { + await test.expect(shouldStopRequestingData.calls).resolves.toEqual([ + [ + state.compile({ + itemsTillEnd: undefined, + childTillEnd: undefined, + maxViewedItem: undefined, + maxViewedChild: undefined, + isRequestsStopped: false, + loadPage: 1 + }), + test.expect.any(Object) + ] + ]); + }); + + test('Should call `shouldPerformDataRequest` once', async () => { + await test.expect(shouldPerformDataRequest.calls).resolves.toEqual([]); + }); + + test('Should call `initLoad` once', async () => { + await test.expect(initLoadSpy.calls).resolves.toEqual([[]]); + }); + + test('Should end the component lifecycle', async () => { + await test.expect(component.waitForLifecycleDone()).resolves.toBeUndefined(); + }); + }); }); - - await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); - await component.build(); - await component.waitForContainerChildCountEqualsTo(providerChunkSize); - - await test.expect(shouldStopRequestingData.calls).resolves.toEqual([ - [ - state.compile({ - itemsTillEnd: undefined, - childTillEnd: undefined, - maxViewedItem: undefined, - maxViewedChild: undefined, - isRequestsStopped: false, - loadPage: 1 - }), - test.expect.any(Object) - ] - ]); - - await test.expect(initLoadSpy.calls).resolves.toEqual([[]]); - await test.expect(shouldPerformDataRequest.calls).resolves.toEqual([]); }); }); From 593065e86ab2dbafe027dc3c593df2952462073a Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 5 Jul 2023 19:59:05 +0300 Subject: [PATCH 052/159] Upd readme --- src/components/base/b-virtual-scroll/README.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 8a7d67911a..7923fa2ed8 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -1,25 +1,11 @@ -- ~~Загруженных данных может не хватит на отрисовку поэтому прятать лоадер можно только когда набралось нужное кол-во данных~~ -> неактуально - PartialRender ? Если указано к отрисовке 10 компонентов а загружено за раз данных 5 - рисовать ли эти 5 и потом еще 5 -> для версии 1.1.0 -- ~~Подумать над форматом данных к отрисовке, раньше клиент получал 10 элементов данных и должен был вернуть 10 элементов к отрисовке~~ -> Реализовано -- Preload нескольких страниц -> есть ли нужда? - Обработка ошибок тесты - Проверка стейта во время ошибок - typedLocalEmitter можно ли избавиться и как-то нормально типизировать события компонента -- ~~кейс: загрузили чанк отрисовали -> загружаем следующий (он грузится 20 сек) -> пока грузится полный скролл внизу -> запрещаем загрузку и переходим в isDone состояние -> данные загрузились -> что произойдет???~~ -> неактуально -- ~~componentInternalState.setLoadingPage(val) -> componentInternalState.set('loadingPage', val);~~ -> неактуально -- ~~стоит ли для state использовать builder like подход~~ -> неактуально -- ~~негативные тест кейсы~~ -> реализовано - улучшить имена интерфейсов (MountedItem, MountedSeparator, ревью имен в ComponentState) - протыкать все метода на использование (удалить неиспользуемые) -- улучшить имена тест кейсов -- ревью и рефакторинг src\components\base\b-virtual-scroll\test\api\helpers\index.ts - dbChange - описать что состояние типа loadPage, renderPage обновляются постфактум (после того как загрузка данных случилась, после того как рендеринг случился) -- ~~лишний вызов shouldStopRequestingData в onDataLoadSuccess~~ -> исправлено -- поддержка onRequestError? -- обработка async replace error в initLoad -- не нравится проверка .then((res) => if (res == null) {this.onDataLoadError()}) -- не спредить стейт компонента а возвращать ссылку - миграция с prev/current/next описать - тесты на dbConverter - debug модуль овверайд в edadeal/core From fce69994c1bbc34481e80df1f06f42aa88784ce3 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 7 Jul 2023 10:20:03 +0300 Subject: [PATCH 053/159] :wrench: --- .../base/b-virtual-scroll/README.md | 133 ------------------ src/components/base/b-virtual-scroll/TODO.md | 133 ++++++++++++++++++ 2 files changed, 133 insertions(+), 133 deletions(-) create mode 100644 src/components/base/b-virtual-scroll/TODO.md diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 7923fa2ed8..e69de29bb2 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -1,133 +0,0 @@ -- PartialRender ? Если указано к отрисовке 10 компонентов а загружено за раз данных 5 - рисовать ли эти 5 и потом еще 5 -> для версии 1.1.0 -- Обработка ошибок тесты -- Проверка стейта во время ошибок -- typedLocalEmitter можно ли избавиться и как-то нормально типизировать события компонента -- улучшить имена интерфейсов (MountedItem, MountedSeparator, ревью имен в ComponentState) -- протыкать все метода на использование (удалить неиспользуемые) -- dbChange -- описать что состояние типа loadPage, renderPage обновляются постфактум (после того как загрузка данных случилась, после того как рендеринг случился) -- миграция с prev/current/next описать -- тесты на dbConverter -- debug модуль овверайд в edadeal/core -- описа про мок модуль, calls и ссылки и как это побеждать - -```mermaid -graph TD; - A[loadDataOrPerformRender] -->|state, ctx| B[renderGuard] - B -->|result = true| C[performRender] - B -->|result = false| E[reason] - E -->|done| F[setIsLifecycleDone=true] - E -->|noData| G[isRequestsStopped] - G -->|false| H[shouldPerformDataRequestWrapper] - H -->|true| I[initLoad] - G -->|true| J[return] - E -->|notEnoughData| K[isRequestsStopped] - K -->|true| L[performRender] - K -->|false| M[shouldPerformDataRequestWrapper] - M -->|true| N[initLoad] - M -->|false| O[isInitialRender] - O -->|true| P[performRender] - O -->|false| Q[return] - - -``` - -## TODO: - -1. Бенчмарк подходов - -## Идеи - -1. При скролле брать оффсет скролла и бинарным поиском искать элементы которые сейчас на экране должны быть и отображать их - -```typescript - const vdomCreate: typeof this['vdom']['create'] = this.vdom.create.bind(this.vdom); - const self = this; - - setNodes.forEach((node) => node.remove()); - - if (!vueInstance) { - vueInstance = new Vue({ - render: function () { - const nodes = getArray(count).map((data: DummyUser) => vdomCreate('b-dummy-user', { - attrs: { - dummyData: data, - key: data.userId, - } - })); - - // const nodes = vdomCreate('keep-alive', { - // attrs: {}, - // children: { - // default: () => getArray(count).map((data) => ({ - // type: 'b-dummy-user', - // attrs: { - // dummyData: data, - // key: data.userId, - // } - // })) - // } - // }) - - return nodes; - }, - - beforeCreate() { - let parent = self; - if (parent != null) { - const - root = Object.create(parent.$root); - - Object.defineProperty(root, '$remoteParent', { - configurable: true, - enumerable: true, - writable: true, - value: parent - }); - - Object.defineProperty(this, 'unsafe', { - configurable: true, - enumerable: true, - writable: true, - value: root - }); - } - } - }); - - const - container = document.createElement('div'); - - mountResult = vueInstance.mount(container); - - const - el = this.block?.element('container'); - el?.append(container); - - } else { - mountResult.$forceUpdate(); - } - - this.nextTick(() => { - Array.from(document.querySelectorAll('.b-dummy-user')).forEach((el) => { - if (setNodes.has(el)) { - return; - } - - setNodes.add(el); - el.component._forkDestroy = el.component.$destroy; - - Object.defineProperty(el.component, '$destroy', { - configurable: true, - enumerable: false, - writable: true, - value: () => { - return false; - } - }); - - }); - }); - - console.log(setNodes); -``` \ No newline at end of file diff --git a/src/components/base/b-virtual-scroll/TODO.md b/src/components/base/b-virtual-scroll/TODO.md new file mode 100644 index 0000000000..7923fa2ed8 --- /dev/null +++ b/src/components/base/b-virtual-scroll/TODO.md @@ -0,0 +1,133 @@ +- PartialRender ? Если указано к отрисовке 10 компонентов а загружено за раз данных 5 - рисовать ли эти 5 и потом еще 5 -> для версии 1.1.0 +- Обработка ошибок тесты +- Проверка стейта во время ошибок +- typedLocalEmitter можно ли избавиться и как-то нормально типизировать события компонента +- улучшить имена интерфейсов (MountedItem, MountedSeparator, ревью имен в ComponentState) +- протыкать все метода на использование (удалить неиспользуемые) +- dbChange +- описать что состояние типа loadPage, renderPage обновляются постфактум (после того как загрузка данных случилась, после того как рендеринг случился) +- миграция с prev/current/next описать +- тесты на dbConverter +- debug модуль овверайд в edadeal/core +- описа про мок модуль, calls и ссылки и как это побеждать + +```mermaid +graph TD; + A[loadDataOrPerformRender] -->|state, ctx| B[renderGuard] + B -->|result = true| C[performRender] + B -->|result = false| E[reason] + E -->|done| F[setIsLifecycleDone=true] + E -->|noData| G[isRequestsStopped] + G -->|false| H[shouldPerformDataRequestWrapper] + H -->|true| I[initLoad] + G -->|true| J[return] + E -->|notEnoughData| K[isRequestsStopped] + K -->|true| L[performRender] + K -->|false| M[shouldPerformDataRequestWrapper] + M -->|true| N[initLoad] + M -->|false| O[isInitialRender] + O -->|true| P[performRender] + O -->|false| Q[return] + + +``` + +## TODO: + +1. Бенчмарк подходов + +## Идеи + +1. При скролле брать оффсет скролла и бинарным поиском искать элементы которые сейчас на экране должны быть и отображать их + +```typescript + const vdomCreate: typeof this['vdom']['create'] = this.vdom.create.bind(this.vdom); + const self = this; + + setNodes.forEach((node) => node.remove()); + + if (!vueInstance) { + vueInstance = new Vue({ + render: function () { + const nodes = getArray(count).map((data: DummyUser) => vdomCreate('b-dummy-user', { + attrs: { + dummyData: data, + key: data.userId, + } + })); + + // const nodes = vdomCreate('keep-alive', { + // attrs: {}, + // children: { + // default: () => getArray(count).map((data) => ({ + // type: 'b-dummy-user', + // attrs: { + // dummyData: data, + // key: data.userId, + // } + // })) + // } + // }) + + return nodes; + }, + + beforeCreate() { + let parent = self; + if (parent != null) { + const + root = Object.create(parent.$root); + + Object.defineProperty(root, '$remoteParent', { + configurable: true, + enumerable: true, + writable: true, + value: parent + }); + + Object.defineProperty(this, 'unsafe', { + configurable: true, + enumerable: true, + writable: true, + value: root + }); + } + } + }); + + const + container = document.createElement('div'); + + mountResult = vueInstance.mount(container); + + const + el = this.block?.element('container'); + el?.append(container); + + } else { + mountResult.$forceUpdate(); + } + + this.nextTick(() => { + Array.from(document.querySelectorAll('.b-dummy-user')).forEach((el) => { + if (setNodes.has(el)) { + return; + } + + setNodes.add(el); + el.component._forkDestroy = el.component.$destroy; + + Object.defineProperty(el.component, '$destroy', { + configurable: true, + enumerable: false, + writable: true, + value: () => { + return false; + } + }); + + }); + }); + + console.log(setNodes); +``` \ No newline at end of file From d7c6edf527e1a3dfe21f3c186ff2f05f6f294b7b Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sat, 8 Jul 2023 21:00:22 +0300 Subject: [PATCH 054/159] WIP --- .../base/b-virtual-scroll/README.md | 322 ++++++++++++++++++ src/components/base/b-virtual-scroll/TODO.md | 4 + .../base/b-virtual-scroll/b-virtual-scroll.ss | 2 +- .../base/b-virtual-scroll/b-virtual-scroll.ts | 94 ++--- src/components/base/b-virtual-scroll/const.ts | 3 +- .../base/b-virtual-scroll/handlers.ts | 4 +- .../b-virtual-scroll/interface/component.ts | 2 +- .../b-virtual-scroll/modules/state/helpers.ts | 2 +- .../b-virtual-scroll/modules/state/index.ts | 19 +- src/components/base/b-virtual-scroll/props.ts | 36 +- .../test/api/component-object/index.ts | 36 +- .../test/unit/functional/emitter/payload.ts | 8 +- .../test/unit/functional/rendering/default.ts | 7 +- .../functional/rendering/items-factory.ts | 12 +- .../test/unit/functional/state/base.ts | 8 +- .../test/unit/functional/state/emitter.ts | 4 +- .../initialization/initialization.ts | 18 +- .../test/unit/lifecycle/slots/slots.ts | 26 +- .../test/unit/scenario/manual-rendering.ts | 8 +- .../test/unit/scenario/props.ts | 109 ++---- .../test/unit/scenario/reload.ts | 171 ++++++++++ .../test/unit/scenario/retry.ts | 28 +- .../p-v4-components-demo.ss | 7 + .../p-v4-components-demo.ts | 18 + .../p-v4-components-demo/test/api/page.ts | 66 +++- src/core/prelude/test-env/components/json.ts | 2 + tests/helpers/component-object/builder.ts | 67 ++-- 27 files changed, 810 insertions(+), 273 deletions(-) create mode 100644 src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index e69de29bb2..454ec1959a 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -0,0 +1,322 @@ +# components/base/b-virtual-scroll + +TBD + +## Synopsis + +* The component extends [[iData]]. + +* The component implements [[iItems]] traits. + +## Modifiers + +See the implemented modifiers or the parent component. + +## Events + +### События компонента + +| EventName | Description | Payload description | Payload | +| ------------------------------- | --------------------------------------------------------------- | --------------------------------------------- | --------------------------- | +| `dataLoadSuccess` | Data loading has succeeded. | `data: object[], isInitialLoading: boolean` | `[data, isInitialLoading]` | +| `dataLoadStart` | Data loading has started. | `isInitialLoading: boolean` | `[isInitialLoading]` | +| `dataLoadError` | An error occurred while loading data. | `isInitialLoading: boolean` | `[isInitialLoading]` | +| `dataEmpty` | Successful load with no data. | | `[]` | +| `resetState` | Reset component state. | | `[]` | +| `lifecycleDone` | All component data is rendered and loaded. | | `[]` | +| `convertDataToDB` | Trigger data conversion to the `DB`. | `data: unknown` | `[data]` | +| `elementEnter` | The element has entered the viewport. | `componentItem: MountedChild` | `[componentItem]` | +| `elementOut` | The element has exited the viewport. | `componentItem: MountedChild` | `[componentItem]` | +| `renderStart` | Rendering of items has started. | | `[]` | +| `renderDone` | Rendering of items has finished. | | `[]` | +| `renderEngineStart` | Rendering of items has started with the render engine. | | `[]` | +| `renderEngineDone` | Rendering of items has finished with the render engine. | | `[]` | +| `domInsertStart` | DOM node insertion has started. | | `[]` | +| `domInsertDone` | DOM node insertion has finished. | | `[]` | + +Also, you can see the implemented traits or the parent component. + +## Usage + +### Rendering Components + +In this example: + +- The `b-virtual-scroll` component is used to render a virtual scroll with 12 items loaded at a time. +It interacts with the `Provider` data provider component to fetch the data. The `request` prop is set to `{ get: { chunkSize: 12 } }`, specifying that each request should fetch 12 items. +- The `requestQuery` function computes additional request parameters based on the component state, specifically the `loadPage` property. These request parameters are merged with the `request` prop. +- The `b-virtual-scroll` component renders `b-dummy` components using the `item` prop. +Each `b-dummy` component receives the `name` and `type` props, which are derived from the `data` object for each item using the `itemProps` function. +- The component includes a `loader` slot that displays the message "Data loading in progress" while the data is being fetched. +- By default, the component stops loading data when it receives an empty response from the `dataProvider`, indicating that there are no more items to load. + +``` +< b-virtual-scroll & + :dataProvider = 'Provider' | + :request = {get: {chunkSize: 12}} | + :requestQuery = (state) => ({page: state.loadPage}) | + :chunkSize = 12 | + :item = 'b-dummy' | + :itemProps = (data) => ({name: data.name, type: data.type}) +. + < template #loader + < .&__loader + Data loading in progress +``` + +### Перезагрузка компонента + +Для перезагрузки компонента b-virtual-scroll можно использовать несколько вариантов: + +1. Вызвав `bVirtualScroll.initLoad` с аргументами `[data: any, {silent: false}]`; +2. Вызвав `bVirtualScroll.reload`; +3. Изменив `request` проп. + +Во всех этих случаях жизненный цикл компонент будет сброшен на изначальное состояние и компонент начнет отрисовку +новых данных которые будет получать, очистив все предидущие. + + +### Should-like функции + +### Переопределение `itemsFactory` + +### Отрисовка по клику + + +## Slots + +The component supports a bunch of slots to provide. + +1. `loader` предоставляют возможность отображать различный контент (обычно скелетоны) пока загружаются данные. + +``` +< b-virtual-scroll + < template #loader + < .&__loader + Data loading in progress +``` + +2. `tombstone` предоставляет возможность отображать различный контент который будет повторяться `tombstonesSize` (обычно скелетоны) количество раз пока загружаются данные. + +``` +< b-virtual-scroll :tombstonesSize = 3 + < template #loader + < .&__skeleton + Skeleton +``` + +3. `retry` предоставляет возможность отображать различный контент (обычно призыв перезагрузить данные) когда произошла ошибка загрузки данных. + +``` +< b-virtual-scroll + < template #retry + < .&__retry @click = initLoad + Retry last request +``` + +4. `empty` предоставляет возможность отображать различный контент когда компонента получил порцию пустых данных при первоначальной загрузке. + +``` +< b-virtual-scroll + < template #empty + < .&__empty + No data +``` + +5. `done` предоставляет возможность отображать различный контент когда компонента завершил загрузку всех данных и так же все их отрисовал. + +``` +< b-virtual-scroll + < template #done + < .&__done + Load and render complete +``` + +6. `renderNext` предоставляет возможность отображать различный контент когда компонента не загружает данные и не перешел в состояние окончания лайфцикла. +Данный слот может быть полезен если необходимо реализовать ленивую отрисовку контента по клику. + +``` +< b-virtual-scroll + < template #render-next + < .&__render-next + Render next +``` + +## API + +### `shouldPerformDataRender` + +- Type: `Function` +- Default: `(state: VirtualScrollState) => state.isInitialRender || state.itemsTillEnd === 0` + +This function is called in the `bVirtualScroll.renderGuard` after other checks are completed. +It receives the component state as input and determines whether the component should render the next chunk of components. +The function should return a boolean value: `true` to allow rendering the next chunk, or `false` to prevent it. + +Example usage: + +```typescript +const shouldPerformDataRender = (state: VirtualScrollState): boolean => { + return state.isInitialRender || state.itemsTillEnd === 0; +}; +``` + +### `shouldPerformDataRequest` + +- Type: `Function` +- Default: `() => state.lastLoadedData.length > 0` + +The `shouldPerformDataRequest` property of `bVirtualScroll` allows you to control whether the component should request additional data based on the component state. +Here's an example of how you can use `shouldPerformDataRequest`: + +```typescript +const shouldPerformDataRequest = (state: VirtualScrollState): boolean => { + // Example: Request data if the remaining items till the end is less than or equal to 10 + return state.itemsTillEnd <= 10; +}; +``` + +In this example, the function checks the `itemsTillEnd` property of the component state. +If the remaining number of items till the end is less than or equal to 10, it returns `true` to indicate that the component should perform a data request. +You can adjust the condition based on your specific requirements. + +By implementing the `shouldPerformDataRequest` function, you have control over when the component should request additional data. +This allows you to customize the data loading behavior based on the state of the component. + +### `shouldStopRequestingData` + +- Type: `Function` +- Default: `(state: VirtualScrollState) => state.lastLoadedData.length > 0` + +This function is called on each data loading cycle. It determines whether the component should stop requesting new data. +The function should return a boolean value: `true` to stop requesting data, or `false` to continue requesting data. + +Here's an example of how you can use `shouldStopRequestingData`: + +```typescript +const shouldStopRequestingData = (state: VirtualScrollState): boolean => { + // Example: Stop requesting data when the total number of items equals the current number of loaded items + return state.lastLoadedRawData?.total === state.data.length; +}; +``` + +In this example, the function compares the total property of `lastLoadedRawData` with the length of the data array. +If the two values are equal, it returns true to indicate that the component should stop requesting new data. +This condition suggests that all available items have been loaded, and there is no need for further data requests. + +You can customize the `shouldStopRequestingData` function to fit your specific scenario. +By implementing this function, you have control over when the component should stop requesting new data, based on the comparison between the total number of items and the current number of loaded items. + +### `chunkSize` + +- Type: `number | Function` +- Default: `10` + +The amount of data required to perform one cycle of item rendering. This prop is used by the `bVirtualScroll` component to determine the number of components to render in each cycle. +It can be either a fixed number or a function that returns the number dynamically based on the component state. + +Here are some examples: + +```typescript +const chunkSize = (state: VirtualScrollState): number => { + // Example 1: Incrementing chunk size for each render page + return (state.renderPage + 1) * 10; + + // Example 2: Dynamic chunk size based on the state + // Replace the condition and calculation with your custom logic + if (state.isInitialRender) { + return 20; + } else if (state.renderPage < 3) { + return 15; + } else { + return 10; + } +}; +``` + +In Example 1, the chunk size increases by 10 for each render page. For the initial render, it will be 10, then 20, 30, and so on. +In Example 2, the chunk size is dynamically determined based on the component state. It assigns different chunk sizes based on different conditions. + +By using a function for `chunkSize`, you have the flexibility to adjust the rendering behavior based on the state of the component and other factors. + +### `requestQuery` + +- Type: `Function` +- Default: `undefined` + +A function that returns the GET parameters for a request. This function is called for each request and receives the current component state as input. +It should return an object containing the request parameters. These parameters will be merged with the parameters from the `request` prop, giving priority to the `request` prop. + +Pagination example: + +```typescript +const requestQuery = (state: VirtualScrollState): Dictionary => { + return { + page: state.loadPage, + limit: 10 + // Other pagination parameters + }; +}; +``` + +### `itemsFactory` + +- Type: `Function` +- Default: See description + +A factory function used to generate an array of `ComponentItem` objects representing the components to be rendered. +This function is called during the rendering process and receives the component state and context as arguments. It should return an array of `ComponentItem` objects. + +The default implementation uses the `chunkSize` and `iItems` trait to slice the data and generate the components. +However, you can override this function to implement a custom rendering strategy. + +Here's an example of how you can use the itemsFactory property to generate ComponentItem objects based on the lastLoadedData property: + +```typescript +const itemsFactory = (state: VirtualScrollState): ComponentItem[] => { + const items: ComponentItem[] = state.lastLoadedData.map((itemData, index) => { + // Construct a ComponentItem object for each item in the lastLoadedData array + return { + type: 'item', + item: 'b-button', + props: { + id: `button-${index}` + }, + key: `item-${index}`, + children: { + default: `Item ${index + 1}` + } + }; + }); + + return items; +}; +``` + +### `tombstonesSize` + +- Type: `number` +- Default: `undefined` + +Specifies the number of times the `tombstone` component will be rendered. This prop can be useful if you want to render multiple `tombstone` components using a single specified element. +For example, if you set `tombstonesSize` to 3, then three `tombstone` components will be rendered on your page. + +Note: The `tombstone` component is used to represent empty or unloaded components in the virtual scroll. It is rendered as a placeholder until the actual component data is loaded and rendered. + +### Other Properties + +The `bVirtualScroll` class extends `iData` and includes additional properties related to slots, component state, and observers. Please refer to the documentation of `iData` for more details on those properties. + +## Миграция с `b-virtual-scroll` версии 3.x.x + +## Deep dive into component + +### Жизненный цикл + +### renderGuard + +### Модули компонента + +### Переопределение в дочерних слоях + +## Возможные улучшения и дальнейшие эксперименты \ No newline at end of file diff --git a/src/components/base/b-virtual-scroll/TODO.md b/src/components/base/b-virtual-scroll/TODO.md index 7923fa2ed8..1494fe7bce 100644 --- a/src/components/base/b-virtual-scroll/TODO.md +++ b/src/components/base/b-virtual-scroll/TODO.md @@ -10,6 +10,10 @@ - тесты на dbConverter - debug модуль овверайд в edadeal/core - описа про мок модуль, calls и ссылки и как это побеждать +- reset.load - перезагрузит ли компонент? написать тест кейсы на перезагрузки с помощью reload, изменения request, вызова reset.load +- описать в документации про тестовый компонент в демо странице и для чего он нужен +- сделать хэндлеры и модули протектед +- импорты наладить порядок ```mermaid graph TD; diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ss b/src/components/base/b-virtual-scroll/b-virtual-scroll.ss index 3c2f2d4590..90bcc85296 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ss +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ss @@ -12,7 +12,7 @@ - template index() extends ['i-data'].index - block body - < .&__wrapper + < .&__wrapper :-chunk-size = chunkSize < .&__container ref = container | -test-ref = container < .&__tombstones & diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts index 680b4cbec3..8fa6d43279 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts @@ -52,46 +52,70 @@ export default class bVirtualScroll extends iVirtualScrollHandlers { } override initLoad(...args: Parameters): ReturnType { - const - state = this.getComponentState(); - - this.componentInternalState.setIsLastErrored(false); - - if (!this.isReady && this.isReadyOnce) { + if (!this.lfc.isBeforeCreate()) { this.reset(); } - if (state.isLoadingInProgress) { - return; - } - this.componentInternalState.setIsLoadingInProgress(true); const - isInitialLoading = !this.isReady; - - const initLoadResult = isInitialLoading ? - super.initLoad(...args) : - this.initLoadNext(); + initLoadResult = super.initLoad(...args); - this.onDataLoadStart(isInitialLoading); + this.onDataLoadStart(true); if (Object.isPromise(initLoadResult)) { initLoadResult - .then((res) => { - if ( - (isInitialLoading && this.db == null) || - (!isInitialLoading && res == null) - ) { + .then(() => { + if (this.db == null) { return; } - this.onDataLoadSuccess(isInitialLoading, isInitialLoading ? this.db : this.convertDataToDB(res)); + this.onDataLoadSuccess(true, this.db); }) .catch(stderr); + + } else { + this.onDataLoadSuccess(true, this.db); } - return >initLoadResult; + return initLoadResult; + } + + /** + * Initializes the loading of the next data chunk. + * @throws {@link ReferenceError} if there is no `dataProvider` set. + */ + initLoadNext(): CanUndef> { + if (!this.dataProvider) { + throw ReferenceError('Missing dataProvider'); + } + + const + state = this.getComponentState(); + + if (state.isLoadingInProgress) { + return; + } + + if (this.db == null) { + return this.initLoad(); + } + + this.onDataLoadStart(false); + + const + params = this.getRequestParams(), + get = this.dataProvider.get(params[0], params[1]); + + return get + .then((res) => { + if (res == null) { + return; + } + + this.onDataLoadSuccess(false, this.convertDataToDB(res)); + }) + .catch(stderr); } /** @@ -156,13 +180,8 @@ export default class bVirtualScroll extends iVirtualScrollHandlers { * * @param state * @returns The chunk size. - * @throws Error if the `chunkSize` size is not defined. */ getChunkSize(state: VirtualScrollState): number { - if (this.chunkSize == null) { - throw new Error('`chunkSize` prop is not defined'); - } - return Object.isFunction(this.chunkSize) ? this.chunkSize(state, this) : this.chunkSize; @@ -177,25 +196,12 @@ export default class bVirtualScroll extends iVirtualScrollHandlers { getNextDataSlice(state: VirtualScrollState, chunkSize: number): object[] { const {data} = state, - nextDataSliceStartIndex = this.componentInternalState.getRenderCursor(), + nextDataSliceStartIndex = this.componentInternalState.getDataCursor(), nextDataSliceEndIndex = nextDataSliceStartIndex + chunkSize; return data.slice(nextDataSliceStartIndex, nextDataSliceEndIndex); } - /** - * Initializes the loading of the next data chunk. - * @throws {@link ReferenceError} if there is no `dataProvider` set. - */ - protected initLoadNext(): Promise { - if (!this.dataProvider) { - throw ReferenceError('Missing dataProvider'); - } - - const params = this.getRequestParams(); - return this.dataProvider.get(params[0], params[1]); - } - protected override convertDataToDB(data: unknown): O | this['DB'] { this.onConvertDataToDB(data); const result = super.convertDataToDB(data); @@ -287,7 +293,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers { } if (this.shouldPerformDataRequestWrapper()) { - void this.initLoad(); + void this.initLoadNext(); } } @@ -297,7 +303,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers { this.onLifecycleDone(); } else if (this.shouldPerformDataRequestWrapper()) { - void this.initLoad(); + void this.initLoadNext(); } else if (state.isInitialRender) { this.performRender(); diff --git a/src/components/base/b-virtual-scroll/const.ts b/src/components/base/b-virtual-scroll/const.ts index 01f6baba9d..2f74bde573 100644 --- a/src/components/base/b-virtual-scroll/const.ts +++ b/src/components/base/b-virtual-scroll/const.ts @@ -128,6 +128,7 @@ export const defaultShouldProps = { }, /** {@link bVirtualScroll.shouldPerformDataRender} */ - shouldPerformDataRender: (_state: VirtualScrollState, _ctx: bVirtualScroll): boolean => false + shouldPerformDataRender: (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => + state.isInitialRender || state.itemsTillEnd === 0 }; diff --git a/src/components/base/b-virtual-scroll/handlers.ts b/src/components/base/b-virtual-scroll/handlers.ts index 7d6206c86f..ec0c82bc31 100644 --- a/src/components/base/b-virtual-scroll/handlers.ts +++ b/src/components/base/b-virtual-scroll/handlers.ts @@ -64,7 +64,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * @param childList */ onDomInsertStart(this: bVirtualScroll, childList: MountedChild[]): void { - this.componentInternalState.updateRenderCursor(); + this.componentInternalState.updateDataCursor(); this.componentInternalState.updateMounted(childList); this.componentInternalState.setIsInitialRender(false); this.componentInternalState.incrementRenderPage(); @@ -123,6 +123,8 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * @param isInitialLoading - Indicates whether it is an initial component loading. */ onDataLoadStart(isInitialLoading: boolean): void { + this.componentInternalState.setIsLoadingInProgress(true); + this.componentInternalState.setIsLastErrored(false); this.slotsStateController.loadingProgressState(isInitialLoading); this.componentEmitter.emit(componentEvents.dataLoadStart, isInitialLoading); diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts index 0640a316c4..cc900b7add 100644 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -163,7 +163,7 @@ export interface PrivateComponentState { /** * Pointer to the index of the data element that was last rendered. */ - renderCursor: number; + dataCursor: number; } /** diff --git a/src/components/base/b-virtual-scroll/modules/state/helpers.ts b/src/components/base/b-virtual-scroll/modules/state/helpers.ts index 70dfe995bb..8812841859 100644 --- a/src/components/base/b-virtual-scroll/modules/state/helpers.ts +++ b/src/components/base/b-virtual-scroll/modules/state/helpers.ts @@ -44,6 +44,6 @@ export function createInitialState(): VirtualScrollState { */ export function createPrivateInitialState(): PrivateComponentState { return { - renderCursor: 0 + dataCursor: 0 }; } diff --git a/src/components/base/b-virtual-scroll/modules/state/index.ts b/src/components/base/b-virtual-scroll/modules/state/index.ts index c27a38f736..db2a92db5a 100644 --- a/src/components/base/b-virtual-scroll/modules/state/index.ts +++ b/src/components/base/b-virtual-scroll/modules/state/index.ts @@ -166,25 +166,20 @@ export class ComponentInternalState extends Friend { /** * Returns the cursor indicating the last index of the last rendered data element. */ - getRenderCursor(): number { - return this.privateState.renderCursor; + getDataCursor(): number { + return this.privateState.dataCursor; } /** * Updates the cursor indicating the last index of the last rendered data element. */ - updateRenderCursor(): void { + updateDataCursor(): void { const - {ctx} = this; + {ctx, state} = this, + current = this.getDataCursor(), + chunkSize = ctx.getChunkSize(state); - if (ctx.chunkSize != null) { - const - {state} = this, - current = this.getRenderCursor(), - chunkSize = ctx.getChunkSize(state); - - this.privateState.renderCursor = current + chunkSize; - } + this.privateState.dataCursor = current + chunkSize; } } diff --git a/src/components/base/b-virtual-scroll/props.ts b/src/components/base/b-virtual-scroll/props.ts index bd15af59d8..761f1bf0ca 100644 --- a/src/components/base/b-virtual-scroll/props.ts +++ b/src/components/base/b-virtual-scroll/props.ts @@ -114,10 +114,6 @@ export default abstract class iVirtualScrollProps extends iData implements iItem @prop({ type: Function, default: (state: VirtualScrollState, ctx: bVirtualScroll) => { - if (ctx.chunkSize == null) { - throw new Error('"chunkSize.getNextDataSlice" is used but "chunkSize" prop is not set.'); - } - const descriptors = ctx.getNextDataSlice(state, ctx.getChunkSize(state)).map((data, i) => ({ key: ctx.itemKey?.(data, i), @@ -163,33 +159,41 @@ export default abstract class iVirtualScrollProps extends iData implements iItem readonly componentStrategy: keyof ComponentStrategy = componentStrategy.intersectionObserver; /** - * Function that returns the GET parameters for a request. + * Function that returns the GET parameters for a request. This function is called for each request. It receives the + * current component state and should return the request parameters. These parameters are merged with the parameters + * from the `request` prop in favor of the `request` prop. + * + * This function is useful when you need to pass pagination parameters or any other parameters that should not trigger + * a component state reload, unlike changing the `request` prop. + * * {@link RequestQueryFn} */ @prop({type: Function}) readonly requestQuery?: RequestQueryFn; /** - * The number of elements to render at once. + * The amount of data required to perform one cycle of item rendering. * - * This prop can also be a function that returns the chunk size to render based on certain criteria - * (e.g., rendering page). + * This prop is primarily used to determine whether a specific action with the data needs to be performed + * ({@link bVirtualScroll.renderGuard}), and only secondarily for component rendering. * - * For example, you can render 6 elements initially, then 12, and then 18 elements based on the - * rendering page. + * By default, this prop is used in {@link bVirtualScroll.itemsFactory} to slice the data + * according to the {@link bVirtualScroll.chunkSize} and render components based on it. + * However, it is possible to define a custom {@link bVirtualScroll.itemsFactory} and render as many components + * as desired in one cycle of rendering. In this case, the `chunkSize` will only have significance for the data. + * + * This prop can also be a function that should return the amount of data required to perform one cycle of rendering. + * For example, different values can be specified depending on the rendering page: * * @example * ```typescript - * const chunkSize = (state: ComponentState) => { + * const chunkSize = (state: VirtualScrollState) => { * return [6, 12, 18][state.renderPage] ?? 18; * } * ``` - * - * It's important to note that this prop is used by default, but it can be ignored and you can return - * any amount of data to render by setting a custom {@link bVirtualScroll.itemsFactory} for the component. */ @prop({type: [Number, Function]}) - readonly chunkSize?: number | ShouldPerform = 10; + readonly chunkSize: number | ShouldPerform = 10; /** * When this function returns `true` the component will stop to request new data. @@ -229,7 +233,7 @@ export default abstract class iVirtualScrollProps extends iData implements iItem * } * ``` */ - @prop(Function) + @prop({type: Function, default: defaultShouldProps.shouldPerformDataRender}) readonly shouldPerformDataRender?: ShouldPerform; /** diff --git a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts index afaf613206..bb2355315f 100644 --- a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { JSHandle, Locator, Page } from 'playwright'; +import type { Locator, Page } from 'playwright'; import { ComponentObject, Scroll } from 'tests/helpers'; @@ -30,6 +30,10 @@ export class VirtualScrollComponentObject extends ComponentObject *'); } - /** - * Overrides the build method to add test styles before building the component. - * - * @param args - The arguments for the build method. - */ - override async build(...args: Parameters['build']>): Promise> { - await this.page.addStyleTag({content: testStyles}); - return super.build(...args); - } - /** * Calls the reload method of the component. */ @@ -180,8 +174,8 @@ export class VirtualScrollComponentObject extends ComponentObject { - await this.setProps({ + withPaginationItemProps(): this { + this.withProps({ item: 'section', itemProps: (item) => ({'data-index': item.i}) }); @@ -194,8 +188,8 @@ export class VirtualScrollComponentObject extends ComponentObject { - await this.setProps({ + withRequestPaginationProps(requestParams: Dictionary = {}): this { + this.withProps({ request: { get: { chunkSize: 10, @@ -211,8 +205,8 @@ export class VirtualScrollComponentObject extends ComponentObject { - await this.setProps({dataProvider: 'Provider'}); + withPaginationProvider(): this { + this.withProps({dataProvider: 'Provider'}); return this; } @@ -224,10 +218,10 @@ export class VirtualScrollComponentObject extends ComponentObject { - await this.withPaginationProvider(); - await this.withPaginationItemProps(); - await this.withRequestPaginationProps(requestParams); + withDefaultPaginationProviderProps(requestParams: Dictionary = {}): this { + this.withPaginationProvider(); + this.withPaginationItemProps(); + this.withRequestPaginationProps(requestParams); return this; } diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts b/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts index e6ccc6142e..7aeab57f66 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts @@ -36,7 +36,7 @@ test.describe(' emitter', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); - await component.setProps({ + await component.withProps({ chunkSize, shouldStopRequestingData: () => true, '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') @@ -78,7 +78,7 @@ test.describe(' emitter', () => { .responseOnce(200, {data: secondDataChunk}) .response(200, {data: []}); - await component.setProps({ + await component.withProps({ chunkSize, shouldPerformDataRequest: () => true, shouldStopRequestingData: ({lastLoadedData}) => lastLoadedData.length === 0, @@ -125,7 +125,7 @@ test.describe(' emitter', () => { .responseOnce(200, {data: firstDataChunk}) .response(200, {data: []}); - await component.setProps({ + await component.withProps({ chunkSize, shouldPerformDataRequest: () => true, shouldStopRequestingData: ({lastLoadedData}) => lastLoadedData.length === 0, @@ -165,7 +165,7 @@ test.describe(' emitter', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); - await component.setProps({ + await component.withProps({ chunkSize, shouldStopRequestingData: () => true, '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts index 0f0f5b265e..bbcd4f1793 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts @@ -45,7 +45,7 @@ test.describe('', () => { ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 ); - await component.setProps({ + await component.withProps({ shouldPerformDataRender, chunkSize }); @@ -77,9 +77,8 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(chunkSize[2])}) .response(200, {data: []}); - await component.setProps({ - chunkSize: (state: VirtualScrollState) => [6, 12, 18][state.renderPage] ?? 18, - shouldPerformDataRender: ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 + await component.withProps({ + chunkSize: (state: VirtualScrollState) => [6, 12, 18][state.renderPage] ?? 18 }); await component.withDefaultPaginationProviderProps(); diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts index a17c04e014..f1cb281dac 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts @@ -52,7 +52,7 @@ test.describe(' rendering via component factory', () => { })); }); - await component.setProps({ + await component.withProps({ itemsFactory, shouldPerformDataRender: () => true, chunkSize @@ -104,7 +104,7 @@ test.describe(' rendering via component factory', () => { return items; }, separator); - await component.setProps({ + await component.withProps({ itemsFactory, shouldPerformDataRender: () => true, chunkSize @@ -144,7 +144,7 @@ test.describe(' rendering via component factory', () => { return items; }); - await component.setProps({ + await component.withProps({ itemsFactory, shouldPerformDataRender: () => true, chunkSize @@ -182,7 +182,7 @@ test.describe(' rendering via component factory', () => { return [...items, ...items]; }); - await component.setProps({ + await component.withProps({ itemsFactory, shouldPerformDataRender: () => true, chunkSize @@ -217,7 +217,7 @@ test.describe(' rendering via component factory', () => { })); }); - await component.setProps({ + await component.withProps({ itemsFactory, shouldPerformDataRender: () => true, chunkSize @@ -260,7 +260,7 @@ test.describe(' rendering via component factory', () => { return [...items, ...items]; }); - await component.setProps({ + await component.withProps({ itemsFactory, shouldPerformDataRender, chunkSize diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/base.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/base.ts index b2ddf87a02..dc78c76f39 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/base.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/base.ts @@ -50,7 +50,7 @@ test.describe('', () => { loadPage: 0 }); - await component.setProps({ + await component.withProps({ '@hook:created': mockFn }); @@ -79,7 +79,7 @@ test.describe('', () => { state.data.addItems(chunkSize); - await component.setProps({ + await component.withProps({ chunkSize, shouldStopRequestingData, shouldPerformDataRequest, @@ -175,7 +175,7 @@ test.describe('', () => { state.data.addItems(chunkSize); state.data.addChild([separator]); - await component.setProps({ + await component.withProps({ itemsFactory, shouldPerformDataRender: () => true, chunkSize @@ -226,7 +226,7 @@ test.describe('', () => { state.data.addSeparators(chunkSize); - await component.setProps({ + await component.withProps({ itemsFactory, shouldPerformDataRender: () => true, chunkSize diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts index e71f6a056e..56c5008db6 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts @@ -61,7 +61,7 @@ test.describe('', () => { .responseOnce(200, {data: state.data.getDataChunk(0)}) .response(200, {data: []}); - await component.setProps({ + await component.withProps({ chunkSize, shouldStopRequestingData: () => true, '@hook:beforeDataCreate': (ctx) => { @@ -142,7 +142,7 @@ test.describe('', () => { .responseOnce(200, {data: state.data.getDataChunk(1)}) .response(200, {data: state.data.getDataChunk(2)}); - await component.setProps({ + await component.withProps({ chunkSize, '@hook:beforeDataCreate': (ctx) => { const original = ctx.emit; diff --git a/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts b/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts index d20ffefcdf..fc3f1b5cea 100644 --- a/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts +++ b/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts @@ -30,6 +30,7 @@ test.describe('', () => { original = ctx.componentInternalState.compile.bind(ctx.componentInternalState); ctx.componentInternalState.compile = () => ({...original()}); + jestMock.spy(ctx, 'initLoadNext'); } }; @@ -65,7 +66,7 @@ test.describe('', () => { state.data.addItems(chunkSize); - await component.setProps({ + await component.withProps({ chunkSize, shouldStopRequestingData, shouldPerformDataRequest, @@ -135,8 +136,15 @@ test.describe('', () => { ]); }); - test('Should call `initLoad` twice', async () => { - await test.expect(initLoadSpy.calls).resolves.toEqual([[], []]); + test('Should call `initLoad` once', async () => { + await test.expect(initLoadSpy.calls).resolves.toEqual([[]]); + }); + + test('Should call `initLoadNext` once', async () => { + const + spy = await component.getSpy((ctx) => ctx.initLoadNext); + + await test.expect(spy.calls).resolves.toEqual([[]]); }); }); }); @@ -160,7 +168,7 @@ test.describe('', () => { state.data.addData(providerChunkSize); state.data.addItems(providerChunkSize); - await component.setProps({ + await component.withProps({ chunkSize, shouldStopRequestingData, shouldPerformDataRequest, @@ -234,7 +242,7 @@ test.describe('', () => { state.data.addData(providerChunkSize); state.data.addItems(providerChunkSize); - await component.setProps({ + await component.withProps({ chunkSize, shouldStopRequestingData, shouldPerformDataRequest, diff --git a/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts b/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts index a410ec7a89..d24329c2c1 100644 --- a/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts +++ b/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts @@ -69,7 +69,7 @@ test.describe('', () => { } }); - await component.setProps({ + await component.withProps({ tombstonesSize: 1 }); }); @@ -82,7 +82,7 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); - await component.setProps({ + await component.withProps({ chunkSize }); @@ -112,7 +112,7 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); - await component.setProps({ + await component.withProps({ chunkSize, shouldPerformDataRender: () => true }); @@ -143,7 +143,7 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(chunkSize / 2)}) .response(200, {data: []}); - await component.setProps({ + await component.withProps({ chunkSize, shouldPerformDataRender: () => true }); @@ -181,7 +181,7 @@ test.describe('', () => { const shouldPerformDataRender = (({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0); - await component.setProps({ + await component.withProps({ chunkSize, shouldPerformDataRequest, shouldPerformDataRender @@ -213,7 +213,7 @@ test.describe('', () => { provider.response(200, {data: []}); - await component.setProps({ + await component.withProps({ chunkSize, shouldPerformDataRender: () => true }); @@ -244,7 +244,7 @@ test.describe('', () => { provider .response(200, {data: state.data.addData(chunkSize)}, {delay: (10).seconds()}); - await component.setProps({ + await component.withProps({ chunkSize }); @@ -274,7 +274,7 @@ test.describe('', () => { provider .response(200, {data: state.data.addData(providerChunkSize)}, {delay: (4).seconds()}); - await component.setProps({ + await component.withProps({ chunkSize }); @@ -311,7 +311,7 @@ test.describe('', () => { provider.response(500, {}); - await component.setProps({ + await component.withProps({ chunkSize, shouldPerformDataRender: () => true }); @@ -343,7 +343,7 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(providerChunkSize)}) .response(500, {}); - await component.setProps({ + await component.withProps({ chunkSize, shouldPerformDataRender: () => true }); @@ -386,7 +386,7 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); - await component.setProps({ + await component.withProps({ chunkSize, disableObserver: true, shouldPerformDataRender: () => true @@ -417,7 +417,7 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}, {delay: (10).seconds()}) .response(200, {data: []}); - await component.setProps({ + await component.withProps({ chunkSize, disableObserver: true, shouldPerformDataRender: () => true @@ -447,7 +447,7 @@ test.describe('', () => { provider.response(500, {data: []}); - await component.setProps({ + await component.withProps({ chunkSize, disableObserver: true, shouldPerformDataRender: () => true diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts index 995b8c4a9d..ddd1988b1f 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts @@ -35,7 +35,7 @@ test.describe('', () => { type: 'div', attrs: { id: 'renderNext', - '@click': () => (>document.querySelector('.b-virtual-scroll')).component?.initLoad() + '@click': () => (>document.querySelector('.b-virtual-scroll')).component?.initLoadNext() } }, @@ -43,7 +43,7 @@ test.describe('', () => { type: 'div', attrs: { id: 'retry', - '@click': () => (>document.querySelector('.b-virtual-scroll')).component?.initLoad() + '@click': () => (>document.querySelector('.b-virtual-scroll')).component?.initLoadNext() } } }); @@ -55,7 +55,7 @@ test.describe('', () => { test.beforeEach(async () => { provider.response(200, () => ({data: state.data.addData(chunkSize)})); - await component.setProps({ + await component.withProps({ chunkSize, disableObserver: true, shouldPerformDataRender: () => true @@ -66,7 +66,7 @@ test.describe('', () => { await component.waitForContainerChildCountEqualsTo(chunkSize); }); - test('Should load and render the next chunk after calling initLoad', async () => { + test('Should load and render the next chunk after calling initLoadNext', async () => { await component.node.locator('#renderNext').click(); test.expect(provider.mock.mock.calls.length).toBe(2); diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts index 720cb3680f..82abf24675 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts @@ -7,123 +7,70 @@ */ /** - * @file Этот файл содержит тест кейсы для проверки функциональности изменения пропов компонентов. + * @file This file contains test cases to verify the functionality of prop changes in components. */ import test from 'tests/config/unit/test'; -import { createTestHelpers, filterEmitterCalls } from 'components/base/b-virtual-scroll/test/api/helpers'; +import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; test.describe('', () => { let component: VirtualScrollTestHelpers['component'], provider: VirtualScrollTestHelpers['provider'], - state: VirtualScrollTestHelpers['state'], - initLoadSpy: VirtualScrollTestHelpers['initLoadSpy']; + state: VirtualScrollTestHelpers['state']; test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); - ({component, provider, state, initLoadSpy} = await createTestHelpers(page)); + ({component, provider, state} = await createTestHelpers(page)); await provider.start(); }); - test.skip('`chunkSize` prop changes after the first chunk has been rendered', () => { - test('Should render the second chunk with the new chunk size', async () => { + test.describe('`chunkSize` prop changes after the first chunk has been rendered', () => { + test('Should render the second chunk with the new chunk size', async ({demoPage}) => { const chunkSize = 12; provider.response(200, () => ({data: state.data.addData(chunkSize)})); - await component.setProps({ - chunkSize, - shouldPerformDataRender: ({isInitialRender, itemsTIllEnd}) => isInitialRender || itemsTIllEnd === 0 - }); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + '@hook:beforeDataCreate': (ctx: bVirtualScroll) => jestMock.spy(ctx.componentFactory, 'produceComponentItems') + }) + .pick(demoPage.buildTestComponent(component.componentName, component.props)); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); await component.waitForContainerChildCountEqualsTo(chunkSize); - - await component.setProps({ - chunkSize: chunkSize * 2 - }); - + await demoPage.updateTestComponent({chunkSize: chunkSize * 2}); await component.scrollToBottom(); + await component.waitForContainerChildCountEqualsTo(chunkSize * 3); + + const + produceSpy = await component.getSpy((ctx) => ctx.componentFactory.produceComponentItems); test.expect(provider.mock.mock.calls.length).toBe(3); + await test.expect(produceSpy.calls).resolves.toHaveLength(2); await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize * 3)).resolves.toBeUndefined(); + await test.expect(component.waitForDataIndexChild(chunkSize * 3 - 1)).resolves.toBeUndefined(); }); }); - test.skip('`request` prop was changed', () => { - test('Should reload the component data', async () => { - const - chunkSize = 12; - - provider - .responseOnce(200, {data: state.data.addData(chunkSize)}) - .responseOnce(200, {data: state.data.addData(chunkSize)}) - .response(200, {data: []}); - - await component.setProps({ - chunkSize, - shouldPerformDataRender: ({isInitialRender, itemsTIllEnd}) => isInitialRender || itemsTIllEnd === 0, - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') + test.skip('`requestQuery`', () => { + test.describe('Prop was changed', () => { + test('Should not reload the entire component', async () => { + // ... }); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); - await component.waitForContainerChildCountEqualsTo(chunkSize); - - await component.setProps({ - get: { - chunkSize: 20 - } + test('Should request the second chunk with the new parameters', async () => { + // ... }); - - await component.waitForDataIndexChild(chunkSize * 2 - 1); - - const - spy = await component.getSpy((ctx) => ctx.emit), - calls = filterEmitterCalls(await spy.calls, true, ['initLoadStart', 'initLoad']).map(([event]) => event); - - test.expect(calls).toEqual([ - 'initLoadStart', - 'dataLoadStart', - 'convertDataToDB', - 'initLoad', - 'dataLoadSuccess', - 'renderStart', - 'renderEngineStart', - 'renderEngineDone', - 'domInsertStart', - 'domInsertDone', - 'renderDone', - 'initLoadStart', - 'dataLoadStart', - 'convertDataToDB', - 'initLoad', - 'dataLoadSuccess', - 'renderStart', - 'renderEngineStart', - 'renderEngineDone', - 'domInsertStart', - 'domInsertDone', - 'renderDone' - ]); - - await test.expect(initLoadSpy.calls).resolves.toBe([[], []]); - await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeTruthy(); - }); - }); - - test.skip('`requestQuery` prop was changed', () => { - test('Should not reload the entire component', async () => { - // ... }); - test('Should request the second chunk with the new parameters', async () => { + test('Передает параметры в GET параметры запроса', async () => { // ... }); }); diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts new file mode 100644 index 0000000000..cbc6d5b0b9 --- /dev/null +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts @@ -0,0 +1,171 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * @file Этот файл содержит тест кейсы для проверки функциональности изменения пропов компонентов. + */ + +import test from 'tests/config/unit/test'; + +import { createTestHelpers, filterEmitterCalls } from 'components/base/b-virtual-scroll/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; + +test.describe('', () => { + let + component: VirtualScrollTestHelpers['component'], + provider: VirtualScrollTestHelpers['provider'], + state: VirtualScrollTestHelpers['state'], + initLoadSpy: VirtualScrollTestHelpers['initLoadSpy']; + + test.beforeEach(async ({demoPage, page}) => { + await demoPage.goto(); + + ({component, provider, state, initLoadSpy} = await createTestHelpers(page)); + await provider.start(); + }); + + test.describe('`request` prop was changed', () => { + test('Should reset state and reload the component data', async ({demoPage}) => { + const + chunkSize = [12, 20]; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize[0])}) + .responseOnce(200, {data: state.data.addData(chunkSize[1])}) + .response(200, {data: []}); + + await component + .withDefaultPaginationProviderProps({chunkSize: chunkSize[0]}) + .withProps({ + chunkSize: chunkSize[0], + shouldPerformDataRequest: ({itemsTillEnd}) => itemsTillEnd === 0, + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') + }) + .pick(demoPage.buildTestComponent(component.componentName, component.props)); + + await component.waitForContainerChildCountEqualsTo(chunkSize[0]); + + await demoPage.updateTestComponent({ + request: { + get: { + chunkSize: chunkSize[1] + } + }, + chunkSize: chunkSize[1] + }); + + await component.waitForDataIndexChild(chunkSize[1] - 1); + + const + spy = await component.getSpy((ctx) => ctx.emit), + calls = filterEmitterCalls(await spy.calls, true, ['initLoadStart', 'initLoad']).map(([event]) => event); + + test.expect(calls).toEqual([ + 'initLoadStart', + 'dataLoadStart', + 'convertDataToDB', + 'initLoad', + 'dataLoadSuccess', + 'renderStart', + 'renderEngineStart', + 'renderEngineDone', + 'domInsertStart', + 'domInsertDone', + 'renderDone', + 'resetState', + 'initLoadStart', + 'dataLoadStart', + 'convertDataToDB', + 'initLoad', + 'dataLoadSuccess', + 'renderStart', + 'renderEngineStart', + 'renderEngineDone', + 'domInsertStart', + 'domInsertDone', + 'renderDone' + ]); + + await test.expect(initLoadSpy.calls).resolves.toEqual([[], []]); + await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize[1])).resolves.toBeUndefined(); + }); + }); + + ['reset', 'reset.silence', 'reset.load', 'reset.load.silence'].forEach((event, i) => { + test.describe(`Случилось событие ${event}`, () => { + test('Should reset state and reload the component data', async () => { + const + chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: []}); + + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + shouldPerformDataRequest: ({itemsTillEnd}) => itemsTillEnd === 0, + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') + }) + .build(); + + await component.waitForDataIndexChild(chunkSize - 1); + await component.component.evaluate((ctx, [event]) => ctx.unsafe.globalEmitter.emit(event), [event]); + await component.waitForDataIndexChild(chunkSize * 2 - 1); + + const + spy = await component.getSpy((ctx) => ctx.emit), + calls = filterEmitterCalls(await spy.calls, true, ['initLoadStart', 'initLoad']).map(([event]) => event); + + test.expect(calls).toEqual([ + 'initLoadStart', + 'dataLoadStart', + 'convertDataToDB', + 'initLoad', + 'dataLoadSuccess', + 'renderStart', + 'renderEngineStart', + 'renderEngineDone', + 'domInsertStart', + 'domInsertDone', + 'renderDone', + 'resetState', + 'initLoadStart', + 'dataLoadStart', + 'convertDataToDB', + 'initLoad', + 'dataLoadSuccess', + 'renderStart', + 'renderEngineStart', + 'renderEngineDone', + 'domInsertStart', + 'domInsertDone', + 'renderDone' + ]); + + const initLoadArgs = [ + [[], []], + [[], [undefined, {silent: true}]], + [[], []], + [[], [undefined, {silent: true}]] + ]; + + await test.expect(initLoadSpy.calls).resolves.toEqual(initLoadArgs[i]); + await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); + }); + }); + }); + +}); + +// * `reset` - reloads all data providers (including the tied storage and router); +// * `reset.silence` - reloads all data providers (including the tied storage and router) in silent mode; +// * `reset.load` - reloads the tied data providers; +// * `reset.load.silence` - reloads the tied data providers in silent mode; diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts index 5d46242220..481c18380b 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts @@ -34,7 +34,7 @@ test.describe('', () => { type: 'div', attrs: { id: 'retry', - '@click': () => (>document.querySelector('.b-virtual-scroll')).component?.initLoad() + '@click': () => (>document.querySelector('.b-virtual-scroll')).component?.initLoadNext() } } }); @@ -49,7 +49,7 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); - await component.setProps({chunkSize}); + await component.withProps({chunkSize}); await component.withDefaultPaginationProviderProps({chunkSize}); await component.build(); @@ -66,7 +66,7 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); - await component.setProps({ + await component.withProps({ chunkSize, '@onRequestError': (_, retryFn) => setTimeout(retryFn, 0) }); @@ -86,7 +86,7 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); - await component.setProps({chunkSize}); + await component.withProps({chunkSize}); await component.withDefaultPaginationProviderProps({chunkSize}); await component.build(); @@ -109,12 +109,9 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); - await component.withDefaultPaginationProviderProps({chunkSize}); - - await component.setProps({ - chunkSize, - shouldPerformDataRender: ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 - }); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({chunkSize}); await component.build(); await component.waitForContainerChildCountEqualsTo(chunkSize); @@ -139,12 +136,9 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(providerChunkSize)}) .response(200, {data: []}); - await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); - - await component.setProps({ - chunkSize, - shouldPerformDataRender: ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 - }); + await component + .withDefaultPaginationProviderProps({chunkSize: providerChunkSize}) + .withProps({chunkSize}); await component.build(); await component.node.locator('#retry').click(); @@ -166,7 +160,7 @@ test.describe('', () => { .responseOnce(500, {}) .response(200, {data: []}); - await component.setProps({chunkSize}); + await component.withProps({chunkSize}); await component.withDefaultPaginationProviderProps({chunkSize}); await component.build(); diff --git a/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss b/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss index 2988d4c196..d69f41d431 100644 --- a/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss +++ b/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss @@ -11,3 +11,10 @@ - include 'components/super/i-static-page/i-static-page.component.ss'|b as placeholder - template index() extends ['i-static-page.component'].index + - block body + < component & + v-if = testComponent | + id = testComponent | + :is = testComponent | + :v-attrs = testComponentAttrs + . \ No newline at end of file diff --git a/src/components/pages/p-v4-components-demo/p-v4-components-demo.ts b/src/components/pages/p-v4-components-demo/p-v4-components-demo.ts index f26cc51114..0c951f7a3b 100644 --- a/src/components/pages/p-v4-components-demo/p-v4-components-demo.ts +++ b/src/components/pages/p-v4-components-demo/p-v4-components-demo.ts @@ -43,6 +43,24 @@ export default class pV4ComponentsDemo extends iStaticPage { @field() someField: unknown = 'foo'; + /** + * Field for tests purposes + */ + @field() + emptyField: unknown = undefined; + + /** + * Name of the test component. + */ + @field() + testComponent?: string; + + /** + * Attributes for the test component. + */ + @field() + testComponentAttrs: Dictionary = {}; + protected beforeCreate(): void { // eslint-disable-next-line no-console console.time('Render'); diff --git a/src/components/pages/p-v4-components-demo/test/api/page.ts b/src/components/pages/p-v4-components-demo/test/api/page.ts index b855094253..4c87124138 100644 --- a/src/components/pages/p-v4-components-demo/test/api/page.ts +++ b/src/components/pages/p-v4-components-demo/test/api/page.ts @@ -6,29 +6,35 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { JSHandle, Page } from 'playwright'; +import type { JSHandle, Locator, Page } from 'playwright'; import { build } from '@config/config'; import { concatURLs } from 'core/url'; import Component from 'tests/helpers/component'; import type bDummy from 'components/dummies/b-dummy/b-dummy'; +import type pV4ComponentsDemo from 'components/pages/p-v4-components-demo/p-v4-components-demo'; +import { expandedStringify } from 'core/prelude/test-env/components/json'; /** * Page object: provides an API to work with `DemoPage` */ export default class DemoPage { - /** {@link Page} */ readonly page: Page; /** - * Server base url + * Server base URL */ readonly baseUrl: string; /** - * Returns an initial page name + * Page component reference. + */ + component?: JSHandle; + + /** + * Returns the initial page name */ get pageName(): string { return ''; @@ -46,8 +52,13 @@ export default class DemoPage { * Opens a demo page */ async goto(): Promise { + const + root = this.page.locator('#root-component'); + await this.page.goto(concatURLs(this.baseUrl, `${build.demoPage()}.html`), {waitUntil: 'networkidle'}); - await this.page.waitForSelector('#root-component', {state: 'attached'}); + await root.waitFor({state: 'attached'}); + + this.component = await root.evaluateHandle((ctx) => ctx.component); return this; } @@ -58,4 +69,49 @@ export default class DemoPage { async createDummy(): Promise> { return Component.createComponent(this.page, 'b-dummy'); } + + /** + * Renders a test component with the specified parameters on the page. + * + * Unlike {@link Component.createComponent}, the component is already present in the page template, + * which means it depends on the context of the page and can react to changes in reactive properties of its parents. + * + * Using a test component that is already embedded in the template can be useful when you need to test the component's + * reaction to changes in parent properties that are passed as props to the component. + * + * @param name - The name of the test component. + * @param attrs - The attributes for the test component. + */ + async buildTestComponent(name: string, attrs?: Dictionary): Promise { + if (!this.component) { + throw new ReferenceError('Missing `DemoPage` component'); + } + + const + serializedAttrs = expandedStringify(attrs ?? {}); + + await this.component.evaluate((ctx, [name, attrs]) => { + ctx.testComponent = name; + ctx.testComponentAttrs = globalThis.expandedParse(attrs); + }, [name, serializedAttrs]); + + return this.page.locator('#testComponent'); + } + + /** + * Updates the state of the test component. + * @param attrs + */ + async updateTestComponent(attrs?: Dictionary): Promise { + if (!this.component) { + throw new ReferenceError('Missing `DemoPage` component'); + } + + const + serializedAttrs = expandedStringify(attrs ?? {}); + + return this.component.evaluate((ctx, [attrs]) => { + Object.assign(ctx.testComponentAttrs, globalThis.expandedParse(attrs)); + }, [serializedAttrs]); + } } diff --git a/src/core/prelude/test-env/components/json.ts b/src/core/prelude/test-env/components/json.ts index 790ae84b39..ff935e9bec 100644 --- a/src/core/prelude/test-env/components/json.ts +++ b/src/core/prelude/test-env/components/json.ts @@ -100,3 +100,5 @@ export function expandedParse(str: string): T { return val; }); } + +globalThis.expandedParse = expandedParse; diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index 06a6be58de..7efa3101f9 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -11,6 +11,7 @@ import type { JSHandle, Locator, Page } from 'playwright'; import { resolve } from '@pzlr/build-core'; import { Component, DOM, Utils } from 'tests/helpers'; +import type ComponentObject from 'tests/helpers/component-object'; import type iBlock from 'components/super/i-block/i-block'; @@ -68,6 +69,13 @@ export default abstract class ComponentObjectBuilder { */ protected componentStore?: JSHandle; + /** + * The component styles that should be inserted into the page. + */ + get componentStyles(): CanUndef { + return undefined; + } + /** * A shorthand for generating selectors for component elements. * {@link DOM.elNameSelectorGenerator} @@ -138,6 +146,10 @@ export default abstract class ComponentObjectBuilder { * using the `setProps` and `setChildren` methods. */ async build(): Promise> { + if (this.componentStyles != null) { + await this.page.addStyleTag({content: this.componentStyles}); + } + this.componentStore = await Component.createComponent(this.page, this.componentName, { attrs: { ...this.props @@ -150,10 +162,10 @@ export default abstract class ComponentObjectBuilder { /** * Picks the `Node` with the provided selector and extracts the `component` property, - * which will be assigned to the `component` property of the `ComponentObject`. + * which will be assigned to the {@link ComponentObject.component}. * - * After this operation, the `ComponentObject` will be marked as built and the `ComponentObject.component` property - * will be accessible. + * After this operation, the `ComponentObject` will be marked as built + * and the {@link ComponentObject.component} property will be accessible. * * @param selector - The selector or locator for the component node */ @@ -161,27 +173,41 @@ export default abstract class ComponentObjectBuilder { /** * Extracts the `component` property from the provided locator, - * which will be assigned to the `component` property of the `ComponentObject`. + * which will be assigned to the {@link ComponentObject.component}. * - * After this operation, the `ComponentObject` will be marked as built and the `ComponentObject.component` property - * will be accessible. + * After this operation, the `ComponentObject` will be marked as built + * and the {@link ComponentObject.component} property will be accessible. * * @param locator - The locator for the component node */ async pick(locator: Locator): Promise; - async pick(selectorOrLocator: string | Locator): Promise { + /** + * Waits for promise to resolve and extracts the `component` property from the provided locator, + * which will be assigned to the {@link ComponentObject.component}. + * + * After this operation, the `ComponentObject` will be marked as built + * and the {@link ComponentObject.component} property will be accessible. + * + * @param locatorPromise - The promise that resolves to locator for the component node + */ + async pick(locatorPromise: Promise): Promise; + + async pick(selectorOrLocator: string | Locator | Promise): Promise { + if (this.componentStyles != null) { + await this.page.addStyleTag({content: this.componentStyles}); + } + + // eslint-disable-next-line no-nested-ternary const locator = Object.isString(selectorOrLocator) ? this.page.locator(selectorOrLocator) : - selectorOrLocator; + Object.isPromise(selectorOrLocator) ? await selectorOrLocator : selectorOrLocator; this.componentStore = await locator.elementHandle().then(async (el) => { await el?.evaluate((ctx, [id]) => ctx.setAttribute('data-test-id', id), [this.id]); return el?.getProperty('component'); }); - await this.applyProps(this.props); - return this; } @@ -191,12 +217,9 @@ export default abstract class ComponentObjectBuilder { * * @param props - The props to set */ - async setProps(props: Dictionary): Promise { + withProps(props: Dictionary): this { if (!this.isBuilded) { Object.assign(this.props, props); - - } else { - await this.applyProps(props); } return this; @@ -212,20 +235,4 @@ export default abstract class ComponentObjectBuilder { Object.assign(this.children, children); return this; } - - /** - * Applies the stored props to the component instance. - * @param props - */ - async applyProps(props: Dictionary): Promise { - const - {component} = this; - - if (!this.isBuilded) { - return this.setProps(props); - } - - await component.evaluate((ctx, [props]) => Object.assign(ctx, props), [props]); - return this; - } } From 8e27ca8deb4862a5877f2e853335e24e5b3aa75c Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sun, 9 Jul 2023 22:07:18 +0300 Subject: [PATCH 055/159] WIP docs --- .../base/b-virtual-scroll/README.md | 185 +++++++++++++++--- 1 file changed, 160 insertions(+), 25 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 454ec1959a..de7f846825 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -64,30 +64,70 @@ Each `b-dummy` component receives the `name` and `type` props, which are derived Data loading in progress ``` -### Перезагрузка компонента +### Converting Data to the Required Format -Для перезагрузки компонента b-virtual-scroll можно использовать несколько вариантов: +The `b-virtual-scroll` component expects data in a specific format: -1. Вызвав `bVirtualScroll.initLoad` с аргументами `[data: any, {silent: false}]`; -2. Вызвав `bVirtualScroll.reload`; -3. Изменив `request` проп. +```typescript +interface VirtualScrollDb { + data: unknown[]; +} +``` -Во всех этих случаях жизненный цикл компонент будет сброшен на изначальное состояние и компонент начнет отрисовку -новых данных которые будет получать, очистив все предидущие. +The `data` array should contain the data items used to render the components. +### Rendering on Click -### Should-like функции +In addition to the standard scroll-based loading, you can implement on-demand loading. -### Переопределение `itemsFactory` +To achieve this, you need to disable the observer module, allow component rendering, and use the special `initLoadNext` method. -### Отрисовка по клику +``` +< b-virtual-scroll & + :disableObserver = true | + :shouldPerformDataRender = () => true | + ref = scroll +. +``` +```typescript +class pSomePage { + @watch('something') + onSomething() { + this.$refs.scroll.initLoadNext(); + } +} +``` + +Additionally, for ease of implementation, when you need to load and render data on a button click, the `renderNext` slot is available. It will be displayed only when the component is not loading data, the last load did not result in an error, and the component's lifecycle is not completed. In combination with the `initLoadNext` method, this allows for easy implementation of lazy rendering on button click. + +``` +< b-virtual-scroll & + :disableObserver = true | + :shouldPerformDataRender = () => true | + ref = scroll +. + < template #renderNext + < .&__render-next @click = $refs.scroll.initLoadNext + Render next +``` + +### Component Reload + +To reload the `b-virtual-scroll` component, you have several options: + +1. Call `bVirtualScroll.initLoad()`. +2. Call `bVirtualScroll.reload()`. +3. Modify the `request` prop. +4. Trigger a global event of type `reset`. + +In all of these cases, the component's lifecycle will be reset to its initial state, and the component will start rendering new data, discarding any previous data. ## Slots The component supports a bunch of slots to provide. -1. `loader` предоставляют возможность отображать различный контент (обычно скелетоны) пока загружаются данные. +1. The `loader` slot allows you to display different content (usually skeletons) while the data is being loaded. ``` < b-virtual-scroll @@ -96,7 +136,7 @@ The component supports a bunch of slots to provide. Data loading in progress ``` -2. `tombstone` предоставляет возможность отображать различный контент который будет повторяться `tombstonesSize` (обычно скелетоны) количество раз пока загружаются данные. +2. The `tombstone` slot allows you to display different content (usually skeletons) that will be repeated `tombstonesSize` times while the data is being loaded. ``` < b-virtual-scroll :tombstonesSize = 3 @@ -105,7 +145,7 @@ The component supports a bunch of slots to provide. Skeleton ``` -3. `retry` предоставляет возможность отображать различный контент (обычно призыв перезагрузить данные) когда произошла ошибка загрузки данных. +3. The `retry` slot allows you to display different content (usually a prompt to retry loading data) when there is an error in data loading. ``` < b-virtual-scroll @@ -114,7 +154,7 @@ The component supports a bunch of slots to provide. Retry last request ``` -4. `empty` предоставляет возможность отображать различный контент когда компонента получил порцию пустых данных при первоначальной загрузке. +4. The `empty` slot allows you to display different content when the component receives an empty data set during the initial loading. ``` < b-virtual-scroll @@ -123,7 +163,7 @@ The component supports a bunch of slots to provide. No data ``` -5. `done` предоставляет возможность отображать различный контент когда компонента завершил загрузку всех данных и так же все их отрисовал. +5. The `done` slot allows you to display different content when the component has finished loading and rendering all the data. ``` < b-virtual-scroll @@ -132,12 +172,12 @@ The component supports a bunch of slots to provide. Load and render complete ``` -6. `renderNext` предоставляет возможность отображать различный контент когда компонента не загружает данные и не перешел в состояние окончания лайфцикла. -Данный слот может быть полезен если необходимо реализовать ленивую отрисовку контента по клику. +6. The `renderNext` slot allows you to display different content when the component is not loading data and has not entered the lifecycle completion state. +This slot can be useful when implementing lazy content rendering on button click. ``` < b-virtual-scroll - < template #render-next + < template #renderNext < .&__render-next Render next ``` @@ -307,16 +347,111 @@ Note: The `tombstone` component is used to represent empty or unloaded component The `bVirtualScroll` class extends `iData` and includes additional properties related to slots, component state, and observers. Please refer to the documentation of `iData` for more details on those properties. -## Миграция с `b-virtual-scroll` версии 3.x.x +## Migration from `b-virtual-scroll` version 3.x.x -## Deep dive into component +### API -### Жизненный цикл +- Prop `renderGap` -> `shouldPerformDataRender`. +- Props with `option-like` -> `iItems` props. +- Method `getDataStateSnapshot` -> `getComponentState`. +- Method `reloadLast` -> `initLoadNext`. +- `VirtualItemEl` interface is removed. Now, the client receives a single data item in the `iItems` methods. To maintain logic with `current`, `prev`, `next`, you can use the following approach: -### renderGuard +```typescript +function getProps(data: DataInterface, index: number): Dictionary { + const + state = this.$refs.scroll.getComponentState(); + + const + current = data, + prev = state.data[index - 1], + next = state.data[index + 1]; +} +``` + +- Interface `DataState` -> `VirtualScrollState`: + - `DataState.currentPage` -> `VirtualScrollState.loadPage`; + - `DataState.lastLoadedChunk.raw` -> `VirtualScrollState.lastLoadedRaw`; + - etc. + +## Deep dive into the component + +### Lifecycle + +The component's lifecycle consists of several events and states. When the component is initialized and starts its initial data loading, it emits two events: `initLoadStart` and `dataLoadStart`. The `initLoadStart` event is a standard event emitted by every component and occurs each time the component's data is initially loaded. The `dataLoadStart` event is emitted for every data loading. + +1. `initLoadStart` - The initial data loading of the component has started. +2. `dataLoadStart` - The data loading of the component has started. + +After successful data loading, the following events are emitted: + +1. `convertDataToDB` - The data conversion has been performed. +2. `initLoad` - The initial data loading of the component has completed. +3. `dataLoadSuccess` - The data loading of the component has completed. + +When the `convertDataToDB` event is emitted, the component's state is already updated with the `lastLoadedRawData` field. The `initLoad` and `dataLoadSuccess` events are emitted after updating the component's state, including `VirtualScrollState.data`, `VirtualScrollState.loadPage`, and some other fields. + +After successful data loading, the component consults the `shouldStopRequestingData` method to determine whether it should stop loading further data. -### Модули компонента +Next, the component invokes the `renderGuard` to determine if the data can be rendered or not. If the `renderGuard` allows rendering, the following events are emitted: -### Переопределение в дочерних слоях +1. `renderStart` - The component rendering has started. +2. `renderEngineStart` - The component rendering using the rendering engine has started. +3. `renderEngineDone` - The component rendering using the rendering engine has completed. +4. `domInsertStart` - The DOM insertion has started. +5. `domInsertDone` - The DOM insertion has completed. This event is asynchronous as it uses RAF (Request Animation Frame) for DOM insertion. +6. `renderDone` - The component rendering has finished. -## Возможные улучшения и дальнейшие эксперименты \ No newline at end of file +Afterward, the component waits for user actions, specifically when the user sees any component on the page. The component then calls the + +`shouldPerformDataRequest` or `shouldPerformDataRender` functions on the client side, depending on the availability of data. This process repeats until all data has been loaded and rendered. + +1. `lifecycleDone` - Occurs when all data has been loaded and rendered on the page. + +### `renderGuard` and `loadDataOrPerformRender` + +The `b-virtual-scroll` component relies on the `renderGuard` and `loadDataOrPerformRender` functions to determine whether to render data, load data, or complete the component's lifecycle. + +The `loadDataOrPerformRender` function is the entry point for the data loading and rendering cycle. This function consults the `renderGuard`, which determines whether data can be rendered based on the data state and provides reasons for rejection. + +The logic of `renderGuard` is as follows: + +```mermaid +graph TD + A[renderGuard] -->|"dataSlice.length = 0"| B["return { result: false, reason: renderGuardRejectionReason.done }"] + A -->|"dataSlice.length < chunkSize"| C["return { result: false, reason: renderGuardRejectionReason.notEnoughData }"] + A -->|"state.isInitialRender"| D["return { result: true }"] + A -->|"default"| E["clientResponse = this.shouldPerformDataRender?.(state, this) || true"] + E -->|"clientResponse = false"| F["return { result: clientResponse, reason: renderGuardRejectionReason.noPermission }"] + E -->|"default"| G["return { result: clientResponse }"] +``` + +The logic of `loadDataOrPerformRender` is as follows: + +```mermaid +graph TB + A[loadDataOrPerformRender] -->|Get component state| B[Is the last request errored?] + B -- True --> X[return] + B -- False ---> C[renderGuard] + C -- If Render Guard Result is True --> D[performRender] + D --> X + C -- If Render Guard Result is False --> E[Check the Render Guard Rejection Reason] + E -- reason=done --> F[onLifecycleDone] + F --> X + E -- reason=noData --> G[isRequestsStopped?] + G -- True --> X + G -- False --> H[shouldPerformDataRequest?] + H -- True --> I["call loadNext"] + I --> X + H -- False --> X + E -- reason=notEnoughData --> J[isRequestsStopped?] + J -- True --> K[performRender and onLifecycleDone] + K --> X + J -- False --> L[shouldPerformDataRequest] + L -- True --> M["call loadNext"] + M --> X + L -- False --> N[initial render?] + N -- True --> P[performRender] + N -- False --> X + P --> X +``` From 9fd65f3522e91eae03e12b2d2c0e96432c6db3a3 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sun, 9 Jul 2023 22:23:56 +0300 Subject: [PATCH 056/159] :art: --- .../base/b-virtual-scroll/README.md | 64 +++++++++---------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index de7f846825..070bd46a09 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -14,8 +14,6 @@ See the implemented modifiers or the parent component. ## Events -### События компонента - | EventName | Description | Payload description | Payload | | ------------------------------- | --------------------------------------------------------------- | --------------------------------------------- | --------------------------- | | `dataLoadSuccess` | Data loading has succeeded. | `data: object[], isInitialLoading: boolean` | `[data, isInitialLoading]` | @@ -38,6 +36,18 @@ Also, you can see the implemented traits or the parent component. ## Usage +### Converting Data to the Required Format + +The `b-virtual-scroll` component expects data in a specific format: + +```typescript +interface VirtualScrollDb { + data: unknown[]; +} +``` + +The `data` array should contain the data items used to render the components. + ### Rendering Components In this example: @@ -64,19 +74,7 @@ Each `b-dummy` component receives the `name` and `type` props, which are derived Data loading in progress ``` -### Converting Data to the Required Format - -The `b-virtual-scroll` component expects data in a specific format: - -```typescript -interface VirtualScrollDb { - data: unknown[]; -} -``` - -The `data` array should contain the data items used to render the components. - -### Rendering on Click +### Rendering on click In addition to the standard scroll-based loading, you can implement on-demand loading. @@ -292,9 +290,11 @@ Pagination example: ```typescript const requestQuery = (state: VirtualScrollState): Dictionary => { return { - page: state.loadPage, - limit: 10 - // Other pagination parameters + get: { + page: state.loadPage, + limit: 10 + // Other pagination parameters + } }; }; ``` @@ -417,13 +417,18 @@ The `loadDataOrPerformRender` function is the entry point for the data loading a The logic of `renderGuard` is as follows: ```mermaid -graph TD - A[renderGuard] -->|"dataSlice.length = 0"| B["return { result: false, reason: renderGuardRejectionReason.done }"] - A -->|"dataSlice.length < chunkSize"| C["return { result: false, reason: renderGuardRejectionReason.notEnoughData }"] - A -->|"state.isInitialRender"| D["return { result: true }"] - A -->|"default"| E["clientResponse = this.shouldPerformDataRender?.(state, this) || true"] - E -->|"clientResponse = false"| F["return { result: clientResponse, reason: renderGuardRejectionReason.noPermission }"] - E -->|"default"| G["return { result: clientResponse }"] +graph TB + A["renderGuard"] -->|Get chunk size and next data slice| B["Is the data slice length = 0?"] + B -- True --> C["Are requests stopped?"] + C -- True --> E["Return: result=false, reason=done"] + E --> X["Function ends"] + C -- False --> F["Return: result=false, reason=noData"] + B -- False --> G["Is the data slice smaller than chunk size?"] + G -- True --> H["Return: result=false, reason=notEnoughData"] + G -- False --> I["Is it initial render?"] + I -- True --> J["Return: result=true"] + I -- False --> K["Get client response from shouldPerformDataRender"] + K --> L["Return: result=clientResponse, reason=noPermission if clientResponse is false"] ``` The logic of `loadDataOrPerformRender` is as follows: @@ -434,24 +439,15 @@ graph TB B -- True --> X[return] B -- False ---> C[renderGuard] C -- If Render Guard Result is True --> D[performRender] - D --> X C -- If Render Guard Result is False --> E[Check the Render Guard Rejection Reason] E -- reason=done --> F[onLifecycleDone] - F --> X E -- reason=noData --> G[isRequestsStopped?] - G -- True --> X G -- False --> H[shouldPerformDataRequest?] H -- True --> I["call loadNext"] - I --> X - H -- False --> X E -- reason=notEnoughData --> J[isRequestsStopped?] J -- True --> K[performRender and onLifecycleDone] - K --> X J -- False --> L[shouldPerformDataRequest] L -- True --> M["call loadNext"] - M --> X L -- False --> N[initial render?] N -- True --> P[performRender] - N -- False --> X - P --> X ``` From fd40c7ebd630cf2b534479c73692bea19034c3f0 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sun, 9 Jul 2023 22:26:48 +0300 Subject: [PATCH 057/159] :art: --- src/components/base/b-virtual-scroll/README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 070bd46a09..0776861f2a 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -437,17 +437,17 @@ The logic of `loadDataOrPerformRender` is as follows: graph TB A[loadDataOrPerformRender] -->|Get component state| B[Is the last request errored?] B -- True --> X[return] - B -- False ---> C[renderGuard] - C -- If Render Guard Result is True --> D[performRender] + B -- False ---> C["renderGuard()"] + C -- If Render Guard Result is True --> D["performRender()"] C -- If Render Guard Result is False --> E[Check the Render Guard Rejection Reason] - E -- reason=done --> F[onLifecycleDone] + E -- reason=done --> F["onLifecycleDone()"] E -- reason=noData --> G[isRequestsStopped?] - G -- False --> H[shouldPerformDataRequest?] - H -- True --> I["call loadNext"] + G -- False --> H["shouldPerformDataRequest()"] + H -- True --> I["initLoadNext()"] E -- reason=notEnoughData --> J[isRequestsStopped?] - J -- True --> K[performRender and onLifecycleDone] - J -- False --> L[shouldPerformDataRequest] - L -- True --> M["call loadNext"] + J -- True --> K["performRender() and onLifecycleDone()"] + J -- False --> L["shouldPerformDataRequest()"] + L -- True --> M["initLoadNext()"] L -- False --> N[initial render?] - N -- True --> P[performRender] + N -- True --> P["performRender()"] ``` From 385262058b5f9d3900c71d8420e8a26f99db05fb Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 10 Jul 2023 10:56:15 +0300 Subject: [PATCH 058/159] :wrench: --- .../test/unit/functional/rendering/default.ts | 4 -- .../test/unit/scenario/props.ts | 40 ++++++++++++++----- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts index bbcd4f1793..7b6436ebad 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts @@ -63,10 +63,6 @@ test.describe('', () => { await test.expect(component.childList).toHaveCount(chunkSize * 3); }); - test.skip('Should render components with child', async () => { - // .. - }); - test.describe('With a different chunk size for each render cycle', () => { test('Should render 6 components first, then 12, then 18', async () => { const chunkSize = [6, 12, 18]; diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts index 82abf24675..f2e47cae7e 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts @@ -15,6 +15,8 @@ import test from 'tests/config/unit/test'; import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { Route } from 'playwright'; +import { fromQueryString } from 'core/url'; test.describe('', () => { let @@ -59,19 +61,35 @@ test.describe('', () => { }); }); - test.skip('`requestQuery`', () => { - test.describe('Prop was changed', () => { - test('Should not reload the entire component', async () => { - // ... - }); + test.describe('`requestQuery`', () => { + test('Should pass the parameters to the GET parameters of the request', async () => { + const + chunkSize = 12; - test('Should request the second chunk with the new parameters', async () => { - // ... - }); - }); + provider.response(200, () => ({data: state.data.addData(chunkSize)})); + + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + requestQuery: () => ({get: {param1: 'param1'}}), + shouldPerformDataRequest: () => false, + '@hook:beforeDataCreate': (ctx: bVirtualScroll) => jestMock.spy(ctx.componentFactory, 'produceComponentItems') + }) + .build(); - test('Передает параметры в GET параметры запроса', async () => { - // ... + await component.waitForContainerChildCountEqualsTo(chunkSize); + + const + providerCalls = provider.mock.mock.calls, + query = fromQueryString(new URL((providerCalls[0][0]).request().url()).search); + + test.expect(providerCalls).toHaveLength(1); + test.expect(query).toEqual({ + param1: 'param1', + chunkSize: 12, + id: test.expect.anything() + }); }); }); }); From 0e5b3266b0e14dd1bff3731025c1615729eefcf1 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 10 Jul 2023 11:24:32 +0300 Subject: [PATCH 059/159] :wrench: --- src/components/base/b-virtual-scroll/TODO.md | 8 ---- .../base/b-virtual-scroll/b-virtual-scroll.ts | 8 +++- .../base/b-virtual-scroll/handlers.ts | 38 ++++++++----------- .../base/b-virtual-scroll/interface/common.ts | 21 ++++++++++ .../b-virtual-scroll/interface/component.ts | 2 + .../observer/engines/intersection-observer.ts | 6 +-- .../b-virtual-scroll/modules/slots/index.ts | 2 +- src/components/base/b-virtual-scroll/props.ts | 14 +++---- .../test/api/component-object/index.ts | 4 +- .../initialization/initialization.ts | 2 +- .../test/unit/scenario/props.ts | 4 +- 11 files changed, 59 insertions(+), 50 deletions(-) diff --git a/src/components/base/b-virtual-scroll/TODO.md b/src/components/base/b-virtual-scroll/TODO.md index 1494fe7bce..c890aec5f8 100644 --- a/src/components/base/b-virtual-scroll/TODO.md +++ b/src/components/base/b-virtual-scroll/TODO.md @@ -1,18 +1,10 @@ -- PartialRender ? Если указано к отрисовке 10 компонентов а загружено за раз данных 5 - рисовать ли эти 5 и потом еще 5 -> для версии 1.1.0 - Обработка ошибок тесты -- Проверка стейта во время ошибок -- typedLocalEmitter можно ли избавиться и как-то нормально типизировать события компонента - улучшить имена интерфейсов (MountedItem, MountedSeparator, ревью имен в ComponentState) -- протыкать все метода на использование (удалить неиспользуемые) - dbChange -- описать что состояние типа loadPage, renderPage обновляются постфактум (после того как загрузка данных случилась, после того как рендеринг случился) -- миграция с prev/current/next описать - тесты на dbConverter - debug модуль овверайд в edadeal/core - описа про мок модуль, calls и ссылки и как это побеждать -- reset.load - перезагрузит ли компонент? написать тест кейсы на перезагрузки с помощью reload, изменения request, вызова reset.load - описать в документации про тестовый компонент в демо странице и для чего он нужен -- сделать хэндлеры и модули протектед - импорты наладить порядок ```mermaid diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts index 8fa6d43279..06590eb69f 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts @@ -15,10 +15,10 @@ import VDOM, { create, render } from 'components/friends/vdom'; import { bVirtualScrollDomInsertAsyncGroup, renderGuardRejectionReason } from 'components/base/b-virtual-scroll/const'; -import iData, { $$, component, RequestParams } from 'components/super/i-data/i-data'; +import iData, { $$, component, RequestParams, UnsafeGetter } from 'components/super/i-data/i-data'; import { iVirtualScrollHandlers } from 'components/base/b-virtual-scroll/handlers'; import type { AsyncOptions } from 'core/async'; -import type { VirtualScrollState, RenderGuardResult } from 'components/base/b-virtual-scroll/interface'; +import type { VirtualScrollState, RenderGuardResult, UnsafeBVirtualScroll } from 'components/base/b-virtual-scroll/interface'; export * from 'components/base/b-virtual-scroll/interface'; export * from 'components/base/b-virtual-scroll/const'; @@ -46,6 +46,10 @@ export default class bVirtualScroll extends iVirtualScrollHandlers { }; } + override get unsafe(): UnsafeGetter> { + return Object.cast(this); + } + override reload(...args: Parameters): ReturnType { this.componentStatus = 'loading'; return super.reload(...args); diff --git a/src/components/base/b-virtual-scroll/handlers.ts b/src/components/base/b-virtual-scroll/handlers.ts index ec0c82bc31..3e92978db1 100644 --- a/src/components/base/b-virtual-scroll/handlers.ts +++ b/src/components/base/b-virtual-scroll/handlers.ts @@ -24,7 +24,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: component reset event. * Resets the component state to its initial state. */ - onReset(): void { + protected onReset(): void { this.componentInternalState.reset(); this.observer.reset(); @@ -37,7 +37,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: render start event. * Triggered when the component rendering starts. */ - onRenderStart(): void { + protected onRenderStart(): void { this.componentEmitter.emit(componentEvents.renderStart); } @@ -45,7 +45,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: render engine start event. * Triggered when the component rendering using the rendering engine starts. */ - onRenderEngineStart(): void { + protected onRenderEngineStart(): void { this.componentEmitter.emit(componentEvents.renderEngineStart); } @@ -53,7 +53,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: render engine done event. * Triggered when the component rendering using the rendering engine is completed. */ - onRenderEngineDone(): void { + protected onRenderEngineDone(): void { this.componentEmitter.emit(componentEvents.renderEngineDone); } @@ -63,7 +63,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * * @param childList */ - onDomInsertStart(this: bVirtualScroll, childList: MountedChild[]): void { + protected onDomInsertStart(this: bVirtualScroll, childList: MountedChild[]): void { this.componentInternalState.updateDataCursor(); this.componentInternalState.updateMounted(childList); this.componentInternalState.setIsInitialRender(false); @@ -76,7 +76,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: DOM insert done event. * Triggered when the insertion of rendered components into the DOM tree is completed. */ - onDomInsertDone(): void { + protected onDomInsertDone(): void { this.componentEmitter.emit(componentEvents.domInsertDone); } @@ -84,7 +84,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: render done event. * Triggered when rendering is completed. */ - onRenderDone(): void { + protected onRenderDone(): void { this.componentEmitter.emit(componentEvents.renderDone); } @@ -92,7 +92,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: lifecycle done event. * Triggered when the internal lifecycle of the component is completed. */ - onLifecycleDone(this: bVirtualScroll): void { + protected onLifecycleDone(this: bVirtualScroll): void { const state = this.getComponentState(); @@ -111,7 +111,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * * @param data - The converted data. */ - onConvertDataToDB(data: unknown): void { + protected onConvertDataToDB(data: unknown): void { this.componentInternalState.setRawLastLoaded(data); this.componentEmitter.emit(componentEvents.convertDataToDB, data); } @@ -122,7 +122,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * * @param isInitialLoading - Indicates whether it is an initial component loading. */ - onDataLoadStart(isInitialLoading: boolean): void { + protected onDataLoadStart(isInitialLoading: boolean): void { this.componentInternalState.setIsLoadingInProgress(true); this.componentInternalState.setIsLastErrored(false); this.slotsStateController.loadingProgressState(isInitialLoading); @@ -138,7 +138,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * @param data - The loaded data. * @throws {@link ReferenceError} if the loaded data does not have a "data" field. */ - onDataLoadSuccess(this: bVirtualScroll, isInitialLoading: boolean, data: unknown): void { + protected onDataLoadSuccess(this: bVirtualScroll, isInitialLoading: boolean, data: unknown): void { this.componentInternalState.setIsLoadingInProgress(false); if (!Object.isPlainObject(data) || !Array.isArray(data.data)) { @@ -174,7 +174,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * * @param isInitialLoading - Indicates whether it is an initial component loading. */ - onDataLoadError(isInitialLoading: boolean): void { + protected onDataLoadError(isInitialLoading: boolean): void { this.componentInternalState.setIsLoadingInProgress(false); this.componentInternalState.setIsLastErrored(true); this.slotsStateController.loadingFailedState(); @@ -182,7 +182,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { this.componentEmitter.emit(componentEvents.dataLoadError, isInitialLoading); } - override onRequestError(this: bVirtualScroll, ...args: Parameters): ReturnType { + protected override onRequestError(this: bVirtualScroll, ...args: Parameters): ReturnType { const err = args[0]; @@ -201,7 +201,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: data empty event. * Triggered when the loaded data is empty. */ - onDataEmpty(): void { + protected onDataEmpty(): void { this.slotsStateController.emptyState(); this.componentEmitter.emit(componentEvents.dataEmpty); @@ -211,18 +211,10 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: component enters the viewport. * @param component - The component that enters the viewport. */ - onElementEnters(this: bVirtualScroll, component: MountedChild): void { + protected onElementEnters(this: bVirtualScroll, component: MountedChild): void { this.componentInternalState.setMaxViewedIndex(component); this.loadDataOrPerformRender(); this.componentEmitter.emit(componentEvents.elementEnter, component); } - - /** - * Handler: component leaves the viewport. - * @param _component - The component that leaves the viewport. - */ - onElementOut(_component: MountedChild): void { - // ... - } } diff --git a/src/components/base/b-virtual-scroll/interface/common.ts b/src/components/base/b-virtual-scroll/interface/common.ts index 50283b0517..24bcc718bd 100644 --- a/src/components/base/b-virtual-scroll/interface/common.ts +++ b/src/components/base/b-virtual-scroll/interface/common.ts @@ -8,6 +8,7 @@ import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; import type { VirtualScrollState } from 'components/base/b-virtual-scroll/interface/component'; +import type { UnsafeIData } from 'components/super/i-data/i-data'; /** * Interface representing the response of the client to the `renderGuard` method for rendering requests. @@ -75,3 +76,23 @@ export interface RenderGuardRejectionReason { export interface ShouldPerform { (state: VirtualScrollState, ctx: bVirtualScroll): RES; } + +// @ts-ignore (extend) +export interface UnsafeBVirtualScroll extends UnsafeIData { + // @ts-ignore (access) + onRenderEngineStart: CTX['onRenderEngineStart']; + // @ts-ignore (access) + onRenderEngineDone: CTX['onRenderEngineDone']; + // @ts-ignore (access) + onElementEnters: CTX['onElementEnters']; + // @ts-ignore (access) + componentEmitter: CTX['componentEmitter']; + // @ts-ignore (access) + slotsStateController: CTX['slotsStateController']; + // @ts-ignore (access) + componentInternalState: CTX['componentInternalState']; + // @ts-ignore (access) + componentFactory: CTX['componentFactory']; + // @ts-ignore (access) + observer: CTX['observer']; +} diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts index cc900b7add..1f78ec13e0 100644 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -301,6 +301,8 @@ export interface ComponentRefs { renderNext?: HTMLElement; } +export type $ComponentRefs = ComponentRefs & Dictionary; + /** * The type of data stored by the component. */ diff --git a/src/components/base/b-virtual-scroll/modules/observer/engines/intersection-observer.ts b/src/components/base/b-virtual-scroll/modules/observer/engines/intersection-observer.ts index 7937255afa..7b975945e4 100644 --- a/src/components/base/b-virtual-scroll/modules/observer/engines/intersection-observer.ts +++ b/src/components/base/b-virtual-scroll/modules/observer/engines/intersection-observer.ts @@ -20,7 +20,8 @@ export default class IoObserver extends Friend implements ObserverEngine { override readonly C!: bVirtualScroll; /** - * @inheritdoc + * {@link ObserverEngine.watchForIntersection} + * @param components */ watchForIntersection(components: MountedChild[]): void { const @@ -36,9 +37,6 @@ export default class IoObserver extends Friend implements ObserverEngine { } } - /** - * @inheritdoc - */ reset(): void { this.async.clearAll({group: new RegExp(observerAsyncGroup)}); } diff --git a/src/components/base/b-virtual-scroll/modules/slots/index.ts b/src/components/base/b-virtual-scroll/modules/slots/index.ts index 126f9c7bfc..e6f08dd433 100644 --- a/src/components/base/b-virtual-scroll/modules/slots/index.ts +++ b/src/components/base/b-virtual-scroll/modules/slots/index.ts @@ -156,7 +156,7 @@ export class SlotsStateController extends Friend { protected setDisplayState(name: keyof SlotsStateObj, state: boolean): void { const ref = this.ctx.$refs[name]; - if (ref) { + if (ref instanceof HTMLElement) { ref.style.display = state ? '' : 'none'; } } diff --git a/src/components/base/b-virtual-scroll/props.ts b/src/components/base/b-virtual-scroll/props.ts index 761f1bf0ca..aaeb54489c 100644 --- a/src/components/base/b-virtual-scroll/props.ts +++ b/src/components/base/b-virtual-scroll/props.ts @@ -19,7 +19,7 @@ import type { ComponentItemFactory, ComponentItemType, ComponentStrategy, - ComponentRefs + $ComponentRefs } from 'components/base/b-virtual-scroll/interface'; @@ -247,24 +247,24 @@ export default abstract class iVirtualScrollProps extends iData implements iItem /** {@link componentTypedEmitter} */ @system((ctx) => componentTypedEmitter(ctx)) - readonly componentEmitter!: ComponentTypedEmitter; + protected readonly componentEmitter!: ComponentTypedEmitter; /** {@link SlotsStateController} */ @system((ctx) => new SlotsStateController(ctx)) - readonly slotsStateController!: SlotsStateController; + protected readonly slotsStateController!: SlotsStateController; /** {@link ComponentInternalState} */ @system((ctx) => new ComponentInternalState(ctx)) - readonly componentInternalState!: ComponentInternalState; + protected readonly componentInternalState!: ComponentInternalState; /** {@link ComponentFactory} */ @system((ctx) => new ComponentFactory(ctx)) - readonly componentFactory!: ComponentFactory; + protected readonly componentFactory!: ComponentFactory; /** {@link Observer} */ @system((ctx) => new Observer(ctx)) - readonly observer!: Observer; + protected readonly observer!: Observer; - protected override readonly $refs!: iData['$refs'] & ComponentRefs; + protected override readonly $refs!: iData['$refs'] & $ComponentRefs; } diff --git a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts index bb2355315f..2a5377d087 100644 --- a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts @@ -19,7 +19,7 @@ import { testStyles } from 'components/base/b-virtual-scroll/test/api/component- /** * The component object API for testing the {@link bVirtualScroll} component. */ -export class VirtualScrollComponentObject extends ComponentObject { +export class VirtualScrollComponentObject extends ComponentObject { /** * The locator for the container ref. */ @@ -122,7 +122,7 @@ export class VirtualScrollComponentObject extends ComponentObject', () => { state: VirtualScrollTestHelpers['state']; const hookProp = { - '@hook:beforeDataCreate': (ctx: bVirtualScroll) => { + '@hook:beforeDataCreate': (ctx: bVirtualScroll['unsafe']) => { const original = ctx.componentInternalState.compile.bind(ctx.componentInternalState); diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts index f2e47cae7e..df0a66d308 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts @@ -42,7 +42,7 @@ test.describe('', () => { .withDefaultPaginationProviderProps({chunkSize}) .withProps({ chunkSize, - '@hook:beforeDataCreate': (ctx: bVirtualScroll) => jestMock.spy(ctx.componentFactory, 'produceComponentItems') + '@hook:beforeDataCreate': (ctx: bVirtualScroll['unsafe']) => jestMock.spy(ctx.componentFactory, 'produceComponentItems') }) .pick(demoPage.buildTestComponent(component.componentName, component.props)); @@ -74,7 +74,7 @@ test.describe('', () => { chunkSize, requestQuery: () => ({get: {param1: 'param1'}}), shouldPerformDataRequest: () => false, - '@hook:beforeDataCreate': (ctx: bVirtualScroll) => jestMock.spy(ctx.componentFactory, 'produceComponentItems') + '@hook:beforeDataCreate': (ctx: bVirtualScroll['unsafe']) => jestMock.spy(ctx.componentFactory, 'produceComponentItems') }) .build(); From e99d388d65878006083783c406290548e43ab761 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 10 Jul 2023 12:21:43 +0300 Subject: [PATCH 060/159] :wrench: --- .../base/b-virtual-scroll/README.md | 59 +++++++++++++++++-- src/components/base/b-virtual-scroll/TODO.md | 3 - .../test/unit/scenario/props.ts | 20 +++++++ 3 files changed, 75 insertions(+), 7 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 0776861f2a..87c9bfeaf7 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -1,6 +1,6 @@ # components/base/b-virtual-scroll -TBD +The `b-virtual-scroll` component is designed for rendering a larger array of various data. ## Synopsis @@ -412,9 +412,10 @@ Afterward, the component waits for user actions, specifically when the user sees The `b-virtual-scroll` component relies on the `renderGuard` and `loadDataOrPerformRender` functions to determine whether to render data, load data, or complete the component's lifecycle. -The `loadDataOrPerformRender` function is the entry point for the data loading and rendering cycle. This function consults the `renderGuard`, which determines whether data can be rendered based on the data state and provides reasons for rejection. +The `loadDataOrPerformRender` function is the entry point for the data loading and rendering cycle. +This function consults the `renderGuard`, which determines whether data can be rendered based on the data state and provides reasons for rejection only if it has not permitted the rendering. -The logic of `renderGuard` is as follows: +Understanding `renderGuard`: ```mermaid graph TB @@ -431,7 +432,7 @@ graph TB K --> L["Return: result=clientResponse, reason=noPermission if clientResponse is false"] ``` -The logic of `loadDataOrPerformRender` is as follows: +Understanding `loadDataOrPerformRender`: ```mermaid graph TB @@ -451,3 +452,53 @@ graph TB L -- False --> N[initial render?] N -- True --> P["performRender()"] ``` + +### Overriding in Child Layers + +The main use case for overriding in child layers is to modify the default behavior of functions or methods. + +For example, it may be useful to override the logic of `shouldStopRequestingData` if you want to implement a default logic that takes into account the `total` field of the response when making a decision. + +There may also be situations where you need to modify the `renderGuard`. Currently, the component loads data until the number of items reaches the `chunkSize` and then renders them. By overriding the `renderGuard`, you can achieve partial rendering, where the component renders the available data regardless of whether it reaches the `chunkSize`. + +## What's Next + +The component currently lacks some features that may improve its functionality and make it more suitable for different scenarios. + +### Streaming Data Rendering + +- Planned for implementation. + +There is a request for streaming data rendering from the server. +This can be implemented using the standard V4 `dataProvider` API, but it requires further modifications to the component to handle streaming data events. + +### Alternative Approach to Component Rendering + +- Planned as an experiment. + +Currently, the component uses the `iBlock.vdom` API, which creates a new rendering engine instance for each chunk. +It is hypothetically possible to reuse the rendering engine instead. However, there are challenges to consider. +For example, the Vue 3 rendering engine removes previously rendered DOM nodes and destroys components when attempting to use the rendering function and `forceUpdate` with a different VNode to render. + +### Partial Rendering (can be achieved easily through `renderGuard`) + +- Not planned for implementation. + +Currently, the component loads data until the number of items reaches the `chunkSize` and then renders them. By overriding the `renderGuard`, you can achieve partial rendering, where the component renders the available data regardless of whether it reaches the `chunkSize`. + +### Updating Nodes in the DOM Tree (describe implementation challenges, component allows inserting different components) + +- Planned as an experiment. + +Currently, `b-virtual-scroll` does not remove old nodes when rendering new chunks within the same lifecycle. Implementing this feature is not a priority, but it should not be ignored either. The main reasons why this feature was not included in the initial release are: + +- Previous experiments showed no performance degradation after rendering and inserting 30x(5-8) components into the DOM tree. +- The inability to reuse DOM nodes: typical components allow reusing DOM nodes, but `b-virtual-scroll` enables clients to easily render different components. It is important to note that reusing DOM nodes provides the greatest benefit, not just simple insertion/removal of entire sections from the DOM tree. +- The need to implement two-way data rendering: Since memory is limited, storing a large number of rendered components in memory is not ideal. This requires destroying previously rendered components and then rendering them again. However, this approach can cause delays when scrolling back up. +- Since scroll events need to be used to render data, additional heuristics or props indicating the scroll direction and the number of columns being rendered may need to be added to correctly maintain the node map. + +### Integration with RTX + +- High priority. + +Why have `b-virtual-scroll` without RTX? diff --git a/src/components/base/b-virtual-scroll/TODO.md b/src/components/base/b-virtual-scroll/TODO.md index c890aec5f8..417f3149c6 100644 --- a/src/components/base/b-virtual-scroll/TODO.md +++ b/src/components/base/b-virtual-scroll/TODO.md @@ -1,7 +1,4 @@ -- Обработка ошибок тесты - улучшить имена интерфейсов (MountedItem, MountedSeparator, ревью имен в ComponentState) -- dbChange -- тесты на dbConverter - debug модуль овверайд в edadeal/core - описа про мок модуль, calls и ссылки и как это побеждать - описать в документации про тестовый компонент в демо странице и для чего он нужен diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts index df0a66d308..26fe9f0b0e 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts @@ -92,4 +92,24 @@ test.describe('', () => { }); }); }); + + test.describe('`dbConverter`', () => { + test('Should convert data to the component', async () => { + const + chunkSize = 12; + + provider.response(200, () => ({data: {nestedData: state.data.addData(chunkSize)}})); + + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + shouldPerformDataRequest: () => false, + dbConverter: ({data: {nestedData}}) => ({data: nestedData}) + }) + .build(); + + await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); + }); + }); }); From a8d0de5245af319087c1f60768b3a645b5ae25e9 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 10 Jul 2023 12:26:35 +0300 Subject: [PATCH 061/159] Remove redundant props --- .../base/b-virtual-scroll/README.md | 1 - src/components/base/b-virtual-scroll/const.ts | 23 +--------- .../b-virtual-scroll/interface/component.ts | 44 ------------------- .../base/b-virtual-scroll/interface/events.ts | 6 --- .../modules/factory/engines/force-update.ts | 16 ------- .../b-virtual-scroll/modules/factory/index.ts | 11 ++--- .../modules/observer/engines/scroll.ts | 35 --------------- .../modules/observer/index.ts | 9 +--- src/components/base/b-virtual-scroll/props.ts | 28 +----------- 9 files changed, 7 insertions(+), 166 deletions(-) delete mode 100644 src/components/base/b-virtual-scroll/modules/factory/engines/force-update.ts delete mode 100644 src/components/base/b-virtual-scroll/modules/observer/engines/scroll.ts diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 87c9bfeaf7..c796b6509c 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -24,7 +24,6 @@ See the implemented modifiers or the parent component. | `lifecycleDone` | All component data is rendered and loaded. | | `[]` | | `convertDataToDB` | Trigger data conversion to the `DB`. | `data: unknown` | `[data]` | | `elementEnter` | The element has entered the viewport. | `componentItem: MountedChild` | `[componentItem]` | -| `elementOut` | The element has exited the viewport. | `componentItem: MountedChild` | `[componentItem]` | | `renderStart` | Rendering of items has started. | | `[]` | | `renderDone` | Rendering of items has finished. | | `[]` | | `renderEngineStart` | Rendering of items has started with the render engine. | | `[]` | diff --git a/src/components/base/b-virtual-scroll/const.ts b/src/components/base/b-virtual-scroll/const.ts index 2f74bde573..fb33588848 100644 --- a/src/components/base/b-virtual-scroll/const.ts +++ b/src/components/base/b-virtual-scroll/const.ts @@ -15,9 +15,7 @@ import type { ComponentLifecycleEvents, ComponentObserverLocalEvents, ComponentRenderLocalEvents, - ComponentRenderStrategy, VirtualScrollState, - ComponentStrategy, RenderGuardRejectionReason } from 'components/base/b-virtual-scroll/interface'; @@ -32,24 +30,6 @@ export const bVirtualScrollAsyncGroup = 'b-virtual-scroll'; */ export const bVirtualScrollDomInsertAsyncGroup = `${bVirtualScrollAsyncGroup}:dom-insert`; -/** - * {@link ComponentRenderStrategy} - */ -export const componentRenderStrategy: ComponentRenderStrategy = { - default: 'default', - reuse: 'reuse' -}; - -/** - * {@link ComponentStrategy} - */ -export const componentStrategy: ComponentStrategy = { - intersectionObserver: 'intersectionObserver', - scroll: 'scroll', - scrollWithDropNodes: 'scrollWithDropNodes', - scrollWithRecycleNodes: 'scrollWithRecycleNodes' -}; - /** * {@link ComponentDataLocalEvents} */ @@ -85,8 +65,7 @@ export const componentRenderLocalEvents: ComponentRenderLocalEvents = { * {@link ComponentObserverLocalEvents} */ export const componentObserverLocalEvents: ComponentObserverLocalEvents = { - elementEnter: 'elementEnter', - elementOut: 'elementOut' + elementEnter: 'elementEnter' }; export const componentEvents = { diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts index 1f78ec13e0..f072d2a610 100644 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -8,50 +8,6 @@ import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -/** - * Render strategy for producing the components. - */ -export interface ComponentRenderStrategy { - /** - * An approach that reuses the current instance of the rendering engine whenever a new rendering is performed. - */ - reuse: 'reuse'; - - /** - * The default approach, which creates a new instance of the rendering engine each time a new rendering is performed. - */ - default: 'default'; -} - -/** - * Strategies for component operation modes. - */ -export interface ComponentStrategy { - /** - * Strategy where element visibility is determined using `intersectionObserver`. - * Nodes will not be removed from the DOM tree. - */ - intersectionObserver: 'intersectionObserver'; - - /** - * Strategy where element visibility is determined by listening to the `scroll` event. - * Nodes will not be removed from the DOM tree. - */ - scroll: 'scroll'; - - /** - * Strategy where element visibility is determined by listening to the `scroll` event. - * Nodes will be removed from and returned to the DOM tree. - */ - scrollWithDropNodes: 'scrollWithDropNodes'; - - /** - * Strategy where element visibility is determined by listening to the `scroll` event. - * Nodes from the DOM tree will be recycled. - */ - scrollWithRecycleNodes: 'scrollWithRecycleNodes'; -} - /** * Component state. * diff --git a/src/components/base/b-virtual-scroll/interface/events.ts b/src/components/base/b-virtual-scroll/interface/events.ts index 73c17698c4..8d9a94c194 100644 --- a/src/components/base/b-virtual-scroll/interface/events.ts +++ b/src/components/base/b-virtual-scroll/interface/events.ts @@ -97,11 +97,6 @@ export interface ComponentObserverLocalEvents { * The element has entered the viewport. */ elementEnter: 'elementEnter'; - - /** - * The element has exited the viewport. - */ - elementOut: 'elementOut'; } /** @@ -128,7 +123,6 @@ export interface LocalEventPayloadMap { [componentLocalEvents.convertDataToDB]: [data: unknown]; [componentObserverLocalEvents.elementEnter]: [componentItem: MountedChild]; - [componentObserverLocalEvents.elementOut]: [componentItem: MountedChild]; [componentRenderLocalEvents.renderStart]: []; [componentRenderLocalEvents.renderDone]: []; diff --git a/src/components/base/b-virtual-scroll/modules/factory/engines/force-update.ts b/src/components/base/b-virtual-scroll/modules/factory/engines/force-update.ts deleted file mode 100644 index 473c648e53..0000000000 --- a/src/components/base/b-virtual-scroll/modules/factory/engines/force-update.ts +++ /dev/null @@ -1,16 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -/** - * Renders the provided `VNodes` to the `HTMLElements` via `$forceUpdate` and single render engine instance. - * - * @param _args - */ -export function render(..._args: any[]): HTMLElement[] { - return []; -} diff --git a/src/components/base/b-virtual-scroll/modules/factory/index.ts b/src/components/base/b-virtual-scroll/modules/factory/index.ts index 2e32c3d911..498ef8d291 100644 --- a/src/components/base/b-virtual-scroll/modules/factory/index.ts +++ b/src/components/base/b-virtual-scroll/modules/factory/index.ts @@ -11,9 +11,8 @@ import type { VNodeDescriptor } from 'components/friends/vdom'; import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; import type { ComponentItem, MountedChild, MountedItem } from 'components/base/b-virtual-scroll/interface'; -import { componentItemType, componentRenderStrategy } from 'components/base/b-virtual-scroll/const'; +import { componentItemType } from 'components/base/b-virtual-scroll/const'; -import * as forceUpdate from 'components/base/b-virtual-scroll/modules/factory/engines/force-update'; import * as vdomRender from 'components/base/b-virtual-scroll/modules/factory/engines/vdom'; /** @@ -89,17 +88,13 @@ export class ComponentFactory extends Friend { const {ctx} = this; - let res; ctx.onRenderEngineStart(); - if (ctx.componentRenderStrategy === componentRenderStrategy.reuse) { - res = forceUpdate.render(ctx, descriptors); - - } else { + const res = vdomRender.render(ctx, descriptors); - } ctx.onRenderEngineDone(); + return res; } } diff --git a/src/components/base/b-virtual-scroll/modules/observer/engines/scroll.ts b/src/components/base/b-virtual-scroll/modules/observer/engines/scroll.ts deleted file mode 100644 index 9307af79a0..0000000000 --- a/src/components/base/b-virtual-scroll/modules/observer/engines/scroll.ts +++ /dev/null @@ -1,35 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { MountedChild } from 'components/base/b-virtual-scroll/interface'; -import { observerAsyncGroup } from 'components/base/b-virtual-scroll/modules/observer/const'; -import type { ObserverEngine } from 'components/base/b-virtual-scroll/modules/observer/interface'; -import Friend from 'components/friends/friend'; - -export default class ScrollObserver extends Friend implements ObserverEngine { - - /** - * {@link bVirtualScroll} - */ - override readonly C!: bVirtualScroll; - - /** - * @inheritdoc - */ - watchForIntersection(_components: MountedChild[]): void { - // ... - } - - /** - * @inheritdoc - */ - reset(): void { - this.async.clearAll({group: new RegExp(observerAsyncGroup)}); - } -} diff --git a/src/components/base/b-virtual-scroll/modules/observer/index.ts b/src/components/base/b-virtual-scroll/modules/observer/index.ts index 585019fed1..dc1c8d663a 100644 --- a/src/components/base/b-virtual-scroll/modules/observer/index.ts +++ b/src/components/base/b-virtual-scroll/modules/observer/index.ts @@ -8,12 +8,10 @@ import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; import type { MountedChild } from 'components/base/b-virtual-scroll/interface'; -import ScrollObserver from 'components/base/b-virtual-scroll/modules/observer/engines/scroll'; import IoObserver from 'components/base/b-virtual-scroll/modules/observer/engines/intersection-observer'; import Friend from 'components/friends/friend'; export { default as IoObserver } from 'components/base/b-virtual-scroll/modules/observer/engines/intersection-observer'; -export { default as ScrollObserver } from 'components/base/b-virtual-scroll/modules/observer/engines/scroll'; /** * Observer class for `bVirtualScroll` component. @@ -24,9 +22,8 @@ export class Observer extends Friend { /** * The observation engine used by the Observer. - * It can be either an {@link IoObserver} or {@link ScrollObserver} instance. */ - protected engine: IoObserver | ScrollObserver; + protected engine: IoObserver; /** * @param ctx - The `bVirtualScroll` component instance. @@ -34,9 +31,7 @@ export class Observer extends Friend { constructor(ctx: bVirtualScroll) { super(ctx); - this.engine = ctx.componentStrategy === 'intersectionObserver' ? - new IoObserver(ctx) : - new ScrollObserver(ctx); + this.engine = new IoObserver(ctx); } /** diff --git a/src/components/base/b-virtual-scroll/props.ts b/src/components/base/b-virtual-scroll/props.ts index aaeb54489c..5fb38d201d 100644 --- a/src/components/base/b-virtual-scroll/props.ts +++ b/src/components/base/b-virtual-scroll/props.ts @@ -13,22 +13,18 @@ import type { VirtualScrollState, ComponentDb, - ComponentRenderStrategy, RequestQueryFn, ShouldPerform, ComponentItemFactory, ComponentItemType, - ComponentStrategy, $ComponentRefs } from 'components/base/b-virtual-scroll/interface'; import { - componentRenderStrategy, defaultShouldProps, - componentItemType, - componentStrategy + componentItemType } from 'components/base/b-virtual-scroll/const'; @@ -136,28 +132,6 @@ export default abstract class iVirtualScrollProps extends iData implements iItem override readonly DB!: ComponentDb; - /** - * The rendering strategy of components. - * Determines which approach will be taken for rendering components within the rendering engine. - * - * * `default` - The default approach, - * which creates a new instance of the rendering engine each time a new rendering is performed. - * - * * `reuse` - An approach - * that reuses the current instance of the rendering engine whenever a new rendering is performed. - * - * {@link ComponentRenderStrategy} - */ - @prop({type: String, validator: (v) => Object.isString(v) && componentRenderStrategy.hasOwnProperty(v)}) - readonly componentRenderStrategy: keyof ComponentRenderStrategy = componentRenderStrategy.default; - - /** - * Strategies for component operation modes. - * {@link ComponentStrategy} - */ - @prop({type: String, validator: (v) => Object.isString(v) && componentStrategy.hasOwnProperty(v)}) - readonly componentStrategy: keyof ComponentStrategy = componentStrategy.intersectionObserver; - /** * Function that returns the GET parameters for a request. This function is called for each request. It receives the * current component state and should return the request parameters. These parameters are merged with the parameters From 2216700a92e7ae008dfd601c5c35ca4b745eb5e3 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 10 Jul 2023 12:45:48 +0300 Subject: [PATCH 062/159] :wrench: --- tests/helpers/component-object/README.md | 101 +++++++++++++++++++++++ tests/helpers/component-object/mock.ts | 2 +- 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 tests/helpers/component-object/README.md diff --git a/tests/helpers/component-object/README.md b/tests/helpers/component-object/README.md new file mode 100644 index 0000000000..d2a4f1d3d6 --- /dev/null +++ b/tests/helpers/component-object/README.md @@ -0,0 +1,101 @@ +# tests/helpers/component-object + +The `ComponentObject` is a base class for testing components. The `component object` pattern allows for a more convenient way to interact with components in a testing environment. + +This class can be used as a generic class for any component or can be extended to create a custom `component object` that implements methods for interacting with a specific component. + +The class provides a universal API for generating a component and setting up mock functions and spy functions. + +## Usage + +### Builder + +#### Basic + +```typescript +import ComponentObjectBuilder from 'path/to/ComponentObjectBuilder'; + +class MyComponentObject extends ComponentObjectBuilder { + // Implement specific methods and properties for interacting with the component during tests +} + +// Create an instance of MyComponentObject +const myComponent = new MyComponentObject(page, 'MyComponent'); + +// Build the component +await myComponent.build(); + +// Access the component +const component = myComponent.component; + +// Perform interactions with the component +await component.evaluate((ctx) => { + // Perform actions on the component +}); +``` + +### Mock + +#### Basic + +```typescript +import ComponentObject from 'path/to/component-object'; + +class MyComponentObject extends ComponentObject { + // Implement specific methods and properties for testing the component in a mock environment +} + +// Create an instance of MyComponentObject +const myComponent = new MyComponentObject(page, 'MyComponent'); + +// Create a spy +const spy = await myComponent.spyOn('someMethod'); + +// Access the component +const component = myComponent.component; + +// Perform interactions with the component +await component.evaluate((ctx) => { + // Perform actions on the component +}); + +// Access the spy +console.log(await spy.calls); + +// Create a mock function +const mockFn = await myComponent.mockFn((arg) => { + // Mock function logic +}); +``` + +#### Spy on emitter + +```typescript +import ComponentObject from 'path/to/component-object'; + +class MyComponentObject extends ComponentObject { + // Implement specific methods and properties for testing the component in a mock environment +} + +// Create an instance of MyComponentObject +const myComponent = new MyComponentObject(page, 'MyComponent'); + +// Create a spy +myComponent.setProps({ + '@hook:beforeDataCreate': (ctx) => jest.spy(ctx, 'emit') +}); + +// Access the component +const component = myComponent.component; + +// Perform interactions with the component +await component.evaluate((ctx) => { + // Perform actions on the component +}); + +// Access the spy +const + spy = await myComponent.getSpy((ctx) => ctx.emit) + +console.log(await spy.calls); +``` diff --git a/tests/helpers/component-object/mock.ts b/tests/helpers/component-object/mock.ts index 2d88ac8d84..80f35062ca 100644 --- a/tests/helpers/component-object/mock.ts +++ b/tests/helpers/component-object/mock.ts @@ -14,7 +14,7 @@ import type { SpyOptions } from 'tests/helpers/component-object/interface'; import type { SpyExtractor, SpyObject } from 'tests/helpers/mock/interface'; /** - * The `ComponentObjectMock` class extends the `ComponentObjectBuilder` class + * The {@link ComponentObjectMock} class extends the {@link ComponentObjectBuilder} class * and provides additional methods for creating spies and mock functions. * * It is used for testing components in a mock environment. From 15fefca2cb65b9dda6b8dbd82fdbb4e3d8adb843 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 10 Jul 2023 12:54:16 +0300 Subject: [PATCH 063/159] :art: docs --- tests/helpers/mock/README.md | 3 + tests/helpers/providers/interceptor/README.md | 129 ++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 tests/helpers/mock/README.md create mode 100644 tests/helpers/providers/interceptor/README.md diff --git a/tests/helpers/mock/README.md b/tests/helpers/mock/README.md new file mode 100644 index 0000000000..8d9fc4a2dd --- /dev/null +++ b/tests/helpers/mock/README.md @@ -0,0 +1,3 @@ +# tests/helpers/mock + +This module provides utility functions for working with `jest-mock` and `Playwright`. diff --git a/tests/helpers/providers/interceptor/README.md b/tests/helpers/providers/interceptor/README.md new file mode 100644 index 0000000000..8ea8c11739 --- /dev/null +++ b/tests/helpers/providers/interceptor/README.md @@ -0,0 +1,129 @@ +# tests/helpers/providers/interceptor + +API that provides a simple way to intercept and respond to any request. + +## Usage + +```typescript +// Create a RequestInterceptor instance +const interceptor = new RequestInterceptor(page, /api/); + +// Set a response for one request using a response handler function +interceptor.responseOnce(async (route, request) => { + // Delay the response for 1 second + await delay(1000); + + // Fulfill the request with a custom response + return route.fulfill({ + status: 200, + body: JSON.stringify({ message: 'Success' }), + contentType: 'application/json' + }); +}); + +// Set a response for every request using a response status and payload +interceptor.response(500, { error: 'Server Error' }); + +// Start the request interception +await interceptor.start(); + +// Make a request to the intercepted route +const response = await page.goto('https://example.com/api/data'); + +// Log the response status +console.log(response.status()); // 200 + +// Log the response body +console.log(await response.json()); // { message: 'Success' } + +// Stop the request interception +await interceptor.stop(); +``` + +```typescript +// Create a RequestInterceptor instance +const interceptor = new RequestInterceptor(page, /api/); + +// Set a response for one request using a response status and payload +interceptor.responseOnce(200, { message: 'OK' }); + +// Set a response for every request using a response handler function +interceptor.response(async (route, request) => { + // Add a delay of 500 milliseconds to each response + await delay(500); + + // Fulfill the request with a custom response + return route.fulfill({ + status: 404, + body: JSON.stringify({ error: 'Not Found' }), + contentType: 'application/json' + }); +}); + +// Start the request interception +await interceptor.start(); + +// Make multiple requests to the intercepted route +const response1 = await page.goto('https://example.com/api/data'); +const response2 = await page.goto('https://example.com/api/users'); + +// Log the response status +console.log(response1.status()); // 200 +console.log(response2.status()); // 404 + +// Log the response body +console.log(await response1.json()); // { message: 'OK' } +console.log(await response2.json()); // { error: 'Not Found' } + +// Stop the request interception +await interceptor.stop(); +``` + +```typescript + +// Create a RequestInterceptor instance +const interceptor = new RequestInterceptor(page, /api/); + +// Set a response for one request using a response handler function +interceptor.responseOnce(async (route, request) => { + // Delay the response for 1 second + await delay(1000); + + // Fulfill the request with a custom response + return route.fulfill({ + status: 200, + body: JSON.stringify({ message: 'Success' }), + contentType: 'application/json' + }); +}); + +// Set a response for every request using a response status and payload +interceptor.response(500, { error: 'Server Error' }); + +// Start the request interception +await interceptor.start(); + +// Make a request to the intercepted route +const response = await page.goto('https://example.com/api/data?param1=param1&chunkSize=12&id=tttt'); + +// Log the response status +console.log(response.status()); // 200 + +// Log the response body +console.log(await response.json()); // { message: 'Success' } + +// Stop the request interception +await interceptor.stop(); + +const + providerCalls = interceptor.mock.mock.calls, + query = fromQueryString(new URL((providerCalls[0][0]).request().url()).search); + +test.expect(providerCalls).toHaveLength(1); +test.expect(query).toEqual({ + param1: 'param1', + chunkSize: 12, + id: test.expect.anything() +}); + +``` \ No newline at end of file From 154f0fbab2571719cfcf6a52ae2eb1694a28a790 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 10 Jul 2023 13:04:41 +0300 Subject: [PATCH 064/159] :wrench: --- components-lock.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/components-lock.json b/components-lock.json index 3c3764d558..425b1fc065 100644 --- a/components-lock.json +++ b/components-lock.json @@ -1,9 +1,5 @@ { -<<<<<<< HEAD - "hash": "f276ed6ff1bb9ba1d1af98bc3a06e4c12b73c653879637c37d2936ad8f3d4de9", -======= - "hash": "17bfa44323e931383d4a43be899be3cf78ecf604c7aa870b764c3f3097aac294", ->>>>>>> 70c96906914896dd28da57e07cb7887491af6eb2 + "hash": "2183515ca30acdcc3fbd34f8abea947dd980fdfdf2b258921d3ad019b4ef9ed2", "data": { "%data": "%data:Map", "%data:Map": [ From ab447449a4a88bdf77341f26e63eec75dce83545 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 10 Jul 2023 13:05:21 +0300 Subject: [PATCH 065/159] :wrench: --- src/core/shims/ssr/request-idle-callback.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/shims/ssr/request-idle-callback.js b/src/core/shims/ssr/request-idle-callback.js index 8c416e6e12..fe3cd4a652 100644 --- a/src/core/shims/ssr/request-idle-callback.js +++ b/src/core/shims/ssr/request-idle-callback.js @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -/* eslint-disable no-var, vars-on-top */ +/* eslint-disable no-var */ var GLOBAL = require('core/shims/global'); From 04cf61bea27ac48fe9c6f9703b522a779f58eb39 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 10 Jul 2023 13:45:25 +0300 Subject: [PATCH 066/159] :art: --- CHANGELOG.md | 11 ++ index.d.ts | 3 + .../base/b-virtual-scroll/CHANGELOG.md | 172 ++++++++++++++++++ .../base/b-virtual-scroll/README.md | 15 +- tests/helpers/component-object/README.md | 33 ++++ tests/helpers/mock/index.ts | 2 + 6 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 src/components/base/b-virtual-scroll/CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f36eed943..d11091a63c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,17 @@ Changelog _Note: Gaps between patch versions are faulty, broken or test releases._ +## v4.0.0-beta.? (2023-??-??) + +#### :boom: Breaking Change + +* `b-virtual-scroll` мажорное обновление. Посмотрите readme компонента чтобы ознакомиться с изменениями и миграционным гайдом `components/base/b-virtual-scroll`. + +#### :rocket: New Feature + +* Добавлено новое тестовое API `ComponentObject` которые позволяет более удобно организовать общение с компонентов в тестовой среде `test/helpers/component-object`; +* Добавлено новое тестовое API для мока и реализации наблюдения за какими-либо функциями в рантайме `test/helpers/mock` + ## v4.0.0-beta.8 (2023-07-07) #### :bug: Bug Fix diff --git a/index.d.ts b/index.d.ts index 0ed18e864c..d1b3446c87 100644 --- a/index.d.ts +++ b/index.d.ts @@ -135,6 +135,9 @@ declare var mock: import('jest-mock').ModuleMocker['fn']; }; +/** + * Результат который возвращает мок или spy функций от `jestMock` в свойстве returns + */ interface JestMockResult { type: 'throw' | 'return'; value: VAL; diff --git a/src/components/base/b-virtual-scroll/CHANGELOG.md b/src/components/base/b-virtual-scroll/CHANGELOG.md new file mode 100644 index 0000000000..011c9aa47d --- /dev/null +++ b/src/components/base/b-virtual-scroll/CHANGELOG.md @@ -0,0 +1,172 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## 4.??.?? (2023-??-??) + +#### :boom: Breaking Change + +* Major update. Visit [readme](./readme) to see migration guide + +## v3.30.1 (2022-10-25) + +#### :bug: Bug Fix + +* Fixed an issue with wrong arguments was provided into `getItemKey` + +## v3.18.2 (2022-03-22) + +#### :bug: Bug Fix + +* Fixed an issue with `initLoad` race condition + +## v3.15.4 (2022-01-24) + +#### :boom: Breaking Change + +* The event`chunkRenderStart` is renamed to `chunkRender:renderStart` and now it emits before a component driver renders components + +#### :rocket: New Feature + +* Added new events `chunkRender:*` + +## v3.9.0 (2021-11-08) + +#### :rocket: New Feature + +* [Added a new event `chunkRenderStart`](https://github.com/V4Fire/Client/issues/651) +* [Added `pageNumber` in `chunkLoaded` event](https://github.com/V4Fire/Client/issues/651) + +## v3.0.0-rc.182 (2021-04-28) + +#### :bug: Bug Fix + +* Fixed an issue with `optionKey` being ignored + +## v3.0.0-rc.181 (2021-04-20) + +#### :bug: Bug Fix + +* [Fixed an issue with `itemProps` not being provided to child components](https://github.com/V4Fire/Client/issues/512) + +## v3.0.0-rc.170 (2021-03-26) + +#### :rocket: New Feature + +* Added a new event `chunkRender` + +## v3.0.0-rc.164 (2021-03-22) + +#### :bug: Bug Fix + +* Now `bVirtualScroll` will throw an error if the rendering of components returns an empty array + +## v3.0.0-rc.153 (2021-03-04) + +#### :house: Internal + +* [`bVirtualScroll` is now implements `iItems` trait](https://github.com/V4Fire/Client/issues/471) + +## v3.0.0-rc.151 (2021-03-04) + +#### :house: Internal + +* Downgraded the delay before initializing to `15ms` +* Some optimizations + +## v3.0.0-rc.126 (2021-01-26) + +#### :bug: Bug Fix + +* Added handling of the empty request + +## v3.0.0-rc.122 (2021-01-13) + +#### :house: Internal + +* Removed iItems implementation. [Issue to move back](https://github.com/V4Fire/Client/issues/471) + +## v3.0.0-rc.102 (2020-11-26) + +#### :bug: Bug Fix + +* Fixed an issue with layout shifts after `reInit` + +## v3.0.0-rc.81 (2020-10-08) + +#### :bug: Bug Fix + +* Fixed an issue with `renderNext`: hasn't been data rendering after a loading error + +## v3.0.0-rc.74 (2020-10-06) + +#### :bug: Bug Fix + +* Fixed an issue with removing the progress modifier + +## v3.0.0-rc.68 (2020-09-23) + +#### :bug: Bug Fix + +* [Fixed an issue with the second data batch load affects initial rendering after reInit](https://github.com/V4Fire/Client/issues/346) + +## v3.0.0-rc.60 (2020-09-01) + +#### :bug: Bug Fix + +* [Fixed a possible memory leak](https://github.com/V4Fire/Client/pull/321) + +## v3.0.0-rc.59 (2020-08-10) + +#### :rocket: New Feature + +* [Added ability to render data manually](https://github.com/V4Fire/Client/issues/202) + +#### :nail_care: Polish + +* Improved documentation + +## v3.0.0-rc.39 (2020-07-22) + +#### :rocket: New Feature + +* [Added life cycle events](https://github.com/V4Fire/Client/issues/205) + +#### :bug: Bug Fix + +* [Fixed an issue when data from `lastLoadedData` and `lastLoadedChunk.normalized` aren't synchronized](https://github.com/V4Fire/Client/issues/281) +* [Fixed `lastLoadedChunk.raw` returns undefined](https://github.com/V4Fire/Client/issues/267) + +#### :house: Internal + +* [Refactoring of tests](https://github.com/V4Fire/Client/pull/293) +* [Fixed ESLint warnings](https://github.com/V4Fire/Client/pull/293) + +## v3.0.0-rc.31 (2020-06-17) + +#### :bug: Bug Fix + +* Fixed a problem with the disappearance of loaders before the content was rendered + +## v3.0.0-rc.25 (2020-06-03) + +#### :bug: Bug Fix + +* [Fixed an issue where skeletons disappeared](https://github.com/V4Fire/Client/issues/230) +* [Fixed an issue with a race condition `chunk-request/init`](https://github.com/V4Fire/Client/issues/203) +* [Fixed an issue where an `empty` slot appeared when there was data](https://github.com/V4Fire/Client/issues/259) + +## v3.0.0-rc.19 (2020-05-26) + +#### :bug: Bug Fix + +* [Fixed rendering of truncated data](https://github.com/V4Fire/Client/issues/231) +* [Fixed rendering of empty slot](https://github.com/V4Fire/Client/issues/241) +* [Fixed clear of `in-view`](https://github.com/V4Fire/Client/pull/201) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 7fdc06ac13..317c3b9bfd 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -48,13 +48,24 @@ interface VirtualScrollDb { ``` The `data` array should contain the data items used to render the components. +The `dbConverter` prop allows you to convert data into a format suitable for `b-virtual-scroll` after data has been loaded. + +``` +< b-virtual-scroll & + ... + :dbConverter = (data) => ({data: data.nestedData.data}) +. + < template #loader + < .&__loader + Data loading in progress +``` ### Rendering Components In this example: -- The `b-virtual-scroll` component is used to render a virtual scroll with 12 items loaded at a time. -It interacts with the `Provider` data provider component to fetch the data. The `request` prop is set to `{ get: { chunkSize: 12 } }`, specifying that each request should fetch 12 items. +- The `b-virtual-scroll` component is used to render 12 items per one render cycle. +It interacts with the `Provider` data provider to fetch the data. The `request` prop is set to `{ get: { chunkSize: 12 } }`, specifying that each request should fetch 12 items. - The `requestQuery` function computes additional request parameters based on the component state, specifically the `loadPage` property. These request parameters are merged with the `request` prop. - The `b-virtual-scroll` component renders `b-dummy` components using the `item` prop. Each `b-dummy` component receives the `name` and `type` props, which are derived from the `data` object for each item using the `itemProps` function. diff --git a/tests/helpers/component-object/README.md b/tests/helpers/component-object/README.md index d2a4f1d3d6..dbbd30de5b 100644 --- a/tests/helpers/component-object/README.md +++ b/tests/helpers/component-object/README.md @@ -99,3 +99,36 @@ const console.log(await spy.calls); ``` + +It is important to understand that spy and mock functions store references, not copies, in their `calls` property. + +```typescript +class Component { + currentState: Dictionary = {}; + onStateUpdate: (state: Dictionary) => void; + + get state() { + this.currentState; + } + + constructor(onStateUpdate: (state: Dictionary) => void) { + this.onStateUpdate = onStateUpdate; + } + + updateState() { + this.currentState.val = this.currentState.val ?? 0; + this.currentState.val++; + this.onStateUpdate(this.state); + } +} + + +const + mock = jestMock.mock((state) => console.log('state update', state)); + c = new Component(mock); + +c.updateState(); +c.updateState(); + +console.log(mock.mock.calls); // [[{val: 2}, {val: 2}]] +``` \ No newline at end of file diff --git a/tests/helpers/mock/index.ts b/tests/helpers/mock/index.ts index 96594ff0a8..9ded8b57db 100644 --- a/tests/helpers/mock/index.ts +++ b/tests/helpers/mock/index.ts @@ -12,6 +12,8 @@ import type { JSHandle, Page } from 'playwright'; import { setSerializerAsMockFn } from 'core/prelude/test-env/components/json'; import type { ExtractFromJSHandle, SpyExtractor, SpyObject } from 'tests/helpers/mock/interface'; +export * from 'tests/helpers/mock/interface'; + /** * Wraps an object as a spy object by adding additional properties for accessing spy information. * From 90afea402be277dba698920bccfa32d45234dc2e Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 10 Jul 2023 13:57:40 +0300 Subject: [PATCH 067/159] :art: --- .../base/b-virtual-scroll/b-virtual-scroll.ts | 8 ++++---- src/components/base/b-virtual-scroll/handlers.ts | 7 +++++-- src/components/base/b-virtual-scroll/props.ts | 15 ++++++--------- .../test/api/component-object/index.ts | 2 +- .../b-virtual-scroll/test/api/helpers/index.ts | 9 ++++++--- .../test/api/helpers/interface.ts | 3 ++- .../test/unit/functional/emitter/payload.ts | 3 +-- .../test/unit/functional/rendering/default.ts | 2 +- .../unit/functional/rendering/items-factory.ts | 4 ++-- .../test/unit/functional/state/base.ts | 5 +++-- .../lifecycle/initialization/initialization.ts | 3 ++- .../test/unit/lifecycle/slots/slots.ts | 8 +++++--- .../test/unit/scenario/manual-rendering.ts | 5 +++-- .../b-virtual-scroll/test/unit/scenario/props.ts | 8 +++++--- .../b-virtual-scroll/test/unit/scenario/reload.ts | 2 +- .../b-virtual-scroll/test/unit/scenario/retry.ts | 5 +++-- 16 files changed, 50 insertions(+), 39 deletions(-) diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts index 06590eb69f..fa578bf637 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts @@ -11,14 +11,14 @@ * @packageDocumentation */ -import VDOM, { create, render } from 'components/friends/vdom'; +import type { AsyncOptions } from 'core/async'; +import VDOM, { create, render } from 'components/friends/vdom'; +import { iVirtualScrollHandlers } from 'components/base/b-virtual-scroll/handlers'; import { bVirtualScrollDomInsertAsyncGroup, renderGuardRejectionReason } from 'components/base/b-virtual-scroll/const'; +import type { VirtualScrollState, RenderGuardResult, UnsafeBVirtualScroll } from 'components/base/b-virtual-scroll/interface'; import iData, { $$, component, RequestParams, UnsafeGetter } from 'components/super/i-data/i-data'; -import { iVirtualScrollHandlers } from 'components/base/b-virtual-scroll/handlers'; -import type { AsyncOptions } from 'core/async'; -import type { VirtualScrollState, RenderGuardResult, UnsafeBVirtualScroll } from 'components/base/b-virtual-scroll/interface'; export * from 'components/base/b-virtual-scroll/interface'; export * from 'components/base/b-virtual-scroll/const'; diff --git a/src/components/base/b-virtual-scroll/handlers.ts b/src/components/base/b-virtual-scroll/handlers.ts index 3e92978db1..eb087e18b2 100644 --- a/src/components/base/b-virtual-scroll/handlers.ts +++ b/src/components/base/b-virtual-scroll/handlers.ts @@ -7,12 +7,15 @@ */ import iVirtualScrollProps from 'components/base/b-virtual-scroll/props'; + import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import { bVirtualScrollAsyncGroup, componentEvents } from 'components/base/b-virtual-scroll/const'; -import iData, { component } from 'components/super/i-data/i-data'; import type { MountedChild } from 'components/base/b-virtual-scroll/interface'; + +import { bVirtualScrollAsyncGroup, componentEvents } from 'components/base/b-virtual-scroll/const'; import { isAsyncReplaceError } from 'components/base/b-virtual-scroll/modules/helpers'; +import iData, { component } from 'components/super/i-data/i-data'; + /** * A class that provides an API to handle events emitted by the {@link bVirtualScroll} component. * This class is designed to work in conjunction with {@link bVirtualScroll}. diff --git a/src/components/base/b-virtual-scroll/props.ts b/src/components/base/b-virtual-scroll/props.ts index 5fb38d201d..05163b3577 100644 --- a/src/components/base/b-virtual-scroll/props.ts +++ b/src/components/base/b-virtual-scroll/props.ts @@ -9,6 +9,8 @@ import type iItems from 'components/traits/i-items/i-items'; import type { CreateFromItemFn } from 'components/traits/i-items/i-items'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; + import type { VirtualScrollState, @@ -21,20 +23,15 @@ import type { } from 'components/base/b-virtual-scroll/interface'; -import { - - defaultShouldProps, - componentItemType +import { defaultShouldProps, componentItemType } from 'components/base/b-virtual-scroll/const'; -} from 'components/base/b-virtual-scroll/const'; - -import iData, { component, prop, system } from 'components/super/i-data/i-data'; import { ComponentTypedEmitter, componentTypedEmitter } from 'components/base/b-virtual-scroll/modules/emitter'; +import { ComponentInternalState } from 'components/base/b-virtual-scroll/modules/state'; import { SlotsStateController } from 'components/base/b-virtual-scroll/modules/slots'; -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; import { ComponentFactory } from 'components/base/b-virtual-scroll/modules/factory'; import { Observer } from 'components/base/b-virtual-scroll/modules/observer'; -import { ComponentInternalState } from 'components/base/b-virtual-scroll/modules/state'; + +import iData, { component, prop, system } from 'components/super/i-data/i-data'; /** * A class that is friendly to {@link bVirtualScroll}. diff --git a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts index 2a5377d087..14ec3c9c4a 100644 --- a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts @@ -11,7 +11,7 @@ import type { Locator, Page } from 'playwright'; import { ComponentObject, Scroll } from 'tests/helpers'; import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { ComponentRefs, VirtualScrollState } from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { ComponentRefs, VirtualScrollState } from 'components/base/b-virtual-scroll/interface'; import type { SlotsStateObj } from 'components/base/b-virtual-scroll/modules/slots'; import { testStyles } from 'components/base/b-virtual-scroll/test/api/component-object/styles'; diff --git a/src/components/base/b-virtual-scroll/test/api/helpers/index.ts b/src/components/base/b-virtual-scroll/test/api/helpers/index.ts index 2a8c9056a9..7b303661d4 100644 --- a/src/components/base/b-virtual-scroll/test/api/helpers/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/helpers/index.ts @@ -7,15 +7,18 @@ */ import type { Page } from 'playwright'; + import test from 'tests/config/unit/test'; +import { createInitialState as createInitialStateObj } from 'components/base/b-virtual-scroll/modules/state/helpers'; import type { MountedChild, ComponentItem, VirtualScrollState, MountedItem } from 'components/base/b-virtual-scroll/interface'; +import { componentEvents, componentObserverLocalEvents } from 'components/base/b-virtual-scroll/const'; + import { paginationHandler } from 'tests/helpers/providers/pagination'; -import { VirtualScrollComponentObject } from 'components/base/b-virtual-scroll/test/api/component-object'; import { RequestInterceptor } from 'tests/helpers/providers/interceptor'; -import { componentEvents, componentObserverLocalEvents } from 'components/base/b-virtual-scroll/const'; + +import { VirtualScrollComponentObject } from 'components/base/b-virtual-scroll/test/api/component-object'; import type { DataConveyor, DataItemCtor, MountedItemCtor, StateApi, VirtualScrollTestHelpers, MountedSeparatorCtor, IndexedObj } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; -import { createInitialState as createInitialStateObj } from 'components/base/b-virtual-scroll/modules/state/helpers'; export * from 'components/base/b-virtual-scroll/test/api/component-object'; diff --git a/src/components/base/b-virtual-scroll/test/api/helpers/interface.ts b/src/components/base/b-virtual-scroll/test/api/helpers/interface.ts index 9366d74fd3..59faad152a 100644 --- a/src/components/base/b-virtual-scroll/test/api/helpers/interface.ts +++ b/src/components/base/b-virtual-scroll/test/api/helpers/interface.ts @@ -7,9 +7,10 @@ */ import type { ComponentItem, VirtualScrollState, MountedChild, MountedItem } from 'components/base/b-virtual-scroll/interface'; -import type { VirtualScrollComponentObject } from 'components/base/b-virtual-scroll/test/api/component-object'; + import type { SpyObject } from 'tests/helpers/mock/interface'; import type { RequestInterceptor } from 'tests/helpers/providers/interceptor'; +import type { VirtualScrollComponentObject } from 'components/base/b-virtual-scroll/test/api/component-object'; /** * The interface defining the data conveyor for convenient data manipulation. diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts b/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts index 7aeab57f66..cb6ec01b4b 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts @@ -7,8 +7,7 @@ */ /** - * @file This file contains test cases to verify the functionality of events emitter in the - * `b-virtual-scroll` component. + * @file This file contains test cases to verify the functionality of events emitter. */ import test from 'tests/config/unit/test'; diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts index 7b6436ebad..93515e7262 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts @@ -12,8 +12,8 @@ import test from 'tests/config/unit/test'; -import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; import type { VirtualScrollState, ShouldPerform } from 'components/base/b-virtual-scroll/interface'; +import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; test.describe('', () => { diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts index f1cb281dac..51985fe9ee 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts @@ -12,9 +12,9 @@ import test from 'tests/config/unit/test'; +import type { ComponentItemFactory, ComponentItem, ShouldPerform } from 'components/base/b-virtual-scroll/interface'; + import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; -import type { ComponentItemFactory } from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { ComponentItem, ShouldPerform } from 'components/base/b-virtual-scroll/interface'; import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; test.describe(' rendering via component factory', () => { diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/base.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/base.ts index dc78c76f39..c9936a8e58 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/base.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/base.ts @@ -12,10 +12,11 @@ import test from 'tests/config/unit/test'; -import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; import { defaultShouldProps } from 'components/base/b-virtual-scroll/const'; -import type { ComponentItem, ShouldPerform } from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { ComponentItem, ShouldPerform } from 'components/base/b-virtual-scroll/interface'; + +import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; test.describe('', () => { diff --git a/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts b/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts index 759ae0104f..54e31c8f2a 100644 --- a/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts +++ b/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts @@ -12,10 +12,11 @@ import test from 'tests/config/unit/test'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; import { defaultShouldProps } from 'components/base/b-virtual-scroll/const'; + import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; test.describe('', () => { let diff --git a/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts b/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts index d24329c2c1..ea578ee9ca 100644 --- a/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts +++ b/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts @@ -14,11 +14,13 @@ import delay from 'delay'; import test from 'tests/config/unit/test'; -import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; -import type { SlotsStateObj } from 'components/base/b-virtual-scroll/modules/slots'; -import type { ShouldPerform } from 'components/base/b-virtual-scroll/b-virtual-scroll'; import { BOM } from 'tests/helpers'; +import type { ShouldPerform } from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { SlotsStateObj } from 'components/base/b-virtual-scroll/modules/slots'; + +import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; + // eslint-disable-next-line max-lines-per-function test.describe('', () => { let diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts index ddd1988b1f..ebefeddd02 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts @@ -13,10 +13,11 @@ import test from 'tests/config/unit/test'; -import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; -import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; import type { ComponentElement } from 'core/component'; + import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; test.describe('', () => { let diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts index 26fe9f0b0e..35d148ae9c 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts @@ -10,13 +10,15 @@ * @file This file contains test cases to verify the functionality of prop changes in components. */ +import type { Route } from 'playwright'; + import test from 'tests/config/unit/test'; +import { fromQueryString } from 'core/url'; + +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { Route } from 'playwright'; -import { fromQueryString } from 'core/url'; test.describe('', () => { let diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts index cbc6d5b0b9..d1b4fd24f8 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts @@ -7,7 +7,7 @@ */ /** - * @file Этот файл содержит тест кейсы для проверки функциональности изменения пропов компонентов. + * @file This file contains a set of test cases to verify the functionality of component reloading. */ import test from 'tests/config/unit/test'; diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts index 481c18380b..4c33ee3877 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts @@ -12,10 +12,11 @@ import test from 'tests/config/unit/test'; -import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; -import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; import type { ComponentElement } from 'core/component'; + import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; test.describe('', () => { let From 69b10111723020424e7bd220f1b00aa1cd7d5f59 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 10 Jul 2023 16:19:40 +0300 Subject: [PATCH 068/159] :art: --- CHANGELOG.md | 6 +- index.d.ts | 2 +- .../base/b-virtual-scroll/CHANGELOG.md | 2 +- .../base/b-virtual-scroll/README.md | 11 + .../base/b-virtual-scroll/b-virtual-scroll.ss | 2 +- .../b-virtual-scroll/b-virtual-scroll.styl | 7 +- .../base/b-virtual-scroll/b-virtual-scroll.ts | 10 +- .../base/b-virtual-scroll/handlers.ts | 1 - .../b-virtual-scroll/interface/component.ts | 2 +- .../base/b-virtual-scroll/interface/events.ts | 2 +- .../b-virtual-scroll/modules/factory/index.ts | 4 +- .../observer/engines/intersection-observer.ts | 4 +- .../modules/observer/index.ts | 4 +- .../b-virtual-scroll/modules/slots/index.ts | 1 + .../b-virtual-scroll/modules/state/helpers.ts | 7 +- .../b-virtual-scroll/modules/state/index.ts | 17 +- src/components/base/b-virtual-scroll/props.ts | 2 +- .../test/api/helpers/index.ts | 6 +- .../test/unit/functional/emitter/payload.ts | 68 +-- .../{scenario => functional/props}/props.ts | 2 +- .../test/unit/functional/rendering/default.ts | 72 +-- .../functional/rendering/items-factory.ts | 478 +++++++++--------- .../functional/state/{base.ts => default.ts} | 59 ++- .../test/unit/functional/state/emitter.ts | 58 ++- .../initialization/initialization.ts | 59 ++- .../test/unit/lifecycle/slots/slots.ts | 153 +++--- .../test/unit/scenario/manual-rendering.ts | 15 +- .../test/unit/scenario/retry.ts | 43 +- .../p-v4-components-demo.ss | 2 +- .../i-lock-page-scroll/test/unit/desktop.ts | 8 +- tests/helpers/component-object/README.md | 47 +- tests/helpers/providers/interceptor/index.ts | 2 +- .../providers/interceptor/interface.ts | 3 + 33 files changed, 624 insertions(+), 535 deletions(-) rename src/components/base/b-virtual-scroll/test/unit/{scenario => functional/props}/props.ts (99%) rename src/components/base/b-virtual-scroll/test/unit/functional/state/{base.ts => default.ts} (88%) diff --git a/CHANGELOG.md b/CHANGELOG.md index d11091a63c..b73918d9c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,12 +15,12 @@ _Note: Gaps between patch versions are faulty, broken or test releases._ #### :boom: Breaking Change -* `b-virtual-scroll` мажорное обновление. Посмотрите readme компонента чтобы ознакомиться с изменениями и миграционным гайдом `components/base/b-virtual-scroll`. +* Major update to `b-virtual-scroll`. Please see to the component's readme for changes and migration guide `components/base/b-virtual-scroll`. #### :rocket: New Feature -* Добавлено новое тестовое API `ComponentObject` которые позволяет более удобно организовать общение с компонентов в тестовой среде `test/helpers/component-object`; -* Добавлено новое тестовое API для мока и реализации наблюдения за какими-либо функциями в рантайме `test/helpers/mock` +* Added new testing API `ComponentObject` that allows for easier interaction with components in the testing environment `test/helpers/component-object`. +* Added new testing API for mocking and spying on functions at runtime `test/helpers/mock`. ## v4.0.0-beta.8 (2023-07-07) diff --git a/index.d.ts b/index.d.ts index d1b3446c87..b4f99e339c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -136,7 +136,7 @@ declare var }; /** - * Результат который возвращает мок или spy функций от `jestMock` в свойстве returns + * The results returned by a mock or spy function from `jestMock`. */ interface JestMockResult { type: 'throw' | 'return'; diff --git a/src/components/base/b-virtual-scroll/CHANGELOG.md b/src/components/base/b-virtual-scroll/CHANGELOG.md index 011c9aa47d..e4603df3ac 100644 --- a/src/components/base/b-virtual-scroll/CHANGELOG.md +++ b/src/components/base/b-virtual-scroll/CHANGELOG.md @@ -13,7 +13,7 @@ Changelog #### :boom: Breaking Change -* Major update. Visit [readme](./readme) to see migration guide +* Major update. Visit [readme](./readme) to see migration guide. ## v3.30.1 (2022-10-25) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 317c3b9bfd..cab7cfa476 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -465,6 +465,17 @@ graph TB N -- True --> P["performRender()"] ``` +### Difference between ComponentItem with type `item` and `separator` + +The component allows rendering two types of components: + +- `item` - Main component (main content). +- `separator` - Other components, such as dividers or separators. + +There is no significant difference between them, except that they are treated differently in fields like `itemsTillEnd` in the `VirtualScrollState`. As the name suggests, the `itemsTillEnd` property only considers components with the `item` type, while `childTillEnd` considers components with both `item` and `separator` types. + +The distinction between `item` and `separator` types is mainly used for calculating certain properties based on the type of components present in the `VirtualScrollState`, such as the number of items till the end of the scroll. + ### Overriding in Child Layers The main use case for overriding in child layers is to modify the default behavior of functions or methods. diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ss b/src/components/base/b-virtual-scroll/b-virtual-scroll.ss index 90bcc85296..3c2f2d4590 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ss +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ss @@ -12,7 +12,7 @@ - template index() extends ['i-data'].index - block body - < .&__wrapper :-chunk-size = chunkSize + < .&__wrapper < .&__container ref = container | -test-ref = container < .&__tombstones & diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.styl b/src/components/base/b-virtual-scroll/b-virtual-scroll.styl index 2f6b64db48..3cc1d3a5ea 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.styl +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.styl @@ -13,4 +13,9 @@ $p = { } b-virtual-scroll extends i-data - // ... + width 100% + + &__container + position relative + box-sizing border-box + width 100% diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts index fa578bf637..1801f46af3 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts @@ -13,6 +13,7 @@ import type { AsyncOptions } from 'core/async'; +import type iItems from 'components/traits/i-items/i-items'; import VDOM, { create, render } from 'components/friends/vdom'; import { iVirtualScrollHandlers } from 'components/base/b-virtual-scroll/handlers'; import { bVirtualScrollDomInsertAsyncGroup, renderGuardRejectionReason } from 'components/base/b-virtual-scroll/const'; @@ -35,7 +36,7 @@ VDOM.addToPrototype(render); * by dynamically rendering chunks of data as the user scrolls. */ @component() -export default class bVirtualScroll extends iVirtualScrollHandlers { +export default class bVirtualScroll extends iVirtualScrollHandlers implements iItems { // @ts-ignore (getter instead readonly) override get requestParams(): iData['requestParams'] { return { @@ -181,9 +182,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers { /** * Returns the chunk size that should be rendered. - * * @param state - * @returns The chunk size. */ getChunkSize(state: VirtualScrollState): number { return Object.isFunction(this.chunkSize) ? @@ -217,11 +216,12 @@ export default class bVirtualScroll extends iVirtualScrollHandlers { * This function is called after successful data loading or when the child components enters the visible area. * * This function asks the client whether rendering can be performed. The client responds with an object - * indicating whether rendering is allowed or the reason for denial. The client's response should be an object - * of type {@link RenderGuardResult}. + * indicating whether rendering is allowed or the reason for denial. * * Based on the result of this function, the component takes appropriate actions. For example, * it may load data if it is not sufficient for rendering, or perform rendering if all conditions are met. + * + * @param state */ protected renderGuard(state: VirtualScrollState): RenderGuardResult { const diff --git a/src/components/base/b-virtual-scroll/handlers.ts b/src/components/base/b-virtual-scroll/handlers.ts index eb087e18b2..ff76522cfd 100644 --- a/src/components/base/b-virtual-scroll/handlers.ts +++ b/src/components/base/b-virtual-scroll/handlers.ts @@ -22,7 +22,6 @@ import iData, { component } from 'components/super/i-data/i-data'; */ @component() export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { - /** * Handler: component reset event. * Resets the component state to its initial state. diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts index f072d2a610..d7b5ead44a 100644 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -13,7 +13,7 @@ import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scro * * @typeParam DATA - Instance of the data element. * @typeParam RAW_DATA - The data loaded from the server but not yet processed. - * This type parameter determines the type of the lastLoadedRawData property + * This type parameter determines the type of the {@link VirtualScrollState.lastLoadedRawData} property */ export interface VirtualScrollState { /** diff --git a/src/components/base/b-virtual-scroll/interface/events.ts b/src/components/base/b-virtual-scroll/interface/events.ts index 8d9a94c194..ba5756912b 100644 --- a/src/components/base/b-virtual-scroll/interface/events.ts +++ b/src/components/base/b-virtual-scroll/interface/events.ts @@ -10,7 +10,7 @@ import type { MountedChild } from 'components/base/b-virtual-scroll/interface/co import { componentDataLocalEvents, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents } from 'components/base/b-virtual-scroll/const'; /** - * Component data-related events (emitted in `localEmitter`). + * Component data-related events (emitted in `selfEmitter`). */ export interface ComponentDataLocalEvents { /** diff --git a/src/components/base/b-virtual-scroll/modules/factory/index.ts b/src/components/base/b-virtual-scroll/modules/factory/index.ts index 498ef8d291..d0c1f41f09 100644 --- a/src/components/base/b-virtual-scroll/modules/factory/index.ts +++ b/src/components/base/b-virtual-scroll/modules/factory/index.ts @@ -11,7 +11,7 @@ import type { VNodeDescriptor } from 'components/friends/vdom'; import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; import type { ComponentItem, MountedChild, MountedItem } from 'components/base/b-virtual-scroll/interface'; -import { componentItemType } from 'components/base/b-virtual-scroll/const'; +import { isItem } from 'components/base/b-virtual-scroll/modules/helpers'; import * as vdomRender from 'components/base/b-virtual-scroll/modules/factory/engines/vdom'; @@ -61,7 +61,7 @@ export class ComponentFactory extends Friend { {items: mountedItems, childList} = ctx.getComponentState(); return items.map((item, i) => { - if (item.type === componentItemType.item) { + if (isItem(item)) { return { ...item, node: nodes[i], diff --git a/src/components/base/b-virtual-scroll/modules/observer/engines/intersection-observer.ts b/src/components/base/b-virtual-scroll/modules/observer/engines/intersection-observer.ts index 7b975945e4..71d74c93d4 100644 --- a/src/components/base/b-virtual-scroll/modules/observer/engines/intersection-observer.ts +++ b/src/components/base/b-virtual-scroll/modules/observer/engines/intersection-observer.ts @@ -6,11 +6,13 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +import Friend from 'components/friends/friend'; + import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; import type { MountedChild } from 'components/base/b-virtual-scroll/interface'; + import { observerAsyncGroup } from 'components/base/b-virtual-scroll/modules/observer/const'; import type { ObserverEngine } from 'components/base/b-virtual-scroll/modules/observer/interface'; -import Friend from 'components/friends/friend'; export default class IoObserver extends Friend implements ObserverEngine { diff --git a/src/components/base/b-virtual-scroll/modules/observer/index.ts b/src/components/base/b-virtual-scroll/modules/observer/index.ts index dc1c8d663a..fa94720159 100644 --- a/src/components/base/b-virtual-scroll/modules/observer/index.ts +++ b/src/components/base/b-virtual-scroll/modules/observer/index.ts @@ -6,10 +6,12 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +import Friend from 'components/friends/friend'; + import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; import type { MountedChild } from 'components/base/b-virtual-scroll/interface'; + import IoObserver from 'components/base/b-virtual-scroll/modules/observer/engines/intersection-observer'; -import Friend from 'components/friends/friend'; export { default as IoObserver } from 'components/base/b-virtual-scroll/modules/observer/engines/intersection-observer'; diff --git a/src/components/base/b-virtual-scroll/modules/slots/index.ts b/src/components/base/b-virtual-scroll/modules/slots/index.ts index e6f08dd433..77dcab7ade 100644 --- a/src/components/base/b-virtual-scroll/modules/slots/index.ts +++ b/src/components/base/b-virtual-scroll/modules/slots/index.ts @@ -10,6 +10,7 @@ import symbolGenerator from 'core/symbol'; import type { AsyncOptions } from 'core/async'; import Friend from 'components/friends/friend'; + import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; import type { SlotsStateObj } from 'components/base/b-virtual-scroll/modules/slots/interface'; diff --git a/src/components/base/b-virtual-scroll/modules/state/helpers.ts b/src/components/base/b-virtual-scroll/modules/state/helpers.ts index 8812841859..e72a0a5abf 100644 --- a/src/components/base/b-virtual-scroll/modules/state/helpers.ts +++ b/src/components/base/b-virtual-scroll/modules/state/helpers.ts @@ -6,13 +6,10 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { VirtualScrollState } from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { PrivateComponentState } from 'components/base/b-virtual-scroll/interface'; +import type { VirtualScrollState, PrivateComponentState } from 'components/base/b-virtual-scroll/interface'; /** * Creates an initial state object for a component. - * - * @returns An object representing the initial state of a component. */ export function createInitialState(): VirtualScrollState { return { @@ -39,8 +36,6 @@ export function createInitialState(): VirtualScrollState { /** * Creates an initial private state object for a component. - * - * @returns An object representing the initial private state of a component. */ export function createPrivateInitialState(): PrivateComponentState { return { diff --git a/src/components/base/b-virtual-scroll/modules/state/index.ts b/src/components/base/b-virtual-scroll/modules/state/index.ts index db2a92db5a..34592a8ee2 100644 --- a/src/components/base/b-virtual-scroll/modules/state/index.ts +++ b/src/components/base/b-virtual-scroll/modules/state/index.ts @@ -6,11 +6,12 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +import Friend from 'components/friends/friend'; + import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { MountedChild, VirtualScrollState, MountedItem, PrivateComponentState } from 'components/base/b-virtual-scroll/interface'; import { isItem } from 'components/base/b-virtual-scroll/modules/helpers'; import { createInitialState, createPrivateInitialState } from 'components/base/b-virtual-scroll/modules/state/helpers'; -import Friend from 'components/friends/friend'; +import type { MountedChild, VirtualScrollState, MountedItem, PrivateComponentState } from 'components/base/b-virtual-scroll/interface'; /** * Friendly to the `bVirtualScroll` class that represents the internal state of a component. @@ -51,6 +52,7 @@ export class ComponentInternalState extends Friend { incrementLoadPage(): void { this.state.loadPage++; } + /** * Increments the render page pointer. */ @@ -73,7 +75,6 @@ export class ComponentInternalState extends Friend { /** * Updates the arrays with mounted child elements of the component. - * * @param mounted - The mounted child elements. */ updateMounted(mounted: MountedChild[]): void { @@ -89,7 +90,6 @@ export class ComponentInternalState extends Friend { /** * Updates the state of the last raw loaded data. - * * @param data - The last raw loaded data. */ setRawLastLoaded(data: unknown): void { @@ -98,7 +98,6 @@ export class ComponentInternalState extends Friend { /** * Sets the flag indicating if it's the initial render cycle. - * * @param value - The value of the flag. */ setIsInitialRender(value: boolean): void { @@ -117,7 +116,6 @@ export class ComponentInternalState extends Friend { /** * Sets the flag indicating if the component's lifecycle is done. - * * @param value - The value of the flag. */ setIsLifecycleDone(value: boolean): void { @@ -126,7 +124,6 @@ export class ComponentInternalState extends Friend { /** * Sets the flag indicating if the component is currently loading data. - * * @param value - The value of the flag. */ setIsLoadingInProgress(value: boolean): void { @@ -134,9 +131,8 @@ export class ComponentInternalState extends Friend { } /** - * Устанавливает флаг который указывает на то, что последняя загрузка завершилась с ошибкой. - * - * @param value + * Sets a flag indicating whether the last load operation ended with an error. + * @param value - The value to set. */ setIsLastErrored(value: boolean): void { this.state.isLastErrored = value; @@ -144,7 +140,6 @@ export class ComponentInternalState extends Friend { /** * Sets the maximum viewed index based on the passed component's index. - * * @param component - The component to compare and update the maximum viewed index. */ setMaxViewedIndex(component: MountedChild): void { diff --git a/src/components/base/b-virtual-scroll/props.ts b/src/components/base/b-virtual-scroll/props.ts index 05163b3577..68412bdbe6 100644 --- a/src/components/base/b-virtual-scroll/props.ts +++ b/src/components/base/b-virtual-scroll/props.ts @@ -38,7 +38,7 @@ import iData, { component, prop, system } from 'components/super/i-data/i-data'; * It contains the properties of the {@link bVirtualScroll} component. */ @component() -export default abstract class iVirtualScrollProps extends iData implements iItems { +export default abstract class iVirtualScrollProps extends iData { /** {@link iItems.item} */ readonly Item!: object; diff --git a/src/components/base/b-virtual-scroll/test/api/helpers/index.ts b/src/components/base/b-virtual-scroll/test/api/helpers/index.ts index 7b303661d4..234dab7ef6 100644 --- a/src/components/base/b-virtual-scroll/test/api/helpers/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/helpers/index.ts @@ -297,7 +297,7 @@ export function createMountedSeparator(data: IndexedObj): MountedChild { * * @param count - The number of items to create. * @param itemCtor - The constructor function to create items. - * @param start - The starting index (default: 0). + * @param [start] - The starting index (default: 0). */ export function createChunk( count: number, @@ -317,7 +317,7 @@ export function createIndexedObj(i: number): IndexedObj { /** * Filters emitter emit calls and removes unnecessary events. - * It only keeps component events, excluding observer-like events. + * It only keeps component events. * * @param emitCalls - The array of emit calls. * @param [filterObserverEvents] - Whether to filter out observer events (default: true). @@ -335,7 +335,7 @@ export function filterEmitterCalls( /** * Filters emitter emit results and removes unnecessary events. - * It only keeps component events, excluding observer-like events. + * It only keeps component events. * * @param results - The array of emit results. * @param [filterObserverEvents] - Whether to filter out observer events (default: true). diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts b/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts index cb6ec01b4b..502e3828a4 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts @@ -7,7 +7,7 @@ */ /** - * @file This file contains test cases to verify the functionality of events emitter. + * @file This file contains test cases to verify the functionality of events emitted by the component. */ import test from 'tests/config/unit/test'; @@ -15,7 +15,7 @@ import test from 'tests/config/unit/test'; import { createTestHelpers, filterEmitterCalls } from 'components/base/b-virtual-scroll/test/api/helpers'; import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; -test.describe(' emitter', () => { +test.describe('', () => { let component: VirtualScrollTestHelpers['component'], provider: VirtualScrollTestHelpers['provider'], @@ -35,14 +35,15 @@ test.describe(' emitter', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); - await component.withProps({ - chunkSize, - shouldStopRequestingData: () => true, - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') - }); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + shouldStopRequestingData: () => true, + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); await component.waitForLifecycleDone(); const @@ -77,15 +78,16 @@ test.describe(' emitter', () => { .responseOnce(200, {data: secondDataChunk}) .response(200, {data: []}); - await component.withProps({ - chunkSize, - shouldPerformDataRequest: () => true, - shouldStopRequestingData: ({lastLoadedData}) => lastLoadedData.length === 0, - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') - }); + await component + .withDefaultPaginationProviderProps({chunkSize: providerChunkSize}) + .withProps({ + chunkSize, + shouldPerformDataRequest: () => true, + shouldStopRequestingData: ({lastLoadedData}) => lastLoadedData.length === 0, + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); - await component.build(); await component.waitForContainerChildCountEqualsTo(chunkSize); const @@ -124,15 +126,16 @@ test.describe(' emitter', () => { .responseOnce(200, {data: firstDataChunk}) .response(200, {data: []}); - await component.withProps({ - chunkSize, - shouldPerformDataRequest: () => true, - shouldStopRequestingData: ({lastLoadedData}) => lastLoadedData.length === 0, - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') - }); + await component + .withDefaultPaginationProviderProps({chunkSize: providerChunkSize}) + .withProps({ + chunkSize, + shouldPerformDataRequest: () => true, + shouldStopRequestingData: ({lastLoadedData}) => lastLoadedData.length === 0, + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); - await component.build(); await component.waitForContainerChildCountEqualsTo(providerChunkSize); await component.waitForLifecycleDone(); @@ -164,14 +167,15 @@ test.describe(' emitter', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); - await component.withProps({ - chunkSize, - shouldStopRequestingData: () => true, - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') - }); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + shouldStopRequestingData: () => true, + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); await component.waitForContainerChildCountEqualsTo(chunkSize); state.reset(); diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts similarity index 99% rename from src/components/base/b-virtual-scroll/test/unit/scenario/props.ts rename to src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts index 35d148ae9c..c0a5ee5b7f 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/props.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts @@ -7,7 +7,7 @@ */ /** - * @file This file contains test cases to verify the functionality of prop changes in components. + * @file This file contains test cases to verify the functionality of props in the component. */ import type { Route } from 'playwright'; diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts index 93515e7262..c2da22925e 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts @@ -31,36 +31,41 @@ test.describe('', () => { await page.setViewportSize({height: 640, width: 360}); }); - test('Should render all loaded data', async () => { - const - chunkSize = 12; - - provider - .responseOnce(200, {data: state.data.addData(chunkSize)}) - .responseOnce(200, {data: state.data.addData(chunkSize)}) - .responseOnce(200, {data: state.data.addData(chunkSize)}) - .response(200, {data: state.data.addData(0)}); - - const shouldPerformDataRender = await component.mockFn( - ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 - ); - - await component.withProps({ - shouldPerformDataRender, - chunkSize - }); + test.describe('ChunkSize is 12', () => { + test.describe('Provider can provide 3 data chunks', () => { + test('Should render 36 items', async () => { + const + chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: state.data.addData(0)}); + + const shouldPerformDataRender = await component.mockFn( + ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 + ); + + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + shouldPerformDataRender, + chunkSize + }); + + await component.build(); + await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.scrollToBottom(); + await component.waitForContainerChildCountEqualsTo(chunkSize * 2); + await component.scrollToBottom(); + await component.waitForContainerChildCountEqualsTo(chunkSize * 3); + await component.scrollToBottom(); + await component.waitForContainerChildCountEqualsTo(chunkSize * 3); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); - await component.waitForContainerChildCountEqualsTo(chunkSize); - await component.scrollToBottom(); - await component.waitForContainerChildCountEqualsTo(chunkSize * 2); - await component.scrollToBottom(); - await component.waitForContainerChildCountEqualsTo(chunkSize * 3); - await component.scrollToBottom(); - await component.waitForContainerChildCountEqualsTo(chunkSize * 3); - - await test.expect(component.childList).toHaveCount(chunkSize * 3); + await test.expect(component.childList).toHaveCount(chunkSize * 3); + }); + }); }); test.describe('With a different chunk size for each render cycle', () => { @@ -73,11 +78,12 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(chunkSize[2])}) .response(200, {data: []}); - await component.withProps({ - chunkSize: (state: VirtualScrollState) => [6, 12, 18][state.renderPage] ?? 18 - }); + await component + .withDefaultPaginationProviderProps() + .withProps({ + chunkSize: (state: VirtualScrollState) => [6, 12, 18][state.renderPage] ?? 18 + }); - await component.withDefaultPaginationProviderProps(); await component.build(); await test.step('First chunk', async () => { diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts index 51985fe9ee..c30a338cda 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts @@ -17,7 +17,7 @@ import type { ComponentItemFactory, ComponentItem, ShouldPerform } from 'compone import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; -test.describe(' rendering via component factory', () => { +test.describe(' rendering via itemsFactory', () => { let component: VirtualScrollTestHelpers['component'], provider: VirtualScrollTestHelpers['provider'], @@ -30,252 +30,270 @@ test.describe(' rendering via component factory', () => { await provider.start(); }); - test('Returned items with type `item` is equal to the provided data', async () => { - const - chunkSize = 12; - - provider - .responseOnce(200, {data: state.data.addData(chunkSize)}) - .response(200, {data: state.data.addData(0)}); - - const itemsFactory = await component.mockFn>((state) => { - const data = state.lastLoadedData; - - return data.map((item) => ({ - item: 'section', - key: Object.cast(undefined), - type: 'item', - children: [], - props: { - 'data-index': item.i - } - })); - }); - - await component.withProps({ - itemsFactory, - shouldPerformDataRender: () => true, - chunkSize + test.describe('Returned items with type `item` is equal to the provided data', () => { + test('Should render all of the items that was returned from `itemsFactory`', async () => { + const + chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: state.data.addData(0)}); + + const itemsFactory = await component.mockFn>((state) => { + const data = state.lastLoadedData; + + return data.map((item) => ({ + item: 'section', + key: Object.cast(undefined), + type: 'item', + children: [], + props: { + 'data-index': item.i + } + })); + }); + + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + itemsFactory, + shouldPerformDataRender: () => true, + chunkSize + }) + .build(); + + await component.waitForContainerChildCountEqualsTo(chunkSize); + + await test.expect(component.childList).toHaveCount(chunkSize); }); - - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); - await component.waitForContainerChildCountEqualsTo(chunkSize); - - await test.expect(component.childList).toHaveCount(chunkSize); }); - test('In additional `item`, `separator` was also returned', async () => { - const - chunkSize = 12; - - provider - .responseOnce(200, {data: state.data.addData(chunkSize)}) - .response(200, {data: state.data.addData(0)}); - - const separator = { - item: 'b-button', - key: '', - children: { - default: 'ima button' - }, - props: { - id: 'button' - }, - type: 'separator' - }; - - const itemsFactory = await component.mockFn((state, ctx, separator) => { + test.describe('In additional `item`, `separator` was also returned', () => { + test('Should render both', async () => { const - data = state.lastLoadedData; - - const items = data.map((item) => ({ - item: 'section', - key: Object.cast(undefined), - type: 'item', - children: [], + chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: state.data.addData(0)}); + + const separator = { + item: 'b-button', + key: '', + children: { + default: 'ima button' + }, props: { - 'data-index': item.i - } - })); - - items.push(separator); - - return items; - }, separator); - - await component.withProps({ - itemsFactory, - shouldPerformDataRender: () => true, - chunkSize + id: 'button' + }, + type: 'separator' + }; + + const itemsFactory = await component.mockFn((state, ctx, separator) => { + const + data = state.lastLoadedData; + + const items = data.map((item) => ({ + item: 'section', + key: Object.cast(undefined), + type: 'item', + children: [], + props: { + 'data-index': item.i + } + })); + + items.push(separator); + + return items; + }, separator); + + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + itemsFactory, + shouldPerformDataRender: () => true, + chunkSize + }) + .build(); + + await component.waitForContainerChildCountEqualsTo(chunkSize + 1); + + await test.expect(component.container.locator('#button')).toBeVisible(); + await test.expect(component.childList).toHaveCount(chunkSize + 1); }); - - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); - await component.waitForContainerChildCountEqualsTo(chunkSize + 1); - - await test.expect(component.container.locator('#button')).toBeVisible(); - await test.expect(component.childList).toHaveCount(chunkSize + 1); }); - test('Returned items with type `item` is less than the provided data', async () => { - const - chunkSize = 12, - renderedChunkSize = chunkSize - 2; - - provider - .responseOnce(200, {data: state.data.addData(chunkSize)}) - .response(200, {data: state.data.addData(0)}); - - const itemsFactory = await component.mockFn>((state) => { - const data = state.lastLoadedData; - - const items = data.map((item) => ({ - item: 'section', - key: Object.cast(undefined), - type: 'item', - children: [], - props: { - 'data-index': item.i - } - })); - - items.length -= 2; - return items; - }); - - await component.withProps({ - itemsFactory, - shouldPerformDataRender: () => true, - chunkSize + test.describe('Returned items with type `item` is less than the provided data', () => { + test('Should render items that was returned from `itemsFactory`', async () => { + const + chunkSize = 12, + renderedChunkSize = chunkSize - 2; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: state.data.addData(0)}); + + const itemsFactory = await component.mockFn>((state) => { + const data = state.lastLoadedData; + + const items = data.map((item) => ({ + item: 'section', + key: Object.cast(undefined), + type: 'item', + children: [], + props: { + 'data-index': item.i + } + })); + + items.length -= 2; + return items; + }); + + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + itemsFactory, + shouldPerformDataRender: () => true, + chunkSize + }) + .build(); + + await component.waitForContainerChildCountEqualsTo(renderedChunkSize); + + await test.expect(component.childList).toHaveCount(renderedChunkSize); }); - - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); - await component.waitForContainerChildCountEqualsTo(renderedChunkSize); - - await test.expect(component.childList).toHaveCount(renderedChunkSize); }); - test('Returned item with type `item` is more than the provided data', async () => { - const - chunkSize = 12, - renderedChunkSize = chunkSize * 2; - - provider - .responseOnce(200, {data: state.data.addData(chunkSize)}) - .response(200, {data: state.data.addData(0)}); - - const itemsFactory = await component.mockFn>((state) => { - const data = state.lastLoadedData; - - const items = data.map((item) => ({ - item: 'section', - key: Object.cast(undefined), - type: 'item', - children: [], - props: { - 'data-index': item.i - } - })); - - return [...items, ...items]; - }); - - await component.withProps({ - itemsFactory, - shouldPerformDataRender: () => true, - chunkSize + test.describe('Returned item with type `item` is more than the provided data', () => { + test('Should render items that was returned from `itemsFactory`', async () => { + const + chunkSize = 12, + renderedChunkSize = chunkSize * 2; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: state.data.addData(0)}); + + const itemsFactory = await component.mockFn>((state) => { + const data = state.lastLoadedData; + + const items = data.map((item) => ({ + item: 'section', + key: Object.cast(undefined), + type: 'item', + children: [], + props: { + 'data-index': item.i + } + })); + + return [...items, ...items]; + }); + + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + itemsFactory, + shouldPerformDataRender: () => true, + chunkSize + }) + .build(); + + await component.waitForContainerChildCountEqualsTo(renderedChunkSize); + + await test.expect(component.childList).toHaveCount(renderedChunkSize); }); - - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); - await component.waitForContainerChildCountEqualsTo(renderedChunkSize); - - await test.expect(component.childList).toHaveCount(renderedChunkSize); }); - test('`item` was not returned, but equal to the number of data, the number of `separator` was returned', async () => { - const - chunkSize = 12; - - provider - .responseOnce(200, {data: state.data.addData(chunkSize)}) - .response(200, {data: state.data.addData(0)}); - - const itemsFactory = await component.mockFn>((state) => { - const data = state.lastLoadedData; - - return data.map((item) => ({ - item: 'section', - key: Object.cast(undefined), - type: 'separator', - children: [], - props: { - 'data-index': item.i - } - })); - }); - - await component.withProps({ - itemsFactory, - shouldPerformDataRender: () => true, - chunkSize + test.describe('`item` was not returned, but equal to the number of data, the number of `separator` was returned', () => { + test('Should render separators that was returned from `itemsFactory`', async () => { + const + chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: state.data.addData(0)}); + + const itemsFactory = await component.mockFn>((state) => { + const data = state.lastLoadedData; + + return data.map((item) => ({ + item: 'section', + key: Object.cast(undefined), + type: 'separator', + children: [], + props: { + 'data-index': item.i + } + })); + }); + + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + itemsFactory, + shouldPerformDataRender: () => true, + chunkSize + }) + .build(); + + await component.waitForContainerChildCountEqualsTo(chunkSize); + + await test.expect(component.childList).toHaveCount(chunkSize); }); - - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); - await component.waitForContainerChildCountEqualsTo(chunkSize); - - await test.expect(component.childList).toHaveCount(chunkSize); }); - test('`itemsFactory` returns twice as much data as `chunkSize`', async () => { - const - chunkSize = 12; - - provider - .responseOnce(200, {data: state.data.addData(chunkSize)}) - .responseOnce(200, {data: state.data.addData(chunkSize)}) - .responseOnce(200, {data: state.data.addData(chunkSize)}) - .response(200, {data: state.data.addData(0)}); - - const shouldPerformDataRender = await component.mockFn( - ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 - ); - - const itemsFactory = await component.mockFn>((state) => { - const data = state.lastLoadedData; - - const items = data.map((item) => ({ - item: 'section', - key: Object.cast(undefined), - type: 'item', - children: [], - props: { - 'data-index': item.i - } - })); - - return [...items, ...items]; - }); - - await component.withProps({ - itemsFactory, - shouldPerformDataRender, - chunkSize + test.describe('`itemsFactory` returns twice as much data as `chunkSize`', () => { + test('Should render twice as much items as `chunkSize`', async () => { + const + chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: state.data.addData(0)}); + + const shouldPerformDataRender = await component.mockFn( + ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 + ); + + const itemsFactory = await component.mockFn>((state) => { + const data = state.lastLoadedData; + + const items = data.map((item) => ({ + item: 'section', + key: Object.cast(undefined), + type: 'item', + children: [], + props: { + 'data-index': item.i + } + })); + + return [...items, ...items]; + }); + + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + itemsFactory, + shouldPerformDataRender, + chunkSize + }) + .build(); + + await component.waitForContainerChildCountEqualsTo(chunkSize * 2); + await component.scrollToBottom(); + await component.waitForContainerChildCountEqualsTo(chunkSize * 2 * 2); + await component.scrollToBottom(); + await component.waitForContainerChildCountEqualsTo(chunkSize * 3 * 2); + await component.scrollToBottom(); + await component.waitForContainerChildCountEqualsTo(chunkSize * 3 * 2); + + await test.expect(component.childList).toHaveCount(chunkSize * 3 * 2); }); - - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); - await component.waitForContainerChildCountEqualsTo(chunkSize * 2); - await component.scrollToBottom(); - await component.waitForContainerChildCountEqualsTo(chunkSize * 2 * 2); - await component.scrollToBottom(); - await component.waitForContainerChildCountEqualsTo(chunkSize * 3 * 2); - await component.scrollToBottom(); - await component.waitForContainerChildCountEqualsTo(chunkSize * 3 * 2); - - await test.expect(component.childList).toHaveCount(chunkSize * 3 * 2); }); }); diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/base.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts similarity index 88% rename from src/components/base/b-virtual-scroll/test/unit/functional/state/base.ts rename to src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts index c9936a8e58..7eb9fd1601 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/base.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts @@ -51,12 +51,12 @@ test.describe('', () => { loadPage: 0 }); - await component.withProps({ - '@hook:created': mockFn - }); - - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + '@hook:created': mockFn + }) + .build(); await test.expect(mockFn.results).resolves.toEqual([{type: 'return', value: expectedState}]); }); @@ -80,15 +80,16 @@ test.describe('', () => { state.data.addItems(chunkSize); - await component.withProps({ - chunkSize, - shouldStopRequestingData, - shouldPerformDataRequest, - shouldPerformDataRender - }); + await component + .withDefaultPaginationProviderProps({chunkSize: providerChunkSize}) + .withProps({ + chunkSize, + shouldStopRequestingData, + shouldPerformDataRequest, + shouldPerformDataRender + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); - await component.build(); await component.waitForContainerChildCountEqualsTo(chunkSize); const @@ -176,14 +177,15 @@ test.describe('', () => { state.data.addItems(chunkSize); state.data.addChild([separator]); - await component.withProps({ - itemsFactory, - shouldPerformDataRender: () => true, - chunkSize - }); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + itemsFactory, + shouldPerformDataRender: () => true, + chunkSize + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); await component.waitForContainerChildCountEqualsTo(chunkSize + 1); await component.waitForLifecycleDone(); @@ -227,14 +229,15 @@ test.describe('', () => { state.data.addSeparators(chunkSize); - await component.withProps({ - itemsFactory, - shouldPerformDataRender: () => true, - chunkSize - }); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + itemsFactory, + shouldPerformDataRender: () => true, + chunkSize + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); await component.waitForContainerChildCountEqualsTo(chunkSize); await component.waitForLifecycleDone(); diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts index 56c5008db6..865d62e140 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts @@ -61,21 +61,22 @@ test.describe('', () => { .responseOnce(200, {data: state.data.getDataChunk(0)}) .response(200, {data: []}); - await component.withProps({ - chunkSize, - shouldStopRequestingData: () => true, - '@hook:beforeDataCreate': (ctx) => { - const original = ctx.emit; - - ctx.emit = jestMock.mock((...args) => { - original(...args); - return [args[0], Object.fastClone(ctx.getComponentState())]; - }); - } - }); - - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + shouldStopRequestingData: () => true, + '@hook:beforeDataCreate': (ctx) => { + const original = ctx.emit; + + ctx.emit = jestMock.mock((...args) => { + original(...args); + return [args[0], Object.fastClone(ctx.getComponentState())]; + }); + } + }) + .build(); + await component.waitForLifecycleDone(); const @@ -142,20 +143,21 @@ test.describe('', () => { .responseOnce(200, {data: state.data.getDataChunk(1)}) .response(200, {data: state.data.getDataChunk(2)}); - await component.withProps({ - chunkSize, - '@hook:beforeDataCreate': (ctx) => { - const original = ctx.emit; - - ctx.emit = jestMock.mock((...args) => { - original(...args); - return [args[0], Object.fastClone(ctx.getComponentState())]; - }); - } - }); + await component + .withDefaultPaginationProviderProps({chunkSize: providerChunkSize}) + .withProps({ + chunkSize, + '@hook:beforeDataCreate': (ctx) => { + const original = ctx.emit; + + ctx.emit = jestMock.mock((...args) => { + original(...args); + return [args[0], Object.fastClone(ctx.getComponentState())]; + }); + } + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); - await component.build(); await component.waitForLifecycleDone(); await component.reload(); await component.waitForLifecycleDone(); diff --git a/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts b/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts index 54e31c8f2a..b2d2645569 100644 --- a/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts +++ b/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts @@ -7,7 +7,7 @@ */ /** - * @file Test cases of the component lifecycle. + * @file Test cases of the component lifecycle initialization. */ import test from 'tests/config/unit/test'; @@ -67,16 +67,17 @@ test.describe('', () => { state.data.addItems(chunkSize); - await component.withProps({ - chunkSize, - shouldStopRequestingData, - shouldPerformDataRequest, - disableObserver: true, - ...hookProp - }); + await component + .withDefaultPaginationProviderProps({chunkSize: providerChunkSize}) + .withProps({ + chunkSize, + shouldStopRequestingData, + shouldPerformDataRequest, + disableObserver: true, + ...hookProp + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); - await component.build(); await component.waitForContainerChildCountEqualsTo(chunkSize); }); @@ -169,16 +170,17 @@ test.describe('', () => { state.data.addData(providerChunkSize); state.data.addItems(providerChunkSize); - await component.withProps({ - chunkSize, - shouldStopRequestingData, - shouldPerformDataRequest, - disableObserver: true, - ...hookProp - }); + await component + .withDefaultPaginationProviderProps({chunkSize: providerChunkSize}) + .withProps({ + chunkSize, + shouldStopRequestingData, + shouldPerformDataRequest, + disableObserver: true, + ...hookProp + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); - await component.build(); await component.waitForContainerChildCountEqualsTo(providerChunkSize); }); @@ -243,16 +245,17 @@ test.describe('', () => { state.data.addData(providerChunkSize); state.data.addItems(providerChunkSize); - await component.withProps({ - chunkSize, - shouldStopRequestingData, - shouldPerformDataRequest, - disableObserver: true, - ...hookProp - }); + await component + .withDefaultPaginationProviderProps({chunkSize: providerChunkSize}) + .withProps({ + chunkSize, + shouldStopRequestingData, + shouldPerformDataRequest, + disableObserver: true, + ...hookProp + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize: providerChunkSize}); - await component.build(); await component.waitForContainerChildCountEqualsTo(providerChunkSize); }); diff --git a/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts b/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts index ea578ee9ca..b54efee0b9 100644 --- a/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts +++ b/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts @@ -84,12 +84,11 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); - await component.withProps({ - chunkSize - }); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({chunkSize}) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); await component.waitForSlotState('done', true); const @@ -114,13 +113,14 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); - await component.withProps({ - chunkSize, - shouldPerformDataRender: () => true - }); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + shouldPerformDataRender: () => true + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); await component.scrollToBottom(); await component.waitForSlotState('done', true); @@ -145,13 +145,14 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(chunkSize / 2)}) .response(200, {data: []}); - await component.withProps({ - chunkSize, - shouldPerformDataRender: () => true - }); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + shouldPerformDataRender: () => true + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); await component.waitForSlotState('done', true); const @@ -183,14 +184,15 @@ test.describe('', () => { const shouldPerformDataRender = (({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0); - await component.withProps({ - chunkSize, - shouldPerformDataRequest, - shouldPerformDataRender - }); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + shouldPerformDataRequest, + shouldPerformDataRender + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); await component.waitForContainerChildCountEqualsTo(chunkSize); await component.waitForSlotState('loader', false); @@ -215,13 +217,14 @@ test.describe('', () => { provider.response(200, {data: []}); - await component.withProps({ - chunkSize, - shouldPerformDataRender: () => true - }); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + shouldPerformDataRender: () => true + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); await component.waitForSlotState('empty', true); const @@ -246,12 +249,11 @@ test.describe('', () => { provider .response(200, {data: state.data.addData(chunkSize)}, {delay: (10).seconds()}); - await component.withProps({ - chunkSize - }); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({chunkSize}) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); await component.waitForSlotState('loader', true); const @@ -276,12 +278,10 @@ test.describe('', () => { provider .response(200, {data: state.data.addData(providerChunkSize)}, {delay: (4).seconds()}); - await component.withProps({ - chunkSize - }); - - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({chunkSize}) + .build(); let i = 0; @@ -313,13 +313,14 @@ test.describe('', () => { provider.response(500, {}); - await component.withProps({ - chunkSize, - shouldPerformDataRender: () => true - }); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + shouldPerformDataRender: () => true + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); await component.waitForSlotState('retry', true); const @@ -345,13 +346,14 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(providerChunkSize)}) .response(500, {}); - await component.withProps({ - chunkSize, - shouldPerformDataRender: () => true - }); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + shouldPerformDataRender: () => true + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); await component.waitForSlotState('retry', true); const @@ -388,14 +390,15 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); - await component.withProps({ - chunkSize, - disableObserver: true, - shouldPerformDataRender: () => true - }); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + disableObserver: true, + shouldPerformDataRender: () => true + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); await component.waitForSlotState('renderNext', true); const @@ -419,14 +422,15 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}, {delay: (10).seconds()}) .response(200, {data: []}); - await component.withProps({ - chunkSize, - disableObserver: true, - shouldPerformDataRender: () => true - }); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + disableObserver: true, + shouldPerformDataRender: () => true + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); await BOM.waitForIdleCallback(page); await component.waitForSlotState('renderNext', false); @@ -449,14 +453,15 @@ test.describe('', () => { provider.response(500, {data: []}); - await component.withProps({ - chunkSize, - disableObserver: true, - shouldPerformDataRender: () => true - }); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + disableObserver: true, + shouldPerformDataRender: () => true + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); await component.waitForSlotState('renderNext', false); await component.waitForSlotState('tombstones', false); diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts index ebefeddd02..7c97a5d7d5 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts @@ -56,14 +56,15 @@ test.describe('', () => { test.beforeEach(async () => { provider.response(200, () => ({data: state.data.addData(chunkSize)})); - await component.withProps({ - chunkSize, - disableObserver: true, - shouldPerformDataRender: () => true - }); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + disableObserver: true, + shouldPerformDataRender: () => true + }) + .build(); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); await component.waitForContainerChildCountEqualsTo(chunkSize); }); diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts index 4c33ee3877..fe6efed690 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts @@ -50,9 +50,10 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); - await component.withProps({chunkSize}); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); + await component + .withProps({chunkSize}) + .withDefaultPaginationProviderProps({chunkSize}) + .build(); await component.node.locator('#retry').click(); @@ -67,13 +68,13 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); - await component.withProps({ - chunkSize, - '@onRequestError': (_, retryFn) => setTimeout(retryFn, 0) - }); - - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + '@onRequestError': (_, retryFn) => setTimeout(retryFn, 0) + }) + .build(); await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); }); @@ -87,9 +88,10 @@ test.describe('', () => { .responseOnce(200, {data: state.data.addData(chunkSize)}) .response(200, {data: []}); - await component.withProps({chunkSize}); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); + await component + .withProps({chunkSize}) + .withDefaultPaginationProviderProps({chunkSize}) + .build(); const event = component.waitForEvent('dataLoadError'); await component.node.locator('#retry').click(); @@ -112,9 +114,9 @@ test.describe('', () => { await component .withDefaultPaginationProviderProps({chunkSize}) - .withProps({chunkSize}); + .withProps({chunkSize}) + .build(); - await component.build(); await component.waitForContainerChildCountEqualsTo(chunkSize); await component.scrollToBottom(); @@ -139,9 +141,9 @@ test.describe('', () => { await component .withDefaultPaginationProviderProps({chunkSize: providerChunkSize}) - .withProps({chunkSize}); + .withProps({chunkSize}) + .build(); - await component.build(); await component.node.locator('#retry').click(); await component.waitForContainerChildCountEqualsTo(chunkSize); @@ -161,9 +163,10 @@ test.describe('', () => { .responseOnce(500, {}) .response(200, {data: []}); - await component.withProps({chunkSize}); - await component.withDefaultPaginationProviderProps({chunkSize}); - await component.build(); + await component + .withProps({chunkSize}) + .withDefaultPaginationProviderProps({chunkSize}) + .build(); await component.node.locator('#retry').click(); diff --git a/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss b/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss index d69f41d431..e19ccca69d 100644 --- a/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss +++ b/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss @@ -17,4 +17,4 @@ id = testComponent | :is = testComponent | :v-attrs = testComponentAttrs - . \ No newline at end of file + . diff --git a/src/components/traits/i-lock-page-scroll/test/unit/desktop.ts b/src/components/traits/i-lock-page-scroll/test/unit/desktop.ts index 4fffa77743..cce185aac4 100644 --- a/src/components/traits/i-lock-page-scroll/test/unit/desktop.ts +++ b/src/components/traits/i-lock-page-scroll/test/unit/desktop.ts @@ -106,21 +106,21 @@ test.describe('components/traits/i-lock-page-scroll - desktop', () => { const getScrollTop = () => page.evaluate(() => document.documentElement.scrollTop); - const VirtualScrollPosition = 500; + const scrollYPosition = 500; await page.evaluate( (yPos) => { document.querySelector('body')!.style.setProperty('height', '5000px'); globalThis.scrollTo(0, yPos); }, - VirtualScrollPosition + scrollYPosition ); - await test.expect(getScrollTop()).resolves.toEqual(VirtualScrollPosition); + await test.expect(getScrollTop()).resolves.toEqual(scrollYPosition); await lock(); await unlock(); - await test.expect(getScrollTop()).resolves.toEqual(VirtualScrollPosition); + await test.expect(getScrollTop()).resolves.toEqual(scrollYPosition); }); }); diff --git a/tests/helpers/component-object/README.md b/tests/helpers/component-object/README.md index dbbd30de5b..c876e7a083 100644 --- a/tests/helpers/component-object/README.md +++ b/tests/helpers/component-object/README.md @@ -1,6 +1,6 @@ # tests/helpers/component-object -The `ComponentObject` is a base class for testing components. The `component object` pattern allows for a more convenient way to interact with components in a testing environment. +The `ComponentObject` is a base class for creating a component object like components for testing. The `component object` pattern allows for a more convenient way to interact with components in a testing environment. This class can be used as a generic class for any component or can be extended to create a custom `component object` that implements methods for interacting with a specific component. @@ -13,14 +13,10 @@ The class provides a universal API for generating a component and setting up moc #### Basic ```typescript -import ComponentObjectBuilder from 'path/to/ComponentObjectBuilder'; - -class MyComponentObject extends ComponentObjectBuilder { - // Implement specific methods and properties for interacting with the component during tests -} +import ComponentObject from 'path/to/component-object'; -// Create an instance of MyComponentObject -const myComponent = new MyComponentObject(page, 'MyComponent'); +// Create an instance of ComponentObject +const myComponent = new ComponentObject(page, 'MyComponent'); // Build the component await myComponent.build(); @@ -48,6 +44,8 @@ class MyComponentObject extends ComponentObject { // Create an instance of MyComponentObject const myComponent = new MyComponentObject(page, 'MyComponent'); +await myComponent.build(); + // Create a spy const spy = await myComponent.spyOn('someMethod'); @@ -68,6 +66,37 @@ const mockFn = await myComponent.mockFn((arg) => { }); ``` +#### Provide mock function as a prop + +```typescript +import ComponentObject from 'path/to/component-object'; + +class MyComponentObject extends ComponentObject { + // Implement specific methods and properties for testing the component in a mock environment +} + +// Create an instance of MyComponentObject +const + myComponent = new MyComponentObject(page, 'MyComponent'), + someProp = await myComponent.mockFn(() => true); + +myComponent.setProps({ + someProp +}); + +await myComponent.build(); + +// Access the component +const component = myComponent.component; + +// Perform interactions with the component +await component.evaluate((ctx) => { + // Perform actions on the component +}); + +console.log(await someProp.calls); +``` + #### Spy on emitter ```typescript @@ -85,6 +114,8 @@ myComponent.setProps({ '@hook:beforeDataCreate': (ctx) => jest.spy(ctx, 'emit') }); +await myComponent.build(); + // Access the component const component = myComponent.component; diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index f0022cadc9..4ab3eca4a9 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -32,7 +32,7 @@ export class RequestInterceptor { readonly routeListener: ResponseHandler; /** - * Экземпляр jest-mock который берет на себя логику имплементации ответов + * An instance of jest-mock that handles the implementation logic of responses. */ readonly mock: ReturnType; diff --git a/tests/helpers/providers/interceptor/interface.ts b/tests/helpers/providers/interceptor/interface.ts index f59e1b03de..c3ddf78ab2 100644 --- a/tests/helpers/providers/interceptor/interface.ts +++ b/tests/helpers/providers/interceptor/interface.ts @@ -14,6 +14,9 @@ export type ResponseHandler = (route: Route, request: Request) => CanPromise Date: Mon, 10 Jul 2023 16:21:05 +0300 Subject: [PATCH 069/159] :art: --- src/core/shims/ssr/request-animation-frame.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/shims/ssr/request-animation-frame.js b/src/core/shims/ssr/request-animation-frame.js index e29e39dc27..528d763bb9 100644 --- a/src/core/shims/ssr/request-animation-frame.js +++ b/src/core/shims/ssr/request-animation-frame.js @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -/* eslint-disable no-var, vars-on-top */ +/* eslint-disable no-var */ var GLOBAL = require('core/shims/global'); From 1bf8d07aff03e75b81a19d14dcbf471b1b350db5 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 10 Jul 2023 16:47:59 +0300 Subject: [PATCH 070/159] :art: --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b73918d9c6..caf122e70b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ _Note: Gaps between patch versions are faulty, broken or test releases._ #### :boom: Breaking Change -* Major update to `b-virtual-scroll`. Please see to the component's readme for changes and migration guide `components/base/b-virtual-scroll`. +* Major update to `b-virtual-scroll`. Please see the component readme for changes and migration guide `components/base/b-virtual-scroll`. #### :rocket: New Feature From 706dad0d831654a58b6139f17386836a4db5d947 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 10 Jul 2023 16:48:33 +0300 Subject: [PATCH 071/159] :art: --- src/components/base/b-virtual-scroll/TODO.md | 126 ------------------- 1 file changed, 126 deletions(-) delete mode 100644 src/components/base/b-virtual-scroll/TODO.md diff --git a/src/components/base/b-virtual-scroll/TODO.md b/src/components/base/b-virtual-scroll/TODO.md deleted file mode 100644 index 417f3149c6..0000000000 --- a/src/components/base/b-virtual-scroll/TODO.md +++ /dev/null @@ -1,126 +0,0 @@ -- улучшить имена интерфейсов (MountedItem, MountedSeparator, ревью имен в ComponentState) -- debug модуль овверайд в edadeal/core -- описа про мок модуль, calls и ссылки и как это побеждать -- описать в документации про тестовый компонент в демо странице и для чего он нужен -- импорты наладить порядок - -```mermaid -graph TD; - A[loadDataOrPerformRender] -->|state, ctx| B[renderGuard] - B -->|result = true| C[performRender] - B -->|result = false| E[reason] - E -->|done| F[setIsLifecycleDone=true] - E -->|noData| G[isRequestsStopped] - G -->|false| H[shouldPerformDataRequestWrapper] - H -->|true| I[initLoad] - G -->|true| J[return] - E -->|notEnoughData| K[isRequestsStopped] - K -->|true| L[performRender] - K -->|false| M[shouldPerformDataRequestWrapper] - M -->|true| N[initLoad] - M -->|false| O[isInitialRender] - O -->|true| P[performRender] - O -->|false| Q[return] - - -``` - -## TODO: - -1. Бенчмарк подходов - -## Идеи - -1. При скролле брать оффсет скролла и бинарным поиском искать элементы которые сейчас на экране должны быть и отображать их - -```typescript - const vdomCreate: typeof this['vdom']['create'] = this.vdom.create.bind(this.vdom); - const self = this; - - setNodes.forEach((node) => node.remove()); - - if (!vueInstance) { - vueInstance = new Vue({ - render: function () { - const nodes = getArray(count).map((data: DummyUser) => vdomCreate('b-dummy-user', { - attrs: { - dummyData: data, - key: data.userId, - } - })); - - // const nodes = vdomCreate('keep-alive', { - // attrs: {}, - // children: { - // default: () => getArray(count).map((data) => ({ - // type: 'b-dummy-user', - // attrs: { - // dummyData: data, - // key: data.userId, - // } - // })) - // } - // }) - - return nodes; - }, - - beforeCreate() { - let parent = self; - if (parent != null) { - const - root = Object.create(parent.$root); - - Object.defineProperty(root, '$remoteParent', { - configurable: true, - enumerable: true, - writable: true, - value: parent - }); - - Object.defineProperty(this, 'unsafe', { - configurable: true, - enumerable: true, - writable: true, - value: root - }); - } - } - }); - - const - container = document.createElement('div'); - - mountResult = vueInstance.mount(container); - - const - el = this.block?.element('container'); - el?.append(container); - - } else { - mountResult.$forceUpdate(); - } - - this.nextTick(() => { - Array.from(document.querySelectorAll('.b-dummy-user')).forEach((el) => { - if (setNodes.has(el)) { - return; - } - - setNodes.add(el); - el.component._forkDestroy = el.component.$destroy; - - Object.defineProperty(el.component, '$destroy', { - configurable: true, - enumerable: false, - writable: true, - value: () => { - return false; - } - }); - - }); - }); - - console.log(setNodes); -``` \ No newline at end of file From a1569dfc24e51b9f1c568c593bfeee8d77d0aa50 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 12 Jul 2023 10:27:22 +0300 Subject: [PATCH 072/159] :wrench: --- .../test/unit/functional/props/props.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts index c0a5ee5b7f..39d96d7e97 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts @@ -113,5 +113,26 @@ test.describe('', () => { await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); }); + + test('Should convert second data chunk to the component', async () => { + const + chunkSize = 12; + + provider.response(200, () => ({data: {nestedData: state.data.addData(chunkSize)}})); + + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + shouldPerformDataRequest: ({itemsTillEnd}) => itemsTillEnd === 0, + dbConverter: ({data: {nestedData}}) => ({data: nestedData}) + }) + .build(); + + await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.scrollToBottom(); + + await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize * 2)).resolves.toBeUndefined(); + }); }); }); From c535d499a72e94e174b607d4eb7f78a0bf4ba429 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 12 Jul 2023 11:12:49 +0300 Subject: [PATCH 073/159] Fixed an issue with possible chunk inserting cancelation --- src/components/base/b-virtual-scroll/b-virtual-scroll.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts index 1801f46af3..98ccb157a1 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts @@ -332,11 +332,13 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI this.onDomInsertStart(mounted); const - fragment = document.createDocumentFragment(); + fragment = document.createDocumentFragment(), + {renderPage} = this.getComponentState(), + asyncGroup = `${bVirtualScrollDomInsertAsyncGroup}:${renderPage}`; for (let i = 0; i < nodes.length; i++) { this.dom.appendChild(fragment, nodes[i], { - group: bVirtualScrollDomInsertAsyncGroup, + group: asyncGroup, destroyIfComponent: true }); } @@ -347,6 +349,6 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI this.onDomInsertDone(); this.onRenderDone(); - }, {label: $$.insertDomRaf, group: bVirtualScrollDomInsertAsyncGroup}); + }, {label: $$.insertDomRaf, group: asyncGroup}); } } From 4dcfd4d24f1e960f16783032bf74ca04e7eb852f Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 12 Jul 2023 11:55:30 +0300 Subject: [PATCH 074/159] Fixed an issue with tillEnd fields was not updated after data loading --- .../b-virtual-scroll/modules/state/index.ts | 27 +++++++++++++++++++ .../test/unit/functional/state/default.ts | 1 - .../test/unit/functional/state/emitter.ts | 25 ++++++++++------- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/components/base/b-virtual-scroll/modules/state/index.ts b/src/components/base/b-virtual-scroll/modules/state/index.ts index 34592a8ee2..188fc3df0a 100644 --- a/src/components/base/b-virtual-scroll/modules/state/index.ts +++ b/src/components/base/b-virtual-scroll/modules/state/index.ts @@ -86,6 +86,8 @@ export class ComponentInternalState extends Friend { childList.push(...mounted); itemsList.push(...newItems); + + this.updateChildTillEnd(); } /** @@ -156,6 +158,8 @@ export class ComponentInternalState extends Friend { state.maxViewedChild = component.childIndex; state.childTillEnd = state.childList.length - 1 - state.maxViewedChild; } + + this.updateChildTillEnd(); } /** @@ -176,5 +180,28 @@ export class ComponentInternalState extends Friend { this.privateState.dataCursor = current + chunkSize; } + + /** + * Updates the state of the tillEnd-like fields. + * Calculates the remaining number of child elements until the end and the remaining number of items until the end. + */ + updateChildTillEnd(): void { + const + {state} = this; + + if (state.maxViewedChild == null) { + state.childTillEnd = state.childList.length - 1; + + } else { + state.childTillEnd = state.childList.length - 1 - state.maxViewedChild; + } + + if (state.maxViewedItem == null) { + state.itemsTillEnd = state.items.length - 1; + + } else { + state.itemsTillEnd = state.items.length - 1 - state.maxViewedItem; + } + } } diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts index 7eb9fd1601..ddc7269817 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts @@ -252,7 +252,6 @@ test.describe('', () => { isLastEmpty: true, isLifecycleDone: true, maxViewedItem: undefined, - itemsTillEnd: undefined, loadPage: 2, renderPage: 1 })); diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts index 865d62e140..36b9863ff9 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts @@ -21,13 +21,18 @@ test.describe('', () => { provider: VirtualScrollTestHelpers['provider'], state: VirtualScrollTestHelpers['state']; - const initialStateFields = { + const observerInitialStateFields = { itemsTillEnd: undefined, childTillEnd: undefined, maxViewedChild: undefined, maxViewedItem: undefined }; + const observerLoadedStateFields = { + maxViewedChild: undefined, + maxViewedItem: undefined + }; + test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); @@ -40,17 +45,17 @@ test.describe('', () => { const chunkSize = 12; const states = [ - state.compile(initialStateFields), + state.compile(observerInitialStateFields), ( state.data.addData(chunkSize), - state.set({loadPage: 1, isRequestsStopped: true}).compile(initialStateFields) + state.set({loadPage: 1, isRequestsStopped: true}).compile(observerInitialStateFields) ), ( state.data.addItems(chunkSize), - state.set({isInitialRender: false, renderPage: 1}).compile(initialStateFields) + state.set({isInitialRender: false, renderPage: 1}).compile(observerLoadedStateFields) ), ( - state.compile(initialStateFields) + state.compile(observerLoadedStateFields) ), ( state.set({isLifecycleDone: true}).compile() @@ -107,21 +112,21 @@ test.describe('', () => { providerChunkSize = chunkSize / 2; const states = [ - state.compile(initialStateFields), + state.compile(observerInitialStateFields), ( state.data.addData(providerChunkSize), - state.set({loadPage: 1}).compile(initialStateFields) + state.set({loadPage: 1}).compile(observerInitialStateFields) ), ( state.data.addData(providerChunkSize), - state.set({loadPage: 2, isInitialLoading: false}).compile(initialStateFields) + state.set({loadPage: 2, isInitialLoading: false}).compile(observerInitialStateFields) ), ( state.data.addItems(chunkSize), - state.set({renderPage: 1, isInitialRender: false}).compile(initialStateFields) + state.set({renderPage: 1, isInitialRender: false}).compile(observerLoadedStateFields) ), ( - state.compile(initialStateFields) + state.compile(observerLoadedStateFields) ), ( state.compile() From 7ba35dff5c0198e14029536d5c0db0839e54d2b4 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 24 Jul 2023 12:01:12 +0300 Subject: [PATCH 075/159] :art: review --- .../base/b-virtual-scroll/README.md | 37 +- .../base/b-virtual-scroll/b-virtual-scroll.ts | 46 ++- src/components/base/b-virtual-scroll/const.ts | 6 +- .../base/b-virtual-scroll/handlers.ts | 22 +- .../b-virtual-scroll/interface/component.ts | 2 +- .../base/b-virtual-scroll/interface/events.ts | 14 +- .../b-virtual-scroll/interface/requests.ts | 4 +- .../modules/factory/engines/vdom.ts | 13 +- .../b-virtual-scroll/modules/state/helpers.ts | 2 +- .../b-virtual-scroll/modules/state/index.ts | 2 +- src/components/base/b-virtual-scroll/props.ts | 41 +- .../test/api/component-object/index.ts | 4 +- .../test/unit/functional/emitter/payload.ts | 362 +++++++++--------- .../test/unit/functional/props/props.ts | 14 +- .../test/unit/functional/rendering/default.ts | 20 +- .../functional/rendering/items-factory.ts | 40 +- .../test/unit/functional/state/default.ts | 20 +- .../test/unit/functional/state/emitter.ts | 12 +- .../initialization/initialization.ts | 64 ++-- .../test/unit/lifecycle/slots/slots.ts | 30 +- .../test/unit/scenario/manual-rendering.ts | 32 +- .../test/unit/scenario/reload.ts | 6 +- .../test/unit/scenario/retry.ts | 38 +- tests/helpers/component-object/README.md | 12 +- tests/helpers/component-object/builder.ts | 4 +- tests/helpers/component-object/mock.ts | 4 +- tests/helpers/mock/index.ts | 6 +- 27 files changed, 443 insertions(+), 414 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index cab7cfa476..bd370f6362 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -1,6 +1,6 @@ # components/base/b-virtual-scroll -The `b-virtual-scroll` component is designed for rendering a larger array of various data. +The `b-virtual-scroll` component is designed for rendering a large array of various data. ## Synopsis @@ -21,7 +21,7 @@ See the implemented modifiers or the parent component. | `dataLoadSuccess` | Data loading has succeeded. | `data: object[], isInitialLoading: boolean` | `[data, isInitialLoading]` | | `dataLoadStart` | Data loading has started. | `isInitialLoading: boolean` | `[isInitialLoading]` | | `dataLoadError` | An error occurred while loading data. | `isInitialLoading: boolean` | `[isInitialLoading]` | -| `dataEmpty` | Successful load with no data. | | `[]` | +| `dataLoadEmpty` | Successful load with no data. | | `[]` | | `resetState` | Reset component state. | | `[]` | | `lifecycleDone` | All component data is rendered and loaded. | | `[]` | | `convertDataToDB` | Trigger data conversion to the `DB`. | `data: unknown` | `[data]` | @@ -62,16 +62,6 @@ The `dbConverter` prop allows you to convert data into a format suitable for `b- ### Rendering Components -In this example: - -- The `b-virtual-scroll` component is used to render 12 items per one render cycle. -It interacts with the `Provider` data provider to fetch the data. The `request` prop is set to `{ get: { chunkSize: 12 } }`, specifying that each request should fetch 12 items. -- The `requestQuery` function computes additional request parameters based on the component state, specifically the `loadPage` property. These request parameters are merged with the `request` prop. -- The `b-virtual-scroll` component renders `b-dummy` components using the `item` prop. -Each `b-dummy` component receives the `name` and `type` props, which are derived from the `data` object for each item using the `itemProps` function. -- The component includes a `loader` slot that displays the message "Data loading in progress" while the data is being fetched. -- By default, the component stops loading data when it receives an empty response from the `dataProvider`, indicating that there are no more items to load. - ``` < b-virtual-scroll & :dataProvider = 'Provider' | @@ -86,6 +76,16 @@ Each `b-dummy` component receives the `name` and `type` props, which are derived Data loading in progress ``` +In this example: + +- The `b-virtual-scroll` component is used to render 12 items per one render cycle. +It interacts with the `Provider` data provider to fetch the data. The `request` prop is set to `{ get: { chunkSize: 12 } }`, specifying that each request should fetch 12 items. +- The `requestQuery` function computes additional request parameters based on the component state, specifically the `loadPage` property. These request parameters are merged with the `request` prop. +- The `b-virtual-scroll` component renders `b-dummy` components using the `item` prop. +Each `b-dummy` component receives the `name` and `type` props, which are derived from the `data` object for each item using the `itemProps` function. +- The component includes a `loader` slot that displays the message "Data loading in progress" while the data is being fetched. +- By default, the component stops loading data when it receives an empty response from the `dataProvider`, indicating that there are no more items to load. + ### Rendering on click In addition to the standard scroll-based loading, you can implement on-demand loading. @@ -135,7 +135,7 @@ In all of these cases, the component's lifecycle will be reset to its initial st ## Slots -The component supports a bunch of slots to provide. +The component supports several slots for customization: 1. The `loader` slot allows you to display different content (usually skeletons) while the data is being loaded. @@ -150,7 +150,7 @@ The component supports a bunch of slots to provide. ``` < b-virtual-scroll :tombstonesSize = 3 - < template #loader + < template #tombstone < .&__skeleton Skeleton ``` @@ -173,7 +173,8 @@ The component supports a bunch of slots to provide. No data ``` -5. The `done` slot allows you to display different content when the component has finished loading and rendering all the data. +5. The `done` slot allows you to display different content when the component has finished loading and rendering all the data. The `done` slot +will be displayed after `lifecycleDone` event is fired. ``` < b-virtual-scroll @@ -201,7 +202,7 @@ This slot can be useful when implementing lazy content rendering on button click This function is called in the `bVirtualScroll.renderGuard` after other checks are completed. It receives the component state as input and determines whether the component should render the next chunk of components. -The function should return a boolean value: `true` to allow rendering the next chunk, or `false` to prevent it. +The function should return a boolean value: `true` to allow the rendering of the next chunk, or `false` to prevent it. Example usage: @@ -214,9 +215,11 @@ const shouldPerformDataRender = (state: VirtualScrollState): boolean => { ### `shouldPerformDataRequest` - Type: `Function` -- Default: `() => state.lastLoadedData.length > 0` +- Default: `(state: VirtualScrollState) => state.lastLoadedData.length > 0` The `shouldPerformDataRequest` property of `bVirtualScroll` allows you to control whether the component should request additional data based on the component state. +This function allows the component to understand whether the data loading lifecycle is complete or not. + Here's an example of how you can use `shouldPerformDataRequest`: ```typescript diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts index 98ccb157a1..8cb19be16d 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts @@ -17,16 +17,21 @@ import type iItems from 'components/traits/i-items/i-items'; import VDOM, { create, render } from 'components/friends/vdom'; import { iVirtualScrollHandlers } from 'components/base/b-virtual-scroll/handlers'; import { bVirtualScrollDomInsertAsyncGroup, renderGuardRejectionReason } from 'components/base/b-virtual-scroll/const'; -import type { VirtualScrollState, RenderGuardResult, UnsafeBVirtualScroll } from 'components/base/b-virtual-scroll/interface'; +import type { VirtualScrollState, RenderGuardResult, $ComponentRefs, UnsafeBVirtualScroll } from 'components/base/b-virtual-scroll/interface'; -import iData, { $$, component, RequestParams, UnsafeGetter } from 'components/super/i-data/i-data'; +import { ComponentTypedEmitter, componentTypedEmitter } from 'components/base/b-virtual-scroll/modules/emitter'; +import { ComponentInternalState } from 'components/base/b-virtual-scroll/modules/state'; +import { SlotsStateController } from 'components/base/b-virtual-scroll/modules/slots'; +import { ComponentFactory } from 'components/base/b-virtual-scroll/modules/factory'; +import { Observer } from 'components/base/b-virtual-scroll/modules/observer'; + +import iData, { $$, component, system, RequestParams, UnsafeGetter } from 'components/super/i-data/i-data'; export * from 'components/base/b-virtual-scroll/interface'; export * from 'components/base/b-virtual-scroll/const'; export * from 'components/super/i-data/i-data'; -VDOM.addToPrototype(create); -VDOM.addToPrototype(render); +VDOM.addToPrototype({create, render}); /** * Component that implements loading and rendering of large data arrays in chunks. @@ -37,6 +42,29 @@ VDOM.addToPrototype(render); */ @component() export default class bVirtualScroll extends iVirtualScrollHandlers implements iItems { + + /** {@link componentTypedEmitter} */ + @system((ctx) => componentTypedEmitter(ctx)) + protected readonly componentEmitter!: ComponentTypedEmitter; + + /** {@link SlotsStateController} */ + @system((ctx) => new SlotsStateController(ctx)) + protected readonly slotsStateController!: SlotsStateController; + + /** {@link ComponentInternalState} */ + @system((ctx) => new ComponentInternalState(ctx)) + protected readonly componentInternalState!: ComponentInternalState; + + /** {@link ComponentFactory} */ + @system((ctx) => new ComponentFactory(ctx)) + protected readonly componentFactory!: ComponentFactory; + + /** {@link Observer} */ + @system((ctx) => new Observer(ctx)) + protected readonly observer!: Observer; + + protected override readonly $refs!: iData['$refs'] & $ComponentRefs; + // @ts-ignore (getter instead readonly) override get requestParams(): iData['requestParams'] { return { @@ -163,8 +191,8 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI shouldStopRequestingDataWrapper(this: bVirtualScroll): boolean { const state = this.getComponentState(); - if (state.isRequestsStopped) { - return state.isRequestsStopped; + if (state.areRequestsStopped) { + return state.areRequestsStopped; } const newVal = this.shouldStopRequestingData(state, this); @@ -229,7 +257,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI dataSlice = this.getNextDataSlice(state, chunkSize); if (dataSlice.length === 0) { - if (state.isRequestsStopped) { + if (state.areRequestsStopped) { return { result: false, reason: renderGuardRejectionReason.done @@ -292,7 +320,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI } if (reason === renderGuardRejectionReason.noData) { - if (state.isRequestsStopped) { + if (state.areRequestsStopped) { return; } @@ -302,7 +330,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI } if (reason === renderGuardRejectionReason.notEnoughData) { - if (state.isRequestsStopped) { + if (state.areRequestsStopped) { this.performRender(); this.onLifecycleDone(); diff --git a/src/components/base/b-virtual-scroll/const.ts b/src/components/base/b-virtual-scroll/const.ts index fb33588848..c73c364ab8 100644 --- a/src/components/base/b-virtual-scroll/const.ts +++ b/src/components/base/b-virtual-scroll/const.ts @@ -37,7 +37,7 @@ export const componentDataLocalEvents: ComponentDataLocalEvents = { dataLoadStart: 'dataLoadStart', dataLoadError: 'dataLoadError', dataLoadSuccess: 'dataLoadSuccess', - dataEmpty: 'dataEmpty' + dataLoadEmpty: 'dataLoadEmpty' }; /** @@ -96,8 +96,8 @@ export const componentItemType: ComponentItemType = { export const defaultShouldProps = { /** {@link bVirtualScroll.shouldStopRequestingData} */ shouldStopRequestingData: (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { - const isLastRequestNotEmpty = () => state.lastLoadedData.length > 0; - return !isLastRequestNotEmpty(); + const isLastRequestEmpty = () => state.lastLoadedData.length === 0; + return isLastRequestEmpty(); }, /** {@link bVirtualScroll.shouldPerformDataRequest} */ diff --git a/src/components/base/b-virtual-scroll/handlers.ts b/src/components/base/b-virtual-scroll/handlers.ts index ff76522cfd..19a0d9f64e 100644 --- a/src/components/base/b-virtual-scroll/handlers.ts +++ b/src/components/base/b-virtual-scroll/handlers.ts @@ -26,7 +26,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: component reset event. * Resets the component state to its initial state. */ - protected onReset(): void { + protected onReset(this: bVirtualScroll): void { this.componentInternalState.reset(); this.observer.reset(); @@ -39,7 +39,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: render start event. * Triggered when the component rendering starts. */ - protected onRenderStart(): void { + protected onRenderStart(this: bVirtualScroll): void { this.componentEmitter.emit(componentEvents.renderStart); } @@ -47,7 +47,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: render engine start event. * Triggered when the component rendering using the rendering engine starts. */ - protected onRenderEngineStart(): void { + protected onRenderEngineStart(this: bVirtualScroll): void { this.componentEmitter.emit(componentEvents.renderEngineStart); } @@ -55,7 +55,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: render engine done event. * Triggered when the component rendering using the rendering engine is completed. */ - protected onRenderEngineDone(): void { + protected onRenderEngineDone(this: bVirtualScroll): void { this.componentEmitter.emit(componentEvents.renderEngineDone); } @@ -78,7 +78,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: DOM insert done event. * Triggered when the insertion of rendered components into the DOM tree is completed. */ - protected onDomInsertDone(): void { + protected onDomInsertDone(this: bVirtualScroll): void { this.componentEmitter.emit(componentEvents.domInsertDone); } @@ -86,7 +86,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: render done event. * Triggered when rendering is completed. */ - protected onRenderDone(): void { + protected onRenderDone(this: bVirtualScroll): void { this.componentEmitter.emit(componentEvents.renderDone); } @@ -113,7 +113,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * * @param data - The converted data. */ - protected onConvertDataToDB(data: unknown): void { + protected onConvertDataToDB(this: bVirtualScroll, data: unknown): void { this.componentInternalState.setRawLastLoaded(data); this.componentEmitter.emit(componentEvents.convertDataToDB, data); } @@ -124,7 +124,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * * @param isInitialLoading - Indicates whether it is an initial component loading. */ - protected onDataLoadStart(isInitialLoading: boolean): void { + protected onDataLoadStart(this: bVirtualScroll, isInitialLoading: boolean): void { this.componentInternalState.setIsLoadingInProgress(true); this.componentInternalState.setIsLastErrored(false); this.slotsStateController.loadingProgressState(isInitialLoading); @@ -176,7 +176,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * * @param isInitialLoading - Indicates whether it is an initial component loading. */ - protected onDataLoadError(isInitialLoading: boolean): void { + protected onDataLoadError(this: bVirtualScroll, isInitialLoading: boolean): void { this.componentInternalState.setIsLoadingInProgress(false); this.componentInternalState.setIsLastErrored(true); this.slotsStateController.loadingFailedState(); @@ -203,10 +203,10 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: data empty event. * Triggered when the loaded data is empty. */ - protected onDataEmpty(): void { + protected onDataEmpty(this: bVirtualScroll): void { this.slotsStateController.emptyState(); - this.componentEmitter.emit(componentEvents.dataEmpty); + this.componentEmitter.emit(componentEvents.dataLoadEmpty); } /** diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts index d7b5ead44a..2d4a1b9e65 100644 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -71,7 +71,7 @@ export interface VirtualScrollState { /** * Indicates if the component has stopped making requests. */ - isRequestsStopped: boolean; + areRequestsStopped: boolean; /** * Indicates if there is an ongoing loading process. diff --git a/src/components/base/b-virtual-scroll/interface/events.ts b/src/components/base/b-virtual-scroll/interface/events.ts index ba5756912b..6fbca2fe08 100644 --- a/src/components/base/b-virtual-scroll/interface/events.ts +++ b/src/components/base/b-virtual-scroll/interface/events.ts @@ -7,7 +7,15 @@ */ import type { MountedChild } from 'components/base/b-virtual-scroll/interface/component'; -import { componentDataLocalEvents, componentLocalEvents, componentObserverLocalEvents, componentRenderLocalEvents } from 'components/base/b-virtual-scroll/const'; + +import { + + componentDataLocalEvents, + componentLocalEvents, + componentObserverLocalEvents, + componentRenderLocalEvents + +} from 'components/base/b-virtual-scroll/const'; /** * Component data-related events (emitted in `selfEmitter`). @@ -31,7 +39,7 @@ export interface ComponentDataLocalEvents { /** * Successful load with no data. */ - dataEmpty: 'dataEmpty'; + dataLoadEmpty: 'dataLoadEmpty'; } /** @@ -116,7 +124,7 @@ export interface LocalEventPayloadMap { [componentDataLocalEvents.dataLoadSuccess]: [data: object[], isInitialLoading: boolean]; [componentDataLocalEvents.dataLoadStart]: [isInitialLoading: boolean]; [componentDataLocalEvents.dataLoadError]: [isInitialLoading: boolean]; - [componentDataLocalEvents.dataEmpty]: []; + [componentDataLocalEvents.dataLoadEmpty]: []; [componentLocalEvents.resetState]: []; [componentLocalEvents.lifecycleDone]: []; diff --git a/src/components/base/b-virtual-scroll/interface/requests.ts b/src/components/base/b-virtual-scroll/interface/requests.ts index 661349eaa4..c28d947626 100644 --- a/src/components/base/b-virtual-scroll/interface/requests.ts +++ b/src/components/base/b-virtual-scroll/interface/requests.ts @@ -16,9 +16,9 @@ export interface RequestQueryFn { /** * Returns the GET parameters for a request. * - * @param params - The component state. + * @param state - The component state. */ - (params: VirtualScrollState): Dictionary; + (state: VirtualScrollState): Dictionary; } /** diff --git a/src/components/base/b-virtual-scroll/modules/factory/engines/vdom.ts b/src/components/base/b-virtual-scroll/modules/factory/engines/vdom.ts index 0e8f3921a2..648f8b5dc5 100644 --- a/src/components/base/b-virtual-scroll/modules/factory/engines/vdom.ts +++ b/src/components/base/b-virtual-scroll/modules/factory/engines/vdom.ts @@ -18,8 +18,17 @@ import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scro export function render(ctx: bVirtualScroll, items: VNodeDescriptor[]): HTMLElement[] { const vnodes = ctx.vdom.create(...items), - // https://github.com/vuejs/core/issues/6061 - nodes = ctx.vdom.render(vnodes).filter((node) => node.nodeType !== node.TEXT_NODE); + nodes = ctx.vdom.render(vnodes); + + // https://github.com/vuejs/core/issues/6061 + if (nodes[0].nodeType === Node.TEXT_NODE) { + nodes.shift(); + } + + // https://github.com/vuejs/core/issues/6061 + if (nodes[nodes.length - 1].nodeType === Node.TEXT_NODE) { + nodes.pop(); + } return nodes; } diff --git a/src/components/base/b-virtual-scroll/modules/state/helpers.ts b/src/components/base/b-virtual-scroll/modules/state/helpers.ts index e72a0a5abf..c83fd0cea7 100644 --- a/src/components/base/b-virtual-scroll/modules/state/helpers.ts +++ b/src/components/base/b-virtual-scroll/modules/state/helpers.ts @@ -27,7 +27,7 @@ export function createInitialState(): VirtualScrollState { items: [], childList: [], isInitialRender: true, - isRequestsStopped: false, + areRequestsStopped: false, isLoadingInProgress: false, isLifecycleDone: false, isLastErrored: false diff --git a/src/components/base/b-virtual-scroll/modules/state/index.ts b/src/components/base/b-virtual-scroll/modules/state/index.ts index 188fc3df0a..3c4c717336 100644 --- a/src/components/base/b-virtual-scroll/modules/state/index.ts +++ b/src/components/base/b-virtual-scroll/modules/state/index.ts @@ -113,7 +113,7 @@ export class ComponentInternalState extends Friend { * @param value - The value of the flag. */ setIsRequestsStopped(value: boolean): void { - this.state.isRequestsStopped = value; + this.state.areRequestsStopped = value; } /** diff --git a/src/components/base/b-virtual-scroll/props.ts b/src/components/base/b-virtual-scroll/props.ts index 68412bdbe6..9781de7bc7 100644 --- a/src/components/base/b-virtual-scroll/props.ts +++ b/src/components/base/b-virtual-scroll/props.ts @@ -18,23 +18,18 @@ import type { RequestQueryFn, ShouldPerform, ComponentItemFactory, - ComponentItemType, - $ComponentRefs + ComponentItemType } from 'components/base/b-virtual-scroll/interface'; import { defaultShouldProps, componentItemType } from 'components/base/b-virtual-scroll/const'; -import { ComponentTypedEmitter, componentTypedEmitter } from 'components/base/b-virtual-scroll/modules/emitter'; -import { ComponentInternalState } from 'components/base/b-virtual-scroll/modules/state'; -import { SlotsStateController } from 'components/base/b-virtual-scroll/modules/slots'; -import { ComponentFactory } from 'components/base/b-virtual-scroll/modules/factory'; import { Observer } from 'components/base/b-virtual-scroll/modules/observer'; -import iData, { component, prop, system } from 'components/super/i-data/i-data'; +import iData, { component, prop } from 'components/super/i-data/i-data'; /** - * A class that is friendly to {@link bVirtualScroll}. + * A class that is a part of the {@link bVirtualScroll}. * It contains the properties of the {@link bVirtualScroll} component. */ @component() @@ -80,10 +75,10 @@ export default abstract class iVirtualScrollProps extends iData { * In other words, the default implementation takes a data slice of length `chunkSize` * and calls the `iItems` functions to generate a `ComponentItem` object. * - * However, nothing prevents the client from implementing any strategy by overriding this function. + * However, the client can implement any required strategy by overriding this function. * * For example, it is possible to define a function - * that takes the last loaded data and draws twice as many components: + * that takes the last loaded data and renders twice as many components: * * @example * ```typescript @@ -135,7 +130,7 @@ export default abstract class iVirtualScrollProps extends iData { * from the `request` prop in favor of the `request` prop. * * This function is useful when you need to pass pagination parameters or any other parameters that should not trigger - * a component state reload, unlike changing the `request` prop. + * a component's state reload, unlike changing the `request` prop. * * {@link RequestQueryFn} */ @@ -208,34 +203,12 @@ export default abstract class iVirtualScrollProps extends iData { readonly shouldPerformDataRender?: ShouldPerform; /** - * If `true`, the element observation module will not be initialized. + * If `true`, the element {@link Observer observation module} will not be initialized. * * Setting this prop to `true` can be useful if you want to implement lazy rendering * and control it using the `renderNext` method. */ @prop(Boolean) readonly disableObserver: boolean = false; - - /** {@link componentTypedEmitter} */ - @system((ctx) => componentTypedEmitter(ctx)) - protected readonly componentEmitter!: ComponentTypedEmitter; - - /** {@link SlotsStateController} */ - @system((ctx) => new SlotsStateController(ctx)) - protected readonly slotsStateController!: SlotsStateController; - - /** {@link ComponentInternalState} */ - @system((ctx) => new ComponentInternalState(ctx)) - protected readonly componentInternalState!: ComponentInternalState; - - /** {@link ComponentFactory} */ - @system((ctx) => new ComponentFactory(ctx)) - protected readonly componentFactory!: ComponentFactory; - - /** {@link Observer} */ - @system((ctx) => new Observer(ctx)) - protected readonly observer!: Observer; - - protected override readonly $refs!: iData['$refs'] & $ComponentRefs; } diff --git a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts index 14ec3c9c4a..dfd95546cf 100644 --- a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts @@ -61,7 +61,7 @@ export class VirtualScrollComponentObject extends ComponentObject { + async getChildCount(): Promise { return this.childList.count(); } @@ -71,7 +71,7 @@ export class VirtualScrollComponentObject extends ComponentObject { + async waitForChildCountEqualsTo(count: number): Promise { await this.childList.nth(count - 1).waitFor({state: 'attached'}); const realCount = await this.childList.count(); diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts b/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts index 502e3828a4..7e25d0447d 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts @@ -28,188 +28,196 @@ test.describe('', () => { await provider.start(); }); - test('All data has been loaded after the initial load', async () => { - const chunkSize = 12; - - provider - .responseOnce(200, {data: state.data.addData(chunkSize)}) - .response(200, {data: []}); - - await component - .withDefaultPaginationProviderProps({chunkSize}) - .withProps({ - chunkSize, - shouldStopRequestingData: () => true, - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') - }) - .build(); - - await component.waitForLifecycleDone(); - - const - spy = await component.getSpy((ctx) => ctx.emit), - calls = filterEmitterCalls(await spy.calls); - - test.expect(calls).toEqual([ - ['dataLoadStart', true], - ['convertDataToDB', {data: state.data.data}], - ['dataLoadSuccess', state.data.data, true], - ['renderStart'], - ['renderEngineStart'], - ['renderEngineDone'], - ['domInsertStart'], - ['domInsertDone'], - ['renderDone'], - ['lifecycleDone'] - ]); + test.describe('all data has been loaded after the initial load', () => { + test('should emit the correct set of events with the correct set of arguments', async () => { + const chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: []}); + + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + shouldStopRequestingData: () => true, + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') + }) + .build(); + + await component.waitForLifecycleDone(); + + const + spy = await component.getSpy((ctx) => ctx.emit), + calls = filterEmitterCalls(await spy.calls); + + test.expect(calls).toEqual([ + ['dataLoadStart', true], + ['convertDataToDB', {data: state.data.data}], + ['dataLoadSuccess', state.data.data, true], + ['renderStart'], + ['renderEngineStart'], + ['renderEngineDone'], + ['domInsertStart'], + ['domInsertDone'], + ['renderDone'], + ['lifecycleDone'] + ]); + }); }); - test('All data has been loaded after the second load', async () => { - const - chunkSize = 12, - providerChunkSize = chunkSize / 2; - - const - firstDataChunk = state.data.addData(providerChunkSize), - secondDataChunk = state.data.addData(providerChunkSize); - - provider - .responseOnce(200, {data: firstDataChunk}) - .responseOnce(200, {data: secondDataChunk}) - .response(200, {data: []}); - - await component - .withDefaultPaginationProviderProps({chunkSize: providerChunkSize}) - .withProps({ - chunkSize, - shouldPerformDataRequest: () => true, - shouldStopRequestingData: ({lastLoadedData}) => lastLoadedData.length === 0, - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') - }) - .build(); - - await component.waitForContainerChildCountEqualsTo(chunkSize); - - const - spy = await component.getSpy((ctx) => ctx.emit), - calls = filterEmitterCalls(await spy.calls); - - test.expect(calls).toEqual([ - ['dataLoadStart', true], - ['convertDataToDB', {data: firstDataChunk}], - ['dataLoadSuccess', firstDataChunk, true], - ['dataLoadStart', false], - ['convertDataToDB', {data: secondDataChunk}], - ['dataLoadSuccess', secondDataChunk, false], - ['renderStart'], - ['renderEngineStart'], - ['renderEngineDone'], - ['domInsertStart'], - ['domInsertDone'], - ['renderDone'], - ['dataLoadStart', false], - ['convertDataToDB', {data: []}], - ['dataLoadSuccess', [], false], - ['lifecycleDone'] - ]); + test.describe('all data has been loaded after the second load', () => { + test('should emit the correct set of events with the correct set of arguments', async () => { + const + chunkSize = 12, + providerChunkSize = chunkSize / 2; + + const + firstDataChunk = state.data.addData(providerChunkSize), + secondDataChunk = state.data.addData(providerChunkSize); + + provider + .responseOnce(200, {data: firstDataChunk}) + .responseOnce(200, {data: secondDataChunk}) + .response(200, {data: []}); + + await component + .withDefaultPaginationProviderProps({chunkSize: providerChunkSize}) + .withProps({ + chunkSize, + shouldPerformDataRequest: () => true, + shouldStopRequestingData: ({lastLoadedData}) => lastLoadedData.length === 0, + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') + }) + .build(); + + await component.waitForChildCountEqualsTo(chunkSize); + + const + spy = await component.getSpy((ctx) => ctx.emit), + calls = filterEmitterCalls(await spy.calls); + + test.expect(calls).toEqual([ + ['dataLoadStart', true], + ['convertDataToDB', {data: firstDataChunk}], + ['dataLoadSuccess', firstDataChunk, true], + ['dataLoadStart', false], + ['convertDataToDB', {data: secondDataChunk}], + ['dataLoadSuccess', secondDataChunk, false], + ['renderStart'], + ['renderEngineStart'], + ['renderEngineDone'], + ['domInsertStart'], + ['domInsertDone'], + ['renderDone'], + ['dataLoadStart', false], + ['convertDataToDB', {data: []}], + ['dataLoadSuccess', [], false], + ['lifecycleDone'] + ]); + }); }); - test('Data loading is completed but data is less than chunkSize', async () => { - const - chunkSize = 12, - providerChunkSize = chunkSize / 2; - - const - firstDataChunk = state.data.addData(providerChunkSize); - - provider - .responseOnce(200, {data: firstDataChunk}) - .response(200, {data: []}); - - await component - .withDefaultPaginationProviderProps({chunkSize: providerChunkSize}) - .withProps({ - chunkSize, - shouldPerformDataRequest: () => true, - shouldStopRequestingData: ({lastLoadedData}) => lastLoadedData.length === 0, - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') - }) - .build(); - - await component.waitForContainerChildCountEqualsTo(providerChunkSize); - await component.waitForLifecycleDone(); - - const - spy = await component.getSpy((ctx) => ctx.emit), - calls = filterEmitterCalls(await spy.calls); - - test.expect(calls).toEqual([ - ['dataLoadStart', true], - ['convertDataToDB', {data: firstDataChunk}], - ['dataLoadSuccess', firstDataChunk, true], - ['dataLoadStart', false], - ['convertDataToDB', {data: []}], - ['dataLoadSuccess', [], false], - ['renderStart'], - ['renderEngineStart'], - ['renderEngineDone'], - ['domInsertStart'], - ['lifecycleDone'], - ['domInsertDone'], - ['renderDone'] - ]); + test.describe('data loading is completed but data is less than chunkSize', () => { + test('should emit the correct set of events with the correct set of arguments', async () => { + const + chunkSize = 12, + providerChunkSize = chunkSize / 2; + + const + firstDataChunk = state.data.addData(providerChunkSize); + + provider + .responseOnce(200, {data: firstDataChunk}) + .response(200, {data: []}); + + await component + .withDefaultPaginationProviderProps({chunkSize: providerChunkSize}) + .withProps({ + chunkSize, + shouldPerformDataRequest: () => true, + shouldStopRequestingData: ({lastLoadedData}) => lastLoadedData.length === 0, + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') + }) + .build(); + + await component.waitForChildCountEqualsTo(providerChunkSize); + await component.waitForLifecycleDone(); + + const + spy = await component.getSpy((ctx) => ctx.emit), + calls = filterEmitterCalls(await spy.calls); + + test.expect(calls).toEqual([ + ['dataLoadStart', true], + ['convertDataToDB', {data: firstDataChunk}], + ['dataLoadSuccess', firstDataChunk, true], + ['dataLoadStart', false], + ['convertDataToDB', {data: []}], + ['dataLoadSuccess', [], false], + ['renderStart'], + ['renderEngineStart'], + ['renderEngineDone'], + ['domInsertStart'], + ['lifecycleDone'], + ['domInsertDone'], + ['renderDone'] + ]); + }); }); - test('Reload was called after data was rendered', async () => { - const chunkSize = 12; - - provider - .responseOnce(200, {data: state.data.addData(chunkSize)}) - .response(200, {data: []}); - - await component - .withDefaultPaginationProviderProps({chunkSize}) - .withProps({ - chunkSize, - shouldStopRequestingData: () => true, - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') - }) - .build(); - - await component.waitForContainerChildCountEqualsTo(chunkSize); - - state.reset(); - provider.responseOnce(200, {data: state.data.addData(chunkSize)}); - - await component.reload(); - await component.waitForContainerChildCountEqualsTo(chunkSize); - - const - spy = await component.getSpy((ctx) => ctx.emit), - calls = filterEmitterCalls(await spy.calls); - - test.expect(calls).toEqual([ - ['dataLoadStart', true], - ['convertDataToDB', {data: state.data.data}], - ['dataLoadSuccess', state.data.data, true], - ['renderStart'], - ['renderEngineStart'], - ['renderEngineDone'], - ['domInsertStart'], - ['domInsertDone'], - ['renderDone'], - ['lifecycleDone'], - ['resetState'], - ['dataLoadStart', true], - ['convertDataToDB', {data: state.data.data}], - ['dataLoadSuccess', state.data.data, true], - ['renderStart'], - ['renderEngineStart'], - ['renderEngineDone'], - ['domInsertStart'], - ['domInsertDone'], - ['renderDone'], - ['lifecycleDone'] - ]); + test.describe('reload was called after data was rendered', () => { + test('should emit the correct set of events with the correct set of arguments', async () => { + const chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: []}); + + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + shouldStopRequestingData: () => true, + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') + }) + .build(); + + await component.waitForChildCountEqualsTo(chunkSize); + + state.reset(); + provider.responseOnce(200, {data: state.data.addData(chunkSize)}); + + await component.reload(); + await component.waitForChildCountEqualsTo(chunkSize); + + const + spy = await component.getSpy((ctx) => ctx.emit), + calls = filterEmitterCalls(await spy.calls); + + test.expect(calls).toEqual([ + ['dataLoadStart', true], + ['convertDataToDB', {data: state.data.data}], + ['dataLoadSuccess', state.data.data, true], + ['renderStart'], + ['renderEngineStart'], + ['renderEngineDone'], + ['domInsertStart'], + ['domInsertDone'], + ['renderDone'], + ['lifecycleDone'], + ['resetState'], + ['dataLoadStart', true], + ['convertDataToDB', {data: state.data.data}], + ['dataLoadSuccess', state.data.data, true], + ['renderStart'], + ['renderEngineStart'], + ['renderEngineDone'], + ['domInsertStart'], + ['domInsertDone'], + ['renderDone'], + ['lifecycleDone'] + ]); + }); }); }); diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts index 39d96d7e97..807434386d 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts @@ -48,17 +48,17 @@ test.describe('', () => { }) .pick(demoPage.buildTestComponent(component.componentName, component.props)); - await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.waitForChildCountEqualsTo(chunkSize); await demoPage.updateTestComponent({chunkSize: chunkSize * 2}); await component.scrollToBottom(); - await component.waitForContainerChildCountEqualsTo(chunkSize * 3); + await component.waitForChildCountEqualsTo(chunkSize * 3); const produceSpy = await component.getSpy((ctx) => ctx.componentFactory.produceComponentItems); test.expect(provider.mock.mock.calls.length).toBe(3); await test.expect(produceSpy.calls).resolves.toHaveLength(2); - await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize * 3)).resolves.toBeUndefined(); + await test.expect(component.waitForChildCountEqualsTo(chunkSize * 3)).resolves.toBeUndefined(); await test.expect(component.waitForDataIndexChild(chunkSize * 3 - 1)).resolves.toBeUndefined(); }); }); @@ -80,7 +80,7 @@ test.describe('', () => { }) .build(); - await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.waitForChildCountEqualsTo(chunkSize); const providerCalls = provider.mock.mock.calls, @@ -111,7 +111,7 @@ test.describe('', () => { }) .build(); - await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); + await test.expect(component.waitForChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); }); test('Should convert second data chunk to the component', async () => { @@ -129,10 +129,10 @@ test.describe('', () => { }) .build(); - await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.waitForChildCountEqualsTo(chunkSize); await component.scrollToBottom(); - await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize * 2)).resolves.toBeUndefined(); + await test.expect(component.waitForChildCountEqualsTo(chunkSize * 2)).resolves.toBeUndefined(); }); }); }); diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts index c2da22925e..658ea98897 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts @@ -31,8 +31,8 @@ test.describe('', () => { await page.setViewportSize({height: 640, width: 360}); }); - test.describe('ChunkSize is 12', () => { - test.describe('Provider can provide 3 data chunks', () => { + test.describe('`chunkSize` is 12', () => { + test.describe('provider can provide 3 data chunks', () => { test('Should render 36 items', async () => { const chunkSize = 12; @@ -55,20 +55,20 @@ test.describe('', () => { }); await component.build(); - await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.waitForChildCountEqualsTo(chunkSize); await component.scrollToBottom(); - await component.waitForContainerChildCountEqualsTo(chunkSize * 2); + await component.waitForChildCountEqualsTo(chunkSize * 2); await component.scrollToBottom(); - await component.waitForContainerChildCountEqualsTo(chunkSize * 3); + await component.waitForChildCountEqualsTo(chunkSize * 3); await component.scrollToBottom(); - await component.waitForContainerChildCountEqualsTo(chunkSize * 3); + await component.waitForChildCountEqualsTo(chunkSize * 3); await test.expect(component.childList).toHaveCount(chunkSize * 3); }); }); }); - test.describe('With a different chunk size for each render cycle', () => { + test.describe('with a different chunk size for each render cycle', () => { test('Should render 6 components first, then 12, then 18', async () => { const chunkSize = [6, 12, 18]; @@ -90,7 +90,7 @@ test.describe('', () => { const expectedIndex = chunkSize[0]; - await test.expect(component.waitForContainerChildCountEqualsTo(expectedIndex)).resolves.toBeUndefined(); + await test.expect(component.waitForChildCountEqualsTo(expectedIndex)).resolves.toBeUndefined(); await test.expect(component.waitForDataIndexChild(expectedIndex - 1)).resolves.toBeUndefined(); }); @@ -100,7 +100,7 @@ test.describe('', () => { await component.scrollToBottom(); - await test.expect(component.waitForContainerChildCountEqualsTo(expectedIndex)).resolves.toBeUndefined(); + await test.expect(component.waitForChildCountEqualsTo(expectedIndex)).resolves.toBeUndefined(); await test.expect(component.waitForDataIndexChild(expectedIndex - 1)).resolves.toBeUndefined(); }); @@ -110,7 +110,7 @@ test.describe('', () => { await component.scrollToBottom(); - await test.expect(component.waitForContainerChildCountEqualsTo(expectedIndex)).resolves.toBeUndefined(); + await test.expect(component.waitForChildCountEqualsTo(expectedIndex)).resolves.toBeUndefined(); await test.expect(component.waitForDataIndexChild(expectedIndex - 1)).resolves.toBeUndefined(); }); diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts index c30a338cda..88ace5a3e7 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts @@ -30,8 +30,8 @@ test.describe(' rendering via itemsFactory', () => { await provider.start(); }); - test.describe('Returned items with type `item` is equal to the provided data', () => { - test('Should render all of the items that was returned from `itemsFactory`', async () => { + test.describe('returned items with type `item` is equal to the provided data', () => { + test('should render all of the items that were returned from `itemsFactory`', async () => { const chunkSize = 12; @@ -62,14 +62,14 @@ test.describe(' rendering via itemsFactory', () => { }) .build(); - await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.waitForChildCountEqualsTo(chunkSize); await test.expect(component.childList).toHaveCount(chunkSize); }); }); test.describe('In additional `item`, `separator` was also returned', () => { - test('Should render both', async () => { + test('should render both', async () => { const chunkSize = 12; @@ -117,15 +117,15 @@ test.describe(' rendering via itemsFactory', () => { }) .build(); - await component.waitForContainerChildCountEqualsTo(chunkSize + 1); + await component.waitForChildCountEqualsTo(chunkSize + 1); await test.expect(component.container.locator('#button')).toBeVisible(); await test.expect(component.childList).toHaveCount(chunkSize + 1); }); }); - test.describe('Returned items with type `item` is less than the provided data', () => { - test('Should render items that was returned from `itemsFactory`', async () => { + test.describe('returned items with type `item` is less than the provided data', () => { + test('should render items that were returned from `itemsFactory`', async () => { const chunkSize = 12, renderedChunkSize = chunkSize - 2; @@ -160,14 +160,14 @@ test.describe(' rendering via itemsFactory', () => { }) .build(); - await component.waitForContainerChildCountEqualsTo(renderedChunkSize); + await component.waitForChildCountEqualsTo(renderedChunkSize); await test.expect(component.childList).toHaveCount(renderedChunkSize); }); }); - test.describe('Returned item with type `item` is more than the provided data', () => { - test('Should render items that was returned from `itemsFactory`', async () => { + test.describe('returned item with type `item` is more than the provided data', () => { + test('should render items that were returned from `itemsFactory`', async () => { const chunkSize = 12, renderedChunkSize = chunkSize * 2; @@ -201,14 +201,14 @@ test.describe(' rendering via itemsFactory', () => { }) .build(); - await component.waitForContainerChildCountEqualsTo(renderedChunkSize); + await component.waitForChildCountEqualsTo(renderedChunkSize); await test.expect(component.childList).toHaveCount(renderedChunkSize); }); }); - test.describe('`item` was not returned, but equal to the number of data, the number of `separator` was returned', () => { - test('Should render separators that was returned from `itemsFactory`', async () => { + test.describe('The items of type `item` were not returned, but the items of type `separator` were returned in the same quantity as the loaded data.', () => { + test('should render separators that were returned from `itemsFactory`', async () => { const chunkSize = 12; @@ -239,14 +239,14 @@ test.describe(' rendering via itemsFactory', () => { }) .build(); - await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.waitForChildCountEqualsTo(chunkSize); await test.expect(component.childList).toHaveCount(chunkSize); }); }); - test.describe('`itemsFactory` returns twice as much data as `chunkSize`', () => { - test('Should render twice as much items as `chunkSize`', async () => { + test.describe('`itemsFactory` returns twice as much data as the `chunkSize`', () => { + test('should render twice as much items as the `chunkSize`', async () => { const chunkSize = 12; @@ -285,13 +285,13 @@ test.describe(' rendering via itemsFactory', () => { }) .build(); - await component.waitForContainerChildCountEqualsTo(chunkSize * 2); + await component.waitForChildCountEqualsTo(chunkSize * 2); await component.scrollToBottom(); - await component.waitForContainerChildCountEqualsTo(chunkSize * 2 * 2); + await component.waitForChildCountEqualsTo(chunkSize * 2 * 2); await component.scrollToBottom(); - await component.waitForContainerChildCountEqualsTo(chunkSize * 3 * 2); + await component.waitForChildCountEqualsTo(chunkSize * 3 * 2); await component.scrollToBottom(); - await component.waitForContainerChildCountEqualsTo(chunkSize * 3 * 2); + await component.waitForChildCountEqualsTo(chunkSize * 3 * 2); await test.expect(component.childList).toHaveCount(chunkSize * 3 * 2); }); diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts index ddc7269817..8abbddcbdf 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts @@ -45,7 +45,7 @@ test.describe('', () => { childTillEnd: undefined, maxViewedItem: undefined, maxViewedChild: undefined, - isRequestsStopped: false, + areRequestsStopped: false, isLoadingInProgress: true, lastLoadedData: [], loadPage: 0 @@ -90,7 +90,7 @@ test.describe('', () => { }) .build(); - await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.waitForChildCountEqualsTo(chunkSize); const currentState = await component.getComponentState(); @@ -98,7 +98,7 @@ test.describe('', () => { test.expect(currentState).toEqual(state.compile({ isInitialLoading: false, isInitialRender: false, - isRequestsStopped: false, + areRequestsStopped: false, isLoadingInProgress: false, loadPage: 2, renderPage: 1 @@ -114,7 +114,7 @@ test.describe('', () => { state.data.addItems(chunkSize); await component.scrollToBottom(); - await component.waitForContainerChildCountEqualsTo(chunkSize * 2); + await component.waitForChildCountEqualsTo(chunkSize * 2); await component.scrollToBottom(); await component.waitForLifecycleDone(); @@ -124,7 +124,7 @@ test.describe('', () => { test.expect(currentState).toEqual(state.compile({ isInitialLoading: false, isInitialRender: false, - isRequestsStopped: true, + areRequestsStopped: true, isLoadingInProgress: false, isLastEmpty: true, isLifecycleDone: true, @@ -134,7 +134,7 @@ test.describe('', () => { }); }); - test.describe('State after rendering via `itemsFactory`', () => { + test.describe('state after rendering via `itemsFactory`', () => { test('`itemsFactory` returns items with `item` and `separator` type', async () => { const chunkSize = 12; @@ -186,7 +186,7 @@ test.describe('', () => { }) .build(); - await component.waitForContainerChildCountEqualsTo(chunkSize + 1); + await component.waitForChildCountEqualsTo(chunkSize + 1); await component.waitForLifecycleDone(); const @@ -195,7 +195,7 @@ test.describe('', () => { test.expect(currentState).toEqual(state.compile({ isInitialLoading: false, isInitialRender: false, - isRequestsStopped: true, + areRequestsStopped: true, isLoadingInProgress: false, isLastEmpty: true, isLifecycleDone: true, @@ -238,7 +238,7 @@ test.describe('', () => { }) .build(); - await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.waitForChildCountEqualsTo(chunkSize); await component.waitForLifecycleDone(); const @@ -247,7 +247,7 @@ test.describe('', () => { test.expect(currentState).toEqual(state.compile({ isInitialLoading: false, isInitialRender: false, - isRequestsStopped: true, + areRequestsStopped: true, isLoadingInProgress: false, isLastEmpty: true, isLifecycleDone: true, diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts index 36b9863ff9..a1bce1bb51 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts @@ -40,15 +40,15 @@ test.describe('', () => { await provider.start(); }); - test.describe('All data has been loaded after the initial load', () => { - test('State at the time of emitting events must be correct', async () => { + test.describe('all data has been loaded after the initial load', () => { + test('state at the time of emitting events must be correct', async () => { const chunkSize = 12; const states = [ state.compile(observerInitialStateFields), ( state.data.addData(chunkSize), - state.set({loadPage: 1, isRequestsStopped: true}).compile(observerInitialStateFields) + state.set({loadPage: 1, areRequestsStopped: true}).compile(observerInitialStateFields) ), ( state.data.addItems(chunkSize), @@ -105,8 +105,8 @@ test.describe('', () => { }); }); - test.describe('All data has been loaded after the second load and reload was called', () => { - test('State at the time of emitting events must be correct', async () => { + test.describe('all data has been loaded after the second load and reload was called', () => { + test('state at the time of emitting events must be correct', async () => { const chunkSize = 12, providerChunkSize = chunkSize / 2; @@ -133,7 +133,7 @@ test.describe('', () => { ), ( state.data.addData(0), - state.set({loadPage: 3, isRequestsStopped: true, isLastEmpty: true}).compile() + state.set({loadPage: 3, areRequestsStopped: true, isLastEmpty: true}).compile() ), ( state.set({isLifecycleDone: true}).compile() diff --git a/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts b/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts index b2d2645569..5e2a48f69c 100644 --- a/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts +++ b/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts @@ -42,8 +42,8 @@ test.describe('', () => { await provider.start(); }); - test.describe('Property `chunkSize` is set to 12', () => { - test.describe('Loaded data array is half length of the `chunkSize` prop', () => { + test.describe('property `chunkSize` is set to 12', () => { + test.describe('loaded data array is half length of the `chunkSize` prop', () => { test.describe('`shouldPerformDataRequest` returns true after the initial loading', () => { let shouldStopRequestingData, @@ -78,14 +78,14 @@ test.describe('', () => { }) .build(); - await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.waitForChildCountEqualsTo(chunkSize); }); - test('Should render 12 items', async () => { - await test.expect(component.getContainerChildCount()).resolves.toBe(chunkSize); + test('should render 12 items', async () => { + await test.expect(component.getChildCount()).resolves.toBe(chunkSize); }); - test('Should call `shouldStopRequestingData` twice', async () => { + test('should call `shouldStopRequestingData` twice', async () => { await test.expect(shouldStopRequestingData.calls).resolves.toEqual([ [ state.compile({ @@ -93,7 +93,7 @@ test.describe('', () => { childTillEnd: undefined, maxViewedItem: undefined, maxViewedChild: undefined, - isRequestsStopped: false, + areRequestsStopped: false, lastLoadedData: firstDataChunk, lastLoadedRawData: {data: firstDataChunk}, data: firstDataChunk, @@ -107,7 +107,7 @@ test.describe('', () => { childTillEnd: undefined, maxViewedItem: undefined, maxViewedChild: undefined, - isRequestsStopped: false, + areRequestsStopped: false, isInitialLoading: false, lastLoadedData: secondDataChunk, lastLoadedRawData: {data: secondDataChunk}, @@ -119,7 +119,7 @@ test.describe('', () => { ]); }); - test('Should call `shouldPerformDataRequest` once', async () => { + test('should call `shouldPerformDataRequest` once', async () => { await test.expect(shouldPerformDataRequest.calls).resolves.toEqual([ [ state.compile({ @@ -127,7 +127,7 @@ test.describe('', () => { childTillEnd: undefined, maxViewedItem: undefined, maxViewedChild: undefined, - isRequestsStopped: false, + areRequestsStopped: false, lastLoadedData: firstDataChunk, lastLoadedRawData: {data: firstDataChunk}, data: firstDataChunk, @@ -138,11 +138,11 @@ test.describe('', () => { ]); }); - test('Should call `initLoad` once', async () => { + test('should call `initLoad` once', async () => { await test.expect(initLoadSpy.calls).resolves.toEqual([[]]); }); - test('Should call `initLoadNext` once', async () => { + test('should call `initLoadNext` once', async () => { const spy = await component.getSpy((ctx) => ctx.initLoadNext); @@ -152,8 +152,8 @@ test.describe('', () => { }); }); - test.describe('Property `chunkSize` is set to 12', () => { - test.describe('Loaded data array is half length of the `chunkSize` prop', () => { + test.describe('property `chunkSize` is set to 12', () => { + test.describe('loaded data array is half length of the `chunkSize` prop', () => { test.describe('`shouldPerformDataRequest` returns false after the initial loading', () => { let shouldStopRequestingData, @@ -181,14 +181,14 @@ test.describe('', () => { }) .build(); - await component.waitForContainerChildCountEqualsTo(providerChunkSize); + await component.waitForChildCountEqualsTo(providerChunkSize); }); - test('Should render 6 items', async () => { - await test.expect(component.getContainerChildCount()).resolves.toBe(providerChunkSize); + test('should render 6 items', async () => { + await test.expect(component.getChildCount()).resolves.toBe(providerChunkSize); }); - test('Should call `shouldStopRequestingData` once', async () => { + test('should call `shouldStopRequestingData` once', async () => { await test.expect(shouldStopRequestingData.calls).resolves.toEqual([ [ state.compile({ @@ -196,7 +196,7 @@ test.describe('', () => { childTillEnd: undefined, maxViewedItem: undefined, maxViewedChild: undefined, - isRequestsStopped: false, + areRequestsStopped: false, loadPage: 1 }), test.expect.any(Object) @@ -204,7 +204,7 @@ test.describe('', () => { ]); }); - test('Should call `shouldPerformDataRequest` once', async () => { + test('should call `shouldPerformDataRequest` once', async () => { await test.expect(shouldPerformDataRequest.calls).resolves.toEqual([ [ state.compile({ @@ -212,7 +212,7 @@ test.describe('', () => { childTillEnd: undefined, maxViewedItem: undefined, maxViewedChild: undefined, - isRequestsStopped: false, + areRequestsStopped: false, loadPage: 1 }), test.expect.any(Object) @@ -220,15 +220,15 @@ test.describe('', () => { ]); }); - test('Should call `initLoad` once', async () => { + test('should call `initLoad` once', async () => { await test.expect(initLoadSpy.calls).resolves.toEqual([[]]); }); }); }); }); - test.describe('Property `chunkSize` is set to 12', () => { - test.describe('Loaded data array is half length of the `chunkSize` prop', () => { + test.describe('property `chunkSize` is set to 12', () => { + test.describe('loaded data array is half length of the `chunkSize` prop', () => { test.describe('`shouldStopRequestingData` returns true after the initial loading', () => { const chunkSize = 12, @@ -256,14 +256,14 @@ test.describe('', () => { }) .build(); - await component.waitForContainerChildCountEqualsTo(providerChunkSize); + await component.waitForChildCountEqualsTo(providerChunkSize); }); - test('Should render 6 items', async () => { - await test.expect(component.getContainerChildCount()).resolves.toBe(providerChunkSize); + test('should render 6 items', async () => { + await test.expect(component.getChildCount()).resolves.toBe(providerChunkSize); }); - test('Should call `shouldStopRequestingData` once', async () => { + test('should call `shouldStopRequestingData` once', async () => { await test.expect(shouldStopRequestingData.calls).resolves.toEqual([ [ state.compile({ @@ -271,7 +271,7 @@ test.describe('', () => { childTillEnd: undefined, maxViewedItem: undefined, maxViewedChild: undefined, - isRequestsStopped: false, + areRequestsStopped: false, loadPage: 1 }), test.expect.any(Object) @@ -279,15 +279,15 @@ test.describe('', () => { ]); }); - test('Should call `shouldPerformDataRequest` once', async () => { + test('should call `shouldPerformDataRequest` once', async () => { await test.expect(shouldPerformDataRequest.calls).resolves.toEqual([]); }); - test('Should call `initLoad` once', async () => { + test('should call `initLoad` once', async () => { await test.expect(initLoadSpy.calls).resolves.toEqual([[]]); }); - test('Should end the component lifecycle', async () => { + test('should end the component lifecycle', async () => { await test.expect(component.waitForLifecycleDone()).resolves.toBeUndefined(); }); }); diff --git a/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts b/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts index b54efee0b9..da0fc78cf2 100644 --- a/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts +++ b/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts @@ -34,7 +34,7 @@ test.describe('', () => { ({component, provider, state} = await createTestHelpers(page)); await provider.start(); - await component.setChildren({ + await component.withChildren({ done: { type: 'div', attrs: { @@ -77,7 +77,7 @@ test.describe('', () => { }); test.describe('`done`', () => { - test('Activates when all data has been loaded after the initial load', async () => { + test('activates when all data has been loaded after the initial load', async () => { const chunkSize = 12; provider @@ -105,7 +105,7 @@ test.describe('', () => { }); }); - test('Activates when all data has been loaded after the second load', async () => { + test('activates when all data has been loaded after the second load', async () => { const chunkSize = 12; provider @@ -138,7 +138,7 @@ test.describe('', () => { }); }); - test('Activates when data loading is completed but data is less than chunkSize', async () => { + test('activates when data loading is completed but data is less than chunkSize', async () => { const chunkSize = 12; provider @@ -169,7 +169,7 @@ test.describe('', () => { }); }); - test('Does not activates if there is more data to download', async () => { + test('does not activates if there is more data to download', async () => { const chunkSize = 12; provider @@ -193,7 +193,7 @@ test.describe('', () => { }) .build(); - await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.waitForChildCountEqualsTo(chunkSize); await component.waitForSlotState('loader', false); const @@ -212,7 +212,7 @@ test.describe('', () => { }); test.describe('empty', () => { - test('Activates when no data has been loaded after the initial load', async () => { + test('activates when no data has been loaded after the initial load', async () => { const chunkSize = 12; provider.response(200, {data: []}); @@ -243,7 +243,7 @@ test.describe('', () => { }); test.describe('tombstone & loader', () => { - test('Activates while initial data loading in progress', async () => { + test('activates while initial data loading in progress', async () => { const chunkSize = 12; provider @@ -270,7 +270,7 @@ test.describe('', () => { }); }); - test('Active while initial load loads all data', async () => { + test('active while initial load loads all data', async () => { const chunkSize = 12, providerChunkSize = chunkSize / 2; @@ -308,7 +308,7 @@ test.describe('', () => { }); test.describe('retry', () => { - test('Activates when a data load error occurred during initial loading', async () => { + test('activates when a data load error occurred during initial loading', async () => { const chunkSize = 12; provider.response(500, {}); @@ -337,7 +337,7 @@ test.describe('', () => { }); }); - test('Activates when a data load error occurred during initial loading of the second data chunk', async () => { + test('activates when a data load error occurred during initial loading of the second data chunk', async () => { const chunkSize = 12, providerChunkSize = chunkSize / 2; @@ -373,7 +373,7 @@ test.describe('', () => { test.describe('renderNext', () => { test.beforeEach(async () => { - await component.setChildren({ + await component.withChildren({ renderNext: { type: 'div', attrs: { @@ -383,7 +383,7 @@ test.describe('', () => { }); }); - test('Activates when data is loaded', async () => { + test('activates when data is loaded', async () => { const chunkSize = 12; provider @@ -415,7 +415,7 @@ test.describe('', () => { }); }); - test('Doesn\'t activates while data is loading', async ({page}) => { + test('doesn\'t activates while data is loading', async ({page}) => { const chunkSize = 12; provider @@ -448,7 +448,7 @@ test.describe('', () => { }); }); - test('Doesn\'t activates if there\'s a data loading error', async () => { + test('doesn\'t activates if there\'s a data loading error', async () => { const chunkSize = 12; provider.response(500, {data: []}); diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts index 7c97a5d7d5..f3bf9d07ee 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts @@ -31,7 +31,7 @@ test.describe('', () => { ({component, provider, state} = await createTestHelpers(page)); await provider.start(); - await component.setChildren({ + await component.withChildren({ renderNext: { type: 'div', attrs: { @@ -50,7 +50,7 @@ test.describe('', () => { }); }); - test.describe('The first chunk of data is loaded and rendered', () => { + test.describe('the first chunk of data is loaded and rendered', () => { const chunkSize = 12; test.beforeEach(async () => { @@ -65,18 +65,18 @@ test.describe('', () => { }) .build(); - await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.waitForChildCountEqualsTo(chunkSize); }); - test('Should load and render the next chunk after calling initLoadNext', async () => { + test('should load and render the next chunk after calling initLoadNext', async () => { await component.node.locator('#renderNext').click(); test.expect(provider.mock.mock.calls.length).toBe(2); await test.expect(component.waitForDataIndexChild(chunkSize * 2 - 1)).resolves.toBeUndefined(); - await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize * 2)).resolves.toBeUndefined(); + await test.expect(component.waitForChildCountEqualsTo(chunkSize * 2)).resolves.toBeUndefined(); }); - test('Should complete the component lifecycle after all data is loaded', async () => { + test('should complete the component lifecycle after all data is loaded', async () => { provider.response(200, {data: []}); await component.node.locator('#renderNext').click(); @@ -84,44 +84,44 @@ test.describe('', () => { await test.expect(component.waitForLifecycleDone()).resolves.toBeUndefined(); await test.expect(component.waitForSlotState('renderNext', false)).resolves.toBeUndefined(); await test.expect(component.waitForDataIndexChild(chunkSize - 1)).resolves.toBeUndefined(); - await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); + await test.expect(component.waitForChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); }); - test.describe('An error occurred while loading the second chunk of data', () => { + test.describe('an error occurred while loading the second chunk of data', () => { test.beforeEach(async () => { provider.responseOnce(500, {data: []}); await component.node.locator('#renderNext').click(); }); - test('Should not display the renderNext slot', async () => { + test('should not display the renderNext slot', async () => { await test.expect(component.waitForSlotState('renderNext', false)).resolves.toBeUndefined(); }); - test('Should display the retry slot', async () => { + test('should display the retry slot', async () => { await test.expect(component.waitForSlotState('retry', true)).resolves.toBeUndefined(); }); - test.describe('Data reload occurred', () => { + test.describe('data reload occurred', () => { test.beforeEach(async () => { await component.node.locator('#retry').click(); }); - test('Should display the loaded data', async () => { + test('should display the loaded data', async () => { await test.expect(component.waitForDataIndexChild(chunkSize * 2 - 1)).resolves.toBeUndefined(); - await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize * 2)).resolves.toBeUndefined(); + await test.expect(component.waitForChildCountEqualsTo(chunkSize * 2)).resolves.toBeUndefined(); }); - test.describe('No more data to display', () => { + test.describe('no more data to display', () => { test.beforeEach(async () => { provider.response(200, {data: []}); await component.node.locator('#renderNext').click(); }); - test('Should complete the component lifecycle after all data is loaded', async () => { + test('should complete the component lifecycle after all data is loaded', async () => { await test.expect(component.waitForLifecycleDone()).resolves.toBeUndefined(); await test.expect(component.waitForSlotState('renderNext', false)).resolves.toBeUndefined(); await test.expect(component.waitForDataIndexChild(chunkSize * 2 - 1)).resolves.toBeUndefined(); - await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize * 2)).resolves.toBeUndefined(); + await test.expect(component.waitForChildCountEqualsTo(chunkSize * 2)).resolves.toBeUndefined(); }); }); }); diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts index d1b4fd24f8..b06157068e 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts @@ -48,7 +48,7 @@ test.describe('', () => { }) .pick(demoPage.buildTestComponent(component.componentName, component.props)); - await component.waitForContainerChildCountEqualsTo(chunkSize[0]); + await component.waitForChildCountEqualsTo(chunkSize[0]); await demoPage.updateTestComponent({ request: { @@ -92,7 +92,7 @@ test.describe('', () => { ]); await test.expect(initLoadSpy.calls).resolves.toEqual([[], []]); - await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize[1])).resolves.toBeUndefined(); + await test.expect(component.waitForChildCountEqualsTo(chunkSize[1])).resolves.toBeUndefined(); }); }); @@ -158,7 +158,7 @@ test.describe('', () => { ]; await test.expect(initLoadSpy.calls).resolves.toEqual(initLoadArgs[i]); - await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); + await test.expect(component.waitForChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); }); }); }); diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts index fe6efed690..642ecbd5ef 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts @@ -30,7 +30,7 @@ test.describe('', () => { ({component, provider, state} = await createTestHelpers(page)); await provider.start(); - await component.setChildren({ + await component.withChildren({ retry: { type: 'div', attrs: { @@ -41,8 +41,8 @@ test.describe('', () => { }); }); - test.describe('Data loading error ocurred on initial loading', () => { - test('Should reload data after initLoad call', async () => { + test.describe('data loading error ocurred on initial loading', () => { + test('should reload data after initLoad call', async () => { const chunkSize = 12; provider @@ -57,10 +57,10 @@ test.describe('', () => { await component.node.locator('#retry').click(); - await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); + await test.expect(component.waitForChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); }); - test('Should reload data after invoking retry function from `onRequestError` handler', async () => { + test('should reload data after invoking retry function from `onRequestError` handler', async () => { const chunkSize = 12; provider @@ -76,10 +76,10 @@ test.describe('', () => { }) .build(); - await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); + await test.expect(component.waitForChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); }); - test('Should goes to retry state after failing to load data twice', async () => { + test('should goes to retry state after failing to load data twice', async () => { const chunkSize = 12; provider @@ -98,12 +98,12 @@ test.describe('', () => { await event; await component.node.locator('#retry').click(); - await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); + await test.expect(component.waitForChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); }); }); - test.describe('Data loading error ocurred on second data chunk loading', () => { - test('Should reload second data chunk after initLoad call', async () => { + test.describe('data loading error ocurred on second data chunk loading', () => { + test('should reload second data chunk after initLoad call', async () => { const chunkSize = 12; provider @@ -117,18 +117,18 @@ test.describe('', () => { .withProps({chunkSize}) .build(); - await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.waitForChildCountEqualsTo(chunkSize); await component.scrollToBottom(); await component.node.locator('#retry').click(); await component.waitForDataIndexChild(chunkSize * 2 - 1); - await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize * 2)).resolves.toBeUndefined(); + await test.expect(component.waitForChildCountEqualsTo(chunkSize * 2)).resolves.toBeUndefined(); }); }); - test.describe('An error occurred while loading the second chunk of data for rendering the first chunk of elements', () => { - test('Should reload second data chunk and perform a render', async () => { + test.describe('an error occurred while loading the second chunk of data for rendering the first chunk of elements', () => { + test('should reload second data chunk and perform a render', async () => { const chunkSize = 12, providerChunkSize = chunkSize / 2; @@ -146,16 +146,16 @@ test.describe('', () => { await component.node.locator('#retry').click(); - await component.waitForContainerChildCountEqualsTo(chunkSize); + await component.waitForChildCountEqualsTo(chunkSize); await component.waitForDataIndexChild(chunkSize - 1); await component.waitForLifecycleDone(); - await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); + await test.expect(component.waitForChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); }); }); - test.describe('An error occurred while loading the last chunk of data', () => { - test('After a successful load, the component lifecycle should complete', async () => { + test.describe('an error occurred while loading the last chunk of data', () => { + test('after a successful load, the component lifecycle should complete', async () => { const chunkSize = 12; provider @@ -172,7 +172,7 @@ test.describe('', () => { test.expect(provider.mock.mock.calls.length).toBe(3); await test.expect(component.waitForLifecycleDone()).resolves.toBeUndefined(); - await test.expect(component.waitForContainerChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); + await test.expect(component.waitForChildCountEqualsTo(chunkSize)).resolves.toBeUndefined(); }); }); }); diff --git a/tests/helpers/component-object/README.md b/tests/helpers/component-object/README.md index c876e7a083..8edf10eaaa 100644 --- a/tests/helpers/component-object/README.md +++ b/tests/helpers/component-object/README.md @@ -16,7 +16,7 @@ The class provides a universal API for generating a component and setting up moc import ComponentObject from 'path/to/component-object'; // Create an instance of ComponentObject -const myComponent = new ComponentObject(page, 'MyComponent'); +const myComponent = new ComponentObject(page, 'b-component'); // Build the component await myComponent.build(); @@ -42,7 +42,7 @@ class MyComponentObject extends ComponentObject { } // Create an instance of MyComponentObject -const myComponent = new MyComponentObject(page, 'MyComponent'); +const myComponent = new MyComponentObject(page, 'b-component'); await myComponent.build(); @@ -77,10 +77,10 @@ class MyComponentObject extends ComponentObject { // Create an instance of MyComponentObject const - myComponent = new MyComponentObject(page, 'MyComponent'), + myComponent = new MyComponentObject(page, 'b-component'), someProp = await myComponent.mockFn(() => true); -myComponent.setProps({ +myComponent.withProps({ someProp }); @@ -107,10 +107,10 @@ class MyComponentObject extends ComponentObject { } // Create an instance of MyComponentObject -const myComponent = new MyComponentObject(page, 'MyComponent'); +const myComponent = new MyComponentObject(page, 'b-component'); // Create a spy -myComponent.setProps({ +myComponent.withProps({ '@hook:beforeDataCreate': (ctx) => jest.spy(ctx, 'emit') }); diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index 7efa3101f9..6cf548aa86 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -143,7 +143,7 @@ export default abstract class ComponentObjectBuilder { /** * Renders the component with the previously set props and children - * using the `setProps` and `setChildren` methods. + * using the `withProps` and `withChildren` methods. */ async build(): Promise> { if (this.componentStyles != null) { @@ -231,7 +231,7 @@ export default abstract class ComponentObjectBuilder { * * @param children - The children to set */ - setChildren(children: VNodeChildren): this { + withChildren(children: VNodeChildren): this { Object.assign(this.children, children); return this; } diff --git a/tests/helpers/component-object/mock.ts b/tests/helpers/component-object/mock.ts index 80f35062ca..56ea02268c 100644 --- a/tests/helpers/component-object/mock.ts +++ b/tests/helpers/component-object/mock.ts @@ -83,7 +83,7 @@ export default abstract class ComponentObjectMock exte * * @example * ```typescript - * await component.setProps({ + * await component.withProps({ * '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx.localEmitter, 'emit') * }); * @@ -113,7 +113,7 @@ export default abstract class ComponentObjectMock exte * component = new ComponentObject(page, 'b-virtual-scroll'), * shouldStopRequestingData = await component.mockFn(() => false); * - * await component.setProps({ + * await component.withProps({ * shouldStopRequestingData * }); * diff --git a/tests/helpers/mock/index.ts b/tests/helpers/mock/index.ts index 9ded8b57db..6fda065938 100644 --- a/tests/helpers/mock/index.ts +++ b/tests/helpers/mock/index.ts @@ -76,7 +76,7 @@ export async function createSpy( } /** - * Retrieves an existing spy object from a `JSHandle`. + * Retrieves an existing {@link SpyObject} from a `JSHandle`. * * @param ctx - The `JSHandle` containing the spy object. * @param spyExtractor - The function to extract the spy object. @@ -113,7 +113,7 @@ export async function getSpy( * @param page - The Page object to inject the mock function into. * @param fn - The mock function. * @param args - The arguments to pass to the function. - * @returns A promise that resolves to the mock function as a spy object. + * @returns A promise that resolves to the mock function as a {@link SpyObject}. * * @example * ```typescript @@ -140,7 +140,7 @@ export async function createMockFn( } /** - * Injects a mock function into a Page object and returns the spy object. + * Injects a mock function into a Page object and returns the {@link SpyObject}. * * This function also returns the ID of the injected mock function, which is stored in `globalThis`. * This binding allows the function to be found during object serialization within the page context. From e60237abcc2562973da34319fc5c0c7956bf66ac Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 28 Jul 2023 16:34:28 +0300 Subject: [PATCH 076/159] :art: review --- .../base/b-virtual-scroll/b-virtual-scroll.ts | 10 +++++++--- .../b-virtual-scroll/modules/factory/engines/vdom.ts | 12 +----------- src/components/super/i-data/i-data.ts | 3 +-- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts index 8cb19be16d..37d919d43c 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts @@ -11,12 +11,13 @@ * @packageDocumentation */ +import symbolGenerator from 'core/symbol'; import type { AsyncOptions } from 'core/async'; import type iItems from 'components/traits/i-items/i-items'; import VDOM, { create, render } from 'components/friends/vdom'; import { iVirtualScrollHandlers } from 'components/base/b-virtual-scroll/handlers'; -import { bVirtualScrollDomInsertAsyncGroup, renderGuardRejectionReason } from 'components/base/b-virtual-scroll/const'; +import { bVirtualScrollAsyncGroup, bVirtualScrollDomInsertAsyncGroup, renderGuardRejectionReason } from 'components/base/b-virtual-scroll/const'; import type { VirtualScrollState, RenderGuardResult, $ComponentRefs, UnsafeBVirtualScroll } from 'components/base/b-virtual-scroll/interface'; import { ComponentTypedEmitter, componentTypedEmitter } from 'components/base/b-virtual-scroll/modules/emitter'; @@ -25,12 +26,14 @@ import { SlotsStateController } from 'components/base/b-virtual-scroll/modules/s import { ComponentFactory } from 'components/base/b-virtual-scroll/modules/factory'; import { Observer } from 'components/base/b-virtual-scroll/modules/observer'; -import iData, { $$, component, system, RequestParams, UnsafeGetter } from 'components/super/i-data/i-data'; +import iData, { component, system, RequestParams, UnsafeGetter } from 'components/super/i-data/i-data'; export * from 'components/base/b-virtual-scroll/interface'; export * from 'components/base/b-virtual-scroll/const'; export * from 'components/super/i-data/i-data'; +const $$ = symbolGenerator(); + VDOM.addToPrototype({create, render}); /** @@ -172,7 +175,8 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI */ getRequestParams(): RequestParams { const label: AsyncOptions = { - label: $$.initLoad, + label: $$.initLoadNext, + group: bVirtualScrollAsyncGroup, join: 'replace' }; diff --git a/src/components/base/b-virtual-scroll/modules/factory/engines/vdom.ts b/src/components/base/b-virtual-scroll/modules/factory/engines/vdom.ts index 648f8b5dc5..6313ee50d6 100644 --- a/src/components/base/b-virtual-scroll/modules/factory/engines/vdom.ts +++ b/src/components/base/b-virtual-scroll/modules/factory/engines/vdom.ts @@ -20,15 +20,5 @@ export function render(ctx: bVirtualScroll, items: VNodeDescriptor[]): HTMLEleme vnodes = ctx.vdom.create(...items), nodes = ctx.vdom.render(vnodes); - // https://github.com/vuejs/core/issues/6061 - if (nodes[0].nodeType === Node.TEXT_NODE) { - nodes.shift(); - } - - // https://github.com/vuejs/core/issues/6061 - if (nodes[nodes.length - 1].nodeType === Node.TEXT_NODE) { - nodes.pop(); - } - - return nodes; + return nodes.slice(1, nodes.length - 1); } diff --git a/src/components/super/i-data/i-data.ts b/src/components/super/i-data/i-data.ts index 5948718eae..9d3d13075b 100644 --- a/src/components/super/i-data/i-data.ts +++ b/src/components/super/i-data/i-data.ts @@ -55,8 +55,7 @@ export { export * from 'components/super/i-block/i-block'; export * from 'components/super/i-data/interface'; -export const - $$ = symbolGenerator(); +const $$ = symbolGenerator(); @component({functional: null}) export default abstract class iData extends iDataHandlers { From 98f4cc5efad8f89c3033a9ae9f548760ee6b5f08 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 28 Jul 2023 16:41:00 +0300 Subject: [PATCH 077/159] :art: review --- tests/helpers/providers/interceptor/index.ts | 71 ++++++++++++++------ 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index 4ab3eca4a9..03fe555aee 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -84,6 +84,17 @@ export class RequestInterceptor { * .responseOnce(500); * ``` * + * Sets the response that will occur with a delay to simulate network latency. + * + * @example + * ```typescript + * const interceptor = new RequestInterceptor(page, /api/); + * + * interceptor + * .responseOnce(200, {content: 1}, {delay: 200}) + * .responseOnce(500, {}, {delay: 300}); + * ``` + * * @param status - The response status. * @param payload - The response payload. * @param opts - The response options. @@ -96,17 +107,7 @@ export class RequestInterceptor { payload?: ResponsePayload | ResponseHandler, opts?: ResponseOptions ): this { - let fn; - - if (Object.isFunction(handlerOrStatus)) { - fn = handlerOrStatus; - - } else { - const status = handlerOrStatus; - fn = this.cookResponseFn(status, payload, opts); - } - - this.mock.mockImplementationOnce(fn); + this.mock.mockImplementationOnce(this.createMockFn(handlerOrStatus, payload, opts)); return this; } @@ -120,6 +121,17 @@ export class RequestInterceptor { * interceptor.response((r: Route) => r.fulfill({status: 200})); * ``` * + * Sets the response that will occur with a delay to simulate network latency. + * + * @example + * ```typescript + * const interceptor = new RequestInterceptor(page, /api/); + * + * interceptor + * .response(200, {content: 1}, {delay: 200}) + * .response(500, {}, {delay: 300}); + * ``` + * * @param handler - The response handler function. * @returns The current instance of RequestInterceptor. */ @@ -147,17 +159,7 @@ export class RequestInterceptor { payload?: ResponsePayload | ResponseHandler, opts?: ResponseOptions ): this { - let fn; - - if (Object.isFunction(handlerOrStatus)) { - fn = handlerOrStatus; - - } else { - const status = handlerOrStatus; - fn = this.cookResponseFn(status, payload, opts); - } - - this.mock.mockImplementation(fn); + this.mock.mockImplementation(this.createMockFn(handlerOrStatus, payload, opts)); return this; } @@ -192,6 +194,31 @@ export class RequestInterceptor { return this; } + /** + * Creates a mock response function. + * + * @param handlerOrStatus + * @param payload + * @param opts + */ + protected createMockFn( + handlerOrStatus: number | ResponseHandler, + payload?: ResponsePayload | ResponseHandler, + opts?: ResponseOptions + ): ResponseHandler { + let fn; + + if (Object.isFunction(handlerOrStatus)) { + fn = handlerOrStatus; + + } else { + const status = handlerOrStatus; + fn = this.cookResponseFn(status, payload, opts); + } + + return fn; + } + /** * Cooks a response handler. * From 7f8213a6cd5fcc0a690a970455e7d26aef7b1ad6 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 28 Jul 2023 17:07:19 +0300 Subject: [PATCH 078/159] :art: review --- .../base/b-virtual-scroll/README.md | 10 ++++---- src/components/base/b-virtual-scroll/const.ts | 2 +- .../b-virtual-scroll/interface/component.ts | 8 +++---- .../b-virtual-scroll/modules/state/helpers.ts | 6 ++--- .../b-virtual-scroll/modules/state/index.ts | 22 ++++++++--------- src/components/base/b-virtual-scroll/props.ts | 4 ++-- .../test/api/helpers/index.ts | 4 ++-- .../test/unit/functional/props/props.ts | 2 +- .../test/unit/functional/rendering/default.ts | 2 +- .../functional/rendering/items-factory.ts | 2 +- .../test/unit/functional/state/default.ts | 12 +++++----- .../test/unit/functional/state/emitter.ts | 4 ++-- .../initialization/initialization.ts | 24 +++++++++---------- .../test/unit/lifecycle/slots/slots.ts | 4 ++-- .../test/unit/scenario/reload.ts | 4 ++-- 15 files changed, 55 insertions(+), 55 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index bd370f6362..99bb078114 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -198,7 +198,7 @@ This slot can be useful when implementing lazy content rendering on button click ### `shouldPerformDataRender` - Type: `Function` -- Default: `(state: VirtualScrollState) => state.isInitialRender || state.itemsTillEnd === 0` +- Default: `(state: VirtualScrollState) => state.isInitialRender || state.remainingItems === 0` This function is called in the `bVirtualScroll.renderGuard` after other checks are completed. It receives the component state as input and determines whether the component should render the next chunk of components. @@ -208,7 +208,7 @@ Example usage: ```typescript const shouldPerformDataRender = (state: VirtualScrollState): boolean => { - return state.isInitialRender || state.itemsTillEnd === 0; + return state.isInitialRender || state.remainingItems === 0; }; ``` @@ -225,11 +225,11 @@ Here's an example of how you can use `shouldPerformDataRequest`: ```typescript const shouldPerformDataRequest = (state: VirtualScrollState): boolean => { // Example: Request data if the remaining items till the end is less than or equal to 10 - return state.itemsTillEnd <= 10; + return state.remainingItems <= 10; }; ``` -In this example, the function checks the `itemsTillEnd` property of the component state. +In this example, the function checks the `remainingItems` property of the component state. If the remaining number of items till the end is less than or equal to 10, it returns `true` to indicate that the component should perform a data request. You can adjust the condition based on your specific requirements. @@ -475,7 +475,7 @@ The component allows rendering two types of components: - `item` - Main component (main content). - `separator` - Other components, such as dividers or separators. -There is no significant difference between them, except that they are treated differently in fields like `itemsTillEnd` in the `VirtualScrollState`. As the name suggests, the `itemsTillEnd` property only considers components with the `item` type, while `childTillEnd` considers components with both `item` and `separator` types. +There is no significant difference between them, except that they are treated differently in fields like `remainingItems` in the `VirtualScrollState`. As the name suggests, the `remainingItems` property only considers components with the `item` type, while `remainingChildren` considers components with both `item` and `separator` types. The distinction between `item` and `separator` types is mainly used for calculating certain properties based on the type of components present in the `VirtualScrollState`, such as the number of items till the end of the scroll. diff --git a/src/components/base/b-virtual-scroll/const.ts b/src/components/base/b-virtual-scroll/const.ts index c73c364ab8..9aef01de93 100644 --- a/src/components/base/b-virtual-scroll/const.ts +++ b/src/components/base/b-virtual-scroll/const.ts @@ -108,6 +108,6 @@ export const defaultShouldProps = { /** {@link bVirtualScroll.shouldPerformDataRender} */ shouldPerformDataRender: (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => - state.isInitialRender || state.itemsTillEnd === 0 + state.isInitialRender || state.remainingItems === 0 }; diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts index 2d4a1b9e65..e255ede8ee 100644 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -29,12 +29,12 @@ export interface VirtualScrollState { /** * The number of components of type `item` that have not yet been visible to the user. */ - itemsTillEnd: CanUndef; + remainingItems: CanUndef; /** * The number of components of any type that have not yet been visible to the user. */ - childTillEnd: CanUndef; + remainingChildren: CanUndef; /** * The current page number for loading data. @@ -119,7 +119,7 @@ export interface PrivateComponentState { /** * Pointer to the index of the data element that was last rendered. */ - dataCursor: number; + dataOffset: number; } /** @@ -130,7 +130,7 @@ export interface ComponentItemType { * This type indicates that the component is the "main" component to render. * * For example, in the {@link VirtualScrollState} interface, you can notice that - * there are specific fields for the `item` type, such as `itemsTillEnd`. + * there are specific fields for the `item` type, such as `remainingItems`. * * Components with this type are stored both in the `items` array and the `childList` array in * {@link VirtualScrollState}. diff --git a/src/components/base/b-virtual-scroll/modules/state/helpers.ts b/src/components/base/b-virtual-scroll/modules/state/helpers.ts index c83fd0cea7..8233eacda5 100644 --- a/src/components/base/b-virtual-scroll/modules/state/helpers.ts +++ b/src/components/base/b-virtual-scroll/modules/state/helpers.ts @@ -15,8 +15,8 @@ export function createInitialState(): VirtualScrollState { return { loadPage: 0, renderPage: 0, - itemsTillEnd: undefined, - childTillEnd: undefined, + remainingItems: undefined, + remainingChildren: undefined, maxViewedItem: undefined, maxViewedChild: undefined, data: [], @@ -39,6 +39,6 @@ export function createInitialState(): VirtualScrollState { */ export function createPrivateInitialState(): PrivateComponentState { return { - dataCursor: 0 + dataOffset: 0 }; } diff --git a/src/components/base/b-virtual-scroll/modules/state/index.ts b/src/components/base/b-virtual-scroll/modules/state/index.ts index 3c4c717336..b24eff782e 100644 --- a/src/components/base/b-virtual-scroll/modules/state/index.ts +++ b/src/components/base/b-virtual-scroll/modules/state/index.ts @@ -87,7 +87,7 @@ export class ComponentInternalState extends Friend { childList.push(...mounted); itemsList.push(...newItems); - this.updateChildTillEnd(); + this.updateRemainingChild(); } /** @@ -151,22 +151,22 @@ export class ComponentInternalState extends Friend { if (isItem(component) && (state.maxViewedItem == null || state.maxViewedItem < component.itemIndex)) { state.maxViewedItem = component.itemIndex; - state.itemsTillEnd = state.items.length - 1 - state.maxViewedItem; + state.remainingItems = state.items.length - 1 - state.maxViewedItem; } if (state.maxViewedChild == null || state.maxViewedChild < childIndex) { state.maxViewedChild = component.childIndex; - state.childTillEnd = state.childList.length - 1 - state.maxViewedChild; + state.remainingChildren = state.childList.length - 1 - state.maxViewedChild; } - this.updateChildTillEnd(); + this.updateRemainingChild(); } /** * Returns the cursor indicating the last index of the last rendered data element. */ getDataCursor(): number { - return this.privateState.dataCursor; + return this.privateState.dataOffset; } /** @@ -178,29 +178,29 @@ export class ComponentInternalState extends Friend { current = this.getDataCursor(), chunkSize = ctx.getChunkSize(state); - this.privateState.dataCursor = current + chunkSize; + this.privateState.dataOffset = current + chunkSize; } /** * Updates the state of the tillEnd-like fields. * Calculates the remaining number of child elements until the end and the remaining number of items until the end. */ - updateChildTillEnd(): void { + updateRemainingChild(): void { const {state} = this; if (state.maxViewedChild == null) { - state.childTillEnd = state.childList.length - 1; + state.remainingChildren = state.childList.length - 1; } else { - state.childTillEnd = state.childList.length - 1 - state.maxViewedChild; + state.remainingChildren = state.childList.length - 1 - state.maxViewedChild; } if (state.maxViewedItem == null) { - state.itemsTillEnd = state.items.length - 1; + state.remainingItems = state.items.length - 1; } else { - state.itemsTillEnd = state.items.length - 1 - state.maxViewedItem; + state.remainingItems = state.items.length - 1 - state.maxViewedItem; } } } diff --git a/src/components/base/b-virtual-scroll/props.ts b/src/components/base/b-virtual-scroll/props.ts index 9781de7bc7..397a865f2c 100644 --- a/src/components/base/b-virtual-scroll/props.ts +++ b/src/components/base/b-virtual-scroll/props.ts @@ -174,7 +174,7 @@ export default abstract class iVirtualScrollProps extends iData { /** * When this function returns `true` the component will be able to request additional data. - * This function will be called on each new element enters the viewport. + * This function will be called each time a new element enters the viewport. */ @prop({ type: Function, @@ -195,7 +195,7 @@ export default abstract class iVirtualScrollProps extends iData { * @example * ```typescript * const shouldPerformDataRender = (state) => { - * return state.isInitialRender || state.itemsTillEnd === 0; + * return state.isInitialRender || state.remainingItems === 0; * } * ``` */ diff --git a/src/components/base/b-virtual-scroll/test/api/helpers/index.ts b/src/components/base/b-virtual-scroll/test/api/helpers/index.ts index 234dab7ef6..a9ad2f3b26 100644 --- a/src/components/base/b-virtual-scroll/test/api/helpers/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/helpers/index.ts @@ -220,8 +220,8 @@ export function createInitialState(state: Partial): VirtualS ...createInitialStateObj(), maxViewedItem: Object.cast(test.expect.any(Number)), maxViewedChild: Object.cast(test.expect.any(Number)), - itemsTillEnd: Object.cast(test.expect.any(Number)), - childTillEnd: 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)), ...state }; diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts index 807434386d..733a91f350 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts @@ -124,7 +124,7 @@ test.describe('', () => { .withDefaultPaginationProviderProps({chunkSize}) .withProps({ chunkSize, - shouldPerformDataRequest: ({itemsTillEnd}) => itemsTillEnd === 0, + shouldPerformDataRequest: ({remainingItems}) => remainingItems === 0, dbConverter: ({data: {nestedData}}) => ({data: nestedData}) }) .build(); diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts index 658ea98897..4d34b61fc8 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts @@ -44,7 +44,7 @@ test.describe('', () => { .response(200, {data: state.data.addData(0)}); const shouldPerformDataRender = await component.mockFn( - ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 + ({isInitialRender, remainingItems: remainingItems}) => isInitialRender || remainingItems === 0 ); await component diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts index 88ace5a3e7..18bf4249d3 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts @@ -257,7 +257,7 @@ test.describe(' rendering via itemsFactory', () => { .response(200, {data: state.data.addData(0)}); const shouldPerformDataRender = await component.mockFn( - ({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0 + ({isInitialRender, remainingItems: remainingItems}) => isInitialRender || remainingItems === 0 ); const itemsFactory = await component.mockFn>((state) => { diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts index 8abbddcbdf..2c63cef0d0 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts @@ -41,8 +41,8 @@ test.describe('', () => { const expectedState = state.compile({ lastLoadedRawData: undefined, - itemsTillEnd: undefined, - childTillEnd: undefined, + remainingItems: undefined, + remainingChildren: undefined, maxViewedItem: undefined, maxViewedChild: undefined, areRequestsStopped: false, @@ -68,10 +68,10 @@ test.describe('', () => { const shouldStopRequestingData = (defaultShouldProps.shouldStopRequestingData), - shouldPerformDataRequest = (({isInitialLoading, itemsTillEnd, isLastEmpty}) => - isInitialLoading || (itemsTillEnd === 0 && !isLastEmpty)), - shouldPerformDataRender = (({isInitialRender, itemsTillEnd}) => - isInitialRender || itemsTillEnd === 0); + shouldPerformDataRequest = (({isInitialLoading, remainingItems: remainingItems, isLastEmpty}) => + isInitialLoading || (remainingItems === 0 && !isLastEmpty)), + shouldPerformDataRender = (({isInitialRender, remainingItems: remainingItems}) => + isInitialRender || remainingItems === 0); await test.step('After rendering first data chunk', async () => { provider diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts index a1bce1bb51..42e2e26826 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts @@ -22,8 +22,8 @@ test.describe('', () => { state: VirtualScrollTestHelpers['state']; const observerInitialStateFields = { - itemsTillEnd: undefined, - childTillEnd: undefined, + remainingItems: undefined, + remainingChildren: undefined, maxViewedChild: undefined, maxViewedItem: undefined }; diff --git a/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts b/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts index 5e2a48f69c..66d9a4fa0d 100644 --- a/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts +++ b/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts @@ -89,8 +89,8 @@ test.describe('', () => { await test.expect(shouldStopRequestingData.calls).resolves.toEqual([ [ state.compile({ - itemsTillEnd: undefined, - childTillEnd: undefined, + remainingItems: undefined, + remainingChildren: undefined, maxViewedItem: undefined, maxViewedChild: undefined, areRequestsStopped: false, @@ -103,8 +103,8 @@ test.describe('', () => { ], [ state.compile({ - itemsTillEnd: undefined, - childTillEnd: undefined, + remainingItems: undefined, + remainingChildren: undefined, maxViewedItem: undefined, maxViewedChild: undefined, areRequestsStopped: false, @@ -123,8 +123,8 @@ test.describe('', () => { await test.expect(shouldPerformDataRequest.calls).resolves.toEqual([ [ state.compile({ - itemsTillEnd: undefined, - childTillEnd: undefined, + remainingItems: undefined, + remainingChildren: undefined, maxViewedItem: undefined, maxViewedChild: undefined, areRequestsStopped: false, @@ -192,8 +192,8 @@ test.describe('', () => { await test.expect(shouldStopRequestingData.calls).resolves.toEqual([ [ state.compile({ - itemsTillEnd: undefined, - childTillEnd: undefined, + remainingItems: undefined, + remainingChildren: undefined, maxViewedItem: undefined, maxViewedChild: undefined, areRequestsStopped: false, @@ -208,8 +208,8 @@ test.describe('', () => { await test.expect(shouldPerformDataRequest.calls).resolves.toEqual([ [ state.compile({ - itemsTillEnd: undefined, - childTillEnd: undefined, + remainingItems: undefined, + remainingChildren: undefined, maxViewedItem: undefined, maxViewedChild: undefined, areRequestsStopped: false, @@ -267,8 +267,8 @@ test.describe('', () => { await test.expect(shouldStopRequestingData.calls).resolves.toEqual([ [ state.compile({ - itemsTillEnd: undefined, - childTillEnd: undefined, + remainingItems: undefined, + remainingChildren: undefined, maxViewedItem: undefined, maxViewedChild: undefined, areRequestsStopped: false, diff --git a/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts b/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts index da0fc78cf2..ffe76223b5 100644 --- a/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts +++ b/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts @@ -179,10 +179,10 @@ test.describe('', () => { .response(200, {data: []}); const shouldPerformDataRequest = - (({isInitialLoading, itemsTillEnd}) => isInitialLoading || itemsTillEnd === 0); + (({isInitialLoading, remainingItems}) => isInitialLoading || remainingItems === 0); const shouldPerformDataRender = - (({isInitialRender, itemsTillEnd}) => isInitialRender || itemsTillEnd === 0); + (({isInitialRender, remainingItems}) => isInitialRender || remainingItems === 0); await component .withDefaultPaginationProviderProps({chunkSize}) diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts index b06157068e..69e81d73e5 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts @@ -43,7 +43,7 @@ test.describe('', () => { .withDefaultPaginationProviderProps({chunkSize: chunkSize[0]}) .withProps({ chunkSize: chunkSize[0], - shouldPerformDataRequest: ({itemsTillEnd}) => itemsTillEnd === 0, + shouldPerformDataRequest: ({remainingItems}) => remainingItems === 0, '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') }) .pick(demoPage.buildTestComponent(component.componentName, component.props)); @@ -111,7 +111,7 @@ test.describe('', () => { .withDefaultPaginationProviderProps({chunkSize}) .withProps({ chunkSize, - shouldPerformDataRequest: ({itemsTillEnd}) => itemsTillEnd === 0, + shouldPerformDataRequest: ({remainingItems}) => remainingItems === 0, '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') }) .build(); From 46e1c54346f41d69dd181592bf648533e81817e2 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 28 Jul 2023 17:16:52 +0300 Subject: [PATCH 079/159] :art: --- ts-definitions/playwright.d.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ts-definitions/playwright.d.ts b/ts-definitions/playwright.d.ts index 4db3a96473..6c63f56f0c 100644 --- a/ts-definitions/playwright.d.ts +++ b/ts-definitions/playwright.d.ts @@ -11,10 +11,15 @@ export {}; declare global { namespace PlaywrightTest { interface Matchers { + /** @deprecated */ toBeResolved(): R; + /** @deprecated */ toBeResolvedTo(val: any): R; + /** @deprecated */ toBeRejected(): R; + /** @deprecated */ toBeRejectedWith(val: any): R; + /** @deprecated */ toBePending(): R; } } From 6933bdb391ab8f720c69c0491c104180cad771d4 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sun, 30 Jul 2023 20:44:50 +0300 Subject: [PATCH 080/159] docs --- tests/helpers/mock/README.md | 156 +++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/tests/helpers/mock/README.md b/tests/helpers/mock/README.md index 8d9fc4a2dd..597b1e3d57 100644 --- a/tests/helpers/mock/README.md +++ b/tests/helpers/mock/README.md @@ -1,3 +1,159 @@ # tests/helpers/mock This module provides utility functions for working with `jest-mock` and `Playwright`. + +## Usage + +Import the required functions from the `tests/helpers/mock` module to use the test helpers in your test files: + +```typescript +import { + wrapAsSpy, + createSpy, + getSpy, + createMockFn, + injectMockIntoPage +} from 'tests/helpers/mock'; +``` + +## API Reference + +### wrapAsSpy + +Wraps an object as a spy object by adding additional properties for accessing spy information. + +```typescript +function wrapAsSpy(agent: JSHandle | ReturnType>, obj: T): T & SpyObject; +``` + +- `agent`: The JSHandle representing the spy or mock function. +- `obj`: The object to wrap as a spy object. +- Returns: The wrapped object with spy properties. + +### createSpy + +Creates a spy object. + +```typescript +async function createSpy( + ctx: T, + spyCtor: (ctx: ExtractFromJSHandle, ...args: ARGS) => ReturnType, + ...argsToCtor: ARGS +): Promise; +``` + +- `ctx`: The `JSHandle` to spy on. +- `spyCtor`: The function that creates the spy. +- `argsToCtor`: The arguments to pass to the spy constructor function. +- Returns: A promise that resolves to the created spy object. + +Usage: + +```typescript +const ctx = ...; // JSHandle to spy on +const spyCtor = (ctx) => jestMock.spy(ctx, 'prop'); // Spy constructor function +const spy = await createSpy(ctx, spyCtor); + +// Access spy properties +console.log(await spy.calls); +console.log(await spy.callsLength); +console.log(await spy.lastCall); +console.log(await spy.results); +``` + +### getSpy + +Retrieves an existing `SpyObject` from a `JSHandle`. + +```typescript +async function getSpy( + ctx: T, + spyExtractor: SpyExtractor, []> +): Promise; +``` + +- `ctx`: The `JSHandle` containing the spy object. +- `spyExtractor`: The function to extract the spy object. +- Returns: A promise that resolves to the spy object. + +Usage: + +```typescript +const component = await Component.createComponent(page, 'b-button', { + attrs: { + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx.localEmitter, 'emit') + } +}); + +const spyExtractor = (ctx) => ctx.unsafe.localEmitter.emit; + +const spyObject = await getSpy(component, spyExtractor); + +// Now you can access spy information from the spy object +console.log(await spyObject.calls); +console.log(await spyObject.callsLength); +console.log(await spyObject.lastCall); +console.log(await spyObject.results); +``` + +### createMockFn + +Creates a mock function and injects it into a Page object. + +```typescript +async function createMockFn( + page: Page, + fn: (...args: any[]) => any, + ...args: any[] +): Promise; +``` + +- `page`: The Page object to inject the mock function into. +- `fn`: The mock function. +- `args`: The arguments to pass to the function. +- Returns: A promise that resolves to the mock function as a `SpyObject`. + +Usage: + +```typescript +const page = ...; // Page object +const fn = () => {}; // The mock function +const mockFn = await createMockFn(page, fn); + * +// Access spy properties +console.log(await mockFn.calls); +console.log(await mockFn.callsLength); +console.log(await mockFn.lastCall); +console.log(await mockFn.results); +``` + +### injectMockIntoPage + +Injects a mock function into a Page object and returns the `SpyObject`. + +```typescript +async function injectMockIntoPage( + page: Page, + fn: (...args: any[]) => any, + ...args: any[] +): Promise<{agent: SpyObject; id: string}>; +``` + +- `page`: The Page object to inject the mock function into. +- `fn`: The mock function. +- `args`: The arguments to pass to the function. +- Returns: A promise that resolves to an object containing the spy object and the ID of the injected mock function. + +Usage: + +```typescript +const page = ...; // Page object +const fn = () => {}; // The mock function +const { agent, id } = await injectMockIntoPage(page, fn); + +// Access spy properties +console.log(await agent.calls); +console.log(await agent.callsLength); +console.log(await agent.lastCall); +console.log(await agent.results); +``` \ No newline at end of file From 27fe394779ac1963cf6f6890e221932042312791 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 31 Jul 2023 14:16:28 +0300 Subject: [PATCH 081/159] :art: --- src/components/base/b-virtual-scroll/handlers.ts | 2 +- .../base/b-virtual-scroll/modules/state/index.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/base/b-virtual-scroll/handlers.ts b/src/components/base/b-virtual-scroll/handlers.ts index 19a0d9f64e..60d1fdb816 100644 --- a/src/components/base/b-virtual-scroll/handlers.ts +++ b/src/components/base/b-virtual-scroll/handlers.ts @@ -66,7 +66,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * @param childList */ protected onDomInsertStart(this: bVirtualScroll, childList: MountedChild[]): void { - this.componentInternalState.updateDataCursor(); + this.componentInternalState.updateDataOffset(); this.componentInternalState.updateMounted(childList); this.componentInternalState.setIsInitialRender(false); this.componentInternalState.incrementRenderPage(); diff --git a/src/components/base/b-virtual-scroll/modules/state/index.ts b/src/components/base/b-virtual-scroll/modules/state/index.ts index b24eff782e..00303f50e3 100644 --- a/src/components/base/b-virtual-scroll/modules/state/index.ts +++ b/src/components/base/b-virtual-scroll/modules/state/index.ts @@ -87,7 +87,7 @@ export class ComponentInternalState extends Friend { childList.push(...mounted); itemsList.push(...newItems); - this.updateRemainingChild(); + this.updateRemainingChildren(); } /** @@ -159,7 +159,7 @@ export class ComponentInternalState extends Friend { state.remainingChildren = state.childList.length - 1 - state.maxViewedChild; } - this.updateRemainingChild(); + this.updateRemainingChildren(); } /** @@ -172,7 +172,7 @@ export class ComponentInternalState extends Friend { /** * Updates the cursor indicating the last index of the last rendered data element. */ - updateDataCursor(): void { + updateDataOffset(): void { const {ctx, state} = this, current = this.getDataCursor(), @@ -185,7 +185,7 @@ export class ComponentInternalState extends Friend { * Updates the state of the tillEnd-like fields. * Calculates the remaining number of child elements until the end and the remaining number of items until the end. */ - updateRemainingChild(): void { + updateRemainingChildren(): void { const {state} = this; From 1af03516da50c5e78eafb3f0334a737673aff4ae Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 23 Aug 2023 16:20:22 +0300 Subject: [PATCH 082/159] Added component processors --- components-lock.json | 34 ++++++- src/components/base/b-virtual-scroll/const.ts | 7 +- .../b-virtual-scroll/interface/component.ts | 12 +++ .../b-virtual-scroll/modules/factory/index.ts | 28 +++++- src/components/base/b-virtual-scroll/props.ts | 97 ++++++++++++++++++- .../test/unit/functional/props/props.ts | 29 ++++++ 6 files changed, 200 insertions(+), 7 deletions(-) diff --git a/components-lock.json b/components-lock.json index 425b1fc065..4dc1ac18ba 100644 --- a/components-lock.json +++ b/components-lock.json @@ -1,5 +1,5 @@ { - "hash": "2183515ca30acdcc3fbd34f8abea947dd980fdfdf2b258921d3ad019b4ef9ed2", + "hash": "06e86e53358c1b27da7c16c79eca66626018d542b8baafb42ad2a27db11e94ba", "data": { "%data": "%data:Map", "%data:Map": [ @@ -873,6 +873,38 @@ "etpl": null } ], + [ + "b-scrolly", + { + "index": "src/components/base/b-scrolly/index.js", + "declaration": { + "name": "b-scrolly", + "parent": "i-data", + "dependencies": [], + "libs": [] + }, + "name": "b-scrolly", + "parent": "i-data", + "dependencies": [], + "libs": [], + "resolvedLibs": { + "%data": "%data:Set", + "%data:Set": [] + }, + "resolvedOwnLibs": { + "%data": "%data:Set", + "%data:Set": [] + }, + "type": "block", + "mixin": false, + "logic": "src/components/base/b-scrolly/b-scrolly.ts", + "styles": [ + "src/components/base/b-scrolly/b-scrolly.styl" + ], + "tpl": "src/components/base/b-scrolly/b-scrolly.ss", + "etpl": null + } + ], [ "b-select", { diff --git a/src/components/base/b-virtual-scroll/const.ts b/src/components/base/b-virtual-scroll/const.ts index 9aef01de93..44510dbcd4 100644 --- a/src/components/base/b-virtual-scroll/const.ts +++ b/src/components/base/b-virtual-scroll/const.ts @@ -16,7 +16,8 @@ import type { ComponentObserverLocalEvents, ComponentRenderLocalEvents, VirtualScrollState, - RenderGuardRejectionReason + RenderGuardRejectionReason, + ItemsProcessors } from 'components/base/b-virtual-scroll/interface'; @@ -111,3 +112,7 @@ export const defaultShouldProps = { state.isInitialRender || state.remainingItems === 0 }; +/** + * {@link bVirtualScroll.itemsProcessors} + */ +export const itemsProcessors: ItemsProcessors = {}; diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts index e255ede8ee..365f2911dc 100644 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -280,3 +280,15 @@ export interface ComponentDb { export interface ComponentItemFactory { (state: VirtualScrollState, ctx: bVirtualScroll): ComponentItem[]; } + +/** + * A middleware function used to modify elements compiled within {@link bVirtualScroll.itemsFactory}. + */ +export interface ItemsProcessor { + (componentItems: ComponentItem[]): ComponentItem[]; +} + +/** + * Type for {@link bVirtualScroll.itemsProcessors}. + */ +export type ItemsProcessors = ItemsProcessor | Record | ItemsProcessor[]; diff --git a/src/components/base/b-virtual-scroll/modules/factory/index.ts b/src/components/base/b-virtual-scroll/modules/factory/index.ts index d0c1f41f09..667cbb7af4 100644 --- a/src/components/base/b-virtual-scroll/modules/factory/index.ts +++ b/src/components/base/b-virtual-scroll/modules/factory/index.ts @@ -10,7 +10,7 @@ import Friend from 'components/friends/friend'; import type { VNodeDescriptor } from 'components/friends/vdom'; import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { ComponentItem, MountedChild, MountedItem } from 'components/base/b-virtual-scroll/interface'; +import type { ComponentItem, ItemsProcessor, MountedChild, MountedItem } from 'components/base/b-virtual-scroll/interface'; import { isItem } from 'components/base/b-virtual-scroll/modules/helpers'; import * as vdomRender from 'components/base/b-virtual-scroll/modules/factory/engines/vdom'; @@ -29,7 +29,7 @@ export class ComponentFactory extends Friend { const {ctx} = this; - return ctx.itemsFactory(ctx.getComponentState(), ctx); + return this.itemsProcessor(ctx.itemsFactory(ctx.getComponentState(), ctx)); } /** @@ -78,6 +78,30 @@ export class ComponentFactory extends Friend { }); } + /** + * Invokes the {@link bVirtualScroll.itemsProcessors} function and returns its result. + * @param items - The list of items to process. + */ + protected itemsProcessor(items: ComponentItem[]): ComponentItem[] { + const + {ctx} = this; + + if (!ctx.itemsProcessors) { + return items; + } + + if (Object.isFunction(ctx.itemsProcessors)) { + return ctx.itemsProcessors(items); + } + + Object.forEach(ctx.itemsProcessors, (processor) => { + items = processor(items); + }); + + return items; + } + + /** * Calls the render engine to render the components based on the provided descriptors. * Returns an array of rendered DOM nodes. diff --git a/src/components/base/b-virtual-scroll/props.ts b/src/components/base/b-virtual-scroll/props.ts index 397a865f2c..707a9b291f 100644 --- a/src/components/base/b-virtual-scroll/props.ts +++ b/src/components/base/b-virtual-scroll/props.ts @@ -18,11 +18,13 @@ import type { RequestQueryFn, ShouldPerform, ComponentItemFactory, - ComponentItemType + ComponentItemType, + ComponentItem, + ItemsProcessors } from 'components/base/b-virtual-scroll/interface'; -import { defaultShouldProps, componentItemType } from 'components/base/b-virtual-scroll/const'; +import { defaultShouldProps, componentItemType, itemsProcessors } from 'components/base/b-virtual-scroll/const'; import { Observer } from 'components/base/b-virtual-scroll/modules/observer'; @@ -122,6 +124,96 @@ export default abstract class iVirtualScrollProps extends iData { readonly itemsFactory!: ComponentItemFactory; + /** + * This processor function enables you to perform additional manipulations on previously compiled + * {@link ComponentItem}s using {@link bVirtualScroll.itemsFactory}. In simpler terms, it serves as middleware for + * components that need to be rendered. + * + * This function can be valuable when implementing global processing for each component + * rendered in `b-virtual-scroll`. + * + * Let's explore scenarios where you might want to use this functionality: + * + * **Scenario**: Adding an advertisement component after each component rendered in `b-virtual-scroll` throughout the entire application. + * + * **Solution**: While you can override {@link bVirtualScroll.itemsFactory} inline, it's not the most convenient approach. + * Instead, you can use {@link bVirtualScroll.itemsProcessors} for a more streamlined and centralized solution to this task. + * + * @example + * ```typescript + * const addAds = (items: ComponentItem[]) => { + * const newItems = []; + * + * items.forEach((item) => { + * newItems.push(item); + * + * if (item.type === 'item') { + * newItems.push({ + * type: 'separator', + * item: 'b-ads-component', + * props: { + * prop: 'val' + * }, + * key: 'uniqkey' + * }); + * } + * }); + * + * return newItems; + * } + * ``` + * + * To set this manipulation as the global component processor in `b-virtual-scroll`, you need to override the + * `itemsProcessors` constant, which `b-virtual-scroll` defaults to, in the `base/b-virtual-scroll/const.ts` file of your layer + * and export it. + * + * @example + * ```typescript + * const itemsProcessors = { + * addAds + * } + * ``` + * + * Once you have this manipulation, the `b-ads-component` will be rendered within each `b-virtual-scroll` in your application + * after an `item` type component. + * + * Additionally, this prop allows you to mutate component props, component names, child elements, and add new elements. + * For example, you might want to replace deprecated `b-card` components with `b-mega-card` components. + * + * @example + * ```typescript + * const itemsProcessors = { + * addAds, + * migrateCardComponent: (items: ComponentItem[]) => { + * return items.map((item) => { + * if (item.item === 'b-card') { + * console.warn('Deprecation warning: b-card is deprecated.'); + * + * return { + * ...item, + * props: convertProps(item.props) + * item: 'b-mega-card' + * } + * } + * + * return item; + * }); + * } + * } + * ``` + * + * Now, `b-card` components will be replaced with `b-mega-card` components, and using the deprecated `b-card` component will trigger a warning. + * + * Just like other props, you can also specify `itemsProcessors` inline. This allows you to establish global behavior, + * such as adding advertisement components, and selectively override it on specific pages. + */ + @prop({ + type: [Function, Object, Array], + default: itemsProcessors + }) + + readonly itemsProcessors?: ItemsProcessors; + override readonly DB!: ComponentDb; /** @@ -211,4 +303,3 @@ export default abstract class iVirtualScrollProps extends iData { @prop(Boolean) readonly disableObserver: boolean = false; } - diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts index 733a91f350..a017f4265c 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts @@ -135,4 +135,33 @@ test.describe('', () => { await test.expect(component.waitForChildCountEqualsTo(chunkSize * 2)).resolves.toBeUndefined(); }); }); + + test.describe('`itemsProcessors`', () => { + test('Should modify components before rendering', async () => { + const + chunkSize = 12; + + provider.response(200, () => ({data: state.data.addData(chunkSize)})); + + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize, + shouldPerformDataRequest: () => false, + itemsProcessors: (items) => items.concat([ + { + item: 'b-dummy', + type: 'separator', + props: {}, + key: 'uniq' + } + ]) + }) + .build(); + + await component.waitForChildCountEqualsTo(chunkSize + 1); + + await test.expect(component.container.locator('.b-dummy').waitFor({state: 'attached'})).resolves.toBeUndefined(); + }); + }); }); From fde71a2eaae028666b5d7346c4c5585d7a21e07e Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 23 Aug 2023 19:44:40 +0300 Subject: [PATCH 083/159] Drop dead code --- src/components/base/b-scrolly/README.md | 3 --- src/components/base/b-scrolly/b-scrolly.ss | 17 ----------------- src/components/base/b-scrolly/b-scrolly.styl | 0 src/components/base/b-scrolly/b-scrolly.ts | 17 ----------------- src/components/base/b-scrolly/index.js | 10 ---------- src/components/base/b-scrolly/interface.ts | 3 --- src/components/base/b-scrolly/modules/map.ts | 6 ------ .../base/b-scrolly/modules/observers.ts | 6 ------ .../test/unit/functional/props/props.ts | 2 +- 9 files changed, 1 insertion(+), 63 deletions(-) delete mode 100644 src/components/base/b-scrolly/README.md delete mode 100644 src/components/base/b-scrolly/b-scrolly.ss delete mode 100644 src/components/base/b-scrolly/b-scrolly.styl delete mode 100644 src/components/base/b-scrolly/b-scrolly.ts delete mode 100644 src/components/base/b-scrolly/index.js delete mode 100644 src/components/base/b-scrolly/interface.ts delete mode 100644 src/components/base/b-scrolly/modules/map.ts delete mode 100644 src/components/base/b-scrolly/modules/observers.ts diff --git a/src/components/base/b-scrolly/README.md b/src/components/base/b-scrolly/README.md deleted file mode 100644 index 397f787423..0000000000 --- a/src/components/base/b-scrolly/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## Загрузка и хранение данных - -## Отрисовка элементов diff --git a/src/components/base/b-scrolly/b-scrolly.ss b/src/components/base/b-scrolly/b-scrolly.ss deleted file mode 100644 index 230c335066..0000000000 --- a/src/components/base/b-scrolly/b-scrolly.ss +++ /dev/null @@ -1,17 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -- namespace [%fileName%] - -- include 'components/super/i-data'|b as placeholder - -- template index() extends ['i-data'].index - - block body - < .&__wrapper - < .&__item v-for = item in items - += self.slot('item', {':item': 'item'}) diff --git a/src/components/base/b-scrolly/b-scrolly.styl b/src/components/base/b-scrolly/b-scrolly.styl deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/components/base/b-scrolly/b-scrolly.ts b/src/components/base/b-scrolly/b-scrolly.ts deleted file mode 100644 index cf86aba227..0000000000 --- a/src/components/base/b-scrolly/b-scrolly.ts +++ /dev/null @@ -1,17 +0,0 @@ -import iData, { component, field, RequestParams } from 'components/super/i-data/i-data'; - -@component() -export default class bScrolly extends iData { - @field({forceUpdate: false}) - readonly data!: unknown[]; - - // @ts-ignore (getter instead readonly) - override get requestParams(): RequestParams { - return { - get: {} - }; - } -} - -// Модель дозапросов - остается такой же, -// изменение request влечет за собой перерендер, а requestQuery возвращает постранично параметры diff --git a/src/components/base/b-scrolly/index.js b/src/components/base/b-scrolly/index.js deleted file mode 100644 index dc7adca297..0000000000 --- a/src/components/base/b-scrolly/index.js +++ /dev/null @@ -1,10 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -package('b-scrolly') - .extends('i-data'); diff --git a/src/components/base/b-scrolly/interface.ts b/src/components/base/b-scrolly/interface.ts deleted file mode 100644 index 0386778934..0000000000 --- a/src/components/base/b-scrolly/interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface Item { - // ... -} diff --git a/src/components/base/b-scrolly/modules/map.ts b/src/components/base/b-scrolly/modules/map.ts deleted file mode 100644 index bffe6059a9..0000000000 --- a/src/components/base/b-scrolly/modules/map.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Создает карту высот компонентов. - */ -export function createMap(): void { - // .... -} diff --git a/src/components/base/b-scrolly/modules/observers.ts b/src/components/base/b-scrolly/modules/observers.ts deleted file mode 100644 index 8ea1bbab35..0000000000 --- a/src/components/base/b-scrolly/modules/observers.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Создает наблюдателя за изменением высоты компонентов. - */ -export function createResizeObserver(): void { - // ... -} diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts index a017f4265c..c6b1e05d29 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts @@ -161,7 +161,7 @@ test.describe('', () => { await component.waitForChildCountEqualsTo(chunkSize + 1); - await test.expect(component.container.locator('.b-dummy').waitFor({state: 'attached'})).resolves.toBeUndefined(); + await test.expect(component.container.locator('.b-dummy')).toHaveCount(1); }); }); }); From 607ccff01f9765f18dac4d249aab38f3e335ebe6 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 31 Aug 2023 20:58:55 +0300 Subject: [PATCH 084/159] Added new features --- components-lock.json | 34 +------- .../b-virtual-scroll/interface/component.ts | 6 ++ .../b-virtual-scroll/modules/factory/index.ts | 9 +-- src/components/base/b-virtual-scroll/props.ts | 80 ++++++++++--------- .../test/unit/functional/props/props.ts | 3 +- .../test/unit/functional/rendering/default.ts | 46 +++++++++++ 6 files changed, 99 insertions(+), 79 deletions(-) diff --git a/components-lock.json b/components-lock.json index 4dc1ac18ba..425b1fc065 100644 --- a/components-lock.json +++ b/components-lock.json @@ -1,5 +1,5 @@ { - "hash": "06e86e53358c1b27da7c16c79eca66626018d542b8baafb42ad2a27db11e94ba", + "hash": "2183515ca30acdcc3fbd34f8abea947dd980fdfdf2b258921d3ad019b4ef9ed2", "data": { "%data": "%data:Map", "%data:Map": [ @@ -873,38 +873,6 @@ "etpl": null } ], - [ - "b-scrolly", - { - "index": "src/components/base/b-scrolly/index.js", - "declaration": { - "name": "b-scrolly", - "parent": "i-data", - "dependencies": [], - "libs": [] - }, - "name": "b-scrolly", - "parent": "i-data", - "dependencies": [], - "libs": [], - "resolvedLibs": { - "%data": "%data:Set", - "%data:Set": [] - }, - "resolvedOwnLibs": { - "%data": "%data:Set", - "%data:Set": [] - }, - "type": "block", - "mixin": false, - "logic": "src/components/base/b-scrolly/b-scrolly.ts", - "styles": [ - "src/components/base/b-scrolly/b-scrolly.styl" - ], - "tpl": "src/components/base/b-scrolly/b-scrolly.ss", - "etpl": null - } - ], [ "b-select", { diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts index 365f2911dc..c563da77e3 100644 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -190,6 +190,12 @@ export interface ComponentItem { * Children nodes of the component. */ children?: VNodeChildren; + + /** + * Meta information for a component that will not be used during rendering, + * but will be available for reading/changing in `itemsProcessors`. + */ + meta?: unknown; } /** diff --git a/src/components/base/b-virtual-scroll/modules/factory/index.ts b/src/components/base/b-virtual-scroll/modules/factory/index.ts index 667cbb7af4..b2c8d20759 100644 --- a/src/components/base/b-virtual-scroll/modules/factory/index.ts +++ b/src/components/base/b-virtual-scroll/modules/factory/index.ts @@ -85,23 +85,22 @@ export class ComponentFactory extends Friend { protected itemsProcessor(items: ComponentItem[]): ComponentItem[] { const {ctx} = this; - + if (!ctx.itemsProcessors) { return items; } - + if (Object.isFunction(ctx.itemsProcessors)) { return ctx.itemsProcessors(items); } - + Object.forEach(ctx.itemsProcessors, (processor) => { items = processor(items); }); - + return items; } - /** * Calls the render engine to render the components based on the provided descriptors. * Returns an array of rendered DOM nodes. diff --git a/src/components/base/b-virtual-scroll/props.ts b/src/components/base/b-virtual-scroll/props.ts index 707a9b291f..5cbd954228 100644 --- a/src/components/base/b-virtual-scroll/props.ts +++ b/src/components/base/b-virtual-scroll/props.ts @@ -58,6 +58,15 @@ export default abstract class iVirtualScrollProps extends iData { @prop({type: [Function, Object], default: () => ({})}) readonly itemProps!: iItems['itemProps']; + /** + * Meta information for a component that will not be used during rendering, + * but will be available for reading/changing in `itemsProcessors`. + * + * If a function is provided, it will be called; otherwise, the value will be preserved "as is". + */ + @prop() + readonly itemMeta?: CreateFromItemFn | unknown; + /** * Specifies the number of times the `tombstone` component will be rendered. * @@ -109,6 +118,7 @@ export default abstract class iVirtualScrollProps extends iData { item: Object.isFunction(ctx.item) ? ctx.item(data, i) : ctx.item, type: Object.isFunction(ctx.itemType) ? ctx.itemType(data, i) : ctx.itemType, + meta: Object.isFunction(ctx.itemMeta) ? ctx.itemMeta(data, i) : ctx.itemMeta, props: Object.isFunction(ctx.itemProps) ? ctx.itemProps(data, i, { @@ -125,20 +135,18 @@ export default abstract class iVirtualScrollProps extends iData { readonly itemsFactory!: ComponentItemFactory; /** - * This processor function enables you to perform additional manipulations on previously compiled - * {@link ComponentItem}s using {@link bVirtualScroll.itemsFactory}. In simpler terms, it serves as middleware for - * components that need to be rendered. - * - * This function can be valuable when implementing global processing for each component - * rendered in `b-virtual-scroll`. - * - * Let's explore scenarios where you might want to use this functionality: - * - * **Scenario**: Adding an advertisement component after each component rendered in `b-virtual-scroll` throughout the entire application. - * - * **Solution**: While you can override {@link bVirtualScroll.itemsFactory} inline, it's not the most convenient approach. - * Instead, you can use {@link bVirtualScroll.itemsProcessors} for a more streamlined and centralized solution to this task. - * + * This processor function enables you to manipulate previously compiled + * {@link ComponentItem}s via {@link bVirtualScroll.itemsFactory}. It allows you to add components to render, + * mutate props, and add children. It acts as middleware for rendering components. + * + * Scenarios where you might use this functionality: + * + * **Scenario**: Add an advertisement component after each rendered component + * in `b-virtual-scroll` throughout the app. + * + * **Solution**: Instead of overriding {@link bVirtualScroll.itemsFactory} inline, + * use {@link bVirtualScroll.itemsProcessors} for a centralized solution. + * * @example * ```typescript * const addAds = (items: ComponentItem[]) => { @@ -151,35 +159,34 @@ export default abstract class iVirtualScrollProps extends iData { * newItems.push({ * type: 'separator', * item: 'b-ads-component', - * props: { - * prop: 'val' - * }, - * key: 'uniqkey' + * props: { prop: 'val' }, + * key: 'uniqueKey' * }); * } * }); - * + * * return newItems; * } * ``` - * - * To set this manipulation as the global component processor in `b-virtual-scroll`, you need to override the - * `itemsProcessors` constant, which `b-virtual-scroll` defaults to, in the `base/b-virtual-scroll/const.ts` file of your layer - * and export it. + * + * To set this function as the global component processor in `b-virtual-scroll`, + * override the `itemsProcessors` constant (in `base/b-virtual-scroll/const.ts`) of your layer and export it. * * @example * ```typescript - * const itemsProcessors = { + * export const itemsProcessors = { * addAds * } * ``` - * - * Once you have this manipulation, the `b-ads-component` will be rendered within each `b-virtual-scroll` in your application - * after an `item` type component. - * - * Additionally, this prop allows you to mutate component props, component names, child elements, and add new elements. - * For example, you might want to replace deprecated `b-card` components with `b-mega-card` components. - * + * + * After redefining this, `b-virtual-scroll` renders `b-ads-component` after + * each `item` component. + * + * **Scenario**: Replace `b-card` components with `b-mega-card` throughout the app + * and modify props. + * + * **Solution**: Add a processor function that changes the component name and mutates props. + * * @example * ```typescript * const itemsProcessors = { @@ -187,13 +194,13 @@ export default abstract class iVirtualScrollProps extends iData { * migrateCardComponent: (items: ComponentItem[]) => { * return items.map((item) => { * if (item.item === 'b-card') { - * console.warn('Deprecation warning: b-card is deprecated.'); + * console.warn('Deprecation: b-card is deprecated.'); * * return { * ...item, - * props: convertProps(item.props) + * props: convertProps(item.props), * item: 'b-mega-card' - * } + * }; * } * * return item; @@ -201,11 +208,6 @@ export default abstract class iVirtualScrollProps extends iData { * } * } * ``` - * - * Now, `b-card` components will be replaced with `b-mega-card` components, and using the deprecated `b-card` component will trigger a warning. - * - * Just like other props, you can also specify `itemsProcessors` inline. This allows you to establish global behavior, - * such as adding advertisement components, and selectively override it on specific pages. */ @prop({ type: [Function, Object, Array], diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts index c6b1e05d29..96519a6194 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts @@ -159,8 +159,7 @@ test.describe('', () => { }) .build(); - await component.waitForChildCountEqualsTo(chunkSize + 1); - + await test.expect(component.waitForChildCountEqualsTo(chunkSize + 1)).resolves.toBeUndefined(); await test.expect(component.container.locator('.b-dummy')).toHaveCount(1); }); }); diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts index 4d34b61fc8..4eafa5e96e 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts @@ -12,9 +12,12 @@ import test from 'tests/config/unit/test'; +import { Scroll } from 'tests/helpers'; + import type { VirtualScrollState, ShouldPerform } from 'components/base/b-virtual-scroll/interface'; import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; test.describe('', () => { let @@ -121,4 +124,47 @@ test.describe('', () => { }); }); }); + + test.describe('`chunkSize` is 6', () => { + test.describe('provider responded once, returning 45 elements', () => { + test.describe('`shouldStopRequestingData` returns true after first request', () => { + test('should render all 45 elements within 8 rendering cycles', async () => { + const + chunkSize = 6, + providerChunkSize = 45; + + provider + .responseOnce(200, {data: state.data.addData(providerChunkSize)}); + + const shouldPerformDataRender = await component.mockFn( + ({isInitialRender, remainingItems: remainingItems}) => isInitialRender || remainingItems === 0 + ); + + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + shouldPerformDataRender, + shouldStopRequestingData: () => true, + chunkSize, + '@hook:beforeDataCreate': (ctx: bVirtualScroll) => jestMock.spy(ctx.unsafe.componentFactory, 'produceNodes') + }); + + await component.build(); + + await Scroll.scrollToBottomWhile(component.page, async () => { + const + isEqual = await component.getChildCount() === providerChunkSize; + + return isEqual; + }); + + const + spy = await component.getSpy((ctx) => ctx.unsafe.componentFactory.produceNodes); + + await test.expect(spy.callsLength).resolves.toBe(8); + await test.expect(component.childList).toHaveCount(providerChunkSize); + }); + }); + }); + }); }); From 7ead1a453097082080c6742a815185730de69ef6 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 1 Sep 2023 18:21:46 +0300 Subject: [PATCH 085/159] wip docs --- .../base/b-virtual-scroll/README.md | 272 +++++++++++++++++- .../test/unit/scenario/reload.ts | 7 +- 2 files changed, 265 insertions(+), 14 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 99bb078114..d6a0cbf820 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -21,7 +21,7 @@ See the implemented modifiers or the parent component. | `dataLoadSuccess` | Data loading has succeeded. | `data: object[], isInitialLoading: boolean` | `[data, isInitialLoading]` | | `dataLoadStart` | Data loading has started. | `isInitialLoading: boolean` | `[isInitialLoading]` | | `dataLoadError` | An error occurred while loading data. | `isInitialLoading: boolean` | `[isInitialLoading]` | -| `dataLoadEmpty` | Successful load with no data. | | `[]` | +| `dataLoadEmpty` | Successful load with no data. | | `[]` | | `resetState` | Reset component state. | | `[]` | | `lifecycleDone` | All component data is rendered and loaded. | | `[]` | | `convertDataToDB` | Trigger data conversion to the `DB`. | `data: unknown` | `[data]` | @@ -37,6 +37,260 @@ Also, you can see the implemented traits or the parent component. ## Usage +### Как реализовать простой рендеринг? + +Для того чтобы реализовать простой рендеринг необходимо проделать несколько простых манипуляций: + +Установите дата-провайдер компоненту. Для примера мы будем использовать провайдер +с именем `Provider` который возвращает N кол-во данных в формате: `{data: object[]}`, где N зависит +от параметра запроса `count`. + +``` +< b-virtual-scroll & + :dataProvider = 'Provider' +. +``` + +> Важно отметить что `b-virtual-scroll` ожидает именно такой (`{data: object[]}`) формат данных, если ваш провайдер +возвращает данные в каком-либо другом формате - вы можете использовать различные процессоры либо в пройдере, либо в компонент с помощью пропа +`convertDataToDb`. + +Допустим мы хотим загружать и рисовать 12 компонентов за раз, для этого необходимо так же компоненту `b-virtual-scroll` +указать пропы: `request` и `chunkSize`. `request` проп отвечает за параметры запроса (стандартное поведение `iData`) а `chunkSize` +отвечает за кол-во отрисоваемых элементов за один цикл отрисовки. + +``` +< b-virtual-scroll & + :dataProvider = 'Provider' | + :request = {get: {count: 12}} | + :chunkSize = 12 +. +``` + +После того как мы установили данные параметры наш провайдер будет загружать одни и те же данные, чтобы этого избежать и загружать +каждый раз различные данные (следующий кусок данных) нам необходимо передавать параметр запроса `page` в `Provider` который указывает +на номер страницы загрузки. + +Для того чтобы реализовать передачу данного параметра запроса компоненту `b-virtual-scroll` необходимо указать дополнительный проп +`requestQuery`. `requestQuery` это функция-проп, которую вызывает `b-virtual-scroll` передавая аргументом свое состояние, перед тем как сделать запрос. +На основе состояния `b-virtual-scroll` можно из этой функции вернуть походящее значение для параметра `page`. Разница между `request` и `requestQuery` заключается в том, +что в случае изменения последнего, не произойдет переинициализации компонента. Эти два пропа, после того как возвращают значения, мержутся между собой и получаются +финальные параметры запроса которые будут переданы в провайдер. + +``` +< b-virtual-scroll & + :dataProvider = 'Provider' | + :request = {get: {count: 12}} | + :requestQuery = (state) => ({page: state.loadPage}) | + :chunkSize = 12 +. +``` + +Как видно из примера выше параметр `page` берется из состояния компонента, а именно `loadPage`, `loadPage` же в свою очередь это число которое увеличивается +после каждой успешной загрузки данных. + +Теперь, когда у нас есть загрузка данных с помощью пагинация, остается вопрос, что же будет отрисовываться в нашем `b-virtual-scroll`? + +Для управления тем что будет отрисовано `b-virtual-scroll` предоставляет несколько пропов: + +- `item` - Имя компонента который должен быть отрисован. Так же `item` может быть функцией, которая будет вызвана и результат которой +будет считаться именем компонента. + +- `itemProps` - Пропы компонента. Обычно это функция которая возвращает пропы компонента. На вход получает один элемент +данных из загруженных данных. + +Отрисовка происходит после загрузки данных, компонент загрузки 12 элементов в массиве `data` и для каждого из 12 +элементов будет вызвана функция `itemProps`. + +``` +< b-virtual-scroll & + :dataProvider = 'Provider' | + :request = {get: {count: 12}} | + :requestQuery = (state) => ({page: state.loadPage}) | + :chunkSize = 12 | + :item = 'b-dummy' | + :itemProps = (data) => ({name: data.name, type: data.type}) +. +``` + +Таким образом, у нас на странице будет отображаться компонент, который загружает 12 элементов и отрисовывает тоже 12 за один раз, +при прокрутке к низу компонента будет происходит новый запрос с другим значением `page` и, после успешной загрузки, отрисовываться новые компоненты. + +Но допустим наш компонент будет загружать данные очень долго, например 1 секунду, таким образом мы получим ситуацию что у нас сначала было пустое место, а потом +бац и появился контент, пользователь будет в шоке от неожиданности. Чтобы этого не допустить компонент `b-virtual-scroll` предоставляет клиентам несколько +слотов которые позволяет отрисовать "загрузчик" который будет показываться пока загружаются данные. + +Давайте добавим слот `loader` нашему компоненту чтобы не пугать пользователя. + +``` +< b-virtual-scroll & + :dataProvider = 'Provider' | + :request = {get: {count: 12}} | + :requestQuery = (state) => ({page: state.loadPage}) | + :chunkSize = 12 | + :item = 'b-dummy' | + :itemProps = (data) => ({name: data.name, type: data.type}) +. + < template #loader + < .&__loader + Data loading in progress +``` + +Теперь пользователь будет виден симпатичную надпись которая намекает на наличие контента который появится чуть позже. + +### Как реализовать рендеринг компонент не по скролу, а по клику? + +Компонент `b-virtual-scroll`, помимо стратегии загрузки по скролу, так же имеет возможность +загружать данные по какому-либо событию, например по клику на кнопку. + +Для реализации такого подхода необходимо проделать несколько шагов. + +1. Отключить наблюдателей за скролом с помощью пропа `disableObserver` и установки его в `true`. + +``` +< b-virtual-scroll & + :dataProvider = 'Provider' | + :disableObserver = true | + ... +. +``` + +2. Установить проп `shouldPerformDataRender` как функцию которая всегда возвращает `true`. Данная функция +будет вызываться при каждой попытке отрисовать данные, более детально о ней мы поговорим в следующих главах. + +``` +< b-virtual-scroll & + :dataProvider = 'Provider' | + :disableObserver = true | + :shouldPerformDataRender = () => true | + ... +. +``` + +3. Получить возможность обращаться к методам `b-virtual-scroll`, для этого используем стандартный механизм `ref`. + +``` +< b-virtual-scroll & + ref = scroll | + :dataProvider = 'Provider' | + :disableObserver = true | + :shouldPerformDataRender = () => true | + ... +. +``` + +После данных манипуляций `b-virtual-scroll` перестанет загружать данные по скролу и будет это делать только после вызова метода `initLoadNext`. +Собственно именно этот метод мы и будем использовать чтобы загружать и отрисовывать данные. + +Теперь необходимо добавить какую-нибудь кнопку которая будет на события клик по себе вызывать метод `initLoadNext`. + +``` +< b-virtual-scroll & + ref = scroll | + :dataProvider = 'Provider' | + :disableObserver = true | + :shouldPerformDataRender = () => true | + ... +. + +< b-button & + @click = $refs.scroll.initLoadNext +. + Load more data +``` + +Отлично, теперь по клику на данную кнопку будет происходить загрузка и отрисовка данных. Но при использовании +можно будет заметить что кнопка загрузки данных не исчезает ни когда все данные загружены, ни когда происходит загрузка данных, ни в случае ошибки. +К счастью `b-virtual-scroll` предоставляет слот для отображения такой кнопки и всю логику скрытия ее во время загрузки, ошибки и так далее берет на себя. +Клиенту в данном случае никакой дополнительной логики реализовывать не надо, нужно просто нашу кнопку переместить в подходящий слот, а именно в слот `renderNext`. + +``` +< b-virtual-scroll & + ref = scroll | + :dataProvider = 'Provider' | + :disableObserver = true | + :shouldPerformDataRender = () => true | + ... +. + + < template #renderNext + < b-button & + @click = $refs.scroll.initLoadNext + . + Load more data +``` + +### Как переинициализировать компонент? + +Часто возникают ситуации когда необходимо перерисовать все данные что были отрисованы с помощью `b-virtual-scroll`, например +включилась какая-то дополнительная фильтрация из-за которой отрисованные ранее данные в `b-virtual-scroll` стали неактуальны. + +В таком случае компонент предоставляет несколько путей повторной инициализации компонента, это позволит очистить состояние на "первозданное", +то есть удалить ранее отрисованные компоненты и скинуть состояние. После того как состояние сброшено компонент опять начнет свой жизненный цикл будто бы +он создан с "нуля". Рассмотрим ниже варианты сброса состояния. + +1. Обновление пропа `request`; + +2. Всплытие какого-либо события в `globalEmitter` из списка: + - `reset`; + - `reset.silence`; + - `reset.load`; + - `reset.load.silence`. + +То есть компонент автоматически будет перезагружаться когда всплывает какое-либо из данных событий (стандартная логика `iData`). + +3. Вызов метода `reload` или `initLoad`. + +В каких случаях какой вариант использовать? + +В случае если у вас есть на странице фильтры и запрос за данными которые должны быть отрисованы с помощью `b-virtual-scroll` - `request` проп +будет как нельзя кстати, вы можете использовать в пропе `request` ссылку на текущее состояние фильтров и таким образом при изменении этого состояния +на странице будет произведена автоматическая переинициализация. + +Рассмотрим на примере: + +__p-page.ts__ +```typescript +@component() +class pPage extends extends iDynamicPage { + @field() + filterUuid: string; + + onFilterClick(newFilter: string): void { + this.filterUuid = newFilter; + } +} +``` + +__p-page.ss__ +``` +< b-virtual-scroll & + :dataProvider = 'Provider' | + :request = {get: {count: 12, filter: filterUuid}} | + ... +. +``` + +Таким образом при изменении поля `filterUuid` на странице `pPage` `b-virtual-scroll` будет выполнять переинициализации и перезагружаться. + +В случае если же вам нужно обновить состояние компонента в какое-то момент времени без зависимости от контекста - можно использовать функции `reload` или `initLoad`. + +### Как использовать should-like функции? + +### iItems и itemsFactory + +### request и requestQuery + +### Использование слотов + +### Глобальные переопределения + + + +### How to implement on click rendering? + +### How + ### Converting Data to the Required Format The `b-virtual-scroll` component expects data in a specific format: @@ -195,7 +449,9 @@ This slot can be useful when implementing lazy content rendering on button click ## API -### `shouldPerformDataRender` +### Props + +#### `shouldPerformDataRender` - Type: `Function` - Default: `(state: VirtualScrollState) => state.isInitialRender || state.remainingItems === 0` @@ -212,7 +468,7 @@ const shouldPerformDataRender = (state: VirtualScrollState): boolean => { }; ``` -### `shouldPerformDataRequest` +#### `shouldPerformDataRequest` - Type: `Function` - Default: `(state: VirtualScrollState) => state.lastLoadedData.length > 0` @@ -236,7 +492,7 @@ You can adjust the condition based on your specific requirements. By implementing the `shouldPerformDataRequest` function, you have control over when the component should request additional data. This allows you to customize the data loading behavior based on the state of the component. -### `shouldStopRequestingData` +#### `shouldStopRequestingData` - Type: `Function` - Default: `(state: VirtualScrollState) => state.lastLoadedData.length > 0` @@ -260,7 +516,7 @@ This condition suggests that all available items have been loaded, and there is You can customize the `shouldStopRequestingData` function to fit your specific scenario. By implementing this function, you have control over when the component should stop requesting new data, based on the comparison between the total number of items and the current number of loaded items. -### `chunkSize` +#### `chunkSize` - Type: `number | Function` - Default: `10` @@ -292,7 +548,7 @@ In Example 2, the chunk size is dynamically determined based on the component st By using a function for `chunkSize`, you have the flexibility to adjust the rendering behavior based on the state of the component and other factors. -### `requestQuery` +#### `requestQuery` - Type: `Function` - Default: `undefined` @@ -314,7 +570,7 @@ const requestQuery = (state: VirtualScrollState): Dictionary => { }; ``` -### `itemsFactory` +#### `itemsFactory` - Type: `Function` - Default: See description @@ -348,7 +604,7 @@ const itemsFactory = (state: VirtualScrollState): ComponentItem[] => { }; ``` -### `tombstonesSize` +#### `tombstonesSize` - Type: `number` - Default: `undefined` diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts index 69e81d73e5..06914c3d7d 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts @@ -97,7 +97,7 @@ test.describe('', () => { }); ['reset', 'reset.silence', 'reset.load', 'reset.load.silence'].forEach((event, i) => { - test.describe(`Случилось событие ${event}`, () => { + test.describe(`${event} fired`, () => { test('Should reset state and reload the component data', async () => { const chunkSize = 12; @@ -164,8 +164,3 @@ test.describe('', () => { }); }); - -// * `reset` - reloads all data providers (including the tied storage and router); -// * `reset.silence` - reloads all data providers (including the tied storage and router) in silent mode; -// * `reset.load` - reloads the tied data providers; -// * `reset.load.silence` - reloads the tied data providers in silent mode; From 3e923753fad0cd943f184319f8b0c557554552bd Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sun, 3 Sep 2023 15:07:43 +0300 Subject: [PATCH 086/159] WIP --- .../base/b-virtual-scroll/README.md | 240 ++++++++++++------ 1 file changed, 166 insertions(+), 74 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index d6a0cbf820..8512f26dd6 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -37,13 +37,11 @@ Also, you can see the implemented traits or the parent component. ## Usage -### Как реализовать простой рендеринг? +### How to Implement Simple Rendering? -Для того чтобы реализовать простой рендеринг необходимо проделать несколько простых манипуляций: +To implement simple rendering, you need to follow several steps: -Установите дата-провайдер компоненту. Для примера мы будем использовать провайдер -с именем `Provider` который возвращает N кол-во данных в формате: `{data: object[]}`, где N зависит -от параметра запроса `count`. +1. Set up a data provider for the component. For example, we'll use a provider named `Provider` that returns data in the format `{data: object[]}`, where the number of objects depends on the request parameter `count`. ``` < b-virtual-scroll & @@ -51,13 +49,9 @@ Also, you can see the implemented traits or the parent component. . ``` -> Важно отметить что `b-virtual-scroll` ожидает именно такой (`{data: object[]}`) формат данных, если ваш провайдер -возвращает данные в каком-либо другом формате - вы можете использовать различные процессоры либо в пройдере, либо в компонент с помощью пропа -`convertDataToDb`. +> It's important to note that `b-virtual-scroll` expects data in this specific format (`{data: object[]}`). If your provider returns data in a different format, you can use processors in either the provider or the component using the `convertDataToDb` prop. -Допустим мы хотим загружать и рисовать 12 компонентов за раз, для этого необходимо так же компоненту `b-virtual-scroll` -указать пропы: `request` и `chunkSize`. `request` проп отвечает за параметры запроса (стандартное поведение `iData`) а `chunkSize` -отвечает за кол-во отрисоваемых элементов за один цикл отрисовки. +2. Let's say we want to load and render 12 components at a time. To achieve this, you need to specify the `request` and `chunkSize` props for the `b-virtual-scroll` component. The `request` prop defines the request parameters (standard behavior of `iData`), and `chunkSize` specifies the number of items to render in each rendering cycle. ``` < b-virtual-scroll & @@ -67,15 +61,9 @@ Also, you can see the implemented traits or the parent component. . ``` -После того как мы установили данные параметры наш провайдер будет загружать одни и те же данные, чтобы этого избежать и загружать -каждый раз различные данные (следующий кусок данных) нам необходимо передавать параметр запроса `page` в `Provider` который указывает -на номер страницы загрузки. +3. To avoid loading the same data repeatedly and load different data for each subsequent request, you need to pass the `page` request parameter to the `Provider`. This parameter indicates the page number of the data to be loaded. -Для того чтобы реализовать передачу данного параметра запроса компоненту `b-virtual-scroll` необходимо указать дополнительный проп -`requestQuery`. `requestQuery` это функция-проп, которую вызывает `b-virtual-scroll` передавая аргументом свое состояние, перед тем как сделать запрос. -На основе состояния `b-virtual-scroll` можно из этой функции вернуть походящее значение для параметра `page`. Разница между `request` и `requestQuery` заключается в том, -что в случае изменения последнего, не произойдет переинициализации компонента. Эти два пропа, после того как возвращают значения, мержутся между собой и получаются -финальные параметры запроса которые будут переданы в провайдер. +To achieve this, use the `requestQuery` prop in the `b-virtual-scroll` component. `requestQuery` is a function prop that `b-virtual-scroll` calls, passing its own state as an argument, before making a request. You can return the appropriate `page` value based on the component's state. The difference between `request` and `requestQuery` is that changes to the latter won't cause the component to reinitialize. These two props are merged to form the final request parameters passed to the provider. ``` < b-virtual-scroll & @@ -86,21 +74,19 @@ Also, you can see the implemented traits or the parent component. . ``` -Как видно из примера выше параметр `page` берется из состояния компонента, а именно `loadPage`, `loadPage` же в свою очередь это число которое увеличивается -после каждой успешной загрузки данных. +In the example above, the `page` parameter is extracted from the component's state, specifically `loadPage`, which increments after each successful data load. -Теперь, когда у нас есть загрузка данных с помощью пагинация, остается вопрос, что же будет отрисовываться в нашем `b-virtual-scroll`? +4. Now that you have set up data loading with pagination, you need to specify what `b-virtual-scroll` will render. -Для управления тем что будет отрисовано `b-virtual-scroll` предоставляет несколько пропов: +To control what `b-virtual-scroll` renders, you can use the following props: -- `item` - Имя компонента который должен быть отрисован. Так же `item` может быть функцией, которая будет вызвана и результат которой -будет считаться именем компонента. +- `item`: The name of the component to be rendered. It can also be a function that returns the component's name. -- `itemProps` - Пропы компонента. Обычно это функция которая возвращает пропы компонента. На вход получает один элемент -данных из загруженных данных. +- `itemProps`: The props for the component. Typically, this is a function that returns the props for each item based on the loaded data. -Отрисовка происходит после загрузки данных, компонент загрузки 12 элементов в массиве `data` и для каждого из 12 -элементов будет вызвана функция `itemProps`. +- `itemKey`: The uniq id of the component. + +Rendering occurs after data is loaded. ``` < b-virtual-scroll & @@ -109,18 +95,18 @@ Also, you can see the implemented traits or the parent component. :requestQuery = (state) => ({page: state.loadPage}) | :chunkSize = 12 | :item = 'b-dummy' | - :itemProps = (data) => ({name: data.name, type: data.type}) + :itemKey = (el) => el.uuid | + :itemProps = (el) => ({name: el.name, type: el.type}) . ``` -Таким образом, у нас на странице будет отображаться компонент, который загружает 12 элементов и отрисовывает тоже 12 за один раз, -при прокрутке к низу компонента будет происходит новый запрос с другим значением `page` и, после успешной загрузки, отрисовываться новые компоненты. +What is `el` you can inquire about in the `itemKey` and `itemProps` functions? `el` is one of the objects in the `data` array loaded by the `dataProvider`. `b-virtual-scroll` takes the array of loaded data and calls these functions for each of the objects in this array, allowing you to transform data into components that are suitable for rendering in the `b-virtual-scroll` component. + +This setup will display a component on the page that loads and renders 12 items at once. When scrolling down, a new request with a different `page` value will be made, and after a successful load, new components will be rendered. -Но допустим наш компонент будет загружать данные очень долго, например 1 секунду, таким образом мы получим ситуацию что у нас сначала было пустое место, а потом -бац и появился контент, пользователь будет в шоке от неожиданности. Чтобы этого не допустить компонент `b-virtual-scroll` предоставляет клиентам несколько -слотов которые позволяет отрисовать "загрузчик" который будет показываться пока загружаются данные. +However, if your component takes a long time to load data (e.g., 1 second), you might notice that there is initially empty space, and then the content suddenly appears, which can be unexpected for users. To avoid this, `b-virtual-scroll` provides slots that allow you to render a "loader" while data is being loaded. -Давайте добавим слот `loader` нашему компоненту чтобы не пугать пользователя. +Let's add a `loader` slot to our component to provide a better user experience during loading: ``` < b-virtual-scroll & @@ -129,23 +115,22 @@ Also, you can see the implemented traits or the parent component. :requestQuery = (state) => ({page: state.loadPage}) | :chunkSize = 12 | :item = 'b-dummy' | - :itemProps = (data) => ({name: data.name, type: data.type}) + :itemProps = (el) => ({name: el.name, type: el.type}) . < template #loader < .&__loader Data loading in progress ``` -Теперь пользователь будет виден симпатичную надпись которая намекает на наличие контента который появится чуть позже. +Now, users will see a friendly message indicating that content will appear shortly, preventing them from being surprised by sudden content changes. -### Как реализовать рендеринг компонент не по скролу, а по клику? +### How to Implement Component Rendering on Click Instead of Scroll? -Компонент `b-virtual-scroll`, помимо стратегии загрузки по скролу, так же имеет возможность -загружать данные по какому-либо событию, например по клику на кнопку. +The `b-virtual-scroll` component, in addition to scroll-based loading, can also load data on other events, such as a click on a button. -Для реализации такого подхода необходимо проделать несколько шагов. +To implement this approach, follow these steps: -1. Отключить наблюдателей за скролом с помощью пропа `disableObserver` и установки его в `true`. +1. Disable scroll observers using the `disableObserver` prop by setting it to `true`. ``` < b-virtual-scroll & @@ -155,8 +140,7 @@ Also, you can see the implemented traits or the parent component. . ``` -2. Установить проп `shouldPerformDataRender` как функцию которая всегда возвращает `true`. Данная функция -будет вызываться при каждой попытке отрисовать данные, более детально о ней мы поговорим в следующих главах. +2. Set the `shouldPerformDataRender` prop to a function that always returns `true`. This function will be called for each attempt to render data. We will discuss this function in more detail in the following sections. ``` < b-virtual-scroll & @@ -167,7 +151,7 @@ Also, you can see the implemented traits or the parent component. . ``` -3. Получить возможность обращаться к методам `b-virtual-scroll`, для этого используем стандартный механизм `ref`. +3. Gain access to the methods of `b-virtual-scroll` using the standard `ref` mechanism. ``` < b-virtual-scroll & @@ -179,10 +163,9 @@ Also, you can see the implemented traits or the parent component. . ``` -После данных манипуляций `b-virtual-scroll` перестанет загружать данные по скролу и будет это делать только после вызова метода `initLoadNext`. -Собственно именно этот метод мы и будем использовать чтобы загружать и отрисовывать данные. +After these manipulations, `b-virtual-scroll` will no longer load data on scroll, and data loading will only occur when the `initLoadNext` method is called. This method will be used to load and render data on a button click event. -Теперь необходимо добавить какую-нибудь кнопку которая будет на события клик по себе вызывать метод `initLoadNext`. +4. Now, you need to add a button that triggers the `initLoadNext` method when clicked. ``` < b-virtual-scroll & @@ -199,10 +182,7 @@ Also, you can see the implemented traits or the parent component. Load more data ``` -Отлично, теперь по клику на данную кнопку будет происходить загрузка и отрисовка данных. Но при использовании -можно будет заметить что кнопка загрузки данных не исчезает ни когда все данные загружены, ни когда происходит загрузка данных, ни в случае ошибки. -К счастью `b-virtual-scroll` предоставляет слот для отображения такой кнопки и всю логику скрытия ее во время загрузки, ошибки и так далее берет на себя. -Клиенту в данном случае никакой дополнительной логики реализовывать не надо, нужно просто нашу кнопку переместить в подходящий слот, а именно в слот `renderNext`. +Now, when you click the button, data will be loaded and rendered. However, you may notice that the data loading button doesn't disappear when all data is loaded, during data loading, or in case of an error. Fortunately, `b-virtual-scroll` provides a slot for displaying such a button, and it handles the logic of hiding it during loading, errors, and so on. Clients don't need to implement additional logic; you just need to move your button to the appropriate slot, specifically the `renderNext` slot. ``` < b-virtual-scroll & @@ -220,34 +200,31 @@ Also, you can see the implemented traits or the parent component. Load more data ``` -### Как переинициализировать компонент? +Now, your button will be displayed only when there's more data to load, and it will automatically hide during data loading and in case of any errors. + +### How to Reinitialize the Component? -Часто возникают ситуации когда необходимо перерисовать все данные что были отрисованы с помощью `b-virtual-scroll`, например -включилась какая-то дополнительная фильтрация из-за которой отрисованные ранее данные в `b-virtual-scroll` стали неактуальны. +There are often situations where you need to redraw all the data that was rendered using `b-virtual-scroll`. For example, additional filtering may have been applied, making previously rendered data in `b-virtual-scroll` outdated. -В таком случае компонент предоставляет несколько путей повторной инициализации компонента, это позволит очистить состояние на "первозданное", -то есть удалить ранее отрисованные компоненты и скинуть состояние. После того как состояние сброшено компонент опять начнет свой жизненный цикл будто бы -он создан с "нуля". Рассмотрим ниже варианты сброса состояния. +In such cases, the component provides several ways to reinitialize it. This allows you to clear the state to its initial state, effectively removing previously rendered components and resetting the state. After the state is reset, the component will start its lifecycle as if it were created from scratch. Let's explore the options for resetting the state. -1. Обновление пропа `request`; +1. Updating the `request` prop. -2. Всплытие какого-либо события в `globalEmitter` из списка: - - `reset`; - - `reset.silence`; - - `reset.load`; - - `reset.load.silence`. +2. Triggering an event in the `globalEmitter` from the following list: + - `reset` + - `reset.silence` + - `reset.load` + - `reset.load.silence` -То есть компонент автоматически будет перезагружаться когда всплывает какое-либо из данных событий (стандартная логика `iData`). + This means the component will automatically reload when any of these events are triggered (standard `iData` logic). -3. Вызов метода `reload` или `initLoad`. +3. Calling the `reload` or `initLoad` method. -В каких случаях какой вариант использовать? +In which cases should you use each option? -В случае если у вас есть на странице фильтры и запрос за данными которые должны быть отрисованы с помощью `b-virtual-scroll` - `request` проп -будет как нельзя кстати, вы можете использовать в пропе `request` ссылку на текущее состояние фильтров и таким образом при изменении этого состояния -на странице будет произведена автоматическая переинициализация. +If you have filters on the page and a data request that should be rendered using `b-virtual-scroll`, the `request` prop is the most suitable option. You can set the `request` prop to reference the current filter state. This way, when the filter state changes on the page, the component will be automatically reinitialized. -Рассмотрим на примере: +Let's consider an example: __p-page.ts__ ```typescript @@ -271,12 +248,127 @@ __p-page.ss__ . ``` -Таким образом при изменении поля `filterUuid` на странице `pPage` `b-virtual-scroll` будет выполнять переинициализации и перезагружаться. +In this example, when the `filterUuid` field on the `pPage` changes, `b-virtual-scroll` will perform reinitialization and reload the data. + +If you need to update the component's state at a specific moment in time, regardless of the context, you can use the `reload` or `initLoad` methods. + +### Component State + +The `b-virtual-scroll` component is quite substantial and has its own internal state that complements the component's state. This internal state is reset when the component is reinitialized to its initial state and changes regularly during the component's lifecycle. The component's state contains a wealth of information useful for the client, such as the loaded data, the number of elements remaining outside the user's viewport, and more. + +To retrieve the component's state, you can use a special method called `getComponentState`: -В случае если же вам нужно обновить состояние компонента в какое-то момент времени без зависимости от контекста - можно использовать функции `reload` или `initLoad`. +__p-page.ts__ +```typescript +@component() +class pPage extends extends iDynamicPage { + protected override readonly $refs!: { + scroll: bVirtualScroll; + }; + + getScrollState(): VirtualScrollState { + return this.$refs.scroll.getComponentState(); + } +} +``` + +This method returns the current "internal" state of the component. ### Как использовать should-like функции? +### Обзор функций + +Компонент предоставляет несколько пропов-функций которые отвечают за необходимость выполнить то или иное действие. +У каждой из этих функций разное назначение, каждая из них вызывается в свой момент времени. Разберем детально каждую из этих функций и для чего они нужны: + +- `shouldPerformDataRequest` - Данная функция указывает на необходимость загрузить данные чанк данных. В случае если она вернет `true` будет выполнен запрос +данных. На вход функция принимает "внутренее" состояние компонента и должна вернуть `boolean` значение. Функция вызывается когда какой-либо компонент отрисованный +`b-virtual-scroll`ом который еще не входил в область видимости вошел в область видимости. + +> Важно отметить что клиенту в данной функции нет нужды проверять идет ли сейчас загрузка данных или нет, компонент `b-virtual-scroll` сам реализует данную проверку +и не позволит инициировать загрузку данных если процесс загрузки уже активен. + +Примером реализации данной функции может быть проверка на то, сколько еще попали во вьюпорт и если попала половина от отрисованных - можно начать загрузку. + +```typescript +const shouldPerformDataRequest = (state: VirtualScrollState): boolean => { + // Example: Request data if the remaining items till the end is less than or equal to 10 + return state.remainingItems <= 10; +}; +``` + +Реализация по умолчанию же проверяет было ли загруженно хоть что-то в последнем запросе и если да, то значит можно сделать еще запрос: + +```typescript +const shouldPerformDataRequest = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { + const isLastRequestNotEmpty = () => state.lastLoadedData.length > 0; + return isLastRequestNotEmpty(); +} +``` + +- `shouldStopRequestingData` - Данная функция указывает на необходимость завершить загрузку данных и указывает компоненту что лайф цикл со стороны загрузки данных завершен. +В случае если она вернет `true` компонент `b-virtual-scroll` не будет пытаться запрашивать данные более до тех пор, пока не будет выполнена переинициализация компонента что приведет +к обновлению лайф цикла. Функция вызывается после каждой успешной загрузки данных. + +Примером реализации данной функции может быть проверка на то, сколько данных загружено и сколько всего может вернуть пагинация по данному запросу: + +```typescript +const shouldStopRequestingData = (state: VirtualScrollState): boolean => { + // Example: Stop requesting data when the total number of items equals the current number of loaded items + return state.lastLoadedRawData?.total === state.data.length; +}; +``` + + +Реализация по умолчанию же проверяет было ли загруженно хоть что-то в последнем запросе и если да, то значит прекращать запросы еще рано: + +```typescript +const shouldPerformDataRequest = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { + const isLastRequestNotEmpty = () => state.lastLoadedData.length > 0; + return isLastRequestNotEmpty(); +} +``` + +- `shouldPerformDataRender` - Данная функция указывает на необходимость произвести отрисовку загруженных данных. +В случае если она вернет `true` компонент `b-virtual-scroll` вызовет функции отрисовки компонент и вставит их в DOM дерево. Функция вызывается в случае если есть загруженные +но не отрисованные данные. + +Примером реализации данной функции может быть проверка на то, сколько элементов осталось до конца контейнера с компонента: + +```typescript +const shouldPerformDataRender = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => state.remainingItems === 0 +``` + +По умолчанию реализация точно такая же как и в примере выше. + +### Best Practice + +Несколько советов который позволит максимально эффективно со стороны клиента реализовать загрузку и при этом доставлять удовольствие от просмотра +контента пользователю (а не заставлять его постоянно упираться в низ страницы) является таким: + +- Загружайте данные прилично заранее чем собираетесь отрисовывать - загрузка данных дело не быстрое, куда быстрее отрисовывать данные, поэтому рекомендуется начинать +загрузку данных сильно заранее, а отрисовку производить уже ближе к концу ленты. Таким образом пользователь будет получать максимально бесшовный скроллинг компонента. + +Например такой подход можно реализовать так: + +```typescript +const shouldPerformDataRequest = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { + // Start loading when half of the components were in the viewport + return state.remainingItems <= chunkSize / 2; +} +``` + +```typescript +const shouldPerformDataRender = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { + // Start rendering when only 2 components are left to the end + return state.remainingItems <= 2 +} +``` + +- Не делайте последнего запроса - разговор про функции `shouldPerformDataRequest` и `shouldStopRequestingData`, по умолчанию данные функции проверяют последний чанк данных, вернул +ли он что-нибудь, лучше этого избегать и заранее сообщать компоненту что все данные загруженны, это можно реализовать в случае если ваш сервер отдает сколько всего элементов будет +с данными параметрами фильтрации с помощью сравнения этого значения и кол-во всех данных что есть в `b-virtual-scroll` (пример выше мы рассматривали). + ### iItems и itemsFactory ### request и requestQuery From 7ea8669149794f02b7ed3ef60289bde36e4326d8 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sun, 3 Sep 2023 15:18:02 +0300 Subject: [PATCH 087/159] WIP --- .../base/b-virtual-scroll/README.md | 133 +++++++++--------- 1 file changed, 63 insertions(+), 70 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 8512f26dd6..c4ae9c1e11 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -274,100 +274,87 @@ class pPage extends extends iDynamicPage { This method returns the current "internal" state of the component. -### Как использовать should-like функции? +### How to Use "Should-Like" Functions? -### Обзор функций +### Overview of Functions -Компонент предоставляет несколько пропов-функций которые отвечают за необходимость выполнить то или иное действие. -У каждой из этих функций разное назначение, каждая из них вызывается в свой момент времени. Разберем детально каждую из этих функций и для чего они нужны: +The component provides several "should-like" props that determine whether to perform certain actions. Each of these functions serves a different purpose and is called at a specific moment in time. Let's take a detailed look at each of these functions and their purposes: -- `shouldPerformDataRequest` - Данная функция указывает на необходимость загрузить данные чанк данных. В случае если она вернет `true` будет выполнен запрос -данных. На вход функция принимает "внутренее" состояние компонента и должна вернуть `boolean` значение. Функция вызывается когда какой-либо компонент отрисованный -`b-virtual-scroll`ом который еще не входил в область видимости вошел в область видимости. +- `shouldPerformDataRequest`: This function indicates the need to load a chunk of data. If it returns `true`, a data request will be made. This function takes the "internal" component state as input and should return a boolean value. It is called when any component rendered by `b-virtual-scroll`, which has not yet entered the viewport, enters the viewport. -> Важно отметить что клиенту в данной функции нет нужды проверять идет ли сейчас загрузка данных или нет, компонент `b-virtual-scroll` сам реализует данную проверку -и не позволит инициировать загрузку данных если процесс загрузки уже активен. + > It's important to note that clients do not need to check whether data is currently being loaded or not; the `b-virtual-scroll` component handles this check itself and prevents data from being requested if a loading process is already active. -Примером реализации данной функции может быть проверка на то, сколько еще попали во вьюпорт и если попала половина от отрисованных - можно начать загрузку. + An example implementation of this function could be to check how many items are left in the viewport, and if half of the rendered items are within the viewport, start loading more: -```typescript -const shouldPerformDataRequest = (state: VirtualScrollState): boolean => { - // Example: Request data if the remaining items till the end is less than or equal to 10 - return state.remainingItems <= 10; -}; -``` + ```typescript + const shouldPerformDataRequest = (state: VirtualScrollState): boolean => { + // Example: Request data if the remaining items till the end is less than or equal to 10 + return state.remainingItems <= 10; + }; + ``` -Реализация по умолчанию же проверяет было ли загруженно хоть что-то в последнем запросе и если да, то значит можно сделать еще запрос: + The default implementation checks whether anything was loaded in the last request and, if so, allows another request: -```typescript -const shouldPerformDataRequest = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { - const isLastRequestNotEmpty = () => state.lastLoadedData.length > 0; - return isLastRequestNotEmpty(); -} -``` - -- `shouldStopRequestingData` - Данная функция указывает на необходимость завершить загрузку данных и указывает компоненту что лайф цикл со стороны загрузки данных завершен. -В случае если она вернет `true` компонент `b-virtual-scroll` не будет пытаться запрашивать данные более до тех пор, пока не будет выполнена переинициализация компонента что приведет -к обновлению лайф цикла. Функция вызывается после каждой успешной загрузки данных. + ```typescript + const shouldPerformDataRequest = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { + const isLastRequestNotEmpty = () => state.lastLoadedData.length > 0; + return isLastRequestNotEmpty(); + }; + ``` -Примером реализации данной функции может быть проверка на то, сколько данных загружено и сколько всего может вернуть пагинация по данному запросу: +- `shouldStopRequestingData`: This function indicates the need to stop requesting data and tells the component that the data loading lifecycle has completed. If it returns `true`, the `b-virtual-scroll` component will not attempt to request more data until the component is reinitialized, which leads to an update of the lifecycle. This function is called after every successful data load. -```typescript -const shouldStopRequestingData = (state: VirtualScrollState): boolean => { - // Example: Stop requesting data when the total number of items equals the current number of loaded items - return state.lastLoadedRawData?.total === state.data.length; -}; -``` + An example implementation of this function could be to check whether the number of loaded items equals the total number of items that can be returned by the pagination for the current query: + ```typescript + const shouldStopRequestingData = (state: VirtualScrollState): boolean => { + // Example: Stop requesting data when the total number of items equals the current number of loaded items + return state.lastLoadedRawData?.total === state.data.length; + }; + ``` -Реализация по умолчанию же проверяет было ли загруженно хоть что-то в последнем запросе и если да, то значит прекращать запросы еще рано: + The default implementation checks whether anything was loaded in the last request and, if so, allows requests to continue: -```typescript -const shouldPerformDataRequest = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { - const isLastRequestNotEmpty = () => state.lastLoadedData.length > 0; - return isLastRequestNotEmpty(); -} -``` + ```typescript + const shouldPerformDataRequest = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { + const isLastRequestNotEmpty = () => state.lastLoadedData.length > 0; + return isLastRequestNotEmpty(); + }; + ``` -- `shouldPerformDataRender` - Данная функция указывает на необходимость произвести отрисовку загруженных данных. -В случае если она вернет `true` компонент `b-virtual-scroll` вызовет функции отрисовки компонент и вставит их в DOM дерево. Функция вызывается в случае если есть загруженные -но не отрисованные данные. +- `shouldPerformDataRender`: This function indicates the need to render the loaded data. If it returns `true`, the `b-virtual-scroll` component will call the component rendering functions and insert them into the DOM tree. This function is called when there is loaded but unrendered data. -Примером реализации данной функции может быть проверка на то, сколько элементов осталось до конца контейнера с компонента: + An example implementation of this function could be to check how many items are left before reaching the end of the component's container: -```typescript -const shouldPerformDataRender = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => state.remainingItems === 0 -``` + ```typescript + const shouldPerformDataRender = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => state.remainingItems === 0; + ``` -По умолчанию реализация точно такая же как и в примере выше. + The default implementation is similar to the example above. -### Best Practice +### Best Practices -Несколько советов который позволит максимально эффективно со стороны клиента реализовать загрузку и при этом доставлять удовольствие от просмотра -контента пользователю (а не заставлять его постоянно упираться в низ страницы) является таким: +Here are some tips for efficiently implementing data loading on the client side while providing a seamless user experience: -- Загружайте данные прилично заранее чем собираетесь отрисовывать - загрузка данных дело не быстрое, куда быстрее отрисовывать данные, поэтому рекомендуется начинать -загрузку данных сильно заранее, а отрисовку производить уже ближе к концу ленты. Таким образом пользователь будет получать максимально бесшовный скроллинг компонента. +- Load data well in advance before you intend to render it. Data loading can be slow, but rendering data is much faster. Therefore, it is recommended to start data loading significantly in advance and perform rendering closer to the end of the scroll. This way, users will experience a smoother scrolling of the component. -Например такой подход можно реализовать так: + For example, you can implement this approach as follows: -```typescript -const shouldPerformDataRequest = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { - // Start loading when half of the components were in the viewport - return state.remainingItems <= chunkSize / 2; -} -``` + ```typescript + const shouldPerformDataRequest = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { + // Start loading when half of the components are in the viewport + return state.remainingItems <= chunkSize / 2; + } + ``` -```typescript -const shouldPerformDataRender = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { - // Start rendering when only 2 components are left to the end - return state.remainingItems <= 2 -} -``` + ```typescript + const shouldPerformDataRender = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { + // Start rendering when only 2 components are left to the end + return state.remainingItems <= 2; + } + ``` -- Не делайте последнего запроса - разговор про функции `shouldPerformDataRequest` и `shouldStopRequestingData`, по умолчанию данные функции проверяют последний чанк данных, вернул -ли он что-нибудь, лучше этого избегать и заранее сообщать компоненту что все данные загруженны, это можно реализовать в случае если ваш сервер отдает сколько всего элементов будет -с данными параметрами фильтрации с помощью сравнения этого значения и кол-во всех данных что есть в `b-virtual-scroll` (пример выше мы рассматривали). +- Avoid making the last useless request: This pertains to the `shouldPerformDataRequest` and `shouldStopRequestingData` functions. By default, these functions check the last data chunk to see if it returned anything. It's better to avoid this and inform the component in advance that all data has been loaded. You can achieve this by comparing the value returned by your server, indicating the total number of items with the current number of items in `b-virtual-scroll`, as demonstrated in the example above. ### iItems и itemsFactory @@ -377,7 +364,13 @@ const shouldPerformDataRender = (state: VirtualScrollState, _ctx: bVirtualScroll ### Глобальные переопределения +### Часто возникающие вопросы + +Q: Можно ли использовать только `shouldPerformDataRequest` и не использовать `shouldStopRequestingData`? +A: +Q: Загрузка данны завершена, но компоненты не отрисовали, почему такое может быть? +A: ### How to implement on click rendering? From fc3798974f68d1661fcabd3425c34dcee646f889 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sun, 3 Sep 2023 18:17:07 +0300 Subject: [PATCH 088/159] :art: --- .../base/b-virtual-scroll/README.md | 420 +++++++++--------- 1 file changed, 215 insertions(+), 205 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index c4ae9c1e11..bf3fbc8f18 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -69,7 +69,7 @@ To achieve this, use the `requestQuery` prop in the `b-virtual-scroll` component < b-virtual-scroll & :dataProvider = 'Provider' | :request = {get: {count: 12}} | - :requestQuery = (state) => ({page: state.loadPage}) | + :requestQuery = (state) => ({get: {page: state.loadPage}}) | :chunkSize = 12 . ``` @@ -92,7 +92,7 @@ Rendering occurs after data is loaded. < b-virtual-scroll & :dataProvider = 'Provider' | :request = {get: {count: 12}} | - :requestQuery = (state) => ({page: state.loadPage}) | + :requestQuery = (state) => ({get: {page: state.loadPage}}) | :chunkSize = 12 | :item = 'b-dummy' | :itemKey = (el) => el.uuid | @@ -112,7 +112,7 @@ Let's add a `loader` slot to our component to provide a better user experience d < b-virtual-scroll & :dataProvider = 'Provider' | :request = {get: {count: 12}} | - :requestQuery = (state) => ({page: state.loadPage}) | + :requestQuery = (state) => ({get: {page: state.loadPage}}) | :chunkSize = 12 | :item = 'b-dummy' | :itemProps = (el) => ({name: el.name, type: el.type}) @@ -274,9 +274,32 @@ class pPage extends extends iDynamicPage { This method returns the current "internal" state of the component. +### Converting Data to the Required Format + +The `b-virtual-scroll` component expects data in a specific format: + +```typescript +interface VirtualScrollDb { + data: unknown[]; +} +``` + +The `data` array should contain the data items used to render the components. +The `dbConverter` prop allows you to convert data into a format suitable for `b-virtual-scroll` after data has been loaded. + +``` +< b-virtual-scroll & + ... + :dbConverter = (data) => ({data: data.nestedData.data}) +. + < template #loader + < .&__loader + Data loading in progress +``` + ### How to Use "Should-Like" Functions? -### Overview of Functions +#### Overview of Functions The component provides several "should-like" props that determine whether to perform certain actions. Each of these functions serves a different purpose and is called at a specific moment in time. Let's take a detailed look at each of these functions and their purposes: @@ -332,7 +355,7 @@ The component provides several "should-like" props that determine whether to per The default implementation is similar to the example above. -### Best Practices +#### Best Practices Here are some tips for efficiently implementing data loading on the client side while providing a seamless user experience: @@ -356,121 +379,206 @@ Here are some tips for efficiently implementing data loading on the client side - Avoid making the last useless request: This pertains to the `shouldPerformDataRequest` and `shouldStopRequestingData` functions. By default, these functions check the last data chunk to see if it returned anything. It's better to avoid this and inform the component in advance that all data has been loaded. You can achieve this by comparing the value returned by your server, indicating the total number of items with the current number of items in `b-virtual-scroll`, as demonstrated in the example above. -### iItems и itemsFactory +### `itemsFactory` -### request и requestQuery +`itemsFactory` is a prop that allows you to take control of component rendering. Suppose you want to render twice as many components for a single data slice. Achieving this using `iItems` props (`item`, `itemProps`, etc.) might not be possible. However, such situations may arise, and this prop is created to solve them. -### Использование слотов +Let's consider a scenario in which we need to add a date separator component before each component with a different date from the next one. To achieve this, we will create an implementation of `itemsFactory` in which: -### Глобальные переопределения +1. We will access the `b-virtual-scroll` state to retrieve the loaded data. +2. We will take the previous element to determine if their dates differ, indicating whether we need to insert a date separator. +3. We will assemble an array with an abstract representation of the components to be rendered and return it from `itemsFactory`. -### Часто возникающие вопросы +```typescript +const itemsFactory = (state, ctx) => { + const + lastLoadedData = state.lastLoadedData, + allData = state.data, + items = []; + + lastLoadedData.forEach((current, i) => { + const + // Retrieve the previous data element relative to the given + prev = allData[(allData.length - lastLoadedData.length + i) - 1], + // Retrieve the next data element relative to the given + next = allData[i + 1]; + + if (!prev || prev.date !== current.date) { + items.push({ + item: 'b-date-separator', + key: current.uuid + 'separator', + type: 'separator', + children: [], + props: { + date: current.date + } + }); + } -Q: Можно ли использовать только `shouldPerformDataRequest` и не использовать `shouldStopRequestingData`? -A: + items.push({ + item: 'b-main-item', + key: current.uuid, + type: 'item', + children: [], + props: { + data: current + } + }); + }); -Q: Загрузка данны завершена, но компоненты не отрисовали, почему такое может быть? -A: + return items; +} +``` -### How to implement on click rendering? +As you can see in the example above, we access the last chunk of loaded data and all component data to find the previous and next data elements relative to the current one. Then, we compare their dates, and if they are not equal, we add the `b-date-separator` component before adding the `b-main-item`. This way, we collect the components to be rendered in an array and return it from the `itemsFactory` function. -### How +### `request` and `requestQuery` -### Converting Data to the Required Format +To pass query parameters from the `b-virtual-scroll` component to the data provider, two props are specified: `request` and `requestQuery`. But why are there two of them, and what is the difference between them? Let's break it down: -The `b-virtual-scroll` component expects data in a specific format: +- `request` is a prop inherited from `iData`. When the value of this prop changes, it triggers the `initLoad` method. In the case of `b-virtual-scroll`, this is interpreted as a need to reset the component's state to its initial state and start a new lifecycle from scratch. In essence, `request` represents static request parameters for one lifecycle of the component. This prop is suitable for parameters that directly affect the need to invalidate the `b-virtual-scroll` state. -```typescript -interface VirtualScrollDb { - data: unknown[]; -} -``` +- `requestQuery` is a prop defined by `b-virtual-scroll`. One key difference from `request` is that this prop can be a function, and whatever is returned from this function will be set as query parameters. This prop is used to implement pagination. It takes the "internal" state of `b-virtual-scroll` as input and returns query parameters. Changing this prop does not lead to the reinitialization of the component. -The `data` array should contain the data items used to render the components. -The `dbConverter` prop allows you to convert data into a format suitable for `b-virtual-scroll` after data has been loaded. +The `request` prop and the result of calling the `requestQuery` function are merged together and then passed to the data provider as query parameters. -``` -< b-virtual-scroll & - ... - :dbConverter = (data) => ({data: data.nestedData.data}) -. - < template #loader - < .&__loader - Data loading in progress -``` +### Component Understanding -### Rendering Components +#### Lifecycle -``` -< b-virtual-scroll & - :dataProvider = 'Provider' | - :request = {get: {chunkSize: 12}} | - :requestQuery = (state) => ({page: state.loadPage}) | - :chunkSize = 12 | - :item = 'b-dummy' | - :itemProps = (data) => ({name: data.name, type: data.type}) -. - < template #loader - < .&__loader - Data loading in progress -``` +The component's lifecycle consists of several events and states. When the component is initialized and starts its initial data loading, it emits two events: `initLoadStart` and `dataLoadStart`. The `initLoadStart` event is a standard event emitted by every component and occurs each time the component's data is initially loaded. The `dataLoadStart` event is emitted for every data loading. -In this example: +1. `initLoadStart` - The initial data loading of the component has started. +2. `dataLoadStart` - The data loading of the component has started. -- The `b-virtual-scroll` component is used to render 12 items per one render cycle. -It interacts with the `Provider` data provider to fetch the data. The `request` prop is set to `{ get: { chunkSize: 12 } }`, specifying that each request should fetch 12 items. -- The `requestQuery` function computes additional request parameters based on the component state, specifically the `loadPage` property. These request parameters are merged with the `request` prop. -- The `b-virtual-scroll` component renders `b-dummy` components using the `item` prop. -Each `b-dummy` component receives the `name` and `type` props, which are derived from the `data` object for each item using the `itemProps` function. -- The component includes a `loader` slot that displays the message "Data loading in progress" while the data is being fetched. -- By default, the component stops loading data when it receives an empty response from the `dataProvider`, indicating that there are no more items to load. +After successful data loading, the following events are emitted: -### Rendering on click +1. `convertDataToDB` - The data conversion has been performed. +2. `initLoad` - The initial data loading of the component has completed. +3. `dataLoadSuccess` - The data loading of the component has completed. -In addition to the standard scroll-based loading, you can implement on-demand loading. +When the `convertDataToDB` event is emitted, the component's state is already updated with the `lastLoadedRawData` field. The `initLoad` and `dataLoadSuccess` events are emitted after updating the component's state, including `VirtualScrollState.data`, `VirtualScrollState.loadPage`, and some other fields. -To achieve this, you need to disable the observer module, allow component rendering, and use the special `initLoadNext` method. +After successful data loading, the component consults the `shouldStopRequestingData` method to determine whether it should stop loading further data. -``` -< b-virtual-scroll & - :disableObserver = true | - :shouldPerformDataRender = () => true | - ref = scroll -. -``` +Next, the component invokes the `renderGuard` to determine if the data can be rendered or not. If the `renderGuard` allows rendering, the following events are emitted: -```typescript -class pSomePage { - @watch('something') - onSomething() { - this.$refs.scroll.initLoadNext(); - } -} -``` +1. `renderStart` - The component rendering has started. +2. `renderEngineStart` - The component rendering using the rendering engine has started. +3. `renderEngineDone` - The component rendering using the rendering engine has completed. +4. `domInsertStart` - The DOM insertion has started. +5. `domInsertDone` - The DOM insertion has completed. This event is asynchronous as it uses RAF (Request Animation Frame) for DOM insertion. +6. `renderDone` - The component rendering has finished. + +Afterward, the component waits for user actions, specifically when the user sees any component on the page. The component then calls the + +`shouldPerformDataRequest` or `shouldPerformDataRender` functions on the client side, depending on the availability of data. This process repeats until all data has been loaded and rendered. -Additionally, for ease of implementation, when you need to load and render data on a button click, the `renderNext` slot is available. It will be displayed only when the component is not loading data, the last load did not result in an error, and the component's lifecycle is not completed. In combination with the `initLoadNext` method, this allows for easy implementation of lazy rendering on button click. +1. `lifecycleDone` - Occurs when all data has been loaded and rendered on the page. + +#### `renderGuard` and `loadDataOrPerformRender` + +The `b-virtual-scroll` component relies on the `renderGuard` and `loadDataOrPerformRender` functions to determine whether to render data, load data, or complete the component's lifecycle. +The `loadDataOrPerformRender` function is the entry point for the data loading and rendering cycle. +This function consults the `renderGuard`, which determines whether data can be rendered based on the data state and provides reasons for rejection only if it has not permitted the rendering. + +Understanding `renderGuard`: + +```mermaid +graph TB + A["renderGuard"] -->|Get chunk size and next data slice| B["Is the data slice length = 0?"] + B -- True --> C["Are requests stopped?"] + C -- True --> E["Return: result=false, reason=done"] + E --> X["Function ends"] + C -- False --> F["Return: result=false, reason=noData"] + B -- False --> G["Is the data slice smaller than chunk size?"] + G -- True --> H["Return: result=false, reason=notEnoughData"] + G -- False --> I["Is it initial render?"] + I -- True --> J["Return: result=true"] + I -- False --> K["Get client response from shouldPerformDataRender"] + K --> L["Return: result=clientResponse, reason=noPermission if clientResponse is false"] ``` -< b-virtual-scroll & - :disableObserver = true | - :shouldPerformDataRender = () => true | - ref = scroll -. - < template #renderNext - < .&__render-next @click = $refs.scroll.initLoadNext - Render next + +Understanding `loadDataOrPerformRender`: + +```mermaid +graph TB + A[loadDataOrPerformRender] -->|Get component state| B[Is the last request errored?] + B -- True --> X[return] + B -- False ---> C["renderGuard()"] + C -- If Render Guard Result is True --> D["performRender()"] + C -- If Render Guard Result is False --> E[Check the Render Guard Rejection Reason] + E -- reason=done --> F["onLifecycleDone()"] + E -- reason=noData --> G[isRequestsStopped?] + G -- False --> H["shouldPerformDataRequest()"] + H -- True --> I["initLoadNext()"] + E -- reason=notEnoughData --> J[isRequestsStopped?] + J -- True --> K["performRender() and onLifecycleDone()"] + J -- False --> L["shouldPerformDataRequest()"] + L -- True --> M["initLoadNext()"] + L -- False --> N[initial render?] + N -- True --> P["performRender()"] ``` -### Component Reload +#### Difference between ComponentItem with type `item` and `separator` + +The component allows rendering two types of components: + +- `item` - Main component (main content). +- `separator` - Other components, such as dividers or separators. + +There is no significant difference between them, except that they are treated differently in fields like `remainingItems` in the `VirtualScrollState`. As the name suggests, the `remainingItems` property only considers components with the `item` type, while `remainingChildren` considers components with both `item` and `separator` types. + +The distinction between `item` and `separator` types is mainly used for calculating certain properties based on the type of components present in the `VirtualScrollState`, such as the number of items till the end of the scroll. + +#### Overriding in Child Layers + +The main use case for overriding in child layers is to modify the default behavior of functions or methods. + +For example, it may be useful to override the logic of `shouldStopRequestingData` if you want to implement a default logic that takes into account the `total` field of the response when making a decision. + +There may also be situations where you need to modify the `renderGuard`. Currently, the component loads data until the number of items reaches the `chunkSize` and then renders them. By overriding the `renderGuard`, you can achieve partial rendering, where the component renders the available data regardless of whether it reaches the `chunkSize`. + +### Frequently Asked Questions + +- Can I use only `shouldPerformDataRequest` and not use `shouldStopRequestingData`? + + Hypothetically, you can. However, this may cause issues with the `done` slot and the `lifecycleDone` event; they will not work correctly. Therefore, it is strongly recommended to separate the logic into whether data should be loaded now (`shouldPerformDataRequest`) and whether data loading is completed (all data is loaded) (`shouldStopRequestingData`). + +- Can I set `chunkSize` to 10 if the request returns 89 items at a time? + + Yes, you can. `b-virtual-scroll` will render the data in chunks until it has rendered all of it. + +- Can I set `chunkSize` to 10 if the request returns 5 items at a time? + + Yes, you can. `b-virtual-scroll` will make requests (one at a time!) until the number of loaded items is greater than or equal to the value specified in `chunkSize`. + +- Suppose I want to load 1000 data items once and not make any more requests. How can I achieve this? + + 1. Set `chunkSize` to a suitable value, for example, 10, if you want 10 components to be rendered in one rendering cycle. + + 2. Set up `dataProvider` and request parameters. + + 3. Set the `shouldStopRequestingData` function to always return `true`. + + After these manipulations, `b-virtual-scroll` will load the data using `dataProvider` once and then render all the loaded data in chunks. + +- Data loading is complete, but the components are not rendering. Why could this happen? + + 1. Ensure that your data has a format suitable for `b-virtual-scroll`, specifically `{data: any[]}`. If your data has a different format, you can convert it using the `dbConverter` prop, which should return the transformed data, or convert the data in another location, such as in the provider's post-processor. + + 2. Make sure that your `should-*` functions are correctly defined, and their conditions are met. + + 3. Ensure that your component is included in the bundle in the `index.js` file of your page or component. + + 4. Verify that there are no errors in specifying the component's name in the `item` prop and no issues with props in `itemProps`. -To reload the `b-virtual-scroll` component, you have several options: +- The same components are being rendered multiple times in a row. Why could this happen? -1. Call `bVirtualScroll.initLoad()`. -2. Call `bVirtualScroll.reload()`. -3. Modify the `request` prop. -4. Trigger a global event of type `reset`. + 1. Ensure that you implement pagination using request parameters, and possibly the `requestQuery` prop. You might be loading the same data repeatedly because the request parameters are not changing. -In all of these cases, the component's lifecycle will be reset to its initial state, and the component will start rendering new data, discarding any previous data. + 2. If you have overridden `itemsFactory` and are managing the data rendering flow yourself, ensure that there are no errors in the data slice you are using for rendering. ## Slots @@ -663,10 +771,10 @@ const requestQuery = (state: VirtualScrollState): Dictionary => { A factory function used to generate an array of `ComponentItem` objects representing the components to be rendered. This function is called during the rendering process and receives the component state and context as arguments. It should return an array of `ComponentItem` objects. -The default implementation uses the `chunkSize` and `iItems` trait to slice the data and generate the components. +The default implementation uses the `chunkSize` and `iItems` trait props to slice the data and generate the components. However, you can override this function to implement a custom rendering strategy. -Here's an example of how you can use the itemsFactory property to generate ComponentItem objects based on the lastLoadedData property: +Here's an example of how you can use the itemsFactory property to generate ComponentItem objects based on the `lastLoadedData` property: ```typescript const itemsFactory = (state: VirtualScrollState): ComponentItem[] => { @@ -707,127 +815,29 @@ The `bVirtualScroll` class extends `iData` and includes additional properties re ### API -- Prop `renderGap` -> `shouldPerformDataRender`. -- Props with `option-like` -> `iItems` props. -- Method `getDataStateSnapshot` -> `getComponentState`. -- Method `reloadLast` -> `initLoadNext`. +- Prop `renderGap` deleted -> use `shouldPerformDataRender`; +- Deprecated props `option-like` deleted -> use `iItems` props; +- Method renamed `getDataStateSnapshot` -> `getComponentState`; +- Method `reloadLast` -> `initLoadNext`; - `VirtualItemEl` interface is removed. Now, the client receives a single data item in the `iItems` methods. To maintain logic with `current`, `prev`, `next`, you can use the following approach: -```typescript -function getProps(data: DataInterface, index: number): Dictionary { - const - state = this.$refs.scroll.getComponentState(); - - const - current = data, - prev = state.data[index - 1], - next = state.data[index + 1]; -} -``` + ```typescript + function getProps(data: DataInterface, index: number): Dictionary { + const + state = this.$refs.scroll.getComponentState(); + + const + current = data, + prev = state.data[index - 1], + next = state.data[index + 1]; + } + ``` - Interface `DataState` -> `VirtualScrollState`: - `DataState.currentPage` -> `VirtualScrollState.loadPage`; - `DataState.lastLoadedChunk.raw` -> `VirtualScrollState.lastLoadedRaw`; - etc. -## Deep dive into the component - -### Lifecycle - -The component's lifecycle consists of several events and states. When the component is initialized and starts its initial data loading, it emits two events: `initLoadStart` and `dataLoadStart`. The `initLoadStart` event is a standard event emitted by every component and occurs each time the component's data is initially loaded. The `dataLoadStart` event is emitted for every data loading. - -1. `initLoadStart` - The initial data loading of the component has started. -2. `dataLoadStart` - The data loading of the component has started. - -After successful data loading, the following events are emitted: - -1. `convertDataToDB` - The data conversion has been performed. -2. `initLoad` - The initial data loading of the component has completed. -3. `dataLoadSuccess` - The data loading of the component has completed. - -When the `convertDataToDB` event is emitted, the component's state is already updated with the `lastLoadedRawData` field. The `initLoad` and `dataLoadSuccess` events are emitted after updating the component's state, including `VirtualScrollState.data`, `VirtualScrollState.loadPage`, and some other fields. - -After successful data loading, the component consults the `shouldStopRequestingData` method to determine whether it should stop loading further data. - -Next, the component invokes the `renderGuard` to determine if the data can be rendered or not. If the `renderGuard` allows rendering, the following events are emitted: - -1. `renderStart` - The component rendering has started. -2. `renderEngineStart` - The component rendering using the rendering engine has started. -3. `renderEngineDone` - The component rendering using the rendering engine has completed. -4. `domInsertStart` - The DOM insertion has started. -5. `domInsertDone` - The DOM insertion has completed. This event is asynchronous as it uses RAF (Request Animation Frame) for DOM insertion. -6. `renderDone` - The component rendering has finished. - -Afterward, the component waits for user actions, specifically when the user sees any component on the page. The component then calls the - -`shouldPerformDataRequest` or `shouldPerformDataRender` functions on the client side, depending on the availability of data. This process repeats until all data has been loaded and rendered. - -1. `lifecycleDone` - Occurs when all data has been loaded and rendered on the page. - -### `renderGuard` and `loadDataOrPerformRender` - -The `b-virtual-scroll` component relies on the `renderGuard` and `loadDataOrPerformRender` functions to determine whether to render data, load data, or complete the component's lifecycle. - -The `loadDataOrPerformRender` function is the entry point for the data loading and rendering cycle. -This function consults the `renderGuard`, which determines whether data can be rendered based on the data state and provides reasons for rejection only if it has not permitted the rendering. - -Understanding `renderGuard`: - -```mermaid -graph TB - A["renderGuard"] -->|Get chunk size and next data slice| B["Is the data slice length = 0?"] - B -- True --> C["Are requests stopped?"] - C -- True --> E["Return: result=false, reason=done"] - E --> X["Function ends"] - C -- False --> F["Return: result=false, reason=noData"] - B -- False --> G["Is the data slice smaller than chunk size?"] - G -- True --> H["Return: result=false, reason=notEnoughData"] - G -- False --> I["Is it initial render?"] - I -- True --> J["Return: result=true"] - I -- False --> K["Get client response from shouldPerformDataRender"] - K --> L["Return: result=clientResponse, reason=noPermission if clientResponse is false"] -``` - -Understanding `loadDataOrPerformRender`: - -```mermaid -graph TB - A[loadDataOrPerformRender] -->|Get component state| B[Is the last request errored?] - B -- True --> X[return] - B -- False ---> C["renderGuard()"] - C -- If Render Guard Result is True --> D["performRender()"] - C -- If Render Guard Result is False --> E[Check the Render Guard Rejection Reason] - E -- reason=done --> F["onLifecycleDone()"] - E -- reason=noData --> G[isRequestsStopped?] - G -- False --> H["shouldPerformDataRequest()"] - H -- True --> I["initLoadNext()"] - E -- reason=notEnoughData --> J[isRequestsStopped?] - J -- True --> K["performRender() and onLifecycleDone()"] - J -- False --> L["shouldPerformDataRequest()"] - L -- True --> M["initLoadNext()"] - L -- False --> N[initial render?] - N -- True --> P["performRender()"] -``` - -### Difference between ComponentItem with type `item` and `separator` - -The component allows rendering two types of components: - -- `item` - Main component (main content). -- `separator` - Other components, such as dividers or separators. - -There is no significant difference between them, except that they are treated differently in fields like `remainingItems` in the `VirtualScrollState`. As the name suggests, the `remainingItems` property only considers components with the `item` type, while `remainingChildren` considers components with both `item` and `separator` types. - -The distinction between `item` and `separator` types is mainly used for calculating certain properties based on the type of components present in the `VirtualScrollState`, such as the number of items till the end of the scroll. - -### Overriding in Child Layers - -The main use case for overriding in child layers is to modify the default behavior of functions or methods. - -For example, it may be useful to override the logic of `shouldStopRequestingData` if you want to implement a default logic that takes into account the `total` field of the response when making a decision. - -There may also be situations where you need to modify the `renderGuard`. Currently, the component loads data until the number of items reaches the `chunkSize` and then renders them. By overriding the `renderGuard`, you can achieve partial rendering, where the component renders the available data regardless of whether it reaches the `chunkSize`. - ## What's Next The component currently lacks some features that may improve its functionality and make it more suitable for different scenarios. From 9575b1c2e62a644017ad292485ddf0c41a7d8ae5 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 4 Sep 2023 16:02:34 +0300 Subject: [PATCH 089/159] :art: --- .../base/b-virtual-scroll/README.md | 131 +++++++++++++++++- .../b-virtual-scroll/interface/component.ts | 20 ++- src/components/base/b-virtual-scroll/props.ts | 17 ++- 3 files changed, 161 insertions(+), 7 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index bf3fbc8f18..4f7bf40295 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -297,6 +297,13 @@ The `dbConverter` prop allows you to convert data into a format suitable for `b- Data loading in progress ``` +### Sliders or Multi-Column Content + +Sometimes, there is a need to render a large amount of data not in a typical vertical strip where one item follows another, but, for example, in a strip consisting of multiple columns or in a slider. + +All of these can be implemented using HTML/CSS layout and providing CSS classes in `b-virtual-scroll`. +There is no need to specify any additional props for `b-virtual-scroll`. For `b-virtual-scroll`, the content layout doesn't matter. + ### How to Use "Should-Like" Functions? #### Overview of Functions @@ -379,7 +386,7 @@ Here are some tips for efficiently implementing data loading on the client side - Avoid making the last useless request: This pertains to the `shouldPerformDataRequest` and `shouldStopRequestingData` functions. By default, these functions check the last data chunk to see if it returned anything. It's better to avoid this and inform the component in advance that all data has been loaded. You can achieve this by comparing the value returned by your server, indicating the total number of items with the current number of items in `b-virtual-scroll`, as demonstrated in the example above. -### `itemsFactory` +### Controlling the Rendering Flow with `itemsFactory` `itemsFactory` is a prop that allows you to take control of component rendering. Suppose you want to render twice as many components for a single data slice. Achieving this using `iItems` props (`item`, `itemProps`, etc.) might not be possible. However, such situations may arise, and this prop is created to solve them. @@ -432,6 +439,108 @@ const itemsFactory = (state, ctx) => { As you can see in the example above, we access the last chunk of loaded data and all component data to find the previous and next data elements relative to the current one. Then, we compare their dates, and if they are not equal, we add the `b-date-separator` component before adding the `b-main-item`. This way, we collect the components to be rendered in an array and return it from the `itemsFactory` function. +### `itemsProcessors` and Global Component Processing + +This prop is a middleware function that is called after `b-virtual-scroll` has compiled the abstract representation of components and before it passes this representation to the rendering engine. Each function in the chain receives the result of the previous function, with the first function in the chain receiving the result of the `itemsFactory` call. The function should return an abstract representation of components that conforms to the `ComponentItem[]` interface. + +Here is an example to illustrate when `itemsProcessors` is called: + +-> itemsFactory -> **itemsProcessors** -> render components via render engine -> insert components into the DOM tree + +With this prop, you can implement various scenarios, such as changing one component to another, adding components, prop migrations, and more. For some scenarios, you can also use global overrides if you need to implement some processing for all `b-virtual-scroll` instances in your application. To add a global processor, you can override the `itemsProcessors` constant located in `base/b-virtual-scroll/const.ts` within your codebase and add a function to it. + +Here's an example scenario where we need to change the name of one component to another: + +__@v4fire/client/components/base/b-virtual-scroll/const.ts__ +```typescript +export const itemsProcessors: ItemsProcessors = {}; +``` + +__your-project/components/base/b-virtual-scroll/const.ts__ +```typescript +import { itemsProcessors } from '@v4fire/client/components/base/b-virtual-scroll/const.ts' + +export const itemsProcessors: ItemsProcessors = { + ...itemsProcessors, + + migrateCardComponent: (items: ComponentItem[]) => { + return items.map((item) => { + if (item.item === 'b-card') { + console.warn('Deprecation: b-card is deprecated.'); + + return { + ...item, + props: convertProps(item.props), + item: 'b-mega-card' + }; + } + + return item; + }); + } +}; +``` + +> It's important to note that `itemsProcessors` functions cannot be asynchronous. + +Let's also look at another common scenario: + +**Task**: Add advertising components after certain components throughout the entire application. + +**Solution**: Instead of manually defining the `itemsFactory` function in multiple places to call a pre-prepared function, you can: + +1. Establish an agreement with clients to mark the components before or after which advertising should be displayed using meta information of the component's abstract representation (`ComponentItem`), which will be passed from the client to the component via the `itemMeta` prop. + + ``` + < b-virtual-scroll & + // ... + :itemMeta = (data) => ({ads: data.component === 'b-card' ? 'after' : false}) + . + ``` + +2. Implement a global `itemsProcessor` that will add advertisements based on the meta-information. + + ```typescript + import { itemsProcessors } from '@v4fire/client/components/base/b-virtual-scroll/const.ts' + + export const itemsProcessors: ItemsProcessors = { + ...itemsProcessors, + + addAds: (items: ComponentItem[]) => { + const newItems: ComponentItem[] = []; + + const adsComponent = { + item: 'b-ads', + key: current.uuid + 'ads', + type: 'item', + children: [], + props: { + // ... + } + } + + return items.map((item) => { + const itemsToPush = []; + itemsToPush.push(item); + + if (item.meta.ads === 'after') { + itemsToPush.push(adsComponent); + } + + if (item.meta.ads === 'before') { + itemsToPush.unshift(adsComponent); + } + + newItems.push(...itemsToPush); + }); + + return newItems; + } + }; + ``` + +After these steps, a neighboring advertising component will be added to all components with the appropriate `meta.ads` value. + ### `request` and `requestQuery` To pass query parameters from the `b-virtual-scroll` component to the data provider, two props are specified: `request` and `requestQuery`. But why are there two of them, and what is the difference between them? Let's break it down: @@ -542,6 +651,17 @@ There may also be situations where you need to modify the `renderGuard`. Current ### Frequently Asked Questions +- How to assign a class to components rendered within `b-virtual-scroll`? + + To achieve this, you need to include the `class` field in the props of the component that should be rendered. You can do this by returning it from the `itemProps` function, like this: + + ```typescript + const itemProps = (this: bMyComponent) => ({ + // ... + class: this.provide.classes({'virtual-scroll-item': true}) + }) + ``` + - Can I use only `shouldPerformDataRequest` and not use `shouldStopRequestingData`? Hypothetically, you can. However, this may cause issues with the `done` slot and the `lifecycleDone` event; they will not work correctly. Therefore, it is strongly recommended to separate the logic into whether data should be loaded now (`shouldPerformDataRequest`) and whether data loading is completed (all data is loaded) (`shouldStopRequestingData`). @@ -797,6 +917,15 @@ const itemsFactory = (state: VirtualScrollState): ComponentItem[] => { }; ``` +#### `itemsProcessors` + +- Type: `Function | Record | Function[]` +- Default: `undefined` + +This prop is a middleware function that is called after `b-virtual-scroll` has compiled the abstract representation of components and before it passes this representation to the rendering engine. + +This function can be useful in cases where you need to implement some processing of the abstract representation of components, such as mutating props or adding additional components. + #### `tombstonesSize` - Type: `number` diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts index c563da77e3..82169b9cb1 100644 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -192,10 +192,24 @@ export interface ComponentItem { children?: VNodeChildren; /** - * Meta information for a component that will not be used during rendering, - * but will be available for reading/changing in `itemsProcessors`. + * {@link ComponentItemMeta} */ - meta?: unknown; + meta?: ComponentItemMeta; +} + +/** + * Meta information for a component that will not be used during rendering, + * but will be available for reading/changing in `itemsProcessors`. + */ +export interface ComponentItemMeta extends Dictionary { + /** + * A conditionally reserved property that contains the data based + * on which this abstract representation of the component was created. + * + * If `iItems` props are used to create representations, `b-virtual-scroll` will automatically add + * this property to the `meta` parameters. + */ + readonly _data?: unknown; } /** diff --git a/src/components/base/b-virtual-scroll/props.ts b/src/components/base/b-virtual-scroll/props.ts index 5cbd954228..cf2d8576c0 100644 --- a/src/components/base/b-virtual-scroll/props.ts +++ b/src/components/base/b-virtual-scroll/props.ts @@ -20,7 +20,8 @@ import type { ComponentItemFactory, ComponentItemType, ComponentItem, - ItemsProcessors + ItemsProcessors, + ComponentItemMeta } from 'components/base/b-virtual-scroll/interface'; @@ -63,9 +64,16 @@ export default abstract class iVirtualScrollProps extends iData { * but will be available for reading/changing in `itemsProcessors`. * * If a function is provided, it will be called; otherwise, the value will be preserved "as is". + * + * @example + * ```typescript + * const itemMeta = (data) => ({ + * componentData: data + * }) + * ``` */ @prop() - readonly itemMeta?: CreateFromItemFn | unknown; + readonly itemMeta?: CreateFromItemFn; /** * Specifies the number of times the `tombstone` component will be rendered. @@ -118,7 +126,10 @@ export default abstract class iVirtualScrollProps extends iData { item: Object.isFunction(ctx.item) ? ctx.item(data, i) : ctx.item, type: Object.isFunction(ctx.itemType) ? ctx.itemType(data, i) : ctx.itemType, - meta: Object.isFunction(ctx.itemMeta) ? ctx.itemMeta(data, i) : ctx.itemMeta, + meta: { + _data: data, + ...Object.isFunction(ctx.itemMeta) ? ctx.itemMeta(data, i) : ctx.itemMeta + }, props: Object.isFunction(ctx.itemProps) ? ctx.itemProps(data, i, { From dd27f88d168ffd573a2bd692df3df6023b5adf8d Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 4 Sep 2023 17:13:29 +0300 Subject: [PATCH 090/159] Add doctoc --- package.json | 1 + .../base/b-virtual-scroll/README.md | 50 ++ yarn.lock | 553 +++++++++++++++++- 3 files changed, 599 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index ad8eb1f175..218619b367 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "cssnano": "5.0.17", "del": "6.0.0", "delay": "5.0.0", + "doctoc": "2.2.1", "extract-loader": "5.1.0", "fast-glob": "3.2.12", "favicons": "7.1.0", diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 4f7bf40295..b57ccf6a8e 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -1,3 +1,53 @@ + + +**Table of Contents** + +- [components/base/b-virtual-scroll](#componentsbaseb-virtual-scroll) + - [Synopsis](#synopsis) + - [Modifiers](#modifiers) + - [Events](#events) + - [Usage](#usage) + - [How to Implement Simple Rendering?](#how-to-implement-simple-rendering) + - [How to Implement Component Rendering on Click Instead of Scroll?](#how-to-implement-component-rendering-on-click-instead-of-scroll) + - [How to Reinitialize the Component?](#how-to-reinitialize-the-component) + - [Component State](#component-state) + - [Converting Data to the Required Format](#converting-data-to-the-required-format) + - [Sliders or Multi-Column Content](#sliders-or-multi-column-content) + - [How to Use "Should-Like" Functions?](#how-to-use-should-like-functions) + - [Overview of Functions](#overview-of-functions) + - [Best Practices](#best-practices) + - [Controlling the Rendering Flow with `itemsFactory`](#controlling-the-rendering-flow-with-itemsfactory) + - [`itemsProcessors` and Global Component Processing](#itemsprocessors-and-global-component-processing) + - [`request` and `requestQuery`](#request-and-requestquery) + - [Component Understanding](#component-understanding) + - [Lifecycle](#lifecycle) + - [`renderGuard` and `loadDataOrPerformRender`](#renderguard-and-loaddataorperformrender) + - [Difference between ComponentItem with type `item` and `separator`](#difference-between-componentitem-with-type-item-and-separator) + - [Overriding in Child Layers](#overriding-in-child-layers) + - [Frequently Asked Questions](#frequently-asked-questions) + - [Slots](#slots) + - [API](#api) + - [Props](#props) + - [`shouldPerformDataRender`](#shouldperformdatarender) + - [`shouldPerformDataRequest`](#shouldperformdatarequest) + - [`shouldStopRequestingData`](#shouldstoprequestingdata) + - [`chunkSize`](#chunksize) + - [`requestQuery`](#requestquery) + - [`itemsFactory`](#itemsfactory) + - [`itemsProcessors`](#itemsprocessors) + - [`tombstonesSize`](#tombstonessize) + - [Other Properties](#other-properties) + - [Migration from `b-virtual-scroll` version 3.x.x](#migration-from-b-virtual-scroll-version-3xx) + - [API](#api-1) + - [What's Next](#whats-next) + - [Streaming Data Rendering](#streaming-data-rendering) + - [Alternative Approach to Component Rendering](#alternative-approach-to-component-rendering) + - [Partial Rendering (can be achieved easily through `renderGuard`)](#partial-rendering-can-be-achieved-easily-through-renderguard) + - [Updating Nodes in the DOM Tree (describe implementation challenges, component allows inserting different components)](#updating-nodes-in-the-dom-tree-describe-implementation-challenges-component-allows-inserting-different-components) + - [Integration with RTX](#integration-with-rtx) + + + # components/base/b-virtual-scroll The `b-virtual-scroll` component is designed for rendering a large array of various data. diff --git a/yarn.lock b/yarn.lock index ac494bb526..d03d1b184c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2600,6 +2600,30 @@ __metadata: languageName: node linkType: hard +"@textlint/ast-node-types@npm:^12.6.1": + version: 12.6.1 + resolution: "@textlint/ast-node-types@npm:12.6.1" + checksum: be3bed9151b02ffdc53dda5af22742cdac49b3b8e844ba40b75622ac03335d5dedbbc30eb8c48be33baf64753d98af4293bfd3b2ca8e5d65351a0faf4dc89f9d + languageName: node + linkType: hard + +"@textlint/markdown-to-ast@npm:^12.1.1": + version: 12.6.1 + resolution: "@textlint/markdown-to-ast@npm:12.6.1" + dependencies: + "@textlint/ast-node-types": ^12.6.1 + debug: ^4.3.4 + mdast-util-gfm-autolink-literal: ^0.1.3 + remark-footnotes: ^3.0.0 + remark-frontmatter: ^3.0.0 + remark-gfm: ^1.0.0 + remark-parse: ^9.0.0 + traverse: ^0.6.7 + unified: ^9.2.2 + checksum: a4d919ed93074ad5d48c9556b27bede3d4f28e811842716b158d1851c68247e422bcaf833f23b7d74a3d8cc82c3f1336fede32d7b3fb3d83f9c792bcc2dab54c + languageName: node + linkType: hard + "@tootallnate/once@npm:1": version: 1.1.2 resolution: "@tootallnate/once@npm:1.1.2" @@ -2846,6 +2870,15 @@ __metadata: languageName: node linkType: hard +"@types/mdast@npm:^3.0.0": + version: 3.0.12 + resolution: "@types/mdast@npm:3.0.12" + dependencies: + "@types/unist": ^2 + checksum: 83adb8679b9d139f69f63554d120af921e9f1289e9903a2c99e0554a327c8524a6c0beccdc0721e4fdbccc606e81964fecb0d390d53df0f74360938e22f1a469 + languageName: node + linkType: hard + "@types/minimatch@npm:*, @types/minimatch@npm:^5.1.2": version: 5.1.2 resolution: "@types/minimatch@npm:5.1.2" @@ -2939,6 +2972,13 @@ __metadata: languageName: node linkType: hard +"@types/unist@npm:^2, @types/unist@npm:^2.0.0, @types/unist@npm:^2.0.2": + version: 2.0.8 + resolution: "@types/unist@npm:2.0.8" + checksum: f4852d10a6752dc70df363917ef74453e5d2fd42824c0f6d09d19d530618e1402193977b1207366af4415aaec81d4e262c64d00345402020c4ca179216e553c7 + languageName: node + linkType: hard + "@types/vinyl@npm:^2.0.4": version: 2.0.7 resolution: "@types/vinyl@npm:2.0.7" @@ -3230,6 +3270,7 @@ __metadata: cssnano: 5.0.17 del: 6.0.0 delay: 5.0.0 + doctoc: 2.2.1 dpdm: 3.10.0 escaper: 3.0.6 eventemitter2: 6.4.5 @@ -3339,6 +3380,8 @@ __metadata: optional: true delay: optional: true + doctoc: + optional: true extract-loader: optional: true fast-glob: @@ -4186,6 +4229,15 @@ __metadata: languageName: node linkType: hard +"anchor-markdown-header@npm:^0.6.0": + version: 0.6.0 + resolution: "anchor-markdown-header@npm:0.6.0" + dependencies: + emoji-regex: ~10.1.0 + checksum: 2fe51a4327d07d93fd7c2b4b5dc077598a29300217f876485ce91747e80823a58cbe34f80396f36b8abac24d1cbe080c27fd730dcf976200630f8ac754275606 + languageName: node + linkType: hard + "ansi-colors@npm:^1.0.1": version: 1.1.0 resolution: "ansi-colors@npm:1.1.0" @@ -5492,6 +5544,13 @@ __metadata: languageName: node linkType: hard +"bail@npm:^1.0.0": + version: 1.0.5 + resolution: "bail@npm:1.0.5" + checksum: 6c334940d7eaa4e656a12fb12407b6555649b6deb6df04270fa806e0da82684ebe4a4e47815b271c794b40f8d6fa286e0c248b14ddbabb324a917fab09b7301a + languageName: node + linkType: hard + "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -6100,6 +6159,13 @@ __metadata: languageName: node linkType: hard +"ccount@npm:^1.0.0": + version: 1.1.0 + resolution: "ccount@npm:1.1.0" + checksum: b335a79d0aa4308919cf7507babcfa04ac63d389ebed49dbf26990d4607c8a4713cde93cc83e707d84571ddfe1e7615dad248be9bc422ae4c188210f71b08b78 + languageName: node + linkType: hard + "center-align@npm:^0.1.1": version: 0.1.3 resolution: "center-align@npm:0.1.3" @@ -6161,6 +6227,27 @@ __metadata: languageName: node linkType: hard +"character-entities-legacy@npm:^1.0.0": + version: 1.1.4 + resolution: "character-entities-legacy@npm:1.1.4" + checksum: fe03a82c154414da3a0c8ab3188e4237ec68006cbcd681cf23c7cfb9502a0e76cd30ab69a2e50857ca10d984d57de3b307680fff5328ccd427f400e559c3a811 + languageName: node + linkType: hard + +"character-entities@npm:^1.0.0": + version: 1.2.4 + resolution: "character-entities@npm:1.2.4" + checksum: e1545716571ead57beac008433c1ff69517cd8ca5b336889321c5b8ff4a99c29b65589a701e9c086cda8a5e346a67295e2684f6c7ea96819fe85cbf49bf8686d + languageName: node + linkType: hard + +"character-reference-invalid@npm:^1.0.0": + version: 1.1.4 + resolution: "character-reference-invalid@npm:1.1.4" + checksum: 20274574c70e05e2f81135f3b93285536bc8ff70f37f0809b0d17791a832838f1e49938382899ed4cb444e5bbd4314ca1415231344ba29f4222ce2ccf24fea0b + languageName: node + linkType: hard + "charcodes@npm:^0.2.0": version: 0.2.0 resolution: "charcodes@npm:0.2.0" @@ -7333,7 +7420,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:*, debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": +"debug@npm:*, debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -7744,6 +7831,22 @@ __metadata: languageName: node linkType: hard +"doctoc@npm:2.2.1": + version: 2.2.1 + resolution: "doctoc@npm:2.2.1" + dependencies: + "@textlint/markdown-to-ast": ^12.1.1 + anchor-markdown-header: ^0.6.0 + htmlparser2: ^7.2.0 + minimist: ^1.2.6 + underscore: ^1.13.2 + update-section: ^0.3.3 + bin: + doctoc: doctoc.js + checksum: 0643c5170fa8c5ec53bde585090c4c140f69eb498fea7d4d4a6a15455bff8b1d04632dcaec8909d2b3719a5f54760947f274aec99f4322fe098bce0c7fb30a0d + languageName: node + linkType: hard + "doctrine@npm:^2.1.0": version: 2.1.0 resolution: "doctrine@npm:2.1.0" @@ -7840,7 +7943,7 @@ __metadata: languageName: node linkType: hard -"domhandler@npm:^4.2.0, domhandler@npm:^4.3.1": +"domhandler@npm:^4.2.0, domhandler@npm:^4.2.2, domhandler@npm:^4.3.1": version: 4.3.1 resolution: "domhandler@npm:4.3.1" dependencies: @@ -8075,6 +8178,13 @@ __metadata: languageName: node linkType: hard +"emoji-regex@npm:~10.1.0": + version: 10.1.0 + resolution: "emoji-regex@npm:10.1.0" + checksum: 5bc780fc4d75f89369155a87c55f7e83a0bf72bcccda7df7f2c570cde4738d8b17d112d12afdadfec16647d1faef6501307b4304f81d35c823a938fe6547df0f + languageName: node + linkType: hard + "emojis-list@npm:^3.0.0": version: 3.0.0 resolution: "emojis-list@npm:3.0.0" @@ -8124,6 +8234,13 @@ __metadata: languageName: node linkType: hard +"entities@npm:^3.0.1": + version: 3.0.1 + resolution: "entities@npm:3.0.1" + checksum: aaf7f12033f0939be91f5161593f853f2da55866db55ccbf72f45430b8977e2b79dbd58c53d0fdd2d00bd7d313b75b0968d09f038df88e308aa97e39f9456572 + languageName: node + linkType: hard + "entities@npm:^4.4.0": version: 4.5.0 resolution: "entities@npm:4.5.0" @@ -9081,6 +9198,15 @@ __metadata: languageName: node linkType: hard +"fault@npm:^1.0.0": + version: 1.0.4 + resolution: "fault@npm:1.0.4" + dependencies: + format: ^0.2.0 + checksum: 5ac610d8b09424e0f2fa8cf913064372f2ee7140a203a79957f73ed557c0e79b1a3d096064d7f40bde8132a69204c1fe25ec23634c05c6da2da2039cff26c4e7 + languageName: node + linkType: hard + "favicons@npm:7.1.0": version: 7.1.0 resolution: "favicons@npm:7.1.0" @@ -9546,6 +9672,13 @@ __metadata: languageName: node linkType: hard +"format@npm:^0.2.0": + version: 0.2.2 + resolution: "format@npm:0.2.2" + checksum: 646a60e1336250d802509cf24fb801e43bd4a70a07510c816fa133aa42cdbc9c21e66e9cc0801bb183c5b031c9d68be62e7fbb6877756e52357850f92aa28799 + languageName: node + linkType: hard + "fraction.js@npm:^4.1.2": version: 4.2.0 resolution: "fraction.js@npm:4.2.0" @@ -10966,6 +11099,18 @@ __metadata: languageName: node linkType: hard +"htmlparser2@npm:^7.2.0": + version: 7.2.0 + resolution: "htmlparser2@npm:7.2.0" + dependencies: + domelementtype: ^2.0.1 + domhandler: ^4.2.2 + domutils: ^2.8.0 + entities: ^3.0.1 + checksum: 96563d9965729cfcb3f5f19c26d013c6831b4cb38d79d8c185e9cd669ea6a9ffe8fb9ccc74d29a068c9078aa0e2767053ed6b19aa32723c41550340d0094bea0 + languageName: node + linkType: hard + "http-cache-semantics@npm:3.8.1": version: 3.8.1 resolution: "http-cache-semantics@npm:3.8.1" @@ -11464,6 +11609,23 @@ __metadata: languageName: node linkType: hard +"is-alphabetical@npm:^1.0.0": + version: 1.0.4 + resolution: "is-alphabetical@npm:1.0.4" + checksum: 6508cce44fd348f06705d377b260974f4ce68c74000e7da4045f0d919e568226dc3ce9685c5a2af272195384df6930f748ce9213fc9f399b5d31b362c66312cb + languageName: node + linkType: hard + +"is-alphanumerical@npm:^1.0.0": + version: 1.0.4 + resolution: "is-alphanumerical@npm:1.0.4" + dependencies: + is-alphabetical: ^1.0.0 + is-decimal: ^1.0.0 + checksum: e2e491acc16fcf5b363f7c726f666a9538dba0a043665740feb45bba1652457a73441e7c5179c6768a638ed396db3437e9905f403644ec7c468fb41f4813d03f + languageName: node + linkType: hard + "is-array-buffer@npm:^3.0.1, is-array-buffer@npm:^3.0.2": version: 3.0.2 resolution: "is-array-buffer@npm:3.0.2" @@ -11533,6 +11695,13 @@ __metadata: languageName: node linkType: hard +"is-buffer@npm:^2.0.0": + version: 2.0.5 + resolution: "is-buffer@npm:2.0.5" + checksum: 764c9ad8b523a9f5a32af29bdf772b08eb48c04d2ad0a7240916ac2688c983bf5f8504bf25b35e66240edeb9d9085461f9b5dae1f3d2861c6b06a65fe983de42 + languageName: node + linkType: hard + "is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.2.7": version: 1.2.7 resolution: "is-callable@npm:1.2.7" @@ -11585,6 +11754,13 @@ __metadata: languageName: node linkType: hard +"is-decimal@npm:^1.0.0": + version: 1.0.4 + resolution: "is-decimal@npm:1.0.4" + checksum: ed483a387517856dc395c68403a10201fddcc1b63dc56513fbe2fe86ab38766120090ecdbfed89223d84ca8b1cd28b0641b93cb6597b6e8f4c097a7c24e3fb96 + languageName: node + linkType: hard + "is-descriptor@npm:^0.1.0": version: 0.1.6 resolution: "is-descriptor@npm:0.1.6" @@ -11744,6 +11920,13 @@ __metadata: languageName: node linkType: hard +"is-hexadecimal@npm:^1.0.0": + version: 1.0.4 + resolution: "is-hexadecimal@npm:1.0.4" + checksum: a452e047587b6069332d83130f54d30da4faf2f2ebaa2ce6d073c27b5703d030d58ed9e0b729c8e4e5b52c6f1dab26781bb77b7bc6c7805f14f320e328ff8cd5 + languageName: node + linkType: hard + "is-inside-container@npm:^1.0.0": version: 1.0.0 resolution: "is-inside-container@npm:1.0.0" @@ -11873,6 +12056,13 @@ __metadata: languageName: node linkType: hard +"is-plain-obj@npm:^2.0.0": + version: 2.1.0 + resolution: "is-plain-obj@npm:2.1.0" + checksum: cec9100678b0a9fe0248a81743041ed990c2d4c99f893d935545cfbc42876cbe86d207f3b895700c690ad2fa520e568c44afc1605044b535a7820c1d40e38daa + languageName: node + linkType: hard + "is-plain-object@npm:^2.0.1, is-plain-object@npm:^2.0.3, is-plain-object@npm:^2.0.4": version: 2.0.4 resolution: "is-plain-object@npm:2.0.4" @@ -13558,6 +13748,13 @@ __metadata: languageName: node linkType: hard +"longest-streak@npm:^2.0.0": + version: 2.0.4 + resolution: "longest-streak@npm:2.0.4" + checksum: 28b8234a14963002c5c71035dee13a0a11e9e9d18ffa320fdc8796ed7437399204495702ed69cd2a7087b0af041a2a8b562829b7c1e2042e73a3374d1ecf6580 + languageName: node + linkType: hard + "longest@npm:^1.0.0, longest@npm:^1.0.1": version: 1.0.1 resolution: "longest@npm:1.0.1" @@ -13809,6 +14006,15 @@ __metadata: languageName: node linkType: hard +"markdown-table@npm:^2.0.0": + version: 2.0.0 + resolution: "markdown-table@npm:2.0.0" + dependencies: + repeat-string: ^1.0.0 + checksum: 9bb634a9300016cbb41216c1eab44c74b6b7083ac07872e296f900a29449cf0e260ece03fa10c3e9784ab94c61664d1d147da0315f95e1336e2bdcc025615c90 + languageName: node + linkType: hard + "marked@npm:^4.0.12": version: 4.3.0 resolution: "marked@npm:4.3.0" @@ -13848,6 +14054,122 @@ __metadata: languageName: node linkType: hard +"mdast-util-find-and-replace@npm:^1.1.0": + version: 1.1.1 + resolution: "mdast-util-find-and-replace@npm:1.1.1" + dependencies: + escape-string-regexp: ^4.0.0 + unist-util-is: ^4.0.0 + unist-util-visit-parents: ^3.0.0 + checksum: e4c9e50d9bce5ae4c728a925bd60080b94d16aaa312c27e2b70b16ddc29a5d0a0844d6e18efaef08aeb22c68303ec528f20183d1b0420504a0c2c1710cebd76f + languageName: node + linkType: hard + +"mdast-util-footnote@npm:^0.1.0": + version: 0.1.7 + resolution: "mdast-util-footnote@npm:0.1.7" + dependencies: + mdast-util-to-markdown: ^0.6.0 + micromark: ~2.11.0 + checksum: 6d05396a9497c289fecf844d68d3210968750b215cf1df3fd1962c77d73560d8598cc4d79cef36a750d5b43f30b71fec079e0563f267b65072cfc38548d39eda + languageName: node + linkType: hard + +"mdast-util-from-markdown@npm:^0.8.0": + version: 0.8.5 + resolution: "mdast-util-from-markdown@npm:0.8.5" + dependencies: + "@types/mdast": ^3.0.0 + mdast-util-to-string: ^2.0.0 + micromark: ~2.11.0 + parse-entities: ^2.0.0 + unist-util-stringify-position: ^2.0.0 + checksum: 5a9d0d753a42db763761e874c22365d0c7c9934a5a18b5ff76a0643610108a208a041ffdb2f3d3dd1863d3d915225a4020a0aade282af0facfd0df110601eee6 + languageName: node + linkType: hard + +"mdast-util-frontmatter@npm:^0.2.0": + version: 0.2.0 + resolution: "mdast-util-frontmatter@npm:0.2.0" + dependencies: + micromark-extension-frontmatter: ^0.2.0 + checksum: 6dbed31233b34b41dd09da4756bbe3bd674fa3033c58f771f419e56675b8c7a601a18838d027fdb9cbef3b958346c661f7926ca401e154e63c071ecb2a3596a2 + languageName: node + linkType: hard + +"mdast-util-gfm-autolink-literal@npm:^0.1.0, mdast-util-gfm-autolink-literal@npm:^0.1.3": + version: 0.1.3 + resolution: "mdast-util-gfm-autolink-literal@npm:0.1.3" + dependencies: + ccount: ^1.0.0 + mdast-util-find-and-replace: ^1.1.0 + micromark: ^2.11.3 + checksum: 9f7b888678631fd8c0a522b0689a750aead2b05d57361dbdf02c10381557f1ce874f746226141f3ace1e0e7952495e8d5ce8f9af423a7a66bb300d4635a918eb + languageName: node + linkType: hard + +"mdast-util-gfm-strikethrough@npm:^0.2.0": + version: 0.2.3 + resolution: "mdast-util-gfm-strikethrough@npm:0.2.3" + dependencies: + mdast-util-to-markdown: ^0.6.0 + checksum: 51aa11ca8f1a5745f1eb9ccddb0eca797b3ede6f0c7bf355d594ad57c02c98d95260f00b1c4b07504018e0b22708531eabb76037841f09ce8465444706a06522 + languageName: node + linkType: hard + +"mdast-util-gfm-table@npm:^0.1.0": + version: 0.1.6 + resolution: "mdast-util-gfm-table@npm:0.1.6" + dependencies: + markdown-table: ^2.0.0 + mdast-util-to-markdown: ~0.6.0 + checksum: eeb43faf833753315b4ccf8d7bc8a6845b31562b2d2dd12a92aa40f9cee1b1954643c7515399a98f9b2e143c95cf6b5c0aac5941a4f609d6a57335587cee99ac + languageName: node + linkType: hard + +"mdast-util-gfm-task-list-item@npm:^0.1.0": + version: 0.1.6 + resolution: "mdast-util-gfm-task-list-item@npm:0.1.6" + dependencies: + mdast-util-to-markdown: ~0.6.0 + checksum: c10480c0ae86547980b38b49fba2ecd36a50bf1f3478d3f12810a0d8e8f821585c2bd7d805dd735518e84493b5eef314afdb8d59807021e2d9aa22d077eb7588 + languageName: node + linkType: hard + +"mdast-util-gfm@npm:^0.1.0": + version: 0.1.2 + resolution: "mdast-util-gfm@npm:0.1.2" + dependencies: + mdast-util-gfm-autolink-literal: ^0.1.0 + mdast-util-gfm-strikethrough: ^0.2.0 + mdast-util-gfm-table: ^0.1.0 + mdast-util-gfm-task-list-item: ^0.1.0 + mdast-util-to-markdown: ^0.6.1 + checksum: 368ed535b2c2e0f33d0225a9e9c985468bf4825a06896815369aea585f6defaccb555ac40ba911e02c8e8c47e79f7efb4348de532de50bca2638a1e568f2d3c9 + languageName: node + linkType: hard + +"mdast-util-to-markdown@npm:^0.6.0, mdast-util-to-markdown@npm:^0.6.1, mdast-util-to-markdown@npm:~0.6.0": + version: 0.6.5 + resolution: "mdast-util-to-markdown@npm:0.6.5" + dependencies: + "@types/unist": ^2.0.0 + longest-streak: ^2.0.0 + mdast-util-to-string: ^2.0.0 + parse-entities: ^2.0.0 + repeat-string: ^1.0.0 + zwitch: ^1.0.0 + checksum: 7ebc47533bff6e8669f85ae124dc521ea570e9df41c0d9e4f0f43c19ef4a8c9928d741f3e4afa62fcca1927479b714582ff5fd684ef240d84ee5b75ab9d863cf + languageName: node + linkType: hard + +"mdast-util-to-string@npm:^2.0.0": + version: 2.0.0 + resolution: "mdast-util-to-string@npm:2.0.0" + checksum: 0b2113ada10e002fbccb014170506dabe2f2ddacaacbe4bc1045c33f986652c5a162732a2c057c5335cdb58419e2ad23e368e5be226855d4d4e280b81c4e9ec2 + languageName: node + linkType: hard + "mdn-data@npm:2.0.14": version: 2.0.14 resolution: "mdn-data@npm:2.0.14" @@ -13943,6 +14265,91 @@ __metadata: languageName: node linkType: hard +"micromark-extension-footnote@npm:^0.3.0": + version: 0.3.2 + resolution: "micromark-extension-footnote@npm:0.3.2" + dependencies: + micromark: ~2.11.0 + checksum: 476c7f7ae86d7b3c33a07cbf7286d2cce06aa9b348eed79867fd6abc98c37519464c335c522e85208c99e573c1abbbcb72a0a27897f2a0b505d993c76df7bd1d + languageName: node + linkType: hard + +"micromark-extension-frontmatter@npm:^0.2.0": + version: 0.2.2 + resolution: "micromark-extension-frontmatter@npm:0.2.2" + dependencies: + fault: ^1.0.0 + checksum: b591494c2910162a47b4413b43d80b1e3f148c0d92955576737ff36525f34deb6cc784cdbede76c4ce6a9d06c75586c7198ffb84f97efee894cafabcc86716e4 + languageName: node + linkType: hard + +"micromark-extension-gfm-autolink-literal@npm:~0.5.0": + version: 0.5.7 + resolution: "micromark-extension-gfm-autolink-literal@npm:0.5.7" + dependencies: + micromark: ~2.11.3 + checksum: 319ec793c2e374e4cc0cbbb07326c1affb78819e507c7c1577f9d14b972852a6bb55e664332ec51f7cca24bdddd43429c5dd55f11e9200b1a00bab1bf494fb2d + languageName: node + linkType: hard + +"micromark-extension-gfm-strikethrough@npm:~0.6.5": + version: 0.6.5 + resolution: "micromark-extension-gfm-strikethrough@npm:0.6.5" + dependencies: + micromark: ~2.11.0 + checksum: 67711633590d3e688759a46aaed9f9d04bcaf29b6615eec17af082eabe1059fbca4beb41ba13db418ae7be3ac90198742fbabe519a70f9b6bb615598c5d6ef1a + languageName: node + linkType: hard + +"micromark-extension-gfm-table@npm:~0.4.0": + version: 0.4.3 + resolution: "micromark-extension-gfm-table@npm:0.4.3" + dependencies: + micromark: ~2.11.0 + checksum: 12c78de985944dd66aae409871c45d801cc65704f55ea5cc8afac422042c6d3b5e777b154c079ae81298b30b83434b257b54981bda51c220a102042dd2524a63 + languageName: node + linkType: hard + +"micromark-extension-gfm-tagfilter@npm:~0.3.0": + version: 0.3.0 + resolution: "micromark-extension-gfm-tagfilter@npm:0.3.0" + checksum: 9369736a203836b2933dfdeacab863e7a4976139b9dd46fa5bd6c2feeef50c7dbbcdd641ae95f0481f577d8aa22396bfa7ed9c38515647d4cf3f2c727cc094a3 + languageName: node + linkType: hard + +"micromark-extension-gfm-task-list-item@npm:~0.3.0": + version: 0.3.3 + resolution: "micromark-extension-gfm-task-list-item@npm:0.3.3" + dependencies: + micromark: ~2.11.0 + checksum: e4ccbe6b440234c8ee05d89315e1204c78773724241af31ac328194470a8a61bc6606eab3ce2d9a83da4401b06e07936038654493da715d40522133d1556dda4 + languageName: node + linkType: hard + +"micromark-extension-gfm@npm:^0.3.0": + version: 0.3.3 + resolution: "micromark-extension-gfm@npm:0.3.3" + dependencies: + micromark: ~2.11.0 + micromark-extension-gfm-autolink-literal: ~0.5.0 + micromark-extension-gfm-strikethrough: ~0.6.5 + micromark-extension-gfm-table: ~0.4.0 + micromark-extension-gfm-tagfilter: ~0.3.0 + micromark-extension-gfm-task-list-item: ~0.3.0 + checksum: 7957a1afd8c92daa0fc165342902729334b22d59feacd85b20a0d9cc453c90bbdd5b5ba85a3d177c01802060aeb3326daf05d3e6d95932fcbc8371827c98336e + languageName: node + linkType: hard + +"micromark@npm:^2.11.3, micromark@npm:~2.11.0, micromark@npm:~2.11.3": + version: 2.11.4 + resolution: "micromark@npm:2.11.4" + dependencies: + debug: ^4.0.0 + parse-entities: ^2.0.0 + checksum: f8a5477d394908a5d770227aea71657a76423d420227c67ea0699e659a5f62eb39d504c1f7d69ec525a6af5aaeb6a7bffcdba95614968c03d41d3851edecb0d6 + languageName: node + linkType: hard + "micromatch@npm:3.1.0": version: 3.1.0 resolution: "micromatch@npm:3.1.0" @@ -15346,6 +15753,20 @@ __metadata: languageName: node linkType: hard +"parse-entities@npm:^2.0.0": + version: 2.0.0 + resolution: "parse-entities@npm:2.0.0" + dependencies: + character-entities: ^1.0.0 + character-entities-legacy: ^1.0.0 + character-reference-invalid: ^1.0.0 + is-alphanumerical: ^1.0.0 + is-decimal: ^1.0.0 + is-hexadecimal: ^1.0.0 + checksum: 7addfd3e7d747521afac33c8121a5f23043c6973809756920d37e806639b4898385d386fcf4b3c8e2ecf1bc28aac5ae97df0b112d5042034efbe80f44081ebce + languageName: node + linkType: hard + "parse-filepath@npm:^1.0.1": version: 1.0.2 resolution: "parse-filepath@npm:1.0.2" @@ -16953,6 +17374,45 @@ __metadata: languageName: node linkType: hard +"remark-footnotes@npm:^3.0.0": + version: 3.0.0 + resolution: "remark-footnotes@npm:3.0.0" + dependencies: + mdast-util-footnote: ^0.1.0 + micromark-extension-footnote: ^0.3.0 + checksum: 5e84afb47b57e512171f8af5146e8ba49817faff761c6cdd14abf26c9f018b497e69e79378d94b3ba529e491d3f0615c60851d3db86de785aa37641bf5bf7f8a + languageName: node + linkType: hard + +"remark-frontmatter@npm:^3.0.0": + version: 3.0.0 + resolution: "remark-frontmatter@npm:3.0.0" + dependencies: + mdast-util-frontmatter: ^0.2.0 + micromark-extension-frontmatter: ^0.2.0 + checksum: 33bbcf36a51ee4c3813106b933766e0dc4b2b50f8eb4da3a4c577ea0278335589b36b182fb2722c9857d0ea9932ac75368a5dff70c8cf2b74f4747c244068976 + languageName: node + linkType: hard + +"remark-gfm@npm:^1.0.0": + version: 1.0.0 + resolution: "remark-gfm@npm:1.0.0" + dependencies: + mdast-util-gfm: ^0.1.0 + micromark-extension-gfm: ^0.3.0 + checksum: 877b0f6472a90a490b5d5a1393f46d22c4ab7451b1e83ebd7362e5be9c661b6ed03e76c28f76894f460bedf23345c589d3f412c273ce0d4d442c6a4d65b0eae4 + languageName: node + linkType: hard + +"remark-parse@npm:^9.0.0": + version: 9.0.0 + resolution: "remark-parse@npm:9.0.0" + dependencies: + mdast-util-from-markdown: ^0.8.0 + checksum: 50104880549639b7dd7ae6f1e23c214915fe9c054f02f3328abdaee3f6de6d7282bf4357c3c5b106958fe75e644a3c248c2197755df34f9955e8e028fc74868f + languageName: node + linkType: hard + "remove-bom-buffer@npm:^3.0.0": version: 3.0.0 resolution: "remove-bom-buffer@npm:3.0.0" @@ -16988,7 +17448,7 @@ __metadata: languageName: node linkType: hard -"repeat-string@npm:^1.5.2, repeat-string@npm:^1.6.1": +"repeat-string@npm:^1.0.0, repeat-string@npm:^1.5.2, repeat-string@npm:^1.6.1": version: 1.6.1 resolution: "repeat-string@npm:1.6.1" checksum: 1b809fc6db97decdc68f5b12c4d1a671c8e3f65ec4a40c238bc5200e44e85bcc52a54f78268ab9c29fcf5fe4f1343e805420056d1f30fa9a9ee4c2d93e3cc6c0 @@ -19070,7 +19530,7 @@ __metadata: languageName: node linkType: hard -"traverse@npm:^0.6.6": +"traverse@npm:^0.6.6, traverse@npm:^0.6.7": version: 0.6.7 resolution: "traverse@npm:0.6.7" checksum: 21018085ab72f717991597e12e2b52446962ed59df591502e4d7e1a709bc0a989f7c3d451aa7d882666ad0634f1546d696c5edecda1f2fc228777df7bb529a1e @@ -19100,6 +19560,13 @@ __metadata: languageName: node linkType: hard +"trough@npm:^1.0.0": + version: 1.0.5 + resolution: "trough@npm:1.0.5" + checksum: d6c8564903ed00e5258bab92134b020724dbbe83148dc72e4bf6306c03ed8843efa1bcc773fa62410dd89161ecb067432dd5916501793508a9506cacbc408e25 + languageName: node + linkType: hard + "ts-jest@npm:29.1.0": version: 29.1.0 resolution: "ts-jest@npm:29.1.0" @@ -19550,7 +20017,7 @@ __metadata: languageName: node linkType: hard -"underscore@npm:1.x.x, underscore@npm:^1.8.3": +"underscore@npm:1.x.x, underscore@npm:^1.13.2, underscore@npm:^1.8.3": version: 1.13.6 resolution: "underscore@npm:1.13.6" checksum: d5cedd14a9d0d91dd38c1ce6169e4455bb931f0aaf354108e47bd46d3f2da7464d49b2171a5cf786d61963204a42d01ea1332a903b7342ad428deaafaf70ec36 @@ -19651,6 +20118,20 @@ __metadata: languageName: node linkType: hard +"unified@npm:^9.2.2": + version: 9.2.2 + resolution: "unified@npm:9.2.2" + dependencies: + bail: ^1.0.0 + extend: ^3.0.0 + is-buffer: ^2.0.0 + is-plain-obj: ^2.0.0 + trough: ^1.0.0 + vfile: ^4.0.0 + checksum: 7c24461be7de4145939739ce50d18227c5fbdf9b3bc5a29dabb1ce26dd3e8bd4a1c385865f6f825f3b49230953ee8b591f23beab3bb3643e3e9dc37aa8a089d5 + languageName: node + linkType: hard + "union-value@npm:^1.0.0": version: 1.0.1 resolution: "union-value@npm:1.0.1" @@ -19698,6 +20179,32 @@ __metadata: languageName: node linkType: hard +"unist-util-is@npm:^4.0.0": + version: 4.1.0 + resolution: "unist-util-is@npm:4.1.0" + checksum: 726484cd2adc9be75a939aeedd48720f88294899c2e4a3143da413ae593f2b28037570730d5cf5fd910ff41f3bc1501e3d636b6814c478d71126581ef695f7ea + languageName: node + linkType: hard + +"unist-util-stringify-position@npm:^2.0.0": + version: 2.0.3 + resolution: "unist-util-stringify-position@npm:2.0.3" + dependencies: + "@types/unist": ^2.0.2 + checksum: f755cadc959f9074fe999578a1a242761296705a7fe87f333a37c00044de74ab4b184b3812989a57d4cd12211f0b14ad397b327c3a594c7af84361b1c25a7f09 + languageName: node + linkType: hard + +"unist-util-visit-parents@npm:^3.0.0": + version: 3.1.1 + resolution: "unist-util-visit-parents@npm:3.1.1" + dependencies: + "@types/unist": ^2.0.0 + unist-util-is: ^4.0.0 + checksum: 1170e397dff88fab01e76d5154981666eb0291019d2462cff7a2961a3e76d3533b42eaa16b5b7e2d41ad42a5ea7d112301458283d255993e660511387bf67bc3 + languageName: node + linkType: hard + "universalify@npm:^0.2.0": version: 0.2.0 resolution: "universalify@npm:0.2.0" @@ -19764,6 +20271,13 @@ __metadata: languageName: node linkType: hard +"update-section@npm:^0.3.3": + version: 0.3.3 + resolution: "update-section@npm:0.3.3" + checksum: 77176459493af2565eccdff16f126a7cec1636ec16a458554bac128631129e734884a0f889d9579cd59739c04c047d46ca19dbe5550e0894736e5bc524fceb18 + languageName: node + linkType: hard + "upper-case@npm:^1.1.1": version: 1.1.3 resolution: "upper-case@npm:1.1.3" @@ -19950,6 +20464,28 @@ __metadata: languageName: node linkType: hard +"vfile-message@npm:^2.0.0": + version: 2.0.4 + resolution: "vfile-message@npm:2.0.4" + dependencies: + "@types/unist": ^2.0.0 + unist-util-stringify-position: ^2.0.0 + checksum: 1bade499790f46ca5aba04bdce07a1e37c2636a8872e05cf32c26becc912826710b7eb063d30c5754fdfaeedc8a7658e78df10b3bc535c844890ec8a184f5643 + languageName: node + linkType: hard + +"vfile@npm:^4.0.0": + version: 4.2.1 + resolution: "vfile@npm:4.2.1" + dependencies: + "@types/unist": ^2.0.0 + is-buffer: ^2.0.0 + unist-util-stringify-position: ^2.0.0 + vfile-message: ^2.0.0 + checksum: ee5726e10d170472cde778fc22e0f7499caa096eb85babea5d0ce0941455b721037ee1c9e6ae506ca2803250acd313d0f464328ead0b55cfe7cb6315f1b462d6 + languageName: node + linkType: hard + "vinyl-fs@npm:3.0.3, vinyl-fs@npm:^3.0.0, vinyl-fs@npm:^3.0.3": version: 3.0.3 resolution: "vinyl-fs@npm:3.0.3" @@ -20845,3 +21381,10 @@ __metadata: checksum: 7ce44ca3c548421b9041a08bacbe47657532c8d0e54250672b1e65b287339cfd3ba38a3405176e1965e3efa88c9fb00bfbf52558f2e36c2af6415372cdf3289b languageName: node linkType: hard + +"zwitch@npm:^1.0.0": + version: 1.0.5 + resolution: "zwitch@npm:1.0.5" + checksum: 28a1bebacab3bc60150b6b0a2ba1db2ad033f068e81f05e4892ec0ea13ae63f5d140a1d692062ac0657840c8da076f35b94433b5f1c329d7803b247de80f064a + languageName: node + linkType: hard From 5a684a37dac076e3c7253e03ea13a3b8ae9261c4 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 4 Sep 2023 17:19:46 +0300 Subject: [PATCH 091/159] :art: --- .../base/b-virtual-scroll/README.md | 104 +++++++++--------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index b57ccf6a8e..02a9267bc6 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -93,64 +93,64 @@ To implement simple rendering, you need to follow several steps: 1. Set up a data provider for the component. For example, we'll use a provider named `Provider` that returns data in the format `{data: object[]}`, where the number of objects depends on the request parameter `count`. -``` -< b-virtual-scroll & - :dataProvider = 'Provider' -. -``` + ``` + < b-virtual-scroll & + :dataProvider = 'Provider' + . + ``` > It's important to note that `b-virtual-scroll` expects data in this specific format (`{data: object[]}`). If your provider returns data in a different format, you can use processors in either the provider or the component using the `convertDataToDb` prop. 2. Let's say we want to load and render 12 components at a time. To achieve this, you need to specify the `request` and `chunkSize` props for the `b-virtual-scroll` component. The `request` prop defines the request parameters (standard behavior of `iData`), and `chunkSize` specifies the number of items to render in each rendering cycle. -``` -< b-virtual-scroll & - :dataProvider = 'Provider' | - :request = {get: {count: 12}} | - :chunkSize = 12 -. -``` + ``` + < b-virtual-scroll & + :dataProvider = 'Provider' | + :request = {get: {count: 12}} | + :chunkSize = 12 + . + ``` 3. To avoid loading the same data repeatedly and load different data for each subsequent request, you need to pass the `page` request parameter to the `Provider`. This parameter indicates the page number of the data to be loaded. -To achieve this, use the `requestQuery` prop in the `b-virtual-scroll` component. `requestQuery` is a function prop that `b-virtual-scroll` calls, passing its own state as an argument, before making a request. You can return the appropriate `page` value based on the component's state. The difference between `request` and `requestQuery` is that changes to the latter won't cause the component to reinitialize. These two props are merged to form the final request parameters passed to the provider. + To achieve this, use the `requestQuery` prop in the `b-virtual-scroll` component. `requestQuery` is a function prop that `b-virtual-scroll` calls, passing its own state as an argument, before making a request. You can return the appropriate `page` value based on the component's state. The difference between `request` and `requestQuery` is that changes to the latter won't cause the component to reinitialize. These two props are merged to form the final request parameters passed to the provider. -``` -< b-virtual-scroll & - :dataProvider = 'Provider' | - :request = {get: {count: 12}} | - :requestQuery = (state) => ({get: {page: state.loadPage}}) | - :chunkSize = 12 -. -``` + ``` + < b-virtual-scroll & + :dataProvider = 'Provider' | + :request = {get: {count: 12}} | + :requestQuery = (state) => ({get: {page: state.loadPage}}) | + :chunkSize = 12 + . + ``` -In the example above, the `page` parameter is extracted from the component's state, specifically `loadPage`, which increments after each successful data load. + In the example above, the `page` parameter is extracted from the component's state, specifically `loadPage`, which increments after each successful data load. 4. Now that you have set up data loading with pagination, you need to specify what `b-virtual-scroll` will render. -To control what `b-virtual-scroll` renders, you can use the following props: + To control what `b-virtual-scroll` renders, you can use the following props: -- `item`: The name of the component to be rendered. It can also be a function that returns the component's name. + - `item`: The name of the component to be rendered. It can also be a function that returns the component's name. -- `itemProps`: The props for the component. Typically, this is a function that returns the props for each item based on the loaded data. + - `itemProps`: The props for the component. Typically, this is a function that returns the props for each item based on the loaded data. -- `itemKey`: The uniq id of the component. + - `itemKey`: The uniq id of the component. -Rendering occurs after data is loaded. + Rendering occurs after data is loaded. -``` -< b-virtual-scroll & - :dataProvider = 'Provider' | - :request = {get: {count: 12}} | - :requestQuery = (state) => ({get: {page: state.loadPage}}) | - :chunkSize = 12 | - :item = 'b-dummy' | - :itemKey = (el) => el.uuid | - :itemProps = (el) => ({name: el.name, type: el.type}) -. -``` + ``` + < b-virtual-scroll & + :dataProvider = 'Provider' | + :request = {get: {count: 12}} | + :requestQuery = (state) => ({get: {page: state.loadPage}}) | + :chunkSize = 12 | + :item = 'b-dummy' | + :itemKey = (el) => el.uuid | + :itemProps = (el) => ({name: el.name, type: el.type}) + . + ``` -What is `el` you can inquire about in the `itemKey` and `itemProps` functions? `el` is one of the objects in the `data` array loaded by the `dataProvider`. `b-virtual-scroll` takes the array of loaded data and calls these functions for each of the objects in this array, allowing you to transform data into components that are suitable for rendering in the `b-virtual-scroll` component. + What is `el` you can inquire about in the `itemKey` and `itemProps` functions? `el` is one of the objects in the `data` array loaded by the `dataProvider`. `b-virtual-scroll` takes the array of loaded data and calls these functions for each of the objects in this array, allowing you to transform data into components that are suitable for rendering in the `b-virtual-scroll` component. This setup will display a component on the page that loads and renders 12 items at once. When scrolling down, a new request with a different `page` value will be made, and after a successful load, new components will be rendered. @@ -158,19 +158,19 @@ However, if your component takes a long time to load data (e.g., 1 second), you Let's add a `loader` slot to our component to provide a better user experience during loading: -``` -< b-virtual-scroll & - :dataProvider = 'Provider' | - :request = {get: {count: 12}} | - :requestQuery = (state) => ({get: {page: state.loadPage}}) | - :chunkSize = 12 | - :item = 'b-dummy' | - :itemProps = (el) => ({name: el.name, type: el.type}) -. - < template #loader - < .&__loader - Data loading in progress -``` + ``` + < b-virtual-scroll & + :dataProvider = 'Provider' | + :request = {get: {count: 12}} | + :requestQuery = (state) => ({get: {page: state.loadPage}}) | + :chunkSize = 12 | + :item = 'b-dummy' | + :itemProps = (el) => ({name: el.name, type: el.type}) + . + < template #loader + < .&__loader + Data loading in progress + ``` Now, users will see a friendly message indicating that content will appear shortly, preventing them from being surprised by sudden content changes. From 46d1c6a5c7a6b65f11ce83a028acf5d75a8caf1a Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 4 Sep 2023 17:21:39 +0300 Subject: [PATCH 092/159] :art: --- .../base/b-virtual-scroll/README.md | 104 +++++++++--------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 02a9267bc6..b57ccf6a8e 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -93,64 +93,64 @@ To implement simple rendering, you need to follow several steps: 1. Set up a data provider for the component. For example, we'll use a provider named `Provider` that returns data in the format `{data: object[]}`, where the number of objects depends on the request parameter `count`. - ``` - < b-virtual-scroll & - :dataProvider = 'Provider' - . - ``` +``` +< b-virtual-scroll & + :dataProvider = 'Provider' +. +``` > It's important to note that `b-virtual-scroll` expects data in this specific format (`{data: object[]}`). If your provider returns data in a different format, you can use processors in either the provider or the component using the `convertDataToDb` prop. 2. Let's say we want to load and render 12 components at a time. To achieve this, you need to specify the `request` and `chunkSize` props for the `b-virtual-scroll` component. The `request` prop defines the request parameters (standard behavior of `iData`), and `chunkSize` specifies the number of items to render in each rendering cycle. - ``` - < b-virtual-scroll & - :dataProvider = 'Provider' | - :request = {get: {count: 12}} | - :chunkSize = 12 - . - ``` +``` +< b-virtual-scroll & + :dataProvider = 'Provider' | + :request = {get: {count: 12}} | + :chunkSize = 12 +. +``` 3. To avoid loading the same data repeatedly and load different data for each subsequent request, you need to pass the `page` request parameter to the `Provider`. This parameter indicates the page number of the data to be loaded. - To achieve this, use the `requestQuery` prop in the `b-virtual-scroll` component. `requestQuery` is a function prop that `b-virtual-scroll` calls, passing its own state as an argument, before making a request. You can return the appropriate `page` value based on the component's state. The difference between `request` and `requestQuery` is that changes to the latter won't cause the component to reinitialize. These two props are merged to form the final request parameters passed to the provider. +To achieve this, use the `requestQuery` prop in the `b-virtual-scroll` component. `requestQuery` is a function prop that `b-virtual-scroll` calls, passing its own state as an argument, before making a request. You can return the appropriate `page` value based on the component's state. The difference between `request` and `requestQuery` is that changes to the latter won't cause the component to reinitialize. These two props are merged to form the final request parameters passed to the provider. - ``` - < b-virtual-scroll & - :dataProvider = 'Provider' | - :request = {get: {count: 12}} | - :requestQuery = (state) => ({get: {page: state.loadPage}}) | - :chunkSize = 12 - . - ``` +``` +< b-virtual-scroll & + :dataProvider = 'Provider' | + :request = {get: {count: 12}} | + :requestQuery = (state) => ({get: {page: state.loadPage}}) | + :chunkSize = 12 +. +``` - In the example above, the `page` parameter is extracted from the component's state, specifically `loadPage`, which increments after each successful data load. +In the example above, the `page` parameter is extracted from the component's state, specifically `loadPage`, which increments after each successful data load. 4. Now that you have set up data loading with pagination, you need to specify what `b-virtual-scroll` will render. - To control what `b-virtual-scroll` renders, you can use the following props: +To control what `b-virtual-scroll` renders, you can use the following props: - - `item`: The name of the component to be rendered. It can also be a function that returns the component's name. +- `item`: The name of the component to be rendered. It can also be a function that returns the component's name. - - `itemProps`: The props for the component. Typically, this is a function that returns the props for each item based on the loaded data. +- `itemProps`: The props for the component. Typically, this is a function that returns the props for each item based on the loaded data. - - `itemKey`: The uniq id of the component. +- `itemKey`: The uniq id of the component. - Rendering occurs after data is loaded. +Rendering occurs after data is loaded. - ``` - < b-virtual-scroll & - :dataProvider = 'Provider' | - :request = {get: {count: 12}} | - :requestQuery = (state) => ({get: {page: state.loadPage}}) | - :chunkSize = 12 | - :item = 'b-dummy' | - :itemKey = (el) => el.uuid | - :itemProps = (el) => ({name: el.name, type: el.type}) - . - ``` +``` +< b-virtual-scroll & + :dataProvider = 'Provider' | + :request = {get: {count: 12}} | + :requestQuery = (state) => ({get: {page: state.loadPage}}) | + :chunkSize = 12 | + :item = 'b-dummy' | + :itemKey = (el) => el.uuid | + :itemProps = (el) => ({name: el.name, type: el.type}) +. +``` - What is `el` you can inquire about in the `itemKey` and `itemProps` functions? `el` is one of the objects in the `data` array loaded by the `dataProvider`. `b-virtual-scroll` takes the array of loaded data and calls these functions for each of the objects in this array, allowing you to transform data into components that are suitable for rendering in the `b-virtual-scroll` component. +What is `el` you can inquire about in the `itemKey` and `itemProps` functions? `el` is one of the objects in the `data` array loaded by the `dataProvider`. `b-virtual-scroll` takes the array of loaded data and calls these functions for each of the objects in this array, allowing you to transform data into components that are suitable for rendering in the `b-virtual-scroll` component. This setup will display a component on the page that loads and renders 12 items at once. When scrolling down, a new request with a different `page` value will be made, and after a successful load, new components will be rendered. @@ -158,19 +158,19 @@ However, if your component takes a long time to load data (e.g., 1 second), you Let's add a `loader` slot to our component to provide a better user experience during loading: - ``` - < b-virtual-scroll & - :dataProvider = 'Provider' | - :request = {get: {count: 12}} | - :requestQuery = (state) => ({get: {page: state.loadPage}}) | - :chunkSize = 12 | - :item = 'b-dummy' | - :itemProps = (el) => ({name: el.name, type: el.type}) - . - < template #loader - < .&__loader - Data loading in progress - ``` +``` +< b-virtual-scroll & + :dataProvider = 'Provider' | + :request = {get: {count: 12}} | + :requestQuery = (state) => ({get: {page: state.loadPage}}) | + :chunkSize = 12 | + :item = 'b-dummy' | + :itemProps = (el) => ({name: el.name, type: el.type}) +. + < template #loader + < .&__loader + Data loading in progress +``` Now, users will see a friendly message indicating that content will appear shortly, preventing them from being surprised by sudden content changes. From bca9e62afd4b6c742e70a47d8a43837032c430f1 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Tue, 5 Sep 2023 18:07:17 +0300 Subject: [PATCH 093/159] WIP --- .../base/b-virtual-scroll/README.md | 45 +- .../base/b-virtual-scroll/b-virtual-scroll.ts | 76 ++-- src/components/base/b-virtual-scroll/props.ts | 1 + .../test/api/component-object/index.ts | 2 +- .../test/unit/scenario/reload.ts | 6 +- src/components/dummies/b-dummy/b-dummy.ss | 10 +- src/components/dummies/b-dummy/b-dummy.ts | 17 +- .../p-v4-components-demo.ss | 6 - .../p-v4-components-demo.ts | 12 - tests/helpers/component-object/README.md | 383 ++++++++++++++---- tests/helpers/component-object/builder.ts | 60 ++- tests/helpers/component-object/interface.ts | 15 + tests/helpers/component/README.md | 9 - tests/helpers/component/index.ts | 34 -- 14 files changed, 472 insertions(+), 204 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index b57ccf6a8e..8580ab445d 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -10,13 +10,14 @@ - [How to Implement Simple Rendering?](#how-to-implement-simple-rendering) - [How to Implement Component Rendering on Click Instead of Scroll?](#how-to-implement-component-rendering-on-click-instead-of-scroll) - [How to Reinitialize the Component?](#how-to-reinitialize-the-component) + - [How to Reload a Failed Request?](#how-to-reload-a-failed-request) - [Component State](#component-state) - [Converting Data to the Required Format](#converting-data-to-the-required-format) - [Sliders or Multi-Column Content](#sliders-or-multi-column-content) - [How to Use "Should-Like" Functions?](#how-to-use-should-like-functions) - [Overview of Functions](#overview-of-functions) - [Best Practices](#best-practices) - - [Controlling the Rendering Flow with `itemsFactory`](#controlling-the-rendering-flow-with-itemsfactory) + - [Control the Rendering Conveyor with `itemsFactory`](#control-the-rendering-conveyor-with-itemsfactory) - [`itemsProcessors` and Global Component Processing](#itemsprocessors-and-global-component-processing) - [`request` and `requestQuery`](#request-and-requestquery) - [Component Understanding](#component-understanding) @@ -36,6 +37,10 @@ - [`itemsFactory`](#itemsfactory) - [`itemsProcessors`](#itemsprocessors) - [`tombstonesSize`](#tombstonessize) + - [Methods](#methods) + - [getNextDataSlice](#getnextdataslice) + - [getComponentState](#getcomponentstate) + - [initLoadNext](#initloadnext) - [Other Properties](#other-properties) - [Migration from `b-virtual-scroll` version 3.x.x](#migration-from-b-virtual-scroll-version-3xx) - [API](#api-1) @@ -302,6 +307,24 @@ In this example, when the `filterUuid` field on the `pPage` changes, `b-virtual- If you need to update the component's state at a specific moment in time, regardless of the context, you can use the `reload` or `initLoad` methods. +### How to Reload a Failed Request? + +Did your data fail to load due to a network or server error? No worries! The `initLoadNext` method comes to the rescue, allowing you to retry the failed request. +In addition to the `initLoadNext` method, `b-virtual-scroll` provides a `retry` slot that is displayed only when the request fails. + +This makes it straightforward to implement a retry mechanism for a failed request: + +``` +< b-virtual-scroll & + :dataProvider = 'Provider' | + :request = {get: {count: 12, filter: filterUuid}} | + ... +. + < template #retry + < .&__retry @click = initLoadNext + Retry last request +``` + ### Component State The `b-virtual-scroll` component is quite substantial and has its own internal state that complements the component's state. This internal state is reset when the component is reinitialized to its initial state and changes regularly during the component's lifecycle. The component's state contains a wealth of information useful for the client, such as the loaded data, the number of elements remaining outside the user's viewport, and more. @@ -436,7 +459,7 @@ Here are some tips for efficiently implementing data loading on the client side - Avoid making the last useless request: This pertains to the `shouldPerformDataRequest` and `shouldStopRequestingData` functions. By default, these functions check the last data chunk to see if it returned anything. It's better to avoid this and inform the component in advance that all data has been loaded. You can achieve this by comparing the value returned by your server, indicating the total number of items with the current number of items in `b-virtual-scroll`, as demonstrated in the example above. -### Controlling the Rendering Flow with `itemsFactory` +### Control the Rendering Conveyor with `itemsFactory` `itemsFactory` is a prop that allows you to take control of component rendering. Suppose you want to render twice as many components for a single data slice. Achieving this using `iItems` props (`item`, `itemProps`, etc.) might not be possible. However, such situations may arise, and this prop is created to solve them. @@ -777,7 +800,7 @@ The component supports several slots for customization: ``` < b-virtual-scroll < template #retry - < .&__retry @click = initLoad + < .&__retry @click = initLoadNext Retry last request ``` @@ -970,7 +993,7 @@ const itemsFactory = (state: VirtualScrollState): ComponentItem[] => { #### `itemsProcessors` - Type: `Function | Record | Function[]` -- Default: `undefined` +- Default: `{}` This prop is a middleware function that is called after `b-virtual-scroll` has compiled the abstract representation of components and before it passes this representation to the rendering engine. @@ -986,6 +1009,20 @@ For example, if you set `tombstonesSize` to 3, then three `tombstone` components Note: The `tombstone` component is used to represent empty or unloaded components in the virtual scroll. It is rendered as a placeholder until the actual component data is loaded and rendered. +### Methods + +#### getNextDataSlice + +Returns the next data slice that should be rendered based on the `chunkSize`. + +#### getComponentState + +Returns the current state of the component. + +#### initLoadNext + +Initializes the loading of the next data chunk. In case the loading fails, calling this method again will attempt to reload it. + ### Other Properties The `bVirtualScroll` class extends `iData` and includes additional properties related to slots, component state, and observers. Please refer to the documentation of `iData` for more details on those properties. diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts index 37d919d43c..2df78cf980 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts @@ -154,13 +154,6 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI .catch(stderr); } - /** - * Resets the component state to its initial state. - */ - reset(): void { - this.onReset(); - } - /** * Returns the component state. * {@link VirtualScrollState} @@ -169,11 +162,43 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI return this.componentInternalState.compile(); } + /** + * Returns the next slice of data that should be rendered. + * + * @param state + * @param chunkSize + */ + getNextDataSlice(state: VirtualScrollState, chunkSize: number): object[] { + const + {data} = state, + nextDataSliceStartIndex = this.componentInternalState.getDataCursor(), + nextDataSliceEndIndex = nextDataSliceStartIndex + chunkSize; + + return data.slice(nextDataSliceStartIndex, nextDataSliceEndIndex); + } + + /** + * Returns the chunk size that should be rendered. + * @param state + */ + getChunkSize(state: VirtualScrollState): number { + return Object.isFunction(this.chunkSize) ? + this.chunkSize(state, this) : + this.chunkSize; + } + + protected override convertDataToDB(data: unknown): O | this['DB'] { + this.onConvertDataToDB(data); + const result = super.convertDataToDB(data); + + return result; + } + /** * Gathers all request parameters from the component fields `requestProp` and `requestQuery`. * {@link RequestParams} */ - getRequestParams(): RequestParams { + protected getRequestParams(): RequestParams { const label: AsyncOptions = { label: $$.initLoadNext, group: bVirtualScrollAsyncGroup, @@ -192,7 +217,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI /** * Wrapper for {@link bVirtualScroll.shouldStopRequestingData}. */ - shouldStopRequestingDataWrapper(this: bVirtualScroll): boolean { + protected shouldStopRequestingDataWrapper(this: bVirtualScroll): boolean { const state = this.getComponentState(); if (state.areRequestsStopped) { @@ -208,40 +233,15 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI /** * Wrapper for {@link bVirtualScroll.shouldPerformDataRequest}. */ - shouldPerformDataRequestWrapper(this: bVirtualScroll): boolean { + protected shouldPerformDataRequestWrapper(this: bVirtualScroll): boolean { return this.shouldPerformDataRequest(this.getComponentState(), this); } /** - * Returns the chunk size that should be rendered. - * @param state - */ - getChunkSize(state: VirtualScrollState): number { - return Object.isFunction(this.chunkSize) ? - this.chunkSize(state, this) : - this.chunkSize; - } - - /** - * Returns the next slice of data that should be rendered. - * - * @param state - * @param chunkSize + * Resets the component state to its initial state. */ - getNextDataSlice(state: VirtualScrollState, chunkSize: number): object[] { - const - {data} = state, - nextDataSliceStartIndex = this.componentInternalState.getDataCursor(), - nextDataSliceEndIndex = nextDataSliceStartIndex + chunkSize; - - return data.slice(nextDataSliceStartIndex, nextDataSliceEndIndex); - } - - protected override convertDataToDB(data: unknown): O | this['DB'] { - this.onConvertDataToDB(data); - const result = super.convertDataToDB(data); - - return result; + protected reset(): void { + this.onReset(); } /** diff --git a/src/components/base/b-virtual-scroll/props.ts b/src/components/base/b-virtual-scroll/props.ts index cf2d8576c0..8e02b5aa1c 100644 --- a/src/components/base/b-virtual-scroll/props.ts +++ b/src/components/base/b-virtual-scroll/props.ts @@ -126,6 +126,7 @@ export default abstract class iVirtualScrollProps extends iData { item: Object.isFunction(ctx.item) ? ctx.item(data, i) : ctx.item, type: Object.isFunction(ctx.itemType) ? ctx.itemType(data, i) : ctx.itemType, + meta: { _data: data, ...Object.isFunction(ctx.itemMeta) ? ctx.itemMeta(data, i) : ctx.itemMeta diff --git a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts index dfd95546cf..019538c6ed 100644 --- a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts @@ -61,7 +61,7 @@ export class VirtualScrollComponentObject extends ComponentObject { + getChildCount(): Promise { return this.childList.count(); } diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts index 06914c3d7d..2a3112ca06 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts @@ -30,7 +30,7 @@ test.describe('', () => { }); test.describe('`request` prop was changed', () => { - test('Should reset state and reload the component data', async ({demoPage}) => { + test('Should reset state and reload the component data', async () => { const chunkSize = [12, 20]; @@ -46,11 +46,11 @@ test.describe('', () => { shouldPerformDataRequest: ({remainingItems}) => remainingItems === 0, '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') }) - .pick(demoPage.buildTestComponent(component.componentName, component.props)); + .build({useDummy: true}); await component.waitForChildCountEqualsTo(chunkSize[0]); - await demoPage.updateTestComponent({ + await component.updatePropsViaDummy({ request: { get: { chunkSize: chunkSize[1] diff --git a/src/components/dummies/b-dummy/b-dummy.ss b/src/components/dummies/b-dummy/b-dummy.ss index c3f8fe180b..72b261584a 100644 --- a/src/components/dummies/b-dummy/b-dummy.ss +++ b/src/components/dummies/b-dummy/b-dummy.ss @@ -12,4 +12,12 @@ - template index() extends ['i-data'].index - block body - += self.slot() + < template v-if = testComponent + < component & + ref = testComponent | + :is = testComponent | + :v-attrs = testComponentAttrs + . + + < template v-else + += self.slot() diff --git a/src/components/dummies/b-dummy/b-dummy.ts b/src/components/dummies/b-dummy/b-dummy.ts index ef8bf94e15..4ac5c8ccfe 100644 --- a/src/components/dummies/b-dummy/b-dummy.ts +++ b/src/components/dummies/b-dummy/b-dummy.ts @@ -11,7 +11,8 @@ * @packageDocumentation */ -import iData, { component } from 'components/super/i-data/i-data'; +import type iBlock from 'components/super/i-block/i-block'; +import iData, { component, field } from 'components/super/i-data/i-data'; export * from 'components/super/i-data/i-data'; @@ -21,7 +22,21 @@ export * from 'components/super/i-data/i-data'; } }) class bDummy extends iData { + /** + * Name of the test component. + */ + @field() + testComponent?: string; + /** + * Attributes for the test component. + */ + @field() + testComponentAttrs: Dictionary = {}; + + protected override readonly $refs!: iData['$refs'] & { + testComponent?: iBlock; + }; } export default bDummy; diff --git a/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss b/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss index e19ccca69d..6c86499141 100644 --- a/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss +++ b/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss @@ -12,9 +12,3 @@ - template index() extends ['i-static-page.component'].index - block body - < component & - v-if = testComponent | - id = testComponent | - :is = testComponent | - :v-attrs = testComponentAttrs - . diff --git a/src/components/pages/p-v4-components-demo/p-v4-components-demo.ts b/src/components/pages/p-v4-components-demo/p-v4-components-demo.ts index 6aedba16d4..0541541eeb 100644 --- a/src/components/pages/p-v4-components-demo/p-v4-components-demo.ts +++ b/src/components/pages/p-v4-components-demo/p-v4-components-demo.ts @@ -52,18 +52,6 @@ export default class pV4ComponentsDemo extends iStaticPage { @field() emptyField: unknown = undefined; - /** - * Name of the test component. - */ - @field() - testComponent?: string; - - /** - * Attributes for the test component. - */ - @field() - testComponentAttrs: Dictionary = {}; - protected beforeCreate(): void { // eslint-disable-next-line no-console console.time('Render'); diff --git a/tests/helpers/component-object/README.md b/tests/helpers/component-object/README.md index 8edf10eaaa..6fa8eda2d1 100644 --- a/tests/helpers/component-object/README.md +++ b/tests/helpers/component-object/README.md @@ -1,165 +1,372 @@ + + +**Table of Contents** + +- [tests/helpers/component-object](#testshelperscomponent-object) + - [Usage](#usage) + - [How to Create a Component and Place It on a Test Page?](#how-to-create-a-component-and-place-it-on-a-test-page) + - [How to Select an Existing Component on the Page?](#how-to-select-an-existing-component-on-the-page) + - [How to Set Props for a Component?](#how-to-set-props-for-a-component) + - [How to Change Props for a Component?](#how-to-change-props-for-a-component) + - [How to Set Child Nodes for a Component?](#how-to-set-child-nodes-for-a-component) + - [How to Track Component Method Calls?](#how-to-track-component-method-calls) + - [Using `spyOn` Method](#using-spyon-method) + - [Tracking Calls on the Prototype](#tracking-calls-on-the-prototype) + - [Setting Up Spies Before Component Initialization](#setting-up-spies-before-component-initialization) + - [How to Set a Mock Function Instead of a Real Method?](#how-to-set-a-mock-function-instead-of-a-real-method) + - [How to Create a `ComponentObject` for My Component?](#how-to-create-a-componentobject-for-my-component) + + + # tests/helpers/component-object -The `ComponentObject` is a base class for creating a component object like components for testing. The `component object` pattern allows for a more convenient way to interact with components in a testing environment. +The `ComponentObject` is a base class for creating a component object like components for testing. -This class can be used as a generic class for any component or can be extended to create a custom `component object` that implements methods for interacting with a specific component. +The `component object` pattern allows for a more convenient way to interact with components in a testing environment. -The class provides a universal API for generating a component and setting up mock functions and spy functions. +This class can be used as a generic class for any component or can be extended to create a custom `component object` that implements methods for interacting with a specific component. ## Usage -### Builder +### How to Create a Component and Place It on a Test Page? -#### Basic +1. Initialize the `ComponentObject` by calling the class constructor and providing the page where the component will be located and the component's name: -```typescript -import ComponentObject from 'path/to/component-object'; + ```typescript + import ComponentObject from 'path/to/component-object'; -// Create an instance of ComponentObject -const myComponent = new ComponentObject(page, 'b-component'); + // Create an instance of ComponentObject + const myComponent = new ComponentObject(page, 'b-component'); + ``` -// Build the component -await myComponent.build(); +2. Set the props and child nodes that need to be rendered with the component: -// Access the component + ```typescript + import ComponentObject from 'path/to/component-object'; + + // Create an instance of ComponentObject + const myComponent = new ComponentObject(page, 'b-component'); + + myComponent + .withProps({ + prop1: 'val' + }) + .withChildren({ + renderNext: { + type: 'div', + attrs: { + id: 'renderNext' + } + } + }); + ``` + +3. Call the `build` method, which generates the component's view and renders it on the page: + + ```typescript + import ComponentObject from 'path/to/component-object'; + + // Create an instance of ComponentObject + const myComponent = new ComponentObject(page, 'b-component'); + + myComponent + .withProps({ + prop1: 'val' + }) + .withChildren({ + renderNext: { + type: 'div', + attrs: { + id: 'renderNext' + } + } + }); + + await myComponent.build(); + ``` + +Now that the component is rendered and placed on the page, you can call any methods on it: + +```typescript +// Component handle const component = myComponent.component; // Perform interactions with the component await component.evaluate((ctx) => { - // Perform actions on the component + // Perform actions on the component + ctx.method(); }); ``` -### Mock +### How to Select an Existing Component on the Page? -#### Basic +Sometimes, you may not want to create a new component but instead select an existing one. To do this, you can use the `pick` method: ```typescript import ComponentObject from 'path/to/component-object'; -class MyComponentObject extends ComponentObject { - // Implement specific methods and properties for testing the component in a mock environment -} +// Create an instance of ComponentObject +const myComponent = new ComponentObject(page, 'b-component'); -// Create an instance of MyComponentObject -const myComponent = new MyComponentObject(page, 'b-component'); +await myComponent.pick('#selector'); +``` -await myComponent.build(); +### How to Set Props for a Component? -// Create a spy -const spy = await myComponent.spyOn('someMethod'); +You can set props for a component using the `withProps` method, which takes a dictionary of props: -// Access the component -const component = myComponent.component; +```typescript +import ComponentObject from 'path/to/component-object'; -// Perform interactions with the component -await component.evaluate((ctx) => { - // Perform actions on the component -}); +// Create an instance of ComponentObject +const myComponent = new ComponentObject(page, 'b-component'); -// Access the spy -console.log(await spy.calls); +myComponent + .withProps({ + prop1: 'val' + }); +``` + +You can use `withProps` multiple times to set props as many times as needed before the component is created using the `build` method. To overwrite a prop, simply use `withProps` again with the new value: + +```typescript +myComponent + .withProps({ + prop1: 'val' + }); -// Create a mock function -const mockFn = await myComponent.mockFn((arg) => { - // Mock function logic +myComponent.withProps({ + prop1: 'newVal' }); + +console.log(myComponent.props) // {prop1: 'newVal'} +``` + +### How to Change Props for a Component? + +Once a component is created (by calling the `build` or `pick` method), you cannot directly change its props because props are `readonly` properties of the component. However, if a prop is linked to a parent component's property, changing the parent's property will also change the prop's value in the component. + +To facilitate this behavior, there is a "sugar" method that encapsulates this logic, using a `b-dummy` component as the parent. To use this sugar method in the `build` method, pass the option `useDummy: true`. This will create a wrapper for the component using `b-dummy`, and you can change props using a special method called `updatePropsViaDummy`: + +```typescript +// Create an instance of ComponentObject +const myComponent = new ComponentObject(page, 'b-component'); + +myComponent + .withProps({ + prop1: 'val' + }); + +await myComponent.build({ useDummy: true }); + +// Change props +await myComponent.updatePropsViaDummy({ prop1: 'newVal' }); ``` -#### Provide mock function as a prop +Please note that there are some nuances to consider when creating a component with a `b-dummy` wrapper, such as you cannot set slots for such a component. + +### How to Set Child Nodes for a Component? + +You can set child nodes (or slots) for a component using the `withChildren` method. It works similarly to `withProps`, but it defines the child elements of the component, not its props. + +Here's an example of setting a child node that should be rendered in the `renderNext` slot: ```typescript import ComponentObject from 'path/to/component-object'; -class MyComponentObject extends ComponentObject { - // Implement specific methods and properties for testing the component in a mock environment -} +// Create an instance of ComponentObject +const myComponent = new ComponentObject(page, 'b-component'); -// Create an instance of MyComponentObject -const - myComponent = new MyComponentObject(page, 'b-component'), - someProp = await myComponent.mockFn(() => true); +myComponent + .withChildren({ + renderNext: { + type: 'div', + attrs: { + id: 'renderNext' + } + } + }); -myComponent.withProps({ - someProp -}); +await myComponent.build(); +``` + +To set the `default` slot, name the child node as `default`: + +```typescript +import ComponentObject from 'path/to/component-object'; + +// Create an instance of ComponentObject +const myComponent = new ComponentObject(page, 'b-component'); + +myComponent + .withChildren({ + default: { + type: 'div', + attrs: { + id: 'renderNext' + } + } + }); await myComponent.build(); +``` + +### How to Track Component Method Calls? + +#### Using `spyOn` Method + +To track calls to a component method, you can use the special `Spy` API, which is based on `jest-mock`. + +To create a spy for a method, use the `spyOn` method: + +```typescript +// Create an instance of MyComponentObject +const myComponent = new MyComponentObject(page, 'b-component'); + +await myComponent.build(); + +// Create a spy +const spy = await myComponent.spyOn('someMethod'); // Access the component const component = myComponent.component; // Perform interactions with the component await component.evaluate((ctx) => { - // Perform actions on the component + ctx.someMethod(); }); -console.log(await someProp.calls); +// Access the spy +console.log(await spy.calls); // [[]] ``` -#### Spy on emitter +In this example, we created a spy for the `someMethod` method. After performing the necessary actions with the component, you can access the `spy` object and use its API to find out how many times the method was called and with what arguments. -```typescript -import ComponentObject from 'path/to/component-object'; +#### Tracking Calls on the Prototype -class MyComponentObject extends ComponentObject { - // Implement specific methods and properties for testing the component in a mock environment -} +Sometimes, you may need to track calls to methods on the prototype, as they may be invoked not from the component instance itself but through functions like `call`, etc. For example, the `initLoad` method may be called from the prototype and the `call` function. In such cases, you can set up a spy on the class prototype. +Let's consider an example of tracking calls to the `initLoad` method of a component. To do this, you can use the `spyOn` method with the additional option `proto`: + +```typescript // Create an instance of MyComponentObject const myComponent = new MyComponentObject(page, 'b-component'); -// Create a spy -myComponent.withProps({ - '@hook:beforeDataCreate': (ctx) => jest.spy(ctx, 'emit') -}); +const initLoadSpy = await myComponent.spyOn('initLoad', { proto: true }); await myComponent.build(); +await sleep(200); -// Access the component -const component = myComponent.component; +// Access the spy +console.log(await initLoadSpy.calls); // [[]] +``` -// Perform interactions with the component -await component.evaluate((ctx) => { - // Perform actions on the component +Note that the spy is created before the component is created using the `build` method. This is important because when setting up a spy on the prototype, it needs to be established before the component is created so that it can track the initial call to `initLoad` during component creation. + +#### Setting Up Spies Before Component Initialization + +Sometimes, you may need to set up spies before the component is initialized. To do this, you can use the `beforeDataCreate` hook and define spies within it. + +Here's an example of setting up a spy to track the `emit` method of a component before its creation: + +```typescript +// Create an instance of MyComponentObject +const myComponent = new MyComponentObject(page, 'b-component'); + +await myComponent.withProps({ + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit'), }); -// Access the spy -const - spy = await myComponent.getSpy((ctx) => ctx.emit) +// Extract the spy +const spy = await myComponent.component.getSpy((ctx) => ctx.emit); +// Access the spy console.log(await spy.calls); ``` -It is important to understand that spy and mock functions store references, not copies, in their `calls` property. +Important: The function set on the `@hook:beforeDataCreate` event will be called within the browser context, not in Node.js. To pass this function from Node.js to the browser context, it will be serialized. `jestMock` is a globally available object that redirects its method calls to the `jest-mock` API. + +### How to Set a Mock Function Instead of a Real Method? + +To set up a mock function instead of a real method, you can use the `beforeDataCreate` hook and the `mock` method of the global `jestMock` object. ```typescript -class Component { - currentState: Dictionary = {}; - onStateUpdate: (state: Dictionary) => void; +// Create an instance of ComponentObject +const myComponent = new ComponentObject(page, 'b-component'); + +await myComponent + .withProps({ + prop1: 'val', + '@hook:beforeDataCreate': (ctx) => { + ctx.module.method = jestMock.mock(() => false); + }, + }) + .build(); + +const result = await myComponent.component.evaluate((ctx) => ctx.module.method()); + +console.log(result); // false +``` + +### How to Create a `ComponentObject` for My Component? + +For each component you want to test, you can use a `ComponentObject`. However, the basic API may not provide all the functionality you need since it does not know about your specific component. To make `ComponentObject` provide a more comfortable API for working with your component, you should create your own class that inherits from the basic `ComponentObject`. In your custom class (let's call it `MyComponentObject`), you can implement additional APIs that allow you to write tests more effectively and clearly. + +Here are the steps to create a `MyComponentObject`: + +1. Create a file for your class and inherit it from `ComponentObject`: + + **src/components/base/b-list/test/api/component-object/index.ts** + ```typescript + export class MyComponentObject extends ComponentObject { + + } + ``` + +2. Add the necessary API, such as the container selector, a method to get the number of child nodes in the container, and so on: + + **src/components/base/b-list/test/api/component-object/index.ts** + ```typescript + export class MyComponentObject extends ComponentObject { + + readonly container: Locator; + readonly childList: Locator; + + constructor(page: Page) { + super(page, 'b-list'); + + this.container = this.node.locator(this.elSelector('container')); + this.childList = this.container.locator('> *'); + } - get state() { - this.currentState; - } + getChildCount(): Promise { + return this.childList.count(); + } + } + ``` - constructor(onStateUpdate: (state: Dictionary) => void) { - this.onStateUpdate = onStateUpdate; - } +3. Use your `MyComponentObject` instead of `ComponentObject`: - updateState() { - this.currentState.val = this.currentState.val ?? 0; - this.currentState.val++; - this.onStateUpdate(this.state); - } -} + ```typescript + import MyComponentObject from 'path/to/my-component-object'; + // Create an instance of MyComponentObject + const myComponent = new MyComponentObject(page); -const - mock = jestMock.mock((state) => console.log('state update', state)); - c = new Component(mock); + myComponent + .withProps({ + prop1: 'val' + }) + .withChildren({ + renderNext: { + type: 'div', + attrs: { + id: 'renderNext' + } + } + }); -c.updateState(); -c.updateState(); + await myComponent.build(); -console.log(mock.mock.calls); // [[{val: 2}, {val: 2}]] -``` \ No newline at end of file + console.log(await myComponent.getChildCount()); + ``` diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index 6cf548aa86..b2da0523af 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -13,7 +13,12 @@ import { resolve } from '@pzlr/build-core'; import { Component, DOM, Utils } from 'tests/helpers'; import type ComponentObject from 'tests/helpers/component-object'; +import { expandedStringify } from 'core/prelude/test-env/components/json'; + import type iBlock from 'components/super/i-block/i-block'; +import type bDummy from 'components/dummies/b-dummy/b-dummy'; + +import type { BuildOptions } from 'tests/helpers/component-object/interface'; /** * A class implementing the `ComponentObject` approach that encapsulates different @@ -69,6 +74,11 @@ export default abstract class ComponentObjectBuilder { */ protected componentStore?: JSHandle; + /** + * Reference to the `b-dummy` wrapper component. + */ + protected dummy?: JSHandle; + /** * The component styles that should be inserted into the page. */ @@ -144,18 +154,30 @@ export default abstract class ComponentObjectBuilder { /** * Renders the component with the previously set props and children * using the `withProps` and `withChildren` methods. + * + * @param [options] */ - async build(): Promise> { + async build(options?: BuildOptions): Promise> { if (this.componentStyles != null) { await this.page.addStyleTag({content: this.componentStyles}); } - this.componentStore = await Component.createComponent(this.page, this.componentName, { - attrs: { - ...this.props - }, - children: this.children - }); + if (options?.useDummy) { + this.dummy = await Component.createComponent(this.page, 'b-dummy', { + attrs: { + functional: false + } + }); + + await this.updatePropsViaDummy(this.props); + this.componentStore = await this.dummy.evaluateHandle((ctx) => ctx.unsafe.$refs.testComponent); + + } else { + this.componentStore = await Component.createComponent(this.page, this.componentName, { + attrs: this.props, + children: this.children + }); + } return this.componentStore; } @@ -235,4 +257,28 @@ export default abstract class ComponentObjectBuilder { Object.assign(this.children, children); return this; } + + /** + * Updates the component's props using the `b-dummy` component. + * + * This method will not work if the component was built without the `useDummy` option. + * + * @param props + * + * @throws {@link ReferenceError} - if the component object was not built or was built without the `useDummy` option + */ + async updatePropsViaDummy(props: Dictionary): Promise { + if (!this.dummy) { + throw new ReferenceError('Failed to update props. Missing "b-dummy" component.'); + } + + const + serializedAttrs = expandedStringify(props); + + return this.dummy.evaluate((ctx, [attrs, componentName]) => { + Object.assign(ctx.testComponentAttrs, globalThis.expandedParse(attrs)); + ctx.testComponent = componentName; + + }, [serializedAttrs, this.componentName]); + } } diff --git a/tests/helpers/component-object/interface.ts b/tests/helpers/component-object/interface.ts index 62f1ec073e..a73b175406 100644 --- a/tests/helpers/component-object/interface.ts +++ b/tests/helpers/component-object/interface.ts @@ -20,3 +20,18 @@ export interface SpyOptions { */ proto?: boolean; } + +/** + * Options for the `build` method. + */ +export interface BuildOptions { + /** + * If `true`, the component will be created inside a `b-dummy`, and its props will be set + * through the `field` property of `b-dummy`. + * + * Building the component with this option allows updating the component's props using the `updateProps` method. + * + * Using this option does not allow creating child nodes!!! + */ + useDummy?: boolean; +} diff --git a/tests/helpers/component/README.md b/tests/helpers/component/README.md index 3c4d256f88..b6d8717fba 100644 --- a/tests/helpers/component/README.md +++ b/tests/helpers/component/README.md @@ -53,15 +53,6 @@ Returns `JSHandles` to the components instances for the specified query selector const componentsHandlers = await Component.getComponents(page, '.b-button'); ``` -### setPropsToComponent - -Sets the specified props for the component which matches the specified query selector. - -```typescript - -await Component.setPropsToComponent(page, '.b-slider', {mode: 'slide'}); -``` - ### waitForRoot Returns `JSHandle` to the root component. diff --git a/tests/helpers/component/index.ts b/tests/helpers/component/index.ts index 3e4ae8ed12..532ccf1ac2 100644 --- a/tests/helpers/component/index.ts +++ b/tests/helpers/component/index.ts @@ -12,7 +12,6 @@ import { expandedStringify } from 'core/prelude/test-env/components/json'; import type iBlock from 'components/super/i-block/i-block'; -import BOM, { WaitForIdleOptions } from 'tests/helpers/bom'; import { isRenderComponentsVnodeParams } from 'tests/helpers/component/helpers'; /** @@ -162,39 +161,6 @@ export default class Component { return components; } - /** - * Sets the passed props to a component by the specified selector and waits for `nextTick` after that - * - * @param page - * @param componentSelector - * @param props - * @param [opts] - */ - static async setPropsToComponent( - page: Page, - componentSelector: string, - props: Dictionary, - opts?: WaitForIdleOptions - ): Promise> { - const ctx = await this.waitForComponentByQuery(page, componentSelector); - - await ctx.evaluate(async (ctx, props) => { - for (let keys = Object.keys(props), i = 0; i < keys.length; i++) { - const - prop = keys[i], - val = props[prop]; - - ctx.field.set(prop, val); - } - - await ctx.nextTick(); - }, props); - - await BOM.waitForIdleCallback(page, opts); - - return this.waitForComponentByQuery(page, componentSelector); - } - /** * Returns the root component * From 0574cd7bd7734131fb4035f0e2d9e0f62082321c Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 7 Sep 2023 12:31:46 +0300 Subject: [PATCH 094/159] :wrench: --- tests/helpers/providers/interceptor/README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/helpers/providers/interceptor/README.md b/tests/helpers/providers/interceptor/README.md index 8ea8c11739..8e425b061e 100644 --- a/tests/helpers/providers/interceptor/README.md +++ b/tests/helpers/providers/interceptor/README.md @@ -1,9 +1,20 @@ # tests/helpers/providers/interceptor -API that provides a simple way to intercept and respond to any request. - +Это API предоставляет возможность вам перехватывать любой запрос и отвечать на него любыми данными. ## Usage +### Как ответить на запрос? + +### Как указать статус код запросу? + +### Как реализовать задержку перед ответом? + +### Как просмотреть сколько запросов было перехвачено? + +### Как посмотреть с какими параметрами были перехвачены запросы? + +### Как реализовать собственный перехватчик на основе `RequestInterceptor`? + ```typescript // Create a RequestInterceptor instance const interceptor = new RequestInterceptor(page, /api/); From 38bfe09f557404ad0913faaed8e54c931829a166 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 7 Sep 2023 14:09:10 +0300 Subject: [PATCH 095/159] :art: --- .../test/unit/functional/props/props.ts | 6 +-- .../p-v4-components-demo/test/api/page.ts | 48 +------------------ 2 files changed, 4 insertions(+), 50 deletions(-) diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts index 96519a6194..38ef654103 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts @@ -34,7 +34,7 @@ test.describe('', () => { }); test.describe('`chunkSize` prop changes after the first chunk has been rendered', () => { - test('Should render the second chunk with the new chunk size', async ({demoPage}) => { + test('Should render the second chunk with the new chunk size', async () => { const chunkSize = 12; @@ -46,10 +46,10 @@ test.describe('', () => { chunkSize, '@hook:beforeDataCreate': (ctx: bVirtualScroll['unsafe']) => jestMock.spy(ctx.componentFactory, 'produceComponentItems') }) - .pick(demoPage.buildTestComponent(component.componentName, component.props)); + .build({useDummy: true}); await component.waitForChildCountEqualsTo(chunkSize); - await demoPage.updateTestComponent({chunkSize: chunkSize * 2}); + await component.updatePropsViaDummy({chunkSize: chunkSize * 2}); await component.scrollToBottom(); await component.waitForChildCountEqualsTo(chunkSize * 3); diff --git a/src/components/pages/p-v4-components-demo/test/api/page.ts b/src/components/pages/p-v4-components-demo/test/api/page.ts index 4c87124138..5195f4b72e 100644 --- a/src/components/pages/p-v4-components-demo/test/api/page.ts +++ b/src/components/pages/p-v4-components-demo/test/api/page.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { JSHandle, Locator, Page } from 'playwright'; +import type { JSHandle, Page } from 'playwright'; import { build } from '@config/config'; import { concatURLs } from 'core/url'; @@ -14,7 +14,6 @@ import Component from 'tests/helpers/component'; import type bDummy from 'components/dummies/b-dummy/b-dummy'; import type pV4ComponentsDemo from 'components/pages/p-v4-components-demo/p-v4-components-demo'; -import { expandedStringify } from 'core/prelude/test-env/components/json'; /** * Page object: provides an API to work with `DemoPage` @@ -69,49 +68,4 @@ export default class DemoPage { async createDummy(): Promise> { return Component.createComponent(this.page, 'b-dummy'); } - - /** - * Renders a test component with the specified parameters on the page. - * - * Unlike {@link Component.createComponent}, the component is already present in the page template, - * which means it depends on the context of the page and can react to changes in reactive properties of its parents. - * - * Using a test component that is already embedded in the template can be useful when you need to test the component's - * reaction to changes in parent properties that are passed as props to the component. - * - * @param name - The name of the test component. - * @param attrs - The attributes for the test component. - */ - async buildTestComponent(name: string, attrs?: Dictionary): Promise { - if (!this.component) { - throw new ReferenceError('Missing `DemoPage` component'); - } - - const - serializedAttrs = expandedStringify(attrs ?? {}); - - await this.component.evaluate((ctx, [name, attrs]) => { - ctx.testComponent = name; - ctx.testComponentAttrs = globalThis.expandedParse(attrs); - }, [name, serializedAttrs]); - - return this.page.locator('#testComponent'); - } - - /** - * Updates the state of the test component. - * @param attrs - */ - async updateTestComponent(attrs?: Dictionary): Promise { - if (!this.component) { - throw new ReferenceError('Missing `DemoPage` component'); - } - - const - serializedAttrs = expandedStringify(attrs ?? {}); - - return this.component.evaluate((ctx, [attrs]) => { - Object.assign(ctx.testComponentAttrs, globalThis.expandedParse(attrs)); - }, [serializedAttrs]); - } } From 47225d8a52e13e1f652a5f068198e81a996979cc Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 7 Sep 2023 15:12:09 +0300 Subject: [PATCH 096/159] :art: --- tests/helpers/providers/interceptor/README.md | 218 +++++++++++------- tests/helpers/providers/interceptor/index.ts | 13 +- 2 files changed, 131 insertions(+), 100 deletions(-) diff --git a/tests/helpers/providers/interceptor/README.md b/tests/helpers/providers/interceptor/README.md index 8e425b061e..06a21b9080 100644 --- a/tests/helpers/providers/interceptor/README.md +++ b/tests/helpers/providers/interceptor/README.md @@ -1,140 +1,182 @@ # tests/helpers/providers/interceptor -Это API предоставляет возможность вам перехватывать любой запрос и отвечать на него любыми данными. +This API allows you to intercept any request and respond to it with custom data. + ## Usage -### Как ответить на запрос? +### How to Initialize a Request Interceptor? + +To initialize a request interceptor, simply create an instance by calling its constructor. Provide the page or context as the first argument and the `url` you want to intercept as the second argument. The `url` can be either a string or a regular expression. + +```typescript +// Create a RequestInterceptor instance +const interceptor = new RequestInterceptor(page, /api/); +``` + +However, after creating an instance of `RequestInterceptor`, request interceptions will not work until you do the following: + +1. Set a response for the request using the `response` method: -### Как указать статус код запросу? + ```typescript + // Create a RequestInterceptor instance + const interceptor = new RequestInterceptor(page, /api/); -### Как реализовать задержку перед ответом? + // Set a response for the request using a response status and payload + interceptor.response(200, { message: 'OK' }); + ``` -### Как просмотреть сколько запросов было перехвачено? +2. Start intercepting requests using the `start` method: -### Как посмотреть с какими параметрами были перехвачены запросы? + ```typescript + // Create a RequestInterceptor instance + const interceptor = new RequestInterceptor(page, /api/); -### Как реализовать собственный перехватчик на основе `RequestInterceptor`? + // Set a response for the request using a response status and payload + interceptor.response(200, { message: 'OK' }); + + // Start intercepting requests + await interceptor.start(); + ``` + +After these steps, every request that matches the specified regular expression will be intercepted, and a response with a status code of 200 and a response body containing an object with a `message` field will be sent. + +### How to Respond to a Request Once? + +To respond to a request only once, you can use the `responseOnce` method: ```typescript // Create a RequestInterceptor instance const interceptor = new RequestInterceptor(page, /api/); -// Set a response for one request using a response handler function -interceptor.responseOnce(async (route, request) => { - // Delay the response for 1 second - await delay(1000); +// Set a response for the request using a response status and payload +interceptor.responseOnce(200, { message: 'OK' }); - // Fulfill the request with a custom response - return route.fulfill({ - status: 200, - body: JSON.stringify({ message: 'Success' }), - contentType: 'application/json' - }); -}); +// Start intercepting requests +await interceptor.start(); +``` -// Set a response for every request using a response status and payload -interceptor.response(500, { error: 'Server Error' }); +This way, you can combine different response scenarios. For example, you can set the first request to respond with a status code of 500, the second with 404, and all others with 200: -// Start the request interception +```typescript +// Create a RequestInterceptor instance +const interceptor = new RequestInterceptor(page, /api/); + +interceptor + .responseOnce(500, { message: 'OK' }) + .responseOnce(404, { message: 'OK' }) + .response(200, { message: 'OK' }); + +// Start intercepting requests await interceptor.start(); +``` + +### How to Implement a Delay Before Responding? -// Make a request to the intercepted route -const response = await page.goto('https://example.com/api/data'); +`RequestInterceptor` provides an option to introduce a delay before responding. You can pass this delay as the third argument in the `response` method: -// Log the response status -console.log(response.status()); // 200 +```typescript +// Create a RequestInterceptor instance +const interceptor = new RequestInterceptor(page, /api/); -// Log the response body -console.log(await response.json()); // { message: 'Success' } +// Set a response for the request using a response status, payload, and delay +interceptor.response(200, { message: 'OK' }, { delay: 200 }); -// Stop the request interception -await interceptor.stop(); +// Start intercepting requests +await interceptor.start(); ``` +This delay causes a 200ms wait before sending a response to the request. Note that using `delay` in tests is generally not recommended, as it can slow down test execution. However, there are cases where it may be necessary, which is why this feature exists. + +### How to Set a Custom Request Handler? + +To set a custom request handler, pass a function instead of response parameters to the `response` method. This allows you to have full control over request interception. + ```typescript // Create a RequestInterceptor instance const interceptor = new RequestInterceptor(page, /api/); -// Set a response for one request using a response status and payload -interceptor.responseOnce(200, { message: 'OK' }); +// Set a custom response handler for the request +interceptor.response((route: Route) => route.fulfill({ status: 200 })); -// Set a response for every request using a response handler function -interceptor.response(async (route, request) => { - // Add a delay of 500 milliseconds to each response - await delay(500); +// Start intercepting requests +await interceptor.start(); +``` - // Fulfill the request with a custom response - return route.fulfill({ - status: 404, - body: JSON.stringify({ error: 'Not Found' }), - contentType: 'application/json' - }); -}); +### How to View the Number of Intercepted Requests? -// Start the request interception -await interceptor.start(); +Since `RequestInterceptor` uses the `jest-mock` API, you can access all the functionality provided by this API. To see the number of intercepted requests, you can access the `mock` property of the class and use the `jest-mock` API. -// Make multiple requests to the intercepted route -const response1 = await page.goto('https://example.com/api/data'); -const response2 = await page.goto('https://example.com/api/users'); +```typescript +// Create a RequestInterceptor instance +const interceptor = new RequestInterceptor(page, /api/); -// Log the response status -console.log(response1.status()); // 200 -console.log(response2.status()); // 404 +// Set a response for the request using a response status and payload +interceptor.responseOnce(200, { message: 'OK' }); -// Log the response body -console.log(await response1.json()); // { message: 'OK' } -console.log(await response2.json()); // { error: 'Not Found' } +// Start intercepting requests +await interceptor.start(); -// Stop the request interception -await interceptor.stop(); +// ... + +// Logs the number of times interception occurred +console.log(interceptor.mock.mock.calls.length); ``` -```typescript +### How to View the Parameters of Intercepted Requests? +```typescript // Create a RequestInterceptor instance const interceptor = new RequestInterceptor(page, /api/); -// Set a response for one request using a response handler function -interceptor.responseOnce(async (route, request) => { - // Delay the response for 1 second - await delay(1000); +// Set a response for the request using a response status and payload +interceptor.responseOnce(200, { message: 'OK' }); + +// Start intercepting requests +await interceptor.start(); + +// ... + +const calls = provider.mock.mock.calls; +const query = fromQueryString(new URL((providerCalls[0][0]).request().url()).search); - // Fulfill the request with a custom response - return route.fulfill({ - status: 200, - body: JSON.stringify({ message: 'Success' }), - contentType: 'application/json' - }); -}); +// Logs the query parameters of the first intercepted request +console.log(query); +``` + +### How to Remove Previously Set Request Handlers? + +To remove handlers set using the `response` and `responseOnce` methods, you can use the `removeHandlers` method: + +```typescript +// Create a RequestInterceptor instance +const interceptor = new RequestInterceptor(page, /api/); -// Set a response for every request using a response status and payload -interceptor.response(500, { error: 'Server Error' }); +// Set a response for the request using a response status and payload +interceptor.response(200, { message: 'OK' }); -// Start the request interception +// Start intercepting requests await interceptor.start(); -// Make a request to the intercepted route -const response = await page.goto('https://example.com/api/data?param1=param1&chunkSize=12&id=tttt'); +// Remove all request handlers +interceptor.removeHandlers(); +``` + +After calling `removeHandlers`, the handler set using the `response` method will no longer trigger. -// Log the response status -console.log(response.status()); // 200 +### How to Stop Intercepting Requests? -// Log the response body -console.log(await response.json()); // { message: 'Success' } +To stop intercepting requests, use the `stop` method: -// Stop the request interception -await interceptor.stop(); +```typescript +// Create a RequestInterceptor instance +const interceptor = new RequestInterceptor(page, /api/); -const - providerCalls = interceptor.mock.mock.calls, - query = fromQueryString(new URL((providerCalls[0][0]).request().url()).search); +// Set a response for the request using a response status and payload +interceptor.response(200, { message: 'OK' }); -test.expect(providerCalls).toHaveLength(1); -test.expect(query).toEqual({ - param1: 'param1', - chunkSize: 12, - id: test.expect.anything() -}); +// Start intercepting requests +await interceptor.start(); -``` \ No newline at end of file +// Stop intercepting requests +await interceptor.stop(); +``` diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index 03fe555aee..8cf929592e 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -121,17 +121,6 @@ export class RequestInterceptor { * interceptor.response((r: Route) => r.fulfill({status: 200})); * ``` * - * Sets the response that will occur with a delay to simulate network latency. - * - * @example - * ```typescript - * const interceptor = new RequestInterceptor(page, /api/); - * - * interceptor - * .response(200, {content: 1}, {delay: 200}) - * .response(500, {}, {delay: 300}); - * ``` - * * @param handler - The response handler function. * @returns The current instance of RequestInterceptor. */ @@ -169,7 +158,7 @@ export class RequestInterceptor { * * @returns The current instance of RequestInterceptor. */ - clearResponseOnce(): this { + removeHandlers(): this { this.mock.mockReset(); return this; } From 510f50591540d0786b307c187ad08beb0924eced Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 7 Sep 2023 16:49:28 +0300 Subject: [PATCH 097/159] :art: --- tests/helpers/mock/README.md | 175 ++++++++++++----------------------- 1 file changed, 57 insertions(+), 118 deletions(-) diff --git a/tests/helpers/mock/README.md b/tests/helpers/mock/README.md index 597b1e3d57..269414c3b4 100644 --- a/tests/helpers/mock/README.md +++ b/tests/helpers/mock/README.md @@ -1,159 +1,98 @@ # tests/helpers/mock -This module provides utility functions for working with `jest-mock` and `Playwright`. +This module provides the ability to create spies and mock functions from a Node.js testing environment and inject them into the page context. ## Usage -Import the required functions from the `tests/helpers/mock` module to use the test helpers in your test files: +### How to Create a Spy? -```typescript -import { - wrapAsSpy, - createSpy, - getSpy, - createMockFn, - injectMockIntoPage -} from 'tests/helpers/mock'; -``` +To create a spy, you need to first identify the object you want to spy on. For example, let's say we have a global object `testObject` that has a method `doSomething`, and we want to track how many times this method is called. -## API Reference +Let's break down the steps to achieve this: -### wrapAsSpy +1. Get a `handle` for the `testObject`: -Wraps an object as a spy object by adding additional properties for accessing spy information. + ```typescript + const testObjHandle = await page.evaluateHandle(() => globalThis.testObject); + ``` -```typescript -function wrapAsSpy(agent: JSHandle | ReturnType>, obj: T): T & SpyObject; -``` +2. Set up a spy on the `doSomething` method: -- `agent`: The JSHandle representing the spy or mock function. -- `obj`: The object to wrap as a spy object. -- Returns: The wrapped object with spy properties. + ```typescript + const spy = await createSpy(testObjHandle, (ctx) => jestMock.spy(ctx, 'doSomething')); + ``` -### createSpy + The first argument to the `createSpy` function is the `handle` of the object on which you want to set up the spy. The second argument is the spy constructor function, which takes the object and the method to monitor. `jestMock` is an object available in the global scope that redirects calls to the `jest-mock` library. -Creates a spy object. + It's important to note that the constructor function is passed from the Node.js context to the browser context, meaning it will be serialized and converted into a string for transmission to the browser. -```typescript -async function createSpy( - ctx: T, - spyCtor: (ctx: ExtractFromJSHandle, ...args: ARGS) => ReturnType, - ...argsToCtor: ARGS -): Promise; -``` +3. After setting up the spy, you can access it and, for example, check how many times it has been called: -- `ctx`: The `JSHandle` to spy on. -- `spyCtor`: The function that creates the spy. -- `argsToCtor`: The arguments to pass to the spy constructor function. -- Returns: A promise that resolves to the created spy object. + ```typescript + const testObjHandle = await page.evaluateHandle(() => globalThis.testObject); + const spy = await createSpy(testObjHandle, (ctx) => jestMock.spy(ctx, 'doSomething')); -Usage: + await page.evaluate(() => globalThis.testObject.doSomething()); -```typescript -const ctx = ...; // JSHandle to spy on -const spyCtor = (ctx) => jestMock.spy(ctx, 'prop'); // Spy constructor function -const spy = await createSpy(ctx, spyCtor); - -// Access spy properties -console.log(await spy.calls); -console.log(await spy.callsLength); -console.log(await spy.lastCall); -console.log(await spy.results); -``` - -### getSpy + console.log(await spy.calls); // [[]] + ``` -Retrieves an existing `SpyObject` from a `JSHandle`. - -```typescript -async function getSpy( - ctx: T, - spyExtractor: SpyExtractor, []> -): Promise; -``` +### How to Create a Spy and Access It Later? -- `ctx`: The `JSHandle` containing the spy object. -- `spyExtractor`: The function to extract the spy object. -- Returns: A promise that resolves to the spy object. +There are cases where a spy is created asynchronously, for example, in response to an event. In such situations, you can access the spy later using the `getSpy` function. -Usage: +Let's consider the scenario below where a spy is created for the `testObject.doSomething` method during a button click: ```typescript -const component = await Component.createComponent(page, 'b-button', { - attrs: { - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx.localEmitter, 'emit') - } +await page.evaluate(() => { + const button = document.querySelector('button'); + + button.onclick = () => { + jestMock.spy(globalThis.testObject, 'doSomething'); + globalThis.testObject.doSomething(); + }; }); -const spyExtractor = (ctx) => ctx.unsafe.localEmitter.emit; +await page.click('button'); + +const testObjHandle = await page.evaluateHandle(() => globalThis.testObject); +const spy = await getSpy(testObjHandle, (ctx) => ctx.doSomething); -const spyObject = await getSpy(component, spyExtractor); +await page.evaluate(() => globalThis.testObject.doSomething()); -// Now you can access spy information from the spy object -console.log(await spyObject.calls); -console.log(await spyObject.callsLength); -console.log(await spyObject.lastCall); -console.log(await spyObject.results); +console.log(await spy.calls); // [[], []] ``` -### createMockFn +> While `getSpy` can be replaced with `createSpy`, it is recommended to use `getSpy` for semantic clarity in such cases. -Creates a mock function and injects it into a Page object. +### How to Create a Mock Function? + +To create a mock function, use the `createMockFn` function. It will create a mock function and automatically inject it into the page. ```typescript -async function createMockFn( - page: Page, - fn: (...args: any[]) => any, - ...args: any[] -): Promise; -``` +import { expandedStringify } from 'core/prelude/test-env/components/json'; -- `page`: The Page object to inject the mock function into. -- `fn`: The mock function. -- `args`: The arguments to pass to the function. -- Returns: A promise that resolves to the mock function as a `SpyObject`. +const mockFn = await createMockFn(page, () => 1); -Usage: +await page.evaluate(([obj]) => { + const parsed = globalThis.expandedParse(obj); -```typescript -const page = ...; // Page object -const fn = () => {}; // The mock function -const mockFn = await createMockFn(page, fn); - * -// Access spy properties -console.log(await mockFn.calls); -console.log(await mockFn.callsLength); -console.log(await mockFn.lastCall); -console.log(await mockFn.results); + parsed.mockFn(); + parsed.mockFn(); + +}, [expandedStringify({ mockFn })]); + +console.log(await mockFn.calls); // [[], []] ``` -### injectMockIntoPage +### How Does This Work? -Injects a mock function into a Page object and returns the `SpyObject`. +#### Mock Functions -```typescript -async function injectMockIntoPage( - page: Page, - fn: (...args: any[]) => any, - ...args: any[] -): Promise<{agent: SpyObject; id: string}>; -``` +A mock function works by converting object representations into strings and then transferring them from Node.js to the browser. For the client, `createMockFn` returns a `SpyObject`, which includes methods for tracking calls, and it also overrides the `toJSON` method. This override is necessary to create a mapping of the mock function's ID to the real function that was previously inserted into the page context. -- `page`: The Page object to inject the mock function into. -- `fn`: The mock function. -- `args`: The arguments to pass to the function. -- Returns: A promise that resolves to an object containing the spy object and the ID of the injected mock function. +#### Spy Functions -Usage: +Unlike mock functions, spy functions do not create anything extra. They are simply attached to a function within the context and return a wrapper with methods that, when called, make requests to the spy for various data. -```typescript -const page = ...; // Page object -const fn = () => {}; // The mock function -const { agent, id } = await injectMockIntoPage(page, fn); - -// Access spy properties -console.log(await agent.calls); -console.log(await agent.callsLength); -console.log(await agent.lastCall); -console.log(await agent.results); -``` \ No newline at end of file +If you have any further questions or need assistance, please feel free to ask. From 8bab5fc1b01b5dd2bff8751a936c4d57c14666f3 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 7 Sep 2023 17:07:17 +0300 Subject: [PATCH 098/159] Added toc --- tests/helpers/mock/README.md | 15 +++++++++++++++ tests/helpers/providers/interceptor/README.md | 17 +++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/tests/helpers/mock/README.md b/tests/helpers/mock/README.md index 269414c3b4..46ccecb386 100644 --- a/tests/helpers/mock/README.md +++ b/tests/helpers/mock/README.md @@ -1,3 +1,18 @@ + + +***Table of Contents* + +- [tests/helpers/mock](#testshelpersmock) + - [Usage](#usage) + - [How to Create a Spy?](#how-to-create-a-spy) + - [How to Create a Spy and Access It Later?](#how-to-create-a-spy-and-access-it-later) + - [How to Create a Mock Function?](#how-to-create-a-mock-function) + - [How Does This Work?](#how-does-this-work) + - [Mock Functions](#mock-functions) + - [Spy Functions](#spy-functions) + + + # tests/helpers/mock This module provides the ability to create spies and mock functions from a Node.js testing environment and inject them into the page context. diff --git a/tests/helpers/providers/interceptor/README.md b/tests/helpers/providers/interceptor/README.md index 06a21b9080..9559f933dd 100644 --- a/tests/helpers/providers/interceptor/README.md +++ b/tests/helpers/providers/interceptor/README.md @@ -1,3 +1,20 @@ + + +**Table of Contents** + +- [tests/helpers/providers/interceptor](#testshelpersprovidersinterceptor) + - [Usage](#usage) + - [How to Initialize a Request Interceptor?](#how-to-initialize-a-request-interceptor) + - [How to Respond to a Request Once?](#how-to-respond-to-a-request-once) + - [How to Implement a Delay Before Responding?](#how-to-implement-a-delay-before-responding) + - [How to Set a Custom Request Handler?](#how-to-set-a-custom-request-handler) + - [How to View the Number of Intercepted Requests?](#how-to-view-the-number-of-intercepted-requests) + - [How to View the Parameters of Intercepted Requests?](#how-to-view-the-parameters-of-intercepted-requests) + - [How to Remove Previously Set Request Handlers?](#how-to-remove-previously-set-request-handlers) + - [How to Stop Intercepting Requests?](#how-to-stop-intercepting-requests) + + + # tests/helpers/providers/interceptor This API allows you to intercept any request and respond to it with custom data. From e2c41b1caf979efe26d42e80cf044652fceec5dd Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 7 Sep 2023 17:20:58 +0300 Subject: [PATCH 099/159] :art: --- src/components/base/b-virtual-scroll/interface/component.ts | 2 +- .../base/b-virtual-scroll/modules/factory/engines/vdom.ts | 2 +- src/components/base/b-virtual-scroll/props.ts | 2 +- .../base/b-virtual-scroll/test/api/helpers/index.ts | 5 ++++- .../b-virtual-scroll/test/unit/functional/state/default.ts | 3 +++ 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts index 82169b9cb1..89ab02c9b5 100644 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -209,7 +209,7 @@ export interface ComponentItemMeta extends Dictionary { * If `iItems` props are used to create representations, `b-virtual-scroll` will automatically add * this property to the `meta` parameters. */ - readonly _data?: unknown; + readonly data?: unknown; } /** diff --git a/src/components/base/b-virtual-scroll/modules/factory/engines/vdom.ts b/src/components/base/b-virtual-scroll/modules/factory/engines/vdom.ts index 6313ee50d6..df4beb229a 100644 --- a/src/components/base/b-virtual-scroll/modules/factory/engines/vdom.ts +++ b/src/components/base/b-virtual-scroll/modules/factory/engines/vdom.ts @@ -20,5 +20,5 @@ export function render(ctx: bVirtualScroll, items: VNodeDescriptor[]): HTMLEleme vnodes = ctx.vdom.create(...items), nodes = ctx.vdom.render(vnodes); - return nodes.slice(1, nodes.length - 1); + return nodes; } diff --git a/src/components/base/b-virtual-scroll/props.ts b/src/components/base/b-virtual-scroll/props.ts index 8e02b5aa1c..22150de22f 100644 --- a/src/components/base/b-virtual-scroll/props.ts +++ b/src/components/base/b-virtual-scroll/props.ts @@ -128,7 +128,7 @@ export default abstract class iVirtualScrollProps extends iData { type: Object.isFunction(ctx.itemType) ? ctx.itemType(data, i) : ctx.itemType, meta: { - _data: data, + data, ...Object.isFunction(ctx.itemMeta) ? ctx.itemMeta(data, i) : ctx.itemMeta }, diff --git a/src/components/base/b-virtual-scroll/test/api/helpers/index.ts b/src/components/base/b-virtual-scroll/test/api/helpers/index.ts index a9ad2f3b26..4ef5eca361 100644 --- a/src/components/base/b-virtual-scroll/test/api/helpers/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/helpers/index.ts @@ -270,7 +270,10 @@ export function createMountedItem(data: IndexedObj): MountedItem { key: Object.cast(undefined), item: 'section', type: 'item', - node: test.expect.anything() + node: test.expect.anything(), + meta: { + data: test.expect.any(Object) + } }; } diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts index 2c63cef0d0..c096803c48 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts @@ -160,6 +160,9 @@ test.describe('', () => { type: 'item', props: { 'data-index': item.i + }, + meta: { + data: item } })); From 78b3a8c03458762c35238d09adade2651e187209 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 8 Sep 2023 15:19:16 +0300 Subject: [PATCH 100/159] :wrench: --- .../base/b-virtual-scroll/README.md | 10 +-- .../base/b-virtual-scroll/b-virtual-scroll.ss | 2 +- .../base/b-virtual-scroll/b-virtual-scroll.ts | 83 +++++++++---------- src/components/base/b-virtual-scroll/props.ts | 23 +++-- .../test/unit/lifecycle/slots/slots.ts | 2 +- .../i-lock-page-scroll/test/unit/desktop.ts | 2 +- tests/helpers/component-object/builder.ts | 27 ++---- tests/helpers/component/index.ts | 48 +++++++++++ tests/helpers/component/interface.ts | 18 ++++ 9 files changed, 130 insertions(+), 85 deletions(-) create mode 100644 tests/helpers/component/interface.ts diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 8580ab445d..21797eb28f 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -36,7 +36,7 @@ - [`requestQuery`](#requestquery) - [`itemsFactory`](#itemsfactory) - [`itemsProcessors`](#itemsprocessors) - - [`tombstonesSize`](#tombstonessize) + - [`tombstoneCount`](#tombstoneCount) - [Methods](#methods) - [getNextDataSlice](#getnextdataslice) - [getComponentState](#getcomponentstate) @@ -786,10 +786,10 @@ The component supports several slots for customization: Data loading in progress ``` -2. The `tombstone` slot allows you to display different content (usually skeletons) that will be repeated `tombstonesSize` times while the data is being loaded. +2. The `tombstone` slot allows you to display different content (usually skeletons) that will be repeated `tombstoneCount` times while the data is being loaded. ``` -< b-virtual-scroll :tombstonesSize = 3 +< b-virtual-scroll :tombstoneCount = 3 < template #tombstone < .&__skeleton Skeleton @@ -999,13 +999,13 @@ This prop is a middleware function that is called after `b-virtual-scroll` has c This function can be useful in cases where you need to implement some processing of the abstract representation of components, such as mutating props or adding additional components. -#### `tombstonesSize` +#### `tombstoneCount` - Type: `number` - Default: `undefined` Specifies the number of times the `tombstone` component will be rendered. This prop can be useful if you want to render multiple `tombstone` components using a single specified element. -For example, if you set `tombstonesSize` to 3, then three `tombstone` components will be rendered on your page. +For example, if you set `tombstoneCount` to 3, then three `tombstone` components will be rendered on your page. Note: The `tombstone` component is used to represent empty or unloaded components in the virtual scroll. It is rendered as a placeholder until the actual component data is loaded and rendered. diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ss b/src/components/base/b-virtual-scroll/b-virtual-scroll.ss index 3c2f2d4590..b218ad189b 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ss +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ss @@ -19,7 +19,7 @@ ref = tombstones | v-if = $slots['tombstone'] . - < .&__tombstone v-for = i in tombstonesSize || chunkSize + < .&__tombstone v-for = i in tombstoneCount || chunkSize += self.slot('tombstone') < .&__loader & diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts index 2df78cf980..474518fa1f 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts @@ -36,13 +36,6 @@ const $$ = symbolGenerator(); VDOM.addToPrototype({create, render}); -/** - * Component that implements loading and rendering of large data arrays in chunks. - * The `bVirtualScroll` component extends the `iData` class and implements the `iItems` interface. - * - * It provides functionality for efficiently loading and displaying large amounts of data - * by dynamically rendering chunks of data as the user scrolls. - */ @component() export default class bVirtualScroll extends iVirtualScrollHandlers implements iItems { @@ -82,41 +75,6 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI return Object.cast(this); } - override reload(...args: Parameters): ReturnType { - this.componentStatus = 'loading'; - return super.reload(...args); - } - - override initLoad(...args: Parameters): ReturnType { - if (!this.lfc.isBeforeCreate()) { - this.reset(); - } - - this.componentInternalState.setIsLoadingInProgress(true); - - const - initLoadResult = super.initLoad(...args); - - this.onDataLoadStart(true); - - if (Object.isPromise(initLoadResult)) { - initLoadResult - .then(() => { - if (this.db == null) { - return; - } - - this.onDataLoadSuccess(true, this.db); - }) - .catch(stderr); - - } else { - this.onDataLoadSuccess(true, this.db); - } - - return initLoadResult; - } - /** * Initializes the loading of the next data chunk. * @throws {@link ReferenceError} if there is no `dataProvider` set. @@ -187,6 +145,41 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI this.chunkSize; } + override reload(...args: Parameters): ReturnType { + this.componentStatus = 'loading'; + return super.reload(...args); + } + + override initLoad(...args: Parameters): ReturnType { + if (!this.lfc.isBeforeCreate()) { + this.reset(); + } + + this.componentInternalState.setIsLoadingInProgress(true); + + const + initLoadResult = super.initLoad(...args); + + this.onDataLoadStart(true); + + if (Object.isPromise(initLoadResult)) { + initLoadResult + .then(() => { + if (this.db == null) { + return; + } + + this.onDataLoadSuccess(true, this.db); + }) + .catch(stderr); + + } else { + this.onDataLoadSuccess(true, this.db); + } + + return initLoadResult; + } + protected override convertDataToDB(data: unknown): O | this['DB'] { this.onConvertDataToDB(data); const result = super.convertDataToDB(data); @@ -368,12 +361,12 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI {renderPage} = this.getComponentState(), asyncGroup = `${bVirtualScrollDomInsertAsyncGroup}:${renderPage}`; - for (let i = 0; i < nodes.length; i++) { - this.dom.appendChild(fragment, nodes[i], { + nodes.forEach((node) => { + this.dom.appendChild(fragment, node, { group: asyncGroup, destroyIfComponent: true }); - } + }); this.async.requestAnimationFrame(() => { this.$refs.container.appendChild(fragment); diff --git a/src/components/base/b-virtual-scroll/props.ts b/src/components/base/b-virtual-scroll/props.ts index 22150de22f..588a03d329 100644 --- a/src/components/base/b-virtual-scroll/props.ts +++ b/src/components/base/b-virtual-scroll/props.ts @@ -79,16 +79,16 @@ export default abstract class iVirtualScrollProps extends iData { * Specifies the number of times the `tombstone` component will be rendered. * * This prop can be useful if you want to render multiple `tombstone` components - * using a single specified element. For example, if you set `tombstonesSize` to 3, + * using a single specified element. For example, if you set `tombstoneCount` to 3, * then three `tombstone` components will be rendered on your page. */ @prop(Number) - readonly tombstonesSize?: number; + readonly tombstoneCount?: number; /** * This factory function is used to pass information about the components that need to be rendered. * The function should return an array of arbitrary length consisting of objects that satisfy the - * `ComponentItem` interface. + * {@link ComponentItem} interface. * * By default, the rendering strategy is based on the `chunkSize` and `iItems` trait. * In other words, the default implementation takes a data slice of length `chunkSize` @@ -231,9 +231,9 @@ export default abstract class iVirtualScrollProps extends iData { override readonly DB!: ComponentDb; /** - * Function that returns the GET parameters for a request. This function is called for each request. It receives the + * A function that returns the GET parameters for a request. This function is called for each request. It receives the * current component state and should return the request parameters. These parameters are merged with the parameters - * from the `request` prop in favor of the `request` prop. + * from the `request` prop in favor of the second one. * * This function is useful when you need to pass pagination parameters or any other parameters that should not trigger * a component's state reload, unlike changing the `request` prop. @@ -268,7 +268,7 @@ export default abstract class iVirtualScrollProps extends iData { readonly chunkSize: number | ShouldPerform = 10; /** - * When this function returns `true` the component will stop to request new data. + * When this function returns true the component will stop to request new data. * This function will be called on each data loading cycle. */ @prop({ @@ -279,7 +279,7 @@ export default abstract class iVirtualScrollProps extends iData { readonly shouldStopRequestingData!: ShouldPerform; /** - * When this function returns `true` the component will be able to request additional data. + * When this function returns true the component will be able to request additional data. * This function will be called each time a new element enters the viewport. */ @prop({ @@ -296,7 +296,7 @@ export default abstract class iVirtualScrollProps extends iData { * should determine whether the component should render the next chunk of components. * * For example, if we want to render the next data chunk only when the client - * has seen all the main (type=item) components, we can implement the following function: + * has seen all the main (`type=item`) components, we can implement the following function: * * @example * ```typescript @@ -309,10 +309,9 @@ export default abstract class iVirtualScrollProps extends iData { readonly shouldPerformDataRender?: ShouldPerform; /** - * If `true`, the element {@link Observer observation module} will not be initialized. - * - * Setting this prop to `true` can be useful if you want to implement lazy rendering - * and control it using the `renderNext` method. + * Setting this property to false will disable the {@link Observer observation module}. This is useful when you + * want to implement lazy rendering not based on scrolling but on some other event, such as a click. In this case, + * you should use manual invocation of the `initLoadNext` method to render chunks. */ @prop(Boolean) readonly disableObserver: boolean = false; diff --git a/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts b/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts index ffe76223b5..67b31d5f9d 100644 --- a/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts +++ b/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts @@ -72,7 +72,7 @@ test.describe('', () => { }); await component.withProps({ - tombstonesSize: 1 + tombstoneCount: 1 }); }); diff --git a/src/components/traits/i-lock-page-scroll/test/unit/desktop.ts b/src/components/traits/i-lock-page-scroll/test/unit/desktop.ts index 626caa63b4..68a533c24c 100644 --- a/src/components/traits/i-lock-page-scroll/test/unit/desktop.ts +++ b/src/components/traits/i-lock-page-scroll/test/unit/desktop.ts @@ -20,7 +20,7 @@ test.describe('components/traits/i-lock-page-scroll - desktop', () => { test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); await Component.waitForComponentTemplate(page, 'b-traits-i-lock-page-scroll-dummy'); - target = await Component.createComponent(page, 'b-traits-i-lock-page-scroll-dummy'); + target = await Component.createComponent(page, 'b-traits-i-lock-page-scroll-dummy', undefined); }); test.describe('lock', () => { diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index b2da0523af..4e1aee1cde 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -13,12 +13,10 @@ import { resolve } from '@pzlr/build-core'; import { Component, DOM, Utils } from 'tests/helpers'; import type ComponentObject from 'tests/helpers/component-object'; -import { expandedStringify } from 'core/prelude/test-env/components/json'; - import type iBlock from 'components/super/i-block/i-block'; -import type bDummy from 'components/dummies/b-dummy/b-dummy'; import type { BuildOptions } from 'tests/helpers/component-object/interface'; +import type { ComponentInDummy } from 'tests/helpers/component/interface'; /** * A class implementing the `ComponentObject` approach that encapsulates different @@ -77,7 +75,7 @@ export default abstract class ComponentObjectBuilder { /** * Reference to the `b-dummy` wrapper component. */ - protected dummy?: JSHandle; + protected dummy?: ComponentInDummy; /** * The component styles that should be inserted into the page. @@ -163,14 +161,10 @@ export default abstract class ComponentObjectBuilder { } if (options?.useDummy) { - this.dummy = await Component.createComponent(this.page, 'b-dummy', { - attrs: { - functional: false - } - }); + const component = await Component.createComponentInDummy(this.page, this.componentName, this.props); - await this.updatePropsViaDummy(this.props); - this.componentStore = await this.dummy.evaluateHandle((ctx) => ctx.unsafe.$refs.testComponent); + this.dummy = component; + this.componentStore = component; } else { this.componentStore = await Component.createComponent(this.page, this.componentName, { @@ -267,18 +261,11 @@ export default abstract class ComponentObjectBuilder { * * @throws {@link ReferenceError} - if the component object was not built or was built without the `useDummy` option */ - async updatePropsViaDummy(props: Dictionary): Promise { + updatePropsViaDummy(props: Dictionary): Promise { if (!this.dummy) { throw new ReferenceError('Failed to update props. Missing "b-dummy" component.'); } - const - serializedAttrs = expandedStringify(props); - - return this.dummy.evaluate((ctx, [attrs, componentName]) => { - Object.assign(ctx.testComponentAttrs, globalThis.expandedParse(attrs)); - ctx.testComponent = componentName; - - }, [serializedAttrs, this.componentName]); + return this.dummy.setProps(props); } } diff --git a/tests/helpers/component/index.ts b/tests/helpers/component/index.ts index 532ccf1ac2..6514e5ad5e 100644 --- a/tests/helpers/component/index.ts +++ b/tests/helpers/component/index.ts @@ -13,6 +13,8 @@ import { expandedStringify } from 'core/prelude/test-env/components/json'; import type iBlock from 'components/super/i-block/i-block'; import { isRenderComponentsVnodeParams } from 'tests/helpers/component/helpers'; +import type { ComponentInDummy } from 'tests/helpers/component/interface'; +import type bDummy from 'components/dummies/b-dummy/b-dummy'; /** * Class provides API to work with components on a page @@ -109,6 +111,52 @@ export default class Component { return this.waitForComponentByQuery(page, `[data-render-id="${renderId}"]`); } + /** + * Creates a component inside the `b-dummy` component and uses the `field-like` property of `b-dummy` + * to pass props to the inner component. + * + * This function can be useful when you need to test changes to component props. + * Since component props are readonly properties, you cannot change them directly; + * changes are only available through the parent component. This is why the `b-dummy` wrapper is created, + * and the props for the component you want to render are passed as references to the property of `b-dummy`. + * + * The function returns a `handle` to the created component (not to `b-dummy`) + * and adds a method and property for convenience: + * + * - `setProps` - a method that allows you to modify the component's props. + * + * - `dummy` - the `handle` of the `b-dummy` component. + * + * @param page + * @param componentName + * @param attrs + */ + static async createComponentInDummy( + page: Page, + componentName: string, + attrs: RenderComponentsVnodeParams['attrs'] + ): Promise> { + const dummy = await this.createComponent(page, 'b-dummy'); + + const setProps = async (props) => { + await dummy.evaluate((ctx, [name, props]) => { + ctx.testComponentAttrs = globalThis.expandedParse(props); + ctx.testComponent = name; + + }, [componentName, expandedStringify(props)]); + }; + + await setProps(attrs); + const component = await dummy.evaluateHandle((ctx) => ctx.unsafe.$refs.testComponent); + + Object.assign(component, { + setProps, + dummy + }); + + return >component; + } + /** * Removes all dynamically created components * @param page diff --git a/tests/helpers/component/interface.ts b/tests/helpers/component/interface.ts new file mode 100644 index 0000000000..700c5562d1 --- /dev/null +++ b/tests/helpers/component/interface.ts @@ -0,0 +1,18 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type bDummy from 'components/dummies/b-dummy/b-dummy'; +import type { JSHandle } from 'playwright'; + +/** + * Handle component interface that was created with a dummy wrapper. + */ +export interface ComponentInDummy extends JSHandle { + setProps(props: Dictionary): Promise; + dummy: JSHandle; +} From 0490e716d82e2b3d0035c6b13a9cee8480b674ec Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 11 Sep 2023 11:30:23 +0300 Subject: [PATCH 101/159] :art: --- .../base/b-virtual-scroll/README.md | 24 +++++++--- .../base/b-virtual-scroll/b-virtual-scroll.ts | 45 +++++++++---------- .../base/b-virtual-scroll/handlers.ts | 6 +-- .../b-virtual-scroll/interface/component.ts | 2 +- .../b-virtual-scroll/modules/factory/index.ts | 4 +- .../test/api/component-object/index.ts | 6 +-- .../test/unit/functional/state/default.ts | 10 ++--- .../test/unit/functional/state/emitter.ts | 4 +- src/core/prelude/test-env/mock/README.md | 3 ++ 9 files changed, 59 insertions(+), 45 deletions(-) create mode 100644 src/core/prelude/test-env/mock/README.md diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 21797eb28f..25aae242a5 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -39,7 +39,7 @@ - [`tombstoneCount`](#tombstoneCount) - [Methods](#methods) - [getNextDataSlice](#getnextdataslice) - - [getComponentState](#getcomponentstate) + - [getVirtualScrollState](#getVirtualScrollState) - [initLoadNext](#initloadnext) - [Other Properties](#other-properties) - [Migration from `b-virtual-scroll` version 3.x.x](#migration-from-b-virtual-scroll-version-3xx) @@ -329,7 +329,7 @@ This makes it straightforward to implement a retry mechanism for a failed reques The `b-virtual-scroll` component is quite substantial and has its own internal state that complements the component's state. This internal state is reset when the component is reinitialized to its initial state and changes regularly during the component's lifecycle. The component's state contains a wealth of information useful for the client, such as the loaded data, the number of elements remaining outside the user's viewport, and more. -To retrieve the component's state, you can use a special method called `getComponentState`: +To retrieve the component's state, you can use a special method called `getVirtualScrollState`: __p-page.ts__ ```typescript @@ -340,7 +340,7 @@ class pPage extends extends iDynamicPage { }; getScrollState(): VirtualScrollState { - return this.$refs.scroll.getComponentState(); + return this.$refs.scroll.getVirtualScrollState(); } } ``` @@ -747,6 +747,18 @@ There may also be situations where you need to modify the `renderGuard`. Current Yes, you can. `b-virtual-scroll` will make requests (one at a time!) until the number of loaded items is greater than or equal to the value specified in `chunkSize`. +- Can I render a different number of items on each render cycle? + + Yes, you can. `b-virtual-scroll` provides two options: + + 1. Specify the `chunkSize` prop as a function that returns a number depending on something. Let’s say we want to render 6 elements at the first render, 12 at the second, and 18 in subsequent ones: + + ```typescript + const chunkSize = (state: VirtualScrollState) => [6, 12, 18][state.renderPage] ?? 18 + ``` + + 2. Use the `itemsFactory` prop and return any number of elements from this function. + - Suppose I want to load 1000 data items once and not make any more requests. How can I achieve this? 1. Set `chunkSize` to a suitable value, for example, 10, if you want 10 components to be rendered in one rendering cycle. @@ -1015,7 +1027,7 @@ Note: The `tombstone` component is used to represent empty or unloaded component Returns the next data slice that should be rendered based on the `chunkSize`. -#### getComponentState +#### getVirtualScrollState Returns the current state of the component. @@ -1033,14 +1045,14 @@ The `bVirtualScroll` class extends `iData` and includes additional properties re - Prop `renderGap` deleted -> use `shouldPerformDataRender`; - Deprecated props `option-like` deleted -> use `iItems` props; -- Method renamed `getDataStateSnapshot` -> `getComponentState`; +- Method renamed `getDataStateSnapshot` -> `getVirtualScrollState`; - Method `reloadLast` -> `initLoadNext`; - `VirtualItemEl` interface is removed. Now, the client receives a single data item in the `iItems` methods. To maintain logic with `current`, `prev`, `next`, you can use the following approach: ```typescript function getProps(data: DataInterface, index: number): Dictionary { const - state = this.$refs.scroll.getComponentState(); + state = this.$refs.scroll.getVirtualScrollState(); const current = data, diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts index 474518fa1f..d72bf27977 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts @@ -65,7 +65,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI override get requestParams(): iData['requestParams'] { return { get: { - ...this.requestQuery?.(this.getComponentState())?.get, + ...this.requestQuery?.(this.getVirtualScrollState())?.get, ...Object.isDictionary(this.request?.get) ? this.request?.get : undefined } }; @@ -85,7 +85,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI } const - state = this.getComponentState(); + state = this.getVirtualScrollState(); if (state.isLoadingInProgress) { return; @@ -113,10 +113,10 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI } /** - * Returns the component state. + * Returns the internal component state. * {@link VirtualScrollState} */ - getComponentState(): Readonly { + getVirtualScrollState(): Readonly { return this.componentInternalState.compile(); } @@ -128,16 +128,15 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI */ getNextDataSlice(state: VirtualScrollState, chunkSize: number): object[] { const - {data} = state, nextDataSliceStartIndex = this.componentInternalState.getDataCursor(), nextDataSliceEndIndex = nextDataSliceStartIndex + chunkSize; - return data.slice(nextDataSliceStartIndex, nextDataSliceEndIndex); + return state.data.slice(nextDataSliceStartIndex, nextDataSliceEndIndex); } /** * Returns the chunk size that should be rendered. - * @param state + * @param state - Current lifecycle state. */ getChunkSize(state: VirtualScrollState): number { return Object.isFunction(this.chunkSize) ? @@ -188,7 +187,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI } /** - * Gathers all request parameters from the component fields `requestProp` and `requestQuery`. + * Merges all request parameters from the component fields `requestProp` and `requestQuery`. * {@link RequestParams} */ protected getRequestParams(): RequestParams { @@ -208,10 +207,12 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI } /** - * Wrapper for {@link bVirtualScroll.shouldStopRequestingData}. + * Short-hand wrapper for calling {@link bVirtualScroll.shouldStopRequestingData}, which also caches the + * result of the call and, if {@link bVirtualScroll.shouldStopRequestingData} returns `true`, does not call + * this function again until the life cycle is updated and the state is reset. */ - protected shouldStopRequestingDataWrapper(this: bVirtualScroll): boolean { - const state = this.getComponentState(); + protected shouldStopRequestingDataWrapper(): boolean { + const state = this.getVirtualScrollState(); if (state.areRequestsStopped) { return state.areRequestsStopped; @@ -224,10 +225,11 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI } /** - * Wrapper for {@link bVirtualScroll.shouldPerformDataRequest}. + * Short-hand wrapper for calling {@link bVirtualScroll.shouldPerformDataRequest}, removing the need to pass + * state and context when calling {@link bVirtualScroll.shouldPerformDataRequest}. */ - protected shouldPerformDataRequestWrapper(this: bVirtualScroll): boolean { - return this.shouldPerformDataRequest(this.getComponentState(), this); + protected shouldPerformDataRequestWrapper(): boolean { + return this.shouldPerformDataRequest(this.getVirtualScrollState(), this); } /** @@ -238,10 +240,9 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI } /** - * This function is called after successful data loading or when the child components enters the visible area. - * - * This function asks the client whether rendering can be performed. The client responds with an object - * indicating whether rendering is allowed or the reason for denial. + * This function asks the client whether rendering can be performed. + * It is called after successful data load or when the child component enters the visible area. + * The client responds with an object indicating whether rendering is allowed or the reason for denial. * * Based on the result of this function, the component takes appropriate actions. For example, * it may load data if it is not sufficient for rendering, or perform rendering if all conditions are met. @@ -285,7 +286,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI return { result: clientResponse, - reason: clientResponse === false ? renderGuardRejectionReason.noPermission : undefined + reason: !clientResponse ? renderGuardRejectionReason.noPermission : undefined }; } @@ -298,7 +299,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI */ protected loadDataOrPerformRender(): void { const - state = this.getComponentState(); + state = this.getVirtualScrollState(); if (state.isLastErrored) { return; @@ -342,8 +343,6 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI /** * Renders components using {@link bVirtualScroll.componentFactory} and inserts them into the DOM tree. - * {@link bVirtualScroll.componentFactory}, in turn, calls {@link bVirtualScroll.itemsFactory} to obtain - * the set of components to render. */ protected performRender(): void { this.onRenderStart(); @@ -358,7 +357,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI const fragment = document.createDocumentFragment(), - {renderPage} = this.getComponentState(), + {renderPage} = this.getVirtualScrollState(), asyncGroup = `${bVirtualScrollDomInsertAsyncGroup}:${renderPage}`; nodes.forEach((node) => { diff --git a/src/components/base/b-virtual-scroll/handlers.ts b/src/components/base/b-virtual-scroll/handlers.ts index 60d1fdb816..1eb006d1e9 100644 --- a/src/components/base/b-virtual-scroll/handlers.ts +++ b/src/components/base/b-virtual-scroll/handlers.ts @@ -96,9 +96,9 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { */ protected onLifecycleDone(this: bVirtualScroll): void { const - state = this.getComponentState(); + state = this.getVirtualScrollState(); - if (state.isLifecycleDone === true) { + if (state.isLifecycleDone) { return; } @@ -193,7 +193,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { } const - state = this.getComponentState(); + state = this.getVirtualScrollState(); this.onDataLoadError(state.isInitialLoading); return super.onRequestError(err, this.initLoad.bind(this)); diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts index 89ab02c9b5..6b6c17e371 100644 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -9,7 +9,7 @@ import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; /** - * Component state. + * State of the current component lifecycle. * * @typeParam DATA - Instance of the data element. * @typeParam RAW_DATA - The data loaded from the server but not yet processed. diff --git a/src/components/base/b-virtual-scroll/modules/factory/index.ts b/src/components/base/b-virtual-scroll/modules/factory/index.ts index b2c8d20759..0e2c19efc6 100644 --- a/src/components/base/b-virtual-scroll/modules/factory/index.ts +++ b/src/components/base/b-virtual-scroll/modules/factory/index.ts @@ -29,7 +29,7 @@ export class ComponentFactory extends Friend { const {ctx} = this; - return this.itemsProcessor(ctx.itemsFactory(ctx.getComponentState(), ctx)); + return this.itemsProcessor(ctx.itemsFactory(ctx.getVirtualScrollState(), ctx)); } /** @@ -58,7 +58,7 @@ export class ComponentFactory extends Friend { produceMounted(items: ComponentItem[], nodes: HTMLElement[]): Array { const {ctx} = this, - {items: mountedItems, childList} = ctx.getComponentState(); + {items: mountedItems, childList} = ctx.getVirtualScrollState(); return items.map((item, i) => { if (isItem(item)) { diff --git a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts index 019538c6ed..2b2a16c51e 100644 --- a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts @@ -54,8 +54,8 @@ export class VirtualScrollComponentObject extends ComponentObject { - return this.component.evaluate((ctx) => ctx.getComponentState()); + getVirtualScrollState(): Promise { + return this.component.evaluate((ctx) => ctx.getVirtualScrollState()); } /** @@ -116,7 +116,7 @@ export class VirtualScrollComponentObject extends ComponentObject { await this.component.evaluate((ctx) => { - const state = ctx.getComponentState(); + const state = ctx.getVirtualScrollState(); if (state.isLifecycleDone) { return; diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts index c096803c48..306b0ba1e4 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts @@ -35,7 +35,7 @@ test.describe('', () => { test('Initial state', async () => { const chunkSize = 12, - mockFn = await component.mockFn((ctx: bVirtualScroll) => ctx.getComponentState()); + mockFn = await component.mockFn((ctx: bVirtualScroll) => ctx.getVirtualScrollState()); provider.response(200, {data: []}, {delay: (10).seconds()}); @@ -93,7 +93,7 @@ test.describe('', () => { await component.waitForChildCountEqualsTo(chunkSize); const - currentState = await component.getComponentState(); + currentState = await component.getVirtualScrollState(); test.expect(currentState).toEqual(state.compile({ isInitialLoading: false, @@ -119,7 +119,7 @@ test.describe('', () => { await component.waitForLifecycleDone(); const - currentState = await component.getComponentState(); + currentState = await component.getVirtualScrollState(); test.expect(currentState).toEqual(state.compile({ isInitialLoading: false, @@ -193,7 +193,7 @@ test.describe('', () => { await component.waitForLifecycleDone(); const - currentState = await component.getComponentState(); + currentState = await component.getVirtualScrollState(); test.expect(currentState).toEqual(state.compile({ isInitialLoading: false, @@ -245,7 +245,7 @@ test.describe('', () => { await component.waitForLifecycleDone(); const - currentState = await component.getComponentState(); + currentState = await component.getVirtualScrollState(); test.expect(currentState).toEqual(state.compile({ isInitialLoading: false, diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts index 42e2e26826..42242abe29 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts @@ -76,7 +76,7 @@ test.describe('', () => { ctx.emit = jestMock.mock((...args) => { original(...args); - return [args[0], Object.fastClone(ctx.getComponentState())]; + return [args[0], Object.fastClone(ctx.getVirtualScrollState())]; }); } }) @@ -157,7 +157,7 @@ test.describe('', () => { ctx.emit = jestMock.mock((...args) => { original(...args); - return [args[0], Object.fastClone(ctx.getComponentState())]; + return [args[0], Object.fastClone(ctx.getVirtualScrollState())]; }); } }) diff --git a/src/core/prelude/test-env/mock/README.md b/src/core/prelude/test-env/mock/README.md new file mode 100644 index 0000000000..670c53c6c0 --- /dev/null +++ b/src/core/prelude/test-env/mock/README.md @@ -0,0 +1,3 @@ +# core/prelude/test-env/mock + +This module provides an API for accessing `jest-mock`, in fact it is just a proxy between the client and the `jest-mock` API which allows accessing `jest-mock` through the global scope. From f0ef53e5b25ce687e8f30e6a117e5edbb232cfe9 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 11 Sep 2023 12:39:14 +0300 Subject: [PATCH 102/159] :art: --- .../base/b-virtual-scroll/modules/slots/index.ts | 2 +- src/components/base/b-virtual-scroll/props.ts | 14 ++++++++++++++ src/core/prelude/test-env/components/json.ts | 9 +++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/components/base/b-virtual-scroll/modules/slots/index.ts b/src/components/base/b-virtual-scroll/modules/slots/index.ts index 77dcab7ade..26066e4c34 100644 --- a/src/components/base/b-virtual-scroll/modules/slots/index.ts +++ b/src/components/base/b-virtual-scroll/modules/slots/index.ts @@ -128,7 +128,7 @@ export class SlotsStateController extends Friend { * Sets the visibility state of the slots. * * @param stateObj - An object specifying the visibility state of each slot. - * @param [immediate] - if set to true, {@link requestAnimationFrame} will not be used to switch the state. + * @param [immediate] - If set to true, {@link requestAnimationFrame} will not be used to switch the state. */ protected setSlotsVisibility(stateObj: Required, immediate: boolean = false): void { this.lastState = stateObj; diff --git a/src/components/base/b-virtual-scroll/props.ts b/src/components/base/b-virtual-scroll/props.ts index 588a03d329..4a742d8299 100644 --- a/src/components/base/b-virtual-scroll/props.ts +++ b/src/components/base/b-virtual-scroll/props.ts @@ -81,6 +81,20 @@ export default abstract class iVirtualScrollProps extends iData { * This prop can be useful if you want to render multiple `tombstone` components * using a single specified element. For example, if you set `tombstoneCount` to 3, * then three `tombstone` components will be rendered on your page. + * + * @example + * ``` + * < b-virtual-scroll :tombstoneCount = 3 + * < template #tombstone + * < .&__skeleton + * Skeleton + * ``` + * + * ```html + *
Skeleton
+ *
Skeleton
+ *
Skeleton
+ * ``` */ @prop(Number) readonly tombstoneCount?: number; diff --git a/src/core/prelude/test-env/components/json.ts b/src/core/prelude/test-env/components/json.ts index ff935e9bec..996fa70854 100644 --- a/src/core/prelude/test-env/components/json.ts +++ b/src/core/prelude/test-env/components/json.ts @@ -23,6 +23,15 @@ export function evalFn(func: T): T { * Overrides the `toJSON` method of the provided object to return the identifier of a mock function * within the page context. * + * @example + * ``` + * const val1 = JSON.stringify({val: 1}); // '{"val": 1}'; + * const val2 = JSON.stringify(setSerializerAsMockFn({val: 1}, 'id')); // '"id"' + * ``` + * + * This function is needed in order to extract a previously inserted mock function + * into the context of a browser page by its ID. + * * @param obj - The object to override the `toJSON` method for. * @param id - The identifier of the mock function. * @returns The modified object with the overridden `toJSON` method. From d1f37f47a8a50d7cc4a21d1eb513521d374aa413 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 11 Sep 2023 12:57:15 +0300 Subject: [PATCH 103/159] :art: --- src/components/base/b-virtual-scroll/const.ts | 102 ++++++++++++++---- .../base/b-virtual-scroll/interface/common.ts | 27 +---- .../base/b-virtual-scroll/interface/events.ts | 94 +++------------- 3 files changed, 96 insertions(+), 127 deletions(-) diff --git a/src/components/base/b-virtual-scroll/const.ts b/src/components/base/b-virtual-scroll/const.ts index 44510dbcd4..80adb33675 100644 --- a/src/components/base/b-virtual-scroll/const.ts +++ b/src/components/base/b-virtual-scroll/const.ts @@ -8,18 +8,7 @@ import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { - - ComponentDataLocalEvents, - ComponentItemType, - ComponentLifecycleEvents, - ComponentObserverLocalEvents, - ComponentRenderLocalEvents, - VirtualScrollState, - RenderGuardRejectionReason, - ItemsProcessors - -} from 'components/base/b-virtual-scroll/interface'; +import type { ComponentItemType, VirtualScrollState, ItemsProcessors } from 'components/base/b-virtual-scroll/interface'; /** * Base group for performing asynchronous operations of the component. @@ -32,57 +21,124 @@ export const bVirtualScrollAsyncGroup = 'b-virtual-scroll'; export const bVirtualScrollDomInsertAsyncGroup = `${bVirtualScrollAsyncGroup}:dom-insert`; /** - * {@link ComponentDataLocalEvents} + * Component data-related events (emitted in `selfEmitter`). */ -export const componentDataLocalEvents: ComponentDataLocalEvents = { +export const componentDataLocalEvents = { + /** + * Data loading has started. + */ dataLoadStart: 'dataLoadStart', + + /** + * An error occurred while loading data. + */ dataLoadError: 'dataLoadError', + + /** + * Data has been successfully loaded. + */ dataLoadSuccess: 'dataLoadSuccess', + + /** + * Successful load with no data. + */ dataLoadEmpty: 'dataLoadEmpty' }; /** - * {@link ComponentLifecycleEvents} + * Component events. */ -export const componentLocalEvents: ComponentLifecycleEvents = { +export const componentLifecycleEvents = { + /** + * Reset component state. + */ resetState: 'resetState', + + /** + * Trigger data conversion to the `DB`. + */ convertDataToDB: 'convertDataToDB', + + /** + * This event is emitted when all component data is rendered and loaded. + */ lifecycleDone: 'lifecycleDone' }; /** - * {@link ComponentRenderLocalEvents} + * Component rendering events. */ -export const componentRenderLocalEvents: ComponentRenderLocalEvents = { +export const componentRenderLocalEvents = { + /** + * Rendering of items has started. + */ renderStart: 'renderStart', + + /** + * Rendering of items has finished. + */ renderDone: 'renderDone', + + /** + * Rendering of items has started with the render engine. + */ renderEngineStart: 'renderEngineStart', + + /** + * Rendering of items has finished with the render engine. + */ renderEngineDone: 'renderEngineDone', + + /** + * DOM node insertion has started. + */ domInsertStart: 'domInsertStart', + + /** + * DOM node insertion has finished. + */ domInsertDone: 'domInsertDone' }; /** - * {@link ComponentObserverLocalEvents} + * Events of the element observer. */ -export const componentObserverLocalEvents: ComponentObserverLocalEvents = { +export const componentObserverLocalEvents = { + /** + * The element has entered the viewport. + */ elementEnter: 'elementEnter' }; export const componentEvents = { ...componentDataLocalEvents, ...componentRenderLocalEvents, - ...componentLocalEvents, + ...componentLifecycleEvents, ...componentObserverLocalEvents }; /** - * {@link RenderGuardRejectionReason} + * Reasons for rejecting a render operation. */ -export const renderGuardRejectionReason: RenderGuardRejectionReason = { +export const renderGuardRejectionReason = { + /** + * Insufficient data to perform a render (e.g., `data.length` is 5 and `chunkSize` is 12). + */ notEnoughData: 'notEnoughData', + + /** + * No data available to perform a render (e.g., `data.length` is 0). + */ noData: 'noData', + + /** + * All rendering operations have been completed. + */ done: 'done', + + /** + * The client returns `false` in `shouldPerformDataRender`. + */ noPermission: 'noPermission' }; diff --git a/src/components/base/b-virtual-scroll/interface/common.ts b/src/components/base/b-virtual-scroll/interface/common.ts index 24bcc718bd..d2377e09f7 100644 --- a/src/components/base/b-virtual-scroll/interface/common.ts +++ b/src/components/base/b-virtual-scroll/interface/common.ts @@ -7,7 +7,10 @@ */ import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; + +import type { renderGuardRejectionReason } from 'components/base/b-virtual-scroll/b-virtual-scroll'; import type { VirtualScrollState } from 'components/base/b-virtual-scroll/interface/component'; + import type { UnsafeIData } from 'components/super/i-data/i-data'; /** @@ -46,29 +49,9 @@ export interface RenderGuardResult { } /** - * Reasons for rejecting a render operation. + * {@link renderGuardRejectionReason} */ -export interface RenderGuardRejectionReason { - /** - * Insufficient data to perform a render (e.g., `data.length` is 5 and `chunkSize` is 12). - */ - notEnoughData: 'notEnoughData'; - - /** - * No data available to perform a render (e.g., `data.length` is 0). - */ - noData: 'noData'; - - /** - * All rendering operations have been completed. - */ - done: 'done'; - - /** - * The client returns `false` in `shouldPerformDataRender`. - */ - noPermission: 'noPermission'; -} +export type RenderGuardRejectionReason = typeof renderGuardRejectionReason; /** * A function used to query the client about whether to perform a specific action or not. diff --git a/src/components/base/b-virtual-scroll/interface/events.ts b/src/components/base/b-virtual-scroll/interface/events.ts index 6fbca2fe08..719024069a 100644 --- a/src/components/base/b-virtual-scroll/interface/events.ts +++ b/src/components/base/b-virtual-scroll/interface/events.ts @@ -11,101 +11,31 @@ import type { MountedChild } from 'components/base/b-virtual-scroll/interface/co import { componentDataLocalEvents, - componentLocalEvents, + componentLifecycleEvents, componentObserverLocalEvents, componentRenderLocalEvents } from 'components/base/b-virtual-scroll/const'; /** - * Component data-related events (emitted in `selfEmitter`). + * {@link componentDataLocalEvents} */ -export interface ComponentDataLocalEvents { - /** - * Data loading has started. - */ - dataLoadStart: 'dataLoadStart'; - - /** - * An error occurred while loading data. - */ - dataLoadError: 'dataLoadError'; - - /** - * Data has been successfully loaded. - */ - dataLoadSuccess: 'dataLoadSuccess'; - - /** - * Successful load with no data. - */ - dataLoadEmpty: 'dataLoadEmpty'; -} +export type ComponentDataLocalEvents = typeof componentDataLocalEvents; /** - * Component events. + * {@link componentLifecycleEvents} */ -export interface ComponentLifecycleEvents { - /** - * Reset component state. - */ - resetState: 'resetState'; - - /** - * Trigger data conversion to the `DB`. - */ - convertDataToDB: 'convertDataToDB'; - - /** - * This event is emitted when all component data is rendered and loaded. - */ - lifecycleDone: 'lifecycleDone'; -} +export type ComponentLifecycleEvents = typeof componentLifecycleEvents; /** - * Component rendering events. + * {@link componentRenderLocalEvents} */ -export interface ComponentRenderLocalEvents { - /** - * Rendering of items has started. - */ - renderStart: 'renderStart'; - - /** - * Rendering of items has finished. - */ - renderDone: 'renderDone'; - - /** - * Rendering of items has started with the render engine. - */ - renderEngineStart: 'renderEngineStart'; - - /** - * Rendering of items has finished with the render engine. - */ - renderEngineDone: 'renderEngineDone'; - - /** - * DOM node insertion has started. - */ - domInsertStart: 'domInsertStart'; - - /** - * DOM node insertion has finished. - */ - domInsertDone: 'domInsertDone'; -} +export type ComponentRenderLocalEvents = typeof componentRenderLocalEvents; /** - * Events of the element observer. + * {@link componentObserverLocalEvents} */ -export interface ComponentObserverLocalEvents { - /** - * The element has entered the viewport. - */ - elementEnter: 'elementEnter'; -} +export type ComponentObserverLocalEvents = typeof componentObserverLocalEvents; /** * Possible component events. @@ -126,9 +56,9 @@ export interface LocalEventPayloadMap { [componentDataLocalEvents.dataLoadError]: [isInitialLoading: boolean]; [componentDataLocalEvents.dataLoadEmpty]: []; - [componentLocalEvents.resetState]: []; - [componentLocalEvents.lifecycleDone]: []; - [componentLocalEvents.convertDataToDB]: [data: unknown]; + [componentLifecycleEvents.resetState]: []; + [componentLifecycleEvents.lifecycleDone]: []; + [componentLifecycleEvents.convertDataToDB]: [data: unknown]; [componentObserverLocalEvents.elementEnter]: [componentItem: MountedChild]; From 0a6f42930dbef2285fe0dcbe9ef453aa324df2ed Mon Sep 17 00:00:00 2001 From: bonkalol Date: Tue, 12 Sep 2023 15:47:17 +0300 Subject: [PATCH 104/159] :wrench: --- src/core/prelude/webpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/prelude/webpack.ts b/src/core/prelude/webpack.ts index c13193565e..06b95ea067 100644 --- a/src/core/prelude/webpack.ts +++ b/src/core/prelude/webpack.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -/* eslint-disable camelcase,no-new-func */ +/* eslint-disable camelcase */ import global from 'core/shims/global'; From 82dd4a36d52f06ca851cc15cd4255f4f2aeb3edd Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 2 Oct 2023 14:25:06 +0300 Subject: [PATCH 105/159] Fix linter --- .../base/b-virtual-scroll/b-virtual-scroll.ts | 10 ++-- .../base/b-virtual-scroll/handlers.ts | 14 ++--- .../b-virtual-scroll/interface/component.ts | 2 +- .../b-virtual-scroll/interface/requests.ts | 2 +- .../b-virtual-scroll/modules/emitter/index.ts | 2 +- .../modules/emitter/interface.ts | 14 ++--- .../b-virtual-scroll/modules/factory/index.ts | 8 +-- .../b-virtual-scroll/modules/helpers/index.ts | 6 +-- .../modules/observer/index.ts | 8 +-- .../b-virtual-scroll/modules/slots/index.ts | 20 +++---- .../b-virtual-scroll/modules/state/helpers.ts | 4 +- .../b-virtual-scroll/modules/state/index.ts | 44 ++++++++-------- .../test/api/component-object/index.ts | 34 ++++++------ .../test/api/helpers/index.ts | 52 +++++++++---------- .../test/api/helpers/interface.ts | 10 ++-- src/core/prelude/test-env/components/json.ts | 4 +- tests/helpers/component-object/builder.ts | 22 ++++---- tests/helpers/component-object/mock.ts | 12 ++--- tests/helpers/mock/index.ts | 26 +++++----- tests/helpers/mock/interface.ts | 2 +- tests/helpers/providers/interceptor/index.ts | 28 +++++----- 21 files changed, 162 insertions(+), 162 deletions(-) diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts index d72bf27977..b6d53b9ffd 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts @@ -76,7 +76,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI } /** - * Initializes the loading of the next data chunk. + * Initializes the loading of the next data chunk * @throws {@link ReferenceError} if there is no `dataProvider` set. */ initLoadNext(): CanUndef> { @@ -135,8 +135,8 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI } /** - * Returns the chunk size that should be rendered. - * @param state - Current lifecycle state. + * Returns the chunk size that should be rendered + * @param state - current lifecycle state. */ getChunkSize(state: VirtualScrollState): number { return Object.isFunction(this.chunkSize) ? @@ -233,7 +233,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI } /** - * Resets the component state to its initial state. + * Resets the component state to its initial state */ protected reset(): void { this.onReset(); @@ -342,7 +342,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI } /** - * Renders components using {@link bVirtualScroll.componentFactory} and inserts them into the DOM tree. + * Renders components using {@link bVirtualScroll.componentFactory} and inserts them into the DOM tree */ protected performRender(): void { this.onRenderStart(); diff --git a/src/components/base/b-virtual-scroll/handlers.ts b/src/components/base/b-virtual-scroll/handlers.ts index 1eb006d1e9..72869c77d7 100644 --- a/src/components/base/b-virtual-scroll/handlers.ts +++ b/src/components/base/b-virtual-scroll/handlers.ts @@ -111,7 +111,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: convert data to database event. * Triggered when the loaded data is converted. * - * @param data - The converted data. + * @param data - the converted data. */ protected onConvertDataToDB(this: bVirtualScroll, data: unknown): void { this.componentInternalState.setRawLastLoaded(data); @@ -122,7 +122,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: data load start event. * Triggered when data loading starts. * - * @param isInitialLoading - Indicates whether it is an initial component loading. + * @param isInitialLoading - indicates whether it is an initial component loading. */ protected onDataLoadStart(this: bVirtualScroll, isInitialLoading: boolean): void { this.componentInternalState.setIsLoadingInProgress(true); @@ -136,8 +136,8 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: data load success event. * Triggered when data loading is successfully completed. * - * @param isInitialLoading - Indicates whether it is an initial component loading. - * @param data - The loaded data. + * @param isInitialLoading - indicates whether it is an initial component loading. + * @param data - the loaded data. * @throws {@link ReferenceError} if the loaded data does not have a "data" field. */ protected onDataLoadSuccess(this: bVirtualScroll, isInitialLoading: boolean, data: unknown): void { @@ -174,7 +174,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Handler: data load error event. * Triggered when data loading fails. * - * @param isInitialLoading - Indicates whether it is an initial component loading. + * @param isInitialLoading - indicates whether it is an initial component loading. */ protected onDataLoadError(this: bVirtualScroll, isInitialLoading: boolean): void { this.componentInternalState.setIsLoadingInProgress(false); @@ -210,8 +210,8 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { } /** - * Handler: component enters the viewport. - * @param component - The component that enters the viewport. + * Handler: component enters the viewport + * @param component - the component that enters the viewport. */ protected onElementEnters(this: bVirtualScroll, component: MountedChild): void { this.componentInternalState.setMaxViewedIndex(component); diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts index 6b6c17e371..b786f7273c 100644 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -12,7 +12,7 @@ import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scro * State of the current component lifecycle. * * @typeParam DATA - Instance of the data element. - * @typeParam RAW_DATA - The data loaded from the server but not yet processed. + * @typeParam RAW_DATA - the data loaded from the server but not yet processed. * This type parameter determines the type of the {@link VirtualScrollState.lastLoadedRawData} property */ export interface VirtualScrollState { diff --git a/src/components/base/b-virtual-scroll/interface/requests.ts b/src/components/base/b-virtual-scroll/interface/requests.ts index c28d947626..4d39dd224f 100644 --- a/src/components/base/b-virtual-scroll/interface/requests.ts +++ b/src/components/base/b-virtual-scroll/interface/requests.ts @@ -16,7 +16,7 @@ export interface RequestQueryFn { /** * Returns the GET parameters for a request. * - * @param state - The component state. + * @param state - the component state. */ (state: VirtualScrollState): Dictionary; } diff --git a/src/components/base/b-virtual-scroll/modules/emitter/index.ts b/src/components/base/b-virtual-scroll/modules/emitter/index.ts index 56cec3a17f..3fa5f7ba70 100644 --- a/src/components/base/b-virtual-scroll/modules/emitter/index.ts +++ b/src/components/base/b-virtual-scroll/modules/emitter/index.ts @@ -15,7 +15,7 @@ import type { ComponentTypedEmitter } from 'components/base/b-virtual-scroll/mod export * from 'components/base/b-virtual-scroll/modules/emitter/interface'; /** - * Provides methods for interacting with the `selfEmitter` using typed events. + * Provides methods for interacting with the `selfEmitter` using typed events * @param ctx */ export function componentTypedEmitter(ctx: bVirtualScroll): ComponentTypedEmitter { diff --git a/src/components/base/b-virtual-scroll/modules/emitter/interface.ts b/src/components/base/b-virtual-scroll/modules/emitter/interface.ts index 45a3536974..7381ff526d 100644 --- a/src/components/base/b-virtual-scroll/modules/emitter/interface.ts +++ b/src/components/base/b-virtual-scroll/modules/emitter/interface.ts @@ -14,8 +14,8 @@ import type { ComponentEvents, LocalEventPayload } from 'components/base/b-virtu */ export interface ComponentTypedEmitter { /** - * @param event - The event name. - * @param handler - The event handler function. + * @param event - the event name. + * @param handler - the event handler function. * @param [asyncOpts] - Optional async options. */ once( @@ -25,8 +25,8 @@ export interface ComponentTypedEmitter { ): void; /** - * @param event - The event name. - * @param handler - The event handler function. + * @param event - the event name. + * @param handler - the event handler function. * @param [asyncOpts] - Optional async options. */ on( @@ -36,7 +36,7 @@ export interface ComponentTypedEmitter { ): void; /** - * @param event - The event name. + * @param event - the event name. * @param [asyncOpts] - Optional async options. */ promisifyOnce( @@ -45,8 +45,8 @@ export interface ComponentTypedEmitter { ): Promise>; /** - * @param event - The event name. - * @param payload - The event payload. + * @param event - the event name. + * @param payload - the event payload. */ emit( event: EVENT, diff --git a/src/components/base/b-virtual-scroll/modules/factory/index.ts b/src/components/base/b-virtual-scroll/modules/factory/index.ts index 0e2c19efc6..5c3eca5f0f 100644 --- a/src/components/base/b-virtual-scroll/modules/factory/index.ts +++ b/src/components/base/b-virtual-scroll/modules/factory/index.ts @@ -36,7 +36,7 @@ export class ComponentFactory extends Friend { * Produces DOM nodes from an array of component items. * Returns an array of DOM nodes representing the component items. * - * @param componentItems - An array of component items. + * @param componentItems - an array of component items */ produceNodes(componentItems: ComponentItem[]): HTMLElement[] { const createDescriptor = (item: ComponentItem): VNodeDescriptor => ({ @@ -79,8 +79,8 @@ export class ComponentFactory extends Friend { } /** - * Invokes the {@link bVirtualScroll.itemsProcessors} function and returns its result. - * @param items - The list of items to process. + * Invokes the {@link bVirtualScroll.itemsProcessors} function and returns its result + * @param items - the list of items to process. */ protected itemsProcessor(items: ComponentItem[]): ComponentItem[] { const @@ -105,7 +105,7 @@ export class ComponentFactory extends Friend { * Calls the render engine to render the components based on the provided descriptors. * Returns an array of rendered DOM nodes. * - * @param descriptors - An array of VNode descriptors. + * @param descriptors - an array of VNode descriptors. */ protected callRenderEngine(descriptors: VNodeDescriptor[]): HTMLElement[] { const diff --git a/src/components/base/b-virtual-scroll/modules/helpers/index.ts b/src/components/base/b-virtual-scroll/modules/helpers/index.ts index 2ab2756b07..dc5832db2e 100644 --- a/src/components/base/b-virtual-scroll/modules/helpers/index.ts +++ b/src/components/base/b-virtual-scroll/modules/helpers/index.ts @@ -10,15 +10,15 @@ import { componentItemType } from 'components/base/b-virtual-scroll/const'; import type { MountedItem } from 'components/base/b-virtual-scroll/interface'; /** - * Returns `true` if the value is of type `MountedItem`, otherwise `false`. - * @param val - The value to check. + * Returns `true` if the value is of type `MountedItem`, otherwise `false` + * @param val - the value to check. */ export function isItem(val: any): val is MountedItem { return Object.isPlainObject(val) && val.type === componentItemType.item; } /** - * Returns `true` if the specified value is an `async replace` error. + * Returns `true` if the specified value is an `async replace` error * @param val */ export function isAsyncReplaceError(val: unknown): boolean { diff --git a/src/components/base/b-virtual-scroll/modules/observer/index.ts b/src/components/base/b-virtual-scroll/modules/observer/index.ts index fa94720159..c87c4c860b 100644 --- a/src/components/base/b-virtual-scroll/modules/observer/index.ts +++ b/src/components/base/b-virtual-scroll/modules/observer/index.ts @@ -28,7 +28,7 @@ export class Observer extends Friend { protected engine: IoObserver; /** - * @param ctx - The `bVirtualScroll` component instance. + * @param ctx - the `bVirtualScroll` component instance. */ constructor(ctx: bVirtualScroll) { super(ctx); @@ -37,8 +37,8 @@ export class Observer extends Friend { } /** - * Starts observing the specified mounted elements. - * @param mounted - An array of elements to be observed. + * Starts observing the specified mounted elements + * @param mounted - an array of elements to be observed. */ observe(mounted: MountedChild[]): void { const @@ -52,7 +52,7 @@ export class Observer extends Friend { } /** - * Resets the module state. + * Resets the module state */ reset(): void { this.engine.reset(); diff --git a/src/components/base/b-virtual-scroll/modules/slots/index.ts b/src/components/base/b-virtual-scroll/modules/slots/index.ts index 26066e4c34..533783de8b 100644 --- a/src/components/base/b-virtual-scroll/modules/slots/index.ts +++ b/src/components/base/b-virtual-scroll/modules/slots/index.ts @@ -41,7 +41,7 @@ export class SlotsStateController extends Friend { protected lastState?: SlotsStateObj; /** - * Displays the slots that should be shown when the data state is empty. + * Displays the slots that should be shown when the data state is empty */ emptyState(): void { this.setSlotsVisibility({ @@ -56,7 +56,7 @@ export class SlotsStateController extends Friend { } /** - * Displays the slots that should be shown when the lifecycle is done. + * Displays the slots that should be shown when the lifecycle is done */ doneState(): void { this.setSlotsVisibility({ @@ -71,7 +71,7 @@ export class SlotsStateController extends Friend { } /** - * Displays the slots that should be shown during data loading progress. + * Displays the slots that should be shown during data loading progress * @param [immediate] - if set to true, {@link requestAnimationFrame} will not be used to switch the state. */ loadingProgressState(immediate: boolean = false): void { @@ -87,7 +87,7 @@ export class SlotsStateController extends Friend { } /** - * Displays the slots that should be shown when data loading fails. + * Displays the slots that should be shown when data loading fails */ loadingFailedState(): void { this.setSlotsVisibility({ @@ -102,7 +102,7 @@ export class SlotsStateController extends Friend { } /** - * Displays the slots that should be shown when data loading is successful. + * Displays the slots that should be shown when data loading is successful */ loadingSuccessState(): void { this.setSlotsVisibility({ @@ -117,7 +117,7 @@ export class SlotsStateController extends Friend { } /** - * Resets the state of the module. + * Resets the state of the module */ reset(): void { this.async.clearAll({group: new RegExp(slotsStateControllerAsyncGroup)}); @@ -127,8 +127,8 @@ export class SlotsStateController extends Friend { /** * Sets the visibility state of the slots. * - * @param stateObj - An object specifying the visibility state of each slot. - * @param [immediate] - If set to true, {@link requestAnimationFrame} will not be used to switch the state. + * @param stateObj - an object specifying the visibility state of each slot. + * @param [immediate] - if set to true, {@link requestAnimationFrame} will not be used to switch the state. */ protected setSlotsVisibility(stateObj: Required, immediate: boolean = false): void { this.lastState = stateObj; @@ -151,8 +151,8 @@ export class SlotsStateController extends Friend { /** * Sets the display state of a slot. * - * @param name - The name of the slot. - * @param state - The visibility state of the slot. + * @param name - the name of the slot. + * @param state - the visibility state of the slot. */ protected setDisplayState(name: keyof SlotsStateObj, state: boolean): void { const ref = this.ctx.$refs[name]; diff --git a/src/components/base/b-virtual-scroll/modules/state/helpers.ts b/src/components/base/b-virtual-scroll/modules/state/helpers.ts index 8233eacda5..f75c5d3feb 100644 --- a/src/components/base/b-virtual-scroll/modules/state/helpers.ts +++ b/src/components/base/b-virtual-scroll/modules/state/helpers.ts @@ -9,7 +9,7 @@ import type { VirtualScrollState, PrivateComponentState } from 'components/base/b-virtual-scroll/interface'; /** - * Creates an initial state object for a component. + * Creates an initial state object for a component */ export function createInitialState(): VirtualScrollState { return { @@ -35,7 +35,7 @@ export function createInitialState(): VirtualScrollState { } /** - * Creates an initial private state object for a component. + * Creates an initial private state object for a component */ export function createPrivateInitialState(): PrivateComponentState { return { diff --git a/src/components/base/b-virtual-scroll/modules/state/index.ts b/src/components/base/b-virtual-scroll/modules/state/index.ts index 00303f50e3..3bac67de1d 100644 --- a/src/components/base/b-virtual-scroll/modules/state/index.ts +++ b/src/components/base/b-virtual-scroll/modules/state/index.ts @@ -39,7 +39,7 @@ export class ComponentInternalState extends Friend { } /** - * Resets the state of the component. + * Resets the state of the component */ reset(): void { this.state = createInitialState(); @@ -47,14 +47,14 @@ export class ComponentInternalState extends Friend { } /** - * Increments the load page pointer. + * Increments the load page pointer */ incrementLoadPage(): void { this.state.loadPage++; } /** - * Increments the render page pointer. + * Increments the render page pointer */ incrementRenderPage(): void { this.state.renderPage++; @@ -63,8 +63,8 @@ export class ComponentInternalState extends Friend { /** * Updates the loaded data state. * - * @param data - The new data to update the state. - * @param isInitialLoading - Indicates if it's the initial loading. + * @param data - the new data to update the state. + * @param isInitialLoading - indicates if it's the initial loading. */ updateData(data: object[], isInitialLoading: boolean): void { this.state.data = this.state.data.concat(data); @@ -74,8 +74,8 @@ export class ComponentInternalState extends Friend { } /** - * Updates the arrays with mounted child elements of the component. - * @param mounted - The mounted child elements. + * Updates the arrays with mounted child elements of the component + * @param mounted - the mounted child elements. */ updateMounted(mounted: MountedChild[]): void { const @@ -91,16 +91,16 @@ export class ComponentInternalState extends Friend { } /** - * Updates the state of the last raw loaded data. - * @param data - The last raw loaded data. + * Updates the state of the last raw loaded data + * @param data - the last raw loaded data. */ setRawLastLoaded(data: unknown): void { this.state.lastLoadedRawData = data; } /** - * Sets the flag indicating if it's the initial render cycle. - * @param value - The value of the flag. + * Sets the flag indicating if it's the initial render cycle + * @param value - the value of the flag. */ setIsInitialRender(value: boolean): void { this.state.isInitialRender = value; @@ -110,39 +110,39 @@ export class ComponentInternalState extends Friend { * Sets the flag indicating if requests are stopped and the component won't make any more requests * until the lifecycle is refreshed. * - * @param value - The value of the flag. + * @param value - the value of the flag. */ setIsRequestsStopped(value: boolean): void { this.state.areRequestsStopped = value; } /** - * Sets the flag indicating if the component's lifecycle is done. - * @param value - The value of the flag. + * Sets the flag indicating if the component's lifecycle is done + * @param value - the value of the flag. */ setIsLifecycleDone(value: boolean): void { this.state.isLifecycleDone = value; } /** - * Sets the flag indicating if the component is currently loading data. - * @param value - The value of the flag. + * Sets the flag indicating if the component is currently loading data + * @param value - the value of the flag. */ setIsLoadingInProgress(value: boolean): void { this.state.isLoadingInProgress = value; } /** - * Sets a flag indicating whether the last load operation ended with an error. - * @param value - The value to set. + * Sets a flag indicating whether the last load operation ended with an error + * @param value - the value to set. */ setIsLastErrored(value: boolean): void { this.state.isLastErrored = value; } /** - * Sets the maximum viewed index based on the passed component's index. - * @param component - The component to compare and update the maximum viewed index. + * Sets the maximum viewed index based on the passed component's index + * @param component - the component to compare and update the maximum viewed index. */ setMaxViewedIndex(component: MountedChild): void { const @@ -163,14 +163,14 @@ export class ComponentInternalState extends Friend { } /** - * Returns the cursor indicating the last index of the last rendered data element. + * Returns the cursor indicating the last index of the last rendered data element */ getDataCursor(): number { return this.privateState.dataOffset; } /** - * Updates the cursor indicating the last index of the last rendered data element. + * Updates the cursor indicating the last index of the last rendered data element */ updateDataOffset(): void { const diff --git a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts index 2b2a16c51e..2b74adb9f8 100644 --- a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts @@ -35,7 +35,7 @@ export class VirtualScrollComponentObject extends ComponentObject { return this.component.evaluate((ctx) => ctx.reload()); } /** - * Returns the internal component state. + * Returns the internal component state */ getVirtualScrollState(): Promise { return this.component.evaluate((ctx) => ctx.getVirtualScrollState()); } /** - * Returns the count of children in the container. + * Returns the count of children in the container */ getChildCount(): Promise { return this.childList.count(); @@ -69,7 +69,7 @@ export class VirtualScrollComponentObject extends ComponentObject { await this.childList.nth(count - 1).waitFor({state: 'attached'}); @@ -84,7 +84,7 @@ export class VirtualScrollComponentObject extends ComponentObject { @@ -94,7 +94,7 @@ export class VirtualScrollComponentObject extends ComponentObject { @@ -104,7 +104,7 @@ export class VirtualScrollComponentObject extends ComponentObject(eventName: string): Promise> { @@ -112,7 +112,7 @@ export class VirtualScrollComponentObject extends ComponentObject { await this.component.evaluate((ctx) => { @@ -129,9 +129,9 @@ export class VirtualScrollComponentObject extends ComponentObject { const slot = this.node.locator(this.elSelector(slotName.dasherize())); @@ -164,7 +164,7 @@ export class VirtualScrollComponentObject extends ComponentObject { await Scroll.scrollToBottom(this.page); @@ -172,7 +172,7 @@ export class VirtualScrollComponentObject extends ComponentObject { const @@ -53,9 +53,9 @@ export async function createTestHelpers(page: Page): Promise( itemsCtor: DataItemCtor, @@ -170,8 +170,8 @@ export function createDataConveyor( /** * 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. + * @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, @@ -213,7 +213,7 @@ export function createStateApi( * 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. + * @param state - the partial component state to override the default values. */ export function createInitialState(state: Partial): VirtualScrollState { return { @@ -228,8 +228,8 @@ export function createInitialState(state: Partial): VirtualS } /** - * Extracts state data from the data conveyor and returns it. - * @param conveyor - The data conveyor to extract state data from. + * 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 { return { @@ -244,9 +244,9 @@ export function extractStateFromDataConveyor(conveyor: DataConveyor): Pick( data: DATA[], @@ -257,8 +257,8 @@ export function createFromData( } /** - * Creates a simple object that matches the {@link MountedItem} interface. - * @param data - The object with index of the mounted item. + * 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 { @@ -278,8 +278,8 @@ export function createMountedItem(data: IndexedObj): MountedItem { } /** - * Creates a simple object that matches the {@link MountedChild}` interface. - * @param data - The object with index of the mounted child. + * 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 { @@ -298,9 +298,9 @@ export function createMountedSeparator(data: IndexedObj): MountedChild { * 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). + * @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( count: number, @@ -311,8 +311,8 @@ export function createChunk( } /** - * Creates a simple indexed object. - * @param i - The index of the object. + * Creates a simple indexed object + * @param i - the index of the object. */ export function createIndexedObj(i: number): IndexedObj { return {i}; @@ -322,8 +322,8 @@ export function createIndexedObj(i: number): IndexedObj { * 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 emitCalls - the array of emit calls. + * @param [filterObserverEvents] - whether to filter out observer events (default: true). * @param [allowedEvents] */ export function filterEmitterCalls( @@ -340,8 +340,8 @@ export function filterEmitterCalls( * 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 results - the array of emit results. + * @param [filterObserverEvents] - whether to filter out observer events (default: true). * @param [allowedEvents] */ export function filterEmitterResults( diff --git a/src/components/base/b-virtual-scroll/test/api/helpers/interface.ts b/src/components/base/b-virtual-scroll/test/api/helpers/interface.ts index 59faad152a..82ab199dd4 100644 --- a/src/components/base/b-virtual-scroll/test/api/helpers/interface.ts +++ b/src/components/base/b-virtual-scroll/test/api/helpers/interface.ts @@ -19,7 +19,7 @@ export interface DataConveyor { /** * Adds a specified number of data items to the conveyor. * - * @param count - The number of data items to add. + * @param count - the number of data items to add. * @returns An array containing the newly added data items. */ addData(count: number): DATA[]; @@ -27,7 +27,7 @@ export interface DataConveyor { /** * Adds a specified number of mounted items to the conveyor. * - * @param count - The number of mounted items to add. + * @param count - the number of mounted items to add. * @returns An array containing the newly added mounted items. */ addItems(count: number): MountedItem[]; @@ -35,7 +35,7 @@ export interface DataConveyor { /** * Adds a specified number of mounted child items (separators) to the conveyor. * - * @param count - The number of mounted child items to add. + * @param count - the number of mounted child items to add. * @returns An array containing the newly added mounted child items. */ addSeparators(count: number): MountedChild[]; @@ -43,7 +43,7 @@ export interface DataConveyor { /** * Adds an array of component items as mounted child items to the conveyor. * - * @param child - The array of component items to add as mounted child items. + * @param child - the array of component items to add as mounted child items. * @returns The updated array of mounted child items. */ addChild(child: ComponentItem[]): MountedChild[]; @@ -51,7 +51,7 @@ export interface DataConveyor { /** * Returns an array of data for the given index added using the `addData` method. * - * @param index - The index of the data chunk. + * @param index - the index of the data chunk. * @returns An array of data. */ getDataChunk(index: number): DATA[]; diff --git a/src/core/prelude/test-env/components/json.ts b/src/core/prelude/test-env/components/json.ts index 996fa70854..23d80803ed 100644 --- a/src/core/prelude/test-env/components/json.ts +++ b/src/core/prelude/test-env/components/json.ts @@ -32,8 +32,8 @@ export function evalFn(func: T): T { * This function is needed in order to extract a previously inserted mock function * into the context of a browser page by its ID. * - * @param obj - The object to override the `toJSON` method for. - * @param id - The identifier of the mock function. + * @param obj - the object to override the `toJSON` method for. + * @param id - the identifier of the mock function. * @returns The modified object with the overridden `toJSON` method. */ export function setSerializerAsMockFn(obj: T, id: string): T { diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index 4e1aee1cde..a57a03e437 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -78,7 +78,7 @@ export default abstract class ComponentObjectBuilder { protected dummy?: ComponentInDummy; /** - * The component styles that should be inserted into the page. + * The component styles that should be inserted into the page */ get componentStyles(): CanUndef { return undefined; @@ -98,7 +98,7 @@ export default abstract class ComponentObjectBuilder { } /** - * Public access to the reference of the component's `JSHandle`. + * Public access to the reference of the component's `JSHandle` * @throws {@link Error} if trying to access a component that has not been built or picked */ get component(): JSHandle { @@ -110,15 +110,15 @@ export default abstract class ComponentObjectBuilder { } /** - * Returns `true` if the component is built or picked. + * Returns `true` if the component is built or picked */ get isBuilded(): boolean { return Boolean(this.componentStore); } /** - * @param page - The page on which the component is located - * @param componentName - The name of the component to be rendered + * @param page - the page on which the component is located + * @param componentName - the name of the component to be rendered */ constructor(page: Page, componentName: string) { this.page = page; @@ -133,7 +133,7 @@ export default abstract class ComponentObjectBuilder { } /** - * Returns the base class of the component. + * Returns the base class of the component */ async getComponentClass(): Promise COMPONENT>> { const {componentClassImportPath} = this; @@ -183,7 +183,7 @@ export default abstract class ComponentObjectBuilder { * After this operation, the `ComponentObject` will be marked as built * and the {@link ComponentObject.component} property will be accessible. * - * @param selector - The selector or locator for the component node + * @param selector - the selector or locator for the component node */ async pick(selector: string): Promise; @@ -194,7 +194,7 @@ export default abstract class ComponentObjectBuilder { * After this operation, the `ComponentObject` will be marked as built * and the {@link ComponentObject.component} property will be accessible. * - * @param locator - The locator for the component node + * @param locator - the locator for the component node */ async pick(locator: Locator): Promise; @@ -205,7 +205,7 @@ export default abstract class ComponentObjectBuilder { * After this operation, the `ComponentObject` will be marked as built * and the {@link ComponentObject.component} property will be accessible. * - * @param locatorPromise - The promise that resolves to locator for the component node + * @param locatorPromise - the promise that resolves to locator for the component node */ async pick(locatorPromise: Promise): Promise; @@ -231,7 +231,7 @@ export default abstract class ComponentObjectBuilder { * Stores the provided props. * The stored props will be assigned when the component is created or picked. * - * @param props - The props to set + * @param props - the props to set */ withProps(props: Dictionary): this { if (!this.isBuilded) { @@ -245,7 +245,7 @@ export default abstract class ComponentObjectBuilder { * Stores the provided children. * The stored children will be assigned when the component is created. * - * @param children - The children to set + * @param children - the children to set */ withChildren(children: VNodeChildren): this { Object.assign(this.children, children); diff --git a/tests/helpers/component-object/mock.ts b/tests/helpers/component-object/mock.ts index 56ea02268c..231ad20524 100644 --- a/tests/helpers/component-object/mock.ts +++ b/tests/helpers/component-object/mock.ts @@ -23,11 +23,11 @@ export default abstract class ComponentObjectMock exte /** * Creates a spy to observe calls to the specified method. * - * @param path - The path to the method relative to the context (component). + * @param path - the path to the method relative to the context (component). * The {@link Object.get} method is used for searching, so you can use a complex path with separators. * - * @param spyOptions - Options for setting up the spy. - * @param spyOptions.proto - If set to `true`, the spy will be installed on the prototype of the component class. + * @param spyOptions - options for setting up the spy. + * @param spyOptions.proto - if set to `true`, the spy will be installed on the prototype of the component class. * In this case, you don't need to add `prototype` to the `path`; it will be added automatically. * * @returns A promise that resolves to the spy object. @@ -78,7 +78,7 @@ export default abstract class ComponentObjectMock exte /** * Extracts the spy using the provided function. The provided function should return a reference to the spy. * - * @param spyExtractor - The function that extracts the spy. + * @param spyExtractor - the function that extracts the spy. * @returns A promise that resolves to the spy object. * * @example @@ -102,8 +102,8 @@ export default abstract class ComponentObjectMock exte /** * Creates a mock function. * - * @param fn - The mock function. - * @param args - Arguments to pass to the mock function. + * @param fn - the mock function. + * @param args - arguments to pass to the mock function. * * @returns A promise that resolves to the mock function object. * diff --git a/tests/helpers/mock/index.ts b/tests/helpers/mock/index.ts index 6fda065938..b6e0be8720 100644 --- a/tests/helpers/mock/index.ts +++ b/tests/helpers/mock/index.ts @@ -17,8 +17,8 @@ export * from 'tests/helpers/mock/interface'; /** * Wraps an object as a spy object by adding additional properties for accessing spy information. * - * @param agent - The JSHandle representing the spy or mock function. - * @param obj - The object to wrap as a spy object. + * @param agent - the JSHandle representing the spy or mock function. + * @param obj - the object to wrap as a spy object. * @returns The wrapped object with spy properties. */ export function wrapAsSpy(agent: JSHandle | ReturnType>, obj: T): T & SpyObject { @@ -46,9 +46,9 @@ export function wrapAsSpy(agent: JSHandle( /** * Retrieves an existing {@link SpyObject} from a `JSHandle`. * - * @param ctx - The `JSHandle` containing the spy object. - * @param spyExtractor - The function to extract the spy object. + * @param ctx - the `JSHandle` containing the spy object. + * @param spyExtractor - the function to extract the spy object. * @returns A promise that resolves to the spy object. * * @example @@ -110,9 +110,9 @@ export async function getSpy( /** * Creates a mock function and injects it into a Page object. * - * @param page - The Page object to inject the mock function into. - * @param fn - The mock function. - * @param args - The arguments to pass to the function. + * @param page - the Page object to inject the mock function into. + * @param fn - the mock function. + * @param args - the arguments to pass to the function. * @returns A promise that resolves to the mock function as a {@link SpyObject}. * * @example @@ -145,9 +145,9 @@ export async function createMockFn( * This function also returns the ID of the injected mock function, which is stored in `globalThis`. * This binding allows the function to be found during object serialization within the page context. * - * @param page - The Page object to inject the mock function into. - * @param fn - The mock function. - * @param args - The arguments to pass to the function. + * @param page - the Page object to inject the mock function into. + * @param fn - the mock function. + * @param args - the arguments to pass to the function. * @returns A promise that resolves to an object containing the spy object and the ID of the injected mock function. * * @example diff --git a/tests/helpers/mock/interface.ts b/tests/helpers/mock/interface.ts index fdbd5ea6da..b005778a03 100644 --- a/tests/helpers/mock/interface.ts +++ b/tests/helpers/mock/interface.ts @@ -41,7 +41,7 @@ export interface SpyExtractor { /** * Extracts or creates a spy object from a `JSHandle`. * - * @param ctx - The `JSHandle` containing the spy object. + * @param ctx - the `JSHandle` containing the spy object. * @param args */ (ctx: CTX, ...args: ARGS): ReturnType; diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index 8cf929592e..a909645d09 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -39,8 +39,8 @@ export class RequestInterceptor { /** * Creates a new instance of RequestInterceptor. * - * @param ctx - The page or browser context. - * @param pattern - The route pattern to match against requests. + * @param ctx - the page or browser context. + * @param pattern - the route pattern to match against requests. */ constructor(ctx: Page | BrowserContext, pattern: string | RegExp) { this.routeCtx = ctx; @@ -66,8 +66,8 @@ export class RequestInterceptor { * .responseOnce((r: Route) => r.fulfill({status: 500})); * ``` * - * @param handler - The response handler function. - * @param opts - The response options. + * @param handler - the response handler function. + * @param opts - the response options. * @returns The current instance of RequestInterceptor. */ responseOnce(handler: ResponseHandler, opts?: ResponseOptions): this; @@ -95,9 +95,9 @@ export class RequestInterceptor { * .responseOnce(500, {}, {delay: 300}); * ``` * - * @param status - The response status. - * @param payload - The response payload. - * @param opts - The response options. + * @param status - the response status. + * @param payload - the response payload. + * @param opts - the response options. * @returns The current instance of RequestInterceptor. */ responseOnce(status: number, payload: ResponsePayload | ResponseHandler, opts?: ResponseOptions): this; @@ -121,7 +121,7 @@ export class RequestInterceptor { * interceptor.response((r: Route) => r.fulfill({status: 200})); * ``` * - * @param handler - The response handler function. + * @param handler - the response handler function. * @returns The current instance of RequestInterceptor. */ response(handler: ResponseHandler): this; @@ -136,9 +136,9 @@ export class RequestInterceptor { * interceptor.response(200, {}); * ``` * - * @param status - The response status. - * @param payload - The response payload. - * @param opts - The response options. + * @param status - the response status. + * @param payload - the response payload. + * @param opts - the response options. * @returns The current instance of RequestInterceptor. */ response(status: number, payload: ResponsePayload | ResponseHandler, opts?: ResponseOptions): this; @@ -211,9 +211,9 @@ export class RequestInterceptor { /** * Cooks a response handler. * - * @param status - The response status. - * @param payload - The response payload. - * @param opts - The response options. + * @param status - the response status. + * @param payload - the response payload. + * @param opts - the response options. * @returns The response handler function. */ protected cookResponseFn( From ff7cce62f74a1758fcc3de36504cfb452d4138ee Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 5 Oct 2023 11:14:03 +0300 Subject: [PATCH 106/159] :wrench: refac component object --- .../test/api/component-object/index.ts | 2 +- .../test/unit/functional/rendering/default.ts | 2 +- tests/helpers/component-object/builder.ts | 34 +++++++++++-------- tests/helpers/component-object/mock.ts | 2 +- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts index 2b74adb9f8..f7c9a4262e 100644 --- a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts @@ -167,7 +167,7 @@ export class VirtualScrollComponentObject extends ComponentObject { - await Scroll.scrollToBottom(this.page); + await Scroll.scrollToBottom(this.pwPage); return this; } diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts index 4eafa5e96e..da50e43bdd 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts @@ -151,7 +151,7 @@ test.describe('', () => { await component.build(); - await Scroll.scrollToBottomWhile(component.page, async () => { + await Scroll.scrollToBottomWhile(component.pwPage, async () => { const isEqual = await component.getChildCount() === providerChunkSize; diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index a57a03e437..88f8cfa0ac 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -60,7 +60,7 @@ export default abstract class ComponentObjectBuilder { /** * The page on which the component is located. */ - readonly page: Page; + readonly pwPage: Page; /** * The unique ID of the component generated when the constructor is called. @@ -99,11 +99,11 @@ export default abstract class ComponentObjectBuilder { /** * Public access to the reference of the component's `JSHandle` - * @throws {@link Error} if trying to access a component that has not been built or picked + * @throws {@link ReferenceError} if trying to access a component that has not been built or picked */ get component(): JSHandle { if (!this.componentStore) { - throw new Error('Bad access to the component without `build` or `pick` call'); + throw new ReferenceError('Bad access to the component without "build" or "pick" call'); } return this.componentStore; @@ -121,7 +121,7 @@ export default abstract class ComponentObjectBuilder { * @param componentName - the name of the component to be rendered */ constructor(page: Page, componentName: string) { - this.page = page; + this.pwPage = page; this.componentName = componentName; this.id = `${this.componentName}_${Math.random().toString()}`; this.props = {'data-testid': this.id}; @@ -143,7 +143,7 @@ export default abstract class ComponentObjectBuilder { } const - classModule = await Utils.import<{default: new () => COMPONENT}>(this.page, componentClassImportPath), + classModule = await Utils.import<{default: new () => COMPONENT}>(this.pwPage, componentClassImportPath), classInstance = await classModule.evaluateHandle((ctx) => ctx.default); return classInstance; @@ -156,18 +156,16 @@ export default abstract class ComponentObjectBuilder { * @param [options] */ async build(options?: BuildOptions): Promise> { - if (this.componentStyles != null) { - await this.page.addStyleTag({content: this.componentStyles}); - } + await this.insertComponentStyles(); if (options?.useDummy) { - const component = await Component.createComponentInDummy(this.page, this.componentName, this.props); + const component = await Component.createComponentInDummy(this.pwPage, this.componentName, this.props); this.dummy = component; this.componentStore = component; } else { - this.componentStore = await Component.createComponent(this.page, this.componentName, { + this.componentStore = await Component.createComponent(this.pwPage, this.componentName, { attrs: this.props, children: this.children }); @@ -210,13 +208,10 @@ export default abstract class ComponentObjectBuilder { async pick(locatorPromise: Promise): Promise; async pick(selectorOrLocator: string | Locator | Promise): Promise { - if (this.componentStyles != null) { - await this.page.addStyleTag({content: this.componentStyles}); - } - + await this.insertComponentStyles(); // eslint-disable-next-line no-nested-ternary const locator = Object.isString(selectorOrLocator) ? - this.page.locator(selectorOrLocator) : + this.pwPage.locator(selectorOrLocator) : Object.isPromise(selectorOrLocator) ? await selectorOrLocator : selectorOrLocator; this.componentStore = await locator.elementHandle().then(async (el) => { @@ -227,6 +222,15 @@ export default abstract class ComponentObjectBuilder { return this; } + /** + * Inserts into the DOM tree styles of components that are defined in the {@link ComponentObject.componentStyles} property + */ + async insertComponentStyles() { + if (this.componentStyles != null) { + await this.pwPage.addStyleTag({content: this.componentStyles}); + } + } + /** * Stores the provided props. * The stored props will be assigned when the component is created or picked. diff --git a/tests/helpers/component-object/mock.ts b/tests/helpers/component-object/mock.ts index 231ad20524..8ac3e28d59 100644 --- a/tests/helpers/component-object/mock.ts +++ b/tests/helpers/component-object/mock.ts @@ -126,6 +126,6 @@ export default abstract class ComponentObjectMock exte >(fn?: FN, ...args: any[]): Promise { fn ??= Object.cast(() => undefined); - return createMockFn(this.page, fn!, ...args); + return createMockFn(this.pwPage, fn!, ...args); } } From 4a335df037ea52f0053b3c1317e498cd15cc792c Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 5 Oct 2023 11:32:51 +0300 Subject: [PATCH 107/159] :art: --- tests/helpers/component-object/builder.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index 88f8cfa0ac..53352a7a06 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -223,9 +223,9 @@ export default abstract class ComponentObjectBuilder { } /** - * Inserts into the DOM tree styles of components that are defined in the {@link ComponentObject.componentStyles} property + * Inserts into page styles of components that are defined in the {@link ComponentObject.componentStyles} property */ - async insertComponentStyles() { + async insertComponentStyles(): Promise { if (this.componentStyles != null) { await this.pwPage.addStyleTag({content: this.componentStyles}); } From 02e2c3c12adda585e99ba350c39066770e71a62a Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 13 Oct 2023 14:08:05 +0300 Subject: [PATCH 108/159] chore: reformat table --- .../base/b-virtual-scroll/README.md | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 25aae242a5..e8ebb46624 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -71,22 +71,22 @@ See the implemented modifiers or the parent component. ## Events -| EventName | Description | Payload description | Payload | -| ------------------------------- | --------------------------------------------------------------- | --------------------------------------------- | --------------------------- | -| `dataLoadSuccess` | Data loading has succeeded. | `data: object[], isInitialLoading: boolean` | `[data, isInitialLoading]` | -| `dataLoadStart` | Data loading has started. | `isInitialLoading: boolean` | `[isInitialLoading]` | -| `dataLoadError` | An error occurred while loading data. | `isInitialLoading: boolean` | `[isInitialLoading]` | -| `dataLoadEmpty` | Successful load with no data. | | `[]` | -| `resetState` | Reset component state. | | `[]` | -| `lifecycleDone` | All component data is rendered and loaded. | | `[]` | -| `convertDataToDB` | Trigger data conversion to the `DB`. | `data: unknown` | `[data]` | -| `elementEnter` | The element has entered the viewport. | `componentItem: MountedChild` | `[componentItem]` | -| `renderStart` | Rendering of items has started. | | `[]` | -| `renderDone` | Rendering of items has finished. | | `[]` | -| `renderEngineStart` | Rendering of items has started with the render engine. | | `[]` | -| `renderEngineDone` | Rendering of items has finished with the render engine. | | `[]` | -| `domInsertStart` | DOM node insertion has started. | | `[]` | -| `domInsertDone` | DOM node insertion has finished. | | `[]` | +| EventName | Description | Payload description | Payload | +|---------------------|---------------------------------------------------------|---------------------------------------------|----------------------------| +| `dataLoadSuccess` | Data loading has succeeded. | `data: object[], isInitialLoading: boolean` | `[data, isInitialLoading]` | +| `dataLoadStart` | Data loading has started. | `isInitialLoading: boolean` | `[isInitialLoading]` | +| `dataLoadError` | An error occurred while loading data. | `isInitialLoading: boolean` | `[isInitialLoading]` | +| `dataLoadEmpty` | Successful load with no data. | | `[]` | +| `resetState` | Reset component state. | | `[]` | +| `lifecycleDone` | All component data is rendered and loaded. | | `[]` | +| `convertDataToDB` | Trigger data conversion to the `DB`. | `data: unknown` | `[data]` | +| `elementEnter` | The element has entered the viewport. | `componentItem: MountedChild` | `[componentItem]` | +| `renderStart` | Rendering of items has started. | | `[]` | +| `renderDone` | Rendering of items has finished. | | `[]` | +| `renderEngineStart` | Rendering of items has started with the render engine. | | `[]` | +| `renderEngineDone` | Rendering of items has finished with the render engine. | | `[]` | +| `domInsertStart` | DOM node insertion has started. | | `[]` | +| `domInsertDone` | DOM node insertion has finished. | | `[]` | Also, you can see the implemented traits or the parent component. From e40811c6715e76bede230b23552522bf42e7df8f Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 18 Oct 2023 15:03:31 +0300 Subject: [PATCH 109/159] :art: --- src/components/base/b-virtual-scroll/props.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/base/b-virtual-scroll/props.ts b/src/components/base/b-virtual-scroll/props.ts index 4a742d8299..72bb025d97 100644 --- a/src/components/base/b-virtual-scroll/props.ts +++ b/src/components/base/b-virtual-scroll/props.ts @@ -27,7 +27,7 @@ import type { import { defaultShouldProps, componentItemType, itemsProcessors } from 'components/base/b-virtual-scroll/const'; -import { Observer } from 'components/base/b-virtual-scroll/modules/observer'; +import type { Observer } from 'components/base/b-virtual-scroll/modules/observer'; import iData, { component, prop } from 'components/super/i-data/i-data'; From 78bfd62882289587d8f197b51623c2e83f7f5a12 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 18 Oct 2023 15:30:36 +0300 Subject: [PATCH 110/159] Added functional optional for componentObject.build --- tests/helpers/component-object/builder.ts | 8 ++++++-- tests/helpers/component-object/interface.ts | 6 ++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index 53352a7a06..01fc808668 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -158,14 +158,18 @@ export default abstract class ComponentObjectBuilder { async build(options?: BuildOptions): Promise> { await this.insertComponentStyles(); + const + name = this.componentName, + fullComponentName = `${name}${options?.functional && !name.endsWith('-functional') ? '-functional' : ''}`; + if (options?.useDummy) { - const component = await Component.createComponentInDummy(this.pwPage, this.componentName, this.props); + const component = await Component.createComponentInDummy(this.pwPage, fullComponentName, this.props); this.dummy = component; this.componentStore = component; } else { - this.componentStore = await Component.createComponent(this.pwPage, this.componentName, { + this.componentStore = await Component.createComponent(this.pwPage, fullComponentName, { attrs: this.props, children: this.children }); diff --git a/tests/helpers/component-object/interface.ts b/tests/helpers/component-object/interface.ts index a73b175406..206d4c09eb 100644 --- a/tests/helpers/component-object/interface.ts +++ b/tests/helpers/component-object/interface.ts @@ -34,4 +34,10 @@ export interface BuildOptions { * Using this option does not allow creating child nodes!!! */ useDummy?: boolean; + + /** + * If true, a functional version of the component will be created. + * The functional version is achieved by adding a -functional suffix to the component name during its creation. + */ + functional?: boolean; } From 2861c065e402b5743a78dbc2a0891d4c8b8c93f5 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 19 Oct 2023 13:30:28 +0300 Subject: [PATCH 111/159] Improved the ResponseOptions interface of the RequestInterceptor module, improved the response* methods API --- tests/helpers/providers/interceptor/index.ts | 10 ++++++++-- tests/helpers/providers/interceptor/interface.ts | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index a909645d09..7c18644c9e 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -226,10 +226,16 @@ export class RequestInterceptor { await delay(opts.delay); } + const + fulfillOpts = Object.reject(opts, 'delay'), + body = Object.isFunction(payload) ? await payload(route, request) : payload, + contentType = fulfillOpts.contentType ?? 'application/json'; + return route.fulfill({ status, - body: JSON.stringify(Object.isFunction(payload) ? await payload(route, request) : payload), - contentType: 'application/json' + body: contentType === 'application/json' && !Object.isString(body) ? JSON.stringify(body) : body, + contentType, + ...fulfillOpts }); }; } diff --git a/tests/helpers/providers/interceptor/interface.ts b/tests/helpers/providers/interceptor/interface.ts index c3ddf78ab2..c129a373ab 100644 --- a/tests/helpers/providers/interceptor/interface.ts +++ b/tests/helpers/providers/interceptor/interface.ts @@ -6,14 +6,20 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { Route, Request } from 'playwright'; +import type { Route, Request, Page } from 'playwright'; export type ResponseHandler = (route: Route, request: Request) => CanPromise; +/** + * {@link Route.fulfill} function options. + * Playwright does not provide an options interface for the fulfill function + */ +export type FulfillOptions = Exclude[0], undefined>; + /** * Interface for response options. */ -export interface ResponseOptions { +export interface ResponseOptions extends Omit { /** * The delay before the response to the request is sent. */ From 3ffba2aa43a507ea64be440fd30da0b21554eb05 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 19 Oct 2023 13:33:37 +0300 Subject: [PATCH 112/159] :art: --- tests/helpers/providers/interceptor/interface.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/helpers/providers/interceptor/interface.ts b/tests/helpers/providers/interceptor/interface.ts index c129a373ab..d6be6143fc 100644 --- a/tests/helpers/providers/interceptor/interface.ts +++ b/tests/helpers/providers/interceptor/interface.ts @@ -6,13 +6,13 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { Route, Request, Page } from 'playwright'; +import type { Route, Request } from 'playwright'; export type ResponseHandler = (route: Route, request: Request) => CanPromise; /** * {@link Route.fulfill} function options. - * Playwright does not provide an options interface for the fulfill function + * Playwright does not provide an options interface for the fulfill function */ export type FulfillOptions = Exclude[0], undefined>; From fed2ebaea590d8c243cf2004dd26548238895a4b Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sat, 21 Oct 2023 23:43:32 +0300 Subject: [PATCH 113/159] Added short-hand for RequestInterceptor.prototype.mock.mock.calls --- tests/helpers/providers/interceptor/README.md | 4 ++-- tests/helpers/providers/interceptor/index.ts | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/helpers/providers/interceptor/README.md b/tests/helpers/providers/interceptor/README.md index 9559f933dd..a3f9d1940b 100644 --- a/tests/helpers/providers/interceptor/README.md +++ b/tests/helpers/providers/interceptor/README.md @@ -136,7 +136,7 @@ await interceptor.start(); // ... // Logs the number of times interception occurred -console.log(interceptor.mock.mock.calls.length); +console.log(interceptor.calls.length); ``` ### How to View the Parameters of Intercepted Requests? @@ -153,7 +153,7 @@ await interceptor.start(); // ... -const calls = provider.mock.mock.calls; +const calls = provider.calls; const query = fromQueryString(new URL((providerCalls[0][0]).request().url()).search); // Logs the query parameters of the first intercepted request diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index 7c18644c9e..53c211bd0d 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -36,6 +36,13 @@ export class RequestInterceptor { */ readonly mock: ReturnType; + /** + * Short-hand for {@link RequestInterceptor.prototype.mock.mock.calls} + */ + get calls(): any[] { + return this.mock.mock.calls; + } + /** * Creates a new instance of RequestInterceptor. * From 47ac9d67a6922ceef64faa66b576363015648399 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 23 Oct 2023 14:00:04 +0300 Subject: [PATCH 114/159] Added request function for RequestInterceptor --- tests/helpers/providers/interceptor/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index 53c211bd0d..957cb18580 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -61,6 +61,14 @@ export class RequestInterceptor { this.mock = mocker.fn(); } + /** + * Returns the intercepted request + * @param at - the index of the request (starting from 0) + */ + request(at: number): CanUndef { + return this.calls[at][0].request(); + } + /** * Sets a response for one request. * From e1072b726444038c6e7322452043244647a3aa23 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 23 Oct 2023 14:44:45 +0300 Subject: [PATCH 115/159] Fixed an issue with wrong inheratance chain in tests/config/project files --- tests/config/project/test.ts | 11 +++++++++++ tests/config/super/test.ts | 11 +++++++++++ tests/config/unit/test.ts | 2 +- tests/helpers/providers/interceptor/index.ts | 16 +++++++++++++--- tests/helpers/providers/interceptor/interface.ts | 10 ++++++++++ 5 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 tests/config/project/test.ts create mode 100644 tests/config/super/test.ts diff --git a/tests/config/project/test.ts b/tests/config/project/test.ts new file mode 100644 index 0000000000..4fa96df656 --- /dev/null +++ b/tests/config/project/test.ts @@ -0,0 +1,11 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { default as base } from 'tests/config/super/test'; + +export default base; diff --git a/tests/config/super/test.ts b/tests/config/super/test.ts new file mode 100644 index 0000000000..558a5a1e86 --- /dev/null +++ b/tests/config/super/test.ts @@ -0,0 +1,11 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { test as base } from '@playwright/test'; + +export default base; diff --git a/tests/config/unit/test.ts b/tests/config/unit/test.ts index 64262fc292..a958ce3f99 100644 --- a/tests/config/unit/test.ts +++ b/tests/config/unit/test.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { test as base } from '@playwright/test'; +import { default as base } from 'tests/config/super/test'; import DemoPage from 'components/pages/p-v4-components-demo/test/api/page'; diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index 957cb18580..d959765fc5 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -10,7 +10,9 @@ import type { BrowserContext, Page, Request, Route } from 'playwright'; import delay from 'delay'; import { ModuleMocker } from 'jest-mock'; -import type { ResponseHandler, ResponseOptions, ResponsePayload } from 'tests/helpers/providers/interceptor/interface'; +import { fromQueryString } from 'core/url'; + +import type { InterceptedRequest, ResponseHandler, ResponseOptions, ResponsePayload } from 'tests/helpers/providers/interceptor/interface'; /** * API that provides a simple way to intercept and respond to any request. @@ -65,8 +67,16 @@ export class RequestInterceptor { * Returns the intercepted request * @param at - the index of the request (starting from 0) */ - request(at: number): CanUndef { - return this.calls[at][0].request(); + request(at: number): CanUndef { + const request: Request = this.calls[at]?.[0]?.request(); + + if (request == null) { + return; + } + + return Object.assign(request, { + query: () => fromQueryString(request.url()) + }); } /** diff --git a/tests/helpers/providers/interceptor/interface.ts b/tests/helpers/providers/interceptor/interface.ts index d6be6143fc..eafaab6c34 100644 --- a/tests/helpers/providers/interceptor/interface.ts +++ b/tests/helpers/providers/interceptor/interface.ts @@ -26,4 +26,14 @@ export interface ResponseOptions extends Omit delay?: number; } +/** + * Instance of the intercepted request with additional methods + */ +export interface InterceptedRequest extends Request { + /** + * Returns an object containing the GET parameters from the request + */ + query(): Dictionary; +} + export type ResponsePayload = object | string | number; From 676007ebbf4286d6fb222422932d8a17baee8243 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Tue, 24 Oct 2023 17:49:28 +0300 Subject: [PATCH 116/159] Fix type of InterceptedRequest.query --- tests/helpers/providers/interceptor/interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helpers/providers/interceptor/interface.ts b/tests/helpers/providers/interceptor/interface.ts index eafaab6c34..42fb741500 100644 --- a/tests/helpers/providers/interceptor/interface.ts +++ b/tests/helpers/providers/interceptor/interface.ts @@ -33,7 +33,7 @@ export interface InterceptedRequest extends Request { /** * Returns an object containing the GET parameters from the request */ - query(): Dictionary; + query(): Record; } export type ResponsePayload = object | string | number; From ec2b7357af853bd3b5e29138629fea99bb8e7f74 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 25 Oct 2023 20:16:00 +0300 Subject: [PATCH 117/159] Added a "responder" mode for RequestInterceptor --- tests/helpers/providers/interceptor/README.md | 27 +++++++ tests/helpers/providers/interceptor/index.ts | 80 ++++++++++++++++--- 2 files changed, 94 insertions(+), 13 deletions(-) diff --git a/tests/helpers/providers/interceptor/README.md b/tests/helpers/providers/interceptor/README.md index a3f9d1940b..b2843e7d85 100644 --- a/tests/helpers/providers/interceptor/README.md +++ b/tests/helpers/providers/interceptor/README.md @@ -197,3 +197,30 @@ await interceptor.start(); // Stop intercepting requests await interceptor.stop(); ``` + +### How to Respond to Requests Using a Method Instead of Automatically? + +Sometimes there are situations where you need to delay the moment of responding to a request for some unknown time in advance, and to solve this problem a special "responder" mode is provided. +In this mode, the `RequestInterceptor` still intercepts requests but does not automatically respond to them. +To respond to a request, you need to call a special method. Let's see how it works using an example: + +```typescript +// Create a RequestInterceptor instance +const interceptor = new RequestInterceptor(page, /api/); + +// Set a response for the request using a response status and payload +interceptor.response(200, { message: 'OK' }); + +// Transform the RequestInterceptor to the responder mode +await interceptor.responder(); + +// Start intercepting requests +await interceptor.start(); + +await sleep(2000); +await makeRequest(); +await sleep(2000); + +// Responds to the first request that was made +await interceptor.respond(); +``` diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index d959765fc5..dfa1913fd8 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -38,6 +38,10 @@ export class RequestInterceptor { */ readonly mock: ReturnType; + protected isResponder: boolean = false; + + protected respondQueue: any[] = []; + /** * Short-hand for {@link RequestInterceptor.prototype.mock.mock.calls} */ @@ -63,6 +67,47 @@ export class RequestInterceptor { this.mock = mocker.fn(); } + /** + * Disables automatic responses to requests and makes the current instance a "responder". + * The responder allows responding to requests not in automatic mode, but by calling + * the {@link RequestInterceptor.respond} method. That is, when a request is intercepted, + * the response will be sent only after the {@link RequestInterceptor.respond} method is called. + * + * The requests themselves are collected in a queue, and calling the {@link RequestInterceptor.respond} method + * responds to the first request in the queue and removes it from the queue. + */ + responder(): this { + this.isResponder = true; + return this; + } + + /** + * Enables automatic responses to requests and responds to all requests in the queue + */ + async unresponder(): Promise { + if (!this.isResponder) { + throw new Error('Failed to call unresponder on an instance that is not a responder'); + } + + this.isResponder = false; + + for (const response of this.respondQueue) { + await response(); + } + } + + /** + * Responds to the first request in the queue and removes it from the queue + */ + respond(): Promise { + if (!this.isResponder) { + throw new Error('Failed to call respond on an instance that is not a responder'); + } + + const response = this.respondQueue.shift(); + return response(); + } + /** * Returns the intercepted request * @param at - the index of the request (starting from 0) @@ -247,21 +292,30 @@ export class RequestInterceptor { opts?: ResponseOptions ): ResponseHandler { return async (route, request) => { - if (opts?.delay != null) { - await delay(opts.delay); + const response = async () => { + if (opts?.delay != null) { + await delay(opts.delay); + } + + const + fulfillOpts = Object.reject(opts, 'delay'), + body = Object.isFunction(payload) ? await payload(route, request) : payload, + contentType = fulfillOpts.contentType ?? 'application/json'; + + return route.fulfill({ + status, + body: contentType === 'application/json' && !Object.isString(body) ? JSON.stringify(body) : body, + contentType, + ...fulfillOpts + }); } - const - fulfillOpts = Object.reject(opts, 'delay'), - body = Object.isFunction(payload) ? await payload(route, request) : payload, - contentType = fulfillOpts.contentType ?? 'application/json'; - - return route.fulfill({ - status, - body: contentType === 'application/json' && !Object.isString(body) ? JSON.stringify(body) : body, - contentType, - ...fulfillOpts - }); + if (this.isResponder) { + this.respondQueue.push(response); + + } else { + return response(); + } }; } } From 7f2f79703bbc60a14e5c1c65991539c66ddcbf2f Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 2 Nov 2023 18:53:39 +0300 Subject: [PATCH 118/159] :wrench: --- tests/helpers/providers/interceptor/index.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index dfa1913fd8..fe835690d6 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -40,7 +40,17 @@ export class RequestInterceptor { protected isResponder: boolean = false; - protected respondQueue: any[] = []; + /** + * Queue of requests awaiting response + */ + protected respondQueue: Function[] = []; + + /** + * Number of requests awaiting response + */ + get respondQueueLength(): number { + return this.respondQueue.length; + } /** * Short-hand for {@link RequestInterceptor.prototype.mock.mock.calls} @@ -105,7 +115,7 @@ export class RequestInterceptor { } const response = this.respondQueue.shift(); - return response(); + return response?.(); } /** From 14e78523ef70d97266200b1467ff336fe79a6f1f Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 9 Nov 2023 11:53:04 +0300 Subject: [PATCH 119/159] :wrench: --- tests/helpers/component-object/index.ts | 24 +++++++++++++++++++- tests/helpers/providers/interceptor/index.ts | 24 ++++++++++++++++---- tests/helpers/utils/index.ts | 9 +++++--- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/tests/helpers/component-object/index.ts b/tests/helpers/component-object/index.ts index e8017e5e24..be9b1bae4e 100644 --- a/tests/helpers/component-object/index.ts +++ b/tests/helpers/component-object/index.ts @@ -10,5 +10,27 @@ import type iBlock from 'components/super/i-block/i-block'; import ComponentObjectMock from 'tests/helpers/component-object/mock'; export default class ComponentObject extends ComponentObjectMock { - // ... + /** + * Returns the current value of the component's modifier. To extract the value, + * the .mods property of the component is used. + * + * @param modName - The name of the modifier. + * @returns A Promise that resolves to the value of the modifier or undefined. + */ + getModVal(modName: string): Promise> { + return this.component.evaluate((ctx, [modName]) => ctx.mods[modName], [modName]); + } + + /** + * Waits for the specified value to be set for the specified modifier + * + * @param modName - The name of the modifier + * @param modVal - The value to wait for + * @returns A Promise that resolves when the specified value is set for the modifier + */ + waitForModVal(modName: string, modVal: string): Promise { + return this.pwPage + .waitForFunction(([ctx, modName, modVal]) => ctx.mods[modName] === modVal, [this.component, modName, modVal]) + .then(() => undefined); + } } diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index fe835690d6..c6c3486946 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -6,6 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +import Async from '@v4fire/core/core/async'; import type { BrowserContext, Page, Request, Route } from 'playwright'; import delay from 'delay'; import { ModuleMocker } from 'jest-mock'; @@ -38,6 +39,15 @@ export class RequestInterceptor { */ readonly mock: ReturnType; + /** + * {@link Async} + */ + protected readonly async: Async = new Async(); + + /** + * If true, intercepted requests are not automatically responded to, instead use the + * {@link RequestInterceptor.respond} method. + */ protected isResponder: boolean = false; /** @@ -48,7 +58,7 @@ export class RequestInterceptor { /** * Number of requests awaiting response */ - get respondQueueLength(): number { + get requestQueueLength(): number { return this.respondQueue.length; } @@ -107,15 +117,19 @@ export class RequestInterceptor { } /** - * Responds to the first request in the queue and removes it from the queue + * Responds to the first request in the queue and removes it from the queue. + * If there are no requests in the queue yet, it will wait for the first received one and respond to it. */ - respond(): Promise { + async respond(): Promise { if (!this.isResponder) { throw new Error('Failed to call respond on an instance that is not a responder'); } - const response = this.respondQueue.shift(); - return response?.(); + if (this.requestQueueLength === 0) { + await this.async.wait(() => this.requestQueueLength > 0) + } + + return this.respondQueue.shift()?.(); } /** diff --git a/tests/helpers/utils/index.ts b/tests/helpers/utils/index.ts index 9fe7a331dc..34ffe8441b 100644 --- a/tests/helpers/utils/index.ts +++ b/tests/helpers/utils/index.ts @@ -11,6 +11,7 @@ import type { Page, JSHandle, ElementHandle } from 'playwright'; import { evalFn } from 'core/prelude/test-env/components/json'; import BOM, { WaitForIdleOptions } from 'tests/helpers/bom'; +import type { ExtractFromJSHandle } from 'tests/helpers/mock'; const logsMap = new WeakMap(); @@ -30,10 +31,12 @@ export default class Utils { * // `ctx` refers to `imgNode` * Utils.waitForFunction(imgNode, (ctx, imgUrl) => ctx.src === imgUrl, imgUrl) * ``` + * + * @deprecated https://playwright.dev/docs/api/class-page#page-wait-for-function */ - static waitForFunction( - ctx: ElementHandle, - fn: (this: any, ctx: any, ...args: ARGS) => unknown, + static waitForFunction( + ctx: CTX, + fn: (this: any, ctx: ExtractFromJSHandle, ...args: ARGS) => unknown, ...args: ARGS ): Promise { const From 0b6e43d2c9cd7fa896998d5f389c612ce895fa1f Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sat, 11 Nov 2023 21:19:26 +0300 Subject: [PATCH 120/159] :art: test-api --- tests/helpers/scroll/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helpers/scroll/index.ts b/tests/helpers/scroll/index.ts index 2da225274b..bc3a80a264 100644 --- a/tests/helpers/scroll/index.ts +++ b/tests/helpers/scroll/index.ts @@ -49,7 +49,7 @@ export default class Scroll { * @param [scrollIntoViewOpts] */ static async scrollIntoViewIfNeeded( - ctx: Page | ElementHandle, + ctx: Page, selector: string, scrollIntoViewOpts: Dictionary ): Promise { From 4d2971481eca7f4efbf10ed948f155f54dffa21a Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 13 Nov 2023 14:00:00 +0300 Subject: [PATCH 121/159] Added activate and deactivate methods to the componentObject --- tests/helpers/component-object/index.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/helpers/component-object/index.ts b/tests/helpers/component-object/index.ts index be9b1bae4e..fe757991ad 100644 --- a/tests/helpers/component-object/index.ts +++ b/tests/helpers/component-object/index.ts @@ -33,4 +33,18 @@ export default class ComponentObject extends .waitForFunction(([ctx, modName, modVal]) => ctx.mods[modName] === modVal, [this.component, modName, modVal]) .then(() => undefined); } + + /** + * Activates the component (a shorthand for {@link iBlock.activate}) + */ + activate(): Promise { + return this.component.evaluate((ctx) => ctx.activate()); + } + + /** + * Deactivates the component (a shorthand for {@link iBlock.deactivate}) + */ + deactivate(): Promise { + return this.component.evaluate((ctx) => ctx.deactivate()); + } } From 563b88fa37e5de41058de1f60588160d99f8684e Mon Sep 17 00:00:00 2001 From: bonkalol Date: Tue, 21 Nov 2023 13:13:48 +0300 Subject: [PATCH 122/159] :art: --- .../base/b-virtual-scroll/test/unit/functional/props/props.ts | 2 +- .../base/b-virtual-scroll/test/unit/scenario/reload.ts | 2 +- tests/helpers/component-object/README.md | 4 ++-- tests/helpers/component-object/builder.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts index 38ef654103..3437ca89ef 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts @@ -49,7 +49,7 @@ test.describe('', () => { .build({useDummy: true}); await component.waitForChildCountEqualsTo(chunkSize); - await component.updatePropsViaDummy({chunkSize: chunkSize * 2}); + await component.updateProps({chunkSize: chunkSize * 2}); await component.scrollToBottom(); await component.waitForChildCountEqualsTo(chunkSize * 3); diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts index 2a3112ca06..13dbbdbb52 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts @@ -50,7 +50,7 @@ test.describe('', () => { await component.waitForChildCountEqualsTo(chunkSize[0]); - await component.updatePropsViaDummy({ + await component.updateProps({ request: { get: { chunkSize: chunkSize[1] diff --git a/tests/helpers/component-object/README.md b/tests/helpers/component-object/README.md index 6fa8eda2d1..aabd9a8645 100644 --- a/tests/helpers/component-object/README.md +++ b/tests/helpers/component-object/README.md @@ -146,7 +146,7 @@ console.log(myComponent.props) // {prop1: 'newVal'} Once a component is created (by calling the `build` or `pick` method), you cannot directly change its props because props are `readonly` properties of the component. However, if a prop is linked to a parent component's property, changing the parent's property will also change the prop's value in the component. -To facilitate this behavior, there is a "sugar" method that encapsulates this logic, using a `b-dummy` component as the parent. To use this sugar method in the `build` method, pass the option `useDummy: true`. This will create a wrapper for the component using `b-dummy`, and you can change props using a special method called `updatePropsViaDummy`: +To facilitate this behavior, there is a "sugar" method that encapsulates this logic, using a `b-dummy` component as the parent. To use this sugar method in the `build` method, pass the option `useDummy: true`. This will create a wrapper for the component using `b-dummy`, and you can change props using a special method called `updateProps`: ```typescript // Create an instance of ComponentObject @@ -160,7 +160,7 @@ myComponent await myComponent.build({ useDummy: true }); // Change props -await myComponent.updatePropsViaDummy({ prop1: 'newVal' }); +await myComponent.updateProps({ prop1: 'newVal' }); ``` Please note that there are some nuances to consider when creating a component with a `b-dummy` wrapper, such as you cannot set slots for such a component. diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index 01fc808668..209c40c6bc 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -269,7 +269,7 @@ export default abstract class ComponentObjectBuilder { * * @throws {@link ReferenceError} - if the component object was not built or was built without the `useDummy` option */ - updatePropsViaDummy(props: Dictionary): Promise { + updateProps(props: Dictionary): Promise { if (!this.dummy) { throw new ReferenceError('Failed to update props. Missing "b-dummy" component.'); } From ccd1d38ae693863c347845e9c0ec4365d5b63f76 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Tue, 21 Nov 2023 13:31:44 +0300 Subject: [PATCH 123/159] :wrench: --- tests/config/project/test.ts | 2 +- tests/config/unit/test.ts | 2 +- tests/helpers/component-object/index.ts | 14 +++++++++----- tests/helpers/providers/interceptor/index.ts | 14 +++++++------- tests/helpers/utils/index.ts | 4 ++-- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/tests/config/project/test.ts b/tests/config/project/test.ts index 4fa96df656..03d032a381 100644 --- a/tests/config/project/test.ts +++ b/tests/config/project/test.ts @@ -6,6 +6,6 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { default as base } from 'tests/config/super/test'; +import base from 'tests/config/super/test'; export default base; diff --git a/tests/config/unit/test.ts b/tests/config/unit/test.ts index a958ce3f99..1eee3d2482 100644 --- a/tests/config/unit/test.ts +++ b/tests/config/unit/test.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { default as base } from 'tests/config/super/test'; +import base from 'tests/config/super/test'; import DemoPage from 'components/pages/p-v4-components-demo/test/api/page'; diff --git a/tests/helpers/component-object/index.ts b/tests/helpers/component-object/index.ts index fe757991ad..d1806ebfb4 100644 --- a/tests/helpers/component-object/index.ts +++ b/tests/helpers/component-object/index.ts @@ -14,8 +14,8 @@ export default class ComponentObject extends * Returns the current value of the component's modifier. To extract the value, * the .mods property of the component is used. * - * @param modName - The name of the modifier. - * @returns A Promise that resolves to the value of the modifier or undefined. + * @param modName - the name of the modifier + * @returns A Promise that resolves to the value of the modifier or undefined */ getModVal(modName: string): Promise> { return this.component.evaluate((ctx, [modName]) => ctx.mods[modName], [modName]); @@ -24,13 +24,17 @@ export default class ComponentObject extends /** * Waits for the specified value to be set for the specified modifier * - * @param modName - The name of the modifier - * @param modVal - The value to wait for + * @param modName - the name of the modifier + * @param modVal - the value to wait for * @returns A Promise that resolves when the specified value is set for the modifier */ waitForModVal(modName: string, modVal: string): Promise { return this.pwPage - .waitForFunction(([ctx, modName, modVal]) => ctx.mods[modName] === modVal, [this.component, modName, modVal]) + .waitForFunction(([ctx, modName, modVal]) => ctx.mods[modName] === modVal, [ + this.component, + modName, + modVal + ]) .then(() => undefined); } diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index c6c3486946..3b4e465081 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -126,7 +126,7 @@ export class RequestInterceptor { } if (this.requestQueueLength === 0) { - await this.async.wait(() => this.requestQueueLength > 0) + await this.async.wait(() => this.requestQueueLength > 0); } return this.respondQueue.shift()?.(); @@ -134,10 +134,10 @@ export class RequestInterceptor { /** * Returns the intercepted request - * @param at - the index of the request (starting from 0) + * @param index - the index of the request (starting from 0) */ - request(at: number): CanUndef { - const request: Request = this.calls[at]?.[0]?.request(); + request(index: number): CanUndef { + const request: CanUndef = this.calls[index]?.[0]?.request(); if (request == null) { return; @@ -320,19 +320,19 @@ export class RequestInterceptor { if (opts?.delay != null) { await delay(opts.delay); } - + const fulfillOpts = Object.reject(opts, 'delay'), body = Object.isFunction(payload) ? await payload(route, request) : payload, contentType = fulfillOpts.contentType ?? 'application/json'; - + return route.fulfill({ status, body: contentType === 'application/json' && !Object.isString(body) ? JSON.stringify(body) : body, contentType, ...fulfillOpts }); - } + }; if (this.isResponder) { this.respondQueue.push(response); diff --git a/tests/helpers/utils/index.ts b/tests/helpers/utils/index.ts index 34ffe8441b..37f35f8d9e 100644 --- a/tests/helpers/utils/index.ts +++ b/tests/helpers/utils/index.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { Page, JSHandle, ElementHandle } from 'playwright'; +import type { Page, JSHandle } from 'playwright'; import { evalFn } from 'core/prelude/test-env/components/json'; @@ -31,7 +31,7 @@ export default class Utils { * // `ctx` refers to `imgNode` * Utils.waitForFunction(imgNode, (ctx, imgUrl) => ctx.src === imgUrl, imgUrl) * ``` - * + * * @deprecated https://playwright.dev/docs/api/class-page#page-wait-for-function */ static waitForFunction( From 70e94130a17bc9b10beee07bd075cd1e038fcd62 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 22 Dec 2023 19:48:39 +0300 Subject: [PATCH 124/159] Fixed an issue with wrong itemIndex calculation; Provide a context to the itemProcessor --- components-lock.json | 2 +- .../b-virtual-scroll/interface/component.ts | 2 +- .../b-virtual-scroll/modules/factory/index.ts | 14 +++- .../test/api/helpers/index.ts | 12 +++ .../test/unit/functional/state/default.ts | 77 ++++++++++++++++++- tests/helpers/mock/index.ts | 6 +- tests/helpers/utils/index.ts | 2 +- 7 files changed, 105 insertions(+), 10 deletions(-) diff --git a/components-lock.json b/components-lock.json index a00c773ea3..d3bfae0ecd 100644 --- a/components-lock.json +++ b/components-lock.json @@ -1,5 +1,5 @@ { - "hash": "370c3dd59f0dcf990c540ca44c191efcf37fd04d2e0119e86c98b20b259d980a", + "hash": "3ae069d52d1989b96e528e8e9288454145d1b772025f020f37e2e2148bfea48d", "data": { "%data": "%data:Map", "%data:Map": [ diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts index b786f7273c..ba8e0591a9 100644 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -305,7 +305,7 @@ export interface ComponentItemFactory { * A middleware function used to modify elements compiled within {@link bVirtualScroll.itemsFactory}. */ export interface ItemsProcessor { - (componentItems: ComponentItem[]): ComponentItem[]; + (componentItems: ComponentItem[], ctx: bVirtualScroll): ComponentItem[]; } /** diff --git a/src/components/base/b-virtual-scroll/modules/factory/index.ts b/src/components/base/b-virtual-scroll/modules/factory/index.ts index 5c3eca5f0f..0e6263acc7 100644 --- a/src/components/base/b-virtual-scroll/modules/factory/index.ts +++ b/src/components/base/b-virtual-scroll/modules/factory/index.ts @@ -60,14 +60,20 @@ export class ComponentFactory extends Friend { {ctx} = this, {items: mountedItems, childList} = ctx.getVirtualScrollState(); + let + itemsCounter = 0; + return items.map((item, i) => { if (isItem(item)) { - return { + const res = { ...item, node: nodes[i], - itemIndex: mountedItems.length + i, + itemIndex: mountedItems.length + itemsCounter, childIndex: childList.length + i }; + + itemsCounter++; + return res; } return { @@ -91,11 +97,11 @@ export class ComponentFactory extends Friend { } if (Object.isFunction(ctx.itemsProcessors)) { - return ctx.itemsProcessors(items); + return ctx.itemsProcessors(items, ctx); } Object.forEach(ctx.itemsProcessors, (processor) => { - items = processor(items); + items = processor(items, ctx); }); return items; diff --git a/src/components/base/b-virtual-scroll/test/api/helpers/index.ts b/src/components/base/b-virtual-scroll/test/api/helpers/index.ts index 7fdeb0284a..8d567b98aa 100644 --- a/src/components/base/b-virtual-scroll/test/api/helpers/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/helpers/index.ts @@ -113,6 +113,8 @@ export function createDataConveyor( }, addChild(list: ComponentItem[]) { + let itemsCounter = 0; + const newChild = list.map((child, i) => { const v = { childIndex: childI + i, @@ -120,11 +122,21 @@ export function createDataConveyor( ...child }; + if (child.type === 'item') { + Object.assign(v, { + itemIndex: items.length + itemsCounter + }); + + itemsCounter++; + } + return v; }); + items.push(...newChild.filter((item) => item.type === 'item')); childList.push(...newChild); childI = childList.length; + itemsI = items.length; return childList; }, diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts index 306b0ba1e4..9e0ae61422 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts @@ -135,7 +135,82 @@ test.describe('', () => { }); test.describe('state after rendering via `itemsFactory`', () => { - test('`itemsFactory` returns items with `item` and `separator` type', async () => { + test.only('`itemsFactory` returns mixed items with `item` and `separator` type', async ({page}) => { + const chunkSize = 12; + + const separator: ComponentItem = { + item: 'b-button', + key: Object.cast(undefined), + children: { + default: 'ima button' + }, + props: { + id: 'button' + }, + type: 'separator' + }; + + const item = (data): ComponentItem => ({ + item: 'section', + key: Object.cast(undefined), + type: 'item', + props: { + 'data-index': data.i + }, + meta: { + data + } + }); + + const compileItemsFn = (state, ctx, separator, item) => { + const + data = state.lastLoadedData, + result: ComponentItem[] = []; + + data.forEach((data) => { + result.push(separator, item(data)); + }); + + return result; + } + + const itemsFactory = await component.mockFn(compileItemsFn, separator, item); + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: state.data.addData(0)}); + + state.data.addChild(compileItemsFn({lastLoadedData: state.data.data}, null, separator, item)); + + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + itemsFactory, + shouldPerformDataRender: () => true, + chunkSize + }) + .build(); + + await component.waitForChildCountEqualsTo(chunkSize * 2); + await component.waitForLifecycleDone(); + + const + currentState = await component.getVirtualScrollState(); + + test.expect(currentState).toEqual(state.compile({ + isInitialLoading: false, + isInitialRender: false, + areRequestsStopped: true, + isLoadingInProgress: false, + isLastEmpty: true, + isLifecycleDone: true, + loadPage: 2, + renderPage: 1 + })); + }); + + + test('`itemsFactory` returns items with `item` and last item with `separator` type', async () => { const chunkSize = 12; const separator: ComponentItem = { diff --git a/tests/helpers/mock/index.ts b/tests/helpers/mock/index.ts index b6e0be8720..19ff73f0f6 100644 --- a/tests/helpers/mock/index.ts +++ b/tests/helpers/mock/index.ts @@ -9,7 +9,7 @@ import type { ModuleMocker } from 'jest-mock'; import type { JSHandle, Page } from 'playwright'; -import { setSerializerAsMockFn } from 'core/prelude/test-env/components/json'; +import { expandedStringify, setSerializerAsMockFn } from 'core/prelude/test-env/components/json'; import type { ExtractFromJSHandle, SpyExtractor, SpyObject } from 'tests/helpers/mock/interface'; export * from 'tests/helpers/mock/interface'; @@ -173,7 +173,9 @@ export async function injectMockIntoPage( const agent = await page.evaluateHandle(([tmpFn, fnString, args]) => // eslint-disable-next-line no-new-func - globalThis[tmpFn] = jestMock.mock((...fnArgs) => Object.cast(new Function(`return ${fnString}`)()(...fnArgs, ...args))), [tmpFn, fn.toString(), args]); + globalThis[tmpFn] = jestMock.mock((...fnArgs) => { + return Object.cast(new Function(`return ${fnString}`)()(...fnArgs, ...globalThis.expandedParse(args))) + }), [tmpFn, fn.toString(), expandedStringify(args)]); return {agent: wrapAsSpy(agent, {}), id: tmpFn}; } diff --git a/tests/helpers/utils/index.ts b/tests/helpers/utils/index.ts index 34ffe8441b..1d000aa84a 100644 --- a/tests/helpers/utils/index.ts +++ b/tests/helpers/utils/index.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { Page, JSHandle, ElementHandle } from 'playwright'; +import type { Page, JSHandle } from 'playwright'; import { evalFn } from 'core/prelude/test-env/components/json'; From a59d13a8623f2fe635079938ddc1b5bc2d31a6a8 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 25 Dec 2023 15:37:14 +0300 Subject: [PATCH 125/159] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BF=D1=80=D0=BE=D0=B1=D0=BB=D0=B5=D0=BC=D1=83?= =?UTF-8?q?=20=D1=81=20=D1=83=D1=87=D0=B5=D1=82=D0=BE=D0=BC=20data-testid?= =?UTF-8?q?=20=D0=B2=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=D0=B5=20pick,=20?= =?UTF-8?q?=D1=82=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20=D0=B2=D0=BC=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=BE=20=D1=8D=D1=82=D0=BE=D0=B3=D0=BE=20=D0=B8=D1=81?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D1=83=D0=B5=D1=82=D1=81=D1=8F=20?= =?UTF-8?q?=D0=B4=D1=80=D1=83=D0=B3=D0=BE=D0=B9=20=D0=B0=D1=82=D1=80=D0=B8?= =?UTF-8?q?=D0=B1=D1=83=D1=82=20=D1=87=D1=82=D0=BE=D0=B1=D1=8B=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D0=B2=D0=BE=D0=B7=D0=BD=D0=B8=D0=BA=D0=B0=D0=BB=D0=BE?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BB=D0=BB=D0=B8=D0=B7=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/helpers/component-object/builder.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index 01fc808668..a82666010a 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -124,8 +124,8 @@ export default abstract class ComponentObjectBuilder { this.pwPage = page; this.componentName = componentName; this.id = `${this.componentName}_${Math.random().toString()}`; - this.props = {'data-testid': this.id}; - this.node = page.getByTestId(this.id); + this.props = {'data-component-object-id': this.id}; + this.node = page.locator(`[data-component-object-id="${this.id}"]`); this.componentClassImportPath = path.join( path.relative(`${process.cwd()}/src`, resolve.blockSync(this.componentName)!), `/${this.componentName}.ts` @@ -219,7 +219,7 @@ export default abstract class ComponentObjectBuilder { Object.isPromise(selectorOrLocator) ? await selectorOrLocator : selectorOrLocator; this.componentStore = await locator.elementHandle().then(async (el) => { - await el?.evaluate((ctx, [id]) => ctx.setAttribute('data-test-id', id), [this.id]); + await el?.evaluate((ctx, [id]) => ctx.setAttribute('data-component-object-id', id), [this.id]); return el?.getProperty('component'); }); From a51460997b956a2d468c5d5d31168310fd821e8a Mon Sep 17 00:00:00 2001 From: bonkalol Date: Sun, 31 Dec 2023 18:56:47 +0300 Subject: [PATCH 126/159] Added isLastRender for b-virtual-scroll state, added getItemsProcessors to allow more flexibility in overriding processors, fixed eslint errors. --- .../base/b-virtual-scroll/b-virtual-scroll.ts | 10 +- .../base/b-virtual-scroll/handlers.ts | 1 + .../b-virtual-scroll/interface/component.ts | 24 ++++ .../b-virtual-scroll/modules/factory/index.ts | 11 +- .../b-virtual-scroll/modules/state/index.ts | 21 +++ .../test/api/helpers/index.ts | 20 ++- .../test/api/helpers/interface.ts | 12 ++ .../test/unit/functional/state/default.ts | 5 +- .../test/unit/functional/state/emitter.ts | 126 +++++++++++++++++- tests/config/project/test.ts | 2 +- tests/config/unit/test.ts | 2 +- tests/helpers/component-object/index.ts | 11 +- tests/helpers/mock/index.ts | 8 +- tests/helpers/providers/interceptor/index.ts | 14 +- tests/helpers/utils/index.ts | 2 +- 15 files changed, 233 insertions(+), 36 deletions(-) diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts index b6d53b9ffd..9b9c7a34c0 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts @@ -18,7 +18,7 @@ import type iItems from 'components/traits/i-items/i-items'; import VDOM, { create, render } from 'components/friends/vdom'; import { iVirtualScrollHandlers } from 'components/base/b-virtual-scroll/handlers'; import { bVirtualScrollAsyncGroup, bVirtualScrollDomInsertAsyncGroup, renderGuardRejectionReason } from 'components/base/b-virtual-scroll/const'; -import type { VirtualScrollState, RenderGuardResult, $ComponentRefs, UnsafeBVirtualScroll } from 'components/base/b-virtual-scroll/interface'; +import type { VirtualScrollState, RenderGuardResult, $ComponentRefs, UnsafeBVirtualScroll, ItemsProcessors } from 'components/base/b-virtual-scroll/interface'; import { ComponentTypedEmitter, componentTypedEmitter } from 'components/base/b-virtual-scroll/modules/emitter'; import { ComponentInternalState } from 'components/base/b-virtual-scroll/modules/state'; @@ -144,6 +144,14 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI this.chunkSize; } + /** + * Returns an items processors + * @returns + */ + getItemsProcessors(): CanUndef { + return this.itemsProcessors; + } + override reload(...args: Parameters): ReturnType { this.componentStatus = 'loading'; return super.reload(...args); diff --git a/src/components/base/b-virtual-scroll/handlers.ts b/src/components/base/b-virtual-scroll/handlers.ts index 72869c77d7..e3480d94c6 100644 --- a/src/components/base/b-virtual-scroll/handlers.ts +++ b/src/components/base/b-virtual-scroll/handlers.ts @@ -40,6 +40,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { * Triggered when the component rendering starts. */ protected onRenderStart(this: bVirtualScroll): void { + this.componentInternalState.updateIsLastRender(); this.componentEmitter.emit(componentEvents.renderStart); } diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts index ba8e0591a9..cf09c5ec86 100644 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -83,6 +83,30 @@ export interface VirtualScrollState { */ isLifecycleDone: boolean; + /** + * Indicates if the current render process is the last one in the current lifecycle. + * + * It is important to understand that the component uses the + * {@link VirtualScrollState.areRequestsStopped} property to calculate this value, + * which means, if your loading completion strategy relies on the fact that no elements + * will be received in the last request, + * then {@link VirtualScrollState.isLastRender} will always be `undefined`. + * + * To achieve correct `isLastRender` behavior, it is necessary to implement a request + * stopping strategy in such a way that the last render occurs after all loading has + * been completed. For example, this can be implemented if your backend pagination + * response has a property indicating the total number of items that can be loaded, + * this property can be used for comparison: + * + * ```typescript + * const shouldStopRequestingData = (state: VirtualScrollState): boolean => { + * // Example: Stop requesting data when the total number of items equals the current number of loaded items + * return state.lastLoadedRawData?.total === state.data.length; + * }; + * ``` + */ + isLastRender?: boolean; + /** * The last loaded data. */ diff --git a/src/components/base/b-virtual-scroll/modules/factory/index.ts b/src/components/base/b-virtual-scroll/modules/factory/index.ts index 0e6263acc7..36ebb8437d 100644 --- a/src/components/base/b-virtual-scroll/modules/factory/index.ts +++ b/src/components/base/b-virtual-scroll/modules/factory/index.ts @@ -90,17 +90,18 @@ export class ComponentFactory extends Friend { */ protected itemsProcessor(items: ComponentItem[]): ComponentItem[] { const - {ctx} = this; + {ctx} = this, + itemsProcessors = ctx.getItemsProcessors(); - if (!ctx.itemsProcessors) { + if (!itemsProcessors) { return items; } - if (Object.isFunction(ctx.itemsProcessors)) { - return ctx.itemsProcessors(items, ctx); + if (Object.isFunction(itemsProcessors)) { + return itemsProcessors(items, ctx); } - Object.forEach(ctx.itemsProcessors, (processor) => { + Object.forEach(itemsProcessors, (processor) => { items = processor(items, ctx); }); diff --git a/src/components/base/b-virtual-scroll/modules/state/index.ts b/src/components/base/b-virtual-scroll/modules/state/index.ts index 3bac67de1d..a6bc28d8e3 100644 --- a/src/components/base/b-virtual-scroll/modules/state/index.ts +++ b/src/components/base/b-virtual-scroll/modules/state/index.ts @@ -90,6 +90,27 @@ export class ComponentInternalState extends Friend { this.updateRemainingChildren(); } + /** + * Updates the indicator that shows whether the current rendering process is the + * last one in this lifecycle. + */ + updateIsLastRender(): void { + const + {state, ctx} = this; + + if (!state.areRequestsStopped) { + return; + } + + const + chunkSize = ctx.getChunkSize(state), + dataOffset = this.getDataCursor() + chunkSize; + + if (>state.data[dataOffset] == null) { + state.isLastRender = true; + } + } + /** * Updates the state of the last raw loaded data * @param data - the last raw loaded data. diff --git a/src/components/base/b-virtual-scroll/test/api/helpers/index.ts b/src/components/base/b-virtual-scroll/test/api/helpers/index.ts index 8d567b98aa..d1f29fddbf 100644 --- a/src/components/base/b-virtual-scroll/test/api/helpers/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/helpers/index.ts @@ -72,7 +72,8 @@ export function createDataConveyor( dataI = 0, itemsI = 0, childI = 0, - page = 0; + page = 0, + total: CanUndef = undefined; const obj: DataConveyor = { addData(count: number) { @@ -145,10 +146,16 @@ export function createDataConveyor( return dataChunks[index]; }, + setTotal(newTotal: number) { + total = newTotal; + return total; + }, + reset() { dataI = 0; itemsI = 0; childI = 0; + total = undefined; childList = []; items = []; data = []; @@ -159,6 +166,10 @@ export function createDataConveyor( return items; }, + get total() { + return total; + }, + get childList() { return childList; }, @@ -247,7 +258,12 @@ export function extractStateFromDataConveyor(conveyor: DataConveyor): Pick { */ getDataChunk(index: number): DATA[]; + /** + * Sets the value of total data + * @param newTotal + */ + setTotal(newTotal: number): number; + /** * Resets the data conveyor, clearing all data and items. */ @@ -66,6 +72,12 @@ export interface DataConveyor { */ get data(): DATA[]; + /** + * Returns the data amount + * @param newTotal + */ + get total(): CanUndef; + /** * Returns a data page. */ diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts index 9e0ae61422..63a865ee4c 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts @@ -135,7 +135,7 @@ test.describe('', () => { }); test.describe('state after rendering via `itemsFactory`', () => { - test.only('`itemsFactory` returns mixed items with `item` and `separator` type', async ({page}) => { + test('`itemsFactory` returns mixed items with `item` and `separator` type', async () => { const chunkSize = 12; const separator: ComponentItem = { @@ -172,7 +172,7 @@ test.describe('', () => { }); return result; - } + }; const itemsFactory = await component.mockFn(compileItemsFn, separator, item); @@ -209,7 +209,6 @@ test.describe('', () => { })); }); - test('`itemsFactory` returns items with `item` and last item with `separator` type', async () => { const chunkSize = 12; diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts index 42242abe29..2e5ddddff0 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts @@ -14,6 +14,7 @@ import test from 'tests/config/unit/test'; import { createTestHelpers, filterEmitterResults } from 'components/base/b-virtual-scroll/test/api/helpers'; import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; +import type { VirtualScrollState } from 'components/base/b-virtual-scroll/interface'; test.describe('', () => { let @@ -50,6 +51,9 @@ test.describe('', () => { state.data.addData(chunkSize), state.set({loadPage: 1, areRequestsStopped: true}).compile(observerInitialStateFields) ), + ( + state.set({isLastRender: true}).compile(observerInitialStateFields) + ), ( state.data.addItems(chunkSize), state.set({isInitialRender: false, renderPage: 1}).compile(observerLoadedStateFields) @@ -94,13 +98,13 @@ test.describe('', () => { ['convertDataToDB', {...states[0], lastLoadedRawData: states[1].lastLoadedRawData}], ['initLoad', {...states[0], lastLoadedRawData: states[1].lastLoadedRawData}], ['dataLoadSuccess', states[1]], - ['renderStart', states[1]], - ['renderEngineStart', states[1]], - ['renderEngineDone', states[1]], - ['domInsertStart', states[2]], - ['domInsertDone', states[3]], - ['renderDone', states[3]], - ['lifecycleDone', states[4]] + ['renderStart', states[2]], + ['renderEngineStart', states[2]], + ['renderEngineDone', states[2]], + ['domInsertStart', states[3]], + ['domInsertDone', states[4]], + ['renderDone', states[4]], + ['lifecycleDone', states[5]] ]); }); }); @@ -212,4 +216,112 @@ test.describe('', () => { ]); }); }); + + test.describe('all data has been loaded after few scrolls', () => { + test('state at the time of emitting events must be correct', async () => { + const + chunkSize = 12, + providerChunkSize = chunkSize, + total = chunkSize * 2; + + state.data.setTotal(total); + + const states = [ + state.compile(observerInitialStateFields), + ( + // 1 + state.data.addData(providerChunkSize), + state.set({loadPage: 1}).compile(observerInitialStateFields) + ), + ( + // 2 + state.data.addItems(chunkSize), + state.set({renderPage: 1, isInitialRender: false}).compile(observerLoadedStateFields) + ), + ( + // 3 + state.compile() + ), + ( + // 4 + state.data.addData(providerChunkSize), + state.set({ + loadPage: 2, + areRequestsStopped: true, + isLastEmpty: false, + isInitialLoading: false + }).compile() + ), + ( + // 5 + state.set({isLastRender: true}).compile() + ), + ( + // 6 + state.data.addItems(chunkSize), + state.set({renderPage: 2}).compile() + ), + ( + // 7 + state.set({isLifecycleDone: true}).compile() + ) + ]; + + provider + .responseOnce(200, {data: state.data.getDataChunk(0), total}) + .responseOnce(200, {data: state.data.getDataChunk(1), total}); + + await component + .withDefaultPaginationProviderProps({chunkSize: providerChunkSize}) + .withProps({ + chunkSize, + shouldStopRequestingData: (state: VirtualScrollState): boolean => + Object.get(state, 'lastLoadedRawData.total') === state.data.length, + + '@hook:beforeDataCreate': (ctx) => { + const original = ctx.emit; + + ctx.emit = jestMock.mock((...args) => { + original(...args); + return [args[0], Object.fastClone(ctx.getVirtualScrollState())]; + }); + } + }) + .build(); + + await component.waitForChildCountEqualsTo(chunkSize); + await component.scrollToBottom(); + await component.waitForChildCountEqualsTo(chunkSize * 2); + await component.scrollToBottom(); + await component.waitForLifecycleDone(); + + const + spy = await component.getSpy((ctx) => ctx.emit), + results = filterEmitterResults(await spy.results, true, ['initLoadStart', 'initLoad']); + + test.expect(results).toEqual([ + ['initLoadStart', {...states[0], isLoadingInProgress: true}], + ['dataLoadStart', {...states[0], isLoadingInProgress: true}], + ['convertDataToDB', {...states[0], lastLoadedRawData: states[1].lastLoadedRawData}], + ['initLoad', {...states[0], lastLoadedRawData: states[1].lastLoadedRawData}], + ['dataLoadSuccess', states[1]], + ['renderStart', states[1]], + ['renderEngineStart', states[1]], + ['renderEngineDone', states[1]], + ['domInsertStart', states[2]], + ['domInsertDone', states[2]], + ['renderDone', states[2]], + ['dataLoadStart', {...states[3], isLoadingInProgress: true}], + ['convertDataToDB', {...states[3], lastLoadedRawData: states[4].lastLoadedRawData}], + ['dataLoadSuccess', states[4]], + ['renderStart', states[5]], + ['renderEngineStart', states[5]], + ['renderEngineDone', states[5]], + ['domInsertStart', states[6]], + ['domInsertDone', states[6]], + ['renderDone', states[6]], + ['lifecycleDone', states[7]] + ]); + }); + }); }); diff --git a/tests/config/project/test.ts b/tests/config/project/test.ts index 4fa96df656..03d032a381 100644 --- a/tests/config/project/test.ts +++ b/tests/config/project/test.ts @@ -6,6 +6,6 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { default as base } from 'tests/config/super/test'; +import base from 'tests/config/super/test'; export default base; diff --git a/tests/config/unit/test.ts b/tests/config/unit/test.ts index a958ce3f99..1eee3d2482 100644 --- a/tests/config/unit/test.ts +++ b/tests/config/unit/test.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { default as base } from 'tests/config/super/test'; +import base from 'tests/config/super/test'; import DemoPage from 'components/pages/p-v4-components-demo/test/api/page'; diff --git a/tests/helpers/component-object/index.ts b/tests/helpers/component-object/index.ts index fe757991ad..22a5d5dd30 100644 --- a/tests/helpers/component-object/index.ts +++ b/tests/helpers/component-object/index.ts @@ -14,7 +14,7 @@ export default class ComponentObject extends * Returns the current value of the component's modifier. To extract the value, * the .mods property of the component is used. * - * @param modName - The name of the modifier. + * @param modName - the name of the modifier. * @returns A Promise that resolves to the value of the modifier or undefined. */ getModVal(modName: string): Promise> { @@ -24,13 +24,16 @@ export default class ComponentObject extends /** * Waits for the specified value to be set for the specified modifier * - * @param modName - The name of the modifier - * @param modVal - The value to wait for + * @param modName - the name of the modifier + * @param modVal - the value to wait for * @returns A Promise that resolves when the specified value is set for the modifier */ waitForModVal(modName: string, modVal: string): Promise { return this.pwPage - .waitForFunction(([ctx, modName, modVal]) => ctx.mods[modName] === modVal, [this.component, modName, modVal]) + .waitForFunction( + ([ctx, modName, modVal]) => ctx.mods[modName] === modVal, + [this.component, modName, modVal] + ) .then(() => undefined); } diff --git a/tests/helpers/mock/index.ts b/tests/helpers/mock/index.ts index 19ff73f0f6..4661dd8e56 100644 --- a/tests/helpers/mock/index.ts +++ b/tests/helpers/mock/index.ts @@ -172,10 +172,10 @@ export async function injectMockIntoPage( tmpFn = `tmp_${Math.random().toString()}`; const agent = await page.evaluateHandle(([tmpFn, fnString, args]) => - // eslint-disable-next-line no-new-func - globalThis[tmpFn] = jestMock.mock((...fnArgs) => { - return Object.cast(new Function(`return ${fnString}`)()(...fnArgs, ...globalThis.expandedParse(args))) - }), [tmpFn, fn.toString(), expandedStringify(args)]); + globalThis[tmpFn] = jestMock.mock((...fnArgs) => + // eslint-disable-next-line no-new-func + Object.cast(new Function(`return ${fnString}`)()(...fnArgs, ...globalThis.expandedParse(args)))), + [tmpFn, fn.toString(), expandedStringify(args)]); return {agent: wrapAsSpy(agent, {}), id: tmpFn}; } diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index c6c3486946..3b4e465081 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -126,7 +126,7 @@ export class RequestInterceptor { } if (this.requestQueueLength === 0) { - await this.async.wait(() => this.requestQueueLength > 0) + await this.async.wait(() => this.requestQueueLength > 0); } return this.respondQueue.shift()?.(); @@ -134,10 +134,10 @@ export class RequestInterceptor { /** * Returns the intercepted request - * @param at - the index of the request (starting from 0) + * @param index - the index of the request (starting from 0) */ - request(at: number): CanUndef { - const request: Request = this.calls[at]?.[0]?.request(); + request(index: number): CanUndef { + const request: CanUndef = this.calls[index]?.[0]?.request(); if (request == null) { return; @@ -320,19 +320,19 @@ export class RequestInterceptor { if (opts?.delay != null) { await delay(opts.delay); } - + const fulfillOpts = Object.reject(opts, 'delay'), body = Object.isFunction(payload) ? await payload(route, request) : payload, contentType = fulfillOpts.contentType ?? 'application/json'; - + return route.fulfill({ status, body: contentType === 'application/json' && !Object.isString(body) ? JSON.stringify(body) : body, contentType, ...fulfillOpts }); - } + }; if (this.isResponder) { this.respondQueue.push(response); diff --git a/tests/helpers/utils/index.ts b/tests/helpers/utils/index.ts index 1d000aa84a..37f35f8d9e 100644 --- a/tests/helpers/utils/index.ts +++ b/tests/helpers/utils/index.ts @@ -31,7 +31,7 @@ export default class Utils { * // `ctx` refers to `imgNode` * Utils.waitForFunction(imgNode, (ctx, imgUrl) => ctx.src === imgUrl, imgUrl) * ``` - * + * * @deprecated https://playwright.dev/docs/api/class-page#page-wait-for-function */ static waitForFunction( From e58e38372bc9a00d6c91ea9c2e037dfb5b151c41 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 8 Jan 2024 16:59:16 +0300 Subject: [PATCH 127/159] :art: --- .../base/b-virtual-scroll/README.md | 387 +++++++++--------- .../base/b-virtual-scroll/b-virtual-scroll.ts | 6 +- src/components/base/b-virtual-scroll/props.ts | 7 +- 3 files changed, 205 insertions(+), 195 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index e8ebb46624..1752300e37 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -36,10 +36,10 @@ - [`requestQuery`](#requestquery) - [`itemsFactory`](#itemsfactory) - [`itemsProcessors`](#itemsprocessors) - - [`tombstoneCount`](#tombstoneCount) + - [`tombstoneCount`](#tombstonecount) - [Methods](#methods) - [getNextDataSlice](#getnextdataslice) - - [getVirtualScrollState](#getVirtualScrollState) + - [getVirtualScrollState](#getvirtualscrollstate) - [initLoadNext](#initloadnext) - [Other Properties](#other-properties) - [Migration from `b-virtual-scroll` version 3.x.x](#migration-from-b-virtual-scroll-version-3xx) @@ -56,14 +56,18 @@ # components/base/b-virtual-scroll The `b-virtual-scroll` component is designed for rendering a large array of various data. +It uses a special approach that renders chunks of components while avoiding changes to the parent component's state. +This allows for optimizing the rendering of large lists of components, making it more efficient. +If you have ever tried to render 100 components using v-for, you may have noticed that the interface starts to lag. +The `b-virtual-scroll` component aims to eliminate this lag by rendering components in portions, providing a better alternative to using v-for for such cases. ## Synopsis -* The component extends [[iData]]. +- The component extends [[iData]]. -* The component implements [[iItems]] traits. +- The component implements [[iItems]] traits. -* By default, the component's root tag is set to `
`. +- By default, the component's root tag is set to `
`. ## Modifiers @@ -92,70 +96,74 @@ Also, you can see the implemented traits or the parent component. ## Usage +The component offers various usage options: it can load and render data on scroll, on click, or even load a large volume of data at once but render it in portions. Would you like to implement a global rendering process for components in order to integrate a specific element (e.g., an advertisement) after each component? No problem - the component provides processor functions that enable this functionality. Do you want to implement your own strategy for "when to load" and "when to render"? The component also offers special functions that allow for this customization. + +Below, we will explore a few basic usage scenarios and delve into the component's API in greater detail. + ### How to Implement Simple Rendering? To implement simple rendering, you need to follow several steps: -1. Set up a data provider for the component. For example, we'll use a provider named `Provider` that returns data in the format `{data: object[]}`, where the number of objects depends on the request parameter `count`. +1. Set up a data provider for the component. For example, we'll use a provider named `Provider` that returns data in the format `{data: object[]}`, where the number of objects depends on the request parameter `count`: -``` -< b-virtual-scroll & - :dataProvider = 'Provider' -. -``` + ```snakeskin + < b-virtual-scroll & + :dataProvider = 'Provider' + . + ``` -> It's important to note that `b-virtual-scroll` expects data in this specific format (`{data: object[]}`). If your provider returns data in a different format, you can use processors in either the provider or the component using the `convertDataToDb` prop. + > It's important to note that `b-virtual-scroll` expects data in this specific format (`{data: object[]}`). If your provider returns data in a different format, you can use processors in either the provider or the component using the `convertDataToDb` prop. -2. Let's say we want to load and render 12 components at a time. To achieve this, you need to specify the `request` and `chunkSize` props for the `b-virtual-scroll` component. The `request` prop defines the request parameters (standard behavior of `iData`), and `chunkSize` specifies the number of items to render in each rendering cycle. +2. Let's say we want to load and render 12 components at a time. To achieve this, you need to specify the `request` and `chunkSize` props for the `b-virtual-scroll` component. The `request` prop defines the request parameters (standard behavior of `iData`), and `chunkSize` specifies the number of items to render in each rendering cycle: -``` -< b-virtual-scroll & - :dataProvider = 'Provider' | - :request = {get: {count: 12}} | - :chunkSize = 12 -. -``` + ```snakeskin + < b-virtual-scroll & + :dataProvider = 'Provider' | + :request = {get: {count: 12}} | + :chunkSize = 12 + . + ``` -3. To avoid loading the same data repeatedly and load different data for each subsequent request, you need to pass the `page` request parameter to the `Provider`. This parameter indicates the page number of the data to be loaded. +3. To avoid loading the same data repeatedly and load different data for each subsequent request, you need to pass the `page` request parameter to the `Provider`. This parameter indicates the page number of the data to be loaded: -To achieve this, use the `requestQuery` prop in the `b-virtual-scroll` component. `requestQuery` is a function prop that `b-virtual-scroll` calls, passing its own state as an argument, before making a request. You can return the appropriate `page` value based on the component's state. The difference between `request` and `requestQuery` is that changes to the latter won't cause the component to reinitialize. These two props are merged to form the final request parameters passed to the provider. + To achieve this, use the `requestQuery` prop in the `b-virtual-scroll` component. `requestQuery` is a function prop that `b-virtual-scroll` calls, passing its own state as an argument, before making a request. You can return the appropriate `page` value based on the component's state. The difference between `request` and `requestQuery` is that changes to the latter won't cause the component to reinitialize. These two props are merged to form the final request parameters passed to the provider. -``` -< b-virtual-scroll & - :dataProvider = 'Provider' | - :request = {get: {count: 12}} | - :requestQuery = (state) => ({get: {page: state.loadPage}}) | - :chunkSize = 12 -. -``` + ```snakeskin + < b-virtual-scroll & + :dataProvider = 'Provider' | + :request = {get: {count: 12}} | + :requestQuery = (state) => ({get: {page: state.loadPage}}) | + :chunkSize = 12 + . + ``` -In the example above, the `page` parameter is extracted from the component's state, specifically `loadPage`, which increments after each successful data load. + In the example above, the `page` parameter is extracted from the component's state, specifically `loadPage`, which increments after each successful data load. -4. Now that you have set up data loading with pagination, you need to specify what `b-virtual-scroll` will render. +4. Now that you have set up data loading with pagination, you need to specify what `b-virtual-scroll` will render: -To control what `b-virtual-scroll` renders, you can use the following props: + To control what `b-virtual-scroll` renders, you can use the following props: -- `item`: The name of the component to be rendered. It can also be a function that returns the component's name. + - `item`: The name of the component to be rendered. It can also be a function that returns the component's name. -- `itemProps`: The props for the component. Typically, this is a function that returns the props for each item based on the loaded data. + - `itemProps`: The props for the component. Typically, this is a function that returns the props for each item based on the loaded data. -- `itemKey`: The uniq id of the component. + - `itemKey`: The uniq id of the component. -Rendering occurs after data is loaded. + Rendering occurs after data is loaded. -``` -< b-virtual-scroll & - :dataProvider = 'Provider' | - :request = {get: {count: 12}} | - :requestQuery = (state) => ({get: {page: state.loadPage}}) | - :chunkSize = 12 | - :item = 'b-dummy' | - :itemKey = (el) => el.uuid | - :itemProps = (el) => ({name: el.name, type: el.type}) -. -``` + ```snakeskin + < b-virtual-scroll & + :dataProvider = 'Provider' | + :request = {get: {count: 12}} | + :requestQuery = (state) => ({get: {page: state.loadPage}}) | + :chunkSize = 12 | + :item = 'b-dummy' | + :itemKey = (el) => el.uuid | + :itemProps = (el) => ({name: el.name, type: el.type}) + . + ``` -What is `el` you can inquire about in the `itemKey` and `itemProps` functions? `el` is one of the objects in the `data` array loaded by the `dataProvider`. `b-virtual-scroll` takes the array of loaded data and calls these functions for each of the objects in this array, allowing you to transform data into components that are suitable for rendering in the `b-virtual-scroll` component. + What is `el` you can inquire about in the `itemKey` and `itemProps` functions? `el` is one of the objects in the `data` array loaded by the `dataProvider`. `b-virtual-scroll` takes the array of loaded data and calls these functions for each of the objects in this array, allowing you to transform data into components that are suitable for rendering in the `b-virtual-scroll` component. This setup will display a component on the page that loads and renders 12 items at once. When scrolling down, a new request with a different `page` value will be made, and after a successful load, new components will be rendered. @@ -163,7 +171,7 @@ However, if your component takes a long time to load data (e.g., 1 second), you Let's add a `loader` slot to our component to provide a better user experience during loading: -``` +```snakeskin < b-virtual-scroll & :dataProvider = 'Provider' | :request = {get: {count: 12}} | @@ -187,59 +195,59 @@ To implement this approach, follow these steps: 1. Disable scroll observers using the `disableObserver` prop by setting it to `true`. -``` -< b-virtual-scroll & - :dataProvider = 'Provider' | - :disableObserver = true | - ... -. -``` + ```snakeskin + < b-virtual-scroll & + :dataProvider = 'Provider' | + :disableObserver = true | + ... + . + ``` 2. Set the `shouldPerformDataRender` prop to a function that always returns `true`. This function will be called for each attempt to render data. We will discuss this function in more detail in the following sections. -``` -< b-virtual-scroll & - :dataProvider = 'Provider' | - :disableObserver = true | - :shouldPerformDataRender = () => true | - ... -. -``` + ```snakeskin + < b-virtual-scroll & + :dataProvider = 'Provider' | + :disableObserver = true | + :shouldPerformDataRender = () => true | + ... + . + ``` 3. Gain access to the methods of `b-virtual-scroll` using the standard `ref` mechanism. -``` -< b-virtual-scroll & - ref = scroll | - :dataProvider = 'Provider' | - :disableObserver = true | - :shouldPerformDataRender = () => true | - ... -. -``` + ```snakeskin + < b-virtual-scroll & + ref = scroll | + :dataProvider = 'Provider' | + :disableObserver = true | + :shouldPerformDataRender = () => true | + ... + . + ``` -After these manipulations, `b-virtual-scroll` will no longer load data on scroll, and data loading will only occur when the `initLoadNext` method is called. This method will be used to load and render data on a button click event. + After these manipulations, `b-virtual-scroll` will no longer load data on scroll, and data loading will only occur when the `initLoadNext` method is called. This method will be used to load and render data on a button click event. 4. Now, you need to add a button that triggers the `initLoadNext` method when clicked. -``` -< b-virtual-scroll & - ref = scroll | - :dataProvider = 'Provider' | - :disableObserver = true | - :shouldPerformDataRender = () => true | - ... -. - -< b-button & - @click = $refs.scroll.initLoadNext -. - Load more data -``` + ```snakeskin + < b-virtual-scroll & + ref = scroll | + :dataProvider = 'Provider' | + :disableObserver = true | + :shouldPerformDataRender = () => true | + ... + . + + < b-button & + @click = $refs.scroll.initLoadNext + . + Load more data + ``` Now, when you click the button, data will be loaded and rendered. However, you may notice that the data loading button doesn't disappear when all data is loaded, during data loading, or in case of an error. Fortunately, `b-virtual-scroll` provides a slot for displaying such a button, and it handles the logic of hiding it during loading, errors, and so on. Clients don't need to implement additional logic; you just need to move your button to the appropriate slot, specifically the `renderNext` slot. -``` +```snakeskin < b-virtual-scroll & ref = scroll | :dataProvider = 'Provider' | @@ -281,10 +289,11 @@ If you have filters on the page and a data request that should be rendered using Let's consider an example: -__p-page.ts__ +**p-page.ts** + ```typescript @component() -class pPage extends extends iDynamicPage { +class pPage extends iDynamicPage { @field() filterUuid: string; @@ -294,8 +303,9 @@ class pPage extends extends iDynamicPage { } ``` -__p-page.ss__ -``` +**p-page.ss** + +```snakeskin < b-virtual-scroll & :dataProvider = 'Provider' | :request = {get: {count: 12, filter: filterUuid}} | @@ -314,7 +324,7 @@ In addition to the `initLoadNext` method, `b-virtual-scroll` provides a `retry` This makes it straightforward to implement a retry mechanism for a failed request: -``` +```snakeskin < b-virtual-scroll & :dataProvider = 'Provider' | :request = {get: {count: 12, filter: filterUuid}} | @@ -331,10 +341,11 @@ The `b-virtual-scroll` component is quite substantial and has its own internal s To retrieve the component's state, you can use a special method called `getVirtualScrollState`: -__p-page.ts__ +**p-page.ts** + ```typescript @component() -class pPage extends extends iDynamicPage { +class pPage extends iDynamicPage { protected override readonly $refs!: { scroll: bVirtualScroll; }; @@ -360,7 +371,7 @@ interface VirtualScrollDb { The `data` array should contain the data items used to render the components. The `dbConverter` prop allows you to convert data into a format suitable for `b-virtual-scroll` after data has been loaded. -``` +```snakeskin < b-virtual-scroll & ... :dbConverter = (data) => ({data: data.nestedData.data}) @@ -514,7 +525,7 @@ As you can see in the example above, we access the last chunk of loaded data and ### `itemsProcessors` and Global Component Processing -This prop is a middleware function that is called after `b-virtual-scroll` has compiled the abstract representation of components and before it passes this representation to the rendering engine. Each function in the chain receives the result of the previous function, with the first function in the chain receiving the result of the `itemsFactory` call. The function should return an abstract representation of components that conforms to the `ComponentItem[]` interface. +This prop is a middleware function that is called after `b-virtual-scroll` has compiled the abstract representation of components, and before it passes this representation to the rendering engine. Each function in the chain receives the result of the previous function, with the first function in the chain receiving the result of the `itemsFactory` call. The function should return an abstract representation of components that conforms to the `ComponentItem[]` interface. Here is an example to illustrate when `itemsProcessors` is called: @@ -524,12 +535,14 @@ With this prop, you can implement various scenarios, such as changing one compon Here's an example scenario where we need to change the name of one component to another: -__@v4fire/client/components/base/b-virtual-scroll/const.ts__ +**@v4fire/client/components/base/b-virtual-scroll/const.ts** + ```typescript export const itemsProcessors: ItemsProcessors = {}; ``` -__your-project/components/base/b-virtual-scroll/const.ts__ +**your-project/components/base/b-virtual-scroll/const.ts** + ```typescript import { itemsProcessors } from '@v4fire/client/components/base/b-virtual-scroll/const.ts' @@ -562,55 +575,55 @@ Let's also look at another common scenario: **Solution**: Instead of manually defining the `itemsFactory` function in multiple places to call a pre-prepared function, you can: -1. Establish an agreement with clients to mark the components before or after which advertising should be displayed using meta information of the component's abstract representation (`ComponentItem`), which will be passed from the client to the component via the `itemMeta` prop. + 1. Establish an agreement with clients to mark the components before or after which advertising should be displayed using meta information of the component's abstract representation (`ComponentItem`), which will be passed from the client to the component via the `itemMeta` prop: - ``` - < b-virtual-scroll & - // ... - :itemMeta = (data) => ({ads: data.component === 'b-card' ? 'after' : false}) - . - ``` + ```snakeskin + < b-virtual-scroll & + // ... + :itemMeta = (data) => ({ads: data.component === 'b-card' ? 'after' : false}) + . + ``` -2. Implement a global `itemsProcessor` that will add advertisements based on the meta-information. + 2. Implement a global `itemsProcessor` that will add advertisements based on the meta-information. - ```typescript - import { itemsProcessors } from '@v4fire/client/components/base/b-virtual-scroll/const.ts' + ```typescript + import { itemsProcessors } from '@v4fire/client/components/base/b-virtual-scroll/const.ts' - export const itemsProcessors: ItemsProcessors = { - ...itemsProcessors, + export const itemsProcessors: ItemsProcessors = { + ...itemsProcessors, - addAds: (items: ComponentItem[]) => { - const newItems: ComponentItem[] = []; + addAds: (items: ComponentItem[]) => { + const newItems: ComponentItem[] = []; - const adsComponent = { - item: 'b-ads', - key: current.uuid + 'ads', - type: 'item', - children: [], - props: { - // ... - } - } + const adsComponent = { + item: 'b-ads', + key: current.uuid + 'ads', + type: 'item', + children: [], + props: { + // ... + } + } - return items.map((item) => { - const itemsToPush = []; - itemsToPush.push(item); + return items.map((item) => { + const itemsToPush = []; + itemsToPush.push(item); - if (item.meta.ads === 'after') { - itemsToPush.push(adsComponent); - } + if (item.meta.ads === 'after') { + itemsToPush.push(adsComponent); + } - if (item.meta.ads === 'before') { - itemsToPush.unshift(adsComponent); - } + if (item.meta.ads === 'before') { + itemsToPush.unshift(adsComponent); + } - newItems.push(...itemsToPush); - }); + newItems.push(...itemsToPush); + }); - return newItems; - } - }; - ``` + return newItems; + } + }; + ``` After these steps, a neighboring advertising component will be added to all components with the appropriate `meta.ads` value. @@ -751,13 +764,13 @@ There may also be situations where you need to modify the `renderGuard`. Current Yes, you can. `b-virtual-scroll` provides two options: - 1. Specify the `chunkSize` prop as a function that returns a number depending on something. Let’s say we want to render 6 elements at the first render, 12 at the second, and 18 in subsequent ones: + 1. Specify the `chunkSize` prop as a function that returns a number depending on something. Let’s say we want to render 6 elements at the first render, 12 at the second, and 18 in subsequent ones: - ```typescript - const chunkSize = (state: VirtualScrollState) => [6, 12, 18][state.renderPage] ?? 18 - ``` + ```typescript + const chunkSize = (state: VirtualScrollState) => [6, 12, 18][state.renderPage] ?? 18 + ``` - 2. Use the `itemsFactory` prop and return any number of elements from this function. + 2. Use the `itemsFactory` prop and return any number of elements from this function. - Suppose I want to load 1000 data items once and not make any more requests. How can I achieve this? @@ -789,61 +802,61 @@ There may also be situations where you need to modify the `renderGuard`. Current The component supports several slots for customization: -1. The `loader` slot allows you to display different content (usually skeletons) while the data is being loaded. +1. The `loader` slot allows you to display different content (usually skeletons) while the data is being loaded: -``` -< b-virtual-scroll - < template #loader - < .&__loader - Data loading in progress -``` + ```snakeskin + < b-virtual-scroll + < template #loader + < .&__loader + Data loading in progress + ``` -2. The `tombstone` slot allows you to display different content (usually skeletons) that will be repeated `tombstoneCount` times while the data is being loaded. +2. The `tombstone` slot allows you to display different content (usually skeletons) that will be repeated `tombstoneCount` times while the data is being loaded: -``` -< b-virtual-scroll :tombstoneCount = 3 - < template #tombstone - < .&__skeleton - Skeleton -``` + ```snakeskin + < b-virtual-scroll :tombstoneCount = 3 + < template #tombstone + < .&__skeleton + Skeleton + ``` -3. The `retry` slot allows you to display different content (usually a prompt to retry loading data) when there is an error in data loading. +3. The `retry` slot allows you to display different content (usually a prompt to retry loading data) when there is an error in data loading: -``` -< b-virtual-scroll - < template #retry - < .&__retry @click = initLoadNext - Retry last request -``` + ```snakeskin + < b-virtual-scroll + < template #retry + < .&__retry @click = initLoadNext + Retry last request + ``` -4. The `empty` slot allows you to display different content when the component receives an empty data set during the initial loading. +4. The `empty` slot allows you to display different content when the component receives an empty data set during the initial loading: -``` -< b-virtual-scroll - < template #empty - < .&__empty - No data -``` + ```snakeskin + < b-virtual-scroll + < template #empty + < .&__empty + No data + ``` 5. The `done` slot allows you to display different content when the component has finished loading and rendering all the data. The `done` slot -will be displayed after `lifecycleDone` event is fired. +will be displayed after `lifecycleDone` event is fired: -``` -< b-virtual-scroll - < template #done - < .&__done - Load and render complete -``` + ```snakeskin + < b-virtual-scroll + < template #done + < .&__done + Load and render complete + ``` 6. The `renderNext` slot allows you to display different content when the component is not loading data and has not entered the lifecycle completion state. -This slot can be useful when implementing lazy content rendering on button click. +This slot can be useful when implementing lazy content rendering on button click: -``` -< b-virtual-scroll - < template #renderNext - < .&__render-next - Render next -``` + ```snakeskin + < b-virtual-scroll + < template #renderNext + < .&__render-next + Render next + ``` ## API @@ -1007,7 +1020,7 @@ const itemsFactory = (state: VirtualScrollState): ComponentItem[] => { - Type: `Function | Record | Function[]` - Default: `{}` -This prop is a middleware function that is called after `b-virtual-scroll` has compiled the abstract representation of components and before it passes this representation to the rendering engine. +This prop is a middleware function that is called after `b-virtual-scroll` has compiled the abstract representation of components, and before it passes this representation to the rendering engine. This function can be useful in cases where you need to implement some processing of the abstract representation of components, such as mutating props or adding additional components. diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts index b6d53b9ffd..b1ec1035c9 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts @@ -113,7 +113,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI } /** - * Returns the internal component state. + * Returns the internal component state * {@link VirtualScrollState} */ getVirtualScrollState(): Readonly { @@ -121,7 +121,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI } /** - * Returns the next slice of data that should be rendered. + * Returns the next slice of data that should be rendered * * @param state * @param chunkSize @@ -187,7 +187,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI } /** - * Merges all request parameters from the component fields `requestProp` and `requestQuery`. + * Merges all request parameters from the component fields `requestProp` and `requestQuery` * {@link RequestParams} */ protected getRequestParams(): RequestParams { diff --git a/src/components/base/b-virtual-scroll/props.ts b/src/components/base/b-virtual-scroll/props.ts index 72bb025d97..2baca51348 100644 --- a/src/components/base/b-virtual-scroll/props.ts +++ b/src/components/base/b-virtual-scroll/props.ts @@ -17,10 +17,11 @@ import type { ComponentDb, RequestQueryFn, ShouldPerform, + ItemsProcessors, + ComponentItemFactory, ComponentItemType, ComponentItem, - ItemsProcessors, ComponentItemMeta } from 'components/base/b-virtual-scroll/interface'; @@ -31,10 +32,6 @@ import type { Observer } from 'components/base/b-virtual-scroll/modules/observer import iData, { component, prop } from 'components/super/i-data/i-data'; -/** - * A class that is a part of the {@link bVirtualScroll}. - * It contains the properties of the {@link bVirtualScroll} component. - */ @component() export default abstract class iVirtualScrollProps extends iData { /** {@link iItems.item} */ From e70e19cac9877bf3a35907f746d63fe007d13f59 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 8 Jan 2024 17:24:07 +0300 Subject: [PATCH 128/159] :art: --- .../base/b-virtual-scroll/README.md | 55 +++++++------------ .../b-virtual-scroll/interface/component.ts | 4 +- 2 files changed, 22 insertions(+), 37 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 1752300e37..9a4ec1c548 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -29,13 +29,13 @@ - [Slots](#slots) - [API](#api) - [Props](#props) - - [`shouldPerformDataRender`](#shouldperformdatarender) - - [`shouldPerformDataRequest`](#shouldperformdatarequest) - - [`shouldStopRequestingData`](#shouldstoprequestingdata) - - [`chunkSize`](#chunksize) - - [`requestQuery`](#requestquery) - - [`itemsFactory`](#itemsfactory) - - [`itemsProcessors`](#itemsprocessors) + - [\[shouldPerformDataRender = `(state: VirtualScrollState) => state.isInitialRender || state.remainingItems === 0`\]](#shouldperformdatarender--state-virtualscrollstate--stateisinitialrender--stateremainingitems--0) + - [\[shouldPerformDataRequest = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`\]](#shouldperformdatarequest--state-virtualscrollstate--statelastloadeddatalength--0) + - [\[shouldStopRequestingData = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`\]](#shouldstoprequestingdata--state-virtualscrollstate--statelastloadeddatalength--0) + - [\[chunkSize = `10`\]](#chunksize--10) + - [\[requestQuery\]](#requestquery) + - [\[itemsFactory\]](#itemsfactory) + - [\[itemsProcessors = `{}`\]](#itemsprocessors--) - [`tombstoneCount`](#tombstonecount) - [Methods](#methods) - [getNextDataSlice](#getnextdataslice) @@ -43,7 +43,7 @@ - [initLoadNext](#initloadnext) - [Other Properties](#other-properties) - [Migration from `b-virtual-scroll` version 3.x.x](#migration-from-b-virtual-scroll-version-3xx) - - [API](#api-1) + - [API Migration](#api-migration) - [What's Next](#whats-next) - [Streaming Data Rendering](#streaming-data-rendering) - [Alternative Approach to Component Rendering](#alternative-approach-to-component-rendering) @@ -862,10 +862,7 @@ This slot can be useful when implementing lazy content rendering on button click ### Props -#### `shouldPerformDataRender` - -- Type: `Function` -- Default: `(state: VirtualScrollState) => state.isInitialRender || state.remainingItems === 0` +#### [shouldPerformDataRender = `(state: VirtualScrollState) => state.isInitialRender || state.remainingItems === 0`] This function is called in the `bVirtualScroll.renderGuard` after other checks are completed. It receives the component state as input and determines whether the component should render the next chunk of components. @@ -879,10 +876,7 @@ const shouldPerformDataRender = (state: VirtualScrollState): boolean => { }; ``` -#### `shouldPerformDataRequest` - -- Type: `Function` -- Default: `(state: VirtualScrollState) => state.lastLoadedData.length > 0` +#### [shouldPerformDataRequest = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`] The `shouldPerformDataRequest` property of `bVirtualScroll` allows you to control whether the component should request additional data based on the component state. This function allows the component to understand whether the data loading lifecycle is complete or not. @@ -903,10 +897,7 @@ You can adjust the condition based on your specific requirements. By implementing the `shouldPerformDataRequest` function, you have control over when the component should request additional data. This allows you to customize the data loading behavior based on the state of the component. -#### `shouldStopRequestingData` - -- Type: `Function` -- Default: `(state: VirtualScrollState) => state.lastLoadedData.length > 0` +#### [shouldStopRequestingData = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`] This function is called on each data loading cycle. It determines whether the component should stop requesting new data. The function should return a boolean value: `true` to stop requesting data, or `false` to continue requesting data. @@ -927,10 +918,7 @@ This condition suggests that all available items have been loaded, and there is You can customize the `shouldStopRequestingData` function to fit your specific scenario. By implementing this function, you have control over when the component should stop requesting new data, based on the comparison between the total number of items and the current number of loaded items. -#### `chunkSize` - -- Type: `number | Function` -- Default: `10` +#### [chunkSize = `10`] The amount of data required to perform one cycle of item rendering. This prop is used by the `bVirtualScroll` component to determine the number of components to render in each cycle. It can be either a fixed number or a function that returns the number dynamically based on the component state. @@ -959,7 +947,7 @@ In Example 2, the chunk size is dynamically determined based on the component st By using a function for `chunkSize`, you have the flexibility to adjust the rendering behavior based on the state of the component and other factors. -#### `requestQuery` +#### [requestQuery] - Type: `Function` - Default: `undefined` @@ -981,10 +969,7 @@ const requestQuery = (state: VirtualScrollState): Dictionary => { }; ``` -#### `itemsFactory` - -- Type: `Function` -- Default: See description +#### [itemsFactory] A factory function used to generate an array of `ComponentItem` objects representing the components to be rendered. This function is called during the rendering process and receives the component state and context as arguments. It should return an array of `ComponentItem` objects. @@ -1015,10 +1000,7 @@ const itemsFactory = (state: VirtualScrollState): ComponentItem[] => { }; ``` -#### `itemsProcessors` - -- Type: `Function | Record | Function[]` -- Default: `{}` +#### [itemsProcessors = `{}`] This prop is a middleware function that is called after `b-virtual-scroll` has compiled the abstract representation of components, and before it passes this representation to the rendering engine. @@ -1054,9 +1036,12 @@ The `bVirtualScroll` class extends `iData` and includes additional properties re ## Migration from `b-virtual-scroll` version 3.x.x -### API +### API Migration - Prop `renderGap` deleted -> use `shouldPerformDataRender`; +- Prop `shouldRequestMore` deleted -> use `shouldPerformDataRequest`; +- Prop `shouldStopRequest` deleted -> use `shouldStopRequestingData`; +- Prop `getData` was removed; - Deprecated props `option-like` deleted -> use `iItems` props; - Method renamed `getDataStateSnapshot` -> `getVirtualScrollState`; - Method `reloadLast` -> `initLoadNext`; @@ -1076,7 +1061,7 @@ The `bVirtualScroll` class extends `iData` and includes additional properties re - Interface `DataState` -> `VirtualScrollState`: - `DataState.currentPage` -> `VirtualScrollState.loadPage`; - - `DataState.lastLoadedChunk.raw` -> `VirtualScrollState.lastLoadedRaw`; + - `DataState.lastLoadedChunk.raw` -> `VirtualScrollState.lastLoadedRawData`; - etc. ## What's Next diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts index cf09c5ec86..7d04aa64a5 100644 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -93,8 +93,8 @@ export interface VirtualScrollState { * then {@link VirtualScrollState.isLastRender} will always be `undefined`. * * To achieve correct `isLastRender` behavior, it is necessary to implement a request - * stopping strategy in such a way that the last render occurs after all loading has - * been completed. For example, this can be implemented if your backend pagination + * stopping strategy in such a way that **the last render occurs after all loading has + * been completed**. For example, this can be implemented if your backend pagination * response has a property indicating the total number of items that can be loaded, * this property can be used for comparison: * From 6bec87556521af8e3ec9032babd9b6e91eeb9d97 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 8 Jan 2024 17:28:23 +0300 Subject: [PATCH 129/159] :art: --- src/components/base/b-virtual-scroll/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 9a4ec1c548..41a86c3124 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -29,13 +29,13 @@ - [Slots](#slots) - [API](#api) - [Props](#props) - - [\[shouldPerformDataRender = `(state: VirtualScrollState) => state.isInitialRender || state.remainingItems === 0`\]](#shouldperformdatarender--state-virtualscrollstate--stateisinitialrender--stateremainingitems--0) - - [\[shouldPerformDataRequest = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`\]](#shouldperformdatarequest--state-virtualscrollstate--statelastloadeddatalength--0) - - [\[shouldStopRequestingData = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`\]](#shouldstoprequestingdata--state-virtualscrollstate--statelastloadeddatalength--0) - - [\[chunkSize = `10`\]](#chunksize--10) - - [\[requestQuery\]](#requestquery) - - [\[itemsFactory\]](#itemsfactory) - - [\[itemsProcessors = `{}`\]](#itemsprocessors--) + - [[shouldPerformDataRender = `(state: VirtualScrollState) => state.isInitialRender || state.remainingItems === 0`]](#shouldperformdatarender--state-virtualscrollstate--stateisinitialrender--stateremainingitems--0) + - [[shouldPerformDataRequest = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`]](#shouldperformdatarequest--state-virtualscrollstate--statelastloadeddatalength--0) + - [[shouldStopRequestingData = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`]](#shouldstoprequestingdata--state-virtualscrollstate--statelastloadeddatalength--0) + - [[chunkSize = `10`]](#chunksize--10) + - [[requestQuery]](#requestquery) + - [[itemsFactory]](#itemsfactory) + - [[itemsProcessors = `{}`]](#itemsprocessors--) - [`tombstoneCount`](#tombstonecount) - [Methods](#methods) - [getNextDataSlice](#getnextdataslice) From 7c986898a6227ab4b4d8a1b7122e4e94d2f9d69f Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 8 Jan 2024 17:46:23 +0300 Subject: [PATCH 130/159] Remove wrong code --- .../pages/p-v4-components-demo/p-v4-components-demo.ss | 1 - src/components/super/i-block/interface.ts | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss b/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss index 6c86499141..2988d4c196 100644 --- a/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss +++ b/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss @@ -11,4 +11,3 @@ - include 'components/super/i-static-page/i-static-page.component.ss'|b as placeholder - template index() extends ['i-static-page.component'].index - - block body diff --git a/src/components/super/i-block/interface.ts b/src/components/super/i-block/interface.ts index 607328d1aa..b6333e616e 100644 --- a/src/components/super/i-block/interface.ts +++ b/src/components/super/i-block/interface.ts @@ -88,9 +88,6 @@ export interface UnsafeIBlock extends UnsafeCompone // @ts-ignore (access) localEmitter: CTX['localEmitter']; - // @ts-ignore (access) - selfEmitter: CTX['selfEmitter']; - // @ts-ignore (access) parentEmitter: CTX['parentEmitter']; From a7cae662d37f5040f3ba71658f1a453e3c01847e Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 8 Jan 2024 18:05:24 +0300 Subject: [PATCH 131/159] Update yarn.lock --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4de1552d48..cbc8d078fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5646,8 +5646,8 @@ __metadata: linkType: hard "@v4fire/core@github:V4Fire/Core#v4": - version: 4.0.0-alpha.13 - resolution: "@v4fire/core@https://github.com/V4Fire/Core.git#commit=8d79a4402a21bfff1df63467118d9b246e82a23e" + version: 4.0.0-alpha.16 + resolution: "@v4fire/core@https://github.com/V4Fire/Core.git#commit=44bf4e874e6c0e3a086cb37c6fc1d9c92d6ad563" dependencies: "@babel/core": "npm:7.17.5" "@babel/helper-module-transforms": "npm:7.16.7" @@ -5791,7 +5791,7 @@ __metadata: optional: true xhr2: optional: true - checksum: bdb8cd0d3e956fedd1dcf29f84685f2345ecff96b4be8043bbd9f2a8056bcf79b7cb114433a8c1916f846cef70321433755614e7ba10ca64a642087a6f6aaac8 + checksum: c9fcab75ac8ca0549beeca11e7ac7d2b2bd2107ec579c7a7482a4108d2de6252548bf1dd8743d3a8ea11816ebf82c6eb6a591fc44df7adca0abd490020590c73 languageName: node linkType: hard From 3a9dc9b9436b43d40df08777cd4c41c381b554f4 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 15 Jan 2024 14:48:00 +0300 Subject: [PATCH 132/159] Fixed an issue with providing child elements in createComponentViaDummy --- .../test/unit/functional/props/props.ts | 2 +- .../test/unit/scenario/reload.ts | 14 +++++---- src/components/dummies/b-dummy/b-dummy.ss | 3 +- src/components/dummies/b-dummy/b-dummy.ts | 12 ++++++-- tests/helpers/component-object/README.md | 2 +- tests/helpers/component-object/builder.ts | 7 +++-- tests/helpers/component/index.ts | 30 +++++++++++++++---- tests/helpers/component/interface.ts | 2 +- 8 files changed, 52 insertions(+), 20 deletions(-) diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts index 3437ca89ef..8ea2461b2b 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts @@ -49,7 +49,7 @@ test.describe('', () => { .build({useDummy: true}); await component.waitForChildCountEqualsTo(chunkSize); - await component.updateProps({chunkSize: chunkSize * 2}); + await component.updateProps({attrs: {chunkSize: chunkSize * 2}}); await component.scrollToBottom(); await component.waitForChildCountEqualsTo(chunkSize * 3); diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts index 13dbbdbb52..9f648d9fdf 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts @@ -51,12 +51,14 @@ test.describe('', () => { await component.waitForChildCountEqualsTo(chunkSize[0]); await component.updateProps({ - request: { - get: { - chunkSize: chunkSize[1] - } - }, - chunkSize: chunkSize[1] + attrs: { + request: { + get: { + chunkSize: chunkSize[1] + } + }, + chunkSize: chunkSize[1] + } }); await component.waitForDataIndexChild(chunkSize[1] - 1); diff --git a/src/components/dummies/b-dummy/b-dummy.ss b/src/components/dummies/b-dummy/b-dummy.ss index 72b261584a..1a0f72333a 100644 --- a/src/components/dummies/b-dummy/b-dummy.ss +++ b/src/components/dummies/b-dummy/b-dummy.ss @@ -16,7 +16,8 @@ < component & ref = testComponent | :is = testComponent | - :v-attrs = testComponentAttrs + :v-attrs = testComponentAttrs | + v-render = testComponentSlots . < template v-else diff --git a/src/components/dummies/b-dummy/b-dummy.ts b/src/components/dummies/b-dummy/b-dummy.ts index dadbae5bf0..639e431bb1 100644 --- a/src/components/dummies/b-dummy/b-dummy.ts +++ b/src/components/dummies/b-dummy/b-dummy.ts @@ -11,6 +11,8 @@ * @packageDocumentation */ +import type { VNode } from 'core/component/engines'; + import type iBlock from 'components/super/i-block/i-block'; import iData, { component, field } from 'components/super/i-data/i-data'; @@ -24,17 +26,23 @@ export * from 'components/super/i-data/i-data'; class bDummy extends iData { /** - * Name of the test component. + * Name of the test component */ @field() testComponent?: string; /** - * Attributes for the test component. + * Attributes for the test component */ @field() testComponentAttrs: Dictionary = {}; + /** + * Slots for the test component + */ + @field() + testComponentSlots?: CanArray + protected override readonly $refs!: iData['$refs'] & { testComponent?: iBlock; }; diff --git a/tests/helpers/component-object/README.md b/tests/helpers/component-object/README.md index aabd9a8645..a2e1e299ac 100644 --- a/tests/helpers/component-object/README.md +++ b/tests/helpers/component-object/README.md @@ -160,7 +160,7 @@ myComponent await myComponent.build({ useDummy: true }); // Change props -await myComponent.updateProps({ prop1: 'newVal' }); +await myComponent.updateProps({ attrs: { prop1: 'newVal' } }); ``` Please note that there are some nuances to consider when creating a component with a `b-dummy` wrapper, such as you cannot set slots for such a component. diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index 33a8098846..b35f4bbdfb 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -163,7 +163,10 @@ export default abstract class ComponentObjectBuilder { fullComponentName = `${name}${options?.functional && !name.endsWith('-functional') ? '-functional' : ''}`; if (options?.useDummy) { - const component = await Component.createComponentInDummy(this.pwPage, fullComponentName, this.props); + const component = await Component.createComponentInDummy(this.pwPage, fullComponentName, { + attrs: this.props, + children: this.children + }); this.dummy = component; this.componentStore = component; @@ -269,7 +272,7 @@ export default abstract class ComponentObjectBuilder { * * @throws {@link ReferenceError} - if the component object was not built or was built without the `useDummy` option */ - updateProps(props: Dictionary): Promise { + updateProps(props: RenderComponentsVnodeParams): Promise { if (!this.dummy) { throw new ReferenceError('Failed to update props. Missing "b-dummy" component.'); } diff --git a/tests/helpers/component/index.ts b/tests/helpers/component/index.ts index c0bc5993be..c712bb0dfc 100644 --- a/tests/helpers/component/index.ts +++ b/tests/helpers/component/index.ts @@ -8,6 +8,7 @@ import type { ElementHandle, JSHandle, Page } from 'playwright'; +import type { VNodeDescriptor } from 'components/friends/vdom'; import { expandedStringify } from 'core/prelude/test-env/components/json'; import type iBlock from 'components/super/i-block/i-block'; @@ -129,28 +130,45 @@ export default class Component { * * @param page * @param componentName - * @param attrs + * @param params */ static async createComponentInDummy( page: Page, componentName: string, - attrs: RenderComponentsVnodeParams['attrs'] + params: RenderComponentsVnodeParams ): Promise> { const dummy = await this.createComponent(page, 'b-dummy'); - const setProps = async (props) => { + const update = async (props) => { await dummy.evaluate((ctx, [name, props]) => { - ctx.testComponentAttrs = globalThis.expandedParse(props); + const parsed: RenderComponentsVnodeParams = globalThis.expandedParse(props); + + ctx.testComponentAttrs = parsed.attrs ?? {}; + + if (parsed.children) { + ctx.testComponentSlots = compileChild(); + } + ctx.testComponent = name; + function compileChild() { + return ctx.vdom.create(Object.entries(parsed.children ?? {}).map(([slotName, child]) => ({ + type: 'template', + attrs: { + slot: slotName + }, + children: ([]).concat(child ?? []) + }))); + } + }, [componentName, expandedStringify(props)]); }; - await setProps(attrs); + await update(params); const component = await dummy.evaluateHandle((ctx) => ctx.unsafe.$refs.testComponent); Object.assign(component, { - setProps, + setProps: update, dummy }); diff --git a/tests/helpers/component/interface.ts b/tests/helpers/component/interface.ts index 700c5562d1..175051272e 100644 --- a/tests/helpers/component/interface.ts +++ b/tests/helpers/component/interface.ts @@ -13,6 +13,6 @@ import type { JSHandle } from 'playwright'; * Handle component interface that was created with a dummy wrapper. */ export interface ComponentInDummy extends JSHandle { - setProps(props: Dictionary): Promise; + setProps(props: RenderComponentsVnodeParams): Promise; dummy: JSHandle; } From 4938eacc30c53057afaf89b78f1a9b2b6199d675 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 24 Jan 2024 16:27:39 +0300 Subject: [PATCH 133/159] Added items prop to b-virtual-scroll --- .../base/b-virtual-scroll/README.md | 27 ++- .../base/b-virtual-scroll/b-virtual-scroll.ts | 53 ++++- src/components/base/b-virtual-scroll/const.ts | 15 ++ .../base/b-virtual-scroll/handlers.ts | 21 +- .../b-virtual-scroll/interface/component.ts | 11 + src/components/base/b-virtual-scroll/props.ts | 4 + .../test/api/component-object/index.ts | 8 + .../test/api/helpers/index.ts | 30 ++- .../test/unit/functional/props/props.ts | 2 +- .../unit/functional/rendering/items-mode.ts | 71 +++++++ .../test/unit/functional/state/emitter.ts | 190 ++++++++++++++++++ .../test/unit/scenario/reload.ts | 2 +- src/components/dummies/b-dummy/b-dummy.ts | 2 +- tests/helpers/component-object/builder.ts | 38 +++- tests/helpers/component-object/mock.ts | 8 +- tests/helpers/component/index.ts | 4 +- 16 files changed, 454 insertions(+), 32 deletions(-) create mode 100644 src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-mode.ts diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 41a86c3124..4f8d762da9 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -100,7 +100,7 @@ The component offers various usage options: it can load and render data on scrol Below, we will explore a few basic usage scenarios and delve into the component's API in greater detail. -### How to Implement Simple Rendering? +### How to Implement Simple Rendering via DataProvider? To implement simple rendering, you need to follow several steps: @@ -187,6 +187,31 @@ Let's add a `loader` slot to our component to provide a better user experience d Now, users will see a friendly message indicating that content will appear shortly, preventing them from being surprised by sudden content changes. +### How to Implement Simple Rendering via `items`? + +The approach to rendering data using the `items` prop is not significantly different from the approach when data is obtained from a `dataProvider`. + +Instead of passing the `dataProvider` and request* parameters, +you need to pass the items prop which contains an array of data to be rendered by the components: + + ```snakeskin + < b-virtual-scroll & + :chunkSize = 12 | + :items = data | + :item = 'b-dummy' | + :itemKey = (el) => el.uuid | + :itemProps = (el) => ({name: el.name, type: el.type}) + . + ``` + + These data can be loaded by some other component or they can be static. + It doesn't matter, what's important is that the `b-virtual-scroll` component will take these data and process them through the rendering pipeline. + +There are also some minor differences in the component's event model. +Unlike `b-virtual-scroll` which uses a `dataProvider`, a component with items will not emit certain events, specifically `dataLoadStart` and `convertDataToDB`. + +The component will also ignore the `shouldPerformDataRequest` and `shouldStopRequestingData` props, as they have no meaning when there is no `dataProvider`. + ### How to Implement Component Rendering on Click Instead of Scroll? The `b-virtual-scroll` component, in addition to scroll-based loading, can also load data on other events, such as a click on a button. diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts index a2dada4ceb..b86fc0d775 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts @@ -17,8 +17,8 @@ import type { AsyncOptions } from 'core/async'; import type iItems from 'components/traits/i-items/i-items'; import VDOM, { create, render } from 'components/friends/vdom'; import { iVirtualScrollHandlers } from 'components/base/b-virtual-scroll/handlers'; -import { bVirtualScrollAsyncGroup, bVirtualScrollDomInsertAsyncGroup, renderGuardRejectionReason } from 'components/base/b-virtual-scroll/const'; -import type { VirtualScrollState, RenderGuardResult, $ComponentRefs, UnsafeBVirtualScroll, ItemsProcessors } from 'components/base/b-virtual-scroll/interface'; +import { bVirtualScrollAsyncGroup, bVirtualScrollDomInsertAsyncGroup, componentModes, renderGuardRejectionReason } from 'components/base/b-virtual-scroll/const'; +import type { VirtualScrollState, RenderGuardResult, $ComponentRefs, UnsafeBVirtualScroll, ItemsProcessors, ComponentMode } from 'components/base/b-virtual-scroll/interface'; import { ComponentTypedEmitter, componentTypedEmitter } from 'components/base/b-virtual-scroll/modules/emitter'; import { ComponentInternalState } from 'components/base/b-virtual-scroll/modules/state'; @@ -26,7 +26,7 @@ import { SlotsStateController } from 'components/base/b-virtual-scroll/modules/s import { ComponentFactory } from 'components/base/b-virtual-scroll/modules/factory'; import { Observer } from 'components/base/b-virtual-scroll/modules/observer'; -import iData, { component, system, RequestParams, UnsafeGetter } from 'components/super/i-data/i-data'; +import iData, { component, system, watch, wait, RequestParams, UnsafeGetter } from 'components/super/i-data/i-data'; export * from 'components/base/b-virtual-scroll/interface'; export * from 'components/base/b-virtual-scroll/const'; @@ -75,6 +75,13 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI return Object.cast(this); } + /** + * {@link ComponentMode} + */ + get componentMode(): ComponentMode { + return this.items ? componentModes.items : componentModes.dataProvider; + } + /** * Initializes the loading of the next data chunk * @throws {@link ReferenceError} if there is no `dataProvider` set. @@ -99,7 +106,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI const params = this.getRequestParams(), - get = this.dataProvider.get(params[0], params[1]); + get = this.dataProvider.get(params[0], {...params[1], showProgress: false}); return get .then((res) => { @@ -157,6 +164,7 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI return super.reload(...args); } + @watch({path: 'items', provideArgs: false}) override initLoad(...args: Parameters): ReturnType { if (!this.lfc.isBeforeCreate()) { this.reset(); @@ -167,6 +175,16 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI const initLoadResult = super.initLoad(...args); + if (this.componentMode === componentModes.items) { + if (Object.isPromise(initLoadResult)) { + return initLoadResult + .then(() => this.initItems()) + .catch(stderr); + } + + return this.initItems(); + } + this.onDataLoadStart(true); if (Object.isPromise(initLoadResult)) { @@ -179,14 +197,26 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI this.onDataLoadSuccess(true, this.db); }) .catch(stderr); - - } else { - this.onDataLoadSuccess(true, this.db); } return initLoadResult; } + /** + * Initializes the data passed through the items prop + */ + @wait({defer: true}) + protected initItems(): CanPromise { + if ( + this.componentMode !== componentModes.items || + !this.items + ) { + return; + } + + this.onItemsInit(this.items); + } + protected override convertDataToDB(data: unknown): O | this['DB'] { this.onConvertDataToDB(data); const result = super.convertDataToDB(data); @@ -220,6 +250,11 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI * this function again until the life cycle is updated and the state is reset. */ protected shouldStopRequestingDataWrapper(): boolean { + if (this.componentMode === componentModes.items) { + this.componentInternalState.setIsRequestsStopped(true); + return true; + } + const state = this.getVirtualScrollState(); if (state.areRequestsStopped) { @@ -237,6 +272,10 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI * state and context when calling {@link bVirtualScroll.shouldPerformDataRequest}. */ protected shouldPerformDataRequestWrapper(): boolean { + if (this.componentMode === componentModes.items) { + return false; + } + return this.shouldPerformDataRequest(this.getVirtualScrollState(), this); } diff --git a/src/components/base/b-virtual-scroll/const.ts b/src/components/base/b-virtual-scroll/const.ts index 80adb33675..6af04a3d70 100644 --- a/src/components/base/b-virtual-scroll/const.ts +++ b/src/components/base/b-virtual-scroll/const.ts @@ -20,6 +20,21 @@ export const bVirtualScrollAsyncGroup = 'b-virtual-scroll'; */ export const bVirtualScrollDomInsertAsyncGroup = `${bVirtualScrollAsyncGroup}:dom-insert`; +/** + * Component modes. + */ +export const componentModes = { + /** + * In this mode, data is not loaded via a data provider, but instead passed in through the items prop. + */ + items: 'items', + + /** + * In this mode, data is loaded via a data provider. + */ + dataProvider: 'dataProvider' +}; + /** * Component data-related events (emitted in `selfEmitter`). */ diff --git a/src/components/base/b-virtual-scroll/handlers.ts b/src/components/base/b-virtual-scroll/handlers.ts index e3480d94c6..136e17a64e 100644 --- a/src/components/base/b-virtual-scroll/handlers.ts +++ b/src/components/base/b-virtual-scroll/handlers.ts @@ -144,24 +144,27 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { protected onDataLoadSuccess(this: bVirtualScroll, isInitialLoading: boolean, data: unknown): void { this.componentInternalState.setIsLoadingInProgress(false); - if (!Object.isPlainObject(data) || !Array.isArray(data.data)) { - throw new ReferenceError('Missing "data" field in the loaded data'); + const + dataToProvide = Object.isPlainObject(data) ? data.data : data; + + if (!Array.isArray(dataToProvide)) { + throw new ReferenceError('Missing data to perform render'); } - this.componentInternalState.updateData(data.data, isInitialLoading); + this.componentInternalState.updateData(dataToProvide, isInitialLoading); this.componentInternalState.incrementLoadPage(); const isRequestsStopped = this.shouldStopRequestingDataWrapper(); - this.componentEmitter.emit(componentEvents.dataLoadSuccess, data.data, isInitialLoading); + this.componentEmitter.emit(componentEvents.dataLoadSuccess, dataToProvide, isInitialLoading); this.slotsStateController.loadingSuccessState(); if ( isInitialLoading && isRequestsStopped && - Object.size(data.data) === 0 + Object.size(dataToProvide) === 0 ) { this.onDataEmpty(); this.onLifecycleDone(); @@ -220,4 +223,12 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { this.componentEmitter.emit(componentEvents.elementEnter, component); } + + /** + * Handler: items to render was updated + * @param items + */ + protected onItemsInit(this: bVirtualScroll, items: Exclude): void { + this.onDataLoadSuccess(true, items); + } } diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts index 7d04aa64a5..d6c918e1c2 100644 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -7,6 +7,7 @@ */ import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { componentModes } from 'components/base/b-virtual-scroll/b-virtual-scroll'; /** * State of the current component lifecycle. @@ -146,6 +147,16 @@ export interface PrivateComponentState { dataOffset: number; } +/** + * {@link componentModes} + */ +export type ComponentModes = typeof componentModes; + +/** + * {@link ComponentModes} + */ +export type ComponentMode = keyof ComponentModes; + /** * Types of rendered components. */ diff --git a/src/components/base/b-virtual-scroll/props.ts b/src/components/base/b-virtual-scroll/props.ts index 2baca51348..58342b2d11 100644 --- a/src/components/base/b-virtual-scroll/props.ts +++ b/src/components/base/b-virtual-scroll/props.ts @@ -44,6 +44,10 @@ export default abstract class iVirtualScrollProps extends iData { @prop({type: [String, Function]}) readonly item?: iItems['item']; + /** {@link iItems.items} */ + @prop({type: [String, Function]}) + readonly items: iItems['items']; + /** {@link iItems.itemKey} */ @prop({type: [String, Function]}) readonly itemKey?: CreateFromItemFn; diff --git a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts index f7c9a4262e..82df6d4ed4 100644 --- a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/component-object/index.ts @@ -171,6 +171,14 @@ export class VirtualScrollComponentObject extends ComponentObject { + await Scroll.scrollToTop(this.pwPage); + return this; + } + /** * Adds default `itemProps` for pagination */ diff --git a/src/components/base/b-virtual-scroll/test/api/helpers/index.ts b/src/components/base/b-virtual-scroll/test/api/helpers/index.ts index d1f29fddbf..9bd608bb71 100644 --- a/src/components/base/b-virtual-scroll/test/api/helpers/index.ts +++ b/src/components/base/b-virtual-scroll/test/api/helpers/index.ts @@ -155,6 +155,7 @@ export function createDataConveyor( dataI = 0; itemsI = 0; childI = 0; + page = 0; total = undefined; childList = []; items = []; @@ -201,28 +202,41 @@ export function createStateApi( dataConveyor: DataConveyor ): StateApi { let - state = createInitialState(initial); + state = createInitialState(initial), + settled = {}; const obj: StateApi = { compile(override?: Partial): VirtualScrollState { - return { + const compiled = { ...state, - ...extractStateFromDataConveyor(dataConveyor), - ...override + ...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): StateApi { - state = { - ...state, - ...props - }; + Object.keys(props).forEach((key) => { + settled[key] = props[key]; + state[key] = props[key]; + }); return obj; }, reset(): void { state = createInitialState(initial); + settled = {}; dataConveyor.reset(); }, diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts index 8ea2461b2b..3437ca89ef 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts @@ -49,7 +49,7 @@ test.describe('', () => { .build({useDummy: true}); await component.waitForChildCountEqualsTo(chunkSize); - await component.updateProps({attrs: {chunkSize: chunkSize * 2}}); + await component.updateProps({chunkSize: chunkSize * 2}); await component.scrollToBottom(); await component.waitForChildCountEqualsTo(chunkSize * 3); diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-mode.ts b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-mode.ts new file mode 100644 index 0000000000..1e7d07180e --- /dev/null +++ b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-mode.ts @@ -0,0 +1,71 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * @file Basic test cases for component rendering data provided in items prop. + */ + +import test from 'tests/config/unit/test'; + +import type { ShouldPerform } from 'components/base/b-virtual-scroll/interface'; +import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; + +test.describe('', () => { + let + component: VirtualScrollTestHelpers['component'], + provider: VirtualScrollTestHelpers['provider'], + state: VirtualScrollTestHelpers['state']; + + test.beforeEach(async ({demoPage, page}) => { + await demoPage.goto(); + + ({component, provider, state} = await createTestHelpers(page)); + await provider.start(); + + await page.setViewportSize({height: 640, width: 360}); + }); + + test.describe('`chunkSize` is 12', () => { + test.describe('provided 36 elements in the items prop', () => { + test('should render 36 items', async () => { + const + chunkSize = 12; + + const items = [ + ...state.data.addData(chunkSize), + ...state.data.addData(chunkSize), + ...state.data.addData(chunkSize) + ]; + + const shouldPerformDataRender = await component.mockFn( + ({isInitialRender, remainingItems: remainingItems}) => isInitialRender || remainingItems === 0 + ); + + await component + .withPaginationItemProps() + .withProps({ + shouldPerformDataRender, + chunkSize, + items + }); + + await component.build(); + await component.waitForChildCountEqualsTo(chunkSize); + await component.scrollToBottom(); + await component.waitForChildCountEqualsTo(chunkSize * 2); + await component.scrollToBottom(); + await component.waitForChildCountEqualsTo(chunkSize * 3); + await component.scrollToBottom(); + await component.waitForChildCountEqualsTo(chunkSize * 3); + + await test.expect(component.childList).toHaveCount(chunkSize * 3); + }); + }); + }); +}); diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts index 2e5ddddff0..9a2bc46996 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts @@ -6,6 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +/* eslint-disable max-lines-per-function */ + /** * @file This file contains test cases that verify the correctness of the component state during event emission. */ @@ -324,4 +326,192 @@ test.describe('', () => { ]); }); }); + + test.describe('24 elements was provided in items prop', () => { + test('state at the time of emitting events must be correct', async () => { + const + chunkSize = 12, + total = chunkSize * 2; + + const states = [ + state.compile(observerInitialStateFields), + ( + state.data.addData(chunkSize * 2), + state.data.setTotal(total), + + state.set({ + loadPage: 1, + lastLoadedRawData: undefined, + areRequestsStopped: true + }).compile(observerInitialStateFields) + ), + ( + state.data.addItems(chunkSize), + state.set({renderPage: 1, isInitialRender: false}).compile(observerLoadedStateFields) + ), + ( + state.set({isLastRender: true}).compile() + ), + ( + state.data.addItems(chunkSize), + state.set({renderPage: 2}).compile() + ), + ( + state.set({isLifecycleDone: true}).compile() + ) + ]; + + await component + .withPaginationItemProps() + .withProps({ + chunkSize, + items: state.data.getDataChunk(0), + + '@hook:beforeDataCreate': (ctx) => { + const original = ctx.emit; + + ctx.emit = jestMock.mock((...args) => { + original(...args); + return [args[0], Object.fastClone(ctx.getVirtualScrollState())]; + }); + } + }) + .build(); + + await component.waitForChildCountEqualsTo(chunkSize); + await component.scrollToBottom(); + await component.waitForChildCountEqualsTo(chunkSize * 2); + await component.scrollToBottom(); + await component.waitForLifecycleDone(); + + const + spy = await component.getSpy((ctx) => ctx.emit), + results = filterEmitterResults(await spy.results, true, ['initLoadStart', 'initLoad']); + + test.expect(results).toEqual([ + ['initLoadStart', {...states[0], isLoadingInProgress: true}], + ['initLoad', {...states[0], isLoadingInProgress: true}], + ['dataLoadSuccess', states[1]], + ['renderStart', states[1]], + ['renderEngineStart', states[1]], + ['renderEngineDone', states[1]], + ['domInsertStart', states[2]], + ['domInsertDone', states[2]], + ['renderDone', states[2]], + ['renderStart', states[3]], + ['renderEngineStart', states[3]], + ['renderEngineDone', states[3]], + ['domInsertStart', states[4]], + ['domInsertDone', states[4]], + ['renderDone', states[4]], + ['lifecycleDone', states[5]] + ]); + }); + }); + + test.describe('24 elements was provided in items prop', () => { + const + chunkSize = 12, + total = chunkSize * 2; + + test.beforeEach(async () => { + state.data.addData(chunkSize * 2); + state.data.setTotal(total); + + await component + .withPaginationItemProps() + .withProps({ + chunkSize, + items: state.data.getDataChunk(0) + }) + .build({useDummy: true}); + + await component.waitForChildCountEqualsTo(chunkSize); + await component.scrollToBottom(); + await component.waitForChildCountEqualsTo(chunkSize * 2); + await component.scrollToBottom(); + await component.waitForLifecycleDone(); + }); + + test.describe('items prop has been updated', () => { + test('state at the time of emitting events must be correct', async () => { + state.reset(); + + await component.component.evaluate((ctx) => { + const original = Object.cast(ctx.emit); + + ctx.emit = jestMock.mock((...args) => { + original(...args); + return [args[0], Object.fastClone(ctx.getVirtualScrollState())]; + }); + }); + + const states = [ + state.compile(observerInitialStateFields), + ( + state.data.addData(chunkSize * 2), + state.data.setTotal(total), + + state.set({ + loadPage: 1, + lastLoadedRawData: undefined, + areRequestsStopped: true + }).compile(observerInitialStateFields) + ), + ( + state.data.addItems(chunkSize), + state.set({renderPage: 1, isInitialRender: false}).compile(observerLoadedStateFields) + ), + ( + state.set({isLastRender: true}).compile() + ), + ( + state.data.addItems(chunkSize), + state.set({renderPage: 2}).compile() + ), + ( + state.set({isLifecycleDone: true}).compile() + ) + ]; + + const resetEvent = component.waitForEvent('resetState'); + + await component.scrollToTop(); + await component.updateProps({ + items: state.data.getDataChunk(0) + }); + + await resetEvent; + await component.waitForChildCountEqualsTo(chunkSize); + await component.scrollToBottom(); + await component.waitForChildCountEqualsTo(chunkSize * 2); + await component.scrollToBottom(); + await component.waitForLifecycleDone(); + + const + spy = await component.getSpy((ctx) => ctx.emit), + results = filterEmitterResults(await spy.results, true, ['initLoadStart', 'initLoad']); + + test.expect(results).toEqual([ + ['resetState', states[0]], + ['initLoadStart', {...states[0], isLoadingInProgress: true}], + ['initLoad', {...states[0], isLoadingInProgress: true}], + ['dataLoadSuccess', states[1]], + ['renderStart', states[1]], + ['renderEngineStart', states[1]], + ['renderEngineDone', states[1]], + ['domInsertStart', states[2]], + ['domInsertDone', states[2]], + ['renderDone', states[2]], + ['renderStart', states[3]], + ['renderEngineStart', states[3]], + ['renderEngineDone', states[3]], + ['domInsertStart', states[4]], + ['domInsertDone', states[4]], + ['renderDone', states[4]], + ['lifecycleDone', states[5]] + ]); + }); + }); + }); }); diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts index 9f648d9fdf..fb9c78591f 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts @@ -50,7 +50,7 @@ test.describe('', () => { await component.waitForChildCountEqualsTo(chunkSize[0]); - await component.updateProps({ + await component.update({ attrs: { request: { get: { diff --git a/src/components/dummies/b-dummy/b-dummy.ts b/src/components/dummies/b-dummy/b-dummy.ts index 639e431bb1..07e90dd434 100644 --- a/src/components/dummies/b-dummy/b-dummy.ts +++ b/src/components/dummies/b-dummy/b-dummy.ts @@ -41,7 +41,7 @@ class bDummy extends iData { * Slots for the test component */ @field() - testComponentSlots?: CanArray + testComponentSlots?: CanArray; protected override readonly $refs!: iData['$refs'] & { testComponent?: iBlock; diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index b35f4bbdfb..7b265c8e8c 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -263,6 +263,23 @@ export default abstract class ComponentObjectBuilder { return this; } + /** + * Updates the component's props or children using the `b-dummy` component. + * + * This method will not work if the component was built without the `useDummy` option. + * + * @param props + * + * @throws {@link ReferenceError} - if the component object was not built or was built without the `useDummy` option + */ + update(props: RenderComponentsVnodeParams): Promise { + if (!this.dummy) { + throw new ReferenceError('Failed to update component. Missing "b-dummy" component.'); + } + + return this.dummy.setProps(props); + } + /** * Updates the component's props using the `b-dummy` component. * @@ -272,11 +289,28 @@ export default abstract class ComponentObjectBuilder { * * @throws {@link ReferenceError} - if the component object was not built or was built without the `useDummy` option */ - updateProps(props: RenderComponentsVnodeParams): Promise { + updateProps(props: RenderComponentsVnodeParams['attrs']): Promise { if (!this.dummy) { throw new ReferenceError('Failed to update props. Missing "b-dummy" component.'); } - return this.dummy.setProps(props); + return this.dummy.setProps({attrs: props}); + } + + /** + * Updates the component's children using the `b-dummy` component. + * + * This method will not work if the component was built without the `useDummy` option. + * + * @param children + * + * @throws {@link ReferenceError} - if the component object was not built or was built without the `useDummy` option + */ + updateChildren(children: RenderComponentsVnodeParams['children']): Promise { + if (!this.dummy) { + throw new ReferenceError('Failed to update children. Missing "b-dummy" component.'); + } + + return this.dummy.setProps({children}); } } diff --git a/tests/helpers/component-object/mock.ts b/tests/helpers/component-object/mock.ts index 8ac3e28d59..4510a414e0 100644 --- a/tests/helpers/component-object/mock.ts +++ b/tests/helpers/component-object/mock.ts @@ -60,10 +60,10 @@ export default abstract class ComponentObjectMock exte path = `prototype.${path}`; } - const pathArray = path.split('.'); - const method = pathArray.pop(); - - const obj = pathArray.length >= 1 ? Object.get(ctx, pathArray.join('.')) : ctx; + const + pathArray = path.split('.'), + method = pathArray.pop(), + obj = pathArray.length >= 1 ? Object.get(ctx, pathArray.join('.')) : ctx; if (!obj) { throw new ReferenceError(`Cannot find object by the provided path: ${path}`); diff --git a/tests/helpers/component/index.ts b/tests/helpers/component/index.ts index c712bb0dfc..03427b9ca3 100644 --- a/tests/helpers/component/index.ts +++ b/tests/helpers/component/index.ts @@ -148,7 +148,7 @@ export default class Component { if (parsed.children) { ctx.testComponentSlots = compileChild(); } - + ctx.testComponent = name; function compileChild() { @@ -157,7 +157,7 @@ export default class Component { attrs: { slot: slotName }, - children: ([]).concat(child ?? []) + children: ([]).concat((child ?? [])) }))); } From f39717ce74fa75cd4ed02abb3855a1c4eaa67ebf Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 25 Jan 2024 12:31:26 +0300 Subject: [PATCH 134/159] :art: --- src/components/base/b-virtual-scroll/README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 4f8d762da9..08a6d36cbb 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -7,7 +7,8 @@ - [Modifiers](#modifiers) - [Events](#events) - [Usage](#usage) - - [How to Implement Simple Rendering?](#how-to-implement-simple-rendering) + - [How to Implement Simple Rendering via DataProvider?](#how-to-implement-simple-rendering-via-dataprovider) + - [How to Implement Simple Rendering via `items`?](#how-to-implement-simple-rendering-via-items) - [How to Implement Component Rendering on Click Instead of Scroll?](#how-to-implement-component-rendering-on-click-instead-of-scroll) - [How to Reinitialize the Component?](#how-to-reinitialize-the-component) - [How to Reload a Failed Request?](#how-to-reload-a-failed-request) @@ -29,13 +30,13 @@ - [Slots](#slots) - [API](#api) - [Props](#props) - - [[shouldPerformDataRender = `(state: VirtualScrollState) => state.isInitialRender || state.remainingItems === 0`]](#shouldperformdatarender--state-virtualscrollstate--stateisinitialrender--stateremainingitems--0) - - [[shouldPerformDataRequest = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`]](#shouldperformdatarequest--state-virtualscrollstate--statelastloadeddatalength--0) - - [[shouldStopRequestingData = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`]](#shouldstoprequestingdata--state-virtualscrollstate--statelastloadeddatalength--0) - - [[chunkSize = `10`]](#chunksize--10) - - [[requestQuery]](#requestquery) - - [[itemsFactory]](#itemsfactory) - - [[itemsProcessors = `{}`]](#itemsprocessors--) + - [\[shouldPerformDataRender = `(state: VirtualScrollState) => state.isInitialRender || state.remainingItems === 0`\]](#shouldperformdatarender--state-virtualscrollstate--stateisinitialrender--stateremainingitems--0) + - [\[shouldPerformDataRequest = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`\]](#shouldperformdatarequest--state-virtualscrollstate--statelastloadeddatalength--0) + - [\[shouldStopRequestingData = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`\]](#shouldstoprequestingdata--state-virtualscrollstate--statelastloadeddatalength--0) + - [\[chunkSize = `10`\]](#chunksize--10) + - [\[requestQuery\]](#requestquery) + - [\[itemsFactory\]](#itemsfactory) + - [\[itemsProcessors = `{}`\]](#itemsprocessors--) - [`tombstoneCount`](#tombstonecount) - [Methods](#methods) - [getNextDataSlice](#getnextdataslice) From 9743031fc5021e9ce74f07cab19c40db9afc718e Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 25 Jan 2024 12:39:46 +0300 Subject: [PATCH 135/159] :art: --- src/components/base/b-virtual-scroll/README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 08a6d36cbb..430165a790 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -1074,14 +1074,16 @@ The `bVirtualScroll` class extends `iData` and includes additional properties re - `VirtualItemEl` interface is removed. Now, the client receives a single data item in the `iItems` methods. To maintain logic with `current`, `prev`, `next`, you can use the following approach: ```typescript - function getProps(data: DataInterface, index: number): Dictionary { + function getProps(dataItem: DataInterface, index: number): Dictionary { const - state = this.$refs.scroll.getVirtualScrollState(); + {data, lastLoadedData} = this.$refs.scroll.getVirtualScrollState(); const - current = data, - prev = state.data[index - 1], - next = state.data[index + 1]; + current = dataItem, + /* Retrieve the previous data element relative to the given */ + prev = allData[(allData.length - lastLoadedData.length + i) - 1], + /* Retrieve the next data element relative to the given */ + next = allData[i + 1]; } ``` From 7f7c8ec20a355744603a02bc0d432b5ef2855896 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 25 Jan 2024 12:50:21 +0300 Subject: [PATCH 136/159] :art: --- .../base/b-virtual-scroll/README.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 430165a790..16a81c7295 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -653,6 +653,48 @@ Let's also look at another common scenario: After these steps, a neighboring advertising component will be added to all components with the appropriate `meta.ads` value. +It is also perfectly valid to do without using global processing itemsProcessors. +To achieve this, you just need to avoid overriding the constant and instead pass processors as props: + + ```typescript + class MyPageComponent { + get itemsProcessors() { + return [ + (items: ComponentItem[]) => { + const newItems: ComponentItem[] = []; + + const adsComponent = { + item: 'b-ads', + key: current.uuid + 'ads', + type: 'item', + children: [], + props: { + // ... + } + } + + return items.map((item) => { + const itemsToPush = []; + itemsToPush.push(item); + + if (item.meta.ads === 'after') { + itemsToPush.push(adsComponent); + } + + if (item.meta.ads === 'before') { + itemsToPush.unshift(adsComponent); + } + + newItems.push(...itemsToPush); + }); + + return newItems; + } + ] + } + } + ``` + ### `request` and `requestQuery` To pass query parameters from the `b-virtual-scroll` component to the data provider, two props are specified: `request` and `requestQuery`. But why are there two of them, and what is the difference between them? Let's break it down: From 5804c6a6ecc36c808b2c9198483a788a512cc0a0 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 25 Jan 2024 15:42:10 +0300 Subject: [PATCH 137/159] :art: --- src/components/base/b-virtual-scroll/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 16a81c7295..55b05e90ef 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -1123,9 +1123,9 @@ The `bVirtualScroll` class extends `iData` and includes additional properties re const current = dataItem, /* Retrieve the previous data element relative to the given */ - prev = allData[(allData.length - lastLoadedData.length + i) - 1], + prev = data[(data.length - lastLoadedData.length + i) - 1], /* Retrieve the next data element relative to the given */ - next = allData[i + 1]; + next = lastLoadedData[i + 1]; } ``` From 40b472949467d353b88539a3b783f9780d0d24f7 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 29 Jan 2024 11:30:31 +0300 Subject: [PATCH 138/159] Improve isLastRender and lastRender behavior, now performing the lastRender will be always attempted --- .../base/b-virtual-scroll/README.md | 105 ++++++--- .../base/b-virtual-scroll/b-virtual-scroll.ts | 25 +-- .../b-virtual-scroll/interface/component.ts | 27 +-- .../b-virtual-scroll/modules/factory/index.ts | 4 + .../b-virtual-scroll/modules/state/helpers.ts | 3 +- .../test/unit/functional/emitter/payload.ts | 2 + .../functional/rendering/items-factory.ts | 10 +- .../test/unit/functional/state/default.ts | 4 + .../test/unit/functional/state/emitter.ts | 11 +- .../test/unit/scenario/last-render.ts | 202 ++++++++++++++++++ 10 files changed, 318 insertions(+), 75 deletions(-) create mode 100644 src/components/base/b-virtual-scroll/test/unit/scenario/last-render.ts diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 55b05e90ef..f5c75d5214 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -26,6 +26,7 @@ - [`renderGuard` and `loadDataOrPerformRender`](#renderguard-and-loaddataorperformrender) - [Difference between ComponentItem with type `item` and `separator`](#difference-between-componentitem-with-type-item-and-separator) - [Overriding in Child Layers](#overriding-in-child-layers) + - [Performing Last Render](#performing-last-render) - [Frequently Asked Questions](#frequently-asked-questions) - [Slots](#slots) - [API](#api) @@ -749,39 +750,36 @@ This function consults the `renderGuard`, which determines whether data can be r Understanding `renderGuard`: ```mermaid -graph TB - A["renderGuard"] -->|Get chunk size and next data slice| B["Is the data slice length = 0?"] - B -- True --> C["Are requests stopped?"] - C -- True --> E["Return: result=false, reason=done"] - E --> X["Function ends"] - C -- False --> F["Return: result=false, reason=noData"] - B -- False --> G["Is the data slice smaller than chunk size?"] - G -- True --> H["Return: result=false, reason=notEnoughData"] - G -- False --> I["Is it initial render?"] - I -- True --> J["Return: result=true"] - I -- False --> K["Get client response from shouldPerformDataRender"] - K --> L["Return: result=clientResponse, reason=noPermission if clientResponse is false"] +flowchart TD + A["Start: renderGuard Function"] --> B["Check if dataSlice.length < chunkSize"] + B -- "True" --> C["Check if state.areRequestsStopped and state.isLastRender"] + C -- "True" --> D["Return: {result: false, reason: 'done'}"] + C -- "False" --> E["Return: {result: false, reason: 'notEnoughData'}"] + B -- "False" --> F["Check if state.isInitialRender"] + F -- "True" --> G["Return: {result: true}"] + F -- "False" --> H["Invoke shouldPerformDataRender"] + H -- "Not Defined or True" --> I["Return: {result: true}"] + H -- "False" --> J["Return: {result: false, reason: 'noPermission'}"] ``` Understanding `loadDataOrPerformRender`: ```mermaid -graph TB - A[loadDataOrPerformRender] -->|Get component state| B[Is the last request errored?] - B -- True --> X[return] - B -- False ---> C["renderGuard()"] - C -- If Render Guard Result is True --> D["performRender()"] - C -- If Render Guard Result is False --> E[Check the Render Guard Rejection Reason] - E -- reason=done --> F["onLifecycleDone()"] - E -- reason=noData --> G[isRequestsStopped?] - G -- False --> H["shouldPerformDataRequest()"] - H -- True --> I["initLoadNext()"] - E -- reason=notEnoughData --> J[isRequestsStopped?] - J -- True --> K["performRender() and onLifecycleDone()"] - J -- False --> L["shouldPerformDataRequest()"] - L -- True --> M["initLoadNext()"] - L -- False --> N[initial render?] - N -- True --> P["performRender()"] +flowchart TD + A["Start: loadDataOrPerformRender Function"] --> B["Check if state.isLastErrored"] + B -- "True" --> C["Return"] + B -- "False" --> D["Invoke renderGuard(state)"] + D --> E["Check renderGuard result"] + E -- "True" --> F["Invoke performRender()"] + E -- "False" --> G["Check renderGuard reason"] + G -- "done" --> H["Invoke onLifecycleDone()"] + G -- "notEnoughData" --> I["Check if state.areRequestsStopped"] + I -- "True" --> J["Invoke performRender() and onLifecycleDone()"] + I -- "False" --> K["Check if shouldPerformDataRequest()"] + K -- "True" --> L["Invoke initLoadNext()"] + K -- "False" --> M["Check if state.isInitialRender"] + M -- "True" --> N["Invoke performRender()"] + M -- "False" --> O["Return"] ``` #### Difference between ComponentItem with type `item` and `separator` @@ -803,6 +801,57 @@ For example, it may be useful to override the logic of `shouldStopRequestingData There may also be situations where you need to modify the `renderGuard`. Currently, the component loads data until the number of items reaches the `chunkSize` and then renders them. By overriding the `renderGuard`, you can achieve partial rendering, where the component renders the available data regardless of whether it reaches the `chunkSize`. +#### Performing Last Render + +The `b-virtual-scroll` component adheres to a strategy where it always performs a "final" rendering. +This final rendering is always triggered after the client has indicated that data requests are complete (`shouldStopRequestingData`) and the data for rendering is nearing its end or has been exhausted. +To inform the client that the current rendering cycle is the last, the component sets the `isLastRender` flag in its state to `true` before the `renderStart` event and before initiating the rendering cycle, as well as before calling `itemsFactory` and `itemsProcessors`. + +Let's consider a scenario where this can be useful. Suppose we need to render 10 items at a time, and after all data has been loaded and rendered, we need to add an advertising block. +Imagine a situation where our provider initially responds with an array of 10 items, and then with an array of 0 items. If our `shouldStopRequestingData` strategy returns `true` when the provider returns less than 10 items, the `b-virtual-scroll` component will still attempt to render, even without data. This rendering will occur with the `isLastRender` flag set to `true`. +The actual rendering through rendering engines will only happen if the chain of `itemsFactory` -> `itemsProcessors` returns components for rendering; otherwise, no rendering will occur. +Also, if no rendering occurs, certain events such as `renderEngine*` and `domInsert*` will not be emitted, as there is nothing to render or insert. +This approach guarantees that the client will always have the opportunity to insert something at the end of the feed. Below is a demonstration of this approach: + +```typescript +@component() +class pPage { + shouldStopRequestingData(state: VirtualScrollState): boolean { + return state.lastLoadedData < 10; + } + + itemsFactory(state: VirtualScrollState): ComponentItem[] { + const items: ComponentItem[] = state.lastLoadedData.map((itemData, index) => { + return { + type: 'item', + item: 'section', + props: { + id: `element-${index}` + }, + key: `item-${index}`, + children: [] + }; + }); + + if (state.isLastRender) { + items.push({ + type: 'item', + item: 'button', + props: { + id: `lastElement` + }, + key: `lastElement`, + children: [] + }) + } + + return items; + } +} +``` + +This example demonstrates the `b-virtual-scroll` component's capability to handle a final rendering phase, even when the incoming data stream has been exhausted. This flexibility allows for dynamic and versatile implementations, like adding a unique element at the end of a list, ensuring a seamless and user-centric experience. + ### Frequently Asked Questions - How to assign a class to components rendered within `b-virtual-scroll`? diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts index b86fc0d775..e8bd54b9d6 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts @@ -301,21 +301,14 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI chunkSize = this.getChunkSize(state), dataSlice = this.getNextDataSlice(state, chunkSize); - if (dataSlice.length === 0) { - if (state.areRequestsStopped) { + if (dataSlice.length < chunkSize) { + if (state.areRequestsStopped && state.isLastRender) { return { result: false, reason: renderGuardRejectionReason.done }; } - return { - result: false, - reason: renderGuardRejectionReason.noData - }; - } - - if (dataSlice.length < chunkSize) { return { result: false, reason: renderGuardRejectionReason.notEnoughData @@ -364,16 +357,6 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI return; } - if (reason === renderGuardRejectionReason.noData) { - if (state.areRequestsStopped) { - return; - } - - if (this.shouldPerformDataRequestWrapper()) { - void this.initLoadNext(); - } - } - if (reason === renderGuardRejectionReason.notEnoughData) { if (state.areRequestsStopped) { this.performRender(); @@ -399,6 +382,10 @@ export default class bVirtualScroll extends iVirtualScrollHandlers implements iI nodes = this.componentFactory.produceNodes(items), mounted = this.componentFactory.produceMounted(items, nodes); + if (mounted.length === 0) { + return this.onRenderDone(); + } + this.observer.observe(mounted); this.onDomInsertStart(mounted); diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts index d6c918e1c2..1dfa44eca4 100644 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -85,28 +85,13 @@ export interface VirtualScrollState { isLifecycleDone: boolean; /** - * Indicates if the current render process is the last one in the current lifecycle. + * Indicates whether the current render process is the last one in the current lifecycle. * - * It is important to understand that the component uses the - * {@link VirtualScrollState.areRequestsStopped} property to calculate this value, - * which means, if your loading completion strategy relies on the fact that no elements - * will be received in the last request, - * then {@link VirtualScrollState.isLastRender} will always be `undefined`. - * - * To achieve correct `isLastRender` behavior, it is necessary to implement a request - * stopping strategy in such a way that **the last render occurs after all loading has - * been completed**. For example, this can be implemented if your backend pagination - * response has a property indicating the total number of items that can be loaded, - * this property can be used for comparison: - * - * ```typescript - * const shouldStopRequestingData = (state: VirtualScrollState): boolean => { - * // Example: Stop requesting data when the total number of items equals the current number of loaded items - * return state.lastLoadedRawData?.total === state.data.length; - * }; - * ``` - */ - isLastRender?: boolean; + * The isLastRender flag is set after each data load. The component checks whether requests have stopped + * ({@link VirtualScrollState.areRequestsStopped}) and whether there is no more data to render. + * If both conditions are met, the next render after the request will have the isLastRender flag set to true. + */ + isLastRender: boolean; /** * The last loaded data. diff --git a/src/components/base/b-virtual-scroll/modules/factory/index.ts b/src/components/base/b-virtual-scroll/modules/factory/index.ts index 36ebb8437d..1ff9e46de6 100644 --- a/src/components/base/b-virtual-scroll/modules/factory/index.ts +++ b/src/components/base/b-virtual-scroll/modules/factory/index.ts @@ -39,6 +39,10 @@ export class ComponentFactory extends Friend { * @param componentItems - an array of component items */ produceNodes(componentItems: ComponentItem[]): HTMLElement[] { + if (componentItems.length === 0) { + return []; + } + const createDescriptor = (item: ComponentItem): VNodeDescriptor => ({ type: item.item, attrs: item.props, diff --git a/src/components/base/b-virtual-scroll/modules/state/helpers.ts b/src/components/base/b-virtual-scroll/modules/state/helpers.ts index f75c5d3feb..df42d3b041 100644 --- a/src/components/base/b-virtual-scroll/modules/state/helpers.ts +++ b/src/components/base/b-virtual-scroll/modules/state/helpers.ts @@ -30,7 +30,8 @@ export function createInitialState(): VirtualScrollState { areRequestsStopped: false, isLoadingInProgress: false, isLifecycleDone: false, - isLastErrored: false + isLastErrored: false, + isLastRender: false }; } diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts b/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts index 7e25d0447d..c3eaabba22 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts @@ -113,6 +113,8 @@ test.describe('', () => { ['dataLoadStart', false], ['convertDataToDB', {data: []}], ['dataLoadSuccess', [], false], + ['renderStart'], + ['renderDone'], ['lifecycleDone'] ]); }); diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts index 18bf4249d3..ffadc30f69 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts @@ -12,7 +12,7 @@ import test from 'tests/config/unit/test'; -import type { ComponentItemFactory, ComponentItem, ShouldPerform } from 'components/base/b-virtual-scroll/interface'; +import type { ComponentItemFactory, ComponentItem, ShouldPerform, VirtualScrollState } from 'components/base/b-virtual-scroll/interface'; import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; @@ -89,9 +89,9 @@ test.describe(' rendering via itemsFactory', () => { type: 'separator' }; - const itemsFactory = await component.mockFn((state, ctx, separator) => { + const itemsFactory = await component.mockFn((state: VirtualScrollState, ctx, separator) => { const - data = state.lastLoadedData; + data = >state.lastLoadedData; const items = data.map((item) => ({ item: 'section', @@ -103,7 +103,9 @@ test.describe(' rendering via itemsFactory', () => { } })); - items.push(separator); + if (items.length > 0) { + items.push(separator); + } return items; }, separator); diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts index 63a865ee4c..d14a258ee0 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts @@ -128,6 +128,7 @@ test.describe('', () => { isLoadingInProgress: false, isLastEmpty: true, isLifecycleDone: true, + isLastRender: true, loadPage: 5, renderPage: 2 })); @@ -204,6 +205,7 @@ test.describe('', () => { isLoadingInProgress: false, isLastEmpty: true, isLifecycleDone: true, + isLastRender: true, loadPage: 2, renderPage: 1 })); @@ -276,6 +278,7 @@ test.describe('', () => { isLoadingInProgress: false, isLastEmpty: true, isLifecycleDone: true, + isLastRender: true, loadPage: 2, renderPage: 1 })); @@ -328,6 +331,7 @@ test.describe('', () => { isLoadingInProgress: false, isLastEmpty: true, isLifecycleDone: true, + isLastRender: true, maxViewedItem: undefined, loadPage: 2, renderPage: 1 diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts index 9a2bc46996..a08250a3cd 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts +++ b/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts @@ -141,6 +141,9 @@ test.describe('', () => { state.data.addData(0), state.set({loadPage: 3, areRequestsStopped: true, isLastEmpty: true}).compile() ), + ( + state.set({isLastRender: true}).compile() + ), ( state.set({isLifecycleDone: true}).compile() ) @@ -195,7 +198,9 @@ test.describe('', () => { ['dataLoadStart', states[5]], ['convertDataToDB', {...states[5], lastLoadedRawData: states[6].lastLoadedRawData}], ['dataLoadSuccess', states[6]], - ['lifecycleDone', states[7]], + ['renderStart', states[7]], + ['renderDone', states[7]], + ['lifecycleDone', states[8]], ['resetState', states[0]], ['initLoadStart', {...states[0], isLoadingInProgress: true}], ['dataLoadStart', {...states[0], isLoadingInProgress: true}], @@ -214,7 +219,9 @@ test.describe('', () => { ['dataLoadStart', states[5]], ['convertDataToDB', {...states[5], lastLoadedRawData: states[6].lastLoadedRawData}], ['dataLoadSuccess', states[6]], - ['lifecycleDone', states[7]] + ['renderStart', states[7]], + ['renderDone', states[7]], + ['lifecycleDone', states[8]] ]); }); }); diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/last-render.ts b/src/components/base/b-virtual-scroll/test/unit/scenario/last-render.ts new file mode 100644 index 0000000000..3cb692b3f8 --- /dev/null +++ b/src/components/base/b-virtual-scroll/test/unit/scenario/last-render.ts @@ -0,0 +1,202 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * @file This test file contains scenarios for checking the functionality of calling the last render in the lifecycle. + */ + +import test from 'tests/config/unit/test'; + +import { createTestHelpers, filterEmitterResults } from 'components/base/b-virtual-scroll/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; +import type { ComponentItem, VirtualScrollState } from 'components/base/b-virtual-scroll/interface'; + +const j = (...str: string[]) => str.join(', '); + +test.describe('', () => { + let + component: VirtualScrollTestHelpers['component'], + provider: VirtualScrollTestHelpers['provider'], + state: VirtualScrollTestHelpers['state']; + + const observerInitialStateFields = { + remainingItems: undefined, + remainingChildren: undefined, + maxViewedChild: undefined, + maxViewedItem: undefined + }; + + const observerLoadedStateFields = { + maxViewedChild: undefined, + maxViewedItem: undefined + }; + + test.beforeEach(async ({demoPage, page}) => { + await demoPage.goto(); + + ({component, provider, state} = await createTestHelpers(page)); + await provider.start(); + }); + + test.describe(j( + 'chunkSize set to 10', + 'provider responds with 10 elements for the first time', + 'next time provider responds with 0 elements', + 'client says that the requests have completed' + ), () => { + test('should fire render events 2 times with correct state', async () => { + const + chunkSize = 10, + providerChunkSize = 10; + + const states = [ + state.compile(observerInitialStateFields), + ( + // 1 + state.data.addData(providerChunkSize), + state.set({loadPage: 1}).compile(observerInitialStateFields) + ), + ( + // 2 + state.data.addItems(chunkSize), + state.set({renderPage: 1, isInitialRender: false}).compile(observerLoadedStateFields) + ), + ( + // 3 + state.compile() + ), + ( + // 4 + state.data.addData(0), + state.set({ + loadPage: 2, + areRequestsStopped: true, + isLastEmpty: true, + isInitialLoading: false + }).compile() + ), + ( + // 5 + state.set({isLastRender: true}).compile() + ), + ( + // 6 + state.set({isLifecycleDone: true}).compile() + ) + ]; + + provider + .responseOnce(200, {data: state.data.getDataChunk(0)}) + .responseOnce(200, {data: state.data.getDataChunk(1)}); + + await component + .withDefaultPaginationProviderProps() + .withProps({ + chunkSize, + shouldStopRequestingData: (state: VirtualScrollState): boolean => state.lastLoadedData.length === 0, + + '@hook:beforeDataCreate': (ctx) => { + const original = ctx.emit; + + ctx.emit = jestMock.mock((...args) => { + original(...args); + return [args[0], Object.fastClone(ctx.getVirtualScrollState())]; + }); + } + }) + .build(); + + await component.waitForChildCountEqualsTo(chunkSize); + await component.scrollToBottom(); + await component.waitForLifecycleDone(); + + const + spy = await component.getSpy((ctx) => ctx.emit), + results = filterEmitterResults(await spy.results, true, ['initLoadStart', 'initLoad']); + + test.expect(results).toEqual([ + ['initLoadStart', {...states[0], isLoadingInProgress: true}], + ['dataLoadStart', {...states[0], isLoadingInProgress: true}], + ['convertDataToDB', {...states[0], lastLoadedRawData: states[1].lastLoadedRawData}], + ['initLoad', {...states[0], lastLoadedRawData: states[1].lastLoadedRawData}], + ['dataLoadSuccess', states[1]], + ['renderStart', states[1]], + ['renderEngineStart', states[1]], + ['renderEngineDone', states[1]], + ['domInsertStart', states[2]], + ['domInsertDone', states[2]], + ['renderDone', states[2]], + ['dataLoadStart', {...states[3], isLoadingInProgress: true}], + ['convertDataToDB', {...states[3], lastLoadedRawData: states[4].lastLoadedRawData}], + ['dataLoadSuccess', states[4]], + ['renderStart', states[5]], + ['renderDone', states[5]], + ['lifecycleDone', states[6]] + ]); + }); + }); + + test.describe(j( + 'itemsFactory creates a component for each data element and always adds another separator to this array', + 'the provider returned 10 elements on the first load', + 'in the second load the provider returned 0 elements' + ), () => { + test('12 elements should be rendered', async () => { + const + chunkSize = 10; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(200, {data: state.data.addData(0)}); + + const separator: ComponentItem = { + item: 'button', + key: Object.cast(undefined), + children: [], + props: {}, + type: 'separator' + }; + + const itemsFactory = await component.mockFn((state, ctx, separator) => { + const data = state.lastLoadedData; + + const result: ComponentItem[] = data.map((item) => ({ + item: 'section', + key: Object.cast(undefined), + type: 'item', + children: [], + props: { + 'data-index': item.i + } + })); + + result.push(separator); + + return result; + }, separator); + + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + itemsFactory, + shouldPerformDataRender: () => true, + chunkSize + }) + .build(); + + await component.waitForDataIndexChild(1); + await component.scrollToBottom(); + await component.waitForLifecycleDone(); + + await test.expect(component.container.locator('section')).toHaveCount(10); + await test.expect(component.container.locator('button')).toHaveCount(2); + await test.expect(component.childList).toHaveCount(12); + }); + }); +}); + From 7dc2fdf75607ced154142f4b7e4f9c3908122134 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 29 Jan 2024 12:11:08 +0300 Subject: [PATCH 139/159] Added one more test for retry slot --- .../test/unit/lifecycle/slots/slots.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts b/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts index 67b31d5f9d..8bd49a8aa9 100644 --- a/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts +++ b/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts @@ -369,6 +369,38 @@ test.describe('', () => { tombstones: false }); }); + + test('activates when a data load error ocurred during loading of second data chunk', async () => { + const chunkSize = 12; + + provider + .responseOnce(200, {data: state.data.addData(chunkSize)}) + .response(500, {}); + + await component + .withDefaultPaginationProviderProps({chunkSize}) + .withProps({ + chunkSize + }) + .build(); + + await component.waitForChildCountEqualsTo(chunkSize); + await component.scrollToBottom(); + await component.waitForSlotState('retry', true); + + const + slots = await component.getSlotsState(); + + test.expect(slots).toEqual(>{ + container: true, + done: false, + empty: false, + loader: false, + renderNext: false, + retry: true, + tombstones: false + }); + }); }); test.describe('renderNext', () => { From 0c3f1fc597685136e7f2ea02a630183043c7f621 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 29 Jan 2024 12:23:52 +0300 Subject: [PATCH 140/159] :art: readme --- .../base/b-virtual-scroll/README.md | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index f5c75d5214..514fd07c0a 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -24,9 +24,9 @@ - [Component Understanding](#component-understanding) - [Lifecycle](#lifecycle) - [`renderGuard` and `loadDataOrPerformRender`](#renderguard-and-loaddataorperformrender) + - [Performing Last Render](#performing-last-render) - [Difference between ComponentItem with type `item` and `separator`](#difference-between-componentitem-with-type-item-and-separator) - [Overriding in Child Layers](#overriding-in-child-layers) - - [Performing Last Render](#performing-last-render) - [Frequently Asked Questions](#frequently-asked-questions) - [Slots](#slots) - [API](#api) @@ -782,25 +782,6 @@ flowchart TD M -- "False" --> O["Return"] ``` -#### Difference between ComponentItem with type `item` and `separator` - -The component allows rendering two types of components: - -- `item` - Main component (main content). -- `separator` - Other components, such as dividers or separators. - -There is no significant difference between them, except that they are treated differently in fields like `remainingItems` in the `VirtualScrollState`. As the name suggests, the `remainingItems` property only considers components with the `item` type, while `remainingChildren` considers components with both `item` and `separator` types. - -The distinction between `item` and `separator` types is mainly used for calculating certain properties based on the type of components present in the `VirtualScrollState`, such as the number of items till the end of the scroll. - -#### Overriding in Child Layers - -The main use case for overriding in child layers is to modify the default behavior of functions or methods. - -For example, it may be useful to override the logic of `shouldStopRequestingData` if you want to implement a default logic that takes into account the `total` field of the response when making a decision. - -There may also be situations where you need to modify the `renderGuard`. Currently, the component loads data until the number of items reaches the `chunkSize` and then renders them. By overriding the `renderGuard`, you can achieve partial rendering, where the component renders the available data regardless of whether it reaches the `chunkSize`. - #### Performing Last Render The `b-virtual-scroll` component adheres to a strategy where it always performs a "final" rendering. @@ -852,6 +833,25 @@ class pPage { This example demonstrates the `b-virtual-scroll` component's capability to handle a final rendering phase, even when the incoming data stream has been exhausted. This flexibility allows for dynamic and versatile implementations, like adding a unique element at the end of a list, ensuring a seamless and user-centric experience. +#### Difference between ComponentItem with type `item` and `separator` + +The component allows rendering two types of components: + +- `item` - Main component (main content). +- `separator` - Other components, such as dividers or separators. + +There is no significant difference between them, except that they are treated differently in fields like `remainingItems` in the `VirtualScrollState`. As the name suggests, the `remainingItems` property only considers components with the `item` type, while `remainingChildren` considers components with both `item` and `separator` types. + +The distinction between `item` and `separator` types is mainly used for calculating certain properties based on the type of components present in the `VirtualScrollState`, such as the number of items till the end of the scroll. + +#### Overriding in Child Layers + +The main use case for overriding in child layers is to modify the default behavior of functions or methods. + +For example, it may be useful to override the logic of `shouldStopRequestingData` if you want to implement a default logic that takes into account the `total` field of the response when making a decision. + +There may also be situations where you need to modify the `renderGuard`. Currently, the component loads data until the number of items reaches the `chunkSize` and then renders them. By overriding the `renderGuard`, you can achieve partial rendering, where the component renders the available data regardless of whether it reaches the `chunkSize`. + ### Frequently Asked Questions - How to assign a class to components rendered within `b-virtual-scroll`? From 5cc81c8062ae8feac8ea55e2a26c5e9cabc5bec7 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 29 Jan 2024 12:24:18 +0300 Subject: [PATCH 141/159] :art: readme --- src/components/base/b-virtual-scroll/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index 514fd07c0a..b2316c6174 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -31,13 +31,13 @@ - [Slots](#slots) - [API](#api) - [Props](#props) - - [\[shouldPerformDataRender = `(state: VirtualScrollState) => state.isInitialRender || state.remainingItems === 0`\]](#shouldperformdatarender--state-virtualscrollstate--stateisinitialrender--stateremainingitems--0) - - [\[shouldPerformDataRequest = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`\]](#shouldperformdatarequest--state-virtualscrollstate--statelastloadeddatalength--0) - - [\[shouldStopRequestingData = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`\]](#shouldstoprequestingdata--state-virtualscrollstate--statelastloadeddatalength--0) - - [\[chunkSize = `10`\]](#chunksize--10) - - [\[requestQuery\]](#requestquery) - - [\[itemsFactory\]](#itemsfactory) - - [\[itemsProcessors = `{}`\]](#itemsprocessors--) + - [[shouldPerformDataRender = `(state: VirtualScrollState) => state.isInitialRender || state.remainingItems === 0`]](#shouldperformdatarender--state-virtualscrollstate--stateisinitialrender--stateremainingitems--0) + - [[shouldPerformDataRequest = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`]](#shouldperformdatarequest--state-virtualscrollstate--statelastloadeddatalength--0) + - [[shouldStopRequestingData = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`]](#shouldstoprequestingdata--state-virtualscrollstate--statelastloadeddatalength--0) + - [[chunkSize = `10`]](#chunksize--10) + - [[requestQuery]](#requestquery) + - [[itemsFactory]](#itemsfactory) + - [[itemsProcessors = `{}`]](#itemsprocessors--) - [`tombstoneCount`](#tombstonecount) - [Methods](#methods) - [getNextDataSlice](#getnextdataslice) From 83475d13a78685a21c2f59fc4f0b04ac34956d6d Mon Sep 17 00:00:00 2001 From: bonkalol Date: Mon, 29 Jan 2024 12:30:46 +0300 Subject: [PATCH 142/159] remove noData reason of renderGuardRejectionReason because its no more needed --- src/components/base/b-virtual-scroll/const.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/components/base/b-virtual-scroll/const.ts b/src/components/base/b-virtual-scroll/const.ts index 6af04a3d70..eab83f3d36 100644 --- a/src/components/base/b-virtual-scroll/const.ts +++ b/src/components/base/b-virtual-scroll/const.ts @@ -141,11 +141,6 @@ export const renderGuardRejectionReason = { */ notEnoughData: 'notEnoughData', - /** - * No data available to perform a render (e.g., `data.length` is 0). - */ - noData: 'noData', - /** * All rendering operations have been completed. */ From 184037f8dcba069ecee1a4a2418a258eb38365f3 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Tue, 30 Jan 2024 11:38:14 +0300 Subject: [PATCH 143/159] :art: --- .../base/b-virtual-scroll/interface/component.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts index 1dfa44eca4..7f048074b4 100644 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ b/src/components/base/b-virtual-scroll/interface/component.ts @@ -87,9 +87,11 @@ export interface VirtualScrollState { /** * Indicates whether the current render process is the last one in the current lifecycle. * - * The isLastRender flag is set after each data load. The component checks whether requests have stopped - * ({@link VirtualScrollState.areRequestsStopped}) and whether there is no more data to render. - * If both conditions are met, the next render after the request will have the isLastRender flag set to true. + * The isLastRender flag is set to true after a request, + * when the client notifies the component that it has finished loading all its data + * ({@link VirtualScrollState.areRequestsStopped} is set to true) and there is either no data left to render + * or there is less than {@link VirtualScrollState.chunkSize} remaining to render. + * When these conditions are met, the isLastRender flag will be set to true. */ isLastRender: boolean; From 47d716292148bbf37c93157a0710d18a7fcc04ea Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 7 Feb 2024 10:18:47 +0300 Subject: [PATCH 144/159] Update branch, chagned b-virtual-scroll -> b-virtual-scroll-new --- components-lock.json | 4 +- .../test/api/component-object/index.ts | 14 +- .../test/api/component-object/styles.ts | 2 +- .../test/api/helpers/index.ts | 14 +- .../test/api/helpers/interface.ts | 6 +- .../test/unit/functional/emitter/payload.ts | 6 +- .../test/unit/functional/props/props.ts | 12 +- .../test/unit/functional/rendering/default.ts | 12 +- .../functional/rendering/items-factory.ts | 8 +- .../unit/functional/rendering/items-mode.ts | 8 +- .../test/unit/functional/state/default.ts | 14 +- .../test/unit/functional/state/emitter.ts | 8 +- .../initialization/initialization.ts | 12 +- .../test/unit/lifecycle/slots/slots.ts | 8 +- .../test/unit/scenario/last-render.ts | 8 +- .../test/unit/scenario/manual-rendering.ts | 12 +- .../test/unit/scenario/reload.ts | 6 +- .../test/unit/scenario/retry.ts | 10 +- .../base/b-virtual-scroll/CHANGELOG.md | 6 - .../base/b-virtual-scroll/README.md | 1252 ++--------------- .../base/b-virtual-scroll/b-virtual-scroll.ss | 2 +- .../base/b-virtual-scroll/b-virtual-scroll.ts | 640 +++++---- .../b-virtual-scroll_theme_demo.styl | 30 + src/components/base/b-virtual-scroll/const.ts | 184 --- .../base/b-virtual-scroll/handlers.ts | 234 --- .../base/b-virtual-scroll/interface.ts | 222 +++ .../base/b-virtual-scroll/interface/common.ts | 81 -- .../b-virtual-scroll/interface/component.ts | 336 ----- .../base/b-virtual-scroll/interface/events.ts | 76 - .../base/b-virtual-scroll/interface/index.ts | 12 - .../b-virtual-scroll/interface/requests.ts | 27 - .../b-virtual-scroll/modules/chunk-render.ts | 403 ++++++ .../b-virtual-scroll/modules/chunk-request.ts | 418 ++++++ .../modules/component-render.ts | 218 +++ .../b-virtual-scroll/modules/emitter/index.ts | 57 - .../modules/emitter/interface.ts | 55 - .../modules/factory/engines/vdom.ts | 24 - .../b-virtual-scroll/modules/factory/index.ts | 134 -- .../base/b-virtual-scroll/modules/helpers.ts | 106 ++ .../b-virtual-scroll/modules/helpers/index.ts | 26 - .../modules/observer/const.ts | 12 - .../observer/engines/intersection-observer.ts | 45 - .../modules/observer/index.ts | 60 - .../modules/observer/interface.ts | 27 - .../b-virtual-scroll/modules/slots/index.ts | 164 --- .../modules/slots/interface.ts | 17 - .../b-virtual-scroll/modules/state/helpers.ts | 45 - .../b-virtual-scroll/modules/state/index.ts | 228 --- src/components/base/b-virtual-scroll/props.ts | 333 ----- .../base/b-virtual-scroll/test/index.js | 32 + .../test/runners/events/chunk-loaded.js | 230 +++ .../test/runners/events/chunk-loading.js | 106 ++ .../test/runners/events/data-change.js | 216 +++ .../test/runners/events/db-change.js | 184 +++ .../test/runners/functional/items.js | 102 ++ .../test/runners/functional/render-next.js | 102 ++ .../test/runners/functional/state.js | 263 ++++ .../test/runners/render/render.js | 205 +++ .../test/runners/slots/empty.js | 123 ++ .../test/runners/slots/render-next.js | 498 +++++++ .../base/b-virtual-scroll/test/unit/render.ts | 139 ++ .../pages/p-v4-components-demo/index.js | 1 + tests/helpers/component-object/builder.ts | 1 + yarn.lock | 6 +- 64 files changed, 4168 insertions(+), 3678 deletions(-) rename src/components/base/{b-virtual-scroll => b-virtual-scroll-new}/test/api/component-object/index.ts (94%) rename src/components/base/{b-virtual-scroll => b-virtual-scroll-new}/test/api/component-object/styles.ts (95%) rename src/components/base/{b-virtual-scroll => b-virtual-scroll-new}/test/api/helpers/index.ts (96%) rename src/components/base/{b-virtual-scroll => b-virtual-scroll-new}/test/api/helpers/interface.ts (95%) rename src/components/base/{b-virtual-scroll => b-virtual-scroll-new}/test/unit/functional/emitter/payload.ts (97%) rename src/components/base/{b-virtual-scroll => b-virtual-scroll-new}/test/unit/functional/props/props.ts (90%) rename src/components/base/{b-virtual-scroll => b-virtual-scroll-new}/test/unit/functional/rendering/default.ts (93%) rename src/components/base/{b-virtual-scroll => b-virtual-scroll-new}/test/unit/functional/rendering/items-factory.ts (97%) rename src/components/base/{b-virtual-scroll => b-virtual-scroll-new}/test/unit/functional/rendering/items-mode.ts (93%) rename src/components/base/{b-virtual-scroll => b-virtual-scroll-new}/test/unit/functional/state/default.ts (95%) rename src/components/base/{b-virtual-scroll => b-virtual-scroll-new}/test/unit/functional/state/emitter.ts (98%) rename src/components/base/{b-virtual-scroll => b-virtual-scroll-new}/test/unit/lifecycle/initialization/initialization.ts (96%) rename src/components/base/{b-virtual-scroll => b-virtual-scroll-new}/test/unit/lifecycle/slots/slots.ts (98%) rename src/components/base/{b-virtual-scroll => b-virtual-scroll-new}/test/unit/scenario/last-render.ts (96%) rename src/components/base/{b-virtual-scroll => b-virtual-scroll-new}/test/unit/scenario/manual-rendering.ts (89%) rename src/components/base/{b-virtual-scroll => b-virtual-scroll-new}/test/unit/scenario/reload.ts (96%) rename src/components/base/{b-virtual-scroll => b-virtual-scroll-new}/test/unit/scenario/retry.ts (93%) create mode 100644 src/components/base/b-virtual-scroll/b-virtual-scroll_theme_demo.styl delete mode 100644 src/components/base/b-virtual-scroll/const.ts delete mode 100644 src/components/base/b-virtual-scroll/handlers.ts create mode 100644 src/components/base/b-virtual-scroll/interface.ts delete mode 100644 src/components/base/b-virtual-scroll/interface/common.ts delete mode 100644 src/components/base/b-virtual-scroll/interface/component.ts delete mode 100644 src/components/base/b-virtual-scroll/interface/events.ts delete mode 100644 src/components/base/b-virtual-scroll/interface/index.ts delete mode 100644 src/components/base/b-virtual-scroll/interface/requests.ts create mode 100644 src/components/base/b-virtual-scroll/modules/chunk-render.ts create mode 100644 src/components/base/b-virtual-scroll/modules/chunk-request.ts create mode 100644 src/components/base/b-virtual-scroll/modules/component-render.ts delete mode 100644 src/components/base/b-virtual-scroll/modules/emitter/index.ts delete mode 100644 src/components/base/b-virtual-scroll/modules/emitter/interface.ts delete mode 100644 src/components/base/b-virtual-scroll/modules/factory/engines/vdom.ts delete mode 100644 src/components/base/b-virtual-scroll/modules/factory/index.ts create mode 100644 src/components/base/b-virtual-scroll/modules/helpers.ts delete mode 100644 src/components/base/b-virtual-scroll/modules/helpers/index.ts delete mode 100644 src/components/base/b-virtual-scroll/modules/observer/const.ts delete mode 100644 src/components/base/b-virtual-scroll/modules/observer/engines/intersection-observer.ts delete mode 100644 src/components/base/b-virtual-scroll/modules/observer/index.ts delete mode 100644 src/components/base/b-virtual-scroll/modules/observer/interface.ts delete mode 100644 src/components/base/b-virtual-scroll/modules/slots/index.ts delete mode 100644 src/components/base/b-virtual-scroll/modules/slots/interface.ts delete mode 100644 src/components/base/b-virtual-scroll/modules/state/helpers.ts delete mode 100644 src/components/base/b-virtual-scroll/modules/state/index.ts delete mode 100644 src/components/base/b-virtual-scroll/props.ts create mode 100644 src/components/base/b-virtual-scroll/test/index.js create mode 100644 src/components/base/b-virtual-scroll/test/runners/events/chunk-loaded.js create mode 100644 src/components/base/b-virtual-scroll/test/runners/events/chunk-loading.js create mode 100644 src/components/base/b-virtual-scroll/test/runners/events/data-change.js create mode 100644 src/components/base/b-virtual-scroll/test/runners/events/db-change.js create mode 100644 src/components/base/b-virtual-scroll/test/runners/functional/items.js create mode 100644 src/components/base/b-virtual-scroll/test/runners/functional/render-next.js create mode 100644 src/components/base/b-virtual-scroll/test/runners/functional/state.js create mode 100644 src/components/base/b-virtual-scroll/test/runners/render/render.js create mode 100644 src/components/base/b-virtual-scroll/test/runners/slots/empty.js create mode 100644 src/components/base/b-virtual-scroll/test/runners/slots/render-next.js create mode 100644 src/components/base/b-virtual-scroll/test/unit/render.ts diff --git a/components-lock.json b/components-lock.json index 8668027300..10dbc7ddf5 100644 --- a/components-lock.json +++ b/components-lock.json @@ -1,5 +1,5 @@ { - "hash": "3ae069d52d1989b96e528e8e9288454145d1b772025f020f37e2e2148bfea48d", + "hash": "098211bcc31aa8c403e2f0f126046ef09ad2fb212477edeb5a60735ae3e661f0", "data": { "%data": "%data:Map", "%data:Map": [ @@ -2263,6 +2263,7 @@ "b-tree", "b-window", "b-virtual-scroll", + "b-virtual-scroll-new", "b-bottom-slide", "b-slider", "b-sidebar", @@ -2305,6 +2306,7 @@ "b-tree", "b-window", "b-virtual-scroll", + "b-virtual-scroll-new", "b-bottom-slide", "b-slider", "b-sidebar", diff --git a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts b/src/components/base/b-virtual-scroll-new/test/api/component-object/index.ts similarity index 94% rename from src/components/base/b-virtual-scroll/test/api/component-object/index.ts rename to src/components/base/b-virtual-scroll-new/test/api/component-object/index.ts index 82df6d4ed4..7931d2c36f 100644 --- a/src/components/base/b-virtual-scroll/test/api/component-object/index.ts +++ b/src/components/base/b-virtual-scroll-new/test/api/component-object/index.ts @@ -10,16 +10,16 @@ import type { Locator, Page } from 'playwright'; import { ComponentObject, Scroll } from 'tests/helpers'; -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { ComponentRefs, VirtualScrollState } from 'components/base/b-virtual-scroll/interface'; -import type { SlotsStateObj } from 'components/base/b-virtual-scroll/modules/slots'; +import type bVirtualScrollNew from 'components/base/b-virtual-scroll-new/b-virtual-scroll-new'; +import type { ComponentRefs, VirtualScrollState } from 'components/base/b-virtual-scroll-new/interface'; +import type { SlotsStateObj } from 'components/base/b-virtual-scroll-new/modules/slots'; -import { testStyles } from 'components/base/b-virtual-scroll/test/api/component-object/styles'; +import { testStyles } from 'components/base/b-virtual-scroll-new/test/api/component-object/styles'; /** - * The component object API for testing the {@link bVirtualScroll} component. + * The component object API for testing the {@link bVirtualScrollNew} component. */ -export class VirtualScrollComponentObject extends ComponentObject { +export class VirtualScrollComponentObject extends ComponentObject { /** * The locator for the container ref. */ @@ -38,7 +38,7 @@ export class VirtualScrollComponentObject extends ComponentObject *'); diff --git a/src/components/base/b-virtual-scroll/test/api/component-object/styles.ts b/src/components/base/b-virtual-scroll-new/test/api/component-object/styles.ts similarity index 95% rename from src/components/base/b-virtual-scroll/test/api/component-object/styles.ts rename to src/components/base/b-virtual-scroll-new/test/api/component-object/styles.ts index 7a7da255dc..aa44ed81ef 100644 --- a/src/components/base/b-virtual-scroll/test/api/component-object/styles.ts +++ b/src/components/base/b-virtual-scroll-new/test/api/component-object/styles.ts @@ -18,7 +18,7 @@ export const testStyles = ` content: attr(data-index); } -.b-virtual-scroll__container { +.b-virtual-scroll-new__container { min-width: 20px; min-height: 20px; } diff --git a/src/components/base/b-virtual-scroll/test/api/helpers/index.ts b/src/components/base/b-virtual-scroll-new/test/api/helpers/index.ts similarity index 96% rename from src/components/base/b-virtual-scroll/test/api/helpers/index.ts rename to src/components/base/b-virtual-scroll-new/test/api/helpers/index.ts index 9bd608bb71..6824e2b947 100644 --- a/src/components/base/b-virtual-scroll/test/api/helpers/index.ts +++ b/src/components/base/b-virtual-scroll-new/test/api/helpers/index.ts @@ -10,20 +10,20 @@ import type { Page } from 'playwright'; import test from 'tests/config/unit/test'; -import { createInitialState as createInitialStateObj } from 'components/base/b-virtual-scroll/modules/state/helpers'; -import type { MountedChild, ComponentItem, VirtualScrollState, MountedItem } from 'components/base/b-virtual-scroll/interface'; -import { componentEvents, componentObserverLocalEvents } from 'components/base/b-virtual-scroll/const'; +import { createInitialState as createInitialStateObj } from 'components/base/b-virtual-scroll-new/modules/state/helpers'; +import type { MountedChild, ComponentItem, VirtualScrollState, MountedItem } from 'components/base/b-virtual-scroll-new/interface'; +import { componentEvents, componentObserverLocalEvents } from 'components/base/b-virtual-scroll-new/const'; import { paginationHandler } from 'tests/helpers/providers/pagination'; import { RequestInterceptor } from 'tests/helpers/providers/interceptor'; -import { VirtualScrollComponentObject } from 'components/base/b-virtual-scroll/test/api/component-object'; -import type { DataConveyor, DataItemCtor, MountedItemCtor, StateApi, VirtualScrollTestHelpers, MountedSeparatorCtor, IndexedObj } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; +import { VirtualScrollComponentObject } from 'components/base/b-virtual-scroll-new/test/api/component-object'; +import type { DataConveyor, DataItemCtor, MountedItemCtor, StateApi, VirtualScrollTestHelpers, MountedSeparatorCtor, IndexedObj } from 'components/base/b-virtual-scroll-new/test/api/helpers/interface'; -export * from 'components/base/b-virtual-scroll/test/api/component-object'; +export * from 'components/base/b-virtual-scroll-new/test/api/component-object'; /** - * Creates a helper API for convenient testing of the `b-virtual-scroll` component + * 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 { diff --git a/src/components/base/b-virtual-scroll/test/api/helpers/interface.ts b/src/components/base/b-virtual-scroll-new/test/api/helpers/interface.ts similarity index 95% rename from src/components/base/b-virtual-scroll/test/api/helpers/interface.ts rename to src/components/base/b-virtual-scroll-new/test/api/helpers/interface.ts index 450ad4b51c..0f0c7c0f4a 100644 --- a/src/components/base/b-virtual-scroll/test/api/helpers/interface.ts +++ b/src/components/base/b-virtual-scroll-new/test/api/helpers/interface.ts @@ -6,11 +6,11 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { ComponentItem, VirtualScrollState, MountedChild, MountedItem } from 'components/base/b-virtual-scroll/interface'; +import type { ComponentItem, VirtualScrollState, MountedChild, MountedItem } from 'components/base/b-virtual-scroll-new/interface'; import type { SpyObject } from 'tests/helpers/mock/interface'; import type { RequestInterceptor } from 'tests/helpers/providers/interceptor'; -import type { VirtualScrollComponentObject } from 'components/base/b-virtual-scroll/test/api/component-object'; +import type { VirtualScrollComponentObject } from 'components/base/b-virtual-scroll-new/test/api/component-object'; /** * The interface defining the data conveyor for convenient data manipulation. @@ -136,7 +136,7 @@ export interface StateApi { */ export interface VirtualScrollTestHelpers { /** - * The component object representing the `bVirtualScroll` component. + * The component object representing the `bVirtualScrollNew` component. */ component: VirtualScrollComponentObject; diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts b/src/components/base/b-virtual-scroll-new/test/unit/functional/emitter/payload.ts similarity index 97% rename from src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts rename to src/components/base/b-virtual-scroll-new/test/unit/functional/emitter/payload.ts index c3eaabba22..3c0e9d3d1a 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/emitter/payload.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/functional/emitter/payload.ts @@ -12,10 +12,10 @@ import test from 'tests/config/unit/test'; -import { createTestHelpers, filterEmitterCalls } from 'components/base/b-virtual-scroll/test/api/helpers'; -import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; +import { createTestHelpers, filterEmitterCalls } from 'components/base/b-virtual-scroll-new/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers/interface'; -test.describe('', () => { +test.describe('', () => { let component: VirtualScrollTestHelpers['component'], provider: VirtualScrollTestHelpers['provider'], diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts b/src/components/base/b-virtual-scroll-new/test/unit/functional/props/props.ts similarity index 90% rename from src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts rename to src/components/base/b-virtual-scroll-new/test/unit/functional/props/props.ts index 3437ca89ef..75702fb07f 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/props/props.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/functional/props/props.ts @@ -16,11 +16,11 @@ import test from 'tests/config/unit/test'; import { fromQueryString } from 'core/url'; -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; -import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; +import type bVirtualScrollNew from 'components/base/b-virtual-scroll-new/b-virtual-scroll-new'; +import { createTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers/interface'; -test.describe('', () => { +test.describe('', () => { let component: VirtualScrollTestHelpers['component'], provider: VirtualScrollTestHelpers['provider'], @@ -44,7 +44,7 @@ test.describe('', () => { .withDefaultPaginationProviderProps({chunkSize}) .withProps({ chunkSize, - '@hook:beforeDataCreate': (ctx: bVirtualScroll['unsafe']) => jestMock.spy(ctx.componentFactory, 'produceComponentItems') + '@hook:beforeDataCreate': (ctx: bVirtualScrollNew['unsafe']) => jestMock.spy(ctx.componentFactory, 'produceComponentItems') }) .build({useDummy: true}); @@ -76,7 +76,7 @@ test.describe('', () => { chunkSize, requestQuery: () => ({get: {param1: 'param1'}}), shouldPerformDataRequest: () => false, - '@hook:beforeDataCreate': (ctx: bVirtualScroll['unsafe']) => jestMock.spy(ctx.componentFactory, 'produceComponentItems') + '@hook:beforeDataCreate': (ctx: bVirtualScrollNew['unsafe']) => jestMock.spy(ctx.componentFactory, 'produceComponentItems') }) .build(); diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts b/src/components/base/b-virtual-scroll-new/test/unit/functional/rendering/default.ts similarity index 93% rename from src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts rename to src/components/base/b-virtual-scroll-new/test/unit/functional/rendering/default.ts index da50e43bdd..e4aa44055d 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/default.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/functional/rendering/default.ts @@ -14,12 +14,12 @@ import test from 'tests/config/unit/test'; import { Scroll } from 'tests/helpers'; -import type { VirtualScrollState, ShouldPerform } from 'components/base/b-virtual-scroll/interface'; -import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; -import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { VirtualScrollState, ShouldPerform } from 'components/base/b-virtual-scroll-new/interface'; +import { createTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers/interface'; +import type bVirtualScrollNew from 'components/base/b-virtual-scroll-new/b-virtual-scroll-new'; -test.describe('', () => { +test.describe('', () => { let component: VirtualScrollTestHelpers['component'], provider: VirtualScrollTestHelpers['provider'], @@ -146,7 +146,7 @@ test.describe('', () => { shouldPerformDataRender, shouldStopRequestingData: () => true, chunkSize, - '@hook:beforeDataCreate': (ctx: bVirtualScroll) => jestMock.spy(ctx.unsafe.componentFactory, 'produceNodes') + '@hook:beforeDataCreate': (ctx: bVirtualScrollNew) => jestMock.spy(ctx.unsafe.componentFactory, 'produceNodes') }); await component.build(); diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts b/src/components/base/b-virtual-scroll-new/test/unit/functional/rendering/items-factory.ts similarity index 97% rename from src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts rename to src/components/base/b-virtual-scroll-new/test/unit/functional/rendering/items-factory.ts index ffadc30f69..e43b8790d9 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-factory.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/functional/rendering/items-factory.ts @@ -12,12 +12,12 @@ import test from 'tests/config/unit/test'; -import type { ComponentItemFactory, ComponentItem, ShouldPerform, VirtualScrollState } from 'components/base/b-virtual-scroll/interface'; +import type { ComponentItemFactory, ComponentItem, ShouldPerform, VirtualScrollState } from 'components/base/b-virtual-scroll-new/interface'; -import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; -import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; +import { createTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers/interface'; -test.describe(' rendering via itemsFactory', () => { +test.describe(' rendering via itemsFactory', () => { let component: VirtualScrollTestHelpers['component'], provider: VirtualScrollTestHelpers['provider'], diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-mode.ts b/src/components/base/b-virtual-scroll-new/test/unit/functional/rendering/items-mode.ts similarity index 93% rename from src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-mode.ts rename to src/components/base/b-virtual-scroll-new/test/unit/functional/rendering/items-mode.ts index 1e7d07180e..dd48e058ce 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/rendering/items-mode.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/functional/rendering/items-mode.ts @@ -12,11 +12,11 @@ import test from 'tests/config/unit/test'; -import type { ShouldPerform } from 'components/base/b-virtual-scroll/interface'; -import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; -import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; +import type { ShouldPerform } from 'components/base/b-virtual-scroll-new/interface'; +import { createTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers/interface'; -test.describe('', () => { +test.describe('', () => { let component: VirtualScrollTestHelpers['component'], provider: VirtualScrollTestHelpers['provider'], diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts b/src/components/base/b-virtual-scroll-new/test/unit/functional/state/default.ts similarity index 95% rename from src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts rename to src/components/base/b-virtual-scroll-new/test/unit/functional/state/default.ts index d14a258ee0..a1577bbfae 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/default.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/functional/state/default.ts @@ -12,14 +12,14 @@ import test from 'tests/config/unit/test'; -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import { defaultShouldProps } from 'components/base/b-virtual-scroll/const'; -import type { ComponentItem, ShouldPerform } from 'components/base/b-virtual-scroll/interface'; +import type bVirtualScrollNew from 'components/base/b-virtual-scroll-new/b-virtual-scroll-new'; +import { defaultShouldProps } from 'components/base/b-virtual-scroll-new/const'; +import type { ComponentItem, ShouldPerform } from 'components/base/b-virtual-scroll-new/interface'; -import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; -import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; +import { createTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers/interface'; -test.describe('', () => { +test.describe('', () => { let component: VirtualScrollTestHelpers['component'], provider: VirtualScrollTestHelpers['provider'], @@ -35,7 +35,7 @@ test.describe('', () => { test('Initial state', async () => { const chunkSize = 12, - mockFn = await component.mockFn((ctx: bVirtualScroll) => ctx.getVirtualScrollState()); + mockFn = await component.mockFn((ctx: bVirtualScrollNew) => ctx.getVirtualScrollState()); provider.response(200, {data: []}, {delay: (10).seconds()}); diff --git a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts b/src/components/base/b-virtual-scroll-new/test/unit/functional/state/emitter.ts similarity index 98% rename from src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts rename to src/components/base/b-virtual-scroll-new/test/unit/functional/state/emitter.ts index a08250a3cd..09e6e4144b 100644 --- a/src/components/base/b-virtual-scroll/test/unit/functional/state/emitter.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/functional/state/emitter.ts @@ -14,11 +14,11 @@ import test from 'tests/config/unit/test'; -import { createTestHelpers, filterEmitterResults } from 'components/base/b-virtual-scroll/test/api/helpers'; -import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; -import type { VirtualScrollState } from 'components/base/b-virtual-scroll/interface'; +import { createTestHelpers, filterEmitterResults } from 'components/base/b-virtual-scroll-new/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers/interface'; +import type { VirtualScrollState } from 'components/base/b-virtual-scroll-new/interface'; -test.describe('', () => { +test.describe('', () => { let component: VirtualScrollTestHelpers['component'], provider: VirtualScrollTestHelpers['provider'], diff --git a/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts b/src/components/base/b-virtual-scroll-new/test/unit/lifecycle/initialization/initialization.ts similarity index 96% rename from src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts rename to src/components/base/b-virtual-scroll-new/test/unit/lifecycle/initialization/initialization.ts index 66d9a4fa0d..758e6d1a61 100644 --- a/src/components/base/b-virtual-scroll/test/unit/lifecycle/initialization/initialization.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/lifecycle/initialization/initialization.ts @@ -12,13 +12,13 @@ import test from 'tests/config/unit/test'; -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import { defaultShouldProps } from 'components/base/b-virtual-scroll/const'; +import type bVirtualScrollNew from 'components/base/b-virtual-scroll-new/b-virtual-scroll-new'; +import { defaultShouldProps } from 'components/base/b-virtual-scroll-new/const'; -import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; -import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; +import { createTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers/interface'; -test.describe('', () => { +test.describe('', () => { let component: VirtualScrollTestHelpers['component'], initLoadSpy: VirtualScrollTestHelpers['initLoadSpy'], @@ -26,7 +26,7 @@ test.describe('', () => { state: VirtualScrollTestHelpers['state']; const hookProp = { - '@hook:beforeDataCreate': (ctx: bVirtualScroll['unsafe']) => { + '@hook:beforeDataCreate': (ctx: bVirtualScrollNew['unsafe']) => { const original = ctx.componentInternalState.compile.bind(ctx.componentInternalState); diff --git a/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts b/src/components/base/b-virtual-scroll-new/test/unit/lifecycle/slots/slots.ts similarity index 98% rename from src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts rename to src/components/base/b-virtual-scroll-new/test/unit/lifecycle/slots/slots.ts index 8bd49a8aa9..44b73fc89d 100644 --- a/src/components/base/b-virtual-scroll/test/unit/lifecycle/slots/slots.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/lifecycle/slots/slots.ts @@ -16,13 +16,13 @@ import test from 'tests/config/unit/test'; import { BOM } from 'tests/helpers'; -import type { ShouldPerform } from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { SlotsStateObj } from 'components/base/b-virtual-scroll/modules/slots'; +import type { ShouldPerform } from 'components/base/b-virtual-scroll-new/b-virtual-scroll-new'; +import type { SlotsStateObj } from 'components/base/b-virtual-scroll-new/modules/slots'; -import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; +import { createTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers'; // eslint-disable-next-line max-lines-per-function -test.describe('', () => { +test.describe('', () => { let component: Awaited>['component'], provider: Awaited>['provider'], diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/last-render.ts b/src/components/base/b-virtual-scroll-new/test/unit/scenario/last-render.ts similarity index 96% rename from src/components/base/b-virtual-scroll/test/unit/scenario/last-render.ts rename to src/components/base/b-virtual-scroll-new/test/unit/scenario/last-render.ts index 3cb692b3f8..0267962350 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/last-render.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/scenario/last-render.ts @@ -12,13 +12,13 @@ import test from 'tests/config/unit/test'; -import { createTestHelpers, filterEmitterResults } from 'components/base/b-virtual-scroll/test/api/helpers'; -import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; -import type { ComponentItem, VirtualScrollState } from 'components/base/b-virtual-scroll/interface'; +import { createTestHelpers, filterEmitterResults } from 'components/base/b-virtual-scroll-new/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers/interface'; +import type { ComponentItem, VirtualScrollState } from 'components/base/b-virtual-scroll-new/interface'; const j = (...str: string[]) => str.join(', '); -test.describe('', () => { +test.describe('', () => { let component: VirtualScrollTestHelpers['component'], provider: VirtualScrollTestHelpers['provider'], diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts b/src/components/base/b-virtual-scroll-new/test/unit/scenario/manual-rendering.ts similarity index 89% rename from src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts rename to src/components/base/b-virtual-scroll-new/test/unit/scenario/manual-rendering.ts index f3bf9d07ee..1f54457352 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/manual-rendering.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/scenario/manual-rendering.ts @@ -15,11 +15,11 @@ import test from 'tests/config/unit/test'; import type { ComponentElement } from 'core/component'; -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; -import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; +import type bVirtualScrollNew from 'components/base/b-virtual-scroll-new/b-virtual-scroll-new'; +import { createTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers/interface'; -test.describe('', () => { +test.describe('', () => { let component: VirtualScrollTestHelpers['component'], provider: VirtualScrollTestHelpers['provider'], @@ -36,7 +36,7 @@ test.describe('', () => { type: 'div', attrs: { id: 'renderNext', - '@click': () => (>document.querySelector('.b-virtual-scroll')).component?.initLoadNext() + '@click': () => (>document.querySelector('.b-virtual-scroll-new')).component?.initLoadNext() } }, @@ -44,7 +44,7 @@ test.describe('', () => { type: 'div', attrs: { id: 'retry', - '@click': () => (>document.querySelector('.b-virtual-scroll')).component?.initLoadNext() + '@click': () => (>document.querySelector('.b-virtual-scroll-new')).component?.initLoadNext() } } }); diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts b/src/components/base/b-virtual-scroll-new/test/unit/scenario/reload.ts similarity index 96% rename from src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts rename to src/components/base/b-virtual-scroll-new/test/unit/scenario/reload.ts index fb9c78591f..3f4223282a 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/reload.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/scenario/reload.ts @@ -12,10 +12,10 @@ import test from 'tests/config/unit/test'; -import { createTestHelpers, filterEmitterCalls } from 'components/base/b-virtual-scroll/test/api/helpers'; -import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; +import { createTestHelpers, filterEmitterCalls } from 'components/base/b-virtual-scroll-new/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers/interface'; -test.describe('', () => { +test.describe('', () => { let component: VirtualScrollTestHelpers['component'], provider: VirtualScrollTestHelpers['provider'], diff --git a/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts b/src/components/base/b-virtual-scroll-new/test/unit/scenario/retry.ts similarity index 93% rename from src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts rename to src/components/base/b-virtual-scroll-new/test/unit/scenario/retry.ts index 642ecbd5ef..e1f24542e4 100644 --- a/src/components/base/b-virtual-scroll/test/unit/scenario/retry.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/scenario/retry.ts @@ -14,11 +14,11 @@ import test from 'tests/config/unit/test'; import type { ComponentElement } from 'core/component'; -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import { createTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers'; -import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll/test/api/helpers/interface'; +import type bVirtualScrollNew from 'components/base/b-virtual-scroll-new/b-virtual-scroll-new'; +import { createTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers'; +import type { VirtualScrollTestHelpers } from 'components/base/b-virtual-scroll-new/test/api/helpers/interface'; -test.describe('', () => { +test.describe('', () => { let component: VirtualScrollTestHelpers['component'], provider: VirtualScrollTestHelpers['provider'], @@ -35,7 +35,7 @@ test.describe('', () => { type: 'div', attrs: { id: 'retry', - '@click': () => (>document.querySelector('.b-virtual-scroll')).component?.initLoadNext() + '@click': () => (>document.querySelector('.b-virtual-scroll-new')).component?.initLoadNext() } } }); diff --git a/src/components/base/b-virtual-scroll/CHANGELOG.md b/src/components/base/b-virtual-scroll/CHANGELOG.md index b90965a1c8..404e88c91c 100644 --- a/src/components/base/b-virtual-scroll/CHANGELOG.md +++ b/src/components/base/b-virtual-scroll/CHANGELOG.md @@ -9,12 +9,6 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v4.0.0-beta.?? (2023-??-??) - -#### :boom: Breaking Change - -* Major update. Visit [readme](./readme) to see migration guide. - ## v4.0.0-beta.36 (2023-10-23) #### :bug: Bug Fix diff --git a/src/components/base/b-virtual-scroll/README.md b/src/components/base/b-virtual-scroll/README.md index b2316c6174..b85f94bbd9 100644 --- a/src/components/base/b-virtual-scroll/README.md +++ b/src/components/base/b-virtual-scroll/README.md @@ -1,1226 +1,218 @@ - - -**Table of Contents** - -- [components/base/b-virtual-scroll](#componentsbaseb-virtual-scroll) - - [Synopsis](#synopsis) - - [Modifiers](#modifiers) - - [Events](#events) - - [Usage](#usage) - - [How to Implement Simple Rendering via DataProvider?](#how-to-implement-simple-rendering-via-dataprovider) - - [How to Implement Simple Rendering via `items`?](#how-to-implement-simple-rendering-via-items) - - [How to Implement Component Rendering on Click Instead of Scroll?](#how-to-implement-component-rendering-on-click-instead-of-scroll) - - [How to Reinitialize the Component?](#how-to-reinitialize-the-component) - - [How to Reload a Failed Request?](#how-to-reload-a-failed-request) - - [Component State](#component-state) - - [Converting Data to the Required Format](#converting-data-to-the-required-format) - - [Sliders or Multi-Column Content](#sliders-or-multi-column-content) - - [How to Use "Should-Like" Functions?](#how-to-use-should-like-functions) - - [Overview of Functions](#overview-of-functions) - - [Best Practices](#best-practices) - - [Control the Rendering Conveyor with `itemsFactory`](#control-the-rendering-conveyor-with-itemsfactory) - - [`itemsProcessors` and Global Component Processing](#itemsprocessors-and-global-component-processing) - - [`request` and `requestQuery`](#request-and-requestquery) - - [Component Understanding](#component-understanding) - - [Lifecycle](#lifecycle) - - [`renderGuard` and `loadDataOrPerformRender`](#renderguard-and-loaddataorperformrender) - - [Performing Last Render](#performing-last-render) - - [Difference between ComponentItem with type `item` and `separator`](#difference-between-componentitem-with-type-item-and-separator) - - [Overriding in Child Layers](#overriding-in-child-layers) - - [Frequently Asked Questions](#frequently-asked-questions) - - [Slots](#slots) - - [API](#api) - - [Props](#props) - - [[shouldPerformDataRender = `(state: VirtualScrollState) => state.isInitialRender || state.remainingItems === 0`]](#shouldperformdatarender--state-virtualscrollstate--stateisinitialrender--stateremainingitems--0) - - [[shouldPerformDataRequest = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`]](#shouldperformdatarequest--state-virtualscrollstate--statelastloadeddatalength--0) - - [[shouldStopRequestingData = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`]](#shouldstoprequestingdata--state-virtualscrollstate--statelastloadeddatalength--0) - - [[chunkSize = `10`]](#chunksize--10) - - [[requestQuery]](#requestquery) - - [[itemsFactory]](#itemsfactory) - - [[itemsProcessors = `{}`]](#itemsprocessors--) - - [`tombstoneCount`](#tombstonecount) - - [Methods](#methods) - - [getNextDataSlice](#getnextdataslice) - - [getVirtualScrollState](#getvirtualscrollstate) - - [initLoadNext](#initloadnext) - - [Other Properties](#other-properties) - - [Migration from `b-virtual-scroll` version 3.x.x](#migration-from-b-virtual-scroll-version-3xx) - - [API Migration](#api-migration) - - [What's Next](#whats-next) - - [Streaming Data Rendering](#streaming-data-rendering) - - [Alternative Approach to Component Rendering](#alternative-approach-to-component-rendering) - - [Partial Rendering (can be achieved easily through `renderGuard`)](#partial-rendering-can-be-achieved-easily-through-renderguard) - - [Updating Nodes in the DOM Tree (describe implementation challenges, component allows inserting different components)](#updating-nodes-in-the-dom-tree-describe-implementation-challenges-component-allows-inserting-different-components) - - [Integration with RTX](#integration-with-rtx) - - - # components/base/b-virtual-scroll -The `b-virtual-scroll` component is designed for rendering a large array of various data. -It uses a special approach that renders chunks of components while avoiding changes to the parent component's state. -This allows for optimizing the rendering of large lists of components, making it more efficient. -If you have ever tried to render 100 components using v-for, you may have noticed that the interface starts to lag. -The `b-virtual-scroll` component aims to eliminate this lag by rendering components in portions, providing a better alternative to using v-for for such cases. +This module provides a component to render component sequences with the support of lazy loading and dynamically updating. +This component can be very efficient if you need to render a good amount of elements. ## Synopsis -- The component extends [[iData]]. - -- The component implements [[iItems]] traits. - -- By default, the component's root tag is set to `
`. +* The component extends [[iData]]. -## Modifiers +* The component implements the [[iItems]] trait. -See the implemented modifiers or the parent component. +* By default, the component's root tag is set to `
`. ## Events -| EventName | Description | Payload description | Payload | -|---------------------|---------------------------------------------------------|---------------------------------------------|----------------------------| -| `dataLoadSuccess` | Data loading has succeeded. | `data: object[], isInitialLoading: boolean` | `[data, isInitialLoading]` | -| `dataLoadStart` | Data loading has started. | `isInitialLoading: boolean` | `[isInitialLoading]` | -| `dataLoadError` | An error occurred while loading data. | `isInitialLoading: boolean` | `[isInitialLoading]` | -| `dataLoadEmpty` | Successful load with no data. | | `[]` | -| `resetState` | Reset component state. | | `[]` | -| `lifecycleDone` | All component data is rendered and loaded. | | `[]` | -| `convertDataToDB` | Trigger data conversion to the `DB`. | `data: unknown` | `[data]` | -| `elementEnter` | The element has entered the viewport. | `componentItem: MountedChild` | `[componentItem]` | -| `renderStart` | Rendering of items has started. | | `[]` | -| `renderDone` | Rendering of items has finished. | | `[]` | -| `renderEngineStart` | Rendering of items has started with the render engine. | | `[]` | -| `renderEngineDone` | Rendering of items has finished with the render engine. | | `[]` | -| `domInsertStart` | DOM node insertion has started. | | `[]` | -| `domInsertDone` | DOM node insertion has finished. | | `[]` | - -Also, you can see the implemented traits or the parent component. - -## Usage - -The component offers various usage options: it can load and render data on scroll, on click, or even load a large volume of data at once but render it in portions. Would you like to implement a global rendering process for components in order to integrate a specific element (e.g., an advertisement) after each component? No problem - the component provides processor functions that enable this functionality. Do you want to implement your own strategy for "when to load" and "when to render"? The component also offers special functions that allow for this customization. - -Below, we will explore a few basic usage scenarios and delve into the component's API in greater detail. - -### How to Implement Simple Rendering via DataProvider? - -To implement simple rendering, you need to follow several steps: - -1. Set up a data provider for the component. For example, we'll use a provider named `Provider` that returns data in the format `{data: object[]}`, where the number of objects depends on the request parameter `count`: - - ```snakeskin - < b-virtual-scroll & - :dataProvider = 'Provider' - . - ``` - - > It's important to note that `b-virtual-scroll` expects data in this specific format (`{data: object[]}`). If your provider returns data in a different format, you can use processors in either the provider or the component using the `convertDataToDb` prop. - -2. Let's say we want to load and render 12 components at a time. To achieve this, you need to specify the `request` and `chunkSize` props for the `b-virtual-scroll` component. The `request` prop defines the request parameters (standard behavior of `iData`), and `chunkSize` specifies the number of items to render in each rendering cycle: - - ```snakeskin - < b-virtual-scroll & - :dataProvider = 'Provider' | - :request = {get: {count: 12}} | - :chunkSize = 12 - . - ``` - -3. To avoid loading the same data repeatedly and load different data for each subsequent request, you need to pass the `page` request parameter to the `Provider`. This parameter indicates the page number of the data to be loaded: - - To achieve this, use the `requestQuery` prop in the `b-virtual-scroll` component. `requestQuery` is a function prop that `b-virtual-scroll` calls, passing its own state as an argument, before making a request. You can return the appropriate `page` value based on the component's state. The difference between `request` and `requestQuery` is that changes to the latter won't cause the component to reinitialize. These two props are merged to form the final request parameters passed to the provider. - - ```snakeskin - < b-virtual-scroll & - :dataProvider = 'Provider' | - :request = {get: {count: 12}} | - :requestQuery = (state) => ({get: {page: state.loadPage}}) | - :chunkSize = 12 - . - ``` - - In the example above, the `page` parameter is extracted from the component's state, specifically `loadPage`, which increments after each successful data load. - -4. Now that you have set up data loading with pagination, you need to specify what `b-virtual-scroll` will render: +| EventName | Description | Payload description | Payload | +|------------------|--------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|-----------------------------| +| dbChange | The event is fired after receiving data from a data provider. The event won't be fired if the data is empty. | Cumulative data of all tied requests | `RemoteData` | +| dataChange | The event is fired after changing a data batch | Data batch value | `unknown[]` | +| chunkLoading | The event is fired before start to load data from a data provider | Current page | `number` | +| chunkLoaded | The event is fired after every successful response from a data provider | A structure with raw and normalized data that takes from a data provider | `LastLoadedChunk`, `number` | +| chunkRenderStart | The event is fired before components are rendered | chunk number | `number` | +| chunkRender | The event is fired after rendered nodes inserted into DOM | Render items, chunk number | `RenderItem[]`, `number` | - To control what `b-virtual-scroll` renders, you can use the following props: +Also, you can see the parent component and the component traits. - - `item`: The name of the component to be rendered. It can also be a function that returns the component's name. - - - `itemProps`: The props for the component. Typically, this is a function that returns the props for each item based on the loaded data. - - - `itemKey`: The uniq id of the component. - - Rendering occurs after data is loaded. - - ```snakeskin - < b-virtual-scroll & - :dataProvider = 'Provider' | - :request = {get: {count: 12}} | - :requestQuery = (state) => ({get: {page: state.loadPage}}) | - :chunkSize = 12 | - :item = 'b-dummy' | - :itemKey = (el) => el.uuid | - :itemProps = (el) => ({name: el.name, type: el.type}) - . - ``` - - What is `el` you can inquire about in the `itemKey` and `itemProps` functions? `el` is one of the objects in the `data` array loaded by the `dataProvider`. `b-virtual-scroll` takes the array of loaded data and calls these functions for each of the objects in this array, allowing you to transform data into components that are suitable for rendering in the `b-virtual-scroll` component. - -This setup will display a component on the page that loads and renders 12 items at once. When scrolling down, a new request with a different `page` value will be made, and after a successful load, new components will be rendered. - -However, if your component takes a long time to load data (e.g., 1 second), you might notice that there is initially empty space, and then the content suddenly appears, which can be unexpected for users. To avoid this, `b-virtual-scroll` provides slots that allow you to render a "loader" while data is being loaded. +## Usage -Let's add a `loader` slot to our component to provide a better user experience during loading: +### Basic -```snakeskin -< b-virtual-scroll & - :dataProvider = 'Provider' | - :request = {get: {count: 12}} | - :requestQuery = (state) => ({get: {page: state.loadPage}}) | - :chunkSize = 12 | - :item = 'b-dummy' | - :itemProps = (el) => ({name: el.name, type: el.type}) -. - < template #loader - < .&__loader - Data loading in progress ``` - -Now, users will see a friendly message indicating that content will appear shortly, preventing them from being surprised by sudden content changes. - -### How to Implement Simple Rendering via `items`? - -The approach to rendering data using the `items` prop is not significantly different from the approach when data is obtained from a `dataProvider`. - -Instead of passing the `dataProvider` and request* parameters, -you need to pass the items prop which contains an array of data to be rendered by the components: - - ```snakeskin - < b-virtual-scroll & - :chunkSize = 12 | - :items = data | - :item = 'b-dummy' | - :itemKey = (el) => el.uuid | - :itemProps = (el) => ({name: el.name, type: el.type}) - . - ``` - - These data can be loaded by some other component or they can be static. - It doesn't matter, what's important is that the `b-virtual-scroll` component will take these data and process them through the rendering pipeline. - -There are also some minor differences in the component's event model. -Unlike `b-virtual-scroll` which uses a `dataProvider`, a component with items will not emit certain events, specifically `dataLoadStart` and `convertDataToDB`. - -The component will also ignore the `shouldPerformDataRequest` and `shouldStopRequestingData` props, as they have no meaning when there is no `dataProvider`. - -### How to Implement Component Rendering on Click Instead of Scroll? - -The `b-virtual-scroll` component, in addition to scroll-based loading, can also load data on other events, such as a click on a button. - -To implement this approach, follow these steps: - -1. Disable scroll observers using the `disableObserver` prop by setting it to `true`. - - ```snakeskin - < b-virtual-scroll & - :dataProvider = 'Provider' | - :disableObserver = true | - ... - . - ``` - -2. Set the `shouldPerformDataRender` prop to a function that always returns `true`. This function will be called for each attempt to render data. We will discuss this function in more detail in the following sections. - - ```snakeskin - < b-virtual-scroll & - :dataProvider = 'Provider' | - :disableObserver = true | - :shouldPerformDataRender = () => true | - ... - . - ``` - -3. Gain access to the methods of `b-virtual-scroll` using the standard `ref` mechanism. - - ```snakeskin - < b-virtual-scroll & - ref = scroll | - :dataProvider = 'Provider' | - :disableObserver = true | - :shouldPerformDataRender = () => true | - ... - . - ``` - - After these manipulations, `b-virtual-scroll` will no longer load data on scroll, and data loading will only occur when the `initLoadNext` method is called. This method will be used to load and render data on a button click event. - -4. Now, you need to add a button that triggers the `initLoadNext` method when clicked. - - ```snakeskin - < b-virtual-scroll & - ref = scroll | - :dataProvider = 'Provider' | - :disableObserver = true | - :shouldPerformDataRender = () => true | - ... - . - - < b-button & - @click = $refs.scroll.initLoadNext - . - Load more data - ``` - -Now, when you click the button, data will be loaded and rendered. However, you may notice that the data loading button doesn't disappear when all data is loaded, during data loading, or in case of an error. Fortunately, `b-virtual-scroll` provides a slot for displaying such a button, and it handles the logic of hiding it during loading, errors, and so on. Clients don't need to implement additional logic; you just need to move your button to the appropriate slot, specifically the `renderNext` slot. - -```snakeskin < b-virtual-scroll & - ref = scroll | - :dataProvider = 'Provider' | - :disableObserver = true | - :shouldPerformDataRender = () => true | - ... + :dataProvider = 'demo.Pagination' | + :request = {get: {chunkSize: 12}} | + :item = 'b-card' | + :itemKey = (el, i) => resolveKey(el) | + :itemProps = getPropsForOption | + :dbConverter = convertDataToVirtual . - - < template #renderNext - < b-button & - @click = $refs.scroll.initLoadNext - . - Load more data ``` -Now, your button will be displayed only when there's more data to load, and it will automatically hide during data loading and in case of any errors. - -### How to Reinitialize the Component? - -There are often situations where you need to redraw all the data that was rendered using `b-virtual-scroll`. For example, additional filtering may have been applied, making previously rendered data in `b-virtual-scroll` outdated. - -In such cases, the component provides several ways to reinitialize it. This allows you to clear the state to its initial state, effectively removing previously rendered components and resetting the state. After the state is reset, the component will start its lifecycle as if it were created from scratch. Let's explore the options for resetting the state. - -1. Updating the `request` prop. - -2. Triggering an event in the `globalEmitter` from the following list: - - `reset` - - `reset.silence` - - `reset.load` - - `reset.load.silence` - - This means the component will automatically reload when any of these events are triggered (standard `iData` logic). - -3. Calling the `reload` or `initLoad` method. - -In which cases should you use each option? - -If you have filters on the page and a data request that should be rendered using `b-virtual-scroll`, the `request` prop is the most suitable option. You can set the `request` prop to reference the current filter state. This way, when the filter state changes on the page, the component will be automatically reinitialized. - -Let's consider an example: - -**p-page.ts** +The component expects that loaded data will have the structure that matches with the `RemoteData` interface. ```typescript -@component() -class pPage extends iDynamicPage { - @field() - filterUuid: string; - - onFilterClick(newFilter: string): void { - this.filterUuid = newFilter; - } +export interface RemoteData extends Dictionary { + /** + * Data to render + */ + data?: object[]; + + /** + * Total number of elements + */ + total?: number; } ``` -**p-page.ss** - -```snakeskin -< b-virtual-scroll & - :dataProvider = 'Provider' | - :request = {get: {count: 12, filter: filterUuid}} | - ... -. -``` +You can use `dbConverter` to convert data to match this interface. -In this example, when the `filterUuid` field on the `pPage` changes, `b-virtual-scroll` will perform reinitialization and reload the data. +To specify what kind of component to render, you have to use the `option` property. +Mind, the property can be defined as a string or function. -If you need to update the component's state at a specific moment in time, regardless of the context, you can use the `reload` or `initLoad` methods. +### Manual data display control -### How to Reload a Failed Request? +By default, data is requested and rendered automatically (when scrolling the page), you can override this behavior to load and render data manually. -Did your data fail to load due to a network or server error? No worries! The `initLoadNext` method comes to the rescue, allowing you to retry the failed request. -In addition to the `initLoadNext` method, `b-virtual-scroll` provides a `retry` slot that is displayed only when the request fails. +To set loading and rendering data in manual mode, set the `loadStrategy` prop to `manual`. -This makes it straightforward to implement a retry mechanism for a failed request: - -```snakeskin +``` < b-virtual-scroll & - :dataProvider = 'Provider' | - :request = {get: {count: 12, filter: filterUuid}} | - ... + :dataProvider = 'demo.Pagination' | + :dbConverter = convertDataToVirtual | + :request = {get: {chunkSize: 12}} | + :loadStrategy = 'manual' | + + :item = 'b-card' | + :itemKey = (el, i) => resolveKey(el) | + :itemProps = getPropsForItem | . - < template #retry - < .&__retry @click = initLoadNext - Retry last request + < template #renderNext = o + < .&__render-next @click = o.ctx.renderNext + Render or load next ``` -### Component State +Initial loading and request will be made automatically, but after that `renderNext` method will need to be used to request and render data. -The `b-virtual-scroll` component is quite substantial and has its own internal state that complements the component's state. This internal state is reset when the component is reinitialized to its initial state and changes regularly during the component's lifecycle. The component's state contains a wealth of information useful for the client, such as the loaded data, the number of elements remaining outside the user's viewport, and more. +## Slots -To retrieve the component's state, you can use a special method called `getVirtualScrollState`: +The component supports a bunch of slots to provide. -**p-page.ts** +1. `tombstone` This slot is displayed only during data loading, it will be duplicated `chunkSize` number of times. +This slot is great if you want to display skeletons while the component is loading data. -```typescript -@component() -class pPage extends iDynamicPage { - protected override readonly $refs!: { - scroll: bVirtualScroll; - }; - - getScrollState(): VirtualScrollState { - return this.$refs.scroll.getVirtualScrollState(); - } -} ``` - -This method returns the current "internal" state of the component. - -### Converting Data to the Required Format - -The `b-virtual-scroll` component expects data in a specific format: - -```typescript -interface VirtualScrollDb { - data: unknown[]; -} +< b-virtual-scroll + < template #tombstone + < .&__skeleton ``` -The `data` array should contain the data items used to render the components. -The `dbConverter` prop allows you to convert data into a format suitable for `b-virtual-scroll` after data has been loaded. +2. `loader` This slot is displayed only during data loading. +This slot is great if you want to display something while the component is loading data. -```snakeskin -< b-virtual-scroll & - ... - :dbConverter = (data) => ({data: data.nestedData.data}) -. +``` +< b-virtual-scroll < template #loader - < .&__loader - Data loading in progress + < b-loader ``` -### Sliders or Multi-Column Content - -Sometimes, there is a need to render a large amount of data not in a typical vertical strip where one item follows another, but, for example, in a strip consisting of multiple columns or in a slider. - -All of these can be implemented using HTML/CSS layout and providing CSS classes in `b-virtual-scroll`. -There is no need to specify any additional props for `b-virtual-scroll`. For `b-virtual-scroll`, the content layout doesn't matter. - -### How to Use "Should-Like" Functions? - -#### Overview of Functions - -The component provides several "should-like" props that determine whether to perform certain actions. Each of these functions serves a different purpose and is called at a specific moment in time. Let's take a detailed look at each of these functions and their purposes: - -- `shouldPerformDataRequest`: This function indicates the need to load a chunk of data. If it returns `true`, a data request will be made. This function takes the "internal" component state as input and should return a boolean value. It is called when any component rendered by `b-virtual-scroll`, which has not yet entered the viewport, enters the viewport. - - > It's important to note that clients do not need to check whether data is currently being loaded or not; the `b-virtual-scroll` component handles this check itself and prevents data from being requested if a loading process is already active. - - An example implementation of this function could be to check how many items are left in the viewport, and if half of the rendered items are within the viewport, start loading more: - - ```typescript - const shouldPerformDataRequest = (state: VirtualScrollState): boolean => { - // Example: Request data if the remaining items till the end is less than or equal to 10 - return state.remainingItems <= 10; - }; - ``` - - The default implementation checks whether anything was loaded in the last request and, if so, allows another request: - - ```typescript - const shouldPerformDataRequest = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { - const isLastRequestNotEmpty = () => state.lastLoadedData.length > 0; - return isLastRequestNotEmpty(); - }; - ``` - -- `shouldStopRequestingData`: This function indicates the need to stop requesting data and tells the component that the data loading lifecycle has completed. If it returns `true`, the `b-virtual-scroll` component will not attempt to request more data until the component is reinitialized, which leads to an update of the lifecycle. This function is called after every successful data load. - - An example implementation of this function could be to check whether the number of loaded items equals the total number of items that can be returned by the pagination for the current query: - - ```typescript - const shouldStopRequestingData = (state: VirtualScrollState): boolean => { - // Example: Stop requesting data when the total number of items equals the current number of loaded items - return state.lastLoadedRawData?.total === state.data.length; - }; - ``` - - The default implementation checks whether anything was loaded in the last request and, if so, allows requests to continue: - - ```typescript - const shouldPerformDataRequest = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { - const isLastRequestNotEmpty = () => state.lastLoadedData.length > 0; - return isLastRequestNotEmpty(); - }; - ``` - -- `shouldPerformDataRender`: This function indicates the need to render the loaded data. If it returns `true`, the `b-virtual-scroll` component will call the component rendering functions and insert them into the DOM tree. This function is called when there is loaded but unrendered data. - - An example implementation of this function could be to check how many items are left before reaching the end of the component's container: - - ```typescript - const shouldPerformDataRender = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => state.remainingItems === 0; - ``` - - The default implementation is similar to the example above. - -#### Best Practices - -Here are some tips for efficiently implementing data loading on the client side while providing a seamless user experience: - -- Load data well in advance before you intend to render it. Data loading can be slow, but rendering data is much faster. Therefore, it is recommended to start data loading significantly in advance and perform rendering closer to the end of the scroll. This way, users will experience a smoother scrolling of the component. - - For example, you can implement this approach as follows: - - ```typescript - const shouldPerformDataRequest = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { - // Start loading when half of the components are in the viewport - return state.remainingItems <= chunkSize / 2; - } - ``` +3. `empty` This slot is displayed if the component has no data at all to render after completing data requests. - ```typescript - const shouldPerformDataRender = (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { - // Start rendering when only 2 components are left to the end - return state.remainingItems <= 2; - } - ``` - -- Avoid making the last useless request: This pertains to the `shouldPerformDataRequest` and `shouldStopRequestingData` functions. By default, these functions check the last data chunk to see if it returned anything. It's better to avoid this and inform the component in advance that all data has been loaded. You can achieve this by comparing the value returned by your server, indicating the total number of items with the current number of items in `b-virtual-scroll`, as demonstrated in the example above. - -### Control the Rendering Conveyor with `itemsFactory` - -`itemsFactory` is a prop that allows you to take control of component rendering. Suppose you want to render twice as many components for a single data slice. Achieving this using `iItems` props (`item`, `itemProps`, etc.) might not be possible. However, such situations may arise, and this prop is created to solve them. - -Let's consider a scenario in which we need to add a date separator component before each component with a different date from the next one. To achieve this, we will create an implementation of `itemsFactory` in which: - -1. We will access the `b-virtual-scroll` state to retrieve the loaded data. -2. We will take the previous element to determine if their dates differ, indicating whether we need to insert a date separator. -3. We will assemble an array with an abstract representation of the components to be rendered and return it from `itemsFactory`. - -```typescript -const itemsFactory = (state, ctx) => { - const - lastLoadedData = state.lastLoadedData, - allData = state.data, - items = []; - - lastLoadedData.forEach((current, i) => { - const - // Retrieve the previous data element relative to the given - prev = allData[(allData.length - lastLoadedData.length + i) - 1], - // Retrieve the next data element relative to the given - next = allData[i + 1]; - - if (!prev || prev.date !== current.date) { - items.push({ - item: 'b-date-separator', - key: current.uuid + 'separator', - type: 'separator', - children: [], - props: { - date: current.date - } - }); - } - - items.push({ - item: 'b-main-item', - key: current.uuid, - type: 'item', - children: [], - props: { - data: current - } - }); - }); - - return items; -} ``` - -As you can see in the example above, we access the last chunk of loaded data and all component data to find the previous and next data elements relative to the current one. Then, we compare their dates, and if they are not equal, we add the `b-date-separator` component before adding the `b-main-item`. This way, we collect the components to be rendered in an array and return it from the `itemsFactory` function. - -### `itemsProcessors` and Global Component Processing - -This prop is a middleware function that is called after `b-virtual-scroll` has compiled the abstract representation of components, and before it passes this representation to the rendering engine. Each function in the chain receives the result of the previous function, with the first function in the chain receiving the result of the `itemsFactory` call. The function should return an abstract representation of components that conforms to the `ComponentItem[]` interface. - -Here is an example to illustrate when `itemsProcessors` is called: - --> itemsFactory -> **itemsProcessors** -> render components via render engine -> insert components into the DOM tree - -With this prop, you can implement various scenarios, such as changing one component to another, adding components, prop migrations, and more. For some scenarios, you can also use global overrides if you need to implement some processing for all `b-virtual-scroll` instances in your application. To add a global processor, you can override the `itemsProcessors` constant located in `base/b-virtual-scroll/const.ts` within your codebase and add a function to it. - -Here's an example scenario where we need to change the name of one component to another: - -**@v4fire/client/components/base/b-virtual-scroll/const.ts** - -```typescript -export const itemsProcessors: ItemsProcessors = {}; +< b-virtual-scroll + < template #empty + < .&__empty + There is no data to render ``` -**your-project/components/base/b-virtual-scroll/const.ts** +4. `retry` This slot is displayed if the component data request error occurs. -```typescript -import { itemsProcessors } from '@v4fire/client/components/base/b-virtual-scroll/const.ts' - -export const itemsProcessors: ItemsProcessors = { - ...itemsProcessors, - - migrateCardComponent: (items: ComponentItem[]) => { - return items.map((item) => { - if (item.item === 'b-card') { - console.warn('Deprecation: b-card is deprecated.'); - - return { - ...item, - props: convertProps(item.props), - item: 'b-mega-card' - }; - } - - return item; - }); - } -}; ``` - -> It's important to note that `itemsProcessors` functions cannot be asynchronous. - -Let's also look at another common scenario: - -**Task**: Add advertising components after certain components throughout the entire application. - -**Solution**: Instead of manually defining the `itemsFactory` function in multiple places to call a pre-prepared function, you can: - - 1. Establish an agreement with clients to mark the components before or after which advertising should be displayed using meta information of the component's abstract representation (`ComponentItem`), which will be passed from the client to the component via the `itemMeta` prop: - - ```snakeskin - < b-virtual-scroll & - // ... - :itemMeta = (data) => ({ads: data.component === 'b-card' ? 'after' : false}) - . - ``` - - 2. Implement a global `itemsProcessor` that will add advertisements based on the meta-information. - - ```typescript - import { itemsProcessors } from '@v4fire/client/components/base/b-virtual-scroll/const.ts' - - export const itemsProcessors: ItemsProcessors = { - ...itemsProcessors, - - addAds: (items: ComponentItem[]) => { - const newItems: ComponentItem[] = []; - - const adsComponent = { - item: 'b-ads', - key: current.uuid + 'ads', - type: 'item', - children: [], - props: { - // ... - } - } - - return items.map((item) => { - const itemsToPush = []; - itemsToPush.push(item); - - if (item.meta.ads === 'after') { - itemsToPush.push(adsComponent); - } - - if (item.meta.ads === 'before') { - itemsToPush.unshift(adsComponent); - } - - newItems.push(...itemsToPush); - }); - - return newItems; - } - }; - ``` - -After these steps, a neighboring advertising component will be added to all components with the appropriate `meta.ads` value. - -It is also perfectly valid to do without using global processing itemsProcessors. -To achieve this, you just need to avoid overriding the constant and instead pass processors as props: - - ```typescript - class MyPageComponent { - get itemsProcessors() { - return [ - (items: ComponentItem[]) => { - const newItems: ComponentItem[] = []; - - const adsComponent = { - item: 'b-ads', - key: current.uuid + 'ads', - type: 'item', - children: [], - props: { - // ... - } - } - - return items.map((item) => { - const itemsToPush = []; - itemsToPush.push(item); - - if (item.meta.ads === 'after') { - itemsToPush.push(adsComponent); - } - - if (item.meta.ads === 'before') { - itemsToPush.unshift(adsComponent); - } - - newItems.push(...itemsToPush); - }); - - return newItems; - } - ] - } - } - ``` - -### `request` and `requestQuery` - -To pass query parameters from the `b-virtual-scroll` component to the data provider, two props are specified: `request` and `requestQuery`. But why are there two of them, and what is the difference between them? Let's break it down: - -- `request` is a prop inherited from `iData`. When the value of this prop changes, it triggers the `initLoad` method. In the case of `b-virtual-scroll`, this is interpreted as a need to reset the component's state to its initial state and start a new lifecycle from scratch. In essence, `request` represents static request parameters for one lifecycle of the component. This prop is suitable for parameters that directly affect the need to invalidate the `b-virtual-scroll` state. - -- `requestQuery` is a prop defined by `b-virtual-scroll`. One key difference from `request` is that this prop can be a function, and whatever is returned from this function will be set as query parameters. This prop is used to implement pagination. It takes the "internal" state of `b-virtual-scroll` as input and returns query parameters. Changing this prop does not lead to the reinitialization of the component. - -The `request` prop and the result of calling the `requestQuery` function are merged together and then passed to the data provider as query parameters. - -### Component Understanding - -#### Lifecycle - -The component's lifecycle consists of several events and states. When the component is initialized and starts its initial data loading, it emits two events: `initLoadStart` and `dataLoadStart`. The `initLoadStart` event is a standard event emitted by every component and occurs each time the component's data is initially loaded. The `dataLoadStart` event is emitted for every data loading. - -1. `initLoadStart` - The initial data loading of the component has started. -2. `dataLoadStart` - The data loading of the component has started. - -After successful data loading, the following events are emitted: - -1. `convertDataToDB` - The data conversion has been performed. -2. `initLoad` - The initial data loading of the component has completed. -3. `dataLoadSuccess` - The data loading of the component has completed. - -When the `convertDataToDB` event is emitted, the component's state is already updated with the `lastLoadedRawData` field. The `initLoad` and `dataLoadSuccess` events are emitted after updating the component's state, including `VirtualScrollState.data`, `VirtualScrollState.loadPage`, and some other fields. - -After successful data loading, the component consults the `shouldStopRequestingData` method to determine whether it should stop loading further data. - -Next, the component invokes the `renderGuard` to determine if the data can be rendered or not. If the `renderGuard` allows rendering, the following events are emitted: - -1. `renderStart` - The component rendering has started. -2. `renderEngineStart` - The component rendering using the rendering engine has started. -3. `renderEngineDone` - The component rendering using the rendering engine has completed. -4. `domInsertStart` - The DOM insertion has started. -5. `domInsertDone` - The DOM insertion has completed. This event is asynchronous as it uses RAF (Request Animation Frame) for DOM insertion. -6. `renderDone` - The component rendering has finished. - -Afterward, the component waits for user actions, specifically when the user sees any component on the page. The component then calls the - -`shouldPerformDataRequest` or `shouldPerformDataRender` functions on the client side, depending on the availability of data. This process repeats until all data has been loaded and rendered. - -1. `lifecycleDone` - Occurs when all data has been loaded and rendered on the page. - -#### `renderGuard` and `loadDataOrPerformRender` - -The `b-virtual-scroll` component relies on the `renderGuard` and `loadDataOrPerformRender` functions to determine whether to render data, load data, or complete the component's lifecycle. - -The `loadDataOrPerformRender` function is the entry point for the data loading and rendering cycle. -This function consults the `renderGuard`, which determines whether data can be rendered based on the data state and provides reasons for rejection only if it has not permitted the rendering. - -Understanding `renderGuard`: - -```mermaid -flowchart TD - A["Start: renderGuard Function"] --> B["Check if dataSlice.length < chunkSize"] - B -- "True" --> C["Check if state.areRequestsStopped and state.isLastRender"] - C -- "True" --> D["Return: {result: false, reason: 'done'}"] - C -- "False" --> E["Return: {result: false, reason: 'notEnoughData'}"] - B -- "False" --> F["Check if state.isInitialRender"] - F -- "True" --> G["Return: {result: true}"] - F -- "False" --> H["Invoke shouldPerformDataRender"] - H -- "Not Defined or True" --> I["Return: {result: true}"] - H -- "False" --> J["Return: {result: false, reason: 'noPermission'}"] -``` - -Understanding `loadDataOrPerformRender`: - -```mermaid -flowchart TD - A["Start: loadDataOrPerformRender Function"] --> B["Check if state.isLastErrored"] - B -- "True" --> C["Return"] - B -- "False" --> D["Invoke renderGuard(state)"] - D --> E["Check renderGuard result"] - E -- "True" --> F["Invoke performRender()"] - E -- "False" --> G["Check renderGuard reason"] - G -- "done" --> H["Invoke onLifecycleDone()"] - G -- "notEnoughData" --> I["Check if state.areRequestsStopped"] - I -- "True" --> J["Invoke performRender() and onLifecycleDone()"] - I -- "False" --> K["Check if shouldPerformDataRequest()"] - K -- "True" --> L["Invoke initLoadNext()"] - K -- "False" --> M["Check if state.isInitialRender"] - M -- "True" --> N["Invoke performRender()"] - M -- "False" --> O["Return"] +< b-virtual-scroll + < template #retry = o + < .&__retry @click = o.ctx.reloadLast + Retry last request ``` -#### Performing Last Render - -The `b-virtual-scroll` component adheres to a strategy where it always performs a "final" rendering. -This final rendering is always triggered after the client has indicated that data requests are complete (`shouldStopRequestingData`) and the data for rendering is nearing its end or has been exhausted. -To inform the client that the current rendering cycle is the last, the component sets the `isLastRender` flag in its state to `true` before the `renderStart` event and before initiating the rendering cycle, as well as before calling `itemsFactory` and `itemsProcessors`. - -Let's consider a scenario where this can be useful. Suppose we need to render 10 items at a time, and after all data has been loaded and rendered, we need to add an advertising block. -Imagine a situation where our provider initially responds with an array of 10 items, and then with an array of 0 items. If our `shouldStopRequestingData` strategy returns `true` when the provider returns less than 10 items, the `b-virtual-scroll` component will still attempt to render, even without data. This rendering will occur with the `isLastRender` flag set to `true`. -The actual rendering through rendering engines will only happen if the chain of `itemsFactory` -> `itemsProcessors` returns components for rendering; otherwise, no rendering will occur. -Also, if no rendering occurs, certain events such as `renderEngine*` and `domInsert*` will not be emitted, as there is nothing to render or insert. -This approach guarantees that the client will always have the opportunity to insert something at the end of the feed. Below is a demonstration of this approach: +5. `renderNext` This slot is displayed if the component has data to render or requests are not stopped. +This slot can be useful if you want to provide the ability to manually request additional data. -```typescript -@component() -class pPage { - shouldStopRequestingData(state: VirtualScrollState): boolean { - return state.lastLoadedData < 10; - } - - itemsFactory(state: VirtualScrollState): ComponentItem[] { - const items: ComponentItem[] = state.lastLoadedData.map((itemData, index) => { - return { - type: 'item', - item: 'section', - props: { - id: `element-${index}` - }, - key: `item-${index}`, - children: [] - }; - }); - - if (state.isLastRender) { - items.push({ - type: 'item', - item: 'button', - props: { - id: `lastElement` - }, - key: `lastElement`, - children: [] - }) - } - - return items; - } -} ``` - -This example demonstrates the `b-virtual-scroll` component's capability to handle a final rendering phase, even when the incoming data stream has been exhausted. This flexibility allows for dynamic and versatile implementations, like adding a unique element at the end of a list, ensuring a seamless and user-centric experience. - -#### Difference between ComponentItem with type `item` and `separator` - -The component allows rendering two types of components: - -- `item` - Main component (main content). -- `separator` - Other components, such as dividers or separators. - -There is no significant difference between them, except that they are treated differently in fields like `remainingItems` in the `VirtualScrollState`. As the name suggests, the `remainingItems` property only considers components with the `item` type, while `remainingChildren` considers components with both `item` and `separator` types. - -The distinction between `item` and `separator` types is mainly used for calculating certain properties based on the type of components present in the `VirtualScrollState`, such as the number of items till the end of the scroll. - -#### Overriding in Child Layers - -The main use case for overriding in child layers is to modify the default behavior of functions or methods. - -For example, it may be useful to override the logic of `shouldStopRequestingData` if you want to implement a default logic that takes into account the `total` field of the response when making a decision. - -There may also be situations where you need to modify the `renderGuard`. Currently, the component loads data until the number of items reaches the `chunkSize` and then renders them. By overriding the `renderGuard`, you can achieve partial rendering, where the component renders the available data regardless of whether it reaches the `chunkSize`. - -### Frequently Asked Questions - -- How to assign a class to components rendered within `b-virtual-scroll`? - - To achieve this, you need to include the `class` field in the props of the component that should be rendered. You can do this by returning it from the `itemProps` function, like this: - - ```typescript - const itemProps = (this: bMyComponent) => ({ - // ... - class: this.provide.classes({'virtual-scroll-item': true}) - }) - ``` - -- Can I use only `shouldPerformDataRequest` and not use `shouldStopRequestingData`? - - Hypothetically, you can. However, this may cause issues with the `done` slot and the `lifecycleDone` event; they will not work correctly. Therefore, it is strongly recommended to separate the logic into whether data should be loaded now (`shouldPerformDataRequest`) and whether data loading is completed (all data is loaded) (`shouldStopRequestingData`). - -- Can I set `chunkSize` to 10 if the request returns 89 items at a time? - - Yes, you can. `b-virtual-scroll` will render the data in chunks until it has rendered all of it. - -- Can I set `chunkSize` to 10 if the request returns 5 items at a time? - - Yes, you can. `b-virtual-scroll` will make requests (one at a time!) until the number of loaded items is greater than or equal to the value specified in `chunkSize`. - -- Can I render a different number of items on each render cycle? - - Yes, you can. `b-virtual-scroll` provides two options: - - 1. Specify the `chunkSize` prop as a function that returns a number depending on something. Let’s say we want to render 6 elements at the first render, 12 at the second, and 18 in subsequent ones: - - ```typescript - const chunkSize = (state: VirtualScrollState) => [6, 12, 18][state.renderPage] ?? 18 - ``` - - 2. Use the `itemsFactory` prop and return any number of elements from this function. - -- Suppose I want to load 1000 data items once and not make any more requests. How can I achieve this? - - 1. Set `chunkSize` to a suitable value, for example, 10, if you want 10 components to be rendered in one rendering cycle. - - 2. Set up `dataProvider` and request parameters. - - 3. Set the `shouldStopRequestingData` function to always return `true`. - - After these manipulations, `b-virtual-scroll` will load the data using `dataProvider` once and then render all the loaded data in chunks. - -- Data loading is complete, but the components are not rendering. Why could this happen? - - 1. Ensure that your data has a format suitable for `b-virtual-scroll`, specifically `{data: any[]}`. If your data has a different format, you can convert it using the `dbConverter` prop, which should return the transformed data, or convert the data in another location, such as in the provider's post-processor. - - 2. Make sure that your `should-*` functions are correctly defined, and their conditions are met. - - 3. Ensure that your component is included in the bundle in the `index.js` file of your page or component. - - 4. Verify that there are no errors in specifying the component's name in the `item` prop and no issues with props in `itemProps`. - -- The same components are being rendered multiple times in a row. Why could this happen? - - 1. Ensure that you implement pagination using request parameters, and possibly the `requestQuery` prop. You might be loading the same data repeatedly because the request parameters are not changing. - - 2. If you have overridden `itemsFactory` and are managing the data rendering flow yourself, ensure that there are no errors in the data slice you are using for rendering. - -## Slots - -The component supports several slots for customization: - -1. The `loader` slot allows you to display different content (usually skeletons) while the data is being loaded: - - ```snakeskin - < b-virtual-scroll - < template #loader - < .&__loader - Data loading in progress - ``` - -2. The `tombstone` slot allows you to display different content (usually skeletons) that will be repeated `tombstoneCount` times while the data is being loaded: - - ```snakeskin - < b-virtual-scroll :tombstoneCount = 3 - < template #tombstone - < .&__skeleton - Skeleton - ``` - -3. The `retry` slot allows you to display different content (usually a prompt to retry loading data) when there is an error in data loading: - - ```snakeskin - < b-virtual-scroll - < template #retry - < .&__retry @click = initLoadNext - Retry last request - ``` - -4. The `empty` slot allows you to display different content when the component receives an empty data set during the initial loading: - - ```snakeskin - < b-virtual-scroll - < template #empty - < .&__empty - No data - ``` - -5. The `done` slot allows you to display different content when the component has finished loading and rendering all the data. The `done` slot -will be displayed after `lifecycleDone` event is fired: - - ```snakeskin - < b-virtual-scroll - < template #done - < .&__done - Load and render complete - ``` - -6. The `renderNext` slot allows you to display different content when the component is not loading data and has not entered the lifecycle completion state. -This slot can be useful when implementing lazy content rendering on button click: - - ```snakeskin - < b-virtual-scroll - < template #renderNext - < .&__render-next - Render next - ``` - -## API - -### Props - -#### [shouldPerformDataRender = `(state: VirtualScrollState) => state.isInitialRender || state.remainingItems === 0`] - -This function is called in the `bVirtualScroll.renderGuard` after other checks are completed. -It receives the component state as input and determines whether the component should render the next chunk of components. -The function should return a boolean value: `true` to allow the rendering of the next chunk, or `false` to prevent it. - -Example usage: - -```typescript -const shouldPerformDataRender = (state: VirtualScrollState): boolean => { - return state.isInitialRender || state.remainingItems === 0; -}; +< b-virtual-scroll + < template #retry = o + < .&__retry @click = o.ctx.renderNext + Render next ``` -#### [shouldPerformDataRequest = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`] - -The `shouldPerformDataRequest` property of `bVirtualScroll` allows you to control whether the component should request additional data based on the component state. -This function allows the component to understand whether the data loading lifecycle is complete or not. - -Here's an example of how you can use `shouldPerformDataRequest`: +6. `done` This slot is displayed if the component rendered and requested all data. -```typescript -const shouldPerformDataRequest = (state: VirtualScrollState): boolean => { - // Example: Request data if the remaining items till the end is less than or equal to 10 - return state.remainingItems <= 10; -}; ``` - -In this example, the function checks the `remainingItems` property of the component state. -If the remaining number of items till the end is less than or equal to 10, it returns `true` to indicate that the component should perform a data request. -You can adjust the condition based on your specific requirements. - -By implementing the `shouldPerformDataRequest` function, you have control over when the component should request additional data. -This allows you to customize the data loading behavior based on the state of the component. - -#### [shouldStopRequestingData = `(state: VirtualScrollState) => state.lastLoadedData.length > 0`] - -This function is called on each data loading cycle. It determines whether the component should stop requesting new data. -The function should return a boolean value: `true` to stop requesting data, or `false` to continue requesting data. - -Here's an example of how you can use `shouldStopRequestingData`: - -```typescript -const shouldStopRequestingData = (state: VirtualScrollState): boolean => { - // Example: Stop requesting data when the total number of items equals the current number of loaded items - return state.lastLoadedRawData?.total === state.data.length; -}; +< b-virtual-scroll + < template + < .&__done + All data are rendered and requested ``` -In this example, the function compares the total property of `lastLoadedRawData` with the length of the data array. -If the two values are equal, it returns true to indicate that the component should stop requesting new data. -This condition suggests that all available items have been loaded, and there is no need for further data requests. - -You can customize the `shouldStopRequestingData` function to fit your specific scenario. -By implementing this function, you have control over when the component should stop requesting new data, based on the comparison between the total number of items and the current number of loaded items. - -#### [chunkSize = `10`] +## API -The amount of data required to perform one cycle of item rendering. This prop is used by the `bVirtualScroll` component to determine the number of components to render in each cycle. -It can be either a fixed number or a function that returns the number dynamically based on the component state. +Also, you can see the parent component and the component traits. -Here are some examples: +### Props -```typescript -const chunkSize = (state: VirtualScrollState): number => { - // Example 1: Incrementing chunk size for each render page - return (state.renderPage + 1) * 10; - - // Example 2: Dynamic chunk size based on the state - // Replace the condition and calculation with your custom logic - if (state.isInitialRender) { - return 20; - } else if (state.renderPage < 3) { - return 15; - } else { - return 10; - } -}; -``` +#### [cacheSize = `400`] -In Example 1, the chunk size increases by 10 for each render page. For the initial render, it will be 10, then 20, 30, and so on. -In Example 2, the chunk size is dynamically determined based on the component state. It assigns different chunk sizes based on different conditions. +The maximum number of elements to cache. -By using a function for `chunkSize`, you have the flexibility to adjust the rendering behavior based on the state of the component and other factors. +#### [renderGap = `10`] -#### [requestQuery] +Number of elements till the page bottom that should initialize a new render iteration. -- Type: `Function` -- Default: `undefined` +#### [chunkSize = `10`] -A function that returns the GET parameters for a request. This function is called for each request and receives the current component state as input. -It should return an object containing the request parameters. These parameters will be merged with the parameters from the `request` prop, giving priority to the `request` prop. +Number of elements per one render chunk. -Pagination example: +#### [tombstonesSize] -```typescript -const requestQuery = (state: VirtualScrollState): Dictionary => { - return { - get: { - page: state.loadPage, - limit: 10 - // Other pagination parameters - } - }; -}; -``` +Number of tombstones to render. -#### [itemsFactory] +#### [clearNodes = `false`] -A factory function used to generate an array of `ComponentItem` objects representing the components to be rendered. -This function is called during the rendering process and receives the component state and context as arguments. It should return an array of `ComponentItem` objects. +If true, then elements are dropped from a DOM tree after scrolling. +This method is recommended to use if you need to display a huge number of elements and prevent an OOM error. -The default implementation uses the `chunkSize` and `iItems` trait props to slice the data and generate the components. -However, you can override this function to implement a custom rendering strategy. +#### [cacheNodes = `true`] -Here's an example of how you can use the itemsFactory property to generate ComponentItem objects based on the `lastLoadedData` property: +If true, then created nodes are cached. -```typescript -const itemsFactory = (state: VirtualScrollState): ComponentItem[] => { - const items: ComponentItem[] = state.lastLoadedData.map((itemData, index) => { - // Construct a ComponentItem object for each item in the lastLoadedData array - return { - type: 'item', - item: 'b-button', - props: { - id: `button-${index}` - }, - key: `item-${index}`, - children: { - default: `Item ${index + 1}` - } - }; - }); - - return items; -}; -``` +#### [requestQuery] -#### [itemsProcessors = `{}`] +A function that returns parameters to make a request. -This prop is a middleware function that is called after `b-virtual-scroll` has compiled the abstract representation of components, and before it passes this representation to the rendering engine. +#### [getData] -This function can be useful in cases where you need to implement some processing of the abstract representation of components, such as mutating props or adding additional components. +A function to request a new data chunk to render. -#### `tombstoneCount` +#### [shouldMakeRequest] -- Type: `number` -- Default: `undefined` +When this function returns true the component will be able to request additional data. -Specifies the number of times the `tombstone` component will be rendered. This prop can be useful if you want to render multiple `tombstone` components using a single specified element. -For example, if you set `tombstoneCount` to 3, then three `tombstone` components will be rendered on your page. +#### [shouldStopRequest] -Note: The `tombstone` component is used to represent empty or unloaded components in the virtual scroll. It is rendered as a placeholder until the actual component data is loaded and rendered. +When this function returns true the component will stop to request new data. ### Methods -#### getNextDataSlice - -Returns the next data slice that should be rendered based on the `chunkSize`. - -#### getVirtualScrollState - -Returns the current state of the component. - -#### initLoadNext - -Initializes the loading of the next data chunk. In case the loading fails, calling this method again will attempt to reload it. - -### Other Properties - -The `bVirtualScroll` class extends `iData` and includes additional properties related to slots, component state, and observers. Please refer to the documentation of `iData` for more details on those properties. - -## Migration from `b-virtual-scroll` version 3.x.x - -### API Migration - -- Prop `renderGap` deleted -> use `shouldPerformDataRender`; -- Prop `shouldRequestMore` deleted -> use `shouldPerformDataRequest`; -- Prop `shouldStopRequest` deleted -> use `shouldStopRequestingData`; -- Prop `getData` was removed; -- Deprecated props `option-like` deleted -> use `iItems` props; -- Method renamed `getDataStateSnapshot` -> `getVirtualScrollState`; -- Method `reloadLast` -> `initLoadNext`; -- `VirtualItemEl` interface is removed. Now, the client receives a single data item in the `iItems` methods. To maintain logic with `current`, `prev`, `next`, you can use the following approach: - - ```typescript - function getProps(dataItem: DataInterface, index: number): Dictionary { - const - {data, lastLoadedData} = this.$refs.scroll.getVirtualScrollState(); - - const - current = dataItem, - /* Retrieve the previous data element relative to the given */ - prev = data[(data.length - lastLoadedData.length + i) - 1], - /* Retrieve the next data element relative to the given */ - next = lastLoadedData[i + 1]; - } - ``` - -- Interface `DataState` -> `VirtualScrollState`: - - `DataState.currentPage` -> `VirtualScrollState.loadPage`; - - `DataState.lastLoadedChunk.raw` -> `VirtualScrollState.lastLoadedRawData`; - - etc. - -## What's Next - -The component currently lacks some features that may improve its functionality and make it more suitable for different scenarios. - -### Streaming Data Rendering - -- Planned for implementation. - -There is a request for streaming data rendering from the server. -This can be implemented using the standard V4 `dataProvider` API, but it requires further modifications to the component to handle streaming data events. - -### Alternative Approach to Component Rendering - -- Planned as an experiment. +#### reInit -Currently, the component uses the `iBlock.vdom` API, which creates a new rendering engine instance for each chunk. -It is hypothetically possible to reuse the rendering engine instead. However, there are challenges to consider. -For example, the Vue 3 rendering engine removes previously rendered DOM nodes and destroys components when attempting to use the rendering function and `forceUpdate` with a different VNode to render. +Re-initializes the component. -### Partial Rendering (can be achieved easily through `renderGuard`) +#### reloadLast -- Not planned for implementation. +Reloads the last request (if there is no `db` or `options` the method calls reload). -Currently, the component loads data until the number of items reaches the `chunkSize` and then renders them. By overriding the `renderGuard`, you can achieve partial rendering, where the component renders the available data regardless of whether it reaches the `chunkSize`. +#### renderNext -### Updating Nodes in the DOM Tree (describe implementation challenges, component allows inserting different components) +Tries to render the next data chunk. +The method emits a new request for data if necessary. -- Planned as an experiment. +#### getCurrentDataState -Currently, `b-virtual-scroll` does not remove old nodes when rendering new chunks within the same lifecycle. Implementing this feature is not a priority, but it should not be ignored either. The main reasons why this feature was not included in the initial release are: +Returns an object with the current data state of the component. -- Previous experiments showed no performance degradation after rendering and inserting 30x(5-8) components into the DOM tree. -- The inability to reuse DOM nodes: typical components allow reusing DOM nodes, but `b-virtual-scroll` enables clients to easily render different components. It is important to note that reusing DOM nodes provides the greatest benefit, not just simple insertion/removal of entire sections from the DOM tree. -- The need to implement two-way data rendering: Since memory is limited, storing a large number of rendered components in memory is not ideal. This requires destroying previously rendered components and then rendering them again. However, this approach can cause delays when scrolling back up. -- Since scroll events need to be used to render data, additional heuristics or props indicating the scroll direction and the number of columns being rendered may need to be added to correctly maintain the node map. +#### getItemAttrs -### Integration with RTX +Returns additional props to pass to an item component. -- High priority. +#### getItemComponentName -Why have `b-virtual-scroll` without RTX? +Returns a component name to render an item. diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ss b/src/components/base/b-virtual-scroll/b-virtual-scroll.ss index b218ad189b..3c2f2d4590 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ss +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ss @@ -19,7 +19,7 @@ ref = tombstones | v-if = $slots['tombstone'] . - < .&__tombstone v-for = i in tombstoneCount || chunkSize + < .&__tombstone v-for = i in tombstonesSize || chunkSize += self.slot('tombstone') < .&__loader & diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts index e8bd54b9d6..1568a4b64a 100644 --- a/src/components/base/b-virtual-scroll/b-virtual-scroll.ts +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll.ts @@ -12,401 +12,481 @@ */ import symbolGenerator from 'core/symbol'; -import type { AsyncOptions } from 'core/async'; -import type iItems from 'components/traits/i-items/i-items'; -import VDOM, { create, render } from 'components/friends/vdom'; -import { iVirtualScrollHandlers } from 'components/base/b-virtual-scroll/handlers'; -import { bVirtualScrollAsyncGroup, bVirtualScrollDomInsertAsyncGroup, componentModes, renderGuardRejectionReason } from 'components/base/b-virtual-scroll/const'; -import type { VirtualScrollState, RenderGuardResult, $ComponentRefs, UnsafeBVirtualScroll, ItemsProcessors, ComponentMode } from 'components/base/b-virtual-scroll/interface'; +import DOM, { watchForIntersection, appendChild } from 'components/friends/dom'; +import VDOM, { render, create } from 'components/friends/vdom'; +import Block, { getFullElementName } from 'components/friends/block'; -import { ComponentTypedEmitter, componentTypedEmitter } from 'components/base/b-virtual-scroll/modules/emitter'; -import { ComponentInternalState } from 'components/base/b-virtual-scroll/modules/state'; -import { SlotsStateController } from 'components/base/b-virtual-scroll/modules/slots'; -import { ComponentFactory } from 'components/base/b-virtual-scroll/modules/factory'; -import { Observer } from 'components/base/b-virtual-scroll/modules/observer'; +import iItems, { IterationKey } from 'components/traits/i-items/i-items'; -import iData, { component, system, watch, wait, RequestParams, UnsafeGetter } from 'components/super/i-data/i-data'; +import iData, { + + component, + computed, + prop, + system, + field, + watch, + wait, + hook, + + RequestParams, + RequestError, + + InitLoadOptions, + RetryRequestFn, + CheckDBEquality, + + UnsafeGetter + +} from 'components/super/i-data/i-data'; + +import ComponentRender from 'components/base/b-virtual-scroll/modules/component-render'; +import ChunkRender from 'components/base/b-virtual-scroll/modules/chunk-render'; +import ChunkRequest from 'components/base/b-virtual-scroll/modules/chunk-request'; + +import { getRequestParams, isAsyncReplaceError } from 'components/base/b-virtual-scroll/modules/helpers'; + +import type { + + GetData, + RemoteData, + + RequestFn, + RequestQueryFn, + + LocalState, + LoadStrategy, + + DataState, + MergeDataStateParams, + + UnsafeBVirtualScroll + +} from 'components/base/b-virtual-scroll/interface'; -export * from 'components/base/b-virtual-scroll/interface'; -export * from 'components/base/b-virtual-scroll/const'; export * from 'components/super/i-data/i-data'; +export * from 'components/base/b-virtual-scroll/modules/helpers'; +export * from 'components/base/b-virtual-scroll/interface'; + +export { RequestFn, RemoteData, RequestQueryFn, GetData }; -const $$ = symbolGenerator(); +DOM.addToPrototype(watchForIntersection, appendChild); +VDOM.addToPrototype(render, create); +Block.addToPrototype(getFullElementName); -VDOM.addToPrototype({create, render}); +const + $$ = symbolGenerator(); @component() -export default class bVirtualScroll extends iVirtualScrollHandlers implements iItems { +export default class bVirtualScroll extends iData implements iItems { + /** {@link iItems.Item} */ + readonly Item!: object; - /** {@link componentTypedEmitter} */ - @system((ctx) => componentTypedEmitter(ctx)) - protected readonly componentEmitter!: ComponentTypedEmitter; + /** {@link iItems.Items} */ + readonly Items!: Array; - /** {@link SlotsStateController} */ - @system((ctx) => new SlotsStateController(ctx)) - protected readonly slotsStateController!: SlotsStateController; + override readonly DB!: RemoteData; - /** {@link ComponentInternalState} */ - @system((ctx) => new ComponentInternalState(ctx)) - protected readonly componentInternalState!: ComponentInternalState; + override readonly checkDBEquality: CheckDBEquality = false; - /** {@link ComponentFactory} */ - @system((ctx) => new ComponentFactory(ctx)) - protected readonly componentFactory!: ComponentFactory; + /** {@link LoadStrategy} */ + @prop({type: String, watch: 'syncPropsWatcher'}) + readonly loadStrategy: LoadStrategy = 'scroll'; - /** {@link Observer} */ - @system((ctx) => new Observer(ctx)) - protected readonly observer!: Observer; + /** {@link iItems.item} */ + @prop({type: [String, Function], required: false}) + readonly item?: iItems['item']; - protected override readonly $refs!: iData['$refs'] & $ComponentRefs; + /** {@link iItems.itemKey} */ + @prop({type: [String, Function], required: false}) + readonly itemKey?: iItems['itemKey']; - // @ts-ignore (getter instead readonly) - override get requestParams(): iData['requestParams'] { - return { - get: { - ...this.requestQuery?.(this.getVirtualScrollState())?.get, - ...Object.isDictionary(this.request?.get) ? this.request?.get : undefined - } - }; - } + /** {@link iItems.itemProps} */ + @prop({type: [Function, Object], default: () => ({})}) + readonly itemProps!: iItems['itemProps']; - override get unsafe(): UnsafeGetter> { - return Object.cast(this); - } + /** {@link iItems.items} */ + @prop(Array) + readonly itemsProp: this['Items'] = []; /** - * {@link ComponentMode} + * The maximum number of elements to cache */ - get componentMode(): ComponentMode { - return this.items ? componentModes.items : componentModes.dataProvider; - } + @prop({type: Number, watch: 'syncPropsWatcher', validator: Number.isNatural}) + readonly cacheSize: number = 400; /** - * Initializes the loading of the next data chunk - * @throws {@link ReferenceError} if there is no `dataProvider` set. + * Number of elements till the page bottom that should initialize a new render iteration */ - initLoadNext(): CanUndef> { - if (!this.dataProvider) { - throw ReferenceError('Missing dataProvider'); - } + @prop({type: Number, validator: Number.isNatural}) + readonly renderGap: number = 10; - const - state = this.getVirtualScrollState(); + /** + * Number of elements per one render chunk + */ + @prop({type: Number, validator: Number.isNatural}) + readonly chunkSize: number = 10; - if (state.isLoadingInProgress) { - return; - } + /** + * Number of tombstones to render + */ + @prop({type: Number, required: false, validator: Number.isNatural}) + readonly tombstonesSize?: number; - if (this.db == null) { - return this.initLoad(); - } + /** + * If true, then elements are dropped from a DOM tree after scrolling. + * This method is recommended to use if you need to display a huge number of elements and prevent an OOM error. + */ + @prop(Boolean) + readonly clearNodes: boolean = false; - this.onDataLoadStart(false); + /** + * If true, then created nodes are cached + */ + @prop({type: Boolean, watch: 'syncPropsWatcher'}) + readonly cacheNodes: boolean = true; - const - params = this.getRequestParams(), - get = this.dataProvider.get(params[0], {...params[1], showProgress: false}); - - return get - .then((res) => { - if (res == null) { - return; - } - - this.onDataLoadSuccess(false, this.convertDataToDB(res)); - }) - .catch(stderr); - } + /** + * Function that returns parameters to make a request + */ + @prop({type: Function, required: false}) + readonly requestQuery?: RequestQueryFn; + + @prop({type: [Object, Array], required: false}) + override readonly request?: RequestParams; /** - * Returns the internal component state - * {@link VirtualScrollState} + * Function to request a new data chunk to render */ - getVirtualScrollState(): Readonly { - return this.componentInternalState.compile(); - } + @prop({type: Function, default: (ctx: bVirtualScroll, query) => ctx.dataProvider?.get(query), required: false}) + readonly getData!: GetData; /** - * Returns the next slice of data that should be rendered - * - * @param state - * @param chunkSize + * When this function returns true the component will be able to request additional data */ - getNextDataSlice(state: VirtualScrollState, chunkSize: number): object[] { - const - nextDataSliceStartIndex = this.componentInternalState.getDataCursor(), - nextDataSliceEndIndex = nextDataSliceStartIndex + chunkSize; + @prop({type: Function, default: (v: DataState) => v.itemsTillBottom <= 10 && !v.isLastEmpty}) + readonly shouldMakeRequest!: RequestFn; - return state.data.slice(nextDataSliceStartIndex, nextDataSliceEndIndex); + /** + * When this function returns true the component will stop to request new data + */ + @prop({type: Function, default: (v) => v.isLastEmpty}) + readonly shouldStopRequest!: RequestFn; + + /** {@link iItems.items} */ + @computed({dependencies: ['itemsStore']}) + get items(): this['Items'] { + return this.itemsStore ?? []; + } + + /** {@link iItems.items} */ + set items(value: this['Items']) { + this.field.set('itemsStore', value); + } + + override get unsafe(): UnsafeGetter> { + return Object.cast(this); } + /** {@link iItems.items} */ + @field((o) => o.sync.link()) + protected itemsStore!: iItems['items']; + /** - * Returns the chunk size that should be rendered - * @param state - current lifecycle state. + * Total amount of items that can be loaded */ - getChunkSize(state: VirtualScrollState): number { - return Object.isFunction(this.chunkSize) ? - this.chunkSize(state, this) : - this.chunkSize; - } + @system() + protected total?: number; /** - * Returns an items processors - * @returns + * Local component state */ - getItemsProcessors(): CanUndef { - return this.itemsProcessors; + protected get localState(): LocalState { + return this.localStateStore; } - override reload(...args: Parameters): ReturnType { - this.componentStatus = 'loading'; - return super.reload(...args); + /** + * @param state + * @emits `localEmitter:localState.loading()` + * @emits `localEmitter:localState.ready()` + * @emits `localEmitter:localState.error()` + */ + protected set localState(state: LocalState) { + this.localStateStore = state; + this.localEmitter.emit(`localState.${state}`); } - @watch({path: 'items', provideArgs: false}) - override initLoad(...args: Parameters): ReturnType { - if (!this.lfc.isBeforeCreate()) { - this.reset(); - } + /** + * Local component state store + */ + @system() + protected localStateStore: LocalState = 'init'; - this.componentInternalState.setIsLoadingInProgress(true); + // @ts-ignore (getter instead readonly) + override get requestParams(): RequestParams { + return { + get: { + ...this.requestQuery?.(this.getDataStateSnapshot())?.get, + ...Object.isDictionary(this.request?.get) ? this.request?.get : undefined + } + }; + } - const - initLoadResult = super.initLoad(...args); + /** + * API for scroll rendering + */ + @system((o) => new ChunkRender(o)) + protected chunkRender!: ChunkRender; - if (this.componentMode === componentModes.items) { - if (Object.isPromise(initLoadResult)) { - return initLoadResult - .then(() => this.initItems()) - .catch(stderr); - } + /** + * API for scroll data requests + */ + @system((o) => new ChunkRequest(o)) + protected chunkRequest!: ChunkRequest; - return this.initItems(); - } + /** + * API for dynamic component rendering + */ + @system((o) => new ComponentRender(o)) + protected componentRender!: ComponentRender; + + protected override readonly $refs!: iData['$refs'] & { + container: HTMLElement; + loader?: HTMLElement; + tombstones?: HTMLElement; + empty?: HTMLElement; + retry?: HTMLElement; + done?: HTMLElement; + renderNext?: HTMLElement; + }; - this.onDataLoadStart(true); + /** + * @param [data] + * @param [opts] + * + * @emits `chunkLoading(page: number)` + * */ + override initLoad(data?: unknown, opts?: InitLoadOptions): CanPromise { + this.async.clearAll({label: 'chunkRequest.waitForInitCalls'}); - if (Object.isPromise(initLoadResult)) { - initLoadResult - .then(() => { - if (this.db == null) { - return; - } + if (!this.lfc.isBeforeCreate()) { + this.reInit(); + } - this.onDataLoadSuccess(true, this.db); - }) - .catch(stderr); + if (this.isActivated) { + this.emit('chunkLoading', 0); } - return initLoadResult; + return super.initLoad(data, opts); } /** - * Initializes the data passed through the items prop + * Re-initializes the component */ - @wait({defer: true}) - protected initItems(): CanPromise { - if ( - this.componentMode !== componentModes.items || - !this.items - ) { - return; - } - - this.onItemsInit(this.items); - } - - protected override convertDataToDB(data: unknown): O | this['DB'] { - this.onConvertDataToDB(data); - const result = super.convertDataToDB(data); - - return result; + reInit(): void { + this.componentRender.reInit(); + this.chunkRequest.reset(); + this.chunkRender.reInit(); } /** - * Merges all request parameters from the component fields `requestProp` and `requestQuery` - * {@link RequestParams} + * Reloads the last request (if there is no `db` or `options` the method calls reload) */ - protected getRequestParams(): RequestParams { - const label: AsyncOptions = { - label: $$.initLoadNext, - group: bVirtualScrollAsyncGroup, - join: 'replace' - }; + reloadLast(): void { + if (!this.db || this.chunkRequest.data.length === 0) { + this.reload().catch(stderr); - const defParams = this.dataProvider?.getDefaultRequestParams('get'); - - if (Array.isArray(defParams)) { - Object.assign(defParams[1], label); + } else { + this.chunkRequest.reloadLast(); } - - return defParams; } /** - * Short-hand wrapper for calling {@link bVirtualScroll.shouldStopRequestingData}, which also caches the - * result of the call and, if {@link bVirtualScroll.shouldStopRequestingData} returns `true`, does not call - * this function again until the life cycle is updated and the state is reset. + * Tries to render the next data chunk. + * The method emits a new request for data if necessary. */ - protected shouldStopRequestingDataWrapper(): boolean { - if (this.componentMode === componentModes.items) { - this.componentInternalState.setIsRequestsStopped(true); - return true; - } - - const state = this.getVirtualScrollState(); + renderNext(): void { + const + {localState, chunkRequest, dataProvider, items} = this; - if (state.areRequestsStopped) { - return state.areRequestsStopped; + if (localState !== 'ready' || dataProvider == null && items.length === 0) { + return; } - const newVal = this.shouldStopRequestingData(state, this); - - this.componentInternalState.setIsRequestsStopped(newVal); - return newVal; + chunkRequest.try().catch(stderr); } /** - * Short-hand wrapper for calling {@link bVirtualScroll.shouldPerformDataRequest}, removing the need to pass - * state and context when calling {@link bVirtualScroll.shouldPerformDataRequest}. + * Returns an object with the current data state of the component + * + * @typeParam ITEM - data item to render + * @typeParam RAW - raw provider data */ - protected shouldPerformDataRequestWrapper(): boolean { - if (this.componentMode === componentModes.items) { - return false; + getCurrentDataState< + ITEM extends object = object, + RAW extends object = object + >(): DataState { + let overrideParams: MergeDataStateParams = {}; + + if (this.componentStatus !== 'ready' || !Object.isTruly(this.dataProvider)) { + overrideParams = { + currentPage: 0, + ...overrideParams + }; } - return this.shouldPerformDataRequest(this.getVirtualScrollState(), this); + return this.getDataStateSnapshot(overrideParams, this.chunkRequest, this.chunkRender); } /** - * Resets the component state to its initial state + * Returns additional props to pass to an item component + * + * @param el + * @param i */ - protected reset(): void { - this.onReset(); + getItemAttrs(el: this['Item'], i: number): CanUndef { + const + {itemProps} = this; + + return Object.isFunction(itemProps) ? + itemProps(el, i, { + key: this.getItemKey(el, i), + ctx: this + }) : + itemProps; } /** - * This function asks the client whether rendering can be performed. - * It is called after successful data load or when the child component enters the visible area. - * The client responds with an object indicating whether rendering is allowed or the reason for denial. - * - * Based on the result of this function, the component takes appropriate actions. For example, - * it may load data if it is not sufficient for rendering, or perform rendering if all conditions are met. + * Returns a component name to render an item * - * @param state + * @param el + * @param i */ - protected renderGuard(state: VirtualScrollState): RenderGuardResult { - const - chunkSize = this.getChunkSize(state), - dataSlice = this.getNextDataSlice(state, chunkSize); - - if (dataSlice.length < chunkSize) { - if (state.areRequestsStopped && state.isLastRender) { - return { - result: false, - reason: renderGuardRejectionReason.done - }; - } - - return { - result: false, - reason: renderGuardRejectionReason.notEnoughData - }; - } - - if (state.isInitialRender) { - return { - result: true - }; - } - - const - clientResponse = this.shouldPerformDataRender?.(state, this) ?? true; + getItemComponentName(el: this['Item'], i: number): string { + const {item} = this; + return Object.isFunction(item) ? item(el, i) : item; + } - return { - result: clientResponse, - reason: !clientResponse ? renderGuardRejectionReason.noPermission : undefined - }; + /** {@link iItems.getItemKey} */ + getItemKey(el: this['Item'], i: number): CanUndef { + return iItems.getItemKey(this, el, i); } /** - * A function that performs actions (data loading/rendering) depending - * on the result of the {@link bVirtualScroll.renderGuard} method. + * Takes a snapshot of the current data state and returns it + * + * @param [overrideParams] + * @param [chunkRequest] + * @param [chunkRender] * - * This function is the "starting point" for rendering components and is called after successful data loading - * or when rendered items enter the viewport. + * @typeParam ITEM - data item to render + * @typeParam RAW - raw provider data */ - protected loadDataOrPerformRender(): void { - const - state = this.getVirtualScrollState(); + protected getDataStateSnapshot< + ITEM extends object = object, + RAW extends unknown = unknown + >( + overrideParams?: MergeDataStateParams, + chunkRequest?: ChunkRequest, + chunkRender?: ChunkRender + ): DataState { + return getRequestParams(chunkRequest, chunkRender, overrideParams); + } - if (state.isLastErrored) { + /** @emits `chunkLoaded(lastLoadedChunk:` [[LastLoadedChunk]]`)` */ + protected override initRemoteData(): void { + if (!this.db) { return; } + this.localState = 'init'; + const - {result, reason} = this.renderGuard(state); + {data, total} = this.db; - if (result) { - return this.performRender(); - } + if (data && data.length > 0) { + const lastLoadedChunk = { + normalized: data, + raw: this.chunkRequest.lastLoadedChunk.raw + }; - if (reason === renderGuardRejectionReason.done) { - this.onLifecycleDone(); - return; - } + const params = this.getDataStateSnapshot({ + data, + total, + lastLoadedData: data, + lastLoadedChunk + }); - if (reason === renderGuardRejectionReason.notEnoughData) { - if (state.areRequestsStopped) { - this.performRender(); - this.onLifecycleDone(); + this.chunkRequest.lastLoadedChunk = lastLoadedChunk; + this.chunkRequest.shouldStopRequest(params); + this.chunkRequest.data = data; + this.total = total; - } else if (this.shouldPerformDataRequestWrapper()) { - void this.initLoadNext(); + } else { + this.chunkRequest.isLastEmpty = true; - } else if (state.isInitialRender) { - this.performRender(); - } + const + params = this.getDataStateSnapshot({isLastEmpty: true}); + + this.chunkRequest.shouldStopRequest(params); } + + this.emit('chunkLoaded', this.chunkRequest.lastLoadedChunk); + this.chunkRequest.init().catch(stderr); + } + + protected override convertDataToDB(data: object): O | this['DB'] { + this.chunkRequest.lastLoadedChunk.raw = data; + return super.convertDataToDB(data); } /** - * Renders components using {@link bVirtualScroll.componentFactory} and inserts them into the DOM tree + * Initializes rendering on the items passed to the component */ - protected performRender(): void { - this.onRenderStart(); - - const - items = this.componentFactory.produceComponentItems(), - nodes = this.componentFactory.produceNodes(items), - mounted = this.componentFactory.produceMounted(items, nodes); + @hook('mounted') + @watch(['itemsStore']) + @wait('ready', {defer: true, label: $$.initOptions}) + protected initItems(): CanPromise { + if (this.dataProvider !== undefined) { + return; + } - if (mounted.length === 0) { - return this.onRenderDone(); + if (this.localState === 'ready') { + this.reInit(); } - this.observer.observe(mounted); - this.onDomInsertStart(mounted); + this.chunkRequest.lastLoadedChunk.normalized = Object.isArray(this.items) ? [...this.items] : []; + this.chunkRequest.init().catch(stderr); + } + /** + * Synchronization of the component props + */ + @wait('ready', {defer: true, label: $$.syncPropsWatcher}) + protected syncPropsWatcher(): CanPromise { + return this.reInit(); + } + + protected override syncDataProviderWatcher(initLoad?: boolean): void { const - fragment = document.createDocumentFragment(), - {renderPage} = this.getVirtualScrollState(), - asyncGroup = `${bVirtualScrollDomInsertAsyncGroup}:${renderPage}`; - - nodes.forEach((node) => { - this.dom.appendChild(fragment, node, { - group: asyncGroup, - destroyIfComponent: true - }); - }); + provider = this.dataProviderProp; - this.async.requestAnimationFrame(() => { - this.$refs.container.appendChild(fragment); + if (provider === undefined) { + this.reInit(); - this.onDomInsertDone(); - this.onRenderDone(); + } else { + super.syncDataProviderWatcher(initLoad); + } + } + + protected override onRequestError(err: Error | RequestError, retry: RetryRequestFn): void { + super.onRequestError(err, retry); + + if (isAsyncReplaceError(err)) { + return; + } - }, {label: $$.insertDomRaf, group: asyncGroup}); + this.localState = 'error'; } } diff --git a/src/components/base/b-virtual-scroll/b-virtual-scroll_theme_demo.styl b/src/components/base/b-virtual-scroll/b-virtual-scroll_theme_demo.styl new file mode 100644 index 0000000000..ce7c80bb2d --- /dev/null +++ b/src/components/base/b-virtual-scroll/b-virtual-scroll_theme_demo.styl @@ -0,0 +1,30 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +b-virtual-scroll_theme_demo extends b-virtual-scroll + /theme &__option-el + position relative + + display flex + justify-content center + align-items center + + size 200px + margin 20px + + background-color red + + &:after + content attr(data-index) + font-size 20px + color #FFF + + /theme &__skeleton + size 200px + margin 20px + background-color gray diff --git a/src/components/base/b-virtual-scroll/const.ts b/src/components/base/b-virtual-scroll/const.ts deleted file mode 100644 index eab83f3d36..0000000000 --- a/src/components/base/b-virtual-scroll/const.ts +++ /dev/null @@ -1,184 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; - -import type { ComponentItemType, VirtualScrollState, ItemsProcessors } from 'components/base/b-virtual-scroll/interface'; - -/** - * Base group for performing asynchronous operations of the component. - */ -export const bVirtualScrollAsyncGroup = 'b-virtual-scroll'; - -/** - * Group for asynchronous operations related to inserting nodes into the DOM tree. - */ -export const bVirtualScrollDomInsertAsyncGroup = `${bVirtualScrollAsyncGroup}:dom-insert`; - -/** - * Component modes. - */ -export const componentModes = { - /** - * In this mode, data is not loaded via a data provider, but instead passed in through the items prop. - */ - items: 'items', - - /** - * In this mode, data is loaded via a data provider. - */ - dataProvider: 'dataProvider' -}; - -/** - * Component data-related events (emitted in `selfEmitter`). - */ -export const componentDataLocalEvents = { - /** - * Data loading has started. - */ - dataLoadStart: 'dataLoadStart', - - /** - * An error occurred while loading data. - */ - dataLoadError: 'dataLoadError', - - /** - * Data has been successfully loaded. - */ - dataLoadSuccess: 'dataLoadSuccess', - - /** - * Successful load with no data. - */ - dataLoadEmpty: 'dataLoadEmpty' -}; - -/** - * Component events. - */ -export const componentLifecycleEvents = { - /** - * Reset component state. - */ - resetState: 'resetState', - - /** - * Trigger data conversion to the `DB`. - */ - convertDataToDB: 'convertDataToDB', - - /** - * This event is emitted when all component data is rendered and loaded. - */ - lifecycleDone: 'lifecycleDone' -}; - -/** - * Component rendering events. - */ -export const componentRenderLocalEvents = { - /** - * Rendering of items has started. - */ - renderStart: 'renderStart', - - /** - * Rendering of items has finished. - */ - renderDone: 'renderDone', - - /** - * Rendering of items has started with the render engine. - */ - renderEngineStart: 'renderEngineStart', - - /** - * Rendering of items has finished with the render engine. - */ - renderEngineDone: 'renderEngineDone', - - /** - * DOM node insertion has started. - */ - domInsertStart: 'domInsertStart', - - /** - * DOM node insertion has finished. - */ - domInsertDone: 'domInsertDone' -}; - -/** - * Events of the element observer. - */ -export const componentObserverLocalEvents = { - /** - * The element has entered the viewport. - */ - elementEnter: 'elementEnter' -}; - -export const componentEvents = { - ...componentDataLocalEvents, - ...componentRenderLocalEvents, - ...componentLifecycleEvents, - ...componentObserverLocalEvents -}; - -/** - * Reasons for rejecting a render operation. - */ -export const renderGuardRejectionReason = { - /** - * Insufficient data to perform a render (e.g., `data.length` is 5 and `chunkSize` is 12). - */ - notEnoughData: 'notEnoughData', - - /** - * All rendering operations have been completed. - */ - done: 'done', - - /** - * The client returns `false` in `shouldPerformDataRender`. - */ - noPermission: 'noPermission' -}; - -/** - * {@link ComponentItemType} - */ -export const componentItemType: ComponentItemType = { - item: 'item', - separator: 'separator' -}; - -export const defaultShouldProps = { - /** {@link bVirtualScroll.shouldStopRequestingData} */ - shouldStopRequestingData: (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { - const isLastRequestEmpty = () => state.lastLoadedData.length === 0; - return isLastRequestEmpty(); - }, - - /** {@link bVirtualScroll.shouldPerformDataRequest} */ - shouldPerformDataRequest: (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => { - const isLastRequestNotEmpty = () => state.lastLoadedData.length > 0; - return isLastRequestNotEmpty(); - }, - - /** {@link bVirtualScroll.shouldPerformDataRender} */ - shouldPerformDataRender: (state: VirtualScrollState, _ctx: bVirtualScroll): boolean => - state.isInitialRender || state.remainingItems === 0 -}; - -/** - * {@link bVirtualScroll.itemsProcessors} - */ -export const itemsProcessors: ItemsProcessors = {}; diff --git a/src/components/base/b-virtual-scroll/handlers.ts b/src/components/base/b-virtual-scroll/handlers.ts deleted file mode 100644 index 136e17a64e..0000000000 --- a/src/components/base/b-virtual-scroll/handlers.ts +++ /dev/null @@ -1,234 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import iVirtualScrollProps from 'components/base/b-virtual-scroll/props'; - -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { MountedChild } from 'components/base/b-virtual-scroll/interface'; - -import { bVirtualScrollAsyncGroup, componentEvents } from 'components/base/b-virtual-scroll/const'; -import { isAsyncReplaceError } from 'components/base/b-virtual-scroll/modules/helpers'; - -import iData, { component } from 'components/super/i-data/i-data'; - -/** - * A class that provides an API to handle events emitted by the {@link bVirtualScroll} component. - * This class is designed to work in conjunction with {@link bVirtualScroll}. - */ -@component() -export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { - /** - * Handler: component reset event. - * Resets the component state to its initial state. - */ - protected onReset(this: bVirtualScroll): void { - this.componentInternalState.reset(); - this.observer.reset(); - - this.async.clearAll({group: new RegExp(bVirtualScrollAsyncGroup)}); - - this.componentEmitter.emit(componentEvents.resetState); - } - - /** - * Handler: render start event. - * Triggered when the component rendering starts. - */ - protected onRenderStart(this: bVirtualScroll): void { - this.componentInternalState.updateIsLastRender(); - this.componentEmitter.emit(componentEvents.renderStart); - } - - /** - * Handler: render engine start event. - * Triggered when the component rendering using the rendering engine starts. - */ - protected onRenderEngineStart(this: bVirtualScroll): void { - this.componentEmitter.emit(componentEvents.renderEngineStart); - } - - /** - * Handler: render engine done event. - * Triggered when the component rendering using the rendering engine is completed. - */ - protected onRenderEngineDone(this: bVirtualScroll): void { - this.componentEmitter.emit(componentEvents.renderEngineDone); - } - - /** - * Handler: DOM insert start event. - * Triggered when the insertion of rendered components into the DOM tree starts. - * - * @param childList - */ - protected onDomInsertStart(this: bVirtualScroll, childList: MountedChild[]): void { - this.componentInternalState.updateDataOffset(); - this.componentInternalState.updateMounted(childList); - this.componentInternalState.setIsInitialRender(false); - this.componentInternalState.incrementRenderPage(); - - this.componentEmitter.emit(componentEvents.domInsertStart); - } - - /** - * Handler: DOM insert done event. - * Triggered when the insertion of rendered components into the DOM tree is completed. - */ - protected onDomInsertDone(this: bVirtualScroll): void { - this.componentEmitter.emit(componentEvents.domInsertDone); - } - - /** - * Handler: render done event. - * Triggered when rendering is completed. - */ - protected onRenderDone(this: bVirtualScroll): void { - this.componentEmitter.emit(componentEvents.renderDone); - } - - /** - * Handler: lifecycle done event. - * Triggered when the internal lifecycle of the component is completed. - */ - protected onLifecycleDone(this: bVirtualScroll): void { - const - state = this.getVirtualScrollState(); - - if (state.isLifecycleDone) { - return; - } - - this.slotsStateController.doneState(); - this.componentInternalState.setIsLifecycleDone(true); - this.componentEmitter.emit(componentEvents.lifecycleDone); - } - - /** - * Handler: convert data to database event. - * Triggered when the loaded data is converted. - * - * @param data - the converted data. - */ - protected onConvertDataToDB(this: bVirtualScroll, data: unknown): void { - this.componentInternalState.setRawLastLoaded(data); - this.componentEmitter.emit(componentEvents.convertDataToDB, data); - } - - /** - * Handler: data load start event. - * Triggered when data loading starts. - * - * @param isInitialLoading - indicates whether it is an initial component loading. - */ - protected onDataLoadStart(this: bVirtualScroll, isInitialLoading: boolean): void { - this.componentInternalState.setIsLoadingInProgress(true); - this.componentInternalState.setIsLastErrored(false); - this.slotsStateController.loadingProgressState(isInitialLoading); - - this.componentEmitter.emit(componentEvents.dataLoadStart, isInitialLoading); - } - - /** - * Handler: data load success event. - * Triggered when data loading is successfully completed. - * - * @param isInitialLoading - indicates whether it is an initial component loading. - * @param data - the loaded data. - * @throws {@link ReferenceError} if the loaded data does not have a "data" field. - */ - protected onDataLoadSuccess(this: bVirtualScroll, isInitialLoading: boolean, data: unknown): void { - this.componentInternalState.setIsLoadingInProgress(false); - - const - dataToProvide = Object.isPlainObject(data) ? data.data : data; - - if (!Array.isArray(dataToProvide)) { - throw new ReferenceError('Missing data to perform render'); - } - - this.componentInternalState.updateData(dataToProvide, isInitialLoading); - this.componentInternalState.incrementLoadPage(); - - const - isRequestsStopped = this.shouldStopRequestingDataWrapper(); - - this.componentEmitter.emit(componentEvents.dataLoadSuccess, dataToProvide, isInitialLoading); - - this.slotsStateController.loadingSuccessState(); - - if ( - isInitialLoading && - isRequestsStopped && - Object.size(dataToProvide) === 0 - ) { - this.onDataEmpty(); - this.onLifecycleDone(); - - } else { - this.loadDataOrPerformRender(); - } - } - - /** - * Handler: data load error event. - * Triggered when data loading fails. - * - * @param isInitialLoading - indicates whether it is an initial component loading. - */ - protected onDataLoadError(this: bVirtualScroll, isInitialLoading: boolean): void { - this.componentInternalState.setIsLoadingInProgress(false); - this.componentInternalState.setIsLastErrored(true); - this.slotsStateController.loadingFailedState(); - - this.componentEmitter.emit(componentEvents.dataLoadError, isInitialLoading); - } - - protected override onRequestError(this: bVirtualScroll, ...args: Parameters): ReturnType { - const - err = args[0]; - - if (isAsyncReplaceError(err)) { - return; - } - - const - state = this.getVirtualScrollState(); - - this.onDataLoadError(state.isInitialLoading); - return super.onRequestError(err, this.initLoad.bind(this)); - } - - /** - * Handler: data empty event. - * Triggered when the loaded data is empty. - */ - protected onDataEmpty(this: bVirtualScroll): void { - this.slotsStateController.emptyState(); - - this.componentEmitter.emit(componentEvents.dataLoadEmpty); - } - - /** - * Handler: component enters the viewport - * @param component - the component that enters the viewport. - */ - protected onElementEnters(this: bVirtualScroll, component: MountedChild): void { - this.componentInternalState.setMaxViewedIndex(component); - this.loadDataOrPerformRender(); - - this.componentEmitter.emit(componentEvents.elementEnter, component); - } - - /** - * Handler: items to render was updated - * @param items - */ - protected onItemsInit(this: bVirtualScroll, items: Exclude): void { - this.onDataLoadSuccess(true, items); - } -} diff --git a/src/components/base/b-virtual-scroll/interface.ts b/src/components/base/b-virtual-scroll/interface.ts new file mode 100644 index 0000000000..03498a508f --- /dev/null +++ b/src/components/base/b-virtual-scroll/interface.ts @@ -0,0 +1,222 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type { UnsafeIData } from 'components/super/i-data/i-data'; + +export interface RequestQueryFn { + (params: DataState): Dictionary; +} +export interface RequestFn { + (params: DataState): boolean; +} + +export interface GetData { + (ctx: bVirtualScroll, query: CanUndef): Promise; +} + +export interface VirtualItemEl { + /** + * Current render data + */ + current: T; + + /** + * Previous render data + */ + prev: CanUndef; + + /** + * Next render data + */ + next: CanUndef; +} + +/** + * @deprecated + * {@link VirtualItemEl} + */ +export type OptionEl = VirtualItemEl; + +/** + * @typeParam ITEM - data item to render + * @typeParam RAW - raw provider data + */ +export interface DataState { + /** + * Number of the last loaded page + */ + currentPage: number; + + /** + * Number of a page to upload + */ + nextPage: number; + + /** + * All loaded data + */ + data: object[]; + + /** + * Number of items to show till the page bottom is reached + */ + itemsTillBottom: number; + + /** + * Items to render + */ + items: Array>; + + /** + * Data that pending to be rendered + */ + pendingData: object[]; + + /** + * True if the last requested data response was empty + */ + isLastEmpty: boolean; + + /** + * Last loaded data chunk + */ + lastLoadedChunk: { + /** + * Normalized data (processed with `dbConverter`) + */ + normalized: ITEM[]; + + /** + * Raw provider data + */ + raw: CanUndef; + }; + + /** + * @deprecated + * {@link DataState.lastLoadedChunk} + */ + lastLoadedData: ITEM[]; + + /** + * `total` property from the loaded data + */ + total: CanUndef; +} + +export interface RemoteData extends Dictionary { + /** + * Data to render components + */ + data?: object[]; + + /** + * Total number of elements + */ + total?: number; +} + +export interface RenderItem { + /** + * Component data + */ + data: T; + + /** + * Component DOM element + */ + node: CanUndef; + + /** + * Component destructor + */ + destructor: CanUndef; + + /** + * Component position in a DOM tree + */ + index: number; +} + +/** + * Attributes of items to render + */ +export type ItemAttrs = Dictionary; + +/** + * Last loaded data chunk + * + * @typeParam DATA - data to render + * @typeParam RAW - raw provider data + */ +export interface LastLoadedChunk { + normalized: DATA; + raw: CanUndef; +} + +export interface DataToRender { + itemAttrs: Dictionary; + itemParams: VirtualItemEl; + index: number; +} + +/** + * Local state of a component: + * + * * `error` - indicates the component loading error appear + * * `init` - indicates the component now loading the first chunk of data + * * `ready` - indicates the component now is ready to render data + */ +export type LocalState = 'init' | 'ready' | 'error'; + +/** + * The loading strategy: + * + * * `scroll` - will prompt the client to load data every time a new element appears in the viewport + * * `manual` - there is only one way to load data: by using `renderNext` method (except the initial load) + */ +export type LoadStrategy = 'scroll' | 'manual'; + +/** + * Display state of the ref + */ +export type RefDisplayState = '' | 'none'; + +/** + * `bVirtualScroll` `$refs` + */ +export type bVirtualScrollRefs = bVirtualScroll['$refs']; + +// @ts-ignore (unsafe) +export interface UnsafeBVirtualScroll extends UnsafeIData { + // @ts-ignore (access) + total: CTX['total']; + + // @ts-ignore (access) + localState: CTX['localState']; + + // @ts-ignore (access) + chunkRender: CTX['chunkRender']; + + // @ts-ignore (access) + chunkRequest: CTX['chunkRequest']; + + // @ts-ignore (access) + componentRender: CTX['componentRender']; + + // @ts-ignore (access) + getDataStateSnapshot: CTX['getDataStateSnapshot']; + + // @ts-ignore (access) + onRequestError: CTX['onRequestError']; +} + +export type MergeDataStateParams = { + [key in keyof DataState]?: DataState[key]; +}; diff --git a/src/components/base/b-virtual-scroll/interface/common.ts b/src/components/base/b-virtual-scroll/interface/common.ts deleted file mode 100644 index d2377e09f7..0000000000 --- a/src/components/base/b-virtual-scroll/interface/common.ts +++ /dev/null @@ -1,81 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; - -import type { renderGuardRejectionReason } from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { VirtualScrollState } from 'components/base/b-virtual-scroll/interface/component'; - -import type { UnsafeIData } from 'components/super/i-data/i-data'; - -/** - * Interface representing the response of the client to the `renderGuard` method for rendering requests. - * - * To grant permission for rendering, the response object should have the following structure: - * - * ```typescript - * const canPerform: RenderGuardResult = { - * result: true - * } - * ``` - * - * To deny rendering, the response object should have the following structure: - * - * ```typescript - * const canPerform: RenderGuardResult = { - * result: false, - * reason: 'notEnoughData' - * } - * ``` - * - * Based on the result of this function, the component takes appropriate actions. For example, - * it may load data if it is not sufficient for rendering, or perform rendering if all conditions are met. - */ -export interface RenderGuardResult { - /** - * If `true`, rendering is permitted; if `false`, rendering is denied. - */ - result: boolean; - - /** - * The reason for rejecting the rendering request. - */ - reason?: keyof RenderGuardRejectionReason; -} - -/** - * {@link renderGuardRejectionReason} - */ -export type RenderGuardRejectionReason = typeof renderGuardRejectionReason; - -/** - * A function used to query the client about whether to perform a specific action or not. - */ -export interface ShouldPerform { - (state: VirtualScrollState, ctx: bVirtualScroll): RES; -} - -// @ts-ignore (extend) -export interface UnsafeBVirtualScroll extends UnsafeIData { - // @ts-ignore (access) - onRenderEngineStart: CTX['onRenderEngineStart']; - // @ts-ignore (access) - onRenderEngineDone: CTX['onRenderEngineDone']; - // @ts-ignore (access) - onElementEnters: CTX['onElementEnters']; - // @ts-ignore (access) - componentEmitter: CTX['componentEmitter']; - // @ts-ignore (access) - slotsStateController: CTX['slotsStateController']; - // @ts-ignore (access) - componentInternalState: CTX['componentInternalState']; - // @ts-ignore (access) - componentFactory: CTX['componentFactory']; - // @ts-ignore (access) - observer: CTX['observer']; -} diff --git a/src/components/base/b-virtual-scroll/interface/component.ts b/src/components/base/b-virtual-scroll/interface/component.ts deleted file mode 100644 index 7f048074b4..0000000000 --- a/src/components/base/b-virtual-scroll/interface/component.ts +++ /dev/null @@ -1,336 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { componentModes } from 'components/base/b-virtual-scroll/b-virtual-scroll'; - -/** - * State of the current component lifecycle. - * - * @typeParam DATA - Instance of the data element. - * @typeParam RAW_DATA - the data loaded from the server but not yet processed. - * This type parameter determines the type of the {@link VirtualScrollState.lastLoadedRawData} property - */ -export interface VirtualScrollState { - /** - * The largest component index of type `item` that appeared in the viewport. - */ - maxViewedItem: CanUndef; - - /** - * The largest component index of any type that appeared in the viewport. - */ - maxViewedChild: CanUndef; - - /** - * The number of components of type `item` that have not yet been visible to the user. - */ - remainingItems: CanUndef; - - /** - * The number of components of any type that have not yet been visible to the user. - */ - remainingChildren: CanUndef; - - /** - * The current page number for loading data. - * It changes after each successful data load. - */ - loadPage: number; - - /** - * The current page number for rendering data. - * It changes after each successful rendering. - */ - renderPage: number; - - /** - * Indicates if the last loaded data is empty. - */ - isLastEmpty: boolean; - - /** - * Indicates if the last data load ended with an error. - */ - isLastErrored: boolean; - - /** - * Indicates if the component is in the initial loading state. - */ - isInitialLoading: boolean; - - /** - * Indicates if the component is in the initial rendering state. - */ - isInitialRender: boolean; - - /** - * Indicates if the component has stopped making requests. - */ - areRequestsStopped: boolean; - - /** - * Indicates if there is an ongoing loading process. - */ - isLoadingInProgress: boolean; - - /** - * Indicates if the component's lifecycle is done, i.e., all data is rendered and loaded. - */ - isLifecycleDone: boolean; - - /** - * Indicates whether the current render process is the last one in the current lifecycle. - * - * The isLastRender flag is set to true after a request, - * when the client notifies the component that it has finished loading all its data - * ({@link VirtualScrollState.areRequestsStopped} is set to true) and there is either no data left to render - * or there is less than {@link VirtualScrollState.chunkSize} remaining to render. - * When these conditions are met, the isLastRender flag will be set to true. - */ - isLastRender: boolean; - - /** - * The last loaded data. - */ - lastLoadedData: Readonly; - - /** - * The component data. - */ - data: Readonly; - - /** - * List of all components of type `item` that have been rendered. - */ - items: Readonly; - - /** - * List of all components that have been rendered. - */ - childList: Readonly; - - /** - * The last loaded raw data. - */ - lastLoadedRawData: CanUndef; -} - -/** - * Private (not accessible to the client) component state. - * - * This state stores all the internal component state that should not be - * accessible to the client. - */ -export interface PrivateComponentState { - /** - * Pointer to the index of the data element that was last rendered. - */ - dataOffset: number; -} - -/** - * {@link componentModes} - */ -export type ComponentModes = typeof componentModes; - -/** - * {@link ComponentModes} - */ -export type ComponentMode = keyof ComponentModes; - -/** - * Types of rendered components. - */ -export interface ComponentItemType { - /** - * This type indicates that the component is the "main" component to render. - * - * For example, in the {@link VirtualScrollState} interface, you can notice that - * there are specific fields for the `item` type, such as `remainingItems`. - * - * Components with this type are stored both in the `items` array and the `childList` array in - * {@link VirtualScrollState}. - */ - item: 'item'; - - /** - * This type indicates that the component is "secondary". - * - * Components with this type are stored in the `childList` array in {@link VirtualScrollState}. - */ - separator: 'separator'; -} - -/** - * Abstract representation of a component to be rendered. - * - * To render a `b-button` component with the default slot, the following set of parameters needs to be passed: - * - * @example - * ```typescript - * const bButton = { - * type: 'item', - * item: 'b-button', - * props: { - * id: 'button' - * }, - * key: 'unique id', - * children: { - * default: 'Hello world' - * } - * } - * ``` - */ -export interface ComponentItem { - /** - * The type of the component (item or separator). - */ - type: keyof ComponentItemType; - - /** - * The name of the component, e.g., `b-button` or `section`. - */ - item: string; - - /** - * The component's properties. - */ - props?: Dictionary; - - /** - * Unique key for this component (data set). - */ - key: string; - - /** - * Children nodes of the component. - */ - children?: VNodeChildren; - - /** - * {@link ComponentItemMeta} - */ - meta?: ComponentItemMeta; -} - -/** - * Meta information for a component that will not be used during rendering, - * but will be available for reading/changing in `itemsProcessors`. - */ -export interface ComponentItemMeta extends Dictionary { - /** - * A conditionally reserved property that contains the data based - * on which this abstract representation of the component was created. - * - * If `iItems` props are used to create representations, `b-virtual-scroll` will automatically add - * this property to the `meta` parameters. - */ - readonly data?: unknown; -} - -/** - * Represents any mounted component (item or separator) within the DOM tree. - */ -export interface MountedChild extends ComponentItem { - /** - * The DOM node associated with the component. - */ - node: HTMLElement; - - /** - * The index of the component within the list of children. - */ - childIndex: number; -} - -/** - * Represents a mounted item component within the DOM tree. - */ -export interface MountedItem extends MountedChild { - /** - * The index of the item within the list of items. - */ - itemIndex: number; -} - -/** - * Represents the nodes of a component. - */ -export interface ComponentRefs { - /** - * The container element in which components are rendered. - */ - container: HTMLElement; - - /** - * The slot that is displayed while data is being loaded. - */ - loader?: HTMLElement; - - /** - * The slot that is displayed for tombstones. - */ - tombstones?: HTMLElement; - - /** - * The slot that is displayed when data loading is complete and there is no data. - */ - empty?: HTMLElement; - - /** - * The slot that is displayed when a data loading error occurs. - */ - retry?: HTMLElement; - - /** - * The slot that is displayed when all data is loaded and rendered. - */ - done?: HTMLElement; - - /** - * The slot that is displayed when there is no active loading. - */ - renderNext?: HTMLElement; -} - -export type $ComponentRefs = ComponentRefs & Dictionary; - -/** - * The type of data stored by the component. - */ -export interface ComponentDb { - /** - * The component data. - */ - data: unknown[]; - - /** - * The total number of data items. - */ - total?: number; -} - -/** - * Typeof {@link bVirtualScroll.itemsFactory}. - */ -export interface ComponentItemFactory { - (state: VirtualScrollState, ctx: bVirtualScroll): ComponentItem[]; -} - -/** - * A middleware function used to modify elements compiled within {@link bVirtualScroll.itemsFactory}. - */ -export interface ItemsProcessor { - (componentItems: ComponentItem[], ctx: bVirtualScroll): ComponentItem[]; -} - -/** - * Type for {@link bVirtualScroll.itemsProcessors}. - */ -export type ItemsProcessors = ItemsProcessor | Record | ItemsProcessor[]; diff --git a/src/components/base/b-virtual-scroll/interface/events.ts b/src/components/base/b-virtual-scroll/interface/events.ts deleted file mode 100644 index 719024069a..0000000000 --- a/src/components/base/b-virtual-scroll/interface/events.ts +++ /dev/null @@ -1,76 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { MountedChild } from 'components/base/b-virtual-scroll/interface/component'; - -import { - - componentDataLocalEvents, - componentLifecycleEvents, - componentObserverLocalEvents, - componentRenderLocalEvents - -} from 'components/base/b-virtual-scroll/const'; - -/** - * {@link componentDataLocalEvents} - */ -export type ComponentDataLocalEvents = typeof componentDataLocalEvents; - -/** - * {@link componentLifecycleEvents} - */ -export type ComponentLifecycleEvents = typeof componentLifecycleEvents; - -/** - * {@link componentRenderLocalEvents} - */ -export type ComponentRenderLocalEvents = typeof componentRenderLocalEvents; - -/** - * {@link componentObserverLocalEvents} - */ -export type ComponentObserverLocalEvents = typeof componentObserverLocalEvents; - -/** - * Possible component events. - */ -export type ComponentEvents = - keyof ComponentDataLocalEvents | - keyof ComponentLifecycleEvents | - keyof ComponentRenderLocalEvents | - keyof ComponentObserverLocalEvents; - -/** - * Mapping of event names and their event arguments. - * [Event Name: Event Arguments] - */ -export interface LocalEventPayloadMap { - [componentDataLocalEvents.dataLoadSuccess]: [data: object[], isInitialLoading: boolean]; - [componentDataLocalEvents.dataLoadStart]: [isInitialLoading: boolean]; - [componentDataLocalEvents.dataLoadError]: [isInitialLoading: boolean]; - [componentDataLocalEvents.dataLoadEmpty]: []; - - [componentLifecycleEvents.resetState]: []; - [componentLifecycleEvents.lifecycleDone]: []; - [componentLifecycleEvents.convertDataToDB]: [data: unknown]; - - [componentObserverLocalEvents.elementEnter]: [componentItem: MountedChild]; - - [componentRenderLocalEvents.renderStart]: []; - [componentRenderLocalEvents.renderDone]: []; - [componentRenderLocalEvents.renderEngineStart]: []; - [componentRenderLocalEvents.renderEngineDone]: []; - [componentRenderLocalEvents.domInsertStart]: []; - [componentRenderLocalEvents.domInsertDone]: []; -} - -/** - * Returns the type of event arguments. - */ -export type LocalEventPayload = LocalEventPayloadMap[T]; diff --git a/src/components/base/b-virtual-scroll/interface/index.ts b/src/components/base/b-virtual-scroll/interface/index.ts deleted file mode 100644 index 9ea53371bd..0000000000 --- a/src/components/base/b-virtual-scroll/interface/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -export * from 'components/base/b-virtual-scroll/interface/events'; -export * from 'components/base/b-virtual-scroll/interface/component'; -export * from 'components/base/b-virtual-scroll/interface/requests'; -export * from 'components/base/b-virtual-scroll/interface/common'; diff --git a/src/components/base/b-virtual-scroll/interface/requests.ts b/src/components/base/b-virtual-scroll/interface/requests.ts deleted file mode 100644 index 4d39dd224f..0000000000 --- a/src/components/base/b-virtual-scroll/interface/requests.ts +++ /dev/null @@ -1,27 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { CreateRequestOptions, RequestQuery } from 'components/super/i-data/i-data'; -import type { VirtualScrollState } from 'components/base/b-virtual-scroll/interface/component'; - -/** - * Function that returns the GET parameters for a request. - */ -export interface RequestQueryFn { - /** - * Returns the GET parameters for a request. - * - * @param state - the component state. - */ - (state: VirtualScrollState): Dictionary; -} - -/** - * Requests parameters. - */ -export type VirtualScrollRequestParams = [RequestQuery, CreateRequestOptions]; diff --git a/src/components/base/b-virtual-scroll/modules/chunk-render.ts b/src/components/base/b-virtual-scroll/modules/chunk-render.ts new file mode 100644 index 0000000000..934697009a --- /dev/null +++ b/src/components/base/b-virtual-scroll/modules/chunk-render.ts @@ -0,0 +1,403 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import symbolGenerator from 'core/symbol'; + +import type { WatchOptions } from 'core/dom/intersection-watcher'; + +import Friend from 'components/friends/friend'; +import DOM, { watchForIntersection, appendChild } from 'components/friends/dom'; + +import type iBlock from 'components/super/i-block/i-block'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; + +import type ComponentRender from 'components/base/b-virtual-scroll/modules/component-render'; +import type ChunkRequest from 'components/base/b-virtual-scroll/modules/chunk-request'; + +import type { RenderItem, VirtualItemEl } from 'components/base/b-virtual-scroll/interface'; + +DOM.addToPrototype({appendChild, watchForIntersection}); + +const + $$ = symbolGenerator(); + +export default class ChunkRender extends Friend { + override readonly C!: bVirtualScroll; + + /** + * Render items + */ + items: RenderItem[] = []; + + /** + * Index of the last element that intersects the viewport + */ + lastIntersectsItem: number = 0; + + /** + * Chunk number of the current render + */ + chunk: number = 0; + + /** + * Last rendered range + */ + lastRenderRange: number[] = [0, 0]; + + /** + * Async group + */ + readonly asyncGroup: string = 'scroll-render:'; + + /** + * Number of items + */ + get itemsCount(): number { + return this.items.length; + } + + /** + * Async in-view label prefix + */ + protected readonly asyncInViewPrefix: string = 'in-view:'; + + /** + * Refs state update map + */ + protected refsUpdateMap: Map = new Map(); + + /** + * API for dynamic component rendering + */ + protected get componentRender(): ComponentRender { + return this.ctx.componentRender; + } + + /** + * API for scroll data requests + */ + protected get chunkRequest(): ChunkRequest { + return this.ctx.chunkRequest; + } + + /** + * Returns a random threshold number + */ + protected get randomThreshold(): number { + return Math.floor((Math.random() * (0.06 - 0.01) + 0.01) * 100) / 100; + } + + constructor(component: iBlock) { + super(component); + this.component.on('hook:mounted', this.initEventHandlers.bind(this)); + } + + /** + * Re-initializes the rendering process + */ + reInit(): void { + this.lastIntersectsItem = 0; + this.lastRenderRange = [0, 0]; + this.chunk = 0; + this.items = []; + this.refsUpdateMap = new Map(); + + this.async.clearAll({group: new RegExp(this.asyncGroup)}); + + this.setLoadersVisibility(true, true); + this.setRefVisibility('retry', false, true); + this.setRefVisibility('done', false, true); + this.setRefVisibility('empty', false, true); + this.setRefVisibility('renderNext', false, true); + + this.initEventHandlers(); + } + + /** + * Initializes render items + * @param data + */ + initItems(data: unknown[]): void { + this.items = this.items.concat(data.map(this.createRenderItem.bind(this))); + } + + /** + * Renders the component content + * + * @emits `chunkRender:renderStart(chunkNumber: number)` + * @emits `chunkRender:renderComplete(chunkNumber: number)` + * @emits `chunkRender:beforeMount(chunkNumber: number)` + * @emits `chunkRender:mounted(renderItems:` [[RenderItem]]`[], chunkNumber: number)` + */ + render(): void { + if (this.ctx.localState !== 'ready') { + return; + } + + const + {ctx, chunk, items} = this; + + const + renderFrom = (chunk - 1) * ctx.chunkSize, + renderTo = chunk * ctx.chunkSize, + renderItems = items.slice(renderFrom, renderTo); + + if ( + renderFrom === this.lastRenderRange[0] && + renderTo === this.lastRenderRange[1] || + renderItems.length === 0 + ) { + return; + } + + const + currentChunk = this.chunk; + + this.chunk++; + this.lastRenderRange = [renderFrom, renderTo]; + + ctx.emit('chunkRender:renderStart', currentChunk); + + const + nodes = this.renderItems(renderItems); + + ctx.emit('chunkRender:renderComplete', currentChunk); + ctx.emit('chunkRender:beforeMount', currentChunk); + + if (nodes.length === 0) { + return; + } + + const + fragment = document.createDocumentFragment(); + + for (let i = 0; i < nodes.length; i++) { + this.dom.appendChild(fragment, nodes[i], { + group: this.asyncGroup, + destroyIfComponent: true + }); + } + + this.async.requestAnimationFrame(() => { + this.refs.container.appendChild(fragment); + ctx.emit('chunkRender:mounted', renderItems, currentChunk); + }, {group: this.asyncGroup}); + } + + /** + * Hides or shows the specified ref + * + * @param ref + * @param show + * @param [immediate] - if settled as `true` will immediately update a DOM tree + */ + setRefVisibility(ref: keyof bVirtualScroll['$refs'], show: boolean, immediate: boolean = false): void { + const + refEl = >this.refs[ref]; + + if (!refEl) { + return; + } + + if (immediate) { + refEl.style.display = show ? '' : 'none'; + return; + } + + this.refsUpdateMap.set(ref, show); + this.performRefsVisibilityUpdate(); + } + + /** + * Hides or shows refs of the loader and tombstones + * + * @param show + * @param [immediate] - if settled as `true` will immediately update a DOM tree + */ + setLoadersVisibility(show: boolean, immediate: boolean = false): void { + this.setRefVisibility('tombstones', show, immediate); + this.setRefVisibility('loader', show, immediate); + } + + /** + * Tries to show the `renderNext` slot + */ + tryShowRenderNextSlot(): void { + const + {ctx, chunkRequest} = this; + + if (ctx.dataProvider == null && ctx.items.length === 0) { + return; + } + + if (chunkRequest.isDone) { + return; + } + + this.setRefVisibility('renderNext', true); + } + + /** + * Updates visibility of refs by using `requestAnimationFrame` + */ + protected performRefsVisibilityUpdate(): void { + this.async.requestAnimationFrame(() => { + this.refsUpdateMap.forEach((show, ref) => { + const + state = show ? '' : 'none', + refEl = >this.refs[ref]; + + if (!refEl) { + return; + } + + refEl.style.display = state; + }); + + this.refsUpdateMap.clear(); + + }, {label: $$.updateRefsVisibility, group: this.asyncGroup, join: true}); + } + + /** + * Event handlers initialization + */ + protected initEventHandlers(): void { + this.ctx.localEmitter.once('localState.ready', this.onReady.bind(this), {label: $$.reInitReady}); + this.ctx.localEmitter.once('localState.error', this.onError.bind(this), {label: $$.reInitError}); + } + + /** + * Renders the specified items + * @param items + */ + protected renderItems(items: RenderItem[]): HTMLElement[] { + const + nodes = this.componentRender.render(items); + + for (let i = 0; i < nodes.length; i++) { + const + node = nodes[i], + item = items[i]; + + item.node = node; + + const itemsData = { + current: item.data, + prev: items[i - 1]?.data, + next: items[i + 1]?.data + }; + + if (!Object.isFunction(node[$$.inView])) { + this.wrapInView(item, itemsData); + } + } + + return nodes; + } + + /** + * Wraps the specified item node with the `in-view` directive + * + * @param item + * @param itemData + */ + protected wrapInView(item: RenderItem, itemData: VirtualItemEl): void { + const + {ctx} = this, + {node} = item; + + if (ctx.loadStrategy === 'manual') { + return; + } + + const + label = `${this.asyncGroup}:${this.asyncInViewPrefix}${ctx.getItemKey(itemData, item.index)}`; + + if (!node) { + return; + } + + const inViewOpts = { + ...this.getInViewOptions(), + group: this.asyncGroup, + label + }; + + this.dom.watchForIntersection(node, inViewOpts, () => this.onNodeIntersect(item.index)); + } + + /** + * Returns a render item by the specified parameters + * + * @param data - data to render in item + * @param index - index of the item + */ + protected createRenderItem(data: object, index: number): RenderItem { + return { + data, + index: this.itemsCount + index, + node: undefined, + destructor: undefined + }; + } + + /** + * Returns options to initialize the `in-view` directive + */ + protected getInViewOptions(): WatchOptions { + return { + delay: 0, + threshold: this.randomThreshold, + once: !this.ctx.clearNodes + }; + } + + /** + * Handler: element becomes visible in the viewport + * @param index + */ + protected onNodeIntersect(index: number): void { + const + {ctx, items, lastIntersectsItem} = this, + {chunkSize, renderGap} = ctx; + + const + currentRender = (this.chunk - 1) * chunkSize; + + this.lastIntersectsItem = index; + + if (index + renderGap + chunkSize >= items.length) { + this.chunkRequest.try().catch(stderr); + } + + if (index >= lastIntersectsItem) { + if (currentRender - index <= renderGap) { + this.render(); + } + } + } + + /** + * Handler: component ready + */ + protected onReady(): void { + this.setLoadersVisibility(false); + this.chunk++; + this.render(); + } + + /** + * Handler: error occurred + */ + protected onError(): void { + this.setLoadersVisibility(false); + this.setRefVisibility('renderNext', false); + this.setRefVisibility('retry', true); + } +} diff --git a/src/components/base/b-virtual-scroll/modules/chunk-request.ts b/src/components/base/b-virtual-scroll/modules/chunk-request.ts new file mode 100644 index 0000000000..49d332b696 --- /dev/null +++ b/src/components/base/b-virtual-scroll/modules/chunk-request.ts @@ -0,0 +1,418 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import symbolGenerator from 'core/symbol'; +import Friend from 'components/friends/friend'; + +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; +import type ChunkRender from 'components/base/b-virtual-scroll/modules/chunk-render'; + +import { isAsyncClearError } from 'components/base/b-virtual-scroll/modules/helpers'; +import type { RemoteData, DataState, LastLoadedChunk } from 'components/base/b-virtual-scroll/interface'; + +const + $$ = symbolGenerator(); + +export default class ChunkRequest extends Friend { + override readonly C!: bVirtualScroll; + + /** + * Current page + */ + page: number = 1; + + /** + * Total amount of elements being loaded + */ + total: number = 0; + + /** + * All loaded data + */ + data: unknown[] = []; + + /** + * Last loaded data chunk that was processed with `dbConverter` + * + * @deprecated + * {@link ChunkRequest.lastLoadedChunk} + */ + lastLoadedData: unknown[] = []; + + /** + * Last loaded data chunk + */ + lastLoadedChunk: LastLoadedChunk = { + normalized: [], + raw: undefined + }; + + /** + * True if all requests for additional data has been requested + */ + isDone: boolean = false; + + /** + * True if the last request returned an empty array or undefined + */ + isLastEmpty: boolean = false; + + /** + * Contains data that pending to be rendered + */ + pendingData: object[] = []; + + /** + * A buffer to accumulate data from the main request and all additional requests. + * Sometimes a data provider can't provide the whole batch of data in one request, + * so you need to emit some extra requests till the data batch is filled. + */ + currentAccumulatedData: CanUndef = undefined; + + /** + * Contains `currentAccumulatedData` from previous requests cycle + */ + previousDataStore: CanUndef = undefined; + + /** {@link ChunkRequest.previousDataStore} */ + get previousData(): CanUndef { + return this.previousDataStore; + } + + /** + * @emits dataChange(v: unknown) + * {@link ChunkRequest.previousDataStore} + */ + set previousData(v: unknown) { + this.previousDataStore = v; + this.ctx.emit('dataChange', v); + } + + /** + * API for scroll rendering + */ + protected get chunkRender(): ChunkRender { + return this.ctx.chunkRender; + } + + /** + * Resets the current state + */ + reset(): void { + this.total = 0; + this.page = 1; + + this.lastLoadedData = []; + this.data = []; + this.lastLoadedChunk = {raw: undefined, normalized: []}; + this.pendingData = []; + + this.isDone = false; + this.isLastEmpty = false; + this.currentAccumulatedData = undefined; + this.previousDataStore = undefined; + + this.async.clearTimeout({label: 'chunkRequest.waitForInitCalls'}); + this.async.cancelRequest({label: $$.request}); + } + + /** + * Reloads the last request + */ + reloadLast(): void { + this.isDone = false; + this.isLastEmpty = false; + + this.chunkRender.setRefVisibility('retry', false); + this.try().catch(stderr); + } + + /** + * Initializes the request module + */ + async init(): Promise { + await this.async.sleep(15, {label: 'chunkRequest.waitForInitCalls'}); + + const + {chunkSize, dataProvider} = this.ctx; + + this.pendingData = [...this.lastLoadedChunk.normalized]; + + if (this.pendingData.length < chunkSize && dataProvider != null && !this.isDone) { + this.currentAccumulatedData = this.ctx.db?.data; + } + + await this.try(false); + + if ( + this.ctx.localState !== 'error' && + this.pendingData.length === 0 && + this.chunkRender.itemsCount === 0 && + this.isDone + ) { + this.chunkRender.setRefVisibility('empty', true); + } + + this.chunkRender.tryShowRenderNextSlot(); + + if (this.previousData === undefined && Array.isArray(this.ctx.db?.data)) { + this.previousData = this.ctx.db!.data; + } + + this.ctx.localState = 'ready'; + } + + /** + * Tries to request additional data + * + * @param [initialCall] + * + * @emits `dbChange(data:` [[RemoteData]]`)` + * @emits `chunkLoading(page: number)` + */ + try(initialCall: boolean = true): Promise> { + const + {ctx, chunkRender} = this, + {chunkSize, dataProvider} = ctx; + + const + resolved = Promise.resolve(undefined); + + const additionParams = { + lastLoadedChunk: { + ...this.lastLoadedChunk, + normalized: this.lastLoadedChunk.normalized + } + }; + + if (this.pendingData.length > 0) { + if (dataProvider == null) { + chunkRender.initItems(this.pendingData.splice(0, chunkSize)); + chunkRender.render(); + + if (this.pendingData.length === 0) { + this.emitDone(); + } + + return resolved; + } + + if (this.pendingData.length >= chunkSize) { + chunkRender.initItems(this.pendingData.splice(0, chunkSize)); + chunkRender.render(); + + if (this.isDone && this.pendingData.length === 0) { + this.emitDone(); + } + + return resolved; + } + } + + const updateCurrentData = () => { + if (this.currentAccumulatedData != null) { + this.previousData = this.currentAccumulatedData; + this.currentAccumulatedData = undefined; + } + }; + + const shouldRequest = ctx.loadStrategy === 'scroll' ? + ctx.shouldMakeRequest(ctx.getDataStateSnapshot(additionParams, this, chunkRender)) : + true; + + if (this.isDone) { + updateCurrentData(); + this.onRequestsDone(); + return resolved; + } + + const cantRequest = () => this.isDone || + !shouldRequest || + ctx.dataProvider == null || + ctx.mods.progress === 'true'; + + if (cantRequest()) { + return resolved; + } + + if (initialCall) { + this.currentAccumulatedData = undefined; + } + + chunkRender.setLoadersVisibility(true); + chunkRender.setRefVisibility('renderNext', false); + + ctx.emit('chunkLoading', this.page); + + return this.load() + .then((v) => { + if (Object.size(v?.data) === 0) { + this.isLastEmpty = true; + + this.shouldStopRequest(this.ctx.getDataStateSnapshot({ + lastLoadedData: [], + lastLoadedChunk: { + raw: undefined, + normalized: [] + } + }, this, chunkRender)); + + chunkRender.setLoadersVisibility(false); + updateCurrentData(); + + return; + } + + const + data = (v).data!; + + this.page++; + this.isLastEmpty = false; + + this.data = this.data.concat(data); + this.pendingData = this.pendingData.concat(data); + this.currentAccumulatedData = Array.concat(this.currentAccumulatedData ?? [], data); + + ctx.emit('dbChange', {...v, data: this.data}); + this.shouldStopRequest(this.ctx.getCurrentDataState()); + + if (this.pendingData.length < ctx.chunkSize) { + return this.try(false); + } + + this.previousData = this.currentAccumulatedData; + this.currentAccumulatedData = undefined; + + chunkRender.setLoadersVisibility(false); + + if (!this.isDone) { + chunkRender.initItems(this.pendingData.splice(0, chunkSize)); + chunkRender.render(); + } + + if (!this.isDone || this.pendingData.length > 0) { + chunkRender.setRefVisibility('renderNext', true); + } + + }).catch((err) => { + if (isAsyncClearError(err)) { + return; + } + + stderr(err); + return undefined; + }); + } + + /** + * Checks for the possibility of stopping data requests + * @param params + */ + shouldStopRequest(params: DataState): boolean { + const {ctx} = this; + this.isDone = ctx.shouldStopRequest(params); + + if (this.isDone) { + this.onRequestsDone(); + } + + return this.isDone; + } + + /** + * Sets `isDone` to `true` and fires `onRequestDone` handler + */ + protected emitDone(): void { + this.isDone = true; + this.onRequestsDone(); + } + + /** + * Loads additional data + * @emits `chunkLoaded(lastLoadedChunk:` [[LastLoadedChunk]]`)` + */ + protected load(): Promise> { + const { + ctx, + chunkRender + } = this; + + void ctx.setMod('progress', true); + + const + defaultRequestParams = ctx.dataProvider?.getDefaultRequestParams('get'), + params = >(defaultRequestParams ?? [])[0]; + + Object.assign(params, ctx.requestQuery?.(ctx.getCurrentDataState())?.get); + + return ctx.async.request(ctx.getData(this.component, params), {label: $$.request}) + .then((data) => { + this.ctx.localState = 'ready'; + void ctx.removeMod('progress', true); + this.lastLoadedChunk.raw = data; + + const + converted = data != null ? ctx.convertDataToDB(data) : undefined; + + this.lastLoadedChunk.normalized = Object.size(converted?.data) <= 0 ? + this.lastLoadedChunk.normalized = [] : + this.lastLoadedChunk.normalized = converted!.data!; + + ctx.emit('chunkLoaded', this.lastLoadedChunk, this.page); + return converted; + }) + + .catch((err) => { + void ctx.removeMod('progress', true); + + if (isAsyncClearError(err)) { + return Promise.reject(err); + } + + chunkRender.setRefVisibility('retry', true); + chunkRender.setRefVisibility('renderNext', false); + + this.ctx.onRequestError(err, this.ctx.reloadLast.bind(this.ctx)); + stderr(err); + + this.lastLoadedChunk.raw = []; + this.lastLoadedChunk.normalized = []; + + return undefined; + }); + } + + /** + * Handler: all requests are done + */ + protected onRequestsDone(): void { + const + {ctx, chunkRender, async: $a} = this, + {chunkSize} = ctx; + + if (this.pendingData.length > 0) { + chunkRender.initItems(this.pendingData.splice(0, chunkSize)); + chunkRender.render(); + } + + if (this.pendingData.length === 0) { + chunkRender.setRefVisibility('done', true); + chunkRender.setRefVisibility('renderNext', false); + } + + $a.wait(() => ctx.localState === 'ready', {label: $$.requestDoneWaitForReady}) + .then(() => { + if (this.pendingData.length === 0) { + chunkRender.setRefVisibility('done', true); + } + + chunkRender.setLoadersVisibility(false); + }) + .catch(stderr); + } +} diff --git a/src/components/base/b-virtual-scroll/modules/component-render.ts b/src/components/base/b-virtual-scroll/modules/component-render.ts new file mode 100644 index 0000000000..11ee3e4e62 --- /dev/null +++ b/src/components/base/b-virtual-scroll/modules/component-render.ts @@ -0,0 +1,218 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import Friend from 'components/friends/friend'; +import { mergeProps } from 'core/component/render'; + +import type ScrollRender from 'components/base/b-virtual-scroll/modules/chunk-render'; +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; + +import type { RenderItem, DataToRender, ItemAttrs, VirtualItemEl } from 'components/base/b-virtual-scroll/interface'; + +export default class ComponentRender extends Friend { + override readonly C!: bVirtualScroll; + + /** + * Async group + */ + readonly asyncGroup: string = 'component-render'; + + /** + * If false, the cache flushing process is not currently running + */ + protected canDropCache: boolean = false; + + /** + * Rendered items cache + */ + protected nodesCache: Dictionary = Object.createDict(); + + /** + * True if rendered nodes can be cached + */ + protected get canCache(): boolean { + return this.ctx.cacheNodes && this.ctx.clearNodes; + } + + /** + * API for scroll rendering + */ + protected get scrollRender(): ScrollRender { + return this.ctx.chunkRender; + } + + /** + * Classname for options + */ + get optionClass(): CanUndef { + return this.ctx.block?.getFullElementName('option-el'); + } + + /** + * Re-initializes component render + */ + reInit(): void { + Object.keys(this.nodesCache).forEach((key) => { + const el = this.nodesCache[key]; + el?.remove(); + }); + + this.nodesCache = Object.createDict(); + } + + /** + * Returns a node from the cache by the specified key + * @param key + */ + getCachedComponent(key: string): CanUndef { + return this.nodesCache[key]; + } + + /** + * Saves a node to the cache by the specified key + * + * @param key + * @param node + */ + cacheNode(key: string, node: HTMLElement): HTMLElement { + if (!this.ctx.cacheNodes) { + return node; + } + + this.nodesCache[key] = node; + + const + {nodesCache, ctx: {cacheSize}} = this; + + if (Object.keys(nodesCache).length > cacheSize) { + this.canDropCache = true; + } + + return node; + } + + /** {@link bVirtualScroll.getOptionKey} */ + getItemKey(data: VirtualItemEl, index: number): string { + return String(this.ctx.getItemKey(data, index)); + } + + /** + * Renders the specified chunk of items + * @param items + */ + render(items: RenderItem[]): HTMLElement[] { + const + {canCache} = this; + + const + res: HTMLElement[] = [], + needRender: Array<[RenderItem, number, VirtualItemEl]> = []; + + for (let i = 0; i < items.length; i++) { + const + item = items[i]; + + if (item.node) { + res[i] = item.node; + continue; + } + + const getItemKeyData = { + current: item.data, + prev: items[i - 1]?.data, + next: items[i + 1]?.data + }; + + if (canCache) { + const + key = this.getItemKey(getItemKeyData, item.index), + node = this.getCachedComponent(key); + + if (node) { + res[i] = node; + item.node = node; + continue; + } + } + + needRender.push([item, i, getItemKeyData]); + } + + if (needRender.length > 0) { + const + nodes = this.createComponents(needRender.map(([item]) => item)); + + for (let i = 0; i < needRender.length; i++) { + const + [item, indexesToAssign, getItemKeyData] = needRender[i], + node = nodes[i]; + + const + key = this.getItemKey(getItemKeyData, item.index); + + if (canCache) { + this.cacheNode(key, item.node = node); + } + + res[indexesToAssign] = node; + } + } + + return res; + } + + /** + * Creates and renders components by the specified parameters + * @param items + */ + protected createComponents(items: RenderItem[]): HTMLElement[] { + const + {ctx: c, scrollRender: {items: totalItems}} = this; + + const render = (children: DataToRender[]) => { + const map = ({itemAttrs, itemParams, index}) => + this.ctx.vdom.create(c.getItemComponentName(itemParams, index), itemAttrs); + + return c.vdom.render(children.map(map)); + }; + + const getChildrenAttrs = (props: ItemAttrs) => ({ + attrs: mergeProps(props, {class: this.optionClass}) + }); + + const getItemEl = (data, i: number) => ({ + current: data, + prev: totalItems[i - 1]?.data, + next: totalItems[i + 1]?.data + }); + + const + children: DataToRender[] = []; + + for (let i = 0; i < items.length; i++) { + const + item = items[i], + itemParams = getItemEl(item.data, item.index), + itemIndex = item.index; + + const attrs = c.getItemAttrs(getItemEl(item.data, item.index), item.index); + + children.push({itemParams, itemAttrs: getChildrenAttrs(attrs!), index: itemIndex}); + } + + const + // https://github.com/vuejs/core/issues/6061 + res = render(children).filter((node) => node.nodeType !== node.TEXT_NODE); + + if (res.length === 0) { + throw new Error('Failed to render components. Possibly an error occurred while creating the components.'); + } + + return res; + } +} diff --git a/src/components/base/b-virtual-scroll/modules/emitter/index.ts b/src/components/base/b-virtual-scroll/modules/emitter/index.ts deleted file mode 100644 index 3fa5f7ba70..0000000000 --- a/src/components/base/b-virtual-scroll/modules/emitter/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { AsyncOptions } from 'core/async'; - -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { ComponentEvents, LocalEventPayload } from 'components/base/b-virtual-scroll/interface'; -import type { ComponentTypedEmitter } from 'components/base/b-virtual-scroll/modules/emitter/interface'; - -export * from 'components/base/b-virtual-scroll/modules/emitter/interface'; - -/** - * Provides methods for interacting with the `selfEmitter` using typed events - * @param ctx - */ -export function componentTypedEmitter(ctx: bVirtualScroll): ComponentTypedEmitter { - const once = ( - event: EVENT, - handler: (...args: LocalEventPayload) => void, - asyncOpts?: AsyncOptions - ) => { - ctx.once(event, handler, asyncOpts); - }; - - const on = ( - event: EVENT, - handler: (...args: LocalEventPayload) => void, - asyncOpts?: AsyncOptions - ) => { - ctx.on(event, handler, asyncOpts); - }; - - const promisifyOnce = ( - event: EVENT, - asyncOpts?: AsyncOptions - ) => ctx.promisifyOnce(event, asyncOpts); - - const emit = ( - event: EVENT, - ...payload: LocalEventPayload - ) => { - ctx.emit(event, ...payload); - }; - - return { - once, - on, - promisifyOnce, - emit - }; -} - diff --git a/src/components/base/b-virtual-scroll/modules/emitter/interface.ts b/src/components/base/b-virtual-scroll/modules/emitter/interface.ts deleted file mode 100644 index 7381ff526d..0000000000 --- a/src/components/base/b-virtual-scroll/modules/emitter/interface.ts +++ /dev/null @@ -1,55 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { AsyncOptions } from 'core/async'; -import type { ComponentEvents, LocalEventPayload } from 'components/base/b-virtual-scroll/interface'; - -/** - * An interface representing the typed `selfEmitter` methods. - */ -export interface ComponentTypedEmitter { - /** - * @param event - the event name. - * @param handler - the event handler function. - * @param [asyncOpts] - Optional async options. - */ - once( - event: EVENT, - handler: (...args: LocalEventPayload) => void, - asyncOpts?: AsyncOptions - ): void; - - /** - * @param event - the event name. - * @param handler - the event handler function. - * @param [asyncOpts] - Optional async options. - */ - on( - event: EVENT, - handler: (...args: LocalEventPayload) => void, - asyncOpts?: AsyncOptions - ): void; - - /** - * @param event - the event name. - * @param [asyncOpts] - Optional async options. - */ - promisifyOnce( - event: EVENT, - asyncOpts?: AsyncOptions - ): Promise>; - - /** - * @param event - the event name. - * @param payload - the event payload. - */ - emit( - event: EVENT, - ...payload: LocalEventPayload - ): void; -} diff --git a/src/components/base/b-virtual-scroll/modules/factory/engines/vdom.ts b/src/components/base/b-virtual-scroll/modules/factory/engines/vdom.ts deleted file mode 100644 index df4beb229a..0000000000 --- a/src/components/base/b-virtual-scroll/modules/factory/engines/vdom.ts +++ /dev/null @@ -1,24 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { VNodeDescriptor } from 'components/friends/vdom'; -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; - -/** - * Renders the provided `VNodes` to the `HTMLElements` via `vdom.render` API. - * - * @param ctx - * @param items - */ -export function render(ctx: bVirtualScroll, items: VNodeDescriptor[]): HTMLElement[] { - const - vnodes = ctx.vdom.create(...items), - nodes = ctx.vdom.render(vnodes); - - return nodes; -} diff --git a/src/components/base/b-virtual-scroll/modules/factory/index.ts b/src/components/base/b-virtual-scroll/modules/factory/index.ts deleted file mode 100644 index 1ff9e46de6..0000000000 --- a/src/components/base/b-virtual-scroll/modules/factory/index.ts +++ /dev/null @@ -1,134 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import Friend from 'components/friends/friend'; -import type { VNodeDescriptor } from 'components/friends/vdom'; - -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { ComponentItem, ItemsProcessor, MountedChild, MountedItem } from 'components/base/b-virtual-scroll/interface'; -import { isItem } from 'components/base/b-virtual-scroll/modules/helpers'; - -import * as vdomRender from 'components/base/b-virtual-scroll/modules/factory/engines/vdom'; - -/** - * A friendly class that provides an API for component production, specifically tailored for the `bVirtualScroll` class. - */ -export class ComponentFactory extends Friend { - override readonly C!: bVirtualScroll; - - /** - * Produces component items based on the current state and context. - * Returns an array of component items. - */ - produceComponentItems(): ComponentItem[] { - const - {ctx} = this; - - return this.itemsProcessor(ctx.itemsFactory(ctx.getVirtualScrollState(), ctx)); - } - - /** - * Produces DOM nodes from an array of component items. - * Returns an array of DOM nodes representing the component items. - * - * @param componentItems - an array of component items - */ - produceNodes(componentItems: ComponentItem[]): HTMLElement[] { - if (componentItems.length === 0) { - return []; - } - - const createDescriptor = (item: ComponentItem): VNodeDescriptor => ({ - type: item.item, - attrs: item.props, - children: item.children - }); - - const descriptors = componentItems.map(createDescriptor); - return this.callRenderEngine(descriptors); - } - - /** - * Augments `ComponentItem` with various properties such as the component node, item index, and child index. - * - * @param items - * @param nodes - */ - produceMounted(items: ComponentItem[], nodes: HTMLElement[]): Array { - const - {ctx} = this, - {items: mountedItems, childList} = ctx.getVirtualScrollState(); - - let - itemsCounter = 0; - - return items.map((item, i) => { - if (isItem(item)) { - const res = { - ...item, - node: nodes[i], - itemIndex: mountedItems.length + itemsCounter, - childIndex: childList.length + i - }; - - itemsCounter++; - return res; - } - - return { - ...item, - node: nodes[i], - childIndex: mountedItems.length + i - }; - }); - } - - /** - * Invokes the {@link bVirtualScroll.itemsProcessors} function and returns its result - * @param items - the list of items to process. - */ - protected itemsProcessor(items: ComponentItem[]): ComponentItem[] { - const - {ctx} = this, - itemsProcessors = ctx.getItemsProcessors(); - - if (!itemsProcessors) { - return items; - } - - if (Object.isFunction(itemsProcessors)) { - return itemsProcessors(items, ctx); - } - - Object.forEach(itemsProcessors, (processor) => { - items = processor(items, ctx); - }); - - return items; - } - - /** - * Calls the render engine to render the components based on the provided descriptors. - * Returns an array of rendered DOM nodes. - * - * @param descriptors - an array of VNode descriptors. - */ - protected callRenderEngine(descriptors: VNodeDescriptor[]): HTMLElement[] { - const - {ctx} = this; - - ctx.onRenderEngineStart(); - - const - res = vdomRender.render(ctx, descriptors); - - ctx.onRenderEngineDone(); - - return res; - } -} diff --git a/src/components/base/b-virtual-scroll/modules/helpers.ts b/src/components/base/b-virtual-scroll/modules/helpers.ts new file mode 100644 index 0000000000..32738cf489 --- /dev/null +++ b/src/components/base/b-virtual-scroll/modules/helpers.ts @@ -0,0 +1,106 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type ChunkRender from 'components/base/b-virtual-scroll/modules/chunk-render'; +import type ChunkRequest from 'components/base/b-virtual-scroll/modules/chunk-request'; +import type { DataState } from 'components/base/b-virtual-scroll/interface'; + +/** + * Returns accumulated data among `b-virtual-scroll`,` chunk-render`, `chunk-request` and passes it to the client + * to make any decisions. For instance, one more chunk of data needs to be loaded. + * + * @param [chunkRequestCtx] + * @param [chunkRenderCtx] + * @param [merge] + * + * @typeParam ITEM - data item to render + * @typeParam RAW - raw provider data + */ +export function getRequestParams( + chunkRequestCtx?: ChunkRequest, + chunkRenderCtx?: ChunkRender, + merge?: Dictionary +): DataState { + const + component = chunkRenderCtx?.component ?? chunkRequestCtx?.component, + pendingData = chunkRequestCtx?.pendingData ?? []; + + const lastLoadedData = >chunkRequestCtx?.lastLoadedChunk.normalized; + + const base: DataState = { + currentPage: 0, + nextPage: 1, + + data: [], + items: [], + isLastEmpty: false, + itemsTillBottom: 0, + total: undefined, + + pendingData, + + lastLoadedData: lastLoadedData ?? [], + lastLoadedChunk: { + raw: undefined, + normalized: lastLoadedData ?? [] + } + }; + + const params = chunkRequestCtx && chunkRenderCtx ? + { + items: chunkRenderCtx.items, + itemsTillBottom: chunkRenderCtx.items.length - chunkRenderCtx.lastIntersectsItem, + + currentPage: chunkRequestCtx.page, + isLastEmpty: chunkRequestCtx.isLastEmpty, + total: component?.unsafe.total, + + pendingData, + data: chunkRequestCtx.data, + + lastLoadedData: lastLoadedData ?? [], + lastLoadedChunk: { + raw: chunkRequestCtx.lastLoadedChunk.raw, + normalized: lastLoadedData ?? [] + } + } : + base; + + const + mergeLastLoadedChunk = merge?.lastLoadedChunk; + + const merged = { + ...params, + ...merge, + lastLoadedChunk: { + ...params.lastLoadedChunk, + ...mergeLastLoadedChunk + } + }; + + return >{ + ...merged, + nextPage: merged.currentPage + 1 + }; +} + +/** + * True if the specified value is an `async replace` error + * @param val + */ +export function isAsyncReplaceError(val: unknown): boolean { + return Object.isPlainObject(val) && val.join === 'replace'; +} + +/** + * True if the specified value is an `async clear` error + * @param val + */ +export function isAsyncClearError(val: unknown): boolean { + return Object.isPlainObject(val) && val.type === 'clearAsync'; +} diff --git a/src/components/base/b-virtual-scroll/modules/helpers/index.ts b/src/components/base/b-virtual-scroll/modules/helpers/index.ts deleted file mode 100644 index dc5832db2e..0000000000 --- a/src/components/base/b-virtual-scroll/modules/helpers/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import { componentItemType } from 'components/base/b-virtual-scroll/const'; -import type { MountedItem } from 'components/base/b-virtual-scroll/interface'; - -/** - * Returns `true` if the value is of type `MountedItem`, otherwise `false` - * @param val - the value to check. - */ -export function isItem(val: any): val is MountedItem { - return Object.isPlainObject(val) && val.type === componentItemType.item; -} - -/** - * Returns `true` if the specified value is an `async replace` error - * @param val - */ -export function isAsyncReplaceError(val: unknown): boolean { - return Object.isPlainObject(val) && val.join === 'replace'; -} diff --git a/src/components/base/b-virtual-scroll/modules/observer/const.ts b/src/components/base/b-virtual-scroll/modules/observer/const.ts deleted file mode 100644 index 9f8a9be196..0000000000 --- a/src/components/base/b-virtual-scroll/modules/observer/const.ts +++ /dev/null @@ -1,12 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -/** - * Group for async operations of the observer module. - */ -export const observerAsyncGroup = '[[OBSERVER]]'; diff --git a/src/components/base/b-virtual-scroll/modules/observer/engines/intersection-observer.ts b/src/components/base/b-virtual-scroll/modules/observer/engines/intersection-observer.ts deleted file mode 100644 index 71d74c93d4..0000000000 --- a/src/components/base/b-virtual-scroll/modules/observer/engines/intersection-observer.ts +++ /dev/null @@ -1,45 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import Friend from 'components/friends/friend'; - -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { MountedChild } from 'components/base/b-virtual-scroll/interface'; - -import { observerAsyncGroup } from 'components/base/b-virtual-scroll/modules/observer/const'; -import type { ObserverEngine } from 'components/base/b-virtual-scroll/modules/observer/interface'; - -export default class IoObserver extends Friend implements ObserverEngine { - - /** - * {@link bVirtualScroll} - */ - override readonly C!: bVirtualScroll; - - /** - * {@link ObserverEngine.watchForIntersection} - * @param components - */ - watchForIntersection(components: MountedChild[]): void { - const - {ctx} = this; - - for (const component of components) { - ctx.dom.watchForIntersection(component.node, { - group: observerAsyncGroup, - label: component.key, - once: true, - delay: 0 - }, () => ctx.onElementEnters(component)); - } - } - - reset(): void { - this.async.clearAll({group: new RegExp(observerAsyncGroup)}); - } -} diff --git a/src/components/base/b-virtual-scroll/modules/observer/index.ts b/src/components/base/b-virtual-scroll/modules/observer/index.ts deleted file mode 100644 index c87c4c860b..0000000000 --- a/src/components/base/b-virtual-scroll/modules/observer/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import Friend from 'components/friends/friend'; - -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { MountedChild } from 'components/base/b-virtual-scroll/interface'; - -import IoObserver from 'components/base/b-virtual-scroll/modules/observer/engines/intersection-observer'; - -export { default as IoObserver } from 'components/base/b-virtual-scroll/modules/observer/engines/intersection-observer'; - -/** - * Observer class for `bVirtualScroll` component. - * It provides observation capabilities using different engines such as IoObserver and ScrollObserver. - */ -export class Observer extends Friend { - override readonly C!: bVirtualScroll; - - /** - * The observation engine used by the Observer. - */ - protected engine: IoObserver; - - /** - * @param ctx - the `bVirtualScroll` component instance. - */ - constructor(ctx: bVirtualScroll) { - super(ctx); - - this.engine = new IoObserver(ctx); - } - - /** - * Starts observing the specified mounted elements - * @param mounted - an array of elements to be observed. - */ - observe(mounted: MountedChild[]): void { - const - {ctx} = this; - - if (ctx.disableObserver) { - return; - } - - this.engine.watchForIntersection(mounted); - } - - /** - * Resets the module state - */ - reset(): void { - this.engine.reset(); - } -} diff --git a/src/components/base/b-virtual-scroll/modules/observer/interface.ts b/src/components/base/b-virtual-scroll/modules/observer/interface.ts deleted file mode 100644 index f5f3a248dc..0000000000 --- a/src/components/base/b-virtual-scroll/modules/observer/interface.ts +++ /dev/null @@ -1,27 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { MountedItem } from 'components/base/b-virtual-scroll/interface'; - -/** - * Interface representing an observer engine for watching components entering the viewport. - */ -export interface ObserverEngine { - /** - * Initializes a watcher to track when components enter the viewport. - * - * @param components - An array of mounted items to be watched. - */ - watchForIntersection(components: MountedItem[]): void; - - /** - * Resets the state of the observer engine. - * This can be used to clear any existing observers and reset the module to its initial state. - */ - reset(): void; -} diff --git a/src/components/base/b-virtual-scroll/modules/slots/index.ts b/src/components/base/b-virtual-scroll/modules/slots/index.ts deleted file mode 100644 index 533783de8b..0000000000 --- a/src/components/base/b-virtual-scroll/modules/slots/index.ts +++ /dev/null @@ -1,164 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import symbolGenerator from 'core/symbol'; -import type { AsyncOptions } from 'core/async'; - -import Friend from 'components/friends/friend'; - -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import type { SlotsStateObj } from 'components/base/b-virtual-scroll/modules/slots/interface'; - -export * from 'components/base/b-virtual-scroll/modules/slots/interface'; - -export const - $$ = symbolGenerator(), - slotsStateControllerAsyncGroup = 'slotsStateController'; - -/** - * A class that manages the visibility of slots based on different states. - */ -export class SlotsStateController extends Friend { - - override readonly C!: bVirtualScroll; - - /** - * Options for the asynchronous operations. - */ - protected readonly asyncUpdateLabel: AsyncOptions = { - label: $$.updateSlotsVisibility, - group: slotsStateControllerAsyncGroup - }; - - /** - * The last state of the slots. - */ - protected lastState?: SlotsStateObj; - - /** - * Displays the slots that should be shown when the data state is empty - */ - emptyState(): void { - this.setSlotsVisibility({ - container: true, - done: true, - empty: true, - loader: false, - renderNext: false, - retry: false, - tombstones: false - }); - } - - /** - * Displays the slots that should be shown when the lifecycle is done - */ - doneState(): void { - this.setSlotsVisibility({ - container: true, - done: true, - empty: this.lastState?.empty ?? false, - loader: false, - renderNext: false, - retry: false, - tombstones: false - }); - } - - /** - * Displays the slots that should be shown during data loading progress - * @param [immediate] - if set to true, {@link requestAnimationFrame} will not be used to switch the state. - */ - loadingProgressState(immediate: boolean = false): void { - this.setSlotsVisibility({ - container: true, - loader: true, - tombstones: true, - done: false, - empty: false, - renderNext: false, - retry: false - }, immediate); - } - - /** - * Displays the slots that should be shown when data loading fails - */ - loadingFailedState(): void { - this.setSlotsVisibility({ - container: true, - retry: true, - done: false, - empty: false, - loader: false, - renderNext: false, - tombstones: false - }); - } - - /** - * Displays the slots that should be shown when data loading is successful - */ - loadingSuccessState(): void { - this.setSlotsVisibility({ - container: true, - done: false, - empty: false, - loader: false, - renderNext: true, - retry: false, - tombstones: false - }); - } - - /** - * Resets the state of the module - */ - reset(): void { - this.async.clearAll({group: new RegExp(slotsStateControllerAsyncGroup)}); - this.lastState = undefined; - } - - /** - * Sets the visibility state of the slots. - * - * @param stateObj - an object specifying the visibility state of each slot. - * @param [immediate] - if set to true, {@link requestAnimationFrame} will not be used to switch the state. - */ - protected setSlotsVisibility(stateObj: Required, immediate: boolean = false): void { - this.lastState = stateObj; - - this.async.cancelAnimationFrame(this.asyncUpdateLabel); - - const update = () => { - for (const [name, state] of Object.entries(stateObj)) { - this.setDisplayState(name, state); - } - }; - - if (immediate) { - return update(); - } - - this.async.requestAnimationFrame(update, this.asyncUpdateLabel); - } - - /** - * Sets the display state of a slot. - * - * @param name - the name of the slot. - * @param state - the visibility state of the slot. - */ - protected setDisplayState(name: keyof SlotsStateObj, state: boolean): void { - const ref = this.ctx.$refs[name]; - - if (ref instanceof HTMLElement) { - ref.style.display = state ? '' : 'none'; - } - } -} diff --git a/src/components/base/b-virtual-scroll/modules/slots/interface.ts b/src/components/base/b-virtual-scroll/modules/slots/interface.ts deleted file mode 100644 index b5599dd4a2..0000000000 --- a/src/components/base/b-virtual-scroll/modules/slots/interface.ts +++ /dev/null @@ -1,17 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { ComponentRefs } from 'components/base/b-virtual-scroll/interface'; - -/** - * Represents the state of slots. - * [slotName: slotVisibility] - */ -export type SlotsStateObj = { - [key in keyof ComponentRefs]: boolean; -}; diff --git a/src/components/base/b-virtual-scroll/modules/state/helpers.ts b/src/components/base/b-virtual-scroll/modules/state/helpers.ts deleted file mode 100644 index df42d3b041..0000000000 --- a/src/components/base/b-virtual-scroll/modules/state/helpers.ts +++ /dev/null @@ -1,45 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { VirtualScrollState, PrivateComponentState } from 'components/base/b-virtual-scroll/interface'; - -/** - * Creates an initial state object for a component - */ -export function createInitialState(): VirtualScrollState { - return { - loadPage: 0, - renderPage: 0, - remainingItems: undefined, - remainingChildren: undefined, - maxViewedItem: undefined, - maxViewedChild: undefined, - data: [], - lastLoadedData: [], - lastLoadedRawData: undefined, - isLastEmpty: false, - isInitialLoading: true, - items: [], - childList: [], - isInitialRender: true, - areRequestsStopped: false, - isLoadingInProgress: false, - isLifecycleDone: false, - isLastErrored: false, - isLastRender: false - }; -} - -/** - * Creates an initial private state object for a component - */ -export function createPrivateInitialState(): PrivateComponentState { - return { - dataOffset: 0 - }; -} diff --git a/src/components/base/b-virtual-scroll/modules/state/index.ts b/src/components/base/b-virtual-scroll/modules/state/index.ts deleted file mode 100644 index a6bc28d8e3..0000000000 --- a/src/components/base/b-virtual-scroll/modules/state/index.ts +++ /dev/null @@ -1,228 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import Friend from 'components/friends/friend'; - -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; -import { isItem } from 'components/base/b-virtual-scroll/modules/helpers'; -import { createInitialState, createPrivateInitialState } from 'components/base/b-virtual-scroll/modules/state/helpers'; -import type { MountedChild, VirtualScrollState, MountedItem, PrivateComponentState } from 'components/base/b-virtual-scroll/interface'; - -/** - * Friendly to the `bVirtualScroll` class that represents the internal state of a component. - */ -export class ComponentInternalState extends Friend { - override readonly C!: bVirtualScroll; - - /** - * Current state of the component. - */ - protected state: VirtualScrollState = createInitialState(); - - /** - * Current private state of the component. - */ - protected privateState: PrivateComponentState = createPrivateInitialState(); - - /** - * Compiles and returns the current state of the component. - * - * @returns The current state of the component. - */ - compile(): Readonly { - return this.state; - } - - /** - * Resets the state of the component - */ - reset(): void { - this.state = createInitialState(); - this.privateState = createPrivateInitialState(); - } - - /** - * Increments the load page pointer - */ - incrementLoadPage(): void { - this.state.loadPage++; - } - - /** - * Increments the render page pointer - */ - incrementRenderPage(): void { - this.state.renderPage++; - } - - /** - * Updates the loaded data state. - * - * @param data - the new data to update the state. - * @param isInitialLoading - indicates if it's the initial loading. - */ - updateData(data: object[], isInitialLoading: boolean): void { - this.state.data = this.state.data.concat(data); - this.state.isLastEmpty = data.length === 0; - this.state.isInitialLoading = isInitialLoading; - this.state.lastLoadedData = data; - } - - /** - * Updates the arrays with mounted child elements of the component - * @param mounted - the mounted child elements. - */ - updateMounted(mounted: MountedChild[]): void { - const - {state} = this, - childList = state.childList, - itemsList = state.items, - newItems = mounted.filter((child) => child.type === 'item'); - - childList.push(...mounted); - itemsList.push(...newItems); - - this.updateRemainingChildren(); - } - - /** - * Updates the indicator that shows whether the current rendering process is the - * last one in this lifecycle. - */ - updateIsLastRender(): void { - const - {state, ctx} = this; - - if (!state.areRequestsStopped) { - return; - } - - const - chunkSize = ctx.getChunkSize(state), - dataOffset = this.getDataCursor() + chunkSize; - - if (>state.data[dataOffset] == null) { - state.isLastRender = true; - } - } - - /** - * Updates the state of the last raw loaded data - * @param data - the last raw loaded data. - */ - setRawLastLoaded(data: unknown): void { - this.state.lastLoadedRawData = data; - } - - /** - * Sets the flag indicating if it's the initial render cycle - * @param value - the value of the flag. - */ - setIsInitialRender(value: boolean): void { - this.state.isInitialRender = value; - } - - /** - * Sets the flag indicating if requests are stopped and the component won't make any more requests - * until the lifecycle is refreshed. - * - * @param value - the value of the flag. - */ - setIsRequestsStopped(value: boolean): void { - this.state.areRequestsStopped = value; - } - - /** - * Sets the flag indicating if the component's lifecycle is done - * @param value - the value of the flag. - */ - setIsLifecycleDone(value: boolean): void { - this.state.isLifecycleDone = value; - } - - /** - * Sets the flag indicating if the component is currently loading data - * @param value - the value of the flag. - */ - setIsLoadingInProgress(value: boolean): void { - this.state.isLoadingInProgress = value; - } - - /** - * Sets a flag indicating whether the last load operation ended with an error - * @param value - the value to set. - */ - setIsLastErrored(value: boolean): void { - this.state.isLastErrored = value; - } - - /** - * Sets the maximum viewed index based on the passed component's index - * @param component - the component to compare and update the maximum viewed index. - */ - setMaxViewedIndex(component: MountedChild): void { - const - {state} = this, - {childIndex} = component; - - if (isItem(component) && (state.maxViewedItem == null || state.maxViewedItem < component.itemIndex)) { - state.maxViewedItem = component.itemIndex; - state.remainingItems = state.items.length - 1 - state.maxViewedItem; - } - - if (state.maxViewedChild == null || state.maxViewedChild < childIndex) { - state.maxViewedChild = component.childIndex; - state.remainingChildren = state.childList.length - 1 - state.maxViewedChild; - } - - this.updateRemainingChildren(); - } - - /** - * Returns the cursor indicating the last index of the last rendered data element - */ - getDataCursor(): number { - return this.privateState.dataOffset; - } - - /** - * Updates the cursor indicating the last index of the last rendered data element - */ - updateDataOffset(): void { - const - {ctx, state} = this, - current = this.getDataCursor(), - chunkSize = ctx.getChunkSize(state); - - this.privateState.dataOffset = current + chunkSize; - } - - /** - * Updates the state of the tillEnd-like fields. - * Calculates the remaining number of child elements until the end and the remaining number of items until the end. - */ - updateRemainingChildren(): void { - const - {state} = this; - - if (state.maxViewedChild == null) { - state.remainingChildren = state.childList.length - 1; - - } else { - state.remainingChildren = state.childList.length - 1 - state.maxViewedChild; - } - - if (state.maxViewedItem == null) { - state.remainingItems = state.items.length - 1; - - } else { - state.remainingItems = state.items.length - 1 - state.maxViewedItem; - } - } -} - diff --git a/src/components/base/b-virtual-scroll/props.ts b/src/components/base/b-virtual-scroll/props.ts deleted file mode 100644 index 58342b2d11..0000000000 --- a/src/components/base/b-virtual-scroll/props.ts +++ /dev/null @@ -1,333 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type iItems from 'components/traits/i-items/i-items'; -import type { CreateFromItemFn } from 'components/traits/i-items/i-items'; - -import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; - -import type { - - VirtualScrollState, - ComponentDb, - RequestQueryFn, - ShouldPerform, - ItemsProcessors, - - ComponentItemFactory, - ComponentItemType, - ComponentItem, - ComponentItemMeta - -} from 'components/base/b-virtual-scroll/interface'; - -import { defaultShouldProps, componentItemType, itemsProcessors } from 'components/base/b-virtual-scroll/const'; - -import type { Observer } from 'components/base/b-virtual-scroll/modules/observer'; - -import iData, { component, prop } from 'components/super/i-data/i-data'; - -@component() -export default abstract class iVirtualScrollProps extends iData { - /** {@link iItems.item} */ - readonly Item!: object; - - /** {@link iItems.Items} */ - readonly Items!: Array; - - /** {@link iItems.item} */ - @prop({type: [String, Function]}) - readonly item?: iItems['item']; - - /** {@link iItems.items} */ - @prop({type: [String, Function]}) - readonly items: iItems['items']; - - /** {@link iItems.itemKey} */ - @prop({type: [String, Function]}) - readonly itemKey?: CreateFromItemFn; - - /** {@link ComponentItemType} */ - @prop({type: [String, Function]}) - readonly itemType: keyof ComponentItemType | CreateFromItemFn = componentItemType.item; - - /** {@link iItems.itemProps} */ - @prop({type: [Function, Object], default: () => ({})}) - readonly itemProps!: iItems['itemProps']; - - /** - * Meta information for a component that will not be used during rendering, - * but will be available for reading/changing in `itemsProcessors`. - * - * If a function is provided, it will be called; otherwise, the value will be preserved "as is". - * - * @example - * ```typescript - * const itemMeta = (data) => ({ - * componentData: data - * }) - * ``` - */ - @prop() - readonly itemMeta?: CreateFromItemFn; - - /** - * Specifies the number of times the `tombstone` component will be rendered. - * - * This prop can be useful if you want to render multiple `tombstone` components - * using a single specified element. For example, if you set `tombstoneCount` to 3, - * then three `tombstone` components will be rendered on your page. - * - * @example - * ``` - * < b-virtual-scroll :tombstoneCount = 3 - * < template #tombstone - * < .&__skeleton - * Skeleton - * ``` - * - * ```html - *
Skeleton
- *
Skeleton
- *
Skeleton
- * ``` - */ - @prop(Number) - readonly tombstoneCount?: number; - - /** - * This factory function is used to pass information about the components that need to be rendered. - * The function should return an array of arbitrary length consisting of objects that satisfy the - * {@link ComponentItem} interface. - * - * By default, the rendering strategy is based on the `chunkSize` and `iItems` trait. - * In other words, the default implementation takes a data slice of length `chunkSize` - * and calls the `iItems` functions to generate a `ComponentItem` object. - * - * However, the client can implement any required strategy by overriding this function. - * - * For example, it is possible to define a function - * that takes the last loaded data and renders twice as many components: - * - * @example - * ```typescript - * const itemsFactory = (state) => { - * const data = state.lastLoadedData; - * - * const items = data.map((item) => ({ - * item: 'section', - * key: Object.cast(undefined), - * type: 'item', - * children: [], - * props: { - * 'data-index': item.i - * } - * })); - * - * return [...items, ...items]; - * } - * ``` - */ - @prop({ - type: Function, - default: (state: VirtualScrollState, ctx: bVirtualScroll) => { - const descriptors = ctx.getNextDataSlice(state, ctx.getChunkSize(state)).map((data, i) => ({ - key: ctx.itemKey?.(data, i), - - item: Object.isFunction(ctx.item) ? ctx.item(data, i) : ctx.item, - type: Object.isFunction(ctx.itemType) ? ctx.itemType(data, i) : ctx.itemType, - - meta: { - data, - ...Object.isFunction(ctx.itemMeta) ? ctx.itemMeta(data, i) : ctx.itemMeta - }, - - props: Object.isFunction(ctx.itemProps) ? - ctx.itemProps(data, i, { - key: ctx.itemKey?.(data, i), - ctx - }) : - ctx.itemProps - })); - - return descriptors; - } - }) - - readonly itemsFactory!: ComponentItemFactory; - - /** - * This processor function enables you to manipulate previously compiled - * {@link ComponentItem}s via {@link bVirtualScroll.itemsFactory}. It allows you to add components to render, - * mutate props, and add children. It acts as middleware for rendering components. - * - * Scenarios where you might use this functionality: - * - * **Scenario**: Add an advertisement component after each rendered component - * in `b-virtual-scroll` throughout the app. - * - * **Solution**: Instead of overriding {@link bVirtualScroll.itemsFactory} inline, - * use {@link bVirtualScroll.itemsProcessors} for a centralized solution. - * - * @example - * ```typescript - * const addAds = (items: ComponentItem[]) => { - * const newItems = []; - * - * items.forEach((item) => { - * newItems.push(item); - * - * if (item.type === 'item') { - * newItems.push({ - * type: 'separator', - * item: 'b-ads-component', - * props: { prop: 'val' }, - * key: 'uniqueKey' - * }); - * } - * }); - * - * return newItems; - * } - * ``` - * - * To set this function as the global component processor in `b-virtual-scroll`, - * override the `itemsProcessors` constant (in `base/b-virtual-scroll/const.ts`) of your layer and export it. - * - * @example - * ```typescript - * export const itemsProcessors = { - * addAds - * } - * ``` - * - * After redefining this, `b-virtual-scroll` renders `b-ads-component` after - * each `item` component. - * - * **Scenario**: Replace `b-card` components with `b-mega-card` throughout the app - * and modify props. - * - * **Solution**: Add a processor function that changes the component name and mutates props. - * - * @example - * ```typescript - * const itemsProcessors = { - * addAds, - * migrateCardComponent: (items: ComponentItem[]) => { - * return items.map((item) => { - * if (item.item === 'b-card') { - * console.warn('Deprecation: b-card is deprecated.'); - * - * return { - * ...item, - * props: convertProps(item.props), - * item: 'b-mega-card' - * }; - * } - * - * return item; - * }); - * } - * } - * ``` - */ - @prop({ - type: [Function, Object, Array], - default: itemsProcessors - }) - - readonly itemsProcessors?: ItemsProcessors; - - override readonly DB!: ComponentDb; - - /** - * A function that returns the GET parameters for a request. This function is called for each request. It receives the - * current component state and should return the request parameters. These parameters are merged with the parameters - * from the `request` prop in favor of the second one. - * - * This function is useful when you need to pass pagination parameters or any other parameters that should not trigger - * a component's state reload, unlike changing the `request` prop. - * - * {@link RequestQueryFn} - */ - @prop({type: Function}) - readonly requestQuery?: RequestQueryFn; - - /** - * The amount of data required to perform one cycle of item rendering. - * - * This prop is primarily used to determine whether a specific action with the data needs to be performed - * ({@link bVirtualScroll.renderGuard}), and only secondarily for component rendering. - * - * By default, this prop is used in {@link bVirtualScroll.itemsFactory} to slice the data - * according to the {@link bVirtualScroll.chunkSize} and render components based on it. - * However, it is possible to define a custom {@link bVirtualScroll.itemsFactory} and render as many components - * as desired in one cycle of rendering. In this case, the `chunkSize` will only have significance for the data. - * - * This prop can also be a function that should return the amount of data required to perform one cycle of rendering. - * For example, different values can be specified depending on the rendering page: - * - * @example - * ```typescript - * const chunkSize = (state: VirtualScrollState) => { - * return [6, 12, 18][state.renderPage] ?? 18; - * } - * ``` - */ - @prop({type: [Number, Function]}) - readonly chunkSize: number | ShouldPerform = 10; - - /** - * When this function returns true the component will stop to request new data. - * This function will be called on each data loading cycle. - */ - @prop({ - type: Function, - default: defaultShouldProps.shouldStopRequestingData - }) - - readonly shouldStopRequestingData!: ShouldPerform; - - /** - * When this function returns true the component will be able to request additional data. - * This function will be called each time a new element enters the viewport. - */ - @prop({ - type: Function, - default: defaultShouldProps.shouldPerformDataRequest - }) - - readonly shouldPerformDataRequest!: ShouldPerform; - - /** - * This function is called in the {@link bVirtualScroll.renderGuard} after other checks are completed. - * - * This function receives the component state as input, based on which the client - * should determine whether the component should render the next chunk of components. - * - * For example, if we want to render the next data chunk only when the client - * has seen all the main (`type=item`) components, we can implement the following function: - * - * @example - * ```typescript - * const shouldPerformDataRender = (state) => { - * return state.isInitialRender || state.remainingItems === 0; - * } - * ``` - */ - @prop({type: Function, default: defaultShouldProps.shouldPerformDataRender}) - readonly shouldPerformDataRender?: ShouldPerform; - - /** - * Setting this property to false will disable the {@link Observer observation module}. This is useful when you - * want to implement lazy rendering not based on scrolling but on some other event, such as a click. In this case, - * you should use manual invocation of the `initLoadNext` method to render chunks. - */ - @prop(Boolean) - readonly disableObserver: boolean = false; -} diff --git a/src/components/base/b-virtual-scroll/test/index.js b/src/components/base/b-virtual-scroll/test/index.js new file mode 100644 index 0000000000..bfc1bf2e89 --- /dev/null +++ b/src/components/base/b-virtual-scroll/test/index.js @@ -0,0 +1,32 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +// @ts-check + +/** + * @typedef {import('playwright').Page} Page + */ + +const + h = include('tests/helpers').default, + u = include('tests/utils').default; + +/** + * Starts a test + * + * @param {Page} page + * @param {object} params + * @returns {Promise} + */ +module.exports = async (page, params) => { + const + test = u.getCurrentTest(); + + await h.utils.setup(page, params.context); + return test(page); +}; diff --git a/src/components/base/b-virtual-scroll/test/runners/events/chunk-loaded.js b/src/components/base/b-virtual-scroll/test/runners/events/chunk-loaded.js new file mode 100644 index 0000000000..20edbc8bae --- /dev/null +++ b/src/components/base/b-virtual-scroll/test/runners/events/chunk-loaded.js @@ -0,0 +1,230 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +// @ts-check + +/** + * @typedef {import('playwright').Page} Page + */ + +const + h = include('tests/helpers').default; + +/** @param {Page} page */ +module.exports = (page) => { + let + component, + node, + container; + + beforeEach(async () => { + await page.evaluate(() => { + globalThis.removeCreatedComponents(); + + const baseAttrs = { + theme: 'demo', + option: 'section', + optionProps: ({current}) => ({'data-index': current.i}) + }; + + const scheme = [ + { + attrs: { + ...baseAttrs, + id: 'target' + } + } + ]; + + globalThis.renderComponents('b-virtual-scroll', scheme); + }); + + await h.bom.waitForIdleCallback(page); + await h.component.waitForComponentStatus(page, '.b-virtual-scroll', 'ready'); + + component = await h.component.waitForComponent(page, '#target'); + node = await h.dom.waitForEl(page, '#target'); + container = await h.dom.waitForRef(node, 'container'); + }); + + const + getArray = (offset = 0, length = 12) => ({data: Array.from(Array(length), (v, i) => ({i: i + offset}))}), + firstChunkExpected = getArray(); + + const subscribe = () => component.evaluate((ctx) => new Promise((res) => ctx.watch(':onChunkLoaded', res))); + + const setProps = (requestProps = {}) => component.evaluate((ctx, requestProps) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.chunkSize = 10; + ctx.request = {get: {chunkSize: 12, id: Math.random(), ...requestProps}}; + }, requestProps); + + describe('b-virtual-scroll `chunkLoaded` event', () => { + describe('emitted', () => { + it('after loading the first chunk', async () => { + const subscribePromise = subscribe(); + await setProps(); + + await expectAsync(subscribePromise).toBeResolved(); + }); + + it('after loading the second chunk', async () => { + await setProps(); + await h.dom.waitForEl(container, 'section'); + + const subscribePromise = subscribe(); + + await h.scroll.scrollToBottom(page); + await expectAsync(subscribePromise).toBeResolved(); + }); + + it('when loading the first chunk after re-initialization', async () => { + await setProps(); + await h.dom.waitForEl(container, 'section'); + + const subscribePromise = subscribe(); + + await component.evaluate((ctx) => ctx.request = {get: {chunkSize: 20}}); + await expectAsync(subscribePromise).toBeResolved(); + }); + + it('three times to get the full data batch', async () => { + await component.evaluate((ctx) => { + ctx.watch(':onChunkLoaded', () => { + ctx.tmp.called = (ctx.tmp.called ?? 0) + 1; + }); + }); + + await setProps({chunkSize: 4}); + await h.dom.waitForEl(container, 'section'); + + expect(await component.evaluate((ctx) => ctx.tmp.called)).toBe(3); + }); + + it('after successful loading of the first chunk without payload', async () => { + const subscribePromise = subscribe(); + + await setProps({chunkSize: 0, total: 0}); + + await expectAsync(subscribePromise).toBeResolved(); + }); + + it('after successful loading of the second chunk without payload', async () => { + await setProps({chunkSize: 12, total: 12}); + await h.dom.waitForEl(container, 'section'); + + const subscribePromise = subscribe(); + + await h.scroll.scrollToBottom(page); + await expectAsync(subscribePromise).toBeResolved(); + }); + }); + + describe('not emitted', () => { + it('if there was a request error', async () => { + await component.evaluate((ctx) => ctx.watch(':onChunkLoaded', () => ctx.tmp.change = true)); + + await setProps({failOn: 0}); + await h.bom.waitForIdleCallback(page, {sleepAfterIdles: 1000}); + + expect(await component.evaluate((ctx) => ctx.tmp.change)).toBeUndefined(); + }); + }); + + describe('has correct payload', () => { + it('after loading the first chunk', async () => { + const subscribePromise = subscribe(); + + await component.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.chunkSize = 10; + ctx.request = {get: {chunkSize: 12, additionalData: {size: 12}}}; + }); + + await expectAsync(subscribePromise).toBeResolvedTo({ + normalized: firstChunkExpected.data, + raw: {data: firstChunkExpected.data, size: 12} + }); + }); + + it('after loading the second chunk', async () => { + await component.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.chunkSize = 10; + ctx.request = {get: {chunkSize: 12, additionalData: {size: 12}}}; + }); + + await h.dom.waitForEl(container, 'section'); + + const subscribePromise = subscribe(); + + await h.scroll.scrollToBottom(page); + await expectAsync(subscribePromise).toBeResolved({ + normalized: firstChunkExpected.data, + raw: {data: firstChunkExpected.data, size: 12} + }); + }); + + it('after loading the first chunk with an empty payload', async () => { + const subscribePromise = subscribe(); + + await component.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.chunkSize = 10; + ctx.request = {get: {id: Math.random(), chunkSize: 0, total: 0, additionalData: {size: 12}}}; + }); + + await expectAsync(subscribePromise).toBeResolved({ + normalized: [], + raw: {data: [], size: 12} + }); + }); + + it('after loading the second chunk with an empty payload', async () => { + await component.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.chunkSize = 10; + ctx.request = {get: {id: Math.random(), chunkSize: 12, total: 12, additionalData: {size: 12}}}; + }); + + await h.dom.waitForEl(container, 'section'); + + const subscribePromise = subscribe(); + + await h.scroll.scrollToBottom(page); + await expectAsync(subscribePromise).toBeResolved({ + normalized: [], + raw: {data: [], size: 12} + }); + }); + + it('when loading the first chunk in parts', async () => { + await component.evaluate((ctx) => { + ctx.tmp.eventAccumulator = {}; + + ctx.watch(':onChunkLoaded', (val) => { + ctx.tmp.called = (ctx.tmp.called ?? 0) + 1; + ctx.tmp.eventAccumulator[ctx.tmp.called] = Object.fastClone(val); + }); + + ctx.dataProvider = 'demo.Pagination'; + ctx.chunkSize = 10; + ctx.request = {get: {chunkSize: 4, id: Math.random(), additionalData: {size: 12}}}; + }); + + await h.dom.waitForEl(container, 'section'); + + expect(await component.evaluate((ctx) => ctx.tmp.eventAccumulator)).toEqual({ + 1: {normalized: getArray(0, 4).data, raw: {data: getArray(0, 4).data, size: 12}}, + 2: {normalized: getArray(4, 4).data, raw: {data: getArray(4, 4).data, size: 12}}, + 3: {normalized: getArray(8, 4).data, raw: {data: getArray(8, 4).data, size: 12}} + }); + }); + }); + }); +}; diff --git a/src/components/base/b-virtual-scroll/test/runners/events/chunk-loading.js b/src/components/base/b-virtual-scroll/test/runners/events/chunk-loading.js new file mode 100644 index 0000000000..286f8e8597 --- /dev/null +++ b/src/components/base/b-virtual-scroll/test/runners/events/chunk-loading.js @@ -0,0 +1,106 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +// @ts-check + +/** + * @typedef {import('playwright').Page} Page + */ + +const + h = include('tests/helpers').default; + +/** @param {Page} page */ +module.exports = (page) => { + let + component, + node, + container; + + beforeEach(async () => { + await page.evaluate(() => { + globalThis.removeCreatedComponents(); + + const baseAttrs = { + theme: 'demo', + option: 'section', + optionProps: ({current}) => ({'data-index': current.i}) + }; + + const scheme = [ + { + attrs: { + ...baseAttrs, + id: 'target' + } + } + ]; + + globalThis.renderComponents('b-virtual-scroll', scheme); + }); + + await h.bom.waitForIdleCallback(page); + await h.component.waitForComponentStatus(page, '.b-virtual-scroll', 'ready'); + + component = await h.component.waitForComponent(page, '#target'); + node = await h.dom.waitForEl(page, '#target'); + container = await h.dom.waitForRef(node, 'container'); + }); + + const subscribe = () => component.evaluate((ctx) => new Promise((res) => ctx.watch(':onChunkLoading', res))); + + const setProps = (requestProps = {}) => component.evaluate((ctx, requestProps) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.chunkSize = 10; + ctx.request = {get: {chunkSize: 12, id: Math.random(), ...requestProps}}; + }, requestProps); + + describe('b-virtual-scroll `chunkLoading` event', () => { + describe('emitted', () => { + it('when loading the first chunk', async () => { + const subscribePromise = subscribe(); + + await setProps(); + await expectAsync(subscribePromise).toBeResolvedTo(0); + }); + + it('when loading the second chunk', async () => { + await setProps(); + await h.dom.waitForEl(container, 'section'); + + const subscribePromise = subscribe(); + + await h.scroll.scrollToBottom(page); + await expectAsync(subscribePromise).toBeResolvedTo(1); + }); + + it('when loading the first chunk after re-initialization', async () => { + await setProps(); + await h.dom.waitForEl(container, 'section'); + + const subscribePromise = subscribe(); + await setProps({id: Math.random()}); + + await expectAsync(subscribePromise).toBeResolvedTo(0); + }); + + it('three times when loading a full chunk', async () => { + await component.evaluate((ctx) => ctx.watch(':onChunkLoading', (val) => { + ctx.tmp.currentCall = (ctx.tmp.currentCall ?? 0) + 1; + ctx.tmp[ctx.tmp.currentCall] = val; + })); + + await setProps({chunkSize: 4}); + await h.dom.waitForEl(container, 'section'); + + expect(await component.evaluate((ctx) => ctx.tmp.currentCall)).toBe(3); + expect(await component.evaluate((ctx) => [ctx.tmp[1], ctx.tmp[2], ctx.tmp[3]])).toEqual([0, 1, 2]); + }); + }); + }); +}; diff --git a/src/components/base/b-virtual-scroll/test/runners/events/data-change.js b/src/components/base/b-virtual-scroll/test/runners/events/data-change.js new file mode 100644 index 0000000000..d05883d4c5 --- /dev/null +++ b/src/components/base/b-virtual-scroll/test/runners/events/data-change.js @@ -0,0 +1,216 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +// @ts-check + +/** + * @typedef {import('playwright').Page} Page + */ + +const + h = include('tests/helpers').default; + +/** @param {Page} page */ +module.exports = (page) => { + let + component, + node, + container; + + const + getArray = (offset = 0, length = 12) => Array.from(Array(length), (v, i) => ({i: i + offset})), + firstChunkExpected = getArray(), + secondChunkExpected = getArray(12); + + const setProps = (requestProps = {}) => component.evaluate((ctx, requestProps) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.chunkSize = 10; + ctx.request = {get: {chunkSize: 12, id: Math.random(), ...requestProps}}; + }, requestProps); + + const subscribe = () => component.evaluate((ctx) => new Promise((res) => ctx.watch(':onDataChange', res))); + + beforeEach(async () => { + await page.evaluate(() => { + globalThis.removeCreatedComponents(); + + const baseAttrs = { + theme: 'demo', + option: 'section', + optionProps: ({current}) => ({'data-index': current.i}) + }; + + const scheme = [ + { + attrs: { + ...baseAttrs, + id: 'target' + } + } + ]; + + globalThis.renderComponents('b-virtual-scroll', scheme); + }); + + await h.bom.waitForIdleCallback(page); + await h.component.waitForComponentStatus(page, '.b-virtual-scroll', 'ready'); + + component = await h.component.waitForComponent(page, '#target'); + node = await h.dom.waitForEl(page, '#target'); + container = await h.dom.waitForRef(node, 'container'); + }); + + describe('b-virtual-scroll `dataChange` event', () => { + describe('emitted', () => { + it('after loading the first chunk', async () => { + const subscribePromise = subscribe(); + + await setProps(); + await expectAsync(subscribePromise).toBeResolved(); + }); + + it('after loading the second chunk', async () => { + await setProps(); + await h.dom.waitForEl(container, 'section'); + + const subscribePromise = subscribe(); + + await h.scroll.scrollToBottom(page); + await expectAsync(subscribePromise).toBeResolved(); + }); + + it('after loading the first part of the batch and stopping further loading because `shouldStopRequest` have returned `true`', async () => { + const subscribePromise = subscribe(); + + await component.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.request = {get: {chunkSize: 4, id: Math.random()}}; + ctx.shouldStopRequest = () => true; + }); + + await expectAsync(subscribePromise).toBeResolvedTo(getArray(0, 4)); + }); + + it('after loading the second part of the batch and stopping further loading because `shouldStopRequest` have returned `true`', async () => { + const subscribePromise = subscribe(); + + await component.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.request = {get: {chunkSize: 4, id: Math.random()}}; + ctx.shouldStopRequest = (v) => v.pendingData.length === 8; + }); + + await expectAsync(subscribePromise).toBeResolvedTo(getArray(0, 8)); + }); + + it('after loading the first part of the second batch and stopping further loading because `shouldStopRequest` have returned `true`', async () => { + await component.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.request = {get: {chunkSize: 4, id: Math.random()}}; + ctx.shouldStopRequest = (v) => { + const {lastLoadedChunk: {normalized}} = v; + return normalized[normalized.length - 1].i === 15; + }; + }); + + await h.dom.waitForEl(container, 'section'); + + const subscribePromise = subscribe(); + await h.scroll.scrollToBottom(page); + + await expectAsync(subscribePromise).toBeResolvedTo(getArray(12, 4)); + }); + + }); + + describe('not emitted', () => { + it('if there was a request error', async () => { + await component.evaluate((ctx) => ctx.watch(':onDataChange', () => ctx.tmp.change = true)); + + await setProps({failOn: 0}); + await h.bom.waitForIdleCallback(page, {sleepAfterIdles: 1000}); + + expect(await component.evaluate((ctx) => ctx.tmp.change)).toBeUndefined(); + }); + + it('if there was a request error on the second chunk', async () => { + await setProps({failOn: 1}); + await h.dom.waitForEl(container, 'section'); + + await component.evaluate((ctx) => ctx.watch(':onDataChange', () => ctx.tmp.change = true)); + + await h.scroll.scrollToBottom(page); + await h.bom.waitForIdleCallback(page, {sleepAfterIdles: 500}); + + expect(await component.evaluate((ctx) => ctx.tmp.change)).toBeUndefined(); + }); + }); + + describe('has correct payload', () => { + it('if nothing was loaded', async () => { + const subscribePromise = subscribe(); + + await setProps({total: 0, chunkSize: 0}); + await expectAsync(subscribePromise).toBeResolvedTo([]); + }); + + describe('after loading', () => { + it('first chunk', async () => { + const subscribePromise = subscribe(); + + await setProps({chunkSize: 12}); + await expectAsync(subscribePromise).toBeResolvedTo(firstChunkExpected); + }); + + it('second chunk', async () => { + await setProps({chunkSize: 12}); + await h.dom.waitForEl(container, 'section'); + + const subscribePromise = subscribe(); + + await h.scroll.scrollToBottom(page); + await expectAsync(subscribePromise).toBeResolvedTo(secondChunkExpected); + }); + }); + + describe('after re-initialization', () => { + it('and loading the first chunk with 2 requests', async () => { + await setProps({id: undefined}); + await h.dom.waitForEl(container, 'section'); + + const subscribePromise = subscribe(); + + await component.evaluate((ctx) => ctx.request = {get: {chunkSize: 6, id: Math.random()}}); + + await expectAsync(subscribePromise).toBeResolvedTo(firstChunkExpected); + }); + + it('and loading the second chunk with 2 requests', async () => { + await setProps({id: undefined}); + await h.dom.waitForEl(container, 'section'); + + await component.evaluate((ctx) => ctx.watch(':onDataChange', (val) => { + ctx.tmp.currentCall = ctx.tmp.currentCall ?? 0; + ctx.tmp[ctx.tmp.currentCall] = val; + ctx.tmp.currentCall++; + })); + + await component.evaluate((ctx) => ctx.request = {get: {chunkSize: 6, id: Math.random()}}); + await h.bom.waitForIdleCallback(page, {sleepAfterIdles: 1000}); + + expect(await component.evaluate((ctx) => ctx.tmp[0])).toEqual(firstChunkExpected); + + await h.scroll.scrollToBottom(page); + await h.bom.waitForIdleCallback(page, {sleepAfterIdles: 1000}); + + expect(await component.evaluate((ctx) => ctx.tmp[1])).toEqual(secondChunkExpected); + }); + }); + }); + }); +}; diff --git a/src/components/base/b-virtual-scroll/test/runners/events/db-change.js b/src/components/base/b-virtual-scroll/test/runners/events/db-change.js new file mode 100644 index 0000000000..b0780bc9a7 --- /dev/null +++ b/src/components/base/b-virtual-scroll/test/runners/events/db-change.js @@ -0,0 +1,184 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +// @ts-check + +/** + * @typedef {import('playwright').Page} Page + */ + +const + h = include('tests/helpers').default; + +/** @param {Page} page */ +module.exports = (page) => { + let + component, + node, + container; + + const + getArray = (offset = 0, length = 12) => ({data: Array.from(Array(length), (v, i) => ({i: i + offset}))}), + firstChunkExpected = getArray(); + + const setProps = (requestProps = {}) => component.evaluate((ctx, requestProps) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.chunkSize = 10; + ctx.request = {get: {chunkSize: 12, id: Math.random(), ...requestProps}}; + }, requestProps); + + const subscribe = () => component.evaluate((ctx) => new Promise((res) => ctx.watch(':onDBChange', res))); + + beforeEach(async () => { + await page.evaluate(() => { + globalThis.removeCreatedComponents(); + + const baseAttrs = { + theme: 'demo', + option: 'section', + optionProps: ({current}) => ({'data-index': current.i}) + }; + + const scheme = [ + { + attrs: { + ...baseAttrs, + id: 'target' + } + } + ]; + + globalThis.renderComponents('b-virtual-scroll', scheme); + }); + + await h.bom.waitForIdleCallback(page); + await h.component.waitForComponentStatus(page, '.b-virtual-scroll', 'ready'); + + component = await h.component.waitForComponent(page, '#target'); + node = await h.dom.waitForEl(page, '#target'); + container = await h.dom.waitForRef(node, 'container'); + }); + + describe('b-virtual-scroll `dbChange` event', () => { + describe('emitted', () => { + it('after loading the first chunk', async () => { + const subscribePromise = subscribe(); + await setProps(); + + await expectAsync(subscribePromise).toBeResolved(); + }); + + it('after loading the second chunk', async () => { + await setProps(); + await h.dom.waitForEl(container, 'section'); + + const subscribePromise = subscribe(); + + await h.scroll.scrollToBottom(page); + await expectAsync(subscribePromise).toBeResolved(); + }); + + it('after loading the first chunk after re-initialization', async () => { + await setProps(); + await h.dom.waitForEl(container, 'section'); + + const subscribePromise = subscribe(); + + await component.evaluate((ctx) => ctx.request = {get: {chunkSize: 20}}); + await expectAsync(subscribePromise).toBeResolved(); + }); + + it('three times to get the full data batch', async () => { + await component.evaluate((ctx) => { + ctx.watch(':onDBChange', () => { + ctx.tmp.called = (ctx.tmp.called ?? 0) + 1; + }); + }); + + await setProps({chunkSize: 4}); + await h.dom.waitForEl(container, 'section'); + + expect(await component.evaluate((ctx) => ctx.tmp.called)).toBe(3); + }); + + it('after successful loading of the first chunk without payload', async () => { + const subscribePromise = subscribe(); + + await setProps({chunkSize: 0, total: 0}); + + await expectAsync(subscribePromise).toBeResolved(); + }); + }); + + describe('not emitted', () => { + it('if there was a request error', async () => { + await component.evaluate((ctx) => { + ctx.watch(':onDBChange', () => { + ctx.tmp.called = (ctx.tmp.called ?? 0) + 1; + }); + }); + + await setProps({failOn: 0}); + await h.bom.waitForIdleCallback(page, {sleepAfterIdles: 500}); + + expect(await component.evaluate((ctx) => ctx.tmp.called)).toBeUndefined(); + }); + + it('after successful loading of the second chunk without payload', async () => { + await setProps({chunkSize: 12, total: 12}); + await h.dom.waitForEl(container, 'section'); + + await component.evaluate((ctx) => { + ctx.watch(':onDBChange', () => { + ctx.tmp.called = (ctx.tmp.called ?? 0) + 1; + }); + }); + + await h.scroll.scrollToBottom(page); + expect(await component.evaluate((ctx) => ctx.tmp.called)).toBeUndefined(); + }); + }); + + describe('has correct payload', () => { + it('after loading the first chunk', async () => { + const subscribePromise = subscribe(); + + await setProps({chunkSize: 4}); + await h.dom.waitForEl(container, 'section'); + + await expectAsync(subscribePromise).toBeResolvedTo(getArray(0, 4)); + }); + + it('after loading two chunks', async () => { + await component.evaluate((ctx) => { + ctx.watch(':onDBChange', (val) => { + ctx.tmp.called = (ctx.tmp.called ?? 0) + 1; + ctx.tmp[ctx.tmp.called] = Object.fastClone(val); + }); + }); + + await setProps({chunkSize: 6}); + await h.dom.waitForEl(container, 'section'); + + expect(await component.evaluate((ctx) => ctx.tmp[1])).toEqual(getArray(0, 6)); + expect(await component.evaluate((ctx) => ctx.tmp[2])).toEqual(getArray(0, 12)); + + }); + + it('after re-initialization and loading the first chunk', async () => { + await setProps({chunkSize: 6}); + await h.dom.waitForEl(container, 'section'); + + const subscribePromise = subscribe(); + + await setProps({chunkSize: 12, id: Math.random()}); + await expectAsync(subscribePromise).toBeResolvedTo(firstChunkExpected); + }); + }); + }); +}; diff --git a/src/components/base/b-virtual-scroll/test/runners/functional/items.js b/src/components/base/b-virtual-scroll/test/runners/functional/items.js new file mode 100644 index 0000000000..970b40be35 --- /dev/null +++ b/src/components/base/b-virtual-scroll/test/runners/functional/items.js @@ -0,0 +1,102 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +// @ts-check + +/** + * @typedef {import('playwright').Page} Page + */ + +const + h = include('tests/helpers').default; + +/** @param {Page} page */ +module.exports = (page) => { + let + node, + container, + component; + + const renderComponent = async (attrs = {}) => { + await page.evaluate(([attrs]) => { + globalThis.removeCreatedComponents(); + + const baseAttrs = { + theme: 'demo', + dataProvider: 'demo.Pagination', + chunkSize: 10, + request: {get: {chunkSize: 10, id: Math.random()}}, + item: 'section', + itemProps: ({current}) => ({'data-index': current.i}), + itemKey: (data) => data.current.i, + optionKey: (data) => data.current.i + }; + + const scheme = [ + { + attrs: { + ...baseAttrs, + id: 'target', + ...attrs + } + } + ]; + + globalThis.renderComponents('b-virtual-scroll', scheme); + }, [attrs]); + + node = await h.dom.waitForEl(page, '#target'); + component = await h.component.waitForComponent(page, '#target'); + container = await h.dom.waitForRef(node, 'container'); + }; + + beforeEach(async () => { + await h.component.waitForComponent(page, '#root-component'); + await page.evaluate(() => globalThis.removeCreatedComponents()); + }); + + describe('b-virtual-scroll with the `iItems` trait', () => { + it('renders a correct item', async () => { + await renderComponent(); + await h.dom.waitForEl(container, 'section'); + expect(await container.$('section')).toBeTruthy(); + }); + + it('renders an item with provided props', async () => { + await renderComponent(); + await h.dom.waitForEl(container, 'section'); + const attrVal = await (await container.$('section')).evaluate((el) => el.getAttribute('data-index')); + expect(parseInt(attrVal, 10)).toBe(0); + }); + + it('uses the deprecated `optionKey` property', async () => { + await renderComponent({ + itemKey: undefined + }); + + const optionKey1 = await component.evaluate((ctx) => ctx.getItemKey({current: {i: 0}})); + expect(optionKey1).toBe(0); + + const optionKey2 = await component.evaluate((ctx) => ctx.getItemKey({current: {i: 1}})); + expect(optionKey2).toBe(1); + }); + + it('uses the `itemKey` property', async () => { + await renderComponent({ + optionKey: undefined + }); + + const itemKey1 = await component.evaluate((ctx) => ctx.getItemKey({current: {i: 0}})); + expect(itemKey1).toBe(0); + + const itemKey2 = await component.evaluate((ctx) => ctx.getItemKey({current: {i: 1}})); + expect(itemKey2).toBe(1); + }); + }); + +}; diff --git a/src/components/base/b-virtual-scroll/test/runners/functional/render-next.js b/src/components/base/b-virtual-scroll/test/runners/functional/render-next.js new file mode 100644 index 0000000000..826fcb7725 --- /dev/null +++ b/src/components/base/b-virtual-scroll/test/runners/functional/render-next.js @@ -0,0 +1,102 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +// @ts-check + +/** + * @typedef {import('playwright').Page} Page + */ + +const + h = include('tests/helpers').default; + +/** @param {Page} page */ +module.exports = (page) => { + let + component, + node, + container; + + const + getContainerChildCount = () => component.evaluate((ctx) => ctx.$refs.container.childElementCount); + + beforeEach(async () => { + await page.evaluate(() => { + globalThis.removeCreatedComponents(); + + const baseAttrs = { + theme: 'demo', + option: 'section', + optionProps: ({current}) => ({'data-index': current.i}) + }; + + const scheme = [ + { + attrs: { + ...baseAttrs, + id: 'target' + } + } + ]; + + globalThis.renderComponents('b-virtual-scroll', scheme); + }); + + await h.bom.waitForIdleCallback(page); + await h.component.waitForComponentStatus(page, '.b-virtual-scroll', 'ready'); + + node = await h.dom.waitForEl(page, '#target'); + component = await h.component.waitForComponent(page, '#target'); + container = await h.dom.waitForRef(node, 'container'); + + await component.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.chunkSize = 10; + ctx.request = {get: {chunkSize: 10, id: Math.random()}}; + }); + }); + + describe('b-virtual-scroll', () => { + ['manual', 'scroll'].forEach((strategy) => { + describe(`renderNext with loadStrategy: ${strategy}`, () => { + it('renders the next data batch', async () => { + await component.evaluate((ctx, strategy) => { + ctx.loadStrategy = strategy; + ctx.request = {get: {chunkSize: 20, id: Math.random()}}; + }, strategy); + + await h.dom.waitForEl(container, 'section'); + + expect(await component.evaluate((ctx) => ctx.chunkRequest.pendingData.length)).toBe(10); + await component.evaluate((ctx) => ctx.renderNext()); + + expect(await component.evaluate((ctx) => ctx.chunkRequest.pendingData.length)).toBe(0); + await h.dom.waitForEl(container, 'section:nth-child(11)'); + + expect(await getContainerChildCount()).toBe(20); + }); + + it('requests and renders the next data batch', async () => { + await component.evaluate((ctx, strategy) => { + ctx.loadStrategy = strategy; + ctx.request = {get: {chunkSize: 10, id: Math.random()}}; + }, strategy); + + await h.dom.waitForEl(container, 'section'); + expect(await component.evaluate((ctx) => ctx.chunkRequest.pendingData.length)).toBe(0); + + await component.evaluate((ctx) => ctx.renderNext()); + await h.dom.waitForEl(container, 'section:nth-child(11)'); + + expect(await component.evaluate((ctx) => ctx.chunkRequest.pendingData.length)).toBe(0); + expect(await getContainerChildCount()).toBe(20); + }); + }); + }); + }); +}; diff --git a/src/components/base/b-virtual-scroll/test/runners/functional/state.js b/src/components/base/b-virtual-scroll/test/runners/functional/state.js new file mode 100644 index 0000000000..85d5e1ef9d --- /dev/null +++ b/src/components/base/b-virtual-scroll/test/runners/functional/state.js @@ -0,0 +1,263 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +// @ts-check + +/** + * @typedef {import('playwright').Page} Page + */ + +const + h = include('tests/helpers').default; + +/** @param {Page} page */ +module.exports = (page) => { + let + component, + node, + container; + + const + getArray = (offset = 0, length = 12) => ({data: Array.from(Array(length), (v, i) => ({i: i + offset}))}), + firstChunkExpected = getArray(), + secondChunkExpected = getArray(12); + + const getExpected = (params = {}) => ({ + items: undefined, + + itemsTillBottom: undefined, + currentPage: 0, + nextPage: 1, + isLastEmpty: false, + total: undefined, + + data: [], + pendingData: [], + + lastLoadedData: [], + lastLoadedChunk: { + raw: undefined, + normalized: [] + }, + ...params + }); + + const getCurrentComponentState = () => component.evaluate((ctx) => ({ + ...ctx.getCurrentDataState(), + itemsTillBottom: undefined, + items: undefined + })); + + const setProps = (requestProps = {}) => component.evaluate((ctx, requestProps) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.chunkSize = 10; + ctx.request = {get: {chunkSize: 12, id: Math.random(), ...requestProps}}; + }, requestProps); + + const + initialTimeout = globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL; + + beforeAll(() => { + globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL = (20).seconds(); + }); + + afterAll(() => { + globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL = initialTimeout; + }); + + beforeEach(async () => { + await h.utils.reloadAndWaitForIdle(page); + await h.component.waitForComponent(page, '#root-component'); + + await page.evaluate(() => { + globalThis.removeCreatedComponents(); + + const baseAttrs = { + theme: 'demo', + option: 'section', + optionProps: ({current}) => ({'data-index': current.i}) + }; + + const scheme = [ + { + attrs: { + ...baseAttrs, + id: 'target' + } + } + ]; + + globalThis.renderComponents('b-virtual-scroll', scheme); + }); + + await h.bom.waitForIdleCallback(page); + await h.component.waitForComponentStatus(page, '.b-virtual-scroll', 'ready'); + + component = await h.component.waitForComponent(page, '#target'); + node = await h.dom.waitForEl(page, '#target'); + container = await h.dom.waitForRef(node, 'container'); + }); + + describe('b-virtual-scroll `getCurrentDataState`', () => { + describe('returns the correct value', () => { + it('if there is no `dataProvider`', async () => { + const + expected = getExpected(), + current = await getCurrentComponentState(); + + expect(current).toEqual(expected); + }); + + it('after loading the first chunk', async () => { + const expected = getExpected({ + currentPage: 1, + nextPage: 2, + data: firstChunkExpected.data, + pendingData: getArray(10, 2).data, + lastLoadedData: firstChunkExpected.data, + lastLoadedChunk: { + raw: firstChunkExpected, + normalized: firstChunkExpected.data + } + }); + + await setProps(); + await h.dom.waitForEl(container, 'section'); + + const current = await getCurrentComponentState(); + expect(current).toEqual(expected); + }); + + it('after loading the second chunk', async () => { + const expected = getExpected({ + currentPage: 2, + nextPage: 3, + data: getArray(0, 24).data, + pendingData: getArray(20, 4).data, + lastLoadedData: secondChunkExpected.data, + lastLoadedChunk: { + raw: secondChunkExpected, + normalized: secondChunkExpected.data + } + }); + + await setProps(); + await h.dom.waitForEl(container, 'section'); + + await h.scroll.scrollToBottom(page); + await h.dom.waitForEl(container, 'section:nth-child(11)'); + + const current = await getCurrentComponentState(); + expect(current).toEqual(expected); + }); + + it('after re-initialization and without `dataProvider`', async () => { + const expected = getExpected({currentPage: 0, nextPage: 1}); + + await setProps(); + await h.dom.waitForEl(container, 'section'); + + await component.evaluate((ctx) => { + ctx.dataProvider = ''; + ctx.request = undefined; + ctx.reInit(); + }); + + await h.dom.waitForEl(container, 'section', {to: 'unmount'}); + await h.bom.waitForIdleCallback(page, {sleepAfterIdles: 1000}); + + const current = await getCurrentComponentState(); + expect(current).toEqual(expected); + }); + + it('after re-initialization and with `dataProvider`', async () => { + const expected = getExpected({ + currentPage: 1, + nextPage: 2, + data: firstChunkExpected.data, + pendingData: getArray(10, 2).data, + lastLoadedData: firstChunkExpected.data, + lastLoadedChunk: { + raw: firstChunkExpected, + normalized: firstChunkExpected.data + } + }); + + await setProps(); + await h.dom.waitForEl(container, 'section'); + + await setProps({id: Math.random()}); + await h.bom.waitForIdleCallback(page); + await h.dom.waitForEl(container, 'section', {to: 'mount'}); + + const current = await getCurrentComponentState(); + expect(current).toEqual(expected); + }); + + it('if for the full loading it was necessary to go several times to `dataProvider`', async () => { + const expected = getExpected({ + currentPage: 3, + nextPage: 4, + data: firstChunkExpected.data, + pendingData: getArray(10, 2).data, + lastLoadedData: getArray(8, 4).data, + lastLoadedChunk: { + raw: getArray(8, 4), + normalized: getArray(8, 4).data + } + }); + + await setProps({chunkSize: 4}); + await h.dom.waitForEl(container, 'section'); + + const current = await getCurrentComponentState(); + expect(current).toEqual(expected); + }); + }); + }); + + describe('b-virtual-scroll `getDataStateSnapshot`', () => { + describe('returns the correct value', () => { + it('with `chunkRequest` and `chunkRender`', async () => { + const + expected = getExpected(), + current = await component.evaluate((ctx) => ctx.getDataStateSnapshot({ + items: undefined, + itemsTillBottom: undefined + })); + + expect(current).toEqual(expected); + }); + + it('with `chunkRequest`', async () => { + const + expected = getExpected(), + current = await component.evaluate((ctx) => ctx.getDataStateSnapshot({ + items: undefined, + itemsTillBottom: undefined + }, ctx.chunkRequest)); + + expect(current).toEqual(expected); + }); + + it('with override params, `chunkRequest` and `chunkRender`', async () => { + const expected = getExpected({ + currentPage: 1, + nextPage: 2 + }); + + const current = await component.evaluate((ctx) => ctx.getDataStateSnapshot({ + items: undefined, + itemsTillBottom: undefined + }, ctx.chunkRequest, ctx.chunkRender)); + + expect(current).toEqual(expected); + }); + }); + }); +}; diff --git a/src/components/base/b-virtual-scroll/test/runners/render/render.js b/src/components/base/b-virtual-scroll/test/runners/render/render.js new file mode 100644 index 0000000000..b2bc6574fd --- /dev/null +++ b/src/components/base/b-virtual-scroll/test/runners/render/render.js @@ -0,0 +1,205 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +// @ts-check + +/** + * @typedef {import('playwright').Page} Page + */ + +const + h = include('tests/helpers').default; + +/** @param {Page} page */ +module.exports = (page) => { + let + component, + node, + container; + + const + getContainerChildCount = () => component.evaluate((ctx) => ctx.$refs.container.childElementCount); + + const setProps = async (reqParams) => { + await component.evaluate((ctx, reqParams) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.chunkSize = 10; + ctx.request = {get: {chunkSize: 10, id: Math.random(), ...reqParams}}; + }, reqParams); + + await h.dom.waitForEl(container, 'section'); + }; + + const + initialTimeout = globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL; + + beforeAll(() => { + globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL = (20).seconds(); + }); + + afterAll(() => { + globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL = initialTimeout; + }); + + beforeEach(async () => { + await page.evaluate(() => { + globalThis.removeCreatedComponents(); + + const baseAttrs = { + theme: 'demo', + option: 'section', + optionProps: ({current}) => ({'data-index': current.i}) + }; + + const scheme = [ + { + attrs: { + ...baseAttrs, + id: 'target' + } + } + ]; + + globalThis.renderComponents('b-virtual-scroll', scheme); + globalThis.componentNode = document.querySelector('.b-virtual-scroll'); + }); + + await h.bom.waitForIdleCallback(page); + await h.component.waitForComponentStatus(page, '.b-virtual-scroll', 'ready'); + + node = await h.dom.waitForEl(page, '#target'); + component = await h.component.waitForComponent(page, '#target'); + container = await h.dom.waitForRef(node, 'container'); + }); + + describe('b-virtual-scroll rendering', () => { + describe('after re-initialization', () => { + describe('by changing the `request` prop', () => { + it('removes old elements', async () => { + await setProps(); + + await component.evaluate((ctx) => ctx.request = {get: {chunkSize: 10, total: 0}}); + await h.dom.waitForEl(container, 'section', {to: 'unmount'}); + + expect(await component.evaluate((ctx) => ctx.$refs.container.childElementCount === 0)).toBeTrue(); + }); + + it('renders new', async () => { + await setProps(); + + const + chunkSize = await component.evaluate((ctx) => ctx.requestParams.get.chunkSize); + + await h.dom.waitForEl(container, `section:nth-child(${chunkSize - 1})`); + expect(await getContainerChildCount()).toBe(chunkSize); + + await component.evaluate((ctx) => ctx.request = {get: {chunkSize: 4, total: 4, id: 'uniq-options'}}); + + await h.dom.waitForEl(container, 'section', {to: 'unmount'}); + await h.dom.waitForEl(container, 'section'); + + const + newChunkSize = await component.evaluate((ctx) => ctx.requestParams.get.chunkSize); + + await h.dom.waitForEl(container, `section:nth-child(${newChunkSize - 1})`); + expect(await getContainerChildCount()).toBe(newChunkSize); + }); + }); + + describe('by changing the `request` prop while second data batch loading is in progress', () => { + it('should render first chunk with correct data', async () => { + await component.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.chunkSize = 2; + ctx.request = {get: {chunkSize: 2, delay: 1500, id: Math.random()}}; + }); + + await h.dom.waitForEl(container, 'section'); + + await component.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.chunkSize = 2; + ctx.request = {get: {chunkSize: 2, i: 10, total: 2, delay: 1500, id: Math.random()}}; + }); + + expect(await h.dom.waitForEl(container, '[data-index="10"]')); + expect(await getContainerChildCount()).toBe(2); + }); + }); + + describe('by changing the `dataProvider` prop', () => { + it('removes old elements', async () => { + await setProps(); + + await component.evaluate((ctx) => ctx.dataProvider = undefined); + await h.dom.waitForEl(container, 'section', {to: 'unmount'}); + + expect(await component.evaluate((ctx) => ctx.$refs.container.childElementCount === 0)).toBeTrue(); + }); + + it('renders new', async () => { + await h.dom.waitForEl(container, 'section', {to: 'unmount'}); + expect(await component.evaluate((ctx) => ctx.$refs.container.childElementCount === 0)).toBeTrue(); + + await component.evaluate((ctx) => ctx.dataProvider = 'demo.Pagination'); + await h.dom.waitForEl(container, 'section'); + + expect(await getContainerChildCount()).toBeGreaterThan(0); + }); + }); + }); + + describe('with `items`', () => { + it('renders the first chunk', async () => { + const + chunkSize = await component.evaluate((ctx) => ctx.chunkSize); + + await component.evaluate((ctx) => ctx.items = Array.from(Array(40), (v, i) => ({i}))); + await h.dom.waitForEl(container, 'section'); + + expect(await getContainerChildCount()).toBe(chunkSize); + }); + + it('renders all available `items`', async () => { + await component.evaluate((ctx) => ctx.items = Array.from(Array(40), (v, i) => ({i}))); + await h.dom.waitForEl(container, 'section'); + + const + total = await component.evaluate((ctx) => ctx.items.length), + checkFn = async () => await getContainerChildCount() === total; + + await h.scroll.scrollToBottomWhile(page, checkFn, {timeout: 1e5}); + expect(await getContainerChildCount()).toBe(total); + }); + + it('does not render more than received data', async () => { + await component.evaluate((ctx) => ctx.items = Array.from(Array(40), (v, i) => ({i}))); + await h.dom.waitForEl(container, 'section'); + + const + total = await component.evaluate((ctx) => ctx.items.length), + checkFn = async () => await getContainerChildCount() === total; + + await h.scroll.scrollToBottomWhile(page, checkFn, {timeout: 1e5}); + expect(await getContainerChildCount()).toBe(total); + + await h.bom.waitForIdleCallback(page); + await h.scroll.scrollToBottom(page); + expect(await getContainerChildCount()).toBe(total); + }); + }); + + describe('without `items` and` dataProvider` specified', () => { + it('does not render anything', async () => { + expect(await component.evaluate((ctx) => ctx.items.length === 0)).toBeTrue(); + expect(await component.evaluate((ctx) => ctx.dataProvider === undefined)).toBeTrue(); + expect(await component.evaluate((ctx) => ctx.$refs.container.childElementCount === 0)).toBeTrue(); + }); + }); + }); +}; diff --git a/src/components/base/b-virtual-scroll/test/runners/slots/empty.js b/src/components/base/b-virtual-scroll/test/runners/slots/empty.js new file mode 100644 index 0000000000..7cc73ee19a --- /dev/null +++ b/src/components/base/b-virtual-scroll/test/runners/slots/empty.js @@ -0,0 +1,123 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +// @ts-check + +/** + * @typedef {import('playwright').Page} Page + */ + +const + h = include('tests/helpers').default; + +/** @param {Page} page */ +module.exports = (page) => { + const components = { + emptyWithSlot: undefined, + emptyNoSlot: undefined, + emptyWithData: undefined + }; + + const nodes = { + emptyWithSlot: undefined, + emptyNoSlot: undefined, + emptyWithData: undefined + }; + + beforeAll(async () => { + await page.evaluate(() => { + const baseAttrs = { + theme: 'demo', + option: 'section', + optionProps: ({current}) => ({'data-index': current.i}) + }; + + const slots = { + empty: { + tag: 'div', + attrs: { + id: 'empty', + 'data-test-ref': 'empty' + }, + content: 'Empty' + } + }; + + const scheme = [ + { + attrs: { + ...baseAttrs, + dataProvider: 'demo.Pagination', + dbConverter: ({data}) => ({data: data.splice(0, 4)}), + id: 'emptyNoSlot' + } + }, + { + attrs: { + ...baseAttrs, + dataProvider: 'demo.Pagination', + dbConverter: ({data}) => ({data: data.splice(0, 4)}), + request: {get: {chunkSize: 8, total: 8}}, + id: 'emptyWithData' + }, + + content: { + empty: slots.empty + } + }, + { + attrs: { + ...baseAttrs, + dataProvider: 'demo.Pagination', + dbConverter: () => ({data: []}), + id: 'emptyWithSlot' + }, + + content: { + empty: slots.empty + } + } + ]; + + globalThis.renderComponents('b-virtual-scroll', scheme); + }); + + await h.bom.waitForIdleCallback(page); + + for (let keys = Object.keys(components), i = 0; i < keys.length; i++) { + const key = keys[i]; + + nodes[key] = await h.dom.waitForEl(page, `#${key}`); + await nodes[key].evaluate((ctx) => ctx.style.display = ''); + + // eslint-disable-next-line require-atomic-updates + components[key] = await h.component.getComponentById(page, key); + } + }); + + describe('b-virtual-scroll slot empty', () => { + describe('does not render `empty slot`', () => { + it('if it is not set', async () => { + expect(await components.emptyNoSlot.evaluate((ctx) => Boolean(ctx.$slots['empty']))).toBe(false); + expect(await h.dom.getRef(nodes.emptyNoSlot, 'empty')).toBeFalsy(); + }); + + it('if there is data', async () => { + expect(await components.emptyWithData.evaluate((ctx) => Boolean(ctx.$slots['empty']))).toBe(true); + expect(await nodes.emptyWithData.waitForSelector('#empty', {state: 'hidden'})).toBeFalsy(); + }); + }); + + describe('render `empty slot`', () => { + it('if it is set and there is no data', async () => { + expect(await components.emptyWithSlot.evaluate((ctx) => Boolean(ctx.$slots['empty']))).toBe(true); + expect(await h.dom.getRef(nodes.emptyWithSlot, 'empty')).toBeTruthy(); + }); + }); + }); +}; diff --git a/src/components/base/b-virtual-scroll/test/runners/slots/render-next.js b/src/components/base/b-virtual-scroll/test/runners/slots/render-next.js new file mode 100644 index 0000000000..8316b3ec40 --- /dev/null +++ b/src/components/base/b-virtual-scroll/test/runners/slots/render-next.js @@ -0,0 +1,498 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/* eslint-disable max-lines-per-function */ +// @ts-check + +/** + * @typedef {import('playwright').Page} Page + */ + +const + h = include('tests/helpers').default; + +/** @param {Page} page */ +module.exports = (page) => { + const components = { + renderNextWithSlot: undefined, + renderNextNoSlot: undefined + }; + + const nodes = { + renderNextWithSlot: undefined, + renderNextNoSlot: undefined + }; + + const containers = { + renderNextWithSlot: undefined, + renderNextNoSlot: undefined + }; + + const isNotHidden = async (selector, ctx) => { + const + el = await ctx.$(selector), + state = await el.evaluate((ctx) => ctx.parentNode.style.display); + + return state === ''; + }; + + const + initialTimeout = globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL; + + beforeAll(() => { + globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL = (20).seconds(); + }); + + beforeEach(async () => { + await h.utils.reloadAndWaitForIdle(page); + await h.component.waitForComponent(page, '#root-component'); + + await page.evaluate(() => { + const dummy = document.querySelector('#dummy-component'); + + if (dummy) { + document.querySelector('#dummy-component').remove(); + } + + globalThis.removeCreatedComponents(); + + const baseAttrs = { + theme: 'demo', + option: 'section', + optionProps: ({current}) => ({'data-index': current.i}) + }; + + const slots = { + renderNext: { + tag: 'div', + attrs: { + id: 'renderNext', + 'data-test-ref': 'renderNext' + } + } + }; + + const scheme = [ + { + attrs: { + ...baseAttrs, + dataProvider: 'demo.Pagination', + loadStrategy: 'manual', + id: 'renderNextNoSlot' + } + }, + + { + attrs: { + ...baseAttrs, + dataProvider: 'demo.Pagination', + loadStrategy: 'manual', + id: 'renderNextWithSlot' + }, + + content: { + renderNext: slots.renderNext + } + } + ]; + + globalThis.renderComponents('b-virtual-scroll', scheme, '.p-v4-components-demo'); + }); + + await h.bom.waitForIdleCallback(page); + await h.component.waitForComponentStatus(page, '.b-virtual-scroll', 'ready'); + + const + allComponents = await page.$$('.b-virtual-scroll'); + + for (let i = 0; i < allComponents.length; i++) { + await allComponents[i].evaluate((ctx) => ctx.style.display = 'none'); + } + + for (let keys = Object.keys(components), i = 0; i < keys.length; i++) { + const key = keys[i]; + + nodes[key] = await h.dom.waitForEl(page, `#${key}`); + await nodes[key].evaluate((ctx) => ctx.style.display = ''); + containers[key] = await h.dom.waitForRef(nodes[key], 'container'); + + // eslint-disable-next-line require-atomic-updates + components[key] = await h.component.getComponentById(page, key); + } + + await components.renderNextWithSlot.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.request = {get: {}}; + ctx.shouldStopRequest = (v) => v.isLastEmpty; + + return new Promise((res) => { + if (ctx.isReady) { + return res(); + } + + ctx.localEmitter.on('localState.ready', res); + }); + }); + + await h.bom.waitForIdleCallback(page); + }); + + afterAll(() => { + globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL = initialTimeout; + }); + + describe('b-virtual-scroll `renderNext` slot', () => { + describe('not render', () => { + it('if it is not set', async () => { + expect(await components.renderNextNoSlot.evaluate((ctx) => Boolean(ctx.$slots['empty']))).toBe(false); + expect(await h.dom.getRef(nodes.renderNextNoSlot, 'empty')).toBeFalsy(); + }); + + it('there are no loaded data', async () => { + await components.renderNextWithSlot.evaluate((ctx) => { + ctx.dbConverter = () => ({data: []}); + ctx.request = {get: {total: 0, chunkSize: 0, id: Math.random()}}; + + return new Promise((res) => { + ctx.localEmitter.on('localState.ready', res); + }); + }); + + await h.bom.waitForIdleCallback(page); + await h.bom.waitForRAF(page); + + expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeFalse(); + }); + + it('there are no data', async () => { + await components.renderNextWithSlot.evaluate((ctx) => { + ctx.dataProvider = undefined; + ctx.items = []; + + return new Promise((res) => { + ctx.localEmitter.on('localState.ready', res); + }); + }); + + await h.bom.waitForRAF(page); + expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeFalse(); + }); + + it('if initial loading in progress', async () => { + await components.renderNextWithSlot.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.request = {get: {chunkSize: 10, sleep: 1000}}; + }); + + await h.bom.waitForIdleCallback(page); + await h.bom.waitForRAF(page); + + await expectAsync(page.waitForFunction(() => { + const + node = document.querySelector('#renderNext'), + parent = node.parentElement; + + return parent.style.display === 'none'; + })).toBeResolved(); + }); + + it('if the second batch of data loading in progress', async () => { + await components.renderNextWithSlot.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.request = {get: {total: 20, chunkSize: 10, id: Math.random(), sleep: 500}}; + ctx.chunkSize = 10; + }); + + await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); + await h.scroll.scrollToBottom(page); + await h.dom.waitForRef(nodes.renderNextWithSlot, 'renderNext'); + await components.renderNextWithSlot.evaluate((ctx) => ctx.renderNext()); + await h.bom.waitForRAF(page); + + expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeFalse(); + }); + + it('if all data were loaded after the initial request', async () => { + await components.renderNextWithSlot.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.request = {get: {total: 10, chunkSize: 10, id: Math.random()}}; + ctx.shouldStopRequest = () => true; + ctx.chunkSize = 10; + }); + + await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); + await h.bom.waitForIdleCallback(page); + + expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeFalse(); + }); + + it('if all data were loaded after the second batch load', async () => { + await components.renderNextWithSlot.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.request = {get: {total: 20, chunkSize: 10, id: Math.random(), sleep: 50}}; + ctx.shouldStopRequest = ({data}) => data.length === 20; + ctx.chunkSize = 10; + }); + + await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); + await h.dom.waitForRef(nodes.renderNextWithSlot, 'renderNext'); + await components.renderNextWithSlot.evaluate((ctx) => ctx.renderNext()); + + await h.dom.waitForEl(containers.renderNextWithSlot, 'section:nth-child(11)'); + await h.bom.waitForIdleCallback(page); + await h.bom.waitForRAF(page); + + expect(await (await nodes.renderNextWithSlot.$('#renderNext')).evaluate((ctx) => ctx.parentNode.style.display)).toBe('none'); + }); + + it('if all items were rendered', async () => { + await components.renderNextWithSlot.evaluate((ctx) => { + ctx.dataProvider = undefined; + ctx.chunkSize = 10; + // @ts-ignore + ctx.items = Array.from(Array(10), (v, i) => ({i})); + }); + + await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); + await h.bom.waitForRAF(page); + expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeFalse(); + }); + + it('if all items were rendered after second render', async () => { + await components.renderNextWithSlot.evaluate((ctx) => { + ctx.dataProvider = undefined; + ctx.chunkSize = 10; + ctx.items = Array.from(Array(20), (v, i) => ({i})); + }); + + await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); + await h.dom.waitForRef(nodes.renderNextWithSlot, 'renderNext'); + await components.renderNextWithSlot.evaluate((ctx) => ctx.renderNext()); + await h.dom.waitForEl(containers.renderNextWithSlot, 'section:nth-child(11)'); + await h.bom.waitForIdleCallback(page); + await h.bom.waitForRAF(page); + + expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeFalse(); + }); + + it('if all data were rendered and loaded', async () => { + await components.renderNextWithSlot.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.shouldStopRequest = ({data}) => data.length === 80; + ctx.request = {get: {total: 80, chunkSize: 40, id: Math.random()}}; + ctx.chunkSize = 20; + + return new Promise((res) => { + ctx.localEmitter.once('localState.ready', res); + }); + }); + + await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); + + let + renders = 1; + + const + totalRenders = 4; + + while (renders < totalRenders) { + await h.dom.waitForRef(nodes.renderNextWithSlot, 'renderNext'); + await components.renderNextWithSlot.evaluate((ctx) => ctx.renderNext()); + await h.dom.waitForEl(containers.renderNextWithSlot, `section:nth-child(${(renders * 20) + 1})`); + await h.bom.waitForIdleCallback(page); + await h.bom.waitForRAF(page); + + renders++; + } + + expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeFalse(); + }); + + it('if an error appears on the initial loading', async () => { + await components.renderNextWithSlot.evaluate((ctx) => { + const p = new Promise((res) => { + ctx.watch(':onRequestError', res); + }); + + ctx.dataProvider = 'demo.Pagination'; + ctx.request = {get: {total: 20, chunkSize: 10, id: Math.random(), failOn: 0, sleep: 500}}; + ctx.chunkSize = 10; + + return p; + }); + + await h.bom.waitForIdleCallback(page); + expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeFalse(); + }); + + it('if an error appears on the second data batch loading', async () => { + const requestErrorPromise = components.renderNextWithSlot.evaluate((ctx) => new Promise((res) => { + ctx.watch(':requestError', res); + })); + + await components.renderNextWithSlot.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.request = {get: {total: 40, chunkSize: 10, id: Math.random(), failOn: 1, sleep: 50}}; + ctx.chunkSize = 10; + }); + + await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); + await h.dom.waitForRef(nodes.renderNextWithSlot, 'renderNext'); + + await components.renderNextWithSlot.evaluate((ctx) => ctx.renderNext()); + await requestErrorPromise; + + await h.bom.waitForIdleCallback(page); + await h.bom.waitForRAF(page); + + expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeFalse(); + }); + }); + + describe('render', () => { + it('after initial loading', async () => { + await components.renderNextWithSlot.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.request = {get: {total: 20, chunkSize: 10, id: Math.random()}}; + ctx.chunkSize = 10; + + return new Promise((res) => { + ctx.localEmitter.on('localState.ready', res); + }); + }); + + await h.bom.waitForIdleCallback(page); + expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeTrue(); + }); + + it('after loading of the second data batch', async () => { + await components.renderNextWithSlot.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.request = {get: {total: 40, chunkSize: 10, id: Math.random()}}; + ctx.chunkSize = 10; + + return new Promise((res) => { + ctx.localEmitter.on('localState.ready', res); + }); + }); + + await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); + await h.dom.waitForRef(nodes.renderNextWithSlot, 'renderNext'); + await components.renderNextWithSlot.evaluate((ctx) => ctx.renderNext()); + await h.dom.waitForEl(containers.renderNextWithSlot, 'section:nth-child(11)'); + await h.bom.waitForIdleCallback(page); + await h.bom.waitForRAF(page); + + expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeTrue(); + }); + + it('after the initial rendering with items provided', async () => { + await components.renderNextWithSlot.evaluate((ctx) => { + ctx.dataProvider = undefined; + // @ts-ignore + ctx.items = Array.from(Array(20), (v, i) => ({i})); + }); + + await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); + await h.bom.waitForIdleCallback(page); + + expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeTrue(); + }); + + it('after the second rendering with items provided', async () => { + await components.renderNextWithSlot.evaluate((ctx) => { + ctx.dataProvider = undefined; + // @ts-ignore + ctx.items = Array.from(Array(40), (v, i) => ({i})); + }); + + await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); + await h.dom.waitForRef(nodes.renderNextWithSlot, 'renderNext'); + await components.renderNextWithSlot.evaluate((ctx) => ctx.renderNext()); + await h.dom.waitForEl(containers.renderNextWithSlot, 'section:nth-child(11)'); + await h.bom.waitForRAF(page); + + expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeTrue(); + }); + + it('until all data are rendered', async () => { + await components.renderNextWithSlot.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.shouldStopRequest = ({data}) => data.length === 60; + ctx.request = {get: {total: 60, chunkSize: 30, id: Math.random()}}; + ctx.chunkSize = 10; + + return new Promise((res) => { + ctx.localEmitter.on('localState.ready', res); + }); + }); + + await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); + + let + renders = 1; + + const + totalRenders = 5; + + while (renders < totalRenders) { + await h.dom.waitForRef(nodes.renderNextWithSlot, 'renderNext'); + await components.renderNextWithSlot.evaluate((ctx) => ctx.renderNext()); + await h.dom.waitForEl(containers.renderNextWithSlot, `section:nth-child(${(renders * 10) + 1})`); + await h.bom.waitForRAF(page); + + expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeTrue(); + renders++; + } + }); + + it('if there was an error on the initial loading, but after retrying all fine', async () => { + const requestErrorPromise = components.renderNextWithSlot.evaluate((ctx) => new Promise((res) => { + ctx.watch(':requestError', res); + })); + + await components.renderNextWithSlot.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.request = {get: {total: 40, chunkSize: 10, id: Math.random(), failOn: 0, failCount: 1, sleep: 50}}; + ctx.chunkSize = 10; + }); + + await requestErrorPromise; + await components.renderNextWithSlot.evaluate((ctx) => ctx.reloadLast()); + + await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); + await h.bom.waitForRAF(page); + + expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeTrue(); + }); + + it('if there was an error on the second data batch loading, but after retrying all fine', async () => { + const requestErrorPromise = components.renderNextWithSlot.evaluate((ctx) => new Promise((res) => { + ctx.watch(':requestError', res); + })); + + await components.renderNextWithSlot.evaluate((ctx) => { + ctx.dataProvider = 'demo.Pagination'; + ctx.request = {get: {total: 40, chunkSize: 10, id: Math.random(), failOn: 1, failCount: 1, sleep: 50}}; + ctx.chunkSize = 10; + }); + + await h.dom.waitForEl(containers.renderNextWithSlot, 'section'); + + await components.renderNextWithSlot.evaluate((ctx) => ctx.renderNext()); + await requestErrorPromise; + await h.bom.waitForIdleCallback(page); + await components.renderNextWithSlot.evaluate((ctx) => ctx.reloadLast()); + await h.dom.waitForEl(containers.renderNextWithSlot, 'section:nth-child(11)'); + await h.bom.waitForRAF(page); + + expect(await isNotHidden('#renderNext', nodes.renderNextWithSlot)).toBeTrue(); + }); + }); + }); +}; diff --git a/src/components/base/b-virtual-scroll/test/unit/render.ts b/src/components/base/b-virtual-scroll/test/unit/render.ts new file mode 100644 index 0000000000..86f2eb133c --- /dev/null +++ b/src/components/base/b-virtual-scroll/test/unit/render.ts @@ -0,0 +1,139 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import test from 'tests/config/unit/test'; + +import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scroll'; + +import Component from 'tests/helpers/component'; +import Scroll from 'tests/helpers/scroll'; +import BOM from 'tests/helpers/bom'; +import { interceptPaginationRequest } from 'tests/helpers/providers/pagination'; + +test.describe('b-virtual-scroll render', () => { + + const baseAttrs = { + theme: 'demo', + item: 'section', + id: 'target', + itemProps: ({current}) => ({'data-index': current.i}) + }; + + const providerProps = (reqParams = {}) => ({ + dataProvider: 'Provider', + chunkSize: 10, + request: {get: {chunkSize: 10, id: Math.random(), ...reqParams}} + }); + + const attrs = (attrs = {}) => ({attrs: { + ...baseAttrs, + ...attrs + }}); + + const + sectionSelector = '.b-virtual-scroll__container section', + buttonSelector = '.b-virtual-scroll__container button', + getContainerChildCount = (c) => c.evaluate((ctx) => ctx.$refs.container.childElementCount); + + test.beforeEach(async ({context, demoPage}) => { + await interceptPaginationRequest(context); + await demoPage.goto(); + }); + + test.describe('with `dataProvider`', () => { + test('renders the first chunk', async ({page}) => { + const + component = await Component.createComponent(page, 'b-virtual-scroll', attrs(providerProps())), + chunkSize = await component.evaluate((ctx) => ctx.chunkSize); + + await page.waitForSelector(sectionSelector, {state: 'attached'}); + test.expect(await getContainerChildCount(component)).toBe(chunkSize); + }); + + test('renders b-button', async ({page}) => { + const + component = await Component.createComponent(page, 'b-virtual-scroll', attrs({...providerProps(), item: 'b-button'})), + chunkSize = await component.evaluate((ctx) => ctx.chunkSize); + + await page.waitForSelector(buttonSelector, {state: 'attached'}); + test.expect(await getContainerChildCount(component)).toBe(chunkSize); + }); + + test('renders all available items', async ({page}) => { + const + component = await Component.createComponent(page, 'b-virtual-scroll', attrs(providerProps({total: 40}))); + + const + total = await component.evaluate((ctx) => ctx.field.get('requestParams.get.total')), + checkFn = async () => await getContainerChildCount(component) === total; + + await Scroll.scrollToBottomWhile(page, checkFn, {timeout: 1e5}); + + test.expect(await getContainerChildCount(component)).toBe(total); + }); + + test('does not render more than received data', async ({page}) => { + const + component = await Component.createComponent(page, 'b-virtual-scroll', attrs(providerProps({total: 40}))); + + const + total = await component.evaluate((ctx) => ctx.field.get('requestParams.get.total')), + checkFn = async () => await getContainerChildCount(component) === total; + + await Scroll.scrollToBottomWhile(page, checkFn, {timeout: 1e5}); + test.expect(await getContainerChildCount(component)).toBe(total); + + await BOM.waitForIdleCallback(page); + await Scroll.scrollToBottom(page); + test.expect(await getContainerChildCount(component)).toBe(total); + }); + + test('renders the first chunk with 3 requests to get the full chunk', async ({page}) => { + const + component = await Component.createComponent(page, 'b-virtual-scroll', attrs(providerProps({chunkSize: 4}))); + + const + chunkSize = await component.evaluate((ctx) => ctx.chunkSize); + + await page.waitForSelector(sectionSelector, {state: 'attached'}); + + test.expect(await getContainerChildCount(component)).toBe(chunkSize); + }); + + test('renders the first chunk with truncated data in all loaded chunks', async ({page}) => { + const component = await Component.createComponent(page, 'b-virtual-scroll', attrs({ + dataProvider: 'Provider', + chunkSize: 4, + request: {get: {chunkSize: 8, total: 32, id: 'uniq'}}, + dbConverter: ({data}) => ({data: data.splice(0, 1)}) + })); + + const + chunkSize = await component.evaluate((ctx) => ctx.chunkSize); + + await page.waitForSelector(sectionSelector, {state: 'attached'}); + + test.expect(await getContainerChildCount(component)).toBe(chunkSize); + }); + + test('renders all data if `shouldStopRequest` returns true', async ({page}) => { + const component = await Component.createComponent(page, 'b-virtual-scroll', attrs({ + dataProvider: 'Provider', + chunkSize: 10, + request: {get: {chunkSize: 40, total: 80, id: Math.random(), delay: 100}}, + shouldStopRequest: ({data}) => data.length === 80 + })); + + const + checkFn = async () => await getContainerChildCount(component) === 80; + + await Scroll.scrollToBottomWhile(page, checkFn, {timeout: 1e5}); + test.expect(await getContainerChildCount(component)).toBe(80); + }); + }); +}); diff --git a/src/components/pages/p-v4-components-demo/index.js b/src/components/pages/p-v4-components-demo/index.js index ec8982afc2..2aa7c0573c 100644 --- a/src/components/pages/p-v4-components-demo/index.js +++ b/src/components/pages/p-v4-components-demo/index.js @@ -19,6 +19,7 @@ package('p-v4-components-demo') 'b-tree', 'b-window', 'b-virtual-scroll', + 'b-virtual-scroll-new', 'b-bottom-slide', 'b-slider', 'b-sidebar', diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index 7b265c8e8c..37a333e819 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -126,6 +126,7 @@ export default abstract class ComponentObjectBuilder { this.id = `${this.componentName}_${Math.random().toString()}`; this.props = {'data-component-object-id': this.id}; this.node = page.locator(`[data-component-object-id="${this.id}"]`); + this.componentClassImportPath = path.join( path.relative(`${process.cwd()}/src`, resolve.blockSync(this.componentName)!), `/${this.componentName}.ts` diff --git a/yarn.lock b/yarn.lock index 2ed6dd3f18..9815d794fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5646,8 +5646,8 @@ __metadata: linkType: hard "@v4fire/core@github:V4Fire/Core#v4": - version: 4.0.0-alpha.16 - resolution: "@v4fire/core@https://github.com/V4Fire/Core.git#commit=44bf4e874e6c0e3a086cb37c6fc1d9c92d6ad563" + version: 4.0.0-alpha.20 + resolution: "@v4fire/core@https://github.com/V4Fire/Core.git#commit=c6f90dac060fff61b4cedf4a98171b75da3a0bc9" dependencies: "@babel/core": "npm:7.17.5" "@babel/helper-module-transforms": "npm:7.16.7" @@ -5791,7 +5791,7 @@ __metadata: optional: true xhr2: optional: true - checksum: c9fcab75ac8ca0549beeca11e7ac7d2b2bd2107ec579c7a7482a4108d2de6252548bf1dd8743d3a8ea11816ebf82c6eb6a591fc44df7adca0abd490020590c73 + checksum: b73d008d57159b8a69c7d4914e50b0f2e82c7ac9a76e52d87cae06bd8e3a34905409a2529e04cebd1173f027629fdebe763a97318daa6fd2950533ff67d7126b languageName: node linkType: hard From 36abd3126789549403290ffcebbf46a673d4ed01 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 7 Feb 2024 10:48:51 +0300 Subject: [PATCH 145/159] :art: --- .../test/api/component-object/index.ts | 4 +++- .../p-v4-components-demo/p-v4-components-demo.ts | 6 ------ .../traits/i-lock-page-scroll/test/unit/desktop.ts | 2 +- tests/helpers/component-object/builder.ts | 12 +++--------- tests/helpers/component/index.ts | 4 ++-- tests/helpers/component/interface.ts | 2 +- tests/helpers/mock/index.ts | 5 +++-- 7 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/components/base/b-virtual-scroll-new/test/api/component-object/index.ts b/src/components/base/b-virtual-scroll-new/test/api/component-object/index.ts index 7931d2c36f..ec8367ecec 100644 --- a/src/components/base/b-virtual-scroll-new/test/api/component-object/index.ts +++ b/src/components/base/b-virtual-scroll-new/test/api/component-object/index.ts @@ -46,6 +46,7 @@ export class VirtualScrollComponentObject extends ComponentObject { return this.component.evaluate((ctx) => ctx.reload()); @@ -53,13 +54,14 @@ export class VirtualScrollComponentObject extends ComponentObject { return this.component.evaluate((ctx) => ctx.getVirtualScrollState()); } /** - * Returns the count of children in the container + * Returns the count of children in the container ref */ getChildCount(): Promise { return this.childList.count(); diff --git a/src/components/pages/p-v4-components-demo/p-v4-components-demo.ts b/src/components/pages/p-v4-components-demo/p-v4-components-demo.ts index 0c095104c8..33b4bd13d4 100644 --- a/src/components/pages/p-v4-components-demo/p-v4-components-demo.ts +++ b/src/components/pages/p-v4-components-demo/p-v4-components-demo.ts @@ -46,12 +46,6 @@ export default class pV4ComponentsDemo extends iStaticPage { @field() someField: unknown = 'foo'; - /** - * Field for tests purposes - */ - @field() - emptyField: unknown = undefined; - protected beforeCreate(): void { //#unless runtime has storybook // eslint-disable-next-line no-console diff --git a/src/components/traits/i-lock-page-scroll/test/unit/desktop.ts b/src/components/traits/i-lock-page-scroll/test/unit/desktop.ts index 68a533c24c..626caa63b4 100644 --- a/src/components/traits/i-lock-page-scroll/test/unit/desktop.ts +++ b/src/components/traits/i-lock-page-scroll/test/unit/desktop.ts @@ -20,7 +20,7 @@ test.describe('components/traits/i-lock-page-scroll - desktop', () => { test.beforeEach(async ({demoPage, page}) => { await demoPage.goto(); await Component.waitForComponentTemplate(page, 'b-traits-i-lock-page-scroll-dummy'); - target = await Component.createComponent(page, 'b-traits-i-lock-page-scroll-dummy', undefined); + target = await Component.createComponent(page, 'b-traits-i-lock-page-scroll-dummy'); }); test.describe('lock', () => { diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index 37a333e819..ce49739d31 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -266,11 +266,9 @@ export default abstract class ComponentObjectBuilder { /** * Updates the component's props or children using the `b-dummy` component. - * * This method will not work if the component was built without the `useDummy` option. * * @param props - * * @throws {@link ReferenceError} - if the component object was not built or was built without the `useDummy` option */ update(props: RenderComponentsVnodeParams): Promise { @@ -278,16 +276,14 @@ export default abstract class ComponentObjectBuilder { throw new ReferenceError('Failed to update component. Missing "b-dummy" component.'); } - return this.dummy.setProps(props); + return this.dummy.update(props); } /** * Updates the component's props using the `b-dummy` component. - * * This method will not work if the component was built without the `useDummy` option. * * @param props - * * @throws {@link ReferenceError} - if the component object was not built or was built without the `useDummy` option */ updateProps(props: RenderComponentsVnodeParams['attrs']): Promise { @@ -295,16 +291,14 @@ export default abstract class ComponentObjectBuilder { throw new ReferenceError('Failed to update props. Missing "b-dummy" component.'); } - return this.dummy.setProps({attrs: props}); + return this.dummy.update({attrs: props}); } /** * Updates the component's children using the `b-dummy` component. - * * This method will not work if the component was built without the `useDummy` option. * * @param children - * * @throws {@link ReferenceError} - if the component object was not built or was built without the `useDummy` option */ updateChildren(children: RenderComponentsVnodeParams['children']): Promise { @@ -312,6 +306,6 @@ export default abstract class ComponentObjectBuilder { throw new ReferenceError('Failed to update children. Missing "b-dummy" component.'); } - return this.dummy.setProps({children}); + return this.dummy.update({children}); } } diff --git a/tests/helpers/component/index.ts b/tests/helpers/component/index.ts index 03427b9ca3..56a5290bda 100644 --- a/tests/helpers/component/index.ts +++ b/tests/helpers/component/index.ts @@ -124,7 +124,7 @@ export default class Component { * The function returns a `handle` to the created component (not to `b-dummy`) * and adds a method and property for convenience: * - * - `setProps` - a method that allows you to modify the component's props. + * - `update` - a method that allows you to modify the component's props. * * - `dummy` - the `handle` of the `b-dummy` component. * @@ -168,7 +168,7 @@ export default class Component { const component = await dummy.evaluateHandle((ctx) => ctx.unsafe.$refs.testComponent); Object.assign(component, { - setProps: update, + update, dummy }); diff --git a/tests/helpers/component/interface.ts b/tests/helpers/component/interface.ts index 175051272e..9589300620 100644 --- a/tests/helpers/component/interface.ts +++ b/tests/helpers/component/interface.ts @@ -13,6 +13,6 @@ import type { JSHandle } from 'playwright'; * Handle component interface that was created with a dummy wrapper. */ export interface ComponentInDummy extends JSHandle { - setProps(props: RenderComponentsVnodeParams): Promise; + update(props: RenderComponentsVnodeParams): Promise; dummy: JSHandle; } diff --git a/tests/helpers/mock/index.ts b/tests/helpers/mock/index.ts index 4661dd8e56..cfc51e49e6 100644 --- a/tests/helpers/mock/index.ts +++ b/tests/helpers/mock/index.ts @@ -169,13 +169,14 @@ export async function injectMockIntoPage( ...args: any[] ): Promise<{agent: SpyObject; id: string}> { const - tmpFn = `tmp_${Math.random().toString()}`; + tmpFn = `tmp_${Math.random().toString()}`, + argsToProvide = [tmpFn, fn.toString(), expandedStringify(args)]; const agent = await page.evaluateHandle(([tmpFn, fnString, args]) => globalThis[tmpFn] = jestMock.mock((...fnArgs) => // eslint-disable-next-line no-new-func Object.cast(new Function(`return ${fnString}`)()(...fnArgs, ...globalThis.expandedParse(args)))), - [tmpFn, fn.toString(), expandedStringify(args)]); + argsToProvide); return {agent: wrapAsSpy(agent, {}), id: tmpFn}; } From 8c81cb741a1f0232000b75ab004e8081db9dfb8c Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 7 Feb 2024 11:12:04 +0300 Subject: [PATCH 146/159] Rollback bad code removing --- .../base/b-virtual-scroll/modules/component-render.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/base/b-virtual-scroll/modules/component-render.ts b/src/components/base/b-virtual-scroll/modules/component-render.ts index 11ee3e4e62..e043a23bbd 100644 --- a/src/components/base/b-virtual-scroll/modules/component-render.ts +++ b/src/components/base/b-virtual-scroll/modules/component-render.ts @@ -172,13 +172,14 @@ export default class ComponentRender extends Friend { */ protected createComponents(items: RenderItem[]): HTMLElement[] { const - {ctx: c, scrollRender: {items: totalItems}} = this; + {ctx: c, scrollRender: {items: totalItems}} = this, + state = c.getCurrentDataState(); const render = (children: DataToRender[]) => { const map = ({itemAttrs, itemParams, index}) => this.ctx.vdom.create(c.getItemComponentName(itemParams, index), itemAttrs); - return c.vdom.render(children.map(map)); + return c.vdom.render(children.map(map), `${this.asyncGroup}:${state.currentPage}`); }; const getChildrenAttrs = (props: ItemAttrs) => ({ From efcb9198b2f0d644806e8ba4b8c6e92171040a5b Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 7 Feb 2024 11:18:02 +0300 Subject: [PATCH 147/159] Rollback bad code removing --- src/components/base/b-virtual-scroll/modules/component-render.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/base/b-virtual-scroll/modules/component-render.ts b/src/components/base/b-virtual-scroll/modules/component-render.ts index e043a23bbd..38b7d8c43f 100644 --- a/src/components/base/b-virtual-scroll/modules/component-render.ts +++ b/src/components/base/b-virtual-scroll/modules/component-render.ts @@ -63,6 +63,7 @@ export default class ComponentRender extends Friend { }); this.nodesCache = Object.createDict(); + this.ctx.async.clearAll({group: new RegExp(this.asyncGroup)}); } /** From 627f6be8db8b8418c5f39b3c21b624ee587baf07 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 7 Feb 2024 11:51:43 +0300 Subject: [PATCH 148/159] Removed useless jest-mock dep --- package.json | 1 - yarn.lock | 41 ----------------------------------------- 2 files changed, 42 deletions(-) diff --git a/package.json b/package.json index 934214d614..e58f9d2348 100644 --- a/package.json +++ b/package.json @@ -163,7 +163,6 @@ "dpdm": "3.10.0", "husky": "7.0.4", "jest": "29.7.0", - "jest-mock": "28.1.3", "jest-runner-eslint": "2.1.2", "nyc": "15.1.0", "playwright": "1.32.1", diff --git a/yarn.lock b/yarn.lock index 9815d794fb..0b3ec6bfa0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2669,15 +2669,6 @@ __metadata: languageName: node linkType: hard -"@jest/schemas@npm:^28.1.3": - version: 28.1.3 - resolution: "@jest/schemas@npm:28.1.3" - dependencies: - "@sinclair/typebox": "npm:^0.24.1" - checksum: 3cf1d4b66c9c4ffda58b246de1ddcba8e6ad085af63dccdf07922511f13b68c0cc480a7bc620cb4f3099a6f134801c747e1df7bfc7a4ef4dceefbdea3e31e1de - languageName: node - linkType: hard - "@jest/schemas@npm:^29.6.3": version: 29.6.3 resolution: "@jest/schemas@npm:29.6.3" @@ -2758,20 +2749,6 @@ __metadata: languageName: node linkType: hard -"@jest/types@npm:^28.1.3": - version: 28.1.3 - resolution: "@jest/types@npm:28.1.3" - dependencies: - "@jest/schemas": "npm:^28.1.3" - "@types/istanbul-lib-coverage": "npm:^2.0.0" - "@types/istanbul-reports": "npm:^3.0.0" - "@types/node": "npm:*" - "@types/yargs": "npm:^17.0.8" - chalk: "npm:^4.0.0" - checksum: a90e636df760799b6c3d91e34e539e701ea803e80312257e674e345a3c23a7c892df7a301afbc7883ec1d623daf3ba266cde57c5965e0692e5f1e61915d3524b - languageName: node - linkType: hard - "@jest/types@npm:^29.6.3": version: 29.6.3 resolution: "@jest/types@npm:29.6.3" @@ -3003,13 +2980,6 @@ __metadata: languageName: node linkType: hard -"@sinclair/typebox@npm:^0.24.1": - version: 0.24.51 - resolution: "@sinclair/typebox@npm:0.24.51" - checksum: 7886847b9deda1d926934066fe69165a1d9bbe7b0f836543c25efb96173c17009ef7a98619f48b379294bf27958844da3428eb35e65f8d941ea43563ad6e961e - languageName: node - linkType: hard - "@sinclair/typebox@npm:^0.27.8": version: 0.27.8 resolution: "@sinclair/typebox@npm:0.27.8" @@ -5432,7 +5402,6 @@ __metadata: imagemin-webp: "npm:6.0.0" is-path-inside: "npm:3.0.3" jest: "npm:29.7.0" - jest-mock: "npm:28.1.3" jest-runner-eslint: "npm:2.1.2" jsdom: "npm:16.7.0" merge2: "npm:1.4.1" @@ -16052,16 +16021,6 @@ __metadata: languageName: node linkType: hard -"jest-mock@npm:28.1.3": - version: 28.1.3 - resolution: "jest-mock@npm:28.1.3" - dependencies: - "@jest/types": "npm:^28.1.3" - "@types/node": "npm:*" - checksum: 43cbec0ceddea795b8b2bc09f8632eecc97b88ef018a9c9737b887ed6cbdbda000a436e9165dce2bccfbb949be8b0daca6faa530dc390d43a0e5e3099f3ae216 - languageName: node - linkType: hard - "jest-mock@npm:^27.0.6": version: 27.5.1 resolution: "jest-mock@npm:27.5.1" From 19168477faf9374d54ef6d38405091396736930a Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 7 Feb 2024 12:50:30 +0300 Subject: [PATCH 149/159] Added aliases for node modules into webpack to prevent node modules inserts into browser runtime --- build/webpack/resolve/alias.js | 7 +++++++ yarn.lock | 31 +++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/build/webpack/resolve/alias.js b/build/webpack/resolve/alias.js index 51e0d6b5bc..a9ad2d5fd9 100644 --- a/build/webpack/resolve/alias.js +++ b/build/webpack/resolve/alias.js @@ -21,6 +21,13 @@ const */ const aliases = { '@super': resolve.rootDependencies[0], + + // This is required for using jest-mock, + // otherwise jest-mock pulls various Node.js modules into the browser environment. + 'graceful-fs': false, + path: false, + picomatch: false, + ...$C(pzlr.dependencies).to({}).reduce((map, el, i) => { const asset = resolve.depMap[el].config.assets; diff --git a/yarn.lock b/yarn.lock index 0b3ec6bfa0..964cf42682 100644 --- a/yarn.lock +++ b/yarn.lock @@ -874,7 +874,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.22.5, @babel/plugin-syntax-jsx@npm:^7.7.2": +"@babel/plugin-syntax-jsx@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-syntax-jsx@npm:7.22.5" dependencies: @@ -885,6 +885,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-jsx@npm:^7.7.2": + version: 7.23.3 + resolution: "@babel/plugin-syntax-jsx@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 89037694314a74e7f0e7a9c8d3793af5bf6b23d80950c29b360db1c66859d67f60711ea437e70ad6b5b4b29affe17eababda841b6c01107c2b638e0493bafb4e + languageName: node + linkType: hard + "@babel/plugin-syntax-logical-assignment-operators@npm:^7.10.4, @babel/plugin-syntax-logical-assignment-operators@npm:^7.8.3": version: 7.10.4 resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4" @@ -3002,11 +3013,11 @@ __metadata: linkType: hard "@sinonjs/commons@npm:^3.0.0": - version: 3.0.0 - resolution: "@sinonjs/commons@npm:3.0.0" + version: 3.0.1 + resolution: "@sinonjs/commons@npm:3.0.1" dependencies: type-detect: "npm:4.0.8" - checksum: 086720ae0bc370829322df32612205141cdd44e592a8a9ca97197571f8f970352ea39d3bda75b347c43789013ddab36b34b59e40380a49bdae1c2df3aa85fe4f + checksum: a0af217ba7044426c78df52c23cedede6daf377586f3ac58857c565769358ab1f44ebf95ba04bbe38814fba6e316ca6f02870a009328294fc2c555d0f85a7117 languageName: node linkType: hard @@ -15689,15 +15700,15 @@ __metadata: linkType: hard "istanbul-lib-instrument@npm:^6.0.0": - version: 6.0.0 - resolution: "istanbul-lib-instrument@npm:6.0.0" + version: 6.0.1 + resolution: "istanbul-lib-instrument@npm:6.0.1" dependencies: "@babel/core": "npm:^7.12.3" "@babel/parser": "npm:^7.14.7" "@istanbuljs/schema": "npm:^0.1.2" istanbul-lib-coverage: "npm:^3.2.0" semver: "npm:^7.5.4" - checksum: a52efe2170ac2deeaaacc84d10fe8de41d97264a86e57df77e05c1e72227a333280f640836137b28fda802a2c71b2affb00a703979e6f7a462cc80047a6aff21 + checksum: 95fd8c66e586840989cb3c7819c6da66c4742a6fedbf16b51a5c7f1898941ad07b79ddff020f479d3a1d76743ecdbf255d93c35221875687477d4b118026e7e7 languageName: node linkType: hard @@ -20721,9 +20732,9 @@ __metadata: linkType: hard "pure-rand@npm:^6.0.0": - version: 6.0.3 - resolution: "pure-rand@npm:6.0.3" - checksum: 68e6ebbc918d0022870cc436c26fd07b8ae6a71acc9aa83145d6e2ec0022e764926cbffc70c606fd25213c3b7234357d10458939182fb6568c2a364d1098cf34 + version: 6.0.4 + resolution: "pure-rand@npm:6.0.4" + checksum: 34fed0abe99d3db7ddc459c12e1eda6bff05db6a17f2017a1ae12202271ccf276fb223b442653518c719671c1b339bbf97f27ba9276dba0997c89e45c4e6a3bf languageName: node linkType: hard From a1f8b0fa97e0a77885f2836cd0e573142b084025 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 7 Feb 2024 13:00:40 +0300 Subject: [PATCH 150/159] Added one more alias for jest --- build/webpack/resolve/alias.js | 1 + 1 file changed, 1 insertion(+) diff --git a/build/webpack/resolve/alias.js b/build/webpack/resolve/alias.js index a9ad2d5fd9..e21ee17d3b 100644 --- a/build/webpack/resolve/alias.js +++ b/build/webpack/resolve/alias.js @@ -27,6 +27,7 @@ const aliases = { 'graceful-fs': false, path: false, picomatch: false, + url: false, ...$C(pzlr.dependencies).to({}).reduce((map, el, i) => { const From 57b1e518d50b5a73472508bee71bd4da899c44c5 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 7 Feb 2024 15:45:34 +0300 Subject: [PATCH 151/159] Fixed an issue where lifecycleDone could be emitted before the last render is done --- .../b-virtual-scroll-new.ts | 3 +++ .../base/b-virtual-scroll-new/handlers.ts | 24 +++++++++++++++---- .../interface/component.ts | 5 ++++ .../modules/state/index.ts | 16 +++++++++++++ .../test/unit/functional/emitter/payload.ts | 4 ++-- 5 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/components/base/b-virtual-scroll-new/b-virtual-scroll-new.ts b/src/components/base/b-virtual-scroll-new/b-virtual-scroll-new.ts index 37a5dbca56..0db29fb523 100644 --- a/src/components/base/b-virtual-scroll-new/b-virtual-scroll-new.ts +++ b/src/components/base/b-virtual-scroll-new/b-virtual-scroll-new.ts @@ -401,8 +401,11 @@ export default class bVirtualScrollNew extends iVirtualScrollHandlers implements }); }); + this.componentInternalState.setIsDomInsertInProgress(true); + this.async.requestAnimationFrame(() => { this.$refs.container.appendChild(fragment); + this.componentInternalState.setIsDomInsertInProgress(false); this.onDomInsertDone(); this.onRenderDone(); diff --git a/src/components/base/b-virtual-scroll-new/handlers.ts b/src/components/base/b-virtual-scroll-new/handlers.ts index 87c1a1bc92..575667ce7d 100644 --- a/src/components/base/b-virtual-scroll-new/handlers.ts +++ b/src/components/base/b-virtual-scroll-new/handlers.ts @@ -6,6 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +import symbolGenerator from 'core/symbol'; + import iVirtualScrollProps from 'components/base/b-virtual-scroll-new/props'; import type bVirtualScrollNew from 'components/base/b-virtual-scroll-new/b-virtual-scroll-new'; @@ -16,6 +18,8 @@ import { isAsyncReplaceError } from 'components/base/b-virtual-scroll-new/module import iData, { component } from 'components/super/i-data/i-data'; +const $$ = symbolGenerator(); + /** * A class that provides an API to handle events emitted by the {@link bVirtualScrollNew} component. * This class is designed to work in conjunction with {@link bVirtualScrollNew}. @@ -97,15 +101,27 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { */ protected onLifecycleDone(this: bVirtualScrollNew): void { const - state = this.getVirtualScrollState(); + state = this.getVirtualScrollState(), + isDomInsertInProgress = this.componentInternalState.getIsDomInsertInProgress(); if (state.isLifecycleDone) { return; } - this.slotsStateController.doneState(); - this.componentInternalState.setIsLifecycleDone(true); - this.componentEmitter.emit(componentEvents.lifecycleDone); + const handler = () => { + this.slotsStateController.doneState(); + this.componentInternalState.setIsLifecycleDone(true); + this.componentEmitter.emit(componentEvents.lifecycleDone); + }; + + if (isDomInsertInProgress) { + return this.componentEmitter.once(componentEvents.renderDone, handler, { + group: bVirtualScrollNewAsyncGroup, + label: $$.waitUntilRenderDone + }); + } + + return handler(); } /** diff --git a/src/components/base/b-virtual-scroll-new/interface/component.ts b/src/components/base/b-virtual-scroll-new/interface/component.ts index 863eb08eb2..8b4bf66eed 100644 --- a/src/components/base/b-virtual-scroll-new/interface/component.ts +++ b/src/components/base/b-virtual-scroll-new/interface/component.ts @@ -132,6 +132,11 @@ export interface PrivateComponentState { * Pointer to the index of the data element that was last rendered. */ dataOffset: number; + + /** + * If true, it means that the process of inserting components into the DOM tree is currently in progress. + */ + isDomInsertInProgress: boolean; } /** diff --git a/src/components/base/b-virtual-scroll-new/modules/state/index.ts b/src/components/base/b-virtual-scroll-new/modules/state/index.ts index 62fc98e086..f0d2b2a162 100644 --- a/src/components/base/b-virtual-scroll-new/modules/state/index.ts +++ b/src/components/base/b-virtual-scroll-new/modules/state/index.ts @@ -127,6 +127,14 @@ export class ComponentInternalState extends Friend { this.state.isInitialRender = value; } + /** + * Sets the flag indicating that the process of inserting components into the DOM tree is currently in progress + * @param value + */ + setIsDomInsertInProgress(value: boolean): void { + this.privateState.isDomInsertInProgress = value; + } + /** * Sets the flag indicating if requests are stopped and the component won't make any more requests * until the lifecycle is refreshed. @@ -190,6 +198,14 @@ export class ComponentInternalState extends Friend { return this.privateState.dataOffset; } + /** + * Returns the value of the flag indicating whether the process + * of inserting components into the DOM tree is currently in progress + */ + getIsDomInsertInProgress(): boolean { + return this.privateState.isDomInsertInProgress; + } + /** * Updates the cursor indicating the last index of the last rendered data element */ diff --git a/src/components/base/b-virtual-scroll-new/test/unit/functional/emitter/payload.ts b/src/components/base/b-virtual-scroll-new/test/unit/functional/emitter/payload.ts index 3c0e9d3d1a..3885b84083 100644 --- a/src/components/base/b-virtual-scroll-new/test/unit/functional/emitter/payload.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/functional/emitter/payload.ts @@ -161,9 +161,9 @@ test.describe('', () => { ['renderEngineStart'], ['renderEngineDone'], ['domInsertStart'], - ['lifecycleDone'], ['domInsertDone'], - ['renderDone'] + ['renderDone'], + ['lifecycleDone'] ]); }); }); From 97eeca5b187437009e6367909dd96715f0586f83 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 7 Feb 2024 19:45:28 +0300 Subject: [PATCH 152/159] :wrecnh: --- .../base/b-virtual-scroll-new/const.ts | 10 ++++++++++ .../base/b-virtual-scroll-new/handlers.ts | 7 +++++-- tests/helpers/component-object/builder.ts | 16 ++++++++++++---- tests/helpers/component/index.ts | 10 ++++++---- tests/helpers/component/interface.ts | 9 ++++++++- 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/components/base/b-virtual-scroll-new/const.ts b/src/components/base/b-virtual-scroll-new/const.ts index e2df9b7e69..d7e915a8cc 100644 --- a/src/components/base/b-virtual-scroll-new/const.ts +++ b/src/components/base/b-virtual-scroll-new/const.ts @@ -137,6 +137,16 @@ export const componentEvents = { ...componentObserverLocalEvents }; +/** + * Internal component events (emitted in localEmitter) + */ +export const componentLocalEvents = { + /** + * The rendering cycle of components has completed (the path from renderStart to renderDone has been traversed) + */ + renderCycleDone: 'renderCycleDone' +}; + /** * Reasons for rejecting a render operation. */ diff --git a/src/components/base/b-virtual-scroll-new/handlers.ts b/src/components/base/b-virtual-scroll-new/handlers.ts index 575667ce7d..c58cff8201 100644 --- a/src/components/base/b-virtual-scroll-new/handlers.ts +++ b/src/components/base/b-virtual-scroll-new/handlers.ts @@ -13,7 +13,7 @@ import iVirtualScrollProps from 'components/base/b-virtual-scroll-new/props'; import type bVirtualScrollNew from 'components/base/b-virtual-scroll-new/b-virtual-scroll-new'; import type { MountedChild } from 'components/base/b-virtual-scroll-new/interface'; -import { bVirtualScrollNewAsyncGroup, componentEvents } from 'components/base/b-virtual-scroll-new/const'; +import { bVirtualScrollNewAsyncGroup, componentEvents, componentLocalEvents } from 'components/base/b-virtual-scroll-new/const'; import { isAsyncReplaceError } from 'components/base/b-virtual-scroll-new/modules/helpers'; import iData, { component } from 'components/super/i-data/i-data'; @@ -93,6 +93,7 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { */ protected onRenderDone(this: bVirtualScrollNew): void { this.componentEmitter.emit(componentEvents.renderDone); + this.localEmitter.emit(componentLocalEvents.renderCycleDone); } /** @@ -115,10 +116,12 @@ export abstract class iVirtualScrollHandlers extends iVirtualScrollProps { }; if (isDomInsertInProgress) { - return this.componentEmitter.once(componentEvents.renderDone, handler, { + this.localEmitter.once(componentLocalEvents.renderCycleDone, handler, { group: bVirtualScrollNewAsyncGroup, label: $$.waitUntilRenderDone }); + + return; } return handler(); diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index ce49739d31..62bf531a71 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -269,29 +269,36 @@ export default abstract class ComponentObjectBuilder { * This method will not work if the component was built without the `useDummy` option. * * @param props + * @param [mixInitialProps] - if true, the initially set props will be mixed with the passed props + * * @throws {@link ReferenceError} - if the component object was not built or was built without the `useDummy` option */ - update(props: RenderComponentsVnodeParams): Promise { + update(props: RenderComponentsVnodeParams, mixInitialProps: boolean = true): Promise { if (!this.dummy) { throw new ReferenceError('Failed to update component. Missing "b-dummy" component.'); } - return this.dummy.update(props); + return this.dummy.update(props, mixInitialProps); } /** * Updates the component's props using the `b-dummy` component. * This method will not work if the component was built without the `useDummy` option. * + * By default, the passed props will be merged with the previously set props, + * but this behavior can be cancelled by specifying the second argument as false. + * * @param props + * @param [mixInitialProps] - if true, the initially set props will be mixed with the passed props + * * @throws {@link ReferenceError} - if the component object was not built or was built without the `useDummy` option */ - updateProps(props: RenderComponentsVnodeParams['attrs']): Promise { + updateProps(props: RenderComponentsVnodeParams['attrs'], mixInitialProps: boolean = true): Promise { if (!this.dummy) { throw new ReferenceError('Failed to update props. Missing "b-dummy" component.'); } - return this.dummy.update({attrs: props}); + return this.dummy.update({attrs: props}, mixInitialProps); } /** @@ -299,6 +306,7 @@ export default abstract class ComponentObjectBuilder { * This method will not work if the component was built without the `useDummy` option. * * @param children + * * @throws {@link ReferenceError} - if the component object was not built or was built without the `useDummy` option */ updateChildren(children: RenderComponentsVnodeParams['children']): Promise { diff --git a/tests/helpers/component/index.ts b/tests/helpers/component/index.ts index 56a5290bda..fd01f32c37 100644 --- a/tests/helpers/component/index.ts +++ b/tests/helpers/component/index.ts @@ -139,11 +139,13 @@ export default class Component { ): Promise> { const dummy = await this.createComponent(page, 'b-dummy'); - const update = async (props) => { - await dummy.evaluate((ctx, [name, props]) => { + const update = async (props, mixInitialProps = false) => { + await dummy.evaluate((ctx, [name, props, mixInitialProps]) => { const parsed: RenderComponentsVnodeParams = globalThis.expandedParse(props); - ctx.testComponentAttrs = parsed.attrs ?? {}; + ctx.testComponentAttrs = mixInitialProps ? + Object.assign(ctx.testComponentAttrs, parsed.attrs) : + parsed.attrs ?? {}; if (parsed.children) { ctx.testComponentSlots = compileChild(); @@ -161,7 +163,7 @@ export default class Component { }))); } - }, [componentName, expandedStringify(props)]); + }, [componentName, expandedStringify(props), mixInitialProps]); }; await update(params); diff --git a/tests/helpers/component/interface.ts b/tests/helpers/component/interface.ts index 9589300620..37d5fa35dd 100644 --- a/tests/helpers/component/interface.ts +++ b/tests/helpers/component/interface.ts @@ -13,6 +13,13 @@ import type { JSHandle } from 'playwright'; * Handle component interface that was created with a dummy wrapper. */ export interface ComponentInDummy extends JSHandle { - update(props: RenderComponentsVnodeParams): Promise; + /** + * Updates props and children of a component + * + * @param params + * @param [mixInitialProps] - if true, then the props will not be overwritten, but added to the current ones + */ + update(params: RenderComponentsVnodeParams, mixInitialProps?: boolean): Promise; + dummy: JSHandle; } From 32427d4b250789870782ad4a8be43b931ae3febc Mon Sep 17 00:00:00 2001 From: bonkalol Date: Wed, 7 Feb 2024 19:48:20 +0300 Subject: [PATCH 153/159] :wrench: --- .../base/b-virtual-scroll-new/modules/state/helpers.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/base/b-virtual-scroll-new/modules/state/helpers.ts b/src/components/base/b-virtual-scroll-new/modules/state/helpers.ts index 8274bdbaef..8530482b89 100644 --- a/src/components/base/b-virtual-scroll-new/modules/state/helpers.ts +++ b/src/components/base/b-virtual-scroll-new/modules/state/helpers.ts @@ -40,6 +40,7 @@ export function createInitialState(): VirtualScrollState { */ export function createPrivateInitialState(): PrivateComponentState { return { - dataOffset: 0 + dataOffset: 0, + isDomInsertInProgress: false }; } From 58cfc751f6590980b9cfce644e8c0bf4471719f4 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Thu, 8 Feb 2024 09:22:55 +0300 Subject: [PATCH 154/159] :art: --- tests/helpers/component-object/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helpers/component-object/README.md b/tests/helpers/component-object/README.md index a2e1e299ac..aabd9a8645 100644 --- a/tests/helpers/component-object/README.md +++ b/tests/helpers/component-object/README.md @@ -160,7 +160,7 @@ myComponent await myComponent.build({ useDummy: true }); // Change props -await myComponent.updateProps({ attrs: { prop1: 'newVal' } }); +await myComponent.updateProps({ prop1: 'newVal' }); ``` Please note that there are some nuances to consider when creating a component with a `b-dummy` wrapper, such as you cannot set slots for such a component. From 0d404c26cce902c0b7aa8358cc50a37de7b458b1 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 9 Feb 2024 10:25:56 +0300 Subject: [PATCH 155/159] :art: review --- .../test/api/helpers/index.ts | 4 +-- .../test/api/helpers/interface.ts | 2 +- .../test/unit/functional/rendering/default.ts | 2 +- .../base/b-virtual-scroll/test/unit/render.ts | 2 +- tests/helpers/component-object/builder.ts | 30 +++++++++---------- tests/helpers/mock/index.ts | 12 ++++---- tests/helpers/mock/interface.ts | 2 +- .../interceptor/README.md | 4 +-- .../interceptor/index.ts | 2 +- .../interceptor/interface.ts | 0 .../pagination/README.md | 2 +- .../pagination/index.ts | 4 +-- .../pagination/interface.ts | 0 13 files changed, 33 insertions(+), 33 deletions(-) rename tests/helpers/{providers => network}/interceptor/README.md (98%) rename tests/helpers/{providers => network}/interceptor/index.ts (99%) rename tests/helpers/{providers => network}/interceptor/interface.ts (100%) rename tests/{helpers/providers => network-interceptors}/pagination/README.md (79%) rename tests/{helpers/providers => network-interceptors}/pagination/index.ts (93%) rename tests/{helpers/providers => network-interceptors}/pagination/interface.ts (100%) diff --git a/src/components/base/b-virtual-scroll-new/test/api/helpers/index.ts b/src/components/base/b-virtual-scroll-new/test/api/helpers/index.ts index 6824e2b947..beac49bd21 100644 --- a/src/components/base/b-virtual-scroll-new/test/api/helpers/index.ts +++ b/src/components/base/b-virtual-scroll-new/test/api/helpers/index.ts @@ -14,8 +14,8 @@ import { createInitialState as createInitialStateObj } from 'components/base/b-v import type { MountedChild, ComponentItem, VirtualScrollState, MountedItem } from 'components/base/b-virtual-scroll-new/interface'; import { componentEvents, componentObserverLocalEvents } from 'components/base/b-virtual-scroll-new/const'; -import { paginationHandler } from 'tests/helpers/providers/pagination'; -import { RequestInterceptor } from 'tests/helpers/providers/interceptor'; +import { paginationHandler } from 'tests/network-interceptors/pagination'; +import { RequestInterceptor } from 'tests/helpers/network/interceptor'; import { VirtualScrollComponentObject } from 'components/base/b-virtual-scroll-new/test/api/component-object'; import type { DataConveyor, DataItemCtor, MountedItemCtor, StateApi, VirtualScrollTestHelpers, MountedSeparatorCtor, IndexedObj } from 'components/base/b-virtual-scroll-new/test/api/helpers/interface'; diff --git a/src/components/base/b-virtual-scroll-new/test/api/helpers/interface.ts b/src/components/base/b-virtual-scroll-new/test/api/helpers/interface.ts index 0f0c7c0f4a..d8420ab72c 100644 --- a/src/components/base/b-virtual-scroll-new/test/api/helpers/interface.ts +++ b/src/components/base/b-virtual-scroll-new/test/api/helpers/interface.ts @@ -9,7 +9,7 @@ import type { ComponentItem, VirtualScrollState, MountedChild, MountedItem } from 'components/base/b-virtual-scroll-new/interface'; import type { SpyObject } from 'tests/helpers/mock/interface'; -import type { RequestInterceptor } from 'tests/helpers/providers/interceptor'; +import type { RequestInterceptor } from 'tests/helpers/network/interceptor'; import type { VirtualScrollComponentObject } from 'components/base/b-virtual-scroll-new/test/api/component-object'; /** diff --git a/src/components/base/b-virtual-scroll-new/test/unit/functional/rendering/default.ts b/src/components/base/b-virtual-scroll-new/test/unit/functional/rendering/default.ts index e4aa44055d..fc0950a8d5 100644 --- a/src/components/base/b-virtual-scroll-new/test/unit/functional/rendering/default.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/functional/rendering/default.ts @@ -161,7 +161,7 @@ test.describe('', () => { const spy = await component.getSpy((ctx) => ctx.unsafe.componentFactory.produceNodes); - await test.expect(spy.callsLength).resolves.toBe(8); + await test.expect(spy.callsCount).resolves.toBe(8); await test.expect(component.childList).toHaveCount(providerChunkSize); }); }); diff --git a/src/components/base/b-virtual-scroll/test/unit/render.ts b/src/components/base/b-virtual-scroll/test/unit/render.ts index 86f2eb133c..d8e096d4be 100644 --- a/src/components/base/b-virtual-scroll/test/unit/render.ts +++ b/src/components/base/b-virtual-scroll/test/unit/render.ts @@ -13,7 +13,7 @@ import type bVirtualScroll from 'components/base/b-virtual-scroll/b-virtual-scro import Component from 'tests/helpers/component'; import Scroll from 'tests/helpers/scroll'; import BOM from 'tests/helpers/bom'; -import { interceptPaginationRequest } from 'tests/helpers/providers/pagination'; +import { interceptPaginationRequest } from 'tests/network-interceptors/pagination'; test.describe('b-virtual-scroll render', () => { diff --git a/tests/helpers/component-object/builder.ts b/tests/helpers/component-object/builder.ts index 62bf531a71..8b7ce7efac 100644 --- a/tests/helpers/component-object/builder.ts +++ b/tests/helpers/component-object/builder.ts @@ -84,19 +84,6 @@ export default abstract class ComponentObjectBuilder { return undefined; } - /** - * A shorthand for generating selectors for component elements. - * {@link DOM.elNameSelectorGenerator} - * - * @example - * ```typescript - * this.elSelector('element') // .${componentName}__element - * ``` - */ - get elSelector(): (elName: string) => string { - return DOM.elNameSelectorGenerator(this.componentName); - } - /** * Public access to the reference of the component's `JSHandle` * @throws {@link ReferenceError} if trying to access a component that has not been built or picked @@ -112,7 +99,7 @@ export default abstract class ComponentObjectBuilder { /** * Returns `true` if the component is built or picked */ - get isBuilded(): boolean { + get isBuilt(): boolean { return Boolean(this.componentStore); } @@ -133,6 +120,19 @@ export default abstract class ComponentObjectBuilder { ); } + /** + * A shorthand for generating selectors for component elements. + * {@link DOM.elNameSelectorGenerator} + * + * @example + * ```typescript + * this.elSelector('element') // .${componentName}__element + * ``` + */ + elSelector(elName: string): string { + return DOM.elNameSelectorGenerator(this.componentName, elName); + } + /** * Returns the base class of the component */ @@ -246,7 +246,7 @@ export default abstract class ComponentObjectBuilder { * @param props - the props to set */ withProps(props: Dictionary): this { - if (!this.isBuilded) { + if (!this.isBuilt) { Object.assign(this.props, props); } diff --git a/tests/helpers/mock/index.ts b/tests/helpers/mock/index.ts index cfc51e49e6..6440e0b4c6 100644 --- a/tests/helpers/mock/index.ts +++ b/tests/helpers/mock/index.ts @@ -27,7 +27,7 @@ export function wrapAsSpy(agent: JSHandle agent.evaluate((ctx) => ctx.mock.calls) }, - callsLength: { + callsCount: { get: () => agent.evaluate((ctx) => ctx.mock.calls.length) }, @@ -59,7 +59,7 @@ export function wrapAsSpy(agent: JSHandle( * * // Access spy properties * console.log(await spy.calls); - * console.log(await spy.callsLength); + * console.log(await spy.callsCount); * console.log(await spy.lastCall); * console.log(await spy.results); * ``` @@ -123,7 +123,7 @@ export async function getSpy( * * // Access spy properties * console.log(await mockFn.calls); - * console.log(await mockFn.callsLength); + * console.log(await mockFn.callsCount); * console.log(await mockFn.lastCall); * console.log(await mockFn.results); * ``` @@ -158,7 +158,7 @@ export async function createMockFn( * * // Access spy properties * console.log(await agent.calls); - * console.log(await agent.callsLength); + * console.log(await agent.callsCount); * console.log(await agent.lastCall); * console.log(await agent.results); * ``` @@ -175,7 +175,7 @@ export async function injectMockIntoPage( const agent = await page.evaluateHandle(([tmpFn, fnString, args]) => globalThis[tmpFn] = jestMock.mock((...fnArgs) => // eslint-disable-next-line no-new-func - Object.cast(new Function(`return ${fnString}`)()(...fnArgs, ...globalThis.expandedParse(args)))), + new Function(`return ${fnString}`)()(...fnArgs, ...globalThis.expandedParse(args))), argsToProvide); return {agent: wrapAsSpy(agent, {}), id: tmpFn}; diff --git a/tests/helpers/mock/interface.ts b/tests/helpers/mock/interface.ts index b005778a03..7fb317eb76 100644 --- a/tests/helpers/mock/interface.ts +++ b/tests/helpers/mock/interface.ts @@ -21,7 +21,7 @@ export interface SpyObject { /** * The number of times the spy function has been called. */ - readonly callsLength: Promise; + readonly callsCount: Promise; /** * The arguments of the most recent call to the spy function. diff --git a/tests/helpers/providers/interceptor/README.md b/tests/helpers/network/interceptor/README.md similarity index 98% rename from tests/helpers/providers/interceptor/README.md rename to tests/helpers/network/interceptor/README.md index b2843e7d85..4c40b85429 100644 --- a/tests/helpers/providers/interceptor/README.md +++ b/tests/helpers/network/interceptor/README.md @@ -2,7 +2,7 @@ **Table of Contents** -- [tests/helpers/providers/interceptor](#testshelpersprovidersinterceptor) +- [tests/helpers/network/interceptor](#testshelpersnetworkinterceptor) - [Usage](#usage) - [How to Initialize a Request Interceptor?](#how-to-initialize-a-request-interceptor) - [How to Respond to a Request Once?](#how-to-respond-to-a-request-once) @@ -15,7 +15,7 @@ -# tests/helpers/providers/interceptor +# tests/helpers/network/interceptor This API allows you to intercept any request and respond to it with custom data. diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/network/interceptor/index.ts similarity index 99% rename from tests/helpers/providers/interceptor/index.ts rename to tests/helpers/network/interceptor/index.ts index 3b4e465081..043ad4a1a3 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/network/interceptor/index.ts @@ -13,7 +13,7 @@ import { ModuleMocker } from 'jest-mock'; import { fromQueryString } from 'core/url'; -import type { InterceptedRequest, ResponseHandler, ResponseOptions, ResponsePayload } from 'tests/helpers/providers/interceptor/interface'; +import type { InterceptedRequest, ResponseHandler, ResponseOptions, ResponsePayload } from 'tests/helpers/network/interceptor/interface'; /** * API that provides a simple way to intercept and respond to any request. diff --git a/tests/helpers/providers/interceptor/interface.ts b/tests/helpers/network/interceptor/interface.ts similarity index 100% rename from tests/helpers/providers/interceptor/interface.ts rename to tests/helpers/network/interceptor/interface.ts diff --git a/tests/helpers/providers/pagination/README.md b/tests/network-interceptors/pagination/README.md similarity index 79% rename from tests/helpers/providers/pagination/README.md rename to tests/network-interceptors/pagination/README.md index f359f724a3..46a3d429d2 100644 --- a/tests/helpers/providers/pagination/README.md +++ b/tests/network-interceptors/pagination/README.md @@ -1,4 +1,4 @@ -# tests/helpers/providers/pagination +# tests/network-interceptors/pagination This module provides API for working with request mocking. diff --git a/tests/helpers/providers/pagination/index.ts b/tests/network-interceptors/pagination/index.ts similarity index 93% rename from tests/helpers/providers/pagination/index.ts rename to tests/network-interceptors/pagination/index.ts index f959363f3e..e9d776c835 100644 --- a/tests/helpers/providers/pagination/index.ts +++ b/tests/network-interceptors/pagination/index.ts @@ -9,9 +9,9 @@ import { fromQueryString } from 'core/url'; import type { BrowserContext, Page, Route } from 'playwright'; -import type { RequestState, RequestQuery } from 'tests/helpers/providers/pagination/interface'; +import type { RequestState, RequestQuery } from 'tests/network-interceptors/pagination/interface'; -export * from 'tests/helpers/providers/pagination/interface'; +export * from 'tests/network-interceptors/pagination/interface'; const requestStates: Dictionary = { diff --git a/tests/helpers/providers/pagination/interface.ts b/tests/network-interceptors/pagination/interface.ts similarity index 100% rename from tests/helpers/providers/pagination/interface.ts rename to tests/network-interceptors/pagination/interface.ts From 265abbaa0311eec4eb12111c8be5294697b8ef33 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 9 Feb 2024 10:33:11 +0300 Subject: [PATCH 156/159] :art: --- CHANGELOG.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6dd679e1e..bd7abe9cd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,16 +11,19 @@ Changelog _Note: Gaps between patch versions are faulty, broken or test releases._ -## v4.0.0-beta.?? (2023-??-??) +## v4.0.0-beta.55 (2024-02-09) -#### :boom: Breaking Change +#### :rocket: New Feature -* Major update to `b-virtual-scroll`. Please see the component readme for changes and migration guide `components/base/b-virtual-scroll`. +* New test APIs: + * Request interceptor `tests/helpers/network/interceptor`; + * ComponentObject `tests/helpers/component-object`; + * Spy and mock `tests/helpers/mock`; + * Component.createComponentInDummy `tests/helpers/component`. -#### :rocket: New Feature +#### :house: Internal -* Added new testing API `ComponentObject` that allows for easier interaction with components in the testing environment `test/helpers/component-object`. -* Added new testing API for mocking and spying on functions at runtime `test/helpers/mock`. +* Added tests for `b-virtual-scroll-new` `components/base/b-virtual-scroll-new` ## v4.0.0-beta.54 (2024-02-06) From e535547ea7d89860bd607ff9c193bef06bf59f65 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 9 Feb 2024 10:41:23 +0300 Subject: [PATCH 157/159] :art: --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8518ed4ebc..7e23901e46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ _Note: Gaps between patch versions are faulty, broken or test releases._ * Spy and mock `tests/helpers/mock`; * Component.createComponentInDummy `tests/helpers/component`. +#### :bug: Bug Fix + +* Fixed the problem that the `lifecycleDone` event could fire before `renderDone` `components/base/b-virtual-scroll-new` + #### :house: Internal * Added tests for `b-virtual-scroll-new` `components/base/b-virtual-scroll-new` From 37a43e7d7ad26124e648d12700c9f21e79ff4448 Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 9 Feb 2024 10:45:07 +0300 Subject: [PATCH 158/159] rollback i-data change --- src/components/super/i-data/i-data.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/super/i-data/i-data.ts b/src/components/super/i-data/i-data.ts index 94f5d1e461..7256584d09 100644 --- a/src/components/super/i-data/i-data.ts +++ b/src/components/super/i-data/i-data.ts @@ -55,7 +55,8 @@ export { export * from 'components/super/i-block/i-block'; export * from 'components/super/i-data/interface'; -const $$ = symbolGenerator(); +const + $$ = symbolGenerator(); @component({functional: null}) export default abstract class iData extends iDataHandlers { From ec1701d72e7fa82bfd58610e0585cc8c7ddff78c Mon Sep 17 00:00:00 2001 From: bonkalol Date: Fri, 9 Feb 2024 11:38:00 +0300 Subject: [PATCH 159/159] :art: --- tests/helpers/mock/index.ts | 2 +- tests/helpers/providers/interceptor/README.md | 10 ++++++++++ tests/helpers/providers/interceptor/index.ts | 7 ++++--- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/helpers/mock/index.ts b/tests/helpers/mock/index.ts index cfc51e49e6..318d69f86a 100644 --- a/tests/helpers/mock/index.ts +++ b/tests/helpers/mock/index.ts @@ -163,7 +163,7 @@ export async function createMockFn( * console.log(await agent.results); * ``` */ -export async function injectMockIntoPage( +async function injectMockIntoPage( page: Page, fn: (...args: any[]) => any, ...args: any[] diff --git a/tests/helpers/providers/interceptor/README.md b/tests/helpers/providers/interceptor/README.md index b2843e7d85..370941d979 100644 --- a/tests/helpers/providers/interceptor/README.md +++ b/tests/helpers/providers/interceptor/README.md @@ -12,6 +12,7 @@ - [How to View the Parameters of Intercepted Requests?](#how-to-view-the-parameters-of-intercepted-requests) - [How to Remove Previously Set Request Handlers?](#how-to-remove-previously-set-request-handlers) - [How to Stop Intercepting Requests?](#how-to-stop-intercepting-requests) + - [How to Respond to Requests Using a Method Instead of Automatically?](#how-to-respond-to-requests-using-a-method-instead-of-automatically) @@ -158,6 +159,15 @@ const query = fromQueryString(new URL((providerCalls[0][0]).request().url // Logs the query parameters of the first intercepted request console.log(query); + +// Or a better way: + +const firstRequest = interceptor.request(0); // Get the first request +// Or +const lastRequest = interceptor.request(-1); // Get the last request + +// Get the query of the first request +const firstRequestQuery = firstRequest?.query(); ``` ### How to Remove Previously Set Request Handlers? diff --git a/tests/helpers/providers/interceptor/index.ts b/tests/helpers/providers/interceptor/index.ts index 3b4e465081..99e143f0ed 100644 --- a/tests/helpers/providers/interceptor/index.ts +++ b/tests/helpers/providers/interceptor/index.ts @@ -134,10 +134,11 @@ export class RequestInterceptor { /** * Returns the intercepted request - * @param index - the index of the request (starting from 0) + * @param at - the index of the request (starting from 0) */ - request(index: number): CanUndef { - const request: CanUndef = this.calls[index]?.[0]?.request(); + request(at: number): CanUndef { + // eslint-disable-next-line no-restricted-syntax + const request: CanUndef = this.calls.at(at)?.[0]?.request(); if (request == null) { return;