Latest Updated Pages
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
Deploy to Production / Build & Verify (push) Failing after 13s
Ping Search Engines / Notify Search Engines (push) Successful in 3s
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 1s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 9m36s
E2E Test Suite / Form Interaction Tests (push) Failing after 12m6s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 11m46s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 9m31s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 11m5s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 15m24s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 6s
E2E Test Suite / Mobile Device Tests (push) Failing after 3h12m28s
Uptime Monitor / Health & Response Time (push) Successful in 5s
Uptime Monitor / SSL Certificate (push) Successful in 3s
Uptime Monitor / Send Alerts (push) Has been skipped
Uptime Monitor / Record Uptime Success (push) Successful in 2s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
Deploy to Production / Build & Verify (push) Failing after 13s
Ping Search Engines / Notify Search Engines (push) Successful in 3s
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 1s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 9m36s
E2E Test Suite / Form Interaction Tests (push) Failing after 12m6s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 11m46s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 9m31s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 11m5s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 15m24s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 6s
E2E Test Suite / Mobile Device Tests (push) Failing after 3h12m28s
Uptime Monitor / Health & Response Time (push) Successful in 5s
Uptime Monitor / SSL Certificate (push) Successful in 3s
Uptime Monitor / Send Alerts (push) Has been skipped
Uptime Monitor / Record Uptime Success (push) Successful in 2s
This commit is contained in:
@@ -818,3 +818,520 @@ test.describe('Header Scroll Behavior', () => {
|
||||
await expect(page.locator('#main-header')).not.toHaveClass(/header-scrolled/);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 11. Redesigned Contact Page — New Elements
|
||||
// ============================================================
|
||||
test.describe('Contact Page: Redesigned Elements', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/contact', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
test('Trust stats strip renders with 4 stat cards', async ({ page }) => {
|
||||
// Hero section contains 4 trust stat cards
|
||||
const statCards = page.locator('section').first().locator('.text-2xl.font-bold.text-white');
|
||||
await expect(statCards).toHaveCount(4);
|
||||
});
|
||||
|
||||
test('Budget range radio chips render and are selectable', async ({ page }) => {
|
||||
const budgetOptions = page.locator('.budget-option');
|
||||
await expect(budgetOptions).toHaveCount(5);
|
||||
|
||||
// Click the first budget chip
|
||||
await budgetOptions.first().click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// The clicked chip should receive the "selected" class via JS
|
||||
await expect(budgetOptions.first()).toHaveClass(/selected/);
|
||||
|
||||
// Only one chip should be selected at a time
|
||||
const selectedChips = page.locator('.budget-option.selected');
|
||||
await expect(selectedChips).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('Budget chips are mutually exclusive (radio behavior)', async ({ page }) => {
|
||||
const budgetOptions = page.locator('.budget-option');
|
||||
|
||||
await budgetOptions.nth(0).click();
|
||||
await page.waitForTimeout(150);
|
||||
await budgetOptions.nth(2).click();
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
// Only the second-clicked chip should be selected
|
||||
await expect(budgetOptions.nth(0)).not.toHaveClass(/selected/);
|
||||
await expect(budgetOptions.nth(2)).toHaveClass(/selected/);
|
||||
});
|
||||
|
||||
test('FAQ accordion sections render (4 questions)', async ({ page }) => {
|
||||
const faqItems = page.locator('.faq-item');
|
||||
await expect(faqItems).toHaveCount(4);
|
||||
|
||||
// All summaries are visible
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await expect(faqItems.nth(i).locator('summary')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('FAQ accordion opens and closes on click', async ({ page }) => {
|
||||
const firstFaq = page.locator('.faq-item').first();
|
||||
const summary = firstFaq.locator('summary');
|
||||
|
||||
// Initially closed
|
||||
const isOpenInitially = await firstFaq.evaluate((el) => (el as HTMLDetailsElement).open);
|
||||
expect(isOpenInitially).toBe(false);
|
||||
|
||||
// Open it
|
||||
await summary.click();
|
||||
await page.waitForTimeout(200);
|
||||
const isOpenAfterClick = await firstFaq.evaluate((el) => (el as HTMLDetailsElement).open);
|
||||
expect(isOpenAfterClick).toBe(true);
|
||||
|
||||
// Content is now visible
|
||||
await expect(firstFaq.locator('p')).toBeVisible();
|
||||
|
||||
// Close it again
|
||||
await summary.click();
|
||||
await page.waitForTimeout(200);
|
||||
const isClosedAgain = await firstFaq.evaluate((el) => (el as HTMLDetailsElement).open);
|
||||
expect(isClosedAgain).toBe(false);
|
||||
});
|
||||
|
||||
test('FAQ accordion is keyboard accessible', async ({ page }) => {
|
||||
const summary = page.locator('.faq-item').first().locator('summary');
|
||||
await summary.focus();
|
||||
|
||||
// Enter key should toggle open
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(200);
|
||||
const isOpen = await page.locator('.faq-item').first().evaluate((el) => (el as HTMLDetailsElement).open);
|
||||
expect(isOpen).toBe(true);
|
||||
});
|
||||
|
||||
test('Social links section renders all 4 platforms', async ({ page }) => {
|
||||
// Social links section contains 4 social link items
|
||||
const socialLinks = page.locator('.bg-secondary-900 a[aria-label]');
|
||||
await expect(socialLinks).toHaveCount(4);
|
||||
|
||||
// Each has an aria-label
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const label = await socialLinks.nth(i).getAttribute('aria-label');
|
||||
expect(label).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('Map placeholder renders with directions link', async ({ page }) => {
|
||||
// Map section has a "Get Directions" link
|
||||
const directionsLink = page.locator('a[aria-label*="Get directions"]');
|
||||
await expect(directionsLink).toBeAttached();
|
||||
await expect(directionsLink).toHaveAttribute('href', /maps\.google\.com/);
|
||||
await expect(directionsLink).toHaveAttribute('target', '_blank');
|
||||
await expect(directionsLink).toHaveAttribute('rel', 'noopener noreferrer');
|
||||
});
|
||||
|
||||
test('Character counter updates as user types in message', async ({ page }) => {
|
||||
const charCount = page.locator('#char-count');
|
||||
await expect(charCount).toBeVisible();
|
||||
|
||||
const initialText = await charCount.textContent();
|
||||
expect(initialText).toBe('0 / 5000');
|
||||
|
||||
await page.fill('#message', 'Hello world');
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
const updatedText = await charCount.textContent();
|
||||
expect(updatedText).toBe('11 / 5000');
|
||||
});
|
||||
|
||||
test('Character counter turns red when approaching limit', async ({ page }) => {
|
||||
// Fill message with 4600 characters (triggers red state at > 4500)
|
||||
const longMsg = 'a'.repeat(4501);
|
||||
await page.fill('#message', longMsg);
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
const charCount = page.locator('#char-count');
|
||||
await expect(charCount).toHaveClass(/text-red-500/);
|
||||
});
|
||||
|
||||
test('Success state is hidden by default', async ({ page }) => {
|
||||
await expect(page.locator('#success-state')).toHaveClass(/hidden/);
|
||||
await expect(page.locator('#contact-form')).not.toHaveClass(/hidden/);
|
||||
});
|
||||
|
||||
test('Toast container is present and has live region', async ({ page }) => {
|
||||
const toastContainer = page.locator('#toast-container');
|
||||
await expect(toastContainer).toBeAttached();
|
||||
await expect(toastContainer).toHaveAttribute('aria-live', 'assertive');
|
||||
});
|
||||
|
||||
test('Scroll-reveal elements are present', async ({ page }) => {
|
||||
const revealElements = page.locator('.reveal-on-scroll');
|
||||
const count = await revealElements.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('Contact info cards render 4 cards (location, phone, email, hours)', async ({ page }) => {
|
||||
const contactCards = page.locator('.contact-card');
|
||||
await expect(contactCards).toHaveCount(4);
|
||||
|
||||
// Cards show expected headings
|
||||
const cardTitles = await page.locator('.contact-card h3').allTextContents();
|
||||
expect(cardTitles).toContain('Visit Us');
|
||||
expect(cardTitles).toContain('Call Us');
|
||||
expect(cardTitles).toContain('Email Us');
|
||||
expect(cardTitles).toContain('Business Hours');
|
||||
});
|
||||
|
||||
test('CTA banner "Start a Project" scrolls to form', async ({ page }) => {
|
||||
const ctaBtn = page.locator('a[href="#contact-form"]');
|
||||
await expect(ctaBtn).toBeVisible();
|
||||
});
|
||||
|
||||
test('Contact page renders correctly at mobile viewport', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await page.goto('/contact', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Form is visible and usable
|
||||
await expect(page.locator('#contact-form')).toBeVisible();
|
||||
|
||||
// Budget chips wrap on mobile (they use flex-wrap)
|
||||
const budgetOptions = page.locator('.budget-option');
|
||||
await expect(budgetOptions.first()).toBeVisible();
|
||||
|
||||
// FAQ section is visible
|
||||
await expect(page.locator('.faq-item').first()).toBeVisible();
|
||||
|
||||
// Social links are visible
|
||||
await expect(page.locator('.bg-secondary-900')).toBeVisible();
|
||||
|
||||
await page.screenshot({
|
||||
path: 'tests/screenshots/mobile-contact-redesign.png',
|
||||
fullPage: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('Contact page renders correctly at tablet viewport', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 768, height: 1024 });
|
||||
await page.goto('/contact', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await expect(page.locator('#contact-form')).toBeVisible();
|
||||
await expect(page.locator('.contact-card').first()).toBeVisible();
|
||||
|
||||
await page.screenshot({
|
||||
path: 'tests/screenshots/tablet-contact-redesign.png',
|
||||
fullPage: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('Contact page renders correctly at desktop viewport', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 800 });
|
||||
await page.goto('/contact', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// 5-column grid: form (3 cols) + sidebar (2 cols)
|
||||
await expect(page.locator('#contact-form')).toBeVisible();
|
||||
await expect(page.locator('.contact-card').first()).toBeVisible();
|
||||
await expect(page.locator('.bg-secondary-900')).toBeVisible();
|
||||
|
||||
await page.screenshot({
|
||||
path: 'tests/screenshots/desktop-contact-redesign.png',
|
||||
fullPage: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 12. Scroll-Reveal Animations (cross-page)
|
||||
// ============================================================
|
||||
test.describe('Scroll-Reveal Animations', () => {
|
||||
const ANIMATION_PAGES = ['/', '/about', '/services', '/contact', '/portfolio'];
|
||||
|
||||
for (const url of ANIMATION_PAGES) {
|
||||
test(`Scroll-reveal elements activate on ${url}`, async ({ page }) => {
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const revealElements = page.locator('.reveal-on-scroll');
|
||||
const count = await revealElements.count();
|
||||
|
||||
if (count === 0) return; // Page may not have reveal elements
|
||||
|
||||
// Simulate scrolling to trigger IntersectionObserver
|
||||
await page.evaluate(() => {
|
||||
window.scrollTo(0, document.body.scrollHeight / 2);
|
||||
});
|
||||
await page.waitForTimeout(800); // Allow CSS transitions
|
||||
|
||||
// At least some elements should be revealed
|
||||
const revealedCount = await page.locator('.reveal-on-scroll.revealed').count();
|
||||
expect(revealedCount).toBeGreaterThan(0);
|
||||
});
|
||||
}
|
||||
|
||||
test('Reduced motion: reveal elements are immediately visible', async ({ page }) => {
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.goto('/contact', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// With reduced motion, CSS transitions are disabled so revealed class may not be added,
|
||||
// but the element should still be visually accessible (opacity: 1, transform: none via CSS)
|
||||
const revealEl = page.locator('.reveal-on-scroll').first();
|
||||
await expect(revealEl).toBeAttached();
|
||||
|
||||
// Verify via computed style — reduced motion CSS sets opacity: 1 directly
|
||||
const opacity = await revealEl.evaluate((el) =>
|
||||
window.getComputedStyle(el).getPropertyValue('opacity')
|
||||
);
|
||||
expect(opacity).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 13. Cross-Browser Visual Snapshots (all redesigned pages)
|
||||
// ============================================================
|
||||
test.describe('Visual Snapshots: Redesigned Pages', () => {
|
||||
const SNAPSHOT_PAGES = [
|
||||
{ name: 'home', url: '/' },
|
||||
{ name: 'about', url: '/about' },
|
||||
{ name: 'services', url: '/services' },
|
||||
{ name: 'portfolio', url: '/portfolio' },
|
||||
{ name: 'contact', url: '/contact' },
|
||||
{ name: 'blog', url: '/blog' },
|
||||
];
|
||||
|
||||
for (const p of SNAPSHOT_PAGES) {
|
||||
test(`Desktop screenshot: ${p.name}`, async ({ page, browserName }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 800 });
|
||||
await page.goto(p.url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Trigger scroll-reveal for above-fold content
|
||||
await page.evaluate(() => window.scrollTo(0, 1));
|
||||
await page.waitForTimeout(500);
|
||||
await page.evaluate(() => window.scrollTo(0, 0));
|
||||
|
||||
await page.screenshot({
|
||||
path: `tests/screenshots/${browserName}-${p.name}-desktop.png`,
|
||||
fullPage: false,
|
||||
clip: { x: 0, y: 0, width: 1280, height: 800 },
|
||||
});
|
||||
});
|
||||
|
||||
test(`Mobile screenshot: ${p.name}`, async ({ page, browserName }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await page.goto(p.url, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await page.screenshot({
|
||||
path: `tests/screenshots/${browserName}-${p.name}-mobile.png`,
|
||||
fullPage: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 14. Cross-Browser Form Validation Behavior
|
||||
// ============================================================
|
||||
test.describe('Cross-Browser: Form Validation', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/contact', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
test('Submitting empty form shows validation errors', async ({ page }) => {
|
||||
const submitBtn = page.locator('#contact-form button[type="submit"]');
|
||||
await submitBtn.click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// At minimum, name, email, subject, and message errors should show
|
||||
const nameError = page.locator('[data-error="name"]');
|
||||
const emailError = page.locator('[data-error="email"]');
|
||||
const subjectError = page.locator('[data-error="subject"]');
|
||||
const messageError = page.locator('[data-error="message"]');
|
||||
|
||||
// At least one error should be visible after empty submit
|
||||
const errorsVisible = await Promise.all([
|
||||
nameError.isVisible(),
|
||||
emailError.isVisible(),
|
||||
subjectError.isVisible(),
|
||||
messageError.isVisible(),
|
||||
]);
|
||||
expect(errorsVisible.some(Boolean)).toBe(true);
|
||||
});
|
||||
|
||||
test('Valid name field clears error and shows checkmark', async ({ page }) => {
|
||||
// First trigger validation
|
||||
await page.locator('#name').fill('A'); // too short
|
||||
await page.locator('#name').blur();
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
await expect(page.locator('[data-error="name"]')).toBeVisible();
|
||||
await expect(page.locator('#name')).toHaveClass(/is-invalid/);
|
||||
|
||||
// Fix the error
|
||||
await page.locator('#name').fill('John Doe');
|
||||
await page.locator('#name').blur();
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
await expect(page.locator('[data-error="name"]')).toBeHidden();
|
||||
await expect(page.locator('#name')).toHaveClass(/is-valid/);
|
||||
});
|
||||
|
||||
test('Invalid email shows error, valid email clears it', async ({ page }) => {
|
||||
await page.locator('#email').fill('not-an-email');
|
||||
await page.locator('#email').blur();
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
await expect(page.locator('[data-error="email"]')).toBeVisible();
|
||||
await expect(page.locator('#email')).toHaveClass(/is-invalid/);
|
||||
|
||||
await page.locator('#email').fill('valid@example.com');
|
||||
await page.locator('#email').blur();
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
await expect(page.locator('[data-error="email"]')).toBeHidden();
|
||||
await expect(page.locator('#email')).toHaveClass(/is-valid/);
|
||||
});
|
||||
|
||||
test('Empty phone (optional) does not show error', async ({ page }) => {
|
||||
await page.locator('#phone').focus();
|
||||
await page.locator('#phone').blur();
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
// Phone is optional — empty value should not trigger error
|
||||
await expect(page.locator('[data-error="phone"]')).toBeHidden();
|
||||
});
|
||||
|
||||
test('Short message triggers error, adequate message clears it', async ({ page }) => {
|
||||
await page.locator('#message').fill('Hi'); // < 10 chars
|
||||
await page.locator('#message').blur();
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
await expect(page.locator('[data-error="message"]')).toBeVisible();
|
||||
|
||||
await page.locator('#message').fill('This is a valid message with enough content.');
|
||||
await page.locator('#message').blur();
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
await expect(page.locator('[data-error="message"]')).toBeHidden();
|
||||
});
|
||||
|
||||
test('Form resets correctly after "Send another message"', async ({ page }) => {
|
||||
// Pre-fill form
|
||||
await page.fill('#name', 'Test User');
|
||||
await page.fill('#email', 'test@example.com');
|
||||
await page.fill('#message', 'Test message content here.');
|
||||
|
||||
// Manually simulate success state (direct DOM manipulation)
|
||||
await page.evaluate(() => {
|
||||
document.getElementById('contact-form')?.classList.add('hidden');
|
||||
document.getElementById('success-state')?.classList.remove('hidden');
|
||||
});
|
||||
|
||||
await expect(page.locator('#success-state')).not.toHaveClass(/hidden/);
|
||||
|
||||
// Click "Send another message"
|
||||
await page.locator('#send-another').click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Form should be visible again, success state hidden
|
||||
await expect(page.locator('#contact-form')).not.toHaveClass(/hidden/);
|
||||
await expect(page.locator('#success-state')).toHaveClass(/hidden/);
|
||||
|
||||
// Fields should be cleared
|
||||
expect(await page.locator('#name').inputValue()).toBe('');
|
||||
expect(await page.locator('#message').inputValue()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 15. Cross-Browser: Services Page Redesign
|
||||
// ============================================================
|
||||
test.describe('Services Page: Cross-Browser Layout', () => {
|
||||
test('Services page loads with all key sections', async ({ page }) => {
|
||||
await page.goto('/services', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Page loads
|
||||
expect((await page.goto('/services'))?.status()).toBe(200);
|
||||
|
||||
await expect(page.locator('h1')).toBeVisible();
|
||||
await expect(page.locator('footer')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Services page has no horizontal scroll overflow at mobile', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await page.goto('/services', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const bodyScrollWidth = await page.evaluate(() => document.body.scrollWidth);
|
||||
const viewportWidth = await page.evaluate(() => window.innerWidth);
|
||||
|
||||
expect(bodyScrollWidth).toBeLessThanOrEqual(viewportWidth + 5); // 5px tolerance
|
||||
});
|
||||
|
||||
test('Services page has no horizontal scroll overflow at desktop', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 800 });
|
||||
await page.goto('/services', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const bodyScrollWidth = await page.evaluate(() => document.body.scrollWidth);
|
||||
const viewportWidth = await page.evaluate(() => window.innerWidth);
|
||||
|
||||
expect(bodyScrollWidth).toBeLessThanOrEqual(viewportWidth + 5);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 16. Cross-Browser: Portfolio Page Advanced Filtering
|
||||
// ============================================================
|
||||
test.describe('Portfolio Page: Advanced Filtering Cross-Browser', () => {
|
||||
test('Portfolio filter count badge updates when filtering', async ({ page }) => {
|
||||
await page.goto('/portfolio', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('.project-card').first()).toBeVisible();
|
||||
|
||||
const initialCount = await page.locator('.project-card:not(.hidden)').count();
|
||||
expect(initialCount).toBeGreaterThan(0);
|
||||
|
||||
// Filter to web
|
||||
await page.locator('button[data-filter="web"]').click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const filteredCount = await page.locator('.project-card:not(.hidden)').count();
|
||||
expect(filteredCount).toBeGreaterThan(0);
|
||||
expect(filteredCount).toBeLessThanOrEqual(initialCount);
|
||||
});
|
||||
|
||||
test('Portfolio gallery modal shows project details', async ({ page }) => {
|
||||
await page.goto('/portfolio', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('.view-details-btn').first()).toBeVisible();
|
||||
|
||||
await page.locator('.view-details-btn').first().click();
|
||||
await page.waitForTimeout(400);
|
||||
|
||||
const modal = page.locator('#case-study-modal');
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// Modal has content
|
||||
await expect(page.locator('#modal-title')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Portfolio page has no horizontal scroll overflow at mobile', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await page.goto('/portfolio', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const bodyScrollWidth = await page.evaluate(() => document.body.scrollWidth);
|
||||
const viewportWidth = await page.evaluate(() => window.innerWidth);
|
||||
expect(bodyScrollWidth).toBeLessThanOrEqual(viewportWidth + 5);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 17. Cross-Browser: No Horizontal Overflow (all pages)
|
||||
// ============================================================
|
||||
test.describe('Cross-Browser: No Horizontal Scroll Overflow', () => {
|
||||
const CHECK_PAGES = ['/', '/about', '/services', '/portfolio', '/contact', '/blog'];
|
||||
|
||||
for (const url of CHECK_PAGES) {
|
||||
test(`No horizontal overflow at mobile on ${url}`, async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const bodyScrollWidth = await page.evaluate(() => document.body.scrollWidth);
|
||||
const viewportWidth = await page.evaluate(() => window.innerWidth);
|
||||
expect(bodyScrollWidth).toBeLessThanOrEqual(viewportWidth + 5);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user