From c3bc63c7b0c9699e49c8f10ae68a62b7e4538116 Mon Sep 17 00:00:00 2001 From: Denis Aleksanov Date: Sun, 30 Nov 2025 00:25:51 +0100 Subject: [PATCH 1/5] issue-66 eof.spec --- tests/.prettierrc | 7 + tests/e2e/fixture/VScrollFixture.ts | 23 +- tests/e2e/fixture/create-fixture.ts | 3 +- tests/e2e/specs/eof.spec.ts | 373 ++++++++++++++++++++++++ tests/e2e/specs/initial-load.spec.ts | 58 ++-- tests/e2e/specs/min-max-indexes.spec.ts | 15 +- tests/e2e/specs/scroll-delay.spec.ts | 6 +- tests/e2e/specs/scroll-fast.spec.ts | 12 +- tests/e2e/specs/viewport.spec.ts | 3 +- tests/e2e/types/global.d.ts | 10 +- tests/e2e/types/index.ts | 2 + 11 files changed, 429 insertions(+), 83 deletions(-) create mode 100644 tests/.prettierrc create mode 100644 tests/e2e/specs/eof.spec.ts diff --git a/tests/.prettierrc b/tests/.prettierrc new file mode 100644 index 00000000..f1b059ba --- /dev/null +++ b/tests/.prettierrc @@ -0,0 +1,7 @@ +{ + "tabWidth": 2, + "singleQuote": true, + "arrowParens": "avoid", + "trailingComma": "none", + "printWidth": 80 +} diff --git a/tests/e2e/fixture/VScrollFixture.ts b/tests/e2e/fixture/VScrollFixture.ts index 5fb97509..12c73118 100644 --- a/tests/e2e/fixture/VScrollFixture.ts +++ b/tests/e2e/fixture/VScrollFixture.ts @@ -160,8 +160,7 @@ export class VScrollFixture { datasourceSettings, datasourceDevSettings, templateFnStr, - noAdapter, - manualRun + noAdapter }) => { const VScroll = window.VScroll; @@ -268,21 +267,27 @@ export class VScrollFixture { const makeScroller = () => (window.__vscroll__.workflow = workflowFactory()()); window.__vscroll__.makeScroller = makeScroller; - - // Create initial workflow (unless manualRun) - if (!manualRun) { - makeScroller(); - } }, { datasourceGetStr, datasourceSettings, datasourceDevSettings, templateFnStr, - noAdapter: !!noAdapter, - manualRun: !!manualRun + noAdapter: !!noAdapter } ); + + // Run onBefore hook + // Workflow is ready but not initialized + // Datasource is instantiated, so subscriptions can be made + if (this.config.onBefore) { + await this.config.onBefore(this.page); + } + + // Create initial workflow (unless manualRun) + if (!manualRun) { + await this.page.evaluate(() => window.__vscroll__.makeScroller?.()); + } } /** diff --git a/tests/e2e/fixture/create-fixture.ts b/tests/e2e/fixture/create-fixture.ts index 8ad824ad..1c9f727f 100644 --- a/tests/e2e/fixture/create-fixture.ts +++ b/tests/e2e/fixture/create-fixture.ts @@ -26,7 +26,8 @@ export const createFixture = async ({ data: { id: number; text: string }; }) => `
${item.$index}: ${item.data.text}
`, noAdapter: config.noAdapter, - manualRun: config.manualRun + manualRun: config.manualRun, + onBefore: config.onBefore }); if (!config.manualRun && !config.noRelaxOnStart) { diff --git a/tests/e2e/specs/eof.spec.ts b/tests/e2e/specs/eof.spec.ts new file mode 100644 index 00000000..42abad99 --- /dev/null +++ b/tests/e2e/specs/eof.spec.ts @@ -0,0 +1,373 @@ +import { test, expect, Page } from '@playwright/test'; +import { + VScrollFixture, + Direction, + type DirectionType +} from '../fixture/VScrollFixture.js'; +import { afterEachLogs } from '../fixture/after-each-logs.js'; +import { createFixture } from '../fixture/create-fixture.js'; +import { ITestConfig } from 'types/index.js'; + +test.afterEach(afterEachLogs); + +// // Capture console logs for comparison +// test.beforeEach(async ({ page }) => { +// page.on('console', msg => { +// console.log('[BROWSER]', msg.text()); +// }); +// }); + +const min = 1; +const max = 100; +const scrollCount = 10; + +type IConfig = ITestConfig; + +// Limited datasource: items [1..100] +const datasourceGetLimited = (index, count, success) => { + const data = []; + for (let i = index; i < index + count; i++) { + if (i >= 1 && i <= 100) { + data.push({ id: i, text: `item #${i}` }); + } + } + setTimeout(() => success(data), 25); +}; + +// Empty datasource: no items at all +const datasourceGetEmpty = (_index, _count, success) => { + setTimeout(() => success([]), 25); +}; + +type BofEofState = { + bufferBof: boolean; + bufferEof: boolean; + adapterBof: boolean; + adapterEof: boolean; + sharedBof?: boolean; + sharedEof?: boolean; + bofCount?: number; + eofCount?: number; +}; + +type Counter = { + count: number; + value: boolean; + off?: () => void; +}; + +type BofEofCountersContainer = { + bof: Counter; + eof: Counter; +}; + +type WithCounters = Window & { __bofEofCounters?: BofEofCountersContainer }; + +/** Setup bof/eof tracking container in browser context */ +const setupBofEofTracking = async (page: Page) => + page.evaluate(() => { + const w = window as WithCounters; + const adapter = w.__vscroll__.datasource.adapter; + const bof: Counter = { + count: 0, + value: adapter.bof + }; + const eof: Counter = { + count: 0, + value: adapter.eof + }; + + w.__bofEofCounters = { bof, eof }; + + adapter.bof$.on((value: boolean) => { + bof.count++; + bof.value = value; + }); + adapter.eof$.on((value: boolean) => { + eof.count++; + eof.value = value; + }); + }); + +// Base configs for BOF and EOF cases +const bofConfig: IConfig = { + datasourceGet: datasourceGetLimited, + datasourceSettings: { + startIndex: min, + bufferSize: 10, + padding: 0.5 + }, + templateSettings: { viewportHeight: 200, itemHeight: 20 }, + onBefore: setupBofEofTracking +}; + +const eofConfig: IConfig = { + datasourceGet: datasourceGetLimited, + datasourceSettings: { + startIndex: max - 10 + 1, + bufferSize: 10, + padding: 0.5 + }, + templateSettings: { viewportHeight: 200, itemHeight: 20 }, + onBefore: setupBofEofTracking +}; + +// Config where datasource always returns empty array +const emptyConfig: IConfig = { + datasourceGet: datasourceGetEmpty, + datasourceSettings: { + startIndex: min, + bufferSize: 10, + padding: 0.5 + }, + templateSettings: { viewportHeight: 200, itemHeight: 20 }, + onBefore: setupBofEofTracking +}; + +// Config with explicit min/max indexes to track bof/eof reactive counters +const multipleScrollsConfig: IConfig = { + datasourceGet: datasourceGetLimited, + datasourceSettings: { + startIndex: min, + bufferSize: 10, + padding: 0.5, + minIndex: min, + maxIndex: max + }, + templateSettings: { viewportHeight: 200, itemHeight: 20 }, + onBefore: setupBofEofTracking +}; + +const getBofEofState = async (fixture: VScrollFixture): Promise => + fixture.page.evaluate(() => { + const w = window as WithCounters; + const workflow = w.__vscroll__.workflow; + const buffer = workflow.scroller.buffer; + const adapter = w.__vscroll__.datasource.adapter; + const counters = w.__bofEofCounters; + + return { + bufferBof: buffer.bof.get(), + bufferEof: buffer.eof.get(), + adapterBof: adapter.bof, + adapterEof: adapter.eof, + sharedBof: counters.bof?.value, + sharedEof: counters.eof?.value, + bofCount: counters.bof?.count || 0, + eofCount: counters.eof?.count || 0 + }; + }); + +/** + * Expect that BOF/EOF limit is reached for the given direction. + */ +const expectLimit = async ( + fixture: VScrollFixture, + config: IConfig, + direction: DirectionType, + noscroll = false +) => { + const forward = direction === Direction.forward; + + const elements = await fixture.getElements(); + const bufferSize = config.datasourceSettings.bufferSize as number; + + expect(elements.length).toBeGreaterThan(bufferSize); + + const forwardPadding = + await fixture.scroller.viewport.paddings[Direction.forward].size; + const backwardPadding = + await fixture.scroller.viewport.paddings[Direction.backward].size; + + if (forward) { + expect(forwardPadding).toBe(0); + if (!noscroll) { + expect(backwardPadding).toBeGreaterThan(0); + } + } else { + expect(backwardPadding).toBe(0); + if (!noscroll) { + expect(forwardPadding).toBeGreaterThan(0); + } + } + + const targetElement = forward + ? (elements[elements.length - 1] as HTMLElement) + : (elements[0] as HTMLElement); + const targetIndex = await fixture.getElementIndex(targetElement); + const expectedIndex = forward ? max : min; + + expect(targetIndex).toBe(expectedIndex); + + const state = await getBofEofState(fixture); + + // bof/eof flags in buffer, adapter and shared container must be in sync + expect(state.bufferBof).toBe(!forward); + expect(state.adapterBof).toBe(state.bufferBof); + if (typeof state.sharedBof !== 'undefined') { + expect(state.sharedBof).toBe(state.bufferBof); + } + + expect(state.bufferEof).toBe(forward); + expect(state.adapterEof).toBe(state.bufferEof); + if (typeof state.sharedEof !== 'undefined') { + expect(state.sharedEof).toBe(state.bufferEof); + } +}; + +const makeLimitTest = ( + title: string, + config: IConfig, + testFunc: (fixture: VScrollFixture, config: IConfig) => Promise +) => { + test(title, async ({ page }) => { + const fixture = await createFixture({ page, config }); + await testFunc(fixture, config); + await fixture.dispose(); + }); +}; + +const runLimitSuite = (kind: 'bof' | 'eof') => { + const isEOF = kind === 'eof'; + const config = isEOF ? eofConfig : bofConfig; + const suiteTitle = isEOF ? 'End of file' : 'Begin of file'; + const limitDirection = isEOF ? Direction.forward : Direction.backward; + const oppositeDirection = isEOF ? Direction.backward : Direction.forward; + + test.describe(suiteTitle, () => { + makeLimitTest( + `should get ${kind} on init`, + config, + async (fixture, cfg) => { + // Initial workflow has already completed in createFixture + await expectLimit(fixture, cfg, limitDirection, true); + } + ); + + makeLimitTest( + `should reset ${kind} after scroll`, + config, + async fixture => { + const initial = await getBofEofState(fixture); + if (isEOF) { + expect(initial.bufferEof).toBe(true); + expect(initial.adapterEof).toBe(true); + expect(initial.sharedEof).toBe(true); + expect(initial.bufferBof).toBe(false); + } else { + expect(initial.bufferBof).toBe(true); + expect(initial.bufferEof).toBe(false); + expect(initial.adapterEof).toBe(false); + expect(initial.sharedEof).toBe(false); + } + + // Scroll away from the current limit + if (isEOF) { + await fixture.scrollMin(); + } else { + await fixture.scrollMax(); + } + + const after = await getBofEofState(fixture); + expect(after.bufferBof).toBe(false); + expect(after.adapterBof).toBe(false); + expect(after.sharedBof).toBe(false); + expect(after.bufferEof).toBe(false); + expect(after.adapterEof).toBe(false); + expect(after.sharedEof).toBe(false); + } + ); + + makeLimitTest( + `should stop when ${kind} is reached again`, + config, + async (fixture, cfg) => { + // Move to the opposite limit and then back + if (isEOF) { + await fixture.scrollMin(); + await fixture.scrollMax(); + } else { + await fixture.scrollMax(); + await fixture.scrollMin(); + } + + await expectLimit(fixture, cfg, limitDirection); + } + ); + + makeLimitTest( + `should reach ${isEOF ? 'bof' : 'eof'} after some scrolls`, + config, + async (fixture, cfg) => { + // From current limit, repeated scrolls must eventually reach the opposite limit + for (let i = 0; i < scrollCount; i++) { + if (isEOF) { + await fixture.scrollMin(); + } else { + await fixture.scrollMax(); + } + } + + await expectLimit(fixture, cfg, oppositeDirection); + } + ); + }); +}; + +test.describe('EOF/BOF Spec', () => { + runLimitSuite('bof'); + runLimitSuite('eof'); + + test.describe('Empty datasource', () => { + test('should reach both bof and eof during the first workflow cycle', async ({ + page + }) => { + const fixture = await createFixture({ page, config: emptyConfig }); + + const state = await getBofEofState(fixture); + expect(state.bufferBof).toBe(true); + expect(state.bufferEof).toBe(true); + expect(state.adapterBof).toBe(true); + expect(state.adapterEof).toBe(true); + expect(state.sharedBof).toBe(true); + expect(state.sharedEof).toBe(true); + expect(state.bofCount).toBe(1); + expect(state.eofCount).toBe(1); + + await fixture.dispose(); + }); + }); + + test.describe('Multiple scrolls', () => { + test('should reach bof/eof multiple times', async ({ page }) => { + const fixture = await createFixture({ + page, + config: multipleScrollsConfig + }); + + // Perform a sequence of scrolls: + // max, min, max, min, ... to toggle bof/eof repeatedly + const COUNT = 10; + for (let i = 1; i <= COUNT; i++) { + const cyclesDone = await fixture.workflow.cyclesDone; + if (cyclesDone === 1) { + await fixture.scrollMax(); + } else if (cyclesDone > 1 && cyclesDone < COUNT) { + if (cyclesDone % 2 === 0) { + await fixture.scrollMin(); + } else { + await fixture.scrollMax(); + } + } else { + break; + } + } + + const state = await getBofEofState(fixture); + expect(state.bofCount || 0).toEqual(COUNT); + expect(state.eofCount || 0).toEqual(COUNT - 1); + + await fixture.dispose(); + }); + }); +}); diff --git a/tests/e2e/specs/initial-load.spec.ts b/tests/e2e/specs/initial-load.spec.ts index 3d6751f6..a3504d3c 100644 --- a/tests/e2e/specs/initial-load.spec.ts +++ b/tests/e2e/specs/initial-load.spec.ts @@ -5,13 +5,6 @@ import { createFixture } from '../fixture/create-fixture.js'; import { ITestConfig } from 'types/index.js'; import { ItemsCounter } from '../helpers/itemsCounter.js'; -// // Capture console logs for comparison -// test.beforeEach(async ({ page }) => { -// page.on('console', msg => { -// console.log('[BROWSER]', msg.text()); -// }); -// }); - test.afterEach(afterEachLogs); // Basic unlimited datasource for all tests @@ -29,8 +22,7 @@ const fixedItemSizeConfigList: ITestConfig[] = [ datasourceSettings: { startIndex: 1, padding: 2, - itemSize: 15, - adapter: true + itemSize: 15 }, templateSettings: { viewportHeight: 20, itemHeight: 15 } }, @@ -39,8 +31,7 @@ const fixedItemSizeConfigList: ITestConfig[] = [ datasourceSettings: { startIndex: 1, padding: 0.5, - itemSize: 20, - adapter: true + itemSize: 20 }, templateSettings: { viewportHeight: 120, itemHeight: 20 } }, @@ -49,8 +40,7 @@ const fixedItemSizeConfigList: ITestConfig[] = [ datasourceSettings: { startIndex: -99, padding: 0.3, - itemSize: 25, - adapter: true + itemSize: 25 }, templateSettings: { viewportHeight: 200, itemHeight: 25 } }, @@ -60,8 +50,7 @@ const fixedItemSizeConfigList: ITestConfig[] = [ startIndex: -77, padding: 0.62, itemSize: 100, - horizontal: true, - adapter: true + horizontal: true }, templateSettings: { viewportWidth: 450, itemWidth: 100, horizontal: true } }, @@ -71,8 +60,7 @@ const fixedItemSizeConfigList: ITestConfig[] = [ startIndex: 1, padding: 0.5, itemSize: 20, - windowViewport: true, - adapter: true + windowViewport: true }, templateSettings: { noViewportClass: true, @@ -89,8 +77,7 @@ const fixedItemSizeAndBigBufferSizeConfigList: ITestConfig[] = [ startIndex: 100, padding: 0.1, itemSize: 20, - bufferSize: 20, - adapter: true + bufferSize: 20 }, templateSettings: { viewportHeight: 100, itemHeight: 20 } }, @@ -101,8 +88,7 @@ const fixedItemSizeAndBigBufferSizeConfigList: ITestConfig[] = [ padding: 0.1, itemSize: 100, bufferSize: 10, - horizontal: true, - adapter: true + horizontal: true }, templateSettings: { viewportWidth: 200, itemWidth: 100, horizontal: true } } @@ -115,8 +101,7 @@ const tunedItemSizeConfigList: ITestConfig[] = [ startIndex: 1, bufferSize: 1, padding: 0.5, - itemSize: 40, - adapter: true + itemSize: 40 }, noRelaxOnStart: true, templateSettings: { viewportHeight: 100, itemHeight: 20 } @@ -127,8 +112,7 @@ const tunedItemSizeConfigList: ITestConfig[] = [ startIndex: -50, bufferSize: 2, padding: 0.5, - itemSize: 30, - adapter: true + itemSize: 30 }, noRelaxOnStart: true, templateSettings: { viewportHeight: 120, itemHeight: 20 } @@ -139,8 +123,7 @@ const tunedItemSizeConfigList: ITestConfig[] = [ startIndex: -77, padding: 0.82, itemSize: 200, - horizontal: true, - adapter: true + horizontal: true }, noRelaxOnStart: true, templateSettings: { viewportWidth: 450, itemWidth: 100, horizontal: true } @@ -151,8 +134,7 @@ const tunedItemSizeConfigList: ITestConfig[] = [ startIndex: -47, padding: 0.3, itemSize: 60, - windowViewport: true, - adapter: true + windowViewport: true }, noRelaxOnStart: true, templateSettings: { @@ -170,8 +152,7 @@ const tunedItemSizeAndBigBufferSizeConfigList: ITestConfig[] = [ startIndex: -50, bufferSize: 7, padding: 0.5, - itemSize: 30, - adapter: true + itemSize: 30 }, noRelaxOnStart: true, templateSettings: { viewportHeight: 120, itemHeight: 20 } @@ -183,8 +164,7 @@ const tunedItemSizeAndBigBufferSizeConfigList: ITestConfig[] = [ padding: 0.33, itemSize: 35, bufferSize: 20, - windowViewport: true, - adapter: true + windowViewport: true }, noRelaxOnStart: true, templateSettings: { @@ -235,8 +215,7 @@ const lackOfItemsOnFirstFetchConfigList: ITestConfig[] = [ startIndex: 100, padding: 0.5, bufferSize: 10, - minIndex: 1, - adapter: true + minIndex: 1 }, noRelaxOnStart: true, templateSettings: { viewportHeight: 300, itemHeight: 20 } @@ -255,8 +234,7 @@ const lackOfItemsOnFirstFetchConfigList: ITestConfig[] = [ startIndex: -70, padding: 0.5, bufferSize: 2, - minIndex: -75, - adapter: true + minIndex: -75 }, noRelaxOnStart: true, templateSettings: { viewportHeight: 200, itemHeight: 20 } @@ -276,8 +254,7 @@ const lackOfItemsOnFirstFetchConfigList: ITestConfig[] = [ padding: 0.1, bufferSize: 12, minIndex: -9, - windowViewport: true, - adapter: true + windowViewport: true }, noRelaxOnStart: true, templateSettings: { @@ -301,8 +278,7 @@ const lackOfItemsOnFirstFetchConfigList: ITestConfig[] = [ padding: 0.3, bufferSize: 4, minIndex: -120, - horizontal: true, - adapter: true + horizontal: true }, noRelaxOnStart: true, templateSettings: { horizontal: true, viewportWidth: 300, itemWidth: 40 } diff --git a/tests/e2e/specs/min-max-indexes.spec.ts b/tests/e2e/specs/min-max-indexes.spec.ts index c9994550..fba6fc1a 100644 --- a/tests/e2e/specs/min-max-indexes.spec.ts +++ b/tests/e2e/specs/min-max-indexes.spec.ts @@ -22,8 +22,7 @@ const configList: ITestConfig[] = [ padding: 0.5, itemSize: 20, minIndex: -49, - maxIndex: 100, - adapter: true + maxIndex: 100 }, templateSettings: { viewportHeight: 200, itemHeight: 20 } }, @@ -42,8 +41,7 @@ const configList: ITestConfig[] = [ padding: 1.2, itemSize: 40, minIndex: -69, - maxIndex: 1300, - adapter: true + maxIndex: 1300 }, templateSettings: { viewportHeight: 100, itemHeight: 40 } }, @@ -62,8 +60,7 @@ const configList: ITestConfig[] = [ padding: 0.7, itemSize: 25, minIndex: 169, - maxIndex: 230, - adapter: true + maxIndex: 230 }, templateSettings: { viewportHeight: 50, itemHeight: 25 } }, @@ -83,8 +80,7 @@ const configList: ITestConfig[] = [ itemSize: 100, minIndex: 20, maxIndex: 230, - horizontal: true, - adapter: true + horizontal: true }, templateSettings: { viewportWidth: 450, itemWidth: 100, horizontal: true } }, @@ -104,8 +100,7 @@ const configList: ITestConfig[] = [ itemSize: 20, minIndex: -40, maxIndex: 159, - windowViewport: true, - adapter: true + windowViewport: true }, templateSettings: { noViewportClass: true, diff --git a/tests/e2e/specs/scroll-delay.spec.ts b/tests/e2e/specs/scroll-delay.spec.ts index b15d4700..bcbc0c14 100644 --- a/tests/e2e/specs/scroll-delay.spec.ts +++ b/tests/e2e/specs/scroll-delay.spec.ts @@ -27,8 +27,7 @@ test.describe('Delay Scroll Spec', () => { startIndex: 1, bufferSize: 5, padding: 0.5, - itemSize: 20, - adapter: true + itemSize: 20 }, datasourceDevSettings: { throttle: 500 @@ -89,8 +88,7 @@ test.describe('Delay Scroll Spec', () => { startIndex: 1, bufferSize: 5, padding: 0.5, - itemSize: 20, - adapter: true + itemSize: 20 }, templateSettings: { viewportHeight: 200, diff --git a/tests/e2e/specs/scroll-fast.spec.ts b/tests/e2e/specs/scroll-fast.spec.ts index 5e2ee1fa..7fba1fad 100644 --- a/tests/e2e/specs/scroll-fast.spec.ts +++ b/tests/e2e/specs/scroll-fast.spec.ts @@ -47,8 +47,7 @@ const configList: IConfig[] = [ bufferSize: 5, padding: 0.5, minIndex: 1, - maxIndex: 100, - adapter: true + maxIndex: 100 }, templateSettings: { viewportHeight: 100, itemHeight: 20 }, custom: { items: 100, scrollCount: 5, start: Direction.backward } @@ -60,8 +59,7 @@ const configList: IConfig[] = [ bufferSize: 3, padding: 0.3, minIndex: 1, - maxIndex: 100, - adapter: true + maxIndex: 100 }, templateSettings: { viewportHeight: 110, itemHeight: 20 }, custom: { items: 100, scrollCount: 8, start: Direction.backward } @@ -73,8 +71,7 @@ const configList: IConfig[] = [ bufferSize: 7, padding: 1.1, minIndex: 51, - maxIndex: 200, - adapter: true + maxIndex: 200 }, templateSettings: { viewportHeight: 69, itemHeight: 20 }, custom: { items: 150, scrollCount: 6, start: Direction.backward } @@ -87,8 +84,7 @@ const configList: IConfig[] = [ padding: 0.2, windowViewport: true, minIndex: 51, - maxIndex: 200, - adapter: true + maxIndex: 200 }, templateSettings: { noViewportClass: true, diff --git a/tests/e2e/specs/viewport.spec.ts b/tests/e2e/specs/viewport.spec.ts index 57068d86..d7575e14 100644 --- a/tests/e2e/specs/viewport.spec.ts +++ b/tests/e2e/specs/viewport.spec.ts @@ -9,7 +9,6 @@ test.afterEach(afterEachLogs); interface ICustom { scrollTo?: number; } - type IConfig = ITestConfig; // Datasource: limited callback (1-100) @@ -32,7 +31,7 @@ const datasourceGet = (index, count, success) => { // Base config: windowViewport with 50px header const windowWith50HeaderConfig: IConfig = { datasourceGet, - datasourceSettings: { startIndex: 1, windowViewport: true, adapter: true }, + datasourceSettings: { startIndex: 1, windowViewport: true }, templateSettings: { itemHeight: 50, noViewportClass: true, diff --git a/tests/e2e/types/global.d.ts b/tests/e2e/types/global.d.ts index 4ebaed39..79ebc788 100644 --- a/tests/e2e/types/global.d.ts +++ b/tests/e2e/types/global.d.ts @@ -3,9 +3,9 @@ * Extends Window with vscroll-specific properties */ +import * as VScroll from '../../../src/index'; import type { IAdapter } from '../../../src/interfaces/adapter'; import type { Workflow } from '../../../src/workflow'; -import type { Direction } from '../../../src/inputs/common'; interface VScrollTest { workflow: Workflow; @@ -19,15 +19,9 @@ interface VScrollTest { }; } -interface VScrollConstructor { - makeDatasource: () => new (config: unknown) => unknown; - Workflow: new (config: unknown) => Workflow; - Direction: typeof Direction; -} - declare global { interface Window { - VScroll: VScrollConstructor; + VScroll: typeof VScroll; __vscroll__: VScrollTest; } } diff --git a/tests/e2e/types/index.ts b/tests/e2e/types/index.ts index 475e1395..6907c495 100644 --- a/tests/e2e/types/index.ts +++ b/tests/e2e/types/index.ts @@ -27,6 +27,7 @@ export interface ITestConfig { noRelaxOnStart?: boolean; manualRun?: boolean; custom?: Custom; + onBefore?: (page: Page) => Promise; } export interface VScrollFixtureConfig { @@ -35,4 +36,5 @@ export interface VScrollFixtureConfig { templateSettings?: ITemplateSettings; templateFn?: (item: unknown) => string; manualRun?: boolean; + onBefore?: (page: Page) => Promise; } From 7a1bb9b82d9c0ba36de7ee22d0450664474a3b98 Mon Sep 17 00:00:00 2001 From: Denis Aleksanov Date: Mon, 1 Dec 2025 12:56:52 +0100 Subject: [PATCH 2/5] issue-66 manualRun refactoring --- tests/e2e/fixture/VScrollFixture.ts | 12 ++- tests/e2e/fixture/create-fixture.ts | 3 +- tests/e2e/specs/recreation.spec.ts | 132 +++++++++++++--------------- tests/e2e/types/global.d.ts | 2 +- tests/e2e/types/index.ts | 2 - 5 files changed, 68 insertions(+), 83 deletions(-) diff --git a/tests/e2e/fixture/VScrollFixture.ts b/tests/e2e/fixture/VScrollFixture.ts index 12c73118..2ae31231 100644 --- a/tests/e2e/fixture/VScrollFixture.ts +++ b/tests/e2e/fixture/VScrollFixture.ts @@ -136,7 +136,7 @@ export class VScrollFixture { * 2. Datasource class with adapter (adapter tests) */ private async initializeWorkflow(): Promise { - const { datasource, templateFn, noAdapter, manualRun } = this.config; + const { datasource, templateFn, noAdapter } = this.config; // Serialize functions and config const datasourceGetStr = datasource.get ? datasource.get.toString() : null; @@ -145,8 +145,8 @@ export class VScrollFixture { // debug enabled, colors disabled, immediateLog disabled (to store logs in logger for retrieval on test failure) const datasourceDevSettings = { debug: true, - logProcessRun: true, - immediateLog: false, + logProcessRun: false, + immediateLog: true, logColor: false, ...(datasource.devSettings || {}) }; @@ -284,10 +284,8 @@ export class VScrollFixture { await this.config.onBefore(this.page); } - // Create initial workflow (unless manualRun) - if (!manualRun) { - await this.page.evaluate(() => window.__vscroll__.makeScroller?.()); - } + // Create initial workflow + await this.page.evaluate(() => window.__vscroll__.makeScroller?.()); } /** diff --git a/tests/e2e/fixture/create-fixture.ts b/tests/e2e/fixture/create-fixture.ts index 1c9f727f..149d2e36 100644 --- a/tests/e2e/fixture/create-fixture.ts +++ b/tests/e2e/fixture/create-fixture.ts @@ -26,11 +26,10 @@ export const createFixture = async ({ data: { id: number; text: string }; }) => `
${item.$index}: ${item.data.text}
`, noAdapter: config.noAdapter, - manualRun: config.manualRun, onBefore: config.onBefore }); - if (!config.manualRun && !config.noRelaxOnStart) { + if (!config.noRelaxOnStart) { // Wait for initial workflow cycle to complete await fixture.relaxNext(); } diff --git a/tests/e2e/specs/recreation.spec.ts b/tests/e2e/specs/recreation.spec.ts index 35f000d4..4752833d 100644 --- a/tests/e2e/specs/recreation.spec.ts +++ b/tests/e2e/specs/recreation.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { test, expect, Page } from '@playwright/test'; import { afterEachLogs } from '../fixture/after-each-logs.js'; import { createFixture } from '../fixture/create-fixture.js'; import { ITestConfig } from 'types/index.js'; @@ -17,17 +17,48 @@ const datasourceGet = ( setTimeout(() => success(data), 0); }; +const baseConfig: ITestConfig = { + datasourceGet, + datasourceSettings: { startIndex: 1, bufferSize: 5, padding: 0.5 }, + templateSettings: { viewportHeight: 200, itemHeight: 20 } +}; + +type RecreationBox = { + adapterInitCount: number; + wfInitOnFirstMake: boolean; +}; +type WithRecreationResult = Window & { __recreationBox?: RecreationBox }; + +const onBefore = async (page: Page) => + page.evaluate(() => { + const w = window as WithRecreationResult; + const { datasource } = w.__vscroll__; + + // Subscribe to init$ BEFORE creating workflow + const box: RecreationBox = { + adapterInitCount: 0, + wfInitOnFirstMake: false + }; + w.__recreationBox = box; + + datasource.adapter.init$.on(value => { + if (!value) { + return; + } + if (++box.adapterInitCount === 1) { + // First init - capture initial workflow state + box.wfInitOnFirstMake = w.__vscroll__.workflow.isInitialized; + } + }); + }); + test.describe('Recreation Spec', () => { test.describe('Destroying (plain DS)', () => { test('should not reset Datasource on destroy', async ({ page }) => { - const config: ITestConfig = { - datasourceGet, - datasourceSettings: { startIndex: 1, bufferSize: 5, padding: 0.5 }, - templateSettings: { viewportHeight: 200, itemHeight: 20 }, - noAdapter: true // plain DS - }; - - const fixture = await createFixture({ page, config }); + const fixture = await createFixture({ + page, + config: { ...baseConfig, noAdapter: true } + }); const getWorkflowData = () => page.evaluate(() => ({ @@ -64,54 +95,25 @@ test.describe('Recreation Spec', () => { test.describe('Recreation via ngIf (instance DS)', () => { test('should switch Adapter.init trice', async ({ page }) => { - const config: ITestConfig = { - datasourceGet, - datasourceSettings: { startIndex: 1, bufferSize: 5, padding: 0.5 }, - templateSettings: { viewportHeight: 200, itemHeight: 20 }, - manualRun: true - }; - - const fixture = await createFixture({ page, config }); - - // Perform dispose and recreate cycle - type Result = { adapterInitCount: number; wfInitOnFirstMake?: boolean }; - const result = await page.evaluate( - () => - new Promise(resolve => { - const { datasource, makeScroller } = window.__vscroll__; - - const result: Result = { adapterInitCount: 0 }; - - // Subscribe to init$ BEFORE creating workflow - const off = datasource.adapter.init$.on(() => { - if (++result.adapterInitCount === 3) { - off(); - resolve(result); - } - }); - - // Create and run initial scroller workflow - makeScroller!(); - - // To make sure the workflow is not yet initialized immediately after creation - result.wfInitOnFirstMake = - window.__vscroll__.workflow.isInitialized; - - const checkAndRecreate = () => { - if (!datasource.adapter.isLoading && datasource.adapter.init) { - window.__vscroll__.workflow.dispose(); - - setTimeout(() => { - // Create and run new scroller workflow - makeScroller!(); - }, 25); - } else { - setTimeout(checkAndRecreate, 10); - } - }; - checkAndRecreate(); - }) - ); + const fixture = await createFixture({ + page, + config: { ...baseConfig, noRelaxOnStart: true, onBefore } + }); + + const result = await page.evaluate(async () => { + const w = window as WithRecreationResult; + const { datasource, makeScroller } = w.__vscroll__; + // 3 recreation cycles: 1 from onBefore and 2 from this loop + for (let i = 0; i < 2; i++) { + // Recreate scroller workflow, and wait for adapter is initialized + w.__vscroll__.workflow.dispose(); + makeScroller!(); + await new Promise(r => datasource.adapter.init$.once(r)); + } + return w.__recreationBox; + }); + + await fixture.adapter.relax(); // Workflow should not be initialized immediately after creation expect(result.wfInitOnFirstMake).toBe(false); @@ -123,13 +125,7 @@ test.describe('Recreation Spec', () => { }); test('should re-render the viewport', async ({ page }) => { - const config: ITestConfig = { - datasourceGet, - datasourceSettings: { startIndex: 1, bufferSize: 5, padding: 0.5 }, - templateSettings: { viewportHeight: 200, itemHeight: 20 } - }; - - const fixture = await createFixture({ page, config }); + const fixture = await createFixture({ page, config: baseConfig }); // Helper to capture state const getState = () => @@ -164,13 +160,7 @@ test.describe('Recreation Spec', () => { }); test('should scroll and take firstVisible', async ({ page }) => { - const config: ITestConfig = { - datasourceGet, - datasourceSettings: { startIndex: 1, bufferSize: 5, padding: 0.5 }, - templateSettings: { viewportHeight: 200, itemHeight: 20 } - }; - - const fixture = await createFixture({ page, config }); + const fixture = await createFixture({ page, config: baseConfig }); await fixture.recreateScroller(); await page.waitForFunction( diff --git a/tests/e2e/types/global.d.ts b/tests/e2e/types/global.d.ts index 79ebc788..945eda01 100644 --- a/tests/e2e/types/global.d.ts +++ b/tests/e2e/types/global.d.ts @@ -12,7 +12,7 @@ interface VScrollTest { datasource: { adapter?: IAdapter; }; - makeScroller?: () => Workflow; // Creates Workflow with fresh oldItems closure (when manualRun) + makeScroller?: () => Workflow; // Creates Workflow with fresh oldItems closure Direction: { forward: string; backward: string; diff --git a/tests/e2e/types/index.ts b/tests/e2e/types/index.ts index 6907c495..2ed06b39 100644 --- a/tests/e2e/types/index.ts +++ b/tests/e2e/types/index.ts @@ -25,7 +25,6 @@ export interface ITestConfig { templateSettings: ITemplateSettings; noAdapter?: boolean; noRelaxOnStart?: boolean; - manualRun?: boolean; custom?: Custom; onBefore?: (page: Page) => Promise; } @@ -35,6 +34,5 @@ export interface VScrollFixtureConfig { noAdapter?: boolean; templateSettings?: ITemplateSettings; templateFn?: (item: unknown) => string; - manualRun?: boolean; onBefore?: (page: Page) => Promise; } From 06bc29151a0cc74c0ccba74a932c5632735d2d5f Mon Sep 17 00:00:00 2001 From: Denis Aleksanov Date: Tue, 2 Dec 2025 21:48:59 +0100 Subject: [PATCH 3/5] issue-66 dynamic-size.zero.spec (1) --- tests/.prettierrc | 2 +- tests/e2e/fixture/VScrollFixture.ts | 54 +++++-------- tests/e2e/fixture/create-fixture.ts | 12 +-- tests/e2e/specs/dynamic-size.zero.spec.ts | 94 +++++++++++++++++++++++ tests/e2e/types/index.ts | 1 + 5 files changed, 123 insertions(+), 40 deletions(-) create mode 100644 tests/e2e/specs/dynamic-size.zero.spec.ts diff --git a/tests/.prettierrc b/tests/.prettierrc index f1b059ba..df31715d 100644 --- a/tests/.prettierrc +++ b/tests/.prettierrc @@ -4,4 +4,4 @@ "arrowParens": "avoid", "trailingComma": "none", "printWidth": 80 -} +} \ No newline at end of file diff --git a/tests/e2e/fixture/VScrollFixture.ts b/tests/e2e/fixture/VScrollFixture.ts index 2ae31231..faa594be 100644 --- a/tests/e2e/fixture/VScrollFixture.ts +++ b/tests/e2e/fixture/VScrollFixture.ts @@ -295,15 +295,12 @@ export class VScrollFixture { const page = this.page; return { get cyclesDone(): Promise { - return page.evaluate(() => { - return window.__vscroll__.workflow.cyclesDone; - }); + return page.evaluate(() => window.__vscroll__.workflow.cyclesDone); }, get innerLoopCount(): Promise { - return page.evaluate(() => { - return window.__vscroll__.workflow.scroller.state.cycle.innerLoop - .total; - }); + return page.evaluate( + () => window.__vscroll__.workflow.scroller.state.cycle.innerLoop.total + ); } }; } @@ -385,46 +382,37 @@ export class VScrollFixture { } /** - * Wait for workflow to start loading (a new cycle to begin) - * This ensures the workflow has started processing before we proceed + * Wait for the workflow cycle to complete + * @param cycle - The cycle to wait for, if not provided, wait for the current cycle to complete + * @returns A promise that resolves when the workflow has finished current or provided cycle */ - waitForLoadingStart(): Promise { + async relaxNext(cycle: number): Promise { return this.page.evaluate( - () => + ({ cycle }: { cycle: number }) => new Promise(resolve => { const workflow = window.__vscroll__.workflow; const ds = workflow.scroller.datasource; - const initialCycles = workflow.cyclesDone; + const cycleToComplete = + typeof cycle === 'number' ? cycle - 1 : workflow.cyclesDone; - // If already loading, we're good + // If already loading, we can wait for the adapter is relaxed if (ds?.adapter?.isLoading) { - resolve(); - return; + return ds.adapter.relax().then(() => resolve()); } - // Otherwise wait for a new cycle to start OR loading to begin - const check = async () => { - if (workflow.cyclesDone > initialCycles && ds?.adapter) { + // Otherwise, wait for the given cycle to complete + const waitForCycle = async () => { + if (workflow.cyclesDone > cycleToComplete && ds?.adapter) { await ds.adapter.relax(); - resolve(); + return resolve(); } else { - requestAnimationFrame(check); + requestAnimationFrame(waitForCycle); } }; - check(); - }) - ); - } - /** - * Wait for the next workflow cycle to start and complete - * Equivalent to: waitForLoadingStart() + adapter.relax() - * This is the recommended way to wait for workflow operations to complete - */ - async relaxNext(): Promise { - await this.waitForLoadingStart(); - await this.page.evaluate(() => - window.__vscroll__.workflow.scroller.datasource.adapter.relax() + waitForCycle(); + }), + { cycle } ); } diff --git a/tests/e2e/fixture/create-fixture.ts b/tests/e2e/fixture/create-fixture.ts index 149d2e36..db1b07fa 100644 --- a/tests/e2e/fixture/create-fixture.ts +++ b/tests/e2e/fixture/create-fixture.ts @@ -10,7 +10,7 @@ export const createFixture = async ({ page, config }: FixtureParams): Promise => { - const { templateSettings } = config; + const { templateSettings, templateFn } = config; const datasource: IDatasource = { get: config.datasourceGet, @@ -21,17 +21,17 @@ export const createFixture = async ({ const fixture = await VScrollFixture.create(page, { datasource, templateSettings, - templateFn: (item: { - $index: number; - data: { id: number; text: string }; - }) => `
${item.$index}: ${item.data.text}
`, + templateFn: + templateFn || + ((item: { $index: number; data: { id: number; text: string } }) => + `
${item.$index}: ${item.data.text}
`), noAdapter: config.noAdapter, onBefore: config.onBefore }); if (!config.noRelaxOnStart) { // Wait for initial workflow cycle to complete - await fixture.relaxNext(); + await fixture.relaxNext(1); } // // Debug: log actual element dimensions diff --git a/tests/e2e/specs/dynamic-size.zero.spec.ts b/tests/e2e/specs/dynamic-size.zero.spec.ts new file mode 100644 index 00000000..d9fac27e --- /dev/null +++ b/tests/e2e/specs/dynamic-size.zero.spec.ts @@ -0,0 +1,94 @@ +import { test, expect } from '@playwright/test'; +import { afterEachLogs } from '../fixture/after-each-logs.js'; +import { createFixture } from '../fixture/create-fixture.js'; +import { SizeStrategy } from '../../../src/index.js'; +import { ITestConfig } from 'types/index.js'; + +test.afterEach(afterEachLogs); + +const dynamicTemplate = (item: { + $index: number; + data: { id: number; text: string; size?: number }; +}): string => { + const size = item.data.size ?? 0; + return `
${item.$index}: ${item.data.text}
`; +}; + +const zeroSizeDatasourceGet: ITestConfig['datasourceGet'] = ( + index, + count, + success +) => { + const data = []; + for (let i = index; i < index + count; i++) { + if (i >= 1 && i <= 100) { + data.push({ id: i, text: `item #${i}`, size: 0 }); + } + } + setTimeout(() => success(data), 0); +}; + +const secondPackZeroSizeDatasourceGet: ITestConfig['datasourceGet'] = ( + index, + count, + success +) => { + const data = []; + for (let i = index; i < index + count; i++) { + if (i >= 1 && i <= 100) { + const size = i >= 6 ? 0 : 20; + data.push({ id: i, text: `item #${i}`, size }); + } + } + setTimeout(() => success(data), 0); +}; + +const baseSettings = { + startIndex: 1, + bufferSize: 5, + minIndex: 1, + padding: 0.5, + sizeStrategy: SizeStrategy.Average +} as const; + +const zeroSizeConfig: ITestConfig = { + datasourceGet: zeroSizeDatasourceGet, + datasourceSettings: { ...baseSettings }, + templateSettings: { viewportHeight: 200 }, + templateFn: dynamicTemplate +}; + +const secondPackConfig: ITestConfig = { + datasourceGet: secondPackZeroSizeDatasourceGet, + datasourceSettings: { ...baseSettings }, + templateSettings: { viewportHeight: 200 }, + templateFn: dynamicTemplate +}; + +test.describe('Dynamic Zero Size Spec', () => { + test('Items with zero size: should stop the Workflow after the first loop', async ({ + page + }) => { + const fixture = await createFixture({ page, config: zeroSizeConfig }); + + const cyclesDone = await fixture.workflow.cyclesDone; + const innerLoopCount = await fixture.workflow.innerLoopCount; + expect(cyclesDone).toBe(1); + expect(innerLoopCount).toBe(1); + + await fixture.dispose(); + }); + + test('Items with zero size started from 2 pack: should stop the Workflow after the second loop', async ({ + page + }) => { + const fixture = await createFixture({ page, config: secondPackConfig }); + + const cyclesDone = await fixture.workflow.cyclesDone; + const innerLoopCount = await fixture.workflow.innerLoopCount; + expect(cyclesDone).toBe(1); + expect(innerLoopCount).toBe(2); + + await fixture.dispose(); + }); +}); diff --git a/tests/e2e/types/index.ts b/tests/e2e/types/index.ts index 2ed06b39..1985afd8 100644 --- a/tests/e2e/types/index.ts +++ b/tests/e2e/types/index.ts @@ -27,6 +27,7 @@ export interface ITestConfig { noRelaxOnStart?: boolean; custom?: Custom; onBefore?: (page: Page) => Promise; + templateFn?: (item: unknown) => string; } export interface VScrollFixtureConfig { From 77d888d16355d25e4d43cb4f2009ae27184cf3dc Mon Sep 17 00:00:00 2001 From: Denis Aleksanov Date: Tue, 2 Dec 2025 22:09:52 +0100 Subject: [PATCH 4/5] issue-66 import type --- src/index.ts | 2 +- src/interfaces/index.ts | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/index.ts b/src/index.ts index 01c4158a..f047f4a5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ import { INVALID_DATASOURCE_PREFIX } from './scroller'; import { AdapterPropName, EMPTY_ITEM, getDefaultAdapterProps } from './classes/adapter/props'; import { Direction, SizeStrategy } from './inputs/index'; -import { +import type { IDatasource, IDatasourceConstructed, IRoutines, diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 13653b24..0b768f56 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -1,4 +1,4 @@ -import { +import type { ObservableLike, DatasourceGet, IDatasourceParams, @@ -6,7 +6,7 @@ import { IDatasource, IDatasourceConstructed } from './datasource'; -import { +import type { OnDataChanged, WorkflowParams, ScrollerWorkflow, @@ -17,8 +17,8 @@ import { StateMachineMethods, StateMachineParams } from './workflow'; -import { Item } from './item'; -import { +import type { Item } from './item'; +import type { IReactivePropConfig, IReactivePropsConfig, IReactivePropsStore, @@ -45,10 +45,10 @@ import { AdapterMethodResult, IAdapter } from './adapter'; -import { Settings, DevSettings } from './settings'; -import { IRoutines, RoutinesClassType } from './routines'; -import { ScrollEventData, State } from './state'; -import { +import type { Settings, DevSettings } from './settings'; +import type { IRoutines, RoutinesClassType } from './routines'; +import type { ScrollEventData, State } from './state'; +import type { ProcessName, ProcessClass, ProcessPayload, @@ -57,7 +57,7 @@ import { IBaseProcess, IBaseAdapterProcess } from './process'; -import { +import type { ValidatedValue, IValidator, ICommonProp, @@ -66,7 +66,7 @@ import { IValidatedData } from './validation'; -export { +export type { ObservableLike, DatasourceGet, IDatasourceParams, From 86423eb617a3f531bc641c8b0b81bd2e64ac3874 Mon Sep 17 00:00:00 2001 From: Denis Aleksanov Date: Tue, 2 Dec 2025 22:56:04 +0100 Subject: [PATCH 5/5] issue-66 dynamic-size.zero.spec (2) --- tests/e2e/specs/dynamic-size.zero.spec.ts | 83 ++++++++++++++++++++++- tests/e2e/specs/eof.spec.ts | 7 -- 2 files changed, 82 insertions(+), 8 deletions(-) diff --git a/tests/e2e/specs/dynamic-size.zero.spec.ts b/tests/e2e/specs/dynamic-size.zero.spec.ts index d9fac27e..9fb4efd8 100644 --- a/tests/e2e/specs/dynamic-size.zero.spec.ts +++ b/tests/e2e/specs/dynamic-size.zero.spec.ts @@ -1,11 +1,21 @@ import { test, expect } from '@playwright/test'; +import { Direction } from '../fixture/VScrollFixture.js'; import { afterEachLogs } from '../fixture/after-each-logs.js'; import { createFixture } from '../fixture/create-fixture.js'; -import { SizeStrategy } from '../../../src/index.js'; import { ITestConfig } from 'types/index.js'; +import { SizeStrategy } from '../../../src/index.js'; test.afterEach(afterEachLogs); +// // Capture console logs for comparison +// test.beforeEach(async ({ page }) => { +// page.on('console', msg => { +// console.log('[BROWSER]', msg.text()); +// }); +// }); + +type WithAsyncSize = Window & { __vscrollAsyncSize?: number }; + const dynamicTemplate = (item: { $index: number; data: { id: number; text: string; size?: number }; @@ -28,6 +38,23 @@ const zeroSizeDatasourceGet: ITestConfig['datasourceGet'] = ( setTimeout(() => success(data), 0); }; +// Async resize datasource: its item size is controlled via a flag on window +const asyncResizeDatasourceGet: ITestConfig['datasourceGet'] = ( + index, + count, + success +) => { + const data = []; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const size = ((window as any).__vscrollAsyncSize as number | undefined) ?? 0; + for (let i = index; i < index + count; i++) { + if (i >= 1 && i <= 100) { + data.push({ id: i, text: `item #${i}`, size }); + } + } + setTimeout(() => success(data), 0); +}; + const secondPackZeroSizeDatasourceGet: ITestConfig['datasourceGet'] = ( index, count, @@ -65,6 +92,18 @@ const secondPackConfig: ITestConfig = { templateFn: dynamicTemplate }; +const asyncResizeConfig: ITestConfig = { + datasourceGet: asyncResizeDatasourceGet, + datasourceSettings: { + startIndex: 1, + bufferSize: 5, + padding: 0.5, + sizeStrategy: SizeStrategy.Average + }, + templateSettings: { viewportHeight: 200 }, + templateFn: dynamicTemplate +}; + test.describe('Dynamic Zero Size Spec', () => { test('Items with zero size: should stop the Workflow after the first loop', async ({ page @@ -91,4 +130,46 @@ test.describe('Dynamic Zero Size Spec', () => { await fixture.dispose(); }); + + test('Items get non-zero size asynchronously: should continue the Workflow after re-size and check', async ({ + page + }) => { + const fixture = await createFixture({ page, config: asyncResizeConfig }); + const viewport = fixture.scroller.viewport; + + const scrollableSizeBefore = await viewport.getScrollableSize(); + const forwardPaddingBefore = + await viewport.paddings[Direction.forward].size; + expect(scrollableSizeBefore).toEqual(forwardPaddingBefore); + expect(scrollableSizeBefore).toBe(200); + + // Switch datasource to produce size=20 for all future items + // and make currently rendered items non-zero-sized via adapter.fix updater + await page.evaluate(() => { + const w = window as WithAsyncSize; + w.__vscrollAsyncSize = 20; + const adapter = w.__vscroll__.datasource.adapter; + return adapter.fix({ + updater: ({ element, data }) => { + (data as { size?: number }).size = 20; + const el = element as HTMLElement; + const child = (el.firstElementChild as HTMLElement) || el; + child.style.height = '20px'; + } + }); + }); + + await fixture.adapter.check(); + + const cyclesDone = await fixture.workflow.cyclesDone; + const scrollableSizeAfter = await viewport.getScrollableSize(); + const forwardPaddingAfter = await viewport.paddings[Direction.forward].size; + + expect(cyclesDone).toBe(2); + expect(forwardPaddingAfter).toBe(0); + expect(scrollableSizeAfter).toBeGreaterThan(0); + expect(scrollableSizeAfter).toBe(300); + + await fixture.dispose(); + }); }); diff --git a/tests/e2e/specs/eof.spec.ts b/tests/e2e/specs/eof.spec.ts index 42abad99..016df814 100644 --- a/tests/e2e/specs/eof.spec.ts +++ b/tests/e2e/specs/eof.spec.ts @@ -10,13 +10,6 @@ import { ITestConfig } from 'types/index.js'; test.afterEach(afterEachLogs); -// // Capture console logs for comparison -// test.beforeEach(async ({ page }) => { -// page.on('console', msg => { -// console.log('[BROWSER]', msg.text()); -// }); -// }); - const min = 1; const max = 100; const scrollCount = 10;