// @vitest-environment jsdom
//
// hydration-fiber-shape integration test: ProvideZero must produce the SAME
// fiber tree shape on SSR and on the client, so React's useId() (which keys
// on the consuming fiber's tree position) returns identical ids on both
// sides and hydration is clean.
//
// regression: before this guarantee was enforced, ProvideZero returned the
// 3 context providers DIRECTLY on SSR but wrapped them in ProvideZeroActive
// on the client. that extra wrapper fiber shifted every descendant's tree
// id by a step → hydration mismatch on every useId consumer (SVG ids,
// form-input ids, etc).
//
// fix: ProvideZero is now ONE component with an early-return for SSG/disable
// before any hook call. both branches render the SAME 3-provider shell with
// the SAME number of child slots inside ZeroContext.Provider — child index
// stability is what keeps useId stable.
import { createSchema, string, table } from '@rocicorp/zero'
import { useId, type ReactNode } from 'react'
import { renderToString } from 'react-dom/server'
import { expect, test } from 'vitest'
import { createZeroClient } from './createZeroClient'
import { IS_SERVER, IS_SERVER_RUNTIME } from './helpers/platform'
const todoTable = table('todo')
.columns({ id: string(), title: string() })
.primaryKey('id')
const schema = createSchema({ tables: [todoTable] })
const client = createZeroClient({
schema,
models: {},
groupedQueries: {},
instanceName: 'provider-shape-test',
})
function IdProbe({ name }: { name: string }) {
const id = useId()
return
}
function Tree({ children, disable }: { children?: ReactNode; disable?: boolean }) {
return (
{children}
)
}
function readIds(html: string): Record {
const out: Record = {}
const re = /data-name="([^"]+)" data-probe-id="([^"]+)"/g
let m: RegExpExecArray | null
while ((m = re.exec(html))) out[m[1]!] = m[2]!
return out
}
test('test environment loads on-zero client runtime', () => {
expect(IS_SERVER_RUNTIME).toBe(false)
expect(IS_SERVER).toBe(false)
})
test('useId is stable across active vs disabled ProvideZero renders', () => {
const active = readIds(renderToString())
const disabled = readIds(renderToString())
expect(Object.keys(active).length).toBe(3)
expect(disabled).toEqual(active)
})
test('rendered DOM structure matches between active and disabled (probe ids stripped)', () => {
const strip = (s: string) => s.replace(/ data-probe-id="[^"]+"/g, '')
const active = strip(renderToString())
const disabled = strip(renderToString())
expect(disabled).toBe(active)
})
// regression for the soot ProjectZeroGate residual re-parenting: the consumer
// pattern toggles disable=true → disable=false in place once user.id resolves
// from the SSR placeholder ('anon-ssr') to the minted client anon id. that
// MUST NOT change the hook-call count of ProvideZero (would violate
// rules-of-hooks) or shift descendant useId (would re-fire hydration
// mismatches). every render of ProvideZero on the client runs the SAME
// hook set; only the returned JSX and the instance-creation effect body
// vary with disable.
test('client-side hook count is stable across disable=true ↔ false', () => {
// proxy: render both states twice and confirm useId values are identical
// each time (a hook-count flip would mis-allocate the useId slots).
const r1 = readIds(renderToString())
const r2 = readIds(renderToString())
const r3 = readIds(renderToString())
const r4 = readIds(renderToString())
expect(r2).toEqual(r1)
expect(r3).toEqual(r1)
expect(r4).toEqual(r1)
})