Skip to content

Commit 501a061

Browse files
committed
refactor(tests): enhance todo-context tests with improved localStorage handling and cleanup
This update refines the todo-context tests by introducing a consistent localStorage polyfill for Node environments, ensuring reliable test execution. It also improves the cleanup logic in the tests to maintain a clean state between runs. Additionally, minor formatting adjustments are made for better readability.
1 parent b12ecc7 commit 501a061

File tree

2 files changed

+79
-75
lines changed

2 files changed

+79
-75
lines changed

apps/todo-app/app/lib/__tests__/todo-context.test.tsx

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { describe, it, expect } from 'vitest';
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
22
import { render, screen, act } from '@testing-library/react';
33
import { TodoProvider, useTodoStore, getFilteredTodos } from '../todo-context';
44
import type { Todo } from '@todo-starter/utils';
5-
import * as Utils from '@todo-starter/utils';
65

76
// Mock crypto.randomUUID for consistent testing
87
Object.defineProperty(global, 'crypto', {
@@ -22,23 +21,15 @@ function TestComponent() {
2221
<button type="button" onClick={() => addTodo('New todo')} data-testid="add-todo">
2322
Add Todo
2423
</button>
25-
<button
26-
type="button"
27-
onClick={() => todos.length > 0 && toggleTodo(todos[0].id)}
28-
data-testid="toggle-todo"
29-
>
24+
<button type="button" onClick={() => todos.length > 0 && toggleTodo(todos[0].id)} data-testid="toggle-todo">
3025
Toggle First Todo
3126
</button>
32-
<button
33-
type="button"
34-
onClick={() => todos.length > 0 && deleteTodo(todos[0].id)}
35-
data-testid="delete-todo"
36-
>
27+
<button type="button" onClick={() => todos.length > 0 && deleteTodo(todos[0].id)} data-testid="delete-todo">
3728
Delete First Todo
3829
</button>
39-
<button
30+
<button
4031
type="button"
41-
onClick={() => todos.length > 0 && updateTodo(todos[0].id, 'Updated text')}
32+
onClick={() => todos.length > 0 && updateTodo(todos[0].id, 'Updated text')}
4233
data-testid="update-todo"
4334
>
4435
Update First Todo
@@ -73,13 +64,21 @@ describe('todo-context', () => {
7364
beforeEach(() => {
7465
// allow storage helpers to operate by switching env off 'test' for these tests
7566
process.env.NODE_ENV = 'development';
76-
try { window.localStorage.removeItem(STORAGE_KEY); } catch {}
67+
try {
68+
window.localStorage.removeItem(STORAGE_KEY);
69+
} catch {
70+
/* ignore */
71+
}
7772
});
7873

7974
afterEach(() => {
8075
// restore jsdom localStorage cleanliness and env
8176
process.env.NODE_ENV = ORIGINAL_ENV;
82-
try { window.localStorage.removeItem(STORAGE_KEY); } catch {}
77+
try {
78+
window.localStorage.removeItem(STORAGE_KEY);
79+
} catch {
80+
/* ignore */
81+
}
8382
});
8483

8584
describe('TodoProvider and useTodoStore', () => {
@@ -172,7 +171,7 @@ describe('todo-context', () => {
172171
// Suppress console.error for this test
173172
const originalError = console.error;
174173
console.error = () => undefined;
175-
174+
176175
expect(() => {
177176
render(<TestComponent />);
178177
}).toThrow('useTodoStore must be used within a TodoProvider');
@@ -220,7 +219,13 @@ describe('todo-context', () => {
220219
it('hydrates and revives date instances on mount when persisted state exists', () => {
221220
const seeded = {
222221
todos: [
223-
{ id: 'x', text: 'seed', completed: false, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }
222+
{
223+
id: 'x',
224+
text: 'seed',
225+
completed: false,
226+
createdAt: new Date().toISOString(),
227+
updatedAt: new Date().toISOString()
228+
}
224229
],
225230
filter: 'all' as const
226231
};
@@ -233,13 +238,19 @@ describe('todo-context', () => {
233238
});
234239

235240
it('persists on addTodo, toggleTodo, setFilter', () => {
236-
const spy = vi.spyOn(Utils, 'saveToStorage');
241+
const spy = vi.spyOn(window.localStorage, 'setItem');
237242

238243
renderWithProvider();
239244

240-
act(() => { screen.getByTestId('add-todo').click(); });
241-
act(() => { screen.getByTestId('toggle-todo').click(); });
242-
act(() => { screen.getByTestId('set-filter').click(); });
245+
act(() => {
246+
screen.getByTestId('add-todo').click();
247+
});
248+
act(() => {
249+
screen.getByTestId('toggle-todo').click();
250+
});
251+
act(() => {
252+
screen.getByTestId('set-filter').click();
253+
});
243254

244255
// Called multiple times through effect
245256
expect(spy).toHaveBeenCalled();
@@ -252,7 +263,9 @@ describe('todo-context', () => {
252263
const original = window.localStorage;
253264
// @ts-ignore - override for test
254265
Object.defineProperty(window, 'localStorage', {
255-
get() { throw new Error('unavailable'); },
266+
get() {
267+
throw new Error('unavailable');
268+
},
256269
configurable: true
257270
});
258271

packages/utils/src/__tests__/storage.test.ts

Lines changed: 43 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,33 @@ const KEY = 'test/storage@v1';
77
const ORIGINAL_ENV = process.env.NODE_ENV;
88

99
describe('storage utils', () => {
10+
function ensureWindowWithLocalStorage() {
11+
// Ensure a Window-like global for Node environment
12+
if (typeof window === 'undefined') {
13+
Object.defineProperty(globalThis, 'window', {
14+
// unknown avoids explicit any; cast to Window shape for tests
15+
value: {} as unknown as Window & typeof globalThis,
16+
configurable: true
17+
});
18+
}
19+
// Polyfill localStorage if missing
20+
if (!('localStorage' in window)) {
21+
const store = new Map<string, string>();
22+
Object.defineProperty(window, 'localStorage', {
23+
value: {
24+
getItem: (k: string) => store.get(k) ?? null,
25+
setItem: (k: string, v: string) => {
26+
store.set(k, v);
27+
},
28+
removeItem: (k: string) => {
29+
store.delete(k);
30+
}
31+
},
32+
configurable: true
33+
});
34+
}
35+
}
36+
1037
beforeEach(() => {
1138
// Ensure clean slate
1239
try {
@@ -35,45 +62,15 @@ describe('storage utils', () => {
3562
it('Malformed JSON returns fallback', () => {
3663
// Enable storage access by switching to a non-test env for this test
3764
process.env.NODE_ENV = 'development';
38-
// Ensure localStorage exists in case test env didn't provide it
39-
if (typeof window === 'undefined' || !('localStorage' in window)) {
40-
// @ts-ignore
41-
global.window = {} as any;
42-
}
43-
if (!('localStorage' in window)) {
44-
const store = new Map<string, string>();
45-
Object.defineProperty(window, 'localStorage', {
46-
value: {
47-
getItem: (k: string) => store.get(k) ?? null,
48-
setItem: (k: string, v: string) => void store.set(k, v),
49-
removeItem: (k: string) => void store.delete(k)
50-
},
51-
configurable: true
52-
});
53-
}
65+
ensureWindowWithLocalStorage();
5466
window.localStorage.setItem(KEY, '{not json');
5567
const result = loadFromStorage(KEY, { good: true });
5668
expect(result).toEqual({ good: true });
5769
});
5870

5971
it('save/remove round-trip behavior works', () => {
6072
process.env.NODE_ENV = 'development';
61-
// Ensure localStorage exists (same polyfill as above)
62-
if (typeof window === 'undefined' || !('localStorage' in window)) {
63-
// @ts-ignore
64-
global.window = {} as any;
65-
}
66-
if (!('localStorage' in window)) {
67-
const store = new Map<string, string>();
68-
Object.defineProperty(window, 'localStorage', {
69-
value: {
70-
getItem: (k: string) => store.get(k) ?? null,
71-
setItem: (k: string, v: string) => void store.set(k, v),
72-
removeItem: (k: string) => void store.delete(k)
73-
},
74-
configurable: true
75-
});
76-
}
73+
ensureWindowWithLocalStorage();
7774

7875
const value = { a: 1, b: 'two' };
7976
saveToStorage(KEY, value);
@@ -88,39 +85,33 @@ describe('storage utils', () => {
8885

8986
it('validate guard: rejects invalid shape and returns fallback', () => {
9087
process.env.NODE_ENV = 'development';
91-
const store = new Map<string, string>();
92-
Object.defineProperty(window, 'localStorage', {
93-
value: {
94-
getItem: (k: string) => store.get(k) ?? null,
95-
setItem: (k: string, v: string) => void store.set(k, v),
96-
removeItem: (k: string) => void store.delete(k)
97-
},
98-
configurable: true
99-
});
88+
ensureWindowWithLocalStorage();
10089

10190
window.localStorage.setItem(KEY, JSON.stringify({ nope: true }));
10291

10392
const fallback = { ok: true };
104-
const result = loadFromStorage(KEY, fallback, (v): v is typeof fallback => typeof (v as any).ok === 'boolean');
93+
const result = loadFromStorage(
94+
KEY,
95+
fallback,
96+
(v): v is typeof fallback =>
97+
typeof v === 'object' && v !== null && 'ok' in v && typeof (v as { ok: unknown }).ok === 'boolean'
98+
);
10599
expect(result).toEqual(fallback);
106100
});
107101

108102
it('validate guard: accepts valid shape', () => {
109103
process.env.NODE_ENV = 'development';
110-
const store = new Map<string, string>();
111-
Object.defineProperty(window, 'localStorage', {
112-
value: {
113-
getItem: (k: string) => store.get(k) ?? null,
114-
setItem: (k: string, v: string) => void store.set(k, v),
115-
removeItem: (k: string) => void store.delete(k)
116-
},
117-
configurable: true
118-
});
104+
ensureWindowWithLocalStorage();
119105

120106
const value = { ok: true };
121107
window.localStorage.setItem(KEY, JSON.stringify(value));
122108

123-
const result = loadFromStorage(KEY, { ok: false }, (v): v is typeof value => typeof (v as any).ok === 'boolean');
109+
const result = loadFromStorage(
110+
KEY,
111+
{ ok: false },
112+
(v): v is typeof value =>
113+
typeof v === 'object' && v !== null && 'ok' in v && typeof (v as { ok: unknown }).ok === 'boolean'
114+
);
124115
expect(result).toEqual(value);
125116
});
126117
});

0 commit comments

Comments
 (0)