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 + +
+
+
+
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; + }); +}