Additive test coverage across server, editor-ext, client and mcp. #192 — AiChatService.stream integration (Section 3, against real Postgres): - new apps/server/test/integration/ai-chat-stream.int-spec.ts drives the real streamText through a seeded ai/test MockLanguageModelV3 and a real Node ServerResponse, covering: onError persists an assistant error record (status 'error' + partial answer + provider cause in metadata); external MCP client closed exactly once on BOTH onFinish and onError; anti-tamper — history is rebuilt from the DB transcript, not from body.messages. #206 — red-team findings (most already fixed+tested in #212): - mdrt-2 (UNFIXED, data loss): turndown.dataloss.test.ts documents that pageBreak / transclusionReference / mention are silently dropped on Markdown export (characterization + it.fails for the desired survive-export contract). - persist-6 (UNFIXED, data loss): persistence-store.spec.ts adds an it.failing documenting that a momentarily-empty live doc overwrites non-empty content (left unfixed — a store-side empty-guard is a behaviour change). #204 — test-strategy plan, highest-priority subset: - Phase 1: mcp-clients.lease.spec.ts covers the external MCP client lease/refcount/eviction lifecycle (leak / premature-close / double-close). - Phase 2 data-integrity pure functions: editor-ext table-utils (transpose/moveRow/convert round-trip) and math tokenizer false-positive guard; client emoji-menu (+ it.fails for the unguarded localStorage JSON.parse bug), sort-cells, normalizeTableColumnWidths; mcp htmlEmbed/ pageBreak markdown data-loss + footnote-diff; server export getInternalLinkPageName extensionless-path bug — FIXED (small/clear) + tested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
127 lines
4.9 KiB
TypeScript
127 lines
4.9 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { normalizeTableColumnWidths } from "./markdown-clipboard";
|
|
|
|
// normalizeTableColumnWidths mutates a DOM subtree (jsdom provides document).
|
|
function root(html: string): HTMLElement {
|
|
const div = document.createElement("div");
|
|
div.innerHTML = html;
|
|
return div;
|
|
}
|
|
|
|
function firstRowColWidths(container: HTMLElement): (string | null)[] {
|
|
const row = container.querySelector("tr");
|
|
return Array.from(row?.children ?? []).map((c) =>
|
|
c.getAttribute("colwidth"),
|
|
);
|
|
}
|
|
|
|
describe("normalizeTableColumnWidths", () => {
|
|
// The core "squash столбцов вставленной таблицы" concern: markdown has no
|
|
// widths, so every pasted table would otherwise render at table-layout:fixed
|
|
// / 100% and squash columns. This stamps an explicit per-column px width.
|
|
it("stamps the default px width on every column when no widths are present", () => {
|
|
const container = root(
|
|
"<table><tbody><tr><td>a</td><td>b</td><td>c</td></tr></tbody></table>",
|
|
);
|
|
normalizeTableColumnWidths(container);
|
|
expect(firstRowColWidths(container)).toEqual(["150", "150", "150"]);
|
|
});
|
|
|
|
it("derives column widths from a colgroup", () => {
|
|
const container = root(
|
|
"<table>" +
|
|
'<colgroup><col style="width:200px"><col style="width:80px"></colgroup>' +
|
|
"<tbody><tr><td>a</td><td>b</td></tr></tbody>" +
|
|
"</table>",
|
|
);
|
|
normalizeTableColumnWidths(container);
|
|
expect(firstRowColWidths(container)).toEqual(["200", "80"]);
|
|
});
|
|
|
|
it("derives column widths from per-cell width attributes", () => {
|
|
const container = root(
|
|
'<table><tbody><tr><td width="120">a</td><td width="90">b</td></tr></tbody></table>',
|
|
);
|
|
normalizeTableColumnWidths(container);
|
|
expect(firstRowColWidths(container)).toEqual(["120", "90"]);
|
|
});
|
|
|
|
it("derives column widths from a cell style:width:px", () => {
|
|
const container = root(
|
|
'<table><tbody><tr><td style="width:140px">a</td><td>b</td></tr></tbody></table>',
|
|
);
|
|
normalizeTableColumnWidths(container);
|
|
// First cell width parsed; a fully-unmeasured column is left untouched
|
|
// (the 100 fallback only fills in NULL gaps inside an otherwise-measured
|
|
// multi-column slice, e.g. a colspan).
|
|
expect(firstRowColWidths(container)).toEqual(["140", null]);
|
|
});
|
|
|
|
it("fills a null gap inside a measured colspanned slice with 100", () => {
|
|
// colgroup gives [200, null]; the single colspan=2 cell spans both, so its
|
|
// slice is [200, null] -> the null is backfilled to 100 => "200,100".
|
|
const container = root(
|
|
"<table>" +
|
|
'<colgroup><col style="width:200px"><col></colgroup>' +
|
|
'<tbody><tr><td colspan="2">merged</td></tr></tbody>' +
|
|
"</table>",
|
|
);
|
|
normalizeTableColumnWidths(container);
|
|
expect(firstRowColWidths(container)).toEqual(["200,100"]);
|
|
});
|
|
|
|
it("splits a measured width across a colspanned cell", () => {
|
|
const container = root(
|
|
'<table><tbody><tr><td colspan="2" width="300">merged</td><td width="100">x</td></tr></tbody></table>',
|
|
);
|
|
normalizeTableColumnWidths(container);
|
|
// 300 / colspan(2) = 150 per underlying column => "150,150" on the merged cell.
|
|
expect(firstRowColWidths(container)).toEqual(["150,150", "100"]);
|
|
});
|
|
|
|
it("falls back to the default width per spanned column when nothing is measurable", () => {
|
|
const container = root(
|
|
'<table><tbody><tr><td colspan="2">merged</td><td>x</td></tr></tbody></table>',
|
|
);
|
|
normalizeTableColumnWidths(container);
|
|
expect(firstRowColWidths(container)).toEqual(["150,150", "150"]);
|
|
});
|
|
|
|
it("leaves cells that already have a colwidth untouched", () => {
|
|
const container = root(
|
|
'<table><tbody><tr><td colwidth="42">a</td><td>b</td></tr></tbody></table>',
|
|
);
|
|
normalizeTableColumnWidths(container);
|
|
expect(firstRowColWidths(container)).toEqual(["42", "150"]);
|
|
});
|
|
|
|
it("normalizes every table in the subtree", () => {
|
|
const container = root(
|
|
"<table><tbody><tr><td>a</td></tr></tbody></table>" +
|
|
"<table><tbody><tr><td>b</td><td>c</td></tr></tbody></table>",
|
|
);
|
|
normalizeTableColumnWidths(container);
|
|
const tables = container.querySelectorAll("table");
|
|
const widths = Array.from(tables).map((t) =>
|
|
Array.from(t.querySelector("tr")!.children).map((c) =>
|
|
c.getAttribute("colwidth"),
|
|
),
|
|
);
|
|
expect(widths).toEqual([["150"], ["150", "150"]]);
|
|
});
|
|
|
|
it("only annotates the first row (column widths are defined once)", () => {
|
|
const container = root(
|
|
"<table><tbody>" +
|
|
"<tr><td>a</td><td>b</td></tr>" +
|
|
"<tr><td>c</td><td>d</td></tr>" +
|
|
"</tbody></table>",
|
|
);
|
|
normalizeTableColumnWidths(container);
|
|
const rows = container.querySelectorAll("tr");
|
|
expect(
|
|
Array.from(rows[1].children).map((c) => c.getAttribute("colwidth")),
|
|
).toEqual([null, null]);
|
|
});
|
|
});
|