Update tests to be complete

This commit is contained in:
Half-Shot 2025-06-12 16:25:23 +01:00
parent 9136d841ee
commit ec13bdc910
8 changed files with 170 additions and 45 deletions

View File

@ -6,7 +6,8 @@ Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test";
test.describe("Custom Component Module", () => {
test.describe("Custom Component API", () => {
test.use({
displayName: "Manny",
config: {
@ -23,31 +24,111 @@ test.describe("Custom Component Module", () => {
await use({ roomId });
},
});
test("should replace the render method of a textual event", { tag: "@screenshot" }, async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Simple message");
await expect(await page.getByText("Simple message")).toMatchScreenshot("custom-component-tile.png");
test.describe("basic functionality", () => {
test(
"should replace the render method of a textual event",
{ tag: "@screenshot" },
async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Simple message");
await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot("custom-component-tile.png", {
// Exclude timestamp and read marker from snapshot
mask: [page.locator(".mx_MessageTimestamp")],
css: `
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
});
},
);
test(
"should fall through if one module does not render a component",
{ tag: "@screenshot" },
async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Fall through here");
await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot(
"custom-component-tile-fall-through.png",
{
// Exclude timestamp and read marker from snapshot
mask: [page.locator(".mx_MessageTimestamp")],
css: `
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
},
);
},
);
test(
"should render the original content of a textual event conditionally",
{ tag: "@screenshot" },
async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Do not replace me");
await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot(
"custom-component-tile-original.png",
{
// Exclude timestamp and read marker from snapshot
mask: [page.locator(".mx_MessageTimestamp")],
css: `
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
},
);
},
);
test("should disallow editing when the allowEditingEvent hint is set to false", async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Do not show edits");
await page.getByText("Do not show edits").hover();
await expect(
await page.getByRole("toolbar", { name: "Message Actions" }).getByRole("button", { name: "Edit" }),
).not.toBeVisible();
});
test(
"should render the next registered component if the filter function throws",
{ tag: "@screenshot" },
async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Crash the filter!");
await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot(
"custom-component-crash-handle-filter.png",
{
// Exclude timestamp and read marker from snapshot
mask: [page.locator(".mx_MessageTimestamp")],
css: `
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
},
);
},
);
test(
"should render original component if the render function throws",
{ tag: "@screenshot" },
async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Crash the renderer!");
await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot(
"custom-component-crash-handle-renderer.png",
{
// Exclude timestamp and read marker from snapshot
mask: [page.locator(".mx_MessageTimestamp")],
css: `
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
},
);
},
);
});
test(
"should fall through if one module does not render a component",
{ tag: "@screenshot" },
async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Fall through here");
await expect(await page.getByText("Fall through here")).toMatchScreenshot(
"custom-component-tile-fall-through.png",
);
},
);
test(
"should render the original content of a textual event conditionally",
{ tag: "@screenshot" },
async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Do not replace me");
await expect(await page.getByText("Do not replace me")).toMatchScreenshot(
"custom-component-tile-original.png",
);
},
);
});

View File

@ -9,6 +9,38 @@ export default class CustomComponentModule {
static moduleApiVersion = "^1.0.0";
constructor(api) {
this.api = api;
this.api.customComponents.registerMessageRenderer(
(evt) => evt.getContent().body === "Do not show edits",
(_props, originalComponent) => {
return originalComponent();
},
{ allowEditingEvent: false },
);
this.api.customComponents.registerMessageRenderer(
(evt) => evt.getContent().body === "Fall through here",
(props) => {
const body = props.mxEvent.getContent().body;
return `Fallthrough text for ${body}`;
},
);
this.api.customComponents.registerMessageRenderer(
(evt) => {
if (evt.getContent().body === "Crash the filter!") {
throw new Error("Fail test!");
}
return false;
},
() => {
return `Should not render!`;
},
);
this.api.customComponents.registerMessageRenderer(
(evt) => evt.getContent().body === "Crash the renderer!",
() => {
throw new Error("Fail test!");
},
);
// Order is specific here to avoid this overriding the other renderers
this.api.customComponents.registerMessageRenderer("m.room.message", (props, originalComponent) => {
const body = props.mxEvent.getContent().body;
if (body === "Do not replace me") {
@ -18,13 +50,6 @@ export default class CustomComponentModule {
}
return `Custom text for ${body}`;
});
this.api.customComponents.registerMessageRenderer(/m\.room\.message/, (props) => {
const body = props.mxEvent.getContent().body;
if (body !== "Fall through here") {
return null;
}
return `Fallthrough text for ${body}`;
});
}
async load() {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -5,6 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import type {
CustomComponentsApi as ICustomComponentsApi,
CustomMessageRenderFunction,
@ -12,25 +14,39 @@ import type {
OriginalComponentProps,
CustomMessageRenderHints,
} from "@element-hq/element-web-module-api";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import type React from "react";
type EventRenderer = {
eventTypeOrFilter: string | ((mxEvent: MatrixEvent) => boolean);
renderer: CustomMessageRenderFunction;
hints: CustomMessageRenderHints,
}
hints: CustomMessageRenderHints;
};
export class CustomComponentsApi implements ICustomComponentsApi {
private readonly registeredMessageRenderers: EventRenderer[] = [];
public registerMessageRenderer(eventTypeOrFilter: string | ((mxEvent: MatrixEvent) => boolean), renderer: CustomMessageRenderFunction, hints: CustomMessageRenderHints = {}): void {
public registerMessageRenderer(
eventTypeOrFilter: string | ((mxEvent: MatrixEvent) => boolean),
renderer: CustomMessageRenderFunction,
hints: CustomMessageRenderHints = {},
): void {
this.registeredMessageRenderers.push({ eventTypeOrFilter: eventTypeOrFilter, renderer, hints });
}
private selectRenderer(mxEvent: MatrixEvent): EventRenderer|undefined {
return this.registeredMessageRenderers.find((rdr) => typeof rdr.eventTypeOrFilter === "string" ? mxEvent.getType().match(rdr.eventTypeOrFilter) : rdr.eventTypeOrFilter(mxEvent));
private selectRenderer(mxEvent: MatrixEvent): EventRenderer | undefined {
return this.registeredMessageRenderers.find((rdr) => {
if (typeof rdr.eventTypeOrFilter === "string") {
return rdr.eventTypeOrFilter === mxEvent.getType();
} else {
try {
return rdr.eventTypeOrFilter(mxEvent);
} catch (ex) {
logger.warn("Message renderer failed to process filter", ex);
return false; // Skip erroring renderers.
}
}
});
}
/**
@ -45,7 +61,12 @@ export class CustomComponentsApi implements ICustomComponentsApi {
): React.JSX.Element | null {
const renderer = this.selectRenderer(props.mxEvent);
if (renderer) {
return renderer.renderer(props, originalComponent);
try {
return renderer.renderer(props, originalComponent);
} catch (ex) {
logger.warn("Message renderer failed to render", ex);
// Fall through to original component. If the module encounters an error we still want to display messages to the user!
}
}
return originalComponent?.() || null;
}
@ -56,9 +77,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
* @param originalComponent Function that will be rendered if no custom renderers are present, or as a child of a custom component.
* @returns A component if a custom renderer exists, or originalComponent returns a value. Otherwise null.
*/
public getHintsForMessage(
mxEvent: MatrixEvent,
): CustomMessageRenderHints {
public getHintsForMessage(mxEvent: MatrixEvent): CustomMessageRenderHints {
const renderer = this.selectRenderer(mxEvent);
if (renderer) {
return renderer.hints;