import { expect, test } from '@playwright/test'; const caseStudyPath = '/projects/qlockify'; async function expectDrawerOpen(page: import('@playwright/test').Page) { const dialog = page.locator('dialog[open]'); await expect(dialog).toHaveAttribute('data-state', 'open'); await expect(dialog).toBeVisible(); return dialog; } test('a pointer click on the card CTA opens a route-backed drawer and restores the portfolio', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); await page.evaluate(() => { window.scrollTo({ top: document.body.scrollHeight * 0.35, behavior: 'instant' }); }); const card = page.getByRole('article', { name: 'Qlockify' }); const cta = card.getByRole('link', { name: 'View case study' }); // Measure the viewport from which the user actually clicks, after any // automatic scroll needed to bring the link into view. await cta.scrollIntoViewIfNeeded(); const portfolioScroll = await page.evaluate(() => window.scrollY); await cta.click(); await expect(page).toHaveURL(new RegExp(`${caseStudyPath}$`)); const dialog = await expectDrawerOpen(page); await expect(dialog.getByRole('button', { name: 'Close case study' })).toBeFocused(); await expect(card.getByRole('link', { name: /Open the live Qlockify/ })).toHaveAttribute('target', '_blank'); await expect(card.getByRole('link', { name: /Open the live Qlockify/ })).toHaveAttribute('rel', 'noopener noreferrer'); await page.keyboard.press('Escape'); await expect(page).toHaveURL(/\/$/); await expect(page.locator('dialog[open]')).toHaveCount(0); await expect(cta).toBeFocused(); await expect.poll(async () => Math.abs(await page.evaluate(() => window.scrollY) - portfolioScroll)) .toBeLessThan((page.viewportSize()?.height ?? 800) / 2); }); test('a shared route opens over the portfolio and adjacent navigation replaces history', async ({ page }) => { await page.goto(caseStudyPath, { waitUntil: 'domcontentloaded' }); const dialog = await expectDrawerOpen(page); await expect(page.getByRole('region', { name: 'Products I\'ve Brought to Life' })).toBeAttached(); const historyLength = await page.evaluate(() => history.length); await dialog.getByRole('link', { name: /Next project Guilan ACE/ }).click(); await expect(page).toHaveURL(/\/projects\/guilan-ace$/); await expect(dialog.getByRole('heading', { name: 'Guilan ACE', level: 1 })).toBeVisible(); expect(await page.evaluate(() => history.length)).toBe(historyLength); await dialog.getByRole('button', { name: 'Close case study' }).click(); await expect(page).toHaveURL(/\/$/); }); test('share fallback, copy feedback, focus containment, and the localized RTL route work', async ({ page, context }) => { // This test exercises the fallback. Native mobile share sheets are outside // the DOM and are covered separately through the supported-share branch. await page.addInitScript(() => Object.defineProperty(navigator, 'share', { configurable: true, value: undefined })); await context.grantPermissions(['clipboard-read', 'clipboard-write']); await page.goto('/fa/projects/qlockify', { waitUntil: 'domcontentloaded' }); const dialog = await expectDrawerOpen(page); await expect(page.locator('html')).toHaveAttribute('dir', 'rtl'); await expect(dialog.getByRole('button', { name: 'بستن مطالعه موردی' })).toBeFocused(); await dialog.getByRole('button', { name: 'اشتراک‌گذاری این مطالعه موردی' }).click(); const shareDialog = page.getByRole('dialog', { name: 'اشتراک‌گذاری مطالعه موردی' }); await expect(shareDialog).toBeVisible(); await shareDialog.getByRole('button', { name: 'کپی پیوند' }).click(); await expect(shareDialog.getByText('پیوند کپی شد', { exact: true }).first()).toBeVisible(); expect(await page.evaluate(() => navigator.clipboard.readText())).toMatch(/\/fa\/projects\/qlockify$/); await shareDialog.getByRole('button', { name: 'بستن گزینه‌های اشتراک‌گذاری' }).click(); await expect(dialog.getByRole('button', { name: 'اشتراک‌گذاری این مطالعه موردی' })).toBeFocused(); await page.keyboard.press('Tab'); expect(await page.evaluate(() => document.activeElement?.closest('dialog') !== null)).toBe(true); }); test('mobile uses native sharing when it is available', async ({ page, isMobile }) => { test.skip(!isMobile, 'Native sharing is used on compact layouts'); await page.addInitScript(() => Object.defineProperty(navigator, 'share', { configurable: true, value: async (data: ShareData) => { document.documentElement.dataset.sharedUrl = data.url; } })); await page.goto(caseStudyPath, { waitUntil: 'domcontentloaded' }); const dialog = await expectDrawerOpen(page); await dialog.getByRole('button', { name: 'Share this case study' }).click(); await expect(page.locator('html')).toHaveAttribute('data-shared-url', new RegExp(`${caseStudyPath}$`)); }); test('the gallery serves original media and opens an accessible full-screen viewer', async ({ page, isMobile }) => { await page.goto(caseStudyPath, { waitUntil: 'domcontentloaded' }); const drawer = await expectDrawerOpen(page); const stage = drawer.getByRole('button', { name: /Open full-screen image/ }); const selectedImage = stage.getByRole('img'); await expect(selectedImage).toHaveAttribute('src', '/images/screenshots/qlockify/qlockify-landing.png'); await stage.click(); const viewer = page.getByRole('dialog', { name: 'Full-screen project gallery' }); await expect(viewer).toBeVisible(); await expect(viewer.getByText('1 of 8')).toBeVisible(); if (isMobile) { await viewer.getByRole('button', { name: /^Show image 2:/ }).click(); } else { await viewer.getByRole('button', { name: 'Next image' }).click(); } await expect(viewer.getByText('2 of 8')).toBeVisible(); await viewer.getByRole('button', { name: 'Close full-screen image' }).click(); await expect(stage).toBeFocused(); }); test('the drawer stays bottom-anchored with its own scrolling surface', async ({ page }) => { await page.goto(caseStudyPath, { waitUntil: 'domcontentloaded' }); const dialog = await expectDrawerOpen(page); const sheet = dialog.locator(':scope > div'); const scrollArea = sheet.locator(':scope > div:last-child'); const box = await sheet.boundingBox(); const viewport = page.viewportSize(); expect(box).not.toBeNull(); expect(viewport).not.toBeNull(); expect(Math.abs((box?.y ?? 0) + (box?.height ?? 0) - (viewport?.height ?? 0))).toBeLessThanOrEqual(2); expect(await page.locator('html').evaluate((element) => getComputedStyle(element).overflow)).toBe('hidden'); expect(await scrollArea.evaluate((element) => element.scrollHeight > element.clientHeight)).toBe(true); });