Initial commit: RHINE LAB · ANALYSIS OS:三维界面与动效实验(本仓库不含个人归档索引数据)
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { test } from "node:test";
|
||||
import {
|
||||
loadContent,
|
||||
validateContent,
|
||||
archiveText,
|
||||
} from "./archive-content.mjs";
|
||||
import { escapeHtml } from "../src/html.ts";
|
||||
|
||||
const content = await loadContent();
|
||||
test("all forty downloads match the shared content, including the UTF-8 BOM", async () => {
|
||||
for (const record of content.records) {
|
||||
assert.equal(
|
||||
(
|
||||
await readFile(
|
||||
new URL(
|
||||
`../public/archives/RHINE-LAB-${record.id}.txt`,
|
||||
import.meta.url,
|
||||
),
|
||||
"utf8",
|
||||
)
|
||||
).replace(/\r\n/g, "\n"),
|
||||
archiveText(record),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const invalidCases = [
|
||||
[
|
||||
"missing title",
|
||||
(c) => {
|
||||
delete c.records[0].title;
|
||||
},
|
||||
/records\[0\].title/,
|
||||
],
|
||||
[
|
||||
"blank abstract",
|
||||
(c) => {
|
||||
c.records[0].abstract = " ";
|
||||
},
|
||||
/abstract/,
|
||||
],
|
||||
[
|
||||
"duplicate ID",
|
||||
(c) => {
|
||||
c.records[1].id = "X-001";
|
||||
},
|
||||
/重复编号/,
|
||||
],
|
||||
[
|
||||
"reordered ID",
|
||||
(c) => {
|
||||
[c.records[0], c.records[1]] = [c.records[1], c.records[0]];
|
||||
},
|
||||
/X-001/,
|
||||
],
|
||||
[
|
||||
"unknown category",
|
||||
(c) => {
|
||||
c.records[0].category = "未知";
|
||||
},
|
||||
/未知分类/,
|
||||
],
|
||||
[
|
||||
"empty column",
|
||||
(c) => {
|
||||
// 把某一列的所有档案都挪到另一列 → 该列一条不剩,必须被拒。
|
||||
const victim = c.columns[0];
|
||||
const donor = c.columns[1];
|
||||
for (const record of c.records) if (record.category === victim) record.category = donor;
|
||||
},
|
||||
/至少要有一条档案/,
|
||||
],
|
||||
[
|
||||
"too few records",
|
||||
(c) => {
|
||||
c.records.length = 4;
|
||||
},
|
||||
/至少需要每个分类一条档案/,
|
||||
],
|
||||
[
|
||||
"null record",
|
||||
(c) => {
|
||||
c.records[0] = null;
|
||||
},
|
||||
/必须是档案对象/,
|
||||
],
|
||||
[
|
||||
"empty findings",
|
||||
(c) => {
|
||||
c.records[0].findings = [];
|
||||
},
|
||||
/findings/,
|
||||
],
|
||||
[
|
||||
"non-text findings",
|
||||
(c) => {
|
||||
c.records[0].findings = [42];
|
||||
},
|
||||
/findings/,
|
||||
],
|
||||
[
|
||||
"unsafe URL",
|
||||
(c) => {
|
||||
c.records[0].source = "javascript:alert(1)";
|
||||
},
|
||||
/HTTPS/,
|
||||
],
|
||||
[
|
||||
"invalid URL",
|
||||
(c) => {
|
||||
c.records[0].source = "example.com";
|
||||
},
|
||||
/HTTPS/,
|
||||
],
|
||||
[
|
||||
"duplicate categories",
|
||||
(c) => {
|
||||
c.categories[1] = c.categories[0];
|
||||
},
|
||||
/不能重复/,
|
||||
],
|
||||
[
|
||||
"reserved category",
|
||||
(c) => {
|
||||
c.categories[0] = "全部档案";
|
||||
},
|
||||
/全部档案/,
|
||||
],
|
||||
[
|
||||
"mismatched columns",
|
||||
(c) => {
|
||||
c.columns[0] = "其他";
|
||||
},
|
||||
/相同的五个分类/,
|
||||
],
|
||||
];
|
||||
for (const [name, mutate, error] of invalidCases) {
|
||||
test(`rejects ${name}`, () => {
|
||||
const invalid = structuredClone(content);
|
||||
mutate(invalid);
|
||||
assert.throws(() => validateContent(invalid), error);
|
||||
});
|
||||
}
|
||||
test("accepts independent filter and column order", () => {
|
||||
const edited = structuredClone(content);
|
||||
edited.categories.reverse();
|
||||
assert.equal(validateContent(edited), edited);
|
||||
});
|
||||
test("accepts unequal column counts instead of forcing eight each", () => {
|
||||
const edited = structuredClone(content);
|
||||
const want = [1, 12, 5, 2, 9]; // 合计 29,且每列都不同
|
||||
const pool = [...edited.records];
|
||||
edited.records = [];
|
||||
edited.columns.forEach((name, lane) => {
|
||||
for (let k = 0; k < want[lane]; k += 1) {
|
||||
const source = pool[lane * 3 + (k % 3)] ?? pool[0];
|
||||
edited.records.push({ ...source, category: name });
|
||||
}
|
||||
});
|
||||
edited.records.forEach((record, i) => {
|
||||
record.id = `X-${String(i + 1).padStart(3, "0")}`;
|
||||
});
|
||||
assert.equal(validateContent(edited), edited);
|
||||
assert.equal(edited.records.length, 29);
|
||||
edited.columns.forEach((name, lane) => {
|
||||
assert.equal(edited.records.filter((r) => r.category === name).length, want[lane]);
|
||||
});
|
||||
});
|
||||
|
||||
test("pads archive ids to the total once it exceeds 999", () => {
|
||||
const edited = structuredClone(content);
|
||||
const base = edited.records[0];
|
||||
edited.records = Array.from({ length: 1200 }, (_, i) => ({
|
||||
...structuredClone(base),
|
||||
category: edited.columns[i % edited.columns.length],
|
||||
id: `X-${String(i + 1).padStart(4, "0")}`,
|
||||
}));
|
||||
assert.equal(validateContent(edited), edited);
|
||||
});
|
||||
|
||||
test("plain-text punctuation stays literal in HTML and downloadable text", () => {
|
||||
const title = `<玻璃> & "实验" 'A'`;
|
||||
const edited = structuredClone(content);
|
||||
edited.records[0].title = title;
|
||||
validateContent(edited);
|
||||
assert.equal(
|
||||
escapeHtml(title),
|
||||
"<玻璃> & "实验" 'A'",
|
||||
);
|
||||
assert.ok(archiveText(edited.records[0]).includes(title));
|
||||
});
|
||||
Reference in New Issue
Block a user