Fix video comment effect wrapping (#5748)

## What changed

- render video-review timecode chips inside the first Markdown paragraph
so comment text wraps naturally around them
- reuse the canonical video-review chip treatment across the timeline,
Inbox previews, and Inbox detail
- preserve video-review context in Inbox so timestamp chips remain
interactive

## Why

Video comments now support Markdown-like effects, but non-player
surfaces rendered the timestamp beside a separate text layout. That kept
the chip and comment from sharing the same inline flow and made Inbox
behavior inconsistent with the player.

## Validation

- `pnpm --dir desktop check`
- 100 focused Markdown, timecode, video-review, and Inbox unit tests
- `pnpm --dir desktop build:e2e`
- focused `video-attachment.spec.ts` Playwright scenario
- pre-push desktop typecheck and 4,761-test desktop suite
- native Builderlab staging with the configured profile

Focused timeline and Inbox snapshots will be attached in a PR comment.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz>
Co-authored-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz>
This commit is contained in:
klopez4212
2026-08-14 17:16:28 +01:00
committed by GitHub
parent 1d51081b8a
commit 17d2147eca
20 changed files with 951 additions and 127 deletions

View File

@@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { clearMarkdownNodeCache, renderCachedMarkdown } from "./nodeCache.ts";
@@ -81,6 +82,103 @@ test("render variants do not collide", () => {
assert.notEqual(interactive, nonInteractive);
});
test("leading inline content is inserted into the first prose-capable block", () => {
const components = {
span: ({ children, node: _node, ...props }) =>
"data-leading-inline-content" in props
? React.createElement(
"button",
{ "data-chip": "", type: "button" },
"00:01",
)
: React.createElement("span", props, children),
};
for (const [content, pattern] of [
["plain note", /<p><button[^>]*>00:01<\/button>plain note<\/p>/],
["> quoted note", /<blockquote>\s*<p><button[^>]*>00:01<\/button>quoted/],
["- list note", /<li><button[^>]*>00:01<\/button>list note<\/li>/],
["# heading", /<h1><button[^>]*>00:01<\/button>heading<\/h1>/],
]) {
const node = renderCachedMarkdown({
...BASE,
components,
content,
leadingInlineContent: true,
variant: "leading",
});
assert.match(renderToStaticMarkup(node), pattern);
}
});
test("leading inline content falls back before code and media blocks", () => {
const components = {
span: ({ children, node: _node, ...props }) =>
"data-leading-inline-content" in props
? React.createElement(
"button",
{ "data-chip": "", type: "button" },
"00:01",
)
: React.createElement("span", props, children),
};
for (const [content, blockPattern] of [
["```js\nconst answer = 42;\n```", "<pre"],
["![](https://example.com/review.png)", "<img"],
["```js\nconst answer = 42;\n```\n\nlater note", "<pre"],
["![](https://example.com/review.png)\n\nlater note", "<img"],
["> ```js\nconst answer = 42;\n```\n\n> later note", "<pre"],
["> ![](https://example.com/review.png)\n\n> later note", "<img"],
]) {
const node = renderCachedMarkdown({
...BASE,
components,
content,
leadingInlineContent: true,
variant: "leading-fallback",
});
const html = renderToStaticMarkup(node);
assert.match(html, /<p><button[^>]*>00:01<\/button><\/p>/);
assert.ok(html.indexOf("<button") < html.indexOf(blockPattern));
}
});
test("leading inline content stays on the outer tight-list item", () => {
const components = {
span: ({ children, node: _node, ...props }) =>
"data-leading-inline-content" in props
? React.createElement(
"button",
{ "data-chip": "", type: "button" },
"00:01",
)
: React.createElement("span", props, children),
};
const node = renderCachedMarkdown({
...BASE,
components,
content: "- parent\n - child",
leadingInlineContent: true,
variant: "leading-tight-nested-list",
});
assert.match(
renderToStaticMarkup(node),
/<li><button[^>]*>00:01<\/button>parent\s*<ul>\s*<li>child<\/li>/,
);
});
test("leading inline content participates in the cache key", () => {
clearMarkdownNodeCache();
const withoutLeading = renderCachedMarkdown({ ...BASE });
const withLeading = renderCachedMarkdown({
...BASE,
leadingInlineContent: true,
});
assert.notEqual(withoutLeading, withLeading);
});
test("crafted values cannot forge key-segment boundaries", () => {
clearMarkdownNodeCache();
// Length-prefixed segments: a single name containing arbitrary bytes must