commit
This commit is contained in:
+646
@@ -0,0 +1,646 @@
|
||||
//! Проверки UI-логики на «сырых» событиях egui (без реального окна).
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use eframe::egui;
|
||||
|
||||
use crate::api::AppApi;
|
||||
use crate::app::MyApp;
|
||||
use crate::editor::EditorState;
|
||||
use crate::keymap::KeyMap;
|
||||
use crate::script::ScriptEngine;
|
||||
use crate::syntax::{HighlightStyle, Language, SyntaxState};
|
||||
use crate::ui::title_bar::TitleBarResponses;
|
||||
|
||||
const SCREEN: egui::Vec2 = egui::vec2(600.0, 400.0);
|
||||
|
||||
/// Один проход кадра для данного экземпляра приложения:
|
||||
/// возвращает ответы заголовка и команды окна.
|
||||
fn step_app(
|
||||
ctx: &egui::Context,
|
||||
app: &mut MyApp,
|
||||
events: Vec<egui::Event>,
|
||||
) -> (TitleBarResponses, Vec<egui::ViewportCommand>) {
|
||||
let raw = egui::RawInput {
|
||||
screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, SCREEN)),
|
||||
events,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut bar = None;
|
||||
let mut out = ctx.run_ui(raw, |ui| {
|
||||
bar = Some(app.show(ui));
|
||||
});
|
||||
|
||||
let commands = out
|
||||
.viewport_output
|
||||
.get(&egui::ViewportId::ROOT)
|
||||
.map(|output| output.commands.clone())
|
||||
.unwrap_or_default();
|
||||
out.textures_delta.clear();
|
||||
|
||||
(bar.expect("заголовок отрисован"), commands)
|
||||
}
|
||||
|
||||
/// Один проход кадра на свежем приложении.
|
||||
fn step(
|
||||
ctx: &egui::Context,
|
||||
events: Vec<egui::Event>,
|
||||
) -> (TitleBarResponses, Vec<egui::ViewportCommand>) {
|
||||
let mut app = MyApp::default();
|
||||
step_app(ctx, &mut app, events)
|
||||
}
|
||||
|
||||
fn button_event(pos: egui::Pos2, pressed: bool) -> egui::Event {
|
||||
egui::Event::PointerButton {
|
||||
pos,
|
||||
button: egui::PointerButton::Primary,
|
||||
pressed,
|
||||
modifiers: egui::Modifiers::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn click_at(ctx: &egui::Context, pos: egui::Pos2) -> Vec<egui::ViewportCommand> {
|
||||
let mut commands = Vec::new();
|
||||
commands.extend(step(ctx, vec![egui::Event::PointerMoved(pos)]).1);
|
||||
commands.extend(step(ctx, vec![button_event(pos, true)]).1);
|
||||
commands.extend(step(ctx, vec![]).1);
|
||||
commands.extend(step(ctx, vec![button_event(pos, false)]).1);
|
||||
commands.extend(step(ctx, vec![]).1);
|
||||
commands
|
||||
}
|
||||
|
||||
fn press_at(ctx: &egui::Context, pos: egui::Pos2) -> Vec<egui::ViewportCommand> {
|
||||
let mut commands = step(ctx, vec![egui::Event::PointerMoved(pos)]).1;
|
||||
commands.extend(step(ctx, vec![button_event(pos, true)]).1);
|
||||
commands
|
||||
}
|
||||
|
||||
/// Событие клавиатуры, у которого логическая клавиша может отличаться от
|
||||
/// физической (как при смене раскладки).
|
||||
fn key_event(
|
||||
physical: egui::Key,
|
||||
logical: egui::Key,
|
||||
pressed: bool,
|
||||
modifiers: egui::Modifiers,
|
||||
) -> egui::Event {
|
||||
egui::Event::Key {
|
||||
key: logical,
|
||||
physical_key: Some(physical),
|
||||
pressed,
|
||||
repeat: false,
|
||||
modifiers,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_bar_buttons_are_hoverable_and_clickable() {
|
||||
let ctx = egui::Context::default();
|
||||
// прогреваем: egui считает раскладку по предыдущему кадру
|
||||
step(&ctx, vec![]);
|
||||
let (bar, _) = step(&ctx, vec![]);
|
||||
|
||||
let close_pos = bar.close.rect.center();
|
||||
let maximize_pos = bar.maximize.rect.center();
|
||||
let minimize_pos = bar.minimize.rect.center();
|
||||
|
||||
// Кнопки должны находиться в заголовке, а не где-то ещё
|
||||
assert!(bar.drag.rect.contains(close_pos));
|
||||
|
||||
// Наведение даёт подсветку
|
||||
step(&ctx, vec![egui::Event::PointerMoved(close_pos)]);
|
||||
let (hovered, _) = step(&ctx, vec![]);
|
||||
assert!(
|
||||
hovered.close.hovered(),
|
||||
"кнопка «закрыть» не подсвечивается"
|
||||
);
|
||||
|
||||
let close_cmds = click_at(&ctx, close_pos);
|
||||
assert!(
|
||||
close_cmds.contains(&egui::ViewportCommand::Close),
|
||||
"клик по «закрыть» не отправил Close: {close_cmds:?}"
|
||||
);
|
||||
|
||||
let max_cmds = click_at(&ctx, maximize_pos);
|
||||
assert!(
|
||||
max_cmds.contains(&egui::ViewportCommand::Maximized(true)),
|
||||
"клик по «развернуть» не отправил Maximized: {max_cmds:?}"
|
||||
);
|
||||
|
||||
let min_cmds = click_at(&ctx, minimize_pos);
|
||||
assert!(
|
||||
min_cmds.contains(&egui::ViewportCommand::Minimized(true)),
|
||||
"клик по «свернуть» не отправил Minimized: {min_cmds:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_title_bar_starts_window_drag_even_over_the_text() {
|
||||
for (name, x_offset) in [("по тексту", 40.0), ("по пустому месту", 300.0)]
|
||||
{
|
||||
let ctx = egui::Context::default();
|
||||
step(&ctx, vec![]);
|
||||
let (bar, _) = step(&ctx, vec![]);
|
||||
|
||||
let start = egui::pos2(bar.drag.rect.left() + x_offset, bar.drag.rect.center().y);
|
||||
let moved = start + egui::vec2(24.0, 12.0);
|
||||
|
||||
step(&ctx, vec![egui::Event::PointerMoved(start)]);
|
||||
let mut commands = step(&ctx, vec![button_event(start, true)]).1;
|
||||
commands.extend(step(&ctx, vec![egui::Event::PointerMoved(moved)]).1);
|
||||
|
||||
assert!(
|
||||
commands.contains(&egui::ViewportCommand::StartDrag),
|
||||
"драг заголовка {name} не инициировал перемещение окна: {commands:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edges_and_corners_start_resize() {
|
||||
use egui::viewport::ResizeDirection as Dir;
|
||||
|
||||
let cases = [
|
||||
(egui::pos2(300.0, 2.0), Dir::North),
|
||||
(egui::pos2(300.0, SCREEN.y - 2.0), Dir::South),
|
||||
(egui::pos2(2.0, 200.0), Dir::West),
|
||||
(egui::pos2(SCREEN.x - 2.0, 200.0), Dir::East),
|
||||
(egui::pos2(2.0, 2.0), Dir::NorthWest),
|
||||
(egui::pos2(SCREEN.x - 2.0, 2.0), Dir::NorthEast),
|
||||
(egui::pos2(SCREEN.x - 2.0, SCREEN.y - 2.0), Dir::SouthEast),
|
||||
(egui::pos2(2.0, SCREEN.y - 2.0), Dir::SouthWest),
|
||||
];
|
||||
|
||||
for (pos, expected) in cases {
|
||||
let ctx = egui::Context::default();
|
||||
step(&ctx, vec![]);
|
||||
step(&ctx, vec![]);
|
||||
let commands = press_at(&ctx, pos);
|
||||
assert!(
|
||||
commands.contains(&egui::ViewportCommand::BeginResize(expected)),
|
||||
"в точке {pos:?} ожидался BeginResize({expected:?}), получено: {commands:?}"
|
||||
);
|
||||
assert!(
|
||||
!commands.contains(&egui::ViewportCommand::StartDrag),
|
||||
"в точке {pos:?} вместо ресайза началось перемещение окна"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_state_keeps_at_least_one_tab() {
|
||||
let mut state = EditorState::default();
|
||||
assert_eq!(state.documents().len(), 2);
|
||||
assert_eq!(state.active().name, "main.rs");
|
||||
|
||||
// Открытие и закрытие новых вкладок.
|
||||
state.open_new();
|
||||
assert_eq!(state.documents().len(), 3);
|
||||
assert_eq!(state.active_index(), 2);
|
||||
|
||||
state.close(2);
|
||||
assert_eq!(state.active_index(), 1);
|
||||
|
||||
// Редактор не должен оставаться пустым.
|
||||
while state.documents().len() > 1 {
|
||||
state.close(0);
|
||||
}
|
||||
state.close(0);
|
||||
assert_eq!(state.documents().len(), 1);
|
||||
assert_eq!(state.active_index(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_tab_before_active_keeps_the_same_document_active() {
|
||||
let mut state = EditorState::default();
|
||||
state.open_new();
|
||||
let name = state.active().name.clone();
|
||||
|
||||
state.close(0);
|
||||
assert_eq!(state.active().name, name);
|
||||
assert_eq!(state.active_index(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editing_marks_document_dirty_and_saving_clears_it() {
|
||||
let mut state = EditorState::default();
|
||||
assert!(!state.active().is_dirty());
|
||||
|
||||
state.active_mut().content.push_str("\n// правка\n");
|
||||
assert!(state.active().is_dirty());
|
||||
|
||||
state.active_mut().mark_saved();
|
||||
assert!(!state.active().is_dirty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_into_editor_updates_active_document() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = MyApp::default();
|
||||
|
||||
// Прогреваем раскладку и кликаем в область редактора ниже панели вкладок.
|
||||
step_app(&ctx, &mut app, vec![]);
|
||||
let editor_pos = egui::pos2(SCREEN.x / 2.0, SCREEN.y - 80.0);
|
||||
step_app(&ctx, &mut app, vec![egui::Event::PointerMoved(editor_pos)]);
|
||||
step_app(&ctx, &mut app, vec![button_event(editor_pos, true)]);
|
||||
step_app(&ctx, &mut app, vec![button_event(editor_pos, false)]);
|
||||
|
||||
step_app(&ctx, &mut app, vec![egui::Event::Text("ZZZ".to_owned())]);
|
||||
|
||||
let editor = app.editor();
|
||||
let doc = editor.active();
|
||||
assert!(
|
||||
doc.content.contains("ZZZ"),
|
||||
"введённый текст не попал в документ: {:?}",
|
||||
doc.content
|
||||
);
|
||||
assert!(doc.is_dirty(), "правка должна помечать документ изменённым");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_lists_tabs_and_closes_them_by_number() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = MyApp::default();
|
||||
step_app(&ctx, &mut app, vec![]);
|
||||
|
||||
let api = AppApi::global();
|
||||
let tabs = api.tabs();
|
||||
assert_eq!(
|
||||
tabs.iter()
|
||||
.map(|tab| (tab.index, tab.title.as_str()))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![(0, "main.rs"), (1, "notes.md")]
|
||||
);
|
||||
|
||||
// Несуществующий номер ничего не закрывает.
|
||||
assert!(!api.close_tab(99));
|
||||
assert_eq!(api.tabs().len(), 2);
|
||||
|
||||
assert!(api.close_tab(1));
|
||||
assert_eq!(api.tabs().len(), 1);
|
||||
assert_eq!(app.editor().documents().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_save_writes_active_tab_to_file_named_after_it() {
|
||||
// Тест пишет во временный файл, а не в каталог проекта.
|
||||
let mut state = EditorState::default();
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"egui-rust-test-save-{}-{}.rs",
|
||||
std::process::id(),
|
||||
state.active().id
|
||||
));
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
{
|
||||
let doc = state.active_mut();
|
||||
doc.name = path.to_string_lossy().into_owned();
|
||||
doc.content = "fn main() {}\n".to_owned();
|
||||
}
|
||||
assert!(state.active().is_dirty());
|
||||
|
||||
let editor = Rc::new(RefCell::new(state));
|
||||
let api = AppApi::install(
|
||||
egui::Context::default(),
|
||||
editor.clone(),
|
||||
Rc::new(RefCell::new(KeyMap::default())),
|
||||
);
|
||||
api.save().expect("сохранение должно удаться");
|
||||
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&path).expect("файл должен существовать"),
|
||||
"fn main() {}\n"
|
||||
);
|
||||
assert!(!editor.borrow().active().is_dirty());
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_switches_tabs_forward_and_back_in_a_cycle() {
|
||||
let editor = Rc::new(RefCell::new(EditorState::default()));
|
||||
let api = AppApi::install(
|
||||
egui::Context::default(),
|
||||
editor.clone(),
|
||||
Rc::new(RefCell::new(KeyMap::default())),
|
||||
);
|
||||
|
||||
assert_eq!(api.active_tab_index(), 0);
|
||||
|
||||
assert_eq!(api.next_tab(), 1);
|
||||
assert_eq!(api.active_tab_index(), 1);
|
||||
|
||||
// С последней вкладки следующая — первая.
|
||||
assert_eq!(api.next_tab(), 0);
|
||||
|
||||
// С первой вкладки предыдущая — последняя.
|
||||
assert_eq!(api.previous_tab(), 1);
|
||||
assert_eq!(api.previous_tab(), 0);
|
||||
assert_eq!(editor.borrow().active_index(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lua_script_can_include_files_and_control_the_app() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = MyApp::default();
|
||||
// Первый кадр публикует AppApi, который использует сценарий.
|
||||
step_app(&ctx, &mut app, vec![]);
|
||||
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"egui-rust-test-lua-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("системные часы")
|
||||
.as_nanos()
|
||||
));
|
||||
std::fs::create_dir_all(&dir).expect("временный каталог сценариев");
|
||||
|
||||
let helper = dir.join("helper.lua");
|
||||
let main = dir.join("main.lua");
|
||||
std::fs::write(
|
||||
&helper,
|
||||
"function lua_config() app:set_active_text(\"from lua\") end\n",
|
||||
)
|
||||
.expect("helper.lua");
|
||||
std::fs::write(
|
||||
&main,
|
||||
format!(
|
||||
"include({:?})\nlua_config()\napp:next_tab()\napp:set_language(1, \"python\")\n",
|
||||
helper.to_string_lossy()
|
||||
),
|
||||
)
|
||||
.expect("main.lua");
|
||||
|
||||
let engine = ScriptEngine::new(AppApi::global()).expect("движок Lua");
|
||||
engine.run_file(&main).expect("сценарий должен выполниться");
|
||||
|
||||
let editor = app.editor();
|
||||
assert_eq!(
|
||||
editor.documents()[0].content,
|
||||
"from lua",
|
||||
"include должен выполнить подключённый файл в общем окружении"
|
||||
);
|
||||
assert_eq!(
|
||||
editor.active_index(),
|
||||
1,
|
||||
"app:next_tab() не переключил вкладку"
|
||||
);
|
||||
assert_eq!(
|
||||
editor.documents()[1].language,
|
||||
Some(Language::Python),
|
||||
"app:set_language() не включил подсветку из Lua"
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lua_key_bindings_use_physical_keys_and_modifiers() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = MyApp::default();
|
||||
// Первый кадр публикует AppApi с картой клавиш приложения.
|
||||
step_app(&ctx, &mut app, vec![]);
|
||||
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"egui-rust-test-keys-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("системные часы")
|
||||
.as_nanos()
|
||||
));
|
||||
std::fs::create_dir_all(&dir).expect("временный каталог сценариев");
|
||||
|
||||
let script = dir.join("keys.lua");
|
||||
std::fs::write(
|
||||
&script,
|
||||
r#"
|
||||
app:map_key("a", function() app:set_active_text("a pressed") end)
|
||||
app:map_key("!1", function() app:set_active_text("1 released") end)
|
||||
app:map_key("<S-c>", function() app:set_active_text("shift c") end)
|
||||
app:map_key("<C-z>", function() app:set_active_text("ctrl z") end)
|
||||
app:map_key("<A-!r>", function() app:set_active_text("alt r released") end)
|
||||
"#,
|
||||
)
|
||||
.expect("keys.lua");
|
||||
|
||||
let engine = ScriptEngine::new(AppApi::global()).expect("движок Lua");
|
||||
engine
|
||||
.run_file(&script)
|
||||
.expect("привязки должны назначиться");
|
||||
|
||||
let text = |app: &MyApp| app.editor().active().content.clone();
|
||||
|
||||
// Логическая клавиша Z, физическая A: срабатывает именно физическая.
|
||||
step_app(
|
||||
&ctx,
|
||||
&mut app,
|
||||
vec![key_event(
|
||||
egui::Key::A,
|
||||
egui::Key::Z,
|
||||
true,
|
||||
egui::Modifiers::default(),
|
||||
)],
|
||||
);
|
||||
assert_eq!(text(&app), "a pressed");
|
||||
|
||||
// Shift+A не должна срабатывать как обычная A.
|
||||
step_app(
|
||||
&ctx,
|
||||
&mut app,
|
||||
vec![key_event(
|
||||
egui::Key::A,
|
||||
egui::Key::A,
|
||||
true,
|
||||
egui::Modifiers {
|
||||
shift: true,
|
||||
..Default::default()
|
||||
},
|
||||
)],
|
||||
);
|
||||
assert_eq!(text(&app), "a pressed");
|
||||
|
||||
// Отпускание цифры 1, логическая клавиша при этом другая.
|
||||
step_app(
|
||||
&ctx,
|
||||
&mut app,
|
||||
vec![key_event(
|
||||
egui::Key::Num1,
|
||||
egui::Key::Num2,
|
||||
false,
|
||||
egui::Modifiers::default(),
|
||||
)],
|
||||
);
|
||||
assert_eq!(text(&app), "1 released");
|
||||
|
||||
step_app(
|
||||
&ctx,
|
||||
&mut app,
|
||||
vec![key_event(
|
||||
egui::Key::C,
|
||||
egui::Key::C,
|
||||
true,
|
||||
egui::Modifiers {
|
||||
shift: true,
|
||||
..Default::default()
|
||||
},
|
||||
)],
|
||||
);
|
||||
assert_eq!(text(&app), "shift c");
|
||||
|
||||
step_app(
|
||||
&ctx,
|
||||
&mut app,
|
||||
vec![key_event(
|
||||
egui::Key::Z,
|
||||
egui::Key::Z,
|
||||
true,
|
||||
egui::Modifiers {
|
||||
ctrl: true,
|
||||
..Default::default()
|
||||
},
|
||||
)],
|
||||
);
|
||||
assert_eq!(text(&app), "ctrl z");
|
||||
|
||||
// Отпускание R при зажатом Alt.
|
||||
step_app(
|
||||
&ctx,
|
||||
&mut app,
|
||||
vec![key_event(
|
||||
egui::Key::R,
|
||||
egui::Key::R,
|
||||
false,
|
||||
egui::Modifiers {
|
||||
alt: true,
|
||||
..Default::default()
|
||||
},
|
||||
)],
|
||||
);
|
||||
assert_eq!(text(&app), "alt r released");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_names_cover_all_supported_grammars() {
|
||||
for (name, expected) in [
|
||||
("rust", Language::Rust),
|
||||
("go", Language::Go),
|
||||
("js", Language::JavaScript),
|
||||
("json", Language::Json),
|
||||
("python", Language::Python),
|
||||
] {
|
||||
assert_eq!(Language::from_name(name), Some(expected), "язык {name}");
|
||||
assert_eq!(Language::from_name(&name.to_uppercase()), Some(expected));
|
||||
}
|
||||
|
||||
// Распространённые короткие имена.
|
||||
assert_eq!(Language::from_name("rs"), Some(Language::Rust));
|
||||
assert_eq!(Language::from_name("golang"), Some(Language::Go));
|
||||
assert_eq!(
|
||||
Language::from_name("javascript"),
|
||||
Some(Language::JavaScript)
|
||||
);
|
||||
assert_eq!(Language::from_name("py"), Some(Language::Python));
|
||||
|
||||
assert_eq!(Language::from_name("cobol"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_sitter_highlights_all_supported_languages() {
|
||||
let samples = [
|
||||
(
|
||||
Language::Rust,
|
||||
"fn main() {\n let x = 42; // комментарий\n println!(\"hi\");\n}\n",
|
||||
HighlightStyle::Keyword,
|
||||
),
|
||||
(
|
||||
Language::Go,
|
||||
"package main\n\nfunc main() {\n\tx := 42 // комментарий\n\t_ = x\n}\n",
|
||||
HighlightStyle::Keyword,
|
||||
),
|
||||
(
|
||||
Language::JavaScript,
|
||||
"function main() {\n const x = 42; // comment\n console.log(\"hi\");\n}\n",
|
||||
HighlightStyle::Keyword,
|
||||
),
|
||||
(
|
||||
Language::Json,
|
||||
"{\"name\": \"value\", \"count\": 42, \"ok\": true}\n",
|
||||
HighlightStyle::Property,
|
||||
),
|
||||
(
|
||||
Language::Python,
|
||||
"def main():\n x = 42 # comment\n print(\"hi\")\n",
|
||||
HighlightStyle::Keyword,
|
||||
),
|
||||
];
|
||||
|
||||
for (language, source, expected) in samples {
|
||||
let mut syntax = SyntaxState::default();
|
||||
let styles: Vec<_> = syntax
|
||||
.spans(language, source)
|
||||
.iter()
|
||||
.map(|span| span.style)
|
||||
.collect();
|
||||
|
||||
assert!(!styles.is_empty(), "{language:?}: подсветка пуста");
|
||||
assert!(
|
||||
styles.contains(&expected),
|
||||
"{language:?}: нет стиля {expected:?} в {styles:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_tree_is_available_for_tasks_beyond_highlighting() {
|
||||
let mut syntax = SyntaxState::default();
|
||||
let tree = syntax
|
||||
.parse(Language::Rust, "fn main() {}\n")
|
||||
.expect("дерево должно разобраться");
|
||||
|
||||
let root = tree.root_node();
|
||||
assert_eq!(root.kind(), "source_file");
|
||||
|
||||
let mut cursor = root.walk();
|
||||
assert!(
|
||||
root.children(&mut cursor)
|
||||
.any(|node| node.kind() == "function_item"),
|
||||
"в дереве должна находиться функция — его можно обходить, а не только красить"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn highlighting_is_off_by_default_and_enabled_through_api() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = MyApp::default();
|
||||
step_app(&ctx, &mut app, vec![]);
|
||||
|
||||
assert!(
|
||||
app.editor()
|
||||
.documents()
|
||||
.iter()
|
||||
.all(|doc| doc.language.is_none()),
|
||||
"подсветка должна быть выключена по умолчанию"
|
||||
);
|
||||
|
||||
let api = AppApi::global();
|
||||
assert!(api.set_language(0, "rust"));
|
||||
assert_eq!(api.language(0).as_deref(), Some("rust"));
|
||||
assert_eq!(app.editor().documents()[0].language, Some(Language::Rust));
|
||||
|
||||
// Неизвестный язык и несуществующая вкладка не меняют состояние.
|
||||
assert!(!api.set_language(0, "cobol"));
|
||||
assert!(!api.set_language(99, "rust"));
|
||||
assert_eq!(api.language(0).as_deref(), Some("rust"));
|
||||
|
||||
// Кадр с включённой подсветкой должен отрисовываться без паники.
|
||||
step_app(&ctx, &mut app, vec![]);
|
||||
|
||||
assert!(api.clear_language(0));
|
||||
assert_eq!(api.language(0), None);
|
||||
assert!(!api.clear_language(99));
|
||||
}
|
||||
Reference in New Issue
Block a user