diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0d79f8d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM node:18-alpine AS build + +WORKDIR /app + +COPY package.json ./ +RUN npm install + +COPY . . +RUN npm run build + +FROM node:18-alpine AS production + +WORKDIR /app + +COPY --from=build /app/dist ./dist +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/package.json ./ +COPY --from=build /app/vite.config.ts ./ + +EXPOSE 5173 + +CMD ["npx", "vite", "preview", "--host", "0.0.0.0", "--port", "5173"] diff --git a/RUNNING.md b/RUNNING.md index 77896cf..ef1efc7 100644 --- a/RUNNING.md +++ b/RUNNING.md @@ -1,33 +1,80 @@ -# Running the Todo API +# Yellow World Web App + +A minimal React/Vite/TypeScript application that displays "yellow world" with yellow-themed styling. + +## TEAM_BRIEF +stack: TypeScript/React+Vite +test_runner: npx vitest run +lint_tool: none +coverage_tool: none +coverage_threshold: 0 +coverage_applies: false ## Prerequisites -- Python 3.10 or later +- Node.js 18+ and npm +- Docker and Docker Compose (for containerised run) -## Install dependencies +## Local Development ```bash -pip install fastapi uvicorn pydantic +# Install dependencies +npm install + +# Start dev server +npm run dev + +# Run tests +npm test + +# Build for production +npm run build + +# Preview production build +npm run preview ``` -For running the test suite you will also need: +## Docker ```bash -pip install httpx pytest +# Build and run with Docker Compose +docker compose up --build + +# Open in browser +# http://localhost:5173 ``` -## Start the server +## Project Structure -```bash -uvicorn main:app --reload --host 0.0.0.0 --port 8000 ``` +├── index.html # HTML entry point +├── src/ +│ ├── main.tsx # React entry point +│ ├── main.css # Global styles +│ ├── App.tsx # Main App component +│ ├── App.module.css # Yellow-themed CSS module +│ ├── setupTests.ts # Test setup (jest-dom matchers) +│ └── __tests__/ +│ └── App.test.tsx # App component test suite +├── vite.config.ts # Vite + Vitest configuration +├── tsconfig.json # TypeScript configuration +├── package.json # Dependencies and scripts +├── Dockerfile # Multi-stage Docker build +└── docker-compose.yml # Docker Compose config +``` + +## Testing -The API will be available at . +Tests use Vitest with React Testing Library and jest-dom matchers. +The test suite verifies: -Interactive docs are served at . +1. The "yellow world" text is rendered in the DOM +2. The text appears inside an `

` element +3. CSS module classes (`container`, `heading`) are correctly applied +4. The DOM structure is correct (div wrapping h1) -## Run the tests +Run tests: ```bash -pytest tests/ +npm test ``` diff --git a/SETUP.md b/SETUP.md index 643c59c..5704866 100644 --- a/SETUP.md +++ b/SETUP.md @@ -1,25 +1,24 @@ # Setup Instructions -## Install Dependencies +This project uses npm for dependency management. Lock files are generated +by the package manager and must not be hand-written. -```bash -pip install -r requirements.txt -``` - -## Install Test Dependencies +## Generate lock file and install dependencies ```bash -pip install pytest httpx +npm install ``` -## Run Tests +This will create `package-lock.json` and populate `node_modules/`. + +## Run tests ```bash -pytest tests/ -v +npm test ``` -## Run the Application +## Build ```bash -uvicorn main:app --reload +npm run build ``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..226cc1d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +version: "3.9" + +services: + yellow-world: + build: + context: . + dockerfile: Dockerfile + ports: + - "5173:5173" + restart: unless-stopped diff --git a/index.html b/index.html new file mode 100644 index 0000000..4f63111 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + Yellow World + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..26e7698 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "yellow-world", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "test": "vitest run" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.1.4", + "@testing-library/react": "^14.1.2", + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "jsdom": "^23.0.1", + "typescript": "^5.3.3", + "vite": "^5.0.8", + "vitest": "^1.1.0" + } +} diff --git a/src/App.module.css b/src/App.module.css new file mode 100644 index 0000000..c7afee3 --- /dev/null +++ b/src/App.module.css @@ -0,0 +1,20 @@ +/* Yellow-themed styling for the App component */ + +.container { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + background-color: #ffd700; +} + +.heading { + color: #8b7500; + font-size: 3rem; + font-weight: bold; + text-align: center; + padding: 2rem; + background-color: #ffecb3; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..cf67e23 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import styles from './App.module.css'; + +/** + * Main application component. + * + * Renders a "yellow world" heading inside a yellow-themed container. + * Styling is applied via CSS modules defined in App.module.css. + */ +const App: React.FC = () => { + return ( +
+

+ yellow world +

+
+ ); +}; + +export default App; diff --git a/src/__tests__/App.test.tsx b/src/__tests__/App.test.tsx new file mode 100644 index 0000000..81fc837 --- /dev/null +++ b/src/__tests__/App.test.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import App from '../App'; + +/** + * Test suite for the App component. + * + * Verifies that the 'yellow world' text is rendered in the DOM and + * that the correct yellow-themed CSS module class names are applied + * to the container and heading elements. + */ +describe('App Component', () => { + it('renders the yellow world text', () => { + render(); + const heading = screen.getByText('yellow world'); + expect(heading).toBeInTheDocument(); + }); + + it('renders the yellow world text in an h1 element', () => { + render(); + const heading = screen.getByRole('heading', { level: 1 }); + expect(heading).toBeInTheDocument(); + expect(heading).toHaveTextContent('yellow world'); + }); + + it('applies the container CSS module class to the wrapper div', () => { + render(); + const container = screen.getByTestId('app-container'); + expect(container).toBeInTheDocument(); + expect(container.className).toContain('container'); + }); + + it('applies the heading CSS module class to the h1 element', () => { + render(); + const heading = screen.getByTestId('app-heading'); + expect(heading).toBeInTheDocument(); + expect(heading.className).toContain('heading'); + }); + + it('renders the heading inside the container', () => { + render(); + const container = screen.getByTestId('app-container'); + const heading = screen.getByTestId('app-heading'); + expect(container).toContainElement(heading); + }); + + it('renders exactly one h1 element', () => { + const { container } = render(); + const headings = container.querySelectorAll('h1'); + expect(headings).toHaveLength(1); + }); + + it('has the correct text content without extra whitespace', () => { + render(); + const heading = screen.getByTestId('app-heading'); + expect(heading.textContent?.trim()).toBe('yellow world'); + }); + + it('container and heading have distinct CSS module classes', () => { + render(); + const container = screen.getByTestId('app-container'); + const heading = screen.getByTestId('app-heading'); + expect(container.className).not.toBe(heading.className); + }); + + it('container element is a div', () => { + render(); + const container = screen.getByTestId('app-container'); + expect(container.tagName).toBe('DIV'); + }); + + it('heading element is an h1', () => { + render(); + const heading = screen.getByTestId('app-heading'); + expect(heading.tagName).toBe('H1'); + }); +}); diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts new file mode 100644 index 0000000..149b658 --- /dev/null +++ b/src/__tests__/setup.ts @@ -0,0 +1,7 @@ +/** + * Vitest setup file. + * + * Extends expect with jest-dom matchers for DOM assertion helpers + * such as toBeInTheDocument() and toHaveClass(). + */ +import "@testing-library/jest-dom"; diff --git a/src/main.css b/src/main.css new file mode 100644 index 0000000..8d492b3 --- /dev/null +++ b/src/main.css @@ -0,0 +1,12 @@ +/* Global reset and base styles */ + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, sans-serif; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..c806033 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './main.css'; + +/** + * Application entry point. + * Mounts the root component into the #root DOM element. + */ +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/src/setupTests.ts b/src/setupTests.ts new file mode 100644 index 0000000..c416b59 --- /dev/null +++ b/src/setupTests.ts @@ -0,0 +1,5 @@ +/** + * Test setup file loaded before each test suite. + * Imports jest-dom matchers for enhanced DOM assertions. + */ +import '@testing-library/jest-dom'; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..9974dff --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,12 @@ +/// + +/** + * Type declarations for CSS module imports. + * + * Allows TypeScript to understand `import styles from '*.module.css'` + * and treat the default export as a record of class name strings. + */ +declare module "*.module.css" { + const classes: { readonly [key: string]: string }; + export default classes; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..863f64f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "vite-env.d.ts"] +} diff --git a/vite-env.d.ts b/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..e1ca860 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + css: { + modules: { + localsConvention: 'camelCase', + }, + }, + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./src/setupTests.ts'], + css: { + modules: { + classNameStrategy: 'non-scoped', + }, + }, + }, +} as any);