mirror of
https://github.com/discourse/discourse.git
synced 2025-01-02 22:54:09 +08:00
47d1703b67
**TL;DR:** Refactor autocomplete to use async markdown parsing for code block detection. Previously, the `inCodeBlock` function in `discourse/app/lib/utilities.js` used regular expressions to determine if a given position in the text was inside a code block. This approach had some limitations and could lead to incorrect behavior in certain edge cases. This commit refactors `inCodeBlock` to use a more robust algorithm that leverages Discourse's markdown parsing library. The new approach works as follows: 1. Check if the text contains any code block markers using a regular expression. If not, return `false` since the cursor can't be in a code block. 1. If potential code blocks exist, find a unique marker character that doesn't appear in the text. 1. Insert the unique marker character into the text at the cursor position. 1. Parse the modified text using Discourse's markdown parser, which converts the markdown into a tree of tokens. 1. Traverse the token tree to find the token that contains the unique marker character. 1. Check if the token's type is one of the types representing code blocks ("code_inline", "code_block", or "fence"). If so, return `true`, indicating that the cursor is inside a code block. Otherwise, return `false`. This algorithm provides a more accurate way to determine the cursor's position in relation to code blocks, accounting for the various ways code blocks can be represented in markdown. To accommodate this change, the autocomplete `triggerRule` option is now an async function. The autocomplete logic in `composer-editor.js`, `d-editor.js`, and `hashtag-autocomplete.js` has been updated to handle the async nature of `inCodeBlock`. Additionally, many of the tests have been refactored to handle async behavior. The test helpers now simulate typing and autocomplete selection in a more realistic, step-by-step manner. This should make the tests more robust and reflective of real-world usage. This is a significant refactor that touches multiple parts of the codebase, but it should lead to more accurate and reliable autocomplete behavior, especially when dealing with code blocks in the editor. > Written by an 🤖 LLM. Edited by a 🧑💻 human.
333 lines
9.7 KiB
JavaScript
333 lines
9.7 KiB
JavaScript
import { click, triggerEvent, visit, waitFor } from "@ember/test-helpers";
|
|
import { skip, test } from "qunit";
|
|
import pretender, { response } from "discourse/tests/helpers/create-pretender";
|
|
import {
|
|
acceptance,
|
|
loggedInUser,
|
|
publishToMessageBus,
|
|
query,
|
|
simulateKeys,
|
|
} from "discourse/tests/helpers/qunit-helpers";
|
|
|
|
acceptance("Chat | User status on mentions", function (needs) {
|
|
const channelId = 1;
|
|
const messageId = 1;
|
|
const actingUser = {
|
|
id: 1,
|
|
username: "acting_user",
|
|
};
|
|
const mentionedUser1 = {
|
|
id: 1000,
|
|
username: "user1",
|
|
status: {
|
|
description: "surfing",
|
|
emoji: "surfing_man",
|
|
},
|
|
};
|
|
const mentionedUser2 = {
|
|
id: 2000,
|
|
username: "user2",
|
|
status: {
|
|
description: "vacation",
|
|
emoji: "desert_island",
|
|
},
|
|
};
|
|
const mentionedUser3 = {
|
|
id: 3000,
|
|
username: "user3",
|
|
status: {
|
|
description: "off to dentist",
|
|
emoji: "tooth",
|
|
},
|
|
};
|
|
const message = {
|
|
id: messageId,
|
|
message: `Hey @${mentionedUser1.username}`,
|
|
cooked: `<p>Hey <a class="mention" href="/u/${mentionedUser1.username}">@${mentionedUser1.username}</a></p>`,
|
|
mentioned_users: [mentionedUser1],
|
|
user: actingUser,
|
|
created_at: "2020-08-04T15:00:00.000Z",
|
|
};
|
|
const newStatus = {
|
|
description: "working remotely",
|
|
emoji: "house",
|
|
};
|
|
const channel = {
|
|
id: channelId,
|
|
chatable_id: 1,
|
|
chatable_type: "Category",
|
|
title: "A category channel",
|
|
meta: { message_bus_last_ids: {}, can_delete_self: true },
|
|
current_user_membership: { following: true },
|
|
chatable: { id: 1 },
|
|
};
|
|
|
|
needs.settings({ chat_enabled: true });
|
|
|
|
needs.user({
|
|
...actingUser,
|
|
has_chat_enabled: true,
|
|
chat_channels: {
|
|
public_channels: [channel],
|
|
direct_message_channels: [],
|
|
meta: { message_bus_last_ids: {} },
|
|
tracking: {},
|
|
},
|
|
});
|
|
|
|
needs.hooks.beforeEach(function () {
|
|
pretender.post(`/chat/1`, () => response({}));
|
|
pretender.put(`/chat/1/edit/${messageId}`, () => response({}));
|
|
pretender.post(`/chat/drafts`, () => response({}));
|
|
pretender.put(`/chat/api/channels/1/read/1`, () => response({}));
|
|
pretender.get(`/chat/api/channels/1/messages`, () =>
|
|
response({
|
|
messages: [message],
|
|
meta: {
|
|
can_load_more_future: false,
|
|
},
|
|
})
|
|
);
|
|
pretender.delete(`/chat/api/channels/1/messages/${messageId}`, () =>
|
|
response({})
|
|
);
|
|
pretender.put(`/chat/api/channels/1/messages/${messageId}/restore`, () =>
|
|
response({})
|
|
);
|
|
|
|
pretender.get("/u/search/users", () =>
|
|
response({
|
|
users: [mentionedUser2, mentionedUser3],
|
|
})
|
|
);
|
|
|
|
pretender.get("/chat/api/mentions/groups.json", () =>
|
|
response({
|
|
unreachable: [],
|
|
over_members_limit: [],
|
|
invalid: ["and"],
|
|
})
|
|
);
|
|
});
|
|
|
|
skip("just posted messages | it shows status on mentions ", async function (assert) {
|
|
await visit(`/chat/c/-/${channelId}`);
|
|
await typeWithAutocompleteAndSend(`mentioning @${mentionedUser2.username}`);
|
|
assertStatusIsRendered(
|
|
assert,
|
|
statusSelector(mentionedUser2.username),
|
|
mentionedUser2.status
|
|
);
|
|
});
|
|
|
|
skip("just posted messages | it updates status on mentions", async function (assert) {
|
|
await visit(`/chat/c/-/${channelId}`);
|
|
await typeWithAutocompleteAndSend(`mentioning @${mentionedUser2.username}`);
|
|
|
|
loggedInUser().appEvents.trigger("user-status:changed", {
|
|
[mentionedUser2.id]: newStatus,
|
|
});
|
|
|
|
const selector = statusSelector(mentionedUser2.username);
|
|
await waitFor(selector);
|
|
assertStatusIsRendered(assert, selector, newStatus);
|
|
});
|
|
|
|
skip("just posted messages | it deletes status on mentions", async function (assert) {
|
|
await visit(`/chat/c/-/${channelId}`);
|
|
|
|
await typeWithAutocompleteAndSend(`mentioning @${mentionedUser2.username}`);
|
|
|
|
loggedInUser().appEvents.trigger("user-status:changed", {
|
|
[mentionedUser2.id]: null,
|
|
});
|
|
|
|
const selector = statusSelector(mentionedUser2.username);
|
|
await waitFor(selector, { count: 0 });
|
|
assert.dom(selector).doesNotExist("status is deleted");
|
|
});
|
|
|
|
skip("edited messages | it shows status on mentions", async function (assert) {
|
|
await visit(`/chat/c/-/${channelId}`);
|
|
|
|
await editMessage(
|
|
".chat-message-content",
|
|
`mentioning @${mentionedUser3.username}`
|
|
);
|
|
|
|
assertStatusIsRendered(
|
|
assert,
|
|
statusSelector(mentionedUser3.username),
|
|
mentionedUser3.status
|
|
);
|
|
});
|
|
|
|
skip("edited messages | it updates status on mentions", async function (assert) {
|
|
await visit(`/chat/c/-/${channelId}`);
|
|
await editMessage(
|
|
".chat-message-content",
|
|
`mentioning @${mentionedUser3.username}`
|
|
);
|
|
|
|
loggedInUser().appEvents.trigger("user-status:changed", {
|
|
[mentionedUser3.id]: newStatus,
|
|
});
|
|
|
|
const selector = statusSelector(mentionedUser3.username);
|
|
await waitFor(selector);
|
|
assertStatusIsRendered(assert, selector, newStatus);
|
|
});
|
|
|
|
skip("edited messages | it deletes status on mentions", async function (assert) {
|
|
await visit(`/chat/c/-/${channelId}`);
|
|
|
|
await editMessage(
|
|
".chat-message-content",
|
|
`mentioning @${mentionedUser3.username}`
|
|
);
|
|
|
|
loggedInUser().appEvents.trigger("user-status:changed", {
|
|
[mentionedUser3.id]: null,
|
|
});
|
|
|
|
const selector = statusSelector(mentionedUser3.username);
|
|
await waitFor(selector, { count: 0 });
|
|
assert.dom(selector).doesNotExist("status is deleted");
|
|
});
|
|
|
|
test("deleted messages | it shows status on mentions", async function (assert) {
|
|
await visit(`/chat/c/-/${channelId}`);
|
|
|
|
await deleteMessage(".chat-message-content");
|
|
await click(".chat-message-expand");
|
|
|
|
assertStatusIsRendered(
|
|
assert,
|
|
statusSelector(mentionedUser1.username),
|
|
mentionedUser1.status
|
|
);
|
|
});
|
|
|
|
test("deleted messages | it updates status on mentions", async function (assert) {
|
|
await visit(`/chat/c/-/${channelId}`);
|
|
|
|
await deleteMessage(".chat-message-content");
|
|
await click(".chat-message-expand");
|
|
|
|
loggedInUser().appEvents.trigger("user-status:changed", {
|
|
[mentionedUser1.id]: newStatus,
|
|
});
|
|
|
|
const selector = statusSelector(mentionedUser1.username);
|
|
await waitFor(selector);
|
|
assertStatusIsRendered(assert, selector, newStatus);
|
|
});
|
|
|
|
test("deleted messages | it deletes status on mentions", async function (assert) {
|
|
await visit(`/chat/c/-/${channelId}`);
|
|
|
|
await deleteMessage(".chat-message-content");
|
|
await click(".chat-message-expand");
|
|
|
|
loggedInUser().appEvents.trigger("user-status:changed", {
|
|
[mentionedUser1.id]: null,
|
|
});
|
|
|
|
const selector = statusSelector(mentionedUser1.username);
|
|
await waitFor(selector, { count: 0 });
|
|
assert.dom(selector).doesNotExist("status is deleted");
|
|
});
|
|
|
|
test("restored messages | it shows status on mentions", async function (assert) {
|
|
await visit(`/chat/c/-/${channelId}`);
|
|
|
|
await deleteMessage(".chat-message-content");
|
|
await restoreMessage(".chat-message-text.-deleted");
|
|
|
|
assertStatusIsRendered(
|
|
assert,
|
|
statusSelector(mentionedUser1.username),
|
|
mentionedUser1.status
|
|
);
|
|
});
|
|
|
|
test("restored messages | it updates status on mentions", async function (assert) {
|
|
await visit(`/chat/c/-/${channelId}`);
|
|
|
|
await deleteMessage(".chat-message-content");
|
|
await restoreMessage(".chat-message-text.-deleted");
|
|
|
|
loggedInUser().appEvents.trigger("user-status:changed", {
|
|
[mentionedUser1.id]: newStatus,
|
|
});
|
|
|
|
const selector = statusSelector(mentionedUser1.username);
|
|
await waitFor(selector);
|
|
assertStatusIsRendered(assert, selector, newStatus);
|
|
});
|
|
|
|
test("restored messages | it deletes status on mentions", async function (assert) {
|
|
await visit(`/chat/c/-/${channelId}`);
|
|
|
|
await deleteMessage(".chat-message-content");
|
|
await restoreMessage(".chat-message-text.-deleted");
|
|
|
|
loggedInUser().appEvents.trigger("user-status:changed", {
|
|
[mentionedUser1.id]: null,
|
|
});
|
|
|
|
const selector = statusSelector(mentionedUser1.username);
|
|
await waitFor(selector, { count: 0 });
|
|
assert.dom(selector).doesNotExist("status is deleted");
|
|
});
|
|
|
|
function assertStatusIsRendered(assert, selector, status) {
|
|
assert
|
|
.dom(selector)
|
|
.exists("status is rendered")
|
|
.hasAttribute(
|
|
"src",
|
|
new RegExp(`${status.emoji}.png`),
|
|
"status emoji is updated"
|
|
);
|
|
}
|
|
|
|
async function deleteMessage(messageSelector) {
|
|
await triggerEvent(query(messageSelector), "mouseenter");
|
|
await click(".more-buttons .select-kit-header-wrapper");
|
|
await click(".select-kit-collection .select-kit-row[data-value='delete']");
|
|
await publishToMessageBus(`/chat/${channelId}`, {
|
|
type: "delete",
|
|
deleted_id: messageId,
|
|
deleted_at: "2022-01-01T08:00:00.000Z",
|
|
});
|
|
}
|
|
|
|
async function editMessage(messageSelector, text) {
|
|
await triggerEvent(query(messageSelector), "mouseenter");
|
|
await click(".more-buttons .select-kit-header-wrapper");
|
|
await click(".select-kit-collection .select-kit-row[data-value='edit']");
|
|
await typeWithAutocompleteAndSend(text);
|
|
}
|
|
|
|
async function restoreMessage(messageSelector) {
|
|
await triggerEvent(query(messageSelector), "mouseenter");
|
|
await click(".more-buttons .select-kit-header-wrapper");
|
|
await click(".select-kit-collection .select-kit-row[data-value='restore']");
|
|
await publishToMessageBus(`/chat/${channelId}`, {
|
|
type: "restore",
|
|
chat_message: message,
|
|
});
|
|
}
|
|
|
|
async function typeWithAutocompleteAndSend(text) {
|
|
await simulateKeys(query(".chat-composer__input"), text);
|
|
await click(".autocomplete.ac-user .selected");
|
|
await click(".chat-composer-button.-send");
|
|
}
|
|
|
|
function statusSelector(username) {
|
|
return `.mention[href='/u/${username}'] .user-status-message img`;
|
|
}
|
|
});
|