From eda751560b1b56766d71cf9796ccc15edc1acf67 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Tue, 16 Jun 2026 14:34:18 -0600 Subject: [PATCH 01/56] Implement a basic node:test test suite that loads ht_render.js in a sandbox. --- centrallix-os/sys/js/tests/_setup.js | 50 ++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 centrallix-os/sys/js/tests/_setup.js diff --git a/centrallix-os/sys/js/tests/_setup.js b/centrallix-os/sys/js/tests/_setup.js new file mode 100644 index 000000000..d573c9d68 --- /dev/null +++ b/centrallix-os/sys/js/tests/_setup.js @@ -0,0 +1,50 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +// Loads ht_render.js into a node:vm sandbox so its cxjs_* functions +// can be exercised under node:test without modifying the source file +// or running a real browser. The sandbox object is exported; tests +// call functions off it (env.cxjs_*) and may mutate page-level +// globals such as pg_username between assertions. + +'use strict'; +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); + +const HT_RENDER_PATH = path.resolve(__dirname, '..', 'ht_render.js'); + +// Minimal stubs for the page/browser globals ht_render.js references. +const sandbox = + { + pg_username: 'test_user', + pg_clockoffset: 0, + pg_expaddpart: () => {}, + window: {}, + document: + { + getElementsByTagName: () => [], + addEventListener: () => {}, + releaseEvents: () => {}, + captureEvents: () => {}, + }, + console: console, + }; +sandbox.globalThis = sandbox; + +vm.createContext(sandbox); +vm.runInContext( + fs.readFileSync(HT_RENDER_PATH, 'utf8'), + sandbox, + { filename: HT_RENDER_PATH } +); + +module.exports = sandbox; From 6d234b24149d6431e2418fa106767c2bf3706c4d Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Tue, 16 Jun 2026 14:34:46 -0600 Subject: [PATCH 02/56] Add testing for cxjs_user_name as a proof-of-concept. --- .../sys/js/tests/cxjs_user_name.test.js | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_user_name.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_user_name.test.js b/centrallix-os/sys/js/tests/cxjs_user_name.test.js new file mode 100644 index 000000000..491285951 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_user_name.test.js @@ -0,0 +1,34 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +describe('cxjs_user_name', () => + { + const cases = + [ + 'alice', + 'bob', + ' !@#$%^&*()":;\' ', + ]; + + for (const name of cases) + { + it(`returns pg_username (\"${name}\")`, () => + { + env.pg_username = name; + assert.equal(env.cxjs_user_name(), name); + }); + } + }); From 0d3efb13affc65d4e4d57e3a8b5cc77b1064c9b5 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Tue, 16 Jun 2026 15:20:39 -0600 Subject: [PATCH 03/56] Fix instanceof causing tests to fail due to cross-domain failures. --- centrallix-os/sys/js/ht_render.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/centrallix-os/sys/js/ht_render.js b/centrallix-os/sys/js/ht_render.js index 547cf9343..02cd39c80 100644 --- a/centrallix-os/sys/js/ht_render.js +++ b/centrallix-os/sys/js/ht_render.js @@ -151,7 +151,7 @@ function cxjs_has_endorsement(e,ctx) function cxjs_min(v) { var lowest = undefined; - if (v instanceof Array) + if (Array.isArray(v)) { for(var i=0; i Date: Tue, 16 Jun 2026 15:37:20 -0600 Subject: [PATCH 04/56] Add tests for cxjs_min() and cxjs_max(). --- centrallix-os/sys/js/tests/cxjs_max.test.js | 64 +++++++++++++++++++++ centrallix-os/sys/js/tests/cxjs_min.test.js | 64 +++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_max.test.js create mode 100644 centrallix-os/sys/js/tests/cxjs_min.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_max.test.js b/centrallix-os/sys/js/tests/cxjs_max.test.js new file mode 100644 index 000000000..08beb6f50 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_max.test.js @@ -0,0 +1,64 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +describe('cxjs_max', () => + { + for (const [ input, result ] of [ + // Input Result + [ [], undefined ], + [ [0, 1], 1 ], + [ [0, 1, 2, 3, 4], 4 ], + [ [3, 4, 2, 1], 4 ], + [ [9, 10.1, 10.2], 10.2 ], + [ [3.4, 3.3, 3.2], 3.4 ], + [ [-1, 0], 0 ], + [ [-1.1, -1], -1 ], + [ [-Infinity, 0], 0 ], + [ [Infinity, 0], Infinity ], + [ [Infinity, Infinity], Infinity ], + [ [-Infinity, -Infinity], -Infinity ], + [ [undefined], undefined ], + [ [undefined, 0], 0 ], + ]) { + test(`cxjs_max(${input}) = ${result}`, () => + { + assert.equal(env.cxjs_max(input), result); + }); + } + + for (const [ input, result ] of [ + // Input Result + [ {}, undefined ], + [ { a: 0, b: 1 }, 1 ], + [ { c: 0, d: 1, e: 2, f: 3, g: 4 }, 4 ], + [ { h: 3, i: 4, j: 2, k: 1 }, 4 ], + [ { l: 9, m: 10.1, n: 10.2 }, 10.2 ], + [ { o: 3.4, p: 3.3, q: 3.2 }, 3.4 ], + [ { r: -1, s: 0 }, 0 ], + [ { t: -1.1, u: -1 }, -1 ], + [ { v: -Infinity, w: 0 }, 0 ], + [ { x: Infinity, y: 0 }, Infinity ], + [ { z: Infinity, A: Infinity }, Infinity ], + [ { B: -Infinity, C: -Infinity }, -Infinity ], + [ { D: undefined }, undefined ], + [ { E: undefined, F: 0 }, 0 ], + ]) { + test(`cxjs_max(${JSON.stringify(input)}) = ${result}`, () => + { + assert.equal(env.cxjs_max(input), result); + }); + } + }); diff --git a/centrallix-os/sys/js/tests/cxjs_min.test.js b/centrallix-os/sys/js/tests/cxjs_min.test.js new file mode 100644 index 000000000..3f9d629e2 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_min.test.js @@ -0,0 +1,64 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +describe('cxjs_min', () => + { + for (const [ input, result ] of [ + // Input Result + [ [], undefined ], + [ [0, 1], 0 ], + [ [0, 1, 2, 3, 4], 0 ], + [ [3, 1, 2, 4], 1 ], + [ [9, 10.1, 10.2], 9 ], + [ [3.4, 3.3, 3.2], 3.2 ], + [ [-1, 0], -1 ], + [ [-1.1, 1], -1.1 ], + [ [-Infinity, 0], -Infinity ], + [ [Infinity, 0], 0 ], + [ [Infinity, Infinity], Infinity ], + [ [-Infinity, -Infinity], -Infinity ], + [ [undefined], undefined ], + [ [undefined, 0], 0 ], + ]) { + test(`cxjs_min(${input}) = ${result}`, () => + { + assert.equal(env.cxjs_min(input), result); + }); + } + + for (const [ input, result ] of [ + // Input Result + [ {}, undefined ], + [ { a: 0, b: 1 }, 0 ], + [ { c: 0, d: 1, e: 2, f: 3, g: 4 }, 0 ], + [ { h: 3, i: 1, j: 2, k: 4 }, 1 ], + [ { l: 9, m: 10.1, n: 10.2 }, 9 ], + [ { o: 3.4, p: 3.3, q: 3.2 }, 3.2 ], + [ { r: -1, s: 0 }, -1 ], + [ { t: -1.1, u: 1 }, -1.1 ], + [ { v: -Infinity, w: 0 }, -Infinity ], + [ { x: Infinity, y: 0 }, 0 ], + [ { z: Infinity, A: Infinity }, Infinity ], + [ { B: -Infinity, C: -Infinity }, -Infinity ], + [ { D: undefined }, undefined ], + [ { E: undefined, F: 0 }, 0 ], + ]) { + test(`cxjs_min(${JSON.stringify(input)}) = ${result}`, () => + { + assert.equal(env.cxjs_min(input), result); + }); + } + }); From 8296278bb7a81c2f92d5f147eb92b4726c19d092 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 10:38:25 -0600 Subject: [PATCH 05/56] Add the test-js target to the Centrallix Makefile. --- centrallix/Makefile.in | 12 ++++++++++++ centrallix/configure.ac | 1 + 2 files changed, 13 insertions(+) diff --git a/centrallix/Makefile.in b/centrallix/Makefile.in index 7c7407f8d..e7771165b 100644 --- a/centrallix/Makefile.in +++ b/centrallix/Makefile.in @@ -51,6 +51,7 @@ AWK = @AWK@ CC = @CC@ STRIP = @STRIP@ SED = @SED@ +NODE = @NODE@ INSTALL = @INSTALL@ INSTALL_PROGRAM = @INSTALL_PROGRAM@ INSTALL_SCRIPT = @INSTALL_SCRIPT@ @@ -413,6 +414,7 @@ V3LSOBJS=$(V3BASEOBJS) $(HTDRIVERS) $(NETDRIVERS) $(WGTRDRIVERS) .PHONY: manpages_install .PHONY: test_install .PHONY: test +.PHONY: test-js all: centrallix mods config manpages cxpasswd test_obj linksign @@ -582,6 +584,16 @@ test: tests/centrallix.conf-test $(TOTESTDEPS) $(TOTESTFILES) $(CBTESTFILES) fi \ done +test-js: + @NODE_BIN="$(NODE)"; \ + [ -n "$$NODE_BIN" ] || NODE_BIN=$$(command -v node); \ + if [ -z "$$NODE_BIN" ]; then \ + echo "Error: node not found (not detected by configure, not on PATH)."; \ + echo "Install Node.js (or load it in nvm with 'nvm use'), then try again."; \ + exit 1; \ + fi; \ + "$$NODE_BIN" --test '../centrallix-os/sys/js/tests/*.test.js' 2>&1 + sfeditor/sfedit.o: sfeditor/sfedit.c sfeditor/*.xpm $(CC) $(CFLAGS) `gtk-config --cflags` $< -c -o $@ diff --git a/centrallix/configure.ac b/centrallix/configure.ac index c96525983..36b0e0841 100644 --- a/centrallix/configure.ac +++ b/centrallix/configure.ac @@ -14,6 +14,7 @@ AC_PROG_AWK AC_PATH_PROG([GZIP], [gzip], []) AC_PATH_PROG([STRIP], [strip], []) AC_PATH_PROG([SED], [sed], []) +AC_PATH_PROG([NODE], [node], []) AC_PROG_INSTALL AC_CACHE_SAVE From 360913f6979a577a95870a24a37ca183f9d7a6dc Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 10:38:32 -0600 Subject: [PATCH 06/56] Update generated files. --- centrallix/configure | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/centrallix/configure b/centrallix/configure index adf0045a8..edb3e3a28 100755 --- a/centrallix/configure +++ b/centrallix/configure @@ -677,6 +677,7 @@ EXPORT_DYNAMIC INSTALL_DATA INSTALL_SCRIPT INSTALL_PROGRAM +NODE SED STRIP GZIP @@ -3427,6 +3428,46 @@ $as_echo "no" >&6; } fi +# Extract the first word of "node", so it can be a program name with args. +set dummy node; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_path_NODE+:} false; then : + $as_echo_n "(cached) " >&6 +else + case $NODE in + [\\/]* | ?:[\\/]*) + ac_cv_path_NODE="$NODE" # Let the user override the test with a path. + ;; + *) + as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_path_NODE="$as_dir/$ac_word$ac_exec_ext" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + + ;; +esac +fi +NODE=$ac_cv_path_NODE +if test -n "$NODE"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $NODE" >&5 +$as_echo "$NODE" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + ac_aux_dir= for ac_dir in "$srcdir" "$srcdir/.." "$srcdir/../.."; do if test -f "$ac_dir/install-sh"; then From c09993734fdd31a6f04c16238d6f9c042bbc356f Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 10:41:34 -0600 Subject: [PATCH 07/56] Improve test name formatting. --- centrallix-os/sys/js/tests/cxjs_max.test.js | 2 +- centrallix-os/sys/js/tests/cxjs_min.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/centrallix-os/sys/js/tests/cxjs_max.test.js b/centrallix-os/sys/js/tests/cxjs_max.test.js index 08beb6f50..6e8ea0ef3 100644 --- a/centrallix-os/sys/js/tests/cxjs_max.test.js +++ b/centrallix-os/sys/js/tests/cxjs_max.test.js @@ -33,7 +33,7 @@ describe('cxjs_max', () => [ [undefined], undefined ], [ [undefined, 0], 0 ], ]) { - test(`cxjs_max(${input}) = ${result}`, () => + test(`cxjs_max(${JSON.stringify(input)}) = ${result}`, () => { assert.equal(env.cxjs_max(input), result); }); diff --git a/centrallix-os/sys/js/tests/cxjs_min.test.js b/centrallix-os/sys/js/tests/cxjs_min.test.js index 3f9d629e2..5fe151fa8 100644 --- a/centrallix-os/sys/js/tests/cxjs_min.test.js +++ b/centrallix-os/sys/js/tests/cxjs_min.test.js @@ -33,7 +33,7 @@ describe('cxjs_min', () => [ [undefined], undefined ], [ [undefined, 0], 0 ], ]) { - test(`cxjs_min(${input}) = ${result}`, () => + test(`cxjs_min(${JSON.stringify(input)}) = ${result}`, () => { assert.equal(env.cxjs_min(input), result); }); From 52ce2cb60d6a2611943585293d752822787cdda1 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 10:43:36 -0600 Subject: [PATCH 08/56] Improve test style & formatting. --- centrallix-os/sys/js/tests/_setup.js | 12 ++++----- .../sys/js/tests/cxjs_user_name.test.js | 26 ++++++++----------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/centrallix-os/sys/js/tests/_setup.js b/centrallix-os/sys/js/tests/_setup.js index d573c9d68..e39c9962e 100644 --- a/centrallix-os/sys/js/tests/_setup.js +++ b/centrallix-os/sys/js/tests/_setup.js @@ -30,12 +30,12 @@ const sandbox = pg_expaddpart: () => {}, window: {}, document: - { - getElementsByTagName: () => [], - addEventListener: () => {}, - releaseEvents: () => {}, - captureEvents: () => {}, - }, + { + getElementsByTagName: () => [], + addEventListener: () => {}, + releaseEvents: () => {}, + captureEvents: () => {}, + }, console: console, }; sandbox.globalThis = sandbox; diff --git a/centrallix-os/sys/js/tests/cxjs_user_name.test.js b/centrallix-os/sys/js/tests/cxjs_user_name.test.js index 491285951..70d9897f3 100644 --- a/centrallix-os/sys/js/tests/cxjs_user_name.test.js +++ b/centrallix-os/sys/js/tests/cxjs_user_name.test.js @@ -16,19 +16,15 @@ const env = require('./_setup'); describe('cxjs_user_name', () => { - const cases = - [ - 'alice', - 'bob', - ' !@#$%^&*()":;\' ', - ]; - - for (const name of cases) - { - it(`returns pg_username (\"${name}\")`, () => - { - env.pg_username = name; - assert.equal(env.cxjs_user_name(), name); - }); - } + for (const name of [ + 'alice', + 'bob', + ' !@#$%^&*()":;\' ', + ]) { + it(`returns pg_username (\"${name}\")`, () => + { + env.pg_username = name; + assert.equal(env.cxjs_user_name(), name); + }); + } }); From 258da92be28c7a5f4b4d4f9d9a22a5e8031b371f Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 10:50:43 -0600 Subject: [PATCH 09/56] Add tests for cxjs_count(). --- centrallix-os/sys/js/tests/cxjs_count.test.js | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_count.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_count.test.js b/centrallix-os/sys/js/tests/cxjs_count.test.js new file mode 100644 index 000000000..4e3e0b4e8 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_count.test.js @@ -0,0 +1,99 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +describe('cxjs_count', () => + { + for (const [ input, result ] of [ + // Input Result + [ [], 0 ], + [ [0, 1], 2 ], + [ [0, 1, 2, 3, 4], 5 ], + [ [9, 10.1, 10.2], 3 ], + [ [-1.1, 1], 2 ], + [ [-Infinity, Infinity], 2 ], + // null and undefined are not counted. + [ [null], 0 ], + [ [undefined], 0 ], + [ [null, undefined], 0 ], + [ [null, 0], 1 ], + [ [undefined, 0], 1 ], + // NaN is not counted. + [ [NaN], 0 ], + [ [NaN, 1], 1 ], + [ [NaN, null, undefined, 5], 1 ], + // Numeric strings count; non-numeric strings do not. "" is numeric (0). + [ ["5", 6], 2 ], + [ ["", 6], 2 ], + [ ["abc", 6], 1 ], + [ ["abc", "def"], 0 ], + ]) { + test(`cxjs_count(${JSON.stringify(input)}) = ${result}`, () => + { + assert.equal(env.cxjs_count(input), result); + }); + } + + for (const [ input, result ] of [ + // Input Result + [ {}, 0 ], + [ { a: 0, b: 1 }, 2 ], + [ { c: 0, d: 1, e: 2, f: 3, g: 4 }, 5 ], + [ { l: 9, m: 10.1, n: 10.2 }, 3 ], + [ { t: -1.1, u: 1 }, 2 ], + [ { v: -Infinity, w: Infinity }, 2 ], + // null and undefined are not counted. + [ { D: null }, 0 ], + [ { E: undefined }, 0 ], + [ { F: null, G: undefined }, 0 ], + [ { H: null, I: 0 }, 1 ], + [ { J: undefined, K: 0 }, 1 ], + // NaN is not counted. + [ { L: NaN }, 0 ], + [ { M: NaN, N: 1 }, 1 ], + [ { O: NaN, P: null, Q: undefined, R: 5 }, 1 ], + // Numeric strings count; non-numeric strings do not. "" is numeric (0). + [ { S: "5", T: 6 }, 2 ], + [ { U: "", V: 6 }, 2 ], + [ { W: "abc", X: 6 }, 1 ], + [ { Y: "abc", Z: "def" }, 0 ], + ]) { + test(`cxjs_count(${JSON.stringify(input)}) = ${result}`, () => + { + assert.equal(env.cxjs_count(input), result); + }); + } + + // Scalar (non-array, non-object) inputs always count as one, even when + // the value is null, undefined, or NaN. + for (const [ input, result ] of [ + // Input Result + [ 0, 1 ], + [ 42, 1 ], + [ -1.1, 1 ], + [ Infinity, 1 ], + [ "foo", 1 ], + [ "", 1 ], + [ true, 1 ], + [ NaN, 1 ], + [ null, 1 ], + [ undefined, 1 ], + ]) { + test(`cxjs_count(${JSON.stringify(input)}) = ${result}`, () => + { + assert.equal(env.cxjs_count(input), result); + }); + } + }); From 7b9c5cc0341e2f46beb6da9081438e2c9b33fbe6 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 10:50:51 -0600 Subject: [PATCH 10/56] Add tests for cxjs_sum(). --- centrallix-os/sys/js/tests/cxjs_sum.test.js | 82 +++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_sum.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_sum.test.js b/centrallix-os/sys/js/tests/cxjs_sum.test.js new file mode 100644 index 000000000..5c23a50da --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_sum.test.js @@ -0,0 +1,82 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +describe('cxjs_sum', () => + { + for (const [ input, result ] of [ + // Input Result + [ [], null ], + [ [0, 1], 1 ], + [ [0, 1, 2, 3, 4], 10 ], + [ [3, 1, 2, 4], 10 ], + [ [0.5, 0.25], 0.75 ], + [ [1.5, 2.5], 4 ], + [ [9, 10, 11], 30 ], + [ [-1, 0], -1 ], + [ [-1.5, 1], -0.5 ], + [ [-1, -2, -3], -6 ], + [ [-Infinity, 0], -Infinity ], + [ [Infinity, 0], Infinity ], + [ [Infinity, Infinity], Infinity ], + [ [-Infinity, -Infinity], -Infinity ], + [ [Infinity, -Infinity], NaN ], + [ [undefined], null ], + [ [undefined, 0], 0 ], + [ [null], null ], + [ [null, 5], 5 ], + [ [undefined, null, 3], 3 ], + [ [NaN], null ], + [ [NaN, 2], 2 ], + [ [1, 2, undefined, 3], 6 ], + ]) { + test(`cxjs_sum(${JSON.stringify(input)}) = ${result}`, () => + { + assert.equal(env.cxjs_sum(input), result); + }); + } + + for (const [ input, result ] of [ + // Input Result + [ {}, null ], + [ { a: 0, b: 1 }, 1 ], + [ { c: 0, d: 1, e: 2, f: 3, g: 4 }, 10 ], + [ { h: 3, i: 1, j: 2, k: 4 }, 10 ], + [ { l: 0.5, m: 0.25 }, 0.75 ], + [ { n: 1.5, o: 2.5 }, 4 ], + [ { p: 9, q: 10, r: 11 }, 30 ], + [ { s: -1, t: 0 }, -1 ], + [ { u: -1.5, v: 1 }, -0.5 ], + [ { w: -1, x: -2, y: -3 }, -6 ], + [ { z: -Infinity, A: 0 }, -Infinity ], + [ { B: Infinity, C: 0 }, Infinity ], + [ { D: Infinity, E: Infinity }, Infinity ], + [ { F: -Infinity, G: -Infinity }, -Infinity ], + [ { H: Infinity, I: -Infinity }, NaN ], + [ { J: undefined }, null ], + [ { K: undefined, L: 0 }, 0 ], + [ { M: null }, null ], + [ { N: null, O: 5 }, 5 ], + [ { P: undefined, Q: null, R: 3 }, 3 ], + [ { S: NaN }, null ], + [ { T: NaN, U: 2 }, 2 ], + [ { V: 1, W: 2, X: undefined, Y: 3 }, 6 ], + ]) { + test(`cxjs_sum(${JSON.stringify(input)}) = ${result}`, () => + { + assert.equal(env.cxjs_sum(input), result); + }); + } + }); From 5adbd57ae43d4523f00b0a4b74cf943ca124f99d Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 10:54:41 -0600 Subject: [PATCH 11/56] Add tests for cxjs_getdate(). --- .../sys/js/tests/cxjs_getdate.test.js | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_getdate.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_getdate.test.js b/centrallix-os/sys/js/tests/cxjs_getdate.test.js new file mode 100644 index 000000000..2bc182207 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_getdate.test.js @@ -0,0 +1,96 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// cxjs_getdate() reads the current time, shifts it backward by pg_clockoffset +// milliseconds, and formats it as "M/D/YYYY H:MM:SS" -- the month, day, and +// hour are NOT zero-padded, but the minute and second always are. new Date() +// is non-deterministic, so each call runs against a fake Date swapped into the +// sandbox: it wraps a genuine Date built from a fixed epoch and answers the +// getters/setter cxjs_getdate() uses in UTC, which keeps the test independent +// of the host timezone while preserving native setMilliseconds() rollover. +function getdateAt(epochMs, clockoffset) + { + class FakeDate + { + constructor() { this._d = new Date(epochMs); } + getMilliseconds() { return this._d.getUTCMilliseconds(); } + setMilliseconds(v) { return this._d.setUTCMilliseconds(v); } + getSeconds() { return this._d.getUTCSeconds(); } + getMinutes() { return this._d.getUTCMinutes(); } + getHours() { return this._d.getUTCHours(); } + getDate() { return this._d.getUTCDate(); } + getMonth() { return this._d.getUTCMonth(); } + getFullYear() { return this._d.getUTCFullYear(); } + } + + env.Date = FakeDate; + env.pg_clockoffset = clockoffset; + try + { + return env.cxjs_getdate(); + } + finally + { + delete env.Date; + env.pg_clockoffset = 0; + } + } + +// Build epoch milliseconds from UTC calendar fields (month is 1-based here). +function utc(year, month, day, hour, min, sec, ms) + { + return Date.UTC(year, month - 1, day, hour, min, sec, ms || 0); + } + +describe('cxjs_getdate', () => + { + // Formatting, with no clock offset. + for (const [ when, result ] of [ + // When Result + [ utc(2026, 1, 1, 0, 5, 9), '1/1/2026 0:05:09' ], // midnight; single-digit min/sec padded, hour not + [ utc(2026, 12, 25, 23, 59, 59), '12/25/2026 23:59:59' ], // two-digit fields left unpadded + [ utc(2026, 6, 15, 10, 10, 10), '6/15/2026 10:10:10' ], // 10 is the pad boundary: no leading zero + [ utc(2026, 7, 4, 12, 0, 0), '7/4/2026 12:00:00' ], // zero min/sec render as "00" + [ utc(2026, 3, 9, 7, 30, 45), '3/9/2026 7:30:45' ], // single-digit hour stays unpadded + [ utc(1999, 12, 31, 23, 59, 59), '12/31/1999 23:59:59' ], // four-digit year from a different century + [ utc(2024, 2, 29, 12, 0, 0), '2/29/2024 12:00:00' ], // leap day + [ utc(2026, 6, 15, 12, 30, 45, 500),'6/15/2026 12:30:45' ], // milliseconds never appear in the output + [ utc(2026, 1, 31, 0, 0, 0), '1/31/2026 0:00:00' ], // January is month 1 (getMonth() + 1) + [ utc(2026, 10, 5, 9, 9, 9), '10/5/2026 9:09:09' ], // two-digit month; 9 is just below the pad boundary + ]) { + test(`cxjs_getdate() = "${result}"`, () => + { + assert.equal(getdateAt(when, 0), result); + }); + } + + // pg_clockoffset shifts the reported time backward by that many ms (a + // negative offset therefore shifts it forward). + for (const [ when, offset, result ] of [ + // When Offset Result + [ utc(2026, 6, 15, 12, 0, 30), 60000, '6/15/2026 11:59:30' ], // back 60s, crossing the minute and hour + [ utc(2026, 6, 15, 12, 0, 30), -90000, '6/15/2026 12:02:00' ], // forward 90s; minute padded back to "02" + [ utc(2026, 6, 15, 0, 0, 30), 60000, '6/14/2026 23:59:30' ], // back across the day boundary + [ utc(2026, 1, 1, 0, 0, 30), 60000, '12/31/2025 23:59:30' ], // back across the month and year boundary + [ utc(2026, 6, 15, 0, 0, 0), 500, '6/14/2026 23:59:59' ], // sub-second offset still rolls the day back + [ utc(2025, 12, 31, 23, 59, 59), -1000, '1/1/2026 0:00:00' ], // forward 1s across the year boundary + ]) { + test(`cxjs_getdate() with pg_clockoffset ${offset}ms = "${result}"`, () => + { + assert.equal(getdateAt(when, offset), result); + }); + } + }); From 29b0c41c3e3c32dda222df4b1550faf17d7422cd Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 10:58:45 -0600 Subject: [PATCH 12/56] Add tests for cxjs_convert(). --- .../sys/js/tests/cxjs_convert.test.js | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_convert.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_convert.test.js b/centrallix-os/sys/js/tests/cxjs_convert.test.js new file mode 100644 index 000000000..19bef255e --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_convert.test.js @@ -0,0 +1,118 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +describe('cxjs_convert', () => + { + // A null datatype or value yields null, regardless of the other + // argument (== null also catches undefined). + for (const [ dt, v, result ] of [ + // Datatype Value Result + [ null, 5, null ], + [ undefined, 5, null ], + [ 'integer', null, null ], + [ 'integer', undefined, null ], + [ 'double', null, null ], + [ 'string', null, null ], + [ null, null, null ], + ]) { + test(`cxjs_convert(${JSON.stringify(dt)}, ${JSON.stringify(v)}) = ${result}`, () => + { + assert.equal(env.cxjs_convert(dt, v), result); + }); + } + + // Conversion to integer. Note the '$' is only stripped when it is + // the *second* character (e.g. 'x$5'); a leading '$5' is not, so it + // parses to NaN. Stripping the '$' also drops a leading sign. + for (const [ dt, v, result ] of [ + // Datatype Value Result + [ 'integer', 0, 0 ], + [ 'integer', 5, 5 ], + [ 'integer', -7, -7 ], + [ 'integer', 5.9, 5 ], + [ 'integer', '42', 42 ], + [ 'integer', '42abc', 42 ], + [ 'integer', ' 10', 10 ], + [ 'integer', '0x1F', 31 ], + [ 'integer', '1e3', 1 ], + [ 'integer', 'x$5', 5 ], + [ 'integer', '-$5', 5 ], + [ 'integer', '$5', NaN ], + [ 'integer', '', NaN ], + [ 'integer', 'abc', NaN ], + [ 'integer', true, NaN ], + ]) { + test(`cxjs_convert(${JSON.stringify(dt)}, ${JSON.stringify(v)}) = ${result}`, () => + { + assert.equal(env.cxjs_convert(dt, v), result); + }); + } + + // Conversion to double. A leading currency marker is stripped in + // several forms ('$', ' $', '+$', '$ ', '-$'); '-$' negates the + // result. Anything else falls through to parseFloat. + for (const [ dt, v, result ] of [ + // Datatype Value Result + [ 'double', 0, 0 ], + [ 'double', 5.5, 5.5 ], + [ 'double', '5.5', 5.5 ], + [ 'double', '3.14abc', 3.14 ], + [ 'double', '$5', 5 ], + [ 'double', '$5.50', 5.5 ], + [ 'double', ' $5', 5 ], + [ 'double', '+$5', 5 ], + [ 'double', '$ 5', 5 ], + [ 'double', '-$5', -5 ], + [ 'double', '-$2.5', -2.5 ], + [ 'double', '$1,000', 1 ], + [ 'double', '$', NaN ], + [ 'double', 'abc', NaN ], + ]) { + test(`cxjs_convert(${JSON.stringify(dt)}, ${JSON.stringify(v)}) = ${result}`, () => + { + assert.equal(env.cxjs_convert(dt, v), result); + }); + } + + // Conversion to string is just '' + v, so every value is coerced. + for (const [ dt, v, result ] of [ + // Datatype Value Result + [ 'string', 0, '0' ], + [ 'string', 5, '5' ], + [ 'string', 5.5, '5.5' ], + [ 'string', -3.2, '-3.2' ], + [ 'string', 'hello', 'hello' ], + [ 'string', true, 'true' ], + ]) { + test(`cxjs_convert(${JSON.stringify(dt)}, ${JSON.stringify(v)}) = ${result}`, () => + { + assert.equal(env.cxjs_convert(dt, v), result); + }); + } + + // An unrecognized datatype returns the value unchanged. + for (const [ dt, v, result ] of [ + // Datatype Value Result + [ 'money', 5, 5 ], + [ 'datetime', 'x', 'x' ], + [ 'MyType', 42, 42 ], + ]) { + test(`cxjs_convert(${JSON.stringify(dt)}, ${JSON.stringify(v)}) = ${result}`, () => + { + assert.equal(env.cxjs_convert(dt, v), result); + }); + } + }); From 3410b41e26d38b69e18bb38c4ab26dd4a13b72c2 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 10:58:52 -0600 Subject: [PATCH 13/56] Add tests for cxjs_isnull(). --- .../sys/js/tests/cxjs_isnull.test.js | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_isnull.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_isnull.test.js b/centrallix-os/sys/js/tests/cxjs_isnull.test.js new file mode 100644 index 000000000..16708ad4d --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_isnull.test.js @@ -0,0 +1,93 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +describe('cxjs_isnull', () => + { + // When the value is null or undefined, the default is returned. + // cxjs_isnull uses loose equality (v == null), so both null and + // undefined are treated as "null". + for (const [ value, dflt, result ] of [ + // Value Default Result + [ null, 5, 5 ], + [ undefined, 5, 5 ], + [ null, "default", "default" ], + [ undefined, "default", "default" ], + [ null, 0, 0 ], + [ null, "", "" ], + [ null, false, false ], + [ null, Infinity, Infinity ], + [ null, NaN, NaN ], + // The default may itself be null or undefined. + [ null, null, null ], + [ null, undefined, undefined ], + [ undefined, null, null ], + [ undefined, undefined, undefined ], + ]) { + test(`cxjs_isnull(${JSON.stringify(value)}, ${JSON.stringify(dflt)}) = ${JSON.stringify(result)}`, () => + { + assert.equal(env.cxjs_isnull(value, dflt), result); + }); + } + + // When the value is not null/undefined, it is returned unchanged and + // the default is ignored. Falsy-but-defined values (0, "", false, NaN) + // are NOT treated as null. + for (const [ value, dflt, result ] of [ + // Value Default Result + [ 0, 5, 0 ], + [ "", 5, "" ], + [ false, 5, false ], + [ NaN, 5, NaN ], + [ 42, 5, 42 ], + [ -1.1, 5, -1.1 ], + [ Infinity, 5, Infinity ], + [ -Infinity, 5, -Infinity ], + [ "foo", "bar", "foo" ], + [ true, false, true ], + // The value is returned even when the default is null/undefined. + [ 0, undefined, 0 ], + [ "", null, "" ], + ]) { + test(`cxjs_isnull(${JSON.stringify(value)}, ${JSON.stringify(dflt)}) = ${JSON.stringify(result)}`, () => + { + assert.equal(env.cxjs_isnull(value, dflt), result); + }); + } + + // Objects and arrays are not null, so they pass through by reference. + for (const value of [ + {}, + { a: 1 }, + [], + [1, 2, 3], + ]) { + test(`cxjs_isnull(${JSON.stringify(value)}, "default") returns the value itself`, () => + { + assert.equal(env.cxjs_isnull(value, "default"), value); + }); + } + + // Only the first argument is consulted; missing the default yields + // undefined when the value is null. + test('cxjs_isnull(null) = undefined (default omitted)', () => + { + assert.equal(env.cxjs_isnull(null), undefined); + }); + test('cxjs_isnull(0) = 0 (default omitted)', () => + { + assert.equal(env.cxjs_isnull(0), 0); + }); + }); From c6a42767e35d88aad5c10ecd3111ea941d2780ce Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 11:02:45 -0600 Subject: [PATCH 14/56] Clean up comments on cxjs_getdate() tests. --- centrallix-os/sys/js/tests/cxjs_getdate.test.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/centrallix-os/sys/js/tests/cxjs_getdate.test.js b/centrallix-os/sys/js/tests/cxjs_getdate.test.js index 2bc182207..166d1f6f1 100644 --- a/centrallix-os/sys/js/tests/cxjs_getdate.test.js +++ b/centrallix-os/sys/js/tests/cxjs_getdate.test.js @@ -14,13 +14,8 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); -// cxjs_getdate() reads the current time, shifts it backward by pg_clockoffset -// milliseconds, and formats it as "M/D/YYYY H:MM:SS" -- the month, day, and -// hour are NOT zero-padded, but the minute and second always are. new Date() -// is non-deterministic, so each call runs against a fake Date swapped into the -// sandbox: it wraps a genuine Date built from a fixed epoch and answers the -// getters/setter cxjs_getdate() uses in UTC, which keeps the test independent -// of the host timezone while preserving native setMilliseconds() rollover. +// Run cxjs_getdate() at a fixed instant: swap in a fake Date pinned to epochMs +// that answers in UTC, so results don't depend on the host timezone. function getdateAt(epochMs, clockoffset) { class FakeDate @@ -57,7 +52,10 @@ function utc(year, month, day, hour, min, sec, ms) describe('cxjs_getdate', () => { - // Formatting, with no clock offset. + // Format is "M/D/YYYY H:MM:SS". Hour is unpadded, + // but minute and second are always zero-padded. + + // Test dates with no pg_clockoffset. for (const [ when, result ] of [ // When Result [ utc(2026, 1, 1, 0, 5, 9), '1/1/2026 0:05:09' ], // midnight; single-digit min/sec padded, hour not @@ -77,8 +75,7 @@ describe('cxjs_getdate', () => }); } - // pg_clockoffset shifts the reported time backward by that many ms (a - // negative offset therefore shifts it forward). + // Test dates with pg_clockoffset, which shifts the time backward in ms (negative = forward). for (const [ when, offset, result ] of [ // When Offset Result [ utc(2026, 6, 15, 12, 0, 30), 60000, '6/15/2026 11:59:30' ], // back 60s, crossing the minute and hour From ff19173e2bbf6aaea46b1a25d43612bf965eb815 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 11:03:25 -0600 Subject: [PATCH 15/56] Add more date test cases. --- centrallix-os/sys/js/tests/cxjs_getdate.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/centrallix-os/sys/js/tests/cxjs_getdate.test.js b/centrallix-os/sys/js/tests/cxjs_getdate.test.js index 166d1f6f1..5b216138f 100644 --- a/centrallix-os/sys/js/tests/cxjs_getdate.test.js +++ b/centrallix-os/sys/js/tests/cxjs_getdate.test.js @@ -83,6 +83,7 @@ describe('cxjs_getdate', () => [ utc(2026, 6, 15, 0, 0, 30), 60000, '6/14/2026 23:59:30' ], // back across the day boundary [ utc(2026, 1, 1, 0, 0, 30), 60000, '12/31/2025 23:59:30' ], // back across the month and year boundary [ utc(2026, 6, 15, 0, 0, 0), 500, '6/14/2026 23:59:59' ], // sub-second offset still rolls the day back + [ utc(2026, 6, 15, 23, 59, 59), -2000, '6/16/2026 0:00:01' ], // forward across the day boundary [ utc(2025, 12, 31, 23, 59, 59), -1000, '1/1/2026 0:00:00' ], // forward 1s across the year boundary ]) { test(`cxjs_getdate() with pg_clockoffset ${offset}ms = "${result}"`, () => From d15cc50a538aedac9d469233d1928bfc75409364 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 11:05:58 -0600 Subject: [PATCH 16/56] Improve spacing. --- .../sys/js/tests/cxjs_getdate.test.js | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/centrallix-os/sys/js/tests/cxjs_getdate.test.js b/centrallix-os/sys/js/tests/cxjs_getdate.test.js index 5b216138f..966d1e557 100644 --- a/centrallix-os/sys/js/tests/cxjs_getdate.test.js +++ b/centrallix-os/sys/js/tests/cxjs_getdate.test.js @@ -57,17 +57,17 @@ describe('cxjs_getdate', () => // Test dates with no pg_clockoffset. for (const [ when, result ] of [ - // When Result - [ utc(2026, 1, 1, 0, 5, 9), '1/1/2026 0:05:09' ], // midnight; single-digit min/sec padded, hour not - [ utc(2026, 12, 25, 23, 59, 59), '12/25/2026 23:59:59' ], // two-digit fields left unpadded - [ utc(2026, 6, 15, 10, 10, 10), '6/15/2026 10:10:10' ], // 10 is the pad boundary: no leading zero - [ utc(2026, 7, 4, 12, 0, 0), '7/4/2026 12:00:00' ], // zero min/sec render as "00" - [ utc(2026, 3, 9, 7, 30, 45), '3/9/2026 7:30:45' ], // single-digit hour stays unpadded - [ utc(1999, 12, 31, 23, 59, 59), '12/31/1999 23:59:59' ], // four-digit year from a different century - [ utc(2024, 2, 29, 12, 0, 0), '2/29/2024 12:00:00' ], // leap day - [ utc(2026, 6, 15, 12, 30, 45, 500),'6/15/2026 12:30:45' ], // milliseconds never appear in the output - [ utc(2026, 1, 31, 0, 0, 0), '1/31/2026 0:00:00' ], // January is month 1 (getMonth() + 1) - [ utc(2026, 10, 5, 9, 9, 9), '10/5/2026 9:09:09' ], // two-digit month; 9 is just below the pad boundary + // When Result + [ utc(2026, 1, 1, 0, 5, 9), '1/1/2026 0:05:09' ], // midnight; single-digit min/sec padded, hour not + [ utc(2026, 12, 25, 23, 59, 59), '12/25/2026 23:59:59' ], // two-digit fields left unpadded + [ utc(2026, 6, 15, 10, 10, 10), '6/15/2026 10:10:10' ], // 10 is the pad boundary: no leading zero + [ utc(2026, 7, 4, 12, 0, 0), '7/4/2026 12:00:00' ], // zero min/sec render as "00" + [ utc(2026, 3, 9, 7, 30, 45), '3/9/2026 7:30:45' ], // single-digit hour stays unpadded + [ utc(1999, 12, 31, 23, 59, 59), '12/31/1999 23:59:59' ], // four-digit year from a different century + [ utc(2024, 2, 29, 12, 0, 0), '2/29/2024 12:00:00' ], // leap day + [ utc(2026, 6, 15, 12, 30, 45, 500), '6/15/2026 12:30:45' ], // milliseconds never appear in the output + [ utc(2026, 1, 31, 0, 0, 0), '1/31/2026 0:00:00' ], // January is month 1 (getMonth() + 1) + [ utc(2026, 10, 5, 9, 9, 9), '10/5/2026 9:09:09' ], // two-digit month; 9 is just below the pad boundary ]) { test(`cxjs_getdate() = "${result}"`, () => { @@ -77,14 +77,14 @@ describe('cxjs_getdate', () => // Test dates with pg_clockoffset, which shifts the time backward in ms (negative = forward). for (const [ when, offset, result ] of [ - // When Offset Result - [ utc(2026, 6, 15, 12, 0, 30), 60000, '6/15/2026 11:59:30' ], // back 60s, crossing the minute and hour - [ utc(2026, 6, 15, 12, 0, 30), -90000, '6/15/2026 12:02:00' ], // forward 90s; minute padded back to "02" - [ utc(2026, 6, 15, 0, 0, 30), 60000, '6/14/2026 23:59:30' ], // back across the day boundary - [ utc(2026, 1, 1, 0, 0, 30), 60000, '12/31/2025 23:59:30' ], // back across the month and year boundary - [ utc(2026, 6, 15, 0, 0, 0), 500, '6/14/2026 23:59:59' ], // sub-second offset still rolls the day back - [ utc(2026, 6, 15, 23, 59, 59), -2000, '6/16/2026 0:00:01' ], // forward across the day boundary - [ utc(2025, 12, 31, 23, 59, 59), -1000, '1/1/2026 0:00:00' ], // forward 1s across the year boundary + // When Offset Result + [ utc(2026, 6, 15, 12, 0, 30), 60000, '6/15/2026 11:59:30' ], // back 60s, crossing the minute and hour + [ utc(2026, 6, 15, 12, 0, 30), -90000, '6/15/2026 12:02:00' ], // forward 90s; minute padded back to "02" + [ utc(2026, 6, 15, 0, 0, 30), 60000, '6/14/2026 23:59:30' ], // back across the day boundary + [ utc(2026, 1, 1, 0, 0, 30), 60000, '12/31/2025 23:59:30' ], // back across the month and year boundary + [ utc(2026, 6, 15, 0, 0, 0), 500, '6/14/2026 23:59:59' ], // sub-second offset still rolls the day back + [ utc(2026, 6, 15, 23, 59, 59), -2000, '6/16/2026 0:00:01' ], // forward across the day boundary + [ utc(2025, 12, 31, 23, 59, 59), -1000, '1/1/2026 0:00:00' ], // forward 1s across the year boundary ]) { test(`cxjs_getdate() with pg_clockoffset ${offset}ms = "${result}"`, () => { From 8bb47aabcd0a80506c8d7530b25b35d9e301690e Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 11:12:07 -0600 Subject: [PATCH 17/56] Clean up comments on cxjs_isnull() tests. --- centrallix-os/sys/js/tests/cxjs_isnull.test.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/centrallix-os/sys/js/tests/cxjs_isnull.test.js b/centrallix-os/sys/js/tests/cxjs_isnull.test.js index 16708ad4d..e72d30cd3 100644 --- a/centrallix-os/sys/js/tests/cxjs_isnull.test.js +++ b/centrallix-os/sys/js/tests/cxjs_isnull.test.js @@ -16,9 +16,7 @@ const env = require('./_setup'); describe('cxjs_isnull', () => { - // When the value is null or undefined, the default is returned. - // cxjs_isnull uses loose equality (v == null), so both null and - // undefined are treated as "null". + // null/undefined values use the default. for (const [ value, dflt, result ] of [ // Value Default Result [ null, 5, 5 ], @@ -42,9 +40,8 @@ describe('cxjs_isnull', () => }); } - // When the value is not null/undefined, it is returned unchanged and - // the default is ignored. Falsy-but-defined values (0, "", false, NaN) - // are NOT treated as null. + // Non null/undefined pass through unchanged. Falsy-but-defined values + // (0, "", false, NaN) are NOT treated as null. for (const [ value, dflt, result ] of [ // Value Default Result [ 0, 5, 0 ], @@ -67,7 +64,7 @@ describe('cxjs_isnull', () => }); } - // Objects and arrays are not null, so they pass through by reference. + // Objects and arrays are treated as not null. for (const value of [ {}, { a: 1 }, From b17e32a73804b89b2567e486e594a184c5ba179c Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 11:13:47 -0600 Subject: [PATCH 18/56] Add tests for cxjs_right(). --- centrallix-os/sys/js/tests/cxjs_right.test.js | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_right.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_right.test.js b/centrallix-os/sys/js/tests/cxjs_right.test.js new file mode 100644 index 000000000..28ffbc422 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_right.test.js @@ -0,0 +1,42 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +describe('cxjs_right', () => + { + for (const [ s, l, result ] of [ + // String Length Result + [ "hello", 2, "lo" ], + [ "hello", 1, "o" ], + [ "hello", 5, "hello" ], // l equals length: whole string + [ "hello", 10, "hello" ], // l exceeds length: whole string + [ "hello", 0, "" ], // zero length: empty + [ "hello", -2, "" ], // negative length: empty + [ "", 3, "" ], // empty string stays empty + [ "", 0, "" ], + [ "x", 1, "x" ], // single char + [ " ab ", 2, "b " ], // trailing whitespace preserved + [ "abc", null, null ], // null length + [ "abc", undefined, null ], // undefined length + [ null, 2, null ], // null string + [ undefined, 2, null ], // undefined string + [ null, null, null ], + ]) { + test(`cxjs_right(${JSON.stringify(s)}, ${l}) = ${JSON.stringify(result)}`, () => + { + assert.equal(env.cxjs_right(s, l), result); + }); + } + }); From 263bba88c669c51593a46d44d843eea7e6157d30 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 11:15:35 -0600 Subject: [PATCH 19/56] Add more cxjs_right() test cases. --- centrallix-os/sys/js/tests/cxjs_right.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/centrallix-os/sys/js/tests/cxjs_right.test.js b/centrallix-os/sys/js/tests/cxjs_right.test.js index 28ffbc422..af38047b0 100644 --- a/centrallix-os/sys/js/tests/cxjs_right.test.js +++ b/centrallix-os/sys/js/tests/cxjs_right.test.js @@ -27,7 +27,8 @@ describe('cxjs_right', () => [ "", 3, "" ], // empty string stays empty [ "", 0, "" ], [ "x", 1, "x" ], // single char - [ " ab ", 2, "b " ], // trailing whitespace preserved + [ " ab ", 2, "b " ], // trailing whitespace preserved + [ " ab ", 4, " ab " ], // preceding whitespace preserved [ "abc", null, null ], // null length [ "abc", undefined, null ], // undefined length [ null, 2, null ], // null string From 37b409674a2dd25de3c6696409fcec5ea8f468a3 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 11:16:03 -0600 Subject: [PATCH 20/56] Fix spacing in cxjs_right() tests. --- centrallix-os/sys/js/tests/cxjs_right.test.js | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/centrallix-os/sys/js/tests/cxjs_right.test.js b/centrallix-os/sys/js/tests/cxjs_right.test.js index af38047b0..1defa46d8 100644 --- a/centrallix-os/sys/js/tests/cxjs_right.test.js +++ b/centrallix-os/sys/js/tests/cxjs_right.test.js @@ -17,23 +17,23 @@ const env = require('./_setup'); describe('cxjs_right', () => { for (const [ s, l, result ] of [ - // String Length Result - [ "hello", 2, "lo" ], - [ "hello", 1, "o" ], - [ "hello", 5, "hello" ], // l equals length: whole string - [ "hello", 10, "hello" ], // l exceeds length: whole string - [ "hello", 0, "" ], // zero length: empty - [ "hello", -2, "" ], // negative length: empty - [ "", 3, "" ], // empty string stays empty - [ "", 0, "" ], - [ "x", 1, "x" ], // single char - [ " ab ", 2, "b " ], // trailing whitespace preserved - [ " ab ", 4, " ab " ], // preceding whitespace preserved - [ "abc", null, null ], // null length - [ "abc", undefined, null ], // undefined length - [ null, 2, null ], // null string - [ undefined, 2, null ], // undefined string - [ null, null, null ], + // String Length Result + [ "hello", 2, "lo" ], + [ "hello", 1, "o" ], + [ "hello", 5, "hello" ], // l equals length: whole string + [ "hello", 10, "hello" ], // l exceeds length: whole string + [ "hello", 0, "" ], // zero length: empty + [ "hello", -2, "" ], // negative length: empty + [ "", 3, "" ], // empty string stays empty + [ "", 0, "" ], + [ "x", 1, "x" ], // single char + [ " ab ", 2, "b " ], // trailing whitespace preserved + [ " ab ", 4, " ab " ], // preceding whitespace preserved + [ "abc", null, null ], // null length + [ "abc", undefined, null ], // undefined length + [ null, 2, null ], // null string + [ undefined, 2, null ], // undefined string + [ null, null, null ], ]) { test(`cxjs_right(${JSON.stringify(s)}, ${l}) = ${JSON.stringify(result)}`, () => { From 9182b7b30b5add2852e933822ba37dd23953d8ec Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 11:20:07 -0600 Subject: [PATCH 21/56] Improve spacing in cxjs_convert() tests. --- .../sys/js/tests/cxjs_convert.test.js | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/centrallix-os/sys/js/tests/cxjs_convert.test.js b/centrallix-os/sys/js/tests/cxjs_convert.test.js index 19bef255e..1b4cccfb6 100644 --- a/centrallix-os/sys/js/tests/cxjs_convert.test.js +++ b/centrallix-os/sys/js/tests/cxjs_convert.test.js @@ -38,22 +38,22 @@ describe('cxjs_convert', () => // the *second* character (e.g. 'x$5'); a leading '$5' is not, so it // parses to NaN. Stripping the '$' also drops a leading sign. for (const [ dt, v, result ] of [ - // Datatype Value Result - [ 'integer', 0, 0 ], - [ 'integer', 5, 5 ], - [ 'integer', -7, -7 ], - [ 'integer', 5.9, 5 ], - [ 'integer', '42', 42 ], - [ 'integer', '42abc', 42 ], - [ 'integer', ' 10', 10 ], - [ 'integer', '0x1F', 31 ], - [ 'integer', '1e3', 1 ], - [ 'integer', 'x$5', 5 ], - [ 'integer', '-$5', 5 ], - [ 'integer', '$5', NaN ], - [ 'integer', '', NaN ], - [ 'integer', 'abc', NaN ], - [ 'integer', true, NaN ], + // Datatype Value Result + [ 'integer', 0, 0 ], + [ 'integer', 5, 5 ], + [ 'integer', -7, -7 ], + [ 'integer', 5.9, 5 ], + [ 'integer', '42', 42 ], + [ 'integer', '42abc', 42 ], + [ 'integer', ' 10', 10 ], + [ 'integer', '0x1F', 31 ], + [ 'integer', '1e3', 1 ], + [ 'integer', 'x$5', 5 ], + [ 'integer', '-$5', 5 ], + [ 'integer', '$5', NaN ], + [ 'integer', '', NaN ], + [ 'integer', 'abc', NaN ], + [ 'integer', true, NaN ], ]) { test(`cxjs_convert(${JSON.stringify(dt)}, ${JSON.stringify(v)}) = ${result}`, () => { @@ -65,21 +65,21 @@ describe('cxjs_convert', () => // several forms ('$', ' $', '+$', '$ ', '-$'); '-$' negates the // result. Anything else falls through to parseFloat. for (const [ dt, v, result ] of [ - // Datatype Value Result - [ 'double', 0, 0 ], - [ 'double', 5.5, 5.5 ], - [ 'double', '5.5', 5.5 ], - [ 'double', '3.14abc', 3.14 ], - [ 'double', '$5', 5 ], - [ 'double', '$5.50', 5.5 ], - [ 'double', ' $5', 5 ], - [ 'double', '+$5', 5 ], - [ 'double', '$ 5', 5 ], - [ 'double', '-$5', -5 ], - [ 'double', '-$2.5', -2.5 ], - [ 'double', '$1,000', 1 ], - [ 'double', '$', NaN ], - [ 'double', 'abc', NaN ], + // Datatype Value Result + [ 'double', 0, 0 ], + [ 'double', 5.5, 5.5 ], + [ 'double', '5.5', 5.5 ], + [ 'double', '3.14abc', 3.14 ], + [ 'double', '$5', 5 ], + [ 'double', '$5.50', 5.5 ], + [ 'double', ' $5', 5 ], + [ 'double', '+$5', 5 ], + [ 'double', '$ 5', 5 ], + [ 'double', '-$5', -5 ], + [ 'double', '-$2.5', -2.5 ], + [ 'double', '$1,000', 1 ], + [ 'double', '$', NaN ], + [ 'double', 'abc', NaN ], ]) { test(`cxjs_convert(${JSON.stringify(dt)}, ${JSON.stringify(v)}) = ${result}`, () => { From 90a6a8102d38067d28319961b6fa9e04a91890ac Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 11:26:46 -0600 Subject: [PATCH 22/56] Improve comments on cxjs_convert() tests. --- .../sys/js/tests/cxjs_convert.test.js | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/centrallix-os/sys/js/tests/cxjs_convert.test.js b/centrallix-os/sys/js/tests/cxjs_convert.test.js index 1b4cccfb6..fe5f2187c 100644 --- a/centrallix-os/sys/js/tests/cxjs_convert.test.js +++ b/centrallix-os/sys/js/tests/cxjs_convert.test.js @@ -34,9 +34,10 @@ describe('cxjs_convert', () => }); } - // Conversion to integer. Note the '$' is only stripped when it is - // the *second* character (e.g. 'x$5'); a leading '$5' is not, so it - // parses to NaN. Stripping the '$' also drops a leading sign. + // Conversion to integer. + // Note: The '$' is only stripped when it is the *second* character + // (e.g. 'x$5'). A leading '$5' is not, so it parses to NaN. Stripping + // the '$' also drops a leading sign. for (const [ dt, v, result ] of [ // Datatype Value Result [ 'integer', 0, 0 ], @@ -45,9 +46,9 @@ describe('cxjs_convert', () => [ 'integer', 5.9, 5 ], [ 'integer', '42', 42 ], [ 'integer', '42abc', 42 ], - [ 'integer', ' 10', 10 ], - [ 'integer', '0x1F', 31 ], - [ 'integer', '1e3', 1 ], + [ 'integer', ' 10', 10 ], // Whitespace is stripped. + [ 'integer', '0x1F', 31 ], // Hex notation is handled. + [ 'integer', '1e3', 1 ], // Scientific notation is ignored. [ 'integer', 'x$5', 5 ], [ 'integer', '-$5', 5 ], [ 'integer', '$5', NaN ], @@ -61,9 +62,10 @@ describe('cxjs_convert', () => }); } - // Conversion to double. A leading currency marker is stripped in - // several forms ('$', ' $', '+$', '$ ', '-$'); '-$' negates the - // result. Anything else falls through to parseFloat. + // Conversion to double. + // A leading currency marker is stripped in several forms ('$', ' $', + // '+$', '$ ', '-$'). '-$' negates the result. Anything else is passed + // through to parseFloat. for (const [ dt, v, result ] of [ // Datatype Value Result [ 'double', 0, 0 ], From ea1181f3933ba876b5b373e2d50d5be2acb1b87c Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 11:32:52 -0600 Subject: [PATCH 23/56] Add more tests to convert(). --- centrallix-os/sys/js/tests/cxjs_convert.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/centrallix-os/sys/js/tests/cxjs_convert.test.js b/centrallix-os/sys/js/tests/cxjs_convert.test.js index fe5f2187c..f779509f6 100644 --- a/centrallix-os/sys/js/tests/cxjs_convert.test.js +++ b/centrallix-os/sys/js/tests/cxjs_convert.test.js @@ -78,6 +78,7 @@ describe('cxjs_convert', () => [ 'double', '+$5', 5 ], [ 'double', '$ 5', 5 ], [ 'double', '-$5', -5 ], + [ 'double', '-$ 5', -5 ], [ 'double', '-$2.5', -2.5 ], [ 'double', '$1,000', 1 ], [ 'double', '$', NaN ], @@ -89,7 +90,7 @@ describe('cxjs_convert', () => }); } - // Conversion to string is just '' + v, so every value is coerced. + // Conversion to string (using standard JS coercion). for (const [ dt, v, result ] of [ // Datatype Value Result [ 'string', 0, '0' ], From b2089fc219e431a8057fa53d1b707e175aa2934c Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 11:32:57 -0600 Subject: [PATCH 24/56] Add tests for cxjs_rtrim(). --- centrallix-os/sys/js/tests/cxjs_rtrim.test.js | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_rtrim.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_rtrim.test.js b/centrallix-os/sys/js/tests/cxjs_rtrim.test.js new file mode 100644 index 000000000..22bc277a4 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_rtrim.test.js @@ -0,0 +1,45 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +describe('cxjs_rtrim', () => + { + for (const [ s, result ] of [ + // String Result + [ 'hello ', 'hello' ], // trailing spaces trimmed + [ ' hello', ' hello' ], // leading spaces preserved + [ ' hello ', ' hello' ], // only trailing trimmed + [ 'hello', 'hello' ], // no change + [ 'a b ', 'a b' ], // internal spaces preserved + [ '', '' ], // empty stays empty + [ ' ', '' ], // single space + [ ' ', '' ], // all spaces trimmed + [ 'hello\t', 'hello\t' ], // trailing tab not trimmed + [ 'hello\n', 'hello\n' ], // trailing newline not trimmed + [ 'hello\t ', 'hello\t' ], // space after tab trimmed + [ 'hello \t', 'hello \t' ], // space before tab preserved + [ 42, '42' ], // number coerced to string + [ 0, '0' ], // falsy but not null: coerced, not dropped + [ true, 'true' ], // boolean coerced to string + [ NaN, 'NaN' ], // NaN coerced to string + [ null, null ], // null returns null + [ undefined, null ], // undefined returns null + ]) { + test(`cxjs_rtrim(${JSON.stringify(s)}) = ${JSON.stringify(result)}`, () => + { + assert.equal(env.cxjs_rtrim(s), result); + }); + } + }); From 2fb949816389771ca6c2677d4d0cc8eedc3729f7 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 11:33:02 -0600 Subject: [PATCH 25/56] Add tests for cxjs_ltrim(). --- centrallix-os/sys/js/tests/cxjs_ltrim.test.js | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_ltrim.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_ltrim.test.js b/centrallix-os/sys/js/tests/cxjs_ltrim.test.js new file mode 100644 index 000000000..9f3c97246 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_ltrim.test.js @@ -0,0 +1,43 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +describe('cxjs_ltrim', () => + { + for (const [ input, result ] of [ + // Input Result + [ ' hello', 'hello' ], // leading spaces stripped + [ 'hello', 'hello' ], // no leading spaces + [ 'hello ', 'hello ' ], // trailing spaces preserved + [ ' a b ', 'a b ' ], // interior/trailing spaces preserved + [ ' ', '' ], // single space + [ ' ', '' ], // all spaces collapse to empty + [ '', '' ], // empty stays empty + [ ' \thello', '\thello' ], // leading space stripped, tab kept + [ '\t hello', '\t hello' ], // leading tab is not a space: unchanged + [ '\nhi', '\nhi' ], // newline is not a space: unchanged + [ 42, '42' ], // non-string is stringified + [ 0, '0' ], // falsy but not null: coerced, not dropped + [ true, 'true' ], // boolean coerced to string + [ NaN, 'NaN' ], // NaN coerced to string + [ null, null ], // null returns null + [ undefined, null ], // undefined returns null + ]) { + test(`cxjs_ltrim(${JSON.stringify(input)}) = ${JSON.stringify(result)}`, () => + { + assert.equal(env.cxjs_ltrim(input), result); + }); + } + }); From 5defbf8a9b5273f4e3c1a21c7bf0ff77d56873ce Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 11:35:57 -0600 Subject: [PATCH 26/56] Convert to consistent use of single quotes. --- centrallix-os/sys/js/tests/cxjs_count.test.js | 24 ++++++++--------- .../sys/js/tests/cxjs_isnull.test.js | 20 +++++++------- centrallix-os/sys/js/tests/cxjs_right.test.js | 26 +++++++++---------- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/centrallix-os/sys/js/tests/cxjs_count.test.js b/centrallix-os/sys/js/tests/cxjs_count.test.js index 4e3e0b4e8..4f71d3b52 100644 --- a/centrallix-os/sys/js/tests/cxjs_count.test.js +++ b/centrallix-os/sys/js/tests/cxjs_count.test.js @@ -34,11 +34,11 @@ describe('cxjs_count', () => [ [NaN], 0 ], [ [NaN, 1], 1 ], [ [NaN, null, undefined, 5], 1 ], - // Numeric strings count; non-numeric strings do not. "" is numeric (0). - [ ["5", 6], 2 ], - [ ["", 6], 2 ], - [ ["abc", 6], 1 ], - [ ["abc", "def"], 0 ], + // Numeric strings count; non-numeric strings do not. '' is numeric (0). + [ ['5', 6], 2 ], + [ ['', 6], 2 ], + [ ['abc', 6], 1 ], + [ ['abc', 'def'], 0 ], ]) { test(`cxjs_count(${JSON.stringify(input)}) = ${result}`, () => { @@ -64,11 +64,11 @@ describe('cxjs_count', () => [ { L: NaN }, 0 ], [ { M: NaN, N: 1 }, 1 ], [ { O: NaN, P: null, Q: undefined, R: 5 }, 1 ], - // Numeric strings count; non-numeric strings do not. "" is numeric (0). - [ { S: "5", T: 6 }, 2 ], - [ { U: "", V: 6 }, 2 ], - [ { W: "abc", X: 6 }, 1 ], - [ { Y: "abc", Z: "def" }, 0 ], + // Numeric strings count; non-numeric strings do not. '' is numeric (0). + [ { S: '5', T: 6 }, 2 ], + [ { U: '', V: 6 }, 2 ], + [ { W: 'abc', X: 6 }, 1 ], + [ { Y: 'abc', Z: 'def' }, 0 ], ]) { test(`cxjs_count(${JSON.stringify(input)}) = ${result}`, () => { @@ -84,8 +84,8 @@ describe('cxjs_count', () => [ 42, 1 ], [ -1.1, 1 ], [ Infinity, 1 ], - [ "foo", 1 ], - [ "", 1 ], + [ 'foo', 1 ], + [ '', 1 ], [ true, 1 ], [ NaN, 1 ], [ null, 1 ], diff --git a/centrallix-os/sys/js/tests/cxjs_isnull.test.js b/centrallix-os/sys/js/tests/cxjs_isnull.test.js index e72d30cd3..3e9abb0c1 100644 --- a/centrallix-os/sys/js/tests/cxjs_isnull.test.js +++ b/centrallix-os/sys/js/tests/cxjs_isnull.test.js @@ -21,10 +21,10 @@ describe('cxjs_isnull', () => // Value Default Result [ null, 5, 5 ], [ undefined, 5, 5 ], - [ null, "default", "default" ], - [ undefined, "default", "default" ], + [ null, 'default', 'default' ], + [ undefined, 'default', 'default' ], [ null, 0, 0 ], - [ null, "", "" ], + [ null, '', '' ], [ null, false, false ], [ null, Infinity, Infinity ], [ null, NaN, NaN ], @@ -40,23 +40,23 @@ describe('cxjs_isnull', () => }); } - // Non null/undefined pass through unchanged. Falsy-but-defined values - // (0, "", false, NaN) are NOT treated as null. + // Non-null/undefined pass through unchanged. Falsy-but-defined values + // (0, '', false, NaN) are NOT treated as null. for (const [ value, dflt, result ] of [ // Value Default Result [ 0, 5, 0 ], - [ "", 5, "" ], + [ '', 5, '' ], [ false, 5, false ], [ NaN, 5, NaN ], [ 42, 5, 42 ], [ -1.1, 5, -1.1 ], [ Infinity, 5, Infinity ], [ -Infinity, 5, -Infinity ], - [ "foo", "bar", "foo" ], + [ 'foo', 'bar', 'foo' ], [ true, false, true ], // The value is returned even when the default is null/undefined. [ 0, undefined, 0 ], - [ "", null, "" ], + [ '', null, '' ], ]) { test(`cxjs_isnull(${JSON.stringify(value)}, ${JSON.stringify(dflt)}) = ${JSON.stringify(result)}`, () => { @@ -71,9 +71,9 @@ describe('cxjs_isnull', () => [], [1, 2, 3], ]) { - test(`cxjs_isnull(${JSON.stringify(value)}, "default") returns the value itself`, () => + test(`cxjs_isnull(${JSON.stringify(value)}, 'default') returns the value itself`, () => { - assert.equal(env.cxjs_isnull(value, "default"), value); + assert.equal(env.cxjs_isnull(value, 'default'), value); }); } diff --git a/centrallix-os/sys/js/tests/cxjs_right.test.js b/centrallix-os/sys/js/tests/cxjs_right.test.js index 1defa46d8..f3c9a9a2f 100644 --- a/centrallix-os/sys/js/tests/cxjs_right.test.js +++ b/centrallix-os/sys/js/tests/cxjs_right.test.js @@ -18,19 +18,19 @@ describe('cxjs_right', () => { for (const [ s, l, result ] of [ // String Length Result - [ "hello", 2, "lo" ], - [ "hello", 1, "o" ], - [ "hello", 5, "hello" ], // l equals length: whole string - [ "hello", 10, "hello" ], // l exceeds length: whole string - [ "hello", 0, "" ], // zero length: empty - [ "hello", -2, "" ], // negative length: empty - [ "", 3, "" ], // empty string stays empty - [ "", 0, "" ], - [ "x", 1, "x" ], // single char - [ " ab ", 2, "b " ], // trailing whitespace preserved - [ " ab ", 4, " ab " ], // preceding whitespace preserved - [ "abc", null, null ], // null length - [ "abc", undefined, null ], // undefined length + [ 'hello', 2, 'lo' ], + [ 'hello', 1, 'o' ], + [ 'hello', 5, 'hello' ], // l equals length: whole string + [ 'hello', 10, 'hello' ], // l exceeds length: whole string + [ 'hello', 0, '' ], // zero length: empty + [ 'hello', -2, '' ], // negative length: empty + [ '', 3, '' ], // empty string stays empty + [ '', 0, '' ], + [ 'x', 1, 'x' ], // single char + [ ' ab ', 2, 'b ' ], // trailing whitespace preserved + [ ' ab ', 4, ' ab ' ], // preceding whitespace preserved + [ 'abc', null, null ], // null length + [ 'abc', undefined, null ], // undefined length [ null, 2, null ], // null string [ undefined, 2, null ], // undefined string [ null, null, null ], From 234fa7b108a0e8caf0ddd5a3dd36f9e3906c05de Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 11:43:44 -0600 Subject: [PATCH 27/56] Add tests for cxjs_plus(). --- centrallix-os/sys/js/tests/cxjs_plus.test.js | 64 ++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_plus.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_plus.test.js b/centrallix-os/sys/js/tests/cxjs_plus.test.js new file mode 100644 index 000000000..46da16c08 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_plus.test.js @@ -0,0 +1,64 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// JSON.stringify renders Infinity/NaN as "null"; show numbers verbatim so +// those rows stay distinct, and quote strings so they're not confused with +// numbers (concatenation vs. addition hinges on the operand type). +const fmt = (v) => (typeof v === 'number') ? String(v) : JSON.stringify(v); + +describe('cxjs_plus', () => + { + for (const [ a, b, result ] of [ + // a b Result + // Null/undefined operands yield null, even alongside a string. + [ null, 1, null ], + [ 1, null, null ], + [ undefined, 1, null ], + [ 1, undefined, null ], + [ null, null, null ], + [ null, 'x', null ], + [ undefined, undefined, null ], + + // Numeric addition. + [ 0, 0, 0 ], + [ 1, 2, 3 ], + [ -1, 1, 0 ], + [ -2, -3, -5 ], + [ 1.5, 2.25, 3.75 ], + [ -2.5, 1.25, -1.25 ], + [ Infinity, 1, Infinity ], + [ -Infinity, 1, -Infinity ], + [ Infinity, Infinity, Infinity ], + [ Infinity, -Infinity, NaN ], // opposite infinities + + // String concatenation when either operand is a string. + [ 'foo', 'bar', 'foobar' ], + [ '', '', '' ], + [ 'a', '', 'a' ], + [ 'x', 1, 'x1' ], + [ 1, 'x', '1x' ], + [ 'x', -1, 'x-1' ], + [ '1', '2', '12' ], + [ '1', 2, '12' ], + [ 0, '', '0' ], // string branch beats numeric 0 + [ 'n', Infinity, 'nInfinity' ], + ]) { + test(`cxjs_plus(${fmt(a)}, ${fmt(b)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_plus(a, b), result); + }); + } + }); From bd55f09aee561f2ef3c37e21fcc93b58f0817b58 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 11:44:51 -0600 Subject: [PATCH 28/56] Add tests for cxjs_minus(). --- centrallix-os/sys/js/tests/cxjs_minus.test.js | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_minus.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_minus.test.js b/centrallix-os/sys/js/tests/cxjs_minus.test.js new file mode 100644 index 000000000..95f814701 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_minus.test.js @@ -0,0 +1,77 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +describe('cxjs_minus', () => + { + // Numeric operands subtract. NaN propagates. + for (const [ a, b, result ] of [ + // a b Result + [ 5, 3, 2 ], + [ 3, 5, -2 ], + [ 0, 0, 0 ], + [ 10.5, 0.5, 10 ], + [ -1, -1, 0 ], + [ -5, 3, -8 ], + [ 3, -5, 8 ], + [ Infinity, 1, Infinity ], + [ 1, Infinity, -Infinity ], + [ Infinity, Infinity, NaN ], + [ -Infinity, -Infinity, NaN ], + [ NaN, 1, NaN ], + [ 1, NaN, NaN ], + ]) { + test(`cxjs_minus(${JSON.stringify(a)}, ${JSON.stringify(b)}) = ${JSON.stringify(result)}`, () => + { + assert.equal(env.cxjs_minus(a, b), result); + }); + } + + // A null/undefined operand yields null. + for (const [ a, b ] of [ + [ null, 5 ], + [ 5, null ], + [ null, null ], + [ undefined, 5 ], + [ 5, undefined ], + ]) { + test(`cxjs_minus(${JSON.stringify(a)}, ${JSON.stringify(b)}) = null`, () => + { + assert.equal(env.cxjs_minus(a, b), null); + }); + } + + // When either operand is a string, both are coerced to strings and a + // matching suffix b is stripped from the end of a. + for (const [ a, b, result ] of [ + // a b Result + [ 'hello', 'lo', 'hel' ], + [ 'hello', 'hello', '' ], + [ 'hello', 'xyz', 'hello' ], // No match: unchanged. + [ 'hello', '', 'hello' ], // Empty suffix: unchanged. + [ 'hello', 'HELLO', 'hello' ], // Case-sensitive: unchanged. + [ 'abcabc', 'abc', 'abc' ], // Only the trailing match. + [ '', '', '' ], + [ 'a', 'abc', 'a' ], // b longer than a: unchanged. + [ 100, '0', '10' ], // Coercion: 100 -> '100'. + [ '5', 5, '' ], + [ 5, '5', '' ], + ]) { + test(`cxjs_minus(${JSON.stringify(a)}, ${JSON.stringify(b)}) = ${JSON.stringify(result)}`, () => + { + assert.equal(env.cxjs_minus(a, b), result); + }); + } + }); From 248c480f199671eec710ed281e6c63db60a5a95d Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 11:46:15 -0600 Subject: [PATCH 29/56] Add a test case for cxjs_plus(). --- centrallix-os/sys/js/tests/cxjs_plus.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/centrallix-os/sys/js/tests/cxjs_plus.test.js b/centrallix-os/sys/js/tests/cxjs_plus.test.js index 46da16c08..a4e2353ba 100644 --- a/centrallix-os/sys/js/tests/cxjs_plus.test.js +++ b/centrallix-os/sys/js/tests/cxjs_plus.test.js @@ -55,6 +55,7 @@ describe('cxjs_plus', () => [ '1', 2, '12' ], [ 0, '', '0' ], // string branch beats numeric 0 [ 'n', Infinity, 'nInfinity' ], + [ 'a', NaN, 'aNaN' ], ]) { test(`cxjs_plus(${fmt(a)}, ${fmt(b)}) = ${fmt(result)}`, () => { From 4573b006013d66bf67cc61ba0ea41998d605565a Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 11:50:39 -0600 Subject: [PATCH 30/56] Add tests for cxjs_condition(). --- .../sys/js/tests/cxjs_condition.test.js | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_condition.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_condition.test.js b/centrallix-os/sys/js/tests/cxjs_condition.test.js new file mode 100644 index 000000000..bbd022148 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_condition.test.js @@ -0,0 +1,53 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +describe('cxjs_condition', () => + { + for (const [ c, vtrue, vfalse, result ] of [ + // Condition vtrue vfalse Result + // null/undefined short-circuit to null before vtrue/vfalse. + [ null, 'T', 'F', null ], + [ undefined, 'T', 'F', null ], + // Truthy conditions yield vtrue. + [ true, 'T', 'F', 'T' ], + [ 1, 'T', 'F', 'T' ], + [ -1, 'T', 'F', 'T' ], + [ 3.14, 'T', 'F', 'T' ], + [ Infinity, 'T', 'F', 'T' ], + [ '0', 'T', 'F', 'T' ], // nonempty string + [ 'false', 'T', 'F', 'T' ], + [ ' ', 'T', 'F', 'T' ], + [ [], 'T', 'F', 'T' ], // empty array is truthy + [ {}, 'T', 'F', 'T' ], + // Falsy conditions yield vfalse. + [ false, 'T', 'F', 'F' ], + [ 0, 'T', 'F', 'F' ], + [ -0, 'T', 'F', 'F' ], + [ NaN, 'T', 'F', 'F' ], + [ '', 'T', 'F', 'F' ], + // vtrue/vfalse pass through verbatim, regardless of type. + [ true, 42, 99, 42 ], + [ false, 42, 99, 99 ], + [ true, null, 'F', null ], // truthy c can still return null + [ true, undefined, 'F', undefined ], + [ false, 'T', undefined, undefined ], + ]) { + test(`cxjs_condition(${JSON.stringify(c)}, ${JSON.stringify(vtrue)}, ${JSON.stringify(vfalse)}) = ${JSON.stringify(result)}`, () => + { + assert.equal(env.cxjs_condition(c, vtrue, vfalse), result); + }); + } + }); From c3dbea8ebb4179a7a817ba748c41dbde992afdb7 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 11:52:02 -0600 Subject: [PATCH 31/56] Add tests for cxjs_quote(). --- centrallix-os/sys/js/tests/cxjs_quote.test.js | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_quote.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_quote.test.js b/centrallix-os/sys/js/tests/cxjs_quote.test.js new file mode 100644 index 000000000..dc028a833 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_quote.test.js @@ -0,0 +1,46 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// cxjs_quote() wraps a value in double quotes, escaping any embedded +// double quote as \". Backslashes are NOT escaped, and non-string +// inputs are coerced via String() first. +describe('cxjs_quote', () => + { + for (const [ input, result ] of [ + // Input Result + [ '', '""' ], + [ 'hello', '"hello"' ], + [ '"', '"\\""' ], // lone quote + [ '""', '"\\"\\""' ], // adjacent quotes + [ 'a"b', '"a\\"b"' ], // quote in middle + [ '\\', '"\\"' ], // backslash quoted as-is + [ '\\"', '"\\\\""' ], // backslash + escaped quote + [ "'x'", '"\'x\'"' ], // single quotes untouched + [ 'a\nb', '"a\nb"' ], // newline preserved + [ '\t ', '"\t "' ], // tab/space preserved + [ 123, '"123"' ], // number coerced + [ true, '"true"' ], // boolean coerced + [ null, '"null"' ], // null coerced + [ undefined, '"undefined"' ], // undefined coerced + [ NaN, '"NaN"' ], // NaN coerced + [ [1, 2], '"1,2"' ], // array coerced (with no spaces) + ]) { + test(`cxjs_quote(${JSON.stringify(input)}) = ${JSON.stringify(result)}`, () => + { + assert.equal(env.cxjs_quote(input), result); + }); + } + }); From 7688b0b10825da2418c9ac52bfd8abb7a1cea48d Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 12:01:55 -0600 Subject: [PATCH 32/56] Add tests for cxjs_char_length(). --- .../sys/js/tests/cxjs_char_length.test.js | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_char_length.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_char_length.test.js b/centrallix-os/sys/js/tests/cxjs_char_length.test.js new file mode 100644 index 000000000..3536942c7 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_char_length.test.js @@ -0,0 +1,52 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// cxjs_char_length() returns null for null/undefined, otherwise the +// String()-coerced length in UTF-16 code units (so surrogate pairs +// count as 2). +describe('cxjs_char_length', () => + { + for (const [ input, result ] of [ + // Input Result + [ null, null ], // null short-circuits to null + [ undefined, null ], // == null also catches undefined + [ '', 0 ], // empty string is 0, not null + [ 'a', 1 ], + [ 'hello', 5 ], + [ ' ', 1 ], // spaces count + [ ' ', 2 ], + [ '\t\n ', 3 ], // whitespace chars count + [ 'a"b\\c', 5 ], // quotes/backslashes are literal chars + [ '๐Ÿ˜€', 2 ], // surrogate pair = 2 code units + [ 'cafรฉ', 4 ], // precomposed accent = 1 code unit + [ 0, 1 ], // 0 != null, coerces to "0" + [ 123, 3 ], + [ -12, 3 ], // sign counts + [ 1.5, 3 ], // decimal point counts + [ false, 5 ], // coerced: "false" + [ true, 4 ], // coerced: "true" + [ NaN, 3 ], // coerced: "NaN" + [ Infinity, 8 ], // coerced: "Infinity" + [ [], 0 ], // coerced: "" + [ [1, 2], 3 ], // coerced: "1,2" + [ {}, 15 ], // coerced: "[object Object]" + ]) { + test(`cxjs_char_length(${JSON.stringify(input)}) = ${result}`, () => + { + assert.equal(env.cxjs_char_length(input), result); + }); + } + }); From f68c63b0e2ea9a59a58bc9eddb0bdded2aded6ad Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 12:05:39 -0600 Subject: [PATCH 33/56] Add tests for cxjs_charindex(). --- .../sys/js/tests/cxjs_charindex.test.js | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_charindex.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_charindex.test.js b/centrallix-os/sys/js/tests/cxjs_charindex.test.js new file mode 100644 index 000000000..b2f1af101 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_charindex.test.js @@ -0,0 +1,53 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// charindex returns the 1-based position of needle in haystack, 0 when +// absent, and null when either argument is strictly null. +describe('cxjs_charindex', () => + { + for (const [ needle, haystack, result ] of [ + // Needle Haystack Result + [ 'h', 'hello', 1 ], // at start + [ 'lo', 'hello', 4 ], // multi-char, mid/end + [ 'o', 'hello', 5 ], // at end + [ 'l', 'hello', 3 ], // first of several matches + [ 'hello', 'hello', 1 ], // needle equals haystack + [ 'z', 'hello', 0 ], // absent + [ 'hellox', 'hello', 0 ], // needle longer than haystack + [ 'H', 'hello', 0 ], // case-sensitive: no match + [ '', 'hello', 1 ], // empty needle matches at start + [ '', '', 1 ], // empty needle, empty haystack + [ 'a', '', 0 ], // empty haystack, non-empty needle + [ ' ', 'a b', 2 ], // whitespace needle + [ '$', 'a$b', 2 ], // special character + [ '๐Ÿ˜€', 'a๐Ÿ˜€b', 2 ], // surrogate pair needle + [ '2', 123, 2 ], // numeric haystack coerced to string + [ 2, '123', 2 ], // numeric needle coerced to string + [ 2, 123, 2 ], // multiple coercions + [ null, 'hello', null ], // null needle + [ 'lo', null, null ], // null haystack + [ null, null, null ], // both null + // undefined is not strictly null, so it is coerced, not short-circuited. + [ undefined, 'hello', 0 ], // searches for 'undefined': absent + [ 'u', undefined, 1 ], // haystack becomes 'undefined' + [ 'x', undefined, 0 ], // 'x' absent from 'undefined' + ]) { + test(`cxjs_charindex(${JSON.stringify(needle)}, ${JSON.stringify(haystack)}) = ${JSON.stringify(result)}`, () => + { + assert.equal(env.cxjs_charindex(needle, haystack), result); + }); + } + }); From ee2c122dc1f247fbaf0c1a4f8e5dff18a779f163 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 12:12:33 -0600 Subject: [PATCH 34/56] Add tests for cxjs_lower(). --- centrallix-os/sys/js/tests/cxjs_lower.test.js | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_lower.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_lower.test.js b/centrallix-os/sys/js/tests/cxjs_lower.test.js new file mode 100644 index 000000000..4c699627a --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_lower.test.js @@ -0,0 +1,69 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// JSON.stringify renders NaN/Infinity as "null" and a standalone undefined as +// undefined (not a string), which would give distinct edge-case rows the same +// (or a broken) test name; fmt renders those verbatim so names stay unique. +function fmt(v) + { + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + +describe('cxjs_lower', () => + { + for (const [ input, result ] of [ + // Input Result + [ 'HELLO', 'hello' ], + [ 'hello', 'hello' ], // already lowercase + [ 'Hello World', 'hello world' ], + [ 'MiXeD', 'mixed' ], + [ '', '' ], + [ '123ABC!?', '123abc!?' ], // digits/symbols pass through + [ ' SPACES ', ' spaces ' ], // leading/trailing spaces kept + [ '\tT\r', '\tt\r' ], // tab/CR whitespace preserved + [ 'ร€ร‰รŽ', 'ร รฉรฎ' ], // non-ASCII letters + [ 'ฤฐ', 'iฬ‡' ], // one char expands to i + combining dot + [ 'ฮŸฮ”ฮŸฮฃ', 'ฮฟฮดฮฟฯ‚' ], // trailing ฮฃ lowercases to final sigma ฯ‚ + ]) { + test(`cxjs_lower(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_lower(input), result); + }); + } + + // null is returned verbatim; everything else is coerced via String() + // before lowercasing, so non-strings get their string form lowercased. + for (const [ input, result ] of [ + // Input Result + [ null, null ], + [ undefined, 'undefined' ], + [ 5, '5' ], + [ 1.5, '1.5' ], + [ true, 'true' ], + [ false, 'false' ], + [ NaN, 'nan' ], + [ Infinity, 'infinity' ], + [ ['A', 'B'], 'a,b' ], // Joins with ',' and no spaces + [ {}, '[object object]' ], + ]) { + test(`cxjs_lower(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_lower(input), result); + }); + } + }); From 414e26e230c6ee560d69c22ec7d6f307a84e498e Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 12:14:03 -0600 Subject: [PATCH 35/56] Add tests for cxjs_upper(). --- centrallix-os/sys/js/tests/cxjs_upper.test.js | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_upper.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_upper.test.js b/centrallix-os/sys/js/tests/cxjs_upper.test.js new file mode 100644 index 000000000..eaccaf8dc --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_upper.test.js @@ -0,0 +1,60 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// JSON.stringify renders undefined as the bare word "undefined" only inside +// arrays; as a standalone value it yields undefined (not a string), which +// breaks template names. fmt renders such values verbatim so names stay unique. +function fmt(v) + { + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + +describe('cxjs_upper', () => + { + for (const [ input, result ] of [ + // Input Result + [ 'abc', 'ABC' ], + [ 'ABC', 'ABC' ], + [ 'AbC', 'ABC' ], + [ '', '' ], + [ 'hello world', 'HELLO WORLD' ], + [ 'a1b2c3', 'A1B2C3' ], // digits pass through + [ '!@# $%^', '!@# $%^' ], // punctuation/space unchanged + [ '\tn\r', '\tN\r' ], // whitespace preserved + [ 'cafรฉ', 'CAFร‰' ], // accented letter uppercases + [ 'รŸ', 'SS' ], // sharp-s expands to two chars + + // Non-strings are String()-coerced first, then uppercased. Only strict + // null short-circuits to null; undefined does not. + [ null, null ], // sole short-circuit case + [ undefined, 'UNDEFINED' ], // not null, so coerced + [ 123, '123' ], + [ 1.5, '1.5' ], + [ true, 'TRUE' ], + [ false, 'FALSE' ], + [ NaN, 'NAN' ], + [ Infinity, 'INFINITY' ], + [ ['a', 'b'], 'A,B' ], // Joins with ',' and no spaces + [ {}, '[OBJECT OBJECT]' ], // plain object stringifies + ]) { + test(`cxjs_upper(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_upper(input), result); + }); + } + }); From c274e0827b4dad408f3374a806fa21cfa6a1fe4a Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 12:23:46 -0600 Subject: [PATCH 36/56] Improve test coverage with many new tests for obscure edge cases. --- .../sys/js/tests/cxjs_char_length.test.js | 2 + .../sys/js/tests/cxjs_charindex.test.js | 8 ++ .../sys/js/tests/cxjs_convert.test.js | 8 ++ centrallix-os/sys/js/tests/cxjs_count.test.js | 5 ++ centrallix-os/sys/js/tests/cxjs_lower.test.js | 2 + centrallix-os/sys/js/tests/cxjs_max.test.js | 70 +++++++++++++++++ centrallix-os/sys/js/tests/cxjs_min.test.js | 75 +++++++++++++++++++ centrallix-os/sys/js/tests/cxjs_minus.test.js | 5 ++ centrallix-os/sys/js/tests/cxjs_plus.test.js | 12 +++ centrallix-os/sys/js/tests/cxjs_quote.test.js | 2 + centrallix-os/sys/js/tests/cxjs_right.test.js | 13 ++++ centrallix-os/sys/js/tests/cxjs_sum.test.js | 53 +++++++++++++ centrallix-os/sys/js/tests/cxjs_upper.test.js | 3 + .../sys/js/tests/cxjs_user_name.test.js | 1 + 14 files changed, 259 insertions(+) diff --git a/centrallix-os/sys/js/tests/cxjs_char_length.test.js b/centrallix-os/sys/js/tests/cxjs_char_length.test.js index 3536942c7..9faf9602d 100644 --- a/centrallix-os/sys/js/tests/cxjs_char_length.test.js +++ b/centrallix-os/sys/js/tests/cxjs_char_length.test.js @@ -40,8 +40,10 @@ describe('cxjs_char_length', () => [ true, 4 ], // coerced: "true" [ NaN, 3 ], // coerced: "NaN" [ Infinity, 8 ], // coerced: "Infinity" + [ -Infinity, 9 ], // coerced: "-Infinity" (sign counts) [ [], 0 ], // coerced: "" [ [1, 2], 3 ], // coerced: "1,2" + [ [1, [2, 3]], 5 ], // nested array flattens: "1,2,3" [ {}, 15 ], // coerced: "[object Object]" ]) { test(`cxjs_char_length(${JSON.stringify(input)}) = ${result}`, () => diff --git a/centrallix-os/sys/js/tests/cxjs_charindex.test.js b/centrallix-os/sys/js/tests/cxjs_charindex.test.js index b2f1af101..7b35bae61 100644 --- a/centrallix-os/sys/js/tests/cxjs_charindex.test.js +++ b/centrallix-os/sys/js/tests/cxjs_charindex.test.js @@ -44,6 +44,14 @@ describe('cxjs_charindex', () => [ undefined, 'hello', 0 ], // searches for 'undefined': absent [ 'u', undefined, 1 ], // haystack becomes 'undefined' [ 'x', undefined, 0 ], // 'x' absent from 'undefined' + [ undefined, undefined, 1 ], // both -> 'undefined'; found at start + // A null in EITHER position short-circuits first, even when the other + // argument is undefined (which would otherwise be coerced). + [ undefined, null, null ], // null haystack wins + [ null, undefined, null ], // null needle wins + // Non-string needle/haystack coerce via indexOf()/new String(). + [ true, 'xtrueb', 2 ], // boolean needle coerced to 'true' + [ 'b', true, 0 ], // boolean haystack coerced to 'true'; 'b' absent ]) { test(`cxjs_charindex(${JSON.stringify(needle)}, ${JSON.stringify(haystack)}) = ${JSON.stringify(result)}`, () => { diff --git a/centrallix-os/sys/js/tests/cxjs_convert.test.js b/centrallix-os/sys/js/tests/cxjs_convert.test.js index f779509f6..9314808e3 100644 --- a/centrallix-os/sys/js/tests/cxjs_convert.test.js +++ b/centrallix-os/sys/js/tests/cxjs_convert.test.js @@ -55,6 +55,11 @@ describe('cxjs_convert', () => [ 'integer', '', NaN ], [ 'integer', 'abc', NaN ], [ 'integer', true, NaN ], + [ 'integer', '-42', -42 ], // Leading sign kept when '$' is not the 2nd char. + [ 'integer', ' -42', -42 ], // Whitespace stripped, sign kept. + [ 'integer', 'a$bc', NaN ], // 2nd char '$' stripped, but 'bc' isn't numeric. + [ 'integer', Infinity, NaN ], // parseInt('Infinity') is NaN. + [ 'integer', false, NaN ], ]) { test(`cxjs_convert(${JSON.stringify(dt)}, ${JSON.stringify(v)}) = ${result}`, () => { @@ -81,6 +86,9 @@ describe('cxjs_convert', () => [ 'double', '-$ 5', -5 ], [ 'double', '-$2.5', -2.5 ], [ 'double', '$1,000', 1 ], + [ 'double', '-5.5', -5.5 ], // Plain negative, no currency marker. + [ 'double', Infinity, Infinity ], // parseFloat('Infinity') is Infinity. + [ 'double', 'Infinity', Infinity ], [ 'double', '$', NaN ], [ 'double', 'abc', NaN ], ]) { diff --git a/centrallix-os/sys/js/tests/cxjs_count.test.js b/centrallix-os/sys/js/tests/cxjs_count.test.js index 4f71d3b52..5a26a6ed5 100644 --- a/centrallix-os/sys/js/tests/cxjs_count.test.js +++ b/centrallix-os/sys/js/tests/cxjs_count.test.js @@ -39,6 +39,9 @@ describe('cxjs_count', () => [ ['', 6], 2 ], [ ['abc', 6], 1 ], [ ['abc', 'def'], 0 ], + // Booleans are numeric under isNaN (true -> 1, false -> 0), so both count. + [ [true], 1 ], + [ [true, false], 2 ], ]) { test(`cxjs_count(${JSON.stringify(input)}) = ${result}`, () => { @@ -69,6 +72,8 @@ describe('cxjs_count', () => [ { U: '', V: 6 }, 2 ], [ { W: 'abc', X: 6 }, 1 ], [ { Y: 'abc', Z: 'def' }, 0 ], + // Booleans are numeric under isNaN (true -> 1, false -> 0), so both count. + [ { a: true, b: false }, 2 ], ]) { test(`cxjs_count(${JSON.stringify(input)}) = ${result}`, () => { diff --git a/centrallix-os/sys/js/tests/cxjs_lower.test.js b/centrallix-os/sys/js/tests/cxjs_lower.test.js index 4c699627a..dee953da1 100644 --- a/centrallix-os/sys/js/tests/cxjs_lower.test.js +++ b/centrallix-os/sys/js/tests/cxjs_lower.test.js @@ -58,6 +58,8 @@ describe('cxjs_lower', () => [ false, 'false' ], [ NaN, 'nan' ], [ Infinity, 'infinity' ], + [ -Infinity, '-infinity' ], + [ [], '' ], // empty array coerces to '' [ ['A', 'B'], 'a,b' ], // Joins with ',' and no spaces [ {}, '[object object]' ], ]) { diff --git a/centrallix-os/sys/js/tests/cxjs_max.test.js b/centrallix-os/sys/js/tests/cxjs_max.test.js index 6e8ea0ef3..e5c1a31ea 100644 --- a/centrallix-os/sys/js/tests/cxjs_max.test.js +++ b/centrallix-os/sys/js/tests/cxjs_max.test.js @@ -14,6 +14,20 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); +// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which +// would make distinct edge-case rows share a test name; fmt renders those +// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + describe('cxjs_max', () => { for (const [ input, result ] of [ @@ -61,4 +75,60 @@ describe('cxjs_max', () => assert.equal(env.cxjs_max(input), result); }); } + + // Scalar (non-array, non-object) inputs hit the else branch and are + // returned verbatim, with no comparison performed. + for (const [ input, result ] of [ + // Input Result + [ 5, 5 ], + [ -3, -3 ], + [ 0, 0 ], + [ Infinity, Infinity ], + [ NaN, NaN ], + [ 'foo', 'foo' ], + [ true, true ], + [ null, null ], // typeof null is "object", but v !== null is false + [ undefined, undefined ], + ]) { + test(`cxjs_max(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_max(input), result); + }); + } + + // null and NaN within a collection are handled specially. + for (const [ input, result ] of [ + // Input Result + [ [5], 5 ], // single element + [ [NaN, 5], 5 ], // leading NaN replaced by a real value + [ [5, NaN], 5 ], // trailing NaN never beats a real value + [ [NaN], NaN ], // all NaN: NaN survives + [ [NaN, NaN], NaN ], + [ [null, 5], 5 ], // 5 > null (0), so 5 is the max + [ [5, null], 5 ], + [ [0, undefined], 0 ], // undefined after a value is ignored + [ { a: NaN, b: 5 }, 5 ], + [ { a: 5, b: NaN }, 5 ], + [ { a: NaN }, NaN ], + [ { a: null, b: 5 }, 5 ], + [ { a: 5, b: null }, 5 ], + ]) { + test(`cxjs_max(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_max(input), result); + }); + } + + // Non-numeric strings. + for (const [ input, result ] of [ + // Input Result + [ ['b', 'a', 'c'], 'c' ], // last element wins + [ ['apple', 'banana'], 'banana' ], + [ ['10', '9', '100'], '9' ], // lexical compare: '100' < '10' < '9' + ]) { + test(`cxjs_max(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_max(input), result); + }); + } }); diff --git a/centrallix-os/sys/js/tests/cxjs_min.test.js b/centrallix-os/sys/js/tests/cxjs_min.test.js index 5fe151fa8..b865f3ea2 100644 --- a/centrallix-os/sys/js/tests/cxjs_min.test.js +++ b/centrallix-os/sys/js/tests/cxjs_min.test.js @@ -14,6 +14,20 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); +// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which +// would make distinct edge-case rows share a test name; fmt renders those +// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + describe('cxjs_min', () => { for (const [ input, result ] of [ @@ -61,4 +75,65 @@ describe('cxjs_min', () => assert.equal(env.cxjs_min(input), result); }); } + + // Scalar (non-array, non-object) inputs hit the else branch and are + // returned verbatim, with no comparison performed. + for (const [ input, result ] of [ + // Input Result + [ 5, 5 ], + [ -3, -3 ], + [ 0, 0 ], + [ Infinity, Infinity ], + [ NaN, NaN ], + [ 'foo', 'foo' ], + [ true, true ], + [ null, null ], // typeof null is "object", but v !== null is false + [ undefined, undefined ], + ]) { + test(`cxjs_min(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_min(input), result); + }); + } + + // null and NaN within a collection are handled specially: a NaN running + // minimum is discarded via the isNaN(lowest) guard, and null compares as 0 + // (so it wins as the minimum) but is preserved in the result. + for (const [ input, result ] of [ + // Input Result + [ [5], 5 ], // single element + [ [NaN, 5], 5 ], // leading NaN replaced by a real value + [ [5, NaN], 5 ], // trailing NaN never beats a real value + [ [NaN], NaN ], // all NaN: NaN survives + [ [NaN, NaN], NaN ], + [ [null, 5], null ], // null compares as 0, so it is the min + [ [5, null], null ], + [ [0, undefined], 0 ], // undefined after a value is ignored + [ { a: NaN, b: 5 }, 5 ], + [ { a: 5, b: NaN }, 5 ], + [ { a: NaN }, NaN ], + [ { a: null, b: 5 }, null ], + [ { a: 5, b: null }, null ], + ]) { + test(`cxjs_min(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_min(input), result); + }); + } + + // Non-numeric strings make isNaN(lowest) true on every pass, so the + // comparison is bypassed and the last element is always returned -- min + // does not actually order non-numeric strings. Numeric strings, by + // contrast, are compared lexically (not by numeric value). + for (const [ input, result ] of [ + // Input Result + [ ['b', 'a', 'c'], 'c' ], // last element wins, not 'a' + [ ['apple', 'banana'], 'banana' ], + [ ['10', '9', '100'], '10' ], // lexical compare: '10' < '9' < '100' + ]) { + test(`cxjs_min(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_min(input), result); + }); + } }); diff --git a/centrallix-os/sys/js/tests/cxjs_minus.test.js b/centrallix-os/sys/js/tests/cxjs_minus.test.js index 95f814701..d883f23a5 100644 --- a/centrallix-os/sys/js/tests/cxjs_minus.test.js +++ b/centrallix-os/sys/js/tests/cxjs_minus.test.js @@ -32,6 +32,8 @@ describe('cxjs_minus', () => [ -Infinity, -Infinity, NaN ], [ NaN, 1, NaN ], [ 1, NaN, NaN ], + [ true, 1, 0 ], // Booleans are not strings: true -> 1. + [ false, false, 0 ], // false -> 0. ]) { test(`cxjs_minus(${JSON.stringify(a)}, ${JSON.stringify(b)}) = ${JSON.stringify(result)}`, () => { @@ -68,6 +70,9 @@ describe('cxjs_minus', () => [ 100, '0', '10' ], // Coercion: 100 -> '100'. [ '5', 5, '' ], [ 5, '5', '' ], + // Only a suffix is stripped: the match must sit at the very end. + [ 'abcabc', 'bc', 'abca' ], // trailing 'bc' removed (lastIndexOf is at the end). + [ 'aXbXc', 'X', 'aXbXc' ], // 'X' occurs, but not at the end: unchanged. ]) { test(`cxjs_minus(${JSON.stringify(a)}, ${JSON.stringify(b)}) = ${JSON.stringify(result)}`, () => { diff --git a/centrallix-os/sys/js/tests/cxjs_plus.test.js b/centrallix-os/sys/js/tests/cxjs_plus.test.js index a4e2353ba..7753746b4 100644 --- a/centrallix-os/sys/js/tests/cxjs_plus.test.js +++ b/centrallix-os/sys/js/tests/cxjs_plus.test.js @@ -56,6 +56,18 @@ describe('cxjs_plus', () => [ 0, '', '0' ], // string branch beats numeric 0 [ 'n', Infinity, 'nInfinity' ], [ 'a', NaN, 'aNaN' ], + + // NaN is a number, so two non-string operands still add (to NaN). + [ NaN, 1, NaN ], + [ NaN, NaN, NaN ], + + // Booleans treated as numbers (true->1, false->0), + // unless a string operand forces concatenation. + [ true, 1, 2 ], + [ true, false, 1 ], + [ false, false, 0 ], + [ true, 'x', 'truex' ], + [ 'x', true, 'xtrue' ], ]) { test(`cxjs_plus(${fmt(a)}, ${fmt(b)}) = ${fmt(result)}`, () => { diff --git a/centrallix-os/sys/js/tests/cxjs_quote.test.js b/centrallix-os/sys/js/tests/cxjs_quote.test.js index dc028a833..076729c76 100644 --- a/centrallix-os/sys/js/tests/cxjs_quote.test.js +++ b/centrallix-os/sys/js/tests/cxjs_quote.test.js @@ -37,6 +37,8 @@ describe('cxjs_quote', () => [ undefined, '"undefined"' ], // undefined coerced [ NaN, '"NaN"' ], // NaN coerced [ [1, 2], '"1,2"' ], // array coerced (with no spaces) + [ {}, '"[object Object]"' ], // plain object coerced + [ { a: 1 }, '"[object Object]"' ], // object contents are irrelevant to String() ]) { test(`cxjs_quote(${JSON.stringify(input)}) = ${JSON.stringify(result)}`, () => { diff --git a/centrallix-os/sys/js/tests/cxjs_right.test.js b/centrallix-os/sys/js/tests/cxjs_right.test.js index f3c9a9a2f..2e45d99d6 100644 --- a/centrallix-os/sys/js/tests/cxjs_right.test.js +++ b/centrallix-os/sys/js/tests/cxjs_right.test.js @@ -24,6 +24,8 @@ describe('cxjs_right', () => [ 'hello', 10, 'hello' ], // l exceeds length: whole string [ 'hello', 0, '' ], // zero length: empty [ 'hello', -2, '' ], // negative length: empty + [ 'hello', 2.5, 'llo' ], // fractional length truncated to 2 + [ 'hello', NaN, 'hello' ], // NaN != null, so substr(NaN) -> substr(0) [ '', 3, '' ], // empty string stays empty [ '', 0, '' ], [ 'x', 1, 'x' ], // single char @@ -40,4 +42,15 @@ describe('cxjs_right', () => assert.equal(env.cxjs_right(s, l), result); }); } + + // Unlike cxjs_substring(), cxjs_right() never coerces s to a string. + for (const s of [ 12345, true ]) + { + test(`cxjs_right(${JSON.stringify(s)}, 2) throws (no String coercion)`, () => + { + // The error originates in the vm sandbox, so it is an instance of the + // sandbox's TypeError, not this realm's; match on name instead. + assert.throws(() => env.cxjs_right(s, 2), (err) => err && err.name === 'TypeError'); + }); + } }); diff --git a/centrallix-os/sys/js/tests/cxjs_sum.test.js b/centrallix-os/sys/js/tests/cxjs_sum.test.js index 5c23a50da..47ffda883 100644 --- a/centrallix-os/sys/js/tests/cxjs_sum.test.js +++ b/centrallix-os/sys/js/tests/cxjs_sum.test.js @@ -14,6 +14,20 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); +// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which +// would make distinct edge-case rows share a test name; fmt renders those +// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + describe('cxjs_sum', () => { for (const [ input, result ] of [ @@ -79,4 +93,43 @@ describe('cxjs_sum', () => assert.equal(env.cxjs_sum(input), result); }); } + + // Scalar (non-array, non-object) inputs hit. + for (const [ input, result ] of [ + // Input Result + [ 5, 5 ], + [ 0, 0 ], + [ -3.5, -3.5 ], + [ Infinity, Infinity ], + [ 'abc', 'abc' ], // returned as-is, not parsed + [ true, true ], // boolean pass through + [ false, false ], + [ NaN, NaN ], // scalar NaN is kept (cnt is 1, not 0) + [ null, null ], // typeof null is "object", but v !== null is false + [ undefined, undefined ], + ]) { + test(`cxjs_sum(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_sum(input), result); + }); + } + + // String and boolean coercion. + for (const [ input, result ] of [ + // Input Result + [ ['5', 6], '056' ], // 0 + '5' -> '05', then '05' + 6 -> '056' + [ [1, '2'], '12' ], // 0 + 1 -> 1, then 1 + '2' -> '12' + [ [''], '0' ], // '' is numeric (0); 0 + '' -> '0' + [ ['', 5], '05' ], + [ [true], 1 ], // true coerces to 1 + [ [true, false], 1 ], // false coerces to 0 + [ [1, true], 2 ], + [ { a: '5', b: 6 }, '056' ], // same concatenation in object form + [ { a: true }, 1 ], + ]) { + test(`cxjs_sum(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_sum(input), result); + }); + } }); diff --git a/centrallix-os/sys/js/tests/cxjs_upper.test.js b/centrallix-os/sys/js/tests/cxjs_upper.test.js index eaccaf8dc..666d7e8c9 100644 --- a/centrallix-os/sys/js/tests/cxjs_upper.test.js +++ b/centrallix-os/sys/js/tests/cxjs_upper.test.js @@ -38,6 +38,7 @@ describe('cxjs_upper', () => [ '\tn\r', '\tN\r' ], // whitespace preserved [ 'cafรฉ', 'CAFร‰' ], // accented letter uppercases [ 'รŸ', 'SS' ], // sharp-s expands to two chars + [ '๏ฌ', 'FI' ], // fi ligature expands to two chars // Non-strings are String()-coerced first, then uppercased. Only strict // null short-circuits to null; undefined does not. @@ -49,6 +50,8 @@ describe('cxjs_upper', () => [ false, 'FALSE' ], [ NaN, 'NAN' ], [ Infinity, 'INFINITY' ], + [ -Infinity, '-INFINITY' ], + [ [], '' ], // empty array coerces to '' [ ['a', 'b'], 'A,B' ], // Joins with ',' and no spaces [ {}, '[OBJECT OBJECT]' ], // plain object stringifies ]) { diff --git a/centrallix-os/sys/js/tests/cxjs_user_name.test.js b/centrallix-os/sys/js/tests/cxjs_user_name.test.js index 70d9897f3..1f5c1aabf 100644 --- a/centrallix-os/sys/js/tests/cxjs_user_name.test.js +++ b/centrallix-os/sys/js/tests/cxjs_user_name.test.js @@ -19,6 +19,7 @@ describe('cxjs_user_name', () => for (const name of [ 'alice', 'bob', + '', ' !@#$%^&*()":;\' ', ]) { it(`returns pg_username (\"${name}\")`, () => From c7d6a73ac50e47263e70def66b3e0ee3703e71bf Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 17 Jun 2026 14:00:19 -0600 Subject: [PATCH 37/56] Add a script for generating coverage files. --- centrallix-os/sys/js/tests/gen-coverage.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100755 centrallix-os/sys/js/tests/gen-coverage.sh diff --git a/centrallix-os/sys/js/tests/gen-coverage.sh b/centrallix-os/sys/js/tests/gen-coverage.sh new file mode 100755 index 000000000..c957d3f2b --- /dev/null +++ b/centrallix-os/sys/js/tests/gen-coverage.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# Regenerate lcov.info for the cxjs_* test suite, viewable in VSCode via the +# Coverage Gutters extension by ryanluker. + +set -e + +# Must run from the repo root so the SF: paths in lcov.info will be +# workspace-relative (a leading ../ breaks file resolution). +root=$(git rev-parse --show-toplevel) +cd "$root" + +node --test --experimental-test-coverage \ + --test-reporter=lcov --test-reporter-destination=lcov.info \ + --test-reporter=spec --test-reporter-destination=stdout \ + 'centrallix-os/sys/js/tests/*.test.js' + +echo "Wrote $root/lcov.info" From 54099760fd008c6d3e51594bc0e1c706d904fd54 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 18 Jun 2026 09:47:08 -0600 Subject: [PATCH 38/56] Add tests for cxjs_abs(). --- centrallix-os/sys/js/tests/cxjs_abs.test.js | 80 +++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_abs.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_abs.test.js b/centrallix-os/sys/js/tests/cxjs_abs.test.js new file mode 100644 index 000000000..a3156abf1 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_abs.test.js @@ -0,0 +1,80 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which +// would make distinct edge-case rows share a test name; fmt renders those +// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + +describe('cxjs_abs', () => + { + for (const [ input, result ] of [ + // Input Result + [ 5, 5 ], + [ 5.5, 5.5 ], + [ -5, 5 ], + [ -5.5, 5.5 ], + [ 0, 0 ], + [ -0, 0 ], + [ Infinity, Infinity ], + [ -Infinity, Infinity ], + [ NaN, NaN ], + ]) { + test(`cxjs_abs(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_abs(input), result); + }); + } + + // null and undefined yield null instead of being coerced. + for (const input of [ null, undefined ]) + { + test(`cxjs_abs(${fmt(input)}) = null`, () => + { + assert.equal(env.cxjs_abs(input), null); + }); + } + + // Non-number inputs are coerced to number before taking the magnitude. + for (const [ input, result ] of [ + // Input Result + [ '5', 5 ], + [ '-5', 5 ], + [ '3.14', 3.14 ], + [ '', 0 ], // empty string coerces to 0 + [ 'foo', NaN ], // non-numeric string coerces to NaN + [ true, 1 ], + [ false, 0 ], + [ [], 0 ], // empty array coerces to 0 + [ [5], 5 ], // single-element array coerces to its element + [ [5, 6], NaN ], // multi-element array coerces to NaN + [ {}, NaN ], // object coerces to NaN + ]) { + test(`cxjs_abs(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_abs(input), result); + }); + } + }); From 582f4656c7f448b151b4326fdbdd201b7a0803d1 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 18 Jun 2026 09:55:51 -0600 Subject: [PATCH 39/56] Add tests for cxjs_reverse(). --- .../sys/js/tests/cxjs_reverse.test.js | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_reverse.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_reverse.test.js b/centrallix-os/sys/js/tests/cxjs_reverse.test.js new file mode 100644 index 000000000..ffd567524 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_reverse.test.js @@ -0,0 +1,85 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// JSON.stringify renders NaN/Infinity as "null" and a standalone undefined as +// undefined (not a string), which would give distinct edge-case rows the same +// (or a broken) test name; fmt renders those verbatim so names stay unique. +function fmt(v) + { + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + +describe('cxjs_reverse', () => + { + for (const [ input, result ] of [ + // Input Result + [ 'abc', 'cba' ], + [ 'a', 'a' ], + [ '', '' ], + [ 'aba', 'aba' ], // palindrome unchanged + [ 'Hello World', 'dlroW olleH' ], + [ 'a b c', 'c b a' ], // spaces reversed + [ '\ta\n', '\na\t' ], // whitespace reversed + [ ' \tx ', ' x\t ' ], // surrounding whitespace preserved + [ '123!?', '?!321' ], // digits/symbols reverse like any char + [ 'ร รฉรฎ', 'รฎรฉร ' ], // non-ASCII letters: a/e/i w/ accents + ]) { + test(`cxjs_reverse(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_reverse(input), result); + }); + } + + // null and undefined both short-circuit to null. Other non-strings are + // coerced before reversal. + for (const [ input, result ] of [ + // Input Result + [ null, null ], + [ undefined, null ], + [ 123, '321' ], + [ 1.5, '5.1' ], + [ true, 'eurt' ], + [ false, 'eslaf' ], + [ NaN, 'NaN' ], // 'NaN' reverses to itself + [ Infinity, 'ytinifnI' ], + [ -Infinity, 'ytinifnI-' ], + [ [], '' ], // empty array coerces to '' + [ ['a', 'b'], 'b,a' ], + ]) { + test(`cxjs_reverse(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_reverse(input), result); + }); + } + + // Reversal walks UTF-16 code units, not whole characters, so surrogate + // pairs and combining marks get split apart and reordered. Escapes are + // used directly so these cases do not depend on the file's encoding. + for (const [ name, input, result ] of [ + // emoji (one code point, two code units) splits into swapped halves + [ 'surrogate pair split', '\uD83D\uDE00', '\uDE00\uD83D' ], + [ 'pair split before char', '\uD83D\uDE00a', 'a\uDE00\uD83D' ], + // 'e' + combining acute reverses to combining acute + 'e' + [ 'combining mark reorder', 'e\u0301', '\u0301e' ], + ]) { + test(`cxjs_reverse: ${name}`, () => + { + assert.equal(env.cxjs_reverse(input), result); + }); + } + }); From 9c7d09984fb2c5bf261fdc687c0eb5932f9f83f3 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 18 Jun 2026 10:09:43 -0600 Subject: [PATCH 40/56] Add tests for cxjs_replace(). --- .../sys/js/tests/cxjs_replace.test.js | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_replace.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_replace.test.js b/centrallix-os/sys/js/tests/cxjs_replace.test.js new file mode 100644 index 000000000..d6f950b3e --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_replace.test.js @@ -0,0 +1,115 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// replace globally substitutes every literal occurrence of search in str with +// replace, returning null when str or search is null/undefined. +describe('cxjs_replace', () => + { + for (const [ str, search, replace, result ] of [ + // Str Search Replace Result + [ 'hello world', 'o', '0', 'hell0 w0rld' ], // replaces every occurrence + [ 'hello world', 'h', 'J', 'Jello world' ], // first character + [ 'hello world', 'd', '!', 'hello worl!' ], // last character + [ 'hello world', ' ', '_', 'hello_world' ], // whitespace + [ 'hello world', 'l', ' ', 'he o wor d' ], + [ 'hello world', 'l', 'l', 'hello world' ], // identical result + [ 'hello world', ' ', ' ', 'hello world' ], + [ 'hello world', 'c', 'c', 'hello world' ], + [ 'aaa', 'a', 'b', 'bbb' ], + [ 'aaa', 'aa', 'b', 'ba' ], // non-overlapping, left to right + [ 'hello', 'z', 'x', 'hello' ], // absent + [ 'hello', 'l', 'LL', 'heLLLLo' ], // replacement longer than match + [ 'Hello', 'h', 'x', 'Hello' ], // case-sensitive: no match + [ 'hello', 'l', '', 'heo' ], // empty replace deletes matches + + // search is matched literally (regex metacharacters have no special meaning). + [ '1.2.3', '.', ',', '1,2,3' ], + [ 'a|b|c', '|', '-', 'a-b-c' ], + [ '*x*', '*', '#', '#x#' ], + [ 'a+b', '+', '-', 'a-b' ], + [ 'a?b', '?', '!', 'a!b' ], + [ 'a^b', '^', '~', 'a~b' ], + [ '$5', '$', 'USD', 'USD5' ], + [ '(x)', '(', '[', '[x)' ], + [ '{x}', '{', '<', ' + { + assert.equal(env.cxjs_replace(str, search, replace), result); + }); + } + + // null or undefined str/search short-circuits to null; a null/undefined replace + // (including an omitted third argument) instead defaults to "". + for (const [ str, search, replace, result ] of [ + // Str Search Replace Result + [ null, 'a', 'b', null ], + [ undefined, 'a', 'b', null ], + [ 'x', null, 'b', null ], + [ 'x', undefined, 'b', null ], + [ null, null, 'b', null ], + [ 'hello', 'l', null, 'heo' ], // null replace -> deletes matches + [ 'hello', 'l', undefined, 'heo' ], // undefined replace -> deletes matches + ]) { + test(`cxjs_replace(${str}, ${search}, ${replace}) = ${JSON.stringify(result)}`, () => + { + assert.equal(env.cxjs_replace(str, search, replace), result); + }); + } + + // replace is uses String.replace() logic, allowing $ patterns to be used: + // $& is the match, $$ a literal $, $` and $' the text before/after. + for (const [ str, search, replace, result ] of [ + // Str Search Replace Result + [ 'cat', 'a', '$&', 'cat' ], // match reinserted unchanged + [ 'cat', 'a', '[$&]', 'c[a]t' ], + [ 'cat', 'a', '$$', 'c$t' ], + [ 'cat', 'a', '$`', 'cct' ], // text preceding the match + [ 'cat', 'a', "$'", 'ctt' ], // text following the match + ]) { + test(`cxjs_replace(` + + `${JSON.stringify(str)}, ` + + `${JSON.stringify(search)}, ` + + `${JSON.stringify(replace)}) ` + + `= ${JSON.stringify(result)}`, + () => + { + assert.equal(env.cxjs_replace(str, search, replace), result); + }); + } + + // Omitted replace defaults to "", deleting every match. + test("cxjs_replace('hello', 'l') = 'heo'", () => + { + assert.equal(env.cxjs_replace('hello', 'l'), 'heo'); + }); + }); From 12736304cfe034f06b7f7453037a37b426ce1f8f Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 18 Jun 2026 10:21:10 -0600 Subject: [PATCH 41/56] Add tests for cxjs_replicate(). --- .../sys/js/tests/cxjs_replicate.test.js | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_replicate.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_replicate.test.js b/centrallix-os/sys/js/tests/cxjs_replicate.test.js new file mode 100644 index 000000000..215d12de1 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_replicate.test.js @@ -0,0 +1,86 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// replicate concatenates n copies of s, returning null if s or n is +// null/undefined or if n is negative. +describe('cxjs_replicate', () => + { + for (const [ s, n, result ] of [ + // Str N Result + [ 'ab', 3, 'ababab' ], + [ 'x', 1, 'x' ], // single copy + [ 'ab', 0, '' ], // zero copies + [ '', 5, '' ], // empty str stays empty + [ '', 0, '' ], + [ 'a b', 2, 'a ba b' ], // whitespace preserved + [ 'abc', 3, 'abcabcabc' ], + + // n is floored toward zero's neighbor, so fractions truncate down. + [ 'ab', 3.9, 'ababab' ], + [ 'ab', 0.5, '' ], // floors to 0 + [ 'ab', 2.0, 'abab' ], + + // Non-string s is coerced to string. + [ 5, 3, '555' ], + [ true, 2, 'truetrue' ], + [ 0, 2, '00' ], + + // NaN n yields "". + [ 'ab', NaN, '' ], + [ 'ab', 'abc', '' ], // String->NaN + ]) { + test(`cxjs_replicate(${JSON.stringify(s)}, ${String(n)}) = ${JSON.stringify(result)}`, () => + { + assert.equal(env.cxjs_replicate(s, n), result); + }); + } + + // null/undefined s or n short-circuits to null. + for (const [ s, n ] of [ + [ null, 3 ], + [ undefined, 3 ], + [ 'x', null ], + [ 'x', undefined ], + [ null, null ], + ]) { + test(`cxjs_replicate(${s}, ${n}) = null`, () => + { + assert.equal(env.cxjs_replicate(s, n), null); + }); + } + + // Negative n returns null. + for (const [ s, n ] of [ + [ 'ab', -1 ], + [ 'ab', -5 ], + [ 'ab', -0.1 ], // Floors away from 0 (-0.1 -> -1), resulting in null. + [ 'ab', -Infinity ], + ]) { + test(`cxjs_replicate(${JSON.stringify(s)}, ${String(n)}) = null`, () => + { + assert.equal(env.cxjs_replicate(s, n), null); + }); + } + + // n is capped at 255 copies. + for (const n of [ 255, 256, 300, Infinity ]) + { + test(`cxjs_replicate('ab', ${String(n)}) caps at 255 copies`, () => + { + assert.equal(env.cxjs_replicate('ab', n), 'ab'.repeat(255)); + }); + } + }); From c3800073f9b536f06c3ab21ebe33f4bc39eb1d77 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 18 Jun 2026 10:26:31 -0600 Subject: [PATCH 42/56] Add tests for cxjs_round(). (Draft) --- centrallix-os/sys/js/tests/cxjs_round.test.js | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_round.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_round.test.js b/centrallix-os/sys/js/tests/cxjs_round.test.js new file mode 100644 index 000000000..e3171096e --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_round.test.js @@ -0,0 +1,139 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which +// would make distinct edge-case rows share a test name; fmt renders those +// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + +function run(rows) + { + for (const [ args, result ] of rows) + { + test(`cxjs_round(${args.map(fmt).join(', ')}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_round(...args), result); + }); + } + } + +describe('cxjs_round', () => + { + // Rounding to the nearest integer (default dec). Exact halves round away + // from zero in both directions. + run([ + // Args Result + [ [2.4], 2 ], + [ [2.5], 3 ], + [ [2.6], 3 ], + [ [3], 3 ], + [ [0.5], 1 ], + [ [1.5], 2 ], + [ [-2.4], -2 ], + [ [-2.5], -3 ], + [ [-2.6], -3 ], + [ [-3], -3 ], + [ [-0.5], -1 ], + [ [-1.5], -2 ], + ]); + + // Rounding to dec decimal places. + run([ + // Args Result + [ [3.14159, 2], 3.14 ], + [ [3.14159, 4], 3.1416 ], + [ [2.345, 2], 2.35 ], + ]); + + // Negative dec rounds to tens, hundreds, thousands, etc. + run([ + // Args Result + [ [1234, -2], 1200 ], + [ [1250, -2], 1300 ], + [ [1251, -2], 1300 ], + [ [1240, -1], 1240 ], + [ [12345, -3], 12000 ], + ]); + + // A non-integer dec is rounded to the nearest integer before use. + run([ + // Args Result + [ [12.345, 1.4], 12.3 ], // dec -> 1 + [ [12.345, 1.5], 12.35 ], // dec -> 2 + [ [1.5, 0.4], 2 ], // dec -> 0 + [ [2.5, -0.5], 3 ], // dec -> 0 + ]); + + // null and undefined yield null instead of being coerced. + run([ + [ [null], null ], + [ [undefined], null ], + [ [null, 2], null ], + ]); + + // Special numeric values pass through; a dec large enough to overflow the + // 10**dec scale factor to Infinity produces NaN. + run([ + // Args Result + [ [Infinity], Infinity ], + [ [Infinity, 2], Infinity ], + [ [-Infinity], -Infinity ], + [ [NaN], NaN ], + [ [5, 400], NaN ], + ]); + + // Sign of a zero result: strictly positive inputs give +0, while zero and + // negative inputs give -0. + run([ + // Args Result + [ [0.1], 0 ], + [ [0.4], 0 ], + [ [0], -0 ], + [ [-0.1], -0 ], + [ [-0.4], -0 ], + ]); + + // Values that are exact halves in decimal are not always representable in + // binary, so they can round down rather than up. + run([ + // Args Result + [ [1.005, 2], 1 ], // not 1.01 + [ [1.255, 2], 1.25 ], // not 1.26 + ]); + + // n is coerced to a number before rounding (matching JS arithmetic rules). + run([ + // Args Result + [ ['2.5'], 3 ], + [ ['foo'], NaN ], // non-numeric string coerces to NaN + [ [''], -0 ], // empty string coerces to 0 + [ [true], 1 ], + [ [false], -0 ], + [ [[5]], 5 ], // single-element array coerces to its element + [ [{}], NaN ], // object coerces to NaN + ]); + }); From 704a9c7a3680f580864626af49ee84a722f11536 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 18 Jun 2026 10:31:00 -0600 Subject: [PATCH 43/56] Update formatting for cxjs_round() tests. --- centrallix-os/sys/js/tests/cxjs_round.test.js | 132 ++++++++---------- 1 file changed, 55 insertions(+), 77 deletions(-) diff --git a/centrallix-os/sys/js/tests/cxjs_round.test.js b/centrallix-os/sys/js/tests/cxjs_round.test.js index e3171096e..b5d86f50a 100644 --- a/centrallix-os/sys/js/tests/cxjs_round.test.js +++ b/centrallix-os/sys/js/tests/cxjs_round.test.js @@ -30,103 +30,76 @@ function fmt(v) return JSON.stringify(v); } -function run(rows) - { - for (const [ args, result ] of rows) - { - test(`cxjs_round(${args.map(fmt).join(', ')}) = ${fmt(result)}`, () => - { - assert.equal(env.cxjs_round(...args), result); - }); - } - } - describe('cxjs_round', () => { - // Rounding to the nearest integer (default dec). Exact halves round away - // from zero in both directions. - run([ - // Args Result - [ [2.4], 2 ], - [ [2.5], 3 ], - [ [2.6], 3 ], - [ [3], 3 ], - [ [0.5], 1 ], - [ [1.5], 2 ], - [ [-2.4], -2 ], - [ [-2.5], -3 ], - [ [-2.6], -3 ], - [ [-3], -3 ], - [ [-0.5], -1 ], - [ [-1.5], -2 ], - ]); - - // Rounding to dec decimal places. - run([ - // Args Result - [ [3.14159, 2], 3.14 ], - [ [3.14159, 4], 3.1416 ], - [ [2.345, 2], 2.35 ], - ]); - - // Negative dec rounds to tens, hundreds, thousands, etc. - run([ - // Args Result - [ [1234, -2], 1200 ], - [ [1250, -2], 1300 ], - [ [1251, -2], 1300 ], - [ [1240, -1], 1240 ], - [ [12345, -3], 12000 ], - ]); - - // A non-integer dec is rounded to the nearest integer before use. - run([ + for (const [ args, result ] of [ + // Rounding to the nearest integer (default dec). + // Exact halves round away from zero in both directions. + // Args Result + [ [2.4], 2 ], + [ [2.5], 3 ], + [ [2.6], 3 ], + [ [3], 3 ], + [ [0.5], 1 ], + [ [1.5], 2 ], + [ [-2.4], -2 ], + [ [-2.5], -3 ], + [ [-2.6], -3 ], + [ [-3], -3 ], + [ [-0.5], -1 ], + [ [-1.5], -2 ], + + // Rounding to dec decimal places. + // Args Result + [ [3.14159, 2], 3.14 ], + [ [3.14159, 4], 3.1416 ], + [ [2.345, 2], 2.35 ], + + // Negative dec rounds to tens, hundreds, thousands, etc. + // Args Result + [ [1234, -2], 1200 ], + [ [1250, -2], 1300 ], + [ [1251, -2], 1300 ], + [ [1240, -1], 1240 ], + [ [12345, -3], 12000 ], + + // A non-integer dec is rounded to the nearest integer before use. // Args Result [ [12.345, 1.4], 12.3 ], // dec -> 1 [ [12.345, 1.5], 12.35 ], // dec -> 2 [ [1.5, 0.4], 2 ], // dec -> 0 [ [2.5, -0.5], 3 ], // dec -> 0 - ]); - - // null and undefined yield null instead of being coerced. - run([ - [ [null], null ], - [ [undefined], null ], - [ [null, 2], null ], - ]); - - // Special numeric values pass through; a dec large enough to overflow the - // 10**dec scale factor to Infinity produces NaN. - run([ + + // null and undefined yield null (no coercion). + // Args Result + [ [null], null ], + [ [undefined], null ], + [ [null, 2], null ], + + // Special numeric values pass through. // Args Result [ [Infinity], Infinity ], [ [Infinity, 2], Infinity ], [ [-Infinity], -Infinity ], [ [NaN], NaN ], - [ [5, 400], NaN ], - ]); - - // Sign of a zero result: strictly positive inputs give +0, while zero and - // negative inputs give -0. - run([ + [ [5, 400], NaN ], // dec exceeds scaling factor. + + // Sign of a zero: strictly positive inputs give +0, while zero and + // negative inputs give -0. // Args Result [ [0.1], 0 ], [ [0.4], 0 ], [ [0], -0 ], [ [-0.1], -0 ], [ [-0.4], -0 ], - ]); - - // Values that are exact halves in decimal are not always representable in - // binary, so they can round down rather than up. - run([ + + // Values that are exact halves in decimal are not always representable in + // binary, so they might be rounded down rather than up. // Args Result [ [1.005, 2], 1 ], // not 1.01 [ [1.255, 2], 1.25 ], // not 1.26 - ]); - - // n is coerced to a number before rounding (matching JS arithmetic rules). - run([ + + // n is coerced to a number before rounding (matching JS arithmetic rules). // Args Result [ ['2.5'], 3 ], [ ['foo'], NaN ], // non-numeric string coerces to NaN @@ -135,5 +108,10 @@ describe('cxjs_round', () => [ [false], -0 ], [ [[5]], 5 ], // single-element array coerces to its element [ [{}], NaN ], // object coerces to NaN - ]); + ]) { + test(`cxjs_round(${args.map(fmt).join(', ')}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_round(...args), result); + }); + } }); From 9d22a59d7b819b7f1c4350b28a797498b97ebca2 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 18 Jun 2026 10:39:45 -0600 Subject: [PATCH 44/56] Add tests for cxjs_round(). --- .../sys/js/tests/cxjs_truncate.test.js | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_truncate.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_truncate.test.js b/centrallix-os/sys/js/tests/cxjs_truncate.test.js new file mode 100644 index 000000000..56bf3ca88 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_truncate.test.js @@ -0,0 +1,105 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which +// would make distinct edge-case rows share a test name; fmt renders those +// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + +describe('cxjs_truncate', () => + { + for (const [ args, result ] of [ + // Truncating to an integer (default dec): values truncate toward + // zero in both directions. + // Args Result + [ [2.4], 2 ], + [ [2.5], 2 ], + [ [2.9], 2 ], + [ [3], 3 ], + [ [0.9], 0 ], + [ [-2.4], -2 ], + [ [-2.5], -2 ], + [ [-2.9], -2 ], + [ [-3], -3 ], + + // Truncating to dec decimal places (digits past dec are dropped). + // Args Result + [ [3.14159, 2], 3.14 ], + [ [3.14159, 4], 3.1415 ], // not rounded up to 3.1416 + [ [-3.14159, 2], -3.14 ], + [ [2.349, 2], 2.34 ], // not rounded up to 2.35 + + // Negative dec truncates to tens, hundreds, thousands, etc. + // Args Result + [ [1234, -2], 1200 ], + [ [1299, -2], 1200 ], + [ [5678, -3], 5000 ], + + // A non-integer dec is rounded to the nearest integer before use. + // Args Result + [ [12.345, 1.4], 12.3 ], // dec -> 1 + [ [12.345, 1.5], 12.34 ], // dec -> 2 + [ [1234.5, -1.6], 1200 ], // dec -> -2 + + // null and undefined yield null (no coercion). + // Args Result + [ [null], null ], + [ [undefined], null ], + [ [null, 2], null ], + + // Special numeric values pass through. + // Args Result + [ [Infinity], Infinity ], + [ [Infinity, 2], Infinity ], + [ [-Infinity], -Infinity ], + [ [NaN], NaN ], + [ [5, 400], NaN ], // dec exceeds scaling factor + + // Sign of a zero: a negative input under 1 truncates to -0, while zero + // and positive inputs give +0. + // Args Result + [ [0], +0 ], + [ [0.1], +0 ], + [ [-0.1], -0 ], + [ [-0.9], -0 ], + + // n is coerced to a number before truncating (matching JS arithmetic). + // Args Result + [ ['2.9'], 2 ], + [ ['foo'], NaN ], // non-numeric string coerces to NaN + [ [''], 0 ], // empty string coerces to 0 + [ [true], 1 ], + [ [false], 0 ], + [ [[5]], 5 ], // single-element array coerces to its element + [ [{}], NaN ], // object coerces to NaN + ]) { + test(`cxjs_truncate(${args.map(fmt).join(', ')}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_truncate(...args), result); + }); + } + }); From f8a71e8f6e88ca51b9b7ee35ab5cfaeeeec0df24 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 18 Jun 2026 10:44:33 -0600 Subject: [PATCH 45/56] Add tests for cxjs_constrain(). --- .../sys/js/tests/cxjs_constrain.test.js | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_constrain.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_constrain.test.js b/centrallix-os/sys/js/tests/cxjs_constrain.test.js new file mode 100644 index 000000000..eb2fa1f91 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_constrain.test.js @@ -0,0 +1,99 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which +// would make distinct edge-case rows share a test name; fmt renders those +// values verbatim so names stay unique. +function fmt(v) + { + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + +describe('cxjs_constrain', () => + { + for (const [ n, min, max, result ] of [ + // Both bounds: clamp into [min, max], and bounds are inclusive. + // n min max Result + [ 5, 0, 10, 5 ], // within range + [ 0, 0, 10, 0 ], // lower bound + [ 10, 0, 10, 10 ], // upper bound + [ -5, 0, 10, 0 ], // below -> min + [ 15, 0, 10, 10 ], // above -> max + [ 0.5, 0, 10, 0.5 ], // fractional, within + [ 5, 5, 5, 5 ], // min == max + + // A 0 bound is a real bound, not skipped like null/undefined. + [ -1, 0, 10, 0 ], + + // Negative ranges and fractional bounds. + [ -5, -10, -1, -5 ], // within + [ -15, -10, -1, -10 ], // below -> min + [ 0, -10, -1, -1 ], // above -> max + [ 2.5, 1.5, 3.5, 2.5 ], // within + + // null/undefined min leaves the result unbounded below. + [ -100, null, 10, -100 ], + [ 5, null, 10, 5 ], + [ -100, undefined, 10, -100 ], + + // null/undefined max leaves the result unbounded above. + [ 100, 0, null, 100 ], + [ 5, 0, null, 5 ], + [ 100, 0, undefined, 100 ], + + // Both bounds absent: n is unchanged. + [ 5, null, null, 5 ], + [ -100, null, null, -100 ], + [ 5, undefined, undefined, 5 ], + + // null/undefined n always yields null, regardless of bounds. + [ null, 0, 10, null ], + [ undefined, 0, 10, null ], + [ null, null, null, null ], + + // A NaN bounds are ignored, so it is effectively ignored; + // the opposite bound (if any) still applies. + [ 5, NaN, 10, 5 ], + [ 15, NaN, 10, 10 ], // max still clamps + [ 5, 0, NaN, 5 ], + [ -5, 0, NaN, 0 ], // min still clamps + [ 5, NaN, NaN, 5 ], // both ignored + + // NaN propagates. + [ NaN, 0, 10, NaN ], + + // Infinity clamps like any other value; an Infinite n survives an + // absent bound on its side. + [ Infinity, 0, 10, 10 ], + [ -Infinity, 0, 10, 0 ], + [ Infinity, 0, null, Infinity ], + [ -Infinity, null, 10, -Infinity ], + [ 5, -Infinity, Infinity, 5 ], + + // Inverted bounds (min > max): min is checked first, so n < min returns + // min; otherwise n > max returns max. + [ 5, 10, 0, 10 ], + [ 100, 10, 0, 0 ], + [ -5, 10, 0, 10 ], + ]) { + test(`cxjs_constrain(${fmt(n)}, ${fmt(min)}, ${fmt(max)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_constrain(n, min, max), result); + }); + } + }); From 2e7408cf3dba42bd822c1403db60264ee74944d1 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 18 Jun 2026 10:48:45 -0600 Subject: [PATCH 46/56] Add tests for cxjs_sqrt(). --- centrallix-os/sys/js/tests/cxjs_sqrt.test.js | 88 ++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_sqrt.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_sqrt.test.js b/centrallix-os/sys/js/tests/cxjs_sqrt.test.js new file mode 100644 index 000000000..cdf49921c --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_sqrt.test.js @@ -0,0 +1,88 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which +// would make distinct edge-case rows share a test name; fmt renders those +// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + +describe('cxjs_sqrt', () => + { + for (const [ input, result ] of [ + // Input Result + [ 4, 2 ], + [ 9, 3 ], + [ 0.25, 0.5 ], + [ 2, Math.SQRT2 ], + [ 1, 1 ], + [ 0, 0 ], + [ -0, -0 ], // preserved: Math.sqrt(-0) is -0, not NaN + [ Infinity, Infinity ], + ]) { + test(`cxjs_sqrt(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_sqrt(input), result); + }); + } + + // Negative inputs and NaN have no real root and yield null. + for (const input of [ -1, -5.5, -Infinity, NaN ]) + { + test(`cxjs_sqrt(${fmt(input)}) = null`, () => + { + assert.equal(env.cxjs_sqrt(input), null); + }); + } + + // null and undefined yield null (no coercion). + for (const input of [ null, undefined ]) + { + test(`cxjs_sqrt(${fmt(input)}) = null`, () => + { + assert.equal(env.cxjs_sqrt(input), null); + }); + } + + // Non-number inputs are coerced to numbers. + // If they yield NaN, sqrt() returns null. + for (const [ input, result ] of [ + // Input Result + [ '4', 2 ], + [ '2.25', 1.5 ], + [ '', 0 ], // empty string coerces to 0 + [ 'foo', null ], // non-numeric string coerces to NaN + [ true, 1 ], + [ false, 0 ], + [ [], 0 ], // empty array coerces to 0 + [ [4], 2 ], // single-element array coerces to its element + [ [4, 9], null ], // multi-element array coerces to NaN + [ {}, null ], // object coerces to NaN + ]) { + test(`cxjs_sqrt(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_sqrt(input), result); + }); + } + }); From 9ba0fbea651723a11cafec8b85b6cce31c1d316a Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 18 Jun 2026 10:55:43 -0600 Subject: [PATCH 47/56] Add tests for cxjs_rand(). --- centrallix-os/sys/js/tests/cxjs_rand.test.js | 102 +++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_rand.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_rand.test.js b/centrallix-os/sys/js/tests/cxjs_rand.test.js new file mode 100644 index 000000000..6a38df0e3 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_rand.test.js @@ -0,0 +1,102 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// Renders NaN/Infinity/undefined verbatim so seed test names stay unique +// (JSON.stringify would collapse them). +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + +// Runs fn with the sandbox's console.warn replaced by a counter, returning +// the number of warnings emitted. Restores the real console afterward. +function captureWarningCount(fn) + { + const real = env.console; + let count = 0; + env.console = { warn: () => { count++; } }; + try { fn(); } + finally { env.console = real; } + return count; + } + +describe('cxjs_rand', () => + { + // Result is always a float in [0,1). + test('returns a float in [0,1)', () => + { + for (let i = 0; i < 1000; i++) + { + const n = env.cxjs_rand(); + assert.equal(typeof n, 'number'); + assert.ok(n >= 0 && n < 1, `${n} not in [0,1)`); + } + }); + + // Output is not constant across calls. + test('produces varied output', () => + { + const seen = new Set(); + for (let i = 0; i < 1000; i++) seen.add(env.cxjs_rand()); + assert.ok(seen.size > 1, 'expected more than one distinct value'); + }); + + // A fixed seed is ignored, so it does not pin the output to one value. + // (Warnings are suppressed here; warn behavior is checked separately.) + test('ignores the seed (output stays non-deterministic)', () => + { + const seen = new Set(); + captureWarningCount(() => + { + for (let i = 0; i < 1000; i++) seen.add(env.cxjs_rand(42)); + }); + assert.ok(seen.size > 1, 'seed should not make output deterministic'); + }); + + // Any non-null/undefined seed -- even a falsy one -- emits one warning + // and still returns a valid result. + for (const seed of [0, 1, 42, -5, 3.14, '', 'abc', false, true, NaN, Infinity, [], {}]) + { + test(`warns when given seed ${fmt(seed)}`, () => + { + let result; + const warnings = captureWarningCount(() => { result = env.cxjs_rand(seed); }); + assert.equal(warnings, 1); + assert.ok(result >= 0 && result < 1, `${result} not in [0,1)`); + }); + } + + // No seed, or an explicit null/undefined, produces no warning. + for (const [ label, rand_fn ] of [ + [ 'no argument', () => env.cxjs_rand() ], + [ 'null', () => env.cxjs_rand(null) ], + [ 'undefined', () => env.cxjs_rand(undefined) ], + ]) { + test(`does not warn with ${label}`, () => + { + let result; + const warnings = captureWarningCount(() => { result = rand_fn(); }); + assert.equal(warnings, 0); + assert.ok(result >= 0 && result < 1, `${result} not in [0,1)`); + }); + } + }); From 34fa594ff5a9e01796d6ebd473333b16606f9d24 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 18 Jun 2026 10:58:14 -0600 Subject: [PATCH 48/56] Add tests for cxjs_degrees(). --- .../sys/js/tests/cxjs_degrees.test.js | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_degrees.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_degrees.test.js b/centrallix-os/sys/js/tests/cxjs_degrees.test.js new file mode 100644 index 000000000..20cb2cd7a --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_degrees.test.js @@ -0,0 +1,84 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which +// would make distinct edge-case rows share a test name; fmt renders those +// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + +describe('cxjs_degrees', () => + { + // Radians to degrees. Some multiples of PI land exactly; others (e.g. PI/6) + // carry floating-point error. + for (const [ input, result ] of [ + // Input Result + [ 0, 0 ], + [ -0, -0 ], // sign of zero is preserved + [ Math.PI, 180 ], + [ -Math.PI, -180 ], + [ Math.PI / 2, 90 ], + [ Math.PI / 4, 45 ], + [ 2 * Math.PI, 360 ], + [ 1, 180 / Math.PI ], // one radian + [ Math.PI / 6, 29.999999999999996 ], // not exactly 30 + [ Infinity, Infinity ], + [ -Infinity, -Infinity ], + [ NaN, NaN ], + ]) { + test(`cxjs_degrees(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_degrees(input), result); + }); + } + + // null and undefined yield null (no coercion). + for (const input of [ null, undefined ]) + { + test(`cxjs_degrees(${fmt(input)}) = null`, () => + { + assert.equal(env.cxjs_degrees(input), null); + }); + } + + // Non-number inputs are coerced to numbers by the arithmetic; those that + // coerce to NaN propagate to a NaN result. + for (const [ input, result ] of [ + // Input Result + [ '1', 180 / Math.PI ], + [ '', 0 ], // empty string coerces to 0 + [ 'foo', NaN ], // non-numeric string coerces to NaN + [ true, 180 / Math.PI ], + [ false, 0 ], + [ [], 0 ], // empty array coerces to 0 + [ [Math.PI], 180 ], // single-element array coerces to its element + [ [1, 2], NaN ], // multi-element array coerces to NaN + [ {}, NaN ], // object coerces to NaN + ]) { + test(`cxjs_degrees(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_degrees(input), result); + }); + } + }); From 65635f864db7b8756f725b8147a78ceaa2976e49 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 18 Jun 2026 10:59:10 -0600 Subject: [PATCH 49/56] Add tests for cxjs_radians(). --- .../sys/js/tests/cxjs_radians.test.js | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_radians.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_radians.test.js b/centrallix-os/sys/js/tests/cxjs_radians.test.js new file mode 100644 index 000000000..64708c79c --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_radians.test.js @@ -0,0 +1,81 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which +// would make distinct edge-case rows share a test name; fmt renders those +// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + +describe('cxjs_radians', () => + { + for (const [ input, result ] of [ + // Input Result + [ 0, 0 ], + [ 180, Math.PI ], + [ 90, Math.PI / 2 ], + [ 45, Math.PI / 4 ], + [ 360, Math.PI * 2 ], + [ 270, Math.PI * 1.5 ], + [ -180, -Math.PI ], + [ -0, -0 ], // sign preserved + [ Infinity, Infinity ], + [ -Infinity, -Infinity ], + [ NaN, NaN ], // NaN propagates (no coercion). + ]) { + test(`cxjs_radians(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_radians(input), result); + }); + } + + // Only null and undefined yield null (no coercion). + for (const input of [ null, undefined ]) + { + test(`cxjs_radians(${fmt(input)}) = null`, () => + { + assert.equal(env.cxjs_radians(input), null); + }); + } + + // Non-number inputs are coerced to numbers by the arithmetic. + // Those that coerce to NaN yield NaN (null is never used). + for (const [ input, result ] of [ + // Input Result + [ '180', Math.PI ], + [ '', 0 ], // empty string coerces to 0 + [ 'foo', NaN ], // non-numeric string coerces to NaN + [ true, Math.PI / 180 ], + [ false, 0 ], + [ [], 0 ], // empty array coerces to 0 + [ [180], Math.PI ], // single-element array coerces to its element + [ [1, 2], NaN ], // multi-element array coerces to NaN + [ {}, NaN ], // object coerces to NaN + ]) { + test(`cxjs_radians(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_radians(input), result); + }); + } + }); From 51b647cc854d33e80ebb8ac781d12739c0000de0 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 18 Jun 2026 11:02:42 -0600 Subject: [PATCH 50/56] Add tests for cxjs_square(). --- .../sys/js/tests/cxjs_square.test.js | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_square.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_square.test.js b/centrallix-os/sys/js/tests/cxjs_square.test.js new file mode 100644 index 000000000..3b11a8e29 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_square.test.js @@ -0,0 +1,88 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which +// would make distinct edge-case rows share a test name; fmt renders those +// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + +describe('cxjs_square', () => + { + for (const [ input, result ] of [ + // Input Result + [ 4, 16 ], + [ 3, 9 ], + [ 0.5, 0.25 ], + [ 1, 1 ], + [ 0, 0 ], + [ -0, +0 ], // (-0)^2 is +0 + [ -2, 4 ], // negatives square to positives + [ -1.5, 2.25 ], + [ Infinity, Infinity ], + [ -Infinity, Infinity ], + [ 1e200, Infinity ], // overflows to Infinity + ]) { + test(`cxjs_square(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_square(input), result); + }); + } + + // null and undefined yield null (no coercion). + for (const input of [ null, undefined ]) + { + test(`cxjs_square(${fmt(input)}) = null`, () => + { + assert.equal(env.cxjs_square(input), null); + }); + } + + // Unlike sqrt(), square() does not filter NaN: NaN squares to NaN, not null. + test(`cxjs_square(${fmt(NaN)}) = ${fmt(NaN)}`, () => + { + assert.equal(env.cxjs_square(NaN), NaN); + }); + + // Non-number inputs are coerced to numbers; those coercing to NaN square + // to NaN (null is reserved for an explicit null/undefined input). + for (const [ input, result ] of [ + // Input Result + [ '4', 16 ], + [ '2.5', 6.25 ], + [ '', 0 ], // empty string coerces to 0 + [ 'foo', NaN ], // non-numeric string coerces to NaN + [ true, 1 ], + [ false, 0 ], + [ [], 0 ], // empty array coerces to 0 + [ [4], 16 ], // single-element array coerces to its element + [ [4, 9], NaN ], // multi-element array coerces to NaN + [ {}, NaN ], // object coerces to NaN + ]) { + test(`cxjs_square(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_square(input), result); + }); + } + }); From 45960506515a80e814a75b8f6c69d5d5b9e842ad Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 18 Jun 2026 11:06:15 -0600 Subject: [PATCH 51/56] Add tests for cxjs_power(). --- centrallix-os/sys/js/tests/cxjs_power.test.js | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 centrallix-os/sys/js/tests/cxjs_power.test.js diff --git a/centrallix-os/sys/js/tests/cxjs_power.test.js b/centrallix-os/sys/js/tests/cxjs_power.test.js new file mode 100644 index 000000000..4095e14a9 --- /dev/null +++ b/centrallix-os/sys/js/tests/cxjs_power.test.js @@ -0,0 +1,152 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which +// would make distinct edge-case rows share a test name; fmt renders those +// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + +describe('cxjs_power', () => + { + for (const [ n, p, result ] of [ + // n p Result + [ 2, 3, 8 ], + [ 5, 2, 25 ], + [ 2, 10, 1024 ], + [ 7, 1, 7 ], // identity exponent + [ 9, 0, 1 ], // zero exponent + [ 0, 0, 1 ], // 0^0 is defined as 1 + [ 0, 5, 0 ], + [ 1, 100, 1 ], + [ 2, -1, 0.5 ], // negative exponent + [ 2, -2, 0.25 ], + [ 10, -2, 0.01 ], + [ 4, 0.5, 2 ], // fractional exponent (root) + [ 2, 0.5, Math.SQRT2 ], + [ 27, 1 / 3, 3 ], + ]) { + test(`cxjs_power(${fmt(n)}, ${fmt(p)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_power(n, p), result); + }); + } + + // Negative base: integer exponents stay real, but a fractional exponent + // has no real root and yields NaN. + for (const [ n, p, result ] of [ + // n p Result + [ -2, 2, 4 ], + [ -2, 3, -8 ], + [ -3, 0, 1 ], + [ -2, -2, 0.25 ], + [ -8, 1 / 3, NaN ], + [ -1, 0.5, NaN ], + ]) { + test(`cxjs_power(${fmt(n)}, ${fmt(p)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_power(n, p), result); + }); + } + + // A null or undefined for either argument short-circuits to null before + // any exponentiation (and so never coerces to NaN). + for (const [ n, p ] of [ + [ null, 2 ], + [ 2, null ], + [ null, null ], + [ undefined, 2 ], + [ 2, undefined ], + [ undefined, undefined ], + [ null, undefined ], + ]) { + test(`cxjs_power(${fmt(n)}, ${fmt(p)}) = null`, () => + { + assert.equal(env.cxjs_power(n, p), null); + }); + } + + // Infinities and the IEEE-754 corner cases of exponentiation: a zero + // exponent always wins (1), a base of 1 or -1 against an infinite exponent + // is NaN, and 0^negative is a signed infinity. + for (const [ n, p, result ] of [ + // n p Result + [ Infinity, 2, Infinity ], + [ Infinity, -1, 0 ], + [ Infinity, 0, 1 ], + [ 2, Infinity, Infinity ], + [ 2, -Infinity, 0 ], + [ 0.5, Infinity, 0 ], + [ 0, Infinity, 0 ], + [ 0, -1, Infinity ], + [ -0, -1, -Infinity ], + [ 1, Infinity, NaN ], + [ -1, Infinity, NaN ], + [ -Infinity, 2, Infinity ], + [ -Infinity, 3, -Infinity ], + [ -Infinity, -1, -0 ], + ]) { + test(`cxjs_power(${fmt(n)}, ${fmt(p)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_power(n, p), result); + }); + } + + // NaN propagates. + for (const [ n, p, result ] of [ + // n p Result + [ NaN, 2, NaN ], + [ 2, NaN, NaN ], + [ NaN, NaN, NaN ], + [ NaN, 0, 1 ], // Exception: 0 exponent yields 1. + ]) { + test(`cxjs_power(${fmt(n)}, ${fmt(p)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_power(n, p), result); + }); + } + + // Arguments that pass the null/undefined guard are coerced to numbers. + for (const [ n, p, result ] of [ + // n p Result + [ '2', '3', 8 ], // numeric strings coerce + [ '4', 0.5, 2 ], + [ '', 2, 0 ], // empty string coerces to 0 + [ 2, '', 1 ], // ...so this is 2^0 + [ 'foo', 2, NaN ], // non-numeric string coerces to NaN + [ 2, 'foo', NaN ], + [ true, 3, 1 ], // true coerces to 1 + [ false, 0, 1 ], // false coerces to 0, then 0^0 is 1 + [ 2, true, 2 ], + [ [], 2, 0 ], // empty array coerces to 0 + [ [3], 2, 9 ], // single-element array coerces to its element + [ 2, [3], 8 ], + [ {}, 2, NaN ], // object coerces to NaN + ]) { + test(`cxjs_power(${fmt(n)}, ${fmt(p)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_power(n, p), result); + }); + } + }); From 88f447db45f197f8f5b85710c9bd8c1ff48b6672 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 18 Jun 2026 13:10:36 -0600 Subject: [PATCH 52/56] Add many test cases to cover even more edge cases. Clean up and improve formatting/style. --- centrallix-os/sys/js/tests/cxjs_abs.test.js | 28 +++- .../sys/js/tests/cxjs_char_length.test.js | 30 +++- .../sys/js/tests/cxjs_charindex.test.js | 90 ++++++++---- .../sys/js/tests/cxjs_condition.test.js | 49 ++++++- .../sys/js/tests/cxjs_constrain.test.js | 36 ++++- .../sys/js/tests/cxjs_convert.test.js | 129 +++++++++++++----- centrallix-os/sys/js/tests/cxjs_count.test.js | 68 ++++++++- .../sys/js/tests/cxjs_degrees.test.js | 60 +++++--- .../sys/js/tests/cxjs_isnull.test.js | 40 +++++- centrallix-os/sys/js/tests/cxjs_lower.test.js | 23 +++- centrallix-os/sys/js/tests/cxjs_ltrim.test.js | 58 +++++--- centrallix-os/sys/js/tests/cxjs_max.test.js | 103 +++++++++++++- centrallix-os/sys/js/tests/cxjs_min.test.js | 118 +++++++++++++--- centrallix-os/sys/js/tests/cxjs_minus.test.js | 49 ++++++- centrallix-os/sys/js/tests/cxjs_plus.test.js | 24 ++++ centrallix-os/sys/js/tests/cxjs_power.test.js | 56 ++++++-- centrallix-os/sys/js/tests/cxjs_quote.test.js | 31 ++++- .../sys/js/tests/cxjs_radians.test.js | 32 ++++- centrallix-os/sys/js/tests/cxjs_rand.test.js | 4 +- .../sys/js/tests/cxjs_replace.test.js | 95 ++++++++----- .../sys/js/tests/cxjs_replicate.test.js | 75 +++++++--- .../sys/js/tests/cxjs_reverse.test.js | 32 ++++- centrallix-os/sys/js/tests/cxjs_right.test.js | 88 ++++++++---- centrallix-os/sys/js/tests/cxjs_round.test.js | 44 +++++- centrallix-os/sys/js/tests/cxjs_rtrim.test.js | 56 +++++--- centrallix-os/sys/js/tests/cxjs_sqrt.test.js | 20 ++- .../sys/js/tests/cxjs_square.test.js | 37 +++-- centrallix-os/sys/js/tests/cxjs_sum.test.js | 99 +++++++++++++- .../sys/js/tests/cxjs_truncate.test.js | 28 ++++ centrallix-os/sys/js/tests/cxjs_upper.test.js | 21 ++- .../sys/js/tests/cxjs_user_name.test.js | 38 +++++- 31 files changed, 1360 insertions(+), 301 deletions(-) diff --git a/centrallix-os/sys/js/tests/cxjs_abs.test.js b/centrallix-os/sys/js/tests/cxjs_abs.test.js index a3156abf1..136c8d450 100644 --- a/centrallix-os/sys/js/tests/cxjs_abs.test.js +++ b/centrallix-os/sys/js/tests/cxjs_abs.test.js @@ -14,15 +14,18 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); -// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which -// would make distinct edge-case rows share a test name; fmt renders those -// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +// JSON.stringify collapses NaN/Infinity to "null", omits undefined, and renders +// -0 as "0", which would make distinct edge-case rows share a test name; fmt +// renders those values verbatim (and -0 distinctly from 0) while otherwise +// matching JSON.stringify, so names stay unique. function fmt(v) { if (Array.isArray(v)) return '[' + v.map(fmt).join(',') + ']'; if (v !== null && typeof v === 'object') return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; if (typeof v === 'number' || v === undefined) return String(v); return JSON.stringify(v); @@ -41,6 +44,13 @@ describe('cxjs_abs', () => [ Infinity, Infinity ], [ -Infinity, Infinity ], [ NaN, NaN ], + + // Extreme finite magnitudes survive unchanged (no overflow/underflow). + // Input Result + [ Number.MAX_VALUE, Number.MAX_VALUE ], + [ -Number.MAX_VALUE, Number.MAX_VALUE ], + [ Number.MIN_VALUE, Number.MIN_VALUE ], // smallest subnormal + [ -Number.MIN_VALUE, Number.MIN_VALUE ], ]) { test(`cxjs_abs(${fmt(input)}) = ${fmt(result)}`, () => { @@ -71,6 +81,18 @@ describe('cxjs_abs', () => [ [5], 5 ], // single-element array coerces to its element [ [5, 6], NaN ], // multi-element array coerces to NaN [ {}, NaN ], // object coerces to NaN + + // String coercion follows JS Number() rules: surrounding whitespace is + // stripped, hex and exponential literals parse, and all-whitespace is 0. + // Input Result + [ ' 5 ', 5 ], // surrounding whitespace stripped + [ ' 3.14 ', 3.14 ], + [ '0x10', 16 ], // hex literal + [ '1e3', 1000 ], // exponential literal + [ '-0', 0 ], // string negative zero -> +0 magnitude + [ ' ', 0 ], // all-whitespace coerces to 0 + [ ['5'], 5 ], // single string-element array coerces to its element + [ [' '], 0 ], // single whitespace-element array coerces to 0 ]) { test(`cxjs_abs(${fmt(input)}) = ${fmt(result)}`, () => { diff --git a/centrallix-os/sys/js/tests/cxjs_char_length.test.js b/centrallix-os/sys/js/tests/cxjs_char_length.test.js index 9faf9602d..9eb751e9d 100644 --- a/centrallix-os/sys/js/tests/cxjs_char_length.test.js +++ b/centrallix-os/sys/js/tests/cxjs_char_length.test.js @@ -17,6 +17,24 @@ const env = require('./_setup'); // cxjs_char_length() returns null for null/undefined, otherwise the // String()-coerced length in UTF-16 code units (so surrogate pairs // count as 2). + +// JSON.stringify collapses NaN/Infinity to "null", omits undefined, and renders +// -0 as "0", which would make distinct edge-case rows share a test name; fmt +// renders those values verbatim, distinguishes -0 from 0, and otherwise matches +// JSON.stringify, so names stay unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + describe('cxjs_char_length', () => { for (const [ input, result ] of [ @@ -45,8 +63,18 @@ describe('cxjs_char_length', () => [ [1, 2], 3 ], // coerced: "1,2" [ [1, [2, 3]], 5 ], // nested array flattens: "1,2,3" [ {}, 15 ], // coerced: "[object Object]" + // Additional edge cases. + [ ' a ', 3 ], // surrounding whitespace counts + [ -0, 1 ], // negative zero coerces to "0" + [ 1e21, 5 ], // large float uses exponent form "1e+21" + [ '๐Ÿ˜€๐Ÿ˜€', 4 ], // two surrogate pairs = 4 code units + [ ['a'], 1 ], // single-element array coerces to "a" + [ ['a', 'b'], 3 ], // coerced: "a,b" + [ [null], 0 ], // null element renders as "" -> length 0 + [ [undefined], 0 ], // undefined element renders as "" -> length 0 + [ [null, null], 1 ], // coerced: "," (one separator) -> length 1 ]) { - test(`cxjs_char_length(${JSON.stringify(input)}) = ${result}`, () => + test(`cxjs_char_length(${fmt(input)}) = ${fmt(result)}`, () => { assert.equal(env.cxjs_char_length(input), result); }); diff --git a/centrallix-os/sys/js/tests/cxjs_charindex.test.js b/centrallix-os/sys/js/tests/cxjs_charindex.test.js index 7b35bae61..1486c89a1 100644 --- a/centrallix-os/sys/js/tests/cxjs_charindex.test.js +++ b/centrallix-os/sys/js/tests/cxjs_charindex.test.js @@ -19,39 +19,69 @@ const env = require('./_setup'); describe('cxjs_charindex', () => { for (const [ needle, haystack, result ] of [ - // Needle Haystack Result - [ 'h', 'hello', 1 ], // at start - [ 'lo', 'hello', 4 ], // multi-char, mid/end - [ 'o', 'hello', 5 ], // at end - [ 'l', 'hello', 3 ], // first of several matches - [ 'hello', 'hello', 1 ], // needle equals haystack - [ 'z', 'hello', 0 ], // absent - [ 'hellox', 'hello', 0 ], // needle longer than haystack - [ 'H', 'hello', 0 ], // case-sensitive: no match - [ '', 'hello', 1 ], // empty needle matches at start - [ '', '', 1 ], // empty needle, empty haystack - [ 'a', '', 0 ], // empty haystack, non-empty needle - [ ' ', 'a b', 2 ], // whitespace needle - [ '$', 'a$b', 2 ], // special character - [ '๐Ÿ˜€', 'a๐Ÿ˜€b', 2 ], // surrogate pair needle - [ '2', 123, 2 ], // numeric haystack coerced to string - [ 2, '123', 2 ], // numeric needle coerced to string - [ 2, 123, 2 ], // multiple coercions - [ null, 'hello', null ], // null needle - [ 'lo', null, null ], // null haystack - [ null, null, null ], // both null + // Needle Haystack Result + [ 'h', 'hello', 1 ], // at start + [ 'lo', 'hello', 4 ], // multi-char, mid/end + [ 'o', 'hello', 5 ], // at end + [ 'l', 'hello', 3 ], // first of several matches + [ 'hello', 'hello', 1 ], // needle equals haystack + [ 'z', 'hello', 0 ], // absent + [ 'hellox', 'hello', 0 ], // needle longer than haystack + [ 'H', 'hello', 0 ], // case-sensitive: no match + [ '', 'hello', 1 ], // empty needle matches at start + [ '', '', 1 ], // empty needle, empty haystack + [ 'a', '', 0 ], // empty haystack, non-empty needle + [ ' ', 'a b', 2 ], // whitespace needle + [ '$', 'a$b', 2 ], // special character + [ '๐Ÿ˜€', 'a๐Ÿ˜€b', 2 ], // surrogate pair needle + [ '2', 123, 2 ], // numeric haystack coerced to string + [ 2, '123', 2 ], // numeric needle coerced to string + [ 2, 123, 2 ], // multiple coercions + [ null, 'hello', null ], // null needle + [ 'lo', null, null ], // null haystack + [ null, null, null ], // both null + // undefined is not strictly null, so it is coerced, not short-circuited. - [ undefined, 'hello', 0 ], // searches for 'undefined': absent - [ 'u', undefined, 1 ], // haystack becomes 'undefined' - [ 'x', undefined, 0 ], // 'x' absent from 'undefined' - [ undefined, undefined, 1 ], // both -> 'undefined'; found at start + // Needle Haystack Result + [ undefined, 'hello', 0 ], // searches for 'undefined': absent + [ 'u', undefined, 1 ], // haystack becomes 'undefined' + [ 'x', undefined, 0 ], // 'x' absent from 'undefined' + [ undefined, undefined, 1 ], // both -> 'undefined'; found at start + // A null in EITHER position short-circuits first, even when the other // argument is undefined (which would otherwise be coerced). - [ undefined, null, null ], // null haystack wins - [ null, undefined, null ], // null needle wins - // Non-string needle/haystack coerce via indexOf()/new String(). - [ true, 'xtrueb', 2 ], // boolean needle coerced to 'true' - [ 'b', true, 0 ], // boolean haystack coerced to 'true'; 'b' absent + // Needle Haystack Result + [ undefined, null, null ], // null haystack wins + [ null, undefined, null ], // null needle wins + + // Non-string needle/haystack coercion. + // Needle Haystack Result + [ true, 'xtrueb', 2 ], // boolean needle coerced to 'true' + [ 'b', true, 0 ], // boolean haystack coerced to 'true'; 'b' absent + [ false, 'xfalseb', 2 ], // boolean needle coerced to 'false' + [ 0, 'a0b', 2 ], // numeric-zero needle coerced to '0' + [ NaN, 'aNaNb', 2 ], // NaN needle coerced to 'NaN' + [ 'N', NaN, 1 ], // NaN haystack coerced to 'NaN' + [ Infinity, 'aInfinityb', 2 ], // Infinity needle coerced to 'Infinity' + + // Array needle/haystack coerce to comma-joined string. + // Needle Haystack Result + [ [], 'a', 1 ], // [] needle -> '' matches at start + [ [ 'a' ], 'bab', 2 ], // single-element array -> 'a' + [ [ 'a', 'b' ], 'a,bc', 1 ], // multi-element array -> 'a,b' (with comma) + [ 'o', [ 'hello' ], 5 ], // single-element array haystack -> 'hello' + + // Plain object needle/haystack coerce to '[object Object]'. + // Needle Haystack Result + [ 'c', {}, 6 ], // 'c' of '[obje[c]t Object]' + [ 'Object', {}, 9 ], // 'Object' substring of '[object Object]' + + // Surrogate-pair (UTF-16) indexing: positions count code units, and a + // lone surrogate half can match inside an emoji. + // Needle Haystack Result + [ '๐Ÿ˜€', '๐Ÿ˜€x', 1 ], // emoji needle found at start + [ 'x', '๐Ÿ˜€x', 3 ], // 'x' after a 2-code-unit emoji + [ '\uDE00', '๐Ÿ˜€', 2 ], // trailing surrogate half matches at unit 2 ]) { test(`cxjs_charindex(${JSON.stringify(needle)}, ${JSON.stringify(haystack)}) = ${JSON.stringify(result)}`, () => { diff --git a/centrallix-os/sys/js/tests/cxjs_condition.test.js b/centrallix-os/sys/js/tests/cxjs_condition.test.js index bbd022148..d45b89e9e 100644 --- a/centrallix-os/sys/js/tests/cxjs_condition.test.js +++ b/centrallix-os/sys/js/tests/cxjs_condition.test.js @@ -14,14 +14,32 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); +// JSON.stringify collapses NaN/Infinity to "null", drops undefined, and +// renders -0 as "0", which would make distinct edge-case rows share a test +// name; fmt renders those verbatim so names stay unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + describe('cxjs_condition', () => { for (const [ c, vtrue, vfalse, result ] of [ - // Condition vtrue vfalse Result // null/undefined short-circuit to null before vtrue/vfalse. + // Condition vtrue vfalse Result [ null, 'T', 'F', null ], [ undefined, 'T', 'F', null ], + // Truthy conditions yield vtrue. + // Condition vtrue vfalse Result [ true, 'T', 'F', 'T' ], [ 1, 'T', 'F', 'T' ], [ -1, 'T', 'F', 'T' ], @@ -32,22 +50,49 @@ describe('cxjs_condition', () => [ ' ', 'T', 'F', 'T' ], [ [], 'T', 'F', 'T' ], // empty array is truthy [ {}, 'T', 'F', 'T' ], + [ -Infinity, 'T', 'F', 'T' ], // any nonzero number is truthy + [ [0], 'T', 'F', 'T' ], // object identity, contents irrelevant + [ 'NaN', 'T', 'F', 'T' ], // nonempty string, not the NaN number + // Falsy conditions yield vfalse. + // Condition vtrue vfalse Result [ false, 'T', 'F', 'F' ], [ 0, 'T', 'F', 'F' ], [ -0, 'T', 'F', 'F' ], [ NaN, 'T', 'F', 'F' ], [ '', 'T', 'F', 'F' ], + // vtrue/vfalse pass through verbatim, regardless of type. + // Condition vtrue vfalse Result [ true, 42, 99, 42 ], [ false, 42, 99, 99 ], [ true, null, 'F', null ], // truthy c can still return null [ true, undefined, 'F', undefined ], [ false, 'T', undefined, undefined ], + [ true, 0, 1, 0 ], // falsy values pass through too + [ false, 1, '', '' ], + + // Both branches omitted: a defined-but-truthy c yields undefined vtrue, + // a falsy c yields undefined vfalse (null c is handled separately above). + // Condition vtrue vfalse Result + [ true, undefined, undefined, undefined ], + [ false, undefined, undefined, undefined ], ]) { - test(`cxjs_condition(${JSON.stringify(c)}, ${JSON.stringify(vtrue)}, ${JSON.stringify(vfalse)}) = ${JSON.stringify(result)}`, () => + test(`cxjs_condition(${fmt(c)}, ${fmt(vtrue)}, ${fmt(vfalse)}) = ${fmt(result)}`, () => { assert.equal(env.cxjs_condition(c, vtrue, vfalse), result); }); } + + // Signed-zero (JSON.stringify(-0) is "0", so name explicitly). -0 is a + // number == null is false, so it reaches the ternary and is falsy -> vfalse; + // and a -0 passed as a branch value comes back unchanged (verbatim). + test('cxjs_condition(-0, "T", "F") = "F" (-0 is falsy)', () => + { + assert.equal(env.cxjs_condition(-0, 'T', 'F'), 'F'); + }); + test('cxjs_condition(false, 1, -0) = -0 (branch value verbatim)', () => + { + assert.equal(env.cxjs_condition(false, 1, -0), -0); + }); }); diff --git a/centrallix-os/sys/js/tests/cxjs_constrain.test.js b/centrallix-os/sys/js/tests/cxjs_constrain.test.js index eb2fa1f91..0045cbfad 100644 --- a/centrallix-os/sys/js/tests/cxjs_constrain.test.js +++ b/centrallix-os/sys/js/tests/cxjs_constrain.test.js @@ -16,9 +16,11 @@ const env = require('./_setup'); // JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which // would make distinct edge-case rows share a test name; fmt renders those -// values verbatim so names stay unique. +// values verbatim (and distinguishes -0 from +0) so names stay unique. function fmt(v) { + if (Object.is(v, -0)) + return '-0'; if (typeof v === 'number' || v === undefined) return String(v); return JSON.stringify(v); @@ -90,6 +92,38 @@ describe('cxjs_constrain', () => [ 5, 10, 0, 10 ], [ 100, 10, 0, 0 ], [ -5, 10, 0, 10 ], + + // Inverted bounds with n equal to a bound: equality is not "<" or ">", + // so n == min still falls through to the max check (and vice versa). + [ 10, 10, 0, 0 ], // n == min, then 10 > 0 -> max + [ 0, 10, 0, 10 ], // n == max, but 0 < 10 -> min + + // Sign of zero: comparisons treat -0 and +0 as equal, so neither bound + // fires and the original (signed) n passes through unchanged. + [ -0, 0, 10, -0 ], // -0 < 0 false, -0 > 10 false + [ -0, 0, 0, -0 ], // both bounds == -0 numerically + [ 0, -0, 10, 0 ], + // A signed-zero bound is returned verbatim when n falls outside it. + [ -5, -0, 10, -0 ], // -5 < -0 -> returns min -0 + + // Bounds are returned without coercion, so a numeric-string bound that + // clamps is returned as the original string (compared numerically). + [ 3, '5', 10, '5' ], // 3 < "5" -> returns "5" + [ 7, '5', 10, 7 ], // 7 < "5" false -> n + [ 15, 0, '10', '10' ], // 15 > "10" -> returns "10" + [ 5, 0, '10', 5 ], + + // Infinite bounds and n: an infinity within infinite bounds is unclamped. + [ Infinity, -Infinity, Infinity, Infinity ], + [ -Infinity, -Infinity, Infinity, -Infinity ], + [ Infinity, 0, Infinity, Infinity ], + + // Both bounds NaN with NaN n: NaN bounds never fire and NaN passes through. + [ NaN, NaN, NaN, NaN ], + + // Extreme finite magnitudes clamp like any other value. + [ Number.MAX_VALUE, 0, 1, 1 ], + [ 1e308, 0, 1e308, 1e308 ], ]) { test(`cxjs_constrain(${fmt(n)}, ${fmt(min)}, ${fmt(max)}) = ${fmt(result)}`, () => { diff --git a/centrallix-os/sys/js/tests/cxjs_convert.test.js b/centrallix-os/sys/js/tests/cxjs_convert.test.js index 9314808e3..f6b2494fb 100644 --- a/centrallix-os/sys/js/tests/cxjs_convert.test.js +++ b/centrallix-os/sys/js/tests/cxjs_convert.test.js @@ -18,7 +18,7 @@ describe('cxjs_convert', () => { // A null datatype or value yields null, regardless of the other // argument (== null also catches undefined). - for (const [ dt, v, result ] of [ + for (const [ datatype, v, result ] of [ // Datatype Value Result [ null, 5, null ], [ undefined, 5, null ], @@ -28,9 +28,9 @@ describe('cxjs_convert', () => [ 'string', null, null ], [ null, null, null ], ]) { - test(`cxjs_convert(${JSON.stringify(dt)}, ${JSON.stringify(v)}) = ${result}`, () => + test(`cxjs_convert(${JSON.stringify(datatype)}, ${JSON.stringify(v)}) = ${result}`, () => { - assert.equal(env.cxjs_convert(dt, v), result); + assert.equal(env.cxjs_convert(datatype, v), result); }); } @@ -38,7 +38,7 @@ describe('cxjs_convert', () => // Note: The '$' is only stripped when it is the *second* character // (e.g. 'x$5'). A leading '$5' is not, so it parses to NaN. Stripping // the '$' also drops a leading sign. - for (const [ dt, v, result ] of [ + for (const [ datatype, v, result ] of [ // Datatype Value Result [ 'integer', 0, 0 ], [ 'integer', 5, 5 ], @@ -60,10 +60,17 @@ describe('cxjs_convert', () => [ 'integer', 'a$bc', NaN ], // 2nd char '$' stripped, but 'bc' isn't numeric. [ 'integer', Infinity, NaN ], // parseInt('Infinity') is NaN. [ 'integer', false, NaN ], + [ 'integer', '+5', 5 ], // Leading '+' handled. + [ 'integer', '0x10', 16 ], // Hex notation handled. + [ 'integer', ' $12', 12 ], // 2nd char '$' stripped, parses '12'. + [ 'integer', 'x$-3', -3 ], // Stripped to '-3', sign honored. + [ 'integer', 'x$', NaN ], // Stripped to '', parseInt('') is NaN. + [ 'integer', [ 42 ], 42 ], // Array stringifies to '42'. + [ 'integer', [ 42, 1 ], 42 ], // '42,1' -> parseInt stops at comma. ]) { - test(`cxjs_convert(${JSON.stringify(dt)}, ${JSON.stringify(v)}) = ${result}`, () => + test(`cxjs_convert(${JSON.stringify(datatype)}, ${JSON.stringify(v)}) = ${result}`, () => { - assert.equal(env.cxjs_convert(dt, v), result); + assert.equal(env.cxjs_convert(datatype, v), result); }); } @@ -71,59 +78,107 @@ describe('cxjs_convert', () => // A leading currency marker is stripped in several forms ('$', ' $', // '+$', '$ ', '-$'). '-$' negates the result. Anything else is passed // through to parseFloat. - for (const [ dt, v, result ] of [ + for (const [ datatype, v, result ] of [ // Datatype Value Result - [ 'double', 0, 0 ], - [ 'double', 5.5, 5.5 ], - [ 'double', '5.5', 5.5 ], - [ 'double', '3.14abc', 3.14 ], - [ 'double', '$5', 5 ], - [ 'double', '$5.50', 5.5 ], - [ 'double', ' $5', 5 ], - [ 'double', '+$5', 5 ], - [ 'double', '$ 5', 5 ], - [ 'double', '-$5', -5 ], - [ 'double', '-$ 5', -5 ], - [ 'double', '-$2.5', -2.5 ], - [ 'double', '$1,000', 1 ], - [ 'double', '-5.5', -5.5 ], // Plain negative, no currency marker. + [ 'double', 0, 0 ], + [ 'double', 5.5, 5.5 ], + [ 'double', '5.5', 5.5 ], + [ 'double', '3.14abc', 3.14 ], + [ 'double', '$5', 5 ], + [ 'double', '$5.50', 5.5 ], + [ 'double', ' $5', 5 ], + [ 'double', '+$5', 5 ], + [ 'double', '$ 5', 5 ], + [ 'double', '-$5', -5 ], + [ 'double', '-$ 5', -5 ], + [ 'double', '-$2.5', -2.5 ], + [ 'double', '$1,000', 1 ], + [ 'double', '-5.5', -5.5 ], // Plain negative, no currency marker. [ 'double', Infinity, Infinity ], // parseFloat('Infinity') is Infinity. [ 'double', 'Infinity', Infinity ], - [ 'double', '$', NaN ], - [ 'double', 'abc', NaN ], + [ 'double', '$', NaN ], + [ 'double', 'abc', NaN ], + [ 'double', '1.5e3', 1500 ], // Scientific notation honored. + [ 'double', ' 12', 12 ], // Leading whitespace tolerated by parseFloat. + [ 'double', '', NaN ], + [ 'double', '-$', NaN ], // -parseFloat('') -> -NaN -> NaN. + [ 'double', ' $', NaN ], // Strips ' $', parseFloat('') is NaN. + [ 'double', '+$', NaN ], + [ 'double', '$ ', NaN ], + [ 'double', ' $5', NaN ], // Two leading spaces: no prefix matches, parseFloat fails. + [ 'double', '1,234', 1 ], // parseFloat stops at the comma. + [ 'double', true, NaN ], // parseFloat('true') is NaN. + [ 'double', false, NaN ], + [ 'double', [ 1.5 ], 1.5 ], // Array stringifies to '1.5'. ]) { - test(`cxjs_convert(${JSON.stringify(dt)}, ${JSON.stringify(v)}) = ${result}`, () => + test(`cxjs_convert(${JSON.stringify(datatype)}, ${JSON.stringify(v)}) = ${result}`, () => { - assert.equal(env.cxjs_convert(dt, v), result); + assert.equal(env.cxjs_convert(datatype, v), result); }); } // Conversion to string (using standard JS coercion). - for (const [ dt, v, result ] of [ + for (const [ datatype, v, result ] of [ // Datatype Value Result - [ 'string', 0, '0' ], - [ 'string', 5, '5' ], - [ 'string', 5.5, '5.5' ], - [ 'string', -3.2, '-3.2' ], - [ 'string', 'hello', 'hello' ], - [ 'string', true, 'true' ], + [ 'string', 0, '0' ], + [ 'string', 5, '5' ], + [ 'string', 5.5, '5.5' ], + [ 'string', -3.2, '-3.2' ], + [ 'string', 'hello', 'hello' ], + [ 'string', true, 'true' ], + [ 'string', false, 'false' ], + [ 'string', Infinity, 'Infinity' ], + [ 'string', -Infinity, '-Infinity' ], + [ 'string', NaN, 'NaN' ], + [ 'string', '', '' ], + [ 'string', [ 1, 2, 3 ], '1,2,3' ], // Arrays join with commas. + [ 'string', [], '' ], + [ 'string', { a: 1 }, '[object Object]' ], ]) { - test(`cxjs_convert(${JSON.stringify(dt)}, ${JSON.stringify(v)}) = ${result}`, () => + test(`cxjs_convert(${JSON.stringify(datatype)}, ${JSON.stringify(v)}) = ${result}`, () => { - assert.equal(env.cxjs_convert(dt, v), result); + assert.equal(env.cxjs_convert(datatype, v), result); + }); + } + + // Signed zero. + test('cxjs_convert("double", "-$0") is -0', () => + { + assert.ok(Object.is(env.cxjs_convert('double', '-$0'), -0)); + }); + test('cxjs_convert("double", "$0") is +0', () => + { + assert.ok(Object.is(env.cxjs_convert('double', '$0'), 0)); + }); + test('cxjs_convert("string", -0) is "0"', () => + { + assert.equal(env.cxjs_convert('string', -0), '0'); + }); + + // datatype that is falsy-but-not-null (0, '', false). + for (const [ datatype, v, result ] of [ + // Datatype Value Result + [ 0, 5, 5 ], + [ '', 5, 5 ], + [ false, 5, 5 ], + [ 'INTEGER', '5abc', '5abc' ], // Wrong case -> unknown datatype -> unchanged. + ]) { + test(`cxjs_convert(${JSON.stringify(datatype)}, ${JSON.stringify(v)}) = ${JSON.stringify(result)}`, () => + { + assert.deepEqual(env.cxjs_convert(datatype, v), result); }); } // An unrecognized datatype returns the value unchanged. - for (const [ dt, v, result ] of [ + for (const [ datatype, v, result ] of [ // Datatype Value Result [ 'money', 5, 5 ], [ 'datetime', 'x', 'x' ], [ 'MyType', 42, 42 ], ]) { - test(`cxjs_convert(${JSON.stringify(dt)}, ${JSON.stringify(v)}) = ${result}`, () => + test(`cxjs_convert(${JSON.stringify(datatype)}, ${JSON.stringify(v)}) = ${result}`, () => { - assert.equal(env.cxjs_convert(dt, v), result); + assert.equal(env.cxjs_convert(datatype, v), result); }); } }); diff --git a/centrallix-os/sys/js/tests/cxjs_count.test.js b/centrallix-os/sys/js/tests/cxjs_count.test.js index 5a26a6ed5..1d4e544da 100644 --- a/centrallix-os/sys/js/tests/cxjs_count.test.js +++ b/centrallix-os/sys/js/tests/cxjs_count.test.js @@ -14,6 +14,23 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); +// JSON.stringify collapses NaN/Infinity to "null", omits undefined, and renders +// -0 as "0", which would make distinct edge-case rows share a test name; fmt +// renders those values verbatim, distinguishes -0 from 0, and otherwise matches +// JSON.stringify, so names stay unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + describe('cxjs_count', () => { for (const [ input, result ] of [ @@ -24,26 +41,30 @@ describe('cxjs_count', () => [ [9, 10.1, 10.2], 3 ], [ [-1.1, 1], 2 ], [ [-Infinity, Infinity], 2 ], + // null and undefined are not counted. [ [null], 0 ], [ [undefined], 0 ], [ [null, undefined], 0 ], [ [null, 0], 1 ], [ [undefined, 0], 1 ], + // NaN is not counted. [ [NaN], 0 ], [ [NaN, 1], 1 ], [ [NaN, null, undefined, 5], 1 ], + // Numeric strings count; non-numeric strings do not. '' is numeric (0). [ ['5', 6], 2 ], [ ['', 6], 2 ], [ ['abc', 6], 1 ], [ ['abc', 'def'], 0 ], + // Booleans are numeric under isNaN (true -> 1, false -> 0), so both count. [ [true], 1 ], [ [true, false], 2 ], ]) { - test(`cxjs_count(${JSON.stringify(input)}) = ${result}`, () => + test(`cxjs_count(${fmt(input)}) = ${fmt(result)}`, () => { assert.equal(env.cxjs_count(input), result); }); @@ -75,7 +96,7 @@ describe('cxjs_count', () => // Booleans are numeric under isNaN (true -> 1, false -> 0), so both count. [ { a: true, b: false }, 2 ], ]) { - test(`cxjs_count(${JSON.stringify(input)}) = ${result}`, () => + test(`cxjs_count(${fmt(input)}) = ${fmt(result)}`, () => { assert.equal(env.cxjs_count(input), result); }); @@ -96,9 +117,50 @@ describe('cxjs_count', () => [ null, 1 ], [ undefined, 1 ], ]) { - test(`cxjs_count(${JSON.stringify(input)}) = ${result}`, () => + test(`cxjs_count(${fmt(input)}) = ${fmt(result)}`, () => { assert.equal(env.cxjs_count(input), result); }); } + + // Nested-array elements are coerced. + for (const [ input, result ] of [ + // Input Result + [ [[1, 2]], 0 ], // [1,2] -> NaN, not counted + [ [[1]], 1 ], // [1] -> 1, counted + [ [[]], 1 ], // [] -> 0, counted + [ [[1], [2]], 2 ], // both single-element, both counted + [ [[1, 2], 3], 1 ], // [1,2] skipped, 3 counted + [ [{}], 0 ], // {} -> NaN, not counted + [ [{ a: 1 }], 0 ], // object -> NaN, not counted + [ [{}, 5], 1 ], // object skipped, 5 counted + [ [Infinity], 1 ], // Infinity is not NaN, counted + [ [-0], 1 ], // -0 counts + [ [-0, 0], 2 ], + ]) { + test(`cxjs_count(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_count(input), result); + }); + } + + // Sparse arrays. + for (const [ input, result ] of [ + // Input Result + [ [1, , 3], 2 ], // one hole between two values + [ [, , 5], 1 ], // two holes (undefined), one value + ]) { + test(`cxjs_count(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_count(input), result); + }); + } + + // A value on the prototype is counted alongside properties. + test('cxjs_count(object with inherited enumerable prop) = 2', () => + { + function Proto() { this.a = 1; } + Proto.prototype.b = 2; + assert.equal(env.cxjs_count(new Proto()), 2); + }); }); diff --git a/centrallix-os/sys/js/tests/cxjs_degrees.test.js b/centrallix-os/sys/js/tests/cxjs_degrees.test.js index 20cb2cd7a..755eb9350 100644 --- a/centrallix-os/sys/js/tests/cxjs_degrees.test.js +++ b/centrallix-os/sys/js/tests/cxjs_degrees.test.js @@ -14,15 +14,18 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); -// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which -// would make distinct edge-case rows share a test name; fmt renders those -// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +// JSON.stringify collapses NaN/Infinity to "null", omits undefined, and renders +// -0 as "0", which would make distinct edge-case rows share a test name; fmt +// renders those values verbatim (and -0 distinctly from 0) while otherwise +// matching JSON.stringify, so names stay unique. function fmt(v) { if (Array.isArray(v)) return '[' + v.map(fmt).join(',') + ']'; if (v !== null && typeof v === 'object') return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; if (typeof v === 'number' || v === undefined) return String(v); return JSON.stringify(v); @@ -33,19 +36,20 @@ describe('cxjs_degrees', () => // Radians to degrees. Some multiples of PI land exactly; others (e.g. PI/6) // carry floating-point error. for (const [ input, result ] of [ - // Input Result - [ 0, 0 ], - [ -0, -0 ], // sign of zero is preserved - [ Math.PI, 180 ], - [ -Math.PI, -180 ], - [ Math.PI / 2, 90 ], - [ Math.PI / 4, 45 ], - [ 2 * Math.PI, 360 ], - [ 1, 180 / Math.PI ], // one radian - [ Math.PI / 6, 29.999999999999996 ], // not exactly 30 - [ Infinity, Infinity ], - [ -Infinity, -Infinity ], - [ NaN, NaN ], + // Input Result + [ 0, 0 ], + [ -0, -0 ], // sign of zero is preserved + [ Math.PI, 180 ], + [ -Math.PI, -180 ], + [ Math.PI / 2, 90 ], + [ Math.PI / 4, 45 ], + [ 2 * Math.PI, 360 ], + [ 1, 180 / Math.PI ], // one radian + [ Math.PI / 6, 29.999999999999996 ], // not exactly 30 + [ 3 * Math.PI / 2, 270 ], + [ Infinity, Infinity ], + [ -Infinity, -Infinity ], + [ NaN, NaN ], ]) { test(`cxjs_degrees(${fmt(input)}) = ${fmt(result)}`, () => { @@ -53,6 +57,25 @@ describe('cxjs_degrees', () => }); } + // Extreme magnitudes follow the exact source expression: a very large value + // overflows to Infinity, a very small one stays finite (no underflow to 0). + for (const input of [ 1e200, Number.MAX_VALUE, 1e-200, -1, Number.MIN_VALUE ]) + { + test(`cxjs_degrees(${fmt(input)}) = (radians*180)/PI`, () => + { + assert.equal(env.cxjs_degrees(input), (input * 180.0) / Math.PI); + }); + } + + // degrees(radians(x)) = x (within float tolerance). + for (const x of [ 1, 45, 123, 360, -90 ]) + { + test(`cxjs_degrees(cxjs_radians(${fmt(x)})) ~= ${fmt(x)}`, () => + { + assert.ok(Math.abs(env.cxjs_degrees(env.cxjs_radians(x)) - x) < 1e-9); + }); + } + // null and undefined yield null (no coercion). for (const input of [ null, undefined ]) { @@ -62,11 +85,12 @@ describe('cxjs_degrees', () => }); } - // Non-number inputs are coerced to numbers by the arithmetic; those that - // coerce to NaN propagate to a NaN result. + // Non-number inputs are coerced to numbers. + // Those that coerce to NaN yield NaN (null is never used). for (const [ input, result ] of [ // Input Result [ '1', 180 / Math.PI ], + [ '-1', -180 / Math.PI ], // negative string coerces to -1 [ '', 0 ], // empty string coerces to 0 [ 'foo', NaN ], // non-numeric string coerces to NaN [ true, 180 / Math.PI ], diff --git a/centrallix-os/sys/js/tests/cxjs_isnull.test.js b/centrallix-os/sys/js/tests/cxjs_isnull.test.js index 3e9abb0c1..7b1160e10 100644 --- a/centrallix-os/sys/js/tests/cxjs_isnull.test.js +++ b/centrallix-os/sys/js/tests/cxjs_isnull.test.js @@ -14,10 +14,26 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); +// JSON.stringify collapses NaN/Infinity to "null", drops undefined, and +// renders -0 as "0", which would make distinct edge-case rows share a test +// name; fmt renders those verbatim so names stay unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + describe('cxjs_isnull', () => { // null/undefined values use the default. - for (const [ value, dflt, result ] of [ + for (const [ value, default_value, result ] of [ // Value Default Result [ null, 5, 5 ], [ undefined, 5, 5 ], @@ -34,15 +50,15 @@ describe('cxjs_isnull', () => [ undefined, null, null ], [ undefined, undefined, undefined ], ]) { - test(`cxjs_isnull(${JSON.stringify(value)}, ${JSON.stringify(dflt)}) = ${JSON.stringify(result)}`, () => + test(`cxjs_isnull(${fmt(value)}, ${fmt(default_value)}) = ${fmt(result)}`, () => { - assert.equal(env.cxjs_isnull(value, dflt), result); + assert.equal(env.cxjs_isnull(value, default_value), result); }); } // Non-null/undefined pass through unchanged. Falsy-but-defined values // (0, '', false, NaN) are NOT treated as null. - for (const [ value, dflt, result ] of [ + for (const [ value, default_value, result ] of [ // Value Default Result [ 0, 5, 0 ], [ '', 5, '' ], @@ -58,9 +74,9 @@ describe('cxjs_isnull', () => [ 0, undefined, 0 ], [ '', null, '' ], ]) { - test(`cxjs_isnull(${JSON.stringify(value)}, ${JSON.stringify(dflt)}) = ${JSON.stringify(result)}`, () => + test(`cxjs_isnull(${fmt(value)}, ${fmt(default_value)}) = ${fmt(result)}`, () => { - assert.equal(env.cxjs_isnull(value, dflt), result); + assert.equal(env.cxjs_isnull(value, default_value), result); }); } @@ -71,7 +87,7 @@ describe('cxjs_isnull', () => [], [1, 2, 3], ]) { - test(`cxjs_isnull(${JSON.stringify(value)}, 'default') returns the value itself`, () => + test(`cxjs_isnull(${fmt(value)}, 'default') returns the value itself`, () => { assert.equal(env.cxjs_isnull(value, 'default'), value); }); @@ -87,4 +103,14 @@ describe('cxjs_isnull', () => { assert.equal(env.cxjs_isnull(0), 0); }); + + // Signed zero is preserved in both positions. + test('cxjs_isnull(-0, 5) = -0 (value not null, passes through)', () => + { + assert.equal(env.cxjs_isnull(-0, 5), -0); + }); + test('cxjs_isnull(null, -0) = -0 (default returned verbatim)', () => + { + assert.equal(env.cxjs_isnull(null, -0), -0); + }); }); diff --git a/centrallix-os/sys/js/tests/cxjs_lower.test.js b/centrallix-os/sys/js/tests/cxjs_lower.test.js index dee953da1..e5efb8f60 100644 --- a/centrallix-os/sys/js/tests/cxjs_lower.test.js +++ b/centrallix-os/sys/js/tests/cxjs_lower.test.js @@ -14,11 +14,18 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); -// JSON.stringify renders NaN/Infinity as "null" and a standalone undefined as -// undefined (not a string), which would give distinct edge-case rows the same -// (or a broken) test name; fmt renders those verbatim so names stay unique. +// JSON.stringify renders NaN/Infinity as "null", a standalone undefined as +// undefined (not a string), and -0 as "0", which would give distinct edge-case +// rows the same (or a broken) test name; fmt renders those verbatim (and +// recurses into arrays/objects) so names stay unique. function fmt(v) { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; if (typeof v === 'number' || v === undefined) return String(v); return JSON.stringify(v); @@ -39,6 +46,13 @@ describe('cxjs_lower', () => [ 'ร€ร‰รŽ', 'ร รฉรฎ' ], // non-ASCII letters [ 'ฤฐ', 'iฬ‡' ], // one char expands to i + combining dot [ 'ฮŸฮ”ฮŸฮฃ', 'ฮฟฮดฮฟฯ‚' ], // trailing ฮฃ lowercases to final sigma ฯ‚ + [ 'ฮฃฮŸฮฃ', 'ฯƒฮฟฯ‚' ], // non-final ฮฃ lowercases to normal sigma ฯƒ + [ '๐Ÿ˜€', '๐Ÿ˜€' ], // surrogate-pair emoji has no case + [ 'cafรฉ', 'cafรฉ' ], // already-lower accented stays + [ 'ร‰', 'รฉ' ], // precomposed accent lowercases + [ ' ', ' ' ], // whitespace-only unchanged + [ '12345', '12345' ], // digits-only unchanged + [ '๐Ÿ˜€ABC', '๐Ÿ˜€abc' ], // emoji kept, letters lowercased ]) { test(`cxjs_lower(${fmt(input)}) = ${fmt(result)}`, () => { @@ -61,6 +75,9 @@ describe('cxjs_lower', () => [ -Infinity, '-infinity' ], [ [], '' ], // empty array coerces to '' [ ['A', 'B'], 'a,b' ], // Joins with ',' and no spaces + [ ['ABC'], 'abc' ], // single-element array unwraps + [ 0, '0' ], // zero coerces to '0' + [ -0, '0' ], // negative zero stringifies to '0' [ {}, '[object object]' ], ]) { test(`cxjs_lower(${fmt(input)}) = ${fmt(result)}`, () => diff --git a/centrallix-os/sys/js/tests/cxjs_ltrim.test.js b/centrallix-os/sys/js/tests/cxjs_ltrim.test.js index 9f3c97246..0da69cd08 100644 --- a/centrallix-os/sys/js/tests/cxjs_ltrim.test.js +++ b/centrallix-os/sys/js/tests/cxjs_ltrim.test.js @@ -17,23 +17,47 @@ const env = require('./_setup'); describe('cxjs_ltrim', () => { for (const [ input, result ] of [ - // Input Result - [ ' hello', 'hello' ], // leading spaces stripped - [ 'hello', 'hello' ], // no leading spaces - [ 'hello ', 'hello ' ], // trailing spaces preserved - [ ' a b ', 'a b ' ], // interior/trailing spaces preserved - [ ' ', '' ], // single space - [ ' ', '' ], // all spaces collapse to empty - [ '', '' ], // empty stays empty - [ ' \thello', '\thello' ], // leading space stripped, tab kept - [ '\t hello', '\t hello' ], // leading tab is not a space: unchanged - [ '\nhi', '\nhi' ], // newline is not a space: unchanged - [ 42, '42' ], // non-string is stringified - [ 0, '0' ], // falsy but not null: coerced, not dropped - [ true, 'true' ], // boolean coerced to string - [ NaN, 'NaN' ], // NaN coerced to string - [ null, null ], // null returns null - [ undefined, null ], // undefined returns null + // Input Result + [ ' hello', 'hello' ], // leading spaces stripped + [ 'hello', 'hello' ], // no leading spaces + [ 'hello ', 'hello ' ], // trailing spaces preserved + [ ' a b ', 'a b ' ], // interior/trailing spaces preserved + [ ' ', '' ], // single space + [ ' ', '' ], // all spaces collapse to empty + [ '', '' ], // empty stays empty + [ ' \thello', '\thello' ], // leading space stripped, tab kept + [ '\t hello', '\t hello' ], // leading tab is not a space: unchanged + [ '\nhi', '\nhi' ], // newline is not a space: unchanged + [ 42, '42' ], // non-string is coerced + [ 0, '0' ], // falsy but not null: coerced, not dropped + [ true, 'true' ], // boolean coerced to string + [ NaN, 'NaN' ], // NaN coerced to string + [ null, null ], // null returns null + [ undefined, null ], // undefined returns null + + // Only the ASCII space (U+0020) is stripped; every other whitespace + // character is left in place by the / */ regex. + // Input Result + [ '\rhi', '\rhi' ], // carriage return is not a space: unchanged + [ '\fhi', '\fhi' ], // form feed is not a space: unchanged + [ '\vhi', '\vhi' ], // vertical tab is not a space: unchanged + [ '\u00a0hi', '\u00a0hi' ], // non-breaking space is not ASCII space: kept + [ '\u3000hi', '\u3000hi' ], // ideographic space is not ASCII space: kept + [ ' \t', '\t' ], // strips leading spaces, stops at the tab + [ ' ๐Ÿ˜€', '๐Ÿ˜€' ], // spaces stripped, surrogate-pair emoji kept + + // More non-string coercions: String() runs before the regex. + // Input Result + [ ' 12', '12' ], // numeric string: leading spaces stripped + [ false, 'false' ], // boolean coerced to string + [ Infinity, 'Infinity' ], // Infinity coerced to string + [ -Infinity, '-Infinity' ], // -Infinity coerced to string + [ [], '' ], // empty array coerces to '' + [ [' hi'], 'hi' ], // single-element array unwraps then trims + [ [' a', ' b'], 'a, b' ], // join inserts ',', only first elem's spaces lead + [ [null], '' ], // [null] coerces to '' (null element -> '') + [ {}, '[object Object]' ], // plain object stringifies, no leading spaces + [ ' xxxxx', 'xxxxx' ], // longer run of leading spaces all stripped ]) { test(`cxjs_ltrim(${JSON.stringify(input)}) = ${JSON.stringify(result)}`, () => { diff --git a/centrallix-os/sys/js/tests/cxjs_max.test.js b/centrallix-os/sys/js/tests/cxjs_max.test.js index e5c1a31ea..b96114955 100644 --- a/centrallix-os/sys/js/tests/cxjs_max.test.js +++ b/centrallix-os/sys/js/tests/cxjs_max.test.js @@ -14,15 +14,18 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); -// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which -// would make distinct edge-case rows share a test name; fmt renders those -// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +// JSON.stringify collapses NaN/Infinity to "null", omits undefined, and renders +// -0 as "0", which would make distinct edge-case rows share a test name; fmt +// renders those values verbatim, distinguishes -0 from 0, and otherwise matches +// JSON.stringify, so names stay unique. function fmt(v) { if (Array.isArray(v)) return '[' + v.map(fmt).join(',') + ']'; if (v !== null && typeof v === 'object') return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; if (typeof v === 'number' || v === undefined) return String(v); return JSON.stringify(v); @@ -47,7 +50,7 @@ describe('cxjs_max', () => [ [undefined], undefined ], [ [undefined, 0], 0 ], ]) { - test(`cxjs_max(${JSON.stringify(input)}) = ${result}`, () => + test(`cxjs_max(${fmt(input)}) = ${fmt(result)}`, () => { assert.equal(env.cxjs_max(input), result); }); @@ -70,14 +73,13 @@ describe('cxjs_max', () => [ { D: undefined }, undefined ], [ { E: undefined, F: 0 }, 0 ], ]) { - test(`cxjs_max(${JSON.stringify(input)}) = ${result}`, () => + test(`cxjs_max(${fmt(input)}) = ${fmt(result)}`, () => { assert.equal(env.cxjs_max(input), result); }); } - // Scalar (non-array, non-object) inputs hit the else branch and are - // returned verbatim, with no comparison performed. + // Scalar are returned as is. for (const [ input, result ] of [ // Input Result [ 5, 5 ], @@ -131,4 +133,91 @@ describe('cxjs_max', () => assert.equal(env.cxjs_max(input), result); }); } + + // Lists with NaNs. + for (const [ input, result ] of [ + // Input Result + [ [5, NaN, 3], 5 ], // 5 retained, NaN never beats it, then 3<5 + [ [NaN, NaN, 5], 5 ], // NaN survives until a real value replaces it + [ [1, NaN, 2, NaN, 3], 3 ], // running max walks 1 -> 2 -> 3 across the NaNs + [ [3, NaN, 5, NaN, 1], 5 ], + ]) { + test(`cxjs_max(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_max(input), result); + }); + } + + // null comparisons to 1 and -1. + for (const [ input, result ] of [ + // Input Result + [ [1, null], 1 ], // 1 > null(0), so 1 wins + [ [null, 1], 1 ], + [ [-1, null], null ], // -1 not > null(0), so null wins and is preserved + [ [null, -1], null ], + ]) { + test(`cxjs_max(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_max(input), result); + }); + } + + // Misc edge cases. + for (const [ input, result ] of [ + // Input Result + [ [Infinity, -Infinity], Infinity ], + [ [-Infinity, Infinity], Infinity ], + [ [-0, 0], -0 ], // -0 < 0 is false, so -0 is kept + [ [0, -0], 0 ], // 0 < -0 is false, so 0 is kept + [ [-0], -0 ], + [ [true, false], true ], // true(1) > false(0) + [ [false, true], true ], + [ [2, true], 2 ], // 2 > true(1), returned as number + [ [0, false], 0 ], // false(0) not > 0, so 0 kept + ]) { + test(`cxjs_max(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_max(input), result); + }); + } + + // Numbers mixed with numeric strings. + for (const [ input, result ] of [ + // Input Result + [ [2, '10'], '10' ], // numeric compare: 10 > 2, kept as a string + [ ['10', 2], '10' ], + [ [5, '3', 4], 5 ], // 5 is numerically greatest + ]) { + test(`cxjs_max(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_max(input), result); + }); + } + + // Sparse arrays. + for (const [ input, result ] of [ + // Input Result + [ [1, , 3], 3 ], // 3 > 1, the hole is skipped + [ [, , 5], 5 ], + ]) { + test(`cxjs_max(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_max(input), result); + }); + } + + test('cxjs_max(array with extra non-index prop) = 3', () => + { + const a = [1, 2, 3]; + a.foo = 99; // ignored: numeric-index loop only + assert.equal(env.cxjs_max(a), 3); + }); + + // Value on the prototype participates in the comparison. + test('cxjs_max(object with inherited enumerable prop) = 3', () => + { + function Proto() { this.a = 3; } + Proto.prototype.b = 1; + assert.equal(env.cxjs_max(new Proto()), 3); + }); }); diff --git a/centrallix-os/sys/js/tests/cxjs_min.test.js b/centrallix-os/sys/js/tests/cxjs_min.test.js index b865f3ea2..59c256091 100644 --- a/centrallix-os/sys/js/tests/cxjs_min.test.js +++ b/centrallix-os/sys/js/tests/cxjs_min.test.js @@ -14,15 +14,18 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); -// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which -// would make distinct edge-case rows share a test name; fmt renders those -// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +// JSON.stringify collapses NaN/Infinity to "null", omits undefined, and renders +// -0 as "0", which would make distinct edge-case rows share a test name; fmt +// renders those values verbatim, distinguishes -0 from 0, and otherwise matches +// JSON.stringify, so names stay unique. function fmt(v) { if (Array.isArray(v)) return '[' + v.map(fmt).join(',') + ']'; if (v !== null && typeof v === 'object') return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; if (typeof v === 'number' || v === undefined) return String(v); return JSON.stringify(v); @@ -47,7 +50,7 @@ describe('cxjs_min', () => [ [undefined], undefined ], [ [undefined, 0], 0 ], ]) { - test(`cxjs_min(${JSON.stringify(input)}) = ${result}`, () => + test(`cxjs_min(${fmt(input)}) = ${fmt(result)}`, () => { assert.equal(env.cxjs_min(input), result); }); @@ -70,14 +73,13 @@ describe('cxjs_min', () => [ { D: undefined }, undefined ], [ { E: undefined, F: 0 }, 0 ], ]) { - test(`cxjs_min(${JSON.stringify(input)}) = ${result}`, () => + test(`cxjs_min(${fmt(input)}) = ${fmt(result)}`, () => { assert.equal(env.cxjs_min(input), result); }); } - // Scalar (non-array, non-object) inputs hit the else branch and are - // returned verbatim, with no comparison performed. + // Scalar (non-array, non-object) inputs. for (const [ input, result ] of [ // Input Result [ 5, 5 ], @@ -96,9 +98,7 @@ describe('cxjs_min', () => }); } - // null and NaN within a collection are handled specially: a NaN running - // minimum is discarded via the isNaN(lowest) guard, and null compares as 0 - // (so it wins as the minimum) but is preserved in the result. + // null and NaN list edge cases. for (const [ input, result ] of [ // Input Result [ [5], 5 ], // single element @@ -121,19 +121,105 @@ describe('cxjs_min', () => }); } - // Non-numeric strings make isNaN(lowest) true on every pass, so the - // comparison is bypassed and the last element is always returned -- min - // does not actually order non-numeric strings. Numeric strings, by - // contrast, are compared lexically (not by numeric value). + // Non-numeric strings. for (const [ input, result ] of [ // Input Result - [ ['b', 'a', 'c'], 'c' ], // last element wins, not 'a' + [ ['b', 'a', 'c'], 'c' ], // last element wins, not 'a' [ ['apple', 'banana'], 'banana' ], - [ ['10', '9', '100'], '10' ], // lexical compare: '10' < '9' < '100' + [ ['10', '9', '100'], '10' ], // lexical compare: '10' < '9' < '100' + ]) { + test(`cxjs_min(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_min(input), result); + }); + } + + // Lists with NaNs. + for (const [ input, result ] of [ + // Input Result + [ [5, NaN, 3], 3 ], + [ [NaN, NaN, 5], 5 ], + [ [3, NaN, 1], 1 ], + [ [1, NaN, 2, NaN, 3], 1 ], + [ [5, NaN, 3, NaN, 1], 1 ], + ]) { + test(`cxjs_min(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_min(input), result); + }); + } + + // null compares as 0. + for (const [ input, result ] of [ + // Input Result + [ [-1, null], -1 ], // -1 < null(0), so -1 wins + [ [null, -1], -1 ], + [ [1, null], null ], // null(0) < 1, so null wins and is preserved + [ [null, 1], null ], + ]) { + test(`cxjs_min(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_min(input), result); + }); + } + + // Misc edge cases. + for (const [ input, result ] of [ + // Input Result + [ [Infinity, -Infinity], -Infinity ], + [ [-Infinity, Infinity], -Infinity ], + [ [-0, 0], -0 ], // -0 > 0 is false, so -0 is kept + [ [0, -0], 0 ], // 0 > -0 is false, so 0 is kept + [ [-0], -0 ], + [ [true, false], false ], // false(0) < true(1) + [ [false, true], false ], + [ [2, true], true ], // true(1) < 2, returned as boolean + [ [0, false], 0 ], // false(0) not < 0, so 0 kept + ]) { + test(`cxjs_min(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_min(input), result); + }); + } + + // Numbers mixed with numeric strings. + for (const [ input, result ] of [ + // Input Result + [ [2, '10'], 2 ], // numeric compare: 2 < 10 + [ ['10', 2], 2 ], + [ [5, '3', 4], '3' ], // '3' is numerically least, kept as a string ]) { test(`cxjs_min(${fmt(input)}) = ${fmt(result)}`, () => { assert.equal(env.cxjs_min(input), result); }); } + + // Sparse arrays. + for (const [ input, result ] of [ + // Input Result + [ [1, , 3], 1 ], // 1 < 3, the hole is skipped + [ [, , 5], 5 ], + ]) { + test(`cxjs_min(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_min(input), result); + }); + } + + test('cxjs_min(array with extra non-index prop) = 1', () => + { + const a = [1, 2, 3]; + a.foo = 99; // ignored: numeric-index loop only + assert.equal(env.cxjs_min(a), 1); + }); + + // for...in over an object visits inherited enumerable properties too, so a + // value on the prototype participates in the comparison. + test('cxjs_min(object with inherited enumerable prop) = 1', () => + { + function Proto() { this.a = 3; } + Proto.prototype.b = 1; + assert.equal(env.cxjs_min(new Proto()), 1); + }); }); diff --git a/centrallix-os/sys/js/tests/cxjs_minus.test.js b/centrallix-os/sys/js/tests/cxjs_minus.test.js index d883f23a5..b86dd5177 100644 --- a/centrallix-os/sys/js/tests/cxjs_minus.test.js +++ b/centrallix-os/sys/js/tests/cxjs_minus.test.js @@ -14,6 +14,22 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); +// JSON.stringify collapses NaN/Infinity to "null", drops undefined, and +// renders -0 as "0", which would make distinct edge-case rows share a test +// name; fmt renders those verbatim so names stay unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + describe('cxjs_minus', () => { // Numeric operands subtract. NaN propagates. @@ -34,8 +50,12 @@ describe('cxjs_minus', () => [ 1, NaN, NaN ], [ true, 1, 0 ], // Booleans are not strings: true -> 1. [ false, false, 0 ], // false -> 0. + [ 5, 0, 5 ], + [ 0, 5, -5 ], + [ true, false, 1 ], // 1 - 0. + [ 5e-324, 5e-324, 0 ], // smallest denormals cancel to 0. ]) { - test(`cxjs_minus(${JSON.stringify(a)}, ${JSON.stringify(b)}) = ${JSON.stringify(result)}`, () => + test(`cxjs_minus(${fmt(a)}, ${fmt(b)}) = ${fmt(result)}`, () => { assert.equal(env.cxjs_minus(a, b), result); }); @@ -49,7 +69,7 @@ describe('cxjs_minus', () => [ undefined, 5 ], [ 5, undefined ], ]) { - test(`cxjs_minus(${JSON.stringify(a)}, ${JSON.stringify(b)}) = null`, () => + test(`cxjs_minus(${fmt(a)}, ${fmt(b)}) = null`, () => { assert.equal(env.cxjs_minus(a, b), null); }); @@ -70,13 +90,36 @@ describe('cxjs_minus', () => [ 100, '0', '10' ], // Coercion: 100 -> '100'. [ '5', 5, '' ], [ 5, '5', '' ], + // Only a suffix is stripped: the match must sit at the very end. + // a b Result [ 'abcabc', 'bc', 'abca' ], // trailing 'bc' removed (lastIndexOf is at the end). [ 'aXbXc', 'X', 'aXbXc' ], // 'X' occurs, but not at the end: unchanged. + + // Overlapping/repeated suffix. + // a b Result + [ 'aaa', 'aa', 'a' ], // lastIndexOf('aa') = 1 = len-2: strips one. + [ 'aaaa', 'aa', 'aa' ], // lastIndexOf('aa') = 2 = len-2: strips one. + [ '', 'abc', '' ], // empty a, longer b: lastIndexOf = -1, but -1 != 0-3, so a unchanged (''). + + // Coercion by only one string. + // a b Result + [ true, 'e', 'tru' ], + [ 'true', true, '' ], + [ '12', 2, '1' ], + [ NaN, 'N', 'Na' ], ]) { - test(`cxjs_minus(${JSON.stringify(a)}, ${JSON.stringify(b)}) = ${JSON.stringify(result)}`, () => + test(`cxjs_minus(${fmt(a)}, ${fmt(b)}) = ${fmt(result)}`, () => { assert.equal(env.cxjs_minus(a, b), result); }); } + + // Signed-zero: subtracting equal values gives +0, even -0 - -0. + // JSON.stringify(-0) is "0", so name these explicitly (Object.is + // distinguishes -0 from +0 in assert/strict). + test('cxjs_minus(-0, -0) = +0', () => + { + assert.equal(env.cxjs_minus(-0, -0), 0); + }); }); diff --git a/centrallix-os/sys/js/tests/cxjs_plus.test.js b/centrallix-os/sys/js/tests/cxjs_plus.test.js index 7753746b4..5c474ba65 100644 --- a/centrallix-os/sys/js/tests/cxjs_plus.test.js +++ b/centrallix-os/sys/js/tests/cxjs_plus.test.js @@ -68,10 +68,34 @@ describe('cxjs_plus', () => [ false, false, 0 ], [ true, 'x', 'truex' ], [ 'x', true, 'xtrue' ], + + // Object/array handling. + [ [1], 2, '12' ], + [ 2, [1], '21' ], + [ [1, 2], [3], '1,23' ], + [ [], [], '' ], // both arrays -> '' + '' = '' + [ {}, 1, '[object Object]1' ], + [ 1, {}, '1[object Object]' ], + [ [1], 'x', '1x' ], // string branch: String([1]) = '1' + [ false, 0, 0 ], // both numeric: 0 + 0 = 0 + [ '', 0, '0' ], // empty-string branch beats numeric 0 + + // Large magnitudes overflow to Infinity rather than wrapping. + [ 1e308, 1e308, Infinity ], ]) { test(`cxjs_plus(${fmt(a)}, ${fmt(b)}) = ${fmt(result)}`, () => { assert.equal(env.cxjs_plus(a, b), result); }); } + + // Signed-zero edge cases. + test('cxjs_plus(-0, -0) = -0', () => + { + assert.equal(env.cxjs_plus(-0, -0), -0); + }); + test('cxjs_plus(-0, 0) = +0', () => + { + assert.equal(env.cxjs_plus(-0, 0), 0); + }); }); diff --git a/centrallix-os/sys/js/tests/cxjs_power.test.js b/centrallix-os/sys/js/tests/cxjs_power.test.js index 4095e14a9..973828f96 100644 --- a/centrallix-os/sys/js/tests/cxjs_power.test.js +++ b/centrallix-os/sys/js/tests/cxjs_power.test.js @@ -14,15 +14,18 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); -// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which -// would make distinct edge-case rows share a test name; fmt renders those -// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +// JSON.stringify collapses NaN/Infinity to "null", omits undefined, and renders +// -0 as "0", which would make distinct edge-case rows share a test name; fmt +// renders those values verbatim (and recurses into arrays/objects) so names +// stay unique. function fmt(v) { if (Array.isArray(v)) return '[' + v.map(fmt).join(',') + ']'; if (v !== null && typeof v === 'object') return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; if (typeof v === 'number' || v === undefined) return String(v); return JSON.stringify(v); @@ -100,12 +103,46 @@ describe('cxjs_power', () => [ 0.5, Infinity, 0 ], [ 0, Infinity, 0 ], [ 0, -1, Infinity ], - [ -0, -1, -Infinity ], + [ 0, -2, Infinity ], [ 1, Infinity, NaN ], [ -1, Infinity, NaN ], + [ 1, -Infinity, NaN ], + [ -1, -Infinity, NaN ], + [ 0.5, -Infinity, Infinity ], // |base|<1 with -Infinity exp blows up [ -Infinity, 2, Infinity ], [ -Infinity, 3, -Infinity ], [ -Infinity, -1, -0 ], + [ -Infinity, -2, 0 ], // even negative exp gives +0 + ]) { + test(`cxjs_power(${fmt(n)}, ${fmt(p)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_power(n, p), result); + }); + } + + // A base of -0 keeps the sign only for odd positive exponents; negative + // exponents give a signed infinity (odd -> -Infinity, even -> +Infinity). + for (const [ n, p, result ] of [ + // n p Result + [ -0, 2, 0 ], // even exponent -> +0 + [ -0, 3, -0 ], // odd exponent keeps the sign + [ -0, -1, -Infinity ], // odd negative exponent + [ -0, -2, Infinity ], // even negative exponent + ]) { + test(`cxjs_power(${fmt(n)}, ${fmt(p)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_power(n, p), result); + }); + } + + // Overflow saturates to Infinity and a fractional exponent on a negative + // base has no real value (NaN), regardless of the base's magnitude. + for (const [ n, p, result ] of [ + // n p Result + [ 2, 1024, Infinity ], // overflows to Infinity + [ Number.MAX_VALUE, 2, Infinity ], + [ 2, -1074, 5e-324 ], // smallest positive subnormal + [ -0.5, 0.5, NaN ], // negative fractional base -> NaN ]) { test(`cxjs_power(${fmt(n)}, ${fmt(p)}) = ${fmt(result)}`, () => { @@ -115,11 +152,12 @@ describe('cxjs_power', () => // NaN propagates. for (const [ n, p, result ] of [ - // n p Result - [ NaN, 2, NaN ], - [ 2, NaN, NaN ], - [ NaN, NaN, NaN ], - [ NaN, 0, 1 ], // Exception: 0 exponent yields 1. + // n p Result + [ NaN, 2, NaN ], + [ 2, NaN, NaN ], + [ NaN, NaN, NaN ], + [ NaN, Infinity, NaN ], + [ NaN, 0, 1 ], // Exception: 0 exponent yields 1. ]) { test(`cxjs_power(${fmt(n)}, ${fmt(p)}) = ${fmt(result)}`, () => { diff --git a/centrallix-os/sys/js/tests/cxjs_quote.test.js b/centrallix-os/sys/js/tests/cxjs_quote.test.js index 076729c76..c581b91fb 100644 --- a/centrallix-os/sys/js/tests/cxjs_quote.test.js +++ b/centrallix-os/sys/js/tests/cxjs_quote.test.js @@ -14,6 +14,22 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); +// JSON.stringify collapses NaN/Infinity to "null", drops undefined, and +// renders -0 as "0", which would make distinct edge-case rows share a test +// name; fmt renders those verbatim so names stay unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + // cxjs_quote() wraps a value in double quotes, escaping any embedded // double quote as \". Backslashes are NOT escaped, and non-string // inputs are coerced via String() first. @@ -35,12 +51,25 @@ describe('cxjs_quote', () => [ true, '"true"' ], // boolean coerced [ null, '"null"' ], // null coerced [ undefined, '"undefined"' ], // undefined coerced + [ Infinity, '"Infinity"' ], // Infinity coerced + [ -Infinity, '"-Infinity"' ], // -Infinity coerced [ NaN, '"NaN"' ], // NaN coerced [ [1, 2], '"1,2"' ], // array coerced (with no spaces) [ {}, '"[object Object]"' ], // plain object coerced [ { a: 1 }, '"[object Object]"' ], // object contents are irrelevant to String() + [ '\\\\', '"\\\\"' ], // two backslashes both pass as is (unescaped) + [ 'a"b"c', '"a\\"b\\"c"' ], // every embedded quote is escaped + [ '\r', '"\r"' ], // carriage return preserved + [ '๐Ÿ˜€', '"๐Ÿ˜€"' ], // multi-byte char passes as is + [ 0, '"0"' ], // numeric zero coerced + [ -0, '"0"' ], // negative zero renders as "0" + [ false, '"false"' ], // boolean false coerced + [ [], '""' ], // empty array coerces to "" + [ [null], '""' ], // null element renders as "" inside array + [ ['a', null], '"a,"' ], // null element is empty between commas + [ ['a"b'], '"a\\"b"' ], // quote inside array element still escaped ]) { - test(`cxjs_quote(${JSON.stringify(input)}) = ${JSON.stringify(result)}`, () => + test(`cxjs_quote(${fmt(input)}) = ${fmt(result)}`, () => { assert.equal(env.cxjs_quote(input), result); }); diff --git a/centrallix-os/sys/js/tests/cxjs_radians.test.js b/centrallix-os/sys/js/tests/cxjs_radians.test.js index 64708c79c..300abf221 100644 --- a/centrallix-os/sys/js/tests/cxjs_radians.test.js +++ b/centrallix-os/sys/js/tests/cxjs_radians.test.js @@ -14,15 +14,18 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); -// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which -// would make distinct edge-case rows share a test name; fmt renders those -// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +// JSON.stringify collapses NaN/Infinity to "null", omits undefined, and renders +// -0 as "0", which would make distinct edge-case rows share a test name; fmt +// renders those values verbatim (and -0 distinctly from 0) while otherwise +// matching JSON.stringify, so names stay unique. function fmt(v) { if (Array.isArray(v)) return '[' + v.map(fmt).join(',') + ']'; if (v !== null && typeof v === 'object') return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; if (typeof v === 'number' || v === undefined) return String(v); return JSON.stringify(v); @@ -39,6 +42,7 @@ describe('cxjs_radians', () => [ 360, Math.PI * 2 ], [ 270, Math.PI * 1.5 ], [ -180, -Math.PI ], + [ -90, -Math.PI / 2 ], [ -0, -0 ], // sign preserved [ Infinity, Infinity ], [ -Infinity, -Infinity ], @@ -50,6 +54,25 @@ describe('cxjs_radians', () => }); } + // Extreme magnitudes follow the exact source expression: a very large value + // overflows to Infinity, a very small one stays finite (no underflow to 0). + for (const input of [ 1e200, Number.MAX_VALUE, 1e-200, -90, Number.MIN_VALUE ]) + { + test(`cxjs_radians(${fmt(input)}) = (degrees*PI)/180`, () => + { + assert.equal(env.cxjs_radians(input), (input * Math.PI) / 180.0); + }); + } + + // radians(degrees(x)) = x (within float tolerance). + for (const x of [ 1, Math.PI / 4, 2 * Math.PI, -1.5 ]) + { + test(`cxjs_radians(cxjs_degrees(${fmt(x)})) ~= ${fmt(x)}`, () => + { + assert.ok(Math.abs(env.cxjs_radians(env.cxjs_degrees(x)) - x) < 1e-9); + }); + } + // Only null and undefined yield null (no coercion). for (const input of [ null, undefined ]) { @@ -59,11 +82,12 @@ describe('cxjs_radians', () => }); } - // Non-number inputs are coerced to numbers by the arithmetic. + // Non-number inputs are coerced to numbers. // Those that coerce to NaN yield NaN (null is never used). for (const [ input, result ] of [ // Input Result [ '180', Math.PI ], + [ '-90', -Math.PI / 2 ], // negative string coerces to -90 [ '', 0 ], // empty string coerces to 0 [ 'foo', NaN ], // non-numeric string coerces to NaN [ true, Math.PI / 180 ], diff --git a/centrallix-os/sys/js/tests/cxjs_rand.test.js b/centrallix-os/sys/js/tests/cxjs_rand.test.js index 6a38df0e3..8ef1f493c 100644 --- a/centrallix-os/sys/js/tests/cxjs_rand.test.js +++ b/centrallix-os/sys/js/tests/cxjs_rand.test.js @@ -61,11 +61,10 @@ describe('cxjs_rand', () => }); // A fixed seed is ignored, so it does not pin the output to one value. - // (Warnings are suppressed here; warn behavior is checked separately.) test('ignores the seed (output stays non-deterministic)', () => { const seen = new Set(); - captureWarningCount(() => + captureWarningCount(() => // Warnings suppressed (tested later). { for (let i = 0; i < 1000; i++) seen.add(env.cxjs_rand(42)); }); @@ -81,6 +80,7 @@ describe('cxjs_rand', () => let result; const warnings = captureWarningCount(() => { result = env.cxjs_rand(seed); }); assert.equal(warnings, 1); + assert.equal(typeof result, 'number'); assert.ok(result >= 0 && result < 1, `${result} not in [0,1)`); }); } diff --git a/centrallix-os/sys/js/tests/cxjs_replace.test.js b/centrallix-os/sys/js/tests/cxjs_replace.test.js index d6f950b3e..4d0993f7a 100644 --- a/centrallix-os/sys/js/tests/cxjs_replace.test.js +++ b/centrallix-os/sys/js/tests/cxjs_replace.test.js @@ -20,42 +20,68 @@ describe('cxjs_replace', () => { for (const [ str, search, replace, result ] of [ // Str Search Replace Result - [ 'hello world', 'o', '0', 'hell0 w0rld' ], // replaces every occurrence - [ 'hello world', 'h', 'J', 'Jello world' ], // first character - [ 'hello world', 'd', '!', 'hello worl!' ], // last character - [ 'hello world', ' ', '_', 'hello_world' ], // whitespace - [ 'hello world', 'l', ' ', 'he o wor d' ], - [ 'hello world', 'l', 'l', 'hello world' ], // identical result - [ 'hello world', ' ', ' ', 'hello world' ], - [ 'hello world', 'c', 'c', 'hello world' ], - [ 'aaa', 'a', 'b', 'bbb' ], - [ 'aaa', 'aa', 'b', 'ba' ], // non-overlapping, left to right - [ 'hello', 'z', 'x', 'hello' ], // absent - [ 'hello', 'l', 'LL', 'heLLLLo' ], // replacement longer than match - [ 'Hello', 'h', 'x', 'Hello' ], // case-sensitive: no match - [ 'hello', 'l', '', 'heo' ], // empty replace deletes matches + [ 'hello world', 'o', '0', 'hell0 w0rld' ], // replaces every occurrence + [ 'hello world', 'h', 'J', 'Jello world' ], // first character + [ 'hello world', 'd', '!', 'hello worl!' ], // last character + [ 'hello world', ' ', '_', 'hello_world' ], // whitespace + [ 'hello world', 'l', ' ', 'he o wor d' ], + [ 'hello world', 'l', 'l', 'hello world' ], // identical result + [ 'hello world', ' ', ' ', 'hello world' ], + [ 'hello world', 'c', 'c', 'hello world' ], + [ 'aaa', 'a', 'b', 'bbb' ], + [ 'aaa', 'aa', 'b', 'ba' ], // non-overlapping, left to right + [ 'hello', 'z', 'x', 'hello' ], // absent + [ 'hello', 'l', 'LL', 'heLLLLo' ], // replacement longer than match + [ 'Hello', 'h', 'x', 'Hello' ], // case-sensitive: no match + [ 'hello', 'l', '', 'heo' ], // empty replace deletes matches + + // Matching is left-to-right and non-overlapping. + [ 'aaaa', 'aa', 'X', 'XX' ], + [ 'abab', 'ab', 'X', 'XX' ], + [ 'aaaaa', 'aaa', 'X', 'Xaa' ], - // search is matched literally (regex metacharacters have no special meaning). - [ '1.2.3', '.', ',', '1,2,3' ], - [ 'a|b|c', '|', '-', 'a-b-c' ], - [ '*x*', '*', '#', '#x#' ], - [ 'a+b', '+', '-', 'a-b' ], - [ 'a?b', '?', '!', 'a!b' ], - [ 'a^b', '^', '~', 'a~b' ], - [ '$5', '$', 'USD', 'USD5' ], - [ '(x)', '(', '[', '[x)' ], - [ '{x}', '{', '<', '"") + [ [1,2,3], ',', '-', '1-2-3' ], // array str coerces + + // Multi-byte / Unicode handling operates on UTF-16 code units. + [ 'cafรฉ', 'รฉ', 'X', 'cafX' ], // single composed code point (U+00E9) + [ 'x๐Ÿ˜€y', '๐Ÿ˜€', 'Z', 'xZy' ], // full surrogate pair matched + [ 'a๐Ÿ˜€b', '๐Ÿ˜€', 'X', 'aXb' ], + [ '๐Ÿ˜€', '', '-', '-\uD83D-\uDE00-' ], // empty search splits the pair's code units + + // Embedded newlines are ordinary characters; a literal '.' does not match them. + [ 'a\nb\nc', '\n', '|', 'a|b|c' ], // newline searched literally + [ 'a\nb', '.', 'X', 'a\nb' ], // '.' is literal, matches nothing here ]) { test(`cxjs_replace(` + `${JSON.stringify(str)}, ` @@ -95,6 +121,11 @@ describe('cxjs_replace', () => [ 'cat', 'a', '$$', 'c$t' ], [ 'cat', 'a', '$`', 'cct' ], // text preceding the match [ 'cat', 'a', "$'", 'ctt' ], // text following the match + [ 'cat', 'a', '$1', 'c$1t' ], // no capture group: $1 left verbatim + [ 'cat', 'a', '$0', 'c$0t' ], // $0 is not special: left verbatim + [ 'cat', 'a', '$', 'c$t' ], // no named groups: $ left verbatim + [ 'cat', 'a', '$$&', 'c$&t' ], // escaped $ then literal &: yields "$&" + [ 'cat', 'a', '$$1', 'c$1t' ], // escaped $ then "1": yields "$1" ]) { test(`cxjs_replace(` + `${JSON.stringify(str)}, ` diff --git a/centrallix-os/sys/js/tests/cxjs_replicate.test.js b/centrallix-os/sys/js/tests/cxjs_replicate.test.js index 215d12de1..526d51fab 100644 --- a/centrallix-os/sys/js/tests/cxjs_replicate.test.js +++ b/centrallix-os/sys/js/tests/cxjs_replicate.test.js @@ -14,35 +14,64 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); +// String(n)/JSON.stringify collapse distinct values into the same test name +// (e.g. the number 3 and the string "3", or NaN/Infinity/-0); fmt renders them +// verbatim (with quotes for strings, "-0" for negative zero) so names stay unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + // replicate concatenates n copies of s, returning null if s or n is // null/undefined or if n is negative. describe('cxjs_replicate', () => { for (const [ s, n, result ] of [ // Str N Result - [ 'ab', 3, 'ababab' ], - [ 'x', 1, 'x' ], // single copy - [ 'ab', 0, '' ], // zero copies - [ '', 5, '' ], // empty str stays empty - [ '', 0, '' ], - [ 'a b', 2, 'a ba b' ], // whitespace preserved - [ 'abc', 3, 'abcabcabc' ], + [ 'ab', 3, 'ababab' ], + [ 'x', 1, 'x' ], // single copy + [ 'ab', 0, '' ], // zero copies + [ '', 5, '' ], // empty str stays empty + [ '', 0, '' ], + [ 'a b', 2, 'a ba b' ], // whitespace preserved + [ 'abc', 3, 'abcabcabc' ], // n is floored toward zero's neighbor, so fractions truncate down. - [ 'ab', 3.9, 'ababab' ], - [ 'ab', 0.5, '' ], // floors to 0 - [ 'ab', 2.0, 'abab' ], + [ 'ab', 3.9, 'ababab' ], + [ 'ab', 0.5, '' ], // floors to 0 + [ 'ab', 2.0, 'abab' ], + + // n is coerced to a number before flooring. + [ 'ab', '3', 'ababab' ], // numeric string + [ 'ab', '2.9', 'abab' ], // numeric string floored + [ 'ab', '', '' ], // "" -> Number 0 + [ 'ab', ' ', '' ], // blank -> Number 0 + [ 'ab', true, 'ab' ], // true -> 1 copy + [ 'ab', false, '' ], // false -> 0 copies // Non-string s is coerced to string. - [ 5, 3, '555' ], - [ true, 2, 'truetrue' ], - [ 0, 2, '00' ], + [ 5, 3, '555' ], + [ true, 2, 'truetrue' ], + [ 0, 2, '00' ], + [ false, 2, 'falsefalse' ], // boolean false coerced + [ NaN, 2, 'NaNNaN' ], // NaN value coerced to "NaN" + [ [1,2], 2, '1,21,2' ], // array via Array.toString + [ {}, 2, '[object Object][object Object]' ], // object via Object.toString + [ '๐Ÿ˜€', 3, '๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€' ], // multi-byte s repeated whole // NaN n yields "". - [ 'ab', NaN, '' ], - [ 'ab', 'abc', '' ], // String->NaN + [ 'ab', NaN, '' ], + [ 'ab', 'abc', '' ], // String->NaN ]) { - test(`cxjs_replicate(${JSON.stringify(s)}, ${String(n)}) = ${JSON.stringify(result)}`, () => + test(`cxjs_replicate(${fmt(s)}, ${fmt(n)}) = ${fmt(result)}`, () => { assert.equal(env.cxjs_replicate(s, n), result); }); @@ -56,7 +85,7 @@ describe('cxjs_replicate', () => [ 'x', undefined ], [ null, null ], ]) { - test(`cxjs_replicate(${s}, ${n}) = null`, () => + test(`cxjs_replicate(${fmt(s)}, ${fmt(n)}) = null`, () => { assert.equal(env.cxjs_replicate(s, n), null); }); @@ -69,16 +98,22 @@ describe('cxjs_replicate', () => [ 'ab', -0.1 ], // Floors away from 0 (-0.1 -> -1), resulting in null. [ 'ab', -Infinity ], ]) { - test(`cxjs_replicate(${JSON.stringify(s)}, ${String(n)}) = null`, () => + test(`cxjs_replicate(${fmt(s)}, ${fmt(n)}) = null`, () => { assert.equal(env.cxjs_replicate(s, n), null); }); } + // Just below the cap: 254.9 floors to 254 copies (one short of the cap). + test("cxjs_replicate('a', 254.9) = 254 copies", () => + { + assert.equal(env.cxjs_replicate('a', 254.9), 'a'.repeat(254)); + }); + // n is capped at 255 copies. - for (const n of [ 255, 256, 300, Infinity ]) + for (const n of [ 255, 255.9, 256, 300, Infinity ]) { - test(`cxjs_replicate('ab', ${String(n)}) caps at 255 copies`, () => + test(`cxjs_replicate('ab', ${fmt(n)}) caps at 255 copies`, () => { assert.equal(env.cxjs_replicate('ab', n), 'ab'.repeat(255)); }); diff --git a/centrallix-os/sys/js/tests/cxjs_reverse.test.js b/centrallix-os/sys/js/tests/cxjs_reverse.test.js index ffd567524..07310e311 100644 --- a/centrallix-os/sys/js/tests/cxjs_reverse.test.js +++ b/centrallix-os/sys/js/tests/cxjs_reverse.test.js @@ -14,11 +14,18 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); -// JSON.stringify renders NaN/Infinity as "null" and a standalone undefined as -// undefined (not a string), which would give distinct edge-case rows the same -// (or a broken) test name; fmt renders those verbatim so names stay unique. +// JSON.stringify renders NaN/Infinity as "null", a standalone undefined as +// undefined (not a string), and -0 as "0", which would give distinct edge-case +// rows the same (or a broken) test name; fmt renders those verbatim (and +// recurses into arrays/objects) so names stay unique. function fmt(v) { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; if (typeof v === 'number' || v === undefined) return String(v); return JSON.stringify(v); @@ -38,6 +45,9 @@ describe('cxjs_reverse', () => [ ' \tx ', ' x\t ' ], // surrounding whitespace preserved [ '123!?', '?!321' ], // digits/symbols reverse like any char [ 'ร รฉรฎ', 'รฎรฉร ' ], // non-ASCII letters: a/e/i w/ accents + [ 'a"b', 'b"a' ], // embedded double quote reverses like any char + [ 'a\\b', 'b\\a' ], // embedded backslash reverses like any char + [ 'aabb', 'bbaa' ], // repeated chars ]) { test(`cxjs_reverse(${fmt(input)}) = ${fmt(result)}`, () => { @@ -53,13 +63,19 @@ describe('cxjs_reverse', () => [ undefined, null ], [ 123, '321' ], [ 1.5, '5.1' ], + [ -12, '21-' ], [ true, 'eurt' ], [ false, 'eslaf' ], [ NaN, 'NaN' ], // 'NaN' reverses to itself [ Infinity, 'ytinifnI' ], [ -Infinity, 'ytinifnI-' ], [ [], '' ], // empty array coerces to '' + [ ['a'], 'a' ], [ ['a', 'b'], 'b,a' ], + [ 0, '0' ], // numeric zero coerces to '0' + [ -0, '0' ], // negative zero renders as '0' + [ [1, 2, 3], '3,2,1' ], // commas reverse with digits + [ {}, ']tcejbO tcejbo['], // object -> '[object Object]' reversed ]) { test(`cxjs_reverse(${fmt(input)}) = ${fmt(result)}`, () => { @@ -69,12 +85,16 @@ describe('cxjs_reverse', () => // Reversal walks UTF-16 code units, not whole characters, so surrogate // pairs and combining marks get split apart and reordered. Escapes are - // used directly so these cases do not depend on the file's encoding. + // used directly so these cases do not depend on file encodings. for (const [ name, input, result ] of [ - // emoji (one code point, two code units) splits into swapped halves + // emoji (one code point, two code units) splits into swapped halves. [ 'surrogate pair split', '\uD83D\uDE00', '\uDE00\uD83D' ], [ 'pair split before char', '\uD83D\uDE00a', 'a\uDE00\uD83D' ], - // 'e' + combining acute reverses to combining acute + 'e' + // each emoji's two halves swap in place, so the run is fully reordered. + [ 'two emoji halves reorder', '\uD83D\uDE00\uD83D\uDE00', '\uDE00\uD83D\uDE00\uD83D' ], + // char before an emoji ends up after the swapped halves. + [ 'char before pair split', 'a\uD83D\uDE00b', 'b\uDE00\uD83Da' ], + // 'e' + combining acute reverses to combining acute + 'e'. [ 'combining mark reorder', 'e\u0301', '\u0301e' ], ]) { test(`cxjs_reverse: ${name}`, () => diff --git a/centrallix-os/sys/js/tests/cxjs_right.test.js b/centrallix-os/sys/js/tests/cxjs_right.test.js index 2e45d99d6..8394b7a12 100644 --- a/centrallix-os/sys/js/tests/cxjs_right.test.js +++ b/centrallix-os/sys/js/tests/cxjs_right.test.js @@ -14,42 +14,84 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); +// JSON.stringify collapses NaN/Infinity to "null", drops undefined, and +// renders -0 as "0", which would make distinct edge-case rows share a test +// name; fmt renders those verbatim so names stay unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + describe('cxjs_right', () => { for (const [ s, l, result ] of [ // String Length Result - [ 'hello', 2, 'lo' ], - [ 'hello', 1, 'o' ], - [ 'hello', 5, 'hello' ], // l equals length: whole string - [ 'hello', 10, 'hello' ], // l exceeds length: whole string - [ 'hello', 0, '' ], // zero length: empty - [ 'hello', -2, '' ], // negative length: empty - [ 'hello', 2.5, 'llo' ], // fractional length truncated to 2 - [ 'hello', NaN, 'hello' ], // NaN != null, so substr(NaN) -> substr(0) - [ '', 3, '' ], // empty string stays empty - [ '', 0, '' ], - [ 'x', 1, 'x' ], // single char - [ ' ab ', 2, 'b ' ], // trailing whitespace preserved - [ ' ab ', 4, ' ab ' ], // preceding whitespace preserved - [ 'abc', null, null ], // null length - [ 'abc', undefined, null ], // undefined length - [ null, 2, null ], // null string - [ undefined, 2, null ], // undefined string - [ null, null, null ], + [ 'hello', 2, 'lo' ], + [ 'hello', 1, 'o' ], + [ 'hello', 5, 'hello' ], // l equals length: whole string + [ 'hello', 10, 'hello' ], // l exceeds length: whole string + [ 'hello', 0, '' ], // zero length: empty + [ 'hello', -2, '' ], // negative length: empty + [ 'hello', 2.5, 'llo' ], // fractional length truncated to 2 + [ 'hello', NaN, 'hello' ], // NaN != null, so substr(NaN) -> substr(0) + [ '', 3, '' ], // empty string stays empty + [ '', 0, '' ], + [ 'x', 1, 'x' ], // single char + [ ' ab ', 2, 'b ' ], // trailing whitespace preserved + [ ' ab ', 4, ' ab ' ], // preceding whitespace preserved + [ 'abc', null, null ], // null length + [ 'abc', undefined, null ], // undefined length + [ null, 2, null ], // null string + [ undefined, 2, null ], // undefined string + [ null, null, null ], + + // l is coerced to a number by the subtraction s.length - l. + [ 'hello', '2', 'lo' ], // numeric string length + [ 'hello', '2.5', 'llo' ], // numeric string truncated + [ 'hello', 'abc', 'hello' ], // non-numeric string -> NaN -> substr(NaN)=substr(0) + [ 'hello', '', '' ], // "" -> Number 0 -> substr(5) -> "" + [ 'hello', true, 'o' ], // true -> 1 + [ 'hello', false, '' ], // false -> 0 -> substr(5) -> "" + [ 'hello', 2.9, 'llo' ], // fractional length: substr start truncated to 2 + [ 'hello', Infinity, 'hello' ], // length-Inf = -Inf -> substr(-Inf)=substr(0) + [ 'hello', -Infinity, '' ], // length+Inf = Inf -> substr past end -> "" + [ 'hello', [2], 'lo' ], // [2] -> Number 2 + [ 'hello', [], '' ], // [] -> Number 0 -> substr(5) -> "" + [ 'hello', {}, 'hello' ], // {} -> NaN -> substr(0) -> whole string + + // s is a string and is sliced by UTF-16 code units, not grapheme clusters. + [ 'a๐Ÿ˜€', 1, '\uDE00' ], // splits the surrogate pair: low half only + [ 'a๐Ÿ˜€', 2, '๐Ÿ˜€' ], // both code units of the pair + [ 'cafรฉ', 1, 'รฉ' ], // composed code point (U+00E9) + [ 'a\nb', 2, '\nb' ], // embedded newline preserved ]) { - test(`cxjs_right(${JSON.stringify(s)}, ${l}) = ${JSON.stringify(result)}`, () => + test(`cxjs_right(${fmt(s)}, ${fmt(l)}) = ${fmt(result)}`, () => { assert.equal(env.cxjs_right(s, l), result); }); } + // A very long string is sliced correctly (substr handles the full length). + test("cxjs_right(<1000 x's>+'abc', 3) = 'abc'", () => + { + assert.equal(env.cxjs_right('x'.repeat(1000) + 'abc', 3), 'abc'); + }); + // Unlike cxjs_substring(), cxjs_right() never coerces s to a string. - for (const s of [ 12345, true ]) + for (const s of [ 12345, true, 0, NaN, [ 1, 2, 3 ], {} ]) { - test(`cxjs_right(${JSON.stringify(s)}, 2) throws (no String coercion)`, () => + test(`cxjs_right(${fmt(s)}, 2) throws (no String coercion)`, () => { - // The error originates in the vm sandbox, so it is an instance of the - // sandbox's TypeError, not this realm's; match on name instead. + // Note: This error originates in the vm sandbox, so it is an instance + // of the sandbox's TypeError, not this realm's, preventing instanceof checks. assert.throws(() => env.cxjs_right(s, 2), (err) => err && err.name === 'TypeError'); }); } diff --git a/centrallix-os/sys/js/tests/cxjs_round.test.js b/centrallix-os/sys/js/tests/cxjs_round.test.js index b5d86f50a..8b2b6b7ac 100644 --- a/centrallix-os/sys/js/tests/cxjs_round.test.js +++ b/centrallix-os/sys/js/tests/cxjs_round.test.js @@ -86,18 +86,50 @@ describe('cxjs_round', () => // Sign of a zero: strictly positive inputs give +0, while zero and // negative inputs give -0. - // Args Result - [ [0.1], 0 ], - [ [0.4], 0 ], - [ [0], -0 ], - [ [-0.1], -0 ], - [ [-0.4], -0 ], + // Args Result + [ [0.1], 0 ], + [ [0.4], 0 ], + [ [0], -0 ], + [ [-0.1], -0 ], + [ [-0.4], -0 ], // Values that are exact halves in decimal are not always representable in // binary, so they might be rounded down rather than up. // Args Result [ [1.005, 2], 1 ], // not 1.01 [ [1.255, 2], 1.25 ], // not 1.26 + [ [2.675, 2], 2.68 ], // stored slightly above 2.675 -> up + [ [0.615, 2], 0.62 ], // stored slightly above 0.615 -> up + [ [8.575, 2], 8.57 ], // stored slightly below 8.575 -> down + + // -0 is preserved across nonzero dec. + // Args Result + [ [-0, 2], -0 ], + [ [-0, 5], -0 ], + [ [0, -2], -0 ], // zero takes the ceil branch -> -0 + + // Extreme magnitudes: scaling can overflow to Infinity, and tiny + // subnormals round to a signed zero. + // Args Result + [ [Number.MAX_VALUE, 2], Infinity ], // n*100 overflows + [ [Number.MAX_VALUE], Number.MAX_VALUE ], + [ [Number.MIN_VALUE], 0 ], // smallest subnormal -> +0 + [ [-Number.MIN_VALUE], -0 ], + + // factor = 10**dec can itself overflow/underflow if |dec| is large. + // Args Result + [ [5, -400], NaN ], + [ [1e300, -400], NaN ], + + // dec coercion. + // Args Result + [ [1.555, '2'], 1.56 ], // dec "2" -> 2 + [ [1.5, ''], 2 ], // dec "" -> 0 + [ [1.5, 'foo'], NaN ], // dec NaN -> factor NaN -> NaN + [ [1.5, NaN], NaN ], + [ [1.5, Infinity], NaN ], // 10**Infinity = Infinity, n*Inf=Inf, /Inf=NaN + [ [1.5, -Infinity], NaN ], // 10**-Infinity = 0 + [ [1.5, -2.5], 0 ], // Math.round(-2.5) = -2 (ties toward +Inf) // n is coerced to a number before rounding (matching JS arithmetic rules). // Args Result diff --git a/centrallix-os/sys/js/tests/cxjs_rtrim.test.js b/centrallix-os/sys/js/tests/cxjs_rtrim.test.js index 22bc277a4..12e46d53d 100644 --- a/centrallix-os/sys/js/tests/cxjs_rtrim.test.js +++ b/centrallix-os/sys/js/tests/cxjs_rtrim.test.js @@ -18,24 +18,44 @@ describe('cxjs_rtrim', () => { for (const [ s, result ] of [ // String Result - [ 'hello ', 'hello' ], // trailing spaces trimmed - [ ' hello', ' hello' ], // leading spaces preserved - [ ' hello ', ' hello' ], // only trailing trimmed - [ 'hello', 'hello' ], // no change - [ 'a b ', 'a b' ], // internal spaces preserved - [ '', '' ], // empty stays empty - [ ' ', '' ], // single space - [ ' ', '' ], // all spaces trimmed - [ 'hello\t', 'hello\t' ], // trailing tab not trimmed - [ 'hello\n', 'hello\n' ], // trailing newline not trimmed - [ 'hello\t ', 'hello\t' ], // space after tab trimmed - [ 'hello \t', 'hello \t' ], // space before tab preserved - [ 42, '42' ], // number coerced to string - [ 0, '0' ], // falsy but not null: coerced, not dropped - [ true, 'true' ], // boolean coerced to string - [ NaN, 'NaN' ], // NaN coerced to string - [ null, null ], // null returns null - [ undefined, null ], // undefined returns null + [ 'hello ', 'hello' ], // trailing spaces trimmed + [ ' hello', ' hello' ], // leading spaces preserved + [ ' hello ', ' hello' ], // only trailing trimmed + [ 'hello', 'hello' ], // no change + [ 'a b ', 'a b' ], // internal spaces preserved + [ '', '' ], // empty stays empty + [ ' ', '' ], // single space + [ ' ', '' ], // all spaces trimmed + [ 'hello\t', 'hello\t' ], // trailing tab not trimmed + [ 'hello\n', 'hello\n' ], // trailing newline not trimmed + [ 'hello\t ', 'hello\t' ], // space after tab trimmed + [ 'hello \t', 'hello \t' ], // space before tab preserved + [ 42, '42' ], // number coerced to string + [ 0, '0' ], // falsy but not null: coerced, not dropped + [ true, 'true' ], // boolean coerced to string + [ NaN, 'NaN' ], // NaN coerced to string + [ null, null ], // null returns null + [ undefined, null ], // undefined returns null + + // Only the ASCII space (U+0020) is stripped; every other trailing + // whitespace character is left in place. + [ 'hi\r', 'hi\r' ], // carriage return is not a space: unchanged + [ 'hi\f', 'hi\f' ], // form feed is not a space: unchanged + [ 'hi\v', 'hi\v' ], // vertical tab is not a space: unchanged + [ 'hi\u00a0', 'hi\u00a0' ], // non-breaking space is not ASCII space: kept + [ '\t ', '\t' ], // strips trailing spaces, stops at the tab + [ '๐Ÿ˜€ ', '๐Ÿ˜€' ], // surrogate-pair emoji kept, spaces stripped + + // Non-string coercion edge cases. + [ '12 ', '12' ], // numeric string: trailing spaces stripped + [ false, 'false' ], // boolean coerced to string + [ Infinity, 'Infinity' ], // Infinity coerced to string + [ -Infinity, '-Infinity' ], // -Infinity coerced to string + [ [], '' ], // empty array coerces to '' + [ ['hi '], 'hi' ], // single-element array unwraps then trims + [ [null], '' ], // [null] coerces to '' (null element -> '') + [ {}, '[object Object]' ], // plain object stringifies, no trailing spaces + [ 'xxxxx ', 'xxxxx' ], // longer run of trailing spaces all stripped ]) { test(`cxjs_rtrim(${JSON.stringify(s)}) = ${JSON.stringify(result)}`, () => { diff --git a/centrallix-os/sys/js/tests/cxjs_sqrt.test.js b/centrallix-os/sys/js/tests/cxjs_sqrt.test.js index cdf49921c..59fcf7746 100644 --- a/centrallix-os/sys/js/tests/cxjs_sqrt.test.js +++ b/centrallix-os/sys/js/tests/cxjs_sqrt.test.js @@ -14,15 +14,18 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); -// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which -// would make distinct edge-case rows share a test name; fmt renders those -// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +// JSON.stringify collapses NaN/Infinity to "null", omits undefined, and renders +// -0 as "0", which would make distinct edge-case rows share a test name; fmt +// renders those values verbatim (and -0 distinctly from 0) while otherwise +// matching JSON.stringify, so names stay unique. function fmt(v) { if (Array.isArray(v)) return '[' + v.map(fmt).join(',') + ']'; if (v !== null && typeof v === 'object') return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; if (typeof v === 'number' || v === undefined) return String(v); return JSON.stringify(v); @@ -40,6 +43,7 @@ describe('cxjs_sqrt', () => [ 0, 0 ], [ -0, -0 ], // preserved: Math.sqrt(-0) is -0, not NaN [ Infinity, Infinity ], + [ 625, 25 ], // larger perfect square ]) { test(`cxjs_sqrt(${fmt(input)}) = ${fmt(result)}`, () => { @@ -47,6 +51,15 @@ describe('cxjs_sqrt', () => }); } + // Extreme magnitudes stay finite. + for (const input of [ 1e308, Number.MAX_VALUE, 1e-200, Number.MIN_VALUE ]) + { + test(`cxjs_sqrt(${fmt(input)}) = Math.sqrt(${fmt(input)})`, () => + { + assert.equal(env.cxjs_sqrt(input), Math.sqrt(input)); + }); + } + // Negative inputs and NaN have no real root and yield null. for (const input of [ -1, -5.5, -Infinity, NaN ]) { @@ -71,6 +84,7 @@ describe('cxjs_sqrt', () => // Input Result [ '4', 2 ], [ '2.25', 1.5 ], + [ '-1', null ], // string coerces to -1, which has no real root [ '', 0 ], // empty string coerces to 0 [ 'foo', null ], // non-numeric string coerces to NaN [ true, 1 ], diff --git a/centrallix-os/sys/js/tests/cxjs_square.test.js b/centrallix-os/sys/js/tests/cxjs_square.test.js index 3b11a8e29..e139517af 100644 --- a/centrallix-os/sys/js/tests/cxjs_square.test.js +++ b/centrallix-os/sys/js/tests/cxjs_square.test.js @@ -14,15 +14,18 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); -// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which -// would make distinct edge-case rows share a test name; fmt renders those -// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +// JSON.stringify collapses NaN/Infinity to "null", omits undefined, and renders +// -0 as "0", which would make distinct edge-case rows share a test name; fmt +// renders those values verbatim (and -0 distinctly from 0) while otherwise +// matching JSON.stringify, so names stay unique. function fmt(v) { if (Array.isArray(v)) return '[' + v.map(fmt).join(',') + ']'; if (v !== null && typeof v === 'object') return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; if (typeof v === 'number' || v === undefined) return String(v); return JSON.stringify(v); @@ -31,18 +34,21 @@ function fmt(v) describe('cxjs_square', () => { for (const [ input, result ] of [ - // Input Result - [ 4, 16 ], - [ 3, 9 ], - [ 0.5, 0.25 ], - [ 1, 1 ], - [ 0, 0 ], - [ -0, +0 ], // (-0)^2 is +0 - [ -2, 4 ], // negatives square to positives - [ -1.5, 2.25 ], - [ Infinity, Infinity ], - [ -Infinity, Infinity ], - [ 1e200, Infinity ], // overflows to Infinity + // Input Result + [ 4, 16 ], + [ 3, 9 ], + [ 0.5, 0.25 ], + [ 1, 1 ], + [ 0, 0 ], + [ -0, +0 ], // (-0)^2 is +0 + [ -2, 4 ], // negatives square to positives + [ -1.5, 2.25 ], + [ Infinity, Infinity ], + [ -Infinity, Infinity ], + [ 1e200, Infinity ], // overflows to Infinity + [ Number.MAX_VALUE, Infinity ], // also overflows + [ 1e-200, 0 ], // underflows to 0 + [ Number.MIN_VALUE, 0 ], // also underflows ]) { test(`cxjs_square(${fmt(input)}) = ${fmt(result)}`, () => { @@ -71,6 +77,7 @@ describe('cxjs_square', () => // Input Result [ '4', 16 ], [ '2.5', 6.25 ], + [ '-2', 4 ], // negative string coerces to -2, squares positive [ '', 0 ], // empty string coerces to 0 [ 'foo', NaN ], // non-numeric string coerces to NaN [ true, 1 ], diff --git a/centrallix-os/sys/js/tests/cxjs_sum.test.js b/centrallix-os/sys/js/tests/cxjs_sum.test.js index 47ffda883..96e844705 100644 --- a/centrallix-os/sys/js/tests/cxjs_sum.test.js +++ b/centrallix-os/sys/js/tests/cxjs_sum.test.js @@ -14,15 +14,18 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const env = require('./_setup'); -// JSON.stringify collapses NaN/Infinity to "null" and omits undefined, which -// would make distinct edge-case rows share a test name; fmt renders those -// values verbatim (and otherwise matches JSON.stringify) so names stay unique. +// JSON.stringify collapses NaN/Infinity to "null", omits undefined, and renders +// -0 as "0", which would make distinct edge-case rows share a test name; fmt +// renders those values verbatim, distinguishes -0 from 0, and otherwise matches +// JSON.stringify, so names stay unique. function fmt(v) { if (Array.isArray(v)) return '[' + v.map(fmt).join(',') + ']'; if (v !== null && typeof v === 'object') return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; if (typeof v === 'number' || v === undefined) return String(v); return JSON.stringify(v); @@ -56,7 +59,7 @@ describe('cxjs_sum', () => [ [NaN, 2], 2 ], [ [1, 2, undefined, 3], 6 ], ]) { - test(`cxjs_sum(${JSON.stringify(input)}) = ${result}`, () => + test(`cxjs_sum(${fmt(input)}) = ${fmt(result)}`, () => { assert.equal(env.cxjs_sum(input), result); }); @@ -88,7 +91,7 @@ describe('cxjs_sum', () => [ { T: NaN, U: 2 }, 2 ], [ { V: 1, W: 2, X: undefined, Y: 3 }, 6 ], ]) { - test(`cxjs_sum(${JSON.stringify(input)}) = ${result}`, () => + test(`cxjs_sum(${fmt(input)}) = ${fmt(result)}`, () => { assert.equal(env.cxjs_sum(input), result); }); @@ -132,4 +135,90 @@ describe('cxjs_sum', () => assert.equal(env.cxjs_sum(input), result); }); } + + // Order-dependent concatenation with type coercions. + for (const [ input, result ] of [ + // Input Result + [ [1, '2', 3], '123' ], // 0+1->1, 1+'2'->'12', '12'+3->'123' + [ ['a', 1, 2], 3 ], // 'a' skipped, then 0+1+2 + [ [1, 2, 'a'], 3 ], // 'a' skipped at the end + [ ['x', 'y'], null ], // all values are non-numeric + [ ['x', true, 'y'], 1 ], // wrapped coercion + [ [true, true, 1], 3 ], // booleans coerce to 1 and add numerically + ]) { + test(`cxjs_sum(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_sum(input), result); + }); + } + + // All-null and all-NaN collections count as nothing. + for (const [ input, result ] of [ + // Input Result + [ [null, null], null ], + [ [NaN, NaN], null ], + [ { a: null, b: null }, null ], + [ { a: NaN, b: NaN }, null ], + ]) { + test(`cxjs_sum(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_sum(input), result); + }); + } + + // Sign of zero and floating-point accumulation. + for (const [ input, result ] of [ + // Input Result + [ [-0], 0 ], // 0 + -0 -> +0 + [ [-0, -0], 0 ], + [ [-0, 0], 0 ], + [ [0.1, 0.2], 0.30000000000000004 ], // binary float rounding + [ [1e308, 1e308], Infinity ], // overflow to Infinity + ]) { + test(`cxjs_sum(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_sum(input), result); + }); + } + + // Nested array behaviors. + for (const [ input, result ] of [ + // Input Result + [ [[1], 2], '012' ], // 0+[1]->'01', '01'+2->'012' + [ [[1], [2]], '012' ], // 0+[1]->'01', '01'+[2]->'012' + [ [[], 5], '05' ], // [] coerces to 0 but 0+[]->'0', '0'+5->'05' + [ [[1, 2], 3], 3 ], // [1,2] is NaN -> skipped, then 0+3 + ]) { + test(`cxjs_sum(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_sum(input), result); + }); + } + + // Sparse arrays. + for (const [ input, result ] of [ + // Input Result + [ [1, , 3], 4 ], // hole between two values + [ [, , 5], 5 ], // two holes, one value + ]) { + test(`cxjs_sum(${fmt(input)}) = ${fmt(result)}`, () => + { + assert.equal(env.cxjs_sum(input), result); + }); + } + + test('cxjs_sum(array with extra non-index prop) = 6', () => + { + const a = [1, 2, 3]; + a.foo = 99; // ignored: numeric-index loop only + assert.equal(env.cxjs_sum(a), 6); + }); + + // Value on the prototype is summed alongside other properties. + test('cxjs_sum(object with inherited enumerable prop) = 4', () => + { + function Proto() { this.a = 3; } + Proto.prototype.b = 1; + assert.equal(env.cxjs_sum(new Proto()), 4); + }); }); diff --git a/centrallix-os/sys/js/tests/cxjs_truncate.test.js b/centrallix-os/sys/js/tests/cxjs_truncate.test.js index 56bf3ca88..707568c25 100644 --- a/centrallix-os/sys/js/tests/cxjs_truncate.test.js +++ b/centrallix-os/sys/js/tests/cxjs_truncate.test.js @@ -86,6 +86,34 @@ describe('cxjs_truncate', () => [ [0.1], +0 ], [ [-0.1], -0 ], [ [-0.9], -0 ], + [ [-0], -0 ], // -0 input passes through + [ [-Number.MIN_VALUE], -0 ], // tiny negative subnormal -> -0 + + // Float traps. + // Args Result + [ [1.005, 2], 1 ], // stored just under 1.005 -> 1.00 + [ [2.349999, 2], 2.34 ], + + // Precision is lost before truncation when n*factor exceeds 2^53, and a + // large n*factor can overflow to Infinity.. + // Args Result + [ [123456789012.3456, 2], 123456789012.34 ], + [ [Number.MAX_VALUE, 2], Infinity ], // n*100 overflows + [ [Number.MAX_VALUE], Number.MAX_VALUE ], + [ [Number.MIN_VALUE], 0 ], // smallest subnormal -> +0 + + // factor = 10**Math.round(dec) underflows to 0 for a large negative dec, + // so n*0 = 0 then 0/0 = NaN (the large positive dec case is covered above). + // Args Result + [ [5, -400], NaN ], // 10**-400 underflows to 0 + + // dec coercion. + // Args Result + [ [1.5, '2'], 1.5 ], // dec "2" -> 2, no change + [ [1.555, '2'], 1.55 ], // truncated, not rounded + [ [1.5, NaN], NaN ], + [ [1.5, Infinity], NaN ], + [ [1.5, -Infinity], NaN ], // n is coerced to a number before truncating (matching JS arithmetic). // Args Result diff --git a/centrallix-os/sys/js/tests/cxjs_upper.test.js b/centrallix-os/sys/js/tests/cxjs_upper.test.js index 666d7e8c9..50cf77105 100644 --- a/centrallix-os/sys/js/tests/cxjs_upper.test.js +++ b/centrallix-os/sys/js/tests/cxjs_upper.test.js @@ -15,10 +15,17 @@ const assert = require('node:assert/strict'); const env = require('./_setup'); // JSON.stringify renders undefined as the bare word "undefined" only inside -// arrays; as a standalone value it yields undefined (not a string), which -// breaks template names. fmt renders such values verbatim so names stay unique. +// arrays; as a standalone value it yields undefined (not a string), and -0 +// renders as "0", which break/collide template names. fmt renders such values +// verbatim (and recurses into arrays/objects) so names stay unique. function fmt(v) { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; if (typeof v === 'number' || v === undefined) return String(v); return JSON.stringify(v); @@ -39,6 +46,13 @@ describe('cxjs_upper', () => [ 'cafรฉ', 'CAFร‰' ], // accented letter uppercases [ 'รŸ', 'SS' ], // sharp-s expands to two chars [ '๏ฌ', 'FI' ], // fi ligature expands to two chars + [ '๐Ÿ˜€', '๐Ÿ˜€' ], // surrogate-pair emoji has no case + [ 'CAFร‰', 'CAFร‰' ], // already-upper accented stays + [ 'ฤฑ', 'I' ], // dotless i uppercases to ASCII I + [ 'ยต', 'ฮœ' ], // micro sign -> Greek capital Mu + [ ' ', ' ' ], // whitespace-only unchanged + [ '12345', '12345' ], // digits-only unchanged + [ '๐Ÿ˜€abc', '๐Ÿ˜€ABC' ], // emoji kept, letters uppercased // Non-strings are String()-coerced first, then uppercased. Only strict // null short-circuits to null; undefined does not. @@ -53,6 +67,9 @@ describe('cxjs_upper', () => [ -Infinity, '-INFINITY' ], [ [], '' ], // empty array coerces to '' [ ['a', 'b'], 'A,B' ], // Joins with ',' and no spaces + [ ['abc'], 'ABC' ], // single-element array unwraps + [ 0, '0' ], // zero coerces to '0' + [ -0, '0' ], // negative zero stringifies to '0' [ {}, '[OBJECT OBJECT]' ], // plain object stringifies ]) { test(`cxjs_upper(${fmt(input)}) = ${fmt(result)}`, () => diff --git a/centrallix-os/sys/js/tests/cxjs_user_name.test.js b/centrallix-os/sys/js/tests/cxjs_user_name.test.js index 1f5c1aabf..3cb9c7385 100644 --- a/centrallix-os/sys/js/tests/cxjs_user_name.test.js +++ b/centrallix-os/sys/js/tests/cxjs_user_name.test.js @@ -10,17 +10,31 @@ // GNU Lesser General Public License for more details. 'use strict'; -const { describe, it } = require('node:test'); -const assert = require('node:assert/strict'); -const env = require('./_setup'); +const { describe, it, after } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +let sandbox_username = env.pg_username; describe('cxjs_user_name', () => { + // pg_username is a shared sandbox global; save and restore + // the the username to prevent affects on other suites. + after(() => { env.pg_username = sandbox_username; }); + for (const name of [ - 'alice', - 'bob', - '', - ' !@#$%^&*()":;\' ', + // Label Value + ['alice'], + ['bob'], + [''], + [' !@#$%^&*()":;\' '], + [ 'null', null ], + [ 'undefined', undefined ], + [ 'number 42', 42 ], + [ 'number 0', 0 ], + [ 'false', false ], + [ 'array', ['a','b'] ], + [ 'object', { x: 1 } ], ]) { it(`returns pg_username (\"${name}\")`, () => { @@ -28,4 +42,14 @@ describe('cxjs_user_name', () => assert.equal(env.cxjs_user_name(), name); }); } + + // Username edgecases + for (const [ label, value ] of [ + ]) { + it(`returns pg_username unchanged (${label})`, () => + { + env.pg_username = value; + assert.equal(env.cxjs_user_name(), value); + }); + } }); From 6b30c8fd3d1bbc4d00a35b1b5b381217d3f37c7a Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 18 Jun 2026 13:10:46 -0600 Subject: [PATCH 53/56] Add tests for htr_boolean(). --- .../sys/js/tests/htr_boolean.test.js | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 centrallix-os/sys/js/tests/htr_boolean.test.js diff --git a/centrallix-os/sys/js/tests/htr_boolean.test.js b/centrallix-os/sys/js/tests/htr_boolean.test.js new file mode 100644 index 000000000..44d4bb8f2 --- /dev/null +++ b/centrallix-os/sys/js/tests/htr_boolean.test.js @@ -0,0 +1,108 @@ +// Copyright (C) 2026 LightSys Technology Services, Inc. +// +// You may use these files and this library under the terms of the +// GNU Lesser General Public License, Version 2.1, contained in the +// included file "COPYING" or http://www.gnu.org/licenses/lgpl.txt. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const env = require('./_setup'); + +// JSON.stringify renders undefined/NaN as "null" and -0 as "0", which would +// collapse distinct edge-case rows into duplicate test names; fmt renders them +// verbatim (and recurses into arrays/objects) so each name stays unique. +function fmt(v) + { + if (Array.isArray(v)) + return '[' + v.map(fmt).join(',') + ']'; + if (v !== null && typeof v === 'object') + return '{' + Object.keys(v).map((k) => JSON.stringify(k) + ':' + fmt(v[k])).join(',') + '}'; + if (Object.is(v, -0)) + return '-0'; + if (typeof v === 'number' || v === undefined) + return String(v); + return JSON.stringify(v); + } + +describe('htr_boolean', () => + { + // False values. + for (const input of [ + null, + undefined, + 0, + -0, + false, + '', + ' ', // whitespace coerces to 0 + '\t', + '0', + '00', + '0.0', + 'no', 'No', 'NO', + 'false', 'False', 'FALSE', + 'off', 'Off', 'OFF', + // More numeric strings that == 0 (compared numerically, not as text). + '0.00', + '+0', + '-0', // the string '-0', distinct from the number -0 above + '0e0', + '0x0', // hex zero also == 0 + '\n', + '\r\n', + '\t\n ', // mixed whitespace coerces to 0 + // Arrays whose string coercion is empty or a single 0-valued element. + [], // [] -> '' -> 0 + [0], // [0] -> '0' -> == 0 + [''], // [''] -> '' -> 0 + ['0'], // ['0'] -> '0' -> == 0 + ]) { + test(`htr_boolean(${fmt(input)}) === false`, () => + { + assert.equal(env.htr_boolean(input), false); + }); + } + + // True values. + for (const input of [ + 1, + -1, + 0.5, + true, + NaN, + '1', + 'yes', + 'true', + 'on', + 'hello', + 'n', + 'nope', + 'no ', // trailing space defeats the 'no' match + ' no', + Infinity, + -Infinity, + {}, // object -> '[object Object]', != 0 + { a: 1 }, + [1], // single non-zero element -> '1', != 0 + [1, 2], // multi-element array -> '1,2', != 0 + [0, 0], // -> '0,0' which is != 0 numerically + ' false', // leading space defeats the 'false' match + 'false ', // trailing space defeats the 'false' match + 'offf', // not exactly 'off' + 'noo', // not exactly 'no' + 'null', // the text, not the value + 'undefined', + 'NaN', // String(NaN) lower-cases to 'nan', no match + ]) { + test(`htr_boolean(${fmt(input)}) === true`, () => + { + assert.equal(env.htr_boolean(input), true); + }); + } + }); From 252c81da43396ac961cd12a65892fd1bd46b180f Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 18 Jun 2026 17:09:27 -0600 Subject: [PATCH 54/56] Fix work in tests for cxjs_user_name() that I forgot to finish before. --- .../sys/js/tests/cxjs_user_name.test.js | 38 +++++++------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/centrallix-os/sys/js/tests/cxjs_user_name.test.js b/centrallix-os/sys/js/tests/cxjs_user_name.test.js index 3cb9c7385..09a44eb9d 100644 --- a/centrallix-os/sys/js/tests/cxjs_user_name.test.js +++ b/centrallix-os/sys/js/tests/cxjs_user_name.test.js @@ -19,34 +19,24 @@ let sandbox_username = env.pg_username; describe('cxjs_user_name', () => { // pg_username is a shared sandbox global; save and restore - // the the username to prevent affects on other suites. + // the username to prevent affects on other suites. after(() => { env.pg_username = sandbox_username; }); - for (const name of [ - // Label Value - ['alice'], - ['bob'], - [''], - [' !@#$%^&*()":;\' '], - [ 'null', null ], - [ 'undefined', undefined ], - [ 'number 42', 42 ], - [ 'number 0', 0 ], - [ 'false', false ], - [ 'array', ['a','b'] ], - [ 'object', { x: 1 } ], - ]) { - it(`returns pg_username (\"${name}\")`, () => - { - env.pg_username = name; - assert.equal(env.cxjs_user_name(), name); - }); - } - - // Username edgecases for (const [ label, value ] of [ + // Label Value + ['alice', 'alice' ], + ['bob', 'bob' ], + ['', '' ], + [' !@#$%^&*()":;\' ', ' !@#$%^&*()":;\' ' ], + ['null', null ], + ['undefined', undefined ], + ['number 42', 42 ], + ['number 0', 0 ], + ['false', false ], + ['array', ['a','b'] ], + ['object', { x: 1 } ], ]) { - it(`returns pg_username unchanged (${label})`, () => + it(`returns pg_username (\"${label}\")`, () => { env.pg_username = value; assert.equal(env.cxjs_user_name(), value); From d5cd84b1fb6e6c18dcedb94e0336236377120737 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 18 Jun 2026 17:13:41 -0600 Subject: [PATCH 55/56] Refactor gen-coverage.sh into the test-js-coverage make rule. --- centrallix-os/sys/js/tests/gen-coverage.sh | 17 ----------------- centrallix/Makefile.in | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 17 deletions(-) delete mode 100755 centrallix-os/sys/js/tests/gen-coverage.sh diff --git a/centrallix-os/sys/js/tests/gen-coverage.sh b/centrallix-os/sys/js/tests/gen-coverage.sh deleted file mode 100755 index c957d3f2b..000000000 --- a/centrallix-os/sys/js/tests/gen-coverage.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh -# Regenerate lcov.info for the cxjs_* test suite, viewable in VSCode via the -# Coverage Gutters extension by ryanluker. - -set -e - -# Must run from the repo root so the SF: paths in lcov.info will be -# workspace-relative (a leading ../ breaks file resolution). -root=$(git rev-parse --show-toplevel) -cd "$root" - -node --test --experimental-test-coverage \ - --test-reporter=lcov --test-reporter-destination=lcov.info \ - --test-reporter=spec --test-reporter-destination=stdout \ - 'centrallix-os/sys/js/tests/*.test.js' - -echo "Wrote $root/lcov.info" diff --git a/centrallix/Makefile.in b/centrallix/Makefile.in index e7771165b..71cf18f86 100644 --- a/centrallix/Makefile.in +++ b/centrallix/Makefile.in @@ -415,6 +415,7 @@ V3LSOBJS=$(V3BASEOBJS) $(HTDRIVERS) $(NETDRIVERS) $(WGTRDRIVERS) .PHONY: test_install .PHONY: test .PHONY: test-js +.PHONY: test-js-coverage all: centrallix mods config manpages cxpasswd test_obj linksign @@ -594,6 +595,20 @@ test-js: fi; \ "$$NODE_BIN" --test '../centrallix-os/sys/js/tests/*.test.js' 2>&1 +test-js-coverage: + @NODE_BIN="$(NODE)"; \ + [ -n "$$NODE_BIN" ] || NODE_BIN=$$(command -v node); \ + if [ -z "$$NODE_BIN" ]; then \ + echo "Error: node not found (not detected by configure, not on PATH)."; \ + echo "Install Node.js (or load it in nvm with 'nvm use'), then try again."; \ + exit 1; \ + fi; \ + cd .. && \ + "$$NODE_BIN" --test --experimental-test-coverage \ + --test-reporter=lcov --test-reporter-destination=lcov.info \ + --test-reporter=spec --test-reporter-destination=stdout \ + 'centrallix-os/sys/js/tests/*.test.js' + sfeditor/sfedit.o: sfeditor/sfedit.c sfeditor/*.xpm $(CC) $(CFLAGS) `gtk-config --cflags` $< -c -o $@ From 38b778738256023809d75ef1c5e1c8f43dccf3bb Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 18 Jun 2026 17:17:12 -0600 Subject: [PATCH 56/56] Update Makefile to find tests in shell code instead of in node. --- centrallix/Makefile.in | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/centrallix/Makefile.in b/centrallix/Makefile.in index 71cf18f86..f232ec09b 100644 --- a/centrallix/Makefile.in +++ b/centrallix/Makefile.in @@ -593,7 +593,7 @@ test-js: echo "Install Node.js (or load it in nvm with 'nvm use'), then try again."; \ exit 1; \ fi; \ - "$$NODE_BIN" --test '../centrallix-os/sys/js/tests/*.test.js' 2>&1 + "$$NODE_BIN" --test ../centrallix-os/sys/js/tests/*.test.js 2>&1 test-js-coverage: @NODE_BIN="$(NODE)"; \ @@ -607,7 +607,7 @@ test-js-coverage: "$$NODE_BIN" --test --experimental-test-coverage \ --test-reporter=lcov --test-reporter-destination=lcov.info \ --test-reporter=spec --test-reporter-destination=stdout \ - 'centrallix-os/sys/js/tests/*.test.js' + centrallix-os/sys/js/tests/*.test.js sfeditor/sfedit.o: sfeditor/sfedit.c sfeditor/*.xpm $(CC) $(CFLAGS) `gtk-config --cflags` $< -c -o $@