From 9100958d4f79dced274a3380de029f8f3ceb1ef6 Mon Sep 17 00:00:00 2001 From: Bradley Shellnut Date: Wed, 27 Aug 2025 18:43:22 -0700 Subject: [PATCH] Updating tests and removing test results --- .github/workflows/svelte_integration.yml | 2 +- package.json | 2 +- pnpm-lock.yaml | 10 +- test-results/.last-run.json | 4 - tests/home.test.ts | 14 +- tests/portfolio.test.ts | 188 ++++++++++++++++++++++ tests/uses.test.ts | 191 +++++++++++++++++++++++ 7 files changed, 393 insertions(+), 18 deletions(-) delete mode 100644 test-results/.last-run.json create mode 100644 tests/portfolio.test.ts create mode 100644 tests/uses.test.ts diff --git a/.github/workflows/svelte_integration.yml b/.github/workflows/svelte_integration.yml index d6e50f9..b662bb1 100644 --- a/.github/workflows/svelte_integration.yml +++ b/.github/workflows/svelte_integration.yml @@ -5,7 +5,7 @@ on: branches: [ main, master ] pull_request: branches: [ main, master ] - + workflow_dispatch: diff --git a/package.json b/package.json index be5ecd5..f7af65e 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "bits-ui": "2.9.4", "flexsearch": "^0.8.205", "ioredis": "^5.7.0", - "lucide-svelte": "^0.541.0", + "lucide-svelte": "^0.542.0", "scrape-it": "^6.1.11", "sharp": "^0.34.3", "svelte-local-storage-store": "^0.6.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 931b4d5..bc8eda2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ importers: specifier: ^5.7.0 version: 5.7.0 lucide-svelte: - specifier: ^0.541.0 - version: 0.541.0(svelte@5.38.5) + specifier: ^0.542.0 + version: 0.542.0(svelte@5.38.5) scrape-it: specifier: ^6.1.11 version: 6.1.11 @@ -2077,8 +2077,8 @@ packages: lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - lucide-svelte@0.541.0: - resolution: {integrity: sha512-Jk+LiOYDl62R/0nWkG1s5XL2k6LHmPq3wUfiJ6qtBhb8jGefB4PU10x5HJrAihwaKqVc2vH5wjKMELGjHJenEQ==} + lucide-svelte@0.542.0: + resolution: {integrity: sha512-KxqJycY4EWaGy1zk/7sqEcf48j4YaP9zEujYczzrsw5j9b9b5pjAJ1qGFKZvD7T6xz9reSYAfUAd6Bz62TdqGw==} peerDependencies: svelte: ^3 || ^4 || ^5.0.0-next.42 @@ -4600,7 +4600,7 @@ snapshots: dependencies: tslib: 2.8.1 - lucide-svelte@0.541.0(svelte@5.38.5): + lucide-svelte@0.542.0(svelte@5.38.5): dependencies: svelte: 5.38.5 diff --git a/test-results/.last-run.json b/test-results/.last-run.json deleted file mode 100644 index 344ea9e..0000000 --- a/test-results/.last-run.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "status": "interrupted", - "failedTests": [] -} \ No newline at end of file diff --git a/tests/home.test.ts b/tests/home.test.ts index 0a592ff..6a489e1 100644 --- a/tests/home.test.ts +++ b/tests/home.test.ts @@ -31,12 +31,12 @@ test.describe('Home page', () => { await expect(link).toBeVisible(); const before = await link.evaluate((el) => { - const cs = getComputedStyle(el as Element) as CSSStyleDeclaration; + const cs = getComputedStyle(el); return { color: cs.color }; }); await link.hover(); const after = await link.evaluate((el) => { - const cs = getComputedStyle(el as Element) as CSSStyleDeclaration; + const cs = getComputedStyle(el); return { color: cs.color }; }); @@ -55,7 +55,7 @@ test.describe('Home page', () => { const nav = page.locator(area); const link = nav.getByRole('link', { name: 'Home', exact: true }); await expect(link).toBeVisible(); - const isActive = await link.evaluate((el) => (el as Element).classList.contains('active')); + const isActive = await link.evaluate((el) => el.classList.contains('active')); expect(isActive).toBeTruthy(); } }); @@ -193,9 +193,9 @@ test.describe('Home page', () => { // Layout: single column and scrollable vertically const scrollInfo = await albumsGrid.evaluate((el) => ({ - overflowY: getComputedStyle(el as HTMLElement).overflowY, - scrollHeight: (el as HTMLElement).scrollHeight, - clientHeight: (el as HTMLElement).clientHeight, + overflowY: getComputedStyle(el).overflowY, + scrollHeight: el.scrollHeight, + clientHeight: el.clientHeight, })); expect(scrollInfo.clientHeight).toBeLessThan(scrollInfo.scrollHeight); expect(['auto', 'scroll']).toContain(scrollInfo.overflowY); @@ -216,7 +216,7 @@ test.describe('Home page', () => { // Articles are a vertical list (same x, increasing y) const boxes = await page.locator('section.articles article.card').evaluateAll((els) => - (els as HTMLElement[]).slice(0, Math.min(4, els.length)).map((el) => el.getBoundingClientRect()) + els.slice(0, Math.min(4, els.length)).map((el) => el.getBoundingClientRect()) ); expect(boxes.length).toBeGreaterThan(0); const x0 = boxes[0].left; diff --git a/tests/portfolio.test.ts b/tests/portfolio.test.ts new file mode 100644 index 0000000..d79af05 --- /dev/null +++ b/tests/portfolio.test.ts @@ -0,0 +1,188 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Portfolio page', () => { + test('has expected main heading', async ({ page }) => { + await page.goto('/portfolio'); + await expect(page.getByRole('heading', { level: 1, name: 'Portfolio!' })).toBeVisible(); + }); + + test('header/footer links hover: color becomes shellYellow', async ({ page }) => { + await page.goto('/portfolio'); + + const shellYellow = await page.evaluate(() => { + const probe = document.createElement('div'); + probe.style.color = 'var(--shellYellow)'; + document.body.appendChild(probe); + const color = getComputedStyle(probe).color; + probe.remove(); + return color; + }); + + const areas = [ + 'header[aria-label="header navigation"]', + 'footer nav[aria-label="footer navigation"]', + ]; + + for (const area of areas) { + const nav = page.locator(area); + await expect(nav).toBeVisible(); + + const link = nav.getByRole('link', { name: 'Portfolio', exact: true }); + await expect(link).toBeVisible(); + + const before = await link.evaluate((el) => getComputedStyle(el as Element).color); + await link.hover(); + const after = await link.evaluate((el) => getComputedStyle(el as Element).color); + + expect(after).toBe(shellYellow); + expect(after).not.toBe(before); + } + }); + + test('current page (Portfolio) link is active in header and footer', async ({ page }) => { + await page.goto('/portfolio'); + const areas = [ + 'header[aria-label="header navigation"]', + 'footer nav[aria-label="footer navigation"]', + ]; + for (const area of areas) { + const nav = page.locator(area); + const portfolioLink = nav.getByRole('link', { name: 'Portfolio', exact: true }); + await expect(portfolioLink).toBeVisible(); + const isActive = await portfolioLink.evaluate((el) => (el as Element).classList.contains('active')); + expect(isActive).toBeTruthy(); + } + }); + + test('tabs render and can switch between Personal and Professional', async ({ page }) => { + await page.goto('/portfolio'); + + // Prefer role-based tab selection; fall back to data attribute if role is not present + const personalTab = page.getByRole('tab', { name: 'Personal' }).or(page.locator('[data-tabs-trigger]', { hasText: 'Personal' })); + const professionalTab = page.getByRole('tab', { name: 'Professional' }).or(page.locator('[data-tabs-trigger]', { hasText: 'Professional' })); + + await expect(personalTab).toBeVisible(); + await expect(professionalTab).toBeVisible(); + + // Personal content visible by default (card heading exists) + const personalCardHeading = page.locator('.portfolio-picture h2', { hasText: 'Personal Website' }).first(); + await expect(personalCardHeading).toBeVisible(); + + // Switch to Professional + await professionalTab.click(); + + // Professional content appears, personal may hide + const professionalCardHeading = page.locator('.portfolio-picture h2', { hasText: 'Mark Shellnut Architect' }).first(); + await expect(professionalCardHeading).toBeVisible(); + }); + + test('personal tab: key cards, images, and external links are accessible', async ({ page }) => { + await page.goto('/portfolio'); + + // Ensure on Personal tab + const personalTab = page.getByRole('tab', { name: 'Personal' }).or(page.locator('[data-tabs-trigger]', { hasText: 'Personal' })); + await personalTab.click(); + + // Headings (scoped to portfolio cards to avoid strict-mode conflicts) + await expect(page.locator('.portfolio-picture h2', { hasText: 'Personal Website' }).first()).toBeVisible(); + await expect(page.locator('.portfolio-picture h2', { hasText: 'Wedding Website' }).first()).toBeVisible(); + await expect(page.locator('.portfolio-picture h2', { hasText: 'Old Personal Website' }).first()).toBeVisible(); + + // Images by alt text + await expect(page.getByAltText("Picture of Bradley Shellnut's Personal Website")).toBeVisible(); + await expect(page.getByAltText('Picture of NextJS Wedding Website')).toBeVisible(); + await expect(page.getByAltText('Home Page of the old bradleyshellnut.com website')).toBeVisible(); + + // External links (use visible link names) + await expect(page.getByRole('link', { name: /GitHub repository/i }).first()).toBeVisible(); + }); + + test('professional tab: card renders with external link', async ({ page }) => { + await page.goto('/portfolio'); + + const professionalTab = page.getByRole('tab', { name: 'Professional' }).or(page.locator('[data-tabs-trigger]', { hasText: 'Professional' })); + await professionalTab.click(); + + const professionalCard = page + .locator('.portfolio') + .filter({ has: page.locator('.portfolio-picture h2', { hasText: 'Mark Shellnut Architect' }) }) + .first(); + + await expect(professionalCard).toBeVisible(); + // Accessible name derived from aria-label in ExternalLink.svelte + await expect( + professionalCard.getByRole('link', { name: /Open\s+View Mark Shellnut Architect\s+externally/i }) + ).toBeVisible(); + }); + + // Mirror header link presence from other pages + test('header navigation shows expected links', async ({ page }) => { + await page.goto('/portfolio'); + const headerNav = page.locator('header[aria-label="header navigation"]'); + await expect(headerNav).toBeVisible(); + await expect(headerNav.getByRole('link', { name: 'Home', exact: true })).toBeVisible(); + await expect(headerNav.getByRole('link', { name: 'About', exact: true })).toBeVisible(); + await expect(headerNav.getByRole('link', { name: 'Portfolio', exact: true })).toBeVisible(); + await expect(headerNav.getByRole('link', { name: 'Uses', exact: true })).toBeVisible(); + }); + + // Mirror navigation flow via header (starting on /portfolio) + test('header navigation links go to correct routes (from /portfolio)', async ({ page }) => { + await page.goto('/portfolio'); + const headerNav = page.locator('header[aria-label="header navigation"]'); + + await headerNav.getByRole('link', { name: 'About', exact: true }).click(); + await expect(page).toHaveURL(/\/about\/?$/); + + await headerNav.getByRole('link', { name: 'Uses', exact: true }).click(); + await expect(page).toHaveURL(/\/uses\/?$/); + + await headerNav.getByRole('link', { name: 'Home', exact: true }).click(); + await expect(page).toHaveURL(/\/?$/); + + await headerNav.getByRole('link', { name: 'Portfolio', exact: true }).click(); + await expect(page).toHaveURL(/\/portfolio\/?$/); + }); + + // Mirror footer link presence from other pages + test('footer shows expected links', async ({ page }) => { + await page.goto('/portfolio'); + const footerNav = page.getByRole('navigation', { name: 'footer navigation' }); + await expect(footerNav).toBeVisible(); + await expect(footerNav.getByRole('link', { name: 'Home', exact: true })).toBeVisible(); + await expect(footerNav.getByRole('link', { name: 'About', exact: true })).toBeVisible(); + await expect(footerNav.getByRole('link', { name: 'Portfolio', exact: true })).toBeVisible(); + await expect(footerNav.getByRole('link', { name: 'Uses', exact: true })).toBeVisible(); + await expect(footerNav.getByRole('link', { name: 'Privacy', exact: true })).toBeVisible(); + await expect(footerNav.getByRole('link', { name: 'Favorite Articles', exact: true })).toBeVisible(); + }); + + // Mirror navigation via footer (starting on /portfolio) + test('footer navigation links go to correct routes (from /portfolio)', async ({ page }) => { + await page.goto('/portfolio'); + const footerNav = page.getByRole('navigation', { name: 'footer navigation' }); + + await footerNav.getByRole('link', { name: 'Privacy', exact: true }).scrollIntoViewIfNeeded(); + await footerNav.getByRole('link', { name: 'Privacy', exact: true }).click(); + await expect(page).toHaveURL(/\/privacy\/?$/); + + // Favorite Articles may route to /articles or /articles/1 + const fav = footerNav.getByRole('link', { name: 'Favorite Articles', exact: true }); + await fav.scrollIntoViewIfNeeded(); + const href = await fav.getAttribute('href'); + expect(href).toBeTruthy(); + if (href) { + expect(href).toMatch(/\/articles(\/\d+)?\/?$/); + await page.goto(href); + await expect(page).toHaveURL(/\/articles(\/\d+)?\/?$/, { timeout: 15000 }); + } + + await footerNav.getByRole('link', { name: 'About', exact: true }).scrollIntoViewIfNeeded(); + await footerNav.getByRole('link', { name: 'About', exact: true }).click(); + await expect(page).toHaveURL(/\/about\/?$/); + + await footerNav.getByRole('link', { name: 'Home', exact: true }).scrollIntoViewIfNeeded(); + await footerNav.getByRole('link', { name: 'Home', exact: true }).click(); + await expect(page).toHaveURL(/\/?$/); + }); +}); diff --git a/tests/uses.test.ts b/tests/uses.test.ts new file mode 100644 index 0000000..fa35fed --- /dev/null +++ b/tests/uses.test.ts @@ -0,0 +1,191 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Uses page', () => { + test('has expected main heading', async ({ page }) => { + await page.goto('/uses'); + await expect(page.getByRole('heading', { level: 1, name: '/Uses' })).toBeVisible(); + }); + + test('header/footer links hover: color becomes shellYellow', async ({ page }) => { + await page.goto('/uses'); + + const shellYellow = await page.evaluate(() => { + const probe = document.createElement('div'); + probe.style.color = 'var(--shellYellow)'; + document.body.appendChild(probe); + const color = getComputedStyle(probe).color; + probe.remove(); + return color; + }); + + const areas = [ + 'header[aria-label="header navigation"]', + 'footer nav[aria-label="footer navigation"]', + ]; + + for (const area of areas) { + const nav = page.locator(area); + await expect(nav).toBeVisible(); + + const link = nav.getByRole('link', { name: 'Uses', exact: true }); + await expect(link).toBeVisible(); + + const before = await link.evaluate((el) => getComputedStyle(el as Element).color); + await link.hover(); + const after = await link.evaluate((el) => getComputedStyle(el as Element).color); + + expect(after).toBe(shellYellow); + expect(after).not.toBe(before); + } + }); + + test('current page (Uses) link is active in header and footer', async ({ page }) => { + await page.goto('/uses'); + const areas = [ + 'header[aria-label="header navigation"]', + 'footer nav[aria-label="footer navigation"]', + ]; + for (const area of areas) { + const nav = page.locator(area); + const usesLink = nav.getByRole('link', { name: 'Uses', exact: true }); + await expect(usesLink).toBeVisible(); + const isActive = await usesLink.evaluate((el) => (el as Element).classList.contains('active')); + expect(isActive).toBeTruthy(); + } + }); + + test('hero image visible with correct alt text', async ({ page }) => { + await page.goto('/uses'); + await expect(page.getByAltText('Clean desk with Samsung monitor and Ducky Keyboard')).toBeVisible(); + }); + + test('intro external links are accessible by aria-label-based names', async ({ page }) => { + await page.goto('/uses'); + + // ExternalLink.svelte sets accessible name as: "Open {ariaLabel|title|href} externally" + await expect(page.getByRole('link', { name: /Open\s+Wes Bos' Website\s+externally/i })).toBeVisible(); + await expect(page.getByRole('link', { name: /Open\s+Wes Bos' Uses Page\s+externally/i })).toBeVisible(); + await expect(page.getByRole('link', { name: /Open\s+Uses\.tech\s+externally/i })).toBeVisible(); + }); + + test('sections and subsections render', async ({ page }) => { + await page.goto('/uses'); + + await expect(page.getByRole('heading', { level: 2, name: 'Development' })).toBeVisible(); + + // h3 subsections in development.svelte + const subsections = [ + 'Terminal & Shell Setup', + 'Useful System Packages', + 'Software', + 'Useful Applications', + 'Browsers', + ]; + for (const name of subsections) { + await expect(page.getByRole('heading', { level: 3, name })).toBeVisible(); + } + }); + + test('a few key external links in Development section are present', async ({ page }) => { + await page.goto('/uses'); + + // Select within the Development section to be robust + const devSection = page.locator('section#uses-development'); + await expect(devSection).toBeVisible(); + + const expectedLinks = [ + /Open\s+Bradley Shellnut Computer Setup\s+externally/i, + /Open\s+Dotfiles\s+externally/i, + /Open\s+Tabby\s+externally/i, + /Open\s+Starship\s+externally/i, + /Open\s+ZimFW\s+externally/i, + /Open\s+Linux Brew\s+externally/i, + /Open\s+Homebrew\s+externally/i, + /Open\s+TLDR Man Pages\s+externally/i, + /Open\s+Trash-CLI\s+externally/i, + /Open\s+VSCodium\s+externally/i, + /Open\s+VSCode Extensions List\s+externally/i, + /Open\s+Sublime Text 3\s+externally/i, + /Open\s+Sublime Text Packages List\s+externally/i, + /Open\s+IntelliJ IDEA\s+externally/i, + /Open\s+IntelliJ Plugins\s+externally/i, + /Open\s+Bruno\s+externally/i, + /Open\s+Brave Browser\s+externally/i, + /Open\s+Firefox\s+externally/i, + ]; + + for (const pattern of expectedLinks) { + await expect(devSection.getByRole('link', { name: pattern })).toBeVisible(); + } + }); + + // Header nav presence + test('header navigation shows expected links', async ({ page }) => { + await page.goto('/uses'); + const headerNav = page.locator('header[aria-label="header navigation"]'); + await expect(headerNav).toBeVisible(); + await expect(headerNav.getByRole('link', { name: 'Home', exact: true })).toBeVisible(); + await expect(headerNav.getByRole('link', { name: 'About', exact: true })).toBeVisible(); + await expect(headerNav.getByRole('link', { name: 'Portfolio', exact: true })).toBeVisible(); + await expect(headerNav.getByRole('link', { name: 'Uses', exact: true })).toBeVisible(); + }); + + // Header navigation flow (starting on /uses) + test('header navigation links go to correct routes (from /uses)', async ({ page }) => { + await page.goto('/uses'); + const headerNav = page.locator('header[aria-label="header navigation"]'); + + await headerNav.getByRole('link', { name: 'About', exact: true }).click(); + await expect(page).toHaveURL(/\/about\/?$/); + + await headerNav.getByRole('link', { name: 'Portfolio', exact: true }).click(); + await expect(page).toHaveURL(/\/portfolio\/?$/); + + await headerNav.getByRole('link', { name: 'Home', exact: true }).click(); + await expect(page).toHaveURL(/\/?$/); + + await headerNav.getByRole('link', { name: 'Uses', exact: true }).click(); + await expect(page).toHaveURL(/\/uses\/?$/); + }); + + // Footer link presence + test('footer shows expected links', async ({ page }) => { + await page.goto('/uses'); + const footerNav = page.getByRole('navigation', { name: 'footer navigation' }); + await expect(footerNav).toBeVisible(); + await expect(footerNav.getByRole('link', { name: 'Home', exact: true })).toBeVisible(); + await expect(footerNav.getByRole('link', { name: 'About', exact: true })).toBeVisible(); + await expect(footerNav.getByRole('link', { name: 'Portfolio', exact: true })).toBeVisible(); + await expect(footerNav.getByRole('link', { name: 'Uses', exact: true })).toBeVisible(); + await expect(footerNav.getByRole('link', { name: 'Privacy', exact: true })).toBeVisible(); + await expect(footerNav.getByRole('link', { name: 'Favorite Articles', exact: true })).toBeVisible(); + }); + + // Footer navigation flow (starting on /uses) + test('footer navigation links go to correct routes (from /uses)', async ({ page }) => { + await page.goto('/uses'); + const footerNav = page.getByRole('navigation', { name: 'footer navigation' }); + + await footerNav.getByRole('link', { name: 'Privacy', exact: true }).scrollIntoViewIfNeeded(); + await footerNav.getByRole('link', { name: 'Privacy', exact: true }).click(); + await expect(page).toHaveURL(/\/privacy\/?$/); + + const fav = footerNav.getByRole('link', { name: 'Favorite Articles', exact: true }); + await fav.scrollIntoViewIfNeeded(); + const href = await fav.getAttribute('href'); + expect(href).toBeTruthy(); + if (href) { + expect(href).toMatch(/\/articles(\/\d+)?\/?$/); + await page.goto(href); + await expect(page).toHaveURL(/\/articles(\/\d+)?\/?$/, { timeout: 15000 }); + } + + await footerNav.getByRole('link', { name: 'About', exact: true }).scrollIntoViewIfNeeded(); + await footerNav.getByRole('link', { name: 'About', exact: true }).click(); + await expect(page).toHaveURL(/\/about\/?$/); + + await footerNav.getByRole('link', { name: 'Home', exact: true }).scrollIntoViewIfNeeded(); + await footerNav.getByRole('link', { name: 'Home', exact: true }).click(); + await expect(page).toHaveURL(/\/?$/); + }); +});