import { test, expect } from '@playwright/test'; /** * E2E Blog Navigation Test Suite * Comprehensive testing of blog listing, filtering, and post reading flows */ test.describe('Blog Navigation - Complete User Flow', () => { test('navigate from homepage to blog and read multiple posts', async ({ page }) => { // Start from homepage await page.goto('/'); // Navigate to blog const blogLink = page.locator('a[href="/blog"]').first(); await blogLink.click(); await expect(page).toHaveURL('/blog'); // Verify blog page loaded await page.waitForLoadState('networkidle'); const posts = page.locator('article, [class*="post"], [class*="card"]'); const postCount = await posts.count(); expect(postCount).toBeGreaterThan(0); // Click first post const firstPost = posts.first().locator('a').first(); await firstPost.click(); await expect(page).toHaveURL(/\/blog\//); // Verify post content await expect(page.locator('h1')).toBeVisible(); await expect(page.locator('article, [class*="prose"]')).toBeVisible(); // Navigate back to blog listing await page.goBack(); await expect(page).toHaveURL('/blog'); // Click different post if (postCount > 1) { const secondPost = posts.nth(1).locator('a').first(); await secondPost.click(); await expect(page).toHaveURL(/\/blog\//); } }); test('blog post deep reading experience', async ({ page }) => { await page.goto('/blog/getting-started-with-astro'); // Verify title loaded const title = page.locator('h1'); await expect(title).toBeVisible(); // Verify content structure const article = page.locator('article, [class*="prose"], main').first(); await expect(article).toBeVisible(); // Check for paragraphs const paragraphs = page.locator('p'); expect(await paragraphs.count()).toBeGreaterThan(0); // Scroll through content await page.evaluate(() => window.scrollTo(0, 500)); await page.waitForTimeout(300); await page.evaluate(() => window.scrollTo(0, 1000)); await page.waitForTimeout(300); // Scroll to bottom await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); await page.waitForTimeout(300); // Verify no layout breaks during scroll const bodyWidth = await page.evaluate(() => document.body.scrollWidth); const viewportWidth = page.viewportSize()?.width || 1280; expect(bodyWidth).toBeLessThanOrEqual(viewportWidth + 20); }); test('blog post metadata visibility', async ({ page }) => { await page.goto('/blog'); const firstPost = page.locator('article, [class*="post"]').first(); // Check for date/time const hasDate = await firstPost.locator('time, [class*="date"]').count() > 0; expect(hasDate).toBeTruthy(); // Click to full post await firstPost.locator('a').first().click(); await expect(page).toHaveURL(/\/blog\//); // Verify metadata on post page const postDate = page.locator('time, [class*="date"]'); if (await postDate.count() > 0) { await expect(postDate.first()).toBeVisible(); } }); test('blog image loading and optimization', async ({ page }) => { await page.goto('/blog/getting-started-with-astro'); await page.waitForLoadState('networkidle'); const images = page.locator('img'); const imageCount = await images.count(); if (imageCount > 0) { // Check first few images for (let i = 0; i < Math.min(3, imageCount); i++) { const img = images.nth(i); if (await img.isVisible()) { // Verify image has loaded const loaded = await img.evaluate((el: HTMLImageElement) => { return el.complete && el.naturalWidth > 0; }); expect(loaded).toBeTruthy(); // Check for alt text (accessibility) const alt = await img.getAttribute('alt'); expect(alt).toBeTruthy(); } } } }); test('blog category/tag navigation', async ({ page }) => { await page.goto('/blog'); // Look for category badges or tags const categories = page.locator('[class*="category"], [class*="tag"], [class*="badge"]'); if (await categories.count() > 0) { // Categories should be visible await expect(categories.first()).toBeVisible(); // If categories are clickable, test navigation const firstCategory = categories.first(); const isLink = (await firstCategory.evaluate(el => el.tagName)) === 'A'; if (isLink) { await firstCategory.click(); // Should filter or navigate await page.waitForLoadState('networkidle'); } } }); test('blog search functionality if available', async ({ page }) => { await page.goto('/blog'); // Check for search input const searchInput = page.locator('input[type="search"], input[placeholder*="search" i]'); if (await searchInput.count() > 0) { await searchInput.fill('astro'); await page.keyboard.press('Enter'); // Wait for search results await page.waitForTimeout(500); // Verify some results or "no results" message const hasResults = await page.locator('article, [class*="post"]').count() > 0 || await page.locator('text=/no results|no posts/i').count() > 0; expect(hasResults).toBeTruthy(); } }); test('blog pagination if available', async ({ page }) => { await page.goto('/blog'); // Look for pagination controls const paginationNext = page.locator('[class*="pagination"] a, button[class*="next"], a:has-text("Next")'); if (await paginationNext.count() > 0) { const nextButton = paginationNext.first(); if (await nextButton.isVisible()) { await nextButton.click(); await page.waitForLoadState('networkidle'); // Should show different posts or page 2 const hasContent = await page.locator('article, [class*="post"]').count() > 0; expect(hasContent).toBeTruthy(); } } }); }); test.describe('Blog Navigation - Mobile Experience', () => { test.use({ viewport: { width: 375, height: 667 } }); test('mobile blog listing is readable', async ({ page }) => { await page.goto('/blog'); // Posts should stack vertically const posts = page.locator('article, [class*="post"]'); await expect(posts.first()).toBeVisible(); // No horizontal scroll const bodyWidth = await page.evaluate(() => document.body.scrollWidth); expect(bodyWidth).toBeLessThanOrEqual(395); // Tap first post await posts.first().locator('a').first().tap(); await expect(page).toHaveURL(/\/blog\//); }); test('mobile blog post reading experience', async ({ page }) => { await page.goto('/blog/getting-started-with-astro'); // Content should be readable const content = page.locator('article, [class*="prose"]'); await expect(content.first()).toBeVisible(); // No horizontal overflow const bodyWidth = await page.evaluate(() => document.body.scrollWidth); expect(bodyWidth).toBeLessThanOrEqual(400); // Font should be readable const fontSize = await page.locator('p').first().evaluate(el => { return parseFloat(window.getComputedStyle(el).fontSize); }); expect(fontSize).toBeGreaterThanOrEqual(14); // Test scroll performance for (let i = 0; i < 5; i++) { await page.evaluate(() => window.scrollBy(0, 200)); await page.waitForTimeout(100); } // Verify images adapt to mobile const images = page.locator('img'); if (await images.count() > 0) { const firstImg = images.first(); const imgWidth = await firstImg.evaluate(el => el.getBoundingClientRect().width); expect(imgWidth).toBeLessThanOrEqual(375); } }); }); test.describe('Blog Navigation - Reading Patterns', () => { test('skimming behavior - quick navigation', async ({ page }) => { await page.goto('/blog'); const posts = page.locator('article, [class*="post"]'); const postCount = await posts.count(); // Simulate user skimming multiple posts quickly for (let i = 0; i < Math.min(3, postCount); i++) { const post = posts.nth(i); await post.scrollIntoViewIfNeeded(); await page.waitForTimeout(200); // Check title is visible during skim const title = post.locator('h1, h2, h3, [class*="title"]'); await expect(title).toBeVisible(); } }); test('deep reading behavior - single post focus', async ({ page }) => { await page.goto('/blog/getting-started-with-astro'); // Simulate reading: scroll slowly through content const contentHeight = await page.evaluate(() => document.body.scrollHeight); const steps = 10; const scrollStep = contentHeight / steps; for (let i = 0; i < steps; i++) { await page.evaluate((step) => window.scrollTo(0, step), scrollStep * i); await page.waitForTimeout(300); // Content should remain stable during scroll const bodyWidth = await page.evaluate(() => document.body.scrollWidth); const viewportWidth = page.viewportSize()?.width || 1280; expect(bodyWidth).toBeLessThanOrEqual(viewportWidth + 20); } }); test('code block interaction in blog posts', async ({ page }) => { await page.goto('/blog/getting-started-with-astro'); const codeBlocks = page.locator('pre, code, [class*="shiki"]'); const codeCount = await codeBlocks.count(); if (codeCount > 0) { const firstCodeBlock = codeBlocks.first(); await firstCodeBlock.scrollIntoViewIfNeeded(); // Verify code is readable await expect(firstCodeBlock).toBeVisible(); // Check for syntax highlighting const hasHighlighting = await firstCodeBlock.locator('[class*="token"], [class*="hl"], span').count() > 0; // Syntax highlighting is optional but beneficial expect(hasHighlighting || true).toBeTruthy(); // Verify monospace font const fontFamily = await firstCodeBlock.evaluate(el => { return window.getComputedStyle(el).fontFamily.toLowerCase(); }); expect(fontFamily).toMatch(/mono|consolas|courier|jetbrains/); } }); test('blog post sharing and social links', async ({ page }) => { await page.goto('/blog/getting-started-with-astro'); // Look for social share buttons const socialLinks = page.locator('a[href*="twitter"], a[href*="linkedin"], a[href*="facebook"], [class*="share"]'); if (await socialLinks.count() > 0) { // Social links should be visible const firstSocial = socialLinks.first(); await firstSocial.scrollIntoViewIfNeeded(); await expect(firstSocial).toBeVisible(); } }); }); test.describe('Blog Navigation - Performance', () => { test('blog listing loads quickly', async ({ page }) => { const startTime = Date.now(); await page.goto('/blog'); await page.waitForLoadState('domcontentloaded'); const loadTime = Date.now() - startTime; // Should load in under 3 seconds expect(loadTime).toBeLessThan(3000); // Content should be visible const posts = page.locator('article, [class*="post"]'); expect(await posts.count()).toBeGreaterThan(0); }); test('blog post loads with minimal delay', async ({ page }) => { const startTime = Date.now(); await page.goto('/blog/getting-started-with-astro'); await page.waitForLoadState('domcontentloaded'); const loadTime = Date.now() - startTime; // Should load in under 3 seconds expect(loadTime).toBeLessThan(3000); // Content should be visible await expect(page.locator('h1')).toBeVisible(); await expect(page.locator('article, [class*="prose"]')).toBeVisible(); }); test('images lazy load efficiently', async ({ page }) => { await page.goto('/blog/getting-started-with-astro'); const images = page.locator('img'); const imageCount = await images.count(); if (imageCount > 0) { // Check for lazy loading attribute const firstImg = images.first(); const loading = await firstImg.getAttribute('loading'); // Modern images should use lazy loading // (native or via JavaScript) expect(['lazy', null].includes(loading)).toBeTruthy(); } }); }); test.describe('Blog Navigation - Edge Cases', () => { test('direct URL access to blog post', async ({ page }) => { // Simulate user accessing post via bookmark or search result await page.goto('/blog/getting-started-with-astro'); // Page should load correctly await expect(page.locator('h1')).toBeVisible(); await expect(page.locator('article')).toBeVisible(); // Navigation should work await expect(page.locator('header')).toBeVisible(); await expect(page.locator('footer')).toBeVisible(); }); test('non-existent blog post returns 404', async ({ page }) => { const response = await page.goto('/blog/this-post-does-not-exist-404'); // Should return 404 or redirect if (response) { const status = response.status(); expect([404, 301, 302].includes(status)).toBeTruthy(); } }); test('blog listing with no posts', async ({ page }) => { await page.goto('/blog'); // Even with no posts, page should render await expect(page.locator('main')).toBeVisible(); // Should show message or empty state const hasContent = await page.locator('article, [class*="post"], [class*="empty"], h1').count() > 0; expect(hasContent).toBeTruthy(); }); });