feat: initial Playwright smoke suite
20 tests across 5 files covering all plugin surfaces on staging: - Homepage load and PHP fatal detection - Reconstitution Calculator shortcode + math - Peptide Search AI widget + AJAX response - Admin: all plugins active, PSA + PRAutoBlogger settings pages - REST API: prc/v1/presets, peptides/v1/search, compounds, CPT types Staging shortcode pages: /reconstitution-calculator/ - [prc_calculator] /search-peptides/ - [peptide_search] /peptide-directory/ - [peptide_directory] GHA workflow: smoke.yml supports manual dispatch + workflow_call for use as a deploy gate from plugin repos.
This commit is contained in:
40
tests/00-homepage.spec.ts
Executable file
40
tests/00-homepage.spec.ts
Executable file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 00-homepage.spec.ts
|
||||
*
|
||||
* What: Verifies the site root loads with no fatal PHP errors and basic
|
||||
* theme elements are present. Catches theme activation failures,
|
||||
* white screens, and missing plugin-provided content.
|
||||
* Who triggers it: Playwright chromium project (post-auth setup).
|
||||
* Dependencies: peptide-starter-theme, PR Core active.
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Homepage', () => {
|
||||
test('loads with HTTP 200 and no fatal error', async ({ page }) => {
|
||||
const response = await page.goto('/');
|
||||
expect(response?.status()).toBe(200);
|
||||
});
|
||||
|
||||
test('has no PHP fatal error or white screen', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
const body = await page.textContent('body');
|
||||
// PHP fatals produce these strings in non-production WP configs
|
||||
expect(body).not.toContain('Fatal error');
|
||||
expect(body).not.toContain('Parse error');
|
||||
expect(body).not.toContain('Warning: ');
|
||||
// White-screen: body is empty or nearly empty
|
||||
expect((body ?? '').trim().length).toBeGreaterThan(200);
|
||||
});
|
||||
|
||||
test('renders site name in page title', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveTitle(/peptiderepo/i);
|
||||
});
|
||||
|
||||
test('wp-admin bar present (logged-in session)', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
// The auth state from global.setup means we should see the admin bar
|
||||
await expect(page.locator('#wpadminbar')).toBeVisible();
|
||||
});
|
||||
});
|
||||
69
tests/01-reconstitution-calculator.spec.ts
Executable file
69
tests/01-reconstitution-calculator.spec.ts
Executable file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 01-reconstitution-calculator.spec.ts
|
||||
*
|
||||
* What: Smoke-tests the Peptide Reconstitution Calculator (PRC) plugin.
|
||||
* Verifies the shortcode renders, presets load, and basic math produces
|
||||
* a non-zero result — catching broken PHP, failed asset enqueue, or
|
||||
* missing PR Core integration.
|
||||
* Who triggers it: Playwright chromium project (post-auth setup).
|
||||
* Dependencies: peptide-reconstitution-calculator plugin active, [prc_calculator]
|
||||
* shortcode on the page at CALC_PAGE_PATH.
|
||||
*
|
||||
* @see C:\Users\ongte\apps\Peptide Repo CTO\app-maps\peptide-reconstitution-calculator.md
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
const CALC_PATH = process.env.CALC_PAGE_PATH ?? '/reconstitution-calculator/';
|
||||
|
||||
test.describe('Reconstitution Calculator', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const res = await page.goto(CALC_PATH);
|
||||
// Tolerate 200 or 301 (redirect to trailing slash)
|
||||
expect([200, 301]).toContain(res?.status());
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('shortcode widget renders on page', async ({ page }) => {
|
||||
// The shortcode wraps the widget in a container with class prc-calculator
|
||||
// or similar; at minimum a <select> for peptide presets must exist.
|
||||
const widget = page.locator('.prc-calculator, [data-prc], #prc-calculator').first();
|
||||
await expect(widget).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
test('peptide preset dropdown is populated', async ({ page }) => {
|
||||
// At minimum the 8 hardcoded defaults; more if PR Core is active
|
||||
const select = page.locator('select').first();
|
||||
await expect(select).toBeVisible();
|
||||
const options = await select.locator('option').count();
|
||||
expect(options).toBeGreaterThan(1); // >1 means at least one peptide beyond placeholder
|
||||
});
|
||||
|
||||
test('fills inputs and gets a numeric result', async ({ page }) => {
|
||||
// Select BPC-157 or the first non-placeholder option
|
||||
const select = page.locator('select').first();
|
||||
const firstPeptide = select.locator('option').nth(1);
|
||||
const firstValue = await firstPeptide.getAttribute('value');
|
||||
await select.selectOption(firstValue ?? '');
|
||||
|
||||
// Fill reconstitution volume (mL) — use a known valid value
|
||||
const volInput = page.locator('input[name*="recon"], input[placeholder*="mL"], input[type="number"]').first();
|
||||
await volInput.fill('2');
|
||||
|
||||
// Fill desired dose
|
||||
const doseInput = page.locator('input[name*="dose"], input[placeholder*="dose"], input[type="number"]').nth(1);
|
||||
await doseInput.fill('250');
|
||||
|
||||
// Trigger calculation (button or auto-calculate on input)
|
||||
const calcButton = page.locator('button[type="submit"], button:has-text("Calculate")');
|
||||
if (await calcButton.count() > 0) {
|
||||
await calcButton.first().click();
|
||||
}
|
||||
|
||||
// Results container should show a non-zero concentration or injection volume
|
||||
const results = page.locator('.prc-results, .calculator-results, [data-prc-result]').first();
|
||||
await expect(results).toBeVisible({ timeout: 5_000 });
|
||||
const resultText = await results.textContent();
|
||||
expect(resultText).toMatch(/\d+(\.\d+)?/); // contains at least one number
|
||||
});
|
||||
});
|
||||
76
tests/02-peptide-search.spec.ts
Executable file
76
tests/02-peptide-search.spec.ts
Executable file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 02-peptide-search.spec.ts
|
||||
*
|
||||
* What: Smoke-tests the Peptide Search AI (PSA) frontend search widget.
|
||||
* Verifies the [peptide_search] shortcode renders, accepts input,
|
||||
* and returns results for a known-good peptide name. Catches AJAX
|
||||
* failures, nonce issues, and missing PR Core dependency.
|
||||
* Who triggers it: Playwright chromium project (post-auth setup).
|
||||
* Dependencies: peptide-search-ai plugin + PR Core active; at least one
|
||||
* published peptide CPT post (e.g., BPC-157) on staging.
|
||||
*
|
||||
* @see C:\Users\ongte\apps\Peptide Repo CTO\app-maps\peptide-search-ai.md
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
const SEARCH_PATH = process.env.SEARCH_PAGE_PATH ?? '/';
|
||||
|
||||
test.describe('Peptide Search AI', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(SEARCH_PATH);
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('search input widget is present on page', async ({ page }) => {
|
||||
// PSA renders a form with class .psa-search-form or input with data-psa attr
|
||||
const searchInput = page.locator(
|
||||
'.psa-search-form input, input[name*="peptide"], input[placeholder*="peptide" i], [data-psa] input'
|
||||
).first();
|
||||
await expect(searchInput).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
test('searching for a known peptide returns results', async ({ page }) => {
|
||||
const searchInput = page.locator(
|
||||
'.psa-search-form input, input[name*="peptide"], input[placeholder*="peptide" i], [data-psa] input'
|
||||
).first();
|
||||
await expect(searchInput).toBeVisible();
|
||||
|
||||
// Type a peptide that should exist on staging
|
||||
await searchInput.fill('BPC-157');
|
||||
await searchInput.press('Enter');
|
||||
|
||||
// Results container should appear and contain something
|
||||
const results = page.locator('.psa-results, .psa-search-results, [data-psa-results]').first();
|
||||
await expect(results).toBeVisible({ timeout: 15_000 });
|
||||
const text = await results.textContent();
|
||||
expect((text ?? '').trim().length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('AJAX response contains no PHP error', async ({ page }) => {
|
||||
// Intercept the AJAX call and inspect raw response
|
||||
const ajaxResponse = page.waitForResponse(
|
||||
(resp) =>
|
||||
resp.url().includes('admin-ajax.php') &&
|
||||
resp.request().method() === 'POST',
|
||||
{ timeout: 15_000 }
|
||||
);
|
||||
|
||||
const searchInput = page.locator(
|
||||
'.psa-search-form input, input[name*="peptide"], input[placeholder*="peptide" i], [data-psa] input'
|
||||
).first();
|
||||
await searchInput.fill('TB-500');
|
||||
await searchInput.press('Enter');
|
||||
|
||||
try {
|
||||
const resp = await ajaxResponse;
|
||||
const body = await resp.text();
|
||||
expect(body).not.toContain('Fatal error');
|
||||
expect(body).not.toContain('Parse error');
|
||||
// Valid PSA response is JSON; check it starts with { or [
|
||||
expect(body.trim()).toMatch(/^[\[{]/);
|
||||
} catch {
|
||||
// AJAX call may not fire if results cached in page HTML — that's fine
|
||||
}
|
||||
});
|
||||
});
|
||||
100
tests/03-admin-plugins.spec.ts
Executable file
100
tests/03-admin-plugins.spec.ts
Executable file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 03-admin-plugins.spec.ts
|
||||
*
|
||||
* What: Verifies all five peptiderepo plugins are active in wp-admin and
|
||||
* their admin pages load without PHP errors. Catches activation failures
|
||||
* or missing plugin files after a deploy.
|
||||
* Who triggers it: Playwright chromium project (post-auth setup).
|
||||
* Dependencies: wp-admin authenticated session (from global.setup.ts).
|
||||
*
|
||||
* Plugin admin page slugs:
|
||||
* - peptide-search-ai → ?page=peptide-search-ai
|
||||
* - pr-autoblogger → ?page=pr-autoblogger
|
||||
* - peptide-repo-core → ?page=peptide-repo-core (if admin page exists)
|
||||
*
|
||||
* @see C:\Users\ongte\apps\Peptide Repo CTO\app-maps\
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
const ADMIN = '/wp-admin';
|
||||
const PLUGINS_PAGE = `${ADMIN}/plugins.php`;
|
||||
|
||||
// All five plugin slugs as WordPress registers them (directory name)
|
||||
const REQUIRED_PLUGINS = [
|
||||
'peptide-search-ai',
|
||||
'peptide-repo-core',
|
||||
'peptide-reconstitution-calculator',
|
||||
'pr-autoblogger',
|
||||
'peptide-starter-theme', // theme, shown differently — checked separately
|
||||
];
|
||||
|
||||
test.describe('WP Admin — Plugin health', () => {
|
||||
test('plugins page loads without fatal error', async ({ page }) => {
|
||||
const res = await page.goto(PLUGINS_PAGE);
|
||||
expect(res?.status()).toBe(200);
|
||||
await expect(page.locator('#wpbody')).toBeVisible();
|
||||
const body = await page.textContent('body');
|
||||
expect(body).not.toContain('Fatal error');
|
||||
});
|
||||
|
||||
test('all required plugins are active', async ({ page }) => {
|
||||
await page.goto(PLUGINS_PAGE);
|
||||
|
||||
for (const slug of REQUIRED_PLUGINS.filter((s) => s !== 'peptide-starter-theme')) {
|
||||
// Active plugins have tr with class "active" and data-slug matching the plugin
|
||||
const row = page.locator(`tr[data-slug="${slug}"]`);
|
||||
if (await row.count() === 0) {
|
||||
// Plugin may be in a subfolder — try partial match
|
||||
const anyRow = page.locator(`tr[data-plugin*="${slug}"]`);
|
||||
await expect(anyRow).toHaveCount(1, {
|
||||
timeout: 5_000,
|
||||
}).catch(() => {
|
||||
throw new Error(`Plugin row not found for slug: ${slug}`);
|
||||
});
|
||||
await expect(anyRow.first()).toHaveClass(/active/);
|
||||
} else {
|
||||
await expect(row).toHaveClass(/active/, { timeout: 5_000 });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('WP Admin — PSA settings page', () => {
|
||||
test('PSA settings page loads', async ({ page }) => {
|
||||
const res = await page.goto(`${ADMIN}/admin.php?page=peptide-search-ai`);
|
||||
expect(res?.status()).toBe(200);
|
||||
await expect(page.locator('#wpbody')).toBeVisible();
|
||||
const body = await page.textContent('body');
|
||||
expect(body).not.toContain('Fatal error');
|
||||
// Settings form must be present
|
||||
await expect(page.locator('form')).toBeVisible();
|
||||
});
|
||||
|
||||
test('PSA settings page shows cost dashboard section', async ({ page }) => {
|
||||
await page.goto(`${ADMIN}/admin.php?page=peptide-search-ai`);
|
||||
// Cost tracking dashboard renders on the same page
|
||||
const dashboard = page.locator(
|
||||
'.psa-cost-dashboard, [id*="cost"], [class*="usage"], table'
|
||||
).first();
|
||||
await expect(dashboard).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('WP Admin — PRAutoBlogger settings page', () => {
|
||||
test('PRAutoBlogger settings page loads', async ({ page }) => {
|
||||
// Real slug confirmed via live inspection: prautoblogger-settings
|
||||
const res = await page.goto(`${ADMIN}/admin.php?page=prautoblogger-settings`);
|
||||
expect(res?.status()).toBe(200);
|
||||
await expect(page.locator('#wpbody')).toBeVisible();
|
||||
const body = await page.textContent('body');
|
||||
expect(body).not.toContain('Fatal error');
|
||||
// PRAutoBlogger uses a tabbed dashboard, not a plain <form>
|
||||
await expect(page.locator('.prautoblogger-dashboard, h1, h2').first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('WP Admin — Peptide Repo Core admin', () => {
|
||||
test('PR Core menu item exists in wp-admin', async ({ page }) => {
|
||||
await page.goto(`${ADMIN}/`);
|
||||
// PR Core registers the peptide CPT which creates a "Peptides" menu item
|
||||
62
tests/04-rest-api.spec.ts
Executable file
62
tests/04-rest-api.spec.ts
Executable file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 04-rest-api.spec.ts
|
||||
*
|
||||
* What: Verifies public REST API endpoints from all plugins respond with
|
||||
* valid JSON and expected shapes. Catches broken REST registration,
|
||||
* PHP errors in REST handlers, or missing PR Core dependency.
|
||||
* Who triggers it: Playwright chromium project (post-auth setup).
|
||||
* Note: these tests do NOT need auth — they test public endpoints.
|
||||
* Dependencies: All plugins active on staging; at least one published peptide.
|
||||
*
|
||||
* Endpoints tested:
|
||||
* - GET /wp-json/prc/v1/presets — Reconstitution Calculator presets
|
||||
* - GET /wp-json/peptides/v1/search — PSA search REST endpoint
|
||||
* - GET /wp-json/peptide-search-ai/v1/compounds — PSA directory endpoint
|
||||
* - GET /wp-json/wp/v2/peptide — PR Core CPT via WP REST
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('REST API — Reconstitution Calculator', () => {
|
||||
test('GET /wp-json/prc/v1/presets returns array', async ({ request }) => {
|
||||
const res = await request.get('/wp-json/prc/v1/presets');
|
||||
expect(res.status()).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(Array.isArray(body)).toBe(true);
|
||||
expect(body.length).toBeGreaterThan(0);
|
||||
// Each preset should have at minimum a slug and name
|
||||
const first = body[0];
|
||||
expect(first).toHaveProperty('slug');
|
||||
expect(first).toHaveProperty('name');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('REST API — Peptide Search AI', () => {
|
||||
test('GET /wp-json/peptides/v1/search returns valid JSON', async ({ request }) => {
|
||||
const res = await request.get('/wp-json/peptides/v1/search?q=BPC');
|
||||
// 200 (results found) or 404 (no results) are both acceptable; 500 is not
|
||||
expect([200, 404]).toContain(res.status());
|
||||
if (res.status() === 200) {
|
||||
const body = await res.json();
|
||||
expect(body).toHaveProperty('status');
|
||||
expect(body).toHaveProperty('results');
|
||||
}
|
||||
});
|
||||
|
||||
test('GET /wp-json/peptide-search-ai/v1/compounds returns paginated list', async ({ request }) => {
|
||||
const res = await request.get('/wp-json/peptide-search-ai/v1/compounds?fields=basic');
|
||||
expect(res.status()).toBe(200);
|
||||
const body = await res.json();
|
||||
// Response should be array or object with data key
|
||||
expect(body).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('REST API — PR Core (CPT registration)', () => {
|
||||
test('peptide CPT is registered and appears in WP REST index', async ({ request }) => {
|
||||
// Confirmed via live inspection: the peptide CPT does NOT expose via /wp/v2/peptide
|
||||
// (not registered with show_in_rest on the default namespace).
|
||||
// Instead we verify the CPT exists via the WP REST types endpoint.
|
||||
const res = await request.get('/wp-json/wp/v2/types/peptide');
|
||||
// 200 = CPT registered; 404 = CPT missing entirely — both are informative
|
||||
|
||||
Reference in New Issue
Block a user