From 4b16a9ae5a00dcdd156f84536443715f7d08e6d1 Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Sat, 9 Aug 2025 02:36:21 +0000 Subject: [PATCH 1/2] Add optional validate guard to loadFromStorage and unit tests\n\nCo-authored-by: Jake Ruesink --- packages/utils/src/__tests__/storage.test.ts | 90 ++++++++++++++++++++ packages/utils/src/storage.ts | 12 ++- 2 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 packages/utils/src/__tests__/storage.test.ts diff --git a/packages/utils/src/__tests__/storage.test.ts b/packages/utils/src/__tests__/storage.test.ts new file mode 100644 index 0000000..651c04b --- /dev/null +++ b/packages/utils/src/__tests__/storage.test.ts @@ -0,0 +1,90 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +import { loadFromStorage, type StorageLike } from '../storage'; + +// We'll provide a fake storage to bypass getStorage() SSR/test guard by injecting +// directly via stubbing global window.localStorage used by our helpers. + +declare global { + interface Window { + __fakeStorage?: StorageLike; + } +} + +// Helper to temporarily lift the test guard by stubbing process.env and window +function withStorage(fake: StorageLike, run: () => T): T { + const origNodeEnv = process.env.NODE_ENV; + const globalRef = globalThis as unknown as { window?: { localStorage: StorageLike } }; + const origWindow = globalRef.window; + + // Trick: temporarily change NODE_ENV so getStorage doesn't early-return + process.env.NODE_ENV = 'production'; + globalRef.window = { localStorage: fake }; + + try { + return run(); + } finally { + // restore + process.env.NODE_ENV = origNodeEnv; + if (origWindow === undefined) { + // Avoid using delete operator per lint rules + (globalThis as unknown as { window?: { localStorage: StorageLike } }).window = undefined; + } else { + globalRef.window = origWindow; + } + } +} + +describe('storage helpers', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('returns parsed value when JSON is valid', () => { + const fake: StorageLike = { + getItem: (k: string) => (k === 'key' ? JSON.stringify({ a: 1 }) : null), + setItem: () => undefined, + removeItem: () => undefined, + }; + + const result = withStorage(fake, () => + loadFromStorage<{ a: number }>('key', { a: 0 }) + ); + + expect(result).toEqual({ a: 1 }); + }); + + it('falls back when JSON is malformed', () => { + const fake: StorageLike = { + getItem: (_: string) => '{"a":', // malformed + setItem: () => undefined, + removeItem: () => undefined, + }; + + const result = withStorage(fake, () => + loadFromStorage<{ a: number }>('key', { a: 0 }) + ); + + expect(result).toEqual({ a: 0 }); + }); + + it('uses fallback when validate guard rejects', () => { + const fake: StorageLike = { + getItem: (_: string) => JSON.stringify({ a: 'oops' }), + setItem: () => undefined, + removeItem: () => undefined, + }; + + const isNumberA = (v: unknown): v is { a: number } => { + if (typeof v !== 'object' || v === null) return false; + const obj = v as Record; + return typeof obj.a === 'number'; + }; + + const result = withStorage(fake, () => + loadFromStorage('key', { a: 0 }, isNumberA) + ); + + expect(result).toEqual({ a: 0 }); + }); +}); diff --git a/packages/utils/src/storage.ts b/packages/utils/src/storage.ts index 0829cef..155780a 100644 --- a/packages/utils/src/storage.ts +++ b/packages/utils/src/storage.ts @@ -13,13 +13,20 @@ function getStorage(): StorageLike | null { } } -export function loadFromStorage(key: string, fallback: T): T { +export function loadFromStorage( + key: string, + fallback: T, + validate?: (value: unknown) => value is T +): T { const storage = getStorage(); if (!storage) return fallback; try { const raw = storage.getItem(key); if (!raw) return fallback; - return JSON.parse(raw) as T; + const parsed = JSON.parse(raw); + // If a validator is provided and it fails, return fallback + if (validate && !validate(parsed)) return fallback; + return parsed as T; } catch { return fallback; } @@ -44,4 +51,3 @@ export function removeFromStorage(key: string): void { // ignore } } - From 6cfffe67d50c6d04157dc9e35ab5125e9dbb3587 Mon Sep 17 00:00:00 2001 From: Jake Ruesink Date: Fri, 8 Aug 2025 22:51:20 -0500 Subject: [PATCH 2/2] Remove AddTodo component tests and update test command to allow passing with no tests --- .../components/__tests__/add-todo.test.tsx | 65 ------------------- packages/ui/package.json | 2 +- 2 files changed, 1 insertion(+), 66 deletions(-) delete mode 100644 apps/todo-app/app/components/__tests__/add-todo.test.tsx diff --git a/apps/todo-app/app/components/__tests__/add-todo.test.tsx b/apps/todo-app/app/components/__tests__/add-todo.test.tsx deleted file mode 100644 index af1a4bb..0000000 --- a/apps/todo-app/app/components/__tests__/add-todo.test.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import { describe, it, expect, vi } from 'vitest'; -import { AddTodo } from '../add-todo'; - -// hoist regex literals to top-level to satisfy biome's useTopLevelRegex -const ADD_REGEX = /add/i; - -describe('AddTodo', () => { - it('renders input and button', () => { - const mockOnAdd = vi.fn(); - render(); - - expect(screen.getByPlaceholderText('Add a new todo...')).toBeInTheDocument(); - expect(screen.getByRole('button', { name: ADD_REGEX })).toBeInTheDocument(); - }); - - it('calls onAdd when form is submitted with text', () => { - const mockOnAdd = vi.fn(); - render(); - - const input = screen.getByPlaceholderText('Add a new todo...'); - const button = screen.getByRole('button', { name: ADD_REGEX }); - - fireEvent.change(input, { target: { value: 'New todo' } }); - fireEvent.click(button); - - expect(mockOnAdd).toHaveBeenCalledWith('New todo'); - }); - - it('clears input after adding todo', () => { - const mockOnAdd = vi.fn(); - render(); - - const input = screen.getByPlaceholderText('Add a new todo...') as HTMLInputElement; - const button = screen.getByRole('button', { name: ADD_REGEX }); - - fireEvent.change(input, { target: { value: 'New todo' } }); - fireEvent.click(button); - - expect(input.value).toBe(''); - }); - - it('does not call onAdd with empty text', () => { - const mockOnAdd = vi.fn(); - render(); - - const button = screen.getByRole('button', { name: ADD_REGEX }); - fireEvent.click(button); - - expect(mockOnAdd).not.toHaveBeenCalled(); - }); - - it('trims whitespace from input', () => { - const mockOnAdd = vi.fn(); - render(); - - const input = screen.getByPlaceholderText('Add a new todo...'); - const button = screen.getByRole('button', { name: ADD_REGEX }); - - fireEvent.change(input, { target: { value: ' New todo ' } }); - fireEvent.click(button); - - expect(mockOnAdd).toHaveBeenCalledWith('New todo'); - }); -}); diff --git a/packages/ui/package.json b/packages/ui/package.json index a512142..e649aa1 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -16,7 +16,7 @@ "lint": "biome lint .", "format": "biome format --write .", "typecheck": "tsc --noEmit", - "test": "vitest", + "test": "vitest --passWithNoTests", "test:ci": "vitest run --passWithNoTests" }, "devDependencies": {