discourse/plugins/chat/test/javascripts/unit/components/chat-presence-behavior-test.gjs
Jarek Radosz 893fcf714b
DEV: Remove plugin names from test titles (#39418)
Those are now automatically included in testem's output.
2026-04-21 19:19:52 +02:00

312 lines
8.6 KiB
Text

import { getOwner } from "@ember/owner";
import { click, render, settled } from "@ember/test-helpers";
import { module, test } from "qunit";
import sinon from "sinon";
import {
clearPresenceCallbacks,
setTestPresence,
} from "discourse/lib/user-presence";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import pretender, { response } from "discourse/tests/helpers/create-pretender";
import { publishToMessageBus } from "discourse/tests/helpers/qunit-helpers";
import ChatChannel from "discourse/plugins/chat/discourse/components/chat-channel";
import ChatThread from "discourse/plugins/chat/discourse/components/chat-thread";
import ChatFabricators from "discourse/plugins/chat/discourse/lib/fabricators";
function setScrollerHeight(scroller, { clientHeight, scrollHeight }) {
Object.defineProperty(scroller, "clientHeight", {
configurable: true,
value: clientHeight,
});
Object.defineProperty(scroller, "scrollHeight", {
configurable: true,
value: scrollHeight,
});
}
module("Unit | Components | presence gating", function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.fabricators = new ChatFabricators(getOwner(this));
setTestPresence(false);
pretender.get(`/chat/api/channels/1/messages`, () =>
response({
messages: [],
meta: {
can_delete_self: true,
can_load_more_future: false,
can_load_more_past: false,
},
})
);
pretender.get(`/chat/api/me/channels`, () =>
response({
direct_message_channels: [],
public_channels: [],
})
);
pretender.get(`/chat/api/channels/1/threads/1/messages`, () =>
response({
messages: [],
meta: {
can_load_more_future: false,
can_load_more_past: false,
},
})
);
});
hooks.afterEach(function () {
setTestPresence(true);
clearPresenceCallbacks();
sinon.restore();
});
test("does not show arrow when user not present but pane cannot scroll", async function (assert) {
const channel = this.fabricators.channel({
id: 1,
currentUserMembership: { following: true, last_read_message_id: 1 },
});
const readUpdateStub = sinon.stub(
ChatChannel.prototype,
"debouncedUpdateLastReadMessage"
);
this.channel = channel;
await render(
<template><ChatChannel @channel={{this.channel}} /></template>
);
setScrollerHeight(this.element.querySelector(".chat-messages-scroller"), {
clientHeight: 500,
scrollHeight: 500,
});
const addMessagesStub = sinon.stub(channel.messagesManager, "addMessages");
readUpdateStub.resetHistory();
publishToMessageBus(`/chat/1`, {
type: "sent",
chat_message: {
id: 999,
message: "hello",
cooked: "<p>hello</p>",
excerpt: "hello",
created_at: "2025-01-01T00:00:00.000Z",
user: { id: 1, username: "eviltrout" },
},
});
await settled();
assert.true(addMessagesStub.calledOnce, "appends messages");
assert.false(readUpdateStub.called, "does not schedule read update");
assert
.dom(".chat-scroll-to-bottom__button.visible")
.doesNotExist("does not show scroll-to-bottom arrow");
});
test("shows arrow when user not present and pane can scroll", async function (assert) {
const channel = this.fabricators.channel({
id: 1,
currentUserMembership: { following: true, last_read_message_id: 1 },
});
this.channel = channel;
await render(
<template><ChatChannel @channel={{this.channel}} /></template>
);
setScrollerHeight(this.element.querySelector(".chat-messages-scroller"), {
clientHeight: 100,
scrollHeight: 500,
});
publishToMessageBus(`/chat/1`, {
type: "sent",
chat_message: {
id: 999,
message: "hello",
cooked: "<p>hello</p>",
excerpt: "hello",
created_at: "2025-01-01T00:00:00.000Z",
user: { id: 1, username: "eviltrout" },
},
});
await settled();
assert
.dom(".chat-scroll-to-bottom__button.visible")
.exists("shows scroll-to-bottom arrow");
});
test("pending manager tracks messages across contexts", async function (assert) {
const channel = this.fabricators.channel({
id: 1,
currentUserMembership: { following: true, last_read_message_id: 1 },
});
this.channel = channel;
await render(
<template><ChatChannel @channel={{this.channel}} /></template>
);
const pendingManager = getOwner(this).lookup(
"service:chat-pane-pending-manager"
);
assert.strictEqual(
pendingManager.totalPendingMessageCount,
0,
"starts at zero"
);
publishToMessageBus(`/chat/1`, {
type: "sent",
chat_message: {
id: 999,
message: "hello",
cooked: "<p>hello</p>",
excerpt: "hello",
created_at: "2025-01-01T00:00:00.000Z",
user: { id: 1, username: "eviltrout" },
},
});
await settled();
assert.strictEqual(
pendingManager.totalPendingMessageCount,
1,
"increments on new message"
);
});
test("clears pending state when scrolling to bottom", async function (assert) {
const channel = this.fabricators.channel({
id: 1,
currentUserMembership: { following: true, last_read_message_id: 1 },
});
this.channel = channel;
await render(
<template><ChatChannel @channel={{this.channel}} /></template>
);
setScrollerHeight(this.element.querySelector(".chat-messages-scroller"), {
clientHeight: 100,
scrollHeight: 500,
});
const pendingManager = getOwner(this).lookup(
"service:chat-pane-pending-manager"
);
publishToMessageBus(`/chat/1`, {
type: "sent",
chat_message: {
id: 999,
message: "hello",
cooked: "<p>hello</p>",
excerpt: "hello",
created_at: "2025-01-01T00:00:00.000Z",
user: { id: 1, username: "eviltrout" },
},
});
await settled();
assert.strictEqual(
pendingManager.totalPendingMessageCount,
1,
"has pending message"
);
await click(".chat-scroll-to-bottom__button");
assert.strictEqual(
pendingManager.totalPendingMessageCount,
0,
"clears pending on scroll to bottom"
);
assert
.dom(".chat-scroll-to-bottom__button.visible")
.doesNotExist("hides arrow after scroll");
});
test("pending count contributes to document title", async function (assert) {
const channel = this.fabricators.channel({
id: 1,
currentUserMembership: { following: true, last_read_message_id: 1 },
});
this.channel = channel;
await render(
<template><ChatChannel @channel={{this.channel}} /></template>
);
const chatService = getOwner(this).lookup("service:chat");
const initialCount = chatService.getDocumentTitleCount();
publishToMessageBus(`/chat/1`, {
type: "sent",
chat_message: {
id: 999,
message: "hello",
cooked: "<p>hello</p>",
excerpt: "hello",
created_at: "2025-01-01T00:00:00.000Z",
user: { id: 1, username: "eviltrout" },
},
});
await settled();
assert.strictEqual(
chatService.getDocumentTitleCount(),
initialCount + 1,
"pending message increments document title count"
);
});
test("thread enqueues messages when user not present", async function (assert) {
const thread = this.fabricators.thread({
id: 1,
channel: this.fabricators.channel({ id: 1 }),
currentUserMembership: { last_read_message_id: 1 },
});
const readUpdateStub = sinon.stub(
ChatThread.prototype,
"debouncedUpdateLastReadMessage"
);
this.thread = thread;
await render(<template><ChatThread @thread={{this.thread}} /></template>);
setScrollerHeight(this.element.querySelector(".chat-messages-scroller"), {
clientHeight: 100,
scrollHeight: 500,
});
const addMessagesStub = sinon.stub(thread.messagesManager, "addMessages");
readUpdateStub.resetHistory();
publishToMessageBus(`/chat/1/thread/1`, {
type: "sent",
chat_message: {
id: 1001,
message: "thread hello",
cooked: "<p>thread hello</p>",
excerpt: "thread hello",
created_at: "2025-01-01T00:00:00.000Z",
user: { id: 1, username: "eviltrout" },
},
});
await settled();
assert.true(addMessagesStub.calledOnce, "appends messages");
assert.false(readUpdateStub.called, "does not schedule read update");
assert
.dom(".chat-scroll-to-bottom__button.visible")
.exists("shows scroll-to-bottom arrow");
});
});