diff --git a/src/App.test.tsx b/src/App.test.tsx new file mode 100644 index 00000000..504d099b --- /dev/null +++ b/src/App.test.tsx @@ -0,0 +1,72 @@ +import { cleanup, render, screen, waitFor } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { Shell } from './App'; +import { SettingsProvider } from './context/SettingsContext'; + +function renderShell(path: string) { + return render( + + + + + + ); +} + +beforeEach(() => { + localStorage.clear(); + vi.stubGlobal( + 'fetch', + vi.fn(async () => ({ ok: true, status: 200, json: async () => [] }) as unknown as Response) + ); +}); + +afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); +}); + +describe('App shell', () => { + it('redirects the root path to the first news page', async () => { + renderShell('/'); + + await waitFor(() => expect(fetch).toHaveBeenCalled()); + expect((fetch as unknown as ReturnType).mock.calls[0][0]).toBe( + 'https://node-hnapi.herokuapp.com/news?page=1' + ); + }); + + it('renders the chrome around the routed content', () => { + const { container } = renderShell('/news/1'); + + expect(container.querySelector('.wrapper')).toBeTruthy(); + expect(screen.getByText('new')).toBeTruthy(); + expect(screen.getByText('GitHub')).toBeTruthy(); + }); + + it('sends unknown feed types back to the news feed', async () => { + renderShell('/nonsense/1'); + + await waitFor(() => expect(fetch).toHaveBeenCalled()); + expect((fetch as unknown as ReturnType).mock.calls[0][0]).toContain('/news?page=1'); + }); + + it('applies the persisted theme as the outer wrapper class', () => { + localStorage.setItem('theme', 'amoledblack'); + const { container } = renderShell('/news/1'); + + expect(container.firstElementChild?.className).toBe('amoledblack'); + }); + + it('routes item and user paths to their pages', async () => { + vi.stubGlobal( + 'fetch', + vi.fn(async () => ({ ok: true, status: 200, json: async () => ({ id: 'ashwin', created: '', karma: 1 }) }) as unknown as Response) + ); + renderShell('/user/ashwin'); + + await waitFor(() => expect(screen.getByText('ashwin')).toBeTruthy()); + }); +}); diff --git a/src/App.tsx b/src/App.tsx index 90edb232..34680c9a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,54 @@ -export default function App() { +import { BrowserRouter, Navigate, Route, Routes, useParams } from 'react-router-dom'; + +import Footer from './components/Footer'; +import Header from './components/Header'; +import { SettingsProvider, useSettings } from './context/SettingsContext'; +import Feed from './pages/Feed'; +import ItemDetails from './pages/ItemDetails'; +import User from './pages/User'; + +import './App.scss'; + +export const FEED_TYPES = ['news', 'newest', 'show', 'ask', 'jobs']; + +/** Angular listed one route per feed; here a single param route rejects unknown feeds. */ +function FeedRoute() { + const { feedType } = useParams<{ feedType: string }>(); + + if (!feedType || !FEED_TYPES.includes(feedType)) { + return ; + } + + return ; +} + +export function Shell() { + const { settings } = useSettings(); + return ( -
-

Angular 2 HN

-

React + TypeScript + Vite scaffolding is up. UI migration lands in the following phases.

+
+
+
+
+ + } /> + } /> + } /> + } /> + } /> + +
+
); } + +export default function App() { + return ( + + + + + + ); +}