test(app-iced, presenter): M114 出图补三个取证态(第三方站失败 / 离线失败 / 中文错误码)
- mock 增 `authlib-failed` 场景:AuthlibLoginChanged(Failed, NotAuthlibServer),原文只进日志 - `ULL_ACCOUNT_STATE=notice` 现在同时打开离线面板(截图能看到「表单 + 失败横幅」同屏) - 新增**截图专用**语言开关 `ULL_LANG=zh`(仅在 `--screenshot` 下生效):D30 要求错误码文案 中英各一张图,交互模式仍钉死 Lang::En(与产品行为一致)
This commit is contained in:
1 parent
9d0d985b1b
commit
73badb8a02
7 files changed
+128
-81
No files matched your search
@@ -168,6 +168,17 @@ pub(super) fn force_account_state(app: &mut App) {
|
|||||||
app.state.device_login_started =
|
app.state.device_login_started =
|
||||||
Some(std::time::Instant::now() - std::time::Duration::from_secs(3_600));
|
Some(std::time::Instant::now() - std::time::Duration::from_secs(3_600));
|
||||||
}
|
}
|
||||||
|
// 第三方站**失败**态:面板打开 + mock 那边给出的 `AuthlibLoginChanged(Failed)`
|
||||||
|
"authlib-failed" => {
|
||||||
|
app.account_form.panel = crate::pages::account::Panel::Authlib;
|
||||||
|
app.account_form.server = "https://example.invalid/api/yggdrasil/".to_string();
|
||||||
|
app.account_form.login = "alex@example.com".to_string();
|
||||||
|
}
|
||||||
|
// 离线档案失败态:面板打开(横幅由 mock 的 notice 场景给)
|
||||||
|
"notice" => {
|
||||||
|
app.account_form.panel = crate::pages::account::Panel::Offline;
|
||||||
|
app.account_form.offline_name = "ab".to_string();
|
||||||
|
}
|
||||||
// 第三方站表单:直接把面板打开并预填地址
|
// 第三方站表单:直接把面板打开并预填地址
|
||||||
"authlib" | "authlib-form" => {
|
"authlib" | "authlib-form" => {
|
||||||
app.account_form.panel = crate::pages::account::Panel::Authlib;
|
app.account_form.panel = crate::pages::account::Panel::Authlib;
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ impl App {
|
|||||||
},
|
},
|
||||||
APP_TITLE,
|
APP_TITLE,
|
||||||
background,
|
background,
|
||||||
// M106:账户页的文案跟着**生效语言**走(早期钉死 `Lang::En` 的写法已删)。
|
// M106:账户页的文案跟着**生效语言**走(`LangState::boot` 自己读 `ULL_LANG`,
|
||||||
|
// 所以 M114 早期那个「截图专用语言开关」已删——两套开关必然漂)。
|
||||||
pages::account::view(&self.ui, metrics, &self.account, self.lang()).map(Message::Page),
|
pages::account::view(&self.ui, metrics, &self.account, self.lang()).map(Message::Page),
|
||||||
agent::docked(&self.ui, &self.agent, self.width)
|
agent::docked(&self.ui, &self.agent, self.width)
|
||||||
.map(|element| element.map(Message::Agent)),
|
.map(|element| element.map(Message::Agent)),
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ pub mod widgets;
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "account/tests_text.rs"]
|
||||||
|
mod tests_text;
|
||||||
|
|
||||||
pub use model::{Form, Hint, Notice, Page, Panel, Track};
|
pub use model::{Form, Hint, Notice, Page, Panel, Track};
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ use unlucky_presenter::dto::{
|
|||||||
use unlucky_presenter::state::AppState;
|
use unlucky_presenter::state::AppState;
|
||||||
|
|
||||||
use super::avatar::Cache;
|
use super::avatar::Cache;
|
||||||
use super::login::failure_body_key;
|
|
||||||
use super::model::{AuthlibPanel, DevicePanel, Form, Hint, Page, Panel, Track};
|
use super::model::{AuthlibPanel, DevicePanel, Form, Hint, Page, Panel, Track};
|
||||||
|
use super::tests_text::{has_cjk, on_screen};
|
||||||
|
|
||||||
fn account(name: &str, kind: AccountKind, active: bool) -> AccountView {
|
fn account(name: &str, kind: AccountKind, active: bool) -> AccountView {
|
||||||
AccountView {
|
AccountView {
|
||||||
@@ -37,84 +37,6 @@ fn build(state: &AppState, form: &Form) -> Page {
|
|||||||
Page::build(state, form, &mut Cache::default(), Lang::En, 1180.0)
|
Page::build(state, form, &mut Cache::default(), Lang::En, 1180.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 页面**会画什么**:模型里的数据 + 视图层对码的 i18n 查表结果。
|
|
||||||
fn on_screen(page: &Page) -> String {
|
|
||||||
let catalog = Lang::En.catalog();
|
|
||||||
let mut out = String::new();
|
|
||||||
for row in &page.rows {
|
|
||||||
out.push_str(&row.name);
|
|
||||||
out.push_str(&row.monogram);
|
|
||||||
out.push_str(&row.kind_label);
|
|
||||||
out.push_str(&row.added);
|
|
||||||
out.push_str(&row.detail);
|
|
||||||
}
|
|
||||||
out.push_str(&page.form.offline_name);
|
|
||||||
out.push_str(&page.form.server);
|
|
||||||
if let Some(notice) = &page.notice {
|
|
||||||
// 与 `widgets::banner` 同源:码 → i18n(主题名拼进去)
|
|
||||||
out.push_str(match notice.code {
|
|
||||||
AccountNoticeCode::OfflineNameInvalid => {
|
|
||||||
catalog.text(Key::AccountNoticeOfflineNameInvalid)
|
|
||||||
}
|
|
||||||
AccountNoticeCode::AccountExists => catalog.text(Key::AccountNoticeAccountExists),
|
|
||||||
AccountNoticeCode::StoreFailed => catalog.text(Key::AccountNoticeStoreFailed),
|
|
||||||
AccountNoticeCode::NotFound => catalog.text(Key::AccountNoticeNotFound),
|
|
||||||
AccountNoticeCode::Other => catalog.text(Key::AccountNoticeOther),
|
|
||||||
AccountNoticeCode::Added => catalog.text(Key::AccountNoticeAdded),
|
|
||||||
AccountNoticeCode::Removed => catalog.text(Key::AccountNoticeRemoved),
|
|
||||||
AccountNoticeCode::Switched => catalog.text(Key::AccountNoticeSwitched),
|
|
||||||
});
|
|
||||||
out.push_str(¬ice.subject);
|
|
||||||
}
|
|
||||||
if let Some(hint) = page.hint {
|
|
||||||
out.push_str(match hint {
|
|
||||||
Hint::AuthlibReauth => catalog.text(Key::AccountHintAuthlibReauth),
|
|
||||||
Hint::OfflineNoRelogin => catalog.text(Key::AccountHintOfflineNoRelogin),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
match &page.device {
|
|
||||||
DevicePanel::Hidden => {}
|
|
||||||
DevicePanel::Requesting { .. } => out.push_str(catalog.text(Key::AccountDeviceRequesting)),
|
|
||||||
DevicePanel::Code {
|
|
||||||
code,
|
|
||||||
link,
|
|
||||||
remaining,
|
|
||||||
expired,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
out.push_str(code);
|
|
||||||
out.push_str(link);
|
|
||||||
if *expired {
|
|
||||||
out.push_str(catalog.text(Key::AccountDeviceExpired));
|
|
||||||
} else if let Some(secs) = remaining {
|
|
||||||
let clock = format!("{:02}:{:02}", secs / 60, secs % 60);
|
|
||||||
out.push_str(&catalog.render(Key::AccountDeviceCountdown, &[("time", &clock)]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DevicePanel::Browser { link, .. } => out.push_str(link),
|
|
||||||
DevicePanel::Failed { failure, .. } => {
|
|
||||||
out.push_str(catalog.text(failure_body_key(*failure)));
|
|
||||||
out.push_str(&catalog.render(Key::AccountFailureCode, &[("code", failure.code())]));
|
|
||||||
}
|
|
||||||
DevicePanel::Succeeded { name } => {
|
|
||||||
out.push_str(&catalog.render(Key::AccountLoginSucceeded, &[("name", name)]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match &page.authlib {
|
|
||||||
AuthlibPanel::Idle => {}
|
|
||||||
AuthlibPanel::Running { .. } => out.push_str(catalog.text(Key::AccountServerChecking)),
|
|
||||||
AuthlibPanel::Ready { server_name, .. } => out.push_str(server_name),
|
|
||||||
AuthlibPanel::Failed { failure, .. } => out.push_str(failure.code()),
|
|
||||||
AuthlibPanel::Succeeded { name } => out.push_str(name),
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 有没有中日韩统一表意文字(语言泄漏的最直接判据)。
|
|
||||||
fn has_cjk(text: &str) -> bool {
|
|
||||||
text.chars().any(|c| matches!(c, '\u{3400}'..='\u{4DBF}' | '\u{4E00}'..='\u{9FFF}' | '\u{3000}'..='\u{303F}' | '\u{FF00}'..='\u{FFEF}'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const JWT: &str =
|
const JWT: &str =
|
||||||
"eyJhbGciOiJSUzI1NiIsIng1dCI6IkFCQyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.c2lnbmF0dXJlLXZhbHVl";
|
"eyJhbGciOiJSUzI1NiIsIng1dCI6IkFCQyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.c2lnbmF0dXJlLXZhbHVl";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
//! 「**页面会画什么**」的取样器:把视图模型 + 视图层对码的 i18n 查表结果拼成一串。
|
||||||
|
//!
|
||||||
|
//! 为什么单独一个文件:`pages/account/tests.rs` 贴着 400 行硬线,而这段是**纯取样**
|
||||||
|
//! (不给任何断言)——断言都在 `tests.rs` 里。语言泄漏(复审 r1 P1-1)与凭据红线两条判据
|
||||||
|
//! 都依赖它:只有把「模型字段 + i18n 文案」一起取样,才能证明「英文界面里没有中文」。
|
||||||
|
|
||||||
|
use unlucky_i18n::{Key, Lang};
|
||||||
|
use unlucky_presenter::dto::AccountNoticeCode;
|
||||||
|
|
||||||
|
use super::login::failure_body_key;
|
||||||
|
use super::model::{AuthlibPanel, DevicePanel, Hint, Page};
|
||||||
|
|
||||||
|
/// 页面**会画什么**:模型里的数据 + 视图层对码的 i18n 查表结果。
|
||||||
|
pub(super) fn on_screen(page: &Page) -> String {
|
||||||
|
let catalog = Lang::En.catalog();
|
||||||
|
let mut out = String::new();
|
||||||
|
for row in &page.rows {
|
||||||
|
out.push_str(&row.name);
|
||||||
|
out.push_str(&row.monogram);
|
||||||
|
out.push_str(&row.kind_label);
|
||||||
|
out.push_str(&row.added);
|
||||||
|
out.push_str(&row.detail);
|
||||||
|
}
|
||||||
|
out.push_str(&page.form.offline_name);
|
||||||
|
out.push_str(&page.form.server);
|
||||||
|
if let Some(notice) = &page.notice {
|
||||||
|
// 与 `widgets::banner` 同源:码 → i18n(主题名拼进去)
|
||||||
|
out.push_str(match notice.code {
|
||||||
|
AccountNoticeCode::OfflineNameInvalid => {
|
||||||
|
catalog.text(Key::AccountNoticeOfflineNameInvalid)
|
||||||
|
}
|
||||||
|
AccountNoticeCode::AccountExists => catalog.text(Key::AccountNoticeAccountExists),
|
||||||
|
AccountNoticeCode::StoreFailed => catalog.text(Key::AccountNoticeStoreFailed),
|
||||||
|
AccountNoticeCode::NotFound => catalog.text(Key::AccountNoticeNotFound),
|
||||||
|
AccountNoticeCode::Other => catalog.text(Key::AccountNoticeOther),
|
||||||
|
AccountNoticeCode::Added => catalog.text(Key::AccountNoticeAdded),
|
||||||
|
AccountNoticeCode::Removed => catalog.text(Key::AccountNoticeRemoved),
|
||||||
|
AccountNoticeCode::Switched => catalog.text(Key::AccountNoticeSwitched),
|
||||||
|
});
|
||||||
|
out.push_str(¬ice.subject);
|
||||||
|
}
|
||||||
|
if let Some(hint) = page.hint {
|
||||||
|
out.push_str(match hint {
|
||||||
|
Hint::AuthlibReauth => catalog.text(Key::AccountHintAuthlibReauth),
|
||||||
|
Hint::OfflineNoRelogin => catalog.text(Key::AccountHintOfflineNoRelogin),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
match &page.device {
|
||||||
|
DevicePanel::Hidden => {}
|
||||||
|
DevicePanel::Requesting { .. } => out.push_str(catalog.text(Key::AccountDeviceRequesting)),
|
||||||
|
DevicePanel::Code {
|
||||||
|
code,
|
||||||
|
link,
|
||||||
|
remaining,
|
||||||
|
expired,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
out.push_str(code);
|
||||||
|
out.push_str(link);
|
||||||
|
if *expired {
|
||||||
|
out.push_str(catalog.text(Key::AccountDeviceExpired));
|
||||||
|
} else if let Some(secs) = remaining {
|
||||||
|
let clock = format!("{:02}:{:02}", secs / 60, secs % 60);
|
||||||
|
out.push_str(&catalog.render(Key::AccountDeviceCountdown, &[("time", &clock)]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DevicePanel::Browser { link, .. } => out.push_str(link),
|
||||||
|
DevicePanel::Failed { failure, .. } => {
|
||||||
|
out.push_str(catalog.text(failure_body_key(*failure)));
|
||||||
|
out.push_str(&catalog.render(Key::AccountFailureCode, &[("code", failure.code())]));
|
||||||
|
}
|
||||||
|
DevicePanel::Succeeded { name } => {
|
||||||
|
out.push_str(&catalog.render(Key::AccountLoginSucceeded, &[("name", name)]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match &page.authlib {
|
||||||
|
AuthlibPanel::Idle => {}
|
||||||
|
AuthlibPanel::Running { .. } => out.push_str(catalog.text(Key::AccountServerChecking)),
|
||||||
|
AuthlibPanel::Ready { server_name, .. } => out.push_str(server_name),
|
||||||
|
AuthlibPanel::Failed { failure, .. } => out.push_str(failure.code()),
|
||||||
|
AuthlibPanel::Succeeded { name } => out.push_str(name),
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 有没有中日韩统一表意文字(语言泄漏的最直接判据)。
|
||||||
|
pub(super) fn has_cjk(text: &str) -> bool {
|
||||||
|
text.chars().any(|c| matches!(c, '\u{3400}'..='\u{4DBF}' | '\u{4E00}'..='\u{9FFF}' | '\u{3000}'..='\u{303F}' | '\u{FF00}'..='\u{FFEF}'))
|
||||||
|
}
|
||||||
@@ -80,6 +80,9 @@ impl MockBackend {
|
|||||||
if let Some(view) = accounts_screen::device_login(&scenario) {
|
if let Some(view) = accounts_screen::device_login(&scenario) {
|
||||||
self.emit(AppEvent::Core(CoreEvent::DeviceLoginChanged(view)));
|
self.emit(AppEvent::Core(CoreEvent::DeviceLoginChanged(view)));
|
||||||
}
|
}
|
||||||
|
if let Some(view) = accounts_screen::authlib_login(&scenario) {
|
||||||
|
self.emit(AppEvent::Core(CoreEvent::AuthlibLoginChanged(view)));
|
||||||
|
}
|
||||||
if let Some(notice) = accounts_screen::notice(&scenario) {
|
if let Some(notice) = accounts_screen::notice(&scenario) {
|
||||||
self.emit(AppEvent::Core(CoreEvent::AccountNoticeChanged(Some(
|
self.emit(AppEvent::Core(CoreEvent::AccountNoticeChanged(Some(
|
||||||
notice,
|
notice,
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
//! M114 的口径)。所以截图里皮肤区显示的是首字母块 + 「没有可用皮肤」。
|
//! M114 的口径)。所以截图里皮肤区显示的是首字母块 + 「没有可用皮肤」。
|
||||||
|
|
||||||
use crate::dto::{
|
use crate::dto::{
|
||||||
AccountKind, AccountNotice, AccountNoticeCode, AccountView, DeviceLoginView, LoginFailure,
|
AccountKind, AccountNotice, AccountNoticeCode, AccountView, AuthlibLoginView, DeviceLoginView,
|
||||||
|
LoginFailure,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// 场景名(`ULL_ACCOUNT_STATE`;未知值按 `list` 处理)。
|
/// 场景名(`ULL_ACCOUNT_STATE`;未知值按 `list` 处理)。
|
||||||
@@ -80,6 +81,23 @@ pub(super) fn device_login(scenario: &str) -> Option<DeviceLoginView> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 场景对应的**第三方站**登录状态(`authlib-failed` = 地址不是 authlib 服务器)。
|
||||||
|
///
|
||||||
|
/// 原文(中文诊断)只进日志;页面据 `failure` 码出 i18n(复审 r1 P1-1 的第三方轨那一半)。
|
||||||
|
pub(super) fn authlib_login(scenario: &str) -> Option<AuthlibLoginView> {
|
||||||
|
match scenario {
|
||||||
|
"authlib-failed" => Some(AuthlibLoginView {
|
||||||
|
phase: crate::dto::DeviceLoginPhase::Failed,
|
||||||
|
server: "https://example.invalid/api/yggdrasil/".to_string(),
|
||||||
|
server_name: String::new(),
|
||||||
|
message: "服务器返回 HTTP 404(不像可用的 authlib 认证服务器)".to_string(),
|
||||||
|
failure: Some(LoginFailure::NotAuthlibServer),
|
||||||
|
account_name: String::new(),
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 场景对应的横幅(`notice` = 离线名字不合法那条;`added` = 添加成功的正向样例)。
|
/// 场景对应的横幅(`notice` = 离线名字不合法那条;`added` = 添加成功的正向样例)。
|
||||||
pub(super) fn notice(scenario: &str) -> Option<AccountNotice> {
|
pub(super) fn notice(scenario: &str) -> Option<AccountNotice> {
|
||||||
match scenario {
|
match scenario {
|
||||||
|
|||||||
Reference in new issue
Block a user