src/lib/dive-mixtures.test.ts
Rama main · réplica de lectura del repositorio original.
import { describe, it, expect } from "vitest";
import {
PPO2_DECO,
PPO2_WORKING,
diveModWarning,
ead,
endDepth,
gasHintParts,
ppO2Limit,
gasName,
tankGas,
mod,
modWarning,
ppO2AtDepth,
} from "./dive-mixtures";
describe("tankGas", () => {
it("marks each gas the way its name reads", () => {
expect(tankGas(20.99, 0)).toBe("air");
expect(tankGas(32, 0)).toBe("nitrox");
expect(tankGas(99.5, 0)).toBe("oxygen");
expect(tankGas(21, 35)).toBe("trimix");
});
it("draws a gas it cannot name plain, as air", () => {
// Unrecorded, and impossible: neither says what was breathed.
expect(tankGas(null, null)).toBe("air");
expect(tankGas(32, null)).toBe("air");
expect(tankGas(50, 60)).toBe("air");
});
});
describe("gasName", () => {
it("names the three gases that have their own word", () => {
expect(gasName(21, 0)).toBe("Air");
expect(gasName(100, 0)).toBe("Oxygen");
expect(gasName(32, 0)).toBe("EAN32");
});
it("treats the spread of fractions devices record for air as air", () => {
// A real analyzer reads 20.9; Suunto exports 21; the API rounds parsed
// fractions to 2 decimals, so 20.99 reaches the UI intact.
expect(gasName(20.9, 0)).toBe("Air");
expect(gasName(20.99, 0)).toBe("Air");
expect(gasName(21.0, 0)).toBe("Air");
});
it("still names a deliberately enriched mix just above air as nitrox", () => {
expect(gasName(22, 0)).toBe("EAN22");
});
it("names trimix as the O2/He pair divers write on the cylinder", () => {
expect(gasName(21, 35)).toBe("21/35");
expect(gasName(18, 45)).toBe("18/45");
expect(gasName(10, 70)).toBe("10/70");
});
it("rounds to whole percent, since the shorthand is integer shorthand", () => {
// The tank cards print the unrounded fractions under the name, so the label
// rounding never hides the real number.
expect(gasName(32.4, 0)).toBe("EAN32");
expect(gasName(20.99, 35.01)).toBe("21/35");
});
it("reads a near-100% fill as pure oxygen", () => {
expect(gasName(99.6, 0)).toBe("Oxygen");
});
it("returns null when either fraction is unrecorded", () => {
// A parsed preview reports what the file held and null for what it didn't.
// Helium alone being unknown is enough: air and a helium mix are then
// indistinguishable, and no honest label covers both.
expect(gasName(null, 0)).toBeNull();
expect(gasName(undefined, 0)).toBeNull();
expect(gasName(32, null)).toBeNull();
expect(gasName(null, null)).toBeNull();
});
it("returns null for a non-finite fraction rather than printing NaN", () => {
// The fallback branch formats its input straight into user-facing text, so a
// NaN has to be turned away before it gets there.
expect(gasName(NaN, 0)).toBeNull();
expect(gasName(21, NaN)).toBeNull();
});
it("refuses to give an impossible mix a plausible-looking name", () => {
// 50/60 passes no CHECK and no refine, but does reach a parsed preview.
expect(gasName(50, 60)).toBe("O₂ 50% / He 60%");
expect(gasName(0, 0)).toBe("O₂ 0% / He 0%");
expect(gasName(-5, 0)).toBe("O₂ -5% / He 0%");
});
it("names a mix summing to exactly 100 as normal", () => {
expect(gasName(30, 70)).toBe("30/70");
});
});
describe("mod", () => {
it("computes the working limit at the default 1.4 bar", () => {
// 1.4 / 0.32 = 4.375 bar ambient -> 3.375 bar of water -> 33.75 m
expect(mod(32)).toBeCloseTo(33.75, 5);
});
it("takes the deco ceiling as an explicit parameter", () => {
// 1.6 / 0.32 = 5 bar -> 40 m, the number on every EAN32 deco table.
expect(mod(32, PPO2_DECO)).toBeCloseTo(40, 5);
});
it("puts air's working limit where recreational depth limits sit", () => {
expect(mod(21, PPO2_WORKING)).toBeCloseTo(56.6667, 3);
});
it("puts pure oxygen at its familiar 6 m deco depth", () => {
expect(mod(100, PPO2_DECO)).toBeCloseTo(6, 5);
});
it("returns null rather than infinity for an oxygen-free mix", () => {
expect(mod(0)).toBeNull();
expect(mod(-1)).toBeNull();
});
it("returns null for an unrecorded or half-typed fraction", () => {
expect(mod(null)).toBeNull();
expect(mod(undefined)).toBeNull();
expect(mod(NaN)).toBeNull();
});
it("returns null for a mix already past its limit at the surface", () => {
// Reachable because `po2_limit` is diver-editable across [0.4, 2.0]: at 1.4 or
// 1.6 this would need oxygen above 140 %. Pins that EAN50 planned to 0.4 does
// not render "-2.0 m" on the mixtures card or in the form hint.
expect(mod(50, 0.4)).toBeNull();
expect(mod(100, 0.4)).toBeNull();
});
it("still answers 0 m for a mix that is exactly at its limit at the surface", () => {
// The boundary is a real answer, not the failure above: 0.4 / 0.4 = 1 bar
// ambient, which is the surface. Only *below* zero is meaningless.
expect(mod(40, 0.4)).toBeCloseTo(0, 5);
});
});
describe("ppO2AtDepth", () => {
it("gives the surface partial pressure at zero depth", () => {
expect(ppO2AtDepth(0, 21)).toBeCloseTo(0.21, 5);
});
it("scales with ambient pressure", () => {
// 30 m = 4 bar ambient; 4 x 0.32 = 1.28
expect(ppO2AtDepth(30, 32)).toBeCloseTo(1.28, 5);
});
it("agrees with mod at the depth mod returns", () => {
const limit = mod(32, PPO2_WORKING);
expect(limit).not.toBeNull();
expect(ppO2AtDepth(limit as number, 32)).toBeCloseTo(PPO2_WORKING, 5);
});
it("returns null for an unrecorded fraction", () => {
expect(ppO2AtDepth(30, null)).toBeNull();
});
});
describe("endDepth", () => {
it("counts everything that is not helium as narcotic by default", () => {
// 21/35 at 45 m: 5.5 bar x 0.65 = 3.575 bar -> 25.75 m
expect(endDepth(45, 35, 21)).toBeCloseTo(25.75, 5);
});
it("leaves a helium-free mix at its actual depth", () => {
expect(endDepth(40, 0, 21)).toBeCloseTo(40, 5);
});
it("does not need the oxygen fraction in the default mode", () => {
// Nothing but helium enters the sum when oxygen is counted narcotic, so an
// unrecorded oxygen must not block the answer.
expect(endDepth(45, 35, null)).toBeCloseTo(25.75, 5);
});
it("gives the shallower older figure when oxygen is not counted narcotic", () => {
// 21/35 at 45 m, N2 only: 5.5 bar x 0.44 = 2.42 bar -> 14.2 m
expect(endDepth(45, 35, 21, { o2Narcotic: false })).toBeCloseTo(14.2, 5);
});
it("needs the oxygen fraction when oxygen is excluded from the sum", () => {
expect(endDepth(45, 35, null, { o2Narcotic: false })).toBeNull();
});
it("returns null when helium is unrecorded", () => {
expect(endDepth(45, null, 21)).toBeNull();
});
it("floors at the surface rather than going negative", () => {
// 18/45 at 4 m works out to -2.3 m: a helium mix in shallow water is less
// narcotic than air at the surface. Reachable in the live form the moment a
// diver has typed the "4" of a "45". Mirrors `ead`'s floor.
expect(endDepth(4, 45, 18)).toBe(0);
expect(endDepth(0, 70, 10)).toBe(0);
});
});
describe("ead", () => {
it("puts a nitrox dive shallower than it was", () => {
// EAN32 at 30 m: 4 bar x 68/79 = 3.4430 bar -> 24.43 m
expect(ead(30, 32)).toBeCloseTo(24.4304, 3);
});
it("leaves air at its actual depth", () => {
expect(ead(30, 21)).toBeCloseTo(30, 5);
});
it("floors at the surface rather than going negative", () => {
// A very rich mix in shallow water is equivalent to standing on the boat.
expect(ead(3, 80)).toBe(0);
});
it("refuses a mix containing helium", () => {
// That gas has an END, not an EAD; answering here would understate the real
// nitrogen load.
expect(ead(45, 21, 35)).toBeNull();
});
it("defaults to the helium-free case so ead(depth, o2) reads naturally", () => {
expect(ead(30, 32)).toBe(ead(30, 32, 0));
});
it("returns null for an unrecorded fraction", () => {
expect(ead(30, null)).toBeNull();
expect(ead(30, 32, null)).toBeNull();
});
it("refuses a non-finite helium instead of ignoring it", () => {
// `helium > 0` compares false for NaN, which would have computed an EAD as
// though the cylinder held no helium at all.
expect(ead(30, 32, NaN)).toBeNull();
});
});
describe("modWarning", () => {
it("stays quiet for a mix well inside its working limit", () => {
// EAN32 works to 33.75 m.
expect(modWarning({ oxygen: 32, helium: 0 }, 30, "metric")).toBeNull();
});
it("stays quiet exactly at the working limit", () => {
expect(modWarning({ oxygen: 32, helium: 0 }, 33.75, "metric")).toBeNull();
});
it("stays quiet at a limit binary arithmetic lands just under", () => {
// The gases whose 1.4 limit is a round number in decimal and a few ULPs under
// it in floats. EAN32 above is float-exact, so only these exercise the tolerance.
expect(modWarning({ oxygen: 28, helium: 0 }, 40, "metric")).toBeNull();
expect(modWarning({ oxygen: 40, helium: 0 }, 25, "metric")).toBeNull();
expect(modWarning({ oxygen: 100, helium: 0 }, 4, "metric")).toBeNull();
});
it("still flags the smallest breach a dive file can record", () => {
// The tolerance is a nanometre, so the centimetre a recorded depth resolves to
// is nowhere near it: EAN28 one centimetre past 40 m is still a breach.
expect(modWarning({ oxygen: 28, helium: 0 }, 40.01, "metric")).toContain(
"40 m working limit",
);
});
it("repeats a number when the depth carries more than the API stores", () => {
// The metric residual, and the only one: a parsed depth is whatever the computer
// wrote (`max_depth` is an unrounded float all the way down), so a breach finer
// than the centimetre the limit prints at has no room to show.
expect(modWarning({ oxygen: 32, helium: 0 }, 33.7512, "metric")).toContain(
"33.75 m is past this mix's 33.75 m working limit",
);
});
it("names a limit the depth is genuinely past, not the depth again", () => {
// 33.8 m is 5 cm past EAN32's 33.75 m. The limit prints at the scale it is true
// at and rounds down, so the sentence never puts one number on both sides of
// "is past".
expect(modWarning({ oxygen: 32, helium: 0 }, 33.8, "metric")).toContain(
"33.8 m is past this mix's 33.75 m working limit",
);
});
it("flags a working-limit breach as a planning note, naming the working limit", () => {
const warning = modWarning({ oxygen: 32, helium: 0 }, 36, "metric");
expect(warning).toContain("33.75 m working limit");
expect(warning).toContain(String(PPO2_WORKING));
// The distinguishing half: still legal for a stop, which is why this is not
// the louder sentence.
expect(warning).toContain("decompression");
});
it("flags a deco-ceiling breach as the harder finding", () => {
// EAN32's 1.6 ceiling is 40 m.
const warning = modWarning({ oxygen: 32, helium: 0 }, 45, "metric");
expect(warning).toContain("40 m limit");
expect(warning).toContain(String(PPO2_DECO));
expect(warning).not.toContain("decompression");
});
it("separates the two thresholds rather than collapsing them", () => {
const working = modWarning({ oxygen: 32, helium: 0 }, 36, "metric");
const deco = modWarning({ oxygen: 32, helium: 0 }, 45, "metric");
expect(working).not.toBeNull();
expect(deco).not.toBeNull();
expect(working).not.toBe(deco);
});
it("catches a deco bottle taken to the bottom", () => {
// EAN50 tops out at 22 m even on the 1.6 ceiling.
expect(modWarning({ oxygen: 50, helium: 0 }, 40, "metric")).toContain(
"22 m limit",
);
});
it("says nothing when the dive records no maximum depth", () => {
// Nothing to judge against, and inventing a depth would be worse than silence.
expect(modWarning({ oxygen: 100, helium: 0 }, null, "metric")).toBeNull();
expect(
modWarning({ oxygen: 100, helium: 0 }, undefined, "metric"),
).toBeNull();
});
it("says nothing when the oxygen fraction is unusable", () => {
expect(modWarning({ oxygen: null, helium: 0 }, 40, "metric")).toBeNull();
expect(modWarning({ oxygen: 0, helium: 0 }, 40, "metric")).toBeNull();
});
it("leaves a normal trimix bottom gas alone at depth", () => {
// 18/45 works to 67.77 m - the whole point of the mix.
expect(modWarning({ oxygen: 18, helium: 45 }, 60, "metric")).toBeNull();
});
});
describe("gasHintParts", () => {
it("always leads with the gas name and its MOD", () => {
expect(
gasHintParts({ oxygen: 32, helium: 0, depth: null, units: "metric" }),
).toEqual(["EAN32", "MOD 33.75 m @ ppO₂ 1.4"]);
});
it("adds EAD for nitrox once a depth is known", () => {
expect(
gasHintParts({ oxygen: 32, helium: 0, depth: 30, units: "metric" }),
).toEqual(["EAN32", "MOD 33.75 m @ ppO₂ 1.4", "EAD 24.4 m at 30 m"]);
});
it("adds END rather than EAD for a helium mix", () => {
const parts = gasHintParts({
oxygen: 21,
helium: 35,
depth: 45,
units: "metric",
});
expect(parts).toContain("END 25.8 m at 45 m (O₂ narcotic)");
expect(parts.some((part) => part.startsWith("EAD"))).toBe(false);
});
it("gives air neither, since both would just restate the depth", () => {
expect(
gasHintParts({ oxygen: 21, helium: 0, depth: 30, units: "metric" }),
).toEqual(["Air", "MOD 56.66 m @ ppO₂ 1.4"]);
});
it("treats the whole air band as air, not just exactly 21", () => {
// Guards the rule against `gasName`'s label: a `!== "Air"` string comparison
// would break silently if that label changed.
expect(
gasHintParts({ oxygen: 20.99, helium: 0, depth: 30, units: "metric" }),
).toHaveLength(2);
expect(
gasHintParts({ oxygen: 20.9, helium: 0, depth: 30, units: "metric" }),
).toHaveLength(2);
});
it("drops the depth-dependent figures when the depth is unknown", () => {
// What a multi-cylinder dive passes: which gas saw which depth is unknowable.
expect(
gasHintParts({ oxygen: 54, helium: 0, depth: null, units: "metric" }),
).toEqual(["EAN54", "MOD 15.92 m @ ppO₂ 1.4"]);
});
it("returns nothing at all while the gas is unidentifiable", () => {
expect(
gasHintParts({
oxygen: undefined,
helium: 0,
depth: 30,
units: "metric",
}),
).toEqual([]);
expect(
gasHintParts({ oxygen: 32, helium: null, depth: 30, units: "metric" }),
).toEqual([]);
});
it("never lets an END run negative in shallow water", () => {
// The floor belongs to `endDepth`, but this is the path that renders it.
const parts = gasHintParts({
oxygen: 18,
helium: 45,
depth: 4,
units: "metric",
});
expect(parts).toContain("END 0.0 m at 4 m (O₂ narcotic)");
expect(parts.some((part) => part.includes("-"))).toBe(false);
});
it("qualifies END with the narcosis convention it used", () => {
// `o2Narcotic` defaults true; the older N2-only convention gives 14.2 m for
// this gas, so the figure has to say which one it is.
expect(
gasHintParts({ oxygen: 21, helium: 35, depth: 45, units: "metric" }),
).toContain("END 25.8 m at 45 m (O₂ narcotic)");
});
it("names an impossible mix but claims nothing else about it", () => {
// Not even a MOD: every figure below the name would be derived from fractions
// that cannot coexist in one cylinder.
expect(
gasHintParts({ oxygen: 50, helium: 60, depth: 30, units: "metric" }),
).toEqual(["O₂ 50% / He 60%"]);
});
});
describe("diveModWarning", () => {
const AIR = { oxygen: 21, helium: 0 };
const EAN32 = { oxygen: 32, helium: 0 };
const EAN54 = { oxygen: 54, helium: 0 };
it("judges a single cylinder against the dive's max depth", () => {
// One tank was breathed throughout, so max depth is a depth this gas saw.
expect(diveModWarning([EAN32], 45, "metric")).toContain("40 m limit");
});
it("keeps the working-limit note for a single cylinder", () => {
expect(diveModWarning([EAN32], 36, "metric")).toContain("working limit");
});
it("stays silent on a single cylinder well within its limit", () => {
expect(diveModWarning([EAN32], 30, "metric")).toBeNull();
});
it("does not blame a staged deco bottle for the dive's max depth", () => {
// The bug this function exists to fix: air + EAN54 to 45.91 m is a normal
// planned deco dive. EAN54 tops out at 19.62 m and was breathed on the
// ascent, never at the bottom - warning about it fires on every correctly
// planned technical dive.
expect(diveModWarning([AIR, EAN54], 45.91, "metric")).toBeNull();
});
it("still catches a dive no gas on board could have been breathed at", () => {
// Air is the deepest-capable of the two and tops out at 66.19 m; nothing here
// reaches 80 m, whichever order they were breathed in.
const warning = diveModWarning([AIR, EAN54], 80, "metric");
expect(warning).toContain("No gas logged for this dive");
expect(warning).toContain("66.19 m");
});
it("names the deepest-capable gas, not the first or the worst", () => {
const warning = diveModWarning([EAN54, EAN32, AIR], 80, "metric");
// Air's 66.19 m, not EAN54's 19.62 m or EAN32's 40 m.
expect(warning).toContain("66.19 m");
});
it("judges a flagged parallel pair holding one gas as that gas", () => {
// A sidemount pair breathed alternately at the same depth is one gas plan,
// not a switch plan: there is one mix on board and it was breathed
// throughout, so the dive's max depth is a depth it genuinely saw. Without
// this the pair falls to the multi-cylinder rule, which only reports a depth
// *no* gas could reach and would clear a real over-MOD dive.
const pair = [
{ ...EAN32, usage: "parallel" as const },
{ ...EAN32, usage: "parallel" as const },
];
expect(diveModWarning(pair, 45, "metric")).toContain("40 m limit");
});
it("keeps the working-limit note for a flagged parallel pair too", () => {
// The half the multi-cylinder rule deliberately drops, and it is right to
// keep here: with one gas on board, exceeding 1.4 is not the normal intended
// state of affairs it is on a dive carrying a deco bottle.
const pair = [
{ ...EAN32, usage: "parallel" as const },
{ ...EAN32, usage: "parallel" as const },
];
expect(diveModWarning(pair, 36, "metric")).toContain("working limit");
});
it("needs the flag as well as the shared gas", () => {
// Two identical cylinders with no flag are not known to have been breathed
// together - a spare of the same mix, carried and switched to, is the same
// two rows. The diver's answer is what makes the single-mix reading sound.
//
// The dive-wide sentence, not the single-mix one, and the difference is
// visible: the flagged pair above names EAN32's own 40 m *limit*, while
// this reports that no gas on board reaches the depth. Same number, two
// different claims.
const warning = diveModWarning([EAN32, EAN32], 45, "metric");
expect(warning).toContain("No gas logged for this dive");
expect(warning).not.toContain("working limit");
});
it("reads an absent helium as zero when comparing a flagged pair's gas", () => {
// `OxygenFractions` allows `helium` absent, while the form writes a flat 0 -
// so a pair mixing the two spellings is the same gas and has to be judged as
// one, not dropped to the dive-wide rule on a `undefined` versus `0`
// comparison.
const pair = [
{ oxygen: 32, usage: "parallel" as const },
{ oxygen: 32, helium: 0, usage: "parallel" as const },
];
expect(diveModWarning(pair, 45, "metric")).toContain("40 m limit");
});
it("needs the shared gas as well as the flag", () => {
// A flagged pair holding *different* gases is a switch plan again, and the
// multi-cylinder rule is what applies: EAN54 tops out at 19.62 m and was not
// necessarily what saw 45 m.
const mixed = [
{ ...AIR, usage: "parallel" as const },
{ ...EAN54, usage: "parallel" as const },
];
expect(diveModWarning(mixed, 45.91, "metric")).toBeNull();
});
it("treats a half-flagged pair as the multi-cylinder set it is", () => {
// One row Parallel, one still unset says nothing about how they were
// breathed together, so nothing stronger than the dive-wide rule is honest.
const half = [{ ...EAN32, usage: "parallel" as const }, EAN32];
expect(diveModWarning(half, 45, "metric")).toContain(
"No gas logged for this dive",
);
});
it("does not apply the 1.4 working limit across several cylinders", () => {
// 45.91 m is past air's 56.66 m working limit? No - but EAN32's is 33.75 m,
// and a multi-cylinder dive must not report that as a problem.
expect(diveModWarning([AIR, EAN32], 45, "metric")).toBeNull();
});
it("says nothing without a max depth to judge against", () => {
expect(diveModWarning([EAN54], null, "metric")).toBeNull();
expect(diveModWarning([AIR, EAN54], undefined, "metric")).toBeNull();
});
it("says nothing for a dive logging no cylinders", () => {
expect(diveModWarning([], 40, "metric")).toBeNull();
// Pinned at a depth that *would* warn against air: 60 m is past air's 56.66 m
// working limit, so a form that seeded an air cylinder would warn about a
// cylinder the page invented. Both forms can hold zero cylinders, so this guard
// is reachable rather than theoretical.
expect(diveModWarning([], 60, "metric")).toBeNull();
expect(diveModWarning([AIR], 60, "metric")).toContain("working limit");
});
it("stays quiet at exactly the deepest gas's ceiling", () => {
// The dive-wide branch carries the same rounding guard as `modWarning`, and
// the boundary it guards is inclusive: a dive sitting on air's 1.6 ceiling is
// one every gas on board reaches, not one none of them do.
const ceiling = mod(21, PPO2_DECO);
expect(ceiling).not.toBeNull();
expect(diveModWarning([AIR, EAN54], ceiling!, "metric")).toBeNull();
});
it("says nothing when no cylinder has a usable oxygen fraction", () => {
expect(
diveModWarning([{ oxygen: null }, { oxygen: 0 }], 40, "metric"),
).toBeNull();
});
it("ignores an unusable cylinder when judging the rest", () => {
// A half-typed row must not drag the deepest-capable figure around.
expect(diveModWarning([AIR, { oxygen: null }], 80, "metric")).toContain(
"66.19 m",
);
});
});
describe("ppO2Limit", () => {
it("uses what the dive recorded", () => {
expect(ppO2Limit({ po2_limit: 1.6 })).toBe(1.6);
});
it("falls back to the working limit when nothing was recorded", () => {
expect(ppO2Limit({})).toBe(PPO2_WORKING);
expect(ppO2Limit({ po2_limit: null })).toBe(PPO2_WORKING);
});
it("falls back rather than propagating a non-number", () => {
// A MOD of NaN would render, and a MOD computed from NaN would render as an
// empty cell that reads like "this gas has no limit".
expect(ppO2Limit({ po2_limit: NaN })).toBe(PPO2_WORKING);
});
});
describe("gasHintParts in imperial", () => {
// Every depth in the hint converts; the ppO₂ beside it does not, because ppO₂ is
// bar on every dive computer ever made whichever units it is set to.
it("writes the MOD in feet and leaves the ppO₂ in bar", () => {
expect(
gasHintParts({ oxygen: 21, helium: 0, depth: null, units: "imperial" }),
).toEqual(["Air", "MOD 185.9 ft @ ppO₂ 1.4"]);
});
it("writes both depths of an EAD in feet", () => {
expect(
gasHintParts({ oxygen: 32, helium: 0, depth: 30, units: "imperial" }),
).toEqual(["EAN32", "MOD 110.7 ft @ ppO₂ 1.4", "EAD 80 ft at 98 ft"]);
});
});
describe("the warnings in imperial", () => {
it("keeps the two depths apart at a tenth of a foot", () => {
// Whole feet cannot state this comparison: 40.02 m and EAN28's 40 m limit are
// both 131 ft. A tenth is 3 cm, the nearest imperial has to what the API stores.
expect(modWarning({ oxygen: 28, helium: 0 }, 40.02, "imperial")).toContain(
"131.3 ft is past this mix's 131.2 ft working limit",
);
});
it("still repeats a number for a breach inside a tenth of a foot", () => {
// The residual a tenth of a foot leaves: 3 cm per step against depths recorded to
// the centimetre. Hundredths of a foot would close it and put two digits nobody
// analyses a cylinder to on every MOD the app shows.
expect(modWarning({ oxygen: 21, helium: 0 }, 56.67, "imperial")).toContain(
"185.9 ft is past this mix's 185.9 ft working limit",
);
});
it("names both depths in feet", () => {
const warning = modWarning({ oxygen: 32, helium: 0 }, 45, "imperial");
expect(warning).toContain("147.6 ft is past this mix's 131.2 ft limit");
expect(warning).toContain("ppO₂ 1.6");
});
});
describe("gasHintParts with a recorded ppO2 limit", () => {
it("computes the MOD at the dive's own limit and names it", () => {
// EAN50 at 1.6 reaches 22 m, against 18 m at the 1.4 working limit - which is
// the whole point of staging it.
const parts = gasHintParts({
oxygen: 50,
helium: 0,
depth: null,
ppO2: 1.6,
units: "metric",
});
expect(parts).toContain("MOD 22 m @ ppO₂ 1.6");
});
it("names the fallback it used when the dive recorded none", () => {
// The label always describes the number beside it, so an absent limit still
// says which one the MOD came from rather than leaving it unqualified.
const parts = gasHintParts({
oxygen: 50,
helium: 0,
depth: null,
units: "metric",
});
expect(parts).toContain(`MOD 18 m @ ppO₂ ${PPO2_WORKING}`);
});
});
describe("a recorded ppO2 limit does not move the warning thresholds", () => {
it("still warns about a gas the dive recorded a high limit for", () => {
// The tank card's MOD would show 30 m for this gas at ppO₂ 2.0, and 32 m is inside
// it. The warning is about what the gas can physiologically take, not about
// what the dive planned - see `diveModWarning`. A file must not be able to
// silence it.
const warning = diveModWarning(
[{ oxygen: 50, po2_limit: 2.0 }],
32,
"metric",
);
expect(warning).not.toBeNull();
expect(warning).toContain("22 m");
});
});