* Exploration of a virtuoso-powered emoji picker moved to shared components Fable generated * fix pnpm lock * format & fix some lint issues * wrong import * fix lint warning * Fix off-by-one and remove manual overflow adjustment: let's leave the default unless it turns out to be necessary. Emoji should not take that long to load. * Convert to functional component * WIP: change to one big virtuoso scroller * Change to use virtuoso's own onRangeChanged and santitise category data and how it's passed around * Convert Tabs to functional component and put the focusing behaviour back with it just keeping track of refs by itself. * Absorb two line config file into main component * Actually add the config to the main file * Convert emoji to functional Also make selected always defined and use useCallback. * QuickReactions to functional component * Non-default exports & doc * Search to functional component * Well it seems to work just fine now * Use ref prop * fix lockfile AGAIN * lint * Remove default export * Remove some mx_ classnames and fix the inputRef to make the arrow keys in the search box work (well, work as much as they ever did). * Remove last of the mx_ id / classnames (except the one in the test) * Use useMemo to memoize * No need to export props interface (I think?) and fix comment now we don't do the mutation stuff anymore * Fix test * Fix axe violations & add screenshots * Avoid comparing dom snapshots in test * Allow more before or after, just compare order of the ones present in both. * Switch existing usages to new emoji picker and kill the old one with fire * Unused stuff * Remove i18n strings * Fix some tests * Update screenshots * Fix test by removing the last of the weird memoized-but-mutated data structure * Move the string somewhere more sensible than 'a11y' * i18n lint * Give the emojis IDs so aria-activedescendant works * Fix more tests * Add a small wrapper emoji picker component This lets us easily memoize the recent emojis when the emoji picker is opened. Also it saves a bit of boilerplate. * Remove old emojipicker css * Typos Co-authored-by: David Langley <davidl@element.io> * Use compound constants * Rethemendex * Use catalog version for emojibase * Add comments * More comments * Fix comment * More comments * more comments (and make them uniform) * More comments * Fix pnpm lock again * Another comment * Apply button types to new version * Add comment * Disable screenshot as per comment --------- Co-authored-by: Will Hunt <2072976+Half-Shot@users.noreply.github.com> Co-authored-by: David Langley <davidl@element.io>
224 lines
10 KiB
TypeScript
224 lines
10 KiB
TypeScript
/*
|
|
Copyright 2024 New Vector Ltd.
|
|
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE files in the repository root for full details.
|
|
*/
|
|
|
|
import { rejectToastIfExists } from "@element-hq/element-web-playwright-common";
|
|
|
|
import { test, expect } from "../../element-web-test";
|
|
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
|
import { getSampleFilePath } from "../../sample-files";
|
|
|
|
const CtrlOrMeta = process.platform === "darwin" ? "Meta" : "Control";
|
|
|
|
test.describe("Composer", () => {
|
|
test.use({
|
|
displayName: "Janet",
|
|
botCreateOpts: {
|
|
displayName: "Bob",
|
|
},
|
|
lockLeftPanelWidth: false,
|
|
});
|
|
|
|
test.use({
|
|
room: async ({ app, user }, use) => {
|
|
const roomId = await app.client.createRoom({ name: "Composing Room" });
|
|
await app.viewRoomByName("Composing Room");
|
|
await use({ roomId });
|
|
},
|
|
});
|
|
|
|
test.beforeEach(async ({ app, room /* trigger room fixture */ }) => {
|
|
await rejectToastIfExists(app.page, "Notifications");
|
|
});
|
|
|
|
test.describe("CIDER", () => {
|
|
test("sends a message when you click send or press Enter", async ({ page }) => {
|
|
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
|
|
|
// Type a message
|
|
await composer.pressSequentially("my message 0");
|
|
// It has not been sent yet
|
|
await expect(page.locator(".mx_EventTile_body", { hasText: "my message 0" })).not.toBeVisible();
|
|
|
|
// Click send
|
|
await page.getByRole("button", { name: "Send message" }).click();
|
|
// It has been sent
|
|
await expect(
|
|
page.locator(".mx_EventTile_last .mx_EventTile_body", { hasText: "my message 0" }),
|
|
).toBeVisible();
|
|
|
|
// Type another and press Enter afterward
|
|
await composer.pressSequentially("my message 1");
|
|
await composer.press("Enter");
|
|
// It was sent
|
|
await expect(
|
|
page.locator(".mx_EventTile_last .mx_EventTile_body", { hasText: "my message 1" }),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("can write formatted text", async ({ page }) => {
|
|
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
|
|
|
await composer.pressSequentially("my bold");
|
|
await composer.press(`${CtrlOrMeta}+KeyB`);
|
|
await composer.pressSequentially(" message");
|
|
await page.getByRole("button", { name: "Send message" }).click();
|
|
// Note: both "bold" and "message" are bold, which is probably surprising
|
|
await expect(page.locator(".mx_EventTile_body strong", { hasText: "bold message" })).toBeVisible();
|
|
});
|
|
|
|
test("should allow user to input emoji via graphical picker", async ({ page, app }) => {
|
|
await app.getComposer(false).getByRole("button", { name: "Emoji" }).click();
|
|
|
|
await page.getByLabel("Emoji picker").getByRole("button", { name: "😇" }).click();
|
|
|
|
await page.locator(".mx_ContextualMenu_background").click(); // Close emoji picker
|
|
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter"); // Send message
|
|
|
|
await expect(page.locator(".mx_EventTile_body", { hasText: "😇" })).toBeVisible();
|
|
});
|
|
|
|
test("renders in narrow viewports", { tag: "@screenshot" }, async ({ page, bot, app }) => {
|
|
// Shrink the viewport
|
|
await page.setViewportSize({ width: 500, height: 1080 });
|
|
// Shrinking the viewport will collapse the left-panel, so manually expand it.
|
|
await app.resizeLeftPanel(150);
|
|
// Now take the screenshot
|
|
await expect(app.getComposer()).toMatchScreenshot("narrow.png");
|
|
});
|
|
|
|
test.describe("render emoji picker with larger viewport height", async () => {
|
|
test.use({ viewport: { width: 1280, height: 720 } });
|
|
test("render emoji picker", { tag: "@screenshot" }, async ({ page, app }) => {
|
|
await app.getComposer(false).getByRole("button", { name: "Emoji" }).click();
|
|
// Mask the background of the screenshot to avoid failing the test just because some
|
|
// other component have changed its rendering.
|
|
await expect(page.getByLabel("Emoji picker")).toMatchScreenshot("emoji-picker.png", {
|
|
css: `
|
|
.mx_ContextualMenu_background {
|
|
background-color: magenta !important;
|
|
}
|
|
`,
|
|
});
|
|
});
|
|
});
|
|
|
|
test.describe("render emoji picker with small viewport height", async () => {
|
|
test.use({ viewport: { width: 1280, height: 360 } });
|
|
test("render emoji picker", { tag: "@screenshot" }, async ({ page, app }) => {
|
|
await app.getComposer(false).getByRole("button", { name: "Emoji" }).click();
|
|
// Mask the background of the screenshot to avoid failing the test just because some
|
|
// other component have changed its rendering.
|
|
await expect(page.getByLabel("Emoji picker")).toMatchScreenshot("emoji-picker-small.png", {
|
|
css: `
|
|
.mx_ContextualMenu_background {
|
|
background-color: magenta !important;
|
|
}
|
|
`,
|
|
});
|
|
});
|
|
});
|
|
|
|
test("should have focus lock in emoji picker", async ({ page, app }) => {
|
|
const emojiButton = app.getComposer(false).getByRole("button", { name: "Emoji" });
|
|
|
|
// Open emoji picker by clicking the button
|
|
await emojiButton.click();
|
|
|
|
// Wait for emoji picker to be visible
|
|
const emojiPicker = page.getByLabel("Emoji picker");
|
|
await expect(emojiPicker).toBeVisible();
|
|
|
|
// Get initial focused element (should be search input)
|
|
const searchInput = emojiPicker.getByRole("textbox", { name: "Search" });
|
|
await expect(searchInput).toBeFocused();
|
|
|
|
// Try to tab multiple times - focus should stay within emoji picker
|
|
await page.keyboard.press("Tab");
|
|
await page.keyboard.press("Tab");
|
|
await page.keyboard.press("Tab");
|
|
await page.keyboard.press("Tab");
|
|
await page.keyboard.press("Tab");
|
|
|
|
// Verify we're still within the emoji picker (not back to composer)
|
|
const focusStillInPicker = await emojiPicker.evaluate((el) => el.contains(document.activeElement));
|
|
expect(focusStillInPicker).toBe(true);
|
|
|
|
// Close with Escape key
|
|
await page.keyboard.press("Escape");
|
|
|
|
// Verify emoji picker is closed
|
|
await expect(emojiPicker).not.toBeVisible();
|
|
|
|
// Verify focus returns to emoji button
|
|
await expect(emojiButton).toBeFocused();
|
|
});
|
|
|
|
test.describe("when Control+Enter is required to send", () => {
|
|
test.beforeEach(async ({ app }) => {
|
|
await app.settings.setValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true);
|
|
});
|
|
|
|
test("only sends when you press Control+Enter", async ({ page }) => {
|
|
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
|
// Type a message and press Enter
|
|
await composer.pressSequentially("my message 3");
|
|
await composer.press("Enter");
|
|
// It has not been sent yet
|
|
await expect(page.locator(".mx_EventTile_body", { hasText: "my message 3" })).not.toBeVisible();
|
|
|
|
// Press Control+Enter
|
|
await composer.press(`${CtrlOrMeta}+Enter`);
|
|
// It was sent
|
|
await expect(
|
|
page.locator(".mx_EventTile_last .mx_EventTile_body", { hasText: "my message 3" }),
|
|
).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test("can send mention", { tag: "@screenshot" }, async ({ page, bot, app }) => {
|
|
// Set up a private room so we have another user to mention
|
|
await app.client.createRoom({
|
|
is_direct: true,
|
|
invite: [bot.credentials!.userId],
|
|
});
|
|
await app.viewRoomByName("Bob");
|
|
|
|
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
|
await composer.click();
|
|
await composer.pressSequentially("@bob");
|
|
|
|
// Note that we include the user ID here as the room tile is also an 'option' role
|
|
// with text 'Bob'
|
|
await page.getByRole("option", { name: `Bob ${bot.credentials!.userId}` }).click();
|
|
await expect(composer.getByText("Bob")).toBeVisible();
|
|
await expect(composer).toMatchScreenshot("mention.png");
|
|
await composer.press("Enter");
|
|
await expect(page.locator(".mx_EventTile_body", { hasText: "Bob" })).toBeVisible();
|
|
});
|
|
|
|
test("renders emoji autocomplete", { tag: "@screenshot" }, async ({ page }) => {
|
|
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
|
|
|
// Type ":+1" to trigger emoji autocomplete
|
|
await composer.pressSequentially(":+1");
|
|
|
|
// Wait for autocomplete to appear
|
|
const autocomplete = page.locator("#mx_Autocomplete");
|
|
await expect(autocomplete).toBeVisible();
|
|
|
|
// Take a screenshot of the autocomplete
|
|
await expect(autocomplete).toMatchScreenshot("emoji-autocomplete.png");
|
|
});
|
|
|
|
test("can paste a file", async ({ page, bot, app }) => {
|
|
await app.composerDragAndPasteFile("room", getSampleFilePath("riot.png"), "image/png");
|
|
await expect(page.locator(".mx_ImageBody")).toBeVisible();
|
|
});
|
|
});
|
|
});
|