ElyShow: a refused show start always says why (S-10)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 21:47:31 -04:00
1 parent f01150be05
commit 4cec71703b
7 files changed
+113 -5

No files matched your search

+49 -2
View File
@@ -141,7 +141,9 @@ impl Deck {
/// Plays a copy from the first or shown slide.
pub fn start_show(&mut self, here: bool, cx: &mut Context<Self>) {
if self.show.is_some() || self.show_starting {
return log::info!("show: already playing or starting");
log::info!("show: already playing or starting");
let text = Deck::text(cx, "show.play.busy", &[]);
return self.toast(text, Severity::Warning, cx);
}
let Some(id) = self.core.active else {
return;
@@ -180,7 +182,9 @@ impl Deck {
let Some(first) = first.filter(|_| deck.core.active == Some(id)) else {
discard(worker, doc, path, cx);
if deck.core.active != Some(id) {
return log::info!("show: tab {id} is no longer shown");
log::info!("show: tab {id} is no longer shown");
let text = Deck::text(cx, "show.play.gone", &[]);
return deck.toast(text, Severity::Warning, cx);
}
let text = Deck::text(cx, "show.play.none", &[]);
return deck.toast(text, Severity::Warning, cx);
@@ -214,6 +218,8 @@ impl Deck {
let (Some(tab), Some(audience)) = (self.tab(starting.tab), displays.get(starting.audience))
else {
log::warn!("show: the tab or screen {} is gone", starting.audience);
let text = Deck::text(cx, "show.play.gone", &[]);
self.toast(text, Severity::Warning, cx);
return discard(worker, starting.doc, starting.copy, cx);
};
let presenter = (displays.iter())
@@ -347,4 +353,45 @@ mod tests {
};
assert_eq!(super::bounds(area), want);
}
#[test]
fn a_start_whose_screen_is_gone_says_so_and_drops_its_copy() {
use ely_office::prelude::*;
use gpui::AppContext as _;
use crate::tests::{settle, update};
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = gpui::TestAppContext::single();
let Some(deck) = crate::tests::deck(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
update(&mut cx, &deck, |deck, cx| deck.new_blank(cx));
settle(&cx, |app| deck.read(app).active_tab().is_some_and(|tab| !tab.extra.slides.list.is_empty()));
// The window's toasts live on its root.
let handle = cx.update(|app| deck.read(app).core.window).expect("a window");
let root = cx
.update_window(handle, |_, window, app| {
let root = ely_ui::document_root(ely_platform::AppId::Show, deck.clone().into(), &ely_ui::commands::FILE, Vec::new(), window, app);
deck.update(app, |deck, _| deck.core.root = root.downgrade());
root
})
.expect("the window lives");
let path = dir.path().join("copy.odp");
let starting = cx.update(|app| {
let deck = deck.read(app);
let tab = deck.active_tab().expect("a tab");
let mut worker = deck.core.worker.lock().expect("the worker lock");
let (_, doc, slides) = super::copy(&mut worker, &tab.doc, (&path, None)).expect("a copy");
super::Starting { tab: tab.id, slides, first: 0, doc, copy: path.clone(), audience: 99, presenter: false }
});
update(&mut cx, &deck, |deck, cx| {
deck.show_starting = true;
deck.open_show(starting, cx);
});
settle(&cx, |_| !path.exists());
let (toasts, gone) = cx.update(|app| (root.read(app).toasts(app), crate::deck::Deck::text(app, "show.play.gone", &[])));
assert!(toasts.iter().any(|(text, _)| *text == gone), "{toasts:?}");
assert!(cx.update(|app| !deck.read(app).show_starting && deck.read(app).show.is_none()));
}
}
+2 -2
View File
@@ -45,7 +45,7 @@ pub(super) fn deck(cx: &mut TestAppContext, dir: &Path) -> Option<Entity<Deck>>
/// Steps the scheduler until `done` or ten seconds pass.
#[track_caller]
fn settle(cx: &TestAppContext, done: impl Fn(&gpui::App) -> bool) {
pub(super) fn settle(cx: &TestAppContext, done: impl Fn(&gpui::App) -> bool) {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
while std::time::Instant::now() < deadline {
if !cx.executor().tick() {
@@ -59,7 +59,7 @@ fn settle(cx: &TestAppContext, done: impl Fn(&gpui::App) -> bool) {
panic!("the state never settled");
}
fn update(
pub(super) fn update(
cx: &mut TestAppContext,
deck: &Entity<Deck>,
f: impl FnOnce(&mut Deck, &mut gpui::Context<Deck>),
+53
View File
@@ -145,3 +145,56 @@ fn a_click_on_media_finds_it_and_elsewhere_does_not() {
assert_eq!(cx.update(|app| show.read(app).media_at(centre)), Some(0));
assert_eq!(cx.update(|app| show.read(app).media_at((0.01, 0.01))), None);
}
#[test]
fn a_second_start_says_the_show_is_busy() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some(deck) = blank(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
// The window's toasts live on its root.
let root = super::slides::in_window(&mut cx, &deck, |deck, window, cx| {
let this = cx.entity();
let root = ely_ui::document_root(ely_platform::AppId::Show, this.into(), &ely_ui::commands::FILE, Vec::new(), window, cx);
deck.core.root = root.downgrade();
root
});
update(&mut cx, &deck, |deck, cx| {
deck.show_starting = true;
deck.start_show(false, cx);
});
cx.run_until_parked();
let (toasts, busy) = cx.update(|app| (root.read(app).toasts(app), Deck::text(app, "show.play.busy", &[])));
assert!(toasts.iter().any(|(text, _)| *text == busy), "{toasts:?}");
}
#[test]
fn a_start_left_for_another_tab_says_so() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some(deck) = blank(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
let first = cx.update(|app| deck.read(app).core.active.expect("a tab"));
update(&mut cx, &deck, |deck, cx| deck.new_blank(cx));
settle(&cx, |app| deck.read(app).core.tabs.len() == 2 && deck.read(app).core.active != Some(first));
let second = cx.update(|app| deck.read(app).core.active.expect("a tab"));
let root = super::slides::in_window(&mut cx, &deck, |deck, window, cx| {
let this = cx.entity();
let root = ely_ui::document_root(ely_platform::AppId::Show, this.into(), &ely_ui::commands::FILE, Vec::new(), window, cx);
deck.core.root = root.downgrade();
root
});
update(&mut cx, &deck, |deck, cx| {
deck.core.active = Some(first);
deck.start_show(false, cx);
// Another tab comes forward while the copy is read.
deck.core.active = Some(second);
});
let gone = cx.update(|app| Deck::text(app, "show.play.gone", &[]));
settle(&cx, |app| root.read(app).toasts(app).iter().any(|(text, _)| *text == gone));
assert!(cx.update(|app| !deck.read(app).show_starting && deck.read(app).show.is_none()));
}
+4
View File
@@ -10,6 +10,8 @@ pub(super) const EN: &[(&str, &str)] = &[
("show.play.screen", "Screen {number} ({width} × {height})"),
("show.play.start", "Start"),
("show.play.none", "Every slide is hidden"),
("show.play.busy", "The show is already starting or playing"),
("show.play.gone", "The show did not start: its deck or screen changed"),
("show.play.back", "Previous"),
("show.play.next", "Next"),
("show.play.place", "Slide {number} of {count}"),
@@ -70,6 +72,8 @@ pub(super) const ZH_CN: &[(&str, &str)] = &[
("show.play.screen", "屏幕 {number}({width} × {height})"),
("show.play.start", "开始放映"),
("show.play.none", "所有幻灯片都已隐藏"),
("show.play.busy", "放映已在开始或进行中"),
("show.play.gone", "放映未开始:文稿或屏幕已变"),
("show.play.back", "上一步"),
("show.play.next", "下一步"),
("show.play.place", "第 {number} 张,共 {count} 张"),
+1 -1
View File
@@ -32,7 +32,7 @@
- [x] S-07 母版视图下演示检查的行按幻灯片序号跳转 — 母版视图下点行提示先关母版,不跳;R1 PASS
- [x] S-08 刚编辑过幻灯片就进备注栏键入,备注可能丢 — 发出的备注待读回确认,键入中读回只移基线不动草稿;R2 PASS
- [x] S-09 改尺寸失败后 `size_asked` 一直留着 — 所要尺寸带输入序号,发送前先记、未入队即清,命令落地后的读回即清;R2 PASS
- [ ] S-10 开始放映的两处拒绝无提示(规则 2)
- [x] S-10 开始放映的两处拒绝无提示(规则 2) — 三处拒绝都提示(另含读回时已换标签);R2 PASS
- [x] P-01 文件系统的 `PermissionDenied` 被拼成不存在的文案键,应用崩溃 — worker 的拒绝改为自有错误类型 Refused;R1 测试不跨平台、不看提示;组件库 f834f55 加 Toaster::shown;R2 PASS
- [x] P-02 每次编辑落地清掉全部图块与缩略图,页面空白、刚画的墨迹消失 — 图块随 R2.6;缩略图在编辑后留到重画;R1、R2 测试未证实际绘制;R3 PASS
- [x] P-03 “编辑文字和图片”先命中图片对象再命中文字;一点抖动就移动了背景图 — 包住文字块的对象让位于文字,移动须过 3 个窗口像素;R1 漏掉同点的小图;R2 PASS
+3
View File
@@ -0,0 +1,3 @@
FAIL
apps/ely-show/src/tests/play.rs:149:S-10 的 gone 拒绝路径缺少回归测试,新增用例只断言 show_starting 时的 busy toast。具体场景:放映副本读取期间切换标签,或选屏后原标签/观众屏消失再开始放映;这些路径分别经过 apps/ely-show/src/slideshow/start.rs:183 和 :218。验证:在独立副本中移除两处 show.play.gone toast,保留日志与 discard,原有 13 个放映测试仍全部通过;本轮补充的两个定向探针在当前实现上通过,移除 toast 后均因窗口根的提示列表为空而失败。修复方向:为上述两条拒绝路径补充回归测试,覆盖标签切换、标签消失与观众屏失效,断言窗口根出现 show.play.gone、启动状态解除且放映副本被清理。
+1
View File
@@ -0,0 +1 @@
PASS