fix(report): group chart cards in the existing grid and stop remounting on scroll (BUG-616, BUG-617)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import test from "node:test";
|
||||
@@ -20,6 +21,9 @@ const SAMPLE_BLOCK: ReportChartBlock = {
|
||||
],
|
||||
};
|
||||
|
||||
const D9_BLOCK: ReportChartBlock = { ...SAMPLE_BLOCK, id: "D9", title: "D9 — Navamsa(婚盘)" };
|
||||
const MOON_BLOCK: ReportChartBlock = { ...SAMPLE_BLOCK, id: "MOON", title: "Moon Chart(月亮参考盘)" };
|
||||
|
||||
const MARKDOWN = [
|
||||
"#### D1 — Rashi Chart(本命盘)",
|
||||
"",
|
||||
@@ -34,6 +38,27 @@ const MARKDOWN = [
|
||||
"```",
|
||||
].join("\n");
|
||||
|
||||
const GOLDEN = JSON.parse(
|
||||
readFileSync(new URL("./fixtures/report-chart-blocks-golden.json", import.meta.url), "utf8"),
|
||||
) as { blocks: ReportChartBlock[] };
|
||||
|
||||
function fence(block: ReportChartBlock): string {
|
||||
return ["```jyotish-chart", JSON.stringify(block), "```"].join("\n");
|
||||
}
|
||||
|
||||
function countClass(markup: string, className: string): number {
|
||||
const escaped = className.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
return (markup.match(new RegExp(`(?:class|className)="[^"]*\\b${escaped}\\b`, "g")) ?? []).length;
|
||||
}
|
||||
|
||||
function cardInnerHtml(markup: string): string[] {
|
||||
const cards: string[] = [];
|
||||
const re = /<div class="personal-report-chart-card">([\s\S]*?)<\/div>/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = re.exec(markup))) cards.push(match[1] ?? "");
|
||||
return cards;
|
||||
}
|
||||
|
||||
test("markdown view draws the North Indian component and skips engine SVG HTML", () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
React.createElement(PersonalReportMarkdownView, { markdown: MARKDOWN }),
|
||||
@@ -46,3 +71,101 @@ test("markdown view draws the North Indian component and skips engine SVG HTML",
|
||||
assert.doesNotMatch(markup, /<script/i);
|
||||
assert.match(markup, new RegExp(planetDisplayLabel("Saturn", 3.4, true)));
|
||||
});
|
||||
|
||||
test("consecutive chart heading+fence pairs become one two-column grid of cards", () => {
|
||||
const markdown = [
|
||||
"#### D1 — Rashi Chart(本命盘)",
|
||||
"",
|
||||
fence(SAMPLE_BLOCK),
|
||||
"",
|
||||
"#### D9 — Navamsa(婚盘)",
|
||||
"",
|
||||
fence(D9_BLOCK),
|
||||
"",
|
||||
"#### Moon Chart(月亮参考盘)",
|
||||
"",
|
||||
"月亮盘画在本命月上,用来对照夜间事件。",
|
||||
"",
|
||||
fence(MOON_BLOCK),
|
||||
"",
|
||||
"#### Shodashvarga 星座总表",
|
||||
"",
|
||||
"| 分盘 | 星座 |",
|
||||
"| --- | --- |",
|
||||
"| D1 | Leo |",
|
||||
].join("\n");
|
||||
const markup = renderToStaticMarkup(
|
||||
React.createElement(PersonalReportMarkdownView, { markdown }),
|
||||
);
|
||||
assert.equal(countClass(markup, "personal-report-chart-grid"), 1);
|
||||
assert.equal(countClass(markup, "is-single"), 0);
|
||||
assert.equal(countClass(markup, "personal-report-chart-card"), 3);
|
||||
const cards = cardInnerHtml(markup);
|
||||
assert.equal(cards.length, 3);
|
||||
assert.match(cards[0] ?? "", /^<h4 class="personal-report-chart-heading">[\s\S]*<\/h4><figure class="personal-report-chart-figure">/);
|
||||
assert.match(cards[1] ?? "", /^<h4 class="personal-report-chart-heading">[\s\S]*<\/h4><figure class="personal-report-chart-figure">/);
|
||||
assert.match(
|
||||
cards[2] ?? "",
|
||||
/^<h4 class="personal-report-chart-heading">[\s\S]*<\/h4><p>月亮盘画在本命月上,用来对照夜间事件。<\/p><figure class="personal-report-chart-figure">/,
|
||||
);
|
||||
const withoutCards = markup.replace(/<div class="personal-report-chart-card">[\s\S]*?<\/div>/g, "");
|
||||
assert.match(withoutCards, /<h4>Shodashvarga 星座总表<\/h4>/);
|
||||
assert.doesNotMatch(withoutCards, /personal-report-chart-card/);
|
||||
});
|
||||
|
||||
test("a single chart pair is wrapped in an is-single grid", () => {
|
||||
const markdown = ["#### D1 — Rashi Chart(本命盘)", "", fence(SAMPLE_BLOCK)].join("\n");
|
||||
const markup = renderToStaticMarkup(
|
||||
React.createElement(PersonalReportMarkdownView, { markdown }),
|
||||
);
|
||||
assert.equal(countClass(markup, "personal-report-chart-grid"), 1);
|
||||
assert.equal(countClass(markup, "is-single"), 1);
|
||||
assert.equal(countClass(markup, "personal-report-chart-card"), 1);
|
||||
});
|
||||
|
||||
test("golden 22 chart figures all sit inside chart cards", () => {
|
||||
const markdown = GOLDEN.blocks.map((block) => [
|
||||
`#### ${block.title}`,
|
||||
"",
|
||||
`<svg viewBox="0 0 420 480"></svg>`,
|
||||
"",
|
||||
fence(block),
|
||||
"",
|
||||
].join("\n")).join("\n");
|
||||
const markup = renderToStaticMarkup(
|
||||
React.createElement(PersonalReportMarkdownView, { markdown }),
|
||||
);
|
||||
assert.equal(countClass(markup, "personal-report-chart-figure"), 22);
|
||||
assert.equal(countClass(markup, "personal-report-chart-card"), 22);
|
||||
const leftover = markup.replace(/<div class="personal-report-chart-card">[\s\S]*?<\/div>/g, "");
|
||||
assert.doesNotMatch(leftover, /personal-report-chart-figure/);
|
||||
});
|
||||
|
||||
test("chart layout CSS does not float personal-report-chart boxes", () => {
|
||||
const css = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8")
|
||||
.replace(/\/\*[\s\S]*?\*\//g, "");
|
||||
for (const chunk of css.split("}")) {
|
||||
if (!chunk.includes("personal-report-chart")) continue;
|
||||
assert.doesNotMatch(chunk, /float\s*:/);
|
||||
assert.doesNotMatch(chunk, /margin-left\s*:\s*-50%/);
|
||||
}
|
||||
});
|
||||
|
||||
test("article markdown trees are memoized and hold no scroll state", () => {
|
||||
const src = readFileSync(
|
||||
new URL("../src/components/personal-report/personal-report-markdown-view.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const viewStart = src.indexOf("export function PersonalReportMarkdownView");
|
||||
const viewEnd = src.indexOf("\nfunction TocList", viewStart);
|
||||
assert.ok(viewStart >= 0 && viewEnd > viewStart);
|
||||
const view = src.slice(viewStart, viewEnd);
|
||||
assert.doesNotMatch(view, /\buseState\b/);
|
||||
const lines = src.split("\n");
|
||||
lines.forEach((line, index) => {
|
||||
if (!line.includes("renderMarkdown(")) return;
|
||||
if (line.includes("function renderMarkdown")) return;
|
||||
const window = lines.slice(Math.max(0, index - 6), index + 1).join("\n");
|
||||
assert.match(window, /useMemo\s*\(/, `renderMarkdown at line ${index + 1} must be inside useMemo`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user