mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 04:07:04 +00:00
Merge pull request #987 from ouroboros-agent/claude/ui-smoke-followup-20260916
UI smoke follow-up: anchor a card crossing the viewport top on what follows it; re-pin two rows
This commit is contained in:
commit
31d62c3998
4 changed files with 72 additions and 5 deletions
|
|
@ -73,7 +73,10 @@ def test_ui_project_completion_pointer_keeps_project_history_scoped(direct_serve
|
|||
summary.wait_for(state="visible", timeout=30_000)
|
||||
summary_text = summary.inner_text()
|
||||
assert target_label in summary_text
|
||||
assert "Release shipped." in summary_text
|
||||
# The Main row is a pointer: one status word and the shared cause sentence,
|
||||
# never the answer excerpt (owner Q5=A); the answer stays in the Project.
|
||||
assert "Open the Project for details." in summary_text
|
||||
assert "Release shipped." not in summary_text
|
||||
assert "Open Project ↗" in summary_text
|
||||
assert project["id"] not in summary_text
|
||||
|
||||
|
|
|
|||
|
|
@ -291,9 +291,11 @@ def test_task_status_stays_factual_in_main_and_project_chat(
|
|||
).last
|
||||
child_line_toggle.focus()
|
||||
child_line_toggle.press("Enter")
|
||||
assert "Reason: delegated_route_unavailable" in child.locator(
|
||||
":scope > [data-live-timeline]"
|
||||
).inner_text()
|
||||
# The card says the cause in the owner's words; the machine code stays on
|
||||
# the record half, never behind a `Reason:` label (PR #970).
|
||||
child_timeline = child.locator(":scope > [data-live-timeline]").inner_text()
|
||||
assert "The selected tool route was unavailable; the parent can continue." in child_timeline
|
||||
assert "Reason:" not in child_timeline
|
||||
assert "delegated_route_unavailable" not in child_summary.inner_text()
|
||||
child_line_toggle.press("Space")
|
||||
child_summary.press("Space")
|
||||
|
|
|
|||
|
|
@ -387,7 +387,7 @@ export function createTimelineAnchors({ messagesDiv, liveCardRecords }) {
|
|||
&& !node.classList.contains('chat-load-older')
|
||||
);
|
||||
const messagesRect = messagesDiv.getBoundingClientRect();
|
||||
const topNode = nodes.find((item) => {
|
||||
let topNode = nodes.find((item) => {
|
||||
const rect = item.getBoundingClientRect();
|
||||
return rect.bottom > messagesRect.top && rect.top < messagesRect.bottom;
|
||||
}) || null;
|
||||
|
|
@ -436,6 +436,18 @@ export function createTimelineAnchors({ messagesDiv, liveCardRecords }) {
|
|||
.filter(({ rect }) => rect.top <= messagesRect.top && rect.bottom > messagesRect.top)
|
||||
.sort((a, b) => b.depth - a.depth);
|
||||
node = belowTop[0]?.node || crossing[0]?.node || topNode;
|
||||
if (node === topNode && topNode.getBoundingClientRect().top < messagesRect.top) {
|
||||
// The card's visible part holds nothing anchorable (a wait row, a
|
||||
// block without work): keep the reader's view of what FOLLOWS the
|
||||
// card. Pinning the card's own top, far above the viewport, would let
|
||||
// the card's shrink or growth move the content the reader is on.
|
||||
const following = nodes.find((item) => {
|
||||
if (item === topNode) return false;
|
||||
const rect = item.getBoundingClientRect();
|
||||
return rect.top >= messagesRect.top && rect.top < messagesRect.bottom;
|
||||
});
|
||||
if (following) { topNode = following; node = following; }
|
||||
}
|
||||
}
|
||||
|
||||
const cardChain = [];
|
||||
|
|
|
|||
|
|
@ -327,3 +327,53 @@ test('a reader inside Reviews stays anchored when content grows above the attemp
|
|||
assert.equal(anchors.restoreVisibleTimelineAnchor(anchor), true);
|
||||
assert.equal(messages.scrollTop, 1120);
|
||||
});
|
||||
|
||||
test('a card crossing the top with nothing anchorable inside keeps the reader on what follows it', () => {
|
||||
// A wait-only block above the viewport (no title, no actions, no timeline
|
||||
// line) used to anchor on its own top; when a wait update shrank the block,
|
||||
// the messages the reader was on moved up. The reader's view of what
|
||||
// FOLLOWS the card is the anchor there.
|
||||
const box = (top, bottom) => ({ top, bottom, left: 0, right: 600, width: 600, height: bottom - top });
|
||||
const makeNode = (name, bounds, classes = [], selectors = []) => {
|
||||
const node = { name, dataset: {}, isConnected: true, parentElement: null, bounds,
|
||||
classNames: new Set(classes), selectors: new Set(selectors) };
|
||||
node.classList = { contains: (value) => node.classNames.has(value) };
|
||||
node.getBoundingClientRect = () => node.bounds;
|
||||
node.getClientRects = () => [node.bounds];
|
||||
node.matches = (selector) => node.selectors.has(selector);
|
||||
node.contains = (candidate) => {
|
||||
for (let current = candidate; current; current = current.parentElement) if (current === node) return true;
|
||||
return false;
|
||||
};
|
||||
node.closest = (selector) => {
|
||||
for (let current = node; current; current = current.parentElement) {
|
||||
if (selector === '.chat-live-card' && current.classNames?.has('chat-live-card')) return current;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
node.querySelectorAll = () => [];
|
||||
return node;
|
||||
};
|
||||
const messages = makeNode('messages', box(0, 900));
|
||||
messages.scrollTop = 300;
|
||||
const card = makeNode('card', box(-244, 124), ['chat-live-card']);
|
||||
card.dataset.taskId = 'wait-task';
|
||||
card.parentElement = messages;
|
||||
const summary = makeNode('summary', box(-243, -200), [], ['[data-live-summary-button]']);
|
||||
summary.parentElement = card;
|
||||
card.querySelectorAll = (selector) => (selector.includes('[data-live-summary-button]') ? [summary] : []);
|
||||
const bubble = makeNode('bubble', box(124, 300), ['chat-bubble']);
|
||||
bubble.dataset.ts = '2026-09-06T21:02:00Z';
|
||||
bubble.parentElement = messages;
|
||||
messages.children = [card, bubble];
|
||||
messages.contains = (candidate) => candidate === card || candidate === bubble || card.contains(candidate);
|
||||
|
||||
const anchors = createTimelineAnchors({ messagesDiv: messages, liveCardRecords: new Map([['wait-task', { root: card }]]) });
|
||||
const anchor = anchors.captureVisibleTimelineAnchor();
|
||||
assert.equal(anchor.node, bubble, 'the following message is the anchor, not the card top');
|
||||
// The wait update shrank the card by 40 px: everything below moved up.
|
||||
card.bounds = box(-244, 84);
|
||||
bubble.bounds = box(84, 260);
|
||||
assert.equal(anchors.restoreVisibleTimelineAnchor(anchor), true);
|
||||
assert.equal(messages.scrollTop, 260, 'the reader stays on the same message');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue