Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { waitFor, click, triggerEvent } from '@ember/test-helpers';
import { waitFor, waitUntil, click, triggerEvent } from '@ember/test-helpers';
import { settled } from '@ember/test-helpers';
import GlimmerComponent from '@glimmer/component';

Expand Down Expand Up @@ -194,6 +194,56 @@ module('Integration | ai-assistant-panel | scrolling', function (hooks) {
return conversationElement.scrollTop < 20;
}

function describeScrollPosition() {
let conversationElement = document.querySelector(
'[data-test-ai-assistant-conversation]',
);
if (!conversationElement) {
return 'no [data-test-ai-assistant-conversation] element';
}
let { scrollHeight, clientHeight, scrollTop } = conversationElement;
// Signed: positive means content still sits below the fold (not scrolled
// far enough down), negative means scrolled past the bottom. The
// scrolled-to-bottom check compares the absolute value against the
// threshold, so the sign is diagnostic-only.
let distanceFromBottom = scrollHeight - clientHeight - scrollTop;
return `scrollHeight=${scrollHeight} clientHeight=${clientHeight} scrollTop=${scrollTop} distanceFromBottom=${distanceFromBottom} bottomThreshold=${BOTTOM_THRESHOLD}`;
}

// The auto-scroll that keeps the newest message in view fires when the last
// message registers its scroller and again whenever that message's subtree
// mutates. Avatars, card pills, and markdown can finish rendering (and shift
// layout) after the test runloop has otherwise settled, which moves the
// scroll position before the component re-scrolls to correct it. A single
// synchronous read races that correction, so poll until the conversation
// settles at the target position. On timeout, report the exact geometry so a
// future failure is diagnosable instead of a bare `expected true`.
async function assertScrolledToBottom(
assert: Assert,
message = 'AI assistant is scrolled to bottom',
) {
try {
await waitUntil(() => isAiAssistantScrolledToBottom(), { timeout: 2000 });
assert.ok(true, message);
} catch (e) {
let reason = e instanceof Error ? e.message : String(e);
assert.ok(false, `${message} — ${describeScrollPosition()} (${reason})`);
}
}

async function assertScrolledToTop(
assert: Assert,
message = 'AI assistant is scrolled to top',
) {
try {
await waitUntil(() => isAiAssistantScrolledToTop(), { timeout: 2000 });
assert.ok(true, message);
} catch (e) {
let reason = e instanceof Error ? e.message : String(e);
assert.ok(false, `${message} — ${describeScrollPosition()} (${reason})`);
}
}

function fillRoomWithReadMessages(
roomId: string,
messagesHaveBeenRead = true,
Expand Down Expand Up @@ -265,11 +315,8 @@ module('Integration | ai-assistant-panel | scrolling', function (hooks) {
});
await waitFor('[data-test-message-idx="40"]');
await click('[data-test-unread-messages-button]');
await new Promise((r) => setTimeout(r, 2000)); // wait for animated scroll to complete
assert.ok(
isAiAssistantScrolledToBottom(),
'AI assistant is scrolled to bottom',
);
// poll until the animated scroll completes and settles at the bottom
await assertScrolledToBottom(assert);
});

test('it does not show unread message indicator when new message received and scrolled to bottom', async function (assert) {
Expand Down Expand Up @@ -307,8 +354,8 @@ module('Integration | ai-assistant-panel | scrolling', function (hooks) {
await settled();
await click('[data-test-open-ai-assistant]');
await waitFor('[data-test-message-idx="39"]');
assert.ok(
isAiAssistantScrolledToTop(),
await assertScrolledToTop(
assert,
'AI assistant is scrolled to top (where the first unread message is)',
);
});
Expand All @@ -329,10 +376,7 @@ module('Integration | ai-assistant-panel | scrolling', function (hooks) {
await settled();
await click('[data-test-open-ai-assistant]');
await waitFor('[data-test-message-idx="39"]');
assert.ok(
isAiAssistantScrolledToBottom(),
'AI assistant is scrolled to bottom',
);
await assertScrolledToBottom(assert);
});

test('scrolling stays at the bottom if a message is streaming in', async function (assert) {
Expand All @@ -351,10 +395,7 @@ module('Integration | ai-assistant-panel | scrolling', function (hooks) {
await settled();
await click('[data-test-open-ai-assistant]');
await waitFor('[data-test-message-idx="39"]');
assert.ok(
isAiAssistantScrolledToBottom(),
'AI assistant is scrolled to bottom',
);
await assertScrolledToBottom(assert);

let eventId = simulateRemoteMessage(roomId, '@aibot:localhost', {
body: `thinking...`,
Expand Down
Loading