src/lib/dive-recordings.test.ts
Rama main · réplica de lectura del repositorio original.
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> = {}): 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> = {}): Recording {
return { uuid: "r1", ordinal: 0, files: [], ...overrides };
}
function profileInfo(
overrides: Partial<DiveProfileInfo> = {},
): 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 `<divemode>` 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();
});
});