Compare commits
11 Commits
claude/e2e
...
claude/reu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6adf255dea | ||
|
|
47acc85a0e | ||
|
|
d0f8c1d65c | ||
|
|
fa1df001ea | ||
|
|
d0f4844af8 | ||
|
|
3fb920e343 | ||
|
|
49daffafb6 | ||
|
|
be686bb3e0 | ||
|
|
84e1bfebfd | ||
|
|
e3be036d19 | ||
|
|
b5e54a4bdd |
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,26 +1,5 @@
|
|||||||
# Changelog
|
# 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.
|
All notable changes to the peptide-e2e smoke suite will be documented here.
|
||||||
|
|
||||||
## [1.6.0] - 2026-06-16
|
## [1.6.0] - 2026-06-16
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "peptide-e2e",
|
"name": "peptide-e2e",
|
||||||
"version": "1.7.0",
|
"version": "1.5.0",
|
||||||
"description": "Playwright smoke tests for peptiderepo.com (staging + production)",
|
"description": "Playwright smoke tests for peptiderepo.com (staging + production)",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user