First Init
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
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
This commit is contained in:
@@ -0,0 +1,404 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user