diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 97e5221..6a0fb68 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -100,7 +100,11 @@ External contributors:
Before submitting a pull request, test your modifications by running the FEAScript library from a local directory. For example, you can load the library in your HTML file as follows:
```javascript
-import { FEAScriptModel, plotSolution, printVersion } from "[USER_DIRECTORY]/FEAScript-core/src/index.js";
+import {
+ FEAScriptModel,
+ plotSolution,
+ printVersion,
+} from "[USER_DIRECTORY]/FEAScript-core/src/index.js";
```
FEAScript can be run on a local server. You **must** start the server from the workspace root directory (the folder that contains both `FEAScript-core/` and `FEAScript-website/`), not from inside either subfolder. The HTML files use relative paths such as `../feascript-website.css` and `../../FEAScript-core/src/index.js` that only resolve correctly from that root.
@@ -127,4 +131,4 @@ Testing can be also performed at the Node.js environment. In this case you can a
npm test
```
-These tests compare the numerical results against stored reference solutions at selected points.
+This command uses the Node.js test runner to discover all test files under `tests/`. The tests compare numerical results against stored reference solutions and verify individual solver and assembler behavior.
diff --git a/examples/Beam1DFEM/Beam1DEuler_Bernoulli.js b/examples/Beam1DFEM/Beam1DEuler_Bernoulli.js
deleted file mode 100644
index ef22f55..0000000
--- a/examples/Beam1DFEM/Beam1DEuler_Bernoulli.js
+++ /dev/null
@@ -1,95 +0,0 @@
-/**
- * ════════════════════════════════════════════════════════════════
- * FEAScript Core Library
- * Lightweight Finite Element Simulation in JavaScript
- * Version: 0.3.0 (RC) | https://feascript.com
- * MIT License © 2023–2026 FEAScript
- * ════════════════════════════════════════════════════════════════
- */
-
-/**
- * Clamped and spring-supported Euler-Bernoulli beam
- *
- * Reproduces the "Bending of a Beam" example from J.N. Reddy, "An Introduction to
- * the Finite Element Method", 3rd ed., McGraw-Hill, 2006 (FEM1D example problems,
- * Chapter 7), solved there with the reference FEM1D Fortran program (MODEL=3,
- * NTYPE=0, IELEM=0 i.e. Hermite cubic elements).
- *
- * Geometry: a 10 m beam, clamped at x=0, resting on a roller at midspan (x=5 m),
- * and connected to a linear transverse spring at the free end (x=10 m).
- * Mesh: 2 Hermite cubic beam elements of 5 m each -> 3 nodes: [1, 2, 3]
- *
- * 1,000 N/m (on 0 <= x <= 5) 2,500 N (down, at node 3)
- * v v v v v v v v v v |
- * /////|--------------------|-------------------| ~~~~ spring, k = 1e-4*EI
- * ///// 1 (clamped) 2 (roller) 3 (free end, spring)
- * |<-------- 5 m ----->|<------- 5 m ------>|
- * ^ moment 1,250 N-m applied at node 2
- *
- * EI = 2e6 N-m^2 (constant), k_spring = 1e-4 * EI = 200 N/m
- *
- * Boundary conditions (see beamBoundaryConditions.js for the condition syntax):
- * - Node 1 (x=0): fixed -> w=0, theta=0 (clamped support)
- * - Node 2 (x=5): pinned + moment -> w=0, applied moment M=1250 N-m
- * - Node 3 (x=10): spring + force -> k=200 N/m, applied point load P=-2500 N
- */
-
-// Import Math.js
-import * as math from "mathjs";
-global.math = math;
-
-// Import FEAScript library
-import { FEAScriptModel, printVersion } from "feascript";
-
-console.log("FEAScript Version:", printVersion);
-
-// Create a new FEAScript model
-const model = new FEAScriptModel();
-
-// Select physics/PDE
-model.setModelConfig("eulerBernoulliBeamScript", {
- coefficientFunctions: {
- EI: (x) => 2.0e6, // Bending stiffness E*I (N-m^2), constant along the beam
- // Distributed transverse load: -1,000 N/m over the first (clamped) span only
- q: (x) => (x <= 5 ? -1000 : 0),
- // c0 defaults to 0 (no elastic foundation) when omitted
- },
-});
-
-// Define mesh configuration
-// elementOrder is 'linear' because that only describes the 2-node beam geometry;
-// the field itself is always interpolated with cubic Hermite shape functions internally
-model.setMeshConfig({
- meshDimension: "1D",
- elementOrder: "linear",
- numElementsX: 2,
- maxX: 10,
-});
-
-// Define boundary conditions (keyed by 1-based global node number)
-model.addBoundaryCondition("1", [["fixed"]]); // Clamped support
-model.addBoundaryCondition("2", [["pinned"], ["moment", 1250]]); // Roller + applied moment
-model.addBoundaryCondition("3", [["spring", 200], ["force", -2500]]); // Spring support + point load
-
-// Set solver method
-model.setSolverMethod("lusolve");
-
-// Solve the problem
-const { solutionVector } = model.solve();
-
-// The solution vector is ordered [w_0, theta_0, w_1, theta_1, w_2, theta_2, ...]
-// (mathjs' lusolve returns a nested array, so flatten defensively before reading it)
-const flatSolution = solutionVector.map((entry) => (Array.isArray(entry) ? entry[0] : entry));
-
-const nodeXCoordinates = [0, 5, 10];
-console.log("\nNode | x (m) | Deflection w (m) | Rotation theta (rad)");
-console.log("-----|----------|-------------------|----------------------");
-for (let nodeIndex = 0; nodeIndex < nodeXCoordinates.length; nodeIndex++) {
- const w = flatSolution[2 * nodeIndex];
- const theta = flatSolution[2 * nodeIndex + 1];
- console.log(
- ` ${nodeIndex + 1} | ${nodeXCoordinates[nodeIndex].toFixed(2).padStart(8)} | ${w
- .toExponential(4)
- .padStart(17)} | ${theta.toExponential(4).padStart(20)}`,
- );
-}
diff --git a/examples/Beam1DFEM/README.md b/examples/eulerBernoulliBeamScript/README.md
similarity index 67%
rename from examples/Beam1DFEM/README.md
rename to examples/eulerBernoulliBeamScript/README.md
index 923e53c..ee14e93 100644
--- a/examples/Beam1DFEM/README.md
+++ b/examples/eulerBernoulliBeamScript/README.md
@@ -1,4 +1,4 @@
-
+
# 1D Euler-Bernoulli Beam Examples
@@ -7,7 +7,7 @@ This directory contains Node.js examples demonstrating how to use the FEAScript
## Examples
-#### 1. Clamped and Spring-Supported Beam (`Beam1DEuler_Bernoulli.js`)
+#### 1. Clamped and Spring-Supported Beam (`clampedSpringSupportedBeam1D.js`)
Reproduces the "Bending of a Beam" example from J.N. Reddy, _An Introduction to the Finite Element
Method_, 3rd ed., McGraw-Hill, 2006 (FEM1D example problems, Chapter 7). A 10 m beam is clamped at
@@ -69,17 +69,20 @@ plus a point load):
```javascript
model.addBoundaryCondition("1", [["fixed"]]); // w=0, theta=0 (clamped)
model.addBoundaryCondition("2", [["pinned"], ["moment", 1250]]); // w=0, plus an applied moment
-model.addBoundaryCondition("3", [["spring", 200], ["force", -2500]]); // elastic support, plus a point load
+model.addBoundaryCondition("3", [
+ ["spring", 200],
+ ["force", -2500],
+]); // elastic support, plus a point load
```
-| Condition type | Kind | Effect |
-| ------------------------------------ | ---------------- | ----------------------------------------------------------- |
-| `["fixed"]` | Essential | `w = 0` and `theta = 0` (clamped support) |
-| `["pinned"]` / `["deflection", v]` | Essential | `w = v` (default `v = 0`; roller/pin support) |
-| `["rotationFixed"]` / `["rotation", v]` | Essential | `theta = v` (default `v = 0`) |
-| `["force", v]` | Natural | Applies a concentrated transverse force `v` at the node |
-| `["moment", v]` | Natural | Applies a concentrated moment `v` at the node |
-| `["spring", k, uRef]` | Mixed (Robin) | Transverse elastic support of stiffness `k` about `uRef` (default `uRef = 0`) |
+| Condition type | Kind | Effect |
+| --------------------------------------- | ------------- | ----------------------------------------------------------------------------- |
+| `["fixed"]` | Essential | `w = 0` and `theta = 0` (clamped support) |
+| `["pinned"]` / `["deflection", v]` | Essential | `w = v` (default `v = 0`; roller/pin support) |
+| `["rotationFixed"]` / `["rotation", v]` | Essential | `theta = v` (default `v = 0`) |
+| `["force", v]` | Natural | Applies a concentrated transverse force `v` at the node |
+| `["moment", v]` | Natural | Applies a concentrated moment `v` at the node |
+| `["spring", k, uRef]` | Mixed (Robin) | Transverse elastic support of stiffness `k` about `uRef` (default `uRef = 0`) |
## Running the Node.js Examples
@@ -98,5 +101,5 @@ npm install feascript
#### 3. Run the example:
```bash
-node Beam1DEuler_Bernoulli.js
+node clampedSpringSupportedBeam1D/clampedSpringSupportedBeam1D.js
```
diff --git a/examples/eulerBernoulliBeamScript/clampedSpringSupportedBeam1D/clampedSpringSupportedBeam1D.js b/examples/eulerBernoulliBeamScript/clampedSpringSupportedBeam1D/clampedSpringSupportedBeam1D.js
new file mode 100644
index 0000000..5c8c390
--- /dev/null
+++ b/examples/eulerBernoulliBeamScript/clampedSpringSupportedBeam1D/clampedSpringSupportedBeam1D.js
@@ -0,0 +1,70 @@
+/**
+ * ════════════════════════════════════════════════════════════════
+ * FEAScript Core Library
+ * Lightweight Finite Element Simulation in JavaScript
+ * Version: 0.3.0 (RC) | https://feascript.com
+ * MIT License © 2023–2026 FEAScript
+ * ════════════════════════════════════════════════════════════════
+ */
+
+// Import Math.js
+import * as math from "mathjs";
+global.math = math;
+
+// Import FEAScript library
+import { FEAScriptModel, printVersion } from "feascript";
+
+console.log("FEAScript Version:", printVersion);
+
+// Create a new FEAScript model
+const model = new FEAScriptModel();
+
+// Select physics/PDE
+model.setModelConfig("eulerBernoulliBeamScript", {
+ coefficientFunctions: {
+ EI: (x) => 2.0e6, // Bending stiffness
+ q: (x) => (x <= 5 ? -1000 : 0),
+ },
+});
+
+// Define mesh configuration
+model.setMeshConfig({
+ meshDimension: "1D",
+ elementOrder: "linear",
+ numElementsX: 2,
+ maxX: 10,
+});
+
+// Define boundary conditions
+model.addBoundaryCondition("1", [["fixed"]]); // Clamped support
+model.addBoundaryCondition("2", [["pinned"], ["moment", 1250]]); // Roller + applied moment
+model.addBoundaryCondition("3", [
+ ["spring", 200],
+ ["force", -2500],
+]); // Spring support + point load
+
+// Set solver method
+model.setSolverMethod("lusolve");
+
+// Solve the problem
+const { solutionVector } = model.solve();
+
+// Print results
+const flatSolution = solutionVector.map((entry) =>
+ Array.isArray(entry) ? entry[0] : entry
+);
+
+const nodeXCoordinates = [0, 5, 10];
+console.log("\nNode | x (m) | Deflection w (m) | Rotation theta (rad)");
+console.log("-----|----------|-------------------|----------------------");
+for (let nodeIndex = 0; nodeIndex < nodeXCoordinates.length; nodeIndex++) {
+ const w = flatSolution[2 * nodeIndex];
+ const theta = flatSolution[2 * nodeIndex + 1];
+ console.log(
+ ` ${nodeIndex + 1} | ${nodeXCoordinates[nodeIndex]
+ .toFixed(2)
+ .padStart(8)} | ${w.toExponential(4).padStart(17)} | ${theta
+ .toExponential(4)
+ .padStart(20)}`
+ );
+}
diff --git a/package.json b/package.json
index e4ae545..fe27839 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,7 @@
"build": "rollup -c",
"prepare": "npm run build",
"prepublishOnly": "npm run build",
- "test": "node tests/run-all-tests.js",
+ "test": "node --test tests",
"format": "prettier --write ."
},
"repository": {
diff --git a/src/mesh/meshUtils.js b/src/mesh/meshUtils.js
index 8447e80..3ac6784 100644
--- a/src/mesh/meshUtils.js
+++ b/src/mesh/meshUtils.js
@@ -238,7 +238,7 @@ export function performIsoparametricMapping2D(params) {
/**
* Function to test if a point is inside a triangle using barycentric coordinates,
- * also returning the natural coordinates (ksi, eta).
+ * also returning the natural coordinates (ksi, eta)
* @param {number} x - X-coordinate of the point
* @param {number} y - Y-coordinate of the point
* @param {array} vertices - Triangle vertices [[x0,y0],[x1,y1],[x2,y2]]
diff --git a/src/models/beamBoundaryConditions.js b/src/models/beamBoundaryConditions.js
index d879e61..cb70ff8 100644
--- a/src/models/beamBoundaryConditions.js
+++ b/src/models/beamBoundaryConditions.js
@@ -121,11 +121,7 @@ export class BeamBoundaryConditions {
} else if (conditionType === "rotationFixed" || conditionType === "rotation") {
applyDirichlet(rotationDOF, value ?? 0);
debugLog(`Node ${nodeKey}: Applied rotation theta=${value ?? 0} (essential BC)`);
- } else if (
- conditionType !== "force" &&
- conditionType !== "moment" &&
- conditionType !== "spring"
- ) {
+ } else if (conditionType !== "force" && conditionType !== "moment" && conditionType !== "spring") {
errorLog(`Unknown beam boundary condition type: "${conditionType}"`);
}
});
diff --git a/src/models/eulerBernoulliBeam.js b/src/models/eulerBernoulliBeam.js
index c061a98..db0b5c0 100644
--- a/src/models/eulerBernoulliBeam.js
+++ b/src/models/eulerBernoulliBeam.js
@@ -81,7 +81,10 @@ export function assembleEulerBernoulliBeamMat(meshData, boundaryConditions, coef
// Cubic Hermite basis functions for the field, with a 4-point Gauss quadrature rule
const basisFunctions = new BasisFunctions({ meshDimension: "1D", elementOrder: "hermiteCubic" });
- const numericalIntegration = new NumericalIntegration({ meshDimension: "1D", elementOrder: "hermiteCubic" });
+ const numericalIntegration = new NumericalIntegration({
+ meshDimension: "1D",
+ elementOrder: "hermiteCubic",
+ });
const { gaussPoints, gaussWeights } = numericalIntegration.getGaussPointsAndWeights();
// Matrix assembly
diff --git a/src/visualization/vtkPlot.js b/src/visualization/vtkPlot.js
index 4ecc8b4..d16a6c9 100644
--- a/src/visualization/vtkPlot.js
+++ b/src/visualization/vtkPlot.js
@@ -492,7 +492,7 @@ function convertElementNodesToLinearCell(elementNodes) {
return [indices[0], indices[6], indices[8], indices[2]];
}
- // Generic fallback for polygonal/high-order cells.
+ // Generic fallback for polygonal/high-order cells
return indices.slice(0, Math.min(4, indices.length));
}
@@ -546,15 +546,23 @@ function buildVTPString(vtkData) {
'',
'',
" ",
- ` `,
+ ` `,
' ',
- ` ${Array.from(vtkData.scalars).join(" ")}`,
+ ` ${Array.from(
+ vtkData.scalars,
+ ).join(" ")}`,
" ",
" ",
- ` ${Array.from(vtkData.points).join(" ")}`,
+ ` ${Array.from(
+ vtkData.points,
+ ).join(" ")}`,
" ",
` <${topologyTag}>`,
- ` ${connectivity.join(" ")}`,
+ ` ${connectivity.join(
+ " ",
+ )}`,
` ${offsets.join(" ")}`,
` ${topologyTag}>`,
" ",
diff --git a/src/workers/worker.js b/src/workers/worker.js
index 4ad4ecc..3780dd7 100644
--- a/src/workers/worker.js
+++ b/src/workers/worker.js
@@ -16,7 +16,7 @@ import * as Comlink from "../vendor/comlink.mjs";
export class FEAScriptWorker {
/**
* Constructor to initialize the FEAScriptWorker class
- * Sets up the worker and initializes the workerWrapper.
+ * Sets up the worker and initializes the workerWrapper
*/
constructor() {
this.worker = null;
diff --git a/tests/regression/EulerBernoulliBeam/REGRESSION.md b/tests/regression/EulerBernoulliBeam/REGRESSION.md
index f78eed9..3c54d5c 100644
--- a/tests/regression/EulerBernoulliBeam/REGRESSION.md
+++ b/tests/regression/EulerBernoulliBeam/REGRESSION.md
@@ -6,7 +6,7 @@ This test guards the numerical output of the 1D Euler-Bernoulli beam example aga
unintended changes to the beam solver, assembler, or mesh-generation logic.
It replicates exactly the problem set up in
-[`Beam1DEuler_Bernoulli.js`](../../../examples/Beam1DFEM/Beam1DEuler_Bernoulli.js) — the
+[`clampedSpringSupportedBeam1D.js`](../../../examples/eulerBernoulliBeamScript/clampedSpringSupportedBeam1D/clampedSpringSupportedBeam1D.js) — the
"Bending of a Beam" example from J.N. Reddy, _An Introduction to the Finite Element Method_,
3rd ed., McGraw-Hill, 2006 (FEM1D example problems, Chapter 7) — and asserts both a set of
known-good baseline values and, independently of those baseline numbers, that the resulting
@@ -14,27 +14,27 @@ finite element solution satisfies global static equilibrium.
## Problem setup
-| Parameter | Value |
-| ----------------------------- | -------------------------------------------------------- |
-| Domain | 1D beam, 0 – 10 m |
-| Mesh | 2 cubic Hermite beam elements of 5 m each (3 nodes) |
-| Bending stiffness EI | 2.0 × 10⁶ N·m² (constant) |
-| Distributed load | −1,000 N/m over 0 ≤ x ≤ 5 m only |
-| Node 1 (x = 0) | Fixed (clamped): w = 0, theta = 0 |
-| Node 2 (x = 5) | Pinned (roller): w = 0, plus an applied moment M = 1,250 N·m |
-| Node 3 (x = 10) | Transverse spring k = 200 N/m, plus a point load P = −2,500 N |
-| Solver | LU decomposition (`lusolve`) |
+| Parameter | Value |
+| -------------------- | ------------------------------------------------------------- |
+| Domain | 1D beam, 0 – 10 m |
+| Mesh | 2 cubic Hermite beam elements of 5 m each (3 nodes) |
+| Bending stiffness EI | 2.0 × 10⁶ N·m² (constant) |
+| Distributed load | −1,000 N/m over 0 ≤ x ≤ 5 m only |
+| Node 1 (x = 0) | Fixed (clamped): w = 0, theta = 0 |
+| Node 2 (x = 5) | Pinned (roller): w = 0, plus an applied moment M = 1,250 N·m |
+| Node 3 (x = 10) | Transverse spring k = 200 N/m, plus a point load P = −2,500 N |
+| Solver | LU decomposition (`lusolve`) |
## Expected values
-| Quantity | Value |
-| ------------------------ | ------------------------- |
-| w₁ (deflection, node 1) | 0 m |
-| θ₁ (rotation, node 1) | 0 rad |
-| w₂ (deflection, node 2) | 0 m |
-| θ₂ (rotation, node 2) | −5.6790761806 × 10⁻³ rad |
-| w₃ (deflection, node 3) | −8.0144777663 × 10⁻² m |
-| θ₃ (rotation, node 3) | −2.1203895209 × 10⁻² rad |
+| Quantity | Value |
+| ----------------------- | ------------------------ |
+| w₁ (deflection, node 1) | 0 m |
+| θ₁ (rotation, node 1) | 0 rad |
+| w₂ (deflection, node 2) | 0 m |
+| θ₂ (rotation, node 2) | −5.6790761806 × 10⁻³ rad |
+| w₃ (deflection, node 3) | −8.0144777663 × 10⁻² m |
+| θ₃ (rotation, node 3) | −2.1203895209 × 10⁻² rad |
Tolerance used in the baseline assertions: `1e-8`.
@@ -77,16 +77,24 @@ node tests/regression/EulerBernoulliBeam/regression.test.js
The `test` script in `package.json` also runs this file, so `npm test` works too.
+A passing run prints a `PASS:` line for each check, followed by a summary line:
+
+```
+8 passed, 0 failed.
+```
+
+A failing run prints one or more `FAIL:` lines, ends with the same summary line format, and exits with code 1.
+
## After modifying the code
-| Situation | Action |
-| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
-| Bug fix that should not change results | Run the test — it must still pass. |
-| Intentional algorithm change (new integration rule, new element type, etc.) | Re-derive the expected values, update `EXPECTED` in `regression.test.js`, and document the reason here. |
-| New boundary condition type | Update both the test and `Beam1DEuler_Bernoulli.js` together. |
+| Situation | Action |
+| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
+| Bug fix that should not change results | Run the test — it must still pass. |
+| Intentional algorithm change (new integration rule, new element type, etc.) | Re-derive the expected values, update `EXPECTED` in `regression.test.js`, and document the reason here. |
+| New boundary condition type | Update both the test and `clampedSpringSupportedBeam1D.js` together. |
## Change log
| Date | Change | New expected values |
-| ---------- | ---------------------------- | -------------------- |
-| 2026-07-17 | Initial regression baseline | See table above |
+| ---------- | --------------------------- | ------------------- |
+| 2026-07-17 | Initial regression baseline | See table above |
diff --git a/tests/regression/EulerBernoulliBeam/regression.test.js b/tests/regression/EulerBernoulliBeam/regression.test.js
index a228fdf..2b731aa 100644
--- a/tests/regression/EulerBernoulliBeam/regression.test.js
+++ b/tests/regression/EulerBernoulliBeam/regression.test.js
@@ -1,15 +1,26 @@
+/**
+ * ════════════════════════════════════════════════════════════════
+ * FEAScript Core Library
+ * Lightweight Finite Element Simulation in JavaScript
+ * Version: 0.3.0 (RC) | https://feascript.com
+ * MIT License © 2023–2026 FEAScript
+ * ════════════════════════════════════════════════════════════════
+ */
+
/**
* Regression test for the 1D Euler-Bernoulli beam model (EulerBernoulliBeam)
*
- * Replicates the exact setup from Beam1DEuler_Bernoulli.js — the "Bending of a
- * Beam" example from J.N. Reddy, "An Introduction to the Finite Element Method",
- * 3rd ed., McGraw-Hill, 2006 (FEM1D example problems, Chapter 7) — and asserts:
+ * Replicates the exact setup from
+ * examples/eulerBernoulliBeamScript/clampedSpringSupportedBeam1D/
+ * clampedSpringSupportedBeam1D.js — the "Bending of a Beam" example from J.N.
+ * Reddy, "An Introduction to the Finite Element Method", 3rd ed., McGraw-Hill,
+ * 2006 (FEM1D example problems, Chapter 7) — and asserts:
* 1) the deflection/rotation solution vector against known-good baseline values
* 2) global force and moment equilibrium of the resulting FE solution, which
* holds regardless of the specific numeric baseline and independently
* confirms the assembled system is physically consistent
*
- * Run: node tests/regression/EulerBernoulliBeam/regression.test.js
+ * Run: node tests/regression/EulerBernoulliBeam/regression.test.js (or npm test)
*/
import * as mathjs from "mathjs";
@@ -57,18 +68,26 @@ function runSimulation() {
model.addBoundaryCondition("1", [["fixed"]]);
model.addBoundaryCondition("2", [["pinned"], ["moment", appliedMoment]]);
- model.addBoundaryCondition("3", [["spring", springConstant], ["force", appliedForce]]);
+ model.addBoundaryCondition("3", [
+ ["spring", springConstant],
+ ["force", appliedForce],
+ ]);
model.setSolverMethod("lusolve");
return model.solve();
}
+let passed = 0;
+let failed = 0;
+
function assert(condition, message) {
if (!condition) {
errorLog(`FAIL: ${message}`);
- process.exit(1);
+ failed++;
+ } else {
+ basicLog(`PASS: ${message}`);
+ passed++;
}
- basicLog(`PASS: ${message}`);
}
basicLog("");
@@ -80,29 +99,20 @@ const { solutionVector } = runSimulation();
const flatSolution = solutionVector.map((entry) => (Array.isArray(entry) ? entry[0] : entry));
const [w1, theta1, w2, theta2, w3, theta3] = flatSolution;
-// ---------------------------------------------------------------------------
-// 1) Baseline values
-// ---------------------------------------------------------------------------
+basicLog("");
+basicLog("[1] Baseline deflection/rotation values");
+
assert(Math.abs(w1 - EXPECTED.w1) < TOLERANCE, `w1: expected ~${EXPECTED.w1}, got ${w1}`);
assert(Math.abs(theta1 - EXPECTED.theta1) < TOLERANCE, `theta1: expected ~${EXPECTED.theta1}, got ${theta1}`);
assert(Math.abs(w2 - EXPECTED.w2) < TOLERANCE, `w2: expected ~${EXPECTED.w2}, got ${w2}`);
-assert(
- Math.abs(theta2 - EXPECTED.theta2) < TOLERANCE,
- `theta2: expected ${EXPECTED.theta2}, got ${theta2}`,
-);
+assert(Math.abs(theta2 - EXPECTED.theta2) < TOLERANCE, `theta2: expected ${EXPECTED.theta2}, got ${theta2}`);
assert(Math.abs(w3 - EXPECTED.w3) < TOLERANCE, `w3: expected ${EXPECTED.w3}, got ${w3}`);
-assert(
- Math.abs(theta3 - EXPECTED.theta3) < TOLERANCE,
- `theta3: expected ${EXPECTED.theta3}, got ${theta3}`,
-);
+assert(Math.abs(theta3 - EXPECTED.theta3) < TOLERANCE, `theta3: expected ${EXPECTED.theta3}, got ${theta3}`);
+
+// Reassemble without boundary conditions to recover reactions for the equilibrium checks
+basicLog("");
+basicLog("[2] Global force and moment equilibrium");
-// ---------------------------------------------------------------------------
-// 2) Global equilibrium check, independent of the specific baseline values above.
-// Recompute the raw (no boundary conditions applied) element assembly and use
-// it to recover support reactions: at any DOF, K_raw.u - F_raw equals whatever
-// external generalized force (reaction, spring force, or applied load) is
-// required to balance that row.
-// ---------------------------------------------------------------------------
const meshDataForCheck = {
nodesXCoordinates: [0, 5, 10],
nop: [
@@ -156,4 +166,11 @@ assert(
`Global moment equilibrium about x=0 holds (sum=${sumMomentsAboutOrigin})`,
);
+basicLog("");
+if (failed > 0) {
+ errorLog(`${passed} passed, ${failed} failed.`);
+} else {
+ basicLog(`${passed} passed, ${failed} failed.`);
+}
basicLog("================================");
+if (failed > 0) process.exit(1);
diff --git a/tests/regression/HeatConduction1DWall/REGRESSION.md b/tests/regression/HeatConduction1DWall/REGRESSION.md
index 6359a5b..0b8b0ba 100644
--- a/tests/regression/HeatConduction1DWall/REGRESSION.md
+++ b/tests/regression/HeatConduction1DWall/REGRESSION.md
@@ -5,9 +5,9 @@
This test guards the numerical output of the 1D heat-conduction-through-a-wall example
against unintended changes to the solver, assembler, or mesh-generation logic.
-It replicates exactly the problem set up in
-[`HeatConduction1DWall.html`](../../../examples/solidHeatTransferScript/HeatConduction1DWall/HeatConduction1DWall.html)
-and asserts a known good value.
+It replicates the physical problem set up in
+[`heatConduction1DWall.js`](../../../examples/heatConductionScript/heatConduction1DWall/heatConduction1DWall.js),
+uses `lusolve` as the regression baseline, and asserts a known good value.
## Problem setup
@@ -35,15 +35,16 @@ From the repository root:
node tests/regression/HeatConduction1DWall/regression.test.js
```
-A passing run prints:
+The `test` script in `package.json` also runs this file, so `npm test` works too.
+
+A passing run prints a `PASS:` line for each check, followed by a summary line:
```
-PASS: T(x=0) = 10.29412 (expected 10.29412)
+PASS: Temperature at node 0: expected 10.29412, got 10.294117647058822 (tolerance 0.0001)
+1 passed, 0 failed.
```
-A failing run prints a `FAIL:` message and exits with code 1.
-
-The `test` script in `package.json` also runs this file, so `npm test` works too.
+A failing run prints one or more `FAIL:` lines, ends with the same summary line format, and exits with code 1.
## After modifying the code
@@ -51,7 +52,7 @@ The `test` script in `package.json` also runs this file, so `npm test` works too
| --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Bug fix that should not change results | Run the test — it must still pass. |
| Intentional algorithm change (new element type, new integration rule, etc.) | Re-derive the expected value, update `EXPECTED_T0` in `regression.test.js`, and document the reason here. |
-| New boundary condition API | Update both the test and the reference HTML example together. |
+| New boundary condition API | Update both the test and the reference JavaScript example together. |
| Adding a new solver method | Add a separate assertion block for the new method; keep the `lusolve` block untouched as the baseline. |
## Change log
diff --git a/tests/regression/HeatConduction1DWall/regression.test.js b/tests/regression/HeatConduction1DWall/regression.test.js
index faf7858..81098e8 100644
--- a/tests/regression/HeatConduction1DWall/regression.test.js
+++ b/tests/regression/HeatConduction1DWall/regression.test.js
@@ -1,10 +1,19 @@
+/**
+ * ════════════════════════════════════════════════════════════════
+ * FEAScript Core Library
+ * Lightweight Finite Element Simulation in JavaScript
+ * Version: 0.3.0 (RC) | https://feascript.com
+ * MIT License © 2023–2026 FEAScript
+ * ════════════════════════════════════════════════════════════════
+ */
+
/**
* Regression test for HeatConduction1DWall
*
- * Replicates the exact setup from HeatConduction1DWall.html and asserts
- * that the temperature at node 0 (convection boundary) remains 10.29412.
+ * Replicates the physical setup from heatConduction1DWall.js using lusolve and
+ * asserts that the temperature at node 0 (convection boundary) remains 10.29412
*
- * Run: node tests/regression/HeatConduction1DWall/regression.test.js
+ * Run: node tests/regression/HeatConduction1DWall/regression.test.js (or npm test)
*/
import * as mathjs from "mathjs";
@@ -36,16 +45,22 @@ function runSimulation() {
return model.solve();
}
+let passed = 0;
+let failed = 0;
+
function assert(condition, message) {
if (!condition) {
errorLog(`FAIL: ${message}`);
- process.exit(1);
+ failed++;
+ } else {
+ basicLog(`PASS: ${message}`);
+ passed++;
}
}
basicLog("");
basicLog("================================");
-basicLog("Starting test in solid heat transfer 1D wall...");
+basicLog("Starting regression test for solid heat transfer in a 1D wall...");
const { solutionVector } = runSimulation();
// solutionVector from math.lusolve is a nested array: [[T0], [T1], ...]
@@ -56,5 +71,11 @@ assert(
`Temperature at node 0: expected ${EXPECTED_T0}, got ${T0} (tolerance ${TOLERANCE})`,
);
-basicLog(`PASS: T(x=0) = ${T0.toFixed(5)} (expected ${EXPECTED_T0})`);
+basicLog("");
+if (failed > 0) {
+ errorLog(`${passed} passed, ${failed} failed.`);
+} else {
+ basicLog(`${passed} passed, ${failed} failed.`);
+}
basicLog("================================");
+if (failed > 0) process.exit(1);
diff --git a/tests/regression/HeatConduction2DFin/REGRESSION.md b/tests/regression/HeatConduction2DFin/REGRESSION.md
index 4c27f0d..a7e2afb 100644
--- a/tests/regression/HeatConduction2DFin/REGRESSION.md
+++ b/tests/regression/HeatConduction2DFin/REGRESSION.md
@@ -5,9 +5,10 @@
This test guards the numerical output of the 2D heat-conduction-in-a-fin example
against unintended changes to the solver, assembler, or mesh-generation logic.
-It replicates exactly the problem set up in
-[`HeatConduction2DFin.html`](../../../examples/solidHeatTransferScript/HeatConduction2DFin/HeatConduction2DFin.html)
-and asserts a known good value at a representative interior point.
+It replicates the physical problem set up in
+[`heatConduction2DFin.js`](../../../examples/heatConductionScript/heatConduction2DFin/heatConduction2DFin.js),
+uses `lusolve` as the regression baseline, and asserts a known good value at a representative
+interior point.
## Problem setup
@@ -41,15 +42,16 @@ From the repository root:
node tests/regression/HeatConduction2DFin/regression.test.js
```
-A passing run prints:
+The `test` script in `package.json` also runs this file, so `npm test` works too.
+
+A passing run prints a `PASS:` line for each check, followed by a summary line:
```
-PASS: T(x=0, y=2) = 81.31873 (expected 81.31873)
+PASS: Temperature at (x=0, y=2): expected 81.31873, got 81.31873348502957 (tolerance 0.0001)
+2 passed, 0 failed.
```
-A failing run prints a `FAIL:` message and exits with code 1.
-
-Running `npm test` executes all regression tests, including this one.
+A failing run prints one or more `FAIL:` lines, ends with the same summary line format, and exits with code 1.
## After modifying the code
@@ -57,7 +59,7 @@ Running `npm test` executes all regression tests, including this one.
| --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| Bug fix that should not change results | Run the test — it must still pass. |
| Intentional algorithm change (new element type, new integration rule, etc.) | Re-derive the expected value, update `EXPECTED_T` in `regression.test.js`, and document the reason here. |
-| New boundary condition API | Update both the test and the reference HTML example together. |
+| New boundary condition API | Update both the test and the reference JavaScript example together. |
| Mesh refinement study | Add a separate assertion block for the refined mesh; keep the current block as the coarse-mesh baseline. |
## Change log
diff --git a/tests/regression/HeatConduction2DFin/regression.test.js b/tests/regression/HeatConduction2DFin/regression.test.js
index 334d3cb..71ec63d 100644
--- a/tests/regression/HeatConduction2DFin/regression.test.js
+++ b/tests/regression/HeatConduction2DFin/regression.test.js
@@ -1,20 +1,25 @@
+/**
+ * ════════════════════════════════════════════════════════════════
+ * FEAScript Core Library
+ * Lightweight Finite Element Simulation in JavaScript
+ * Version: 0.3.0 (RC) | https://feascript.com
+ * MIT License © 2023–2026 FEAScript
+ * ════════════════════════════════════════════════════════════════
+ */
+
/**
* Regression test for HeatConduction2DFin
*
- * Replicates the exact setup from HeatConduction2DFin.html and asserts
- * that the temperature at (x=0, y=2) remains 81.31873.
+ * Replicates the physical setup from heatConduction2DFin.js using lusolve and
+ * asserts that the temperature at (x=0, y=2) remains 81.31873
*
- * Run: node tests/regression/HeatConduction2DFin/regression.test.js
+ * Run: node tests/regression/HeatConduction2DFin/regression.test.js (or npm test)
*/
import * as mathjs from "mathjs";
import { FEAScriptModel } from "../../../src/FEAScript.js";
import { basicLog, errorLog } from "../../../src/utilities/logging.js";
-basicLog("");
-basicLog("================================");
-basicLog("Starting test in solid heat transfer 2D fin...");
-
// FEAScript.js references `math` as a global (loaded via CDN in browser).
// Set it here before any solve() call.
globalThis.math = mathjs;
@@ -49,10 +54,20 @@ function runSimulation() {
function assert(condition, message) {
if (!condition) {
errorLog(`FAIL: ${message}`);
- process.exit(1);
+ failed++;
+ } else {
+ basicLog(`PASS: ${message}`);
+ passed++;
}
}
+let passed = 0;
+let failed = 0;
+
+basicLog("");
+basicLog("================================");
+basicLog("Starting regression test for solid heat transfer in a 2D fin...");
+
const { solutionVector, nodesCoordinates } = runSimulation();
const { nodesXCoordinates, nodesYCoordinates } = nodesCoordinates;
@@ -62,7 +77,7 @@ const nodeIndex = nodesXCoordinates.findIndex(
(x, i) => Math.abs(x - EXPECTED_X) < 1e-10 && Math.abs(nodesYCoordinates[i] - EXPECTED_Y) < 1e-10,
);
-assert(nodeIndex !== -1, `No node found at (x=${EXPECTED_X}, y=${EXPECTED_Y})`);
+assert(nodeIndex !== -1, `Found node at (x=${EXPECTED_X}, y=${EXPECTED_Y})`);
// solutionVector from math.lusolve is a nested array: [[T0], [T1], ...]
const T = Array.isArray(solutionVector[nodeIndex]) ? solutionVector[nodeIndex][0] : solutionVector[nodeIndex];
@@ -72,6 +87,11 @@ assert(
`Temperature at (x=${EXPECTED_X}, y=${EXPECTED_Y}): expected ${EXPECTED_T}, got ${T} (tolerance ${TOLERANCE})`,
);
-basicLog(`PASS: T(x=${EXPECTED_X}, y=${EXPECTED_Y}) = ${T.toFixed(5)} (expected ${EXPECTED_T})`);
-
+basicLog("");
+if (failed > 0) {
+ errorLog(`${passed} passed, ${failed} failed.`);
+} else {
+ basicLog(`${passed} passed, ${failed} failed.`);
+}
basicLog("================================");
+if (failed > 0) process.exit(1);
diff --git a/tests/run-all-tests.js b/tests/run-all-tests.js
deleted file mode 100644
index e1f848d..0000000
--- a/tests/run-all-tests.js
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * Test runner — discovers and executes every *.test.js file under tests/.
- * Add a new test file anywhere in this tree and it runs automatically.
- *
- * Usage: node tests/run-all-tests.js
- */
-
-import { readdirSync, statSync } from "fs";
-import { join, relative } from "path";
-import { spawnSync } from "child_process";
-import { fileURLToPath } from "url";
-import { basicLog, errorLog, warnLog } from "../src/utilities/logging.js";
-
-const __dirname = fileURLToPath(new URL(".", import.meta.url));
-
-function collectTestFiles(dir) {
- const entries = readdirSync(dir);
- const files = [];
- for (const entry of entries) {
- const fullPath = join(dir, entry);
- if (statSync(fullPath).isDirectory()) {
- files.push(...collectTestFiles(fullPath));
- } else if (entry.endsWith(".test.js")) {
- files.push(fullPath);
- }
- }
- return files;
-}
-
-const testFiles = collectTestFiles(__dirname);
-
-if (testFiles.length === 0) {
- warnLog("No test files found.");
- process.exit(0);
-}
-
-basicLog(`Found ${testFiles.length} test file(s).`);
-basicLog("");
-
-let passed = 0;
-let failed = 0;
-
-for (const file of testFiles) {
- const label = relative(__dirname, file);
- const result = spawnSync(process.execPath, [file], { stdio: "inherit" });
- if (result.status === 0) {
- passed++;
- } else {
- errorLog(`FAILED: ${label}`);
- basicLog("");
- failed++;
- }
-}
-
-if (failed > 0) {
- errorLog(`${passed} passed, ${failed} failed.`);
-} else {
- basicLog(`${passed} passed, ${failed} failed.`);
-}
-process.exit(failed > 0 ? 1 : 0);
diff --git a/tests/unit/README.md b/tests/unit/README.md
index dee4e93..f6d2718 100644
--- a/tests/unit/README.md
+++ b/tests/unit/README.md
@@ -1,8 +1,14 @@
# Unit Tests
-This folder will contain unit tests for individual FEAScript modules (solvers, assemblers, utilities, etc.).
+This folder contains unit tests for individual FEAScript modules (solvers, assemblers, utilities, etc.).
-Each test file should target a single module and be runnable with:
+Run all unit and regression tests from the repository root with:
+
+```bash
+npm test
+```
+
+Each unit test can also be run directly with:
```bash
node tests/unit/.js
diff --git a/tests/unit/eulerBernoulliBeam.test.js b/tests/unit/eulerBernoulliBeam.test.js
index 8b71882..02f471e 100644
--- a/tests/unit/eulerBernoulliBeam.test.js
+++ b/tests/unit/eulerBernoulliBeam.test.js
@@ -1,3 +1,12 @@
+/**
+ * ════════════════════════════════════════════════════════════════
+ * FEAScript Core Library
+ * Lightweight Finite Element Simulation in JavaScript
+ * Version: 0.3.0 (RC) | https://feascript.com
+ * MIT License © 2023–2026 FEAScript
+ * ════════════════════════════════════════════════════════════════
+ */
+
/**
* Unit tests for the 1D Euler-Bernoulli beam model (assembleEulerBernoulliBeamMat)
*
@@ -7,7 +16,7 @@
* - Cantilever beam under a tip point load vs. the classical closed-form
* solution (w_tip = P*L^3/(3EI), theta_tip = P*L^2/(2EI))
*
- * Run: node tests/unit/eulerBernoulliBeam.test.js
+ * Run: node tests/unit/eulerBernoulliBeam.test.js (or npm test)
*/
import * as mathjs from "mathjs";
@@ -59,25 +68,33 @@ function maxAbsDiff(A, B) {
return diff;
}
-// ---------------------------------------------------------------------------
-// 1. Single-element stiffness matrix vs. closed-form matrix
-// ---------------------------------------------------------------------------
basicLog("");
-basicLog("[1] Single-element stiffness matrix vs. closed-form Hermite beam matrix");
+basicLog(
+ "[1] Single-element stiffness matrix vs. closed-form Hermite beam matrix"
+);
for (const [EI, L] of [
[2.0e6, 5],
[7.5e4, 2.5],
]) {
- const meshData = prepareMesh({ meshDimension: "1D", elementOrder: "linear", numElementsX: 1, maxX: L });
- const { jacobianMatrix } = assembleEulerBernoulliBeamMat(meshData, {}, { EI: () => EI });
+ const meshData = prepareMesh({
+ meshDimension: "1D",
+ elementOrder: "linear",
+ numElementsX: 1,
+ maxX: L,
+ });
+ const { jacobianMatrix } = assembleEulerBernoulliBeamMat(
+ meshData,
+ {},
+ { EI: () => EI }
+ );
const diff = maxAbsDiff(jacobianMatrix, closedFormBeamStiffness(EI, L));
- assert(diff < 1e-6, `Element stiffness matches closed-form matrix for EI=${EI}, L=${L} (diff=${diff})`);
+ assert(
+ diff < 1e-6,
+ `Element stiffness matches closed-form matrix for EI=${EI}, L=${L} (diff=${diff})`
+ );
}
-// ---------------------------------------------------------------------------
-// 2. Cantilever beam under a tip point load vs. classical closed-form solution
-// ---------------------------------------------------------------------------
basicLog("");
basicLog("[2] Cantilever beam under a tip point load");
@@ -85,37 +102,49 @@ const L = 4;
const EI = 1.0e5;
const P = -1000; // Downward tip load
-const meshData = prepareMesh({ meshDimension: "1D", elementOrder: "linear", numElementsX: 1, maxX: L });
+const meshData = prepareMesh({
+ meshDimension: "1D",
+ elementOrder: "linear",
+ numElementsX: 1,
+ maxX: L,
+});
const { jacobianMatrix, residualVector } = assembleEulerBernoulliBeamMat(
meshData,
{ 1: [["fixed"]], 2: [["force", P]] },
- { EI: () => EI },
+ { EI: () => EI }
+);
+const { solutionVector } = solveLinearSystem(
+ "lusolve",
+ jacobianMatrix,
+ residualVector
+);
+const flatSolution = solutionVector.map((entry) =>
+ Array.isArray(entry) ? entry[0] : entry
);
-const { solutionVector } = solveLinearSystem("lusolve", jacobianMatrix, residualVector);
-const flatSolution = solutionVector.map((entry) => (Array.isArray(entry) ? entry[0] : entry));
const wExact = (P * L ** 3) / (3 * EI);
const thetaExact = (P * L ** 2) / (2 * EI);
const tolerance = 1e-9;
assert(
- Math.abs(flatSolution[0]) < tolerance && Math.abs(flatSolution[1]) < tolerance,
- "Clamped end has zero deflection and rotation",
+ Math.abs(flatSolution[0]) < tolerance &&
+ Math.abs(flatSolution[1]) < tolerance,
+ "Clamped end has zero deflection and rotation"
);
assert(
Math.abs(flatSolution[2] - wExact) < tolerance,
- `Tip deflection matches closed form (got ${flatSolution[2]}, expected ${wExact})`,
+ `Tip deflection matches closed form (got ${flatSolution[2]}, expected ${wExact})`
);
assert(
Math.abs(flatSolution[3] - thetaExact) < tolerance,
- `Tip rotation matches closed form (got ${flatSolution[3]}, expected ${thetaExact})`,
+ `Tip rotation matches closed form (got ${flatSolution[3]}, expected ${thetaExact})`
);
-// ---------------------------------------------------------------------------
basicLog("");
if (failed > 0) {
errorLog(`${passed} passed, ${failed} failed.`);
- process.exit(1);
} else {
basicLog(`${passed} passed, ${failed} failed.`);
}
+basicLog("================================");
+if (failed > 0) process.exit(1);
diff --git a/tests/unit/jacobiMethod.test.js b/tests/unit/jacobiMethod.test.js
index 070ce5f..fbb1a1f 100644
--- a/tests/unit/jacobiMethod.test.js
+++ b/tests/unit/jacobiMethod.test.js
@@ -1,3 +1,12 @@
+/**
+ * ════════════════════════════════════════════════════════════════
+ * FEAScript Core Library
+ * Lightweight Finite Element Simulation in JavaScript
+ * Version: 0.3.0 (RC) | https://feascript.com
+ * MIT License © 2023–2026 FEAScript
+ * ════════════════════════════════════════════════════════════════
+ */
+
/**
* Unit tests for jacobiSolver
*
@@ -5,7 +14,7 @@
* - Success case on a diagonally dominant 2x2 system
* - Non-convergence when maxIterations is too small
*
- * Run: node tests/unit/jacobiMethod.test.js
+ * Run: node tests/unit/jacobiMethod.test.js (or npm test)
*/
import { jacobiSolver } from "../../src/methods/jacobiSolver.js";
@@ -28,12 +37,9 @@ function assert(condition, message) {
}
}
-// ---------------------------------------------------------------------------
-// 1. SUCCESS CASE — diagonally dominant 2x2 system
-//
+// Diagonally dominant system with exact solution x = 1, y = 1
// 10x + 2y = 12 exact solution: x = 1, y = 1
// 1x + 5y = 6
-// ---------------------------------------------------------------------------
basicLog("");
basicLog("[1] Success case");
@@ -54,12 +60,15 @@ assert(result.iterations > 0, "At least one iteration was performed");
assert(result.iterations <= 500, "Converges within configured iteration limit");
const tolerance = 1e-6;
-assert(Math.abs(result.solutionVector[0] - 1) < tolerance, `x ~= 1 (got ${result.solutionVector[0]})`);
-assert(Math.abs(result.solutionVector[1] - 1) < tolerance, `y ~= 1 (got ${result.solutionVector[1]})`);
+assert(
+ Math.abs(result.solutionVector[0] - 1) < tolerance,
+ `x ~= 1 (got ${result.solutionVector[0]})`
+);
+assert(
+ Math.abs(result.solutionVector[1] - 1) < tolerance,
+ `y ~= 1 (got ${result.solutionVector[1]})`
+);
-// ---------------------------------------------------------------------------
-// 2. NON-CONVERGENCE CASE — force early stop
-// ---------------------------------------------------------------------------
basicLog("");
basicLog("[2] Non-convergence case");
@@ -68,17 +77,24 @@ const hardResult = jacobiSolver(A, b, x0, {
tolerance: 1e-20,
});
-assert(hardResult.converged === false, "Method reports non-convergence with too few iterations");
-assert(hardResult.iterations === 1, "Method reports the configured iteration cap");
-assert(hardResult.solutionVector.length === 2, "Returns a solution vector of expected size");
+assert(
+ hardResult.converged === false,
+ "Method reports non-convergence with too few iterations"
+);
+assert(
+ hardResult.iterations === 1,
+ "Method reports the configured iteration cap"
+);
+assert(
+ hardResult.solutionVector.length === 2,
+ "Returns a solution vector of expected size"
+);
-// ---------------------------------------------------------------------------
-// Summary
-// ---------------------------------------------------------------------------
+basicLog("");
if (failed > 0) {
- errorLog(`${passed + failed} assertions — ${passed} passed, ${failed} failed.`);
+ errorLog(`${passed} passed, ${failed} failed.`);
} else {
- basicLog(`${passed + failed} assertions — ${passed} passed, ${failed} failed.`);
+ basicLog(`${passed} passed, ${failed} failed.`);
}
basicLog("================================");
if (failed > 0) process.exit(1);