From f44521fe7ba1f0ae0a41992bfbe1c48e332dc1fd Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Tue, 22 Sep 2026 18:06:11 -0300 Subject: [PATCH 1/2] test(e2e): cover the Mail chip label after a POST-redirect-GET with a seeded fixture pair. --- e2e/toolbar-mail.spec.js | 68 +++++++++++++++++++++ package.json | 4 +- tools/quality/fixture-contract.json | 8 +++ tools/seed-debug-fixtures.php | 93 +++++++++++++++++++++++++++-- 4 files changed, 166 insertions(+), 7 deletions(-) create mode 100644 e2e/toolbar-mail.spec.js diff --git a/e2e/toolbar-mail.spec.js b/e2e/toolbar-mail.spec.js new file mode 100644 index 0000000..0ac9b8a --- /dev/null +++ b/e2e/toolbar-mail.spec.js @@ -0,0 +1,68 @@ +import { expect, test } from "@playwright/test"; + +import { + collectRuntimeDiagnostics, + expandToolbar, + expectNoRuntimeDiagnostics, + waitForToolbar, +} from "./support/debug-ui.js"; +import { debugApps, fixtureTag } from "./support/environment.js"; + +const postTag = fixtureTag("prgPost"); +const getTag = fixtureTag("prgGet"); +const postTagPattern = new RegExp(`[?&]tag=${postTag}(?:&|$)`); + +for (const app of debugApps()) { + test(`${app.name} Mail chip label opens the capture that sent the mail`, async ({ + page, + }) => { + test.skip( + app.name === "yii3", + "Yii3 has no Mail panel until the Phase 5 port.", + ); + + const diagnostics = collectRuntimeDiagnostics(page); + + // Serve the page toolbar from the seeded GET that follows the mail-sending POST; later AJAX loads are untouched. + await page.route( + /toolbar-data/, + (route) => { + const url = new URL(route.request().url()); + + url.searchParams.set("tag", getTag); + + return route.continue({ url: url.href }); + }, + { times: 1 }, + ); + + const response = await page.goto(app.baseURL, { + waitUntil: "domcontentloaded", + }); + + expect(response?.ok(), `${app.baseURL} must load successfully`).toBe(true); + + const toolbar = await waitForToolbar(page); + await expandToolbar(toolbar); + + const mail = toolbar.getByRole("group", { name: "Mail" }); + const label = mail.locator(".panel-link"); + const badge = mail.locator(".metric"); + + await expect(label).toHaveAttribute("data-debug-url", postTagPattern); + await expect(badge).toHaveAttribute( + "data-debug-url", + (await label.getAttribute("data-debug-url")) ?? "", + ); + + await label.click(); + + const frame = toolbar.locator("iframe[title='Yii debug panel']"); + + await expect(frame).toHaveAttribute("src", postTagPattern); + await expect( + frame.contentFrame().locator(".yii-debug-page").first(), + ).toContainText("Toolbar PRG fixture message 0"); + await expectNoRuntimeDiagnostics(diagnostics, `${app.name} Mail chip`); + }); +} diff --git a/package.json b/package.json index 6fa08a3..0480795 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "check:icons:strict": "node tools/check-icons.mjs --strict", "contact-sheet": "node tools/contact-sheet.mjs", "test:events": "DEBUG_UI_SEED_FIXTURES=0 playwright test e2e/events.spec.js", - "test:e2e": "playwright test e2e/smoke.spec.js e2e/security.spec.js e2e/db-controls.spec.js e2e/history-layout.spec.js", + "test:e2e": "playwright test e2e/smoke.spec.js e2e/security.spec.js e2e/db-controls.spec.js e2e/history-layout.spec.js e2e/toolbar-mail.spec.js", "test:e2e:list": "DEBUG_UI_SEED_FIXTURES=0 playwright test --list", "test:a11y": "playwright test e2e/accessibility.spec.js", "test:security": "playwright test e2e/security.spec.js", @@ -32,7 +32,7 @@ "test:visual:update": "DEBUG_UI_VISUAL_COMPARE=1 playwright test e2e/visual.spec.js --update-snapshots", "test:perf": "DEBUG_UI_SEED_FIXTURES=0 playwright test e2e/large-data.spec.js --project=desktop-1440", "test:perf:live": "playwright test e2e/live-performance.spec.js --project=desktop-1440", - "test:browser": "playwright test e2e/smoke.spec.js e2e/security.spec.js e2e/db-controls.spec.js e2e/history-layout.spec.js e2e/accessibility.spec.js e2e/visual.spec.js e2e/events.spec.js" + "test:browser": "playwright test e2e/smoke.spec.js e2e/security.spec.js e2e/db-controls.spec.js e2e/history-layout.spec.js e2e/toolbar-mail.spec.js e2e/accessibility.spec.js e2e/visual.spec.js e2e/events.spec.js" }, "engines": { "node": "^22.12.0 || >=24.0.0" diff --git a/tools/quality/fixture-contract.json b/tools/quality/fixture-contract.json index 22f8de4..ed760f8 100644 --- a/tools/quality/fixture-contract.json +++ b/tools/quality/fixture-contract.json @@ -20,6 +20,14 @@ "tag": "quality-fixture-dense-v1", "timestamp": 1787529601.0, "rows": 80 + }, + "prgPost": { + "tag": "toolbar-prg-post-v1", + "timestamp": 1787529602.0 + }, + "prgGet": { + "tag": "toolbar-prg-get-v1", + "timestamp": 1787529603.0 } }, "apps": [ diff --git a/tools/seed-debug-fixtures.php b/tools/seed-debug-fixtures.php index 582fbd2..1ebe7f8 100644 --- a/tools/seed-debug-fixtures.php +++ b/tools/seed-debug-fixtures.php @@ -313,6 +313,53 @@ function emptyPanels(float $timestamp): array ); } +/** + * Builds one half of a POST-redirect-GET pair: the POST sends `$mailCount` messages, the GET that follows sends none, + * so the host toolbar must surface the POST's mail as a cross-request chip on the GET. + * + * @return array> + */ +function prgPanels(float $timestamp, string $method, int $mailCount): array +{ + $panels = emptyPanels($timestamp); + + $panels['request'] = RequestSnapshot::capture( + [ + 'statusCode' => $method === 'POST' ? 302 : 200, + 'general' => ['method' => $method], + 'requestHeaders' => [], + 'responseHeaders' => [], + 'requestBody' => [], + 'GET' => [], + 'POST' => [], + 'FILES' => [], + 'COOKIE' => [], + 'SERVER' => [], + ], + )->jsonSerialize(); + $panels['mail'] = [ + 'entries' => rows( + $mailCount, + static fn(int $index): array => [ + 'from' => 'Debug Fixture ', + 'to' => ["prg-recipient-{$index}@example.test"], + 'cc' => [], + 'bcc' => [], + 'replyTo' => [], + 'subject' => "Toolbar PRG fixture message {$index}", + 'body' => "Sent by the POST half of the toolbar PRG fixture {$index}.", + 'headers' => "From: fixture@example.test\nX-Quality-Fixture: prg-{$index}", + 'charset' => 'UTF-8', + 'file' => '', + 'isSuccessful' => true, + 'time' => (int) $timestamp + $index, + ], + ), + ]; + + return normalizePanels($panels); +} + /** * @param array $sensitiveFixture * @@ -729,11 +776,12 @@ function snapshot( float $timestamp, array $panels, int $statusCode, + string $method = 'GET', ): DebugSnapshot { $dbEntries = $panels['db']['entries'] ?? null; $mailEntries = $panels['mail']['entries'] ?? null; $summary = RequestSummary::create($tag) - ->withRequest(url: $url, method: 'GET', ip: '127.0.0.1', time: $timestamp) + ->withRequest(url: $url, method: $method, ip: '127.0.0.1', time: $timestamp) ->withResponse($statusCode) ->withDatabase(is_array($dbEntries) ? count($dbEntries) : 0) ->withMail(is_array($mailEntries) ? count($mailEntries) : 0, []) @@ -818,7 +866,13 @@ function assertPersistedRedaction(string $file, array $contract): void $targets = resolveTargets($contract, $options); $states = $contract['states'] ?? null; -if (!is_array($states) || !is_array($states['empty'] ?? null) || !is_array($states['dense'] ?? null)) { +if ( + !is_array($states) + || !is_array($states['empty'] ?? null) + || !is_array($states['dense'] ?? null) + || !is_array($states['prgPost'] ?? null) + || !is_array($states['prgGet'] ?? null) +) { throw new RuntimeException("Fixture contract field 'states' is incomplete."); } @@ -826,6 +880,10 @@ function assertPersistedRedaction(string $file, array $contract): void $emptyTime = $states['empty']['timestamp'] ?? null; $denseTag = $states['dense']['tag'] ?? null; $denseTime = $states['dense']['timestamp'] ?? null; +$prgPostTag = $states['prgPost']['tag'] ?? null; +$prgPostTime = $states['prgPost']['timestamp'] ?? null; +$prgGetTag = $states['prgGet']['tag'] ?? null; +$prgGetTime = $states['prgGet']['timestamp'] ?? null; $rowCount = $options['rows'] ?? ($states['dense']['rows'] ?? null); $rowCount = is_string($rowCount) && ctype_digit($rowCount) ? (int) $rowCount : $rowCount; @@ -834,6 +892,10 @@ function assertPersistedRedaction(string $file, array $contract): void || !is_numeric($emptyTime) || !is_string($denseTag) || !is_numeric($denseTime) + || !is_string($prgPostTag) + || !is_numeric($prgPostTime) + || !is_string($prgGetTag) + || !is_numeric($prgGetTime) || !is_int($rowCount) || $rowCount < 1 || $rowCount > 5_000 @@ -855,7 +917,7 @@ function assertPersistedRedaction(string $file, array $contract): void ); } - $historySize = max(200, count($manifest->entries) + 2); + $historySize = max(200, count($manifest->entries) + 4); $empty = snapshot( $emptyTag, $target['baseURL'] . '/quality-fixture/empty', @@ -871,17 +933,38 @@ function assertPersistedRedaction(string $file, array $contract): void 200, ); - // Upsert the stable tags without deleting developers' live snapshots. + $prgPost = snapshot( + $prgPostTag, + $target['baseURL'] . '/quality-fixture/prg', + (float) $prgPostTime, + prgPanels((float) $prgPostTime, 'POST', 1), + 302, + 'POST', + ); + $prgGet = snapshot( + $prgGetTag, + $target['baseURL'] . '/quality-fixture/prg', + (float) $prgGetTime, + prgPanels((float) $prgGetTime, 'GET', 0), + 200, + ); + + // Upsert the stable tags without deleting developers' live snapshots. Each write moves its tag to the newest + // manifest position, so the PRG pair stays adjacent with the GET immediately after the POST. $store->writeSnapshot($empty, $historySize); $store->writeSnapshot($dense, $historySize); + $store->writeSnapshot($prgPost, $historySize); + $store->writeSnapshot($prgGet, $historySize); assertPersistedRedaction($target['path'] . "/runtime/debug/{$denseTag}.json", $contract); if (!isset($options['quiet'])) { printf( - "%s: seeded %s and %s (%d dense rows) in %s\n", + "%s: seeded %s, %s, %s and %s (%d dense rows) in %s\n", $target['name'], $emptyTag, $denseTag, + $prgPostTag, + $prgGetTag, $rowCount, $target['path'] . '/runtime/debug', ); From ddf1c8f32e1fbda6252402ad2401b2997b392b15 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Tue, 22 Sep 2026 18:18:25 -0300 Subject: [PATCH 2/2] Apply fixed Coderabbitai review. --- e2e/support/environment.js | 16 +++++++++++----- e2e/toolbar-mail.spec.js | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/e2e/support/environment.js b/e2e/support/environment.js index b26109f..5c6b7f5 100644 --- a/e2e/support/environment.js +++ b/e2e/support/environment.js @@ -80,11 +80,17 @@ export function debugApps() { ? configuredURLs : defaults.map((app) => app.baseURL); - return urls.map((baseURL, index) => ({ - name: - configuredNames?.[index] ?? defaults[index]?.name ?? `app-${index + 1}`, - baseURL: validatedBaseURL(baseURL, index), - })); + // `id` is the fixture-contract identity and never follows DEBUG_UI_APP_NAMES, so specs can branch on it safely; + // `name` is the display label used in test titles. + return urls.map((baseURL, index) => { + const id = defaults[index]?.name ?? `app-${index + 1}`; + + return Object.freeze({ + id, + name: configuredNames?.[index] ?? id, + baseURL: validatedBaseURL(baseURL, index), + }); + }); } export function fixtureTag(state = "dense") { diff --git a/e2e/toolbar-mail.spec.js b/e2e/toolbar-mail.spec.js index 0ac9b8a..1ea250d 100644 --- a/e2e/toolbar-mail.spec.js +++ b/e2e/toolbar-mail.spec.js @@ -17,7 +17,7 @@ for (const app of debugApps()) { page, }) => { test.skip( - app.name === "yii3", + app.id === "yii3", "Yii3 has no Mail panel until the Phase 5 port.", );