-
Notifications
You must be signed in to change notification settings - Fork 3
Increase web coverage to 85% #273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
da10060
7d9717f
5c223d5
1332490
d011941
397391c
2450c63
73ddbd0
18df836
971c860
75ea81b
af33181
fb89caf
1b26fca
1e543ab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import { render, screen } from '@testing-library/react'; | ||
| import { beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| const { mockUseInView, mockUseReducedMotion } = vi.hoisted(() => ({ | ||
| mockUseInView: vi.fn(), | ||
| mockUseReducedMotion: vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock('framer-motion', async () => { | ||
| const React = await import('react'); | ||
| const createComponent = (tag: string) => | ||
| React.forwardRef(({ animate: _animate, initial: _initial, transition: _transition, whileHover: _whileHover, ...props }: any, ref: any) => | ||
| React.createElement(tag, { ...props, ref }, props.children) | ||
| ); | ||
|
|
||
| return { | ||
| motion: { | ||
| div: createComponent('div'), | ||
| h2: createComponent('h2'), | ||
| p: createComponent('p'), | ||
| }, | ||
| useInView: (...args: unknown[]) => mockUseInView(...args), | ||
| useReducedMotion: () => mockUseReducedMotion(), | ||
| }; | ||
| }); | ||
|
|
||
| import { FeatureGrid } from '@/components/landing/FeatureGrid'; | ||
|
|
||
| describe('FeatureGrid', () => { | ||
| beforeEach(() => { | ||
| mockUseInView.mockReturnValue(true); | ||
| mockUseReducedMotion.mockReturnValue(false); | ||
| }); | ||
|
|
||
| it('renders every feature card with its terminal command', () => { | ||
| render(<FeatureGrid />); | ||
|
|
||
| expect(screen.getByText('AI Chat')).toBeInTheDocument(); | ||
| expect(screen.getByText('Moderation')).toBeInTheDocument(); | ||
| expect(screen.getByText('Starboard')).toBeInTheDocument(); | ||
| expect(screen.getByText('Analytics')).toBeInTheDocument(); | ||
| expect(screen.getByText('$ ai --model claude')).toBeInTheDocument(); | ||
| expect(screen.getByText('$ analytics --export')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('still renders correctly when reduced motion is enabled', () => { | ||
| mockUseReducedMotion.mockReturnValue(true); | ||
|
|
||
| render(<FeatureGrid />); | ||
|
|
||
| expect(screen.getByText(/Everything you need, nothing you don't/i)).toBeInTheDocument(); | ||
| expect(screen.getAllByText(/^\$/)).not.toHaveLength(0); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import { act, render, screen } from '@testing-library/react'; | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| const { mockUseInView } = vi.hoisted(() => ({ | ||
| mockUseInView: vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock('framer-motion', async () => { | ||
| const React = await import('react'); | ||
| const createComponent = (tag: string) => | ||
| React.forwardRef(({ animate: _animate, initial: _initial, transition: _transition, whileHover: _whileHover, ...props }: any, ref: any) => | ||
| React.createElement(tag, { ...props, ref }, props.children) | ||
| ); | ||
|
|
||
| return { | ||
| motion: { | ||
| div: createComponent('div'), | ||
| h1: createComponent('h1'), | ||
| p: createComponent('p'), | ||
| span: createComponent('span'), | ||
| }, | ||
| useInView: (...args: unknown[]) => mockUseInView(...args), | ||
| }; | ||
| }); | ||
|
|
||
| import { Hero } from '@/components/landing/Hero'; | ||
|
|
||
| describe('Hero', () => { | ||
| beforeEach(() => { | ||
| vi.useFakeTimers(); | ||
| mockUseInView.mockReturnValue(true); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.useRealTimers(); | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| it('shows the blinking cursor before the typewriter finishes', () => { | ||
| render(<Hero />); | ||
|
|
||
| expect(screen.getByText('>')).toBeInTheDocument(); | ||
| expect(document.querySelector('.terminal-cursor')).not.toBeNull(); | ||
| }); | ||
|
|
||
| it('reveals the typed headline and CTAs after the timer completes', async () => { | ||
| render(<Hero />); | ||
|
|
||
| act(() => { | ||
| vi.advanceTimersByTime(1_500); | ||
| }); | ||
|
|
||
| expect(screen.getByText(/The AI-powered Discord bot for modern communities/i)).toBeInTheDocument(); | ||
| expect(screen.getByRole('link', { name: /Open Dashboard/i })).toHaveAttribute( | ||
| 'href', | ||
| '/login', | ||
| ); | ||
| expect(screen.getByRole('link', { name: /View on GitHub/i })).toHaveAttribute( | ||
| 'href', | ||
| 'https://github.com/VolvoxLLC/volvox-bot', | ||
| ); | ||
| expect(document.querySelector('.terminal-cursor')).toBeNull(); | ||
| }); | ||
|
BillChirico marked this conversation as resolved.
|
||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,79 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { render, screen } from '@testing-library/react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import userEvent from '@testing-library/user-event'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { beforeEach, describe, expect, it, vi } from 'vitest'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { mockUseInView, mockGetBotInviteUrl } = vi.hoisted(() => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mockUseInView: vi.fn(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mockGetBotInviteUrl: vi.fn(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| vi.mock('framer-motion', async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const React = await import('react'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const createComponent = (tag: string) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| React.forwardRef(({ animate: _animate, initial: _initial, transition: _transition, whileHover: _whileHover, ...props }: any, ref: any) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| React.createElement(tag, { ...props, ref }, props.children) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| motion: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| div: createComponent('div'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p: createComponent('p'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useInView: (...args: unknown[]) => mockUseInView(...args), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+10
to
+24
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Make the This stub only defines ♻️ Suggested change vi.mock('framer-motion', async () => {
const React = await import('react');
const createComponent = (tag: string) =>
React.forwardRef(({ animate: _animate, initial: _initial, transition: _transition, whileHover: _whileHover, ...props }: any, ref: any) =>
React.createElement(tag, { ...props, ref }, props.children)
);
+ const motion = new Proxy(
+ {},
+ {
+ get: (_target, tag) => createComponent(String(tag)),
+ },
+ );
return {
- motion: {
- div: createComponent('div'),
- p: createComponent('p'),
- },
+ motion,
useInView: (...args: unknown[]) => mockUseInView(...args),
};
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| vi.mock('@/lib/discord', () => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getBotInviteUrl: () => mockGetBotInviteUrl(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Pricing } from '@/components/landing/Pricing'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('Pricing', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| beforeEach(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mockUseInView.mockReturnValue(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mockGetBotInviteUrl.mockReturnValue('https://discord.com/invite/bot'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('renders monthly pricing by default and switches to annual billing', async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const user = userEvent.setup(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| render(<Pricing />); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByRole('switch', { name: /toggle annual billing/i })).toHaveAttribute( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'aria-checked', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'false', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByText('$14.99')).toBeInTheDocument(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getAllByText('/mo')).toHaveLength(3); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await user.click(screen.getByRole('switch', { name: /toggle annual billing/i })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByRole('switch', { name: /toggle annual billing/i })).toHaveAttribute( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'aria-checked', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'true', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByText('$115')).toBeInTheDocument(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getAllByText('/year')).toHaveLength(3); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByText('Save $64.88/year')).toBeInTheDocument(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByText('Save $129.88/year')).toBeInTheDocument(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('uses github for the free tier and disables paid ctas when no invite url exists', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
BillChirico marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mockGetBotInviteUrl.mockReturnValue(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| render(<Pricing />); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByRole('link', { name: 'git clone' })).toHaveAttribute( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'href', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'https://github.com/VolvoxLLC/volvox-bot', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const installButtons = [screen.getByText('npm install'), screen.getByText('curl | bash')]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const buttonLabel of installButtons) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const button = buttonLabel.closest('button'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(button).not.toBeNull(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(button).toBeDisabled(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+72
to
+77
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Query the disabled CTAs by role instead of climbing from text nodes.
♻️ Suggested change- const installButtons = [screen.getByText('npm install'), screen.getByText('curl | bash')];
- for (const buttonLabel of installButtons) {
- const button = buttonLabel.closest('button');
- expect(button).not.toBeNull();
- expect(button).toBeDisabled();
- }
+ for (const name of ['npm install', 'curl | bash']) {
+ expect(screen.getByRole('button', { name })).toBeDisabled();
+ }🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import { render, screen, waitFor } from '@testing-library/react'; | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| const { mockUseInView } = vi.hoisted(() => ({ | ||
| mockUseInView: vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock('framer-motion', async () => { | ||
| const React = await import('react'); | ||
| const createComponent = (tag: string) => | ||
| React.forwardRef(({ animate: _animate, initial: _initial, transition: _transition, whileHover: _whileHover, ...props }: any, ref: any) => | ||
| React.createElement(tag, { ...props, ref }, props.children) | ||
| ); | ||
|
|
||
| return { | ||
| motion: { | ||
| div: createComponent('div'), | ||
| h2: createComponent('h2'), | ||
| p: createComponent('p'), | ||
| span: createComponent('span'), | ||
| }, | ||
| useInView: (...args: unknown[]) => mockUseInView(...args), | ||
| }; | ||
| }); | ||
|
|
||
| import { Stats } from '@/components/landing/Stats'; | ||
|
|
||
| describe('Stats', () => { | ||
| const originalRequestAnimationFrame = globalThis.requestAnimationFrame; | ||
| const originalCancelAnimationFrame = globalThis.cancelAnimationFrame; | ||
| let timestamp = 0; | ||
|
|
||
| beforeEach(() => { | ||
| mockUseInView.mockReturnValue(true); | ||
| timestamp = 0; | ||
| globalThis.requestAnimationFrame = vi.fn((callback: FrameRequestCallback) => { | ||
| return window.setTimeout(() => { | ||
|
Check warning on line 37 in web/tests/components/landing/stats.test.tsx
|
||
| timestamp += 2_000; | ||
| callback(timestamp); | ||
| }, 0); | ||
| }); | ||
| globalThis.cancelAnimationFrame = vi.fn((handle: number) => clearTimeout(handle)); | ||
|
BillChirico marked this conversation as resolved.
Outdated
|
||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.restoreAllMocks(); | ||
| globalThis.requestAnimationFrame = originalRequestAnimationFrame; | ||
| globalThis.cancelAnimationFrame = originalCancelAnimationFrame; | ||
| }); | ||
|
|
||
| it('renders formatted live stats after a successful fetch', async () => { | ||
| vi.spyOn(global, 'fetch').mockResolvedValue({ | ||
|
Check warning on line 52 in web/tests/components/landing/stats.test.tsx
|
||
| ok: true, | ||
| json: async () => ({ | ||
| servers: 1_234, | ||
| members: 1_200_000, | ||
| commandsServed: 999, | ||
| activeConversations: 12, | ||
| uptime: 97_200, | ||
| messagesProcessed: 5_500, | ||
| cachedAt: '2026-03-11T12:34:56.000Z', | ||
| }), | ||
| } as Response); | ||
|
BillChirico marked this conversation as resolved.
Outdated
|
||
|
|
||
| render(<Stats />); | ||
|
|
||
| await waitFor(() => { | ||
| expect(screen.getByText('1.2K')).toBeInTheDocument(); | ||
| expect(screen.getByText('1.2M')).toBeInTheDocument(); | ||
| expect(screen.getByText('999')).toBeInTheDocument(); | ||
| expect(screen.getByText('12')).toBeInTheDocument(); | ||
| expect(screen.getByText('1d 3h')).toBeInTheDocument(); | ||
| expect(screen.getByText('5.5K')).toBeInTheDocument(); | ||
| }); | ||
| expect(screen.getByText(/as of/i)).toBeInTheDocument(); | ||
| expect(screen.getByText('Loved by developers')).toBeInTheDocument(); | ||
| expect(fetch).toHaveBeenCalledWith('/api/stats'); | ||
| }); | ||
|
|
||
| it('renders the error fallback when fetching stats fails', async () => { | ||
| vi.spyOn(global, 'fetch').mockRejectedValue(new Error('boom')); | ||
|
Check warning on line 81 in web/tests/components/landing/stats.test.tsx
|
||
|
|
||
| render(<Stats />); | ||
|
|
||
| await waitFor(() => { | ||
| expect(screen.getAllByText('—')).toHaveLength(6); | ||
| }); | ||
| expect(screen.getByText('Trusted by teams at leading tech companies and thousands of open-source communities')).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.