feat: complete Quantumancy web app — full frontend + docs
Frontend (React 18 + TS + Vite): - Landing: glitching hero, live /api/stats veil ticker, mode cards, featured spirits - Séance: three.js shader ghost (hue/form per entity, mood + audio-reactive), Ouija planchette board spelling utterances, transcript with TTS replay, entity dossier, direct contact streaming, passive/active listening - Modes: Wire Ghost telemetry panel, EVP mic anomaly detection, WebUSB RTL-SDR sweep + waterfall (hardware pass pending), Ouija/Direct Contact - Codex: public registry + entity dossiers, rarity tiers, i18n EN/ES complete - State: VeilSocket (reconnect/backoff), seance reducer, auth context - 72 vitest tests green; served by FastAPI at :7777 Docs: README + as-built plans 3-7
This commit is contained in:
@@ -1,31 +1,64 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
||||
import './i18n'
|
||||
import App from './App'
|
||||
import * as api from './api'
|
||||
|
||||
vi.mock('./api')
|
||||
|
||||
const statsBody = { entities: 7, sessions: 42, utterances: 133, anomalies: 1024 }
|
||||
const codexBody = { entities: [] }
|
||||
|
||||
function mockFetch() {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input)
|
||||
const body = url.includes('/api/stats') ? statsBody : codexBody
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
describe('App', () => {
|
||||
it('renders the Quantumancy title', async () => {
|
||||
beforeEach(() => {
|
||||
mockFetch()
|
||||
vi.mocked(api.me).mockRejectedValue(new Error('not authenticated'))
|
||||
render(<App />)
|
||||
await waitFor(() => expect(screen.getByText('Quantumancy')).toBeInTheDocument())
|
||||
})
|
||||
|
||||
it('logs in and shows the contact-established state', async () => {
|
||||
vi.mocked(api.me).mockRejectedValue(new Error('not authenticated'))
|
||||
vi.mocked(api.login).mockResolvedValue({ id: '1', username: 'medium1' })
|
||||
it('renders the landing page with the Quantumancy title', async () => {
|
||||
render(<App />)
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Quantumancy')).toBeInTheDocument())
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Username'), 'medium1')
|
||||
await userEvent.type(screen.getByLabelText('Password'), 'spookyspooky')
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Enter' }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Contact established as medium1.')).toBeInTheDocument(),
|
||||
expect(screen.getAllByText('QUANTUMANCY').length).toBeGreaterThan(0),
|
||||
)
|
||||
})
|
||||
|
||||
it('shows live veil activity from /api/stats', async () => {
|
||||
render(<App />)
|
||||
// counters animate up from 0 over ~900ms — wait for the final values
|
||||
await waitFor(
|
||||
() => {
|
||||
const values = Array.from(document.querySelectorAll('.stat-value')).map(
|
||||
(el) => el.textContent,
|
||||
)
|
||||
expect(values).toContain('42')
|
||||
expect(values).toContain('7')
|
||||
},
|
||||
{ timeout: 4000 },
|
||||
)
|
||||
})
|
||||
|
||||
it('sends anonymous visitors to /enter from the séance CTA', async () => {
|
||||
render(<App />)
|
||||
await waitFor(() =>
|
||||
expect(screen.getAllByText('QUANTUMANCY').length).toBeGreaterThan(0),
|
||||
)
|
||||
const cta = screen.getByRole('button', { name: /begin the séance/i })
|
||||
await userEvent.click(cta)
|
||||
await waitFor(() => expect(window.location.pathname).toBe('/enter'))
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user