476 lines
16 KiB
TypeScript
476 lines
16 KiB
TypeScript
import assert from "node:assert/strict";
|
|||
|
|
import {
|
||
|
|
HoverIntent,
|
||
|
|
sameScope,
|
||
|
|
processorMatches,
|
||
|
|
preparationQueue,
|
||
|
|
selectSuccessfulAnnotationJobs,
|
||
|
|
mergeCachedAnnotationEntries,
|
||
|
|
selectAnnotationRetryJobs,
|
||
|
|
annotationRetryStatus,
|
||
|
|
explicitAnnotationRetry,
|
||
|
|
} from "../src/work.js";
|
||
|
|
const oldProcessor = { key: "processor-v1:scope" };
|
||
|
|
assert(processorMatches(oldProcessor, "processor-v1"));
|
||
|
|
assert(
|
||
|
|
!processorMatches(oldProcessor, "processor-v2"),
|
||
|
|
"A checked processor edit invalidates saved results even when document and feature settings are unchanged",
|
||
|
|
);
|
||
|
|
assert(!processorMatches({ key: "processor-v10:scope" }, "processor-v1"));
|
||
|
|
const feature = {
|
||
|
|
processor: "definition",
|
||
|
|
generation: "focus",
|
||
|
|
scope: "word",
|
||
|
|
reveal: "hover",
|
||
|
|
configRevision: 2,
|
||
|
|
enabled: true,
|
||
|
|
};
|
||
|
|
assert(
|
||
|
|
sameScope(
|
||
|
|
{ sourceRevision: "s", configuration: "compute", featureRevision: 2, start: 0, end: 2, status: "ready" },
|
||
|
|
"s",
|
||
|
|
"compute",
|
||
|
|
{
|
||
|
|
start: 0,
|
||
|
|
end: 2,
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
assert(
|
||
|
|
!sameScope(
|
||
|
|
{ sourceRevision: "s", configuration: "different", featureRevision: 1, start: 0, end: 2, status: "ready" },
|
||
|
|
"s",
|
||
|
|
"compute",
|
||
|
|
{
|
||
|
|
start: 0,
|
||
|
|
end: 2,
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
const intent = new HoverIntent(10);
|
||
|
|
let calls: string[] = [];
|
||
|
|
intent.schedule(() => calls.push("old"));
|
||
|
|
intent.schedule(() => calls.push("new"));
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, 30));
|
||
|
|
assert.deepEqual(calls, ["new"]);
|
||
|
|
intent.schedule(() => calls.push("left"));
|
||
|
|
intent.cancel();
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, 30));
|
||
|
|
assert.deepEqual(calls, ["new"]);
|
||
|
|
intent.schedule(() => calls.push("unmounted"));
|
||
|
|
intent.cancel();
|
||
|
|
console.log("PASS heavy hover intent, cancellation, scopes and stale cache guards");
|
||
|
|
|
||
|
|
const scope = (key: string, configuration: string, start: number, local = false) => ({
|
||
|
|
key,
|
||
|
|
configuration,
|
||
|
|
range: { start, end: start + 10 },
|
||
|
|
local,
|
||
|
|
});
|
||
|
|
const empty = new Set<string>();
|
||
|
|
const a = scope("a", "pinyin", 0),
|
||
|
|
b = scope("b", "pinyin", 10),
|
||
|
|
c = scope("c", "pinyin", 20);
|
||
|
|
assert.deepEqual(
|
||
|
|
preparationQueue([a, b, c], { start: 0, end: 30 }, 0, empty, empty).admit,
|
||
|
|
[a, b],
|
||
|
|
"Independent sections of the same annotator can use both model slots",
|
||
|
|
);
|
||
|
|
const d = scope("d", "names", 0);
|
||
|
|
assert.deepEqual(
|
||
|
|
preparationQueue([a, b, c, d], { start: 0, end: 30 }, 0, empty, empty).admit,
|
||
|
|
[a, d],
|
||
|
|
"Features take turns when their scopes are equally close",
|
||
|
|
);
|
||
|
|
const full = preparationQueue([a, b, c], { start: 0, end: 30 }, 2, empty, empty);
|
||
|
|
assert.deepEqual(full.admit, []);
|
||
|
|
assert.deepEqual(full.waiting, [a, b, c], "Unadmitted scopes remain available for queued outlines");
|
||
|
|
assert.deepEqual(
|
||
|
|
preparationQueue([a, a, b, c], { start: 0, end: 30 }, 0, new Set(["b"]), new Set(["a"])).waiting,
|
||
|
|
[c],
|
||
|
|
"Cached ready or failed scopes, admitted intents and shared-computation duplicates do not get queued outlines",
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
preparationQueue([a, b, c], { start: 10, end: 20 }, 1, empty, empty).admit,
|
||
|
|
[b],
|
||
|
|
"Visible work takes the released slot before neighboring scopes",
|
||
|
|
);
|
||
|
|
const local = scope("local", "local", 20, true);
|
||
|
|
assert.deepEqual(
|
||
|
|
preparationQueue([a, local], { start: 0, end: 30 }, 2, empty, empty).admit,
|
||
|
|
[local],
|
||
|
|
"Local annotations do not wait for inference slots",
|
||
|
|
);
|
||
|
|
console.log("PASS queued outlines, bounded independent scope admission, fair features and cache exclusion");
|
||
|
|
|
||
|
|
import { presentAnnotations } from "../src/annotation-display.js";
|
||
|
|
const readings = [
|
||
|
|
{ start: 0, end: 2, text: "Běijīng", tag: "hsk:3" },
|
||
|
|
{ start: 2, end: 4, text: "xīngqīliù", tag: "hsk:1" },
|
||
|
|
];
|
||
|
|
assert.equal(presentAnnotations("pinyin", readings, { minimumLevel: "2" }).length, 1);
|
||
|
|
assert.equal(presentAnnotations("pinyin", readings, { minimumLevel: "0" }).length, 2);
|
||
|
|
assert.equal(
|
||
|
|
presentAnnotations("pinyinWord", readings, { minimumLevel: "8" }).length,
|
||
|
|
2,
|
||
|
|
"Hover includes easy words too",
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
presentAnnotations("names", [{ ...readings[0], tag: "place" }], { placeColor: "#123456" })[0].color,
|
||
|
|
"#123456",
|
||
|
|
);
|
||
|
|
console.log("PASS local HSK filtering, all-word hover pinyin and category recoloring from raw cache");
|
||
|
|
|
||
|
|
import { renderAnnotations } from "../src/render-hooks.js";
|
||
|
|
import type { Annotation } from "../src/canvas/types.js";
|
||
|
|
const source = "🙂 Un mot littéraire.";
|
||
|
|
const semantic: Annotation = {
|
||
|
|
id: "item",
|
||
|
|
feature: "source",
|
||
|
|
start: 6,
|
||
|
|
end: 9,
|
||
|
|
text: "word",
|
||
|
|
style: "replace",
|
||
|
|
color: "#123456",
|
||
|
|
};
|
||
|
|
const outputs = renderAnnotations(semantic, source, [
|
||
|
|
{ style: "replace", reveal: "always" },
|
||
|
|
{ style: "color", reveal: "always" },
|
||
|
|
{ style: "tooltip", reveal: "hover", content: "source" },
|
||
|
|
]);
|
||
|
|
assert.deepEqual(
|
||
|
|
outputs.map(({ style, text, reveal }) => ({ style, text, reveal })),
|
||
|
|
[
|
||
|
|
{ style: "replace", text: "word", reveal: "always" },
|
||
|
|
{ style: "color", text: "word", reveal: "always" },
|
||
|
|
{ style: "tooltip", text: "mot", reveal: "hover" },
|
||
|
|
],
|
||
|
|
);
|
||
|
|
assert.equal(new Set(outputs.map((output) => output.id)).size, 3);
|
||
|
|
assert(outputs.every((output) => output.feature === semantic.feature && output.start === 6 && output.end === 9));
|
||
|
|
assert.equal(semantic.text, "word", "Rendering never mutates cached semantic data");
|
||
|
|
assert.deepEqual(renderAnnotations(semantic, source, undefined), [], "Missing channels never invent an appearance");
|
||
|
|
console.log("PASS one source rendered as replacement, color and original-text hover tooltip");
|
||
|
|
|
||
|
|
assert(
|
||
|
|
sameScope(
|
||
|
|
{
|
||
|
|
sourceRevision: "doc@1",
|
||
|
|
featureRevision: 4,
|
||
|
|
start: 0,
|
||
|
|
end: 6,
|
||
|
|
status: "ready",
|
||
|
|
configuration: JSON.stringify({ processor: "pinyin", scope: "word", generation: "section", options: {} }),
|
||
|
|
},
|
||
|
|
"doc@1",
|
||
|
|
JSON.stringify({ processor: "pinyin", scope: "word", options: {} }),
|
||
|
|
{ start: 0, end: 6 },
|
||
|
|
),
|
||
|
|
"Changing preparation timing reuses a persisted semantic result",
|
||
|
|
);
|
||
|
|
|
||
|
|
assert(processorMatches({ key: "verified-previous:light-fast:result" }, "current", ["verified-previous"]));
|
||
|
|
assert(!processorMatches({ key: "unknown:light-fast:result" }, "current", ["verified-previous"]));
|
||
|
|
|
||
|
|
const scopeConfiguration = JSON.stringify({ processor: "pinyin", scope: "word", options: {} });
|
||
|
|
const savedJob = (object: string, createdAt: string, items: unknown[], key = "current:scope") => ({
|
||
|
|
_camino: { ref: { object } },
|
||
|
|
configuration: scopeConfiguration,
|
||
|
|
data: {
|
||
|
|
key,
|
||
|
|
kind: "annotation",
|
||
|
|
status: "ready",
|
||
|
|
sourceRevision: "doc@1",
|
||
|
|
start: 0,
|
||
|
|
end: 6,
|
||
|
|
createdAt,
|
||
|
|
output: JSON.stringify({ items }),
|
||
|
|
},
|
||
|
|
});
|
||
|
|
const oldFacts = savedJob("old", "2026-10-01", [
|
||
|
|
{ start: 0, end: 2 },
|
||
|
|
{ start: 2, end: 4 },
|
||
|
|
]),
|
||
|
|
clearedFacts = savedJob("cleared", "2026-10-02", []),
|
||
|
|
narrowedFacts = savedJob("narrowed", "2026-10-03", [{ start: 0, end: 2 }]),
|
||
|
|
failedFacts = {
|
||
|
|
...savedJob("failed", "2026-10-04", []),
|
||
|
|
data: { ...oldFacts.data, status: "failed", createdAt: "2026-10-04" },
|
||
|
|
};
|
||
|
|
const accepts = (job: typeof oldFacts) => processorMatches(job.data, "current", ["verified-previous"]);
|
||
|
|
assert.deepEqual(
|
||
|
|
selectSuccessfulAnnotationJobs([oldFacts, clearedFacts], accepts),
|
||
|
|
[clearedFacts],
|
||
|
|
"An empty successful regeneration clears its old items",
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
selectSuccessfulAnnotationJobs([oldFacts, clearedFacts, narrowedFacts, failedFacts], accepts),
|
||
|
|
[narrowedFacts],
|
||
|
|
"A smaller successful output replaces the whole scope; failure does not erase success",
|
||
|
|
);
|
||
|
|
const oldAlias = {
|
||
|
|
...oldFacts,
|
||
|
|
data: { ...oldFacts.data, key: "verified-previous:light-fast:legacy" },
|
||
|
|
configuration: JSON.stringify({ processor: "pinyin", scope: "word", generation: "section", options: {} }),
|
||
|
|
};
|
||
|
|
const unknown = savedJob("unknown", "2026-10-05", [{ start: 4, end: 6 }], "unknown:scope");
|
||
|
|
assert.deepEqual(
|
||
|
|
selectSuccessfulAnnotationJobs([oldAlias, clearedFacts, unknown], accepts),
|
||
|
|
[clearedFacts],
|
||
|
|
"Verified revision aliases and old generation formatting describe the same semantic scope; unknown revisions cannot replace it",
|
||
|
|
);
|
||
|
|
const anotherRange = { ...oldFacts, data: { ...oldFacts.data, start: 6, end: 8 } },
|
||
|
|
anotherSource = { ...oldFacts, data: { ...oldFacts.data, sourceRevision: "doc@2" } },
|
||
|
|
anotherInput = {
|
||
|
|
...oldFacts,
|
||
|
|
configuration: JSON.stringify({ processor: "pinyin", scope: "word", options: { targetLanguage: "French" } }),
|
||
|
|
};
|
||
|
|
assert.equal(
|
||
|
|
selectSuccessfulAnnotationJobs([clearedFacts, anotherRange, anotherSource, anotherInput], accepts).length,
|
||
|
|
4,
|
||
|
|
"Different semantic inputs, ranges and sources retain independent results",
|
||
|
|
);
|
||
|
|
const canonical = mergeCachedAnnotationEntries([{ node: oldFacts }], [{ node: narrowedFacts }, { node: failedFacts }]);
|
||
|
|
assert.deepEqual(
|
||
|
|
canonical.map(({ node }) => node._camino.ref.object),
|
||
|
|
["old", "failed"],
|
||
|
|
"Indexed success takes precedence over newer attempt history for its exact key",
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
selectSuccessfulAnnotationJobs(
|
||
|
|
canonical.map(({ node }) => node),
|
||
|
|
accepts,
|
||
|
|
),
|
||
|
|
[oldFacts],
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
mergeCachedAnnotationEntries([{ node: clearedFacts }, { node: oldFacts }], []),
|
||
|
|
[{ node: clearedFacts }],
|
||
|
|
"Overlapping pagination snapshots retain one canonical pointer per key",
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
JSON.parse(oldFacts.data.output).items,
|
||
|
|
[
|
||
|
|
{ start: 0, end: 2 },
|
||
|
|
{ start: 2, end: 4 },
|
||
|
|
],
|
||
|
|
"Selection never mutates retained historical output",
|
||
|
|
);
|
||
|
|
console.log(
|
||
|
|
"PASS canonical cache pointers and whole-scope replacement, including empty/narrowed successes, aliases and failed retries",
|
||
|
|
);
|
||
|
|
|
||
|
|
const retryFeature = {
|
||
|
|
feature: "pinyin",
|
||
|
|
configuration: scopeConfiguration,
|
||
|
|
revision: "current",
|
||
|
|
aliases: ["verified-previous"],
|
||
|
|
styles: ["overhead"],
|
||
|
|
};
|
||
|
|
const retryAttempt = (object: string, status: string, start = 0, createdAt = "2026-10-04") => ({
|
||
|
|
...savedJob(object, createdAt, []),
|
||
|
|
data: { ...savedJob(object, createdAt, []).data, status, start, end: start + 6 },
|
||
|
|
});
|
||
|
|
const skipped = retryAttempt("skipped", "skipped"),
|
||
|
|
failed = retryAttempt("failed", "failed", 6),
|
||
|
|
uncertain = retryAttempt("uncertain", "uncertain", 12),
|
||
|
|
offPage = retryAttempt("off-page", "skipped", 30),
|
||
|
|
ready = retryAttempt("ready", "ready", 18),
|
||
|
|
queued = retryAttempt("queued", "queued", 24);
|
||
|
|
const retryHistory = [skipped, failed, uncertain, offPage, ready, queued];
|
||
|
|
const retryHistorySnapshot = structuredClone(retryHistory);
|
||
|
|
const visiblePage = { start: 0, end: 30 };
|
||
|
|
assert.deepEqual(
|
||
|
|
selectAnnotationRetryJobs(
|
||
|
|
retryHistory,
|
||
|
|
[retryFeature, { ...retryFeature, feature: "hover pinyin", styles: ["tooltip"] }],
|
||
|
|
"doc@1",
|
||
|
|
visiblePage,
|
||
|
|
).map(({ feature, job }) => [feature, job._camino.ref.object]),
|
||
|
|
[
|
||
|
|
["pinyin", "skipped"],
|
||
|
|
["pinyin", "failed"],
|
||
|
|
["pinyin", "uncertain"],
|
||
|
|
],
|
||
|
|
"Failed/skipped/uncertain visible scopes appear once across shared annotators; queued/ready/off-page scopes do not",
|
||
|
|
);
|
||
|
|
const legacySkipped = {
|
||
|
|
...skipped,
|
||
|
|
configuration: oldAlias.configuration,
|
||
|
|
data: { ...skipped.data, key: "verified-previous:legacy", createdAt: "2026-10-03" },
|
||
|
|
};
|
||
|
|
assert.equal(
|
||
|
|
selectAnnotationRetryJobs([legacySkipped, skipped], [retryFeature], "doc@1", visiblePage).length,
|
||
|
|
1,
|
||
|
|
"Verified aliases and legacy scheduling configuration deduplicate the same scope",
|
||
|
|
);
|
||
|
|
for (const status of ["ready", "queued"]) {
|
||
|
|
assert.deepEqual(
|
||
|
|
selectAnnotationRetryJobs(
|
||
|
|
[skipped, legacySkipped, retryAttempt("replacement", status, 0, "2026-10-05")],
|
||
|
|
[retryFeature],
|
||
|
|
"doc@1",
|
||
|
|
visiblePage,
|
||
|
|
),
|
||
|
|
[],
|
||
|
|
"Newer queued or successful work supersedes old errors",
|
||
|
|
);
|
||
|
|
}
|
||
|
|
assert.deepEqual(
|
||
|
|
selectAnnotationRetryJobs([skipped], [{ ...retryFeature, styles: ["sidebar"] }], "doc@1", visiblePage),
|
||
|
|
[],
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
selectAnnotationRetryJobs([skipped], [{ ...retryFeature, styles: ["underline", "sidebar"] }], "doc@1", visiblePage),
|
||
|
|
[],
|
||
|
|
"Multi-hook sidebar processors already have controls",
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
selectAnnotationRetryJobs(
|
||
|
|
[skipped],
|
||
|
|
[retryFeature, { ...retryFeature, feature: "notes", styles: ["sidebar"] }],
|
||
|
|
"doc@1",
|
||
|
|
visiblePage,
|
||
|
|
),
|
||
|
|
[],
|
||
|
|
"A shared sidebar renderer does not get duplicate controls",
|
||
|
|
);
|
||
|
|
assert.deepEqual(selectAnnotationRetryJobs([skipped], [retryFeature], "doc@2", visiblePage), []);
|
||
|
|
assert.deepEqual(selectAnnotationRetryJobs([skipped], [retryFeature], "doc@1", { start: 0, end: 0 }), []);
|
||
|
|
assert.deepEqual(
|
||
|
|
selectAnnotationRetryJobs(
|
||
|
|
[{ ...skipped, data: { ...skipped.data, kind: "variant" } }],
|
||
|
|
[retryFeature],
|
||
|
|
"doc@1",
|
||
|
|
visiblePage,
|
||
|
|
),
|
||
|
|
[],
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
selectAnnotationRetryJobs(
|
||
|
|
[{ ...unknown, data: { ...unknown.data, status: "failed" } }],
|
||
|
|
[retryFeature],
|
||
|
|
"doc@1",
|
||
|
|
visiblePage,
|
||
|
|
),
|
||
|
|
[],
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
selectAnnotationRetryJobs(
|
||
|
|
[{ ...anotherInput, data: { ...anotherInput.data, status: "failed" } }],
|
||
|
|
[retryFeature],
|
||
|
|
"doc@1",
|
||
|
|
visiblePage,
|
||
|
|
),
|
||
|
|
[],
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
retryHistory,
|
||
|
|
retryHistorySnapshot,
|
||
|
|
"Status selection preserves all cached values and attempt history",
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
annotationRetryStatus("skipped", "AGENT_SETUP_UNAVAILABLE: Authentication required"),
|
||
|
|
"Setup was unavailable. Try again when connected.",
|
||
|
|
);
|
||
|
|
assert.equal(annotationRetryStatus("uncertain", "raw transport error"), "Preparation could not be confirmed.");
|
||
|
|
const retrying = new Set<string>();
|
||
|
|
let explicitCalls = 0,
|
||
|
|
finishRetry: () => void = () => {},
|
||
|
|
rerenders = 0;
|
||
|
|
const dispatch = (settled: () => void) => {
|
||
|
|
explicitCalls++;
|
||
|
|
// The view delegates the original scope with retry:true, without replanning.
|
||
|
|
const { feature, job } = selectAnnotationRetryJobs([skipped], [retryFeature], "doc@1", visiblePage)[0];
|
||
|
|
assert.deepEqual(
|
||
|
|
{ feature, start: job.data.start, end: job.data.end, retry: true },
|
||
|
|
{ feature: "pinyin", start: 0, end: 6, retry: true },
|
||
|
|
);
|
||
|
|
finishRetry = settled;
|
||
|
|
return true;
|
||
|
|
};
|
||
|
|
assert.equal(explicitCalls, 0, "Selecting failed statuses never dispatches inference");
|
||
|
|
assert(explicitAnnotationRetry("skipped", retrying, dispatch, () => rerenders++));
|
||
|
|
assert(!explicitAnnotationRetry("skipped", retrying, dispatch, () => rerenders++));
|
||
|
|
assert.equal(explicitCalls, 1, "Double clicks cannot repeat an in-flight retry");
|
||
|
|
finishRetry();
|
||
|
|
assert.equal(explicitCalls, 1, "Settlement never automatically retries");
|
||
|
|
assert.equal(retrying.size, 0);
|
||
|
|
assert.equal(rerenders, 2);
|
||
|
|
assert(
|
||
|
|
!explicitAnnotationRetry(
|
||
|
|
"not-ready",
|
||
|
|
retrying,
|
||
|
|
() => false,
|
||
|
|
() => {},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
assert.equal(retrying.size, 0, "An unavailable operation releases the button");
|
||
|
|
assert.throws(() =>
|
||
|
|
explicitAnnotationRetry(
|
||
|
|
"rejected",
|
||
|
|
retrying,
|
||
|
|
() => {
|
||
|
|
throw Error("rejected");
|
||
|
|
},
|
||
|
|
() => {},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
assert.equal(retrying.size, 0);
|
||
|
|
console.log(
|
||
|
|
"PASS explicit nearby annotator retries, shared identities/aliases, sidebar/off-page exclusion and superseded attempts",
|
||
|
|
);
|
||
|
|
|
||
|
|
// Two channels use independently declared structs, not a single feature type.
|
||
|
|
import { configDefaults } from "../src/config-schema.js";
|
||
|
|
const raw = { ...semantic, color: "" };
|
||
|
|
const multi = renderAnnotations(
|
||
|
|
raw,
|
||
|
|
source,
|
||
|
|
[
|
||
|
|
{ style: "underline", reveal: "always", options: "primary" },
|
||
|
|
{ style: "underline", reveal: "always", options: "secondary" },
|
||
|
|
{ style: "replace", reveal: "click" },
|
||
|
|
{ style: "tooltip", reveal: "hover", content: "source" },
|
||
|
|
],
|
||
|
|
{
|
||
|
|
primary: { color: "#112233", underlinePattern: "squiggly" },
|
||
|
|
secondary: { color: "#445566", underlinePattern: "dashed" },
|
||
|
|
},
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
multi.slice(0, 2).map((a) => [a.color, a.underlinePattern]),
|
||
|
|
[
|
||
|
|
["#112233", "squiggly"],
|
||
|
|
["#445566", "dashed"],
|
||
|
|
],
|
||
|
|
);
|
||
|
|
const changed = renderAnnotations(raw, source, [{ style: "underline", reveal: "always", options: "primary" }], {
|
||
|
|
primary: { color: "#112233", underlinePattern: "solid" },
|
||
|
|
});
|
||
|
|
assert.equal(changed[0].underlinePattern, "solid");
|
||
|
|
assert.equal(raw.text, "word");
|
||
|
|
assert.throws(
|
||
|
|
() =>
|
||
|
|
renderAnnotations(raw, source, [{ style: "overhead", reveal: "always", options: "label" }], {
|
||
|
|
label: { fontSize: 500 },
|
||
|
|
}),
|
||
|
|
/Invalid annotator render channel/,
|
||
|
|
);
|
||
|
|
assert.throws(
|
||
|
|
() => renderAnnotations(raw, source, [{ style: "overhead", reveal: "hover" }]),
|
||
|
|
/Invalid annotator render channel/,
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
configDefaults([
|
||
|
|
{
|
||
|
|
kind: "group",
|
||
|
|
key: "underlines",
|
||
|
|
label: "Underlines",
|
||
|
|
fields: [{ kind: "select", key: "pattern", label: "Pattern", default: "dashed" }],
|
||
|
|
},
|
||
|
|
]),
|
||
|
|
{ underlines: { pattern: "dashed" } },
|
||
|
|
);
|
||
|
|
console.log("PASS code-owned settings bind independent render channels without replacing semantic data");
|