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: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Changed
- The prompt that the Copy prompt button of a snippet page copies installs and runs Playwright 1.65, the version the CLI is tested with, which brings Chromium 159. That Chromium does not run `unload` handlers unless the page asks for them with `Permissions-Policy: unload=(self)`, so on a page that does not, Back-Forward-Cache no longer finds an `unload` handler to report.

### Fixed
- Copy as Markdown of the [visualizer](https://webperf-snippets.net/visualizer) exported an audit that has a rating and no value, such as Script-Loading, as a metric: a heading with `undefined` and none of its issues or items, and nothing at all when its unit is a score. The export picks its format as the page picks its view, so the audit comes with its issues and its items, and a result the page shows as JSON is exported as JSON instead of "No issues found".

---

## [2.6.3] — 2026-11-08
Expand Down
3 changes: 2 additions & 1 deletion components/SnippetVisualizer/AuditRenderer.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { renderToStaticMarkup } from "react-dom/server";
import { AuditRenderer } from "./AuditRenderer";
import { SnippetVisualizer, detectRenderer } from "./index";
import { SnippetVisualizer } from "./index";
import { detectRenderer } from "./detectRenderer";

const fixture = (name) =>
JSON.parse(readFileSync(new URL(`../../scripts/fixtures/results/seeded-${name}.json`, import.meta.url), "utf8"));
Expand Down
7 changes: 7 additions & 0 deletions components/SnippetVisualizer/detectRenderer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function detectRenderer(result) {
if (result.script === "Fonts-Preloaded-Loaded-and-used-above-the-fold") return "fonts";
// A metric has a value to rate; an audit such as Script-Loading has a rating and no value
if (result.rating != null && typeof result.value === "number") return "cwv";
if (Array.isArray(result.issues) || Array.isArray(result.items)) return "audit";
return "raw";
}
13 changes: 10 additions & 3 deletions components/SnippetVisualizer/exportMarkdown.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { detectRenderer } from "./detectRenderer";

const RATING_ICON = {
good: "✅",
"needs-improvement": "⚠️",
Expand Down Expand Up @@ -100,8 +102,13 @@ function auditMarkdown(result) {
return lines.join("\n");
}

function rawMarkdown(result) {
return [`## ${result.script ?? "Result"}`, "", "```json", JSON.stringify(result, null, 2), "```"].join("\n");
}

const MARKDOWN = { fonts: fontsMarkdown, cwv: cwvMarkdown, audit: auditMarkdown, raw: rawMarkdown };

// The same view the page renders, so the export never reads a field the result does not have
export function exportMarkdown(result) {
if (result.script === "Fonts-Preloaded-Loaded-and-used-above-the-fold") return fontsMarkdown(result);
if (result.rating != null) return cwvMarkdown(result);
return auditMarkdown(result);
return MARKDOWN[detectRenderer(result)](result);
}
38 changes: 38 additions & 0 deletions components/SnippetVisualizer/exportMarkdown.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, it, expect } from "vitest";
import { readFileSync, readdirSync } from "node:fs";
import { exportMarkdown } from "./exportMarkdown";

const dir = new URL("../../scripts/fixtures/results/", import.meta.url);
const read = (file) => JSON.parse(readFileSync(new URL(file, dir), "utf8"));
const fixture = (name) => read(`seeded-${name}.json`);

describe("exportMarkdown", () => {
it("exports an audit that has a rating and no value as an audit", () => {
const md = exportMarkdown(fixture("Script-Loading"));
expect(md).not.toContain("undefined");
expect(md).toContain("### Items (3)");
expect(md).toContain("third.js");
});

it("exports a score that has a rating and no value without failing", () => {
const md = exportMarkdown({ script: "Demo", status: "ok", unit: "score", rating: "good", issues: [] });
expect(md).toContain("## Demo");
expect(md).not.toContain("undefined");
});

it("exports a metric with its value and its rating", () => {
// The icon of the rating sits between the value and the rating
expect(exportMarkdown(fixture("CLS"))).toMatch(/^## CLS — 0\.0638 \S+ good$/);
});

it("exports a result with neither issues nor items as its JSON, as the page shows it", () => {
const result = { script: "Demo", status: "ok", details: { count: 2 } };
const md = exportMarkdown(result);
expect(md).toContain("```json\n" + JSON.stringify(result, null, 2) + "\n```");
expect(md).not.toContain("No issues found");
});

it.each(readdirSync(dir).filter((file) => file.endsWith(".json")))("exports %s with every value defined", (file) => {
expect(exportMarkdown(read(file))).not.toMatch(/\bundefined\b|\bNaN\b/);
});
});
9 changes: 1 addition & 8 deletions components/SnippetVisualizer/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { FontsRenderer } from "./FontsRenderer";
import { AuditRenderer } from "./AuditRenderer";
import { Icon } from "../Icon";
import { exportMarkdown } from "./exportMarkdown";
import { detectRenderer } from "./detectRenderer";

const PLACEHOLDER = `Paste the return value of any snippet here.

Expand All @@ -14,14 +15,6 @@ and choose "Copy object", then paste it here.

Or run: JSON.stringify(result) and paste the output.`;

export function detectRenderer(result) {
if (result.script === "Fonts-Preloaded-Loaded-and-used-above-the-fold") return "fonts";
// A metric has a value to rate; an audit such as Script-Loading has a rating and no value
if (result.rating != null && typeof result.value === "number") return "cwv";
if (Array.isArray(result.issues) || Array.isArray(result.items)) return "audit";
return "raw";
}

function parseInput(text) {
if (!text.trim()) return null;
try {
Expand Down
Loading