Files
vectorcraft/crates/engine/src/tests/spelling.rs
T
HassanKhan 5bef0ed79a Part of #4936: custom dictionary and Ignore All commands
text.spelling.addWord, removeWord and changeWord are Check Spelling's Add
and Edit Custom Dictionary's Add, Delete and Change; text.spelling.dictionary
lists the words. They live in craft-proof, which every language's check
reads. The desktop app and vectorcraft-cli keep them in
`Custom Dictionary.txt` next to the preferences (none without preferences);
elsewhere they last until the app closes.

text.spelling.ignoreAll is Ignore All: the active document's checks skip the
word while it is open; not saved, not undoable, and not a change to the
document's revision.

Behaviour: https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/check-and-correct-spelling.html
2026-11-09 13:54:08 +05:00

204 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Edit → Spelling (`text.spelling.check`, `text.spelling.suggest`): the unknown words of the
//! document's type, at the bytes `text.editRange` changes, each in its run's language.
use serde_json::{Value, json};
use crate::*;
/// A session with a new 400 × 400 document.
fn session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
s
}
/// Runs a command that must succeed and gives its result.
fn run(s: &mut Session, id: &str, p: Value) -> Value {
s.execute(id, &p).unwrap()
}
/// New point type at (10, `y`) holding `t`.
fn text(s: &mut Session, y: f64, t: &str) -> NodeId {
NodeId(run(s, "text.create", json!({"x": 10, "y": y, "text": t}))["id"].as_u64().unwrap())
}
/// The unknown words a check lists: (id, start, end, word).
fn unknown(s: &mut Session, p: Value) -> Vec<(u64, u64, u64, String)> {
let r = run(s, "text.spelling.check", p);
assert_eq!(r["more"], false);
r["words"]
.as_array()
.unwrap()
.iter()
.map(|w| (w["id"].as_u64().unwrap(), w["start"].as_u64().unwrap(), w["end"].as_u64().unwrap(), w["word"].as_str().unwrap().to_string()))
.collect()
}
#[test]
fn check_lists_unknown_words_at_the_bytes_edit_range_changes() {
let mut s = session();
// "—" is three bytes, so the unknown word starts at byte 12, not character 10. Punctuation,
// not a word, so the result doesn't depend on which dictionary is built in.
let t = text(&mut s, 50.0, "drawing — qzxvw tools");
assert_eq!(unknown(&mut s, json!({})), [(t.0, 12, 17, "qzxvw".to_string())]);
let w = &run(&mut s, "text.spelling.check", json!({}))["words"][0];
assert_eq!(w["lang"], "en", "text with no language is checked in English");
// Change: the bytes the check gave, as one undo step.
run(&mut s, "text.editRange", json!({"id": t.0, "start": 12, "end": 17, "insert": "vector"}));
assert_eq!(unknown(&mut s, json!({})), []);
run(&mut s, "edit.undo", json!({}));
assert_eq!(unknown(&mut s, json!({})).len(), 1, "undo brings the word back");
}
#[test]
fn check_goes_through_visible_unlocked_type_in_paint_order_or_the_ids_given() {
let mut s = session();
let a = text(&mut s, 50.0, "first qzxvw");
let b = text(&mut s, 100.0, "second wvxzq");
let hidden = text(&mut s, 150.0, "hidden qqzzx");
let locked = text(&mut s, 200.0, "locked zzqqx");
run(&mut s, "object.hide", json!({"ids": [hidden.0]}));
run(&mut s, "object.lock", json!({"ids": [locked.0]}));
let ids: Vec<u64> = unknown(&mut s, json!({})).iter().map(|w| w.0).collect();
assert_eq!(ids, [a.0, b.0], "hidden and locked type are left out");
assert_eq!(unknown(&mut s, json!({"ids": [b.0]})), [(b.0, 7, 12, "wvxzq".to_string())]);
assert_eq!(unknown(&mut s, json!({"ids": null})).len(), 2, "null ids is the default");
let both = [(a.0, 6, 11, "qzxvw".to_string()), (b.0, 7, 12, "wvxzq".to_string())];
assert_eq!(unknown(&mut s, json!({"ids": [b.0, a.0]})), both, "given ids are listed in paint order too");
for ids in [json!(b.0), json!("all"), json!({"id": b.0}), json!([-1]), json!(["x"]), json!([999_999])] {
assert!(s.execute("text.spelling.check", &json!({"ids": ids})).is_err(), "malformed ids {ids} is refused");
}
}
#[test]
fn a_language_without_a_dictionary_marks_nothing() {
let mut s = session();
let t = text(&mut s, 50.0, "qzxvw");
run(&mut s, "text.setFormat", json!({"ids": [t.0], "language": "German"}));
assert_eq!(unknown(&mut s, json!({})), [], "German has no spelling dictionary in craft-proof");
}
#[test]
fn suggest_offers_dictionary_words() {
let mut s = session();
let r = run(&mut s, "text.spelling.suggest", json!({"word": "drawng"}));
let got: Vec<&str> = r["suggestions"].as_array().unwrap().iter().map(|w| w.as_str().unwrap()).collect();
assert!(got.len() <= 5, "five at most by default: {got:?}");
assert!(got.contains(&"drawing"), "{got:?}");
assert_eq!(run(&mut s, "text.spelling.suggest", json!({"word": "drawng", "max": 1}))["suggestions"].as_array().unwrap().len(), 1);
assert!(s.execute("text.spelling.suggest", &json!({})).is_err(), "a word is required");
assert!(s.execute("text.spelling.suggest", &json!({"word": " "})).is_err(), "a blank word is refused");
for max in [json!(0), json!(21), json!(-1), json!("3")] {
assert!(s.execute("text.spelling.suggest", &json!({"word": "drawng", "max": max})).is_err(), "max {max}");
}
}
#[test]
fn each_paragraph_is_checked_on_its_own_at_the_bytes_of_the_whole_text() {
let mut s = session();
// Read as one block, the Vietnamese first paragraph would make the whole text Vietnamese,
// which has no dictionary, and the English typo below it would go unmarked.
let vi = "Cảm ơn những người bạn";
let t = text(&mut s, 50.0, &format!("{vi}\ndrawing qzxvw tools"));
let at = vi.len() + 1 + "drawing ".len();
assert_eq!(unknown(&mut s, json!({})), [(t.0, at as u64, at as u64 + 5, "qzxvw".to_string())]);
}
// The custom dictionary is craft-proof's, shared by the whole process, so each test below uses
// made-up words of its own: tests running at the same time never see each other's.
/// The words a check of the whole active document lists.
fn marked(s: &mut Session) -> Vec<String> {
unknown(s, json!({})).into_iter().map(|w| w.3).collect()
}
#[test]
fn added_words_are_right_in_every_check_until_deleted() {
let mut s = session();
text(&mut s, 50.0, "drawing Zqvwkx tools");
assert_eq!(marked(&mut s), ["Zqvwkx"]);
// Kept in lower case, so the capitalized word is right too.
let r = run(&mut s, "text.spelling.addWord", json!({"word": " Zqvwkx "}));
assert_eq!((r["word"].as_str(), r["added"].as_bool()), (Some("zqvwkx"), Some(true)));
assert_eq!(marked(&mut s), Vec::<String>::new());
assert_eq!(run(&mut s, "text.spelling.addWord", json!({"word": "zqvwkx"}))["added"], false, "already there");
let words = run(&mut s, "text.spelling.dictionary", json!({}))["words"].clone();
assert!(words.as_array().unwrap().contains(&json!("zqvwkx")), "{words}");
// Another document's check takes it as right too.
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
text(&mut s, 50.0, "zqvwkx");
assert_eq!(marked(&mut s), Vec::<String>::new());
run(&mut s, "text.spelling.removeWord", json!({"word": "ZQVWKX"}));
assert_eq!(marked(&mut s), ["zqvwkx"]);
let e = s.execute("text.spelling.removeWord", &json!({"word": "zqvwkx"})).unwrap_err().to_string();
assert!(e.contains("isn't in the custom dictionary"), "{e}");
}
#[test]
fn a_changed_word_replaces_the_old_one() {
let mut s = session();
text(&mut s, 50.0, "xkvzqa xkvzqb");
run(&mut s, "text.spelling.addWord", json!({"word": "xkvzqa"}));
assert_eq!(marked(&mut s), ["xkvzqb"]);
assert_eq!(run(&mut s, "text.spelling.changeWord", json!({"word": "xkvzqa", "to": "Xkvzqb"}))["word"], "xkvzqb");
assert_eq!(marked(&mut s), ["xkvzqa"]);
let e = s.execute("text.spelling.changeWord", &json!({"word": "xkvzqa", "to": "x"})).unwrap_err().to_string();
assert!(e.contains("isn't in the custom dictionary"), "{e}");
run(&mut s, "text.spelling.removeWord", json!({"word": "xkvzqb"}));
}
#[test]
fn the_custom_dictionary_file_is_read_and_kept_up_to_date() {
let dir = vectorcraft_testkit::temp_dir("spelling-dictionary");
let file = dir.join("Custom Dictionary.txt");
std::fs::write(&file, "kqzvwx\nnot one word\n\n").unwrap();
let mut s = session();
s.attach_custom_dictionary(file.clone());
text(&mut s, 50.0, "kqzvwx vkqzwx");
assert_eq!(marked(&mut s), ["vkqzwx"], "the file's word is right; its line of three words is skipped");
run(&mut s, "text.spelling.addWord", json!({"word": "vkqzwx"}));
let saved = std::fs::read_to_string(&file).unwrap();
assert!(saved.lines().any(|l| l == "kqzvwx") && saved.lines().any(|l| l == "vkqzwx"), "{saved}");
run(&mut s, "text.spelling.changeWord", json!({"word": "kqzvwx", "to": "kqzvwy"}));
run(&mut s, "text.spelling.removeWord", json!({"word": "vkqzwx"}));
let saved = std::fs::read_to_string(&file).unwrap();
assert!(saved.lines().any(|l| l == "kqzvwy"), "{saved}");
assert!(!saved.lines().any(|l| l == "kqzvwx" || l == "vkqzwx"), "changed and deleted words are gone from the file: {saved}");
run(&mut s, "text.spelling.removeWord", json!({"word": "kqzvwy"}));
}
#[test]
fn ignore_all_skips_a_word_in_the_active_document_only() {
let mut s = session();
text(&mut s, 50.0, "Wqzxkv drawing wqzxkv vqzxkw");
run(&mut s, "text.spelling.ignoreAll", json!({"word": "WQZXKV"}));
assert_eq!(marked(&mut s), ["vqzxkw"], "every occurrence, whatever its case");
let words = run(&mut s, "text.spelling.dictionary", json!({}))["words"].clone();
assert!(!words.as_array().unwrap().contains(&json!("wqzxkv")), "not added to the custom dictionary");
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
text(&mut s, 50.0, "wqzxkv");
assert_eq!(marked(&mut s), ["wqzxkv"], "another document still marks it");
}
#[test]
fn custom_dictionary_commands_refuse_what_isnt_one_word() {
let mut s = session();
let long = "q".repeat(65);
for (id, p) in [
("text.spelling.addWord", json!({})),
("text.spelling.addWord", json!({"word": " "})),
("text.spelling.addWord", json!({"word": "two words"})),
("text.spelling.addWord", json!({"word": long})),
("text.spelling.addWord", json!({"word": 5})),
("text.spelling.removeWord", json!({})),
("text.spelling.changeWord", json!({"word": "qzx"})),
("text.spelling.changeWord", json!({"word": "qzx", "to": "a b"})),
("text.spelling.ignoreAll", json!({"word": "a\tb"})),
] {
assert!(s.execute(id, &p).is_err(), "{id} {p} is refused");
}
let mut empty = Session::new();
assert!(empty.execute("text.spelling.ignoreAll", &json!({"word": "qzx"})).is_err(), "Ignore All needs a document");
assert!(empty.execute("text.spelling.dictionary", &json!({})).is_ok(), "the dictionary is the app's");
}