diff --git a/apps/website/e2e/website.spec.ts b/apps/website/e2e/website.spec.ts index 37fe6a0f0..a57b6017e 100644 --- a/apps/website/e2e/website.spec.ts +++ b/apps/website/e2e/website.spec.ts @@ -182,10 +182,20 @@ test('docs page renders sidebar and content', async ({ page }) => { test('docs landing page shows library cards', async ({ page }) => { await page.goto('/docs'); - await expect(page.getByText('LangGraph').first()).toBeVisible(); - await expect(page.getByText('Render').first()).toBeVisible(); - await expect(page.getByText('Chat').first()).toBeVisible(); - await expect(page.getByText('AG-UI').first()).toBeVisible(); + // Assert on card titles, not page text. A bare getByText('Render') passed on + // a substring of "json-render"; hasText on the card would match the Chat + // card too, whose blurb mentions json-render. Only the title is the card. + const titles = page.locator('.docs-index-card-title'); + await expect(titles.filter({ hasText: /^LangGraph$/ })).toBeVisible(); + await expect(titles.filter({ hasText: /^json-render$/ })).toBeVisible(); + await expect(titles.filter({ hasText: /^AG-UI$/ })).toBeVisible(); + await expect(titles.filter({ hasText: /^Chat$/ })).toBeVisible(); +}); + +test('docs landing page carries the control plane', async ({ page }) => { + await page.goto('/docs'); + await expect(page.getByRole('navigation', { name: 'Docs modes' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Choose a library' })).toBeVisible(); }); test('api reference renders in docs', async ({ page }) => { diff --git a/apps/website/src/app/docs/docs-index-shell.spec.tsx b/apps/website/src/app/docs/docs-index-shell.spec.tsx new file mode 100644 index 000000000..567672a05 --- /dev/null +++ b/apps/website/src/app/docs/docs-index-shell.spec.tsx @@ -0,0 +1,56 @@ +// @vitest-environment jsdom +import React from 'react'; +import { fireEvent, render, screen, within } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import DocsLandingPage from './page'; + +vi.mock('next/navigation', () => ({ + usePathname: () => '/docs', + useRouter: () => ({ push: vi.fn() }), +})); + +beforeEach(() => { + window.localStorage.clear(); +}); + +describe('docs index', () => { + it('wears the same control plane as every other docs route', () => { + render(); + + const scope = screen.getByRole('heading', { name: 'Scope' }).closest('section'); + if (!scope) throw new Error('Expected a Scope section'); + // Library-neutral: the index is where you pick one, so it claims none. + expect(within(scope).getByText('Docs')).toBeTruthy(); + expect(within(scope).getByText('Overview')).toBeTruthy(); + expect(screen.getByRole('button', { name: 'Choose a library' })).toBeTruthy(); + }); + + it('keeps the landing content out of the prose measure', () => { + const { container } = render(); + + // The card grids need their own width; the [slug] route's md:max-w-3xl + // article measure would flatten them into a single column. + const body = container.querySelector('.docs-index-body'); + expect(body).toBeTruthy(); + expect(body?.className).not.toContain('max-w-3xl'); + }); + + it('calls the render library what the docs call it', () => { + render(); + + // The picker menu is closed on mount, so its labels are not in the DOM + // until it is opened. Asserting without this click passes even when + // docsConfig still says "Render" — it only ever sees the index card. + fireEvent.click(screen.getByRole('button', { name: 'Choose a library' })); + const menu = screen.getByRole('menu'); + const pickerNames = within(menu) + .getAllByRole('menuitemradio') + .map((item) => item.querySelector('.docs-sidebar-lib-item-title')?.textContent); + + // 85 occurrences of "json-render" across docs content vs one "Render" in + // docsConfig. With the control plane on this page both labels are on + // screen at once, so they have to agree. + expect(pickerNames).toContain('json-render'); + expect(pickerNames).not.toContain('Render'); + }); +}); diff --git a/apps/website/src/app/docs/page.tsx b/apps/website/src/app/docs/page.tsx index b36ebbf8e..bad5598a7 100644 --- a/apps/website/src/app/docs/page.tsx +++ b/apps/website/src/app/docs/page.tsx @@ -6,6 +6,9 @@ import { Eyebrow } from '../../components/ui/Eyebrow'; import { Card } from '../../components/ui/Card'; import { Pill } from '../../components/ui/Pill'; import { CopyButton } from '../../components/docs/CopyButton'; +import { DocsControlPlane } from '../../components/docs/DocsControlPlane'; +import { DocsSearch } from '../../components/docs/DocsSearch'; +import { DOCS_INDEX_TITLE } from '../../lib/docs-config'; import { createPageMetadata } from '../../lib/site-metadata'; export const metadata = createPageMetadata({ @@ -152,7 +155,19 @@ function GlyphChip({ size, children }: { size: number; children: ReactNode }) { export default function DocsLandingPage() { return ( - <> +
+ + {/* The index is library-neutral: it is where you pick one. */} + + {/* Deliberately outside the article measure the [slug] route uses — the + * card grids need their own width, and the prose column would flatten + * them. The shell supplies the chrome, not the content width. */} +
{/* Hero */}
@@ -309,6 +324,7 @@ export default function DocsLandingPage() {
- +
+ ); } diff --git a/apps/website/src/components/shared/Nav.spec.tsx b/apps/website/src/components/shared/Nav.spec.tsx index 54cd141dc..0d75e83e0 100644 --- a/apps/website/src/components/shared/Nav.spec.tsx +++ b/apps/website/src/components/shared/Nav.spec.tsx @@ -26,6 +26,21 @@ describe('Docs mobile navigation', () => { pathnameRef.current = '/docs/langgraph/guides/streaming'; }); + it('names the docs index the same way the page does', () => { + pathnameRef.current = '/docs'; + render(