fix: widen undo dialog and polish its summary line
The undo/redo confirmation dialog capped at 60 columns and wrapped even a short insert on wide terminals, showed a lowercase "snapshot taken", a raw ISO-8601 timestamp, and lowercase yes/no labels. Grow the dialog to fit its longest line (bounded 34–100), capitalise Snapshot/Yes/No, and render the snapshot timestamp in local time, human-formatted (24 May 2026, 11:00) via a new chrono dependency (clock feature only; English month names). Yes/No capitalisation also applies to the rebuild-confirm dialog.
This commit is contained in:
Generated
+51
@@ -23,6 +23,15 @@ version = "0.2.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.102"
|
version = "1.0.102"
|
||||||
@@ -155,6 +164,17 @@ dependencies = [
|
|||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"num-traits",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "compact_str"
|
name = "compact_str"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -189,6 +209,12 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -675,6 +701,30 @@ version = "0.4.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.65"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "id-arena"
|
name = "id-arena"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@@ -1370,6 +1420,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
"chrono",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"csv",
|
"csv",
|
||||||
"directories",
|
"directories",
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ publish = false
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.102"
|
anyhow = "1.0.102"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
|
# `clock` brings local-timezone support (UTC → machine local) for
|
||||||
|
# the undo-dialog snapshot timestamp (issue #13). No locale feature:
|
||||||
|
# month names stay English; `unstable-locales` is deliberately avoided.
|
||||||
|
chrono = { version = "0.4.44", default-features = false, features = ["clock"] }
|
||||||
crossterm = { version = "0.29.0", features = ["event-stream"] }
|
crossterm = { version = "0.29.0", features = ["event-stream"] }
|
||||||
csv = "1.4.0"
|
csv = "1.4.0"
|
||||||
directories = "6.0.0"
|
directories = "6.0.0"
|
||||||
|
|||||||
@@ -453,7 +453,11 @@ handoff-14 cleanup; 449 after B2/C2.)
|
|||||||
undone / re-applied (`Y` confirms). *(Implemented 2026-05-24:
|
undone / re-applied (`Y` confirms). *(Implemented 2026-05-24:
|
||||||
`undo` / `redo` app commands, `Modal::UndoConfirm`, runtime
|
`undo` / `redo` app commands, `Modal::UndoConfirm`, runtime
|
||||||
prepare→confirm→restore→refresh; `--no-undo` reports undo is off,
|
prepare→confirm→restore→refresh; `--no-undo` reports undo is off,
|
||||||
empty stacks report "nothing to undo/redo".)*
|
empty stacks report "nothing to undo/redo". UX polish 2026-05-29,
|
||||||
|
issue #13: the confirm dialog grows to fit its summary on one row,
|
||||||
|
capitalises `Snapshot` / `Yes` / `No`, and renders the snapshot
|
||||||
|
timestamp in local time, human-formatted (`24 May 2026, 11:00`) via
|
||||||
|
the new `chrono` dependency.)*
|
||||||
- [x] **U3** `history.log` records every submitted command in
|
- [x] **U3** `history.log` records every submitted command in
|
||||||
append-only form, tagged with its outcome (Iteration 2;
|
append-only form, tagged with its outcome (Iteration 2;
|
||||||
broadened by ADR-0034). Format: `<ISO-8601 Z>|<status>|<source>`
|
broadened by ADR-0034). Format: `<ISO-8601 Z>|<status>|<source>`
|
||||||
|
|||||||
@@ -763,7 +763,7 @@ modal:
|
|||||||
redo_confirm_title: "Redo last undone change"
|
redo_confirm_title: "Redo last undone change"
|
||||||
undo_confirm_command: "This will undo:"
|
undo_confirm_command: "This will undo:"
|
||||||
redo_confirm_command: "This will re-apply:"
|
redo_confirm_command: "This will re-apply:"
|
||||||
undo_confirm_when: "snapshot taken {timestamp}"
|
undo_confirm_when: "Snapshot taken {timestamp}"
|
||||||
undo_confirm_prompt: "Restore that earlier state?"
|
undo_confirm_prompt: "Restore that earlier state?"
|
||||||
undo_cancelled: "undo cancelled"
|
undo_cancelled: "undo cancelled"
|
||||||
redo_cancelled: "redo cancelled"
|
redo_cancelled: "redo cancelled"
|
||||||
@@ -807,8 +807,8 @@ shortcut:
|
|||||||
submit: "submit"
|
submit: "submit"
|
||||||
confirm: "confirm"
|
confirm: "confirm"
|
||||||
cancel: "cancel"
|
cancel: "cancel"
|
||||||
yes: "yes"
|
yes: "Yes"
|
||||||
no: "no"
|
no: "No"
|
||||||
load: "load"
|
load: "load"
|
||||||
select: "select"
|
select: "select"
|
||||||
browse_path: "browse path"
|
browse_path: "browse path"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
|
assertion_line: 1469
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
||||||
@@ -16,7 +17,7 @@ expression: snapshot
|
|||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │Continue? │ │
|
│ │Continue? │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │[Y] yes [N] no Esc cancel │ │
|
│ │[Y] Yes [N] No Esc cancel │ │
|
||||||
│ ╰──────────────────────────────────────────────────────────╯─────────╯
|
│ ╰──────────────────────────────────────────────────────────╯─────────╯
|
||||||
│ │╭ SIMPLE ──────────────────────────────────────────╮
|
│ │╭ SIMPLE ──────────────────────────────────────────╮
|
||||||
│ ││ │
|
│ ││ │
|
||||||
|
|||||||
@@ -317,6 +317,52 @@ fn render_rebuild_confirm(summary: &str, theme: &Theme, frame: &mut Frame<'_>, a
|
|||||||
frame.render_widget(paragraph, dialog_area);
|
frame.render_widget(paragraph, dialog_area);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format a stored ISO-8601 UTC timestamp for display in a
|
||||||
|
/// confirmation dialog (issue #13): parse it, convert to the
|
||||||
|
/// machine's local timezone, and render a fixed human-friendly
|
||||||
|
/// form (`24 May 2026, 11:00`). Month names stay English — no
|
||||||
|
/// locale feature. Falls back to the raw input if it can't be
|
||||||
|
/// parsed; this is defensive only, since stored values are always
|
||||||
|
/// `utc_iso8601_now()` output.
|
||||||
|
fn format_snapshot_timestamp(iso: &str) -> String {
|
||||||
|
chrono::DateTime::parse_from_rfc3339(iso)
|
||||||
|
.map(|dt| format_local_datetime(dt.with_timezone(&chrono::Local)))
|
||||||
|
.unwrap_or_else(|_| iso.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a timezone-aware datetime in the fixed display form.
|
||||||
|
/// Split out from [`format_snapshot_timestamp`] so the format can
|
||||||
|
/// be unit-tested deterministically with a fixed offset (the
|
||||||
|
/// `Local` conversion itself is machine-dependent).
|
||||||
|
fn format_local_datetime<Tz>(dt: chrono::DateTime<Tz>) -> String
|
||||||
|
where
|
||||||
|
Tz: chrono::TimeZone,
|
||||||
|
Tz::Offset: std::fmt::Display,
|
||||||
|
{
|
||||||
|
dt.format("%-d %b %Y, %H:%M").to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Preferred outer width (columns) for the undo/redo confirm
|
||||||
|
/// dialog (issue #13): wide enough to hold the longest content
|
||||||
|
/// line on a single row, clamped to sane bounds and the available
|
||||||
|
/// area so a short insert no longer wraps on roomy terminals.
|
||||||
|
fn undo_dialog_width(
|
||||||
|
content_widths: impl IntoIterator<Item = usize>,
|
||||||
|
area_width: u16,
|
||||||
|
) -> u16 {
|
||||||
|
/// Floor — comfortably fits the button row plus borders.
|
||||||
|
const MIN: u16 = 34;
|
||||||
|
/// Ceiling for outlier (ultra-wide) terminals.
|
||||||
|
const MAX: u16 = 100;
|
||||||
|
let widest = content_widths.into_iter().max().unwrap_or(0);
|
||||||
|
// +4: left/right border (2) + one padding column each side (2).
|
||||||
|
let preferred =
|
||||||
|
u16::try_from(widest).unwrap_or(u16::MAX).saturating_add(4);
|
||||||
|
let upper = area_width.min(MAX);
|
||||||
|
let lower = MIN.min(upper);
|
||||||
|
preferred.clamp(lower, upper)
|
||||||
|
}
|
||||||
|
|
||||||
/// `undo` / `redo` confirmation modal (ADR-0006 Amendment 1). Names
|
/// `undo` / `redo` confirmation modal (ADR-0006 Amendment 1). Names
|
||||||
/// the command that will be undone / re-applied and when its
|
/// the command that will be undone / re-applied and when its
|
||||||
/// snapshot was taken, then prompts `Y` / `N`.
|
/// snapshot was taken, then prompts `Y` / `N`.
|
||||||
@@ -326,19 +372,48 @@ fn render_undo_confirm(
|
|||||||
frame: &mut Frame<'_>,
|
frame: &mut Frame<'_>,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
) {
|
) {
|
||||||
let dialog_w = area.width.clamp(20, 60);
|
|
||||||
let inner_w = dialog_w.saturating_sub(4) as usize;
|
|
||||||
|
|
||||||
let intro = if m.is_redo {
|
let intro = if m.is_redo {
|
||||||
crate::t!("modal.redo_confirm_command")
|
crate::t!("modal.redo_confirm_command")
|
||||||
} else {
|
} else {
|
||||||
crate::t!("modal.undo_confirm_command")
|
crate::t!("modal.undo_confirm_command")
|
||||||
};
|
};
|
||||||
let mut body_lines: Vec<String> = wrap_lines(&format!("{intro} {}", m.command), inner_w);
|
let title = if m.is_redo {
|
||||||
body_lines.extend(wrap_lines(
|
crate::t!("modal.redo_confirm_title")
|
||||||
&crate::t!("modal.undo_confirm_when", timestamp = m.timestamp),
|
} else {
|
||||||
inner_w,
|
crate::t!("modal.undo_confirm_title")
|
||||||
));
|
};
|
||||||
|
let intro_line = format!("{intro} {}", m.command);
|
||||||
|
// Local-time, human-formatted snapshot stamp (issue #13).
|
||||||
|
let when_display = format_snapshot_timestamp(&m.timestamp);
|
||||||
|
let when_line =
|
||||||
|
crate::t!("modal.undo_confirm_when", timestamp = when_display);
|
||||||
|
let prompt = crate::t!("modal.undo_confirm_prompt");
|
||||||
|
// Reconstruct the button row purely to measure its width — the
|
||||||
|
// styled spans are built below. Keep this in sync with them.
|
||||||
|
let buttons_measure = format!(
|
||||||
|
"[Y] {} [N] {} Esc {}",
|
||||||
|
crate::t!("shortcut.yes"),
|
||||||
|
crate::t!("shortcut.no"),
|
||||||
|
crate::t!("shortcut.cancel"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Grow the dialog to fit the longest content line on one row
|
||||||
|
// (issue #13). The title sits in the border, so it needs two
|
||||||
|
// extra columns for the surrounding spaces.
|
||||||
|
let dialog_w = undo_dialog_width(
|
||||||
|
[
|
||||||
|
title.chars().count() + 2,
|
||||||
|
intro_line.chars().count(),
|
||||||
|
when_line.chars().count(),
|
||||||
|
prompt.chars().count(),
|
||||||
|
buttons_measure.chars().count(),
|
||||||
|
],
|
||||||
|
area.width,
|
||||||
|
);
|
||||||
|
let inner_w = dialog_w.saturating_sub(4) as usize;
|
||||||
|
|
||||||
|
let mut body_lines: Vec<String> = wrap_lines(&intro_line, inner_w);
|
||||||
|
body_lines.extend(wrap_lines(&when_line, inner_w));
|
||||||
let body_height = body_lines.len() as u16;
|
let body_height = body_lines.len() as u16;
|
||||||
// Title row + blank + body + blank + prompt + blank + keys + borders (2).
|
// Title row + blank + body + blank + prompt + blank + keys + borders (2).
|
||||||
let dialog_h = body_height.saturating_add(7).min(area.height);
|
let dialog_h = body_height.saturating_add(7).min(area.height);
|
||||||
@@ -354,11 +429,6 @@ fn render_undo_confirm(
|
|||||||
|
|
||||||
frame.render_widget(ratatui::widgets::Clear, dialog_area);
|
frame.render_widget(ratatui::widgets::Clear, dialog_area);
|
||||||
|
|
||||||
let title = if m.is_redo {
|
|
||||||
crate::t!("modal.redo_confirm_title")
|
|
||||||
} else {
|
|
||||||
crate::t!("modal.undo_confirm_title")
|
|
||||||
};
|
|
||||||
let title_style = Style::default().fg(theme.fg).add_modifier(Modifier::BOLD);
|
let title_style = Style::default().fg(theme.fg).add_modifier(Modifier::BOLD);
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
@@ -376,7 +446,7 @@ fn render_undo_confirm(
|
|||||||
text_lines.push(Line::from(line));
|
text_lines.push(Line::from(line));
|
||||||
}
|
}
|
||||||
text_lines.push(Line::from(""));
|
text_lines.push(Line::from(""));
|
||||||
text_lines.push(Line::from(crate::t!("modal.undo_confirm_prompt")));
|
text_lines.push(Line::from(prompt));
|
||||||
text_lines.push(Line::from(""));
|
text_lines.push(Line::from(""));
|
||||||
text_lines.push(Line::from(vec![
|
text_lines.push(Line::from(vec![
|
||||||
Span::styled("[Y]", Style::default().fg(theme.fg).add_modifier(Modifier::BOLD)),
|
Span::styled("[Y]", Style::default().fg(theme.fg).add_modifier(Modifier::BOLD)),
|
||||||
@@ -1399,6 +1469,92 @@ mod tests {
|
|||||||
insta::assert_snapshot!("rebuild_confirm_modal_dark", snapshot);
|
insta::assert_snapshot!("rebuild_confirm_modal_dark", snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Issue #13: undo confirm dialog -------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_local_datetime_renders_fixed_human_form() {
|
||||||
|
// Deterministic: a fixed offset (not Local) so the output
|
||||||
|
// does not depend on the test machine's timezone.
|
||||||
|
let dt = chrono::DateTime::parse_from_rfc3339("2026-05-24T11:05:00+02:00")
|
||||||
|
.expect("valid rfc3339");
|
||||||
|
assert_eq!(format_local_datetime(dt), "24 May 2026, 11:05");
|
||||||
|
// Single-digit day: no leading zero on the day, but zero-
|
||||||
|
// padded hour.
|
||||||
|
let dt = chrono::DateTime::parse_from_rfc3339("2026-05-04T09:05:00+00:00")
|
||||||
|
.expect("valid rfc3339");
|
||||||
|
assert_eq!(format_local_datetime(dt), "4 May 2026, 09:05");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_snapshot_timestamp_drops_machine_syntax() {
|
||||||
|
// The stored UTC string is reformatted: no 'T'/'Z' machine
|
||||||
|
// syntax survives, and the year is preserved. (Day/month
|
||||||
|
// can shift across the date line depending on local TZ, so
|
||||||
|
// we assert only the stable parts.)
|
||||||
|
let out = format_snapshot_timestamp("2026-07-24T10:00:00Z");
|
||||||
|
assert!(!out.contains('T'), "no date/time 'T' separator: {out}");
|
||||||
|
assert!(!out.contains('Z'), "no UTC 'Z' suffix: {out}");
|
||||||
|
assert!(out.contains("2026"), "year preserved: {out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_snapshot_timestamp_falls_back_on_garbage() {
|
||||||
|
assert_eq!(format_snapshot_timestamp("not a timestamp"), "not a timestamp");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn undo_dialog_width_grows_to_fit_and_clamps() {
|
||||||
|
// Grows to the widest line + 4 (borders + padding).
|
||||||
|
assert_eq!(undo_dialog_width([50usize], 120), 54);
|
||||||
|
// Floors at MIN (34) for tiny content.
|
||||||
|
assert_eq!(undo_dialog_width([3usize], 120), 34);
|
||||||
|
// Caps at MAX (100) for absurdly long content.
|
||||||
|
assert_eq!(undo_dialog_width([400usize], 120), 100);
|
||||||
|
// Never exceeds the available area, and never panics when
|
||||||
|
// the area is narrower than MIN.
|
||||||
|
assert_eq!(undo_dialog_width([50usize], 40), 40);
|
||||||
|
assert_eq!(undo_dialog_width([50usize], 10), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn undo_modal_command_does_not_wrap_on_wide_terminal() {
|
||||||
|
use crate::app::{Modal, UndoConfirmModal};
|
||||||
|
let mut app = App::new();
|
||||||
|
app.modal = Some(Modal::UndoConfirm(UndoConfirmModal {
|
||||||
|
command: "insert into Customers values (1, 'Oliver Sturm')".to_string(),
|
||||||
|
timestamp: "2026-05-24T10:00:00Z".to_string(),
|
||||||
|
is_redo: false,
|
||||||
|
}));
|
||||||
|
let theme = Theme::dark();
|
||||||
|
let out = render_to_string(&mut app, &theme, 120, 30);
|
||||||
|
assert!(
|
||||||
|
out.lines().any(|l| l.contains(
|
||||||
|
"This will undo: insert into Customers values (1, 'Oliver Sturm')"
|
||||||
|
)),
|
||||||
|
"command must sit on one row on a wide terminal:\n{out}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn undo_modal_uses_capitalized_labels_and_formatted_time() {
|
||||||
|
use crate::app::{Modal, UndoConfirmModal};
|
||||||
|
let mut app = App::new();
|
||||||
|
app.modal = Some(Modal::UndoConfirm(UndoConfirmModal {
|
||||||
|
command: "delete from T where id = 1".to_string(),
|
||||||
|
timestamp: "2026-05-24T10:00:00Z".to_string(),
|
||||||
|
is_redo: false,
|
||||||
|
}));
|
||||||
|
let theme = Theme::dark();
|
||||||
|
let out = render_to_string(&mut app, &theme, 120, 30);
|
||||||
|
assert!(out.contains("Snapshot taken"), "capitalized Snapshot:\n{out}");
|
||||||
|
assert!(out.contains("[Y] Yes"), "capitalized Yes:\n{out}");
|
||||||
|
assert!(out.contains("[N] No"), "capitalized No:\n{out}");
|
||||||
|
assert!(
|
||||||
|
!out.contains("2026-05-24T10:00:00Z"),
|
||||||
|
"raw ISO timestamp must not appear:\n{out}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn populated_with_table_snapshot() {
|
fn populated_with_table_snapshot() {
|
||||||
// Items panel lists tables; output panel shows the
|
// Items panel lists tables; output panel shows the
|
||||||
|
|||||||
Reference in New Issue
Block a user