ReviewOS

also looking at this

stacks/stx

fix(native): preserve authored keys in mutation nodes

#2084
Closed chris wants to merge codex/native-mutation-authored-keys into main
3 files +77 -3

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

packages/stx/test/native/native-authored-keys.test.tsadded+68-0
Changes to packages/stx/test/native/native-authored-keys.test.ts
@@ -0,0 +1,68 @@
1import { describe, expect, it } from 'bun:test'
2import path from 'node:path'
3import vm from 'node:vm'
4import { compileSharedScreenBundle } from '../../src/native/compiler/bundle'
5import { createNode } from '../../src/native/compiler/ir'
6import { createNativeHost } from '../../src/native/runtime/native-host'
7import { createTempDir } from '../../src/temp-dir'
8
9describe('authored native reconciliation keys', () => {
10 it('preserves the initial key when a production conditional creates its replacement', async () => {
11 const root = createTempDir('stx-native-authored-keys-')
12 const file = path.join(root, 'Keys.stx')
13 await Bun.write(file, `<script>
14const safe = state(false)
15globalThis.toggleKeyProbe = () => safe.set(!safe())
16</script>
17<View>
18 <View :if="!safe()" key="retained-container" testID="key-probe"><Text>Plain</Text></View>
19 <SafeAreaView :if="safe()" key="retained-container" testID="key-probe"><Text>Safe</Text></SafeAreaView>
20</View>`)
21 const { code } = await compileSharedScreenBundle(file, { minify: true })
22 const messages: any[] = []
23 const scope: Record<string, any> = {
24 console, Promise, Date, Map, Set, WeakMap, WeakSet,
25 __stxNativeBridge: { platform: 'android', capabilityProtocolVersion: 1, capabilities: [],
26 postMessage: (raw: string) => messages.push(JSON.parse(raw)), onMessage() {} },
27 }
28 vm.runInNewContext(code, scope)
29 const settle = async () => { for (let i = 0; i < 6; i++) await Bun.sleep(0) }
30 await settle()
31 function find(node: any): any {
32 if (node?.props?.testID === 'key-probe') return node
33 for (const child of node?.children ?? []) {
34 const found = find(child)
35 if (found) return found
36 }
37 }
38 expect(find(messages.find(message => message.type === 'RENDER')?.payload.document)?.key).toBe('retained-container')
39 for (const type of ['SafeAreaView', 'View', 'SafeAreaView']) {
40 const before = messages.length
41 scope.toggleKeyProbe()
42 await settle()
43 const created = messages.slice(before).flatMap(message => message.payload?.operations ?? [])
44 .find(operation => operation.op === 'createNode' && operation.node?.props?.testID === 'key-probe')
45 expect(created?.node.type).toBe(type)
46 expect(created?.node.key).toBe('retained-container')
47 }
48 })
49
50 it('retains authored keys through cloning, removal and rematerialization', () => {
51 const operations: any[] = []
52 const host = createNativeHost({ send: batch => operations.push(...batch), schedule() {} })
53 const parent = host.adopt({ __stxId: 'root' }, 'View')
54 const descriptor = createNode('TextInput')
55 descriptor.key = 'draft-key'
56 const original = host.adopt({ __stxId: 'original' }, 'TextInput', undefined, descriptor)
57 const cloned = host.clone(original)
58 host.insert(parent, cloned)
59 host.flush()
60 expect(operations.find(operation => operation.op === 'createNode' && operation.id === cloned.__stxId)?.node.key).toBe('draft-key')
61 host.remove(cloned)
62 host.flush()
63 operations.length = 0
64 host.insert(parent, cloned)
65 host.flush()
66 expect(operations.find(operation => operation.op === 'createNode' && operation.id === cloned.__stxId)?.node.key).toBe('draft-key')
67 })
68})