import { describe, expect, it } from "vitest"; import type { DiveFileInfo, DiveProfileInfo, Recording } from "@/lib/api/dives"; import { canMergeDive, deleteFileConfirmation, deleteRecordingConfirmation, diveFileRows, diveRecordings, entryFixAfterSave, noFileKeptSentence, owedDeco, primaryRecording, recordingDeviceLabel, recordingLabel, recordingReadoutsLabel, recordingSettingsLabel, sameDevice, UNNAMED_DEVICE_LABEL, } from "@/lib/dive-recordings"; import { dive } from "@/test/dive-fixtures"; function file(overrides: Partial = {}): DiveFileInfo { return { uuid: "f1", original_filename: "dive.fit", content_type: "application/octet-stream", byte_size: 4096, parser_key: "fit", attach_order: 0, ...overrides, }; } function recording(overrides: Partial = {}): Recording { return { uuid: "r1", ordinal: 0, files: [], ...overrides }; } function profileInfo( overrides: Partial = {}, ): DiveProfileInfo { return { uuid: "p1", // Milliseconds, the profile's own axis. duration: 3_163_000, depth_sample_count: 314, provenance: "file", channels: ["depth"], ...overrides, }; } describe("recordingDeviceLabel", () => { it("puts the brand and model first, then the serial", () => { // The Shearwater UDDF's device, verbatim. `name` is `Perdix 3` here too and // is deliberately not repeated in parentheses. expect( recordingDeviceLabel({ brand: "Shearwater Research, Inc", model: "Perdix 3", serial: "D9772626", name: "Perdix 3", }), ).toBe("Shearwater Research, Inc Perdix 3 · D9772626"); }); it("names the computer its owner named, where that is not the model", () => { // The Suunto app's JSON: a brand, a serial, a name the diver set, and no // model at all. expect( recordingDeviceLabel({ brand: "Suunto", serial: "253810000400", name: "Porvoo", firmware: "2.51.28", }), ).toBe("Suunto · 253810000400 (Porvoo)"); }); it("does not say the brand twice when the model already carries it", () => { // A FIT decodes its maker to the lowercase `suunto` beside the model // `Suunto Ocean`, and the API stores both exactly as the file wrote them. // The naive join reads "suunto Suunto Ocean". expect( recordingDeviceLabel({ brand: "suunto", model: "Suunto Ocean" }), ).toBe("Suunto Ocean"); }); it("has nothing to say about a device that only records its firmware", () => { // Not a broken row - a format may carry a version string and no identity - // so the caller substitutes a word rather than rendering a blank. expect(recordingDeviceLabel({ firmware: "2.51.28" })).toBeNull(); expect(recordingDeviceLabel(null)).toBeNull(); expect(recordingDeviceLabel(undefined)).toBeNull(); }); it("treats a blank member as absent rather than as a value", () => { // A format that writes an empty element rather than omitting it. Joining on // truthiness alone would render " · D9772626" with a leading separator. expect(recordingDeviceLabel({ brand: " ", serial: "D9772626" })).toBe( "D9772626", ); }); }); describe("diveRecordings", () => { it("orders by ordinal rather than trusting the response", () => { const ordered = diveRecordings( dive({ recordings: [ recording({ uuid: "second", ordinal: 1 }), recording({ uuid: "first", ordinal: 0 }), ], }), ); expect(ordered.map((item) => item.uuid)).toEqual(["first", "second"]); }); it("reads an absent list as empty", () => { // `recordings` is absent - not `[]` - on a list row and on any detail // payload the API cached before recordings existed. expect(diveRecordings(dive())).toEqual([]); expect(primaryRecording(dive())).toBeNull(); }); it("finds the primary wherever the response put it", () => { expect( primaryRecording( dive({ recordings: [ recording({ uuid: "second", ordinal: 1 }), recording({ uuid: "first", ordinal: 0 }), ], }), )?.uuid, ).toBe("first"); }); }); describe("owedDeco", () => { it("takes a list row's flag as given", () => { expect(owedDeco(dive({ deco: true }))).toBe(true); expect(owedDeco(dive({ deco: false }))).toBe(false); }); it("reads the ceiling off the first recording with a profile, as the API does", () => { const deco = profileInfo({ max_ceiling: 3 }); expect( owedDeco( dive({ recordings: [ recording({ uuid: "later", ordinal: 2, profile: profileInfo() }), recording({ uuid: "charted", ordinal: 1, profile: deco }), recording({ uuid: "no-samples", ordinal: 0 }), ], }), ), ).toBe(true); expect( owedDeco( dive({ recordings: [ recording({ uuid: "charted", ordinal: 0, profile: profileInfo() }), recording({ uuid: "backup", ordinal: 1, profile: deco }), ], }), ), ).toBe(false); }); it("says no for a dive logged by hand", () => { expect(owedDeco(dive())).toBe(false); }); }); describe("noFileKeptSentence", () => { it("says nothing about a recording that kept a file", () => { expect(noFileKeptSentence(recording({ files: [file()] }))).toBeNull(); }); it("names the converter for a recording logbook import built from a document", () => { expect( noFileKeptSentence( recording({ profile: profileInfo({ provenance: "divejson_import" }) }), ), ).toBe("No file kept — these samples were imported through the converter."); }); it("names the merge for a recording two others were folded into", () => { // The distinction the recording's own shape cannot make: a merge keeps // whatever files either half had, so an empty `files` says only that // neither half had one. expect( noFileKeptSentence( recording({ profile: profileInfo({ provenance: "merge" }) }), ), ).toBe("No file kept — these samples were merged from two recordings."); }); it("claims nothing about samples for a recording that carries none", () => { // A device and nothing else - what logbook import writes for a document // whose recording had no profile and no files. The two sentences above // would both be false here. const sentence = noFileKeptSentence(recording()); expect(sentence).toMatch(/no file kept/i); expect(sentence).not.toMatch(/samples/i); }); }); describe("diveFileRows", () => { it("gives every file a row and every file-less recording one of its own", () => { const rows = diveFileRows([ recording({ uuid: "perdix", ordinal: 0, device: { model: "Perdix 3", serial: "D9772626" }, }), recording({ uuid: "ocean", ordinal: 1, device: { brand: "Suunto", serial: "253810000400" }, files: [ file({ uuid: "json", original_filename: "dive.json", parser_key: "suunto_json", }), file({ uuid: "fit", original_filename: "dive.fit" }), ], }), ]); expect(rows.map((row) => row.kind)).toEqual(["empty", "file", "file"]); expect(rows.map((row) => row.key)).toEqual([ "recording:perdix", "file:json", "file:fit", ]); }); it("carries the device onto every one of a recording's rows", () => { const rows = diveFileRows([ recording({ device: { brand: "Suunto", model: "Suunto Ocean" }, files: [file({ uuid: "a" }), file({ uuid: "b" })], }), ]); expect(rows.map((row) => row.deviceLabel)).toEqual([ "Suunto Ocean", "Suunto Ocean", ]); }); it("names which recording a row belongs to only when there are several", () => { // On a single-recording dive every row belongs to the same device, and a // per-row "Recording 1" is noise. const alone = diveFileRows([recording({ files: [file()] })]); expect(alone[0].recordingName).toBeNull(); const several = diveFileRows([ recording({ uuid: "a", ordinal: 0, files: [file({ uuid: "a1" })] }), recording({ uuid: "b", ordinal: 1, files: [file({ uuid: "b1" })] }), ]); expect(several.map((row) => row.recordingName)).toEqual([ "Recording 1", "Recording 2", ]); }); it("labels the file's format as the import does rather than handing on the raw id", () => { const [row] = diveFileRows([ recording({ files: [file({ parser_key: "suunto_json" })] }), ]); expect(row.kind === "file" && row.formatLabel).toBe("Suunto app JSON"); }); it("labels a file no reader of this build reads as the dive computer's original", () => { const [row] = diveFileRows([ recording({ files: [file({ parser_key: "divejson_import" })] }), ]); expect(row.kind === "file" && row.formatLabel).toBe( "Dive computer original (.bin)", ); }); it("falls back to a word for a recording whose source named no computer", () => { // Asserted here as well as in `recordingDeviceLabel` because this is the // value the rows actually carry: `null`, for the component to substitute. const [row] = diveFileRows([recording({ files: [file()] })]); expect(row.deviceLabel).toBeNull(); expect(UNNAMED_DEVICE_LABEL).toBe("Dive computer"); }); }); describe("recordingLabel", () => { it("counts from one, because ordinals are the API's and not the diver's", () => { expect(recordingLabel(recording({ ordinal: 0 }))).toBe("Recording 1"); expect(recordingLabel(recording({ ordinal: 1 }))).toBe("Recording 2"); }); }); describe("canMergeDive", () => { it("offers nothing on a dive logged by hand", () => { // The API refuses to merge a hand-entered dive, so an action offered here // could only ever 422. expect(canMergeDive(dive())).toBe(false); }); it("offers the action as soon as a computer recorded the dive", () => { expect(canMergeDive(dive({ recordings: [recording()] }))).toBe(true); }); }); // The Suunto that exported one dive twice, plus a second computer beside it - // the shape the understated copy was found on. `suunto` holds a JSON and a FIT // and is shown by default; `perdix` is the one that takes over when it goes. const suuntoJson = file({ uuid: "sj", original_filename: "dive.json" }); const suuntoFit = file({ uuid: "sf", original_filename: "dive.fit" }); const perdixFile = file({ uuid: "pf", original_filename: "perdix.uddf" }); describe("deleteFileConfirmation", () => { it("keeps the plain title while the recording keeps other files", () => { const suunto = recording({ files: [suuntoJson, suuntoFit] }); const { title, description } = deleteFileConfirmation([suunto], "sf"); expect(title).toBe("Delete this file?"); expect(description).toContain("profile and readouts are re-read"); // The one recording there is writes the dive's positions, so they move with // it even though the recording itself survives. expect(description).toContain("re-read along with it"); }); it("says the positions are left alone for a recording that does not write them", () => { // The Perdix's last file, so its recording goes - its own readouts with it - // but it is not the one shown by default, and the dive's positions are the // Suunto's either way. const suunto = recording({ files: [suuntoJson, suuntoFit] }); const perdix = recording({ uuid: "r2", ordinal: 1, files: [perdixFile] }); const { title, description } = deleteFileConfirmation( [suunto, perdix], "pf", ); expect(title).toBe("Delete this file and its recording?"); expect(description).toContain("the whole recording goes with it"); expect(description).toContain("its readouts"); expect(description).toContain("are left alone"); }); it("says the recording goes, and which one takes over, on the last file", () => { // The reported case exactly: the Suunto's FIT is already gone, deleting its // JSON takes the recording, and the Perdix becomes the charted profile. const suunto = recording({ files: [suuntoJson] }); const perdix = recording({ uuid: "r2", ordinal: 1, files: [perdixFile] }); const { title, description } = deleteFileConfirmation( [suunto, perdix], "sj", ); expect(title).toBe("Delete this file and its recording?"); expect(description).toContain("the whole recording goes with it"); expect(description).toContain("the next one takes over"); expect(description).toContain("re-read from that instead"); expect(description).toContain("The dive itself stays"); }); it("does not promise a handover to a recording with no file to read", () => { // `renumber_ordinals` promotes in ordinal order without skipping a file-less // recording, and `refresh_tech_scalars` then finds nothing on it - so the // positions are cleared exactly as if no recording were left, and the // reassuring sentence would be the one lie this whole change exists to stop. const suunto = recording({ files: [suuntoJson] }); const imported = recording({ uuid: "r2", ordinal: 1, files: [], profile: profileInfo({ provenance: "divejson_import" }), }); const { title, description } = deleteFileConfirmation( [suunto, imported], "sj", ); expect(title).toBe("Delete this file and its recording?"); expect(description).toContain("the next one takes over"); expect(description).toContain("it holds no file"); expect(description).toContain("are cleared"); expect(description).not.toContain("re-read from that instead"); }); it("finds the recording that takes over by ordinal, not by list position", () => { // The edit form passes `dive.recordings` straight through, unsorted, so the // successor is not simply the next entry in the array. const suunto = recording({ files: [suuntoJson] }); const third = recording({ uuid: "r3", ordinal: 2, files: [] }); const second = recording({ uuid: "r2", ordinal: 1, files: [perdixFile] }); const { description } = deleteFileConfirmation( [third, suunto, second], "sj", ); expect(description).toContain("re-read from that instead"); }); it("says the positions are cleared when nothing is left to read them from", () => { const suunto = recording({ files: [suuntoJson] }); const { title, description } = deleteFileConfirmation([suunto], "sj"); expect(title).toBe("Delete this file and its recording?"); expect(description).toContain("the dive's only recording"); expect(description).toContain("nothing is left to read them from"); expect(description).not.toContain("takes over"); }); it.each([ ["merge", "they were merged from two recordings"], ["divejson_import", "they came in through the converter"], ] as const)( "keeps a recording whose %s samples no file could produce again", (provenance, clause) => { const merged = recording({ files: [suuntoJson], profile: profileInfo({ provenance }), }); const { title, description } = deleteFileConfirmation([merged], "sj"); // The recording survives file-less, so the title must not promise its // removal, and its readouts stay with it - but the dive's positions still // go, having come off the file. expect(title).toBe("Delete this file?"); expect(description).toContain("nothing to download"); expect(description).toContain(clause); expect(description).toContain("Its samples and readouts stay"); expect(description).toContain("are cleared with the last of those files"); }, ); it("removes a recording whose samples were read off the file", () => { const suunto = recording({ files: [suuntoJson], profile: profileInfo({ provenance: "file" }), }); expect(deleteFileConfirmation([suunto], "sj").title).toBe( "Delete this file and its recording?", ); }); it("still calls a secondary deletion a no-op when the primary holds no file", () => { // A converted logbook import writes its document's positions onto the dive // beside a file-less primary, and `refresh_tech_scalars` re-reading that // primary - finding nothing - would clear them on a deletion that never // touched it. It takes a `touched_primary` and returns without writing, so // this is the no-op it looks like and the copy says so. const imported = recording({ files: [], profile: profileInfo({ provenance: "divejson_import" }), }); const perdix = recording({ uuid: "r2", ordinal: 1, files: [perdixFile] }); const { description } = deleteFileConfirmation([imported, perdix], "pf"); expect(description).toContain("are left alone"); }); it("calls a secondary deletion a no-op while the primary still has files", () => { const suunto = recording({ files: [suuntoJson] }); const perdix = recording({ uuid: "r2", ordinal: 1, files: [perdixFile] }); expect( deleteFileConfirmation([suunto, perdix], "pf").description, ).toContain("are left alone"); }); it("leaves the positions alone when a secondary merely loses one of its files", () => { // The same answer by a second route: `_rederive_recording` re-reads that // recording's own readouts and returns before the dive's positions for any // recording that is not ordinal 0. const imported = recording({ files: [], profile: profileInfo({ provenance: "divejson_import" }), }); const perdix = recording({ uuid: "r2", ordinal: 1, files: [perdixFile, file({ uuid: "pf2", original_filename: "b.fit" })], }); expect( deleteFileConfirmation([imported, perdix], "pf").description, ).toContain("are left alone"); }); it("falls back to a conservative sentence for a file nothing holds", () => { // The dialog can outlive a re-read that removed the row behind it. Claiming // the recording survives would be the wrong guess of the two. const { title, description } = deleteFileConfirmation([], "gone"); expect(title).toBe("Delete this file?"); expect(description).toContain("may change with it"); }); }); describe("deleteRecordingConfirmation", () => { it("names what takes over from the recording shown by default", () => { const imported = recording({ files: [], profile: profileInfo({ provenance: "divejson_import" }), }); const perdix = recording({ uuid: "r2", ordinal: 1, files: [perdixFile] }); const { title, description } = deleteRecordingConfirmation( [imported, perdix], "r1", ); expect(title).toBe("Delete this recording?"); expect(description).toContain("the next one takes over"); expect(description).toContain("The dive itself stays"); }); it("says the positions are cleared when it was the dive's only recording", () => { const imported = recording({ files: [], profile: profileInfo({ provenance: "merge" }), }); // Nothing here ever had a file to read them off, so the sentence must not // claim one - this recording's samples came in through a document. expect(deleteRecordingConfirmation([imported], "r1").description).toContain( "the dive's only recording", ); }); it("leaves the positions alone for a recording that is not the one shown by default", () => { // The whole-recording route's half of the API's `touched_primary`: removing // a second computer's recording says nothing about positions the primary - // or a document - put on the dive. const imported = recording({ files: [], profile: profileInfo({ provenance: "divejson_import" }), }); const perdix = recording({ uuid: "r2", ordinal: 1, files: [perdixFile] }); expect( deleteRecordingConfirmation([imported, perdix], "r2").description, ).toContain("are left alone"); }); it("covers the files a recording holds rather than assuming it holds none", () => { // The dive page offers this route only for a file-less recording, which is a // fact about that button and not about the endpoint. const suunto = recording({ files: [suuntoJson, suuntoFit] }); expect(deleteRecordingConfirmation([suunto], "r1").description).toContain( "every file it holds", ); }); it("falls back to a conservative sentence for a recording nothing holds", () => { expect(deleteRecordingConfirmation([], "gone").description).toContain( "may change with it", ); }); }); describe("recordingSettingsLabel", () => { it("says the mode and the model on one line", () => { expect( recordingSettingsLabel( recording({ mode: "open_circuit", deco_model: { algorithm: "buhlmann", gf_low: 30, gf_high: 85 }, }), ), ).toBe("Open circuit · Bühlmann GF 30/85"); }); it("prefers the device's own name for its model over the family", () => { // The same call `recordingDeviceLabel` makes between a model and a brand: // "Suunto Fused RGBM 2" says more than "RGBM", and the two together say one // of them twice. expect( recordingSettingsLabel( recording({ mode: "open_circuit", deco_model: { algorithm: "rgbm", name: "Suunto Fused RGBM 2" }, }), ), ).toBe("Open circuit · Suunto Fused RGBM 2"); }); it("names a freedive with no model to name", () => { expect(recordingSettingsLabel(recording({ mode: "freedive" }))).toBe( "Freedive", ); }); it("says the model alone where the file recorded no mode", () => { expect( recordingSettingsLabel( recording({ deco_model: { name: "ZHL-16C", gf_low: 40, gf_high: 85 } }), ), ).toBe("ZHL-16C GF 40/85"); }); it("says nothing at all rather than guessing open circuit", () => { // UDDF documents an absent `` as meaning open circuit. That is the // source format's claim about its own default rather than the device's about // the dive, and reading it would label a CCR dive whose importer never set // the mode as open circuit. expect(recordingSettingsLabel(recording())).toBeNull(); expect(recordingSettingsLabel(recording({ mode: null }))).toBeNull(); expect( recordingSettingsLabel(recording({ mode: null, deco_model: null })), ).toBeNull(); }); it("says nothing for a mode this build has never heard of", () => { // The vocabulary is closed in the API's *current* build, and the two repos // deploy independently - so a raw `semi_closed_rebreather` on screen is the // failure this guards against, and a wrong word is worse than no word. expect( recordingSettingsLabel( recording({ mode: "semi_closed_rebreather", deco_model: null }), ), ).toBeNull(); // Which does not silence what the file *did* record. expect( recordingSettingsLabel( recording({ mode: "semi_closed_rebreather", deco_model: { name: "VPM-B" }, }), ), ).toBe("VPM-B"); }); it("carries a conservatism as its own clause, sign and all", () => { // On the device's own scale - Suunto's P-2 to P2 - which means nothing // without the model and the computer beside it, and this line sits under // both. `0` is a setting rather than an absence. expect( recordingSettingsLabel( recording({ mode: "gauge", deco_model: { name: "Suunto Fused RGBM 2", conservatism: 0 }, }), ), ).toBe("Gauge · Suunto Fused RGBM 2 · conservatism 0"); expect( recordingSettingsLabel( recording({ deco_model: { name: "Fused RGBM", conservatism: 1 } }), ), ).toBe("Fused RGBM · conservatism +1"); expect( recordingSettingsLabel( recording({ deco_model: { name: "Fused RGBM", conservatism: -2 } }), ), ).toBe("Fused RGBM · conservatism -2"); }); it("drops a half-recorded gradient-factor pair rather than printing one", () => { // The API records both or neither, and a lone "GF 30/" would be a claim the // file never made. expect( recordingSettingsLabel( recording({ mode: "closed_circuit", deco_model: { algorithm: "buhlmann", gf_low: 30 }, }), ), ).toBe("Closed circuit · Bühlmann"); }); it("is null for a deco model that recorded nothing worth a word", () => { expect(recordingSettingsLabel(recording({ deco_model: {} }))).toBeNull(); }); it("names the salinity the way the computer's own menu does", () => { expect( recordingSettingsLabel( recording({ mode: "open_circuit", deco_model: { algorithm: "buhlmann", gf_low: 30, gf_high: 85 }, salinity: "en13319", }), ), ).toBe("Open circuit · Bühlmann GF 30/85 · salinity EN13319"); expect(recordingSettingsLabel(recording({ salinity: "fresh" }))).toBe( "salinity Fresh", ); expect(recordingSettingsLabel(recording({ salinity: "salt" }))).toBe( "salinity Salt", ); }); it("says nothing for a salinity this build has never heard of", () => { // On `mode`'s terms: the vocabulary may grow in the API before this bundle // ships a word for it, and a raw slug is worse than silence. expect( recordingSettingsLabel(recording({ salinity: "custom" })), ).toBeNull(); expect(recordingSettingsLabel(recording({ salinity: null }))).toBeNull(); }); }); describe("recordingReadoutsLabel", () => { it("says every readout the recording carries on one line", () => { expect( recordingReadoutsLabel( recording({ cns_start: 0, cns_end: 9, otu_start: 0, otu_end: 22, surface_pressure_bar: 1.012, }), ), ).toBe("CNS 0% → 9% · OTU 0 → 22 · Surface pressure 1.012 bar"); }); it("keeps a half-recorded pair a pair", () => { // A FIT file records an end-OTU and no start - the exposure card's em dash, // so the arrow still reads "from, to". expect(recordingReadoutsLabel(recording({ otu_end: 23 }))).toBe( "OTU — → 23", ); }); it("says a surface pressure alone, which a recording may carry by itself", () => { expect( recordingReadoutsLabel(recording({ surface_pressure_bar: 1.009 })), ).toBe("Surface pressure 1.009 bar"); }); it("treats a recorded zero as a reading", () => { expect(recordingReadoutsLabel(recording({ cns_start: 0 }))).toBe( "CNS 0% → —", ); }); it("is null for a recording that reported none, nulls or absent keys", () => { expect(recordingReadoutsLabel(recording())).toBeNull(); expect( recordingReadoutsLabel( recording({ cns_start: null, cns_end: null, otu_start: null, otu_end: null, surface_pressure_bar: null, }), ), ).toBeNull(); }); }); describe("sameDevice", () => { it("reads a Suunto Ocean's FIT and JSON as one computer", () => { expect( sameDevice( { brand: "suunto", model: "Suunto Ocean" }, { brand: "Suunto", serial: "253810000400" }, ), ).toBe(true); }); it("tells two makers apart", () => { expect( sameDevice( { brand: "suunto", model: "Suunto Ocean" }, { brand: "Shearwater", model: "Perdix 3", serial: "D9772626" }, ), ).toBe(false); }); it("settles on the serials where both carry one", () => { expect( sameDevice( { brand: "Shearwater", model: "Perdix 3", serial: "D9772626" }, { model: "Shearwater Perdix 3", serial: "d9772626 " }, ), ).toBe(true); expect( sameDevice( { brand: "Suunto", serial: "253810000400" }, { brand: "Suunto", serial: "253810000401" }, ), ).toBe(false); }); it("tells two models apart with at most one serial in play", () => { expect( sameDevice( { model: "Suunto Ocean" }, { model: "Suunto D5", serial: "1" }, ), ).toBe(false); }); }); describe("entryFixAfterSave", () => { const BLUE_HOLE = { latitude: 28.5721, longitude: 34.5412 }; const CANYON = { latitude: 28.5523, longitude: 34.5205 }; const fixed = ( uuid: string, attachOrder: number, fix: { latitude: number; longitude: number }, ) => file({ uuid, attach_order: attachOrder, entry_latitude: fix.latitude, entry_longitude: fix.longitude, }); it("takes the first file with a fix in attach order, whatever its recording", () => { expect( entryFixAfterSave({ recordings: [ recording({ files: [ file({ uuid: "a", attach_order: 0 }), fixed("c", 2, BLUE_HOLE), ], }), recording({ uuid: "r2", ordinal: 1, files: [fixed("b", 1, CANYON)] }), ], pending: [{ entry: BLUE_HOLE }], }), ).toEqual(CANYON); }); it("passes over the files struck off, to the next stored one and then the picked ones", () => { const recordings = [ recording({ files: [fixed("a", 0, BLUE_HOLE), fixed("b", 1, CANYON)] }), ]; expect(entryFixAfterSave({ recordings, removed: ["a"] })).toEqual(CANYON); expect( entryFixAfterSave({ recordings, removed: ["a", "b"], pending: [{ entry: null }, { entry: BLUE_HOLE }], }), ).toEqual(BLUE_HOLE); }); it("has none where no file left has one", () => { expect( entryFixAfterSave({ recordings: [recording({ files: [fixed("a", 0, BLUE_HOLE)] })], removed: ["a"], stored: BLUE_HOLE, }), ).toBeNull(); }); it("keeps the dive's own fix while no file is attached or deleted", () => { expect(entryFixAfterSave({ stored: BLUE_HOLE })).toEqual(BLUE_HOLE); // A merge or a logbook document gave it, and no file has it. expect( entryFixAfterSave({ recordings: [recording({ files: [file({ uuid: "a" })] })], stored: BLUE_HOLE, }), ).toEqual(BLUE_HOLE); expect(entryFixAfterSave({})).toBeNull(); }); });