Validation and Testing - matej-hron/decojs GitHub Wiki
npm testRuns node tests/run-tests.mjs. No external test framework — tests/run-tests.mjs implements describe/test/expect inline (lines 10–140) with matchers .toBe, .toEqual, .toBeCloseTo, .toBeGreaterThan, .toBeLessThan, .toHaveProperty, .toHaveLength, .toBeDefined. Output is one line per test, then a pass/fail summary.
640 tests pass. The Jest configuration in package.json is vestigial — test:jest and test:watch still work but are not the canonical runner; the CI gate is npm test, run on every pull request by .github/workflows/ci.yml.
npm test is required to pass before every commit per CLAUDE.md.
The algorithm suite. Directly imports from js/decoModel.js and js/tissueCompartments.js.
-
Constants & pressure. Checks
SURFACE_PRESSURE,WATER_VAPOR_PRESSURE,N2_FRACTION,PRESSURE_PER_METER,getAmbientPressure,getAlveolarN2Pressure. - Haldane and Schreiner equations. Verifies the closed-form output at known half-times (1, 5, 10, 20 min) and compares Schreiner against a time-stepped Haldane reference.
-
M-values, ceilings, GF interpolation. Exercises
getMValue,getAdjustedMValue,getCompartmentCeiling,getDiveCeiling,interpolateGF, and thecalculateInstantGF/calculateMaxGFpair. -
GF-low anchor and first stop. Covers
findFirstStopAtGFLowacross a range of depths and GF settings, including edge cases where the unrounded ceiling coincides with a 3 m grid depth and where gas switches occur en route. -
Deco schedule. Full
generateDecoScheduleruns for air, EAN50, and O₂ deco; asserts stop count, per-stop time bounds, total-deco bounds, and theDecoCapExceededErrorpath. -
Tissue loading end-to-end.
calculateTissueLoadingon simple profiles; verifies monotonicity on descent, exponential shape, and surface equilibration. -
Variant switching.
setZHL16Variant('A'|'B'|'C')followed by re-checking a reference dive. - M-values theory page. Verifies glossary markup and locale parity across Czech, English, and Spanish, including the pressure-comparison conditions; checks the formula sandbox link and unambiguous ceiling definition; and proves the direct-ascent example violates TC3 while the safe-stop profile remains below all raw M-value limits.
- Gradient-factors theory page. Verifies canonical quantity symbols, GF subscripts, localized decimals, non-breaking unit spacing, and the sourced Richard Pyle/deep-stop history across Czech, English, and Spanish. The history test distinguishes personal observation, independent bubble models, gradient factors, the scope of the NEDU result, and current uncertainty. Focused source tests require the symmetric air/nitrox GF recommendation to link to Alain Foret's accessible publication of the CMAS recommendations and each supporting French, NEDU, and Belgian bullet to include its own study link. Direct CMAS URLs that return HTTP 403 are rejected. The French bullet must use the accessible NCBI/PubMed record. The Czech no-decompression heading is also checked for natural wording.
-
Gradient-factor input limits. Requires both GF values to stay in the 10–100% range across setup validation, compact links, shared repetitive-dive trips, and both planner UIs. Manual values below the limit do not trigger a recalculation and are clamped to 10% when confirmed, preventing
0from silently becoming100. - Gradient-factor UI lock. Verifies the default-on equality checkbox, two-way GF synchronization, GF Low ≤ GF High correction while unlocked, normalization of reversed imported pairs, and automatic unlock after any preset, deco-gas addition, waypoint deco-gas selection, or asymmetric profile load. It also covers empty GF input recovery and preservation of a manually unlocked state across language changes. The lock remains editor-only and is not serialized into the dive setup.
- Dive-plan runtime marker. Verifies that both split dive-plan tables bind the shared runtime definition to their Runtime heading with a superscript asterisk. Practical mode adds no redundant calculation note; departure mode keeps its execution-specific detail as a separate unmarked note.
-
Dive-plan Tank and practical runtime. The instant gas-switch marker row consumes no gas (the new gas shows its full start pressure); practical runtimes round up on ascent rows, never earlier than the model, and moves between stops use the planner's
ASCENT_SPEED(10 m/min, overridable viaopts.ascentRate). -
Video walkthroughs. The sandbox has one inline video host per section, each beside (never inside) its translated heading and naming its video through a
data-video-titlekey present in Czech, English, and Spanish;VideoWalkthrough.jssets that name asaria-label/title, relabels it onlanguagechange, and renders each host once even when initialised twice. The Dive Profile host follows the active chart tab: every tab inchartButtonshas an entry inprofileVideoswhose title key exists in Czech, English and Spanish and whose video, poster and subtitle files are published;setVideoWalkthrough()re-points a host (new accessible name in both languages, the next click opens the new video). -
Dive profile chart rendering. Renders the real
DiveProfileChartin jsdom with a stubChartthat captures the config, for the 40 m / 20 min air + EAN50 GF 30/70 series dive: the stage-gas tank line does not reuse the depth blue (or the AVG, MAX/reserve or gas-switch colours), no colour in the tank-line palette (GAS_LINE_COLORS) reuses one that already means something on the chart, the pO₂ 1.4 and 1.6 limit labels sit on opposite sides of their lines so they cannot overlap, and the Tissue Loading legend formats half-times with the locale decimal separator (TC3 (12,5 min)in Czech). - Oxygen warning terminology. Verifies across Czech, English, and Spanish that the 1.4 bar warning describes the bottom-phase limit rather than incorrectly referring to occupational or “working” dives; the phase/gas warning logic remains unchanged.
- M-value sandbox chart interaction. Verifies the ambient-equilibrium line pt = pamb, a full-width square 0–10 bar plot with equal geometric scaling on both pressure axes, hover/focus legend highlighting for all four chart elements, legend placement directly below the graph, and dedicated equal-size SVG spans for the axis-unit labels.
- Tissue-saturation depth input. Verifies that typed depth changes wait for a 250 ms debounce, while slider input remains immediate; change, blur, and Enter flush pending input, and reset/destruction clear pending timers.
- Project origin and CMAS I3 context. Verifies that the home and About pages describe DecoJS as a completed candidate project for CMAS I3 instructor qualification, acknowledge the SPČR Training Commission's expert support, state its purpose for instructors and diving students, and no longer claim that the project has never been reviewed.
- NDL deco-gas propagation. Verifies that a no-decompression profile carrying EAN50 and O₂ still switches at the scheduler's MOD depths, applies configured switch time without being reclassified as decompression, preserves a longer safety stop at the same depth, retains gas-switch audit events, and feeds the changed N₂ fractions and tissue pressures into the shared chart calculation.
Configuration and gas management.
-
Setup loading.
getDefaultSetup,extendDiveSetup, localStorage round-trip viasaveDiveSetup/loadSavedSetup. -
Profile generation.
generateSimpleProfiledescent/ascent rates, bottom-time semantics (measured from dive start, not depth arrival), safety stop insertion. -
Gas helpers.
calculateMOD,getGases,getGasAtWaypoint,getGasAtTime,getGasSwitchEvents,insertGasSwitchWaypointswith 3 m MOD rounding. -
computeGasConsumption. SAC accounting at switch depths — explicitly tests that the switch-stop window bills at the bottom SAC rate, not the deco SAC rate.
Waypoint-array validation.
- Structure (first waypoint at surface, monotonic time, non-negative depth).
-
parseProfileInputon tab-separated and comma-separated text. -
calculateRatesclassification into descent / ascent / level. -
getDiveStatsmaxima and totals.
The sea-level matrix also validates the practical runtime convention across all 3,900 profiles: model stop durations remain whole minutes, inter-stop ascents use the planner's 10 m/min (18 seconds per 3 m), total timeline drift stays below 30 seconds, and the re-simulated destination ceiling may differ by at most 1 cm.
The canonical suite directly checks the 3900 sea-level scenarios in
tests/decotengu-reference.json and 15,986 altitude scenarios in
tests/decotengu-altitude-reference.json. It also checks 105 scenarios split
equally across EN 13319, freshwater, and seawater in
tests/decotengu-water-reference.json. The reporting script remains runnable
standalone as node tests/decotengu-comparison.test.mjs.
The same runner also contains source-level teaching regressions for the Haldane and
Schreiner sandboxes. They verify fixed-target versus moving-target hierarchy,
the equilibrium surface start of the single basic Schreiner example, the
schreinerEquation() for positive, negative, and zero
rates, multiple initial pressures, times, and half-times. They also verify the
step-by-step expansion into the canonical form while keeping calculus outside
the main teaching path.
The M-value sandbox regressions verify canonical quantity notation, direct
ambient-pressure control, a fixed 0–10 bar vertical scale, and removal of the
auxiliary getMValue()
calculation for all three values. The four quantity cards and the three
coefficient-derivation cards each collapse as one group. The generated
coefficient table must contain all 16 compartments from
getCompartmentsForVariant(), preserve the TC1 half-time and
The reusable M-value chart marks the current controlling compartment only when it creates the deepest intersection with the continuous GF ramp. The status and the selected compartment's ruler therefore use the same ceiling definition. The selector highlight does not alter which compartments the user selected, and the current points retain their standard appearance. When the ceiling is clear, the chart explicitly reports that no compartment is currently controlling decompression. If no GF ramp is created, the intersection uses GF High. Both the status and ruler panel show the current depth. The ruler omits the unused tissue-equals-ambient intersection.
The ruler labels its self-consistent intersection with the continuous GF ramp explicitly, distinguishing it from the scheduler's fixed-GF check at the next discrete stop depth.
The M-value chart can open the existing decision audit in a modal with its
Audit control or the A shortcut. Pressing A again closes it, including when
focus is inside the modal. Opening and closing the audit preserves both
fullscreen mode and the selected timeline position. Each audit row separates
compact phase, runtime, depth, and controlling-compartment columns from the
longer calculation explanation; hypothetical anchor simulations are labeled
separately and do not claim a position on the actual runtime.
Per-level departure rows show the destination and its target-depth GF, while
the controlling compartment stays in its dedicated column. They deliberately
omit the intermediate fixed-GF ceiling value so it cannot be confused with the
chart's continuous GF-ramp ceiling.
The coefficient-derivation graph labels bar
unit, and dimensionless
The pressure-page notation regressions require glossary-registered quantity
symbols in formulas (p, h, V, and Δp), the explicit initial
atmospheric-pressure subscript, and non-breaking separators in generated
volume and percentage values. They also reject language-specific prose used as
a formula variable. Dynamic MOD and narcosis exercise rows must re-render
their reveal labels, warning text, and decimal formatting after i18n
initialization and every language change. The Czech VENTID-C list retains each
original English symptom name beside its Czech explanation so the initials
remain meaningful.
The tissue-loading regressions require a content wrapper that prevents the
sticky table of contents from setting the first section's grid-row height.
They also enforce canonical localized tissue-pressure subscripts, the
registered h, γ, and r symbols, upright exponential e, non-breaking
micrometre units in the bubble canvas, and translated chart labels. The gas
pathway represents its partial-pressure gradient with four discrete stage
colors, reverses their order during off-gassing, and never paints a gradient
inside an individual anatomical stage. The bubble explanation distinguishes
gas pressure inside a bubble (p_amb + 2γ/r) from dissolved-gas tension in
the surrounding tissue, which determines the direction of diffusion. The
detailed bubble simulation lives in a dedicated responsive sandbox with a
larger, fixed-size canvas that is never stretched by its layout; the theory
page links to it instead of embedding the cramped visualization. Canvas formulas draw italic quantity
symbols and smaller upright localized subscripts instead of exposing code-like
underscore notation.
The transfilling sandbox regressions require locale-aware runtime formatting,
the glossary's upright l symbol, localized final-pressure subscripts and
result explanations, and re-rendering after a language change. Cylinder gas
content is presented as surface-equivalent volume at the explicit 1 bar
reference pressure rather than as the less intuitive bar·l product.
The cascade-filling sandbox follows the same convention. Its cylinder cards, remaining supply, connection states, and structured fill log must use locale-aware pressure and volume formatting, preserve decimal pressures, and re-render existing log entries when the language changes.
The gas-law sandbox regressions require the glossary's upright l symbol,
locale-aware pressure, temperature, and volume values in generated output,
localized safety text before and after revealing the result, and re-rendering
of the hidden cylinder state when the language changes. The expanded fire
temperature scale must also suppress colliding reference labels while keeping
the current-temperature markers visible. Its collapsed model-limitations note
must identify the gauge-versus-absolute pressure difference as a small
high-pressure approximation, while giving appropriate prominence to real-gas
behaviour, thermal gradients, cylinder-volume and gauge uncertainty, and the
loss of cylinder material strength at fire temperatures.
The standard staged-mode gate also checks schedule structure, not only total time: every emitted stop is on the 3 m grid and lasts at least one minute. Decision-audit regressions additionally prove that enabling the trace leaves stops, gas switches, total time, and anchor unchanged; they cover direct-ascent, anchor-selection, and per-level events plus runtime/depth context and the localized text renderer. Across all 19,886 scenarios, the current implementation matches Decotengu's exact stop-depth list in 96.1%, the complete depth/time schedule in 82.2%, and has a mean absolute total-decompression difference of 0.13 min (maximum 3 min).
Procedural style rather than describe/test — loops over every scenario in tests/decotengu-reference.json, reports pass/fail counts, and exits non-zero on regression.
Pinned regression: passing {gasSwitchTime: 0} to generateDecoSchedule must produce byte-identical schedules to the baseline captured before the gasSwitchTime feature was added. Baseline file: tests/decojs-baseline.json. Tolerance is 0 — exact match required across all 3900 scenarios.
This is the primary numerical-correctness evidence for the DecoJS algorithm.
Reference data. tests/decotengu-reference.json holds 3900 pre-generated deco scenarios produced by decotengu v0.14.1 (see References). Reference data is regenerated with python3 scripts/generate_decotengu_reference.py > tests/decotengu-reference.json.
Altitude reference data. tests/decotengu-altitude-reference.json adds 15,986
scenarios at 500, 1000, 1500, and 2500 m. Regenerate it with
python3 scripts/generate_decotengu_altitude_reference.py > tests/decotengu-altitude-reference.json. Decotengu receives the absolute
engine.surface_pressure; the normal test run only reads the generated JSON and
does not require Python or Decotengu.
Water-mode reference data. tests/decotengu-water-reference.json adds
35 scenarios for each of the EN 13319, freshwater, and seawater modes.
Regenerate it with
python3 scripts/generate_decotengu_water_reference.py > tests/decotengu-water-reference.json. DecoTengu 0.14.1 has no public water
density API, so the pinned generator deliberately overrides both copied private
conversion fields, Engine._meter_to_bar and Engine._p3m, and records that
method plus the exact factor in the JSON metadata.
Scenario coverage.
- Depths: 15 m, 18 m, 21 m, … 60 m (step 3 m, 16 values)
- Bottom times: NDL+3, NDL+6, … NDL+30 min (step 3 min, 10 values per depth)
-
Gas configs:
air,air+EAN50,air+O2,air+EAN50+O2 - GF presets: 100/100 (Bühlmann), 90/95, 80/85, 70/80, 50/80, 40/80, 30/80, 20/80 (Deco Planner-style)
- Algorithm: Bühlmann ZH-L16C for both sides
Tolerances. Per-scenario assertion is |total_deco_diff| ≤ max(5 min, 20% of reference_total_deco). Per-stop tolerance is ±1 min.
These tolerances cover stop-time discretization noise — both implementations round stop times to whole minutes, and the continuous ceiling crossing can land on either side of a minute boundary, giving ±1 min per stop.
Match statistics across all 3900 scenarios:
| Pass rate | 100 % (3900/3900) |
| Exact match on total deco | 56 % (2196 scenarios) |
| Within ±2 min | 95 % (P95) |
| Mean |diff| | 0.6 min |
| Median |diff| | 0 min |
| Max |diff| | 5 min |
| DecoJS gives less | 41 % |
| DecoJS gives more | 3 % |
The largest residuals are concentrated at GF 20/80 (the most aggressive setting in the matrix) where stop-time discretization between the two implementations occasionally lands a stop on different minute boundaries.
Bühlmann constants (SURFACE_PRESSURE=1.01325, N2_FRACTION=0.7902, TC1 b-coefficient variant-specific) match decotengu — which itself cross-references the HeinrichsWeikamp OSTC firmware. See References for the provenance chain.
Validates the repetitive-dive engine (surface-interval off-gassing, seeded deco, and multi-dive trip chaining) against decotengu 0.14.1. This harness is not part of npm test — run it on demand.
Run:
node tests/decotengu-repetitive-comparison.test.mjsRegenerate reference data:
python3 scripts/generate_decotengu_repetitive_reference.py > tests/decotengu-repetitive-reference.jsonSeam A — surface-interval off-gassing. Calls simulateDepthTime at depth 0 for each surface-interval duration and compares the resulting compartment pressures against decotengu's model.load run at surface pressure. Tolerance: 0.001 bar per compartment (compartments 2–16).
Seam B — deco from a pre-saturated seed. Seeds DecoJS's generateDecoProfile with initialTissuePressures taken from a preceding dive, then runs decotengu's ascent from the same saturated state. Tolerance: max(5 min, 20%) of reference total deco.
Trips — multi-dive chaining end-to-end. Chains two or three dives through planTrip, compares per-dive total deco against decotengu run as one continuous profile. Tolerance: max(5 min, 20%).
Compartment 1. DecoJS uses the ZH-L16 "1b" first compartment (half-time 5.0 min); decotengu uses 4.0 min. This is a deliberate model choice. Compartment 1 is excluded from the Seam A pass/fail gate — its divergence (max ~0.012 bar) is reported as informational only.
Scenario bottom times. Reference scenarios use recreational/light-tech bottom times chosen so that every dive stays within DecoJS's 300-min/stop usable range. The test guards beyond-range profiles rather than crashing on them.
| Section | Metric | Value |
|---|---|---|
| Seam A | Max |diff| comp 2–16 | 2.22 × 10⁻¹⁶ bar (machine epsilon) |
| Seam A | Compartment 1 max |diff| (informational) | 1.19 × 10⁻² bar |
| Seam B | Mean |diff| | 0.3 min |
| Seam B | Max |diff| | 1 min |
| Trips | Mean |diff| | 0.3 min |
| Trips | Max |diff| | 2 min |
| Trips | Dive comparisons | 54 |
Off-gas agreement on compartments 2–16 is at floating-point machine epsilon, confirming the Schreiner equation and surface-interval logic are numerically identical to decotengu. Seeded deco and trip totals agree within 1–2 min — within the stop-discretization noise explained in the single-dive section above.
Tests live in tests/ with filenames *.test.js (ES modules via import) or *.test.mjs. Structure is:
// tests/myThing.test.js
import { myFn } from '../js/decoModel.js';
describe('myFn', () => {
test('returns the expected value at surface', () => {
expect(myFn(0)).toBeCloseTo(1.01325, 4);
});
});For a new algorithm feature:
- Add unit tests covering the equation at a few hand-calculated points and the equation's boundary behaviour (surface, max depth, zero time).
- If the feature changes deco output, regenerate
tests/decojs-baseline.jsondeliberately and commit the diff in a separate commit so the regression test stays meaningful. - If the feature could diverge from decotengu, note the divergence in
tests/decotengu-comparison.test.mjscomment block and widen the tolerance locally for the affected scenarios rather than globally.
Bug fixes should include a regression test that fails without the fix (per CLAUDE.md convention).
What is currently not covered — honest inventory so callers know where to be careful:
-
UI components.
DiveSetupEditorhas narrow jsdom regressions for cylinder-volume notation, bottom/deco MOD labels, and debounced automatic profile generation. The P-P charts have focused state and interaction regressions, including timeline synchronization and the M-value intersection ruler, but no pixel-level render tests; Chart.js output is not asserted. -
i18n. No tests for translation loading,
data-i18nsubstitution, or thelanguagechangeevent fan-out to components. -
Keyboard shortcuts. The M-value ruler's
Tshortcut and the fullscreenFshortcut in all three chart classes are tested. Arrow-key / space / home / end playback shortcuts are not. -
Helium.
COMPARTMENTScarries He coefficients but the algorithm lumps He into N₂ vian2Fraction. Full trimix (separate He kinetics) is not implemented and not tested. Gas definitions accepthe > 0but no decotengu-reference scenarios exercise it. -
SAC / gas consumption edge cases.
computeGasConsumptionhas basic coverage but not realistic multi-dive or bail-out scenarios. - Water mode. Unit tests cover the exact EN 13319, freshwater, and seawater factors; compatibility tests prove missing configuration remains exactly equivalent to the historical 0.1 bar/m path; URL and editor tests cover selection and fallback; and 105 pinned DecoTengu scenarios cover schedules.
Cross-link: see the individual algorithm chapters (Algo-02-NDL-Calculation, Algo-03-First-Stop-Ramped-GF, Algo-04-Deco-Stop-Loop, Algo-05-Multi-Gas-Switching) for which algorithm each test file exercises.