diff --git a/apps/web/.eslintrc.cjs b/apps/web/.eslintrc.cjs index d984454c71..84c6d8dda9 100644 --- a/apps/web/.eslintrc.cjs +++ b/apps/web/.eslintrc.cjs @@ -234,21 +234,27 @@ module.exports = { { files: [ "src/**/*.test.{ts,tsx}", + "src/test/**/*.{ts,tsx}", "src/**/__mocks__/*.{ts,tsx}", - "src/test/*.ts", "test/**/*.{ts,tsx}", "playwright/**/*.ts", ], - extends: ["plugin:matrix-org/jest"], + extends: [], rules: { // We don't need super strict typing in test utilities "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-member-accessibility": "off", "@typescript-eslint/no-empty-object-type": "off", "@typescript-eslint/unbound-method": "off", - - // Jest/Playwright specific - + // These are fine in tests + "no-restricted-globals": "off", + "react-compiler/react-compiler": "off", + }, + }, + { + files: ["test/**/*.{ts,tsx}"], + extends: ["plugin:matrix-org/jest"], + rules: { // Disabled tests are a reality for now but as soon as all of the xits are // eliminated, we should enforce this. "jest/no-disabled-tests": "off", @@ -260,11 +266,6 @@ module.exports = { additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly"], }, ], - - // These are fine in tests - "no-restricted-globals": "off", - "react-compiler/react-compiler": "off", - "jest/no-mocks-import": "off", }, }, { diff --git a/apps/web/test/unit-tests/actions/handlers/viewUserDeviceSettings-test.ts b/apps/web/src/actions/handlers/viewUserDeviceSettings.test.ts similarity index 62% rename from apps/web/test/unit-tests/actions/handlers/viewUserDeviceSettings-test.ts rename to apps/web/src/actions/handlers/viewUserDeviceSettings.test.ts index f76c5cee20..88c96cbac6 100644 --- a/apps/web/test/unit-tests/actions/handlers/viewUserDeviceSettings-test.ts +++ b/apps/web/src/actions/handlers/viewUserDeviceSettings.test.ts @@ -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 { viewUserDeviceSettings } from "../../../../src/actions/handlers/viewUserDeviceSettings"; -import { UserTab } from "../../../../src/components/views/dialogs/UserTab"; -import { Action } from "../../../../src/dispatcher/actions"; -import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; +import { vi, describe, it, expect, beforeEach } from "vitest"; + +import { viewUserDeviceSettings } from "./viewUserDeviceSettings"; +import { UserTab } from "../../components/views/dialogs/UserTab"; +import { Action } from "../../dispatcher/actions"; +import defaultDispatcher from "../../dispatcher/dispatcher"; describe("viewUserDeviceSettings()", () => { - const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch"); + const dispatchSpy = vi.spyOn(defaultDispatcher, "dispatch"); beforeEach(() => { dispatchSpy.mockClear(); diff --git a/apps/web/test/unit-tests/components/views/Validation-test.ts b/apps/web/src/components/views/elements/Validation.test.ts similarity index 87% rename from apps/web/test/unit-tests/components/views/Validation-test.ts rename to apps/web/src/components/views/elements/Validation.test.ts index 7682213d66..54c4608896 100644 --- a/apps/web/test/unit-tests/components/views/Validation-test.ts +++ b/apps/web/src/components/views/elements/Validation.test.ts @@ -6,7 +6,9 @@ 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 withValidation from "../../../../src/components/views/elements/Validation"; +import { describe, it, expect } from "vitest"; + +import withValidation from "./Validation"; describe("Validation", () => { it("should handle 0 rules", () => { diff --git a/apps/web/src/editor/__mocks__/index.ts b/apps/web/src/editor/__mocks__/index.ts new file mode 100644 index 0000000000..d80e78ae42 --- /dev/null +++ b/apps/web/src/editor/__mocks__/index.ts @@ -0,0 +1,8 @@ +/* +Copyright 2026 Element Creations 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. +*/ + +export * from "../../../test/unit-tests/editor/mock"; diff --git a/apps/web/test/unit-tests/editor/caret-test.ts b/apps/web/src/editor/caret.test.ts similarity index 82% rename from apps/web/test/unit-tests/editor/caret-test.ts rename to apps/web/src/editor/caret.test.ts index fd56404731..a32ded7224 100644 --- a/apps/web/test/unit-tests/editor/caret-test.ts +++ b/apps/web/src/editor/caret.test.ts @@ -6,16 +6,21 @@ 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 { getLineAndNodePosition } from "../../../src/editor/caret"; -import EditorModel from "../../../src/editor/model"; -import { createPartCreator } from "./mock"; +import { describe, it, expect } from "vitest"; + +import { getLineAndNodePosition } from "./caret"; +import EditorModel from "./model"; +import { createPartCreator } from "./__mocks__"; describe("editor/caret: DOM position for caret", function () { describe("basic text handling", function () { it("at end of single line", function () { const pc = createPartCreator(); const model = new EditorModel([pc.plain("hello")], pc); - const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { index: 0, offset: 5 }); + const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { + index: 0, + offset: 5, + }); expect(lineIndex).toBe(0); expect(nodeIndex).toBe(0); expect(offset).toBe(5); @@ -23,7 +28,10 @@ describe("editor/caret: DOM position for caret", function () { it("at start of single line", function () { const pc = createPartCreator(); const model = new EditorModel([pc.plain("hello")], pc); - const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { index: 0, offset: 0 }); + const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { + index: 0, + offset: 0, + }); expect(lineIndex).toBe(0); expect(nodeIndex).toBe(0); expect(offset).toBe(0); @@ -31,7 +39,10 @@ describe("editor/caret: DOM position for caret", function () { it("at middle of single line", function () { const pc = createPartCreator(); const model = new EditorModel([pc.plain("hello")], pc); - const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { index: 0, offset: 2 }); + const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { + index: 0, + offset: 2, + }); expect(lineIndex).toBe(0); expect(nodeIndex).toBe(0); expect(offset).toBe(2); @@ -41,7 +52,10 @@ describe("editor/caret: DOM position for caret", function () { it("at start of first line which is empty", function () { const pc = createPartCreator(); const model = new EditorModel([pc.newline(), pc.plain("hello world")], pc); - const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { index: 0, offset: 0 }); + const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { + index: 0, + offset: 0, + }); expect(lineIndex).toBe(0); expect(nodeIndex).toBe(-1); expect(offset).toBe(0); @@ -49,7 +63,10 @@ describe("editor/caret: DOM position for caret", function () { it("at end of last line", function () { const pc = createPartCreator(); const model = new EditorModel([pc.plain("hello"), pc.newline(), pc.plain("world")], pc); - const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { index: 2, offset: 5 }); + const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { + index: 2, + offset: 5, + }); expect(lineIndex).toBe(1); expect(nodeIndex).toBe(0); expect(offset).toBe(5); @@ -57,7 +74,10 @@ describe("editor/caret: DOM position for caret", function () { it("at start of last line", function () { const pc = createPartCreator(); const model = new EditorModel([pc.plain("hello"), pc.newline(), pc.plain("world")], pc); - const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { index: 2, offset: 0 }); + const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { + index: 2, + offset: 0, + }); expect(lineIndex).toBe(1); expect(nodeIndex).toBe(0); expect(offset).toBe(0); @@ -65,7 +85,10 @@ describe("editor/caret: DOM position for caret", function () { it("before empty line", function () { const pc = createPartCreator(); const model = new EditorModel([pc.plain("hello"), pc.newline(), pc.newline(), pc.plain("world")], pc); - const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { index: 0, offset: 5 }); + const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { + index: 0, + offset: 5, + }); expect(lineIndex).toBe(0); expect(nodeIndex).toBe(0); expect(offset).toBe(5); @@ -73,7 +96,10 @@ describe("editor/caret: DOM position for caret", function () { it("in empty line", function () { const pc = createPartCreator(); const model = new EditorModel([pc.plain("hello"), pc.newline(), pc.newline(), pc.plain("world")], pc); - const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { index: 1, offset: 1 }); + const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { + index: 1, + offset: 1, + }); expect(lineIndex).toBe(1); expect(nodeIndex).toBe(-1); expect(offset).toBe(0); @@ -81,7 +107,10 @@ describe("editor/caret: DOM position for caret", function () { it("after empty line", function () { const pc = createPartCreator(); const model = new EditorModel([pc.plain("hello"), pc.newline(), pc.newline(), pc.plain("world")], pc); - const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { index: 3, offset: 0 }); + const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { + index: 3, + offset: 0, + }); expect(lineIndex).toBe(2); expect(nodeIndex).toBe(0); expect(offset).toBe(0); @@ -94,7 +123,10 @@ describe("editor/caret: DOM position for caret", function () { [pc.plain("hello"), pc.userPill("Alice", "@alice:hs.tld"), pc.plain("!")], pc, ); - const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { index: 1, offset: 0 }); + const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { + index: 1, + offset: 0, + }); expect(lineIndex).toBe(0); expect(nodeIndex).toBe(0); expect(offset).toBe(5); @@ -105,7 +137,10 @@ describe("editor/caret: DOM position for caret", function () { [pc.plain("hello"), pc.userPill("Alice", "@alice:hs.tld"), pc.plain("!")], pc, ); - const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { index: 1, offset: 2 }); + const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { + index: 1, + offset: 2, + }); expect(lineIndex).toBe(0); expect(nodeIndex).toBe(2); expect(offset).toBe(0); @@ -113,7 +148,10 @@ describe("editor/caret: DOM position for caret", function () { it("at start of non-editable part (without plain text around)", function () { const pc = createPartCreator(); const model = new EditorModel([pc.userPill("Alice", "@alice:hs.tld")], pc); - const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { index: 0, offset: 0 }); + const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { + index: 0, + offset: 0, + }); expect(lineIndex).toBe(0); //presumed nodes on line are (caret, pill, caret) expect(nodeIndex).toBe(0); @@ -122,7 +160,10 @@ describe("editor/caret: DOM position for caret", function () { it("in middle of non-editable part (without plain text around)", function () { const pc = createPartCreator(); const model = new EditorModel([pc.userPill("Alice", "@alice:hs.tld")], pc); - const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { index: 0, offset: 1 }); + const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { + index: 0, + offset: 1, + }); expect(lineIndex).toBe(0); //presumed nodes on line are (caret, pill, caret) expect(nodeIndex).toBe(2); @@ -134,7 +175,10 @@ describe("editor/caret: DOM position for caret", function () { [pc.userPill("Alice", "@alice:hs.tld"), pc.userPill("Bob", "@bob:hs.tld")], pc, ); - const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { index: 0, offset: 1 }); + const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { + index: 0, + offset: 1, + }); expect(lineIndex).toBe(0); //presumed nodes on line are (caret, pill, caret, pill, caret) expect(nodeIndex).toBe(2); @@ -146,7 +190,10 @@ describe("editor/caret: DOM position for caret", function () { [pc.userPill("Alice", "@alice:hs.tld"), pc.userPill("Bob", "@bob:hs.tld")], pc, ); - const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { index: 1, offset: 0 }); + const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { + index: 1, + offset: 0, + }); expect(lineIndex).toBe(0); //presumed nodes on line are (caret, pill, caret, pill, caret) expect(nodeIndex).toBe(2); @@ -158,7 +205,10 @@ describe("editor/caret: DOM position for caret", function () { [pc.userPill("Alice", "@alice:hs.tld"), pc.userPill("Bob", "@bob:hs.tld")], pc, ); - const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { index: 1, offset: 1 }); + const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, { + index: 1, + offset: 1, + }); expect(lineIndex).toBe(0); //presumed nodes on line are (caret, pill, caret, pill, caret) expect(nodeIndex).toBe(4); diff --git a/apps/web/test/unit-tests/editor/diff-test.ts b/apps/web/src/editor/diff.test.ts similarity index 98% rename from apps/web/test/unit-tests/editor/diff-test.ts rename to apps/web/src/editor/diff.test.ts index f5ec0db29e..8e0e2969d0 100644 --- a/apps/web/test/unit-tests/editor/diff-test.ts +++ b/apps/web/src/editor/diff.test.ts @@ -6,7 +6,9 @@ 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 { diffDeletion, diffAtCaret } from "../../../src/editor/diff"; +import { describe, it, expect } from "vitest"; + +import { diffDeletion, diffAtCaret } from "./diff"; describe("editor/diff", function () { describe("diffDeletion", function () { diff --git a/apps/web/test/unit-tests/editor/history-test.ts b/apps/web/src/editor/history.test.ts similarity index 97% rename from apps/web/test/unit-tests/editor/history-test.ts rename to apps/web/src/editor/history.test.ts index 7ff8916247..af651b082f 100644 --- a/apps/web/test/unit-tests/editor/history-test.ts +++ b/apps/web/src/editor/history.test.ts @@ -6,9 +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 HistoryManager, { type IHistory, MAX_STEP_LENGTH } from "../../../src/editor/history"; -import type EditorModel from "../../../src/editor/model"; -import DocumentPosition from "../../../src/editor/position"; +import { describe, it, expect } from "vitest"; + +import HistoryManager, { type IHistory, MAX_STEP_LENGTH } from "./history"; +import type EditorModel from "./model"; +import DocumentPosition from "./position"; describe("editor/history", function () { it("push, then undo", function () { @@ -107,7 +109,9 @@ describe("editor/history", function () { const firstCaret = new DocumentPosition(0, 0); history.tryPush(model, firstCaret, "insertText", {}); parts[0] = "helloo"; - const result = history.tryPush(model, new DocumentPosition(0, 0), "insertText", { added: "o" }); + const result = history.tryPush(model, new DocumentPosition(0, 0), "insertText", { + added: "o", + }); expect(result).toEqual(false); expect(history.canUndo()).toEqual(true); const undoState = history.undo(model) as IHistory; diff --git a/apps/web/test/unit-tests/editor/model-test.ts b/apps/web/src/editor/model.test.ts similarity index 98% rename from apps/web/test/unit-tests/editor/model-test.ts rename to apps/web/src/editor/model.test.ts index e1bac72884..2976a81e84 100644 --- a/apps/web/test/unit-tests/editor/model-test.ts +++ b/apps/web/src/editor/model.test.ts @@ -6,11 +6,13 @@ 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 EditorModel from "../../../src/editor/model"; -import { createPartCreator, createRenderer, type MockAutoComplete } from "./mock"; -import DocumentOffset from "../../../src/editor/offset"; -import { type PillPart } from "../../../src/editor/parts"; -import type DocumentPosition from "../../../src/editor/position"; +import { describe, it, expect } from "vitest"; + +import EditorModel from "./model"; +import { createPartCreator, createRenderer, type MockAutoComplete } from "./__mocks__"; +import DocumentOffset from "./offset"; +import { type PillPart } from "./parts"; +import type DocumentPosition from "./position"; describe("editor/model", function () { describe("plain text manipulation", function () { diff --git a/apps/web/test/unit-tests/editor/operations-test.ts b/apps/web/src/editor/operations.test.ts similarity index 98% rename from apps/web/test/unit-tests/editor/operations-test.ts rename to apps/web/src/editor/operations.test.ts index 90e98b161c..b00eec94e8 100644 --- a/apps/web/test/unit-tests/editor/operations-test.ts +++ b/apps/web/src/editor/operations.test.ts @@ -6,18 +6,20 @@ 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 EditorModel from "../../../src/editor/model"; -import { createPartCreator, createRenderer } from "./mock"; +import { describe, it, expect } from "vitest"; + +import EditorModel from "./model"; +import { createPartCreator, createRenderer } from "./__mocks__"; import { formatRange, formatRangeAsCode, formatRangeAsLink, selectRangeOfWordAtCaret, toggleInlineFormat, -} from "../../../src/editor/operations"; -import { Formatting } from "../../../src/components/views/rooms/MessageComposerFormatBar"; -import { longestBacktickSequence } from "../../../src/editor/deserialize"; -import type DocumentPosition from "../../../src/editor/position"; +} from "./operations"; +import { Formatting } from "../components/views/rooms/MessageComposerFormatBar"; +import { longestBacktickSequence } from "./deserialize"; +import type DocumentPosition from "./position"; const SERIALIZED_NEWLINE = { text: "\n", type: "newline" }; diff --git a/apps/web/test/unit-tests/editor/parts-test.ts b/apps/web/src/editor/parts.test.ts similarity index 86% rename from apps/web/test/unit-tests/editor/parts-test.ts rename to apps/web/src/editor/parts.test.ts index 6a8db13641..d5bbc69a81 100644 --- a/apps/web/test/unit-tests/editor/parts-test.ts +++ b/apps/web/src/editor/parts.test.ts @@ -6,8 +6,12 @@ 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 { EmojiPart, PlainPart } from "../../../src/editor/parts"; -import { createPartCreator } from "./mock"; +// @vitest-environment happy-dom + +import { describe, it, expect } from "vitest"; + +import { EmojiPart, PlainPart } from "./parts"; +import { createPartCreator } from "./__mocks__"; describe("editor/parts", () => { describe("appendUntilRejected", () => { diff --git a/apps/web/test/unit-tests/editor/position-test.ts b/apps/web/src/editor/position.test.ts similarity index 94% rename from apps/web/test/unit-tests/editor/position-test.ts rename to apps/web/src/editor/position.test.ts index 6f1928e05c..3d614ce020 100644 --- a/apps/web/test/unit-tests/editor/position-test.ts +++ b/apps/web/src/editor/position.test.ts @@ -6,8 +6,10 @@ 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 EditorModel from "../../../src/editor/model"; -import { createPartCreator, createRenderer } from "./mock"; +import { describe, it, expect } from "vitest"; + +import EditorModel from "./model"; +import { createPartCreator, createRenderer } from "./__mocks__"; describe("editor/position", function () { it("move first position backward in empty model", function () { diff --git a/apps/web/test/unit-tests/editor/range-test.ts b/apps/web/src/editor/range.test.ts similarity index 96% rename from apps/web/test/unit-tests/editor/range-test.ts rename to apps/web/src/editor/range.test.ts index c4e7dc05fb..fd07a8f181 100644 --- a/apps/web/test/unit-tests/editor/range-test.ts +++ b/apps/web/src/editor/range.test.ts @@ -6,8 +6,10 @@ 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 EditorModel from "../../../src/editor/model"; -import { createPartCreator, createRenderer } from "./mock"; +import { describe, it, expect } from "vitest"; + +import EditorModel from "./model"; +import { createPartCreator, createRenderer } from "./__mocks__"; const pillChannel = "#riot-dev:matrix.org"; diff --git a/apps/web/test/unit-tests/editor/roundtrip-test.ts b/apps/web/src/editor/roundtrip.test.ts similarity index 95% rename from apps/web/test/unit-tests/editor/roundtrip-test.ts rename to apps/web/src/editor/roundtrip.test.ts index 08c80a849c..615f085258 100644 --- a/apps/web/test/unit-tests/editor/roundtrip-test.ts +++ b/apps/web/src/editor/roundtrip.test.ts @@ -6,13 +6,16 @@ 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. */ +// @vitest-environment happy-dom + +import { describe, it, expect } from "vitest"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { parseEvent } from "../../../src/editor/deserialize"; -import EditorModel from "../../../src/editor/model"; -import DocumentOffset from "../../../src/editor/offset"; -import { htmlSerializeIfNeeded, textSerialize } from "../../../src/editor/serialize"; -import { createPartCreator } from "./mock"; +import { parseEvent } from "./deserialize"; +import EditorModel from "./model"; +import DocumentOffset from "./offset"; +import { htmlSerializeIfNeeded, textSerialize } from "./serialize"; +import { createPartCreator } from "./__mocks__"; function htmlMessage(formattedBody: string, msgtype = "m.text") { return { @@ -50,7 +53,7 @@ async function roundTripHtml(html: string): Promise { describe("editor/roundtrip", function () { describe("markdown messages should round-trip if they contain", function () { - test.each([ + it.each([ ["newlines", "hello\nworld"], ["pills", "text message for @room"], ["pills with interesting characters in mxid", "text message for @alice\\\\\\_\\]#>&:hs.example.com"], @@ -77,7 +80,7 @@ describe("editor/roundtrip", function () { expect(await roundTripMarkdown(markdown)).toEqual(markdown); }); - test.skip.each([ + it.skip.each([ // Removes trailing spaces ["a code block followed by newlines", "```\nfoo(bar).baz();\n\n3\n```\n\n"], // Adds a space after the code block @@ -115,7 +118,7 @@ describe("editor/roundtrip", function () { }); describe("HTML messages should round-trip if they contain", function () { - test.each([ + it.each([ ["backslashes", "C:\\Program Files"], [ "nested blockquotes", @@ -140,7 +143,7 @@ describe("editor/roundtrip", function () { expect(await roundTripHtml(html)).toEqual(html); }); - test.skip.each([ + it.skip.each([ // Strips out the pill - maybe needs some user lookup to work? ["user pills", 'Alice'], // Appends a slash to the URL diff --git a/apps/web/test/unit-tests/editor/serialize-test.ts b/apps/web/src/editor/serialize.test.ts similarity index 93% rename from apps/web/test/unit-tests/editor/serialize-test.ts rename to apps/web/src/editor/serialize.test.ts index 915b78bdfa..011a1737ea 100644 --- a/apps/web/test/unit-tests/editor/serialize-test.ts +++ b/apps/web/src/editor/serialize.test.ts @@ -6,14 +6,16 @@ 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 { mocked } from "jest-mock"; +// @vitest-environment happy-dom -import EditorModel from "../../../src/editor/model"; -import { htmlSerializeFromMdIfNeeded, htmlSerializeIfNeeded } from "../../../src/editor/serialize"; -import { createPartCreator } from "./mock"; -import { type IConfigOptions } from "../../../src/IConfigOptions"; -import SettingsStore from "../../../src/settings/SettingsStore"; -import SdkConfig from "../../../src/SdkConfig"; +import { vi, describe, it, expect, beforeEach, afterEach } from "vitest"; + +import EditorModel from "./model"; +import { htmlSerializeFromMdIfNeeded, htmlSerializeIfNeeded } from "./serialize"; +import { createPartCreator } from "./__mocks__"; +import { type IConfigOptions } from "../IConfigOptions"; +import SettingsStore from "../settings/SettingsStore"; +import SdkConfig from "../SdkConfig"; describe("editor/serialize", function () { describe("with markdown", function () { @@ -109,7 +111,7 @@ describe("editor/serialize", function () { describe("with permalink_prefix set", function () { const sdkConfigGet = SdkConfig.get; beforeEach(() => { - jest.spyOn(SdkConfig, "get").mockImplementation((key: keyof IConfigOptions, altCaseName?: string) => { + vi.spyOn(SdkConfig, "get").mockImplementation((key: keyof IConfigOptions, altCaseName?: string) => { if (key === "permalink_prefix") { return "https://element.fs.tld"; } else return sdkConfigGet(key, altCaseName); @@ -129,7 +131,7 @@ describe("editor/serialize", function () { expect(html).toBe('#room:hs.tld'); }); afterEach(() => { - mocked(SdkConfig.get).mockRestore(); + vi.mocked(SdkConfig.get).mockRestore(); }); }); }); @@ -171,7 +173,7 @@ describe("editor/serialize", function () { describe("feature_latex_maths", () => { beforeEach(() => { - jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => feature === "feature_latex_maths"); + vi.spyOn(SettingsStore, "getValue").mockImplementation((feature) => feature === "feature_latex_maths"); }); it("should support inline katex", () => { diff --git a/apps/web/test/unit-tests/components/views/emojipicker/recent-test.tsx b/apps/web/src/emojipicker/recent.test.ts similarity index 91% rename from apps/web/test/unit-tests/components/views/emojipicker/recent-test.tsx rename to apps/web/src/emojipicker/recent.test.ts index 5816300c17..3879ddbd0f 100644 --- a/apps/web/test/unit-tests/components/views/emojipicker/recent-test.tsx +++ b/apps/web/src/emojipicker/recent.test.ts @@ -5,7 +5,9 @@ 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 { translateLegacyEmojiData, mergeEmojiData } from "../../../../../src/emojipicker/recent.ts"; +import { describe, it, expect } from "vitest"; + +import { translateLegacyEmojiData, mergeEmojiData } from "./recent.ts"; describe("recent", () => { describe("translateLegacyEmojiData", () => { diff --git a/apps/web/test/unit-tests/hooks/useDebouncedCallback-test.tsx b/apps/web/src/hooks/spotlight/useDebouncedCallback.test.ts similarity index 78% rename from apps/web/test/unit-tests/hooks/useDebouncedCallback-test.tsx rename to apps/web/src/hooks/spotlight/useDebouncedCallback.test.ts index 5d0e03b930..1fa2218354 100644 --- a/apps/web/test/unit-tests/hooks/useDebouncedCallback-test.tsx +++ b/apps/web/src/hooks/spotlight/useDebouncedCallback.test.ts @@ -6,13 +6,16 @@ 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 { renderHook } from "jest-matrix-react"; +// @vitest-environment happy-dom -import { useDebouncedCallback } from "../../../src/hooks/spotlight/useDebouncedCallback"; +import { vi, describe, it, expect, beforeAll, afterAll } from "vitest"; +import { renderHook } from "test-utils-rtl"; + +import { useDebouncedCallback } from "./useDebouncedCallback"; describe("useDebouncedCallback", () => { - beforeAll(() => jest.useFakeTimers()); - afterAll(() => jest.useRealTimers()); + beforeAll(() => vi.useFakeTimers()); + afterAll(() => vi.useRealTimers()); function render(enabled: boolean, callback: (...params: any[]) => void, params: any[]) { return renderHook(({ enabled, callback, params }) => useDebouncedCallback(enabled, callback, params), { @@ -27,15 +30,15 @@ describe("useDebouncedCallback", () => { it("should be able to handle empty parameters", async () => { // When const params: any[] = []; - const callback = jest.fn(); + const callback = vi.fn(); render(true, callback, params); - jest.advanceTimersByTime(1); + vi.advanceTimersByTime(1); // Then expect(callback).toHaveBeenCalledTimes(0); // When - jest.advanceTimersByTime(500); + vi.advanceTimersByTime(500); // Then expect(callback).toHaveBeenCalledTimes(1); @@ -44,9 +47,9 @@ describe("useDebouncedCallback", () => { it("should call the callback with the parameters", async () => { // When const params = ["USER NAME"]; - const callback = jest.fn(); + const callback = vi.fn(); render(true, callback, params); - jest.advanceTimersByTime(500); + vi.advanceTimersByTime(500); // Then expect(callback).toHaveBeenCalledTimes(1); @@ -56,12 +59,12 @@ describe("useDebouncedCallback", () => { it("should call the callback with the parameters when parameters change during the timeout", async () => { // When const params = ["USER NAME"]; - const callback = jest.fn(); + const callback = vi.fn(); const { rerender } = render(true, callback, []); - jest.advanceTimersByTime(1); + vi.advanceTimersByTime(1); rerender({ enabled: true, callback, params }); - jest.advanceTimersByTime(500); + vi.advanceTimersByTime(500); // Then expect(callback).toHaveBeenCalledTimes(1); @@ -71,12 +74,12 @@ describe("useDebouncedCallback", () => { it("should handle multiple parameters", async () => { // When const params = [4, 8, 15, 16, 23, 42]; - const callback = jest.fn(); + const callback = vi.fn(); const { rerender } = render(true, callback, []); - jest.advanceTimersByTime(1); + vi.advanceTimersByTime(1); rerender({ enabled: true, callback, params }); - jest.advanceTimersByTime(500); + vi.advanceTimersByTime(500); // Then expect(callback).toHaveBeenCalledTimes(1); @@ -100,17 +103,17 @@ describe("useDebouncedCallback", () => { "USER NAM", "USER NAME", ]; - const callback = jest.fn(); + const callback = vi.fn(); const { rerender } = render(true, callback, []); - jest.advanceTimersByTime(1); + vi.advanceTimersByTime(1); for (const query of queries) { rerender({ enabled: true, callback, params: [query] }); - jest.advanceTimersByTime(50); + vi.advanceTimersByTime(50); } - jest.advanceTimersByTime(500); + vi.advanceTimersByTime(500); // Then const query = queries[queries.length - 1]; @@ -135,16 +138,16 @@ describe("useDebouncedCallback", () => { "USER NAM", "USER NAME", ]; - const callback = jest.fn(); + const callback = vi.fn(); const { rerender } = render(true, callback, []); - jest.advanceTimersByTime(1); + vi.advanceTimersByTime(1); for (const query of queries) { rerender({ enabled: true, callback, params: [query] }); - jest.advanceTimersByTime(200); + vi.advanceTimersByTime(200); } - jest.advanceTimersByTime(500); + vi.advanceTimersByTime(500); // Then const query = queries[queries.length - 1]; @@ -169,16 +172,16 @@ describe("useDebouncedCallback", () => { "USER NAM", "USER NAME", ]; - const callback = jest.fn(); + const callback = vi.fn(); const { rerender } = render(false, callback, []); - jest.advanceTimersByTime(1); + vi.advanceTimersByTime(1); for (const query of queries) { rerender({ enabled: false, callback, params: [query] }); - jest.advanceTimersByTime(200); + vi.advanceTimersByTime(200); } - jest.advanceTimersByTime(500); + vi.advanceTimersByTime(500); // Then expect(callback).toHaveBeenCalledTimes(0); diff --git a/apps/web/test/unit-tests/hooks/useLatestResult-test.tsx b/apps/web/src/hooks/useLatestResult.test.ts similarity index 87% rename from apps/web/test/unit-tests/hooks/useLatestResult-test.tsx rename to apps/web/src/hooks/useLatestResult.test.ts index 74848b0b9d..db32d3d290 100644 --- a/apps/web/test/unit-tests/hooks/useLatestResult-test.tsx +++ b/apps/web/src/hooks/useLatestResult.test.ts @@ -6,14 +6,17 @@ 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 { renderHook, type RenderHookResult } from "jest-matrix-react"; +// @vitest-environment happy-dom -import { useLatestResult } from "../../../src/hooks/useLatestResult"; +import { vi, describe, it, expect, beforeEach } from "vitest"; +import { renderHook, type RenderHookResult } from "test-utils-rtl"; + +import { useLatestResult } from "./useLatestResult"; // All tests use fake timers throughout, comments will show the elapsed time in ms -jest.useFakeTimers(); +vi.useFakeTimers(); -const mockSetter = jest.fn(); +const mockSetter = vi.fn(); beforeEach(() => { mockSetter.mockClear(); @@ -39,7 +42,7 @@ describe("renderhook tests", () => { expect(mockSetter).not.toHaveBeenCalled(); // advance timer until the timeout elapses, check we have called the setter - jest.advanceTimersToNextTimer(); + vi.advanceTimersToNextTimer(); expect(mockSetter).toHaveBeenCalledTimes(1); expect(mockSetter).toHaveBeenLastCalledWith(query.result); }); @@ -54,13 +57,13 @@ describe("renderhook tests", () => { simulateRequest(hookResult, fastQuery); // advance to fastQuery response, check the setter call - jest.advanceTimersToNextTimer(); + vi.advanceTimersToNextTimer(); expect(mockSetter).toHaveBeenCalledTimes(1); expect(mockSetter).toHaveBeenLastCalledWith(fastQuery.result); // advance time to slowQuery response, check the setter has _not_ been // called again and that the result is still from the fast query - jest.advanceTimersToNextTimer(); + vi.advanceTimersToNextTimer(); expect(mockSetter).toHaveBeenCalledTimes(1); expect(mockSetter).toHaveBeenLastCalledWith(fastQuery.result); }); @@ -75,23 +78,23 @@ describe("renderhook tests", () => { // ELAPSED: 0ms, no queries sent simulateRequest(hookResult, query1); - jest.advanceTimersByTime(100); + vi.advanceTimersByTime(100); // ELAPSED: 100ms, query1 sent, no responses expect(mockSetter).not.toHaveBeenCalled(); simulateRequest(hookResult, query2); - jest.advanceTimersByTime(70); + vi.advanceTimersByTime(70); // ELAPSED: 170ms, query1 and query2 sent, no responses expect(mockSetter).not.toHaveBeenCalled(); simulateRequest(hookResult, query3); - jest.advanceTimersByTime(70); + vi.advanceTimersByTime(70); // ELAPSED: 240ms, all queries sent, responses for query1 and query2 expect(mockSetter).not.toHaveBeenCalled(); // ELAPSED: 360ms, all queries sent, all queries have responses - jest.advanceTimersByTime(120); + vi.advanceTimersByTime(120); expect(mockSetter).toHaveBeenLastCalledWith(query3.result); }); @@ -104,17 +107,17 @@ describe("renderhook tests", () => { // ELAPSED: 0ms, no queries sent simulateRequest(hookResult, query1); - jest.advanceTimersByTime(5); + vi.advanceTimersByTime(5); // ELAPSED: 5ms, query1 sent, response from query1 expect(mockSetter).toHaveBeenCalledTimes(1); expect(mockSetter).toHaveBeenLastCalledWith(query1.result); simulateRequest(hookResult, query2); - jest.advanceTimersByTime(5); + vi.advanceTimersByTime(5); // ELAPSED: 10ms, query1 and query2 sent, response from query1 simulateRequest(hookResult, query3); - jest.advanceTimersByTime(5); + vi.advanceTimersByTime(5); // ELAPSED: 15ms, all queries sent, responses from query1 and query3 expect(mockSetter).toHaveBeenCalledTimes(2); @@ -122,7 +125,7 @@ describe("renderhook tests", () => { // ELAPSED: 65ms, all queries sent, all queries have responses // so check that the result is still from query3, not query2 - jest.advanceTimersByTime(50); + vi.advanceTimersByTime(50); expect(mockSetter).toHaveBeenLastCalledWith(query3.result); }); }); diff --git a/apps/web/test/unit-tests/hooks/useWindowWidth-test.ts b/apps/web/src/hooks/useWindowWidth.test.ts similarity index 77% rename from apps/web/test/unit-tests/hooks/useWindowWidth-test.ts rename to apps/web/src/hooks/useWindowWidth.test.ts index 9ddc9750e2..010027df24 100644 --- a/apps/web/test/unit-tests/hooks/useWindowWidth-test.ts +++ b/apps/web/src/hooks/useWindowWidth.test.ts @@ -6,10 +6,13 @@ 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 { renderHook, act } from "jest-matrix-react"; +// @vitest-environment happy-dom -import UIStore, { UI_EVENTS } from "../../../src/stores/UIStore"; -import { useWindowWidth } from "../../../src/hooks/useWindowWidth"; +import { describe, it, expect, beforeEach } from "vitest"; +import { renderHook, act } from "test-utils-rtl"; + +import UIStore, { UI_EVENTS } from "../stores/UIStore"; +import { useWindowWidth } from "./useWindowWidth"; describe("useWindowWidth", () => { beforeEach(() => { diff --git a/apps/web/src/test/setupGlobals.ts b/apps/web/src/test/setupGlobals.ts index f1cd3153d7..83aa3a0c57 100644 --- a/apps/web/src/test/setupGlobals.ts +++ b/apps/web/src/test/setupGlobals.ts @@ -25,5 +25,6 @@ if (globalThis.window === undefined) { // things try to access it before the beforeEach blocks run) addEventListener: vi.fn(), location: locationStub, + setTimeout: globalThis.setTimeout, }); } diff --git a/apps/web/test/unit-tests/editor/mock.ts b/apps/web/test/unit-tests/editor/mock.ts index e7bbe1c8ba..29ecc4cf50 100644 --- a/apps/web/test/unit-tests/editor/mock.ts +++ b/apps/web/test/unit-tests/editor/mock.ts @@ -13,6 +13,7 @@ import type AutocompleteWrapperModel from "../../../src/editor/autocomplete"; import { type Caret } from "../../../src/editor/caret"; import { type PillPart, type Part, PartCreator } from "../../../src/editor/parts"; import DocumentPosition from "../../../src/editor/position"; +import { vi } from "../../setup/adapter.ts"; export class MockAutoComplete { public _updateCallback; @@ -68,8 +69,8 @@ export function createPartCreator(completions: PillPart[] = []) { }; const room = new MockRoom() as unknown as Room; const client = { - getRooms: jest.fn().mockReturnValue([]), - getRoom: jest.fn().mockReturnValue(null), + getRooms: vi.fn().mockReturnValue([]), + getRoom: vi.fn().mockReturnValue(null), } as unknown as MatrixClient; return new PartCreator(room, client, autoCompleteCreator); }