From 40fbe1fae12fb5c35096acd580525613f01683f9 Mon Sep 17 00:00:00 2001 From: rbondesson Date: Mon, 25 May 2026 09:10:10 +0200 Subject: [PATCH] Stabilise flaky QR code rendering tests (#33593) * Stabilise QR code rendering tests * Extract QR mocking code to test-utils --- apps/web/test/test-utils/qrcode.ts | 44 +++++++++++++++++++ .../components/views/elements/QRCode-test.tsx | 12 +++-- .../crypto/VerificationQRCode-test.tsx | 8 +++- .../settings/devices/LoginWithQRFlow-test.tsx | 8 +++- 4 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 apps/web/test/test-utils/qrcode.ts diff --git a/apps/web/test/test-utils/qrcode.ts b/apps/web/test/test-utils/qrcode.ts new file mode 100644 index 0000000000..3141d4fb5b --- /dev/null +++ b/apps/web/test/test-utils/qrcode.ts @@ -0,0 +1,44 @@ +/* +Copyright 2026 New Vector Ltd. + +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 { act } from "jest-matrix-react"; +import { toDataURL, type QRCodeSegment, type QRCodeToDataURLOptions } from "qrcode"; + +jest.mock("qrcode", () => ({ + ...jest.requireActual("qrcode"), + toDataURL: jest.fn(), +})); + +const realQRCode = jest.requireActual("qrcode") as { toDataURL: typeof toDataURL }; +const mockedToDataURL = jest.mocked(toDataURL); + +let qrCodeRenderPromise: Promise | undefined; + +export function mockQRCodeRender(): void { + // Keep real PNG generation, but capture the promise so the test can await it directly. + mockedToDataURL.mockImplementation(((data: string | QRCodeSegment[], options?: QRCodeToDataURLOptions) => { + qrCodeRenderPromise = realQRCode.toDataURL(data, options); + return qrCodeRenderPromise; + }) as typeof toDataURL); +} + +export async function waitForQRCodeRender(): Promise { + if (!qrCodeRenderPromise) { + throw new Error("mockQRCodeRender() must be called before waitForQRCodeRender()"); + } + + // Flush the React state update scheduled by QRCode after toDataURL resolves. + await act(async () => { + await qrCodeRenderPromise; + await Promise.resolve(); + }); +} + +export function resetQRCodeMock(): void { + mockedToDataURL.mockReset(); + qrCodeRenderPromise = undefined; +} diff --git a/apps/web/test/unit-tests/components/views/elements/QRCode-test.tsx b/apps/web/test/unit-tests/components/views/elements/QRCode-test.tsx index ec688b286b..672ed56213 100644 --- a/apps/web/test/unit-tests/components/views/elements/QRCode-test.tsx +++ b/apps/web/test/unit-tests/components/views/elements/QRCode-test.tsx @@ -6,13 +6,15 @@ 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 { render, waitFor, cleanup } from "jest-matrix-react"; +import { render, cleanup } from "jest-matrix-react"; import React from "react"; +import { mockQRCodeRender, resetQRCodeMock, waitForQRCodeRender } from "../../../../test-utils/qrcode"; import QRCode from "../../../../../src/components/views/elements/QRCode"; describe("", () => { afterEach(() => { + resetQRCodeMock(); cleanup(); }); @@ -22,14 +24,18 @@ describe("", () => { }); it("renders a QR with defaults", async () => { + mockQRCodeRender(); const { container, getAllByAltText } = render(); - await waitFor(() => getAllByAltText("QR Code").length === 1); + await waitForQRCodeRender(); + expect(getAllByAltText("QR Code")).toHaveLength(1); expect(container).toMatchSnapshot(); }); it("renders a QR with high error correction level", async () => { + mockQRCodeRender(); const { container, getAllByAltText } = render(); - await waitFor(() => getAllByAltText("QR Code").length === 1); + await waitForQRCodeRender(); + expect(getAllByAltText("QR Code")).toHaveLength(1); expect(container).toMatchSnapshot(); }); }); diff --git a/apps/web/test/unit-tests/components/views/elements/crypto/VerificationQRCode-test.tsx b/apps/web/test/unit-tests/components/views/elements/crypto/VerificationQRCode-test.tsx index 504eb8a764..13f2206da0 100644 --- a/apps/web/test/unit-tests/components/views/elements/crypto/VerificationQRCode-test.tsx +++ b/apps/web/test/unit-tests/components/views/elements/crypto/VerificationQRCode-test.tsx @@ -6,22 +6,26 @@ 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 { cleanup, render, waitFor } from "jest-matrix-react"; +import { cleanup, render } from "jest-matrix-react"; import React from "react"; +import { mockQRCodeRender, resetQRCodeMock, waitForQRCodeRender } from "../../../../../test-utils/qrcode"; import VerificationQRCode from "../../../../../../src/components/views/elements/crypto/VerificationQRCode"; describe("", () => { afterEach(() => { + resetQRCodeMock(); cleanup(); }); it("renders a QR code", async () => { + mockQRCodeRender(); const { container, getAllByAltText } = render( , ); // wait for the spinner to go away - await waitFor(() => getAllByAltText("QR Code").length === 1, { timeout: 2000 }); + await waitForQRCodeRender(); + expect(getAllByAltText("QR Code")).toHaveLength(1); expect(container).toMatchSnapshot(); }); }); diff --git a/apps/web/test/unit-tests/components/views/settings/devices/LoginWithQRFlow-test.tsx b/apps/web/test/unit-tests/components/views/settings/devices/LoginWithQRFlow-test.tsx index 5de2e70f1b..99cfa25924 100644 --- a/apps/web/test/unit-tests/components/views/settings/devices/LoginWithQRFlow-test.tsx +++ b/apps/web/test/unit-tests/components/views/settings/devices/LoginWithQRFlow-test.tsx @@ -6,10 +6,11 @@ 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 { cleanup, fireEvent, render, screen, waitFor } from "jest-matrix-react"; +import { cleanup, fireEvent, render, screen } from "jest-matrix-react"; import React from "react"; import { ClientRendezvousFailureReason, MSC4108FailureReason } from "matrix-js-sdk/src/rendezvous"; +import { mockQRCodeRender, resetQRCodeMock, waitForQRCodeRender } from "../../../../../test-utils/qrcode"; import LoginWithQRFlow from "../../../../../../src/components/views/auth/LoginWithQRFlow"; import { LoginWithQRFailureReason, type FailureReason } from "../../../../../../src/components/views/auth/LoginWithQR"; import { Click, Phase } from "../../../../../../src/components/views/auth/LoginWithQR-types"; @@ -31,6 +32,7 @@ describe("", () => { beforeEach(() => {}); afterEach(() => { + resetQRCodeMock(); onClick.mockReset(); cleanup(); }); @@ -47,11 +49,13 @@ describe("", () => { }); it("renders QR code", async () => { + mockQRCodeRender(); const { container } = render( getComponent({ phase: Phase.ShowingQR, code: new TextEncoder().encode("mock-code") }), ); // QR code is rendered async so we wait for it: - await waitFor(() => screen.getAllByAltText("QR Code").length === 1); + await waitForQRCodeRender(); + expect(screen.getAllByAltText("QR Code")).toHaveLength(1); expect(container).toMatchSnapshot(); });