test(e2e): add smoke specs for Peptide News + PR Vision admin (#20)

* feat(e2e): add Peptide News admin smoke spec (07)

Covers dashboard (HTTP 200, no fatal, heading + summary cards + date filter),
settings (form present), articles, and LLM costs sub-pages.
Skip-if-absent guard on all tests: when plugin is not installed on the
environment, tests skip loudly instead of hard-failing.

Selectors sourced from:
  admin/class-pn-admin-menu.php  (slugs)
  admin/partials/dashboard.php   (.pn-summary-cards, .pn-date-filter, h1)

Agent-Session: claude/e2e-news-vision-admin-20260617

* feat(e2e): add PR Vision admin smoke spec (08)

Covers:
- Dashboard: HTTP 200, no fatal, h1 "PR Vision" heading visible
- Settings: page loads + API-key-manager card (#prv-key-card) present +
  status indicator (#prv-key-source-badge) visible
- Write-only invariant: #prv_api_key input has value="" (stored key never
  output); full page HTML asserted to not contain "sk-or-" prefix
- Costs (pr-vision-costs): loads without fatal
- Call Log (pr-vision-calls): loads without fatal

Skip-if-absent on every test: plugin absent or Settings page (v0.2.0
admin) not yet deployed => loud annotation + test.skip().

Selectors sourced from:
  includes/core/class-prv-admin-page.php      (h1 heading)
  includes/core/class-prv-key-manager-renderer.php (#prv-key-card,
    #prv-key-source-badge, #prv_api_key)
  includes/core/class-prv-settings-page.php   (MENU_SLUG)
  includes/core/class-prv-costs-page.php      (MENU_SLUG)
  includes/core/class-prv-call-log-page.php   (MENU_SLUG)

Agent-Session: claude/e2e-news-vision-admin-20260617

* chore(e2e): CHANGELOG 1.7.0 — Peptide News + PR Vision admin smoke specs

Agent-Session: claude/e2e-news-vision-admin-20260617

* chore(e2e): bump version to 1.7.0

Agent-Session: claude/e2e-news-vision-admin-20260617

* fix(e2e): guard-first ordering in specs 07+08 — skip before Invalid-plugin-page assert

In 07-peptide-news-admin (Articles + LLM Costs) and 08-pr-vision-admin
(Costs + Call Log), the hard expect(body).not.toContain('Invalid plugin page')
assertion fired BEFORE the isPluginAbsent() skip guard. If either plugin is
absent from staging, those tests would hard-fail — gating prod for all apps.

Fix: move isPluginAbsent() + test.skip() above the Invalid-plugin-page assert
in all four affected tests, mirroring the Dashboard/Settings guard-first pattern
already used in the same files.

- 07 Articles: skip guard now before Invalid plugin page check
- 07 LLM Costs: skip guard now before Invalid plugin page check
- 08 Costs: skip guard now before Invalid plugin page check
- 08 Call Log: skip guard now before Invalid plugin page check

Validated: npx playwright test --list lists all 38 tests cleanly.

Agent-Session: engineer-infra-e2e-guardfix-20260617

---------

Co-authored-by: peptiderepo <bot@peptiderepo.com>
This commit was merged in pull request #20.
This commit is contained in:
peptiderepo
2026-06-17 01:41:11 +07:00
committed by GitHub
parent 7c7f46b127
commit 4f574a3986
4 changed files with 374 additions and 1 deletions

View File

@@ -1,5 +1,26 @@
# Changelog
## [1.7.0] - 2026-06-17
### Added
- **Peptide News admin smoke (07)** (`07-peptide-news-admin.spec.ts`).
Covers the Peptide News Plugin admin surfaces: dashboard (HTTP 200, no fatal,
h1 "Peptide News Analytics" heading, `.pn-summary-cards`, `.pn-date-filter`),
settings (form present), articles, and LLM costs sub-pages. Skip-if-absent guard
on all tests using the standard 05/06 merge-early binding pattern. Slugs sourced
from `admin/class-pn-admin-menu.php`.
- **PR Vision admin smoke (08)** (`08-pr-vision-admin.spec.ts`).
Covers the PR Vision plugin admin surfaces: dashboard (HTTP 200, no fatal,
h1 "PR Vision" visible), Settings page (API-key-manager card `#prv-key-card`
present, status badge `#prv-key-source-badge` visible), write-only security
invariant (`#prv_api_key` value="" + full HTML must not contain "sk-or-"),
Costs sub-page (`pr-vision-costs`), and Call Log sub-page (`pr-vision-calls`).
Skip-if-absent at two levels: plugin absent (loud annotation + skip), and
`#prv-key-card` absent (Settings v0.2.0 not yet deployed on environment).
Selectors sourced from `class-prv-admin-page.php`, `class-prv-key-manager-renderer.php`.
All notable changes to the peptide-e2e smoke suite will be documented here.
## [1.6.0] - 2026-06-16

View File

@@ -1,6 +1,6 @@
{
"name": "peptide-e2e",
"version": "1.5.0",
"version": "1.7.0",
"description": "Playwright smoke tests for peptiderepo.com (staging + production)",
"private": true,
"scripts": {

View File

@@ -0,0 +1,144 @@
/**
* 07-peptide-news-admin.spec.ts
*
* What: Smoke tests for the Peptide News Plugin admin surfaces.
* Verifies the dashboard, settings, articles, and cost sub-pages
* load without PHP fatals and checks expected UI elements.
* Guards against activation failures, missing menu registration, or
* PHP fatals introduced by a bad deploy.
*
* Who triggers it: Playwright chromium project (post-auth setup).
* Dependencies: wp-admin authenticated session (from global.setup.ts).
*
* SKIP-IF-ABSENT: When the Peptide News Plugin is not installed/activated on
* this environment, each test detects the absent menu page and skips with a
* loud annotation — consistent with the 05/06 merge-early binding pattern.
*
* Admin menu slugs (sourced from admin/class-pn-admin-menu.php):
* Dashboard -> admin.php?page=peptide-news-dashboard
* Settings -> admin.php?page=peptide-news-settings
* Articles -> admin.php?page=peptide-news-articles
* LLM Costs -> admin.php?page=peptide-news-costs
*
* Selectors sourced from admin/partials/dashboard.php:
* h1 "Peptide News Analytics" - page heading (text match)
* .pn-summary-cards - summary card row
* .pn-date-filter - date range filter form
*
* @see peptiderepo/peptide-news-plugin admin/class-pn-admin-menu.php
* @see peptiderepo/peptide-news-plugin admin/partials/dashboard.php
*/
import { test, expect } from '@playwright/test';
const ADMIN = '/wp-admin';
/**
* Detect the "page not registered" condition WordPress returns when a plugin
* menu slug is unknown. Returns true when the plugin is absent.
*
* WordPress either:
* (a) Shows "You do not have sufficient permissions to access this page."
* (b) Shows a notice that the page cannot be found
* (c) Redirects back to /wp-admin/ stripping the ?page= param
*/
function isPluginAbsent(body: string, url: string): boolean {
return (
body.includes('Invalid plugin page') ||
body.includes('do not have sufficient permissions') ||
body.includes('The page you are looking for') ||
( url.includes('/wp-admin/') && ! url.includes('page=peptide-news') )
);
}
/** Push a loud skip annotation and call test.skip(). */
function skipMissing(): void {
test.info().annotations.push({
type: 'skip',
description:
'[peptide-news smoke] Peptide News Plugin is not installed or activated ' +
'on this environment. Skipping per the merge-early binding.',
});
test.skip();
}
test.describe('WP Admin - Peptide News Plugin dashboard', () => {
test('dashboard page loads (HTTP 200, no PHP fatal)', async ({ page }) => {
const res = await page.goto(`${ADMIN}/admin.php?page=peptide-news-dashboard`);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
});
test('dashboard renders analytics heading, summary cards, and date filter', async ({ page }) => {
await page.goto(`${ADMIN}/admin.php?page=peptide-news-dashboard`);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
// Heading from admin/partials/dashboard.php: <h1>Peptide News Analytics</h1>
await expect(
page.locator('h1').filter({ hasText: 'Peptide News Analytics' })
).toBeVisible({ timeout: 10_000 });
// Summary card row: <div class="pn-summary-cards">
await expect(page.locator('.pn-summary-cards')).toBeVisible({ timeout: 10_000 });
// Date range filter form: <form class="pn-date-filter">
await expect(page.locator('.pn-date-filter')).toBeVisible({ timeout: 10_000 });
});
});
test.describe('WP Admin - Peptide News Plugin settings page', () => {
test('settings page loads (HTTP 200, no fatal, settings form present)', async ({ page }) => {
const res = await page.goto(`${ADMIN}/admin.php?page=peptide-news-settings`);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
// Every settings page rendered by class-pn-admin-settings-page.php contains a form
await expect(page.locator('form').first()).toBeVisible({ timeout: 10_000 });
});
});
test.describe('WP Admin - Peptide News Plugin articles page', () => {
test('articles page loads without fatal error', async ({ page }) => {
const res = await page.goto(`${ADMIN}/admin.php?page=peptide-news-articles`);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
expect(body).not.toContain('Invalid plugin page');
});
});
test.describe('WP Admin - Peptide News Plugin LLM costs page', () => {
test('costs page loads without fatal error', async ({ page }) => {
const res = await page.goto(`${ADMIN}/admin.php?page=peptide-news-costs`);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
expect(body).not.toContain('Invalid plugin page');
});
});

View File

@@ -0,0 +1,208 @@
/**
* 08-pr-vision-admin.spec.ts
*
* What: Smoke tests for the PR Vision plugin admin surfaces.
* Covers the dashboard, Settings (API-key-manager card + write-only
* invariant), Costs, and Call Log sub-pages. Guards against PHP fatals,
* missing menu registration, and key leakage regressions.
*
* Who triggers it: Playwright chromium project (post-auth setup).
* Dependencies: wp-admin authenticated session (from global.setup.ts).
*
* SKIP-IF-ABSENT: When PR Vision is not installed/activated on this
* environment, each test detects the absent page and skips with a loud
* annotation - consistent with the 05/06 merge-early binding pattern.
*
* Admin menu slugs (sourced from includes/core/class-prv-admin-page.php
* and includes/core/class-prv-settings-page.php,
* includes/core/class-prv-costs-page.php,
* includes/core/class-prv-call-log-page.php):
* Dashboard -> admin.php?page=pr-vision (MENU_SLUG = 'pr-vision')
* Settings -> admin.php?page=pr-vision-settings (MENU_SLUG = 'pr-vision-settings')
* Costs -> admin.php?page=pr-vision-costs (MENU_SLUG = 'pr-vision-costs')
* Call Log -> admin.php?page=pr-vision-calls (MENU_SLUG = 'pr-vision-calls')
*
* Selectors sourced from includes/core/class-prv-admin-page.php:
* h1 "PR Vision - AI Visibility" - dashboard page heading
*
* Selectors sourced from includes/core/class-prv-key-manager-renderer.php:
* #prv-key-card - the API-key-manager card container
* #prv-key-source-badge - key source status indicator span
* #prv_api_key - password input (must render empty - write-only invariant)
*
* Write-only invariant (security):
* The stored OpenRouter key must NEVER appear in page source.
* #prv_api_key renders with value="" on every page load regardless of
* whether a key is stored. This spec asserts the empty value and asserts
* that known API key prefixes ("sk-or-") are absent from the full body.
*
* @see peptiderepo/pr-vision includes/core/class-prv-admin-page.php
* @see peptiderepo/pr-vision includes/core/class-prv-settings-page.php
* @see peptiderepo/pr-vision includes/core/class-prv-key-manager-renderer.php
*/
import { test, expect } from '@playwright/test';
const ADMIN = '/wp-admin';
/**
* Detect the "page not registered" condition WordPress returns when a plugin
* menu slug is unknown. Returns true when the plugin is absent.
*/
function isPluginAbsent(body: string, url: string): boolean {
return (
body.includes('Invalid plugin page') ||
body.includes('do not have sufficient permissions') ||
body.includes('The page you are looking for') ||
( url.includes('/wp-admin/') && ! url.includes('page=pr-vision') )
);
}
/** Push a loud skip annotation and call test.skip(). */
function skipMissing(): void {
test.info().annotations.push({
type: 'skip',
description:
'[pr-vision smoke] PR Vision plugin is not installed or activated ' +
'on this environment. Skipping per the merge-early binding.',
});
test.skip();
}
test.describe('WP Admin - PR Vision dashboard', () => {
test('dashboard page loads (HTTP 200, no PHP fatal)', async ({ page }) => {
const res = await page.goto(`${ADMIN}/admin.php?page=pr-vision`);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
// Heading from class-prv-admin-page.php render_page():
// echo '<h1>' . esc_html__( 'PR Vision - AI Visibility', 'pr-vision' ) . '</h1>';
await expect(
page.locator('h1').filter({ hasText: 'PR Vision' })
).toBeVisible({ timeout: 10_000 });
});
});
test.describe('WP Admin - PR Vision Settings page (API-key-manager)', () => {
test('settings page loads (HTTP 200, no PHP fatal)', async ({ page }) => {
const res = await page.goto(`${ADMIN}/admin.php?page=pr-vision-settings`);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
});
test('API-key-manager card is present and status indicator visible', async ({ page }) => {
await page.goto(`${ADMIN}/admin.php?page=pr-vision-settings`);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
// ── SKIP-IF-ABSENT guard: Settings page may not have key-card if Settings
// subpage (v0.2.0) is not yet deployed on this environment.
const keyCard = page.locator('#prv-key-card');
const cardPresent = await keyCard.count() > 0;
if ( ! cardPresent ) {
test.info().annotations.push({
type: 'skip',
description:
'[pr-vision smoke] #prv-key-card absent: PR Vision Settings page ' +
'(v0.2.0 admin) is not deployed on this environment. Skipping key-manager assertions.',
});
test.skip();
return;
}
// API-key-manager card container from class-prv-key-manager-renderer.php:
// <div class="prv-card" id="prv-key-card">
await expect(keyCard).toBeVisible({ timeout: 10_000 });
// Key source status indicator: <span ... id="prv-key-source-badge">
// Shows one of: "Set via wp-config", "Set via admin", "Not set"
await expect(page.locator('#prv-key-source-badge')).toBeVisible({ timeout: 10_000 });
});
test('password input is present and renders EMPTY (write-only invariant)', async ({ page }) => {
await page.goto(`${ADMIN}/admin.php?page=pr-vision-settings`);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
const keyCard = page.locator('#prv-key-card');
const cardPresent = await keyCard.count() > 0;
if ( ! cardPresent ) {
test.info().annotations.push({
type: 'skip',
description:
'[pr-vision smoke] #prv-key-card absent: skipping write-only invariant check.',
});
test.skip();
return;
}
// Password input from class-prv-key-manager-renderer.php render_key_form():
// <input type="password" id="prv_api_key" name="prv_api_key" ... value="" ...>
// The renderer explicitly passes value="" -- the stored key is NEVER output.
const keyInput = page.locator('#prv_api_key');
// When the key source is SOURCE_CONSTANT the input is disabled but still rendered.
// In all cases it must be present in the DOM.
await expect(keyInput).toBeAttached({ timeout: 10_000 });
// Write-only invariant: value attribute must be empty string, not the stored key.
const inputValue = await keyInput.getAttribute('value');
expect(inputValue ?? '').toBe('');
// Security check: known OpenRouter key prefixes must not appear anywhere in
// the page HTML -- guards against accidental server-side leakage.
const html = await page.content();
expect(html).not.toContain('sk-or-');
});
});
test.describe('WP Admin - PR Vision Costs page', () => {
test('costs page loads without fatal error', async ({ page }) => {
const res = await page.goto(`${ADMIN}/admin.php?page=pr-vision-costs`);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
expect(body).not.toContain('Invalid plugin page');
});
});
test.describe('WP Admin - PR Vision Call Log page', () => {
test('call log page loads without fatal error', async ({ page }) => {
const res = await page.goto(`${ADMIN}/admin.php?page=pr-vision-calls`);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
expect(body).not.toContain('Invalid plugin page');
});
});