From eee00437a98191cd7011fc60ed7862356bbb0319 Mon Sep 17 00:00:00 2001 From: Denis Hilt Date: Thu, 22 Oct 2026 21:07:21 +0200 Subject: [PATCH] Fix virtual insertion indexing for single-item buffers --- src/classes/buffer.ts | 5 +- .../e2e/specs/adapter.append-prepend.spec.ts | 90 ++++++++++--- tests/unit/buffer.spec.ts | 118 ++++++++++++++---- 3 files changed, 167 insertions(+), 46 deletions(-) diff --git a/src/classes/buffer.ts b/src/classes/buffer.ts index 58ec3521..9642b4a8 100644 --- a/src/classes/buffer.ts +++ b/src/classes/buffer.ts @@ -218,10 +218,11 @@ export class Buffer { if (!this.checkCall.insertVirtual(items, index, direction)) { return false; } + const indexToInsert = index + (direction === Direction.forward ? 1 : 0); let shift = 0; - if (index <= this.firstIndex && !fixRight) { + if (indexToInsert <= this.firstIndex && !fixRight) { shift = items.length; - } else if (index >= this.lastIndex && fixRight) { + } else if (indexToInsert > this.lastIndex && fixRight) { shift = -items.length; } if (shift) { diff --git a/tests/e2e/specs/adapter.append-prepend.spec.ts b/tests/e2e/specs/adapter.append-prepend.spec.ts index ce021b4d..21f7a4a1 100644 --- a/tests/e2e/specs/adapter.append-prepend.spec.ts +++ b/tests/e2e/specs/adapter.append-prepend.spec.ts @@ -111,15 +111,15 @@ const addItems = ( ) => operation === Adapter.append ? misc.adapter.append({ - items, - decrease: opposite, - ...(virtualize ? { virtualize: true } : { eof: !!boundary }) - }) + items, + decrease: opposite, + ...(virtualize ? { virtualize: true } : { eof: !!boundary }) + }) : misc.adapter.prepend({ - items, - increase: opposite, - ...(virtualize ? { virtualize: true } : { bof: !!boundary }) - }); + items, + increase: opposite, + ...(virtualize ? { virtualize: true } : { bof: !!boundary }) + }); const createBasicConfig = (operation: Operation): TestConfig => ({ datasource: () => getDatasource({ min, max }), @@ -155,6 +155,19 @@ const createVirtualConfig = ( templateSettings: { viewportHeight: 200, itemHeight: itemSize } }); +const createSingleItemConfig = (): TestConfig => ({ + datasource: () => new MutableDatasource(min, min), + datasourceSettings: { + startIndex: min, + minIndex: min, + maxIndex: min, + itemSize, + bufferSize: 1, + padding + }, + templateSettings: { viewportHeight: itemSize, itemHeight: itemSize } +}); + const createEmptyConfig = ( operation: Operation, indexed: boolean @@ -162,16 +175,16 @@ const createEmptyConfig = ( datasource: () => getDatasource({ min: 1, max: 0 }), datasourceSettings: indexed ? { - startIndex: - operation === Adapter.append - ? max - emptyAmount + 1 - : min + emptyAmount - 1, - ...(operation === Adapter.append - ? { maxIndex: max - emptyAmount + 1 } - : { minIndex: min + emptyAmount - 1 }), - bufferSize: emptyAmount, - padding - } + startIndex: + operation === Adapter.append + ? max - emptyAmount + 1 + : min + emptyAmount - 1, + ...(operation === Adapter.append + ? { maxIndex: max - emptyAmount + 1 } + : { minIndex: min + emptyAmount - 1 }), + bufferSize: emptyAmount, + padding + } : {}, templateSettings: { viewportHeight: 200, itemHeight: itemSize } }); @@ -300,6 +313,46 @@ const registerVirtual = ( } }); +const registerSingleItem = (operation: Operation, opposite: boolean): void => + makeTest({ + config: createSingleItemConfig(), + title: + `should ${operation} virtually next to a single buffered item` + + (opposite ? ' (opposite)' : ''), + it: misc => async () => { + await misc.relaxNext(); + const append = operation === Adapter.append; + const fixRight = append ? opposite : !opposite; + const shift = opposite ? (append ? -amount : amount) : 0; + const { buffer } = misc.scroller; + const [item] = buffer.items; + const items = makeItems(append ? min + 1 : min - amount, amount); + + expect(buffer.size).toBe(1); + + misc.source().insert(items, min, !append, fixRight); + const result = await addItems(misc, operation, items, opposite, false, true); + expect(result.success).toBe(true); + + expect(item.$index).toBe(min + shift); + expect(buffer.items.find(({ data }) => data.id === min)).toBe(item); + expect(buffer.absMinIndex).toBe(min - (fixRight ? amount : 0)); + expect(buffer.absMaxIndex).toBe(min + (fixRight ? 0 : amount)); + misc.expect.domIndexesMatchBuffer(); + expect(misc.checkElementContent(item.$index, min)).toBe(true); + + if (append) { + await misc.scrollMaxRelax(); + } else { + await misc.scrollMinRelax(); + } + const edgeIndex = append ? buffer.absMaxIndex : buffer.absMinIndex; + const edgeId = append ? min + amount : min - amount; + expect(misc.checkElementContent(edgeIndex, edgeId)).toBe(true); + misc.expect.domIndexesMatchBuffer(); + } + }); + const registerEmpty = ( operation: Operation, opposite: boolean, @@ -358,6 +411,7 @@ describe('Adapter Append-Prepend Spec', () => [false, true].forEach(boundary => registerEmpty(operation, opposite, true, boundary) ); + registerSingleItem(operation, opposite); registerEmpty(operation, opposite, false); }) )); diff --git a/tests/unit/buffer.spec.ts b/tests/unit/buffer.spec.ts index 27e10b96..efe86c88 100644 --- a/tests/unit/buffer.spec.ts +++ b/tests/unit/buffer.spec.ts @@ -43,29 +43,29 @@ const makeBuffer = (params: BufferParams): Buffer => { const checkUpdate = ({ min, max, predicate, fixRight, list }: BufferUpdateConfig) => - () => { - const buffer = makeBuffer({ min, max }); - buffer.updateItems(predicate, cb, NaN, fixRight); - // console.log(buffer.items.map(i => i.get())); - expect(buffer.size).toEqual(list.length); - list.forEach((current, index) => { - const $index = Number(Object.keys(current)[0]); - if (index === 0) { - expect(buffer.firstIndex).toEqual($index); - expect(buffer.minIndex).toEqual($index); - expect(buffer.absMinIndex).toEqual($index); - } - if (index === list.length - 1) { - expect(buffer.lastIndex).toEqual($index); - expect(buffer.maxIndex).toEqual($index); - expect(buffer.absMaxIndex).toEqual($index); - } - const id = current[$index]; - const item = buffer.items[index]; - expect(item.$index).toEqual($index); - expect(item.data.id).toEqual(id); - }); - }; + () => { + const buffer = makeBuffer({ min, max }); + buffer.updateItems(predicate, cb, NaN, fixRight); + // console.log(buffer.items.map(i => i.get())); + expect(buffer.size).toEqual(list.length); + list.forEach((current, index) => { + const $index = Number(Object.keys(current)[0]); + if (index === 0) { + expect(buffer.firstIndex).toEqual($index); + expect(buffer.minIndex).toEqual($index); + expect(buffer.absMinIndex).toEqual($index); + } + if (index === list.length - 1) { + expect(buffer.lastIndex).toEqual($index); + expect(buffer.maxIndex).toEqual($index); + expect(buffer.absMaxIndex).toEqual($index); + } + const id = current[$index]; + const item = buffer.items[index]; + expect(item.$index).toEqual($index); + expect(item.data.id).toEqual(id); + }); + }; const checkIndexTrackingOnUpdate = (params: BufferUpdateTrackConfig) => () => { const { index, predicate, fixRight, result, debug = false } = params; @@ -99,22 +99,32 @@ tracked index: ${trackedIndex ? `${trackedIndex}~${trackedItem ? trackedItem.get const checkInsert = (params: BufferInsertConfig) => () => { const bufferBefore = makeBuffer(params); const buffer = makeBuffer(params); + const beforeItems = [...buffer.items]; const { result } = params; const items = params.items.map(id => generateItem(id)); - buffer.insertVirtually( + const resultInsert = buffer.insertVirtually( items, params.index, params.direction, params.fixRight ); + expect(resultInsert).toBe(!!result); + expect(buffer.size).toBe(beforeItems.length); buffer.items.forEach((item, i) => { const $index = result ? Number(Object.keys(result.list[i])[0]) : bufferBefore.items[i].$index; const id = result ? result.list[i][$index] : bufferBefore.items[i].data.id; + expect(item).toBe(beforeItems[i]); expect(item.$index).toBe($index); expect(item.data.id).toBe(id); + if (i === 0) { + expect(buffer.minIndex).toBe($index); + } + if (i === buffer.size - 1) { + expect(buffer.maxIndex).toBe($index); + } }); expect(buffer.absMinIndex).toEqual( @@ -152,7 +162,7 @@ describe('Buffer Spec', () => { case 4: return () => null; case 5: - return class {}; + return class { }; case 6: return new Map(); case 7: @@ -1160,9 +1170,65 @@ describe('Buffer Spec', () => { index: -101, direction: Direction.backward, fixRight: true + }, + { + title: 'before a single buffered item, backward', + items: ['A', 'B'], + index: 5, + direction: Direction.backward, + fixRight: false, + min: 5, + max: 5, + result: { + list: [{ 7: 5 }], + absMin: -100, + absMax: 102 + } + }, + { + title: 'before a single buffered item, backward + fixRight', + items: ['A', 'B'], + index: 5, + direction: Direction.backward, + fixRight: true, + min: 5, + max: 5, + result: { + list: [{ 5: 5 }], + absMin: -102, + absMax: 100 + } + }, + { + title: 'after a single buffered item, forward', + items: ['A', 'B'], + index: 5, + direction: Direction.forward, + fixRight: false, + min: 5, + max: 5, + result: { + list: [{ 5: 5 }], + absMin: -100, + absMax: 102 + } + }, + { + title: 'after a single buffered item, forward + fixRight', + items: ['A', 'B'], + index: 5, + direction: Direction.forward, + fixRight: true, + min: 5, + max: 5, + result: { + list: [{ 3: 5 }], + absMin: -102, + absMax: 100 + } } ] - .map(c => ({ ...c, min: -1, max: 1, absMin: -100, absMax: 100 })) + .map(c => ({ min: -1, max: 1, absMin: -100, absMax: 100, ...c })) .forEach(config => it(config.title, checkInsert(config as BufferInsertConfig)) ));