test(integrations/client/packages): batch 2-4 unit coverage + zip-slip guard extraction
Batch 2-4 of the test-strategy rollout. Test-only except one minimal, behaviour-preserving extraction in file.utils.ts. All suites green: server 82 suites/836+1todo, editor-ext 86, mcp 270, client (new files) 86. integrations (server): - file.utils.ts: extract pure `isEntryPathSafe(entryName, targetDir)` from extractZipInternal so the zip-slip/path-traversal guard is unit-testable; call site rerouted, behaviour identical (only a warn-message string merged). - file.utils.zip-safety.spec.ts: traversal/strip/__MACOSX/prefix-confusion cases (mutation-resistant: fails if containment loses the path.sep). - import-formatter / import.utils / table-utils / export utils / import.service extractTitleAndRemoveHeading: pure import/export transforms, Notion/XWiki formatting, table colspan widths (idempotent), slug/link rewriting. client: - safeRedirectPath: open-redirect guard, every reject branch independently. - buildChatMarkdown (fence anti-breakout), label-colors, normalize-label, share tree build, page URL builders, notification time-grouping (fake clock). packages: - editor-ext: deriveFootnoteId golden table, parseHtmlEmbedHeight crafted values, orphan footnote extraction. - mcp: deriveFootnoteId parity (drift guard vs editor-ext), applyTextEdits idempotency + cross-block replaceAll, diffDocs/summarizeChange on reorder. Reviewed (APPROVE): extraction behaviour-preserving, assertions mutation-resistant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { load, CheerioAPI, Cheerio } from 'cheerio';
|
||||
import { normalizeTableColumnWidths } from './table-utils';
|
||||
|
||||
/**
|
||||
* Unit tests for normalizeTableColumnWidths: it writes a `colwidth` attribute
|
||||
* onto the first-row cells of every <table>, deriving widths from a <colgroup>
|
||||
* or the first row, accounting for colspan, and falling back to a default
|
||||
* per-column width (150px) when no pixel widths are present. Re-running the
|
||||
* transform on its own output must be a no-op (idempotent).
|
||||
*/
|
||||
|
||||
const DEFAULT = 150;
|
||||
|
||||
function run(html: string): { $: CheerioAPI; $root: Cheerio<any> } {
|
||||
const $ = load(html);
|
||||
const $root = $.root();
|
||||
normalizeTableColumnWidths($, $root);
|
||||
return { $, $root };
|
||||
}
|
||||
|
||||
function firstRowColwidths($root: Cheerio<any>): (string | undefined)[] {
|
||||
return $root
|
||||
.find('table')
|
||||
.first()
|
||||
.find('> tbody > tr, > thead > tr, > tr')
|
||||
.first()
|
||||
.children('td, th')
|
||||
.map((_, el) => (el as any).attribs?.colwidth)
|
||||
.get();
|
||||
}
|
||||
|
||||
describe('normalizeTableColumnWidths', () => {
|
||||
it('applies colgroup <col width> to the first-row cells', () => {
|
||||
const html =
|
||||
'<table>' +
|
||||
'<colgroup><col width="120"><col width="80"></colgroup>' +
|
||||
'<tbody><tr><td>a</td><td>b</td></tr></tbody>' +
|
||||
'</table>';
|
||||
const { $root } = run(html);
|
||||
|
||||
expect(firstRowColwidths($root)).toEqual(['120', '80']);
|
||||
});
|
||||
|
||||
it('falls back to first-row cell widths when there is no colgroup', () => {
|
||||
const html =
|
||||
'<table><tbody>' +
|
||||
'<tr><td style="width: 200px">a</td><td width="90">b</td></tr>' +
|
||||
'</tbody></table>';
|
||||
const { $root } = run(html);
|
||||
|
||||
expect(firstRowColwidths($root)).toEqual(['200', '90']);
|
||||
});
|
||||
|
||||
it('splits a colspan width across the spanned columns', () => {
|
||||
// colspan=2 with width 100 => each derived column ~50, the spanning cell
|
||||
// then gets the joined slice "50,50".
|
||||
const html =
|
||||
'<table><tbody>' +
|
||||
'<tr><td colspan="2" width="100">merged</td></tr>' +
|
||||
'</tbody></table>';
|
||||
const { $root } = run(html);
|
||||
|
||||
expect(firstRowColwidths($root)).toEqual(['50,50']);
|
||||
});
|
||||
|
||||
it('ignores em/% widths (treated as no width) and applies the default', () => {
|
||||
const html =
|
||||
'<table><tbody>' +
|
||||
'<tr><td style="width: 10em">a</td><td style="width: 50%">b</td></tr>' +
|
||||
'</tbody></table>';
|
||||
const { $root } = run(html);
|
||||
|
||||
expect(firstRowColwidths($root)).toEqual([String(DEFAULT), String(DEFAULT)]);
|
||||
});
|
||||
|
||||
it('applies the default per-column width to a markdown-style table with no widths', () => {
|
||||
const html =
|
||||
'<table><tbody>' +
|
||||
'<tr><td>a</td><td>b</td><td>c</td></tr>' +
|
||||
'<tr><td>1</td><td>2</td><td>3</td></tr>' +
|
||||
'</tbody></table>';
|
||||
const { $root } = run(html);
|
||||
|
||||
expect(firstRowColwidths($root)).toEqual([
|
||||
String(DEFAULT),
|
||||
String(DEFAULT),
|
||||
String(DEFAULT),
|
||||
]);
|
||||
});
|
||||
|
||||
it('is idempotent: re-running on its own output changes nothing', () => {
|
||||
const html =
|
||||
'<table>' +
|
||||
'<colgroup><col width="120"><col width="80"></colgroup>' +
|
||||
'<tbody><tr><td>a</td><td>b</td></tr></tbody>' +
|
||||
'</table>';
|
||||
const { $, $root } = run(html);
|
||||
const afterFirst = $root.html();
|
||||
|
||||
// second pass
|
||||
normalizeTableColumnWidths($, $root);
|
||||
expect($root.html()).toBe(afterFirst);
|
||||
expect(firstRowColwidths($root)).toEqual(['120', '80']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user