mirror of
https://github.com/vernu/textbee.git
synced 2026-07-30 17:07:46 -04:00
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>
150 lines
5.1 KiB
TypeScript
150 lines
5.1 KiB
TypeScript
import { expect, test } from '@playwright/test'
|
|
import { authenticate } from './session'
|
|
import { mockApi } from './mock-api'
|
|
|
|
// Guards for the app chrome: the footer used to be rendered above the
|
|
// dashboard's fixed sidebar, so the sidebar painted over its left edge, and the
|
|
// fixed mobile tab bar covered its bottom edge.
|
|
|
|
test.describe('app chrome (mocked API, no real backend)', () => {
|
|
test('desktop: sidebar does not overlap the footer', async ({
|
|
page,
|
|
context,
|
|
}) => {
|
|
await page.setViewportSize({ width: 1280, height: 900 })
|
|
await authenticate(context)
|
|
await mockApi(page)
|
|
await page.goto('/dashboard')
|
|
|
|
const footer = page.locator('footer')
|
|
await expect(footer).toBeVisible()
|
|
|
|
const sidebar = page.locator('aside')
|
|
const footerBox = await footer.boundingBox()
|
|
const sidebarBox = await sidebar.boundingBox()
|
|
|
|
expect(footerBox, 'footer must have a layout box').not.toBeNull()
|
|
expect(sidebarBox, 'sidebar must have a layout box').not.toBeNull()
|
|
|
|
// The footer must start where the sidebar ends, not underneath it.
|
|
expect(
|
|
footerBox!.x,
|
|
'footer must start at or after the sidebar right edge'
|
|
).toBeGreaterThanOrEqual(sidebarBox!.x + sidebarBox!.width - 1)
|
|
})
|
|
|
|
test('mobile: bottom tab bar does not cover the footer', async ({
|
|
page,
|
|
context,
|
|
}) => {
|
|
await page.setViewportSize({ width: 375, height: 800 })
|
|
await authenticate(context)
|
|
await mockApi(page)
|
|
await page.goto('/dashboard')
|
|
|
|
const footer = page.locator('footer')
|
|
await expect(footer).toBeVisible()
|
|
const tabBar = page.locator('nav.fixed').first()
|
|
|
|
// 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 }) => {
|
|
await page.setViewportSize({ width: 375, height: 800 })
|
|
await authenticate(context)
|
|
await mockApi(page)
|
|
await page.goto('/dashboard')
|
|
|
|
const links = page.locator('footer nav a')
|
|
const count = await links.count()
|
|
expect(count).toBeGreaterThan(2)
|
|
|
|
// Stacked means every link sits on its own row, so each y is distinct.
|
|
const ys: number[] = []
|
|
for (let i = 0; i < count; i += 1) {
|
|
const box = await links.nth(i).boundingBox()
|
|
expect(box).not.toBeNull()
|
|
ys.push(Math.round(box!.y))
|
|
}
|
|
expect(new Set(ys).size, 'each footer link should be on its own row').toBe(
|
|
count
|
|
)
|
|
})
|
|
|
|
test('desktop: footer links share rows instead of stacking', async ({
|
|
page,
|
|
context,
|
|
}) => {
|
|
await page.setViewportSize({ width: 1280, height: 900 })
|
|
await authenticate(context)
|
|
await mockApi(page)
|
|
await page.goto('/dashboard')
|
|
// Text metrics decide wrapping, so wait for fonts before measuring.
|
|
await page.evaluate(() => document.fonts.ready)
|
|
|
|
const links = page.locator('footer nav a')
|
|
const count = await links.count()
|
|
|
|
const ys: number[] = []
|
|
for (let i = 0; i < count; i += 1) {
|
|
const box = await links.nth(i).boundingBox()
|
|
ys.push(Math.round(box!.y))
|
|
}
|
|
|
|
// The row layout wraps by design, so the guarantee is "not one per row",
|
|
// not "exactly one row": asserting a single row made this flaky whenever
|
|
// font metrics pushed a link onto a second line.
|
|
expect(new Set(ys).size).toBeLessThan(count)
|
|
})
|
|
|
|
test('top bar is reduced to brand and account', async ({ page, context }) => {
|
|
await authenticate(context)
|
|
await mockApi(page)
|
|
await page.goto('/dashboard')
|
|
|
|
const header = page.locator('header')
|
|
await expect(
|
|
header.getByRole('link', { name: /contribute/i })
|
|
).toHaveCount(0)
|
|
await expect(header.getByRole('button', { name: 'Toggle theme' })).toHaveCount(
|
|
0
|
|
)
|
|
})
|
|
|
|
test('theme control lives in the sidebar', async ({ page, context }) => {
|
|
await page.setViewportSize({ width: 1280, height: 900 })
|
|
await authenticate(context)
|
|
await mockApi(page)
|
|
await page.goto('/dashboard')
|
|
|
|
const themeGroup = page
|
|
.locator('aside')
|
|
.getByRole('group', { name: 'Color theme' })
|
|
await expect(themeGroup).toBeVisible()
|
|
|
|
await themeGroup.getByRole('button', { name: 'Dark' }).click()
|
|
await expect(page.locator('html')).toHaveClass(/dark/)
|
|
|
|
await themeGroup.getByRole('button', { name: 'Light' }).click()
|
|
await expect(page.locator('html')).not.toHaveClass(/dark/)
|
|
})
|
|
})
|