Compare commits
No commits in common. "244f9ec314739e911e5e64cb3d63aa83c9b174d5" and "5777946faf3e097566f5ba8aaf042a974dd8e1e6" have entirely different histories.
244f9ec314
...
5777946faf
8 changed files with 3 additions and 1887 deletions
1774
frontend/package-lock.json
generated
1774
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -9,8 +9,6 @@
|
||||||
"lint": "oxlint",
|
"lint": "oxlint",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check .",
|
||||||
"test": "vitest run",
|
|
||||||
"test:watch": "vitest",
|
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -19,20 +17,14 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.3.1",
|
"@tailwindcss/vite": "^4.3.1",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
|
||||||
"@testing-library/react": "^16.3.2",
|
|
||||||
"@testing-library/user-event": "^14.6.1",
|
|
||||||
"@types/node": "^24.13.2",
|
"@types/node": "^24.13.2",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.2.17",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^6.0.2",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"jsdom": "^29.1.1",
|
|
||||||
"msw": "^2.14.6",
|
|
||||||
"oxlint": "^1.69.0",
|
"oxlint": "^1.69.0",
|
||||||
"prettier": "^3.9.1",
|
"prettier": "^3.9.1",
|
||||||
"tailwindcss": "^4.3.1",
|
"tailwindcss": "^4.3.1",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"vite": "^8.1.0",
|
"vite": "^8.1.0"
|
||||||
"vitest": "^4.1.9"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { render, screen } from '@testing-library/react'
|
|
||||||
import { describe, expect, it } from 'vitest'
|
|
||||||
import Button from './Button'
|
|
||||||
|
|
||||||
describe('Button', () => {
|
|
||||||
it('renders its children as a button', () => {
|
|
||||||
render(<Button>Add Bucket</Button>)
|
|
||||||
|
|
||||||
expect(
|
|
||||||
screen.getByRole('button', { name: 'Add Bucket' }),
|
|
||||||
).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { http, HttpResponse } from 'msw'
|
|
||||||
import { describe, expect, it } from 'vitest'
|
|
||||||
import { server } from '../test/server'
|
|
||||||
import { ApiError, api } from './api'
|
|
||||||
|
|
||||||
describe('api.postJson', () => {
|
|
||||||
it('sends application/json and returns the parsed body', async () => {
|
|
||||||
let sentContentType: string | null = null
|
|
||||||
|
|
||||||
server.use(
|
|
||||||
http.post('http://localhost/api/login', async ({ request }) => {
|
|
||||||
sentContentType = request.headers.get('Content-Type')
|
|
||||||
return HttpResponse.json({ id: 'u1', email: 'a@b.com' })
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const result = await api.postJson<{ id: string; email: string }>('/login', {
|
|
||||||
username: 'a@b.com',
|
|
||||||
password: 'secret',
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(sentContentType).toBe('application/json')
|
|
||||||
expect(result).toEqual({ id: 'u1', email: 'a@b.com' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throws ApiError on a non-2xx response', async () => {
|
|
||||||
server.use(
|
|
||||||
http.post('http://localhost/api/login', () =>
|
|
||||||
HttpResponse.json({ message: 'bad creds' }, { status: 401 }),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const error = await api.postJson('/login', {}).catch((e: unknown) => e)
|
|
||||||
|
|
||||||
expect(error).toBeInstanceOf(ApiError)
|
|
||||||
expect((error as ApiError).status).toBe(401)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -4,11 +4,7 @@
|
||||||
* Auth is cookie-session (#30): every request sends credentials so the session
|
* Auth is cookie-session (#30): every request sends credentials so the session
|
||||||
* cookie set by POST /api/login flows on subsequent calls. In dev the Vite proxy
|
* cookie set by POST /api/login flows on subsequent calls. In dev the Vite proxy
|
||||||
* keeps /api same-origin, so no CORS is involved. JSON-LD (Hydra) is the wire
|
* keeps /api same-origin, so no CORS is involved. JSON-LD (Hydra) is the wire
|
||||||
* format for API Platform resources; PATCH uses application/merge-patch+json.
|
* format; PATCH uses application/merge-patch+json per API Platform.
|
||||||
*
|
|
||||||
* The auth endpoints (login/register/logout) are NOT API Platform resources —
|
|
||||||
* json_login expects application/json and rejects ld+json — so they use the
|
|
||||||
* `postJson` path (plain application/json) instead of `post`.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const BASE = '/api'
|
const BASE = '/api'
|
||||||
|
|
@ -27,14 +23,6 @@ export class ApiError extends Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||||
// Paths are relative to BASE — callers pass `/login`, not `/api/login`.
|
|
||||||
// Guard against accidentally double-prefixing (a real footgun in tests).
|
|
||||||
if (path.startsWith(BASE)) {
|
|
||||||
throw new Error(
|
|
||||||
`API path "${path}" must not include the "${BASE}" prefix — it is added automatically.`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(`${BASE}${path}`, {
|
const res = await fetch(`${BASE}${path}`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
...init,
|
...init,
|
||||||
|
|
@ -68,20 +56,6 @@ export const api = {
|
||||||
headers: { 'Content-Type': 'application/ld+json' },
|
headers: { 'Content-Type': 'application/ld+json' },
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/**
|
|
||||||
* POST with plain application/json — for the auth endpoints (login, register,
|
|
||||||
* logout) which are not API Platform resources and reject ld+json.
|
|
||||||
*/
|
|
||||||
postJson: <T>(path: string, body: unknown): Promise<T> =>
|
|
||||||
request<T>(path, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
patch: <T>(path: string, body: unknown): Promise<T> =>
|
patch: <T>(path: string, body: unknown): Promise<T> =>
|
||||||
request<T>(path, {
|
request<T>(path, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { setupServer } from 'msw/node'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared MSW server for component/unit tests. Handlers are registered per-test
|
|
||||||
* via `server.use(...)`; the global setup resets them after each test.
|
|
||||||
*/
|
|
||||||
export const server = setupServer()
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import '@testing-library/jest-dom/vitest'
|
|
||||||
import { afterAll, afterEach, beforeAll } from 'vitest'
|
|
||||||
import { server } from './server'
|
|
||||||
|
|
||||||
// Start the MSW server before all tests, reset handlers between tests so
|
|
||||||
// per-test overrides don't leak, and close it when the run finishes.
|
|
||||||
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
|
||||||
afterEach(() => server.resetHandlers())
|
|
||||||
afterAll(() => server.close())
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/// <reference types="vitest/config" />
|
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
|
@ -6,16 +5,6 @@ import { defineConfig } from 'vite'
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
test: {
|
|
||||||
environment: 'jsdom',
|
|
||||||
// Give jsdom a real origin so the client's relative `/api/...` fetches
|
|
||||||
// resolve to an absolute URL MSW can intercept.
|
|
||||||
environmentOptions: { jsdom: { url: 'http://localhost' } },
|
|
||||||
globals: true,
|
|
||||||
setupFiles: ['./src/test/setup.ts'],
|
|
||||||
// Keep the future Playwright e2e suite out of the Vitest run.
|
|
||||||
exclude: ['**/node_modules/**', '**/dist/**', 'e2e/**'],
|
|
||||||
},
|
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 5173,
|
port: 5173,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue