WIP red: a records coordinate rename, from the browser (improvise-fdk1)
This commit is contained in:
@@ -771,3 +771,164 @@ fn a_typed_number_crosses_the_serialized_wire_exactly() {
|
||||
"the host must store the number the user typed, not its neighbour"
|
||||
);
|
||||
}
|
||||
|
||||
// ── a coordinate rename over the wire (improvise-fdk1) ───────────────────
|
||||
|
||||
/// Every frame the host addressed, to whichever of the two clients it names.
|
||||
fn route(
|
||||
frames: &[Addressed],
|
||||
a: (improvise_command::ui::project::SessionId, &mut Client),
|
||||
b: (improvise_command::ui::project::SessionId, &mut Client),
|
||||
) {
|
||||
for Addressed { to, msg } in frames {
|
||||
let json = serde_json::to_string(msg).expect("a server frame serializes");
|
||||
if *to == a.0 {
|
||||
a.1.on_frame(&json).expect("a folds every frame");
|
||||
}
|
||||
if *to == b.0 {
|
||||
b.1.on_frame(&json).expect("b folds every frame");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Client A renames North's `Region` to `Zed` in records mode; when `racing`,
|
||||
/// client B writes North's Revenue while A's editor is open. Evidence file.
|
||||
fn rename_north(racing: bool) -> (Host, Vec<Addressed>) {
|
||||
let mut host = host();
|
||||
let (mut a, mut b) = (Client::new(), Client::new());
|
||||
let (a_id, a_snap) = host.connect(10, 10);
|
||||
let (b_id, b_snap) = host.connect(10, 10);
|
||||
a.set_viewport(10, 10);
|
||||
b.set_viewport(10, 10);
|
||||
deliver(&mut a, &[a_snap]);
|
||||
deliver(&mut b, &[b_snap]);
|
||||
|
||||
let mut press_a = |host: &mut Host, a: &mut Client, b: &mut Client, key: &str| {
|
||||
let Some(frame) = a.key(&BrowserKey::new(key)) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let json = serde_json::to_string(&frame).expect("a client frame serializes");
|
||||
let frames = send(host, a_id, &json);
|
||||
route(&frames, (a_id, &mut *a), (b_id, &mut *b));
|
||||
frames
|
||||
};
|
||||
|
||||
press_a(&mut host, &mut a, &mut b, "T");
|
||||
for (category, key) in [("_Index", "r"), ("_Dim", "c")] {
|
||||
let at = a
|
||||
.state()
|
||||
.tiles
|
||||
.tiles
|
||||
.iter()
|
||||
.position(|t| t.category == category)
|
||||
.unwrap_or_else(|| panic!("the tile bar must carry {category}"));
|
||||
while a.state().view.tile_cat_idx < at {
|
||||
press_a(&mut host, &mut a, &mut b, "l");
|
||||
}
|
||||
while a.state().view.tile_cat_idx > at {
|
||||
press_a(&mut host, &mut a, &mut b, "h");
|
||||
}
|
||||
press_a(&mut host, &mut a, &mut b, key);
|
||||
}
|
||||
press_a(&mut host, &mut a, &mut b, "Escape");
|
||||
assert!(a.state().grid.records, "A must be standing in records mode");
|
||||
|
||||
let coordinate = (0..a.state().grid.extent.cols)
|
||||
.find(|&c| {
|
||||
format!("{:?}", a.state().grid.cell(0, c).and_then(|x| x.id.clone()))
|
||||
.starts_with("Some(Coordinate")
|
||||
})
|
||||
.expect("records mode projects a coordinate column");
|
||||
while a.state().view.cursor().selected.1 < coordinate {
|
||||
press_a(&mut host, &mut a, &mut b, "l");
|
||||
}
|
||||
while a.state().view.cursor().selected.1 > coordinate {
|
||||
press_a(&mut host, &mut a, &mut b, "h");
|
||||
}
|
||||
press_a(&mut host, &mut a, &mut b, "i");
|
||||
assert!(
|
||||
a.state().view.mode.is_editing(),
|
||||
"A's editor must open on the coordinate, or nothing here is renamed"
|
||||
);
|
||||
assert_eq!(
|
||||
a.state().view.buffers.get("edit").map(String::as_str),
|
||||
Some("North")
|
||||
);
|
||||
|
||||
if racing {
|
||||
let revenue = (0..b.state().grid.extent.cols)
|
||||
.find(|&c| {
|
||||
b.record_cell_at(0, c)
|
||||
.is_some_and(|cell| cell.measure.as_deref() == Some("Revenue"))
|
||||
})
|
||||
.expect("B sees North's Revenue");
|
||||
let frame = b.typed_cell_frame(0, revenue, "999").expect("B composes a write");
|
||||
let frames = send(
|
||||
&mut host,
|
||||
b_id,
|
||||
&serde_json::to_string(&frame).expect("json"),
|
||||
);
|
||||
assert!(
|
||||
frames.iter().any(|f| f.to == b_id
|
||||
&& matches!(f.msg, improvise_protocol::ServerFrame::Accepted)),
|
||||
"B's write must land, or A has nothing to be stale against: {frames:?}"
|
||||
);
|
||||
route(&frames, (a_id, &mut a), (b_id, &mut b));
|
||||
}
|
||||
|
||||
for key in ["Backspace"; 5] {
|
||||
press_a(&mut host, &mut a, &mut b, key);
|
||||
}
|
||||
for key in ["Z", "e", "d"] {
|
||||
press_a(&mut host, &mut a, &mut b, key);
|
||||
}
|
||||
let answers = press_a(&mut host, &mut a, &mut b, "Enter");
|
||||
(host, answers)
|
||||
}
|
||||
|
||||
fn region(host: &Host, item: &str) -> Option<CellValue> {
|
||||
host.app()
|
||||
.model_state
|
||||
.workbook
|
||||
.model
|
||||
.get_cell(&CellKey::new(vec![
|
||||
("Region".into(), item.into()),
|
||||
("_Measure".into(), "Revenue".into()),
|
||||
]))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// improvise-fdk1: the rename moves the record, claimed at the version the
|
||||
/// editor opened on. Evidence: `context/test-evidence/clients.md`.
|
||||
#[test]
|
||||
fn a_browser_renames_a_records_coordinate_and_the_host_moves_the_record() {
|
||||
let (host, answers) = rename_north(false);
|
||||
assert!(
|
||||
answers
|
||||
.iter()
|
||||
.any(|f| matches!(f.msg, improvise_protocol::ServerFrame::Accepted)),
|
||||
"the rename must be accepted: {answers:?}"
|
||||
);
|
||||
assert_eq!(region(&host, "Zed"), Some(CellValue::Number(1200.0)));
|
||||
assert_eq!(region(&host, "North"), None, "the record moved, not copied");
|
||||
}
|
||||
|
||||
/// improvise-fdk1 / improvise-0j0f: another client wrote the record while the
|
||||
/// editor was open, so the rename is stale. Evidence file.
|
||||
#[test]
|
||||
fn a_coordinate_rename_another_client_moved_is_refused() {
|
||||
let (host, answers) = rename_north(true);
|
||||
assert!(
|
||||
answers.iter().any(|f| matches!(
|
||||
f.msg,
|
||||
improvise_protocol::ServerFrame::Rejected(improvise_protocol::Rejection::RecordMoved(_))
|
||||
)),
|
||||
"a rename composed against a record someone else wrote must be refused: {answers:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
region(&host, "North"),
|
||||
Some(CellValue::Number(999.0)),
|
||||
"the other client's write must survive"
|
||||
);
|
||||
assert_eq!(region(&host, "Zed"), None, "nothing was renamed");
|
||||
}
|
||||
|
||||
@@ -1950,3 +1950,67 @@ fn every_projected_edit_text_commits_back_to_its_value() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A coordinate rename (improvise-fdk1).
|
||||
|
||||
/// Two records, North renamed past South so the sorted records order moves.
|
||||
/// Evidence: `context/test-evidence/clients.md`.
|
||||
#[test]
|
||||
fn a_locally_committed_coordinate_rename_agrees_with_the_host() {
|
||||
let r = KeyEvent::new(KeyCode::Char('R'), KeyModifiers::NONE);
|
||||
let two_records = || {
|
||||
let mut app = furnished_host();
|
||||
let south = north_revenue().with("Region", "South");
|
||||
app.model_state
|
||||
.workbook
|
||||
.model
|
||||
.set_cell(south, improvise_protocol::CellValue::Number(900.0));
|
||||
app.rebuild_layout();
|
||||
reduce_key(&mut app, r);
|
||||
app
|
||||
};
|
||||
let (mut app, mut twin) = (two_records(), two_records());
|
||||
let region = (0..app.layout.col_count())
|
||||
.find(|&c| app.layout.col_label(c) == "Region")
|
||||
.expect("a Region column");
|
||||
let north = 0;
|
||||
assert_eq!(
|
||||
app.layout.record_row(north).unwrap().coords.get("Region"),
|
||||
Some("North"),
|
||||
"the fixture renames North, at row 0"
|
||||
);
|
||||
for host in [&mut app, &mut twin] {
|
||||
host.cursor_mut().selected = (north, region);
|
||||
}
|
||||
let mut client = records_client(&app);
|
||||
|
||||
both_ends(&mut app, &mut twin, &mut client, "i");
|
||||
assert_eq!(
|
||||
client.view.buffers.get("edit").map(String::as_str),
|
||||
Some("North"),
|
||||
"the editor opens on the coordinate it renames"
|
||||
);
|
||||
for key in [
|
||||
"Backspace",
|
||||
"Backspace",
|
||||
"Backspace",
|
||||
"Backspace",
|
||||
"Backspace",
|
||||
"Z",
|
||||
"e",
|
||||
"d",
|
||||
] {
|
||||
both_ends(&mut app, &mut twin, &mut client, key);
|
||||
}
|
||||
both_ends(&mut app, &mut twin, &mut client, "Enter");
|
||||
|
||||
let text = document_text(&app);
|
||||
assert!(
|
||||
text.contains("Region=Zed") && !text.contains("Region=North"),
|
||||
"the rename must move North's cells to Zed: {text}"
|
||||
);
|
||||
assert!(
|
||||
text.contains("Region=South"),
|
||||
"South is a bystander and must survive: {text}"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user