Files
textbee/web/e2e/messaging.spec.ts
isra el 1349134107 fix: give every messaging tab the same content column
Switching tabs shifted the layout. The cause was broader than History being
full width: all four subroutes set their own width, so nothing lined up.

  Send    max-w-xl   576px
  Bulk    max-w-3xl  768px
  API     max-w-4xl  896px
  History none       full bleed

The header and tab strip were also full width while the view beneath them was
constrained, so on Send the tabs ran roughly 200px past the form.

The column is now defined once in the messaging layout and wraps the header,
the tabs and the children together. The per-page wrappers are gone, so a new
subroute inherits the width instead of inventing one.

Guard: an e2e visits all four tabs, measures the view rendered beneath the tab
strip, and asserts the widths match and that the column is constrained rather
than stretched.

Getting that guard to actually work took three attempts, each a real hole:
- Comparing the column against the viewport passed even with the constraint
  removed, because the sidebar and padding already made it narrower.
- Comparing against the parent's border box passed too, since that box
  includes the padding the column sits inside. It now subtracts the padding.
- Measuring the layout column could not detect a page re-constraining itself,
  because that column is identical on every tab by construction. It now
  measures the rendered view instead.
Verified by reintroducing both regressions separately and watching it fail
each one, with the offending tab named in the message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 00:34:46 +03:00

125 lines
4.4 KiB
TypeScript

import { expect, test } from '@playwright/test'
import { authenticate } from './session'
import { mockApi } from './mock-api'
test.describe('messaging (mocked API, no real backend)', () => {
test('renders the send view with route tabs', async ({ page, context }) => {
await authenticate(context)
await mockApi(page)
await page.goto('/dashboard/messaging')
await expect(
page.getByRole('heading', { name: 'Messaging', level: 2 })
).toBeVisible()
// Route tabs are links now.
const nav = page.getByRole('navigation', { name: 'Section navigation' })
await expect(nav.getByRole('link', { name: 'Send', exact: true })).toBeVisible()
await expect(nav.getByRole('link', { name: 'History' })).toBeVisible()
// Send is the active tab on the index route.
await expect(
nav.getByRole('link', { name: 'Send', exact: true })
).toHaveAttribute('aria-current', 'page')
})
test('history tab navigates to its own URL and renders mocked messages', async ({
page,
context,
}) => {
await authenticate(context)
await mockApi(page)
await page.goto('/dashboard/messaging')
await page
.getByRole('navigation', { name: 'Section navigation' })
.getByRole('link', { name: 'History' })
.click()
await expect(page).toHaveURL(/\/dashboard\/messaging\/history/)
await expect(page.getByText('Hello from textbee')).toBeVisible()
})
test('history deep link survives refresh (route-based tabs)', async ({
page,
context,
}) => {
await authenticate(context)
await mockApi(page)
// Load the subroute directly: this is the refresh-survival guarantee.
await page.goto('/dashboard/messaging/history')
const nav = page.getByRole('navigation', { name: 'Section navigation' })
await expect(nav.getByRole('link', { name: 'History' })).toHaveAttribute(
'aria-current',
'page'
)
await expect(page.getByText('Hello from textbee')).toBeVisible()
// Never fell through to an error boundary.
await expect(page.getByText('This page couldn')).toHaveCount(0)
})
test('every tab renders in the same content column', async ({
page,
context,
}) => {
await page.setViewportSize({ width: 1440, height: 900 })
await authenticate(context)
await mockApi(page)
// Each subroute used to set its own width (Send xl, Bulk 3xl, API 4xl,
// History none), so the layout jumped on every tab change.
const columns: Record<string, number> = {}
let available = 0
for (const path of [
'/dashboard/messaging',
'/dashboard/messaging/bulk',
'/dashboard/messaging/history',
'/dashboard/messaging/api-guide',
]) {
await page.goto(path)
await expect(
page.getByRole('navigation', { name: 'Section navigation' })
).toBeVisible()
// Measure the view rendered beneath the tabs, not the layout column.
// The column is identical on every tab by construction, so measuring it
// cannot detect a page that re-constrains its own content.
const measured = await page.evaluate(() => {
const nav = document.querySelector(
'nav[aria-label="Section navigation"]'
)
const content = nav!.nextElementSibling as HTMLElement
const parent = nav!.parentElement!.parentElement!
const style = getComputedStyle(parent)
return {
content: Math.round(content.getBoundingClientRect().width),
// Content width, with the parent's padding subtracted. Using the
// border box would include that padding, so an unconstrained
// full-width column would still measure as narrower than its parent.
available: Math.round(
parent.clientWidth -
parseFloat(style.paddingLeft) -
parseFloat(style.paddingRight)
),
}
})
columns[path] = measured.content
available = measured.available
}
const widths = Object.values(columns)
expect(
new Set(widths).size,
`tabs should share one column width, got ${JSON.stringify(columns)}`
).toBe(1)
// Compared against the space actually available, not the viewport:
// dropping the constraint makes every tab equally full-bleed, which would
// otherwise satisfy the equality check above.
expect(
widths[0],
'the column should be constrained, not stretched to fill'
).toBeLessThan(available)
})
})