feat(client-runtime): carry the layered view and a field reset through the settings scope
A form needs two things the snapshot did not carry. The `user` layer tells it which fields the user overrode — presence, not value equality, because an override equal to the composition default is still an override — and `base` is what a cleared field reverts to. `unset` is that clear, sharing `set`'s queue, revision fence, and rejected-write recovery through one write path.
This commit is contained in:
@@ -293,6 +293,77 @@ describe('SettingsScopeController', () => {
|
||||
expect(describeCall).not.toHaveBeenCalled()
|
||||
expect(mutate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('carries the composition base and the user layer into the snapshot', async () => {
|
||||
const layered: SettingsNamespaceView = {
|
||||
...view({ preference: 'dark' }, 3),
|
||||
base: { preference: 'system' },
|
||||
user: { preference: 'dark' },
|
||||
}
|
||||
const describeCall = vi.fn()
|
||||
.mockResolvedValueOnce(ok({ writable: true, hasDocument: true, namespaces: [layered] }))
|
||||
const scope = new SettingsScopeController<UiTestSettings>(
|
||||
{ settings: { describe: describeCall } } as never,
|
||||
{ namespace: 'ui-test' },
|
||||
)
|
||||
|
||||
await scope.load()
|
||||
|
||||
expect(scope.getSnapshot()).toMatchObject({
|
||||
value: { preference: 'dark' },
|
||||
base: { preference: 'system' },
|
||||
user: { preference: 'dark' },
|
||||
})
|
||||
})
|
||||
|
||||
it('reports an inherited field as absent from the user layer', async () => {
|
||||
const inherited: SettingsNamespaceView = { ...view({ preference: 'system' }, 1), base: { preference: 'system' } }
|
||||
const describeCall = vi.fn()
|
||||
.mockResolvedValueOnce(ok({ writable: true, hasDocument: true, namespaces: [inherited] }))
|
||||
const scope = new SettingsScopeController<UiTestSettings>(
|
||||
{ settings: { describe: describeCall } } as never,
|
||||
{ namespace: 'ui-test' },
|
||||
)
|
||||
|
||||
await scope.load()
|
||||
|
||||
expect(scope.getSnapshot().user).toBeUndefined()
|
||||
})
|
||||
|
||||
it('clears one field through an unset op fenced by the held revision', async () => {
|
||||
const mutate = vi.fn().mockResolvedValueOnce(ok(view({ preference: 'system' }, 4)))
|
||||
const describeCall = vi.fn().mockResolvedValueOnce(described({ preference: 'dark' }, 3))
|
||||
const scope = new SettingsScopeController<UiTestSettings>(
|
||||
{ settings: { describe: describeCall, mutate } } as never,
|
||||
{ namespace: 'ui-test' },
|
||||
)
|
||||
await scope.load()
|
||||
|
||||
await scope.unset('preference')
|
||||
|
||||
expect(mutate).toHaveBeenCalledWith({
|
||||
ns: 'ui-test',
|
||||
ops: [{ op: 'unset', path: ['preference'] }],
|
||||
expectedRevision: 3,
|
||||
})
|
||||
expect(scope.getSnapshot()).toMatchObject({ value: { preference: 'system' }, revision: 4 })
|
||||
})
|
||||
|
||||
it('recovers the Host state when the latest clear is refused', async () => {
|
||||
const mutate = vi.fn().mockResolvedValueOnce(rejected())
|
||||
const describeCall = vi.fn()
|
||||
.mockResolvedValueOnce(described({ preference: 'dark' }, 3))
|
||||
.mockResolvedValueOnce(described({ preference: 'light' }, 5))
|
||||
const scope = new SettingsScopeController<UiTestSettings>(
|
||||
{ settings: { describe: describeCall, mutate } } as never,
|
||||
{ namespace: 'ui-test' },
|
||||
)
|
||||
await scope.load()
|
||||
|
||||
await scope.unset('preference')
|
||||
|
||||
expect(scope.getSnapshot()).toMatchObject({ value: { preference: 'light' }, revision: 5 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('bindSettingsScope', () => {
|
||||
|
||||
Reference in New Issue
Block a user