Compare commits

...

3 Commits

Author SHA1 Message Date
peptiderepo
d194e2109f feat: add board Generate Now smoke spec (M4, skip-if-absent) — v1.5.0
Adds 06-board-generate-now.spec.ts for the PRAutoBlogger board Generate
Now control shipping in v0.21.0 (PR #166). Two tests:

1. Board page loads and #prab-generate-now button is present with nonce.
2. Kick-off AJAX (prautoblogger_generate_now) returns valid JSON — no PHP
   fatal, no wp_send_json_error from a missing nonce.

SKIP-IF-ABSENT: when #prab-board or #prab-generate-now is absent (M4 not
yet deployed) both tests skip with loud annotations so this e2e PR can
merge before prautoblogger PR #166 per the M2/M3 merge-order binding.

READ-ONLY: never waits for generation to complete; kick_off() is
idempotent (guards against duplicate cron scheduling).

Agent-Session: phase2-admin-m4-20260614
2026-06-14 14:02:29 +00:00
peptiderepo
7fa91ec975 fix(ci): sync package-lock.json with package.json (unblock smoke npm ci) (#17)
Lockfile-only sync (Playwright 1.60.0 + typescript). Unblocks org-wide deploy smoke gate.

Agent-Session: e2e-lockfile-sync
2026-06-13 13:19:43 +07:00
peptiderepo
65c5de61cb test: dossier edit+rerun smoke (M3, skip-if-absent) — v1.4.0 (#15)
QA: APPROVE (companion verdict in qa-reviews/prautoblogger/2026-06-12-6ed639d.md check 10). Skip-if-absent per M3 binding.

Agent-Session: cto-e2e-merge-20260612
2026-06-12 22:04:20 +07:00
5 changed files with 351 additions and 16 deletions

View File

@@ -2,6 +2,30 @@
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.5.0] - 2026-06-14
### Added
- **Board Generate Now smoke (M4, skip-if-absent)** (`06-board-generate-now.spec.ts`).
Asserts the PRAutoBlogger board page loads, `#prab-generate-now` button is present
and carries a nonce, and the kick-off AJAX action returns valid JSON (no PHP fatal).
READ-ONLY: never waits for generation to complete; `kick_off()` is idempotent.
SKIPS with a loud annotation when `#prab-board` or `#prab-generate-now` is absent
(v0.21.0 M4 not yet deployed) so the e2e PR can merge before prautoblogger PR #166
per the M2/M3 merge-order binding. 27 tests total (was 25), delta = 2.
## [1.4.0] - 2026-06-12
### Added
- **Dossier edit + re-run smoke (M3, skip-if-absent)** (`05-dossier-edit.spec.ts`).
Opens a discovered post's dossier and exercises the v0.20.0 edit surface READ-ONLY:
opens/closes the first edit panel (asserts textareas, Save/Re-run buttons, the
fork-preserved + queued-execution copy), asserts every disabled edit affordance
carries a visible reason, and checks the M3 sidebar cards (Run Spend, Models &
Prompts). NEVER clicks Save or any Re-run button (those fork inputs / queue paid
LLM jobs). SKIPS with a loud annotation when `.prab-stage-rerun-footer` is absent
(plugin v0.20.0 not yet deployed) so this PR can merge before prautoblogger PR #161
per the M3 merge-order binding. 25 tests total (was 24), delta = 1.
## [1.3.0] - 2026-06-12 ## [1.3.0] - 2026-06-12
### Fixed ### Fixed

44
package-lock.json generated
View File

@@ -1,25 +1,28 @@
{ {
"name": "peptide-e2e", "name": "peptide-e2e",
"version": "1.0.0", "version": "1.4.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "peptide-e2e", "name": "peptide-e2e",
"version": "1.0.0", "version": "1.4.0",
"dependencies": {
"typescript": "^6.0.3"
},
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.43.0", "@playwright/test": "^1.60.0",
"dotenv": "^16.4.5" "dotenv": "^16.4.5"
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.59.1", "version": "1.60.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
"integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.59.1" "playwright": "1.60.0"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -57,13 +60,13 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.59.1", "version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.59.1" "playwright-core": "1.60.0"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -76,9 +79,9 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.59.1", "version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
@@ -87,6 +90,19 @@
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
},
"node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
} }
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "peptide-e2e", "name": "peptide-e2e",
"version": "1.3.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": {
@@ -11,7 +11,10 @@
"report": "playwright show-report" "report": "playwright show-report"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.43.0", "@playwright/test": "^1.60.0",
"dotenv": "^16.4.5" "dotenv": "^16.4.5"
},
"dependencies": {
"typescript": "^6.0.3"
} }
} }

View File

@@ -0,0 +1,136 @@
/**
* 05-dossier-edit.spec.ts
*
* What: Smoke for the PRAutoBlogger dossier EDIT + RE-RUN surface
* (plugin v0.20.0, Phase 2 admin M3). Read-only: opens/closes the
* edit panel and asserts structure — it NEVER clicks Save or any
* Re-run button (those fork inputs / queue paid LLM jobs).
* Who triggers it: Playwright chromium project (post-auth setup).
* Dependencies: wp-admin authenticated session (from global.setup.ts).
*
* SKIP-IF-ABSENT (M3 binding): this spec targets a surface that may not
* be deployed yet. When the M3 markup (.prab-stage-rerun-footer) is
* absent from the dossier, the test SKIPS with a loud annotation so this
* PR can merge before the plugin PR ships. Once v0.20.0 is live, the
* skip path goes dead and the assertions gate as usual.
*
* Selectors sourced from prautoblogger v0.20.0:
* templates/admin/dossier-stage-section.php (.prab-stage-rerun-footer,
* .prab-edit-toggle, .prab-edit-unavailable, .prab-rerun-from)
* templates/admin/dossier-edit-panel.php (.prab-edit-panel,
* .prab-edit-textarea, .prab-edit-save, .prab-edit-rerun)
* templates/admin/dossier-sidebar-cards.php (.prab-run-spend,
* .prab-models-card)
*
* @see prautoblogger PR #161 (v0.20.0) — the surface under test.
*/
import { test, expect } from '@playwright/test';
const ADMIN = '/wp-admin';
const CANDIDATE_POST_IDS = [925, 930]; // canonical staging posts
test.describe('WP Admin — PRAutoBlogger dossier edit + re-run (M3)', () => {
test('edit affordances render structurally (no mutations)', async ({ page, request }) => {
// ── Post discovery (same defensive strategy as the dossier smoke) ──
let targetPostId: number | null = null;
for (const pid of CANDIDATE_POST_IDS) {
const apiRes = await request.get(`/wp-json/wp/v2/posts/${pid}`);
if (apiRes.ok()) {
targetPostId = pid;
break;
}
}
if (targetPostId === null) {
const listRes = await request.get('/wp-json/wp/v2/posts?per_page=5&orderby=date&order=desc');
if (listRes.ok()) {
const posts: Array<{ id: number }> = await listRes.json();
if (posts.length > 0) {
targetPostId = posts[0].id;
}
}
}
if (targetPostId === null) {
test.info().annotations.push({
type: 'warning',
description:
'[dossier-edit smoke] No post found on this environment — ' +
'cannot exercise the edit surface. Skipping.',
});
test.skip();
return;
}
const res = await page.goto(`${ADMIN}/admin.php?page=prautoblogger-dossier&post_id=${targetPostId}`);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = await page.textContent('body');
expect(body).not.toContain('Fatal error');
expect(body).not.toContain('Invalid plugin page');
// ── SKIP-IF-ABSENT gate: M3 markup not deployed yet ────────────────
const footers = page.locator('.prab-stage-rerun-footer');
const footerCount = await footers.count();
if (footerCount === 0) {
test.info().annotations.push({
type: 'warning',
description:
'[dossier-edit smoke] .prab-stage-rerun-footer absent — the M3 ' +
'edit+rerun surface (plugin v0.20.0) is not deployed on this ' +
'environment, or this post has no stage sections. Skipping per ' +
'the merge-early binding.',
});
test.skip();
return;
}
console.log(`[dossier-edit smoke] Found ${footerCount} stage rerun footers on post id=${targetPostId}`);
// ── Disabled affordances must explain themselves ───────────────────
const unavailable = page.locator('.prab-edit-unavailable');
const unavailableCount = await unavailable.count();
for (let i = 0; i < unavailableCount; i++) {
const reason = (await unavailable.nth(i).textContent())?.trim() ?? '';
expect(reason.length, 'disabled edit affordances must carry a visible reason').toBeGreaterThan(10);
}
// ── Edit panel open/close (read-only — NEVER click save/rerun) ─────
const toggles = page.locator('.prab-edit-toggle');
const toggleCount = await toggles.count();
if (toggleCount === 0) {
test.info().annotations.push({
type: 'info',
description:
'[dossier-edit smoke] No editable stage on this post (no recorded ' +
'request_json yet, or post is published/frozen). Disabled-state ' +
'reasons were asserted; panel interaction not exercisable.',
});
return;
}
const firstToggle = toggles.first();
const panelId = await firstToggle.getAttribute('aria-controls');
expect(panelId).toBeTruthy();
const panel = page.locator(`#${panelId}`);
await firstToggle.click();
await expect(panel).toBeVisible();
await expect(firstToggle).toHaveAttribute('aria-expanded', 'true');
// Panel structure: textareas + save + rerun buttons + fork/queue copy.
expect(await panel.locator('.prab-edit-textarea').count()).toBeGreaterThan(0);
await expect(panel.locator('.prab-edit-save')).toBeVisible();
await expect(panel.locator('.prab-edit-rerun')).toBeVisible();
const copy = (await panel.textContent()) ?? '';
expect(copy).toContain('original is preserved'); // fork copy (guardrail 1)
expect(copy.toLowerCase()).toContain('queued'); // chained-cron copy
// Close it again — leave the page exactly as found.
await firstToggle.click();
await expect(panel).toBeHidden();
// ── M3 sidebar cards present when run data exists ──────────────────
await expect(page.locator('.prab-run-spend')).toBeVisible();
await expect(page.locator('.prab-models-card')).toBeVisible();
});
});

View File

@@ -0,0 +1,156 @@
/**
* 06-board-generate-now.spec.ts
*
* What: Smoke for the PRAutoBlogger board "Generate Now" control (v0.21.0, M4).
* Asserts that the board page renders the generate-now button and that
* clicking it sends the kick-off AJAX request without a PHP fatal.
* Read-only assertions only — this test NEVER waits for article generation
* to complete (that would make LLM API calls in CI).
*
* SKIP-IF-ABSENT (M4 binding): the generate-now surface ships with v0.21.0
* (prautoblogger PR #166). This spec guards against the board container
* (#prab-board) being absent or the generate-now button (#prab-generate-now)
* missing. When those selectors are absent (v0.21.0 not yet deployed), the
* entire test suite skips with a loud annotation so this PR can merge before
* the plugin PR — matching the M2 merge-order binding.
*
* Selectors sourced from prautoblogger v0.21.0:
* templates/admin/board-page.php: #prab-board, #prab-generate-now
* assets/js/board-generate.js: data-nonce, wp.ajax / admin-ajax.php
*
* Who triggers it: Playwright chromium project (post-auth setup).
* Dependencies: wp-admin authenticated session (from global.setup.ts).
*
* @see prautoblogger PR #166 (v0.21.0) — the surface under test.
*/
import { test, expect } from '@playwright/test';
const ADMIN = '/wp-admin';
const BOARD_URL = `${ADMIN}/admin.php?page=prautoblogger-board`;
test.describe('WP Admin — PRAutoBlogger board Generate Now (M4)', () => {
test('board page loads and generate-now button is present (skip-if-absent)', async ({ page }) => {
// Navigate to the board page.
const res = await page.goto(BOARD_URL);
// Must be HTTP 200 (guards menu-registration regression).
expect(res?.status()).toBe(200);
// wp-admin chrome must be present.
await expect(page.locator('#wpbody')).toBeVisible();
// Body must not contain PHP fatal or wp_die error strings.
const body = await page.textContent('body');
expect(body).not.toContain('Fatal error');
expect(body).not.toContain('Invalid plugin page');
// ── SKIP-IF-ABSENT guard ────────────────────────────────────────────────
// The generate-now surface ships with v0.21.0. If the board container or
// the button is absent, the M4 plugin is not yet deployed — skip loudly.
const boardContainer = page.locator('#prab-board');
const boardPresent = await boardContainer.count() > 0;
if ( ! boardPresent ) {
test.info().annotations.push({
type: 'skip',
description:
'[generate-now smoke] #prab-board absent: v0.21.0 (M4) not yet deployed. ' +
'Skipping generate-now assertions — they will gate once the plugin PR ships.',
});
test.skip();
return;
}
// Board container must be visible.
await expect(boardContainer).toBeVisible({ timeout: 10_000 });
// ── Generate-now button (skip-if-absent) ────────────────────────────────
const generateBtn = page.locator('#prab-generate-now');
const btnPresent = await generateBtn.count() > 0;
if ( ! btnPresent ) {
test.info().annotations.push({
type: 'skip',
description:
'[generate-now smoke] #prab-generate-now absent: M4 button not yet in deployed build. ' +
'Skipping button assertions.',
});
test.skip();
return;
}
// Button must be visible and carry a nonce for the AJAX call.
await expect(generateBtn).toBeVisible({ timeout: 5_000 });
const nonce = await generateBtn.getAttribute('data-nonce');
expect(nonce).toBeTruthy();
console.log(
'[generate-now smoke] Generate Now button found with nonce — M4 surface is deployed.'
);
});
test('generate-now AJAX responds with JSON (no fatal) — skip-if-absent', async ({ page, request }) => {
// Navigate first to establish auth cookies for the request context.
await page.goto(BOARD_URL);
const body = await page.textContent('body');
if ( ! body?.includes('prab-board') ) {
test.info().annotations.push({
type: 'skip',
description: '[generate-now smoke] Board container absent — v0.21.0 not deployed. Skipping.',
});
test.skip();
return;
}
const generateBtn = page.locator('#prab-generate-now');
if ( await generateBtn.count() === 0 ) {
test.info().annotations.push({
type: 'skip',
description: '[generate-now smoke] #prab-generate-now absent — skipping AJAX test.',
});
test.skip();
return;
}
// Read the nonce from the button (set by wp_localize_script).
const nonce = await generateBtn.getAttribute('data-nonce') ?? '';
// Fire the kick-off AJAX action (same as board-generate.js would).
// We use the request context to intercept without actually triggering
// the full pipeline — the action is idempotent (kick_off() guards against
// duplicate scheduling with wp_next_scheduled()).
//
// NOTE: We assert the response is valid JSON with a 'success' or 'data'
// key only. We do NOT check that generation actually runs (LLM calls).
const adminAjax = `${ADMIN}/admin-ajax.php`;
const formData = new URLSearchParams({
action: 'prautoblogger_generate_now',
nonce,
});
const ajaxRes = await request.post(adminAjax, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: formData.toString(),
});
// Must be HTTP 200 (not 403/500).
expect(ajaxRes.status()).toBe(200);
// Response must be valid JSON (no PHP fatal/notice dumped as HTML).
const text = await ajaxRes.text();
let parsed: unknown;
try {
parsed = JSON.parse(text);
} catch {
throw new Error(
`[generate-now smoke] AJAX response is not valid JSON — possible PHP fatal.\nRaw: ${text.slice(0, 500)}`
);
}
// WordPress wp_send_json_success / _error always wraps in { success: bool }.
expect(parsed).toHaveProperty('success');
console.log('[generate-now smoke] AJAX response:', JSON.stringify(parsed).slice(0, 200));
});
});