import { describe, it, expect } from "vitest"; import { applyParsedDiveToForm, type InferredFigures, } from "./dive-file-import"; import { describeMixtureImport } from "@/lib/dive-import"; import { DEFAULT_MIXTURE } from "./mixture-fields"; import type { ParsedDive, ParsedDiveMixture } from "@/lib/api/dives"; import type { DiveMixtureInput } from "@/lib/validations/dive"; // A mixture exactly as the API returns one, i.e. with every field explicitly // present - `null` is a real answer here ("the file didn't record this"), not an // omission, so the fixture spells all of them out. function parsed(overrides: Partial = {}): ParsedDiveMixture { return { volume: null, start_pressure: null, end_pressure: null, oxygen: null, helium: null, po2_limit: null, gas_number: null, role: null, ...overrides, }; } // A cylinder already on the form, as an earlier import or the diver left it. function onForm(overrides: Partial = {}): DiveMixtureInput { return { ...DEFAULT_MIXTURE, ...overrides }; } // A form stub with just the two methods `applyParsedDiveToForm` touches. The // mixtures it returns are the literal initial state both dive pages use. function formHolding(mixtures: DiveMixtureInput[]) { return { getValues: (name?: string) => (name === "mixtures" ? mixtures : undefined), setValue: () => {}, } as unknown as Parameters[0]; } function parsedDive( mixtures: ParsedDiveMixture[], overrides: Partial = {}, ) { return { dive_number: null, start_time: null, submerged_time: null, max_depth: null, avg_depth: null, bottom_temperature: null, mixtures, // The rest of the dive, which most dive-computer files leave unstated. notes: null, visibility: null, weight: null, water_type: null, altitude: null, type: null, rating: null, air_temperature: null, current: null, waves: null, weather: null, entry_type: null, boat_name: null, tags: [], // A setting of the device, never applied to the form - null is the ordinary // case, since only a FIT file records it at all. salinity: null, // Returned by the parse but never applied to the form - the API writes these // itself when the file is attached. Spelled out so this fixture stays a complete // `ParsedDive` rather than a partial one the compiler happens to accept. cns_start: null, cns_end: null, otu_start: null, otu_end: null, surface_pressure_bar: null, file_token: "token", ...overrides, }; } // Records what `applyParsedDiveToForm` wrote, for the scalar fields the mixture // tests above don't reach. `formHolding`'s stub swallows `setValue` entirely. function recordingForm(mixtures: DiveMixtureInput[] = []) { const written: Record = {}; const form = { getValues: (name?: string) => (name === "mixtures" ? mixtures : undefined), setValue: (name: string, value: unknown) => { written[name] = value; }, } as unknown as Parameters[0]; return { form, written }; } describe("applyParsedDiveToForm", () => { // What an import actually meets, rather than the hand-picked `existing` the unit // tests above pass in. Two real starting states, and the difference is the // whole point: the create form starts empty (see `new-dive-page-content.tsx` - a form must // not write gas the diver never entered), while a form the diver has added a // cylinder to, or that the last-dive prefill filled in, holds one. const seededForm = () => formHolding([{ ...DEFAULT_MIXTURE }]); const emptyForm = () => formHolding([]); it("flags a cylinder size the file didn't record, against a seeded form", () => { // Pins that the note fires even though the seed has a value: requiring that // *no* source had one silences it on the ordinary one-cylinder import, and the // seeded 11.1 L goes in looking like a reading - the ~26 %-low RMV case. A // seeded cylinder's size is the only way this note fires, and it is exactly // the case worth firing on. const notes = applyParsedDiveToForm( seededForm(), parsedDive([parsed({ oxygen: 32 })]), () => {}, ); expect(notes.guessed.volume).toBe("form"); expect(describeMixtureImport(notes)).toContain("cylinder size"); }); it("flags gas and volume for an export that records neither", () => { // The 2026 Suunto Ocean JSON shape: pressures and nothing else. const notes = applyParsedDiveToForm( seededForm(), parsedDive([parsed({ start_pressure: 205.11, end_pressure: 91.55 })]), () => {}, ); expect(notes.guessed).toEqual({ volume: "form", oxygen: "form", helium: "form", }); const note = describeMixtureImport(notes); expect(note).toContain("cylinder size"); expect(note).toContain("gas mix"); }); it("leaves the size blank and says nothing when the form held nothing", () => { // The create form's actual starting state. `existingMixtureFor` won't pair a // one-cylinder file against a zero-cylinder form, so nothing supplies the volume // and it stays empty - which is what the API stores and what the dive page shows. // There is nothing on screen to warn about, and the empty box says it better // than a sentence 2 200 px above it could. let applied: DiveMixtureInput[] = []; const notes = applyParsedDiveToForm( emptyForm(), parsedDive([parsed({ oxygen: 32 })]), (mixtures) => { applied = mixtures; }, ); expect(applied[0].volume).toBe(""); expect(applied[0].helium).toBe(""); expect(applied[0].oxygen).toBe(32); expect(notes.guessed).toEqual({}); expect(describeMixtureImport(notes)).toBeNull(); }); it("reports no lost pressures importing onto an empty form", () => { // The counts differ (nought against one), which is what makes // `existingMixtureFor` refuse to pair - and on the *seeded* form that refusal // could cost real pressures. Here there was nothing to lose, so saying they were // cleared would send the diver looking for data that never existed. const notes = applyParsedDiveToForm( emptyForm(), parsedDive([parsed({ oxygen: 32, helium: 0, volume: 12 })]), () => {}, ); expect(notes.discardedPressures).toBe(false); expect(describeMixtureImport(notes)).toBeNull(); }); it("says nothing when the file recorded the lot", () => { const notes = applyParsedDiveToForm( seededForm(), parsedDive([parsed({ volume: 12, oxygen: 32, helium: 0 })]), () => {}, ); expect(notes.guessed).toEqual({}); expect(describeMixtureImport(notes)).toBeNull(); }); it("reports a carried-over value as the form's, not as a default", () => { // The diver's own 15 L is still not something the file recorded, so it is still // worth flagging - but it is not a default, and saying so is the whole distinction. const notes = applyParsedDiveToForm( formHolding([{ ...DEFAULT_MIXTURE, volume: 15 }]), parsedDive([parsed({ oxygen: 32 })]), () => {}, ); expect(notes.guessed.volume).toBe("form"); expect(describeMixtureImport(notes)).toContain("already on this form"); }); it("reports pressures carried onto a file that had none", () => { const notes = applyParsedDiveToForm( formHolding([{ ...DEFAULT_MIXTURE, start_pressure: 211.62 }]), parsedDive([parsed({ oxygen: 21, helium: 0, volume: 12 })]), () => {}, ); expect(notes.keptPressures).toBe(true); }); it("does not pair cylinders when the counts differ", () => { // The safety-critical half of the carry-over: position is the only signal, // so pairing a one-cylinder form onto a two-cylinder file could put a back // gas's pressures on a stage bottle and yield a confidently wrong RMV. // Inverting or deleting the guard left the whole suite green. let applied: DiveMixtureInput[] = []; const notes = applyParsedDiveToForm( formHolding([onForm({ start_pressure: 211.62, end_pressure: 127.16 })]), parsedDive([parsed({ oxygen: 21 }), parsed({ oxygen: 50 })]), (mixtures) => { applied = mixtures; }, ); expect(applied[0].start_pressure).toBe(""); // Blank rather than the form's 11.1 L, for the same reason the pressure above it // is blank: with no pairing there is no cylinder to carry anything from, and the // import path invents nothing. expect(applied[0].volume).toBe(""); expect(notes.keptPressures).toBe(false); // ...and the diver is told the pressures went, which on the edit form takes the // dive's `gas_use` with it. expect(notes.discardedPressures).toBe(true); expect(describeMixtureImport(notes)).toContain("were cleared"); }); describe("over a tank linked to the diver's cylinder", () => { const vessel = { uuid: "vessel-1", name: "AL80", rented: false, is_archived: false, volume: 11.1, }; const linked = onForm({ volume: 11.1, gear_item_uuid: vessel.uuid, gear_item: vessel, }); it("keeps the link and its volume where the counts match", () => { let applied: DiveMixtureInput[] = []; const notes = applyParsedDiveToForm( formHolding([linked]), parsedDive([parsed({ volume: 12, oxygen: 32, helium: 0 })]), (mixtures) => { applied = mixtures; }, ); expect(applied[0].gear_item_uuid).toBe(vessel.uuid); expect(applied[0].volume).toBe(11.1); expect(applied[0].oxygen).toBe(32); expect(notes.keptLinkedVolume).toBe(true); }); it("replaces the tanks and their links where the counts differ", () => { let applied: DiveMixtureInput[] = []; const notes = applyParsedDiveToForm( formHolding([linked]), parsedDive([parsed({ volume: 12 }), parsed({ volume: 7 })]), (mixtures) => { applied = mixtures; }, ); expect(applied.map((row) => row.gear_item_uuid)).toEqual(["", ""]); expect(applied.map((row) => row.volume)).toEqual([12, 7]); expect(notes.droppedLinks).toBe(true); }); it("keeps the link on a later file's fill, which touches only blanks", () => { const replaced: DiveMixtureInput[][] = []; const { form } = formHoldingValues({}, [{ ...linked, oxygen: "" }]); applyParsedDiveToForm( form, parsedDive([parsed({ volume: 12, oxygen: 32 })]), (mixtures) => replaced.push(mixtures), "fill-only", ); expect(replaced[0][0].gear_item_uuid).toBe(vessel.uuid); expect(replaced[0][0].volume).toBe(11.1); expect(replaced[0][0].oxygen).toBe(32); }); }); it("returns empty notes when the file carries no mixtures at all", () => { const notes = applyParsedDiveToForm(seededForm(), parsedDive([]), () => {}); expect(notes).toEqual({ guessed: {}, keptPressures: false, discardedPressures: false, keptLinkedVolume: false, droppedLinks: false, }); }); it("never takes the dive's water type from a file's salinity", () => { // A FIT file's `dive_settings.water_type` is the density the computer was // set to - `en13319` is a calibration, not a kind of water - so it stays the // recording's setting, and the dive's water type stays the diver's answer. const { form, written } = recordingForm(); applyParsedDiveToForm( form, parsedDive([], { salinity: "en13319" }), () => {}, ); expect(written).not.toHaveProperty("water_type"); }); it("takes the water type a file states for the dive itself", () => { const { form, written } = recordingForm(); applyParsedDiveToForm( form, parsedDive([], { water_type: "fresh", salinity: "en13319" }), () => {}, ); expect(written.water_type).toBe("fresh"); }); it("prefills the rest of the dive a logbook file states", () => { // A UDDF dive carries the diver's own entries beside the readings, and logbook // import stores every one of them; the form takes the same file the same way. const { form, written } = recordingForm(); applyParsedDiveToForm( form, parsedDive([], { notes: "Turtle at the safety stop", visibility: 15, weight: 4, altitude: 0, rating: 4, air_temperature: 31.5, current: "light", entry_type: "boat", tags: ["reef"], }), () => {}, ); expect(written).toMatchObject({ notes: "Turtle at the safety stop", visibility: 15, weight: 4, altitude: 0, rating: 4, air_temperature: 31.5, current: "light", entry_type: "boat", tags: ["reef"], }); }); it("leaves a member the file states nothing about to the form", () => { // The Suunto app writes `""` for notes on every Ocean file, and a file with no // tags reads as an empty list: neither may wipe what the last dive carried over. const { form, written } = recordingForm(); applyParsedDiveToForm( form, parsedDive([], { notes: "", tags: [], weight: null, boat_name: null }), () => {}, ); expect(written).not.toHaveProperty("notes"); expect(written).not.toHaveProperty("tags"); expect(written).not.toHaveProperty("weight"); expect(written).not.toHaveProperty("boat_name"); }); }); // A form already holding values, for the fill-only cases below. Unlike // `recordingForm` above it answers `getValues(name)` for scalars as well as for // `mixtures`, because fill-only asks the form what it already has before writing // anything - which is the whole mechanism under test. function formHoldingValues( values: Record, mixtures: DiveMixtureInput[] = [], ) { const written: Record = {}; const form = { getValues: (name?: string) => name === "mixtures" ? mixtures : values[name as string], setValue: (name: string, value: unknown) => { written[name] = value; values[name] = value; }, } as unknown as Parameters[0]; return { form, written }; } describe("applyParsedDiveToForm with a file that states only a day", () => { it("keeps a date-only dive a date rather than giving it a midnight", () => { const { form, written } = formHoldingValues({ start_time: "2002-06-18" }); applyParsedDiveToForm( form, parsedDive([], { start_time: "2002-06-19" }), () => {}, ); expect(written.start_time).toBe("2002-06-19"); }); it("still gives a dive with a clock the file's day at midnight", () => { // The create form needs an instant, and a diver can correct the hour. const { form, written } = formHoldingValues({ start_time: "2026-04-17T11:49:23+02:00", }); applyParsedDiveToForm( form, parsedDive([], { start_time: "2002-06-19" }), () => {}, ); expect(written.start_time).toMatch(/^2002-06-19T00:00:00[+-]\d{2}:\d{2}$/); }); }); describe("applyParsedDiveToForm in fill-only mode", () => { it("leaves a figure the form already carries exactly as it is", () => { // The case the rule exists for: the Suunto app's JSON has been imported, and // the same computer's FIT of the same dive follows. The FIT's own `avg_depth` // 9.49 and `submerged_time` 3474 are that machine's second telling of numbers the // form already holds from the first, and overwriting them would silently // replace figures the diver has been looking at - and may have corrected. // Marked as stating them: a figure the file derived is the case below. const { form, written } = formHoldingValues({ avg_depth: 10.74, submerged_time: "50:51", max_depth: 19.04, }); applyParsedDiveToForm( form, parsedDive([], { avg_depth: 9.49, submerged_time: 3474, max_depth: 19.1, inferred: [], }), () => {}, "fill-only", ); expect(written).not.toHaveProperty("avg_depth"); expect(written).not.toHaveProperty("submerged_time"); expect(written).not.toHaveProperty("max_depth"); }); it("still fills a field the form left empty", () => { // "Fills, never overwrites" is two claims, and this is the half that makes // the second file worth attaching at all. A bottom temperature the JSON // never recorded is one the FIT can supply. const { form, written } = formHoldingValues({ avg_depth: 10.74, bottom_temperature: undefined, }); applyParsedDiveToForm( form, parsedDive([], { avg_depth: 9.49, bottom_temperature: 21.8 }), () => {}, "fill-only", ); expect(written.bottom_temperature).toBe(21.8); expect(written).not.toHaveProperty("avg_depth"); }); it("reads a cleared number input as empty rather than as a value", () => { // A number field the diver cleared reads back as `NaN`, not `undefined` - // the one non-obvious empty state on this form, and the one a `!= null` // guard gets wrong. const { form, written } = formHoldingValues({ max_depth: Number.NaN }); applyParsedDiveToForm( form, parsedDive([], { max_depth: 19.1 }), () => {}, "fill-only", ); expect(written.max_depth).toBe(19.1); }); it("fills the dive's other members only where the form is empty", () => { const { form, written } = formHoldingValues({ notes: "Typed on the boat", tags: [], weight: undefined, }); applyParsedDiveToForm( form, parsedDive([], { notes: "From the logbook", tags: ["reef"], weight: 4 }), () => {}, "fill-only", ); expect(written).not.toHaveProperty("notes"); expect(written.tags).toEqual(["reef"]); expect(written.weight).toBe(4); }); it("treats zero as a reading, not as an absence", () => { // A freedive that surfaced logs `max_depth` 0, and a falsiness check would // overwrite it from the second file. const { form, written } = formHoldingValues({ max_depth: 0 }); applyParsedDiveToForm( form, parsedDive([], { max_depth: 19.1 }), () => {}, "fill-only", ); expect(written).not.toHaveProperty("max_depth"); }); it("fills a blank cylinder member without touching the ones that are filled", () => { // The case the whole rule was designed around, on the form: the Suunto // app's JSON gives cylinder 0 its pressures and no oxygen fraction, and the // same computer's FIT of the same dive has the fraction. The FIT's oxygen // lands; the pressures stay the JSON's, because the FIT's would be a second // fill's figures read against the first's. const replaced: DiveMixtureInput[][] = []; const { form } = formHoldingValues({}, [ onForm({ start_pressure: 205.11, end_pressure: 91.55, oxygen: "" }), ]); applyParsedDiveToForm( form, parsedDive([ parsed({ oxygen: 33, start_pressure: 210, end_pressure: 80 }), ]), (mixtures) => replaced.push(mixtures), "fill-only", ); expect(replaced).toHaveLength(1); expect(replaced[0][0].oxygen).toBe(33); expect(replaced[0][0].start_pressure).toBe(205.11); expect(replaced[0][0].end_pressure).toBe(91.55); }); it("will not reshape a cylinder list it cannot line up", () => { // A file describing a different number of cylinders cannot be lined up // with the form's rows by position, and the API pairs it at the attach. // Carrying a stage bottle's readings onto a back gas is worse than leaving // a blank. const replaced: DiveMixtureInput[][] = []; const { form } = formHoldingValues({}, [onForm({ oxygen: 32 })]); applyParsedDiveToForm( form, parsedDive([parsed({ oxygen: 33 }), parsed({ oxygen: 50 })]), (mixtures) => replaced.push(mixtures), "fill-only", ); expect(replaced).toHaveLength(0); }); it("takes the file's cylinders whole onto a form that has none", () => { // Filling by definition: there is nothing to overwrite. The create form // starts here, and so does an edit form for a dive logged without gas. const replaced: DiveMixtureInput[][] = []; const { form } = formHoldingValues({}, []); applyParsedDiveToForm( form, parsedDive([parsed({ oxygen: 33 })]), (mixtures) => replaced.push(mixtures), "fill-only", ); expect(replaced[0]).toHaveLength(1); expect(replaced[0][0].oxygen).toBe(33); }); it("raises no guessed-value notes, having guessed nothing", () => { // Those sentences exist to flag a value the import invented and the diver // should check. A fill that writes only into blanks has invented nothing, // and a note here would send them to check a field nothing touched. const { form } = formHoldingValues({}, [onForm({ volume: 12 })]); const notes = applyParsedDiveToForm( form, parsedDive([parsed({ oxygen: 33 })]), () => {}, "fill-only", ); expect(notes.guessed).toEqual({}); expect(describeMixtureImport(notes)).toBeNull(); }); }); // A form row recording only what the overrides say - `onForm` starts from // `DEFAULT_MIXTURE`'s air, which would be a recorded mix in every case below. function blankRow(overrides: Partial = {}): DiveMixtureInput { return { volume: "", start_pressure: "", end_pressure: "", oxygen: "", helium: "", po2_limit: "", role: "", usage: "", ...overrides, }; } // Applies a later file's cylinders onto a form holding `rows`, and returns the // list the form was given, or `undefined` where it was left alone. function fillOnto(rows: DiveMixtureInput[], cylinders: ParsedDiveMixture[]) { const replaced: DiveMixtureInput[][] = []; const { form } = formHoldingValues({}, rows); applyParsedDiveToForm( form, parsedDive(cylinders), (mixtures) => replaced.push(mixtures), "fill-only", ); return replaced[0]; } describe("a later file's cylinders on a form that has some", () => { it("fills no cylinder from a file listing the form's in another order", () => { expect( fillOnto( [ blankRow({ oxygen: 21, helium: 0 }), blankRow({ oxygen: 50, helium: 0 }), ], [ parsed({ oxygen: 50, helium: 0, start_pressure: 200 }), parsed({ oxygen: 21, helium: 0, start_pressure: 210 }), ], ), ).toBeUndefined(); }); it("fills none where a cylinder's mix is another row's", () => { // The API pairs the file's 21 % with the form's second row by mix, and the // file's other cylinder with the first row by position - not the pairs a // positional fill would make. expect( fillOnto( [blankRow(), blankRow({ oxygen: 21, helium: 0 })], [ parsed({ oxygen: 21, helium: 0, volume: 12 }), parsed({ start_pressure: 200, end_pressure: 60 }), ], ), ).toBeUndefined(); }); it("fills none where a position pair records two different fractions", () => { expect( fillOnto([blankRow({ oxygen: 32 })], [parsed({ oxygen: 33 })]), ).toBeUndefined(); }); it("fills none from pressures with no mix over rows of 21 % and 50 %", () => { // Which tank drained to 60 bar is a guess the file's mixes would settle. expect( fillOnto( [ blankRow({ oxygen: 21, helium: 0 }), blankRow({ oxygen: 50, helium: 0 }), ], [ parsed({ start_pressure: 200, end_pressure: 60 }), parsed({ start_pressure: 210, end_pressure: 150 }), ], ), ).toBeUndefined(); }); it("fills none from a second computer's mixes over rows carrying pressures and no mix", () => { expect( fillOnto( [ blankRow({ start_pressure: 200, end_pressure: 60 }), blankRow({ start_pressure: 210, end_pressure: 150 }), ], [parsed({ oxygen: 21, helium: 0 }), parsed({ oxygen: 50, helium: 0 })], ), ).toBeUndefined(); }); it("fills a row that records nothing, and leaves the one that records a mix", () => { const rows = [blankRow({ oxygen: 21, helium: 0 }), blankRow()]; const filled = fillOnto(rows, [ parsed({ start_pressure: 200, end_pressure: 60 }), parsed({ start_pressure: 210, end_pressure: 150 }), ]); expect(filled?.[0]).toEqual(rows[0]); expect(filled?.[1].start_pressure).toBe(210); expect(filled?.[1].end_pressure).toBe(150); }); it("fills every row a cylinder pairs with by mix", () => { const filled = fillOnto( [ blankRow({ oxygen: 21, helium: 0 }), blankRow({ oxygen: 50, helium: 0 }), ], [ parsed({ oxygen: 21, helium: 0, start_pressure: 200 }), parsed({ oxygen: 50, helium: 0, start_pressure: 210 }), ], ); expect(filled?.map((row) => row.start_pressure)).toEqual([200, 210]); }); it("fills the one-cylinder pair's row whichever file came first", () => { // The Suunto Ocean's JSON carries the pressures and no mix, its FIT the mix // and no pressures. One row meeting one cylinder is a pair no later file can // re-pair, so it fills even though each side records what the other lacks. const fitThenJson = fillOnto( [blankRow({ oxygen: 21, helium: 0 })], [parsed({ start_pressure: 212.81, end_pressure: 83.59, gas_number: 0 })], ); const jsonThenFit = fillOnto( [blankRow({ start_pressure: 212.81, end_pressure: 83.59 })], [parsed({ oxygen: 21, helium: 0 })], ); for (const filled of [fitThenJson, jsonThenFit]) { expect(filled?.[0]).toMatchObject({ oxygen: 21, helium: 0, start_pressure: 212.81, end_pressure: 83.59, }); } }); it("writes no label, leaving the one a row has", () => { const filled = fillOnto( [blankRow(), blankRow({ gas_number: 1 })], [ parsed({ start_pressure: 200, gas_number: 0 }), parsed({ start_pressure: 210, gas_number: 3 }), ], ); expect(filled?.[0].start_pressure).toBe(200); expect(filled?.[0].gas_number).toBeUndefined(); expect(filled?.[1].gas_number).toBe(1); }); it("writes no label onto a form that had no cylinders", () => { const filled = fillOnto([], [parsed({ oxygen: 21, gas_number: 0 })]); expect(filled?.[0].oxygen).toBe(21); expect(filled?.[0].gas_number).toBeUndefined(); }); }); describe("a later file's fill that would re-pair the rows it fills", () => { it("fills none where a mix it writes makes a row another cylinder's match", () => { // Two air tanks. The form's second row records nothing, so it could take // the file's second cylinder's 21 %; but then the API, pairing the saved // rows by mix, gives it the file's first cylinder and that tank's // pressures, beside the first row's own. expect( fillOnto( [blankRow({ start_pressure: 200, end_pressure: 50 }), blankRow()], [ parsed({ oxygen: 21, start_pressure: 198, end_pressure: 52 }), parsed({ oxygen: 21 }), ], ), ).toBeUndefined(); }); }); describe("a later file's cylinders sharing one mix", () => { it("fills two blank rows from two air cylinders, which the API pairs in order", () => { const filled = fillOnto( [blankRow(), blankRow()], [ parsed({ oxygen: 21, helium: 0, start_pressure: 200 }), parsed({ oxygen: 21, helium: 0, start_pressure: 190 }), ], ); expect(filled?.map((row) => [row.oxygen, row.start_pressure])).toEqual([ [21, 200], [21, 190], ]); }); it("fills none where the API would give a shared mix's first row to a later cylinder", () => { // The first cylinder records no mix, so the API gives the second the first // row recording 21 % - the form's first, not its second. expect( fillOnto( [ blankRow({ oxygen: 21, helium: 0 }), blankRow({ oxygen: 21, helium: 0 }), ], [parsed({ start_pressure: 200 }), parsed({ oxygen: 21, helium: 0 })], ), ).toBeUndefined(); }); }); describe("applyParsedDiveToForm with a figure an earlier file derived", () => { // One Suunto Ocean dive's two exports, as the API reads them. The FIT states // neither figure, so the reader derives both; the JSON states both. Their // devices are as each file names the watch, and are one computer. const oceanFit = () => parsedDive([], { submerged_time: 3440, avg_depth: 10.58, max_depth: 19.1, inferred: ["submerged_time", "avg_depth"], device: { brand: "suunto", model: "Suunto Ocean" }, }); const oceanJson = () => parsedDive([], { submerged_time: 3424, avg_depth: 10.62, max_depth: 19.04, inferred: [], device: { brand: "Suunto", serial: "253810000400" }, }); const perdixUddf = () => parsedDive([], { submerged_time: 3380, avg_depth: 10.4, max_depth: 18.9, inferred: [], device: { brand: "Shearwater", model: "Perdix 3", serial: "D9772626" }, }); // Applies each file as the form does within one session: the first prefills, // every later one fills, all against the one session's marks. function applyInTurn( values: Record, ...files: (ReturnType | ((v: typeof values) => void))[] ) { const { form } = formHoldingValues(values); const marks: InferredFigures = {}; let first = true; for (const file of files) { if (typeof file === "function") { file(values); continue; } applyParsedDiveToForm( form, file, () => {}, first ? "prefill" : "fill-only", marks, ); first = false; } return values; } it("takes the JSON's figures when the FIT came first", () => { const values = applyInTurn({}, oceanFit(), oceanJson()); expect(values.submerged_time).toBe("57:04"); expect(values.avg_depth).toBe(10.62); // The FIT stated its maximum, so the JSON only fills. expect(values.max_depth).toBe(19.1); }); it("keeps the JSON's figures when the FIT comes second", () => { const values = applyInTurn({}, oceanJson(), oceanFit()); expect(values.submerged_time).toBe("57:04"); expect(values.avg_depth).toBe(10.62); }); it("keeps a dive time the diver typed between the two", () => { const values = applyInTurn( {}, oceanFit(), (v) => { v.submerged_time = "57:00"; }, oceanJson(), ); expect(values.submerged_time).toBe("57:00"); expect(values.avg_depth).toBe(10.62); }); it("keeps the first of two FITs, neither stating its figures", () => { const second = { ...oceanFit(), submerged_time: 3500, avg_depth: 10.1 }; const values = applyInTurn({}, oceanFit(), second); expect(values.submerged_time).toBe("57:20"); expect(values.avg_depth).toBe(10.58); }); it("keeps the Ocean's derived figures against another computer's stated ones", () => { const values = applyInTurn({}, oceanFit(), perdixUddf()); expect(values.submerged_time).toBe("57:20"); expect(values.avg_depth).toBe(10.58); }); it("replaces a derived figure only once", () => { // The JSON's figure is stated, so a third file of the computer only fills. const values = applyInTurn({}, oceanFit(), oceanJson(), { ...oceanJson(), submerged_time: 3400, }); expect(values.submerged_time).toBe("57:04"); }); it("fills only, as before, from an API build that marks nothing", () => { const { inferred: _fit, ...fit } = oceanFit(); const { inferred: _json, ...json } = oceanJson(); const values = applyInTurn({}, fit, json); expect(values.submerged_time).toBe("57:20"); expect(values.avg_depth).toBe(10.58); }); });