fix(frontend): preserve edited server drafts (#6156)

* fix(frontend): preserve edited server drafts

* fix(frontend): retain Xray server projections

* fix(frontend): keep draft controls internal

* fix(frontend): rehydrate saved redacted settings

* fix(frontend): order saved draft hydration

* fix(frontend): preserve draft baselines on security saves

---------

Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local>
This commit is contained in:
PathGao
2026-07-30 08:59:53 +08:00
committed by GitHub
parent 8d02ae28f5
commit 66740b7ef4
6 changed files with 333 additions and 47 deletions
+86
View File
@@ -0,0 +1,86 @@
import { StrictMode } from 'react';
import { act, renderHook } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { useServerDraft } from '@/hooks/useServerDraft';
describe('useServerDraft', () => {
it('keeps an edited draft when the server refetches', () => {
const { result, rerender } = renderHook(
({ server }) => useServerDraft(server, (value) => ({ ...value }), (left, right) => left.value === right.value),
{ initialProps: { server: { value: 'one' } } },
);
act(() => result.current.setDraft({ value: 'edited' }));
rerender({ server: { value: 'two' } });
expect(result.current.draft).toEqual({ value: 'edited' });
expect(result.current.isDirty).toBe(true);
});
it('accepts a refetch that matches the saved draft', () => {
const { result, rerender } = renderHook(
({ server }) => useServerDraft(server, (value) => ({ ...value }), (left, right) => left.value === right.value),
{ initialProps: { server: { value: 'one' } } },
);
act(() => result.current.setDraft({ value: 'saved' }));
rerender({ server: { value: 'saved' } });
expect(result.current.draft).toEqual({ value: 'saved' });
expect(result.current.isDirty).toBe(false);
});
it('compares a preserved draft with the latest server value', () => {
const { result, rerender } = renderHook(
({ server }) => useServerDraft(server, (value) => ({ ...value }), (left, right) => left.value === right.value),
{ initialProps: { server: { value: 'one' } } },
);
act(() => result.current.setDraft({ value: 'later' }));
rerender({ server: { value: 'saved' } });
act(() => result.current.setDraft({ value: 'one' }));
expect(result.current.isDirty).toBe(true);
});
it('hydrates clean drafts under StrictMode', () => {
const { result, rerender } = renderHook(
({ server }) => useServerDraft(server, (value) => ({ ...value }), (left, right) => left.value === right.value),
{
initialProps: { server: { value: 'one' } },
wrapper: StrictMode,
},
);
rerender({ server: { value: 'two' } });
expect(result.current.draft).toEqual({ value: 'two' });
expect(result.current.isDirty).toBe(false);
});
it('preserves an edit made before the first server response', () => {
const { result, rerender } = renderHook(
({ server }) => useServerDraft(server, (value) => ({ ...value }), (left, right) => left.value === right.value),
{ initialProps: { server: undefined as { value: string } | undefined } },
);
act(() => result.current.setDraft({ value: 'edited' }));
rerender({ server: { value: 'one' } });
expect(result.current.draft).toEqual({ value: 'edited' });
expect(result.current.isDirty).toBe(true);
});
it('marks a sent draft clean before its refetch arrives', () => {
const { result } = renderHook(
({ server }) => useServerDraft(server, (value) => ({ ...value }), (left, right) => left.value === right.value),
{ initialProps: { server: { value: 'one' } } },
);
act(() => result.current.setDraft({ value: 'saved' }));
act(() => result.current.markSaved({ value: 'saved' }));
expect(result.current.isDirty).toBe(false);
});
});