Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions e2e/support/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
68 changes: 68 additions & 0 deletions e2e/toolbar-mail.spec.js
Original file line number Diff line number Diff line change
@@ -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.id === "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`);
});
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand Down
8 changes: 8 additions & 0 deletions tools/quality/fixture-contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
93 changes: 88 additions & 5 deletions tools/seed-debug-fixtures.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, array<string, mixed>>
*/
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 <fixture@example.test>',
'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<string, string> $sensitiveFixture
*
Expand Down Expand Up @@ -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, [])
Expand Down Expand Up @@ -818,14 +866,24 @@ 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.");
}

$emptyTag = $states['empty']['tag'] ?? null;
$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;

Expand All @@ -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
Expand All @@ -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',
Expand All @@ -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',
);
Expand Down
Loading