refactor: add currentTarget back to regular listeners, add childTarget to onChild listeners (@fehmer) (#7273)

rework `onChild` to behave mostly like jQuery `.on` with selector.

- we remove `currentTarget` from the `onChild` event handler because
native events and jQuery events have different values for it
- the jQuery `currentTarget` is available with `childTarget` in our
events

---------

Co-authored-by: Miodec <jack@monkeytype.com>
This commit is contained in:
Christian Fehmer 2025-12-20 13:41:11 +01:00 committed by GitHub
parent 86ed9c2570
commit bd3cd75c94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 386 additions and 92 deletions

View file

@ -0,0 +1,6 @@
import $ from "jquery";
//@ts-expect-error add to global
global["$"] = $;
//@ts-expect-error add to global
global["jQuery"] = $;

View file

@ -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", () => ({

View file

@ -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"]
}

View file

@ -0,0 +1,150 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { screen } from "@testing-library/dom";
import { userEvent } from "@testing-library/user-event";
import { qs } from "../../src/ts/utils/dom";
describe("dom", () => {
describe("ElementWithUtils", () => {
describe("onChild", () => {
const handler = vi.fn();
function registerOnChild(event: string, selector: string): void {
const parent = qs("#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 = `
<div id="parent" data-testid="parent">
<section id="decoy">
<div id="mid1" data-testid="mid1" class="middle">
<div id="inner1" class="inner">test</div>
<div id="inner2" data-testid="inner2" class="inner">
test
<button id="button" data-testid="button">click</button>
</div>
</div>
<div id="mid2" class="middle">
<div id="inner3" class="inner">test</div>
<div id="inner4" class="inner">test</div>
</div>
</section>
</div>
`;
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 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 <section> 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"),
}),
);
});
});
});
});

View file

@ -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",

View file

@ -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<T>(
callback: (result: ValidationResult) => void,
validation: Validation<T>,
inputValueConvert?: (val: string) => T,
): (e: DomUtilsEvent) => Promise<void> {
): (e: Event) => Promise<void> {
let callIsValid =
validation.isValid !== undefined
? debounceIfNeeded(

View file

@ -105,11 +105,18 @@ type ElementWithValue =
| HTMLTextAreaElement
| HTMLSelectElement;
export type DomUtilsEvent<T extends Event = Event> = Omit<T, "currentTarget">;
//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<T extends Event = Event> = Omit<T, "currentTarget"> & {
/**
* 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<T extends HTMLElement = HTMLElement> {
/**
@ -244,56 +251,80 @@ export class ElementWithUtils<T extends HTMLElement = HTMLElement> {
*/
on<K extends keyof HTMLElementEventMap>(
event: K,
handler: (this: T, ev: DomUtilsEvent<HTMLElementEventMap[K]>) => 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<K extends keyof HTMLElementEventMap>(
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<HTMLElementEventMap[K]>,
ev: OnChildEvent<HTMLElementEventMap[K]>,
) => 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) {
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 +711,20 @@ export class ElementsWithUtils<
*/
on<K extends keyof HTMLElementEventMap>(
event: K,
handler: (this: T, ev: DomUtilsEvent<HTMLElementEventMap[K]>) => 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
*/

View file

@ -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: {} }),
],
});

155
pnpm-lock.yaml generated
View file

@ -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:

View file

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