Skip to content
Merged
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
191 changes: 191 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
name: Tests

on:
push:
branches:
- main
- 'feature/**'
- 'fix/**'
- 'ci/**'
pull_request:
schedule:
# Nightly: this repository ships by tag, so a framework released after this
# theme was tagged can break it without a single commit landing here.
- cron: '0 5 * * *'
workflow_dispatch:

permissions:
contents: read

jobs:
browser:
name: Browser (framework ${{ matrix.framework }})
runs-on: ubuntu-latest
timeout-minutes: 30

if: github.repository == 'Pollora/theme-buzz'

# locked: the framework a new project installs, which is the skeleton's lock.
# develop: the next framework release, before it ships.
strategy:
fail-fast: false
matrix:
framework: [locked, develop]

steps:
- name: Checkout the theme
uses: actions/checkout@v6
with:
path: theme

- name: Setup DDEV
uses: ddev/github-action-setup-ddev@v1
with:
autostart: false

# Pinned to the beta on purpose: with no version, create-project installs
# the last *stable* skeleton (v13.4.0), which predates block-theme support.
- name: Create the skeleton
run: |
composer create-project pollora/pollora site '^13.32@beta' \
--no-install --no-scripts --no-interaction

- name: Configure DDEV
working-directory: site
run: |
ddev config \
--project-name=buzz-ci \
--project-type=wordpress \
--docroot=public \
--php-version=8.4 \
--webserver-type=apache-fpm \
--database=mariadb:10.11 \
--nodejs-version=22 \
--disable-settings-management

- name: Start DDEV
working-directory: site
run: ddev start

- name: Prepare .env
working-directory: site
run: |
cp .env.example .env
sed -i \
-e 's#^APP_URL=.*#APP_URL=https://buzz-ci.ddev.site#' \
-e 's#^DB_CONNECTION=.*#DB_CONNECTION=mysql#' \
-e 's/^# DB_HOST=.*/DB_HOST=db/' \
-e 's/^# DB_PORT=.*/DB_PORT=3306/' \
-e 's/^# DB_DATABASE=.*/DB_DATABASE=db/' \
-e 's/^# DB_USERNAME=.*/DB_USERNAME=db/' \
-e 's/^# DB_PASSWORD=.*/DB_PASSWORD=db/' \
.env

- name: Install Composer dependencies
working-directory: site
run: ddev composer install --no-interaction --no-progress

- name: Use framework develop
if: matrix.framework == 'develop'
working-directory: site
run: ddev composer require pollora/framework:dev-develop --with-all-dependencies --no-interaction --no-progress

- name: Generate application key
working-directory: site
run: ddev exec php artisan key:generate --no-interaction

# --no-scripts: this only replaces core files, and the skeleton's
# post-autoload-dump hook opens a database connection that has no TTY to
# fall back on.
- name: Use the latest WordPress
working-directory: site
run: |
ddev composer update johnpbloch/wordpress --with-all-dependencies \
--no-scripts --no-interaction --no-progress

- name: Check the WordPress core is complete
working-directory: site
run: |
ddev exec test -f public/cms/wp-includes/l10n.php \
|| { echo "::error::WordPress core is incomplete — wp-includes is missing."; exit 1; }
ddev exec wp core version

- name: Install WordPress
working-directory: site
run: |
ddev exec php artisan pollora:install --install \
--title="Buzz CI" \
--description="Pollora theme-buzz CI" \
--admin-user=admin \
--admin-email=admin@example.com \
--admin-password=buzz-ci \
--locale=en_US \
--public=false \
--no-interaction || true
ddev exec wp core is-installed

# The commit under test, copied as it is: Buzz carries no scaffolding
# placeholders. The directory name matters — the Vite build folder is
# named after it.
- name: Install and build the theme
working-directory: site
run: |
cp -r ../theme themes/buzz
rm -rf themes/buzz/.git themes/buzz/bin themes/buzz/.github
ddev exec bash -c 'cd themes/buzz && npm install --no-audit --no-fund && npm run build'
ddev exec wp theme activate buzz
ddev exec wp rewrite flush

- name: Fetch the framework's browser test harness
working-directory: site
run: |
# develop is aliased (13.x-dev), so dev-develop is not always the first version listed.
ref=$(ddev composer show pollora/framework --format=json \
| jq -r '.versions | if index("dev-develop") then "develop" else .[0] end')
echo "Harness from pollora/framework@$ref"
git clone --quiet --depth 1 --branch "$ref" https://github.com/Pollora/framework.git ../framework
mkdir -p ../framework/tests/e2e/specs/buzz
cp ../theme/bin/tests/e2e/*.spec.ts ../framework/tests/e2e/specs/buzz/

- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: '22'

- name: Install the browser tests
working-directory: framework/tests/e2e
run: |
npm ci
npx playwright install --with-deps chromium

- name: Trust the site's certificate
run: |
if command -v mkcert >/dev/null && [ -f "$(mkcert -CAROOT)/rootCA.pem" ]; then
echo "NODE_EXTRA_CA_CERTS=$(mkcert -CAROOT)/rootCA.pem" >> "$GITHUB_ENV"
else
echo "::warning::No mkcert authority on the runner; certificate checks are off for this disposable site"
echo "NODE_TLS_REJECT_UNAUTHORIZED=0" >> "$GITHUB_ENV"
fi

- name: Browser tests
working-directory: framework/tests/e2e
env:
E2E_HOME_URL: https://buzz-ci.ddev.site
E2E_SITE_DIR: ${{ github.workspace }}/site
E2E_RUN_ID: ${{ github.run_id }}
run: npx playwright test specs/buzz

- name: Upload the browser test report
if: failure()
uses: actions/upload-artifact@v7
with:
name: e2e-report-${{ matrix.framework }}
path: |
framework/tests/e2e/playwright-report
framework/tests/e2e/test-results
if-no-files-found: ignore

- name: Laravel log
if: failure()
working-directory: site
run: tail -n 200 storage/logs/laravel.log || true
1 change: 1 addition & 0 deletions app/Providers/AssetServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public function boot(): void
Asset::add('buzz/script', 'app.js')
->container('theme')
->toFrontend()
->loadInFooter()
->useVite();
}
}
9 changes: 9 additions & 0 deletions bin/tests/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Browser tests

Playwright specs for this theme, run on the framework's E2E harness: CI checks out
`pollora/framework`'s `tests/e2e` at the version the site runs, copies these files to
`specs/buzz/` and runs them there (so they import `../../support/site`).

The spec creates the content it needs with WP-CLI and deletes it afterwards. The theme
must be the active one. `bin/` is stripped by the scaffolder: none of this reaches a
project generated from Buzz.
150 changes: 150 additions & 0 deletions bin/tests/e2e/buzz.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { expect, test } from '@playwright/test';
import { homeUrl, renderedTemplate, runId, wp } from '../../support/site';

/**
* Buzz is a block theme: WordPress resolves templates/*.html itself, so the framework's
* marker names the canvas (`template-canvas`), never a Blade view. What tells the
* templates apart is the body class WordPress writes for the request, and what each
* template's pattern puts on the page.
*/

const tag = `buzz-e2e-${runId}`;
const ids: Record<string, string> = {};
const urls: Record<string, string> = {};

test.beforeAll(() => {
const create = (...args: string[]): string => wp('post', 'create', '--post_status=publish', '--porcelain', ...args);
const link = (id: string): string => new URL(wp('post', 'get', id, '--field=url')).pathname;

const category = wp('term', 'create', 'category', `${tag}-cat`, '--porcelain');
ids.post = create('--post_type=post', `--post_title=Headline ${tag}`, `--post_content=<!-- wp:paragraph --><p>Body ${tag}</p><!-- /wp:paragraph -->`, `--post_category=${category}`);
ids.page = create('--post_type=page', `--post_title=Page ${tag}`, `--post_content=<!-- wp:paragraph --><p>Page body ${tag}</p><!-- /wp:paragraph -->`);
ids.category = category;

urls.post = link(ids.post);
urls.page = link(ids.page);
urls.category = new URL(wp('term', 'get', 'category', category, '--field=url')).pathname;
});

test.afterAll(() => {
for (const key of ['post', 'page']) {
if (ids[key]) {
wp('post', 'delete', ids[key], '--force');
}
}

if (ids.category) {
wp('term', 'delete', 'category', ids.category);
}
});

async function visit(page: import('@playwright/test').Page, path: string, status = 200): Promise<string> {
const errors: string[] = [];
page.on('pageerror', (error) => errors.push(error.message));

const response = await page.goto(homeUrl(path));

expect(response?.status(), `status of ${path}`).toBe(status);
expect(errors, 'no uncaught page error').toEqual([]);

return page.content();
}

test('the theme under test is Buzz, a block theme', () => {
expect(wp('theme', 'list', '--status=active', '--field=name')).toBe('buzz');
expect(wp('eval', 'echo wp_is_block_theme() ? "yes" : "no";')).toBe('yes');
});

test('the masthead and the colophon frame every template', async ({ page }) => {
for (const path of ['/', urls.post, urls.page, '/?s=nothing', '/no-such-page-' + tag]) {
await visit(page, path, path.startsWith('/no-such') ? 404 : 200);
await expect(page.locator('header.buzz-masthead .buzz-nameplate'), path).toBeVisible();
await expect(page.locator('footer.buzz-colophon'), path).toContainText(String(new Date().getFullYear()));
}
});

test('the front page lists the posts', async ({ page }) => {
const html = await visit(page, '/');

expect(renderedTemplate(html)?.template).toBe('template-canvas');
await expect(page.locator('body')).toHaveClass(/\bhome\b/);
await expect(page.locator('main .buzz-index-list')).toContainText(`Headline ${tag}`);
});

test('a post renders its title and body', async ({ page }) => {
await visit(page, urls.post);

await expect(page.locator('body')).toHaveClass(/\bsingle-post\b/);
await expect(page.locator('main h1')).toContainText(`Headline ${tag}`);
await expect(page.locator('main')).toContainText(`Body ${tag}`);
});

test('a page renders its title and body', async ({ page }) => {
await visit(page, urls.page);

await expect(page.locator('body')).toHaveClass(/\bpage\b/);
await expect(page.locator('main h1')).toContainText(`Page ${tag}`);
await expect(page.locator('main')).toContainText(`Page body ${tag}`);
});

test('a category archive lists its posts under its own title', async ({ page }) => {
await visit(page, urls.category);

await expect(page.locator('body')).toHaveClass(/\bcategory\b/);
await expect(page.locator('main .wp-block-query-title')).toContainText(`${tag}-cat`);
await expect(page.locator('main .buzz-index-list')).toContainText(`Headline ${tag}`);
});

test('a search finds a post, and says so when it finds nothing', async ({ page }) => {
await visit(page, `/?s=${encodeURIComponent(`Headline ${tag}`)}`);
await expect(page.locator('body')).toHaveClass(/\bsearch-results\b/);
await expect(page.locator('main .buzz-index-list')).toContainText(`Headline ${tag}`);

await visit(page, `/?s=zzz-no-match-${runId}`);
await expect(page.locator('body')).toHaveClass(/\bsearch-no-results\b/);
await expect(page.locator('main')).toContainText('Nothing here yet.');
});

test("an unknown URL renders the theme's 404, with a 404 status", async ({ page }) => {
await visit(page, `/no-such-page-${tag}`, 404);

await expect(page.locator('body')).toHaveClass(/\berror404\b/);
await expect(page.locator('main h1')).toHaveText('Page missing');
await expect(page.locator('main .buzz-index-list')).toHaveCount(0);
});

test('the design-system pattern is offered to authors', () => {
const inserter = wp('eval', 'echo WP_Block_Patterns_Registry::get_instance()->is_registered("buzz/design-system") ? "yes" : "no";');

expect(inserter).toBe('yes');
});

test.describe('on a phone', () => {
test.use({ viewport: { width: 375, height: 700 } });

test('the navigation collapses behind a button that opens and closes it', async ({ page }) => {
await visit(page, '/');

const open = page.locator('.wp-block-navigation__responsive-container-open');
const menu = page.locator('.wp-block-navigation__responsive-container');

await expect(open).toBeVisible();
await expect(menu).not.toHaveClass(/\bis-menu-open\b/);

await open.click();
await expect(menu).toHaveClass(/\bis-menu-open\b/);
await expect(menu.getByRole('link', { name: `Page ${tag}` })).toBeVisible();

await page.locator('.wp-block-navigation__responsive-container-close').click();
await expect(menu).not.toHaveClass(/\bis-menu-open\b/);
});

test('nothing overflows the viewport sideways', async ({ page }) => {
for (const path of ['/', urls.post]) {
await visit(page, path);

const overflow = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
expect(overflow, `horizontal overflow on ${path}`).toBeLessThanOrEqual(0);
}
});
});
Loading