Files
cjk-autospace/demo/e2e.html
T

375 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-Hans">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>聚珍浏览器验收</title>
<link rel="stylesheet" href="../dist/juzhen.css">
<style>
body
{
margin: 0;
padding: 2rem;
color: #172033;
background: #f5f7fb;
font: 16px/1.75 system-ui, sans-serif;
}
h1 { margin-top: 0; font-size: 1.5rem; }
#summary, .panel
{
max-width: 52rem;
margin: 1rem 0;
padding: 1rem 1.25rem;
border: 1px solid #cbd4e2;
border-radius: .5rem;
background: #fff;
}
#summary[data-state="pass"] { border-color: #238636; }
#summary[data-state="fail"] { border-color: #cf222e; }
#results { padding-left: 1.5rem; }
#results li[data-state="pass"] { color: #1a7f37; }
#results li[data-state="fail"] { color: #cf222e; font-weight: 600; }
#fixture
{
max-width: 18em;
font-family: "Noto Serif SC", "Noto Serif CJK SC", serif;
}
#fixture p { text-align: justify; }
code { font-family: ui-monospace, monospace; }
</style>
</head>
<body>
<h1>聚珍浏览器验收</h1>
<section id="summary" data-state="running" aria-live="polite">
<strong id="summary-text">正在运行……</strong>
<button id="run-core" type="button">运行核心验收</button>
<button id="run-observer" type="button" disabled>运行 MutationObserver 验收</button>
<ul id="results"></ul>
</section>
<main id="fixture" class="panel juzhen" lang="zh-Hans">
<p id="spacing-case">中文ABC与TypeScript混排。</p>
<p id="punctuation-case">开明式:中文,测试(括号)与句号。</p>
<p id="ivs-forward"><strong id="author-forward">甲&#x20000;</strong>&#xE0100;A</p>
<p id="ivs-forward-no-orphan" data-juzhen-off="orphan"><strong>甲&#x20000;</strong>&#xE0100;A</p>
<p id="ivs-forward-same-node" data-juzhen-off="orphan">&#x20000;&#xE0100;A</p>
<p id="ivs-forward-same-node-prefix" data-juzhen-off="orphan">甲&#x20000;&#xE0100;A</p>
<p id="ivs-reverse">A<strong id="author-reverse">&#x20000;</strong>&#xE0100;乙</p>
<p id="ivs-line-break" data-juzhen-off="orphan" style="width:2.5em;text-align:justify"><strong>甲&#x20000;</strong>&#xE0100;A</p>
<p id="ivs-nowrap" data-juzhen-off="orphan" style="width:1em;white-space:nowrap"><strong>甲&#x20000;</strong>&#xE0100;A</p>
<p id="ivs-lineedge-open" lang="zh-Hant">「&#x20000;&#xE0100;甲</p>
<p id="ivs-lineedge-close" lang="zh-Hant">甲&#x20000;&#xE0100;」</p>
<p id="observer-case">观察初始内容</p>
</main>
<script src="../dist/juzhen.iife.js"></script>
<script>
(function ()
{
"use strict";
var results = document.getElementById("results");
var summary = document.getElementById("summary");
var summaryText = document.getElementById("summary-text");
var coreButton = document.getElementById("run-core");
var observerButton = document.getElementById("run-observer");
var fixture = document.getElementById("fixture");
var authorForward = document.getElementById("author-forward");
var authorReverse = document.getElementById("author-reverse");
var originalHtml = fixture.innerHTML;
var originalText = fixture.textContent;
var clickCount = 0;
var failures = 0;
function record(name, condition, detail)
{
var item = document.createElement("li");
item.dataset.state = condition ? "pass" : "fail";
item.textContent = (condition ? "PASS " : "FAIL ") + name
+ (detail ? " — " + detail : "");
results.appendChild(item);
if (!condition) { failures += 1; }
}
function hasUnpairedSurrogate(text)
{
for (var i = 0; i < text.length; i += 1)
{
var unit = text.charCodeAt(i);
if (unit >= 0xD800 && unit <= 0xDBFF)
{
if (i + 1 >= text.length) { return true; }
var next = text.charCodeAt(i + 1);
if (next < 0xDC00 || next > 0xDFFF) { return true; }
i += 1;
}
else if (unit >= 0xDC00 && unit <= 0xDFFF)
{
return true;
}
}
return false;
}
function delay()
{
return new Promise(function (resolve)
{
setTimeout(resolve, 30);
});
}
authorForward.addEventListener("click", function () { clickCount += 1; });
authorReverse.addEventListener("click", function () { clickCount += 1; });
coreButton.addEventListener("click", async function ()
{
coreButton.disabled = true;
try
{
// 浏览器扩展可能在 DOMContentLoaded 后异步补写作者属性;以验收
// 按钮真正启动前的 DOM 为 revert 基线,避免把外部并发变更误报为库回归。
originalHtml = fixture.innerHTML;
originalText = fixture.textContent;
var jz = Juzhen.createJuzhen({
scope: { include: "#fixture" },
jiya: { halfWidth: "margin" },
});
jz.render(fixture);
var firstGapCount = fixture.querySelectorAll("jz-hws").length;
var firstCharCount = fixture.querySelectorAll("jz-char").length;
record("已生成中西间隙", firstGapCount > 0, firstGapCount + " 个");
record("已生成标点原子", firstCharCount > 0, firstCharCount + " 个");
record("copy-clean 文本保持", fixture.textContent === originalText);
record("无孤立 UTF-16 代理项", !hasUnpairedSurrogate(fixture.textContent));
var ivs = String.fromCodePoint(0x20000, 0xE0100);
var forward = document.getElementById("ivs-forward");
var forwardNoOrphan = document.getElementById("ivs-forward-no-orphan");
var forwardSameNode = document.getElementById("ivs-forward-same-node");
var forwardSameNodePrefix = document.getElementById("ivs-forward-same-node-prefix");
var reverse = document.getElementById("ivs-reverse");
var forwardCarrier = forward.querySelector("jz-hws");
var carrierKind = forwardCarrier?.getAttribute("data-jz-kind") ?? "none";
record("跨节点 IVS 正向完整", forward.textContent.includes(ivs + "A"));
record(
"关闭 orphan 时 IVS 正向间隙保留",
forwardNoOrphan.querySelectorAll("jz-hws").length === 1,
"gaps=" + forwardNoOrphan.querySelectorAll("jz-hws").length
+ ",HTML=" + forwardNoOrphan.innerHTML,
);
record(
"同一 Text 节点的 IVS 正向间隙保留",
forwardSameNode.querySelectorAll("jz-hws").length === 1,
"gaps=" + forwardSameNode.querySelectorAll("jz-hws").length
+ ",HTML=" + forwardSameNode.innerHTML,
);
record(
"带前导 CJK 的同节点 IVS 正向间隙保留",
forwardSameNodePrefix.querySelectorAll("jz-hws").length === 1,
"gaps=" + forwardSameNodePrefix.querySelectorAll("jz-hws").length
+ ",HTML=" + forwardSameNodePrefix.innerHTML,
);
record("跨节点 IVS 反向完整", reverse.textContent.includes("A" + ivs));
record(
"跨节点 IVS 在间隙载体中保持完整",
carrierKind === "wrap"
&& forwardCarrier?.textContent === ivs
&& forwardCarrier?.hasAttribute("data-jz-cross-node-atom")
&& Array.from(forwardCarrier.childNodes)
.filter(function (node) { return node.nodeType === Node.TEXT_NODE; })
.length === 2,
"forward gaps=" + forward.querySelectorAll("jz-hws").length
+ ",reverse gaps=" + reverse.querySelectorAll("jz-hws").length
+ ",carrier kind=" + carrierKind
+ ",carrier text=" + JSON.stringify(forwardCarrier?.textContent ?? null)
+ ",HTML=" + forward.innerHTML,
);
authorForward.click();
authorReverse.click();
record(
"作者元素身份与监听器保持",
document.getElementById("author-forward") === authorForward
&& document.getElementById("author-reverse") === authorReverse
&& document.querySelectorAll("#author-forward").length === 1
&& document.querySelectorAll("#author-reverse").length === 1
&& clickCount === 2,
);
var gap = fixture.querySelector("jz-hws");
var gapStyle = getComputedStyle(gap);
var gapRatio = parseFloat(gapStyle.marginRight) / parseFloat(gapStyle.fontSize);
record("间隙视觉宽度约为四分之一字", gapRatio >= 0.2 && gapRatio <= 0.3, gapRatio.toFixed(3) + "em");
var selection = window.getSelection();
var selectionRange = document.createRange();
selectionRange.selectNodeContents(forwardNoOrphan);
selection?.removeAllRanges();
selection?.addRange(selectionRange);
var selectionText = selection?.toString() ?? "";
record(
"Selection 文本不含生成断行字符",
selectionText === forwardNoOrphan.textContent,
"zwsp=" + selectionText.includes("\u200B")
+ ",length delta=" + (selectionText.length - forwardNoOrphan.textContent.length),
);
selection?.removeAllRanges();
await delay();
await delay();
var lineBreak = document.getElementById("ivs-line-break");
var lineGap = lineBreak.querySelector("jz-hws");
var lineNext = lineBreak.lastChild;
var lineGapRect = lineGap?.getBoundingClientRect();
var lineNextRange = document.createRange();
if (lineNext?.nodeType === Node.TEXT_NODE)
{
lineNextRange.setStart(lineNext, 0);
lineNextRange.setEnd(lineNext, 1);
}
var lineNextRect = lineNextRange.getBoundingClientRect();
var lineBoxRect = lineBreak.getBoundingClientRect();
var lineGapStyle = lineGap ? getComputedStyle(lineGap) : null;
var lineGapMargin = lineGapStyle
? parseFloat(lineGapStyle.marginRight) / parseFloat(lineGapStyle.fontSize)
: -1;
var nextLineIndent = (lineNextRect.left - lineBoxRect.left)
/ parseFloat(lineGapStyle?.fontSize || "16");
record(
"行末间隙载体留在前行并清零,下一行无缩进",
!!lineGapRect
&& lineNextRect.top > lineGapRect.top + 2
&& lineGap.classList.contains("jz-hws-trim")
&& Math.abs(lineGapMargin) <= 0.01
&& Math.abs(nextLineIndent) <= 0.05,
"trim=" + !!lineGap?.classList.contains("jz-hws-trim")
+ ",margin=" + lineGapMargin.toFixed(3)
+ "em,indent=" + nextLineIndent.toFixed(3) + "em",
);
var nowrapCase = document.getElementById("ivs-nowrap");
var nowrapGap = nowrapCase.querySelector("jz-hws");
var nowrapNext = nowrapCase.lastChild;
var nowrapNextRange = document.createRange();
if (nowrapNext?.nodeType === Node.TEXT_NODE)
{
nowrapNextRange.setStart(nowrapNext, 0);
nowrapNextRange.setEnd(nowrapNext, 1);
}
var nowrapGapRect = nowrapGap?.getBoundingClientRect();
var nowrapNextRect = nowrapNextRange.getBoundingClientRect();
record(
"生成断行机会不突破 nowrap",
!!nowrapGapRect && Math.abs(nowrapGapRect.top - nowrapNextRect.top) <= 2,
);
var ivsOpen = document.getElementById("ivs-lineedge-open").querySelector("jz-char");
var ivsClose = document.getElementById("ivs-lineedge-close").querySelector("jz-char");
record(
"IVS 相邻行首开标点保持行缘半形",
!!ivsOpen?.classList.contains("jz-half-le"),
);
record(
"IVS 相邻行尾闭标点保持行缘半形",
!!ivsClose?.classList.contains("jz-half-le"),
);
var comma = Array.from(fixture.querySelectorAll("jz-char"))
.find(function (item) { return item.textContent === ","; });
var commaStyle = comma ? getComputedStyle(comma) : null;
var commaRatio = comma && commaStyle
? comma.getBoundingClientRect().width / parseFloat(commaStyle.fontSize)
: 0;
record(
"margin 后备的句内逗号为半形",
commaRatio >= 0.45 && commaRatio <= 0.55,
commaRatio.toFixed(3) + "em",
);
jz.render(fixture);
record(
"重复 render 幂等",
fixture.querySelectorAll("jz-hws").length === firstGapCount
&& fixture.querySelectorAll("jz-char").length === firstCharCount,
);
jz.revert(fixture);
var internalComments = [];
var commentWalker = document.createTreeWalker(fixture, NodeFilter.SHOW_COMMENT);
while (commentWalker.nextNode())
{
if (commentWalker.currentNode.textContent?.startsWith("jz-spacing-") === true)
{
internalComments.push(commentWalker.currentNode);
}
}
record(
"revert 清除全部产物",
fixture.querySelectorAll("[data-jz]").length === 0
&& internalComments.length === 0
&& document.getElementById("author-forward") === authorForward
&& document.getElementById("author-reverse") === authorReverse
&& fixture.innerHTML === originalHtml
&& fixture.textContent === originalText,
);
jz.render(fixture);
observerButton.disabled = false;
observerButton.addEventListener("click", async function ()
{
observerButton.disabled = true;
try
{
jz.observe(fixture);
document.getElementById("observer-case").textContent = "观察Observer恢复";
await delay();
await delay();
record(
"MutationObserver 增量重排",
document.getElementById("observer-case").querySelectorAll("jz-hws").length > 0,
);
}
catch (error)
{
record(
"MutationObserver 增量重排",
false,
error instanceof Error ? error.message : String(error),
);
}
finally
{
jz.disconnect();
summary.dataset.state = failures === 0 ? "pass" : "fail";
summaryText.textContent = failures === 0
? "PASS:全部浏览器验收项通过"
: "FAIL:" + failures + " 项浏览器验收失败";
document.title = failures === 0
? "PASS — 聚珍浏览器验收"
: "FAIL — 聚珍浏览器验收";
}
});
}
catch (error)
{
record("验收脚本未抛错", false, error instanceof Error ? error.message : String(error));
}
summary.dataset.state = failures === 0 ? "pass" : "fail";
summaryText.textContent = failures === 0
? "核心验收通过;请运行 MutationObserver 验收"
: "FAIL:" + failures + " 项浏览器验收失败";
document.title = failures === 0
? "CORE PASS — 聚珍浏览器验收"
: "FAIL — 聚珍浏览器验收";
});
summaryText.textContent = "待运行核心验收";
document.title = "READY — 聚珍浏览器验收";
}());
</script>
</body>
</html>