Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { FormlyFieldConfig } from "@ngx-formly/core";
import { applySafeNumericParser, parseNumericInput } from "./numeric-input-parser.util";

describe("parseNumericInput", () => {
it("returns the number for a numeric value", () => {
expect(parseNumericInput(10)).toEqual(10);
expect(parseNumericInput(0)).toEqual(0);
expect(parseNumericInput(-5)).toEqual(-5);
});

it("parses numeric strings", () => {
expect(parseNumericInput("10")).toEqual(10);
expect(parseNumericInput("-5")).toEqual(-5);
expect(parseNumericInput("0")).toEqual(0);
expect(parseNumericInput("3.5")).toEqual(3.5);
});

it("treats empty / null / undefined as null", () => {
expect(parseNumericInput("")).toBeNull();
expect(parseNumericInput(null)).toBeNull();
expect(parseNumericInput(undefined)).toBeNull();
});

it("returns null for non-numeric input instead of throwing", () => {
expect(parseNumericInput("abc")).toBeNull();
expect(parseNumericInput("1a")).toBeNull();
expect(parseNumericInput({})).toBeNull();
});

it("preserves negative and zero values (does not clamp to 1)", () => {
// Guards the reported bug: a negative limit must pass through unchanged here,
// not be turned into 1 or dropped.
expect(parseNumericInput(-100)).toEqual(-100);
expect(parseNumericInput("-1")).toEqual(-1);
});

it("parses negative decimals and whitespace-padded numbers", () => {
expect(parseNumericInput("-3.5")).toEqual(-3.5);
expect(parseNumericInput(" 10 ")).toEqual(10);
});

it("treats whitespace-only strings as null", () => {
expect(parseNumericInput(" ")).toBeNull();
expect(parseNumericInput(" ")).toBeNull();
expect(parseNumericInput("\t")).toBeNull();
});

it("rejects NaN and infinities as null", () => {
expect(parseNumericInput(NaN)).toBeNull();
expect(parseNumericInput(Infinity)).toBeNull();
expect(parseNumericInput(-Infinity)).toBeNull();
expect(parseNumericInput("Infinity")).toBeNull();
});

it("rejects non-number/non-string values (boolean, array, object) as null", () => {
expect(parseNumericInput(true)).toBeNull();
expect(parseNumericInput(false)).toBeNull();
expect(parseNumericInput([5])).toBeNull();
expect(parseNumericInput([1, 2])).toBeNull();
});

it("preserves large finite numbers", () => {
expect(parseNumericInput(1_000_000_000)).toEqual(1_000_000_000);
expect(parseNumericInput("2000000")).toEqual(2000000);
});

it("never touches the DOM (safe to call without an element)", () => {
// The whole point of the replacement: no document.querySelector / .validity access.
expect(() => parseNumericInput("42")).not.toThrow();
expect(parseNumericInput("42")).toEqual(42);
});
});

describe("applySafeNumericParser", () => {
it("replaces a numeric field's existing (crash-prone) parser with the safe one", () => {
// Mimics @ngx-formly's parser that throws on nz-input-number when the value is null.
const crashingParser = () => {
throw new TypeError("Cannot read properties of undefined (reading 'badInput')");
};
const field: FormlyFieldConfig = { type: "integer", parsers: [crashingParser] };

applySafeNumericParser(field);

// the crash-prone parser is gone
expect(field.parsers).toHaveLength(1);
expect(field.parsers?.[0]).not.toBe(crashingParser);
// the null intermediate state that used to crash is now handled safely
expect(() => (field.parsers?.[0] as (v: unknown, f: FormlyFieldConfig) => unknown)(null, field)).not.toThrow();
expect((field.parsers?.[0] as (v: unknown, f: FormlyFieldConfig) => unknown)(null, field)).toBeNull();
// and it still converts real values
expect((field.parsers?.[0] as (v: unknown, f: FormlyFieldConfig) => unknown)("10", field)).toEqual(10);
expect((field.parsers?.[0] as (v: unknown, f: FormlyFieldConfig) => unknown)("-5", field)).toEqual(-5);
});

it("leaves non-numeric fields (without parsers) untouched", () => {
const field: FormlyFieldConfig = { type: "string" };

applySafeNumericParser(field);

expect(field.parsers).toBeUndefined();
});

it("does not add parsers to a field that had none", () => {
const field: FormlyFieldConfig = { key: "someBoolean", type: "boolean" };

applySafeNumericParser(field);

expect(field.parsers).toBeUndefined();
});

it("the replacement parser treats the clear-field cases ('' and null) as null", () => {
const field: FormlyFieldConfig = {
type: "integer",
parsers: [
() => {
throw new Error("original parser should not run");
},
],
};

applySafeNumericParser(field);
const parser = field.parsers?.[0] as (v: unknown, f: FormlyFieldConfig) => unknown;

expect(parser("", field)).toBeNull();
expect(parser(null, field)).toBeNull();
});

it("collapses multiple existing parsers into a single safe one", () => {
const field: FormlyFieldConfig = { type: "integer", parsers: [() => 1, () => 2] };

applySafeNumericParser(field);

expect(field.parsers).toHaveLength(1);
expect((field.parsers?.[0] as (v: unknown, f: FormlyFieldConfig) => unknown)("7", field)).toEqual(7);
});

it("leaves an empty parsers array untouched (nothing to replace)", () => {
const field: FormlyFieldConfig = { type: "integer", parsers: [] };

applySafeNumericParser(field);

expect(field.parsers).toEqual([]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { FormlyFieldConfig } from "@ngx-formly/core";

/**
* Null-safe parser for numeric (integer/number) property fields.
*
* It replaces @ngx-formly/core's json-schema integer/number parser, which reads
* `document.querySelector('#' + field.id).validity.badInput`. That code assumes the
* field id is on the native `<input>`, but ng-zorro's `nz-input-number` puts the id
* on its host element (which has no `.validity`), so the parser throws
* "Cannot read properties of undefined (reading 'badInput')" and typed edits never
* commit. This version does no DOM access: it just converts the value to a number, or
* null when it is empty/undefined/not a number.
*/
export function parseNumericInput(value: unknown): number | null {
if (typeof value === "number") {
// Reject NaN and ±Infinity — only real, finite numbers are valid.
return Number.isFinite(value) ? value : null;
}
if (typeof value === "string") {
const trimmed = value.trim();
if (trimmed === "") {
return null;
}
const parsed = Number(trimmed);
return Number.isFinite(parsed) ? parsed : null;
}
// null, undefined, boolean, object, array, etc. are not valid numeric input.
return null;
}

/**
* Replace a numeric field's crash-prone Formly parser with {@link parseNumericInput}.
*
* FormlyJsonschema sets `parsers` only on integer/number fields, and that parser is
* the one that throws on ng-zorro's nz-input-number. When the field carries such a
* parser, swap it for the null-safe one; leave every other field untouched.
*/
export function applySafeNumericParser(field: FormlyFieldConfig): void {
if (field.parsers && field.parsers.length > 0) {
field.parsers = [value => parseNumericInput(value)];
}
}
Comment on lines +57 to +61
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import { WorkflowPveService } from "../../../service/virtual-environment/virtual
import { ComputingUnitStatusService } from "../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service";
import { of } from "rxjs";
import { map, switchMap, take } from "rxjs/operators";
import { applySafeNumericParser } from "./numeric-input-parser.util";

Quill.register("modules/cursors", QuillCursors);

Expand Down Expand Up @@ -488,6 +489,15 @@ export class OperatorPropertyEditFrameComponent implements OnInit, OnChanges, On
},
};

// @ngx-formly/core's json-schema parser for integer/number fields reads
// document.querySelector('#' + field.id).validity.badInput, assuming the id is on
// the native <input>. ng-zorro's nz-input-number puts the id on its host element
// (which has no `.validity`), so that parser throws
// "Cannot read properties of undefined (reading 'badInput')" and typed edits never
// commit — the field gets stuck (only the +/- steppers work). FormlyJsonschema only
// sets `parsers` for numeric fields, so replace it with a null-safe parser here.
applySafeNumericParser(mappedField);

// Disable dummy operator for user
if (mappedField.key === "dummyOperator") {
mappedField.expressions = {
Expand Down
Loading