Skip to content

Commit 3120ee8

Browse files
authored
Ensure objectsAreEqual() checks all keys in both objects (#2705)
* wip * reacrt/svelte
1 parent f9ff18c commit 3120ee8

File tree

10 files changed

+319
-2
lines changed

10 files changed

+319
-2
lines changed

packages/core/src/objectUtils.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const objectsAreEqual = <T>(
1+
export const objectsAreEqual = <T extends Record<string, any>>(
22
obj1: T,
33
obj2: T,
44
excludeKeys: {
@@ -9,6 +9,7 @@ export const objectsAreEqual = <T>(
99
return true
1010
}
1111

12+
// Check keys in obj1
1213
for (const key in obj1) {
1314
if (excludeKeys.includes(key)) {
1415
continue
@@ -23,6 +24,17 @@ export const objectsAreEqual = <T>(
2324
}
2425
}
2526

27+
// Check keys that exist in obj2 but not in obj1
28+
for (const key in obj2) {
29+
if (excludeKeys.includes(key)) {
30+
continue
31+
}
32+
33+
if (!(key in obj1)) {
34+
return false
35+
}
36+
}
37+
2638
return true
2739
}
2840

packages/react/test-app/Pages/DeferredProps/Page1.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export default () => {
2525

2626
<Link href="/deferred-props/page-1">Page 1</Link>
2727
<Link href="/deferred-props/page-2">Page 2</Link>
28+
<Link href="/deferred-props/page-3" prefetch>
29+
Page 3
30+
</Link>
2831
</>
2932
)
3033
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Deferred, usePage } from '@inertiajs/react'
2+
3+
const Alpha = () => {
4+
const { alpha } = usePage<{ alpha?: string }>().props
5+
6+
return alpha
7+
}
8+
9+
const Beta = () => {
10+
const { beta } = usePage<{ beta?: string }>().props
11+
12+
return beta
13+
}
14+
15+
export default () => {
16+
return (
17+
<>
18+
<Deferred data="alpha" fallback={<div>Loading alpha...</div>}>
19+
<Alpha />
20+
</Deferred>
21+
22+
<Deferred data="beta" fallback={<div>Loading beta...</div>}>
23+
<Beta />
24+
</Deferred>
25+
</>
26+
)
27+
}

packages/svelte/test-app/Pages/DeferredProps/Page1.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@
2121

2222
<a href="/deferred-props/page-1" use:inertia>Page 1</a>
2323
<a href="/deferred-props/page-2" use:inertia>Page 2</a>
24+
<a href="/deferred-props/page-3" use:inertia={{ prefetch: true }}>Page 3</a>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script lang="ts">
2+
import { Deferred } from '@inertiajs/svelte'
3+
4+
export let alpha: string | undefined
5+
export let beta: string | undefined
6+
</script>
7+
8+
<Deferred data="alpha">
9+
<div slot="fallback">Loading alpha...</div>
10+
{alpha}
11+
</Deferred>
12+
13+
<Deferred data="beta">
14+
<div slot="fallback">Loading beta...</div>
15+
{beta}
16+
</Deferred>

packages/vue3/test-app/Pages/DeferredProps/Page1.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ defineProps<{
2424

2525
<Link href="/deferred-props/page-1">Page 1</Link>
2626
<Link href="/deferred-props/page-2">Page 2</Link>
27+
<Link href="/deferred-props/page-3" prefetch>Page 3</Link>
2728
</template>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script setup lang="ts">
2+
import { Deferred } from '@inertiajs/vue3'
3+
4+
defineProps<{
5+
alpha?: string
6+
beta?: string
7+
}>()
8+
</script>
9+
10+
<template>
11+
<Deferred data="alpha">
12+
<template #fallback>
13+
<div>Loading alpha...</div>
14+
</template>
15+
{{ alpha }}
16+
</Deferred>
17+
18+
<Deferred data="beta">
19+
<template #fallback>
20+
<div>Loading beta...</div>
21+
</template>
22+
{{ beta }}
23+
</Deferred>
24+
</template>

tests/app/server.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,30 @@ app.get('/deferred-props/page-2', (req, res) => {
782782
}
783783
})
784784

785+
app.get('/deferred-props/page-3', (req, res) => {
786+
if (!req.headers['x-inertia-partial-data']) {
787+
return inertia.render(req, res, {
788+
component: 'DeferredProps/Page3',
789+
deferredProps: {
790+
default: ['alpha', 'beta'],
791+
},
792+
props: {},
793+
})
794+
}
795+
796+
setTimeout(
797+
() =>
798+
inertia.render(req, res, {
799+
component: 'DeferredProps/Page3',
800+
props: {
801+
alpha: req.headers['x-inertia-partial-data']?.includes('alpha') ? 'alpha value' : undefined,
802+
beta: req.headers['x-inertia-partial-data']?.includes('beta') ? 'beta value' : undefined,
803+
},
804+
}),
805+
500,
806+
)
807+
})
808+
785809
app.get('/deferred-props/many-groups', (req, res) => {
786810
const props = ['foo', 'bar', 'baz', 'qux', 'quux']
787811
const requestedProps = req.headers['x-inertia-partial-data']

tests/core/objectUtils.test.ts

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import test, { expect } from '@playwright/test'
2+
import { objectsAreEqual } from '../../packages/core/src/objectUtils'
3+
4+
test.describe('objectUtils.ts', () => {
5+
test.describe('objectsAreEqual', () => {
6+
test.describe('basic equality', () => {
7+
test('returns true for identical objects', () => {
8+
const obj1 = { a: 1, b: 2, c: 3 }
9+
const obj2 = { a: 1, b: 2, c: 3 }
10+
11+
expect(objectsAreEqual(obj1, obj2, [])).toBe(true)
12+
})
13+
14+
test('returns true for same reference', () => {
15+
const obj = { a: 1, b: 2 }
16+
17+
expect(objectsAreEqual(obj, obj, [])).toBe(true)
18+
})
19+
20+
test('returns true for empty objects', () => {
21+
expect(objectsAreEqual({}, {}, [])).toBe(true)
22+
})
23+
24+
test('returns false for objects with different values', () => {
25+
const obj1 = { a: 1, b: 2 }
26+
const obj2 = { a: 1, b: 3 }
27+
28+
expect(objectsAreEqual(obj1, obj2, [])).toBe(false)
29+
})
30+
})
31+
32+
test.describe('asymmetric objects', () => {
33+
test('returns false when second object has additional keys', () => {
34+
const obj1 = { a: 1, b: 2 }
35+
const obj2 = { a: 1, b: 2, c: 3 }
36+
37+
expect(objectsAreEqual(obj1, obj2, [])).toBe(false)
38+
})
39+
40+
test('returns false when first object has additional keys', () => {
41+
const obj1 = { a: 1, b: 2, c: 3 }
42+
const obj2 = { a: 1, b: 2 }
43+
44+
expect(objectsAreEqual(obj1, obj2, [])).toBe(false)
45+
})
46+
47+
test('handles objects with overlapping and unique keys', () => {
48+
const obj1 = {
49+
name: 'test',
50+
type: 'example',
51+
config: { option: 'value' },
52+
}
53+
54+
const obj2 = {
55+
name: 'test',
56+
type: 'example',
57+
config: { option: 'value' },
58+
extra: ['additional'],
59+
}
60+
61+
expect(objectsAreEqual(obj1, obj2, [])).toBe(false)
62+
})
63+
})
64+
65+
test.describe('key exclusion', () => {
66+
test('ignores excluded keys', () => {
67+
const obj1 = { a: 1, b: 2, ignore: 'foo' }
68+
const obj2 = { a: 1, b: 2, ignore: 'bar' }
69+
70+
expect(objectsAreEqual(obj1, obj2, ['ignore'])).toBe(true)
71+
})
72+
73+
test('does not ignore non-excluded keys', () => {
74+
const obj1 = { a: 1, b: 2, keep: 'foo' }
75+
const obj2 = { a: 1, b: 2, keep: 'bar' }
76+
77+
// @ts-expect-error - Ignore key doesn't exist in obj1
78+
expect(objectsAreEqual(obj1, obj2, ['ignore'])).toBe(false)
79+
})
80+
81+
test('handles asymmetric objects with excluded keys', () => {
82+
const obj1 = { a: 1, b: 2 }
83+
const obj2 = { a: 1, b: 2, ignore: 'value' }
84+
85+
// Should be true because the extra key is excluded
86+
// @ts-expect-error - Ignore key doesn't exist in obj1
87+
expect(objectsAreEqual(obj1, obj2, ['ignore'])).toBe(true)
88+
})
89+
90+
test('handles asymmetric objects with non-excluded keys', () => {
91+
const obj1 = { a: 1, b: 2 }
92+
const obj2 = { a: 1, b: 2, keep: 'value' }
93+
94+
// Should be false because the extra key is not excluded
95+
// @ts-expect-error - Keep key doesn't exist in obj1
96+
expect(objectsAreEqual(obj1, obj2, ['ignore'])).toBe(false)
97+
})
98+
})
99+
100+
test.describe('value types', () => {
101+
test('handles undefined values', () => {
102+
const obj1 = { a: 1, b: undefined }
103+
const obj2 = { a: 1, b: undefined }
104+
105+
expect(objectsAreEqual(obj1, obj2, [])).toBe(true)
106+
})
107+
108+
test('handles null values', () => {
109+
const obj1 = { a: 1, b: null }
110+
const obj2 = { a: 1, b: null }
111+
112+
expect(objectsAreEqual(obj1, obj2, [])).toBe(true)
113+
})
114+
115+
test('distinguishes between undefined and null', () => {
116+
const obj1 = { a: 1, b: undefined }
117+
const obj2 = { a: 1, b: null }
118+
119+
// @ts-expect-error - Different types for key 'b'
120+
expect(objectsAreEqual(obj1, obj2, [])).toBe(false)
121+
})
122+
123+
test('handles boolean values', () => {
124+
const obj1 = { a: true, b: false }
125+
const obj2 = { a: true, b: false }
126+
127+
expect(objectsAreEqual(obj1, obj2, [])).toBe(true)
128+
})
129+
130+
test('handles string values', () => {
131+
const obj1 = { a: 'hello', b: 'world' }
132+
const obj2 = { a: 'hello', b: 'world' }
133+
134+
expect(objectsAreEqual(obj1, obj2, [])).toBe(true)
135+
})
136+
137+
test('handles array values', () => {
138+
const obj1 = { a: [1, 2, 3], b: ['x', 'y'] }
139+
const obj2 = { a: [1, 2, 3], b: ['x', 'y'] }
140+
141+
expect(objectsAreEqual(obj1, obj2, [])).toBe(true)
142+
})
143+
144+
test('detects different array values', () => {
145+
const obj1 = { a: [1, 2, 3] }
146+
const obj2 = { a: [1, 2, 4] }
147+
148+
expect(objectsAreEqual(obj1, obj2, [])).toBe(false)
149+
})
150+
151+
test('handles nested objects', () => {
152+
const obj1 = { a: 1, nested: { x: 1, y: 2 } }
153+
const obj2 = { a: 1, nested: { x: 1, y: 2 } }
154+
155+
expect(objectsAreEqual(obj1, obj2, [])).toBe(true)
156+
})
157+
158+
test('detects different nested objects', () => {
159+
const obj1 = { a: 1, nested: { x: 1, y: 2 } }
160+
const obj2 = { a: 1, nested: { x: 1, y: 3 } }
161+
162+
expect(objectsAreEqual(obj1, obj2, [])).toBe(false)
163+
})
164+
165+
test('handles functions', () => {
166+
const fn = () => 'test'
167+
const obj1 = { a: 1, fn }
168+
const obj2 = { a: 1, fn }
169+
170+
expect(objectsAreEqual(obj1, obj2, [])).toBe(true)
171+
})
172+
173+
test('detects different functions', () => {
174+
const obj1 = { a: 1, fn: () => 'test1' }
175+
const obj2 = { a: 1, fn: () => 'test2' }
176+
177+
expect(objectsAreEqual(obj1, obj2, [])).toBe(false)
178+
})
179+
})
180+
})
181+
})

tests/deferred-props.spec.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, test } from '@playwright/test'
2-
import { clickAndWaitForResponse } from './support'
2+
import { clickAndWaitForResponse, consoleMessages } from './support'
33

44
test('can load deferred props', async ({ page }) => {
55
await page.goto('/deferred-props/page-1')
@@ -244,3 +244,31 @@ test('can partial reload deferred props independently', async ({ page }) => {
244244
expect(finalFooTimestamp).toBe(newFooTimestamp) // foo unchanged
245245
expect(finalBarTimestamp).not.toBe(newBarTimestamp) // bar changed
246246
})
247+
248+
test('prefetch works with deferred props without errors', async ({ page }) => {
249+
consoleMessages.listen(page)
250+
const prefetch = page.waitForResponse('/deferred-props/page-3')
251+
252+
await page.goto('/deferred-props/page-1')
253+
await expect(page.getByRole('link', { name: 'Page 3' })).toBeVisible()
254+
255+
consoleMessages.errors = []
256+
257+
await page.getByRole('link', { name: 'Page 3' }).hover()
258+
await prefetch
259+
260+
const deferred = page.waitForResponse(
261+
(response) =>
262+
response.url().includes('/deferred-props/page-3') && 'x-inertia-partial-data' in response.request().headers(),
263+
)
264+
265+
await page.getByRole('link', { name: 'Page 3' }).click()
266+
await page.waitForURL('/deferred-props/page-3')
267+
268+
await deferred
269+
270+
await expect(page.getByText('alpha value')).toBeVisible()
271+
await expect(page.getByText('beta value')).toBeVisible()
272+
273+
expect(consoleMessages.errors).toHaveLength(0)
274+
})

0 commit comments

Comments
 (0)