diff --git a/.github/workflows/on_pr_test.yaml b/.github/workflows/on_pr_test.yaml index 8bd68dc9..2d2bf146 100644 --- a/.github/workflows/on_pr_test.yaml +++ b/.github/workflows/on_pr_test.yaml @@ -4,32 +4,6 @@ on: branches: - master - main - paths: - - '_posts/**' - - 'content/**' - - 'assets/**' - - 'src/**' - - 'index.md' - - '_includes/**' - - '_data/**' - - '_image_sources/**' - - 'galleries/**' - - 'navigation_and_indexes/**' - - 'products/**' - - '.github/workflows/on_push_to_master_test_and_deploy.yaml' - - '.github/workflows/on_call_build_site.yaml' - - '.github/workflows/on_call_staging_test.yaml' - - '.github/workflows/on_pr_test.yaml' - - 'package.json' - - 'package-lock.json' - - 'webpack.config.js' - - 'favicon.png' - - '.eleventy.*' - - '_config.yml' - - 'google*.html' - - 'ads.txt' - - 'Dockerfile' - - 'docker-compose.yml' jobs: detect_base_image_changes: diff --git a/README.md b/README.md index 36704c33..9a10a9f3 100644 --- a/README.md +++ b/README.md @@ -34,22 +34,7 @@ This project uses BDD (Behavior-Driven Development) tests with Gherkin syntax po ### Running Integration Tests -To run the BDD integration tests locally: -```bash -npm run test:bdd -``` - -Alternatively, you can use Docker Compose (recommended for CI/workflows): -```bash -docker compose run test -``` - -To run tests in Docker: -```bash -docker compose run test -``` - -The tests are located in `tests/staging/features/` and use Gherkin feature files to describe expected behavior. +See [tests/README.md](tests/README.md) for details on running the BDD integration tests. ## Preparing to contribute diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..d9be92a7 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,20 @@ +# BDD Tests + +This directory contains BDD tests for validating article content and layout. + +## Test Files + +### `article-content.feature` +Contains BDD scenarios for testing article content and layout validation. + +## Running Tests + +To run the BDD integration tests: +```bash +docker compose run test +``` + +## Prerequisites + +- Playwright browsers installed (handled automatically in Docker) +- Site built and served (see project README) \ No newline at end of file diff --git a/tests/staging/features/article-content.feature b/tests/staging/features/article-content.feature new file mode 100644 index 00000000..d55f2e36 --- /dev/null +++ b/tests/staging/features/article-content.feature @@ -0,0 +1,18 @@ +Feature: Article Content Tests + + Scenario: Article has required content elements + Given the Staging site is started + When I navigate to the article "/2025/07/08/08-comparing-anker-power-packs.html" + Then the article should have a set of tags in a nav linking to tag slugs + And the article should have a post header in an H2 element + And the page title should contain the post title and "orionrobots" + And the article should have visible images inside the article tag + And the article should have a date and author in a div element + And the page should have a footer with Discord and YouTube links + And the page should have the main menu in a nav element at the top + + Scenario: Desktop view layout does not overflow + Given the Staging site is started + When I navigate to the article "/2025/07/08/08-comparing-anker-power-packs.html" + And I am in desktop view + Then the images, tables and text should not overflow the article container \ No newline at end of file diff --git a/tests/staging/step_definitions/website_steps.js b/tests/staging/step_definitions/website_steps.js index 73d0bebe..a1b59f11 100644 --- a/tests/staging/step_definitions/website_steps.js +++ b/tests/staging/step_definitions/website_steps.js @@ -126,3 +126,242 @@ Then('each recent post should have a picture tag with an img element', async fun } } }); + +When('I navigate to the article {string}', async function (articlePath) { + if (!page) { + throw new Error('Page not initialized. Make sure "Given the Staging site is started" step is executed first.'); + } + + const fullUrl = BASE_URL + articlePath; + const response = await page.goto(fullUrl, { + waitUntil: 'networkidle', + timeout: 30000 + }); + + if (!response || !response.ok()) { + throw new Error(`Article page failed to load. Status: ${response ? response.status() : 'No response'}`); + } +}); + +Then('the article should have a set of tags in a nav linking to tag slugs', async function () { + if (!page) { + throw new Error('Page not initialized. Make sure previous steps are executed first.'); + } + + try { + // Look for the tags navigation section + const tagsNav = await page.locator('nav.tag-row, nav:has(a[href*="/tags/"])').first(); + await tagsNav.waitFor({ state: 'visible', timeout: 10000 }); + + // Check that there are tag links present within the tags navigation + const tagLinks = await tagsNav.locator('a[href*="/tags/"]').all(); + if (tagLinks.length === 0) { + throw new Error('No tag links found within tags navigation'); + } + + // Verify at least one tag link has the expected format + for (const tagLink of tagLinks) { + const href = await tagLink.getAttribute('href'); + if (href && href.includes('/tags/') && href !== '/tags') { + // Found at least one tag with a slug + return; + } + } + throw new Error('No tag links with proper slug format found in tags navigation'); + } catch (error) { + throw new Error(`Tags navigation not found or invalid: ${error.message}`); + } +}); + +Then('the article should have a post header in an H2 element', async function () { + if (!page) { + throw new Error('Page not initialized. Make sure previous steps are executed first.'); + } + + try { + const h2Header = await page.locator('h2.page-header, h2:has-text("Comparing anker power packs")').first(); + await h2Header.waitFor({ state: 'visible', timeout: 10000 }); + + const headerText = await h2Header.textContent(); + if (!headerText || headerText.trim() === '') { + throw new Error('H2 header is empty'); + } + } catch (error) { + throw new Error(`Post header H2 element not found: ${error.message}`); + } +}); + +Then('the page title should contain the post title and "orionrobots"', async function () { + if (!page) { + throw new Error('Page not initialized. Make sure previous steps are executed first.'); + } + + const title = await page.title(); + if (!title.toLowerCase().includes('comparing anker power packs')) { + throw new Error(`Page title does not contain post title. Found: ${title}`); + } + if (!title.toLowerCase().includes('orionrobots')) { + throw new Error(`Page title does not contain "orionrobots". Found: ${title}`); + } +}); + +Then('the article should have visible images inside the article tag', async function () { + if (!page) { + throw new Error('Page not initialized. Make sure previous steps are executed first.'); + } + + try { + // Look for images within the article element + const articleImages = await page.locator('article img').all(); + + if (articleImages.length === 0) { + throw new Error('No images found inside the article tag'); + } + + // Check that at least one image is visible and has a valid src + let validImagesFound = 0; + for (const img of articleImages) { + try { + await img.waitFor({ state: 'visible', timeout: 5000 }); + const src = await img.getAttribute('src'); + if (src && src.trim() !== '' && !src.includes('data:')) { + validImagesFound++; + } + } catch (e) { + // Image might not be visible, continue checking others + } + } + + if (validImagesFound === 0) { + throw new Error('No visible images with valid src found inside the article tag'); + } + } catch (error) { + throw new Error(`Article images check failed: ${error.message}`); + } +}); + +Then('the article should have a date and author in a div element', async function () { + if (!page) { + throw new Error('Page not initialized. Make sure previous steps are executed first.'); + } + + try { + // Look for the date and author div + const dateDiv = await page.locator('div.date, div:has(time):has(.author)').first(); + await dateDiv.waitFor({ state: 'visible', timeout: 10000 }); + + // Check for date + const timeElement = await page.locator('time').first(); + await timeElement.waitFor({ state: 'visible', timeout: 5000 }); + + // Check for author + const authorElement = await page.locator('.author, div:has-text("Danny Staple")').first(); + await authorElement.waitFor({ state: 'visible', timeout: 5000 }); + } catch (error) { + throw new Error(`Date and author div not found: ${error.message}`); + } +}); + +Then('the page should have a footer with Discord and YouTube links', async function () { + if (!page) { + throw new Error('Page not initialized. Make sure previous steps are executed first.'); + } + + try { + // Look for Discord link in footer + const discordLink = await page.locator('footer a[href*="discord"]').first(); + await discordLink.waitFor({ state: 'visible', timeout: 10000 }); + + // Look for YouTube link in footer + const youtubeLink = await page.locator('footer a[href*="youtube"]').first(); + await youtubeLink.waitFor({ state: 'visible', timeout: 10000 }); + } catch (error) { + throw new Error(`Footer with Discord and YouTube links not found: ${error.message}`); + } +}); + +Then('the page should have the main menu in a nav element at the top', async function () { + if (!page) { + throw new Error('Page not initialized. Make sure previous steps are executed first.'); + } + + try { + // Look for the main navigation at the top + const mainNav = await page.locator('nav.navbar').first(); + await mainNav.waitFor({ state: 'visible', timeout: 10000 }); + + // Check that it contains menu items + const navItems = await page.locator('nav.navbar .nav-link').all(); + if (navItems.length === 0) { + throw new Error('No navigation items found in main nav'); + } + } catch (error) { + throw new Error(`Main navigation menu not found: ${error.message}`); + } +}); + +When('I am in desktop view', async function () { + if (!page) { + throw new Error('Page not initialized. Make sure previous steps are executed first.'); + } + + // Set viewport to desktop size + await page.setViewportSize({ width: 1200, height: 800 }); + + // Wait a moment for layout to adjust + await page.waitForTimeout(1000); +}); + +Then('the images, tables and text should not overflow the article container', async function () { + if (!page) { + throw new Error('Page not initialized. Make sure previous steps are executed first.'); + } + + try { + // Get the article container bounds + const articleContainer = await page.locator('article, #col-main .content').first(); + await articleContainer.waitFor({ state: 'visible', timeout: 10000 }); + + const containerBox = await articleContainer.boundingBox(); + if (!containerBox) { + throw new Error('Could not get article container bounds'); + } + + // Check images don't overflow + const images = await page.locator('article img, #col-main img').all(); + for (const img of images) { + try { + const imgBox = await img.boundingBox(); + if (imgBox && imgBox.x + imgBox.width > containerBox.x + containerBox.width + 10) { + throw new Error(`Image overflows container by ${(imgBox.x + imgBox.width) - (containerBox.x + containerBox.width)} pixels`); + } + } catch (e) { + // Image might not be visible, continue + } + } + + // Check tables don't overflow + const tables = await page.locator('article table, #col-main table').all(); + for (const table of tables) { + try { + const tableBox = await table.boundingBox(); + if (tableBox && tableBox.x + tableBox.width > containerBox.x + containerBox.width + 10) { + throw new Error(`Table overflows container by ${(tableBox.x + tableBox.width) - (containerBox.x + containerBox.width)} pixels`); + } + } catch (e) { + // Table might not be visible, continue + } + } + + // Check for horizontal scrollbars indicating overflow + const hasHorizontalScrollbar = await page.evaluate(() => { + return document.documentElement.scrollWidth > document.documentElement.clientWidth; + }); + + if (hasHorizontalScrollbar) { + throw new Error('Page has horizontal scrollbar indicating content overflow'); + } + } catch (error) { + throw new Error(`Content overflow check failed: ${error.message}`); + } +});