import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, describe, expect, it } from 'vitest' import App from './App' import { demoSeries } from './api/demo' import { LanguageProvider } from './i18n' import { activeHeroBanMap } from './series-utils' describe('Mixmaker frontend', () => { afterEach(() => { cleanup() window.history.replaceState({}, '', '/') }) it('renders the event dashboard', async () => { window.history.replaceState({}, '', '/events') render() expect(await screen.findByRole('heading', { name: 'Your events' })).toBeInTheDocument() expect(screen.getByText('Saturday Night Scrim')).toBeInTheDocument() expect(screen.getByRole('link', { name: 'Open Summer Clash #4' })).toHaveClass('status-live') expect(screen.getByRole('link', { name: 'Open Cancelled Community Mix' })).toHaveClass('status-cancelled') expect(screen.getByText('Ember Wolves')).toBeInTheDocument() expect(screen.getByText('Neon Foxes')).toBeInTheDocument() }) it('renders the Discord login screen', async () => { window.history.replaceState({}, '', '/login') render() expect(await screen.findByRole('link', { name: /continue with discord/i })).toHaveAttribute('href', '/api/auth/discord') }) it('locks RSVP and exposes live navigation after the scrim starts', async () => { window.history.replaceState({}, '', '/events/event-3') render() expect(await screen.findByRole('link', { name: /^open live$/i })).toHaveAttribute('href', '/events/event-3/live') expect(screen.getByRole('heading', { name: 'Balanced teams' })).toBeInTheDocument() expect(screen.getByRole('heading', { name: 'Ember Wolves' })).toBeInTheDocument() expect(screen.getByRole('heading', { name: 'Azure Phantoms' })).toBeInTheDocument() expect(screen.getByRole('button', { name: 'Going' })).toBeDisabled() expect(screen.getByText('Registration has been closed by the organizer.')).toBeInTheDocument() }) it('keeps the full scrollable match and Bo3 ban history visible', async () => { window.history.replaceState({}, '', '/watch/series-1') render() expect(await screen.findByRole('heading', { name: 'Bo3 hero ban history' })).toBeInTheDocument() expect(screen.getByText('Ember Wolves won the toss')).toBeInTheDocument() expect(screen.getByText('Winston')).toBeInTheDocument() }) it('opens a roster participant in series mode', async () => { window.history.replaceState({}, '', '/events/event-3/live') render() expect(await screen.findByRole('heading', { name: 'Series control' })).toBeInTheDocument() expect(screen.getByRole('link', { name: /spectator view/i })).toHaveAttribute('href', '/watch/series-1') fireEvent.click(screen.getByRole('tab', { name: /DPS/i })) fireEvent.change(screen.getByRole('searchbox', { name: /search hero/i }), { target: { value: 'tRaC' } }) expect(screen.getByRole('radio', { name: /Tracer/i })).toBeInTheDocument() expect(screen.queryByRole('radio', { name: /Ana/i })).not.toBeInTheDocument() }) it('does not reuse completed-map hero bans during map pick', () => { expect(activeHeroBanMap({ ...demoSeries, currentStep: { ...demoSeries.currentStep, kind: 'map_pick' }, })).toBeUndefined() }) it('explains map selection and captain rules in the FAQ', async () => { window.history.replaceState({}, '', '/faq') render() expect(await screen.findByRole('heading', { name: 'Frequently asked questions' })).toBeInTheDocument() expect(screen.getByRole('heading', { name: 'Who picks later maps?' })).toBeInTheDocument() expect(screen.getByText(/team that lost that map immediately chooses/i)).toBeInTheDocument() expect(screen.getByRole('link', { name: /open discord/i })).toHaveAttribute('href', 'https://discord.gg/mixmaker') }) it('opens linked players and renders profile stats and history', async () => { window.history.replaceState({}, '', '/players/p2') render() expect(await screen.findByRole('heading', { name: 'Nova', level: 1 })).toBeInTheDocument() expect(screen.getByText('Nova#1002')).toBeInTheDocument() expect(screen.getByRole('region', { name: 'Player statistics' })).toBeInTheDocument() expect(screen.getByRole('heading', { name: 'Recent matches' })).toBeInTheDocument() expect(screen.getByRole('link', { name: /Summer Clash #4/i })).toHaveAttribute('href', '/watch/series-1') }) it('links player names without wrapping RSVP controls', async () => { window.history.replaceState({}, '', '/events/event-3') render() const nova = await screen.findAllByRole('link', { name: 'Nova' }) expect(nova.some((link) => link.getAttribute('href') === '/players/p2')).toBe(true) expect(screen.getByRole('button', { name: 'Going' })).toBeDisabled() }) it('exposes community settings to administrators', async () => { window.history.replaceState({}, '', '/admin') render() fireEvent.click(await screen.findByRole('tab', { name: 'Community' })) expect(screen.getByRole('heading', { name: 'Community Discord' })).toBeInTheDocument() expect(screen.getByRole('textbox', { name: 'Discord invite URL' })).toHaveValue('https://discord.gg/mixmaker') }) it('edits player identity', async () => { window.history.replaceState({}, '', '/profile') render() const nickname = await screen.findByRole('textbox', { name: 'Nickname' }) fireEvent.change(nickname, { target: { value: 'Lemintare Prime' } }) fireEvent.change(screen.getByRole('textbox', { name: /BattleTag/i }), { target: { value: 'Lemi#2026' } }) fireEvent.click(screen.getByRole('button', { name: 'Save profile' })) expect(await screen.findByText('Saved')).toBeInTheDocument() }) })