Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
eda7515
Implement a basic node:test test suite that loads ht_render.js in a s…
Lightning11wins Jun 16, 2026
6d234b2
Add testing for cxjs_user_name as a proof-of-concept.
Lightning11wins Jun 16, 2026
0d3efb1
Fix instanceof causing tests to fail due to cross-domain failures.
Lightning11wins Jun 16, 2026
e0e1957
Add tests for cxjs_min() and cxjs_max().
Lightning11wins Jun 16, 2026
8296278
Add the test-js target to the Centrallix Makefile.
Lightning11wins Jun 17, 2026
360913f
Update generated files.
Lightning11wins Jun 17, 2026
c099937
Improve test name formatting.
Lightning11wins Jun 17, 2026
52ce2cb
Improve test style & formatting.
Lightning11wins Jun 17, 2026
258da92
Add tests for cxjs_count().
Lightning11wins Jun 17, 2026
7b9c5cc
Add tests for cxjs_sum().
Lightning11wins Jun 17, 2026
5adbd57
Add tests for cxjs_getdate().
Lightning11wins Jun 17, 2026
29b0c41
Add tests for cxjs_convert().
Lightning11wins Jun 17, 2026
3410b41
Add tests for cxjs_isnull().
Lightning11wins Jun 17, 2026
c6a4276
Clean up comments on cxjs_getdate() tests.
Lightning11wins Jun 17, 2026
ff19173
Add more date test cases.
Lightning11wins Jun 17, 2026
d15cc50
Improve spacing.
Lightning11wins Jun 17, 2026
8bb47aa
Clean up comments on cxjs_isnull() tests.
Lightning11wins Jun 17, 2026
b17e32a
Add tests for cxjs_right().
Lightning11wins Jun 17, 2026
263bba8
Add more cxjs_right() test cases.
Lightning11wins Jun 17, 2026
37b4096
Fix spacing in cxjs_right() tests.
Lightning11wins Jun 17, 2026
9182b7b
Improve spacing in cxjs_convert() tests.
Lightning11wins Jun 17, 2026
90a6a81
Improve comments on cxjs_convert() tests.
Lightning11wins Jun 17, 2026
ea1181f
Add more tests to convert().
Lightning11wins Jun 17, 2026
b2089fc
Add tests for cxjs_rtrim().
Lightning11wins Jun 17, 2026
2fb9498
Add tests for cxjs_ltrim().
Lightning11wins Jun 17, 2026
5defbf8
Convert to consistent use of single quotes.
Lightning11wins Jun 17, 2026
234fa7b
Add tests for cxjs_plus().
Lightning11wins Jun 17, 2026
bd55f09
Add tests for cxjs_minus().
Lightning11wins Jun 17, 2026
248c480
Add a test case for cxjs_plus().
Lightning11wins Jun 17, 2026
4573b00
Add tests for cxjs_condition().
Lightning11wins Jun 17, 2026
c3dbea8
Add tests for cxjs_quote().
Lightning11wins Jun 17, 2026
7688b0b
Add tests for cxjs_char_length().
Lightning11wins Jun 17, 2026
f68c63b
Add tests for cxjs_charindex().
Lightning11wins Jun 17, 2026
ee2c122
Add tests for cxjs_lower().
Lightning11wins Jun 17, 2026
414e26e
Add tests for cxjs_upper().
Lightning11wins Jun 17, 2026
c274e08
Improve test coverage with many new tests for obscure edge cases.
Lightning11wins Jun 17, 2026
c7d6a73
Add a script for generating coverage files.
Lightning11wins Jun 17, 2026
5409976
Add tests for cxjs_abs().
Lightning11wins Jun 18, 2026
582f465
Add tests for cxjs_reverse().
Lightning11wins Jun 18, 2026
9c7d099
Add tests for cxjs_replace().
Lightning11wins Jun 18, 2026
1273630
Add tests for cxjs_replicate().
Lightning11wins Jun 18, 2026
c380007
Add tests for cxjs_round(). (Draft)
Lightning11wins Jun 18, 2026
704a9c7
Update formatting for cxjs_round() tests.
Lightning11wins Jun 18, 2026
9d22a59
Add tests for cxjs_round().
Lightning11wins Jun 18, 2026
f8a71e8
Add tests for cxjs_constrain().
Lightning11wins Jun 18, 2026
2e7408c
Add tests for cxjs_sqrt().
Lightning11wins Jun 18, 2026
9ba0fbe
Add tests for cxjs_rand().
Lightning11wins Jun 18, 2026
34fa594
Add tests for cxjs_degrees().
Lightning11wins Jun 18, 2026
65635f8
Add tests for cxjs_radians().
Lightning11wins Jun 18, 2026
51b647c
Add tests for cxjs_square().
Lightning11wins Jun 18, 2026
4596050
Add tests for cxjs_power().
Lightning11wins Jun 18, 2026
88f447d
Add many test cases to cover even more edge cases.
Lightning11wins Jun 18, 2026
6b30c8f
Add tests for htr_boolean().
Lightning11wins Jun 18, 2026
252c81d
Fix work in tests for cxjs_user_name() that I forgot to finish before.
Lightning11wins Jun 18, 2026
d5cd84b
Refactor gen-coverage.sh into the test-js-coverage make rule.
Lightning11wins Jun 18, 2026
38b7787
Update Makefile to find tests in shell code instead of in node.
Lightning11wins Jun 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions centrallix-os/sys/js/ht_render.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,15 @@ 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<v.length; i++)
{
if (lowest === undefined || isNaN(lowest) || lowest > v[i])
lowest = v[i];
}
}
else if (v instanceof Object)
else if (typeof v === "object" && v !== null)
{
for(var i in v)
{
Expand All @@ -176,15 +176,15 @@ function cxjs_min(v)
function cxjs_max(v)
{
var highest = undefined;
if (v instanceof Array)
if (Array.isArray(v))
{
for(var i=0; i<v.length; i++)
{
if (highest === undefined || isNaN(highest) || highest < v[i])
highest = v[i];
}
}
else if (v instanceof Object)
else if (typeof v === "object" && v !== null)
{
for(var i in v)
{
Expand All @@ -202,7 +202,7 @@ function cxjs_sum(v)
{
var cnt = 0;
var sum = 0;
if (v instanceof Array)
if (Array.isArray(v))
{
for(var i=0; i<v.length; i++)
{
Expand All @@ -213,7 +213,7 @@ function cxjs_sum(v)
}
}
}
else if (v instanceof Object)
else if (typeof v === "object" && v !== null)
{
for(var i in v)
{
Expand All @@ -234,14 +234,14 @@ function cxjs_sum(v)
function cxjs_count(v)
{
var cnt = 0;
if (v instanceof Array)
if (Array.isArray(v))
{
for(var i=0; i<v.length; i++)
{
if (v[i] != null && !isNaN(v[i])) cnt++;
}
}
else if (v instanceof Object)
else if (typeof v === "object" && v !== null)
{
for(var i in v)
{
Expand Down
50 changes: 50 additions & 0 deletions centrallix-os/sys/js/tests/_setup.js
Original file line number Diff line number Diff line change
@@ -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;
102 changes: 102 additions & 0 deletions centrallix-os/sys/js/tests/cxjs_abs.test.js
Original file line number Diff line number Diff line change
@@ -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');

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

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 ],

// 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)}`, () =>
{
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

// 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)}`, () =>
{
assert.equal(env.cxjs_abs(input), result);
});
}
});
82 changes: 82 additions & 0 deletions centrallix-os/sys/js/tests/cxjs_char_length.test.js
Original file line number Diff line number Diff line change
@@ -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');

// 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 [
// 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"
[ -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]"
// 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(${fmt(input)}) = ${fmt(result)}`, () =>
{
assert.equal(env.cxjs_char_length(input), result);
});
}
});
91 changes: 91 additions & 0 deletions centrallix-os/sys/js/tests/cxjs_charindex.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// 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.
// 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).
// 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)}`, () =>
{
assert.equal(env.cxjs_charindex(needle, haystack), result);
});
}
});
Loading