perf(web): register only the Prism languages used, scope Google OAuth

The API guide imported react-syntax-highlighter's default entry, which is
the highlight.js build with every language compiled in. It also applied
Prism themes to it, and the two use different class names, so the samples
were largely uncoloured on top of being heavy. Switch to PrismLight and
register the six languages the guide actually renders.

GoogleOAuthProvider wrapped the whole app but only the login and register
pages use it, so the Google Identity SDK initialised on every dashboard
page. Move it inside LoginWithGoogle, which is where both consumers go
through.

Total client chunks drop from 3.1M to 2.3M.

Also fixes four e2e tests that were relying on page.goto returning a
fully loaded page. Adding loading.tsx introduced a short skeleton state
that did not exist before, and each test measured or interacted during
it. They now wait on real signals instead of timing:

- bulk send waits for the dropzone row cap, which comes from
  useSubscription inside that page's own hook. The previous wait on the
  devices response was wrong: the dashboard layout requests the same
  query key, so it can resolve before the page hydrates.
- the mobile overflow sweep and the API guide replace networkidle, which
  needs a 500ms window with zero connections that link prefetching keeps
  pushing out of reach.
- the footer/tab-bar check re-scrolls as the page grows, instead of
  scrolling once while the skeleton is still short.
- the overflow sweep measures the billing tab rather than
  /dashboard/account, a redirect stub with no layout of its own whose
  client-side redirect tore down the measurement.

A new test asserts the samples carry Prism token markup in all five
languages, so a revert to the highlight.js entry fails instead of
silently dropping the colours.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
isra el
2026-07-22 14:36:34 +03:00
parent ab96e731a6
commit 3eba8d3444
7 changed files with 127 additions and 39 deletions

View File

@@ -2,11 +2,28 @@
import { Routes } from '@/config/routes'
import { toast } from '@/hooks/use-toast'
import { CredentialResponse, GoogleLogin } from '@react-oauth/google'
import {
CredentialResponse,
GoogleLogin,
GoogleOAuthProvider,
} from '@react-oauth/google'
import { signIn } from 'next-auth/react'
import { useRouter, useSearchParams } from 'next/navigation'
// The provider lives here rather than in the app-wide tree so the Google
// Identity SDK is only loaded on the two pages that render this button
// (login and register) instead of on every dashboard page.
export default function LoginWithGoogle() {
return (
<GoogleOAuthProvider
clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID ?? ''}
>
<GoogleLoginButton />
</GoogleOAuthProvider>
)
}
function GoogleLoginButton() {
const router = useRouter()
const searchParams = useSearchParams()
const redirect = searchParams.get('redirect')

View File

@@ -3,11 +3,33 @@
import { useState } from 'react'
import { useTheme } from 'next-themes'
import { Check, Copy } from 'lucide-react'
import SyntaxHighlighter from 'react-syntax-highlighter'
import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter'
import bash from 'react-syntax-highlighter/dist/esm/languages/prism/bash'
import go from 'react-syntax-highlighter/dist/esm/languages/prism/go'
import javascript from 'react-syntax-highlighter/dist/esm/languages/prism/javascript'
import json from 'react-syntax-highlighter/dist/esm/languages/prism/json'
import php from 'react-syntax-highlighter/dist/esm/languages/prism/php'
import python from 'react-syntax-highlighter/dist/esm/languages/prism/python'
import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
// Prism, registering only the languages the guide uses. The bare
// react-syntax-highlighter entry is the highlight.js build with every language
// compiled in, and it emits hljs-* class names that the Prism themes below do
// not style, so this both shrinks the chunk and makes the theme apply.
// The languages come from LANGUAGES in snippets.ts plus json for responses.
for (const [name, definition] of [
['bash', bash],
['go', go],
['javascript', javascript],
['json', json],
['php', php],
['python', python],
] as const) {
SyntaxHighlighter.registerLanguage(name, definition)
}
// The previous guide hardcoded a dark slate background regardless of theme, so
// code blocks were the only dark thing on a light page.
export default function CodeBlock({

View File

@@ -1,6 +1,5 @@
'use client'
import { GoogleOAuthProvider } from '@react-oauth/google'
import { SessionProvider, useSession } from 'next-auth/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useEffect, useState, type PropsWithChildren } from 'react'
@@ -54,13 +53,7 @@ export default function Providers({
return (
<SessionProvider session={session} refetchOnWindowFocus={false}>
<SessionTokenBridge />
<QueryClientProvider client={queryClient}>
<GoogleOAuthProvider
clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID ?? ''}
>
{children}
</GoogleOAuthProvider>
</QueryClientProvider>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</SessionProvider>
)
}

View File

@@ -55,6 +55,33 @@ test.describe('api guide (mocked API, no real backend)', () => {
await expect(page.getByText('package main').first()).toBeVisible()
})
// The guide applies Prism themes, so it must render with the Prism
// highlighter. It previously used the package's default export, which is the
// highlight.js build: that emits hljs-* classes the Prism theme never styles
// (so samples went uncoloured) and compiles in every language it supports.
test('samples are highlighted by Prism, in every language', async ({
page,
context,
}) => {
await authenticate(context)
await mockApi(page)
await page.goto('/dashboard/messaging/api-guide')
for (const language of ['cURL', 'Node.js', 'Python', 'PHP', 'Go']) {
await page.getByRole('tab', { name: language, exact: true }).click()
await expect(
page.locator('pre code .token').first(),
`${language} samples must carry Prism token markup`
).toBeVisible()
}
// A registered language yields more than one token type; a missing one
// would fall back to a single undifferentiated text run.
expect(
await page.locator('pre code .token').count()
).toBeGreaterThan(5)
})
test('code can be copied', async ({ page, context }) => {
await authenticate(context)
await mockApi(page)
@@ -63,6 +90,10 @@ test.describe('api guide (mocked API, no real backend)', () => {
await page.getByRole('button', { name: 'Copy code' }).first().click()
// The button relabels itself only after the async clipboard write resolves,
// so this is the signal that there is something to read back.
await expect(page.getByRole('button', { name: 'Copied' }).first()).toBeVisible()
const clipboard = await page.evaluate(() =>
navigator.clipboard.readText()
)
@@ -74,7 +105,10 @@ test.describe('api guide (mocked API, no real backend)', () => {
await authenticate(context)
await mockApi(page)
await page.goto('/dashboard/messaging/api-guide')
await page.waitForLoadState('networkidle')
// Not networkidle: link prefetching keeps the network busy, so that state
// can go unreached under parallel workers. A rendered sample proves the
// code blocks (the thing that would widen the page) are laid out.
await expect(page.getByText('curl -X POST').first()).toBeVisible()
// Code blocks are the classic way to widen a mobile page.
const { scrollWidth, innerWidth } = await page.evaluate(() => ({

View File

@@ -24,13 +24,17 @@ async function uploadCsv(page: import('@playwright/test').Page, csv: string) {
}
// The section streams behind a loading.tsx boundary, so the form's HTML can
// paint before React hydrates it; interacting that early is silently lost.
// The devices query only fires after hydration effects run, so its response
// is proof the dropzone and inputs have their handlers attached.
// paint before React hydrates it, and a file offered during that gap is
// silently discarded (react-dropzone binds its change handler on mount).
// The row cap in the dropzone copy is derived from useSubscription inside this
// page's own hook, so seeing it proves this component hydrated and has data.
// Waiting on the devices response would not: the dashboard layout requests the
// same query key, so it can resolve before this page has hydrated.
async function gotoBulkPage(page: import('@playwright/test').Page) {
const devicesLoaded = page.waitForResponse('**/gateway/devices')
await page.goto('/dashboard/messaging/bulk')
await devicesLoaded
await expect(
page.getByText(/Up to 1 MB and [\d,]+ rows on your plan/)
).toBeVisible()
}
// Keyboard selection rather than clicking the option: the Radix popper can

View File

@@ -42,23 +42,29 @@ test.describe('app chrome (mocked API, no real backend)', () => {
await mockApi(page)
await page.goto('/dashboard')
// Scroll to the very bottom so the footer is in view.
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight))
const footer = page.locator('footer')
await expect(footer).toBeVisible()
const tabBar = page.locator('nav.fixed').first()
const footerBox = await footer.boundingBox()
const tabBarBox = await page.locator('nav.fixed').first().boundingBox()
expect(footerBox, 'footer must have a layout box').not.toBeNull()
expect(tabBarBox, 'mobile tab bar must have a layout box').not.toBeNull()
// The footer's last pixel must sit above the tab bar's first pixel.
expect(
footerBox!.y + footerBox!.height,
'footer bottom must clear the fixed tab bar'
).toBeLessThanOrEqual(tabBarBox!.y + 1)
// Scrolled again on every attempt rather than once up front: the page
// starts as a short loading skeleton and grows as each card's data lands,
// so a single scroll to the bottom can be measured from a document that is
// still shorter than it ends up, leaving the footer below the viewport.
await expect
.poll(
async () => {
await page.evaluate(() =>
window.scrollTo(0, document.body.scrollHeight)
)
const footerBox = await footer.boundingBox()
const tabBarBox = await tabBar.boundingBox()
if (!footerBox || !tabBarBox) return Number.POSITIVE_INFINITY
// The footer's last pixel must sit above the tab bar's first pixel.
return footerBox.y + footerBox.height - (tabBarBox.y + 1)
},
{ message: 'footer bottom must clear the fixed tab bar' }
)
.toBeLessThanOrEqual(0)
})
test('mobile: footer links stack vertically', async ({ page, context }) => {

View File

@@ -15,7 +15,10 @@ const AUTHED_PAGES = [
'/dashboard/messaging/api-guide',
'/dashboard/webhooks',
'/dashboard/webhooks/deliveries',
'/dashboard/account',
// The billing tab, not /dashboard/account: that route is a redirect stub
// with no layout of its own, and measuring it raced the redirect. The
// redirect itself is covered in account.spec.ts.
'/dashboard/account/billing',
'/dashboard/account/profile',
'/dashboard/account/security',
'/dashboard/account/support',
@@ -24,13 +27,22 @@ const AUTHED_PAGES = [
'/dashboard/community',
]
// Deterministic replacement for waitForLoadState('networkidle'), which needs a
// 500ms window with zero connections. Link prefetching keeps issuing and
// aborting RSC requests, so under parallel workers that window can be missed
// until the test times out. Waiting for the page's own heading and for its
// loading placeholders to clear proves the same thing: content is laid out.
async function waitForContent(page: import('@playwright/test').Page) {
await expect(page.locator('main#main-content h2').first()).toBeVisible()
await expect(page.locator('main#main-content [role="status"]')).toHaveCount(0)
}
async function expectNoHorizontalScroll(page: import('@playwright/test').Page) {
const { scrollWidth, innerWidth } = await page.evaluate(() => ({
scrollWidth: document.documentElement.scrollWidth,
innerWidth: window.innerWidth,
}))
expect(scrollWidth, 'page must not scroll sideways at 375px').toBeLessThanOrEqual(
innerWidth
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth - window.innerWidth
)
expect(overflow, 'page must not scroll sideways at 375px').toBeLessThanOrEqual(
0
)
}
@@ -42,7 +54,7 @@ test.describe('no horizontal overflow at 375px (mocked API)', () => {
await authenticate(context)
await mockApi(page)
await page.goto(path)
await page.waitForLoadState('networkidle')
await waitForContent(page)
await expectNoHorizontalScroll(page)
})
}
@@ -70,7 +82,7 @@ test.describe('no horizontal overflow at 375px (mocked API)', () => {
test('login page', async ({ page }) => {
await mockApi(page)
await page.goto('/login')
await page.waitForLoadState('networkidle')
await expect(page.getByText('Welcome back')).toBeVisible()
await expectNoHorizontalScroll(page)
})
})