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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ PHP 8.3 or later and the `ctype`, `intl`, and `mbstring` extensions are required
The core owns snapshot capture, persistence, comparison, and the shared UI. A framework adapter remains responsible
for:

- collecting framework data and converting it into immutable snapshots;
- collecting framework data through `PHPForge\Debug\CollectorInterface`, the single collector contract shared with
provider-owned collectors, and returning the persisted payload from `capture()`;
- exposing the toolbar data endpoints and deciding when a response receives the toolbar;
- defining and publishing assets through its own framework;
- rendering the shared templates with its view component;
Expand Down
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"ui-awesome/html-helper": "^0.7",
"ui-awesome/html-interop": "^0.4",
"ui-awesome/html-mixin": "^0.8.2",
"ui-awesome/html-svg": "^0.6"
"ui-awesome/html-svg": "^0.6",
"php-forge/debug": "^0.1@dev"
},
"require-dev": {
"infection/infection": "^0.35",
Expand All @@ -39,7 +40,9 @@
"phpstan/phpstan-strict-rules": "^2.0.3",
"phpunit/phpunit": "^12.5",
"xepozz/internal-mocker": "^1.4",
"yii2-extensions/scaffold": "^0.2"
"yii2-extensions/scaffold": "^0.2",
"php-forge/vite": "^0.2@dev",
"php-forge/inertia": "^0.2@dev"
},
"autoload": {
"psr-4": {
Expand Down
93 changes: 93 additions & 0 deletions e2e/external-cache.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";

// Start the application-owned consumer fixtures documented in the php-forge/debug capture guide.
for (const [host, baseURL] of [
["Yii2", "http://127.0.0.1:8092"],
["Yii3", "http://127.0.0.1:8093"],
]) {
for (const theme of ["light", "dark"]) {
test(`${host} external cache capture, history, second provider ${theme}`, async ({
page,
request,
}, testInfo) => {
const errors = [];
page.on("pageerror", (error) => errors.push(error.message));
const capture = await request.get(`${baseURL}/?state=dense`);
expect(capture.status()).toBe(200);
const tag = capture.headers()["x-debug-tag"];
expect(tag).toBeTruthy();
const changed = await request.get(`${baseURL}/?state=changed`);
expect(changed.status()).toBe(200);
for (const [id, title] of [
["cache-operations", "Cache operations"],
["independent-second-cache", "Secondary cache"],
]) {
const response = await page.goto(
`${baseURL}/debug/view?tag=${tag}&panel=${id}&yii_debug_theme=${theme}`,
);
expect(response.status()).toBe(200);
await expect(
page.locator(
".yii-debug-nav-link.is-active .yii-debug-nav-link-icon svg",
),
).toHaveCount(id === "cache-operations" ? 1 : 0);
await expect(
page.getByRole("heading", { name: title, level: 1, exact: true }),
).toHaveCount(1);
await expect(
page.getByText("Extensions", { exact: true }),
).toBeVisible();
await expect(
page.getByRole("region", { name: "Operation, Key, Result" }),
).toContainText("historical-key0");
await expect(page.locator("body")).not.toContainText(
"changed-live-key",
);
await expect(page.locator("body")).not.toContainText(
"private contents",
);
const more = page.locator("[data-yii-debug-toggle=cell-more]").last();
await expect(more).toHaveAttribute("aria-expanded", "false");
await more.click();
await expect(more).toHaveAttribute("aria-expanded", "true");
await more.click();
await expect(more).toHaveAttribute("aria-expanded", "false");
const accessibility = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa"])
.analyze();
expect(accessibility.violations).toEqual([]);
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= window.innerWidth,
),
).toBe(true);
await page.screenshot({
path: testInfo.outputPath(`${host}-${id}-${theme}.png`),
fullPage: true,
});
}
const raw = await page.goto(
`${baseURL}/debug/view?tag=${tag}&panel=cache-operations&_fixture_raw=1&yii_debug_theme=${theme}`,
);
expect(raw.status()).toBe(200);
await expect(page.locator("body")).toContainText("historical-key0");
await expect(page.locator("body")).not.toContainText("private contents");
expect(
(
await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa"])
.analyze()
).violations,
).toEqual([]);
const empty = await request.get(`${baseURL}/?state=empty`);
await page.goto(
`${baseURL}/debug/view?tag=${empty.headers()["x-debug-tag"]}&panel=cache-operations&yii_debug_theme=${theme}`,
);
await expect(
page.getByRole("heading", { name: /^No cache operations/ }),
).toBeVisible();
expect(errors).toEqual([]);
});
}
}
69 changes: 69 additions & 0 deletions e2e/portable-panels.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";

import { debugApps } from "./support/environment.js";

for (const app of debugApps()) {
for (const theme of ["light", "dark"]) {
test(`${app.name} portable Vite and Inertia panels ${theme}`, async ({
page,
request,
}, testInfo) => {
const errors = [];
page.on("pageerror", (error) => errors.push(error.message));
const capture = await request.get(`${app.baseURL}/`);
expect(capture.ok()).toBe(true);
const tag = capture.headers()["x-debug-tag"];
expect(tag).toBeTruthy();

for (const panel of ["vite", "inertia"]) {
const response = await page.goto(
`${app.baseURL}/debug/view?tag=${encodeURIComponent(tag)}&panel=${panel}&yii_debug_theme=${theme}`,
);
expect(response.status()).toBe(200);
await expect(
page.getByRole("heading", {
level: 1,
name: panel === "vite" ? "Vite" : "Inertia",
exact: true,
}),
).toHaveCount(1);
await expect(page.locator(".yii-debug-table").first()).toBeVisible();
await expect(
page.locator(".yii-debug-grid-summary").first(),
).not.toBeEmpty();
expect(
await page.locator(".yii-debug-table-wrap").count(),
).toBeGreaterThan(0);
if (panel === "vite") {
await expect(
page.locator(".yii-debug-panel-group").first(),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Build chunks" }).first(),
).toBeVisible();
} else {
const raw = page
.locator("details.yii-debug-disclosure")
.filter({ hasText: "Raw payload" });
await raw.locator("summary").click();
await expect(raw.locator("pre")).toBeVisible();
}
const accessibility = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa"])
.analyze();
expect(accessibility.violations).toEqual([]);
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= window.innerWidth,
),
).toBe(true);
await page.screenshot({
path: testInfo.outputPath(`${panel}-${theme}.png`),
fullPage: true,
});
}
expect(errors).toEqual([]);
});
}
}
96 changes: 96 additions & 0 deletions e2e/provider-events.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";

for (const [host, baseURL] of [
["Yii2", "http://127.0.0.1:8092"],
["Yii3", "http://127.0.0.1:8093"],
]) {
for (const theme of ["light", "dark"]) {
test(`${host} PSR-14 Vite and Inertia capture ${theme}`, async ({
page,
request,
}, testInfo) => {
const errors = [];
page.on("pageerror", (error) => errors.push(error.message));
const capture = await request.get(`${baseURL}/`);
expect(capture.status()).toBe(200);
const tag = capture.headers()["x-debug-tag"];
expect(tag).toBeTruthy();
expect((await request.get(`${baseURL}/?state=changed`)).status()).toBe(
200,
);
for (const panel of ["vite", "inertia"]) {
const response = await page.goto(
`${baseURL}/debug/view?tag=${tag}&panel=${panel}&yii_debug_theme=${theme}`,
);
expect(response.status()).toBe(200);
await expect(
page.getByRole("heading", {
level: 1,
name: panel === "vite" ? "Vite" : "Inertia",
exact: true,
}),
).toBeVisible();
await expect(
page.getByText("Extensions", { exact: true }),
).toBeVisible();
await expect(page.locator("body")).not.toContainText("private-prop");
await expect(page.locator("body")).not.toContainText("ChangedLivePage");
await expect(page.locator("body")).not.toContainText("changed-live.js");
if (panel === "vite") {
await expect(page.locator("body")).toContainText("observed-entry.js");
await expect(
page.getByRole("heading", { name: "Build chunks" }),
).toBeVisible();
} else {
await expect(page.locator("body")).toContainText("ObservedPage");
await expect(page.locator("body")).toContainText("resolved-value");
const more = page.locator("[data-yii-debug-toggle=cell-more]").last();
await expect(more).toHaveAttribute("aria-expanded", "false");
await more.click();
await expect(more).toHaveAttribute("aria-expanded", "true");
const raw = page
.locator("details.yii-debug-disclosure")
.filter({ hasText: "Raw payload" });
await raw.locator("summary").click();
await expect(raw.locator("pre")).toBeVisible();
}
expect(
(
await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa"])
.analyze()
).violations,
).toEqual([]);
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= window.innerWidth,
),
).toBe(true);
await page.screenshot({
path: testInfo.outputPath(`${host}-${panel}-${theme}.png`),
fullPage: true,
});
}
const navigation = await request.get(`${baseURL}/?state=location`);
expect(navigation.status()).toBe(200);
await page.goto(
`${baseURL}/debug/view?tag=${navigation.headers()["x-debug-tag"]}&panel=inertia&yii_debug_theme=${theme}`,
);
await expect(
page.getByRole("heading", { name: /^External location visit/ }),
).toBeVisible();
await expect(page.locator("body")).not.toContainText("Version conflict");
await expect(page.locator("body")).not.toContainText("token=secret");
const empty = await request.get(`${baseURL}/?state=empty`);
expect(empty.status()).toBe(200);
await page.goto(
`${baseURL}/debug/view?tag=${empty.headers()["x-debug-tag"]}&panel=vite&yii_debug_theme=${theme}`,
);
await expect(
page.getByRole("heading", { name: /^No Vite integrations captured/ }),
).toBeVisible();
expect(errors).toEqual([]);
});
}
}
2 changes: 1 addition & 1 deletion resources/assets/dist/css/debug.min.css

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions resources/src/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -2218,7 +2218,7 @@

.yii-debug-disclosure-summary:hover .yii-debug-disclosure-hint,
.yii-debug-disclosure-summary:focus-visible .yii-debug-disclosure-hint {
color: var(--yii-debug-panel-link-hover);
color: var(--yii-debug-panel-link);
}

details[open] > summary .yii-debug-disclosure-hint {
Expand Down Expand Up @@ -2770,13 +2770,15 @@
}
}

.yii-debug-table-vite-overview tbody th {
.yii-debug-table-vite-overview tbody th,
.yii-debug-table-overview tbody th {
width: 200px;
min-width: 160px;
max-width: 200px;
}

.yii-debug-vite-component + .yii-debug-vite-component {
.yii-debug-vite-component + .yii-debug-vite-component,
.yii-debug-panel-group + .yii-debug-panel-group {
margin-top: var(--yii-debug-space-6);
}

Expand Down
22 changes: 17 additions & 5 deletions src/Collector/CollectorCoordinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@
namespace PHPForge\Debug\Collector;

use InvalidArgumentException;
use PHPForge\Debug\CollectorInterface;
use PHPForge\Debug\Exception\Message;
use PHPForge\Debug\Instrumentation\InstrumentationGuard;
use PHPForge\Debug\Storage\{DebugSnapshot, PanelFailure, RequestSummary};
use PHPForge\Debug\Storage\{DebugSnapshot, Json, PanelFailure, RequestSummary};
use Throwable;

use function trim;

/**
* Coordinates validated collectors and isolates their snapshot capture failures.
* Coordinates validated collectors and isolates their capture failures.
*
* Every collector, built-in or provider-owned, implements {@see CollectorInterface} and returns the payload already
* shaped for persistence, so one JSON boundary and one failure-isolation rule apply to all of them.
*/
final class CollectorCoordinator
{
Expand Down Expand Up @@ -70,10 +74,10 @@ public function capture(RequestSummary $summary): DebugSnapshot

foreach ($this->collectors as $id => $collector) {
try {
$snapshot = $collector->capture();
$payload = $collector->capture();

if ($snapshot !== null) {
$panels[$id] = $snapshot->jsonSerialize();
if ($payload !== null) {
$panels[$id] = Json::payload($payload);
}
} catch (Throwable $throwable) {
$failures[$id] = PanelFailure::fromThrowable(
Expand Down Expand Up @@ -102,6 +106,14 @@ public function collector(string $id): CollectorInterface|null
return $this->collectors[$id] ?? null;
}

/**
* @return array<string, CollectorInterface> Registered collectors for host-local instrumentation hooks.
*/
public function collectors(): array
{
return $this->collectors;
}

/**
* Returns whether a collector is registered under the given stable ID.
*
Expand Down
Loading
Loading