mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-22 06:38:34 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecc1a7de24 | ||
|
|
84369dee0d | ||
|
|
ebab30171b | ||
|
|
28b6b8069a |
+2
-2
@@ -170,7 +170,7 @@
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "7.0.87",
|
||||
"@typescript/native": "npm:[email protected]",
|
||||
"@vitest/coverage-v8": "4.1.11",
|
||||
"@vitest/coverage-v8": "4.1.10",
|
||||
"babel-loader": "10.1.1",
|
||||
"babel-plugin-polyfill-corejs3": "1.0.0",
|
||||
"browserslist": "4.28.8",
|
||||
@@ -215,7 +215,7 @@
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.67.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.11",
|
||||
"vitest": "4.1.10",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.4.1#~/.yarn/patches/workbox-build-npm-7.4.1-c84561662c.patch"
|
||||
|
||||
+178
-62
@@ -1,29 +1,39 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
query,
|
||||
queryAll,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import { deepActiveElement } from "../../common/dom/deep-active-element";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-form/ha-form";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-form/ha-form";
|
||||
import type { HaDialog } from "../../components/ha-dialog";
|
||||
import type { HaForm } from "../../components/ha-form/ha-form";
|
||||
import { DirtyStateProviderMixin } from "../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HassDialog, ShowDialogParams } from "../make-dialog-manager";
|
||||
import type { FormDialogData, FormDialogParams } from "./show-form-dialog";
|
||||
import type { HaForm } from "../../components/ha-form/ha-form";
|
||||
|
||||
interface StackEntry {
|
||||
params: FormDialogParams;
|
||||
initialData: FormDialogData;
|
||||
data: FormDialogData;
|
||||
nestedField?: string;
|
||||
scrollTop: number;
|
||||
focusTarget?: Element;
|
||||
error?: Record<string, string>;
|
||||
}
|
||||
|
||||
@customElement("dialog-form")
|
||||
export class DialogForm
|
||||
extends DirtyStateProviderMixin<FormDialogData>()(LitElement)
|
||||
extends DirtyStateProviderMixin<FormDialogData[]>()(LitElement)
|
||||
implements HassDialog<FormDialogData>
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@@ -32,6 +42,8 @@ export class DialogForm
|
||||
|
||||
@state() private _data: FormDialogData = {};
|
||||
|
||||
private _initialData: FormDialogData = {};
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _closeState?: "canceled" | "submitted";
|
||||
@@ -40,14 +52,19 @@ export class DialogForm
|
||||
|
||||
@state() private _error?: Record<string, string>;
|
||||
|
||||
@query("ha-form") private _form?: HaForm;
|
||||
@query("ha-dialog") private _dialog?: HaDialog;
|
||||
|
||||
@query("ha-form:not([hidden])") private _form?: HaForm;
|
||||
|
||||
@queryAll("ha-form") private _forms!: NodeListOf<HaForm>;
|
||||
|
||||
public async showDialog(params: FormDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._data = params.data || {};
|
||||
this._initialData = deepClone(this._data);
|
||||
this._open = true;
|
||||
this._error = undefined;
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
this._resetDirtyTracking();
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
@@ -55,54 +72,119 @@ export class DialogForm
|
||||
return true;
|
||||
}
|
||||
|
||||
private _initialDirtyState(): FormDialogData[] {
|
||||
return [
|
||||
...this._stack.map((entry) => entry.initialData),
|
||||
this._initialData,
|
||||
];
|
||||
}
|
||||
|
||||
private _currentDirtyState(): FormDialogData[] {
|
||||
return [...this._stack.map((entry) => entry.data), this._data];
|
||||
}
|
||||
|
||||
private _resetDirtyTracking(): void {
|
||||
this._initDirtyTracking({ type: "deep" }, this._initialDirtyState());
|
||||
this._updateDirtyState(this._currentDirtyState());
|
||||
}
|
||||
|
||||
private _handleNestedShowDialog = (
|
||||
ev: HASSDomEvent<ShowDialogParams<unknown>>
|
||||
) => {
|
||||
if (ev.detail.dialogTag !== "dialog-form") {
|
||||
if (
|
||||
ev.detail.dialogTag !== "dialog-form" ||
|
||||
ev.currentTarget !== this._form
|
||||
) {
|
||||
return;
|
||||
}
|
||||
ev.stopPropagation();
|
||||
|
||||
const origin = ev.composedPath()[0] as HTMLElement & { name?: string };
|
||||
const nested = ev.detail.dialogParams as FormDialogParams;
|
||||
if (!nested.submit || !nested.cancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.stopPropagation();
|
||||
const focusTarget = deepActiveElement();
|
||||
|
||||
this._stack = [
|
||||
...this._stack,
|
||||
{
|
||||
params: this._params!,
|
||||
initialData: this._initialData,
|
||||
data: this._data,
|
||||
nestedField: origin?.name,
|
||||
scrollTop: this._dialog?.bodyContainer.scrollTop ?? 0,
|
||||
focusTarget: focusTarget ?? undefined,
|
||||
error: this._error,
|
||||
},
|
||||
];
|
||||
const nested = ev.detail.dialogParams as FormDialogParams;
|
||||
|
||||
this._params = nested;
|
||||
this._data = nested?.data || {};
|
||||
this._data = nested.data || {};
|
||||
this._initialData = deepClone(this._data);
|
||||
this._error = undefined;
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
this._resetDirtyTracking();
|
||||
};
|
||||
|
||||
private _popStack(): string | undefined {
|
||||
private _popStack(): StackEntry | undefined {
|
||||
if (!this._stack.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const prev = this._stack[this._stack.length - 1];
|
||||
|
||||
this._stack = this._stack.slice(0, -1);
|
||||
this._params = prev.params;
|
||||
this._initialData = prev.initialData;
|
||||
this._data = prev.data;
|
||||
this._error = prev.error;
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
return prev.nestedField;
|
||||
this._resetDirtyTracking();
|
||||
|
||||
return prev;
|
||||
}
|
||||
|
||||
private async _restoreFocusAndScroll(
|
||||
scrollTop: number,
|
||||
expectedParams: FormDialogParams,
|
||||
focusTarget?: Element
|
||||
): Promise<void> {
|
||||
await this.updateComplete;
|
||||
await this._form?.updateComplete;
|
||||
await new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => resolve());
|
||||
});
|
||||
|
||||
if (!this._open || this._params !== expectedParams || !this._dialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (focusTarget instanceof HTMLElement && focusTarget.isConnected) {
|
||||
focusTarget.focus();
|
||||
}
|
||||
|
||||
this._dialog.bodyContainer.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
if (!this._closeState) {
|
||||
this._params?.cancel?.();
|
||||
|
||||
for (let index = this._stack.length - 1; index >= 0; index--) {
|
||||
this._stack[index].params.cancel?.();
|
||||
}
|
||||
}
|
||||
|
||||
if (this._closeState !== "submitted") {
|
||||
this._discardDirtyStateChanges();
|
||||
}
|
||||
|
||||
this._closeState = undefined;
|
||||
this._stack = [];
|
||||
this._params = undefined;
|
||||
this._initialData = {};
|
||||
this._data = {};
|
||||
this._open = false;
|
||||
this._error = undefined;
|
||||
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -114,53 +196,69 @@ export class DialogForm
|
||||
return;
|
||||
}
|
||||
|
||||
this._closeState = "submitted";
|
||||
const submit = this._params?.submit;
|
||||
const data = this._data;
|
||||
const nestedField = this._popStack();
|
||||
const stackEntry = this._popStack();
|
||||
|
||||
submit?.(data);
|
||||
|
||||
if (!nestedField) {
|
||||
if (!stackEntry) {
|
||||
this._closeState = "submitted";
|
||||
submit?.(data);
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
const schemaField = this._params?.schema.find(
|
||||
(f) => "selector" in f && f.name === nestedField
|
||||
submit!(data);
|
||||
void this._restoreFocusAndScroll(
|
||||
stackEntry.scrollTop,
|
||||
stackEntry.params,
|
||||
stackEntry.focusTarget
|
||||
);
|
||||
const isMultiple =
|
||||
schemaField &&
|
||||
"selector" in schemaField &&
|
||||
"object" in schemaField.selector &&
|
||||
schemaField.selector.object?.multiple === true;
|
||||
|
||||
const current = this._data[nestedField];
|
||||
const newValue = isMultiple
|
||||
? [...(Array.isArray(current) ? current : []), data]
|
||||
: data;
|
||||
|
||||
this._data = deepClone({ ...this._data, [nestedField]: newValue });
|
||||
this._error = undefined;
|
||||
this._updateDirtyState(this._data);
|
||||
}
|
||||
|
||||
private _cancel(): void {
|
||||
this._closeState = "canceled";
|
||||
const cancel = this._params?.cancel;
|
||||
const nestedField = this._popStack();
|
||||
const stackEntry = this._popStack();
|
||||
|
||||
cancel?.();
|
||||
|
||||
if (!nestedField) {
|
||||
if (!stackEntry) {
|
||||
this._closeState = "canceled";
|
||||
cancel?.();
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
cancel!();
|
||||
void this._restoreFocusAndScroll(
|
||||
stackEntry.scrollTop,
|
||||
stackEntry.params,
|
||||
stackEntry.focusTarget
|
||||
);
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
this._data = ev.detail.value;
|
||||
this._error = undefined;
|
||||
this._updateDirtyState(this._data);
|
||||
const levelIndex = Array.from(this._forms).indexOf(
|
||||
ev.currentTarget as HaForm
|
||||
);
|
||||
|
||||
if (levelIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = ev.detail.value as FormDialogData;
|
||||
|
||||
if (levelIndex === this._stack.length) {
|
||||
this._data = data;
|
||||
this._error = undefined;
|
||||
this._updateDirtyState(this._currentDirtyState());
|
||||
return;
|
||||
}
|
||||
|
||||
if (levelIndex < this._stack.length) {
|
||||
this._stack = this._stack.map((entry, index) =>
|
||||
index === levelIndex ? { ...entry, data, error: undefined } : entry
|
||||
);
|
||||
this._updateDirtyState(this._currentDirtyState());
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -168,35 +266,53 @@ export class DialogForm
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const params = this._params;
|
||||
const levels = [
|
||||
...this._stack,
|
||||
{
|
||||
params,
|
||||
initialData: this._initialData,
|
||||
data: this._data,
|
||||
error: this._error,
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
header-title=${this._params.title}
|
||||
header-title=${params.title}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-form
|
||||
autofocus
|
||||
.hass=${this.hass}
|
||||
.computeLabel=${this._params.computeLabel}
|
||||
.computeHelper=${this._params.computeHelper}
|
||||
.data=${this._data}
|
||||
.schema=${this._params.schema}
|
||||
.error=${this._error}
|
||||
@value-changed=${this._valueChanged}
|
||||
@show-dialog=${this._handleNestedShowDialog}
|
||||
>
|
||||
</ha-form>
|
||||
${levels.map((level, index) => {
|
||||
const isActive = index === levels.length - 1;
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
?hidden=${!isActive}
|
||||
?autofocus=${isActive}
|
||||
.hass=${this.hass}
|
||||
.computeLabel=${level.params.computeLabel}
|
||||
.computeHelper=${level.params.computeHelper}
|
||||
.data=${level.data}
|
||||
.schema=${level.params.schema}
|
||||
.error=${level.error}
|
||||
@value-changed=${this._valueChanged}
|
||||
@show-dialog=${this._handleNestedShowDialog}
|
||||
>
|
||||
</ha-form>
|
||||
`;
|
||||
})}
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this._cancel}
|
||||
>
|
||||
${this._params.cancelText || this.hass.localize("ui.common.cancel")}
|
||||
${params.cancelText || this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._submit}>
|
||||
${this._params.submitText || this.hass.localize("ui.common.save")}
|
||||
${params.submitText || this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import type { HaObjectSelector } from "../../../src/components/ha-selector/ha-selector-object";
|
||||
import type { FormDialogParams } from "../../../src/dialogs/form/show-form-dialog";
|
||||
import "../../../src/components/ha-selector/ha-selector-object";
|
||||
|
||||
vi.mock("../../../src/components/ha-input-helper-text", () => {
|
||||
customElements.define("ha-input-helper-text", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
|
||||
vi.mock("../../../src/components/ha-md-list", () => {
|
||||
customElements.define("ha-md-list", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
|
||||
vi.mock("../../../src/components/ha-md-list-item", () => {
|
||||
customElements.define("ha-md-list-item", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
|
||||
vi.mock("../../../src/components/ha-sortable", () => {
|
||||
customElements.define("ha-sortable", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
|
||||
vi.mock("../../../src/components/ha-yaml-editor", () => {
|
||||
customElements.define("ha-yaml-editor", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
|
||||
const hass = {
|
||||
localize: (key: string) => key,
|
||||
locale: "en-US",
|
||||
floors: {},
|
||||
areas: {},
|
||||
devices: {},
|
||||
states: {},
|
||||
formatEntityName: () => "",
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const selectorConfig = {
|
||||
object: {
|
||||
multiple: true,
|
||||
fields: {
|
||||
name: { selector: { text: {} } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const getInternals = (selector: HaObjectSelector) =>
|
||||
selector as unknown as Record<string, unknown>;
|
||||
|
||||
const mountSelector = async (value: Record<string, string>[]) => {
|
||||
const selector = document.createElement(
|
||||
"ha-selector-object"
|
||||
) as HaObjectSelector;
|
||||
selector.hass = hass;
|
||||
selector.selector = selectorConfig;
|
||||
selector.value = value;
|
||||
document.body.append(selector);
|
||||
await selector.updateComplete;
|
||||
return selector;
|
||||
};
|
||||
|
||||
const resolveFormDialog = async (
|
||||
selector: HaObjectSelector,
|
||||
action: "_addItem" | "_editItem",
|
||||
result: Record<string, string> | null,
|
||||
item?: Record<string, string>,
|
||||
index?: number
|
||||
) => {
|
||||
let params: FormDialogParams | undefined;
|
||||
const dialogShown = new Promise<void>((resolve) => {
|
||||
selector.addEventListener(
|
||||
"show-dialog",
|
||||
(event) => {
|
||||
const dialogParams = event.detail.dialogParams as FormDialogParams;
|
||||
params = dialogParams;
|
||||
if (result === null) {
|
||||
dialogParams.cancel!();
|
||||
} else {
|
||||
dialogParams.submit!(result);
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
});
|
||||
|
||||
const event = {
|
||||
stopPropagation: vi.fn(),
|
||||
currentTarget: { item, index },
|
||||
};
|
||||
const operation = (
|
||||
getInternals(selector)[action] as (ev: typeof event) => Promise<void>
|
||||
)(event);
|
||||
await dialogShown;
|
||||
await operation;
|
||||
return params!;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
document.body.replaceChildren();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("ha-selector-object form dialog flow", () => {
|
||||
it("appends an item through the real Add flow", async () => {
|
||||
const first = { name: "A" };
|
||||
const selector = await mountSelector([first]);
|
||||
const valueChanged = vi.fn();
|
||||
selector.addEventListener("value-changed", valueChanged);
|
||||
|
||||
await resolveFormDialog(selector, "_addItem", { name: "B" });
|
||||
|
||||
expect(valueChanged).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: { value: [first, { name: "B" }] },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("replaces an item at its original index through the real Edit flow", async () => {
|
||||
const first = { name: "A" };
|
||||
const second = { name: "B" };
|
||||
const selector = await mountSelector([first, second]);
|
||||
const valueChanged = vi.fn();
|
||||
selector.addEventListener("value-changed", valueChanged);
|
||||
|
||||
await resolveFormDialog(
|
||||
selector,
|
||||
"_editItem",
|
||||
{ name: "B updated" },
|
||||
second,
|
||||
1
|
||||
);
|
||||
|
||||
const value = valueChanged.mock.calls[0][0].detail.value;
|
||||
expect(value).toEqual([first, { name: "B updated" }]);
|
||||
expect(value).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("leaves the array unchanged when Add or Edit is canceled", async () => {
|
||||
const first = { name: "A" };
|
||||
const selector = await mountSelector([first]);
|
||||
const valueChanged = vi.fn();
|
||||
selector.addEventListener("value-changed", valueChanged);
|
||||
|
||||
await resolveFormDialog(selector, "_addItem", null);
|
||||
await resolveFormDialog(selector, "_editItem", null, first, 0);
|
||||
|
||||
expect(valueChanged).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,370 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { deepActiveElement } from "../../../src/common/dom/deep-active-element";
|
||||
import type {
|
||||
FormDialogData,
|
||||
FormDialogParams,
|
||||
} from "../../../src/dialogs/form/show-form-dialog";
|
||||
import type { DialogForm } from "../../../src/dialogs/form/dialog-form";
|
||||
import "../../../src/dialogs/form/dialog-form";
|
||||
|
||||
vi.mock("../../../src/components/ha-button", () => {
|
||||
customElements.define("ha-button", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
|
||||
vi.mock("../../../src/components/ha-dialog", () => {
|
||||
customElements.define(
|
||||
"ha-dialog",
|
||||
class extends HTMLElement {
|
||||
public bodyContainer = document.createElement("div");
|
||||
}
|
||||
);
|
||||
return {};
|
||||
});
|
||||
|
||||
vi.mock("../../../src/components/ha-dialog-footer", () => {
|
||||
customElements.define("ha-dialog-footer", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
|
||||
vi.mock("../../../src/components/ha-form/ha-form", () => {
|
||||
customElements.define(
|
||||
"ha-form",
|
||||
class extends HTMLElement {
|
||||
public reportValidity = vi.fn(() => true);
|
||||
}
|
||||
);
|
||||
return {};
|
||||
});
|
||||
|
||||
const getInternals = (dialog: DialogForm) =>
|
||||
dialog as unknown as Record<string, unknown>;
|
||||
|
||||
const getForms = (dialog: DialogForm): HTMLElement[] =>
|
||||
Array.from(dialog.shadowRoot!.querySelectorAll("ha-form"));
|
||||
|
||||
const outerParams = (data: FormDialogData = {}): FormDialogParams => ({
|
||||
title: "Outer",
|
||||
schema: [{ name: "value", selector: { text: {} } }],
|
||||
data,
|
||||
submit: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
});
|
||||
|
||||
const nestedParams = (data: FormDialogData = {}): FormDialogParams => ({
|
||||
title: "Nested",
|
||||
schema: [{ name: "value", selector: { text: {} } }],
|
||||
data,
|
||||
submit: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
});
|
||||
|
||||
const hass = {
|
||||
localize: (key: string) => key,
|
||||
} as never;
|
||||
|
||||
const openDialog = async (params = outerParams()) => {
|
||||
const dialog = document.createElement("dialog-form") as DialogForm;
|
||||
dialog.hass = hass;
|
||||
document.body.append(dialog);
|
||||
await dialog.showDialog(params);
|
||||
await dialog.updateComplete;
|
||||
return dialog;
|
||||
};
|
||||
|
||||
const showNestedDialog = async (
|
||||
dialog: DialogForm,
|
||||
form: Element,
|
||||
params: FormDialogParams,
|
||||
dialogTag = "dialog-form",
|
||||
origin: Element = form
|
||||
) => {
|
||||
origin.dispatchEvent(
|
||||
new CustomEvent("show-dialog", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: { dialogTag, dialogParams: params },
|
||||
})
|
||||
);
|
||||
await dialog.updateComplete;
|
||||
};
|
||||
|
||||
const submit = (dialog: DialogForm) =>
|
||||
(getInternals(dialog)["_submit"] as () => void)();
|
||||
|
||||
const cancel = (dialog: DialogForm) =>
|
||||
(getInternals(dialog)["_cancel"] as () => void)();
|
||||
|
||||
afterEach(() => {
|
||||
document.body.replaceChildren();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("dialog-form mounted nested forms", () => {
|
||||
it("keeps parent forms mounted while nested", async () => {
|
||||
const dialog = await openDialog();
|
||||
const parent = getForms(dialog)[0];
|
||||
|
||||
await showNestedDialog(dialog, parent, nestedParams());
|
||||
|
||||
const forms = getForms(dialog);
|
||||
expect(forms).toHaveLength(2);
|
||||
expect(forms[0].hidden).toBe(true);
|
||||
expect(forms[1].hidden).toBe(false);
|
||||
expect(forms[0].hasAttribute("autofocus")).toBe(false);
|
||||
expect(forms[1].hasAttribute("autofocus")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns to the parent after nested submit", async () => {
|
||||
const dialog = await openDialog();
|
||||
const nested = nestedParams({ value: "nested" });
|
||||
const parent = getForms(dialog)[0];
|
||||
|
||||
await showNestedDialog(dialog, parent, nested);
|
||||
submit(dialog);
|
||||
await dialog.updateComplete;
|
||||
|
||||
expect(nested.submit).toHaveBeenCalledWith({ value: "nested" });
|
||||
expect(getInternals(dialog)["_open"]).toBe(true);
|
||||
expect(getInternals(dialog)["_stack"]).toHaveLength(0);
|
||||
expect(getForms(dialog)[0].hidden).toBe(false);
|
||||
});
|
||||
|
||||
it.each(["submit", "cancel"] as const)(
|
||||
"restores focus to the opener after nested %s",
|
||||
async (action) => {
|
||||
const dialog = await openDialog();
|
||||
const parent = getForms(dialog)[0];
|
||||
const opener = document.createElement("button");
|
||||
parent.append(opener);
|
||||
opener.focus();
|
||||
|
||||
await showNestedDialog(dialog, parent, nestedParams());
|
||||
|
||||
const child = getForms(dialog)[1];
|
||||
const childFocusTarget = document.createElement("button");
|
||||
child.append(childFocusTarget);
|
||||
childFocusTarget.focus();
|
||||
|
||||
expect(deepActiveElement()).toBe(childFocusTarget);
|
||||
|
||||
if (action === "submit") {
|
||||
submit(dialog);
|
||||
} else {
|
||||
cancel(dialog);
|
||||
}
|
||||
|
||||
await vi.waitUntil(() => deepActiveElement() === opener);
|
||||
}
|
||||
);
|
||||
|
||||
it("keeps the parent open after a custom object selector nested save", async () => {
|
||||
const dialog = await openDialog();
|
||||
const nested = {
|
||||
...nestedParams({ items: [] }),
|
||||
schema: [
|
||||
{
|
||||
name: "items",
|
||||
selector: {
|
||||
object: {
|
||||
multiple: true,
|
||||
fields: { name: { selector: { text: {} } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
} satisfies FormDialogParams;
|
||||
|
||||
const descendant = document.createElement("div");
|
||||
getForms(dialog)[0].append(descendant);
|
||||
await showNestedDialog(
|
||||
dialog,
|
||||
getForms(dialog)[0],
|
||||
nested,
|
||||
"dialog-form",
|
||||
descendant
|
||||
);
|
||||
submit(dialog);
|
||||
await dialog.updateComplete;
|
||||
|
||||
expect(nested.submit).toHaveBeenCalledWith({ items: [] });
|
||||
expect(getInternals(dialog)["_open"]).toBe(true);
|
||||
expect(getInternals(dialog)["_stack"]).toHaveLength(0);
|
||||
expect(getForms(dialog)[0].hidden).toBe(false);
|
||||
});
|
||||
|
||||
it("returns to the parent after nested cancel", async () => {
|
||||
const parentData = { value: "parent" };
|
||||
const dialog = await openDialog(outerParams(parentData));
|
||||
const nested = nestedParams({ value: "nested" });
|
||||
|
||||
await showNestedDialog(dialog, getForms(dialog)[0], nested);
|
||||
cancel(dialog);
|
||||
await dialog.updateComplete;
|
||||
|
||||
expect(nested.cancel).toHaveBeenCalledOnce();
|
||||
expect(getInternals(dialog)["_open"]).toBe(true);
|
||||
expect((getInternals(dialog)["_data"] as FormDialogData).value).toBe(
|
||||
"parent"
|
||||
);
|
||||
expect(getForms(dialog)[0].hidden).toBe(false);
|
||||
});
|
||||
|
||||
it("routes hidden parent value changes to its stack entry", async () => {
|
||||
const dialog = await openDialog(outerParams({ value: "original" }));
|
||||
const parent = getForms(dialog)[0];
|
||||
|
||||
await showNestedDialog(dialog, parent, nestedParams());
|
||||
parent.dispatchEvent(
|
||||
new CustomEvent("value-changed", {
|
||||
detail: { value: { value: "updated" } },
|
||||
})
|
||||
);
|
||||
|
||||
expect(
|
||||
(getInternals(dialog)["_stack"] as Record<string, unknown>[])[0].data
|
||||
).toEqual({ value: "updated" });
|
||||
|
||||
cancel(dialog);
|
||||
expect((getInternals(dialog)["_data"] as FormDialogData).value).toBe(
|
||||
"updated"
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps multiple nested levels mounted and pops them in order", async () => {
|
||||
const dialog = await openDialog();
|
||||
await showNestedDialog(dialog, getForms(dialog)[0], nestedParams());
|
||||
await showNestedDialog(dialog, getForms(dialog)[1], nestedParams());
|
||||
|
||||
expect(getForms(dialog)).toHaveLength(3);
|
||||
expect(getForms(dialog).map((form) => form.hidden)).toEqual([
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
]);
|
||||
|
||||
cancel(dialog);
|
||||
await dialog.updateComplete;
|
||||
expect(getForms(dialog).map((form) => form.hidden)).toEqual([true, false]);
|
||||
|
||||
cancel(dialog);
|
||||
await dialog.updateComplete;
|
||||
expect(getForms(dialog).map((form) => form.hidden)).toEqual([false]);
|
||||
});
|
||||
|
||||
it("accepts active show-dialog events only", async () => {
|
||||
const dialog = await openDialog();
|
||||
const parent = getForms(dialog)[0];
|
||||
const nested = nestedParams();
|
||||
|
||||
await showNestedDialog(dialog, parent, nested);
|
||||
const child = getForms(dialog)[1];
|
||||
await showNestedDialog(dialog, parent, nestedParams());
|
||||
expect(getInternals(dialog)["_stack"]).toHaveLength(1);
|
||||
|
||||
await showNestedDialog(dialog, child, nestedParams(), "not-dialog-form");
|
||||
expect(getInternals(dialog)["_stack"]).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("cancels all pending levels when physically closed", async () => {
|
||||
const cancelOrder: string[] = [];
|
||||
const root = outerParams();
|
||||
const nested = nestedParams();
|
||||
const grandchild = nestedParams();
|
||||
|
||||
root.cancel = vi.fn(() => cancelOrder.push("root"));
|
||||
nested.cancel = vi.fn(() => cancelOrder.push("nested"));
|
||||
grandchild.cancel = vi.fn(() => cancelOrder.push("grandchild"));
|
||||
|
||||
const dialog = await openDialog(root);
|
||||
getForms(dialog)[0].dispatchEvent(
|
||||
new CustomEvent("value-changed", {
|
||||
detail: { value: { value: "dirty" } },
|
||||
})
|
||||
);
|
||||
expect(dialog.isDirtyState).toBe(true);
|
||||
|
||||
await showNestedDialog(dialog, getForms(dialog)[0], nested);
|
||||
await showNestedDialog(dialog, getForms(dialog)[1], grandchild);
|
||||
|
||||
(getInternals(dialog)["_dialogClosed"] as () => void)();
|
||||
|
||||
expect(cancelOrder).toEqual(["grandchild", "nested", "root"]);
|
||||
expect(grandchild.cancel).toHaveBeenCalledOnce();
|
||||
expect(nested.cancel).toHaveBeenCalledOnce();
|
||||
expect(root.cancel).toHaveBeenCalledOnce();
|
||||
expect(getInternals(dialog)["_stack"]).toHaveLength(0);
|
||||
expect(getInternals(dialog)["_params"]).toBeUndefined();
|
||||
expect(getInternals(dialog)["_data"]).toEqual({});
|
||||
expect(getInternals(dialog)["_initialData"]).toEqual({});
|
||||
expect(getInternals(dialog)["_open"]).toBe(false);
|
||||
expect(dialog.isDirtyState).toBe(false);
|
||||
});
|
||||
|
||||
it("tracks dirty state across nested levels", async () => {
|
||||
const dialog = await openDialog();
|
||||
expect(dialog.isDirtyState).toBe(false);
|
||||
|
||||
getForms(dialog)[0].dispatchEvent(
|
||||
new CustomEvent("value-changed", {
|
||||
detail: { value: { value: "changed" } },
|
||||
})
|
||||
);
|
||||
expect(dialog.isDirtyState).toBe(true);
|
||||
|
||||
await showNestedDialog(dialog, getForms(dialog)[0], nestedParams());
|
||||
cancel(dialog);
|
||||
expect(dialog.isDirtyState).toBe(true);
|
||||
|
||||
const cleanDialog = await openDialog();
|
||||
getForms(cleanDialog)[0].dispatchEvent(
|
||||
new CustomEvent("value-changed", {
|
||||
detail: { value: { value: "changed" } },
|
||||
})
|
||||
);
|
||||
expect(cleanDialog.isDirtyState).toBe(true);
|
||||
|
||||
cancel(cleanDialog);
|
||||
(getInternals(cleanDialog)["_dialogClosed"] as () => void)();
|
||||
expect(cleanDialog.isDirtyState).toBe(false);
|
||||
|
||||
const cleanNestedDialog = await openDialog();
|
||||
await showNestedDialog(
|
||||
cleanNestedDialog,
|
||||
getForms(cleanNestedDialog)[0],
|
||||
nestedParams()
|
||||
);
|
||||
cancel(cleanNestedDialog);
|
||||
expect(cleanNestedDialog.isDirtyState).toBe(false);
|
||||
|
||||
submit(cleanNestedDialog);
|
||||
expect(cleanNestedDialog.isDirtyState).toBe(false);
|
||||
|
||||
submit(dialog);
|
||||
expect(dialog.isDirtyState).toBe(false);
|
||||
});
|
||||
|
||||
it("validates the active form before submitting", async () => {
|
||||
const dialog = await openDialog();
|
||||
const parent = getForms(dialog)[0];
|
||||
const nested = nestedParams();
|
||||
await showNestedDialog(dialog, parent, nested);
|
||||
|
||||
const child = getForms(dialog)[1];
|
||||
const parentReportValidity = vi.fn(() => true);
|
||||
const childReportValidity = vi.fn(() => false);
|
||||
Object.defineProperty(parent, "reportValidity", {
|
||||
value: parentReportValidity,
|
||||
});
|
||||
Object.defineProperty(child, "reportValidity", {
|
||||
value: childReportValidity,
|
||||
});
|
||||
|
||||
submit(dialog);
|
||||
|
||||
expect(parentReportValidity).not.toHaveBeenCalled();
|
||||
expect(childReportValidity).toHaveBeenCalledOnce();
|
||||
expect(nested.submit).not.toHaveBeenCalled();
|
||||
expect(getInternals(dialog)["_open"]).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -6452,12 +6452,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/coverage-v8@npm:4.1.11":
|
||||
version: 4.1.11
|
||||
resolution: "@vitest/coverage-v8@npm:4.1.11"
|
||||
"@vitest/coverage-v8@npm:4.1.10":
|
||||
version: 4.1.10
|
||||
resolution: "@vitest/coverage-v8@npm:4.1.10"
|
||||
dependencies:
|
||||
"@bcoe/v8-coverage": "npm:^1.0.2"
|
||||
"@vitest/utils": "npm:4.1.11"
|
||||
"@vitest/utils": "npm:4.1.10"
|
||||
ast-v8-to-istanbul: "npm:^1.0.0"
|
||||
istanbul-lib-coverage: "npm:^3.2.2"
|
||||
istanbul-lib-report: "npm:^3.0.1"
|
||||
@@ -6467,34 +6467,34 @@ __metadata:
|
||||
std-env: "npm:^4.0.0-rc.1"
|
||||
tinyrainbow: "npm:^3.1.0"
|
||||
peerDependencies:
|
||||
"@vitest/browser": 4.1.11
|
||||
vitest: 4.1.11
|
||||
"@vitest/browser": 4.1.10
|
||||
vitest: 4.1.10
|
||||
peerDependenciesMeta:
|
||||
"@vitest/browser":
|
||||
optional: true
|
||||
checksum: 10/b6171ec592e0017c3b10954a9400b10af0becf944a0533af01b002a4cc35b6e562a353b4837451a44b73c5561de358c23a2c135d4e1e79bc9041ebb241a68440
|
||||
checksum: 10/e593f5205a65d10f200e68a99e720d7a9f5e9be65d8160e4f0b6b5f1a7a87f0453c03e79fd1c022dbd7fb26a22657ca4d9410ae27b36da90d41d7ca04f681ab1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/expect@npm:4.1.11":
|
||||
version: 4.1.11
|
||||
resolution: "@vitest/expect@npm:4.1.11"
|
||||
"@vitest/expect@npm:4.1.10":
|
||||
version: 4.1.10
|
||||
resolution: "@vitest/expect@npm:4.1.10"
|
||||
dependencies:
|
||||
"@standard-schema/spec": "npm:^1.1.0"
|
||||
"@types/chai": "npm:^5.2.2"
|
||||
"@vitest/spy": "npm:4.1.11"
|
||||
"@vitest/utils": "npm:4.1.11"
|
||||
"@vitest/spy": "npm:4.1.10"
|
||||
"@vitest/utils": "npm:4.1.10"
|
||||
chai: "npm:^6.2.2"
|
||||
tinyrainbow: "npm:^3.1.0"
|
||||
checksum: 10/9bfcfe5ad926ab58beea1c700dc057f17422f14516506f8fc12c9881ed3e81d4c2faadb768042c8497fdee7007e201c1bd3e7d2e91157dbb47fb5c07c4c02aaa
|
||||
checksum: 10/487fcad404a68968a54ae5fb9d099f12170cd793420a04b34a5606516317090c50a8303ab687c70166ee181864e3e138941d4a96d0405434dcd37696b3105350
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/mocker@npm:4.1.11":
|
||||
version: 4.1.11
|
||||
resolution: "@vitest/mocker@npm:4.1.11"
|
||||
"@vitest/mocker@npm:4.1.10":
|
||||
version: 4.1.10
|
||||
resolution: "@vitest/mocker@npm:4.1.10"
|
||||
dependencies:
|
||||
"@vitest/spy": "npm:4.1.11"
|
||||
"@vitest/spy": "npm:4.1.10"
|
||||
estree-walker: "npm:^3.0.3"
|
||||
magic-string: "npm:^0.30.21"
|
||||
peerDependencies:
|
||||
@@ -6505,56 +6505,56 @@ __metadata:
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
checksum: 10/00b6e1266d8403194b49313e3a9a1af0dff2c773f4b2df11f4955fa0f244fd4b59484cd23381dfef8af30298e2651c1aaa42b439fdbc871bb4bb911de38a9509
|
||||
checksum: 10/ae9645d1bcdad3ab7de7182feb4f1c9148a5ff97cef19581eec9257112aace94889eee9a1ad12e40ce59453ac05f52453b5fdb49ff76a31af8ccdbaaa4471ef3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/pretty-format@npm:4.1.11":
|
||||
version: 4.1.11
|
||||
resolution: "@vitest/pretty-format@npm:4.1.11"
|
||||
"@vitest/pretty-format@npm:4.1.10":
|
||||
version: 4.1.10
|
||||
resolution: "@vitest/pretty-format@npm:4.1.10"
|
||||
dependencies:
|
||||
tinyrainbow: "npm:^3.1.0"
|
||||
checksum: 10/2dfc2f20dbe1c4dbea33ec42e85a8b5648aa6585521bea47573406f5cefb81cd3b86b71f981c4e3d69946e77252cb42a700416c1dc656bb0478cb2932c953cdc
|
||||
checksum: 10/e4f6907143ab0e40dda29d70b17027586c92921d622091321f10512e660b3995dcee7aa56e17b750b72560f295e25f96035372348415f18ebfd39b66a55b4704
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/runner@npm:4.1.11":
|
||||
version: 4.1.11
|
||||
resolution: "@vitest/runner@npm:4.1.11"
|
||||
"@vitest/runner@npm:4.1.10":
|
||||
version: 4.1.10
|
||||
resolution: "@vitest/runner@npm:4.1.10"
|
||||
dependencies:
|
||||
"@vitest/utils": "npm:4.1.11"
|
||||
"@vitest/utils": "npm:4.1.10"
|
||||
pathe: "npm:^2.0.3"
|
||||
checksum: 10/5247df824fa28b458ba0102592dfec50707982193b62076db941fbe5d7c88fb7067e68a33c194db0092bbe35459cfbeaed33b3c667e5f02192c18baeb4f56239
|
||||
checksum: 10/2c962cb13af0880990036808a35679b7ac6657c8f542490234c2faa6ffd2ab080ac6bf21b487c64d84aa635cfb37b49eb679098c2003a100dfc6c4d5e87bf055
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/snapshot@npm:4.1.11":
|
||||
version: 4.1.11
|
||||
resolution: "@vitest/snapshot@npm:4.1.11"
|
||||
"@vitest/snapshot@npm:4.1.10":
|
||||
version: 4.1.10
|
||||
resolution: "@vitest/snapshot@npm:4.1.10"
|
||||
dependencies:
|
||||
"@vitest/pretty-format": "npm:4.1.11"
|
||||
"@vitest/utils": "npm:4.1.11"
|
||||
"@vitest/pretty-format": "npm:4.1.10"
|
||||
"@vitest/utils": "npm:4.1.10"
|
||||
magic-string: "npm:^0.30.21"
|
||||
pathe: "npm:^2.0.3"
|
||||
checksum: 10/5d096373fb4b102f65ff884844a18c2d2e7d88caf68a64a842a245573311b2d531ca5a94ebf7e4fe39324e71a78670f74c949d4ec2cad3764c3f4c272b84d982
|
||||
checksum: 10/7940d83ffd2fbebf9a04ea31e196b7e8bf981093ec739950959fe8dd29caa33c80823780fb4b1063d9459c44a0a8d8b2748c00dfb6941becd7404e6d687eea01
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/spy@npm:4.1.11":
|
||||
version: 4.1.11
|
||||
resolution: "@vitest/spy@npm:4.1.11"
|
||||
checksum: 10/d49a7ed7501080e5f817d61250a169a46fcc7901887e4985a1e08705ce79aea8d1edcffd74f4dc6669ea1bc3d717a39354ce89c67188d81a63dc439d42f195f6
|
||||
"@vitest/spy@npm:4.1.10":
|
||||
version: 4.1.10
|
||||
resolution: "@vitest/spy@npm:4.1.10"
|
||||
checksum: 10/7c1b79a95474338e0659f0f2e43be4df1ef7939ff5b37b044954e0287582947803bd417508f44a7f244809672309d9b3dd67660b704ec3fe7f323cc958ae47a3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/utils@npm:4.1.11":
|
||||
version: 4.1.11
|
||||
resolution: "@vitest/utils@npm:4.1.11"
|
||||
"@vitest/utils@npm:4.1.10":
|
||||
version: 4.1.10
|
||||
resolution: "@vitest/utils@npm:4.1.10"
|
||||
dependencies:
|
||||
"@vitest/pretty-format": "npm:4.1.11"
|
||||
"@vitest/pretty-format": "npm:4.1.10"
|
||||
convert-source-map: "npm:^2.0.0"
|
||||
tinyrainbow: "npm:^3.1.0"
|
||||
checksum: 10/f05381e12d0926db7b01bfaae9a577fa664d36b96c23df185e4b0f6dad3a9fb59ac00931613da53b4511ee6ab473a14ac500c72c5ec5e9b3c3042875051f20c4
|
||||
checksum: 10/95484aad55c7b00bbcd4963e27cbb86fe207620a6093973d68da9d0a06bad37c388d84c9ab43d5f35d88e46c8f376a5592d9c54c025c958361160f4802bb25ee
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -10047,7 +10047,7 @@ __metadata:
|
||||
"@types/tar": "npm:7.0.87"
|
||||
"@typescript/native": "npm:[email protected]"
|
||||
"@vibrant/color": "npm:4.0.4"
|
||||
"@vitest/coverage-v8": "npm:4.1.11"
|
||||
"@vitest/coverage-v8": "npm:4.1.10"
|
||||
"@vvo/tzdb": "npm:6.198.0"
|
||||
"@webcomponents/scoped-custom-element-registry": "npm:0.0.10"
|
||||
"@webcomponents/webcomponentsjs": "npm:2.8.0"
|
||||
@@ -10134,7 +10134,7 @@ __metadata:
|
||||
typescript: "npm:6.0.3"
|
||||
typescript-eslint: "npm:8.67.0"
|
||||
vite-tsconfig-paths: "npm:6.1.1"
|
||||
vitest: "npm:4.1.11"
|
||||
vitest: "npm:4.1.10"
|
||||
webpack-stats-plugin: "npm:1.1.3"
|
||||
webpackbar: "npm:7.0.0"
|
||||
weekstart: "npm:2.0.0"
|
||||
@@ -15470,17 +15470,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vitest@npm:4.1.11":
|
||||
version: 4.1.11
|
||||
resolution: "vitest@npm:4.1.11"
|
||||
"vitest@npm:4.1.10":
|
||||
version: 4.1.10
|
||||
resolution: "vitest@npm:4.1.10"
|
||||
dependencies:
|
||||
"@vitest/expect": "npm:4.1.11"
|
||||
"@vitest/mocker": "npm:4.1.11"
|
||||
"@vitest/pretty-format": "npm:4.1.11"
|
||||
"@vitest/runner": "npm:4.1.11"
|
||||
"@vitest/snapshot": "npm:4.1.11"
|
||||
"@vitest/spy": "npm:4.1.11"
|
||||
"@vitest/utils": "npm:4.1.11"
|
||||
"@vitest/expect": "npm:4.1.10"
|
||||
"@vitest/mocker": "npm:4.1.10"
|
||||
"@vitest/pretty-format": "npm:4.1.10"
|
||||
"@vitest/runner": "npm:4.1.10"
|
||||
"@vitest/snapshot": "npm:4.1.10"
|
||||
"@vitest/spy": "npm:4.1.10"
|
||||
"@vitest/utils": "npm:4.1.10"
|
||||
es-module-lexer: "npm:^2.0.0"
|
||||
expect-type: "npm:^1.3.0"
|
||||
magic-string: "npm:^0.30.21"
|
||||
@@ -15498,12 +15498,12 @@ __metadata:
|
||||
"@edge-runtime/vm": "*"
|
||||
"@opentelemetry/api": ^1.9.0
|
||||
"@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0
|
||||
"@vitest/browser-playwright": 4.1.11
|
||||
"@vitest/browser-preview": 4.1.11
|
||||
"@vitest/browser-webdriverio": 4.1.11
|
||||
"@vitest/coverage-istanbul": 4.1.11
|
||||
"@vitest/coverage-v8": 4.1.11
|
||||
"@vitest/ui": 4.1.11
|
||||
"@vitest/browser-playwright": 4.1.10
|
||||
"@vitest/browser-preview": 4.1.10
|
||||
"@vitest/browser-webdriverio": 4.1.10
|
||||
"@vitest/coverage-istanbul": 4.1.10
|
||||
"@vitest/coverage-v8": 4.1.10
|
||||
"@vitest/ui": 4.1.10
|
||||
happy-dom: "*"
|
||||
jsdom: "*"
|
||||
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
@@ -15534,7 +15534,7 @@ __metadata:
|
||||
optional: false
|
||||
bin:
|
||||
vitest: ./vitest.mjs
|
||||
checksum: 10/054f1e25d90d911693b0b93c5b85a7c3105775aa3e39c3279c7d3e7af719e2d94070a898d6d74292445366c1215bb91d07063989ac0965973f0c5ae22ad3b06b
|
||||
checksum: 10/020843460fe696c23be2a363634dde4daf54625f1c443c24066ba3f87c478b0ccfdd5124343ba30eb092f54902ffea09f4bed0af4a16a9ee805e494ee2dce34e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user