style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
@@ -11,10 +11,10 @@
|
||||
* Uses React Query's QueryClient directly (no mocked server).
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { act } from '@testing-library/react'
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
import type { ListItem, ListItemsResponse } from '../api/listsClient.js'
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { act } from '@testing-library/react';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import type { ListItem, ListItemsResponse } from '../api/listsClient.js';
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -26,94 +26,88 @@ function makeItem(overrides: Partial<ListItem> = {}): ListItem {
|
||||
checked: false,
|
||||
rank: 'a0',
|
||||
...overrides,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function makeItemsResponse(items: ListItem[]): ListItemsResponse {
|
||||
return { items }
|
||||
return { items };
|
||||
}
|
||||
|
||||
// ── Tests ──────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('ListDetail — D-07 optimistic update + rollback', () => {
|
||||
let queryClient: QueryClient
|
||||
const LIST_ID = 10
|
||||
let queryClient: QueryClient;
|
||||
const LIST_ID = 10;
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('checking an item immediately updates the UI before server responds', async () => {
|
||||
const item = makeItem({ checked: false })
|
||||
queryClient.setQueryData<ListItemsResponse>(['list', LIST_ID], makeItemsResponse([item]))
|
||||
const item = makeItem({ checked: false });
|
||||
queryClient.setQueryData<ListItemsResponse>(['list', LIST_ID], makeItemsResponse([item]));
|
||||
|
||||
// Simulate the onMutate optimistic update (D-07 pattern)
|
||||
await act(async () => {
|
||||
await queryClient.cancelQueries({ queryKey: ['list', LIST_ID] })
|
||||
await queryClient.cancelQueries({ queryKey: ['list', LIST_ID] });
|
||||
queryClient.setQueryData<ListItemsResponse>(['list', LIST_ID], (old) => ({
|
||||
items: (old?.items ?? []).map((i) =>
|
||||
i.id === item.id ? { ...i, checked: true } : i,
|
||||
),
|
||||
}))
|
||||
})
|
||||
items: (old?.items ?? []).map((i) => (i.id === item.id ? { ...i, checked: true } : i)),
|
||||
}));
|
||||
});
|
||||
|
||||
const updated = queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID])
|
||||
expect(updated?.items[0].checked).toBe(true)
|
||||
})
|
||||
const updated = queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID]);
|
||||
expect(updated?.items[0].checked).toBe(true);
|
||||
});
|
||||
|
||||
it('unchecking an item immediately updates the UI before server responds', async () => {
|
||||
const item = makeItem({ checked: true })
|
||||
queryClient.setQueryData<ListItemsResponse>(['list', LIST_ID], makeItemsResponse([item]))
|
||||
const item = makeItem({ checked: true });
|
||||
queryClient.setQueryData<ListItemsResponse>(['list', LIST_ID], makeItemsResponse([item]));
|
||||
|
||||
await act(async () => {
|
||||
await queryClient.cancelQueries({ queryKey: ['list', LIST_ID] })
|
||||
await queryClient.cancelQueries({ queryKey: ['list', LIST_ID] });
|
||||
queryClient.setQueryData<ListItemsResponse>(['list', LIST_ID], (old) => ({
|
||||
items: (old?.items ?? []).map((i) =>
|
||||
i.id === item.id ? { ...i, checked: false } : i,
|
||||
),
|
||||
}))
|
||||
})
|
||||
items: (old?.items ?? []).map((i) => (i.id === item.id ? { ...i, checked: false } : i)),
|
||||
}));
|
||||
});
|
||||
|
||||
const updated = queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID])
|
||||
expect(updated?.items[0].checked).toBe(false)
|
||||
})
|
||||
const updated = queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID]);
|
||||
expect(updated?.items[0].checked).toBe(false);
|
||||
});
|
||||
|
||||
it('rolls back the checked state if the server PATCH returns an error', () => {
|
||||
const item = makeItem({ checked: false })
|
||||
queryClient.setQueryData<ListItemsResponse>(['list', LIST_ID], makeItemsResponse([item]))
|
||||
const item = makeItem({ checked: false });
|
||||
queryClient.setQueryData<ListItemsResponse>(['list', LIST_ID], makeItemsResponse([item]));
|
||||
|
||||
// Step 1: capture previous state (as onMutate would)
|
||||
const previous = queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID])
|
||||
const previous = queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID]);
|
||||
|
||||
// Step 2: apply optimistic update
|
||||
act(() => {
|
||||
queryClient.setQueryData<ListItemsResponse>(['list', LIST_ID], (old) => ({
|
||||
items: (old?.items ?? []).map((i) =>
|
||||
i.id === item.id ? { ...i, checked: true } : i,
|
||||
),
|
||||
}))
|
||||
})
|
||||
items: (old?.items ?? []).map((i) => (i.id === item.id ? { ...i, checked: true } : i)),
|
||||
}));
|
||||
});
|
||||
|
||||
// Verify it was applied
|
||||
const after = queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID])
|
||||
expect(after?.items[0].checked).toBe(true)
|
||||
const after = queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID]);
|
||||
expect(after?.items[0].checked).toBe(true);
|
||||
|
||||
// Step 3: simulate onError rollback
|
||||
act(() => {
|
||||
if (previous) {
|
||||
queryClient.setQueryData(['list', LIST_ID], previous)
|
||||
queryClient.setQueryData(['list', LIST_ID], previous);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Should be rolled back
|
||||
const rolledBack = queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID])
|
||||
expect(rolledBack?.items[0].checked).toBe(false)
|
||||
})
|
||||
const rolledBack = queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID]);
|
||||
expect(rolledBack?.items[0].checked).toBe(false);
|
||||
});
|
||||
|
||||
it('adding an item shows it in the list immediately (optimistic insert)', () => {
|
||||
queryClient.setQueryData<ListItemsResponse>(['list', LIST_ID], makeItemsResponse([]))
|
||||
queryClient.setQueryData<ListItemsResponse>(['list', LIST_ID], makeItemsResponse([]));
|
||||
|
||||
const optimisticItem: ListItem = {
|
||||
id: -Date.now(),
|
||||
@@ -121,30 +115,30 @@ describe('ListDetail — D-07 optimistic update + rollback', () => {
|
||||
text: 'milk',
|
||||
checked: false,
|
||||
rank: 'a0',
|
||||
}
|
||||
};
|
||||
|
||||
act(() => {
|
||||
queryClient.setQueryData<ListItemsResponse>(['list', LIST_ID], (old) => ({
|
||||
items: [...(old?.items ?? []), optimisticItem],
|
||||
}))
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
||||
const data = queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID])
|
||||
expect(data?.items).toHaveLength(1)
|
||||
expect(data?.items[0].text).toBe('milk')
|
||||
const data = queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID]);
|
||||
expect(data?.items).toHaveLength(1);
|
||||
expect(data?.items[0].text).toBe('milk');
|
||||
// Optimistic item has negative id
|
||||
expect(data?.items[0].id).toBeLessThan(0)
|
||||
})
|
||||
expect(data?.items[0].id).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('removes the optimistically-added item if the server POST returns an error', () => {
|
||||
const existingItem = makeItem({ id: 1 })
|
||||
const existingItem = makeItem({ id: 1 });
|
||||
queryClient.setQueryData<ListItemsResponse>(
|
||||
['list', LIST_ID],
|
||||
makeItemsResponse([existingItem]),
|
||||
)
|
||||
);
|
||||
|
||||
// Capture previous before optimistic add
|
||||
const previous = queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID])
|
||||
const previous = queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID]);
|
||||
|
||||
// Apply optimistic add
|
||||
const optimisticItem: ListItem = {
|
||||
@@ -153,65 +147,63 @@ describe('ListDetail — D-07 optimistic update + rollback', () => {
|
||||
text: 'optimistic',
|
||||
checked: false,
|
||||
rank: 'a1',
|
||||
}
|
||||
};
|
||||
|
||||
act(() => {
|
||||
queryClient.setQueryData<ListItemsResponse>(['list', LIST_ID], (old) => ({
|
||||
items: [...(old?.items ?? []), optimisticItem],
|
||||
}))
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
||||
expect(
|
||||
queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID])?.items,
|
||||
).toHaveLength(2)
|
||||
expect(queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID])?.items).toHaveLength(2);
|
||||
|
||||
// Simulate rollback on error
|
||||
act(() => {
|
||||
if (previous) queryClient.setQueryData(['list', LIST_ID], previous)
|
||||
})
|
||||
if (previous) queryClient.setQueryData(['list', LIST_ID], previous);
|
||||
});
|
||||
|
||||
const rolledBack = queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID])
|
||||
expect(rolledBack?.items).toHaveLength(1)
|
||||
expect(rolledBack?.items[0].id).toBe(1) // original item
|
||||
})
|
||||
const rolledBack = queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID]);
|
||||
expect(rolledBack?.items).toHaveLength(1);
|
||||
expect(rolledBack?.items[0].id).toBe(1); // original item
|
||||
});
|
||||
|
||||
it('completed items sink to the "completed" section at the bottom (D-05)', () => {
|
||||
const activeItem = makeItem({ id: 1, checked: false, rank: 'a0', text: 'active' })
|
||||
const completedItem = makeItem({ id: 2, checked: true, rank: 'a1', text: 'done' })
|
||||
const items = [activeItem, completedItem]
|
||||
const activeItem = makeItem({ id: 1, checked: false, rank: 'a0', text: 'active' });
|
||||
const completedItem = makeItem({ id: 2, checked: true, rank: 'a1', text: 'done' });
|
||||
const items = [activeItem, completedItem];
|
||||
|
||||
// The ListDetail splits items into active and completed sections
|
||||
const activeItems = items.filter((i) => !i.checked).sort((a, b) =>
|
||||
a.rank < b.rank ? -1 : a.rank > b.rank ? 1 : 0,
|
||||
)
|
||||
const completedItems = items.filter((i) => i.checked)
|
||||
const activeItems = items
|
||||
.filter((i) => !i.checked)
|
||||
.sort((a, b) => (a.rank < b.rank ? -1 : a.rank > b.rank ? 1 : 0));
|
||||
const completedItems = items.filter((i) => i.checked);
|
||||
|
||||
expect(activeItems).toHaveLength(1)
|
||||
expect(activeItems[0].text).toBe('active')
|
||||
expect(completedItems).toHaveLength(1)
|
||||
expect(completedItems[0].text).toBe('done')
|
||||
expect(activeItems).toHaveLength(1);
|
||||
expect(activeItems[0].text).toBe('active');
|
||||
expect(completedItems).toHaveLength(1);
|
||||
expect(completedItems[0].text).toBe('done');
|
||||
// Active comes before completed by data structure (rendered above completed section)
|
||||
})
|
||||
});
|
||||
|
||||
it('delete removes item immediately with NO rollback (delete-wins D-09)', async () => {
|
||||
const item1 = makeItem({ id: 1, text: 'keep', rank: 'a0' })
|
||||
const item2 = makeItem({ id: 2, text: 'delete-me', rank: 'a1' })
|
||||
const item1 = makeItem({ id: 1, text: 'keep', rank: 'a0' });
|
||||
const item2 = makeItem({ id: 2, text: 'delete-me', rank: 'a1' });
|
||||
queryClient.setQueryData<ListItemsResponse>(
|
||||
['list', LIST_ID],
|
||||
makeItemsResponse([item1, item2]),
|
||||
)
|
||||
);
|
||||
|
||||
// onMutate for delete: remove immediately, no previous state saved (D-09)
|
||||
await act(async () => {
|
||||
await queryClient.cancelQueries({ queryKey: ['list', LIST_ID] })
|
||||
await queryClient.cancelQueries({ queryKey: ['list', LIST_ID] });
|
||||
queryClient.setQueryData<ListItemsResponse>(['list', LIST_ID], (old) => ({
|
||||
items: (old?.items ?? []).filter((i) => i.id !== 2),
|
||||
}))
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
||||
const data = queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID])
|
||||
expect(data?.items).toHaveLength(1)
|
||||
expect(data?.items[0].id).toBe(1)
|
||||
const data = queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID]);
|
||||
expect(data?.items).toHaveLength(1);
|
||||
expect(data?.items[0].id).toBe(1);
|
||||
// item2 is gone, no rollback mechanism exists (delete-wins)
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user