]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test(runtime-vapor): port tests from rendererChildren.spec.ts (#13649)
authoredison <daiwei521@126.com>
Tue, 22 Jul 2025 07:00:41 +0000 (15:00 +0800)
committerGitHub <noreply@github.com>
Tue, 22 Jul 2025 07:00:41 +0000 (15:00 +0800)
packages/runtime-vapor/__tests__/_utils.ts
packages/runtime-vapor/__tests__/for.spec.ts
packages/runtime-vapor/src/apiCreateFor.ts

index d1ede2a6c9a49546f6ab313fdfd63c9a1b9c77e9..12efebf7c7a79ae3cb5f4a5a364bd6320904bbd1 100644 (file)
@@ -135,3 +135,21 @@ export function makeInteropRender(): (comp: Component) => InteropRenderContext {
 
   return define
 }
+
+export function shuffle(array: Array<any>): 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
+}
index dc247b6d4caa7158f9abfd47d018e6a82cd028e5..4e4dcf0e98f2d17604e8ebf7e7d52994553e260b 100644 (file)
@@ -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('<li>2</li><!--for-->')
     })
   })
+
+  // ported from packages/runtime-core/__tests__/rendererChildren.spec.ts
+  describe('renderer: keyed children', () => {
+    const render = (arr: Ref<number[]>) => {
+      return define({
+        setup() {
+          const n0 = createFor(
+            () => arr.value,
+            _for_item0 => {
+              const n2 = template('<span> </span>', 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<number[]>([1])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(1)
+      expect(html()).toBe('<span>1</span><!--for-->')
+
+      arr.value = [1, 2, 3]
+      await nextTick()
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        '<span>1</span><span>2</span><span>3</span><!--for-->',
+      )
+    })
+
+    test.todo('prepend', async () => {
+      const arr = ref<number[]>([4, 5])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(2)
+      expect(html()).toBe('<span>4</span><span>5</span><!--for-->')
+
+      arr.value = [1, 2, 3, 4, 5]
+      await nextTick()
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<span>5</span>` +
+          `<!--for-->`,
+      )
+    })
+
+    test('insert in middle', async () => {
+      const arr = ref<number[]>([1, 2, 4, 5])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(4)
+      expect(html()).toBe(
+        '<span>1</span><span>2</span><span>4</span><span>5</span><!--for-->',
+      )
+
+      arr.value = [1, 2, 3, 4, 5]
+      await nextTick()
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<span>5</span>` +
+          `<!--for-->`,
+      )
+    })
+
+    test('insert at beginning and end', async () => {
+      const arr = ref<number[]>([2, 3, 4])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>2</span><span>3</span><span>4</span><!--for-->`,
+      )
+
+      arr.value = [1, 2, 3, 4, 5]
+      await nextTick()
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<span>5</span>` +
+          `<!--for-->`,
+      )
+    })
+
+    test('insert to empty parent', async () => {
+      const arr = ref<number[]>([])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(0)
+      expect(html()).toBe('<!--for-->')
+
+      arr.value = [1, 2, 3, 4, 5]
+      await nextTick()
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<span>5</span>` +
+          `<!--for-->`,
+      )
+    })
+
+    test('remove all children from parent', async () => {
+      const arr = ref<number[]>([1, 2, 3, 4, 5])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<span>5</span>` +
+          `<!--for-->`,
+      )
+
+      arr.value = []
+      await nextTick()
+      expect(host.children.length).toBe(0)
+      expect(html()).toBe('<!--for-->')
+    })
+
+    test('remove from beginning', async () => {
+      const arr = ref<number[]>([1, 2, 3, 4, 5])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<span>5</span>` +
+          `<!--for-->`,
+      )
+
+      arr.value = [3, 4, 5]
+      await nextTick()
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>3</span><span>4</span><span>5</span><!--for-->`,
+      )
+    })
+
+    test('remove from end', async () => {
+      const arr = ref<number[]>([1, 2, 3, 4, 5])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<span>5</span>` +
+          `<!--for-->`,
+      )
+
+      arr.value = [1, 2, 3]
+      await nextTick()
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>1</span><span>2</span><span>3</span><!--for-->`,
+      )
+    })
+
+    test('remove from middle', async () => {
+      const arr = ref<number[]>([1, 2, 3, 4, 5])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<span>5</span>` +
+          `<!--for-->`,
+      )
+
+      arr.value = [1, 2, 4, 5]
+      await nextTick()
+      expect(host.children.length).toBe(4)
+      expect(html()).toBe(
+        `<span>1</span><span>2</span><span>4</span><span>5</span><!--for-->`,
+      )
+    })
+
+    test.todo('remove from beginning and insert at end', async () => {
+      const arr = ref<number[]>([1, 2, 3])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>1</span><span>2</span><span>3</span><!--for-->`,
+      )
+
+      arr.value = [2, 3, 4]
+      await nextTick()
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>2</span><span>3</span><span>4</span><!--for-->`,
+      )
+    })
+
+    test('moving single child forward', async () => {
+      const arr = ref<number[]>([1, 2, 3, 4])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(4)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<!--for-->`,
+      )
+
+      arr.value = [2, 3, 1, 4]
+      await nextTick()
+      expect(host.children.length).toBe(4)
+      expect(html()).toBe(
+        `<span>2</span><span>3</span><span>1</span><span>4</span><!--for-->`,
+      )
+    })
+
+    test('moving single child backwards', async () => {
+      const arr = ref<number[]>([1, 2, 3, 4])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(4)
+      expect(html()).toBe(
+        `<span>1</span>` +
+          `<span>2</span>` +
+          `<span>3</span>` +
+          `<span>4</span>` +
+          `<!--for-->`,
+      )
+
+      arr.value = [1, 4, 2, 3]
+      await nextTick()
+      expect(host.children.length).toBe(4)
+      expect(html()).toBe(
+        `<span>1</span><span>4</span><span>2</span><span>3</span><!--for-->`,
+      )
+    })
+
+    test('moving single child to end', async () => {
+      const arr = ref<number[]>([1, 2, 3])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>1</span><span>2</span><span>3</span><!--for-->`,
+      )
+
+      arr.value = [2, 3, 1]
+      await nextTick()
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>2</span><span>3</span><span>1</span><!--for-->`,
+      )
+    })
+
+    test('swap first and last', async () => {
+      const arr = ref<number[]>([1, 2, 3, 4])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(4)
+      expect(html()).toBe(
+        `<span>1</span><span>2</span><span>3</span><span>4</span><!--for-->`,
+      )
+
+      arr.value = [4, 2, 3, 1]
+      await nextTick()
+      expect(host.children.length).toBe(4)
+      expect(html()).toBe(
+        `<span>4</span><span>2</span><span>3</span><span>1</span><!--for-->`,
+      )
+    })
+
+    test.todo('move to left & replace', async () => {
+      const arr = ref<number[]>([1, 2, 3, 4, 5])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>1</span><span>2</span><span>3</span><span>4</span><span>5</span><!--for-->`,
+      )
+
+      arr.value = [4, 1, 2, 3, 6]
+      await nextTick()
+      expect(host.children.length).toBe(5)
+      expect(html()).toBe(
+        `<span>4</span><span>1</span><span>2</span><span>3</span><span>6</span><!--for-->`,
+      )
+    })
+
+    test.todo('move to left and leaves hold', async () => {
+      const arr = ref<number[]>([1, 4, 5])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>1</span><span>4</span><span>5</span><!--for-->`,
+      )
+
+      arr.value = [4, 6]
+      await nextTick()
+      expect(host.children.length).toBe(2)
+      expect(html()).toBe(`<span>4</span><span>6</span><!--for-->`)
+    })
+
+    test.todo(
+      'moved and set to undefined element ending at the end',
+      async () => {
+        const arr = ref<number[]>([2, 4, 5])
+        const { host, html } = render(arr)
+        expect(host.children.length).toBe(3)
+        expect(html()).toBe(
+          `<span>2</span><span>4</span><span>5</span><!--for-->`,
+        )
+
+        arr.value = [4, 5, 3]
+        await nextTick()
+        expect(host.children.length).toBe(3)
+        expect(html()).toBe(
+          `<span>4</span><span>5</span><span>3</span><!--for-->`,
+        )
+      },
+    )
+
+    test('reverse element', async () => {
+      const arr = ref<number[]>([1, 2, 3, 4, 5, 6, 7, 8])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(8)
+      expect(html()).toBe(
+        `<span>1</span><span>2</span><span>3</span><span>4</span>` +
+          `<span>5</span><span>6</span><span>7</span><span>8</span><!--for-->`,
+      )
+
+      arr.value = [8, 7, 6, 5, 4, 3, 2, 1]
+      await nextTick()
+      expect(host.children.length).toBe(8)
+      expect(html()).toBe(
+        `<span>8</span><span>7</span><span>6</span><span>5</span>` +
+          `<span>4</span><span>3</span><span>2</span><span>1</span><!--for-->`,
+      )
+    })
+
+    test('something', async () => {
+      const arr = ref<number[]>([0, 1, 2, 3, 4, 5])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(6)
+      expect(html()).toBe(
+        `<span>0</span><span>1</span><span>2</span><span>3</span><span>4</span>` +
+          `<span>5</span><!--for-->`,
+      )
+
+      arr.value = [4, 3, 2, 1, 5, 0]
+      await nextTick()
+      expect(host.children.length).toBe(6)
+      expect(html()).toBe(
+        `<span>4</span><span>3</span><span>2</span><span>1</span>` +
+          `<span>5</span><span>0</span><!--for-->`,
+      )
+    })
+
+    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<string[]>([])
+
+        const host = document.createElement('div')
+        define({
+          setup() {
+            const n3 = template('<span></span>')() as any
+            setInsertionState(n3)
+            createFor(
+              () => arr.value,
+              _for_item0 => {
+                const n2 = template('<span> </span>')() 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('<div> </div>')() as any
+                const x4 = child(n4) as any
+                renderEffect(() =>
+                  setText(x4, toDisplayString(_for_item0.value.text)),
+                )
+                return n4
+              },
+              () => {
+                const n6 = template('<span> </span>', 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('<div> </div>')() 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<number[]>([1, 2, 3])
+      const { host, html } = render(arr)
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>1</span><span>2</span><span>3</span><!--for-->`,
+      )
+
+      arr.value = [1, 2, 2]
+      await nextTick()
+      expect(host.children.length).toBe(3)
+      expect(html()).toBe(
+        `<span>1</span><span>2</span><span>2</span><!--for-->`,
+      )
+      expect(`Duplicate keys`).toHaveBeenWarned()
+    })
+  })
+
+  describe('renderer: unkeyed children', () => {
+    const render = (arr: Ref<number[]>) => {
+      return define({
+        setup() {
+          const n0 = createFor(
+            () => arr.value,
+            _for_item0 => {
+              const n2 = template('<span> </span>', 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<any[]>([1, 'a', 'b', 'c'])
+      const { host, html } = define({
+        setup() {
+          const n0 = createFor(
+            () => arr.value,
+            _for_item0 => {
+              const n2 = template('<span> </span>', 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(
+        `<span>1</span><span>a</span><span>b</span><span>c</span><!--for-->`,
+      )
+
+      arr.value = ['d', 'a', 'b', 'c', 1, 'e']
+      await nextTick()
+      expect(host.children.length).toBe(6)
+      expect(html()).toBe(
+        `<span>d</span><span>a</span><span>b</span><span>c</span>` +
+          `<span>1</span><span>e</span><!--for-->`,
+      )
+    })
+
+    test('append elements with updating children without keys', async () => {
+      const arr = ref<string[]>(['hello'])
+      const { html } = render(arr as any)
+      expect(html()).toBe('<span>hello</span><!--for-->')
+
+      arr.value = ['hello', 'world']
+      await nextTick()
+      expect(html()).toBe('<span>hello</span><span>world</span><!--for-->')
+    })
+
+    test('updating children without keys', async () => {
+      const arr = ref<string[]>(['hello', 'foo'])
+      const { html } = render(arr as any)
+      expect(html()).toBe('<span>hello</span><span>foo</span><!--for-->')
+
+      arr.value = ['hello', 'bar']
+      await nextTick()
+      expect(html()).toBe('<span>hello</span><span>bar</span><!--for-->')
+
+      arr.value = ['world', 'bar']
+      await nextTick()
+      expect(html()).toBe('<span>world</span><span>bar</span><!--for-->')
+    })
+
+    test('prepend element with updating children without keys', async () => {
+      const arr = ref<string[]>(['foo', 'bar'])
+      const { html } = render(arr as any)
+      expect(html()).toBe('<span>foo</span><span>bar</span><!--for-->')
+
+      arr.value = ['hello', 'foo', 'bar']
+      await nextTick()
+      expect(html()).toBe(
+        '<span>hello</span><span>foo</span><span>bar</span><!--for-->',
+      )
+    })
+
+    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('<div> </div>')() as any
+                const x4 = child(n4) as any
+                renderEffect(() =>
+                  setText(x4, toDisplayString(_for_item0.value.text)),
+                )
+                return n4
+              },
+              () => {
+                const n6 = template('<span> </span>', 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('<span>world</span><!--if--><!--for-->')
+
+      items.value = [
+        { tag: 'div', text: 'hello' },
+        { tag: 'span', text: 'world' },
+      ]
+      await nextTick()
+      expect(host.children.length).toBe(2)
+      expect(html()).toBe(
+        '<div>hello</div><!--if--><span>world</span><!--if--><!--for-->',
+      )
+    })
+
+    test('remove elements with updating children without keys', async () => {
+      const arr = ref<string[]>(['one', 'two', 'three'])
+      const { html } = render(arr as any)
+      expect(html()).toBe(
+        '<span>one</span><span>two</span><span>three</span><!--for-->',
+      )
+
+      arr.value = ['two']
+      await nextTick()
+      expect(html()).toBe('<span>two</span><!--for-->')
+    })
+
+    test('remove a single node when children are updated', async () => {
+      const arr = ref<string[]>(['one'])
+      const { html } = render(arr as any)
+      expect(html()).toBe('<span>one</span><!--for-->')
+
+      arr.value = ['two', 'three']
+      await nextTick()
+      expect(html()).toBe('<span>two</span><span>three</span><!--for-->')
+    })
+
+    test('reorder elements', async () => {
+      const arr = ref<string[]>(['one', 'two', 'three'])
+      const { html } = render(arr as any)
+      expect(html()).toBe(
+        '<span>one</span><span>two</span><span>three</span><!--for-->',
+      )
+
+      arr.value = ['three', 'two', 'one']
+      await nextTick()
+      expect(html()).toBe(
+        '<span>three</span><span>two</span><span>one</span><!--for-->',
+      )
+    })
+  })
 })
index 9ffdf6dca571ec2217632f6e10dc85abc8c25095..dbc9ebc83195f996fc8d2e0eb81f3d050a9d6189 100644 (file)
@@ -153,6 +153,24 @@ export const createFor = (
           unmount(oldBlocks[i])
         }
       } else {
+        if (__DEV__) {
+          const keyToIndexMap: Map<any, number> = 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: [