From 5e935207c5b48546715cc48a3434fe53b7fd04cf Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 1 May 2026 10:15:40 +0100 Subject: [PATCH 1/5] Make DateUtils test non-timezone dependent (#33343) This was failing locally because my dev box is set to local time rather than UTC and so the Date class's special date parsing was interpreting some of the dates as a different day. Use Date's methods to build the actual date we want so we're not reliant on the date objet's parsing - that's not what we're trying to test. --- .../src/core/utils/DateUtils.test.ts | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/shared-components/src/core/utils/DateUtils.test.ts b/packages/shared-components/src/core/utils/DateUtils.test.ts index b4438ae2ba..3ae4f81a36 100644 --- a/packages/shared-components/src/core/utils/DateUtils.test.ts +++ b/packages/shared-components/src/core/utils/DateUtils.test.ts @@ -9,6 +9,13 @@ import { describe, it, expect } from "vitest"; import { formatSeconds, formatDateForInput } from "./DateUtils"; +function makeDate(year: number, month: number, day: number): Date { + const date = new Date(0); + date.setFullYear(year, month - 1, day); + date.setHours(0, 0, 0, 0); + return date; +} + describe("formatSeconds", () => { it("correctly formats time with hours", () => { expect(formatSeconds(60 * 60 * 3 + 60 * 31 + 55)).toBe("03:31:55"); @@ -26,10 +33,12 @@ describe("formatSeconds", () => { }); describe("formatDateForInput", () => { - it.each([["1993-11-01"], ["1066-10-14"], ["0571-04-22"], ["0062-02-05"]])( - "should format %s", - (dateString: string) => { - expect(formatDateForInput(new Date(dateString))).toBe(dateString); - }, - ); + it.each([ + ["1993-11-01", makeDate(1993, 11, 1)], + ["1066-10-14", makeDate(1066, 10, 14)], + ["0571-04-22", makeDate(571, 4, 22)], + ["0062-02-05", makeDate(62, 2, 5)], + ])("should format %s", (dateString: string, date: Date) => { + expect(formatDateForInput(date)).toBe(dateString); + }); }); From 990efa20dbe02444661a38e6011a492b9f0110d7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 1 May 2026 10:58:11 +0100 Subject: [PATCH 2/5] Include tsx test files in common vite config (#33342) * Include tsx test files in common vite config Otherwise we miss a bunch of the shared component tests * Add storybook/preview-api to optimizeDeps which is what vitest is telling me to do, which will hopefully stop it flaking out. --- packages/shared-components/vitest.config.ts | 1 + packages/vite-common/vite.config.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/shared-components/vitest.config.ts b/packages/shared-components/vitest.config.ts index 07cbe8c8e9..7000986700 100644 --- a/packages/shared-components/vitest.config.ts +++ b/packages/shared-components/vitest.config.ts @@ -110,6 +110,7 @@ export default mergeConfig( "vite-plugin-node-polyfills/shims/buffer", "vite-plugin-node-polyfills/shims/process", "@vector-im/compound-design-tokens/assets/web/icons", + "storybook/preview-api", ], }, resolve: { diff --git a/packages/vite-common/vite.config.ts b/packages/vite-common/vite.config.ts index f455b2d191..b45cd45912 100644 --- a/packages/vite-common/vite.config.ts +++ b/packages/vite-common/vite.config.ts @@ -56,6 +56,6 @@ export default defineConfig({ reporters, pool: "threads", globals: false, - include: ["src/**/*.test.ts"], + include: ["src/**/*.test.{ts,tsx}"], }, }); From 38c3d4f8a39eb175ca18c4d3e5a7010366a91292 Mon Sep 17 00:00:00 2001 From: Will Hunt <2072976+Half-Shot@users.noreply.github.com> Date: Fri, 1 May 2026 12:25:49 +0100 Subject: [PATCH 3/5] Playwright test for pasting files (#33350) * Add tests for pasting * Add tests for pasting files. * Remove redundant fn * rm comment --- .../web/playwright/e2e/composer/CIDER.spec.ts | 12 +++++++ apps/web/playwright/e2e/composer/RTE.spec.ts | 21 ++++++++++++ .../playwright/e2e/threads/threads.spec.ts | 22 ++++++++++++ apps/web/playwright/pages/ElementAppPage.ts | 34 +++++++++++++++++++ 4 files changed, 89 insertions(+) diff --git a/apps/web/playwright/e2e/composer/CIDER.spec.ts b/apps/web/playwright/e2e/composer/CIDER.spec.ts index d164a689e1..89f4cad276 100644 --- a/apps/web/playwright/e2e/composer/CIDER.spec.ts +++ b/apps/web/playwright/e2e/composer/CIDER.spec.ts @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. 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"; @@ -198,5 +199,16 @@ test.describe("Composer", () => { // Take a screenshot of the autocomplete await expect(autocomplete).toMatchScreenshot("emoji-autocomplete.png"); }); + + test("can paste a file", 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"); + await app.composerDragAndPasteFile("room", getSampleFilePath("riot.png"), "image/png"); + await expect(page.locator(".mx_MImageBody")).toBeVisible(); + }); }); }); diff --git a/apps/web/playwright/e2e/composer/RTE.spec.ts b/apps/web/playwright/e2e/composer/RTE.spec.ts index e88dd827fc..2c5f8071ec 100644 --- a/apps/web/playwright/e2e/composer/RTE.spec.ts +++ b/apps/web/playwright/e2e/composer/RTE.spec.ts @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. 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"; @@ -195,6 +196,26 @@ test.describe("Composer", () => { await expect(page.locator(".mx_EventTile_body strong").getByText("bold")).toBeVisible(); }); + test("can paste a file", async ({ page, bot, app }) => { + await app.composerDragAndPasteFile("room", getSampleFilePath("riot.png"), "image/png"); + await expect(page.locator(".mx_MImageBody")).toBeVisible(); + }); + + test("can paste a file in a thread", async ({ page, app }) => { + // Send a message + const composer = page.locator("div[contenteditable=true]"); + await composer.pressSequentially("my first message"); + await page.getByRole("button", { name: "Send message" }).click(); + + // Click reply + const tile = page.locator(".mx_EventTile_last"); + await tile.hover(); + await tile.getByRole("button", { name: "Reply in thread" }).click(); + + await app.composerDragAndPasteFile("thread", getSampleFilePath("riot.png"), "image/png"); + await expect(page.locator(".mx_MImageBody")).toBeVisible(); + }); + test.describe("when Control+Enter is required to send", () => { test.beforeEach(async ({ app }) => { await app.settings.setValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true); diff --git a/apps/web/playwright/e2e/threads/threads.spec.ts b/apps/web/playwright/e2e/threads/threads.spec.ts index a454031933..c74ca570ea 100644 --- a/apps/web/playwright/e2e/threads/threads.spec.ts +++ b/apps/web/playwright/e2e/threads/threads.spec.ts @@ -405,6 +405,28 @@ test.describe("Threads", () => { await app.composerDragAndUploadFiles("thread", getSampleFilePath("riot.png"), "image/png"); await expect(page.locator(".mx_ThreadView .mx_EventTile_image")).toHaveCount(1); }); + test("can send files via paste", async ({ page, app, user }) => { + // Increase right-panel size, so that files fit + await page.evaluate(() => { + window.localStorage.setItem("mx_rhs_size", "600"); + }); + + const roomId = await app.client.createRoom({}); + await page.goto("/#/room/" + roomId); + + // Send message + const locator = page.locator(".mx_RoomView_body"); + await locator.getByRole("textbox", { name: "Send an unencrypted message…" }).fill("Hello Mr. Bot"); + await locator.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter"); + // Create thread + const locator2 = locator.locator(".mx_EventTile[data-scroll-tokens]").filter({ hasText: "Hello Mr. Bot" }); + await locator2.hover(); + await locator2.getByRole("button", { name: "Reply in thread" }).click(); + + await expect(page.locator(".mx_ThreadView_timelinePanelWrapper")).toHaveCount(1); + await app.composerDragAndPasteFile("thread", getSampleFilePath("riot.png"), "image/png"); + await expect(page.locator(".mx_ThreadView .mx_EventTile_image")).toHaveCount(1); + }); }); test( diff --git a/apps/web/playwright/pages/ElementAppPage.ts b/apps/web/playwright/pages/ElementAppPage.ts index ac9212fd10..41abc8f980 100644 --- a/apps/web/playwright/pages/ElementAppPage.ts +++ b/apps/web/playwright/pages/ElementAppPage.ts @@ -218,6 +218,40 @@ export class ElementAppPage { await this.page.locator(".mx_Dialog").getByRole("button", { name: "Upload" }).click(); } + /** + * Paste a "file" into the specified locator and automatically uploads it. + * @param location Should the drop target the main room or the thread. + * @param path The path to the sample file so it can be read. + * @param type The mimetype of the file. + */ + public async composerDragAndPasteFile(location: "room" | "thread", path: string, type: string): Promise { + // Based on https://github.com/microsoft/playwright/issues/10667#issuecomment-2742123424 + // This read a file, encodes it into base64 and then sends it along to the page to be treated + // as a DataTransfer (the mechanism for drag and dropped files). + const buffer = await readFile(path); + const name = basename(path); + const composer = this.getComposerField(location === "thread"); + + await composer.evaluate( + async (element, [buffer, name, type]) => { + const clipboardData = new DataTransfer(); + const file = new File([Uint8Array.fromBase64(buffer)], name, { + type, + }); + clipboardData.items.add(file); + element.dispatchEvent( + new ClipboardEvent("paste", { + clipboardData, + bubbles: true, + cancelable: true, + }), + ); + }, + [buffer.toString("base64"), name, type], + ); + await this.page.locator(".mx_Dialog").getByRole("button", { name: "Upload" }).click(); + } + /** * Returns the space panel space button based on a name. The space * must be visible in the space panel From 244a2ca011149db768eb988fc02a1e3fd241dc1a Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 1 May 2026 14:05:28 +0200 Subject: [PATCH 4/5] Show the right cursor when hovering over a space (#33351) Fixes a regression in b0ee6f5323400897dde29b9a715f16f59b91d731 where a 'grab' cursor would be shown when hovering over a space, even though the most salient way to interact with it is by clicking. --- apps/web/res/css/structures/_SpacePanel.pcss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/res/css/structures/_SpacePanel.pcss b/apps/web/res/css/structures/_SpacePanel.pcss index 448795a0d4..5b6acf7596 100644 --- a/apps/web/res/css/structures/_SpacePanel.pcss +++ b/apps/web/res/css/structures/_SpacePanel.pcss @@ -125,7 +125,8 @@ Please see LICENSE files in the repository root for full details. align-items: center; padding: 4px 4px 4px 0; width: 100%; - cursor: pointer; + /* Override the unlayered cursor: grab; rule from react-beautiful-dnd */ + cursor: pointer !important; &.mx_SpaceButton_active { &:not(.mx_SpaceButton_narrow) .mx_SpaceButton_selectionWrapper { From bd369a51093fce51d7b80e0cad21ff0097099776 Mon Sep 17 00:00:00 2001 From: Ginger Date: Fri, 1 May 2026 09:31:34 -0400 Subject: [PATCH 5/5] Properly save `undefined` id tokens from OIDC login (#33345) * Properly save `undefined` id tokens from OIDC login * Fix tests * Fix tests again * Fix lints --- apps/web/src/utils/oidc/authorize.ts | 2 +- apps/web/src/utils/oidc/persistOidcSettings.ts | 10 ++++++++-- .../unit-tests/utils/oidc/persistOidcSettings-test.ts | 7 +++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/apps/web/src/utils/oidc/authorize.ts b/apps/web/src/utils/oidc/authorize.ts index 2e1420e32e..18bd910fc1 100644 --- a/apps/web/src/utils/oidc/authorize.ts +++ b/apps/web/src/utils/oidc/authorize.ts @@ -86,7 +86,7 @@ type CompleteOidcLoginResponse = { // refreshToken gained from OIDC token issuer, when falsy token cannot be refreshed refreshToken?: string; // idToken gained from OIDC token issuer - idToken: string; + idToken?: string; // this client's id as registered with the OIDC issuer clientId: string; // issuer used during authentication diff --git a/apps/web/src/utils/oidc/persistOidcSettings.ts b/apps/web/src/utils/oidc/persistOidcSettings.ts index 38426e79cb..63d4515869 100644 --- a/apps/web/src/utils/oidc/persistOidcSettings.ts +++ b/apps/web/src/utils/oidc/persistOidcSettings.ts @@ -25,10 +25,16 @@ const idTokenClaimsStorageKey = "mx_oidc_id_token_claims"; * @param idToken * @param idTokenClaims */ -export const persistOidcAuthenticatedSettings = (clientId: string, issuer: string, idToken: string): void => { +export const persistOidcAuthenticatedSettings = ( + clientId: string, + issuer: string, + idToken: string | undefined, +): void => { localStorage.setItem(clientIdStorageKey, clientId); localStorage.setItem(tokenIssuerStorageKey, issuer); - localStorage.setItem(idTokenStorageKey, idToken); + if (idToken) { + localStorage.setItem(idTokenStorageKey, idToken); + } }; /** diff --git a/apps/web/test/unit-tests/utils/oidc/persistOidcSettings-test.ts b/apps/web/test/unit-tests/utils/oidc/persistOidcSettings-test.ts index 5b81d142b0..86695b4c26 100644 --- a/apps/web/test/unit-tests/utils/oidc/persistOidcSettings-test.ts +++ b/apps/web/test/unit-tests/utils/oidc/persistOidcSettings-test.ts @@ -48,6 +48,13 @@ describe("persist OIDC settings", () => { expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_token_issuer", issuer); expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_id_token", idToken); }); + + it("should not set idToken in localStorage when idToken is undefined", () => { + persistOidcAuthenticatedSettings(clientId, issuer, undefined); + expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_client_id", clientId); + expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_token_issuer", issuer); + expect(localStorage.getItem("mx_oidc_id_token")).toBeFalsy(); + }); }); describe("getStoredOidcTokenIssuer()", () => {