Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,7 @@ src/
├── app/
│ ├── api/
│ │ └── reflect/ # AI processing endpoint
│ ├── components-demo/ # Component showcase page
│ └── page.tsx # Main application page
│ │ └── page.tsx # Main application page
├── components/
│ ├── ui/ # UI components (buttons, inputs, etc.)
│ └── __tests__/ # Component unit tests
Expand Down
318 changes: 84 additions & 234 deletions e2e/responsive-validation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,277 +1,127 @@
import { test, expect } from '@playwright/test'

const breakpoints = {
mobile: { width: 375, height: 667, name: 'Mobile' },
xs: { width: 480, height: 800, name: 'XS' },
sm: { width: 640, height: 800, name: 'SM' },
md: { width: 768, height: 1024, name: 'MD' },
lg: { width: 1024, height: 768, name: 'LG' },
xl: { width: 1280, height: 1024, name: 'XL' },
'2xl': { width: 1536, height: 1024, name: '2XL' },
mobile: 375,
xs: 480,
sm: 640,
md: 768,
lg: 1024,
xl: 1280,
'2xl': 1536,
}

test.describe('Responsive Layout Validation', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
})

for (const [key, viewport] of Object.entries(breakpoints)) {
test(`should render correctly at ${viewport.name} (${viewport.width}x${viewport.height})`, async ({
test.describe('Journal App Responsive Validation', () => {
const testBreakpoints = [
{ name: 'Mobile', width: breakpoints.mobile, height: 667 },
{ name: 'XS', width: breakpoints.xs, height: 800 },
{ name: 'SM', width: breakpoints.sm, height: 800 },
{ name: 'MD', width: breakpoints.md, height: 1024 },
{ name: 'LG', width: breakpoints.lg, height: 768 },
{ name: 'XL', width: breakpoints.xl, height: 1024 },
{ name: '2XL', width: breakpoints['2xl'], height: 1024 },
]

testBreakpoints.forEach(({ name, width, height }) => {
test(`should render correctly at ${name} (${width}x${height})`, async ({
page,
}) => {
// Set viewport size
await page.setViewportSize({
width: viewport.width,
height: viewport.height,
})

// Wait for layout to stabilize
await page.waitForLoadState('networkidle')

// Check that main content is visible
await expect(page.locator('main')).toBeVisible()
await expect(
page.getByRole('heading', { name: 'Reflect App' })
).toBeVisible()

// Verify no horizontal overflow
const bodyScrollWidth = await page.evaluate(
() => document.body.scrollWidth
await page.goto('/')
await page.setViewportSize({ width, height })

// Core app elements should be visible and functional
await expect(page.locator('h1')).toBeVisible()
await expect(page.getByTestId('journal-entry-input')).toBeVisible()
await expect(page.getByTestId('reflect-now-button')).toBeVisible()

// Test actual user interaction
const input = page.getByTestId('journal-entry-input')
await input.fill(
'This is a test entry to verify the app works at this screen size.'
)
const windowInnerWidth = await page.evaluate(() => window.innerWidth)
expect(bodyScrollWidth).toBeLessThanOrEqual(windowInnerWidth + 1) // Allow 1px tolerance

// Check that current breakpoint indicator is showing correctly
if (key === 'mobile') {
const mobileIndicator = page
.locator('.bg-destructive')
.filter({ hasText: /Mobile/ })
await expect(mobileIndicator).toBeVisible()
} else if (key === 'xs') {
const xsIndicator = page
.locator('.bg-chart-1')
.filter({ hasText: /XS \(480px\+\)/ })
await expect(xsIndicator).toBeVisible()
} else if (key === 'sm') {
const smIndicator = page
.locator('.bg-chart-2')
.filter({ hasText: /SM \(640px\+\)/ })
await expect(smIndicator).toBeVisible()
} else if (key === 'md') {
const mdIndicator = page
.locator('.bg-chart-3')
.filter({ hasText: /MD \(768px\+\)/ })
await expect(mdIndicator).toBeVisible()
} else if (key === 'lg') {
const lgIndicator = page
.locator('.bg-chart-4')
.filter({ hasText: /LG \(1024px\+\)/ })
await expect(lgIndicator).toBeVisible()
} else if (key === 'xl') {
const xlIndicator = page
.locator('.bg-chart-5')
.filter({ hasText: /XL \(1280px\+\)/ })
await expect(xlIndicator).toBeVisible()
} else if (key === '2xl') {
const x2xlIndicator = page
.locator('.bg-primary')
.filter({ hasText: /2XL \(1536px\+\)/ })
await expect(x2xlIndicator).toBeVisible()
}
await expect(page.getByTestId('reflect-now-button')).toBeEnabled()

// Verify grid layout adapts correctly
const breakpointGrid = page.locator('[data-testid="breakpoint-grid"]')
await expect(breakpointGrid).toBeVisible()
// Clear input should work
await input.clear()
await expect(page.getByTestId('reflect-now-button')).toBeDisabled()

// Take screenshot for visual validation
await page.screenshot({
path: `e2e/screenshots/responsive-${key}-${viewport.width}x${viewport.height}.png`,
fullPage: true,
// Theme toggle should be accessible
const themeToggle = page.getByTestId('theme-toggle-advanced')
await expect(themeToggle).toBeVisible()

// No horizontal overflow
const hasHorizontalOverflow = await page.evaluate(() => {
return document.body.scrollWidth > document.body.clientWidth
})
expect(hasHorizontalOverflow).toBe(false)
})
}

test('should handle smooth transitions between breakpoints', async ({
page,
}) => {
// Start at mobile size
await page.setViewportSize({ width: 375, height: 667 })
await page.waitForLoadState('networkidle')

// Gradually increase width to test transitions
const testWidths = [480, 640, 768, 1024, 1280, 1536]

for (const width of testWidths) {
await page.setViewportSize({ width, height: 800 })

// Wait for layout to stabilize after viewport change
await page.waitForLoadState('networkidle')

// Wait for the appropriate breakpoint indicator to be visible
// This ensures the CSS transitions have completed
if (width >= 1536) {
await expect(
page.locator('.bg-primary').filter({ hasText: /2XL \(1536px\+\)/ })
).toBeVisible()
} else if (width >= 1280) {
await expect(
page.locator('.bg-chart-5').filter({ hasText: /XL \(1280px\+\)/ })
).toBeVisible()
} else if (width >= 1024) {
await expect(
page.locator('.bg-chart-4').filter({ hasText: /LG \(1024px\+\)/ })
).toBeVisible()
} else if (width >= 768) {
await expect(
page.locator('.bg-chart-3').filter({ hasText: /MD \(768px\+\)/ })
).toBeVisible()
} else if (width >= 640) {
await expect(
page.locator('.bg-chart-2').filter({ hasText: /SM \(640px\+\)/ })
).toBeVisible()
} else if (width >= 480) {
await expect(
page.locator('.bg-chart-1').filter({ hasText: /XS \(480px\+\)/ })
).toBeVisible()
}

// Verify no layout breaks
const bodyScrollWidth = await page.evaluate(
() => document.body.scrollWidth
)
const windowInnerWidth = await page.evaluate(() => window.innerWidth)
expect(bodyScrollWidth).toBeLessThanOrEqual(windowInnerWidth + 1)

// Verify content is still visible
await expect(
page.getByRole('heading', { name: 'Reflect App' })
).toBeVisible()
}
})

test('should maintain proper spacing and alignment at all breakpoints', async ({
page,
}) => {
for (const [, viewport] of Object.entries(breakpoints)) {
await page.setViewportSize({
width: viewport.width,
height: viewport.height,
})
await page.waitForLoadState('networkidle')
await page.goto('/')

// Check header layout
const header = page.locator('header')
await expect(header).toBeVisible()
for (const { width, height } of testBreakpoints) {
await page.setViewportSize({ width, height })

// Check that cards maintain proper spacing
const cards = page.locator('[data-testid="card"], .group')
const cardCount = await cards.count()
// Main container should be centered
const main = page.locator('main').first()
await expect(main).toBeVisible()

if (cardCount > 0) {
for (let i = 0; i < cardCount; i++) {
await expect(cards.nth(i)).toBeVisible()
}
}
// Elements should have proper spacing
const input = page.getByTestId('journal-entry-input')
const button = page.getByTestId('reflect-now-button')

await expect(input).toBeVisible()
await expect(button).toBeVisible()

// Verify form elements are properly sized
const buttons = page.locator('button')
const buttonCount = await buttons.count()
// Button should be below input with proper spacing
const inputBox = await input.boundingBox()
const buttonBox = await button.boundingBox()

for (let i = 0; i < buttonCount; i++) {
const button = buttons.nth(i)
if (await button.isVisible()) {
const boundingBox = await button.boundingBox()
if (boundingBox) {
// Buttons should have reasonable touch target size on mobile (< 480px)
// Note: Some buttons might have borders/padding that affect rendered size
if (viewport.width < 480) {
expect(boundingBox.height).toBeGreaterThanOrEqual(36)
} else {
// At xs+ breakpoints, can be smaller
expect(boundingBox.height).toBeGreaterThanOrEqual(28)
}
}
}
if (inputBox && buttonBox) {
// Button should be below input (allowing for some margin)
expect(buttonBox.y).toBeGreaterThan(inputBox.y + inputBox.height - 10)
}
}
})

test('should handle container behavior correctly', async ({ page }) => {
for (const [, viewport] of Object.entries(breakpoints)) {
await page.setViewportSize({
width: viewport.width,
height: viewport.height,
})
await page.waitForLoadState('networkidle')

// Check container max-width behavior
const container = page.locator('.container').first()
const containerBoundingBox = await container.boundingBox()

if (containerBoundingBox) {
// Container should never exceed viewport width
expect(containerBoundingBox.width).toBeLessThanOrEqual(viewport.width)
await page.goto('/')

// Container should be properly centered
const containerLeft = containerBoundingBox.x
const containerRight = containerLeft + containerBoundingBox.width
const viewportCenter = viewport.width / 2
const containerCenter = (containerLeft + containerRight) / 2
for (const { width, height } of testBreakpoints.slice(0, 4)) {
// Test smaller breakpoints
await page.setViewportSize({ width, height })

// Allow some tolerance for centering
expect(Math.abs(containerCenter - viewportCenter)).toBeLessThanOrEqual(
20
)
}
// Container should not exceed viewport width
const bodyWidth = await page.evaluate(() => document.body.scrollWidth)
expect(bodyWidth).toBeLessThanOrEqual(width + 1) // +1 for rounding
}
})

test('should handle theme toggle interactions across breakpoints', async ({
page,
}) => {
for (const [, viewport] of Object.entries(breakpoints)) {
await page.setViewportSize({
width: viewport.width,
height: viewport.height,
})
await page.waitForLoadState('networkidle')

// Find theme toggle buttons
const themeToggle = page.locator('[data-testid="theme-toggle"]').first()
const themeToggleAdvanced = page
.locator('[data-testid="theme-toggle-advanced"]')
.first()

// Test basic theme toggle
if (await themeToggle.isVisible()) {
await themeToggle.click()

// Wait for theme change by checking that toggle button is still responsive
// and that the page has finished any theme transitions
await page.waitForLoadState('networkidle')

// Simple verification that the theme system is working
await expect(themeToggle).toBeVisible()

// Verify the toggle button has theme-aware icons (Sun/Moon) visible
const sunIcon = themeToggle.locator('svg').first()
await expect(sunIcon).toBeVisible()
}
await page.goto('/')

// Test advanced theme toggle
if (await themeToggleAdvanced.isVisible()) {
await themeToggleAdvanced.click()
for (const { width, height } of [
testBreakpoints[0], // Mobile
testBreakpoints[3], // MD
testBreakpoints[5], // XL
]) {
await page.setViewportSize({ width, height })

// Wait for any network activity to settle after theme change
await page.waitForLoadState('networkidle')
const themeToggle = page.getByTestId('theme-toggle-advanced')
await expect(themeToggle).toBeVisible()

// Verify button is still visible and functional
await expect(themeToggleAdvanced).toBeVisible()
// Should be clickable at all screen sizes
await themeToggle.click()
await expect(themeToggle).toBeVisible()

// Verify button contains valid theme text (Light, Dark, or System)
const buttonText = await themeToggleAdvanced.textContent()
expect(buttonText).toMatch(/\b(Light|Dark|System)\b/)
}
// App should remain functional after theme change
await expect(page.getByTestId('journal-entry-input')).toBeVisible()
await expect(page.getByTestId('reflect-now-button')).toBeVisible()
}
})
})
Loading
Loading