Fix flaky AppTile tests (#31442)

* Bit of cleanup

* Attempts to fix

* uncomment

* Restructure tests

* Better reset

* Descrew up tests

* fix comment

* Remove redundant calls
This commit is contained in:
Will Hunt
2025-12-05 17:41:27 +00:00
committed by GitHub
parent 9faee160e9
commit d610c3d1ae
3 changed files with 74 additions and 56 deletions

View File

@@ -122,7 +122,6 @@ export default class ThemeWatcher extends TypedEventEmitter<ThemeWatcherEvent, T
return theme; return theme;
} }
} }
logger.log("returning theme value");
return SettingsStore.getValue("theme"); return SettingsStore.getValue("theme");
} }

View File

@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react"; import React from "react";
import { Room, type MatrixClient } from "matrix-js-sdk/src/matrix"; import { Room, type MatrixClient } from "matrix-js-sdk/src/matrix";
import { type IWidget, MatrixWidgetType } from "matrix-widget-api"; import { type IWidget, MatrixWidgetType } from "matrix-widget-api";
import { act, render, type RenderResult, waitForElementToBeRemoved, waitFor } from "jest-matrix-react"; import { act, render, waitForElementToBeRemoved, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { import {
type ApprovalOpts, type ApprovalOpts,
@@ -51,6 +51,8 @@ jest.mock("../../../../../src/stores/OwnProfileStore", () => ({
}, },
})); }));
const realGetValue = SettingsStore.getValue;
describe("AppTile", () => { describe("AppTile", () => {
let cli: MatrixClient; let cli: MatrixClient;
let sdkContext: SdkContextClass; let sdkContext: SdkContextClass;
@@ -106,27 +108,40 @@ describe("AppTile", () => {
if (roomId === "r2") return [app2]; if (roomId === "r2") return [app2];
return []; return [];
}); });
});
afterAll(async () => {
jest.restoreAllMocks();
});
beforeEach(async () => {
// Do not carry across settings from previous tests
SettingsStore.reset();
sdkContext = new SdkContextClass();
// @ts-ignore
await WidgetMessagingStore.instance.onReady();
// Wake up various stores we rely on // Wake up various stores we rely on
WidgetLayoutStore.instance.useUnitTestClient(cli); WidgetLayoutStore.instance.useUnitTestClient(cli);
// @ts-ignore // @ts-ignore
await WidgetLayoutStore.instance.onReady(); await WidgetLayoutStore.instance.onReady();
RightPanelStore.instance.useUnitTestClient(cli); RightPanelStore.instance.useUnitTestClient(cli);
// @ts-ignore // @ts-ignore
await RightPanelStore.instance.onReady(); await RightPanelStore.instance.onReady();
}); });
beforeEach(async () => { afterEach(async () => {
sdkContext = new SdkContextClass();
jest.spyOn(SettingsStore, "getValue").mockRestore(); jest.spyOn(SettingsStore, "getValue").mockRestore();
// @ts-ignore // @ts-ignore
await WidgetMessagingStore.instance.onReady(); await WidgetLayoutStore.instance.onNotReady();
// @ts-ignore
await RightPanelStore.instance.onNotReady();
}); });
it("destroys non-persisted right panel widget on room change", async () => { it("destroys non-persisted right panel widget on room change", async () => {
// Set up right panel state // Set up right panel state
const realGetValue = SettingsStore.getValue; jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
const mockSettings = jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
if (name !== "RightPanel.phases") return realGetValue(name, roomId); if (name !== "RightPanel.phases") return realGetValue(name, roomId);
if (roomId === "r1") { if (roomId === "r1") {
return { return {
@@ -189,8 +204,6 @@ describe("AppTile", () => {
expect(renderResult.queryByText("Example 1")).not.toBeInTheDocument(); expect(renderResult.queryByText("Example 1")).not.toBeInTheDocument();
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(false); expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(false);
mockSettings.mockRestore();
}); });
it("distinguishes widgets with the same ID in different rooms", async () => { it("distinguishes widgets with the same ID in different rooms", async () => {
@@ -327,50 +340,57 @@ describe("AppTile", () => {
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true); expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
}); });
afterAll(async () => {
// @ts-ignore
await WidgetLayoutStore.instance.onNotReady();
// @ts-ignore
await RightPanelStore.instance.onNotReady();
jest.restoreAllMocks();
});
describe("for a pinned widget", () => { describe("for a pinned widget", () => {
let renderResult: RenderResult;
let moveToContainerSpy: jest.SpyInstance<void, [room: Room, widget: IWidget, toContainer: Container]>; let moveToContainerSpy: jest.SpyInstance<void, [room: Room, widget: IWidget, toContainer: Container]>;
beforeEach(async () => { beforeEach(async () => {
renderResult = render( moveToContainerSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
});
it("should render", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}> <MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} /> <AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>, </MatrixClientContext.Provider>,
); );
moveToContainerSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar")); await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
});
it("should render", () => {
const { asFragment } = renderResult; const { asFragment } = renderResult;
expect(asFragment()).toMatchSnapshot(); // Take a snapshot of the pinned widget expect(asFragment()).toMatchSnapshot(); // Take a snapshot of the pinned widget
}); });
it("should not display the »Popout widget« button", () => { it("should not display the »Popout widget« button", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
expect(renderResult.queryByLabelText("Popout widget")).not.toBeInTheDocument(); expect(renderResult.queryByLabelText("Popout widget")).not.toBeInTheDocument();
}); });
it("clicking 'minimise' should send the widget to the right", async () => { it("clicking 'minimise' should send the widget to the right", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
await userEvent.click(renderResult.getByLabelText("Minimise")); await userEvent.click(renderResult.getByLabelText("Minimise"));
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Right); expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Right);
}); });
it("clicking 'maximise' should send the widget to the center", async () => { it("clicking 'maximise' should send the widget to the center", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
await userEvent.click(renderResult.getByLabelText("Maximise")); await userEvent.click(renderResult.getByLabelText("Maximise"));
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Center); expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Center);
}); });
it("should render permission request", () => { it("should render permission request", async () => {
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => { jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => {
if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === app1.id) { if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === app1.id) {
(opts as ApprovalOpts).approved = false; (opts as ApprovalOpts).approved = false;
@@ -378,21 +398,17 @@ describe("AppTile", () => {
}); });
// userId and creatorUserId are different // userId and creatorUserId are different
const renderResult = render( const { container, asFragment, queryByRole } = render(
<MatrixClientContext.Provider value={cli}> <MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" /> <AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />
</MatrixClientContext.Provider>, </MatrixClientContext.Provider>,
); );
const { container, asFragment } = renderResult;
expect(container.querySelector(".mx_Spinner")).toBeFalsy(); expect(container.querySelector(".mx_Spinner")).toBeFalsy();
expect(queryByRole("button", { name: "Continue" })).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
expect(renderResult.queryByRole("button", { name: "Continue" })).toBeInTheDocument();
}); });
it("should not display 'Continue' button on permission load", () => { it("should not display 'Continue' button on permission load", async () => {
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => { jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => {
if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === app1.id) { if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === app1.id) {
(opts as ApprovalOpts).approved = true; (opts as ApprovalOpts).approved = true;
@@ -405,6 +421,7 @@ describe("AppTile", () => {
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" /> <AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />
</MatrixClientContext.Provider>, </MatrixClientContext.Provider>,
); );
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
expect(renderResult.queryByRole("button", { name: "Continue" })).not.toBeInTheDocument(); expect(renderResult.queryByRole("button", { name: "Continue" })).not.toBeInTheDocument();
}); });
@@ -418,7 +435,17 @@ describe("AppTile", () => {
); );
}); });
afterEach(() => {
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockRestore();
});
it("clicking 'un-maximise' should send the widget to the top", async () => { it("clicking 'un-maximise' should send the widget to the top", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
await userEvent.click(renderResult.getByLabelText("Un-maximise")); await userEvent.click(renderResult.getByLabelText("Un-maximise"));
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Top); expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Top);
}); });
@@ -440,36 +467,28 @@ describe("AppTile", () => {
const mockWidget = new ElementWidget(app1); const mockWidget = new ElementWidget(app1);
WidgetMessagingStore.instance.storeMessaging(mockWidget, r1.roomId, messaging); WidgetMessagingStore.instance.storeMessaging(mockWidget, r1.roomId, messaging);
});
renderResult = render( it("should display the »Popout widget« button", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}> <MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} /> <AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>, </MatrixClientContext.Provider>,
); );
}); await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
it("should display the »Popout widget« button", () => {
expect(renderResult.getByLabelText("Popout widget")).toBeInTheDocument(); expect(renderResult.getByLabelText("Popout widget")).toBeInTheDocument();
}); });
}); });
}); });
describe("for a persistent app", () => { describe("for a persistent app", () => {
let renderResult: RenderResult; it("should render", async () => {
const { asFragment, queryByRole } = render(
beforeEach(async () => {
renderResult = render(
<MatrixClientContext.Provider value={cli}> <MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} fullWidth={true} room={r1} miniMode={true} showMenubar={false} /> <AppTile key={app1.id} app={app1} room={r1} fullWidth={true} miniMode={true} showMenubar={false} />
</MatrixClientContext.Provider>, </MatrixClientContext.Provider>,
); );
await waitForElementToBeRemoved(() => queryByRole("progressbar"));
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
});
it("should render", async () => {
const { asFragment } = renderResult;
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
}); });

View File

@@ -131,7 +131,7 @@ exports[`AppTile for a pinned widget should render 1`] = `
class="mx_AppTileMenuBar_widgets" class="mx_AppTileMenuBar_widgets"
> >
<div <div
aria-label="Un-maximise" aria-label="Maximise"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button" class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button" role="button"
tabindex="0" tabindex="0"
@@ -145,7 +145,7 @@ exports[`AppTile for a pinned widget should render 1`] = `
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
d="M12 11.034a1 1 0 0 0 .29.702l.005.005c.18.18.43.29.705.29h8a1 1 0 0 0 0-2h-5.586L22 3.445a1 1 0 0 0-1.414-1.414L14 8.617V3.031a1 1 0 1 0-2 0zm0 1.963a1 1 0 0 0-.29-.702l-.005-.004A1 1 0 0 0 11 12H3a1 1 0 1 0 0 2h5.586L2 20.586A1 1 0 1 0 3.414 22L10 15.414V21a1 1 0 0 0 2 0z" d="M21 3.997a1 1 0 0 0-.29-.702l-.005-.004A1 1 0 0 0 20 3h-8a1 1 0 1 0 0 2h5.586L5 17.586V12a1 1 0 1 0-2 0v8.003a1 1 0 0 0 .29.702l.005.004c.18.18.43.291.705.291h8a1 1 0 1 0 0-2H6.414L19 6.414V12a1 1 0 1 0 2 0z"
/> />
</svg> </svg>
</div> </div>
@@ -244,7 +244,7 @@ exports[`AppTile for a pinned widget should render permission request 1`] = `
class="mx_AppTileMenuBar_widgets" class="mx_AppTileMenuBar_widgets"
> >
<div <div
aria-label="Un-maximise" aria-label="Maximise"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button" class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button" role="button"
tabindex="0" tabindex="0"
@@ -258,7 +258,7 @@ exports[`AppTile for a pinned widget should render permission request 1`] = `
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
d="M12 11.034a1 1 0 0 0 .29.702l.005.005c.18.18.43.29.705.29h8a1 1 0 0 0 0-2h-5.586L22 3.445a1 1 0 0 0-1.414-1.414L14 8.617V3.031a1 1 0 1 0-2 0zm0 1.963a1 1 0 0 0-.29-.702l-.005-.004A1 1 0 0 0 11 12H3a1 1 0 1 0 0 2h5.586L2 20.586A1 1 0 1 0 3.414 22L10 15.414V21a1 1 0 0 0 2 0z" d="M21 3.997a1 1 0 0 0-.29-.702l-.005-.004A1 1 0 0 0 20 3h-8a1 1 0 1 0 0 2h5.586L5 17.586V12a1 1 0 1 0-2 0v8.003a1 1 0 0 0 .29.702l.005.004c.18.18.43.291.705.291h8a1 1 0 1 0 0-2H6.414L19 6.414V12a1 1 0 1 0 2 0z"
/> />
</svg> </svg>
</div> </div>
@@ -340,8 +340,8 @@ exports[`AppTile for a pinned widget should render permission request 1`] = `
<span> <span>
Using this widget may share data Using this widget may share data
<div <div
aria-describedby="_r_2n_" aria-describedby="_r_2f_"
aria-labelledby="_r_2m_" aria-labelledby="_r_2e_"
class="mx_TextWithTooltip_target mx_TextWithTooltip_target--helpIcon" class="mx_TextWithTooltip_target mx_TextWithTooltip_target--helpIcon"
> >
<svg <svg