Files
2026-08-28 21:47:48 +05:30

127 lines
5.3 KiB
Rust

// What the grammar checker promises the frontend, asserted against the real engine.
//
// Three things, and the first is the one that would be invisible everywhere else. Offsets are in
// characters, and src/editor/proofing.ts adds a ProseMirror position to them without converting
// anything, so an engine that counted bytes would draw its underlines further and further to the
// left of the words they are about for every non-ASCII character earlier in the paragraph. That is
// a bug nobody writing in English would ever see and everybody writing in French or Polish would
// see immediately, and no other test in this repository is in a position to notice it: the Rust
// suite otherwise asserts about files, and the browser suite talks to the fixture in
// src/dev/mockIpc.ts rather than to Harper.
//
// The second is that Harper's own spell rule is off. It is turned off in src-tauri/src/grammar.rs because
// NSSpellChecker already does the spelling and knows the user's own names and languages, and a
// second dictionary underlining "Yoshinari" would be exactly the noise that gets a checker switched
// off for good. Left on, everything would still be green: there would just be spelling issues
// arriving through the grammar command, drawn in the grammar colour, with no Learn item on them.
//
// The third is that the engine survives being asked twice. It is a static built on first use and a
// `LintGroup` that lints through `&mut self`, so a second call is the one that would find a lock
// held or a state left dirty by the first.
use margin_docs_lib::dto::GrammarIssue;
use margin_docs_lib::grammar::{grammar_available, grammar_check};
/// The text a lint is about, sliced the way the frontend slices it: by character.
fn flagged(text: &str, issue: &GrammarIssue) -> String {
text.chars()
.skip(issue.start)
.take(issue.end - issue.start)
.collect()
}
fn check(text: &str) -> Vec<GrammarIssue> {
grammar_check(text.to_string()).expect("the checker answered")
}
#[test]
fn a_build_with_harper_in_it_says_so() {
assert!(grammar_available().expect("availability answered"));
}
#[test]
fn a_repeated_word_is_found_and_can_be_corrected() {
let issues = check("I put the the book down.");
let repeat = issues
.iter()
.find(|issue| flagged("I put the the book down.", issue).contains("the the"))
.expect("the repeated word was found");
assert!(!repeat.kind.is_empty(), "a lint carries the rule's category");
assert!(!repeat.message.is_empty(), "and something to show the reader");
assert!(
repeat.suggestions.iter().any(|s| s.trim() == "the"),
"with the correction on it: {:?}",
repeat.suggestions
);
}
#[test]
fn offsets_are_characters_and_not_bytes() {
// Every character before the mistake is three bytes in UTF-8, so a checker counting bytes would
// report a span three times too far along and the assertion below would slice the wrong words.
let text = "Zażółć gęślą jaźń, and then I put the the book down.";
let issues = check(text);
let repeat = issues
.iter()
.find(|issue| flagged(text, issue).contains("the the"))
.expect("the repeated word was found after a run of non-ASCII text");
assert_eq!(flagged(text, repeat), "the the");
assert!(
repeat.end <= text.chars().count(),
"and it ends inside the run it was told about"
);
}
#[test]
fn spelling_is_left_to_the_system_checker() {
// Not a word in any dictionary, and Harper's curated one included. Nothing may come back about
// it, because the only rule that would have is the one src-tauri/src/grammar.rs turns off.
let text = "The flurbulent maglifter needs oiling.";
for issue in check(text) {
assert_ne!(
issue.kind, "Spelling",
"the grammar checker reported a spelling issue: {issue:?}"
);
}
}
#[test]
fn nothing_to_say_about_nothing() {
assert!(check("").is_empty());
}
/// Harper flags two spaces between sentences as "French spaces", and src-tauri/src/grammar.rs drops every
/// lint whose whole span is whitespace for the reasons written there: an underline nobody can see
/// or click on, and, when the spaces are the ones src/editor/proofing.ts writes in place of an
/// inline code span, an underline over the one thing that file went out of its way not to send.
///
/// These two inputs are the ones that reach the filter, so taking the filter out fails this test
/// rather than leaving it green. What it cannot see is a future Harper that stops raising the lint
/// at all, which would leave the guard unreached and this file none the wiser.
#[test]
fn a_lint_about_nothing_but_spaces_is_not_reported() {
assert!(
check("This is fine. Two spaces there.").is_empty(),
"the two spaces between the sentences were reported"
);
// The shape src/editor/proofing.ts produces from a blanked inline code span.
assert!(
check("The value is nine.").is_empty(),
"a blanked code span was reported as a run of spaces"
);
}
#[test]
fn the_engine_is_kept_and_answers_the_same_way_twice() {
let text = "I put the the book down.";
let first = check(text);
let second = check(text);
assert_eq!(first.len(), second.len());
assert_eq!(
first.first().map(|i| (i.start, i.end)),
second.first().map(|i| (i.start, i.end))
);
}