Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/improsocial/impro
An extensible Bluesky client for web impro.social
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412import { describe, it } from "node:test";import assert from "node:assert/strict";import { PluginRichTextDispatcher } from "/js/plugins/pluginRichTextDispatcher.js";import { PluginRenderer } from "/js/plugins/pluginRendering.js";
// The dispatcher mounts node tokens through the owning plugin's renderer; the// real PluginRenderer keeps the sanitization path under test.function makeDispatcher() { return new PluginRichTextDispatcher({ getRenderer: (pluginId) => new PluginRenderer(null, pluginId), });}
describe("PluginRichTextDispatcher - claimed facet types", () => { it("is empty when no transforms are registered", () => { assert.deepEqual([...makeDispatcher().getClaimedFacetTypes()], []); });
it("unions handlesFacetTypes across registered transforms", () => { const dispatcher = makeDispatcher(); dispatcher.register({ pluginId: "alpha", handlesFacetTypes: ["blue.moji.richtext.facet", "com.domain.foo"], invoke: () => {}, }); dispatcher.register({ pluginId: "beta", handlesFacetTypes: ["com.domain.foo"], invoke: () => {}, }); assert.deepEqual([...dispatcher.getClaimedFacetTypes()].sort(), [ "blue.moji.richtext.facet", "com.domain.foo", ]); });
it("drops entries when a transform unregisters", () => { const dispatcher = makeDispatcher(); const dispose = dispatcher.register({ pluginId: "alpha", handlesFacetTypes: ["blue.moji.richtext.facet"], invoke: () => {}, }); dispose(); assert.deepEqual([...dispatcher.getClaimedFacetTypes()], []); });
it("tolerates a transform registered without handlesFacetTypes", () => { const dispatcher = makeDispatcher(); dispatcher.register({ pluginId: "alpha", invoke: () => {} }); assert.deepEqual([...dispatcher.getClaimedFacetTypes()], []); });});
describe("PluginRichTextDispatcher - transform pipeline", () => { const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
function makeContext({ uri = "at://did:test/app.bsky.feed.post/1", surface = "largePost", text = "hello", facets = [], } = {}) { return { surface, uri, did: "did:test", numberOfLines: null, source: { text, facets }, }; }
function addTransform(dispatcher, pluginId, invoke) { return dispatcher.register({ pluginId, invoke }); }
function silencingErrors(run) { const originalError = console.error; console.error = () => {}; return Promise.resolve() .then(run) .finally(() => { console.error = originalError; }); }
it("resolves null with no transforms registered", async () => { const dispatcher = makeDispatcher(); const tokens = [{ type: "text", value: "hello" }]; assert.deepEqual( await dispatcher.transformTokens(tokens, makeContext()), null, ); });
it("resolves the transformed tokens and caches them per post and surface", async () => { const dispatcher = makeDispatcher(); const batches = []; addTransform(dispatcher, "alpha", async (batch) => { batches.push(batch); return batch.map(({ tokens }) => ({ value: [...tokens, { type: "text", value: "!" }], })); }); const tokens = [{ type: "text", value: "hello" }]; const context = makeContext();
const transformed = await dispatcher.transformTokens(tokens, context); assert.deepEqual(transformed, [ { type: "text", value: "hello" }, { type: "text", value: "!" }, ]);
// Second request hits the cache: same result, no extra plugin call. assert.deepEqual( await dispatcher.transformTokens(tokens, context), transformed, ); assert.deepEqual(batches.length, 1); });
it("batches all posts of a render burst into one call per plugin", async () => { const dispatcher = makeDispatcher(); const batches = []; addTransform(dispatcher, "alpha", async (batch) => { batches.push(batch); return batch.map(({ tokens }) => ({ value: tokens })); });
await Promise.all([ dispatcher.transformTokens( [{ type: "text", value: "one" }], makeContext({ uri: "at://post/1", text: "one" }), ), dispatcher.transformTokens( [{ type: "text", value: "two" }], makeContext({ uri: "at://post/2", text: "two" }), ), ]);
assert.deepEqual(batches.length, 1); assert.deepEqual(batches[0].length, 2); assert.deepEqual(batches[0][0].tokens, [{ type: "text", value: "one" }]); assert.deepEqual(batches[0][1].tokens, [{ type: "text", value: "two" }]); });
it("shares one run between concurrent requests for the same post and surface", async () => { const dispatcher = makeDispatcher(); const batches = []; addTransform(dispatcher, "alpha", async (batch) => { batches.push(batch); return batch.map(({ tokens }) => ({ value: tokens })); }); const tokens = [{ type: "text", value: "hello" }]; const context = makeContext();
const [first, second] = await Promise.all([ dispatcher.transformTokens(tokens, context), dispatcher.transformTokens(tokens, context), ]);
assert.deepEqual(first, second); assert.deepEqual(batches.length, 1); assert.deepEqual(batches[0].length, 1); });
it("chains transforms in registration order", async () => { const dispatcher = makeDispatcher(); addTransform(dispatcher, "alpha", async (batch) => batch.map(({ tokens }) => ({ value: [...tokens, { type: "text", value: "A" }], })), ); addTransform(dispatcher, "beta", async (batch) => batch.map(({ tokens }) => ({ value: [...tokens, { type: "text", value: "B" }], })), );
const transformed = await dispatcher.transformTokens( [{ type: "text", value: "hello" }], makeContext(), );
assert.deepEqual( transformed.map((token) => token.value), ["hello", "A", "B"], ); });
it("fails open when a transform throws", async () => { const dispatcher = makeDispatcher(); addTransform(dispatcher, "alpha", async () => { throw new Error("boom"); }); addTransform(dispatcher, "beta", async (batch) => batch.map(({ tokens }) => ({ value: [...tokens, { type: "text", value: "B" }], })), );
const transformed = await silencingErrors(() => dispatcher.transformTokens( [{ type: "text", value: "hello" }], makeContext(), ), );
assert.deepEqual( transformed.map((token) => token.value), ["hello", "B"], ); });
it("fails open per item on error entries and malformed tokens", async () => { const dispatcher = makeDispatcher(); addTransform(dispatcher, "alpha", async (batch) => batch.map(({ context }) => context.uri.endsWith("/1") ? { error: "no thanks" } : { value: [{ type: "bogus" }] }, ), );
const [first, second] = await silencingErrors(() => Promise.all([ dispatcher.transformTokens( [{ type: "text", value: "one" }], makeContext({ uri: "at://post/1", text: "one" }), ), dispatcher.transformTokens( [{ type: "text", value: "two" }], makeContext({ uri: "at://post/2", text: "two" }), ), ]), );
assert.deepEqual(first, [{ type: "text", value: "one" }]); assert.deepEqual(second, [{ type: "text", value: "two" }]); });
it("re-hydrates returned facet tokens to the host originals", async () => { const dispatcher = makeDispatcher(); const facet = { index: { byteStart: 0, byteEnd: 4 }, features: [{ $type: "app.bsky.richtext.facet#tag", tag: "tag" }], }; const facetToken = { type: "facet", facet, text: "#tag" }; // Simulate the structured-clone boundary: the plugin returns a copy. addTransform(dispatcher, "alpha", async (batch) => batch.map(({ tokens }) => ({ value: JSON.parse(JSON.stringify(tokens)), })), );
const transformed = await dispatcher.transformTokens( [facetToken, { type: "text", value: " in front" }], makeContext({ text: "#tag in front", facets: [facet] }), );
assert( transformed[0] === facetToken, "facet token should be the host object", ); });
it("rejects a result containing an unrecognized facet", async () => { const dispatcher = makeDispatcher(); addTransform(dispatcher, "alpha", async (batch) => batch.map(() => ({ value: [ { type: "facet", facet: { index: { byteStart: 0, byteEnd: 99 }, features: [] }, text: "forged", }, ], })), ); const tokens = [{ type: "text", value: "hello" }];
const transformed = await silencingErrors(() => dispatcher.transformTokens(tokens, makeContext()), );
assert.deepEqual(transformed, tokens); });
it("stamps inline/block tokens with the emitting transform's pluginId and preserves earlier ids", async () => { const dispatcher = makeDispatcher(); const node = { tag: "code", text: "x" }; addTransform(dispatcher, "alpha", async (batch) => batch.map(() => ({ value: [{ type: "inline", node }] })), ); addTransform(dispatcher, "beta", async (batch) => batch.map(({ tokens }) => ({ value: [...tokens, { type: "block", node }], })), );
const transformed = await dispatcher.transformTokens( [{ type: "text", value: "hello" }], makeContext(), );
assert.deepEqual( transformed.map((token) => token.pluginId), ["alpha", "beta"], ); });
it("re-stamps a forged pluginId naming another plugin", async () => { const dispatcher = makeDispatcher(); const node = { tag: "code", text: "x" }; addTransform(dispatcher, "alpha", async (batch) => batch.map(() => ({ value: [{ type: "inline", pluginId: "victim", node }], })), );
const transformed = await dispatcher.transformTokens( [{ type: "text", value: "hello" }], makeContext(), );
assert.deepEqual( transformed.map((token) => token.pluginId), ["alpha"], ); });
it("clears cached results when the transform set changes", async () => { const dispatcher = makeDispatcher(); const batches = []; addTransform(dispatcher, "alpha", async (batch) => { batches.push(batch); return batch.map(({ tokens }) => ({ value: tokens })); }); const tokens = [{ type: "text", value: "hello" }]; const context = makeContext();
await dispatcher.transformTokens(tokens, context); dispatcher._invalidate(); await dispatcher.transformTokens(tokens, context);
assert.deepEqual(batches.length, 2); });
it("resolves in-flight requests with null when transforms change mid-run", async () => { const dispatcher = makeDispatcher(); let releaseTransform; const gate = new Promise((resolve) => { releaseTransform = resolve; }); addTransform(dispatcher, "alpha", async (batch) => { await gate; return batch.map(({ tokens }) => ({ value: tokens })); }); const request = dispatcher.transformTokens( [{ type: "text", value: "hello" }], makeContext(), ); await flush(); dispatcher._invalidate(); releaseTransform();
assert.deepEqual(await request, null); assert.deepEqual(dispatcher._cache.size, 0); });
it("re-runs when the cached entry no longer matches the source text", async () => { const dispatcher = makeDispatcher(); const batches = []; addTransform(dispatcher, "alpha", async (batch) => { batches.push(batch); return batch.map(({ tokens }) => ({ value: tokens })); }); const context = makeContext({ text: "before" });
await dispatcher.transformTokens( [{ type: "text", value: "before" }], context, ); const transformed = await dispatcher.transformTokens( [{ type: "text", value: "after" }], makeContext({ text: "after" }), );
assert.deepEqual(batches.length, 2); assert.deepEqual(transformed, [{ type: "text", value: "after" }]); });
it("renderRichTextNodeToken mounts a sanitized element and reuses it per token and host", () => { const dispatcher = makeDispatcher(); const token = { type: "inline", pluginId: "alpha", node: { tag: "code", attrs: {}, text: "x", children: [], events: {} }, }; const host = document.createElement("div");
const element = dispatcher.renderNodeToken(token, host); assert.deepEqual(element.localName, "code"); assert.deepEqual(element.textContent, "x"); assert(dispatcher.renderNodeToken(token, host) === element); const otherHost = document.createElement("div"); const otherElement = dispatcher.renderNodeToken(token, otherHost); assert.deepEqual(otherElement.localName, "code"); assert(otherElement !== element); });});