From: edison Date: Tue, 22 Jul 2025 07:00:41 +0000 (+0800) Subject: test(runtime-vapor): port tests from rendererChildren.spec.ts (#13649) X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=f70f9d1a6b80fe6da038cbbfc36d5162809421d2;p=thirdparty%2Fvuejs%2Fcore.git test(runtime-vapor): port tests from rendererChildren.spec.ts (#13649) --- diff --git a/packages/runtime-vapor/__tests__/_utils.ts b/packages/runtime-vapor/__tests__/_utils.ts index d1ede2a6c9..12efebf7c7 100644 --- a/packages/runtime-vapor/__tests__/_utils.ts +++ b/packages/runtime-vapor/__tests__/_utils.ts @@ -135,3 +135,21 @@ export function makeInteropRender(): (comp: Component) => InteropRenderContext { return define } + +export function shuffle(array: Array): any[] { + let currentIndex = array.length + let temporaryValue + let randomIndex + + // while there remain elements to shuffle... + while (currentIndex !== 0) { + // pick a remaining element... + randomIndex = Math.floor(Math.random() * currentIndex) + currentIndex -= 1 + // and swap it with the current element. + temporaryValue = array[currentIndex] + array[currentIndex] = array[randomIndex] + array[randomIndex] = temporaryValue + } + return array +} diff --git a/packages/runtime-vapor/__tests__/for.spec.ts b/packages/runtime-vapor/__tests__/for.spec.ts index dc247b6d4c..4e4dcf0e98 100644 --- a/packages/runtime-vapor/__tests__/for.spec.ts +++ b/packages/runtime-vapor/__tests__/for.spec.ts @@ -1,18 +1,27 @@ import { + child, createFor, + createIf, getDefaultValue, getRestElement, renderEffect, + setClass, + setInsertionState, + setStyle, + setText, + template, } from '../src' import { + type Ref, nextTick, reactive, readonly, ref, shallowRef, + toDisplayString, triggerRef, } from '@vue/runtime-dom' -import { makeRender } from './_utils' +import { makeRender, shuffle } from './_utils' const define = makeRender() @@ -734,4 +743,732 @@ describe('createFor', () => { expect(host.innerHTML).toBe('
  • 2
  • ') }) }) + + // ported from packages/runtime-core/__tests__/rendererChildren.spec.ts + describe('renderer: keyed children', () => { + const render = (arr: Ref) => { + return define({ + setup() { + const n0 = createFor( + () => arr.value, + _for_item0 => { + const n2 = template(' ', true)() as any + const x2 = child(n2) as any + renderEffect(() => setText(x2, toDisplayString(_for_item0.value))) + return n2 + }, + item => item, + ) + return n0 + }, + }).render() + } + + test('append', async () => { + const arr = ref([1]) + const { host, html } = render(arr) + expect(host.children.length).toBe(1) + expect(html()).toBe('1') + + arr.value = [1, 2, 3] + await nextTick() + expect(host.children.length).toBe(3) + expect(html()).toBe( + '123', + ) + }) + + test.todo('prepend', async () => { + const arr = ref([4, 5]) + const { host, html } = render(arr) + expect(host.children.length).toBe(2) + expect(html()).toBe('45') + + arr.value = [1, 2, 3, 4, 5] + await nextTick() + expect(host.children.length).toBe(5) + expect(html()).toBe( + `1` + + `2` + + `3` + + `4` + + `5` + + ``, + ) + }) + + test('insert in middle', async () => { + const arr = ref([1, 2, 4, 5]) + const { host, html } = render(arr) + expect(host.children.length).toBe(4) + expect(html()).toBe( + '1245', + ) + + arr.value = [1, 2, 3, 4, 5] + await nextTick() + expect(host.children.length).toBe(5) + expect(html()).toBe( + `1` + + `2` + + `3` + + `4` + + `5` + + ``, + ) + }) + + test('insert at beginning and end', async () => { + const arr = ref([2, 3, 4]) + const { host, html } = render(arr) + expect(host.children.length).toBe(3) + expect(html()).toBe( + `234`, + ) + + arr.value = [1, 2, 3, 4, 5] + await nextTick() + expect(host.children.length).toBe(5) + expect(html()).toBe( + `1` + + `2` + + `3` + + `4` + + `5` + + ``, + ) + }) + + test('insert to empty parent', async () => { + const arr = ref([]) + const { host, html } = render(arr) + expect(host.children.length).toBe(0) + expect(html()).toBe('') + + arr.value = [1, 2, 3, 4, 5] + await nextTick() + expect(host.children.length).toBe(5) + expect(html()).toBe( + `1` + + `2` + + `3` + + `4` + + `5` + + ``, + ) + }) + + test('remove all children from parent', async () => { + const arr = ref([1, 2, 3, 4, 5]) + const { host, html } = render(arr) + expect(host.children.length).toBe(5) + expect(html()).toBe( + `1` + + `2` + + `3` + + `4` + + `5` + + ``, + ) + + arr.value = [] + await nextTick() + expect(host.children.length).toBe(0) + expect(html()).toBe('') + }) + + test('remove from beginning', async () => { + const arr = ref([1, 2, 3, 4, 5]) + const { host, html } = render(arr) + expect(host.children.length).toBe(5) + expect(html()).toBe( + `1` + + `2` + + `3` + + `4` + + `5` + + ``, + ) + + arr.value = [3, 4, 5] + await nextTick() + expect(host.children.length).toBe(3) + expect(html()).toBe( + `345`, + ) + }) + + test('remove from end', async () => { + const arr = ref([1, 2, 3, 4, 5]) + const { host, html } = render(arr) + expect(host.children.length).toBe(5) + expect(html()).toBe( + `1` + + `2` + + `3` + + `4` + + `5` + + ``, + ) + + arr.value = [1, 2, 3] + await nextTick() + expect(host.children.length).toBe(3) + expect(html()).toBe( + `123`, + ) + }) + + test('remove from middle', async () => { + const arr = ref([1, 2, 3, 4, 5]) + const { host, html } = render(arr) + expect(host.children.length).toBe(5) + expect(html()).toBe( + `1` + + `2` + + `3` + + `4` + + `5` + + ``, + ) + + arr.value = [1, 2, 4, 5] + await nextTick() + expect(host.children.length).toBe(4) + expect(html()).toBe( + `1245`, + ) + }) + + test.todo('remove from beginning and insert at end', async () => { + const arr = ref([1, 2, 3]) + const { host, html } = render(arr) + expect(host.children.length).toBe(3) + expect(html()).toBe( + `123`, + ) + + arr.value = [2, 3, 4] + await nextTick() + expect(host.children.length).toBe(3) + expect(html()).toBe( + `234`, + ) + }) + + test('moving single child forward', async () => { + const arr = ref([1, 2, 3, 4]) + const { host, html } = render(arr) + expect(host.children.length).toBe(4) + expect(html()).toBe( + `1` + + `2` + + `3` + + `4` + + ``, + ) + + arr.value = [2, 3, 1, 4] + await nextTick() + expect(host.children.length).toBe(4) + expect(html()).toBe( + `2314`, + ) + }) + + test('moving single child backwards', async () => { + const arr = ref([1, 2, 3, 4]) + const { host, html } = render(arr) + expect(host.children.length).toBe(4) + expect(html()).toBe( + `1` + + `2` + + `3` + + `4` + + ``, + ) + + arr.value = [1, 4, 2, 3] + await nextTick() + expect(host.children.length).toBe(4) + expect(html()).toBe( + `1423`, + ) + }) + + test('moving single child to end', async () => { + const arr = ref([1, 2, 3]) + const { host, html } = render(arr) + expect(host.children.length).toBe(3) + expect(html()).toBe( + `123`, + ) + + arr.value = [2, 3, 1] + await nextTick() + expect(host.children.length).toBe(3) + expect(html()).toBe( + `231`, + ) + }) + + test('swap first and last', async () => { + const arr = ref([1, 2, 3, 4]) + const { host, html } = render(arr) + expect(host.children.length).toBe(4) + expect(html()).toBe( + `1234`, + ) + + arr.value = [4, 2, 3, 1] + await nextTick() + expect(host.children.length).toBe(4) + expect(html()).toBe( + `4231`, + ) + }) + + test.todo('move to left & replace', async () => { + const arr = ref([1, 2, 3, 4, 5]) + const { host, html } = render(arr) + expect(host.children.length).toBe(5) + expect(html()).toBe( + `12345`, + ) + + arr.value = [4, 1, 2, 3, 6] + await nextTick() + expect(host.children.length).toBe(5) + expect(html()).toBe( + `41236`, + ) + }) + + test.todo('move to left and leaves hold', async () => { + const arr = ref([1, 4, 5]) + const { host, html } = render(arr) + expect(host.children.length).toBe(3) + expect(html()).toBe( + `145`, + ) + + arr.value = [4, 6] + await nextTick() + expect(host.children.length).toBe(2) + expect(html()).toBe(`46`) + }) + + test.todo( + 'moved and set to undefined element ending at the end', + async () => { + const arr = ref([2, 4, 5]) + const { host, html } = render(arr) + expect(host.children.length).toBe(3) + expect(html()).toBe( + `245`, + ) + + arr.value = [4, 5, 3] + await nextTick() + expect(host.children.length).toBe(3) + expect(html()).toBe( + `453`, + ) + }, + ) + + test('reverse element', async () => { + const arr = ref([1, 2, 3, 4, 5, 6, 7, 8]) + const { host, html } = render(arr) + expect(host.children.length).toBe(8) + expect(html()).toBe( + `1234` + + `5678`, + ) + + arr.value = [8, 7, 6, 5, 4, 3, 2, 1] + await nextTick() + expect(host.children.length).toBe(8) + expect(html()).toBe( + `8765` + + `4321`, + ) + }) + + test('something', async () => { + const arr = ref([0, 1, 2, 3, 4, 5]) + const { host, html } = render(arr) + expect(host.children.length).toBe(6) + expect(html()).toBe( + `01234` + + `5`, + ) + + arr.value = [4, 3, 2, 1, 5, 0] + await nextTick() + expect(host.children.length).toBe(6) + expect(html()).toBe( + `4321` + + `50`, + ) + }) + + test('random shuffle', async () => { + const elms = 14 + const samples = 5 + + for (let n = 0; n < samples; ++n) { + const arr = ref([...Array(elms).keys()]) + const opacities = ref([]) + + const host = document.createElement('div') + define({ + setup() { + const n3 = template('')() as any + setInsertionState(n3) + createFor( + () => arr.value, + _for_item0 => { + const n2 = template(' ')() as any + const x2 = child(n2) as any + renderEffect(() => { + const _opacities = opacities.value + const _item = _for_item0.value + const _opacities_item = _opacities[_item] || '1' + setStyle(n2, { opacity: _opacities_item }) + setText(x2, toDisplayString(_item)) + }) + return n2 + }, + item => item, + 1, + ) + return n3 + }, + }).render(undefined, host) + + let rootChild = host.children[0] + for (let i = 0; i < elms; ++i) { + const child = rootChild.children[i] as HTMLSpanElement + expect(child.innerHTML).toBe(i.toString()) + expect(child.style.opacity).toBe('1') + opacities.value[i] = Math.random().toFixed(5).toString() + } + + await nextTick() + for (let i = 0; i < elms; ++i) { + const child = rootChild.children[i] as any + expect(child.$sty.opacity).toBe(opacities.value[i]) + } + + arr.value = shuffle(arr.value.slice(0)) + await nextTick() + + for (let i = 0; i < elms; ++i) { + const child = rootChild.children[i] as any + expect(child.innerHTML).toBe(arr.value[i].toString()) + expect(child.$sty.opacity).toBe(opacities.value[arr.value[i]]) + } + } + }) + + test('children with the same key but with different tag', async () => { + const items = ref([ + { key: 1, tag: 'div', text: 'one' }, + { key: 2, tag: 'div', text: 'two' }, + { key: 3, tag: 'div', text: 'three' }, + { key: 4, tag: 'div', text: 'four' }, + ]) + + const { host } = define(() => { + return createFor( + () => items.value, + _for_item0 => { + const n2 = createIf( + () => _for_item0.value.tag === 'div', + () => { + const n4 = template('
    ')() as any + const x4 = child(n4) as any + renderEffect(() => + setText(x4, toDisplayString(_for_item0.value.text)), + ) + return n4 + }, + () => { + const n6 = template(' ', true)() as any + const x6 = child(n6) as any + renderEffect(() => + setText(x6, toDisplayString(_for_item0.value.text)), + ) + return n6 + }, + ) + return n2 + }, + item => item.key, + ) + }).render() + + expect(host.children.length).toBe(4) + expect( + Array.from(host.children).map(c => + (c as HTMLElement).tagName.toLowerCase(), + ), + ).toEqual(['div', 'div', 'div', 'div']) + expect(Array.from(host.children).map(c => c.textContent)).toEqual([ + 'one', + 'two', + 'three', + 'four', + ]) + + items.value = [ + { key: 4, tag: 'div', text: 'four' }, + { key: 3, tag: 'span', text: 'three' }, + { key: 2, tag: 'span', text: 'two' }, + { key: 1, tag: 'div', text: 'one' }, + ] + await nextTick() + + expect(host.children.length).toBe(4) + expect( + Array.from(host.children).map(c => + (c as HTMLElement).tagName.toLowerCase(), + ), + ).toEqual(['div', 'span', 'span', 'div']) + expect(Array.from(host.children).map(c => c.textContent)).toEqual([ + 'four', + 'three', + 'two', + 'one', + ]) + }) + + test('children with the same tag, same key, but one with data and one without data', async () => { + const items = ref([{ key: 1, text: 'one', className: 'hi' }]) + const { host } = define(() => { + const n1 = createFor( + () => items.value, + _for_item0 => { + const n2 = template('
    ')() as any + const x2 = child(n2) as any + + renderEffect(() => { + const _item = _for_item0.value + setText(x2, _item.text) + + if (_item.className) { + setClass(n2, _item.className) + } else { + setClass(n2, null) + } + }) + + return n2 + }, + item => item.key, + ) + return n1 + }).render() + + expect(host.children.length).toBe(1) + const firstChild = host.children[0] as HTMLDivElement + expect(firstChild.textContent?.trim()).toBe('one') + expect(firstChild.className).toBe('hi') + + items.value = [{ key: 1, text: 'four', className: null as any }] + await nextTick() + + expect(host.children.length).toBe(1) + const updatedChild = host.children[0] as HTMLDivElement + expect(updatedChild.textContent?.trim()).toBe('four') + expect(updatedChild.className).toBe('') + + expect(updatedChild).toBe(firstChild) + }) + + test('should warn with duplicate keys', async () => { + const arr = ref([1, 2, 3]) + const { host, html } = render(arr) + expect(host.children.length).toBe(3) + expect(html()).toBe( + `123`, + ) + + arr.value = [1, 2, 2] + await nextTick() + expect(host.children.length).toBe(3) + expect(html()).toBe( + `122`, + ) + expect(`Duplicate keys`).toHaveBeenWarned() + }) + }) + + describe('renderer: unkeyed children', () => { + const render = (arr: Ref) => { + return define({ + setup() { + const n0 = createFor( + () => arr.value, + _for_item0 => { + const n2 = template(' ', true)() as any + const x2 = child(n2) as any + renderEffect(() => setText(x2, toDisplayString(_for_item0.value))) + return n2 + }, + ) + return n0 + }, + }).render() + } + + test.todo('move a key in non-keyed nodes with a size up', async () => { + const arr = ref([1, 'a', 'b', 'c']) + const { host, html } = define({ + setup() { + const n0 = createFor( + () => arr.value, + _for_item0 => { + const n2 = template(' ', true)() as any + const x2 = child(n2) as any + renderEffect(() => setText(x2, toDisplayString(_for_item0.value))) + return n2 + }, + item => (typeof item === 'number' ? item : undefined), + ) + return n0 + }, + }).render() + + expect(host.children.length).toBe(4) + expect(html()).toBe( + `1abc`, + ) + + arr.value = ['d', 'a', 'b', 'c', 1, 'e'] + await nextTick() + expect(host.children.length).toBe(6) + expect(html()).toBe( + `dabc` + + `1e`, + ) + }) + + test('append elements with updating children without keys', async () => { + const arr = ref(['hello']) + const { html } = render(arr as any) + expect(html()).toBe('hello') + + arr.value = ['hello', 'world'] + await nextTick() + expect(html()).toBe('helloworld') + }) + + test('updating children without keys', async () => { + const arr = ref(['hello', 'foo']) + const { html } = render(arr as any) + expect(html()).toBe('hellofoo') + + arr.value = ['hello', 'bar'] + await nextTick() + expect(html()).toBe('hellobar') + + arr.value = ['world', 'bar'] + await nextTick() + expect(html()).toBe('worldbar') + }) + + test('prepend element with updating children without keys', async () => { + const arr = ref(['foo', 'bar']) + const { html } = render(arr as any) + expect(html()).toBe('foobar') + + arr.value = ['hello', 'foo', 'bar'] + await nextTick() + expect(html()).toBe( + 'hellofoobar', + ) + }) + + test('prepend element of different tag type with updating children without keys', async () => { + const items = ref([{ tag: 'span', text: 'world' }]) + const { host, html } = define(() => { + return createFor( + () => items.value, + _for_item0 => { + const n2 = createIf( + () => _for_item0.value.tag === 'div', + () => { + const n4 = template('
    ')() as any + const x4 = child(n4) as any + renderEffect(() => + setText(x4, toDisplayString(_for_item0.value.text)), + ) + return n4 + }, + () => { + const n6 = template(' ', true)() as any + const x6 = child(n6) as any + renderEffect(() => + setText(x6, toDisplayString(_for_item0.value.text)), + ) + return n6 + }, + ) + return n2 + }, + ) + }).render() + + expect(host.children.length).toBe(1) + expect(html()).toBe('world') + + items.value = [ + { tag: 'div', text: 'hello' }, + { tag: 'span', text: 'world' }, + ] + await nextTick() + expect(host.children.length).toBe(2) + expect(html()).toBe( + '
    hello
    world', + ) + }) + + test('remove elements with updating children without keys', async () => { + const arr = ref(['one', 'two', 'three']) + const { html } = render(arr as any) + expect(html()).toBe( + 'onetwothree', + ) + + arr.value = ['two'] + await nextTick() + expect(html()).toBe('two') + }) + + test('remove a single node when children are updated', async () => { + const arr = ref(['one']) + const { html } = render(arr as any) + expect(html()).toBe('one') + + arr.value = ['two', 'three'] + await nextTick() + expect(html()).toBe('twothree') + }) + + test('reorder elements', async () => { + const arr = ref(['one', 'two', 'three']) + const { html } = render(arr as any) + expect(html()).toBe( + 'onetwothree', + ) + + arr.value = ['three', 'two', 'one'] + await nextTick() + expect(html()).toBe( + 'threetwoone', + ) + }) + }) }) diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 9ffdf6dca5..dbc9ebc831 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -153,6 +153,24 @@ export const createFor = ( unmount(oldBlocks[i]) } } else { + if (__DEV__) { + const keyToIndexMap: Map = new Map() + for (let i = 0; i < newLength; i++) { + const item = getItem(source, i) + const key = getKey(...item) + if (key != null) { + if (keyToIndexMap.has(key)) { + warn( + `Duplicate keys found during update:`, + JSON.stringify(key), + `Make sure keys are unique.`, + ) + } + keyToIndexMap.set(key, i) + } + } + } + const sharedBlockCount = Math.min(oldLength, newLength) const previousKeyIndexPairs: [any, number][] = new Array(oldLength) const queuedBlocks: [