mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-08-22 23:05:37 +00:00
Prefer cached process render metadata over mutable DOM-tail state when re-rendering warnings. Add focused regression coverage and document the replay contract.
532 lines
20 KiB
Python
532 lines
20 KiB
Python
import base64
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
|
|
import pytest
|
|
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
MESSAGE_WINDOW_JS = PROJECT_ROOT / "webui" / "js" / "message-window.js"
|
|
SCROLLER_JS = PROJECT_ROOT / "webui" / "js" / "scroller.js"
|
|
PROCESS_GROUP_DOM_JS = (
|
|
PROJECT_ROOT
|
|
/ "webui"
|
|
/ "components"
|
|
/ "messages"
|
|
/ "process-group"
|
|
/ "process-group-dom.js"
|
|
)
|
|
MESSAGE_COLLAPSE_JS = PROJECT_ROOT / "webui" / "js" / "message-collapse.js"
|
|
|
|
|
|
def test_message_window_keeps_tail_and_pages_bidirectionally():
|
|
if not shutil.which("node"):
|
|
pytest.skip("Node.js is required to execute the message-window regression.")
|
|
|
|
source = MESSAGE_WINDOW_JS.read_bytes()
|
|
module_url = "data:text/javascript;base64," + base64.b64encode(source).decode("ascii")
|
|
script = f"""
|
|
import {{ MessageWindow, classifyMessageRenderUnits }} from {module_url!r};
|
|
|
|
function assert(condition, message) {{
|
|
if (!condition) throw new Error(message);
|
|
}}
|
|
|
|
const pluginBackedGroup = [
|
|
{{ no: 1, id: "step-1", type: "info" }},
|
|
{{ no: 2, id: "step-2", type: "agent" }},
|
|
{{ no: 3, id: "step-3", type: "code_exe" }},
|
|
{{ no: 4, id: "step-4", type: "agent" }},
|
|
{{ no: 5, id: "step-5", type: "code_exe" }},
|
|
{{ no: 6, id: "response-1", type: "response", agentno: 0 }},
|
|
];
|
|
const pluginBackedUnits = classifyMessageRenderUnits(pluginBackedGroup);
|
|
assert(
|
|
pluginBackedUnits.every((unit) => unit.key === pluginBackedUnits[0].key),
|
|
"code execution records must remain inside their surrounding process group",
|
|
);
|
|
assert(
|
|
pluginBackedUnits.filter((unit) => unit.isStep).length === 5,
|
|
"the root response must close the group without becoming a process step",
|
|
);
|
|
|
|
const prefixedUtilityGroup = [
|
|
{{ no: 20, type: "util" }},
|
|
{{ no: 21, type: "util" }},
|
|
{{ no: 22, type: "agent", id: "agent-with-prefix" }},
|
|
{{ no: 23, type: "response", id: "agent-with-prefix", agentno: 0 }},
|
|
];
|
|
const prefixedUtilityUnits = classifyMessageRenderUnits(prefixedUtilityGroup);
|
|
assert(
|
|
prefixedUtilityUnits.every((unit) => unit.key === prefixedUtilityUnits[0].key),
|
|
"utilities immediately before a real process step must stay in that group",
|
|
);
|
|
|
|
const utilityOnlyResponse = [
|
|
{{ no: 30, type: "util" }},
|
|
{{ no: 31, type: "util" }},
|
|
{{ no: 32, type: "response", id: "response-without-step", agentno: 0 }},
|
|
{{ no: 33, type: "util" }},
|
|
{{ no: 34, type: "user", id: "next-user" }},
|
|
];
|
|
const utilityOnlyUnits = classifyMessageRenderUnits(utilityOnlyResponse);
|
|
assert(
|
|
utilityOnlyUnits.every((unit) => unit.group === null && !unit.isStep),
|
|
"orphan utilities must not create or reopen a process group around a response",
|
|
);
|
|
|
|
const sharedIdGroup = new MessageWindow({{ initialLimit: 60 }});
|
|
sharedIdGroup.reset([
|
|
{{ no: 1, id: "shared-run-id", type: "agent", content: "final generation" }},
|
|
{{ no: 2, id: "shared-run-id", type: "response", content: "final response" }},
|
|
]);
|
|
assert(sharedIdGroup.size === 2, "a shared id must not merge GEN and response records");
|
|
assert(
|
|
sharedIdGroup.visibleMessages().map((entry) => entry.type).join(",") ===
|
|
"agent,response",
|
|
"replay must retain the final GEN immediately before its response",
|
|
);
|
|
sharedIdGroup.merge([
|
|
{{ no: 2, id: "shared-run-id", type: "response", content: "updated response" }},
|
|
]);
|
|
assert(sharedIdGroup.size === 2, "updates to one typed record must not duplicate it");
|
|
assert(
|
|
sharedIdGroup.visibleMessages().at(-1).content === "updated response",
|
|
"typed cache keys must still replace updates to the same message",
|
|
);
|
|
|
|
const logs = Array.from({{ length: 1000 }}, (_, no) => ({{
|
|
no,
|
|
type: no % 20 === 0 ? "user" : "tool",
|
|
content: `log-${{no}}`,
|
|
}}));
|
|
const windowed = new MessageWindow({{ initialLimit: 60, pageSize: 60, maxWindow: 120 }});
|
|
windowed.reset(logs);
|
|
|
|
assert(windowed.start === 940 && windowed.end === 1000, "initial render must start at the tail");
|
|
assert(windowed.visibleMessages()[0].no === 940, "tail slice must be ordered");
|
|
assert(windowed.olderCount === 940 && windowed.newerCount === 0, "tail counts must be accurate");
|
|
|
|
const unordered = new MessageWindow({{ initialLimit: 3, pageSize: 2, maxWindow: 4 }});
|
|
unordered.reset([logs[2], logs[0], logs[1]]);
|
|
assert(unordered.visibleMessages().map((entry) => entry.no).join(",") === "0,1,2", "out-of-order records must be sorted once");
|
|
|
|
const groupedLogs = Array.from({{ length: 300 }}, (_, no) => ({{
|
|
no,
|
|
unit: no >= 135 && no < 195 ? "large-process-group" : `entry-${{no}}`,
|
|
}}));
|
|
const groupedWindow = new MessageWindow({{
|
|
initialLimit: 60,
|
|
pageSize: 60,
|
|
maxWindow: 120,
|
|
getUnitKeys: (messages) => messages.map((message) => message.unit),
|
|
}});
|
|
groupedWindow.reset(groupedLogs);
|
|
groupedWindow.shiftOlder();
|
|
assert(groupedWindow.visibleStart === 135 && groupedWindow.visibleEnd === 300, "a page boundary must expand to the complete process group");
|
|
assert(groupedWindow.visibleMessages().filter((message) => message.unit === "large-process-group").length === 60, "a process group must never be split across the window");
|
|
groupedWindow.shiftOlder();
|
|
assert(groupedWindow.visibleStart === 120 && groupedWindow.visibleEnd === 240, "older paging must retain the whole intersecting group");
|
|
groupedWindow.shiftNewer();
|
|
assert(groupedWindow.visibleStart === 135 && groupedWindow.visibleEnd === 300, "newer paging must restore the whole-group tail range");
|
|
|
|
windowed.shiftOlder();
|
|
assert(windowed.start === 880 && windowed.end === 1000, "first older page should retain the tail overlap");
|
|
windowed.shiftOlder();
|
|
assert(windowed.start === 820 && windowed.end === 940, "older paging must retain exactly the adjacent page");
|
|
assert(windowed.hasOlder && windowed.hasNewer, "a historical window must page in both directions");
|
|
|
|
windowed.shiftNewer();
|
|
assert(windowed.start === 880 && windowed.end === 1000, "newer paging must reverse the older-page swap exactly");
|
|
assert(windowed.renderedCount === 120, "a shifted window must contain exactly two pages");
|
|
|
|
windowed.showHead();
|
|
assert(windowed.start === 0 && windowed.end === 60, "the initial head view must contain one page");
|
|
windowed.shiftNewer();
|
|
assert(windowed.start === 0 && windowed.end === 120, "the first forward shift must retain page A and append page B");
|
|
windowed.shiftNewer();
|
|
assert(windowed.start === 60 && windowed.end === 180, "the second forward shift must retain B and append C");
|
|
windowed.shiftNewer();
|
|
assert(windowed.start === 120 && windowed.end === 240, "the third forward shift must retain C and append D");
|
|
windowed.shiftOlder();
|
|
assert(windowed.start === 60 && windowed.end === 180, "a reverse shift must restore B beside C");
|
|
windowed.shiftOlder();
|
|
assert(windowed.start === 0 && windowed.end === 120, "a second reverse shift must restore A beside B");
|
|
|
|
windowed.showTail();
|
|
windowed.shiftOlder();
|
|
assert(windowed.start === 880 && windowed.end === 1000, "tail paging must restore a two-page live window");
|
|
|
|
windowed.merge(Array.from({{ length: 20 }}, (_, offset) => ({{
|
|
no: 1000 + offset,
|
|
type: "tool",
|
|
content: `new-${{offset}}`,
|
|
}})));
|
|
assert(windowed.end === 1020, "live tail appends must remain visible");
|
|
assert(windowed.compactTailIfNeeded(), "an oversized live window must compact");
|
|
assert(windowed.start === 900 && windowed.end === 1020, "compaction must retain two complete tail pages");
|
|
assert(windowed.renderedCount === 120, "tail compaction must use the same two-page bound");
|
|
|
|
windowed.merge([{{ no: 1019, type: "response", content: "updated" }}]);
|
|
const updated = windowed.visibleMessages().at(-1);
|
|
assert(updated.type === "response" && updated.content === "updated", "existing log updates must replace cached data");
|
|
|
|
windowed.reset(logs);
|
|
windowed.merge([{{ no: 1000, type: "tool", content: "unread" }}], {{ followTail: false }});
|
|
assert(windowed.end === 1000, "an unfollowed live append must not move the visible window");
|
|
assert(windowed.newerCount === 1, "an unfollowed live append must remain available as a newer page");
|
|
"""
|
|
subprocess.run(
|
|
["node", "--input-type=module", "-e", script],
|
|
check=True,
|
|
text=True,
|
|
)
|
|
|
|
|
|
def test_warning_replay_prefers_classified_process_group():
|
|
messages = (PROJECT_ROOT / "webui" / "js" / "messages.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
warning_handler = messages.split(
|
|
"export function drawMessageWarning", maxsplit=1
|
|
)[1].split("export function drawMessageError", maxsplit=1)[0]
|
|
|
|
assert "arguments[0][PROCESS_GROUP_RENDER_INFO]" in warning_handler
|
|
assert "getLastProcessGroup(false)" in warning_handler
|
|
|
|
|
|
def test_collapsed_process_details_are_deferred_and_discarded():
|
|
messages = (PROJECT_ROOT / "webui" / "js" / "messages.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
process_group_dom = (
|
|
PROJECT_ROOT
|
|
/ "webui"
|
|
/ "components"
|
|
/ "messages"
|
|
/ "process-group"
|
|
/ "process-group-dom.js"
|
|
).read_text(encoding="utf-8")
|
|
|
|
assert "detailPending: !shouldRenderDetail" in messages
|
|
assert "discardProcessStepDetail(step)" in messages
|
|
assert 'step.__renderDetail !== "function"' in messages
|
|
assert "estimateKvpTextSize(kvps)" in messages
|
|
assert "kvps: expanded ? kvps : null" in messages
|
|
assert "await Promise.allSettled(pending)" in process_group_dom
|
|
assert "step.__setExpanded(shouldExpandStep)" in process_group_dom
|
|
|
|
|
|
def test_user_messages_share_collapse_behavior_without_clipping_attachments():
|
|
messages = (PROJECT_ROOT / "webui" / "js" / "messages.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
message_css = (PROJECT_ROOT / "webui" / "css" / "messages.css").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
action_button_css = (
|
|
PROJECT_ROOT
|
|
/ "webui"
|
|
/ "components"
|
|
/ "messages"
|
|
/ "action-buttons"
|
|
/ "simple-action-buttons.css"
|
|
).read_text(encoding="utf-8")
|
|
|
|
assert 'contentSelector = ":scope > .message-body"' in messages
|
|
assert 'collapseContent?.classList.add("message-collapse-content")' in messages
|
|
assert 'refreshCollapsibleMessageOverflow(history)' in messages
|
|
assert 'refreshCollapsibleMessageOverflow(entry.target)' in messages
|
|
assert 'measureMessageCollapseOverflow(collapseContent' in messages
|
|
assert '":scope > .message-text",\n );' in messages
|
|
assert "attachmentsContainer.classList.add(\"attachments-container\")" in messages
|
|
assert ".message.message-collapsible .message-collapse-content" in message_css
|
|
assert ".message.message-agent-response.message-collapsible" in message_css
|
|
assert ".attachments-container.message-collapse-content" not in message_css
|
|
assert ".message-user .step-action-buttons .expand-btn" in action_button_css
|
|
assert "order: 1" in action_button_css
|
|
|
|
|
|
def test_message_collapse_ignores_hidden_replay_geometry_and_real_short_text():
|
|
if not shutil.which("node"):
|
|
pytest.skip("Node.js is required to execute the collapse regression.")
|
|
|
|
source = MESSAGE_COLLAPSE_JS.read_bytes()
|
|
module_url = "data:text/javascript;base64," + base64.b64encode(source).decode(
|
|
"ascii"
|
|
)
|
|
script = f"""
|
|
import {{ measureMessageCollapseOverflow }} from {module_url!r};
|
|
|
|
function assert(condition, message) {{
|
|
if (!condition) throw new Error(message);
|
|
}}
|
|
|
|
let historyWidth = 0;
|
|
const history = {{
|
|
get clientWidth() {{ return historyWidth || 24; }},
|
|
}};
|
|
const content = {{
|
|
isConnected: true,
|
|
clientWidth: 210,
|
|
clientHeight: 34,
|
|
scrollHeight: 280,
|
|
getBoundingClientRect: () => ({{ width: 210 }}),
|
|
closest: (selector) => selector === "#chat-history" ? history : null,
|
|
}};
|
|
globalThis.getComputedStyle = (element) => ({{
|
|
fontSize: "16px",
|
|
paddingLeft: element === history ? "12px" : "0px",
|
|
paddingRight: element === history ? "12px" : "0px",
|
|
}});
|
|
|
|
assert(
|
|
measureMessageCollapseOverflow(content) === null,
|
|
"zero-width replay staging must not mark short text as overflowing",
|
|
);
|
|
|
|
historyWidth = 800;
|
|
content.scrollHeight = 34;
|
|
assert(
|
|
measureMessageCollapseOverflow(content) === false,
|
|
"a laid-out one-line message must not expose Show More",
|
|
);
|
|
|
|
content.clientHeight = 240;
|
|
content.scrollHeight = 420;
|
|
assert(
|
|
measureMessageCollapseOverflow(content) === true,
|
|
"a body taller than the collapsed preview must expose Show More",
|
|
);
|
|
assert(
|
|
measureMessageCollapseOverflow(content, {{ expanded: true }}) === true,
|
|
"expanded long bodies must retain their Show Less control",
|
|
);
|
|
"""
|
|
subprocess.run(
|
|
["node", "--input-type=module", "-e", script],
|
|
check=True,
|
|
text=True,
|
|
)
|
|
|
|
|
|
def test_process_groups_are_atomic_and_page_steps_in_fifties():
|
|
messages = (PROJECT_ROOT / "webui" / "js" / "messages.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
group_css = (
|
|
PROJECT_ROOT
|
|
/ "webui"
|
|
/ "components"
|
|
/ "messages"
|
|
/ "process-group"
|
|
/ "process-group.css"
|
|
).read_text(encoding="utf-8")
|
|
|
|
assert "const PROCESS_GROUP_STEP_PAGE_SIZE = 50" in messages
|
|
assert 'classifyMessageRenderUnits(messages)' in messages
|
|
assert '"code_exe",' in MESSAGE_WINDOW_JS.read_text(encoding="utf-8")
|
|
assert "getUnitKeys: getMessageRenderUnitKeys" in messages
|
|
assert "getProcessGroupRenderMessages(windowMessages)" in messages
|
|
assert 'button.className = "process-group-show-more"' in messages
|
|
assert "current + PROCESS_GROUP_STEP_PAGE_SIZE" in messages
|
|
assert "group.dataset.fullStartTimestamp" in messages
|
|
assert 'else if (log.type === "util")' in messages
|
|
assert 'group?.classList.contains("utility-only")' in messages
|
|
assert "allowCompletedGroup: false" in messages
|
|
assert ".process-group.utility-only {" in group_css
|
|
assert ".show-utility-messages .process-group.utility-only" in group_css
|
|
assert ".process-group.utility-only[hidden]" not in group_css
|
|
assert ".process-group-show-more" in group_css
|
|
show_more_css = group_css.split(".process-group-show-more {", 1)[1].split(
|
|
"}", 1
|
|
)[0]
|
|
assert "text-decoration: none" in show_more_css
|
|
assert "opacity: 0.7" in show_more_css
|
|
assert "text-decoration: underline" not in show_more_css
|
|
|
|
|
|
def test_detail_preferences_await_materialization_and_select_current_step():
|
|
if not shutil.which("node"):
|
|
pytest.skip("Node.js is required to execute the detail-mode regression.")
|
|
|
|
source = PROCESS_GROUP_DOM_JS.read_text(encoding="utf-8").replace(
|
|
'import { store as preferencesStore } from '
|
|
'"/components/sidebar/bottom/preferences/preferences-store.js";\n',
|
|
'const preferencesStore = { detailMode: "current", showUtils: false };\n',
|
|
)
|
|
module_url = "data:text/javascript;base64," + base64.b64encode(
|
|
source.encode("utf-8")
|
|
).decode("ascii")
|
|
script = f"""
|
|
import {{ applyModeSteps }} from {module_url!r};
|
|
|
|
function assert(condition, message) {{
|
|
if (!condition) throw new Error(message);
|
|
}}
|
|
|
|
function makeClasses(initial = []) {{
|
|
const values = new Set(initial);
|
|
return {{
|
|
contains: (name) => values.has(name),
|
|
toggle(name, force) {{
|
|
if (force) values.add(name);
|
|
else values.delete(name);
|
|
}},
|
|
}};
|
|
}}
|
|
|
|
function makeStep(name, util = false) {{
|
|
const step = {{
|
|
name,
|
|
classList: makeClasses(util ? ["message-util"] : []),
|
|
detailReady: false,
|
|
}};
|
|
step.__setExpanded = (expanded) => new Promise((resolve) => {{
|
|
queueMicrotask(() => {{
|
|
step.classList.toggle("expanded", expanded);
|
|
step.detailReady = expanded;
|
|
resolve();
|
|
}});
|
|
}});
|
|
return step;
|
|
}}
|
|
|
|
function makeGroup(steps, complete = false) {{
|
|
const group = {{
|
|
steps,
|
|
complete,
|
|
classList: makeClasses(),
|
|
hasAttribute: (name) => complete && name === "data-group-complete",
|
|
querySelector: (selector) =>
|
|
complete && selector === ".process-group-response" ? {{}} : null,
|
|
querySelectorAll: (selector) => selector === ".process-step" ? steps : [],
|
|
}};
|
|
group.__setExpanded = async (expanded) => {{
|
|
group.classList.toggle("expanded", expanded);
|
|
}};
|
|
return group;
|
|
}}
|
|
|
|
const completedSteps = [makeStep("old")];
|
|
const currentSteps = [makeStep("first"), makeStep("current"), makeStep("util", true)];
|
|
const groups = [makeGroup(completedSteps, true), makeGroup(currentSteps)];
|
|
const history = {{
|
|
dataset: {{ messageWindowEnd: "100", messageWindowTotal: "100" }},
|
|
querySelectorAll: (selector) => selector === ".process-group" ? groups : [],
|
|
}};
|
|
globalThis.document = {{
|
|
getElementById: (id) => id === "chat-history" ? history : null,
|
|
}};
|
|
|
|
await applyModeSteps("expanded", false);
|
|
assert(
|
|
[...completedSteps, ...currentSteps].every((step) => step.detailReady),
|
|
"ALL must await every visible step detail",
|
|
);
|
|
|
|
await applyModeSteps("collapsed", false);
|
|
assert(
|
|
[...completedSteps, ...currentSteps].every((step) => !step.detailReady),
|
|
"NO must collapse every visible step",
|
|
);
|
|
|
|
await applyModeSteps("current", false);
|
|
assert(!completedSteps[0].detailReady, "STEP must not open completed history");
|
|
assert(!currentSteps[0].detailReady, "STEP must collapse older active steps");
|
|
assert(currentSteps[1].detailReady, "STEP must materialize the current visible step");
|
|
assert(!currentSteps[2].detailReady, "hidden utility steps must not become current");
|
|
|
|
history.dataset.messageWindowEnd = "50";
|
|
await applyModeSteps("current", false);
|
|
assert(
|
|
currentSteps.every((step) => !step.detailReady),
|
|
"STEP must not treat a historical window boundary as the live current step",
|
|
);
|
|
"""
|
|
subprocess.run(
|
|
["node", "--input-type=module", "-e", script],
|
|
check=True,
|
|
text=True,
|
|
)
|
|
|
|
|
|
def test_virtual_rebuild_cancels_stale_scroller_effects():
|
|
if not shutil.which("node"):
|
|
pytest.skip("Node.js is required to execute the scroller regression.")
|
|
|
|
source = SCROLLER_JS.read_bytes()
|
|
module_url = "data:text/javascript;base64," + base64.b64encode(source).decode(
|
|
"ascii"
|
|
)
|
|
script = f"""
|
|
import {{ cancelPendingScroll }} from {module_url!r};
|
|
|
|
function assert(condition, message) {{
|
|
if (!condition) throw new Error(message);
|
|
}}
|
|
|
|
let delayedScrollRan = false;
|
|
const timeoutId = setTimeout(() => {{ delayedScrollRan = true; }}, 30);
|
|
const element = {{
|
|
dataset: {{
|
|
scrollerTimeout: String(Number(timeoutId)),
|
|
scrollerReapplySnapshot: "200",
|
|
scrollingTo: "900",
|
|
}},
|
|
scrollTop: 240,
|
|
scrollCalls: [],
|
|
scrollTo(options) {{ this.scrollCalls.push(options); }},
|
|
}};
|
|
|
|
cancelPendingScroll(element);
|
|
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
|
|
assert(!delayedScrollRan, "a stale delayed auto-scroll must be canceled");
|
|
assert(element.scrollCalls.length === 1, "an in-flight smooth scroll must be stopped");
|
|
assert(element.scrollCalls[0].top === 240, "canceling must retain the current offset");
|
|
assert(!("scrollerTimeout" in element.dataset), "timeout state must be cleared");
|
|
assert(!("scrollingTo" in element.dataset), "smooth-scroll state must be cleared");
|
|
"""
|
|
subprocess.run(
|
|
["node", "--input-type=module", "-e", script],
|
|
check=True,
|
|
text=True,
|
|
)
|
|
|
|
|
|
def test_virtual_window_preserves_live_and_navigation_contracts():
|
|
messages = (PROJECT_ROOT / "webui" / "js" / "messages.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
message_css = (PROJECT_ROOT / "webui" / "css" / "messages.css").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
navigation = (
|
|
PROJECT_ROOT
|
|
/ "webui"
|
|
/ "components"
|
|
/ "chat"
|
|
/ "navigation"
|
|
/ "chat-navigation-store.js"
|
|
).read_text(encoding="utf-8")
|
|
|
|
assert "_messageRenderGeneration" in messages
|
|
assert "result: { element: null, virtualized: true, dontScroll: true }" in messages
|
|
assert 'scrollMessageWindowToEdge("start")' in navigation
|
|
assert 'scrollMessageWindowToEdge("end")' in navigation
|
|
assert 'loadAdjacentMessageWindow("older")' in navigation
|
|
assert 'loadAdjacentMessageWindow("newer")' in navigation
|
|
assert "_messageWindowFollowTail" in messages
|
|
assert "hasUserScrollIntent" in messages
|
|
assert "cancelPendingScroll(history)" in messages
|
|
assert "createMessageWindowStagingHistory(history)" in messages
|
|
assert "history.replaceChildren(...stagedChildren)" in messages
|
|
assert 'element.classList.add("message-window-restored")' in messages
|
|
assert "createMessageWindowIndicator" in messages
|
|
assert 'document.createElement("div")' in messages
|
|
assert "Loading ${label} messages" in messages
|
|
assert "Load ${Math.min" not in messages
|
|
assert "overflow-anchor: none" in message_css
|
|
assert ".message-container.message-window-restored" in message_css
|