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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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;
}
}
logger.log("returning theme value");
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 { Room, type MatrixClient } from "matrix-js-sdk/src/matrix";
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 {
type ApprovalOpts,
@ -51,6 +51,8 @@ jest.mock("../../../../../src/stores/OwnProfileStore", () => ({
},
}));
const realGetValue = SettingsStore.getValue;
describe("AppTile", () => {
let cli: MatrixClient;
let sdkContext: SdkContextClass;
@ -106,27 +108,40 @@ describe("AppTile", () => {
if (roomId === "r2") return [app2];
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
WidgetLayoutStore.instance.useUnitTestClient(cli);
// @ts-ignore
await WidgetLayoutStore.instance.onReady();
RightPanelStore.instance.useUnitTestClient(cli);
// @ts-ignore
await RightPanelStore.instance.onReady();
});
beforeEach(async () => {
sdkContext = new SdkContextClass();
afterEach(async () => {
jest.spyOn(SettingsStore, "getValue").mockRestore();
// @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 () => {
// Set up right panel state
const realGetValue = SettingsStore.getValue;
const mockSettings = jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
if (name !== "RightPanel.phases") return realGetValue(name, roomId);
if (roomId === "r1") {
return {
@ -189,8 +204,6 @@ describe("AppTile", () => {
expect(renderResult.queryByText("Example 1")).not.toBeInTheDocument();
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(false);
mockSettings.mockRestore();
});
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);
});
afterAll(async () => {
// @ts-ignore
await WidgetLayoutStore.instance.onNotReady();
// @ts-ignore
await RightPanelStore.instance.onNotReady();
jest.restoreAllMocks();
});
describe("for a pinned widget", () => {
let renderResult: RenderResult;
let moveToContainerSpy: jest.SpyInstance<void, [room: Room, widget: IWidget, toContainer: Container]>;
beforeEach(async () => {
renderResult = render(
moveToContainerSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
});
it("should render", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
);
moveToContainerSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
});
it("should render", () => {
const { asFragment } = renderResult;
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();
});
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"));
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Right);
});
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"));
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) => {
if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === app1.id) {
(opts as ApprovalOpts).approved = false;
@ -378,21 +398,17 @@ describe("AppTile", () => {
});
// userId and creatorUserId are different
const renderResult = render(
const { container, asFragment, queryByRole } = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />
</MatrixClientContext.Provider>,
);
const { container, asFragment } = renderResult;
expect(container.querySelector(".mx_Spinner")).toBeFalsy();
expect(queryByRole("button", { name: "Continue" })).toBeInTheDocument();
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) => {
if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === app1.id) {
(opts as ApprovalOpts).approved = true;
@ -405,6 +421,7 @@ describe("AppTile", () => {
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />
</MatrixClientContext.Provider>,
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
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 () => {
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"));
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Top);
});
@ -440,36 +467,28 @@ describe("AppTile", () => {
const mockWidget = new ElementWidget(app1);
WidgetMessagingStore.instance.storeMessaging(mockWidget, r1.roomId, messaging);
});
renderResult = render(
it("should display the »Popout widget« button", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
);
});
it("should display the »Popout widget« button", () => {
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
expect(renderResult.getByLabelText("Popout widget")).toBeInTheDocument();
});
});
});
describe("for a persistent app", () => {
let renderResult: RenderResult;
beforeEach(async () => {
renderResult = render(
it("should render", async () => {
const { asFragment, queryByRole } = render(
<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>,
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
});
it("should render", async () => {
const { asFragment } = renderResult;
await waitForElementToBeRemoved(() => queryByRole("progressbar"));
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -131,7 +131,7 @@ exports[`AppTile for a pinned widget should render 1`] = `
class="mx_AppTileMenuBar_widgets"
>
<div
aria-label="Un-maximise"
aria-label="Maximise"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
@ -145,7 +145,7 @@ exports[`AppTile for a pinned widget should render 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<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>
</div>
@ -244,7 +244,7 @@ exports[`AppTile for a pinned widget should render permission request 1`] = `
class="mx_AppTileMenuBar_widgets"
>
<div
aria-label="Un-maximise"
aria-label="Maximise"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
@ -258,7 +258,7 @@ exports[`AppTile for a pinned widget should render permission request 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<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>
</div>
@ -340,8 +340,8 @@ exports[`AppTile for a pinned widget should render permission request 1`] = `
<span>
Using this widget may share data
<div
aria-describedby="_r_2n_"
aria-labelledby="_r_2m_"
aria-describedby="_r_2f_"
aria-labelledby="_r_2e_"
class="mx_TextWithTooltip_target mx_TextWithTooltip_target--helpIcon"
>
<svg