Compare commits

...

3 Commits

Author SHA1 Message Date
Aidan Timson ef22539175 Remove hass from clock card editor 2026-07-17 14:22:42 +01:00
Aidan Timson 7346e9408a Use conditional visibility in clock card editor 2026-07-17 14:18:09 +01:00
Paul Bottein 6b0f543fdb Add conditional field visibility to ha-form (#53165) 2026-07-17 14:06:04 +01:00
8 changed files with 490 additions and 276 deletions
+73
View File
@@ -0,0 +1,73 @@
import type {
HaFormBaseSchema,
HaFormCondition,
HaFormDataContainer,
HaFormFieldCondition,
HaFormSchema,
} from "./types";
const isEmpty = (value: unknown): boolean =>
value === undefined || value === null || value === "";
const matchFieldCondition = (
condition: HaFormFieldCondition,
data: HaFormDataContainer | undefined
): boolean => {
const actual = data?.[condition.field];
switch (condition.operator ?? "eq") {
case "eq":
return actual === condition.value;
case "not_eq":
return actual !== condition.value;
case "in":
return (
Array.isArray(condition.value) &&
condition.value.includes(actual as any)
);
case "not_in":
return (
Array.isArray(condition.value) &&
!condition.value.includes(actual as any)
);
case "exists":
return !isEmpty(actual);
case "not_exists":
return isEmpty(actual);
default:
return false;
}
};
export const evaluateCondition = (
condition: HaFormCondition,
data: HaFormDataContainer | undefined
): boolean => {
if ("condition" in condition) {
switch (condition.condition) {
case "and":
return condition.conditions.every((c) => evaluateCondition(c, data));
case "or":
return condition.conditions.some((c) => evaluateCondition(c, data));
case "not":
return !condition.conditions.some((c) => evaluateCondition(c, data));
default:
return false;
}
}
return matchFieldCondition(condition, data);
};
export const isFieldHidden = (
schema: HaFormSchema,
data: HaFormDataContainer | undefined
): boolean => {
const { hidden } = schema as HaFormBaseSchema;
if (!hidden) {
return false;
}
if (hidden === true) {
return true;
}
const conditions = Array.isArray(hidden) ? hidden : [hidden];
return conditions.every((condition) => evaluateCondition(condition, data));
};
+16 -13
View File
@@ -2,6 +2,7 @@ import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, queryAll } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import { isFieldHidden } from "./conditions";
import "./ha-form";
import type { HaForm } from "./ha-form";
import type {
@@ -68,19 +69,21 @@ export class HaFormGrid extends LitElement implements HaFormElement {
protected render(): TemplateResult {
return html`
${this.schema.schema.map(
(item) => html`
<ha-form
.hass=${this.hass}
.data=${this.data}
.schema=${[item]}
.disabled=${this.disabled}
.computeLabel=${this.computeLabel}
.computeHelper=${this.computeHelper}
.localizeValue=${this.localizeValue}
></ha-form>
`
)}
${this.schema.schema
.filter((item) => !isFieldHidden(item, this.data))
.map(
(item) => html`
<ha-form
.hass=${this.hass}
.data=${this.data}
.schema=${[item]}
.disabled=${this.disabled}
.computeLabel=${this.computeLabel}
.computeHelper=${this.computeHelper}
.localizeValue=${this.localizeValue}
></ha-form>
`
)}
`;
}
+11 -2
View File
@@ -1,11 +1,12 @@
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant } from "../../types";
import "../ha-alert";
import "../ha-selector/ha-selector";
import { isFieldHidden } from "./conditions";
import type { HaFormDataContainer, HaFormElement, HaFormSchema } from "./types";
const LOAD_ELEMENTS = {
@@ -98,7 +99,11 @@ export class HaForm extends LitElement implements HaFormElement {
let isValid = true;
let firstInvalidElement: HTMLElement | undefined;
this.schema.forEach((item, index) => {
const visibleSchema = this.schema.filter(
(item) => !isFieldHidden(item, this.data)
);
visibleSchema.forEach((item, index) => {
const element = elements[index];
if (!element) {
return;
@@ -164,6 +169,10 @@ export class HaForm extends LitElement implements HaFormElement {
: ""
}
${this.schema.map((item) => {
if (isFieldHidden(item, this.data)) {
return nothing;
}
const error = getError(this.error, item);
const warning = getWarning(this.warning, item);
+33
View File
@@ -22,6 +22,9 @@ export interface HaFormBaseSchema {
default?: HaFormData;
required?: boolean;
disabled?: boolean;
// Field is hidden while the condition holds. Serializable so it can be
// shared with the backend and other renderers.
hidden?: boolean | HaFormCondition | HaFormCondition[];
description?: {
suffix?: string;
// This value will be set initially when form is loaded
@@ -30,6 +33,36 @@ export interface HaFormBaseSchema {
context?: Record<string, string>;
}
export type HaFormConditionOperator =
"eq" | "not_eq" | "in" | "not_in" | "exists" | "not_exists";
export interface HaFormFieldCondition {
field: string;
operator?: HaFormConditionOperator;
value?: HaFormData | readonly HaFormData[];
}
export interface HaFormAndCondition {
condition: "and";
conditions: readonly HaFormCondition[];
}
export interface HaFormOrCondition {
condition: "or";
conditions: readonly HaFormCondition[];
}
export interface HaFormNotCondition {
condition: "not";
conditions: readonly HaFormCondition[];
}
export type HaFormCondition =
| HaFormFieldCondition
| HaFormAndCondition
| HaFormOrCondition
| HaFormNotCondition;
export interface HaFormGridSchema extends HaFormBaseSchema {
type: "grid";
flatten?: boolean;
@@ -1,5 +1,5 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
array,
@@ -12,14 +12,15 @@ import {
optional,
string,
} from "superstruct";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import type { ValueChangedEvent } from "../../../../types";
import type { ClockCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
@@ -64,17 +65,13 @@ export class HuiClockCardEditor
extends LitElement
implements LovelaceCardEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@consumeLocalize()
private _localize!: LocalizeFunc;
@state() private _config?: ClockCardConfig;
private _schema = memoizeOne(
(
localize: LocalizeFunc,
clockStyle: ClockCardConfig["clock_style"],
ticks: ClockCardConfig["ticks"],
showSeconds: boolean | undefined
) =>
(localize: LocalizeFunc) =>
[
{ name: "title", selector: { text: {} } },
{
@@ -114,124 +111,122 @@ export class HuiClockCardEditor
ui_clock_date_format: {},
},
},
...(clockStyle === "digital"
? ([
{
name: "time_format",
selector: {
select: {
mode: "dropdown",
options: ["auto", ...Object.values(TimeFormat)].map(
(value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.time_formats.${value}`
),
})
),
},
},
},
] as const satisfies readonly HaFormSchema[])
: clockStyle === "analog"
? ([
{
name: "border",
description: {
suffix: localize(
`ui.panel.lovelace.editor.card.clock.border.description`
),
},
default: false,
selector: {
boolean: {},
},
},
{
name: "ticks",
description: {
suffix: localize(
`ui.panel.lovelace.editor.card.clock.ticks.description`
),
},
default: "hour",
selector: {
select: {
mode: "dropdown",
options: ["none", "quarter", "hour", "minute"].map(
(value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.ticks.${value}.label`
),
description: localize(
`ui.panel.lovelace.editor.card.clock.ticks.${value}.description`
),
})
),
},
},
},
...(showSeconds
? ([
{
name: "seconds_motion",
description: {
suffix: localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.description`
),
},
default: "continuous",
selector: {
select: {
mode: "dropdown",
options: ["continuous", "tick"].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.${value}.label`
),
description: localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.${value}.description`
),
})),
},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
...(ticks !== "none"
? ([
{
name: "face_style",
description: {
suffix: localize(
`ui.panel.lovelace.editor.card.clock.face_style.description`
),
},
default: "markers",
selector: {
select: {
mode: "dropdown",
options: [
"markers",
"numbers_upright",
"roman",
].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.face_style.${value}.label`
),
description: localize(
`ui.panel.lovelace.editor.card.clock.face_style.${value}.description`
),
})),
},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
] as const satisfies readonly HaFormSchema[])
: []),
{
name: "time_format",
hidden: {
field: "clock_style",
operator: "not_eq",
value: "digital",
},
selector: {
select: {
mode: "dropdown",
options: ["auto", ...Object.values(TimeFormat)].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.time_formats.${value}`
),
})),
},
},
},
{
name: "border",
hidden: { field: "clock_style", operator: "not_eq", value: "analog" },
description: {
suffix: localize(
`ui.panel.lovelace.editor.card.clock.border.description`
),
},
default: false,
selector: {
boolean: {},
},
},
{
name: "ticks",
hidden: { field: "clock_style", operator: "not_eq", value: "analog" },
description: {
suffix: localize(
`ui.panel.lovelace.editor.card.clock.ticks.description`
),
},
default: "hour",
selector: {
select: {
mode: "dropdown",
options: ["none", "quarter", "hour", "minute"].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.ticks.${value}.label`
),
description: localize(
`ui.panel.lovelace.editor.card.clock.ticks.${value}.description`
),
})),
},
},
},
{
name: "seconds_motion",
hidden: {
condition: "or",
conditions: [
{ field: "clock_style", operator: "not_eq", value: "analog" },
{ field: "show_seconds", operator: "not_eq", value: true },
],
},
description: {
suffix: localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.description`
),
},
default: "continuous",
selector: {
select: {
mode: "dropdown",
options: ["continuous", "tick"].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.${value}.label`
),
description: localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.${value}.description`
),
})),
},
},
},
{
name: "face_style",
hidden: {
condition: "or",
conditions: [
{ field: "clock_style", operator: "not_eq", value: "analog" },
{ field: "ticks", value: "none" },
],
},
description: {
suffix: localize(
`ui.panel.lovelace.editor.card.clock.face_style.description`
),
},
default: "markers",
selector: {
select: {
mode: "dropdown",
options: ["markers", "numbers_upright", "roman"].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.face_style.${value}.label`
),
description: localize(
`ui.panel.lovelace.editor.card.clock.face_style.${value}.description`
),
})),
},
},
},
{ name: "time_zone", selector: { timezone: {} } },
] as const satisfies readonly HaFormSchema[]
);
@@ -265,20 +260,14 @@ export class HuiClockCardEditor
}
protected render() {
if (!this.hass || !this._config) {
if (!this._config) {
return nothing;
}
return html`
<ha-form
.hass=${this.hass}
.data=${this._data(this._config)}
.schema=${this._schema(
this.hass.localize,
this._data(this._config).clock_style,
this._data(this._config).ticks,
this._data(this._config).show_seconds
)}
.schema=${this._schema(this._localize)}
.computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@value-changed=${this._valueChanged}
@@ -327,51 +316,43 @@ export class HuiClockCardEditor
) => {
switch (schema.name) {
case "title":
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.title"
);
return this._localize("ui.panel.lovelace.editor.card.generic.title");
case "clock_style":
return this.hass!.localize(
return this._localize(
`ui.panel.lovelace.editor.card.clock.clock_style`
);
case "clock_size":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.clock_size`
);
return this._localize(`ui.panel.lovelace.editor.card.clock.clock_size`);
case "time_format":
return this.hass!.localize(
return this._localize(
`ui.panel.lovelace.editor.card.clock.time_format`
);
case "time_zone":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.time_zone`
);
return this._localize(`ui.panel.lovelace.editor.card.clock.time_zone`);
case "show_seconds":
return this.hass!.localize(
return this._localize(
`ui.panel.lovelace.editor.card.clock.show_seconds`
);
case "no_background":
return this.hass!.localize(
return this._localize(
`ui.panel.lovelace.editor.card.clock.no_background`
);
case "date_format":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.date.label`
);
return this._localize(`ui.panel.lovelace.editor.card.clock.date.label`);
case "border":
return this.hass!.localize(
return this._localize(
`ui.panel.lovelace.editor.card.clock.border.label`
);
case "ticks":
return this.hass!.localize(
return this._localize(
`ui.panel.lovelace.editor.card.clock.ticks.label`
);
case "seconds_motion":
return this.hass!.localize(
return this._localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.label`
);
case "face_style":
return this.hass!.localize(
return this._localize(
`ui.panel.lovelace.editor.card.clock.face_style.label`
);
default:
@@ -384,23 +365,23 @@ export class HuiClockCardEditor
) => {
switch (schema.name) {
case "date_format":
return this.hass!.localize(
return this._localize(
`ui.panel.lovelace.editor.card.clock.date.description`
);
case "border":
return this.hass!.localize(
return this._localize(
`ui.panel.lovelace.editor.card.clock.border.description`
);
case "ticks":
return this.hass!.localize(
return this._localize(
`ui.panel.lovelace.editor.card.clock.ticks.description`
);
case "seconds_motion":
return this.hass!.localize(
return this._localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.description`
);
case "face_style":
return this.hass!.localize(
return this._localize(
`ui.panel.lovelace.editor.card.clock.face_style.description`
);
default:
@@ -54,6 +54,65 @@ const cardConfigStruct = assign(
})
);
const SCHEMA = [
{ name: "title", selector: { text: {} } },
{
name: "",
type: "grid",
schema: [
{
name: "hours_to_show",
default: DEFAULT_HOURS_TO_SHOW,
selector: { number: { min: 0, step: "any", mode: "box" } },
},
{
name: "show_names",
default: true,
required: false,
selector: { boolean: {} },
},
{
name: "logarithmic_scale",
required: false,
selector: { boolean: {} },
},
{
name: "expand_legend",
required: false,
selector: { boolean: {} },
},
],
},
{
name: "",
type: "grid",
schema: [
{
name: "min_y_axis",
required: false,
selector: { number: { mode: "box", step: "any" } },
},
{
name: "max_y_axis",
required: false,
selector: { number: { mode: "box", step: "any" } },
},
],
},
{
name: "fit_y_data",
required: false,
hidden: {
condition: "and",
conditions: [
{ field: "min_y_axis", operator: "not_exists" },
{ field: "max_y_axis", operator: "not_exists" },
],
},
selector: { boolean: {} },
},
] as const satisfies readonly HaFormSchema[];
@customElement("hui-history-graph-card-editor")
export class HuiHistoryGraphCardEditor
extends LitElement
@@ -70,65 +129,6 @@ export class HuiHistoryGraphCardEditor
this._config = config;
}
private _schema = memoizeOne(
(showFitOption: boolean) =>
[
{ name: "title", selector: { text: {} } },
{
name: "",
type: "grid",
schema: [
{
name: "hours_to_show",
default: DEFAULT_HOURS_TO_SHOW,
selector: { number: { min: 0, step: "any", mode: "box" } },
},
{
name: "show_names",
default: true,
required: false,
selector: { boolean: {} },
},
{
name: "logarithmic_scale",
required: false,
selector: { boolean: {} },
},
{
name: "expand_legend",
required: false,
selector: { boolean: {} },
},
],
},
{
name: "",
type: "grid",
schema: [
{
name: "min_y_axis",
required: false,
selector: { number: { mode: "box", step: "any" } },
},
{
name: "max_y_axis",
required: false,
selector: { number: { mode: "box", step: "any" } },
},
],
},
...(showFitOption
? [
{
name: "fit_y_data",
required: false,
selector: { boolean: {} },
},
]
: []),
] as const
);
private _subForm = memoizeOne((localize: LocalizeFunc, entityId: string) => ({
schema: [
{ name: "entity", selector: { entity: {} }, required: true },
@@ -176,11 +176,6 @@ export class HuiHistoryGraphCardEditor
`;
}
const schema = this._schema(
this._config!.min_y_axis !== undefined ||
this._config!.max_y_axis !== undefined
);
const configEntities = this._config.entities
? (processEditorEntities(this._config.entities) as GraphEntityConfig[])
: [];
@@ -188,7 +183,7 @@ export class HuiHistoryGraphCardEditor
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${schema}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
@@ -283,9 +278,7 @@ export class HuiHistoryGraphCardEditor
) as HistoryGraphCardConfig;
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
switch (schema.name) {
case "show_names":
case "logarithmic_scale":
@@ -92,7 +92,6 @@ export class HuiTileCardEditor
(
localize: LocalizeFunc,
entityId: string | undefined,
hideState: boolean,
showTimeFormat: boolean
) =>
[
@@ -144,31 +143,25 @@ export class HuiTileCardEditor
},
],
},
...(!hideState
? ([
{
name: "state_content",
selector: {
ui_state_content: {
allow_context: true,
},
},
context: {
filter_entity: "entity",
},
},
] as const satisfies readonly HaFormSchema[])
: []),
...(showTimeFormat
? ([
{
name: "time_format",
selector: {
ui_time_format: {},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
{
name: "state_content",
hidden: { field: "hide_state", value: true },
selector: {
ui_state_content: {
allow_context: true,
},
},
context: {
filter_entity: "entity",
},
},
{
name: "time_format",
hidden: !showTimeFormat,
selector: {
ui_time_format: {},
},
},
{
name: "content_layout",
required: true,
@@ -293,12 +286,7 @@ export class HuiTileCardEditor
this._config.state_content
);
const schema = this._schema(
this.hass.localize,
entityId,
this._config.hide_state ?? false,
showTimeFormat
);
const schema = this._schema(this.hass.localize, entityId, showTimeFormat);
const vertical = this._config.vertical ?? false;
@@ -0,0 +1,134 @@
import { describe, expect, it } from "vitest";
import { isFieldHidden } from "../../../src/components/ha-form/conditions";
import type { HaFormSchema } from "../../../src/components/ha-form/types";
const field = (hidden: HaFormSchema["hidden"]): HaFormSchema =>
({ name: "field", selector: { text: {} }, hidden }) as HaFormSchema;
describe("isFieldHidden", () => {
it("shows a field without a hidden condition", () => {
expect(isFieldHidden(field(undefined), { a: 1 })).toBe(false);
});
it("honors a boolean hidden", () => {
expect(isFieldHidden(field(true), {})).toBe(true);
expect(isFieldHidden(field(false), {})).toBe(false);
});
describe("operators", () => {
it("eq (default) matches equal values", () => {
expect(isFieldHidden(field({ field: "a", value: 1 }), { a: 1 })).toBe(
true
);
expect(isFieldHidden(field({ field: "a", value: 1 }), { a: 2 })).toBe(
false
);
});
it("not_eq matches different values", () => {
const schema = field({ field: "a", operator: "not_eq", value: 1 });
expect(isFieldHidden(schema, { a: 2 })).toBe(true);
expect(isFieldHidden(schema, { a: 1 })).toBe(false);
});
it("in matches membership", () => {
const schema = field({ field: "a", operator: "in", value: ["x", "y"] });
expect(isFieldHidden(schema, { a: "y" })).toBe(true);
expect(isFieldHidden(schema, { a: "z" })).toBe(false);
});
it("not_in matches non-membership", () => {
const schema = field({
field: "a",
operator: "not_in",
value: ["x", "y"],
});
expect(isFieldHidden(schema, { a: "z" })).toBe(true);
expect(isFieldHidden(schema, { a: "x" })).toBe(false);
});
it("exists matches a defined non-empty value", () => {
const schema = field({ field: "a", operator: "exists" });
expect(isFieldHidden(schema, { a: "x" })).toBe(true);
expect(isFieldHidden(schema, { a: "" })).toBe(false);
expect(isFieldHidden(schema, {})).toBe(false);
});
it("not_exists matches a missing or empty value", () => {
const schema = field({ field: "a", operator: "not_exists" });
expect(isFieldHidden(schema, {})).toBe(true);
expect(isFieldHidden(schema, { a: null } as any)).toBe(true);
expect(isFieldHidden(schema, { a: "x" })).toBe(false);
});
});
describe("combinators", () => {
it("and requires every condition", () => {
const schema = field({
condition: "and",
conditions: [
{ field: "a", value: 1 },
{ field: "b", value: 2 },
],
});
expect(isFieldHidden(schema, { a: 1, b: 2 })).toBe(true);
expect(isFieldHidden(schema, { a: 1, b: 9 })).toBe(false);
});
it("or requires any condition", () => {
const schema = field({
condition: "or",
conditions: [
{ field: "a", value: 1 },
{ field: "b", value: 2 },
],
});
expect(isFieldHidden(schema, { a: 9, b: 2 })).toBe(true);
expect(isFieldHidden(schema, { a: 9, b: 9 })).toBe(false);
});
it("not negates its conditions", () => {
const schema = field({
condition: "not",
conditions: [{ field: "a", value: 1 }],
});
expect(isFieldHidden(schema, { a: 2 })).toBe(true);
expect(isFieldHidden(schema, { a: 1 })).toBe(false);
});
it("nests combinators", () => {
const schema = field({
condition: "and",
conditions: [
{ field: "a", value: 1 },
{
condition: "or",
conditions: [
{ field: "b", value: 2 },
{ field: "c", value: 3 },
],
},
],
});
expect(isFieldHidden(schema, { a: 1, b: 9, c: 3 })).toBe(true);
expect(isFieldHidden(schema, { a: 1, b: 9, c: 9 })).toBe(false);
expect(isFieldHidden(schema, { a: 9, b: 2, c: 3 })).toBe(false);
});
});
it("treats an array of conditions as AND", () => {
const schema = field([
{ field: "a", value: 1 },
{ field: "b", value: 2 },
]);
expect(isFieldHidden(schema, { a: 1, b: 2 })).toBe(true);
expect(isFieldHidden(schema, { a: 1, b: 9 })).toBe(false);
});
it("handles missing data", () => {
expect(isFieldHidden(field({ field: "a", value: 1 }), undefined)).toBe(
false
);
});
});