Company details (EIN, state number, address, every profile value), an entry's number and amount, an account's code and balance, a document's file name and SHA-256 copy on click: a pointer and copy icon on hover, a check for a moment after. Amounts copy as their exact plain decimal (fmt::plain: no "$", commas, or parentheses; "-" for negatives); where a click already drills down (report cells, register and transaction rows, general ledger lines), a right-click menu copies the amounts, the entry number, or the description. The sidebar's chain check copies the journal head and data folder. tests/copy.rs: every display style copies back to the exact value (property), and clicking the EIN, an entry's amount, and its number puts exactly that on the clipboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
1 parent
8ac4e5438d
commit
eba85f9c77
10 files changed
+339
-12
No files matched your search
@@ -675,12 +675,23 @@ fn live_status(ui: &mut Ui, b: &Books, since_update: f64, live: bool) {
|
||||
} else {
|
||||
(icon::WARNING_CIRCLE, p.negative)
|
||||
};
|
||||
ui.label(
|
||||
let r = ui
|
||||
.label(
|
||||
RichText::new(format!("{glyph} {text}"))
|
||||
.font(theme::regular(11.5))
|
||||
.color(color),
|
||||
)
|
||||
.on_hover_text(tip);
|
||||
if text == "Journal chain verified" {
|
||||
r.context_menu(|ui| {
|
||||
crate::widgets::copy_item(ui, "Copy journal head", &b.head);
|
||||
crate::widgets::copy_item(
|
||||
ui,
|
||||
"Copy data folder",
|
||||
&b.data_dir.display().to_string(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,6 +87,31 @@ fn group(d: Decimal) -> String {
|
||||
format!("{out}.{frac}")
|
||||
}
|
||||
|
||||
/// The plain decimal behind a displayed amount, for the clipboard:
|
||||
/// `$1,234.56` → `1234.56`, `(207.68)` and `−12.00` → `-207.68`, `-12.00`.
|
||||
/// Exact (the display never rounds). None when the text isn't an amount.
|
||||
pub fn plain(display: &str) -> Option<String> {
|
||||
let t = display.trim();
|
||||
let (neg, t) = match t.strip_prefix('(').and_then(|x| x.strip_suffix(')')) {
|
||||
Some(inner) => (true, inner),
|
||||
None => (false, t),
|
||||
};
|
||||
let (neg, t) = match t.strip_prefix('\u{2212}').or_else(|| t.strip_prefix('-')) {
|
||||
Some(rest) => (!neg, rest),
|
||||
None => (neg, t.strip_prefix('+').unwrap_or(t)),
|
||||
};
|
||||
let digits: String = t.chars().filter(|c| !matches!(c, '$' | ',')).collect();
|
||||
let d: Decimal = digits.parse().ok()?;
|
||||
if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit() || c == '.') {
|
||||
return None;
|
||||
}
|
||||
Some(if neg && !d.is_zero() {
|
||||
format!("-{digits}")
|
||||
} else {
|
||||
digits
|
||||
})
|
||||
}
|
||||
|
||||
/// A percent change from `prev` to `now`, if `prev` isn't zero.
|
||||
pub fn pct_change(prev: Decimal, now: Decimal) -> Option<f64> {
|
||||
if prev.is_zero() {
|
||||
@@ -213,5 +238,12 @@ mod tests {
|
||||
assert_eq!(label("ein"), "EIN");
|
||||
assert_eq!(label("irs_name_control"), "IRS name control");
|
||||
assert_eq!(words("PrepaidExpense"), "Prepaid expense");
|
||||
assert_eq!(plain("$1,234.56").as_deref(), Some("1234.56"));
|
||||
assert_eq!(plain("(207.68)").as_deref(), Some("-207.68"));
|
||||
assert_eq!(plain("\u{2212}$12.00").as_deref(), Some("-12.00"));
|
||||
assert_eq!(plain("+1,000.005").as_deref(), Some("1000.005"));
|
||||
assert_eq!(plain("0.00").as_deref(), Some("0.00"));
|
||||
assert_eq!(plain("Debit"), None);
|
||||
assert_eq!(plain(""), None);
|
||||
}
|
||||
}
|
||||
@@ -168,18 +168,22 @@ pub fn show_account(ui: &mut Ui, b: &Books, id: u64, shell: &mut Shell<'_>) {
|
||||
.range
|
||||
.unwrap_or((first.min(b.today), b.today));
|
||||
page(ui, "account", |ui| {
|
||||
ui.label(
|
||||
widgets::copyable(
|
||||
ui,
|
||||
RichText::new(format!("{:?} account · {}", a.account_type, a.code))
|
||||
.font(theme::regular(13.0))
|
||||
.color(p.muted),
|
||||
&a.code,
|
||||
);
|
||||
ui.label(RichText::new(&a.name).font(theme::bold(26.0)));
|
||||
ui.add_space(4.0);
|
||||
let bal = b.balance_on(id, b.today);
|
||||
ui.label(
|
||||
widgets::copyable(
|
||||
ui,
|
||||
RichText::new(format!("Balance today {}", fmt::dollars(bal)))
|
||||
.font(theme::regular(15.0))
|
||||
.color(p.muted),
|
||||
&fmt::plain(&fmt::dollars(bal)).unwrap_or_default(),
|
||||
);
|
||||
ui.add_space(14.0);
|
||||
let hist = balance_history(&b.ledger, &[id]);
|
||||
@@ -274,6 +278,25 @@ pub fn show_account(ui: &mut Ui, b: &Books, id: u64, shell: &mut Shell<'_>) {
|
||||
vec2(w, if sub.is_empty() { 30.0 } else { 42.0 }),
|
||||
Sense::click(),
|
||||
);
|
||||
resp.context_menu(|ui| {
|
||||
for (label, v) in [("Copy debit", d), ("Copy credit", c)] {
|
||||
if !v.is_zero() {
|
||||
widgets::copy_item(
|
||||
ui,
|
||||
label,
|
||||
&fmt::plain(&fmt::money(v)).unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
}
|
||||
widgets::copy_item(
|
||||
ui,
|
||||
"Copy balance",
|
||||
&fmt::plain(&fmt::money(bal)).unwrap_or_default(),
|
||||
);
|
||||
if let Some(id) = sub.strip_prefix('#').and_then(|t| t.split(' ').next()) {
|
||||
widgets::copy_item(ui, "Copy entry number", &format!("#{id}"));
|
||||
}
|
||||
});
|
||||
if resp.hovered() && !strong {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(6), p.hover);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ fn kv(ui: &mut Ui, k: &str, v: &str) {
|
||||
// Narrow, the label goes above its value.
|
||||
if ui.available_width() < 360.0 {
|
||||
ui.label(RichText::new(k).font(theme::regular(12.0)).color(p.muted));
|
||||
ui.add(egui::Label::new(RichText::new(v).font(theme::regular(13.0))).wrap());
|
||||
widgets::copyable(ui, RichText::new(v).font(theme::regular(13.0)), v);
|
||||
ui.add_space(4.0);
|
||||
return;
|
||||
}
|
||||
@@ -34,7 +34,7 @@ fn kv(ui: &mut Ui, k: &str, v: &str) {
|
||||
theme::regular(13.0),
|
||||
p.muted,
|
||||
);
|
||||
ui.add(egui::Label::new(RichText::new(v).font(theme::regular(13.0))).wrap());
|
||||
widgets::copyable(ui, RichText::new(v).font(theme::regular(13.0)), v);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -246,7 +246,11 @@ fn detail(ui: &mut Ui, b: &Books, a: &Attachment, shell: &mut Shell<'_>) {
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.label(RichText::new(&a.file_name).font(theme::bold(20.0)));
|
||||
widgets::copyable(
|
||||
ui,
|
||||
RichText::new(&a.file_name).font(theme::bold(20.0)),
|
||||
&a.file_name,
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
egui::Grid::new(("docfacts", a.id))
|
||||
.num_columns(2)
|
||||
@@ -265,8 +269,10 @@ fn detail(ui: &mut Ui, b: &Books, a: &Attachment, shell: &mut Shell<'_>) {
|
||||
ui.label(fmt::timestamp(&a.created_at));
|
||||
ui.end_row();
|
||||
k(ui, "SHA-256");
|
||||
ui.add(
|
||||
egui::Label::new(RichText::new(&a.blob_sha).monospace().size(11.5)).wrap(),
|
||||
widgets::copyable(
|
||||
ui,
|
||||
RichText::new(&a.blob_sha).monospace().size(11.5),
|
||||
&a.blob_sha,
|
||||
);
|
||||
ui.end_row();
|
||||
if let Some(e) = a.entry_id {
|
||||
|
||||
@@ -24,10 +24,12 @@ pub fn detail(ui: &mut Ui, b: &Books, e: &EntryInfo, shell: &mut Shell<'_>) {
|
||||
.auto_shrink(false)
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
widgets::copyable(
|
||||
ui,
|
||||
RichText::new(format!("Entry #{}", e.id))
|
||||
.font(theme::regular(12.5))
|
||||
.color(p.muted),
|
||||
&format!("#{}", e.id),
|
||||
);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui
|
||||
@@ -106,7 +108,11 @@ pub fn detail(ui: &mut Ui, b: &Books, e: &EntryInfo, shell: &mut Shell<'_>) {
|
||||
ui.label(fmt::date(e.date));
|
||||
ui.end_row();
|
||||
k(ui, "Amount");
|
||||
ui.label(RichText::new(fmt::dollars(e.amount)).font(theme::semibold(14.0)));
|
||||
widgets::copyable(
|
||||
ui,
|
||||
RichText::new(fmt::dollars(e.amount)).font(theme::semibold(14.0)),
|
||||
&fmt::plain(&fmt::dollars(e.amount)).unwrap_or_default(),
|
||||
);
|
||||
ui.end_row();
|
||||
if let Some(c) = &created {
|
||||
k(ui, "Recorded");
|
||||
|
||||
@@ -1540,15 +1540,37 @@ fn render(ui: &mut Ui, t: &Table, pdf: &PdfInfo, shell: &mut Shell<'_>) {
|
||||
egui::pos2(x1 - col_w + 8.0, rr.top()),
|
||||
egui::pos2(x1, rr.bottom()),
|
||||
);
|
||||
let copy = fmt::plain(&text);
|
||||
let cresp = ui.interact(
|
||||
cell,
|
||||
ui.id().with(("cell", ri, i)),
|
||||
if c.drill.is_some() {
|
||||
if c.drill.is_some() || copy.is_some() {
|
||||
Sense::click()
|
||||
} else {
|
||||
Sense::hover()
|
||||
},
|
||||
);
|
||||
// An amount copies as its plain decimal: a click
|
||||
// where the cell doesn't drill down, the right-click
|
||||
// menu where it does.
|
||||
if let Some(v) = © {
|
||||
if c.drill.is_some() {
|
||||
cresp.context_menu(|ui| widgets::copy_item(ui, "Copy amount", v));
|
||||
} else {
|
||||
if cresp.hovered() {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
|
||||
}
|
||||
if cresp.clicked() {
|
||||
widgets::copy_now(ui, cresp.id, v);
|
||||
}
|
||||
let note = if widgets::recently_copied(ui, cresp.id) {
|
||||
"Copied"
|
||||
} else {
|
||||
"Click to copy"
|
||||
};
|
||||
cresp.clone().on_hover_text(note);
|
||||
}
|
||||
}
|
||||
let hovered = c.drill.is_some() && cresp.hovered();
|
||||
let vcolor = if hovered { p.accent } else { color };
|
||||
let vfont = if matches!(r.kind, Kind::Line) {
|
||||
@@ -1677,6 +1699,15 @@ fn gl(ui: &mut Ui, b: &Books, s: NaiveDate, e: NaiveDate, shell: &mut Shell<'_>)
|
||||
color: egui::Color32|
|
||||
-> egui::Response {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(w, 24.0), Sense::click());
|
||||
resp.context_menu(|ui| {
|
||||
for v in &vals {
|
||||
if let Some(id) = v.strip_prefix('#') {
|
||||
widgets::copy_item(ui, "Copy entry number", &format!("#{id}"));
|
||||
} else if let Some(a) = fmt::plain(v) {
|
||||
widgets::copy_item(ui, &format!("Copy {v}"), &a);
|
||||
}
|
||||
}
|
||||
});
|
||||
if resp.hovered() && !strong {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(4), p.hover);
|
||||
}
|
||||
|
||||
@@ -134,6 +134,22 @@ fn list(ui: &mut Ui, b: &Books, selected: Option<u64>, shell: &mut Shell<'_>) {
|
||||
fn row(ui: &mut Ui, b: &Books, e: &EntryInfo, on: bool, since_update: f64) -> egui::Response {
|
||||
let p = pal(ui.ctx());
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 50.0), Sense::click());
|
||||
resp.context_menu(|ui| {
|
||||
widgets::copy_item(
|
||||
ui,
|
||||
"Copy amount",
|
||||
&fmt::plain(&fmt::money(e.amount)).unwrap_or_default(),
|
||||
);
|
||||
if !e.cash_effect.is_zero() {
|
||||
widgets::copy_item(
|
||||
ui,
|
||||
"Copy cash effect",
|
||||
&fmt::plain(&fmt::money(e.cash_effect)).unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
widgets::copy_item(ui, "Copy entry number", &format!("#{}", e.id));
|
||||
widgets::copy_item(ui, "Copy description", &e.description);
|
||||
});
|
||||
let fresh = b.fresh.contains(&e.id) && since_update < 8.0;
|
||||
if on {
|
||||
ui.painter()
|
||||
|
||||
@@ -114,15 +114,35 @@ pub fn amount_cell(
|
||||
} else {
|
||||
theme::regular(14.0)
|
||||
};
|
||||
// An amount copies as its plain decimal: a click when the cell doesn't
|
||||
// drill down, the right-click menu when it does.
|
||||
let copy = crate::fmt::plain(text);
|
||||
let (rect, resp) = ui.allocate_exact_size(
|
||||
vec2(width, 22.0),
|
||||
if drill {
|
||||
if drill || copy.is_some() {
|
||||
Sense::click()
|
||||
} else {
|
||||
Sense::hover()
|
||||
},
|
||||
);
|
||||
let p = pal(ui.ctx());
|
||||
let copy_id = resp.id.with("copy");
|
||||
if let Some(v) = © {
|
||||
if drill {
|
||||
resp.context_menu(|ui| copy_item(ui, "Copy amount", v));
|
||||
} else if resp.clicked() {
|
||||
copy_now(ui, copy_id, v);
|
||||
}
|
||||
}
|
||||
let copied = copy.is_some() && !drill && recently_copied(ui, copy_id);
|
||||
if copy.is_some() && !drill && resp.hovered() {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
|
||||
}
|
||||
let resp = if copy.is_some() && !drill {
|
||||
resp.on_hover_text(if copied { "Copied" } else { "Click to copy" })
|
||||
} else {
|
||||
resp
|
||||
};
|
||||
let hovered = drill && resp.hovered();
|
||||
let galley = ui.painter().layout_no_wrap(
|
||||
text.to_string(),
|
||||
@@ -143,6 +163,15 @@ pub fn amount_cell(
|
||||
Stroke::new(1.0, p.accent),
|
||||
);
|
||||
}
|
||||
if copied {
|
||||
ui.painter().text(
|
||||
egui::pos2(pos.x - 4.0, rect.center().y),
|
||||
egui::Align2::RIGHT_CENTER,
|
||||
egui_phosphor::regular::CHECK,
|
||||
egui::FontId::proportional(12.0),
|
||||
p.positive,
|
||||
);
|
||||
}
|
||||
ui.painter().galley(pos, galley, color);
|
||||
resp
|
||||
}
|
||||
@@ -223,6 +252,73 @@ pub fn empty(ui: &mut Ui, icon: &str, title: &str, text: &str) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Put `value` on the clipboard, remembering when under `id` (for the
|
||||
/// brief "Copied" check).
|
||||
pub fn copy_now(ui: &Ui, id: egui::Id, value: &str) {
|
||||
ui.ctx().copy_text(value.to_string());
|
||||
let now = ui.input(|i| i.time);
|
||||
ui.ctx().data_mut(|d| d.insert_temp(id, now));
|
||||
}
|
||||
|
||||
/// Whether `id` was copied in the last moment; keeps repainting until the
|
||||
/// check fades.
|
||||
pub fn recently_copied(ui: &Ui, id: egui::Id) -> bool {
|
||||
let at: Option<f64> = ui.ctx().data(|d| d.get_temp(id));
|
||||
let now = ui.input(|i| i.time);
|
||||
let recent = at.is_some_and(|t| now - t < 1.2);
|
||||
if recent {
|
||||
ui.ctx()
|
||||
.request_repaint_after(std::time::Duration::from_millis(200));
|
||||
}
|
||||
recent
|
||||
}
|
||||
|
||||
/// A value that copies itself when clicked: a pointer and a copy icon on
|
||||
/// hover, a check for a moment after.
|
||||
pub fn copyable(ui: &mut Ui, text: RichText, value: &str) -> Response {
|
||||
let id = ui.id().with(("copy", value));
|
||||
let p = pal(ui.ctx());
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 4.0;
|
||||
let r = ui.add(egui::Label::new(text).sense(Sense::click()));
|
||||
let recent = recently_copied(ui, id);
|
||||
let (slot, _) = ui.allocate_exact_size(vec2(14.0, 14.0), Sense::hover());
|
||||
if r.hovered() || recent {
|
||||
ui.painter().text(
|
||||
slot.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
if recent {
|
||||
egui_phosphor::regular::CHECK
|
||||
} else {
|
||||
egui_phosphor::regular::COPY
|
||||
},
|
||||
egui::FontId::proportional(12.0),
|
||||
if recent { p.positive } else { p.muted },
|
||||
);
|
||||
}
|
||||
if r.hovered() {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
|
||||
}
|
||||
let r = r.on_hover_text(if recent { "Copied" } else { "Click to copy" });
|
||||
if r.clicked() {
|
||||
copy_now(ui, id, value);
|
||||
}
|
||||
r
|
||||
})
|
||||
.inner
|
||||
}
|
||||
|
||||
/// A "Copy …" item for a right-click menu.
|
||||
pub fn copy_item(ui: &mut Ui, label: &str, value: &str) {
|
||||
if ui
|
||||
.button(format!("{} {label}", egui_phosphor::regular::COPY))
|
||||
.clicked()
|
||||
{
|
||||
ui.ctx().copy_text(value.to_string());
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// "Copy prompt": puts an agent prompt on the clipboard.
|
||||
pub fn copy_prompt(ui: &mut Ui, prompt: &str) {
|
||||
let p = pal(ui.ctx());
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
//! Click to copy: values leave the viewer exactly.
|
||||
//!
|
||||
//! ```text
|
||||
//! GOAL: what people retype into forms (the EIN, an amount, an entry
|
||||
//! number) can be copied instead, and copies exactly.
|
||||
//! SOURCE: DECISION (Stephen, 2026-10-14: click-to-copy on the company
|
||||
//! page, amounts, and identifiers).
|
||||
//! INVARIANTS:
|
||||
//! 1. An amount copies as its plain decimal: fmt::plain of money,
|
||||
//! accounting, or dollars text is the exact value, with "-" for a
|
||||
//! negative and no "$", commas, or parentheses.
|
||||
//! 2. Clicking a company value or an entry's amount puts exactly that
|
||||
//! value on the clipboard.
|
||||
//! ```
|
||||
|
||||
use egui::OutputCommand;
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use ledger_gui::nav::Route;
|
||||
use ledger_gui::{App, Source, data, fmt};
|
||||
use proptest::prelude::*;
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
proptest! {
|
||||
/// INVARIANT 1: every display style copies back to the same value.
|
||||
#[test]
|
||||
fn an_amount_copies_as_its_exact_value(
|
||||
mantissa in -10i128.pow(16)..10i128.pow(16),
|
||||
scale in 0u32..8,
|
||||
) {
|
||||
let d = Decimal::from_i128_with_scale(mantissa, scale);
|
||||
for shown in [fmt::money(d), fmt::accounting(d), fmt::dollars(d)] {
|
||||
let plain = fmt::plain(&shown).expect("an amount");
|
||||
prop_assert!(!plain.contains(['$', ',', '(', ')', '\u{2212}']), "{}", plain);
|
||||
prop_assert_eq!(plain.parse::<Decimal>().unwrap(), d, "{} copied as {}", shown, plain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn copied(h: &Harness<'_, App>) -> Vec<String> {
|
||||
h.output()
|
||||
.platform_output
|
||||
.commands
|
||||
.iter()
|
||||
.filter_map(|c| match c {
|
||||
OutputCommand::CopyText(t) => Some(t.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// INVARIANT 2, on the demo books.
|
||||
#[test]
|
||||
fn clicking_a_value_copies_it() {
|
||||
let dir = std::env::temp_dir().join(format!("ledger-gui-copy-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
let today = chrono::NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
|
||||
ledger_gui::demo::build(&dir, today).unwrap();
|
||||
let source = Source {
|
||||
data_dir: dir.join("books-data"),
|
||||
books_toml: Some(dir.join("wanderbooks.toml")),
|
||||
today: Some(today),
|
||||
};
|
||||
let open = |route: Route| {
|
||||
let books = data::load_once(&source).unwrap();
|
||||
let mut h = Harness::builder()
|
||||
.with_size(egui::vec2(1280.0, 900.0))
|
||||
.build_eframe(|cc| App::with_books(&cc.egui_ctx, books, false, route));
|
||||
h.run_steps(3);
|
||||
h
|
||||
};
|
||||
|
||||
// The company's EIN, as written in wanderbooks.toml.
|
||||
let ein = data::load_once(&source)
|
||||
.unwrap()
|
||||
.settings
|
||||
.company
|
||||
.profile
|
||||
.iter()
|
||||
.find(|(k, _)| k == "ein")
|
||||
.map(|(_, v)| v.clone())
|
||||
.expect("the demo has an EIN");
|
||||
let mut h = open(Route::Company);
|
||||
h.get_by_label(&ein).click();
|
||||
h.step();
|
||||
assert_eq!(copied(&h), [ein]);
|
||||
|
||||
// An entry's amount, as a plain decimal.
|
||||
let books = data::load_once(&source).unwrap();
|
||||
let e = books.entries.iter().find(|e| !e.amount.is_zero()).unwrap();
|
||||
let (id, amount) = (e.id, e.amount);
|
||||
drop(books);
|
||||
let mut h = open(Route::Entry(id));
|
||||
let shown = fmt::dollars(amount);
|
||||
h.get_by_label(&shown).click();
|
||||
h.step();
|
||||
assert_eq!(copied(&h), [fmt::plain(&shown).unwrap()]);
|
||||
assert_eq!(copied(&h)[0].parse::<Decimal>().unwrap(), amount);
|
||||
|
||||
// The entry number.
|
||||
let mut h = open(Route::Entry(id));
|
||||
h.get_by_label(&format!("Entry #{id}")).click();
|
||||
h.step();
|
||||
assert_eq!(copied(&h), [format!("#{id}")]);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
Reference in new issue
Block a user