Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions web/src/components/layout/dashboard-title-sync.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
import { APP_TITLE, getDashboardDocumentTitle } from '@/lib/page-titles';

/**
Expand All @@ -14,18 +14,26 @@ import { APP_TITLE, getDashboardDocumentTitle } from '@/lib/page-titles';
*/
export function DashboardTitleSync() {
const pathname = usePathname();
const lastSyncedTitleRef = useRef<string | null>(null);

useEffect(() => {
const computed = getDashboardDocumentTitle(pathname);
const current = document.title;
const lastSyncedTitle = lastSyncedTitleRef.current;

// If the current title already ends with our app suffix and is more specific
// than what we'd set (i.e. different prefix), respect the page-level metadata.
if (current.endsWith(APP_TITLE) && current !== computed && current !== APP_TITLE) {
if (
current.endsWith(APP_TITLE) &&
current !== computed &&
current !== APP_TITLE &&
current !== lastSyncedTitle
) {
Comment thread
BillChirico marked this conversation as resolved.
return;
}

document.title = computed;
lastSyncedTitleRef.current = computed;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}, [pathname]);

return null;
Expand Down
4 changes: 3 additions & 1 deletion web/tests/api/log-stream-ws-ticket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ describe('GET /api/log-stream/ws-ticket', () => {

const body = (await response.json()) as { wsUrl: string; ticket: string };
expect(body.wsUrl).toBe('wss://bot.internal:3001/ws/logs');
expect(body.ticket.split('.')).toHaveLength(3);
const ticketParts = body.ticket.split('.');
expect(ticketParts).toHaveLength(4);
expect(ticketParts[2]).toBe('guild-1');
});
});
54 changes: 54 additions & 0 deletions web/tests/components/landing/feature-grid.test.tsx
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);
});
});
64 changes: 64 additions & 0 deletions web/tests/components/landing/hero.test.tsx
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();
});
Comment thread
BillChirico marked this conversation as resolved.
});
79 changes: 79 additions & 0 deletions web/tests/components/landing/pricing.test.tsx
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Make the framer-motion mock tolerant of new motion.* tags.

This stub only defines motion.div and motion.p. A harmless wrapper change in Pricing.tsx to motion.section, motion.h2, etc. will break the test harness before the pricing behavior is exercised.

♻️ 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

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)
);
return {
motion: {
div: createComponent('div'),
p: createComponent('p'),
},
useInView: (...args: unknown[]) => mockUseInView(...args),
};
});
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,
useInView: (...args: unknown[]) => mockUseInView(...args),
};
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/tests/components/landing/pricing.test.tsx` around lines 10 - 24, The
framer-motion mock only defines motion.div and motion.p so new motion tags break
tests; update the mock (the vi.mock callback that returns motion: { ... } and
the createComponent helper) to dynamically create any motion.* element accessed
(e.g., by using a Proxy or by generating components on demand) so calls like
motion.section or motion.h2/h3 map to createComponent(tag) instead of hardcoding
div/p; ensure useInView continues to forward to mockUseInView.


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', () => {
Comment thread
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

getByText(...).closest('button') couples the assertion to the current wrapper structure. getByRole('button', { name }) keeps the test aligned with the actual accessible control.

♻️ 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
Verify each finding against the current code and only fix it if needed.

In `@web/tests/components/landing/pricing.test.tsx` around lines 72 - 77, The test
currently finds buttons by text nodes then climbs to the enclosing button using
getByText(...).closest('button'); update the assertions to query the actual
accessible controls via role: use screen.getByRole('button', { name: 'npm
install' }) and screen.getByRole('button', { name: 'curl | bash' }) (or iterate
over an array of those labels and call screen.getByRole for each) and then
assert that the returned element is defined and toBeDisabled(); replace the
installButtons array and loop to use screen.getByRole('button', { name }) calls
instead of .closest.

});
});
90 changes: 90 additions & 0 deletions web/tests/components/landing/stats.test.tsx
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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZzb55mdMid-3ub394HP&open=AZzb55mdMid-3ub394HP&pullRequest=273
timestamp += 2_000;
callback(timestamp);
}, 0);
});
globalThis.cancelAnimationFrame = vi.fn((handle: number) => clearTimeout(handle));
Comment thread
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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZzb55mdMid-3ub394HQ&open=AZzb55mdMid-3ub394HQ&pullRequest=273
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);
Comment thread
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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZzb55mdMid-3ub394HR&open=AZzb55mdMid-3ub394HR&pullRequest=273

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();
});
});
Loading
Loading