Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/agent/tests/pagination.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,12 @@ it.each(['array', 'object', 'map', 'set'] as const)(
'pages through nested %s values in order',
async (kind) => {
const values = Array.from({ length: 121 }, (_, i) => i)
// Keep the fixture's numeric values aligned with alphabetical object keys.
const payload =
kind === 'array'
? values
: kind === 'object'
? Object.fromEntries(values.map((i) => [`key${i}`, i]))
? Object.fromEntries(values.map((i) => [`key${String(i).padStart(3, '0')}`, i]))
: kind === 'map'
? new Map(values.map((i) => [`key${i}`, i]))
: new Set(values)
Expand Down Expand Up @@ -295,11 +296,12 @@ it.each(['array', 'object', 'string'] as const)(
'paginates cached setup computed %s values',
async (kind) => {
const values = Array.from({ length: 121 }, (_, i) => i)
// Keep the fixture's numeric values aligned with alphabetical object keys.
const payload =
kind === 'array'
? values
: kind === 'object'
? Object.fromEntries(values.map((i) => [`key${i}`, i]))
? Object.fromEntries(values.map((i) => [`key${String(i).padStart(3, '0')}`, i]))
: 'x'.repeat(6000)
const getter = vi.fn(() => payload)
const cached = computed(getter)
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/codec/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ function encodeUnsafe(value: unknown, state: EncodeState): EncodedValue {

function encodeObject(value: object, handle: ValueHandle, state: EncodeState): EncodedValue {
const keys = state.entryKeys ?? safeOwnEnumerableStringKeys(value)
if (!state.entryKeys) keys.sort()
const previewKeys = keys.slice(state.entryOffset, state.entryOffset + state.maxEntries)
return {
kind: 'object',
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export class ComponentStateCollector {
const setItems = value instanceof Set ? Array.from(value) : undefined
const objectKeys =
value && typeof value === 'object' && !Array.isArray(value) && !mapEntries && !setItems
? safeOwnEnumerableStringKeys(value)
? safeOwnEnumerableStringKeys(value).sort()
: undefined
const encodeOptions = {
handles: this.handles,
Expand Down
35 changes: 34 additions & 1 deletion tests/unit/codec/encode-value.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,44 @@ describe('encodeValue', () => {
expect(encoded).toMatchObject({ entries: 2, kind: 'object' })
if (encoded.kind !== 'object') return
expect(encoded.preview).toEqual([
{ key: 'ok', value: { kind: 'number', value: 1 } },
{ key: 'broken', value: { kind: 'error', message: 'getter exploded', name: 'Error' } },
{ key: 'ok', value: { kind: 'number', value: 1 } },
])
})

it('preserves an explicit key snapshot order without mutating it', () => {
const entryKeys = ['zebra', 'alpha', 'middle']
const encoded = encodeValue(
{ alpha: 1, middle: 2, zebra: 3 },
{ entryKeys, entryOffset: 1, maxEntries: 1 },
)

expect(encoded).toMatchObject({
kind: 'object',
preview: [{ key: 'alpha', value: { kind: 'number', value: 1 } }],
})
expect(entryKeys).toEqual(['zebra', 'alpha', 'middle'])
})

it('preserves array indices and Map/Set insertion order', () => {
const values = [
{ value: ['zebra', 'alpha'], keys: ['0', '1'] },
{
value: new Map([
['zebra', 1],
['alpha', 2],
]),
keys: ['zebra', 'alpha'],
},
{ value: new Set(['zebra', 'alpha']), keys: ['0', '1'] },
]

for (const { value, keys } of values) {
const encoded = encodeValue(value)
expect('preview' in encoded && encoded.preview.map((entry) => entry.key)).toEqual(keys)
}
})

it('never executes prototype accessors of class instances', () => {
let calls = 0
class Model {
Expand Down
61 changes: 61 additions & 0 deletions tests/unit/runtime/state-value-order.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// @vitest-environment happy-dom

import { describe, expect, it } from 'vitest'
import { createDevtoolsRuntime } from '../../../packages/kit/src/runtime/runtime'

describe('object property pagination', () => {
it('keeps preview, show-more expansion, and cursor pages in one alphabetical order', () => {
const keys = Array.from({ length: 65 }, (_, index) => `field${String(index).padStart(2, '0')}`)
const payload = Object.fromEntries([...keys].reverse().map((key) => [key, key]))
const instance = {
uid: 0,
type: { name: 'ObjectOrder' },
setupState: { payload },
subTree: {},
}
const runtime = createDevtoolsRuntime()
try {
runtime.dispatch({
type: 'app:init',
app: { _instance: instance },
version: '3.5.0',
vueTypes: {},
time: 1,
})
const collector = runtime.componentState
const componentId = runtime.registry.getComponentId(instance)!
const snapshot = collector.snapshot(componentId, 30, 30)!
const value = snapshot.sections
.find((section) => section.id === 'setup')!
.entries.find((entry) => entry.key === 'payload')!.value
if (value.kind !== 'object' || !value.handle) throw new Error('Missing object preview')
expect(value.preview.map((entry) => entry.key)).toEqual(keys.slice(0, 30))

const expanded = collector.expand(value.handle, [], 60)
if (!expanded || !('value' in expanded) || expanded.value.kind !== 'object')
throw new Error('Missing expanded object')
expect(expanded.value.preview.map((entry) => entry.key)).toEqual(keys.slice(0, 60))

let page = 1
let snapshotId: string | undefined
const allKeys: string[] = []
while (true) {
const result = collector.expandPage(
'ordered-object',
{ page, pageSize: 30, snapshotId },
value.handle,
)
if (!result || !('value' in result) || result.value.kind !== 'object')
throw new Error('Missing object page')
allKeys.push(...result.value.preview.map((entry) => entry.key))
if (!result.pagination?.next) break
page = result.pagination.next
snapshotId = result.snapshotId
}
expect(allKeys).toEqual(keys)
expect(Object.keys(payload)).toEqual([...keys].reverse())
} finally {
runtime.dispose()
}
})
})
Loading