diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e57d37..712635b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package.json b/package.json index 21bbf9c..95ae492 100755 --- a/package.json +++ b/package.json @@ -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": { diff --git a/tests/07-peptide-news-admin.spec.ts b/tests/07-peptide-news-admin.spec.ts new file mode 100644 index 0000000..612cbe6 --- /dev/null +++ b/tests/07-peptide-news-admin.spec.ts @@ -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:

Peptide News Analytics

+ await expect( + page.locator('h1').filter({ hasText: 'Peptide News Analytics' }) + ).toBeVisible({ timeout: 10_000 }); + + // Summary card row:
+ await expect(page.locator('.pn-summary-cards')).toBeVisible({ timeout: 10_000 }); + + // Date range filter form:
+ 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'); + }); +}); diff --git a/tests/08-pr-vision-admin.spec.ts b/tests/08-pr-vision-admin.spec.ts new file mode 100644 index 0000000..223bcb8 --- /dev/null +++ b/tests/08-pr-vision-admin.spec.ts @@ -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 '

' . esc_html__( 'PR Vision - AI Visibility', 'pr-vision' ) . '

'; + 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: + //
+ await expect(keyCard).toBeVisible({ timeout: 10_000 }); + + // Key source status indicator: + // 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(): + // + // 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'); + }); +});