Skip to content

Add BDD tests for article content validation and enable test workflow #263

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 0 additions & 26 deletions .github/workflows/on_pr_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
17 changes: 1 addition & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 20 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -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)
18 changes: 18 additions & 0 deletions tests/staging/features/article-content.feature
Original file line number Diff line number Diff line change
@@ -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
239 changes: 239 additions & 0 deletions tests/staging/step_definitions/website_steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
});