Migration to new Rust-based engine (#401)

* wasm first steps

* parsing start

* parsing continued

* progress

* progress

* progress

* progress

* progress

* progress

* progress

* progress

* fixes

* reorganize

* change type

* fix

* progress

* refactoring

* progress

* progress

* progress

* keep style info

* progress

* progress

* remove (copy) in name

* progress

* merge methods into 1

* splice

* progress

* progress

* progress

* cleaning

* progress

* progress

* progress

* visibility

* progress

* progress

* progress

* progress

* progress

* fixes

* progress

* fix markers

* fix

* progress

* progress

* progress

* progress

* progress

* progress

* small fix

* use engine info

* fix popups

* small fix

* progress

* progress

* progress

* clean tool

* handle self closing elements

* reduce data exchanges across boundary

* copy on wasm side to avoid any data corruption

* speed fix

* routing

* refactoring

* reverse

* reverse

* crop

* split

* merge

* progress

* time tool

* fix bounds for waypoints

* reduce

* fix reactivity

* handle corrupt time data

* export

* progress

* remove gpx js package

* remove todo

* update readme

* cleaning

* progress

* fix link fields

* remove unused fields

* refactoring and tests

* import and export of routes, ref #192

* per-chunk categories storage

* cross-tab sync

* outdated state

* refactoring

* reorder methods

* refactoring

* fix

* improve flow

* renaming

* progress

* progress

* fix

* progress

* per file history

* keep original flag

* use history variant

* fix

* progress

* file explorer

* progress

* fix

* sticky folders

* improved sticky folders

* fix sortable

* fix sortable

* fix

* fix tab size

* no file delete on desktop

* fix routing on other file

* fix sorting

* remember language

* open links in browser

* commit missing changes

* avoid selecting on resize

* clarify filename

* explorer actions

* fix folder styling

* drop anywhere in folder

* cleaning

* fixes

* remove focus-ring

* full screen

* small fix

* consistent highlight color

* improved preview

* performance improvements

* sparser data transfers

* sparser data transfers

* show popup once data is loaded

* fix routing control size

* fix

* sparse desktop build

* remove custom ign styles

* use new endpoint for openhikingmap

* strip native menu

* reorder context menu items

* route full support

* route - track conversion

* create route file action

* route export, filter out anchors at high zoom level

* clarify name handling

* move hide & center to view

* routes continued

* update docs

* fix power format, closes #378

* lint fixes

* fix display of additional data with gaps, closes #361

* fix linting issues

* fix import modules

* fix multidrag

* change base to resolve

* fix type errors

* fix layer typings

* fix last type error

* each keys

* improve typings

* deploy to cloudflare

---------

Co-authored-by: vcoppe <vcoppe@hexaly.com>
This commit is contained in:
vcoppeandvcoppe authored and GitHub committed 2026-10-31 18:08:22 +01:00
1 parent 6a0d434371
commit 8fe7d8d64d
484 files changed
+53949 -41152

No files matched your search

+520
View File
@@ -0,0 +1,520 @@
//! Tests of the JS interface, run with `wasm-pack test --node wasm` (they need a JS environment).
//!
//! Every test starts a new engine, which is what the page does once.
#![allow(clippy::needless_borrows_for_generic_args)]
use js_sys::{Array, Float64Array, Object, Reflect};
use wasm_bindgen::prelude::*;
use wasm_bindgen_test::*;
use crate::command::history::*;
use crate::session::*;
use crate::ts::Outcome;
use crate::types::*;
const SIMPLE: &[u8] = include_bytes!("../../gpx-rs/data/simple.gpx");
const WITH_TIME: &[u8] = include_bytes!("../../gpx-rs/data/with_time.gpx");
const WITH_SURFACE: &[u8] = include_bytes!("../../gpx-rs/data/with_surface.gpx");
const WITH_ROUTES: &[u8] = include_bytes!("../../gpx-rs/data/with_routes.gpx");
thread_local! {
static LAST: std::cell::RefCell<JsValue> = const { std::cell::RefCell::new(JsValue::UNDEFINED) };
}
/// Keeps the outcome of a call for the checks that follow it, and tells whether it changed anything.
fn run(outcome: Outcome) -> bool {
let outcome = JsValue::from(outcome);
let changed = get(&outcome, "changed").as_bool().unwrap();
LAST.with(|last| *last.borrow_mut() = outcome);
changed
}
fn last_outcome() -> JsValue {
LAST.with(|last| last.borrow().clone())
}
fn last_error() -> Option<String> {
get(&last_outcome(), "error").as_string()
}
fn get(object: impl AsRef<JsValue>, key: &str) -> JsValue {
Reflect::get(object.as_ref(), &key.into()).unwrap()
}
fn array(value: impl Into<JsValue>) -> Array {
Array::from(&value.into())
}
fn strings(value: impl Into<JsValue>) -> Vec<String> {
array(value)
.iter()
.map(|v| v.as_string().unwrap())
.collect()
}
/// Starts a new engine and loads the given files (one command).
fn started_with(files: &[(&[u8], &str)]) {
start();
if files.is_empty() {
return;
}
let data: Vec<u8> = files
.iter()
.flat_map(|(data, _)| data.iter().copied())
.collect();
let lengths: Vec<u32> = files.iter().map(|(data, _)| data.len() as u32).collect();
let names = files.iter().map(|(_, name)| name.to_string()).collect();
assert!(run(crate::command::files::load_files(
&data, &lengths, names
)));
}
fn order() -> Vec<String> {
strings(crate::queries::files::file_order())
}
fn uuid_bytes(ids: &[String]) -> Vec<u8> {
ids.iter()
.flat_map(|id| *uuid::Uuid::parse_str(id).unwrap().as_bytes())
.collect()
}
#[wasm_bindgen_test]
fn test_new_file_is_listed_and_selected() {
start();
assert!(run(crate::command::files::new_file(
"route", None, None, None, false
)));
assert_eq!(last_error(), None);
let ids = order();
assert_eq!(ids.len(), 1);
let structure = crate::queries::files::file_structure(&ids[0]);
assert_eq!(
get(&structure, "name").as_string().as_deref(),
Some("route")
);
assert_eq!(array(get(&structure, "tracks")).length(), 0);
let selection = crate::queries::files::selection();
assert_eq!(get(&selection, "type").as_string().as_deref(), Some("file"));
assert_eq!(strings(get(&selection, "fileIds")), ids);
let update = last_outcome();
assert_eq!(strings(get(&update, "added")), ids);
assert_eq!(get(&update, "orderChanged").as_bool(), Some(true));
}
#[wasm_bindgen_test]
fn test_unknown_file_has_no_structure() {
started_with(&[]);
let id = uuid::Uuid::new_v4().to_string();
assert!(crate::queries::files::file_structure(&id).is_undefined());
assert!(crate::queries::files::file_structure("not an id").is_undefined());
assert!(crate::queries::files::file_statistics(&id).is_undefined());
assert!(crate::queries::files::export_file(&id, true, true, true, true, true, true).is_none());
assert_eq!(
crate::queries::coordinates::segment_coordinates(&id).length(),
0
);
assert_eq!(
crate::queries::coordinates::waypoint_coordinates("nope").length(),
0
);
}
#[wasm_bindgen_test]
fn test_loaded_file_has_statistics_and_coordinates() {
started_with(&[(SIMPLE, "simple")]);
let id = order().remove(0);
let stats = crate::queries::files::file_statistics(&id);
assert!(get(&stats, "totalDistance").as_f64().unwrap() > 0.0);
// the file is selected: the buffers describe it
let selection_stats = crate::queries::files::selection_statistics();
assert_eq!(
get(&selection_stats, "totalDistance").as_f64(),
get(&stats, "totalDistance").as_f64()
);
let n = crate::queries::statistics::total_distance().length();
assert!(n > 0);
assert_eq!(crate::queries::statistics::lng().length(), n);
assert_eq!(crate::queries::statistics::ele().length(), n);
// no timestamps in this file
let point = crate::queries::statistics::selection_point(0);
assert!(get(&point, "slopeSegmentDistance").as_f64().is_some());
assert!(get(&point, "time").is_undefined());
assert!(crate::queries::statistics::selection_point(n as usize).is_undefined());
let structure = crate::queries::files::file_structure(&id);
let segment = get(
&array(get(&array(get(&structure, "tracks")).get(0), "segments")).get(0),
"id",
);
let coordinates =
crate::queries::coordinates::segment_coordinates(&segment.as_string().unwrap());
assert_eq!(coordinates.length(), 2 * n);
// the routing buffers: one segment, anchored at both ends
assert_eq!(crate::queries::routing::segment_starts().to_vec(), vec![0]);
let anchors = crate::queries::routing::anchor_indices().to_vec();
assert_eq!(anchors.first(), Some(&0));
assert_eq!(anchors.last(), Some(&(n - 1)));
}
#[wasm_bindgen_test]
fn test_timestamps_and_categories() {
started_with(&[(WITH_TIME, "time")]);
let point = crate::queries::statistics::selection_point(0);
assert!(get(&point, "time").as_f64().unwrap() > 0.0);
assert!(crate::queries::statistics::total_time().is_some());
started_with(&[(WITH_SURFACE, "surface")]);
let surfaces = strings(crate::queries::statistics::surfaces());
assert!(!surfaces.is_empty());
let values = crate::queries::statistics::surface_values().to_vec();
assert!(values.iter().any(|v| *v > 0));
assert!(values.iter().all(|v| (*v as usize) <= surfaces.len()));
assert_eq!(
crate::queries::statistics::surface_starts().length(),
values.len() as u32
);
// the name of the surface of a trackpoint that has one
let starts = crate::queries::statistics::surface_starts().to_vec();
let known = values.iter().position(|v| *v > 0).unwrap();
let point = crate::queries::statistics::selection_point(starts[known] as usize);
assert_eq!(
get(&point, "surface").as_string(),
Some(surfaces[values[known] as usize - 1].clone())
);
}
#[wasm_bindgen_test]
fn test_the_reason_of_a_failure_can_be_read() {
started_with(&[]);
// nothing to do
assert!(!run(crate::command::tools::reverse()));
assert_eq!(last_error().as_deref(), Some("nothing to do"));
// the next call starts afresh
assert!(run(crate::command::files::new_file(
"a", None, None, None, false
)));
assert_eq!(last_error(), None);
// invalid data
assert!(!run(crate::command::files::load_files(
b"<gpx><trk></gpx>",
&[16],
vec!["bad".into()]
)));
assert!(last_error().unwrap().starts_with("invalid data"));
assert_eq!(order().len(), 1);
// arguments that do not make sense
assert!(!run(crate::command::files::load_files(
b"",
&[1, 2],
vec!["a".into()]
)));
assert!(last_error().unwrap().contains("lengths"));
assert!(!run(crate::command::files::load_files(
b"abc",
&[10],
vec!["a".into()]
)));
assert!(last_error().unwrap().contains("shorter"));
assert!(!run(crate::command::files::reorder(&[1, 2, 3], 0)));
assert!(last_error().unwrap().contains("UUIDs"));
assert!(!run(crate::command::tools::crop(5, 1)));
assert!(last_error().unwrap().contains("reversed"));
assert!(!run(crate::command::waypoints::delete_waypoint("x", "y")));
assert!(last_error().unwrap().contains("UUID"));
assert!(!run(crate::selection::select_tracks(
"x",
&[],
SelectMode::Replace
)));
assert!(last_error().unwrap().contains("UUIDs"));
// a stale revision
let stale = crate::queries::routing::routing_revision().wrapping_add(1);
assert!(!run(crate::command::routing::insert_anchor(
stale, 0.0, 0.0
)));
assert!(last_error().unwrap().contains("routing buffers"));
assert!(!run(crate::command::routing::change_loop_start(stale, 0)));
assert!(!run(crate::command::routing::split(
stale,
0,
SplitType::Files
)));
assert!(last_error().is_some());
// answers are not failures
assert!(run(crate::command::files::new_file(
"b", None, None, None, false
)));
assert!(!crate::command::files::can_paste());
assert_eq!(last_error(), None);
}
#[wasm_bindgen_test]
fn test_undo_and_redo() {
started_with(&[]);
assert!(!can_undo() && !can_redo());
assert!(run(crate::command::files::new_file(
"a", None, None, None, false
)));
assert!(can_undo() && !can_redo());
assert!(run(undo()));
assert!(order().is_empty());
assert!(can_redo());
assert!(run(redo()));
assert_eq!(order().len(), 1);
// nothing to redo is not an error of the engine
assert!(!run(redo()));
}
#[wasm_bindgen_test]
fn test_export_and_exportable_data() {
started_with(&[(SIMPLE, "plain"), (WITH_TIME, "timed")]);
let ids = order();
let bytes =
crate::queries::files::export_file(&ids[0], true, true, true, true, true, true).unwrap();
assert!(String::from_utf8(bytes).unwrap().contains("<gpx"));
// the time is the first bit
assert_eq!(
crate::queries::files::exportable_data(&uuid_bytes(&ids[..1])) & 1,
0
);
assert_eq!(
crate::queries::files::exportable_data(&uuid_bytes(&ids[1..])) & 1,
1
);
assert_eq!(
crate::queries::files::exportable_data(&uuid_bytes(&ids)) & 1,
1
);
assert_eq!(crate::queries::files::exportable_data(&[1, 2, 3]), 0);
}
#[wasm_bindgen_test]
fn test_export_tracks_and_routes() {
started_with(&[(SIMPLE, "track"), (WITH_ROUTES, "routes")]);
let text = |id: &str| {
let bytes =
crate::queries::files::export_file(id, true, true, true, true, true, true).unwrap();
String::from_utf8(bytes).unwrap()
};
let ids = order();
let tracks = text(&ids[0]);
assert!(tracks.contains("<trk>") && !tracks.contains("<rte>"));
let routes = text(&ids[1]);
assert!(routes.contains("<rte>") && !routes.contains("<trk>"));
assert!(routes.contains("<rtept"));
}
#[wasm_bindgen_test]
fn test_select_and_reorder_files() {
started_with(&[(SIMPLE, "a"), (WITH_TIME, "b")]);
let ids = order();
assert!(run(crate::selection::select(
&uuid_bytes(&ids[1..]),
SelectMode::Replace
)));
assert_eq!(
strings(get(&crate::queries::files::selection(), "fileIds")),
ids[1..]
);
assert!(run(crate::selection::select(
&uuid_bytes(&ids[..1]),
SelectMode::Add
)));
assert_eq!(
array(get(&crate::queries::files::selection(), "fileIds")).length(),
2
);
assert!(run(crate::selection::select(
&uuid_bytes(&ids[..1]),
SelectMode::Toggle
)));
assert_eq!(
strings(get(&crate::queries::files::selection(), "fileIds")),
ids[1..]
);
// reordering is a change of the order, nothing else
let reversed: Vec<String> = ids.iter().rev().cloned().collect();
assert!(run(crate::command::files::reorder(
&uuid_bytes(&reversed[..1]),
0
)));
assert_eq!(order(), reversed);
assert_eq!(get(&last_outcome(), "orderChanged").as_bool(), Some(true));
}
#[wasm_bindgen_test]
fn test_move_elements_takes_js_objects() {
started_with(&[(SIMPLE, "a"), (WITH_TIME, "b")]);
let ids = order();
assert!(run(crate::selection::select(
&uuid_bytes(&ids[1..]),
SelectMode::Replace
)));
let target = Object::new();
Reflect::set(&target, &"type".into(), &"files".into()).unwrap();
Reflect::set(&target, &"index".into(), &0.into()).unwrap();
let what = JsValue::from(crate::queries::files::selection());
assert!(run(crate::command::files::move_elements(
what.clone().unchecked_into(),
JsValue::from(target).unchecked_into()
)));
assert_eq!(order(), vec![ids[1].clone(), ids[0].clone()]);
// an unknown target
let bad = Object::new();
Reflect::set(&bad, &"type".into(), &"nowhere".into()).unwrap();
Reflect::set(&bad, &"index".into(), &0.into()).unwrap();
assert!(!run(crate::command::files::move_elements(
what.unchecked_into(),
JsValue::from(bad).unchecked_into()
)));
assert!(last_error().unwrap().contains("not valid"));
}
#[wasm_bindgen_test]
fn test_waypoints() {
started_with(&[]);
assert!(run(crate::command::files::new_file(
"file", None, None, None, false
)));
let file = order().remove(0);
assert!(run(crate::command::waypoints::new_waypoint(
4.0, 50.0, 100.0, "summit", "", "", ""
)));
let structure = crate::queries::files::file_structure(&file);
let waypoints = array(get(&structure, "waypoints"));
assert_eq!(waypoints.length(), 1);
let id = get(&waypoints.get(0), "id").as_string().unwrap();
let details = crate::queries::files::waypoint(&file, &id);
assert_eq!(get(&details, "name").as_string().as_deref(), Some("summit"));
assert_eq!(get(&details, "ele").as_f64(), Some(100.0));
assert_eq!(
crate::queries::coordinates::waypoint_coordinates(&file).to_vec(),
vec![4.0, 50.0]
);
assert!(run(crate::command::waypoints::update_waypoint(
&file, &id, 5.0, 51.0, 120.0, "peak", "", "", ""
)));
let details = crate::queries::files::waypoint(&file, &id);
assert_eq!(get(&details, "name").as_string().as_deref(), Some("peak"));
assert!(run(crate::command::waypoints::move_waypoint(
&file, &id, 6.0, 52.0, 130.0
)));
assert_eq!(
crate::queries::coordinates::waypoint_coordinates(&file).to_vec(),
vec![6.0, 52.0]
);
// the structure tells the coordinates changed
let rev = get(
&crate::queries::files::file_structure(&file),
"waypointsRev",
);
assert!(run(crate::command::waypoints::move_waypoint(
&file, &id, 7.0, 52.0, 130.0
)));
assert_ne!(
get(
&crate::queries::files::file_structure(&file),
"waypointsRev"
),
rev
);
assert!(run(crate::command::waypoints::delete_waypoint(&file, &id)));
assert!(crate::queries::files::waypoint(&file, &id).is_undefined());
// already gone
assert!(!run(crate::command::waypoints::delete_waypoint(&file, &id)));
assert!(last_error().is_some());
}
#[wasm_bindgen_test]
fn test_clipboard() {
started_with(&[(SIMPLE, "a")]);
assert!(crate::queries::files::clipboard().is_undefined());
assert!(run(crate::command::files::copy()));
let clipboard = crate::queries::files::clipboard();
assert_eq!(
get(&clipboard, "type").as_string().as_deref(),
Some("files")
);
assert_eq!(get(&clipboard, "cut").as_bool(), Some(false));
assert!(crate::command::files::can_paste());
assert!(run(crate::command::files::paste()));
assert_eq!(order().len(), 2);
}
#[wasm_bindgen_test]
fn test_editing_tools_change_the_statistics() {
started_with(&[(SIMPLE, "a")]);
let before = crate::queries::statistics::total_distance().length();
assert!(run(crate::command::tools::crop(0, 9)));
assert_eq!(crate::queries::statistics::total_distance().length(), 10);
assert!(run(undo()));
assert_eq!(
crate::queries::statistics::total_distance().length(),
before
);
// one elevation per trackpoint
let ele = Float64Array::new_with_length(before);
assert!(run(crate::command::tools::elevation(&ele.to_vec())));
assert!(
crate::queries::statistics::ele()
.to_vec()
.iter()
.all(|e| *e == 0.0)
);
assert!(!run(crate::command::tools::elevation(&[1.0])));
assert!(last_error().unwrap().contains("elevations"));
assert!(
run(crate::command::tools::reduce(5.0)) || last_error().as_deref() == Some("nothing to do")
);
assert!(crate::queries::statistics::total_distance().length() <= before);
}
#[wasm_bindgen_test]
fn test_last_update_describes_the_last_call_only() {
started_with(&[]);
assert!(run(crate::command::files::new_file(
"a", None, None, None, false
)));
let update = last_outcome();
assert_eq!(array(get(&update, "added")).length(), 1);
assert_eq!(get(&update, "orderChanged").as_bool(), Some(true));
// a call that does nothing replaces it, whether the engine refuses or the arguments are wrong
assert!(!run(crate::command::tools::reverse()));
let update = last_outcome();
assert_eq!(array(get(&update, "added")).length(), 0);
assert_eq!(get(&update, "orderChanged").as_bool(), Some(false));
assert_eq!(get(&update, "selectionChanged").as_bool(), Some(false));
assert!(run(crate::command::files::new_file(
"b", None, None, None, false
)));
assert!(!run(crate::command::tools::crop(3, 1)));
let update = last_outcome();
assert_eq!(array(get(&update, "added")).length(), 0);
assert_eq!(get(&update, "orderChanged").as_bool(), Some(false));
}