diff --git a/frontend/__tests__/setup-jsdom.ts b/frontend/__tests__/setup-jsdom.ts
new file mode 100644
index 000000000000..ae671c34afdb
--- /dev/null
+++ b/frontend/__tests__/setup-jsdom.ts
@@ -0,0 +1,6 @@
+import $ from "jquery";
+
+//@ts-expect-error add to global
+global["$"] = $;
+//@ts-expect-error add to global
+global["jQuery"] = $;
diff --git a/frontend/__tests__/setup-tests.ts b/frontend/__tests__/setup-tests.ts
index c2de9f3545fb..88e3e4ec4075 100644
--- a/frontend/__tests__/setup-tests.ts
+++ b/frontend/__tests__/setup-tests.ts
@@ -2,9 +2,9 @@ import { vi } from "vitest";
import $ from "jquery";
import { ElementWithUtils } from "../src/ts/utils/dom";
-//@ts-expect-error add to globl
+//@ts-expect-error add to global
global["$"] = $;
-//@ts-expect-error add to globl
+//@ts-expect-error add to global
global["jQuery"] = $;
vi.mock("../src/ts/constants/env-config", () => ({
diff --git a/frontend/__tests__/tsconfig.json b/frontend/__tests__/tsconfig.json
index 135644168739..97e01a3900fa 100644
--- a/frontend/__tests__/tsconfig.json
+++ b/frontend/__tests__/tsconfig.json
@@ -6,5 +6,5 @@
"noEmit": true
},
"files": ["vitest.d.ts"],
- "include": ["./**/*.spec.ts", "./setup-tests.ts"]
+ "include": ["./**/*.spec.ts", "./**/*.jsdom-spec.ts", "./setup-tests.ts"]
}
diff --git a/frontend/__tests__/utils/dom.jsdom-spec.ts b/frontend/__tests__/utils/dom.jsdom-spec.ts
new file mode 100644
index 000000000000..8d38ebf56f40
--- /dev/null
+++ b/frontend/__tests__/utils/dom.jsdom-spec.ts
@@ -0,0 +1,171 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { screen } from "@testing-library/dom";
+import { userEvent } from "@testing-library/user-event";
+import { ElementWithUtils, qsr } from "../../src/ts/utils/dom";
+
+describe("dom", () => {
+ describe("ElementWithUtils", () => {
+ describe("onChild", () => {
+ const handler = vi.fn();
+
+ function registerOnChild(
+ event: string,
+ selector: string,
+ options?: {
+ parent?: ElementWithUtils;
+ },
+ ): void {
+ const parent = options?.parent ?? qsr("#parent");
+ parent?.onChild(event, selector, (e) =>
+ handler({
+ target: e.target,
+ childTarget: e.childTarget,
+ //@ts-expect-error will be added later, check TODO on the ChildEvent
+ currentTarget: e.currentTarget,
+ }),
+ );
+ }
+
+ beforeEach(() => {
+ handler.mockReset();
+
+ document.body.innerHTML = "";
+ const root = document.createElement("div");
+
+ root.innerHTML = `
+
+
+
+
test
+
+ test
+
+
+
+
+
+
+ `;
+ document.body.appendChild(root);
+ });
+
+ it("should not fire when parent element is clicked", async () => {
+ //GIVEN
+ registerOnChild("click", "div");
+
+ //WHEN
+ await userEvent.click(screen.getByTestId("parent"));
+
+ //THEN
+ expect(handler).not.toHaveBeenCalled();
+ });
+
+ it("should not fire when selector doesnt match", async () => {
+ //GIVEN
+ const buttonEl = qsr("#button");
+ registerOnChild("click", "div", { parent: buttonEl });
+
+ //WHEN
+ await userEvent.click(screen.getByTestId("icon"));
+
+ //THEN
+ expect(handler).not.toHaveBeenCalled();
+ });
+
+ it("should fire when selector is clicked", async () => {
+ //GIVEN
+ registerOnChild("click", "div");
+
+ //WHEN
+ const clickTarget = screen.getByTestId("mid1");
+ await userEvent.click(clickTarget);
+
+ //THEN
+ expect(handler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ target: clickTarget,
+ childTarget: clickTarget,
+ currentTarget: screen.getByTestId("parent"),
+ }),
+ );
+ });
+
+ it("should fire when child of selector is clicked", async () => {
+ //GIVEN
+ registerOnChild("click", "div.middle");
+
+ //WHEN
+ const selectorTarget = screen.getByTestId("mid1");
+ const clickTarget = screen.getByTestId("button");
+ await userEvent.click(clickTarget);
+
+ //THEN
+ expect(handler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ target: clickTarget,
+ childTarget: selectorTarget,
+ currentTarget: screen.getByTestId("parent"),
+ }),
+ );
+ });
+
+ it("should fire on each element matching the selector from the child up to the parent", async () => {
+ //GIVEN
+ registerOnChild("click", "div.middle, div.inner");
+
+ //WHEN
+ let clickTarget = screen.getByTestId("button");
+ await userEvent.click(clickTarget);
+
+ //THEN
+
+ //This is the same behavior as jQuery `.on` with selector.
+ //The handler will be called two times,
+ //It does NOT call on the or the parent element itself
+ expect(handler).toHaveBeenCalledTimes(2);
+
+ //First call is for childTarget inner2 (grand child of parent)
+ expect(handler).toHaveBeenNthCalledWith(
+ 1,
+ expect.objectContaining({
+ target: clickTarget,
+ childTarget: screen.getByTestId("inner2"),
+ currentTarget: screen.getByTestId("parent"),
+ }),
+ );
+
+ //Second call is for childTarget mid1 (child of parent)
+ expect(handler).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({
+ target: clickTarget,
+ childTarget: screen.getByTestId("mid1"),
+ currentTarget: screen.getByTestId("parent"),
+ }),
+ );
+
+ //WHEN click on mid1 handler is only called one time
+ handler.mockReset();
+ clickTarget = screen.getByTestId("mid1");
+ await userEvent.click(clickTarget);
+
+ //THEN
+ expect(handler).toHaveBeenCalledTimes(1);
+
+ expect(handler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ target: clickTarget,
+ childTarget: clickTarget,
+ currentTarget: screen.getByTestId("parent"),
+ }),
+ );
+ });
+ });
+ });
+});
diff --git a/frontend/package.json b/frontend/package.json
index 45e8aa050c2f..642a216eb95d 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -61,6 +61,8 @@
"@fortawesome/fontawesome-free": "5.15.4",
"@monkeytype/oxlint-config": "workspace:*",
"@monkeytype/typescript-config": "workspace:*",
+ "@testing-library/dom": "10.4.1",
+ "@testing-library/user-event": "14.6.1",
"@types/canvas-confetti": "1.4.3",
"@types/chartjs-plugin-trendline": "1.0.1",
"@types/damerau-levenshtein": "1.0.0",
diff --git a/frontend/src/ts/elements/input-validation.ts b/frontend/src/ts/elements/input-validation.ts
index 42bb455a2d20..21316ae46025 100644
--- a/frontend/src/ts/elements/input-validation.ts
+++ b/frontend/src/ts/elements/input-validation.ts
@@ -8,7 +8,7 @@ import {
} from "@monkeytype/schemas/configs";
import Config, { setConfig } from "../config";
import * as Notifications from "../elements/notifications";
-import { DomUtilsEvent, ElementWithUtils } from "../utils/dom";
+import { ElementWithUtils } from "../utils/dom";
export type ValidationResult = {
status: "checking" | "success" | "failed" | "warning";
@@ -60,7 +60,7 @@ export function createInputEventHandler(
callback: (result: ValidationResult) => void,
validation: Validation,
inputValueConvert?: (val: string) => T,
-): (e: DomUtilsEvent) => Promise {
+): (e: Event) => Promise {
let callIsValid =
validation.isValid !== undefined
? debounceIfNeeded(
diff --git a/frontend/src/ts/utils/dom.ts b/frontend/src/ts/utils/dom.ts
index 14872fcfd3bf..d6f894c5c7cb 100644
--- a/frontend/src/ts/utils/dom.ts
+++ b/frontend/src/ts/utils/dom.ts
@@ -105,11 +105,18 @@ type ElementWithValue =
| HTMLTextAreaElement
| HTMLSelectElement;
-export type DomUtilsEvent = Omit;
+//TODO: after the migration from jQuery to dom-utils we might want to add currentTarget back to the event object, if we have a use-case for it.
+// For now we remove it because currentTarget is not the same element when using dom-utils intead of jQuery to get compile errors.
+export type OnChildEvent = Omit & {
+ /**
+ * target element matching the selector. This emulates the behavior of `currentTarget` in jQuery events registered with `.on(events, selector, handler)`
+ */
+ childTarget: EventTarget | null;
+};
-type DomUtilsEventListenerOrEventListenerObject =
- | { (evt: DomUtilsEvent): void }
- | { handleEvent(object: DomUtilsEvent): void };
+type OnChildEventListenerOrEventListenerObject =
+ | { (evt: OnChildEvent): void }
+ | { handleEvent(object: OnChildEvent): void };
export class ElementWithUtils {
/**
@@ -244,56 +251,84 @@ export class ElementWithUtils {
*/
on(
event: K,
- handler: (this: T, ev: DomUtilsEvent) => void,
+ handler: (this: T, ev: HTMLElementEventMap[K]) => void,
): this;
- on(event: string, handler: DomUtilsEventListenerOrEventListenerObject): this;
+ on(event: string, handler: EventListenerOrEventListenerObject): this;
on(
event: keyof HTMLElementEventMap | string,
handler:
- | DomUtilsEventListenerOrEventListenerObject
- | ((this: T, ev: DomUtilsEvent) => void),
+ | EventListenerOrEventListenerObject
+ | ((this: T, ev: Event) => void),
): this {
// this type was some AI magic but if it works it works
this.native.addEventListener(
event,
- handler as DomUtilsEventListenerOrEventListenerObject,
+ handler as EventListenerOrEventListenerObject,
);
return this;
}
/**
- * Attach an event listener to child elements matching the query.
+ * Attach an event listener to child elements matching the selector.
* Useful for dynamically added elements.
+ *
+ * The handler is not called when the event occurs directly on the bound element, but only for descendants (inner elements)
+ * that match the selector. Bubbles the event from the event target up to the element where the handler is attached
+ * (i.e., innermost to outermost element) and runs the handler for any elements along that path matching the selector.
*/
onChild(
event: K,
- query: string,
+ /**
+ * A selector string to filter the descendants of the selected elements that will call the handler.
+ */
+ selector: string,
handler: (
this: HTMLElement,
- ev: DomUtilsEvent,
+ ev: OnChildEvent,
) => void,
): this;
onChild(
event: string,
- query: string,
- handler: DomUtilsEventListenerOrEventListenerObject,
+ /**
+ * A selector string to filter the descendants of the selected elements that will call the handler.
+ */
+ selector: string,
+ handler: OnChildEventListenerOrEventListenerObject,
): this;
onChild(
event: keyof HTMLElementEventMap | string,
- query: string,
+ /**
+ * A selector string to filter the descendants of the selected elements that will call the handler.
+ */
+ selector: string,
handler:
- | DomUtilsEventListenerOrEventListenerObject
- | ((this: HTMLElement, ev: DomUtilsEvent) => void),
+ | OnChildEventListenerOrEventListenerObject
+ | ((this: HTMLElement, ev: OnChildEvent) => void),
): this {
- // this type was some AI magic but if it works it works
this.native.addEventListener(event, (e) => {
const target = e.target as HTMLElement;
- if (target !== null && target.matches(query)) {
+ if (target === null) return; //ignore event
+
+ let childTarget = target.closest(selector);
+ //bubble up until no match found or the parent element is reached
+ while (
+ childTarget !== null &&
+ childTarget !== this.native && //stop on parent
+ this.native.contains(childTarget) //stop above parent
+ ) {
if (typeof handler === "function") {
- handler.call(target, e);
+ handler.call(
+ childTarget as HTMLElement,
+ Object.assign(e, { childTarget }),
+ );
} else {
- handler.handleEvent(e);
+ handler.handleEvent(Object.assign(e, { childTarget }));
}
+
+ childTarget =
+ childTarget.parentElement !== null
+ ? childTarget.parentElement.closest(selector)
+ : null;
}
});
return this;
@@ -680,21 +715,20 @@ export class ElementsWithUtils<
*/
on(
event: K,
- handler: (this: T, ev: DomUtilsEvent) => void,
+ handler: (this: T, ev: HTMLElementEventMap[K]) => void,
): this;
- on(event: string, handler: DomUtilsEventListenerOrEventListenerObject): this;
+ on(event: string, handler: EventListenerOrEventListenerObject): this;
on(
event: keyof HTMLElementEventMap | string,
handler:
- | DomUtilsEventListenerOrEventListenerObject
- | ((this: T, ev: DomUtilsEvent) => void),
+ | EventListenerOrEventListenerObject
+ | ((this: T, ev: Event) => void),
): this {
for (const item of this) {
item.on(event, handler);
}
return this;
}
-
/**
* Set attribute value on all elements in the array
*/
diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts
index 28ec5c5802c2..1b720dddc1b0 100644
--- a/frontend/vitest.config.ts
+++ b/frontend/vitest.config.ts
@@ -1,13 +1,39 @@
-import { defineConfig } from "vitest/config";
+import { defineConfig, UserWorkspaceConfig } from "vitest/config";
import { languageHashes } from "./vite-plugins/language-hashes";
import { envConfig } from "./vite-plugins/env-config";
+const plugins = [
+ languageHashes({ skip: true }),
+ envConfig({ isDevelopment: true, clientVersion: "TESTING", env: {} }),
+];
+
+export const projects: UserWorkspaceConfig[] = [
+ {
+ test: {
+ name: { label: "unit", color: "blue" },
+ include: ["__tests__/**/*.spec.ts"],
+ exclude: ["__tests__/**/*.jsdom-spec.ts"],
+ environment: "happy-dom",
+ globalSetup: "__tests__/global-setup.ts",
+ setupFiles: ["__tests__/setup-tests.ts"],
+ },
+ plugins,
+ },
+ {
+ test: {
+ name: { label: "jsdom", color: "yellow" },
+ include: ["__tests__/**/*.jsdom-spec.ts"],
+ exclude: ["__tests__/**/*.spec.ts"],
+ environment: "happy-dom",
+ globalSetup: "__tests__/global-setup.ts",
+ setupFiles: ["__tests__/setup-jsdom.ts"],
+ },
+ plugins,
+ },
+];
export default defineConfig({
test: {
- environment: "happy-dom",
- globalSetup: "__tests__/global-setup.ts",
- setupFiles: ["__tests__/setup-tests.ts"],
-
+ projects: projects,
coverage: {
include: ["**/*.ts"],
},
@@ -19,9 +45,4 @@ export default defineConfig({
},
},
},
-
- plugins: [
- languageHashes({ skip: true }),
- envConfig({ isDevelopment: true, clientVersion: "TESTING", env: {} }),
- ],
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 17e59006f00c..ad546b1d9e09 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -370,6 +370,12 @@ importers:
'@monkeytype/typescript-config':
specifier: workspace:*
version: link:../packages/typescript-config
+ '@testing-library/dom':
+ specifier: 10.4.1
+ version: 10.4.1
+ '@testing-library/user-event':
+ specifier: 14.6.1
+ version: 14.6.1(@testing-library/dom@10.4.1)
'@types/canvas-confetti':
specifier: 1.4.3
version: 1.4.3
@@ -1198,10 +1204,6 @@ packages:
resolution: {integrity: sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==}
engines: {node: '>=6.9.0'}
- '@babel/runtime@7.28.2':
- resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==}
- engines: {node: '>=6.9.0'}
-
'@babel/runtime@7.28.4':
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'}
@@ -3111,6 +3113,16 @@ packages:
'@surma/rollup-plugin-off-main-thread@2.2.3':
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
+ '@testing-library/dom@10.4.1':
+ resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
+ engines: {node: '>=18'}
+
+ '@testing-library/user-event@14.6.1':
+ resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==}
+ engines: {node: '>=12', npm: '>=6'}
+ peerDependencies:
+ '@testing-library/dom': '>=7.21.4'
+
'@tootallnate/once@2.0.0':
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
engines: {node: '>= 10'}
@@ -3173,6 +3185,9 @@ packages:
'@tsconfig/node16@1.0.4':
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
+ '@types/aria-query@5.0.4':
+ resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
+
'@types/bcrypt@5.0.2':
resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==}
@@ -3552,10 +3567,6 @@ packages:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
- ansi-regex@6.0.1:
- resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
- engines: {node: '>=12'}
-
ansi-regex@6.2.2:
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
engines: {node: '>=12'}
@@ -3611,6 +3622,9 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ aria-query@5.3.0:
+ resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
+
arity-n@1.0.4:
resolution: {integrity: sha512-fExL2kFDC1Q2DUOx3whE/9KoN66IzkY4b4zUHUBFM1ojEYjZZYDcUW3bek/ufGionX9giIKDC5redH2IlGqcQQ==}
@@ -4628,6 +4642,10 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+
destroy@1.2.0:
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
@@ -4715,6 +4733,9 @@ packages:
resolution: {integrity: sha512-iND4mcOWhPaCNh54WmK/KoSb35AFqPAUWFMffTQcp52uQt36b5uNwEJTSXntJZBbeGad72Crbi/hvDIv6us/6Q==}
engines: {node: '>= 8.0'}
+ dom-accessibility-api@0.5.16:
+ resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
+
dom-serializer@1.4.1:
resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==}
@@ -6452,6 +6473,10 @@ packages:
resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==}
engines: {node: '>=12'}
+ lz-string@1.5.0:
+ resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
+ hasBin: true
+
lz-ts@1.1.2:
resolution: {integrity: sha512-ye8sVndmvzs46cPgX1Yjlk3o/Sueu0VHn253rKpsWiK2/bAbsVkD7DEJiaueiPfbZTi17GLRPkv3W5O3BUNd2g==}
@@ -7552,6 +7577,10 @@ packages:
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
engines: {node: ^14.13.1 || >=16.0.0}
+ pretty-format@27.5.1:
+ resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
+ engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+
pretty-format@29.7.0:
resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -7715,6 +7744,9 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+ react-is@17.0.2:
+ resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
@@ -10225,8 +10257,6 @@ snapshots:
dependencies:
regenerator-runtime: 0.14.1
- '@babel/runtime@7.28.2': {}
-
'@babel/runtime@7.28.4': {}
'@babel/template@7.25.0':
@@ -12194,6 +12224,21 @@ snapshots:
magic-string: 0.25.9
string.prototype.matchall: 4.0.12
+ '@testing-library/dom@10.4.1':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/runtime': 7.28.4
+ '@types/aria-query': 5.0.4
+ aria-query: 5.3.0
+ dom-accessibility-api: 0.5.16
+ lz-string: 1.5.0
+ picocolors: 1.1.1
+ pretty-format: 27.5.1
+
+ '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)':
+ dependencies:
+ '@testing-library/dom': 10.4.1
+
'@tootallnate/once@2.0.0': {}
'@tootallnate/quickjs-emscripten@0.23.0': {}
@@ -12240,6 +12285,8 @@ snapshots:
'@tsconfig/node16@1.0.4': {}
+ '@types/aria-query@5.0.4': {}
+
'@types/bcrypt@5.0.2':
dependencies:
'@types/node': 24.9.1
@@ -12734,8 +12781,6 @@ snapshots:
ansi-regex@5.0.1: {}
- ansi-regex@6.0.1: {}
-
ansi-regex@6.2.2: {}
ansi-styles@4.3.0:
@@ -12790,6 +12835,10 @@ snapshots:
argparse@2.0.1: {}
+ aria-query@5.3.0:
+ dependencies:
+ dequal: 2.0.3
+
arity-n@1.0.4: {}
array-buffer-byte-length@1.0.2:
@@ -13853,6 +13902,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ dequal@2.0.3: {}
+
destroy@1.2.0: {}
detect-libc@2.0.3: {}
@@ -13956,6 +14007,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ dom-accessibility-api@0.5.16: {}
+
dom-serializer@1.4.1:
dependencies:
domelementtype: 2.3.0
@@ -16181,6 +16234,8 @@ snapshots:
luxon@3.4.4: {}
+ lz-string@1.5.0: {}
+
lz-ts@1.1.2: {}
madge@8.0.0(typescript@5.9.3):
@@ -16426,7 +16481,7 @@ snapshots:
mjml-accordion@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16434,7 +16489,7 @@ snapshots:
mjml-body@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16442,7 +16497,7 @@ snapshots:
mjml-button@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16450,7 +16505,7 @@ snapshots:
mjml-carousel@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16458,7 +16513,7 @@ snapshots:
mjml-cli@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
chokidar: 3.6.0
glob: 10.4.5
html-minifier: 4.0.0
@@ -16474,7 +16529,7 @@ snapshots:
mjml-column@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16482,7 +16537,7 @@ snapshots:
mjml-core@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
cheerio: 1.0.0-rc.12
detect-node: 2.1.0
html-minifier: 4.0.0
@@ -16497,7 +16552,7 @@ snapshots:
mjml-divider@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16505,7 +16560,7 @@ snapshots:
mjml-group@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16513,7 +16568,7 @@ snapshots:
mjml-head-attributes@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16521,7 +16576,7 @@ snapshots:
mjml-head-breakpoint@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16529,7 +16584,7 @@ snapshots:
mjml-head-font@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16537,7 +16592,7 @@ snapshots:
mjml-head-html-attributes@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16545,7 +16600,7 @@ snapshots:
mjml-head-preview@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16553,7 +16608,7 @@ snapshots:
mjml-head-style@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16561,7 +16616,7 @@ snapshots:
mjml-head-title@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16569,7 +16624,7 @@ snapshots:
mjml-head@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16577,7 +16632,7 @@ snapshots:
mjml-hero@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16585,7 +16640,7 @@ snapshots:
mjml-image@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16593,7 +16648,7 @@ snapshots:
mjml-migrate@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
js-beautify: 1.15.1
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
@@ -16604,7 +16659,7 @@ snapshots:
mjml-navbar@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16612,14 +16667,14 @@ snapshots:
mjml-parser-xml@4.15.0:
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
detect-node: 2.1.0
htmlparser2: 9.1.0
lodash: 4.17.21
mjml-preset-core@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
mjml-accordion: 4.15.0(encoding@0.1.13)
mjml-body: 4.15.0(encoding@0.1.13)
mjml-button: 4.15.0(encoding@0.1.13)
@@ -16650,7 +16705,7 @@ snapshots:
mjml-raw@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16658,7 +16713,7 @@ snapshots:
mjml-section@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16666,7 +16721,7 @@ snapshots:
mjml-social@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16674,7 +16729,7 @@ snapshots:
mjml-spacer@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16682,7 +16737,7 @@ snapshots:
mjml-table@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16690,7 +16745,7 @@ snapshots:
mjml-text@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -16698,11 +16753,11 @@ snapshots:
mjml-validator@4.13.0:
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
mjml-wrapper@4.15.0(encoding@0.1.13):
dependencies:
- '@babel/runtime': 7.25.0
+ '@babel/runtime': 7.28.4
lodash: 4.17.21
mjml-core: 4.15.0(encoding@0.1.13)
mjml-section: 4.15.0(encoding@0.1.13)
@@ -17393,7 +17448,7 @@ snapshots:
polished@4.3.1:
dependencies:
- '@babel/runtime': 7.28.2
+ '@babel/runtime': 7.28.4
portfinder@1.0.32:
dependencies:
@@ -17478,6 +17533,12 @@ snapshots:
pretty-bytes@6.1.1: {}
+ pretty-format@27.5.1:
+ dependencies:
+ ansi-regex: 5.0.1
+ ansi-styles: 5.2.0
+ react-is: 17.0.2
+
pretty-format@29.7.0:
dependencies:
'@jest/schemas': 29.6.3
@@ -17667,6 +17728,8 @@ snapshots:
react-is@16.13.1: {}
+ react-is@17.0.2: {}
+
react-is@18.3.1: {}
react-tabs@6.0.2(react@18.3.1):
@@ -18561,7 +18624,7 @@ snapshots:
strip-ansi@7.1.0:
dependencies:
- ansi-regex: 6.0.1
+ ansi-regex: 6.2.2
strip-ansi@7.1.2:
dependencies:
@@ -19208,7 +19271,7 @@ snapshots:
dependencies:
browserslist: 4.23.3
escalade: 3.2.0
- picocolors: 1.0.1
+ picocolors: 1.1.1
update-browserslist-db@1.1.3(browserslist@4.24.4):
dependencies:
diff --git a/vitest.config.ts b/vitest.config.ts
index 5702907d3c2c..22005f4254fc 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -1,15 +1,37 @@
import { defineConfig, UserWorkspaceConfig } from "vitest/config";
import { projects as backendProjects } from "./backend/vitest.config";
+import { projects as frontendProjects } from "./frontend/vitest.config";
export default defineConfig({
test: {
projects: [
- ...backendProjects.map(
- (it) =>
- ({ test: { ...it.test, root: "backend" } }) as UserWorkspaceConfig,
- ),
- "frontend/vitest.config.ts",
+ ...convertTests(backendProjects, "backend"),
+ ...convertTests(frontendProjects, "frontend"),
"packages/**/vitest.config.ts",
],
},
});
+
+function convertTests(
+ projects: unknown[],
+ root: string,
+): UserWorkspaceConfig[] {
+ return (projects as UserWorkspaceConfig[]).map((it) => {
+ const test = it.test ?? {};
+ const name: string | { label: string } = test.name ?? "unknown";
+
+ let updatedName =
+ name === null || typeof name === "string"
+ ? `${name} (${root})`
+ : { ...name, label: `${name.label} (${root})` };
+
+ return {
+ ...it,
+ test: {
+ ...test,
+ root,
+ name: updatedName,
+ },
+ } as UserWorkspaceConfig;
+ });
+}