Deploy to Production / Build & Verify (push) Failing after 5m56s
Ping Search Engines / Notify Search Engines (push) Successful in 2s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 2s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Smoke Tests (P0) (push) Failing after 11m26s
E2E Test Suite / Form Interaction Tests (push) Failing after 11m42s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 12m2s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 16m14s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 17m45s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 25m23s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 20s
E2E Test Suite / Mobile Device Tests (push) Failing after 2h49m9s
Uptime Monitor / Health & Response Time (push) Failing after 2s
Uptime Monitor / SSL Certificate (push) Successful in 2s
Uptime Monitor / Send Alerts (push) Failing after 3s
Uptime Monitor / Record Uptime Success (push) Has been skipped
405 lines
13 KiB
TypeScript
405 lines
13 KiB
TypeScript
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();
|
|
});
|
|
});
|