diff --git "a/docs/02_ENV/ENV_04_\351\226\213\347\231\272\343\202\263\343\203\236\343\203\263\343\203\211.md" "b/docs/02_ENV/ENV_04_\351\226\213\347\231\272\343\202\263\343\203\236\343\203\263\343\203\211.md" index e8a5a17..db97184 100644 --- "a/docs/02_ENV/ENV_04_\351\226\213\347\231\272\343\202\263\343\203\236\343\203\263\343\203\211.md" +++ "b/docs/02_ENV/ENV_04_\351\226\213\347\231\272\343\202\263\343\203\236\343\203\263\343\203\211.md" @@ -26,7 +26,7 @@ | コンソールスクリプトで起動 | `uv run tiasjudge` | | 起動可能性のみ確認(画面を出さずに終了) | `uv run python -m tiasjudge --check` | -※ 診断を実行すると結果画面(モードレス)が開く.結果画面は日本語 ⇔ 英語を切り替えられる. +※ 診断を実行すると結果画面(モードレス)が開く.結果画面は表示専用で,言語切替と「閉じる」はメイン画面の「結果画面の操作」から行う. ※ ヘッドレス環境(画面の無い CI 等)では `QT_QPA_PLATFORM=offscreen` を付けて `--check` を実行する. ### デバッグ用 (Debug) @@ -37,9 +37,10 @@ | --- | --- | | 結果画面をダミー結果で開く(成功パターン) | `uv run tiasjudge --preview-result` | | 結果画面をダミー結果で開く(診断失敗パターン) | `uv run tiasjudge --preview-result --failed` | +| 結果画面を英語表示で開く | `uv run tiasjudge --preview-result --english` | | プレビュー画面の構築のみ確認(即終了) | `uv run tiasjudge --preview-result --check` | -※ 日本語で開く.英語表示は画面内の言語切替ボタンで確認する. +※ 既定は日本語表示.結果画面は表示専用(言語切替ボタンを持たない)ため,英語表示の確認には `--english` を使う. ※ プライマリ以外のモニタがあればそこに最大化して開く(2 画面運用の確認.SPEC_01「表示先モニタ」参照).モニタ 1 台なら通常サイズで開く. ※ 結果画面を閉じるとアプリが終了する. ※ ダミー値は成功パターンが淡紅 55% / 紅 25% / 暗赤 10% / 紫 5% / 淡白 5%(上位 2 色は淡紅・紅),失敗パターンが旧実装の失敗時スコア組(紫 42 / 暗赤 0 / 紅 5 / 淡紅 11 / 淡白 3). diff --git "a/docs/04_SPEC/SPEC_01_\347\224\273\351\235\242\344\273\225\346\247\230.md" "b/docs/04_SPEC/SPEC_01_\347\224\273\351\235\242\344\273\225\346\247\230.md" index d333c0e..2d47f6a 100644 --- "a/docs/04_SPEC/SPEC_01_\347\224\273\351\235\242\344\273\225\346\247\230.md" +++ "b/docs/04_SPEC/SPEC_01_\347\224\273\351\235\242\344\273\225\346\247\230.md" @@ -24,6 +24,8 @@ | L\*a\*b\* 表示 | 診断時に算出した L\*・a\*・b\* を表示 | | 5 色スコア表示 | 紫・暗赤・紅・淡紅・淡白 の各一致率(%)を表示 | | [診断続行] ボタン | 診断後に有効化.「同じ変換行列を使用するか」を確認し,画像・クリック状態をリセット | +| 結果画面の操作: 言語切替ボタン | 開いている全結果画面の表示言語を切替(ボタンラベルは切替先の言語名.日本語表示中は "English").**常時有効**で,次に開く結果画面にも適用される(診断続行後も維持).言語状態はメイン画面が保持(初期値は日本語) | +| 結果画面の操作: [結果画面を閉じる] ボタン | 開いている全結果画面を閉じる.結果画面が 1 枚も無いときは無効 | ### 入力ガード(エラー表示)(Input Guards) @@ -40,10 +42,12 @@ | --- | --- | | 診断画像 | クロップ後の元画像を 1/2 縮小表示 | | 結果パネル | 5 色の行(色名 + 説明)を一覧表示し,**上位 2 色のみ**スコア(%)と下線を描画 | -| 言語切替ボタン | 日本語 ⇔ 英語を切替(ボタンラベルは切替先の言語名) | | 診断失敗表示 | 診断失敗時(SPEC_02「診断失敗の判定」参照)に「Diagnosis failed / Please try again.」を表示 | | 注記 | 「本結果は疾患の診断を行うものではない」「表示画像は明るさ補正されている」 | +※ 結果画面は**表示専用**(患者側モニタに操作 UI を出さないため,ボタンを持たない).言語切替と「閉じる」はメイン画面の「結果画面の操作」から行う(タイトルバーの × でのみ直接閉じられる).当初は結果画面内に言語切替・閉じるボタンがあったが,2 画面運用に合わせてメイン画面へ移した. +※ 英語の説明文は長文の行のみフォントサイズを小さく固定する(Red / Dark red は 36 px,Pale red は 30 px,他は 40 px.スコア表示と重ならない実測最大値.強調の有無でサイズは変えない). + ### 表示先モニタ(2 画面運用)(Target Monitor) 2 画面運用(施術者側=メイン画面,患者側=結果画面)を前提とし,結果画面は**メイン画面が表示されているモニタとは別のモニタに最大化**して表示する. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 0bcce1a..a73fa16 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -56,3 +56,6 @@ - [x] **結果画面のサブモニタ最大化表示(2 画面運用)**: ユーザー要望による追加仕様(旧実装に無い).結果画面をメイン画面と別のモニタに最大化表示する(SPEC_01「表示先モニタ」節を新設) - 設計判断: 基準はプライマリ固定でなく「メイン画面が載っているモニタ」.モニタ 1 台時は従来表示にフォールバック.同一モニタ判定は identity → 同名の 2 段(Qt が別ラッパを返すケース対策).移動 → 最大化の順序を厳守(逆だと元モニタで最大化される) - 2 画面実機での動作確認済み(ユーザー確認) +- [x] **結果画面の表示専用化(操作をメイン画面へ移動)+英語説明文の行単位サイズ**: ユーザー要望による変更(実機確認済み) + - 結果画面から言語切替・閉じるボタンを削除し表示専用に(患者側モニタに操作 UI を出さない).メイン画面に「結果画面の操作」グループを新設(言語切替は常時有効・閉じるは結果画面が開いている時のみ有効.言語状態はメイン画面が保持し次に開く画面へ継承).プレビューは `--english` オプションで英語確認 + - 英語の説明文サイズは行単位の固定値(Red / Dark red 36 px・Pale red 30 px・他 40 px).一律縮小 → 行単位と試行し,「スコア付き強調行でも縮小なしで収まる実測最大値」を行ごとに採用(強調時にサイズが変わるチカつきを排除).Red は当初 40 px 指定だったが実測で収まらず 36 px に変更(ユーザー承認済み) diff --git a/src/tiasjudge/__main__.py b/src/tiasjudge/__main__.py index 4ddc742..79b57fa 100644 --- a/src/tiasjudge/__main__.py +++ b/src/tiasjudge/__main__.py @@ -8,6 +8,8 @@ - `--check`: 画面を生成して即終了する(起動可能性の確認用.イベントループを回さない) - `--preview-result`: 結果画面(SPEC_01「結果画面」)だけをダミー結果で開く(デバッグ用) - `--failed`: `--preview-result` と併用し,診断失敗パターンで開く +- `--english`: `--preview-result` と併用し,英語表示で開く(結果画面は表示専用のため, + プレビューでは画面内で言語を切り替えられない) """ from __future__ import annotations @@ -77,6 +79,11 @@ action="store_true", help="--preview-result と併用し,診断失敗パターンで開く", ) + parser.add_argument( + "--english", + action="store_true", + help="--preview-result と併用し,英語表示で開く", + ) return parser @@ -112,13 +119,18 @@ ) -def _run_preview(app: QApplication, *, failed: bool, check: bool) -> int: - """結果画面だけをダミー結果で開く(`--preview-result`).""" +def _run_preview(app: QApplication, *, failed: bool, check: bool, english: bool = False) -> int: + """結果画面だけをダミー結果で開く(`--preview-result`). + + 結果画面は表示専用(言語切替ボタンを持たない)ため,プレビューの言語は + `--english` オプションで指定する. + """ # 関数内 import は起動時のみ GUI を読み込むため(`core` は GUI 非依存) from tiasjudge.gui.result_window import Language, ResultWindow, show_on_secondary_screen - logger.info("結果画面のプレビューを開きます(failed=%s)", failed) - window = ResultWindow(_preview_result(failed=failed), _preview_image(), Language.JAPANESE) + language = Language.ENGLISH if english else Language.JAPANESE + logger.info("結果画面のプレビューを開きます(failed=%s・language=%s)", failed, language.value) + window = ResultWindow(_preview_result(failed=failed), _preview_image(), language) if check: window.close() return 0 @@ -146,7 +158,12 @@ app = QApplication.instance() or QApplication(sys.argv[:1]) if arguments.preview_result: - return _run_preview(app, failed=arguments.failed, check=arguments.check) + return _run_preview( + app, + failed=arguments.failed, + english=arguments.english, + check=arguments.check, + ) from tiasjudge.gui.main_window import MainWindow diff --git a/src/tiasjudge/gui/main_window.py b/src/tiasjudge/gui/main_window.py index 468c9eb..bd3ce00 100644 --- a/src/tiasjudge/gui/main_window.py +++ b/src/tiasjudge/gui/main_window.py @@ -14,7 +14,9 @@ 診断結果は結果画面(`result_window.ResultWindow`)にモードレスで表示する.結果の 受け渡し口として `MainWindow.diagnosis_finished` シグナルと `result_presenter` -(差し替え用のコールバック)を用意してある. +(差し替え用のコールバック)を用意してある.結果画面は表示専用(患者側モニタに操作 UI を +出さない)のため,言語切替と「閉じる」は本画面の「結果画面の操作」から行う.表示言語は +本画面が保持し,開いている結果画面と次に開く結果画面の両方に適用する. """ from __future__ import annotations @@ -56,8 +58,11 @@ from tiasjudge.core.svm import COLOR_KEYS, DiagnosisResult, diagnose from tiasjudge.gui.result_window import ( IMAGE_DISPLAY_DIVISOR, + Language, ResultWindow, + next_language, show_on_secondary_screen, + switch_button_label, to_display_pixmap, ) @@ -457,6 +462,8 @@ self._last_directory = Path.home() #: 開いている結果画面(閉じるまで保持する) self._result_windows: list[ResultWindow] = [] + #: 結果画面の表示言語(メイン画面が保持し,次に開く結果画面にも引き継ぐ) + self._result_language = Language.JAPANESE #: 結果画面の表示関数(未設定なら `ResultWindow` を開く) self.result_presenter: ResultPresenter | None = None @@ -514,10 +521,26 @@ buttons.addWidget(self._quit_button) layout.addLayout(buttons) + layout.addWidget(self._build_result_control_group(panel)) layout.addWidget(self._build_result_group(panel)) layout.addStretch(1) return panel + def _build_result_control_group(self, parent: QWidget) -> QGroupBox: + """結果画面(患者側モニタ)を操作するボタン群を作る. + + 結果画面は表示専用なので,言語切替と「閉じる」はここ(施術者側)で操作する. + """ + group = QGroupBox("結果画面の操作", parent) + self._result_language_button = QPushButton(group) + self._result_language_button.clicked.connect(self.toggle_result_language) + self._result_close_button = QPushButton("結果画面を閉じる", group) + self._result_close_button.clicked.connect(self.close_result_windows) + row = QHBoxLayout(group) + row.addWidget(self._result_language_button) + row.addWidget(self._result_close_button) + return group + def _build_file_row( self, parent: QWidget, title: str, on_browse: Callable[[], None] ) -> tuple[QGroupBox, QLineEdit, QPushButton]: @@ -722,6 +745,11 @@ self._matrix_button.setEnabled(not self._state.diagnosed) self._image_button.setEnabled(not self._state.diagnosed) + # 言語切替は「次に開く画面の言語」を先に決められるよう常時有効とし, + # 「閉じる」は対象がある時だけ有効にする + self._result_language_button.setText(switch_button_label(self._result_language)) + self._result_close_button.setEnabled(bool(self._result_windows)) + def _show_scores(self, result: DiagnosisResult) -> None: """L*a*b* と 5 色スコアをパネルに表示する.""" for key, value in zip(LAB_LABELS, lab_components(result.lab), strict=True): @@ -757,12 +785,14 @@ Returns: 開いた結果画面. """ - window = ResultWindow(result, image, parent=self) + # 現在の言語を引き継ぐ(診断続行後も維持される) + window = ResultWindow(result, image, self._result_language, parent=self) window.closed.connect(self._on_result_window_closed) self._result_windows.append(window) # 2 画面運用(施術者側にメイン画面・患者側に結果画面)を想定し, # メイン画面と別のモニタがあればそこへ最大化する(1 台なら従来どおり通常表示) show_on_secondary_screen(window, QApplication.screens(), self.screen()) + self._update_controls() return window def _on_result_window_closed(self) -> None: @@ -771,6 +801,37 @@ if isinstance(window, ResultWindow) and window in self._result_windows: self._result_windows.remove(window) window.deleteLater() + self._update_controls() + + # --- 結果画面の操作(結果画面は表示専用なのでメイン画面から操作する)--- + + @property + def result_language(self) -> Language: + """次に開く結果画面に適用される表示言語.""" + return self._result_language + + def toggle_result_language(self) -> None: + """結果画面の表示言語を切り替える(開いている全画面に反映する). + + 言語はメイン画面が保持するため,結果画面が 1 枚も開いていなくても切り替えられる + (次に開く画面の言語を先に決めておける). + """ + self._result_language = next_language(self._result_language) + # 結果画面側も切替時にログを出すため,こちらは対象枚数を添えて区別する + logger.info( + "結果画面の表示言語を切り替えました: %s(対象 %d 枚)", + self._result_language.value, + len(self._result_windows), + ) + for window in self._result_windows: + window.set_language(self._result_language) + self._update_controls() + + def close_result_windows(self) -> None: + """開いている結果画面をすべて閉じる.""" + # `closed` シグナル経由で `_result_windows` が変化するため複製を回す + for window in list(self._result_windows): + window.close() # --- 補助 --- diff --git a/src/tiasjudge/gui/result_window.py b/src/tiasjudge/gui/result_window.py index 630d719..d00f333 100644 --- a/src/tiasjudge/gui/result_window.py +++ b/src/tiasjudge/gui/result_window.py @@ -5,7 +5,8 @@ - 左: 診断に用いたクロップ後画像を 1/2 縮小表示する - 右: 5 色(淡紅・紅・暗赤・紫・淡白紅)の行を一覧表示し,**上位 2 色のみ**スコア(%)と 下線を描画する(`pale` の表示名はメイン画面の「淡白」と異なり,旧背景画像どおり「淡白紅」) -- 下: 言語切替ボタン(日本語 ⇔ 英語.ラベルは切替**先**の言語名) +- 操作ボタンは持たない(表示専用).言語切替と「閉じる」はメイン画面側のボタンで行う + (患者側モニタに操作 UI を出さないため) - 診断失敗時(`DiagnosisResult.failed`)は結果パネルに「Diagnosis failed / Please try again.」 を重ねて描画する @@ -50,8 +51,6 @@ QFrame, QHBoxLayout, QLabel, - QPushButton, - QVBoxLayout, QWidget, ) @@ -174,8 +173,19 @@ AFFILIATION_FONT_SIZE = 19 NOTE_FONT_SIZE = 28 NAME_FONT_SIZE = 44 +#: 説明文の基準サイズ(既定).行ごとの値は `ColorRowText.description_font_size` で上書きする DESCRIPTION_FONT_SIZE = 40 -#: 説明文を縮めてよい下限(強調行は右側にスコア欄がある分だけ幅が狭いため) +#: 説明文が長い行のサイズ(px).英語は語数が多く,基準の 40 px では強調行の上限幅 +#: (`DESCRIPTION_MAX_WIDTH_HIGHLIGHTED` = 324 px.スコア欄を除いた幅)に収まらない行がある. +#: 収まらないまま描くと強調された行だけ動的縮小が働き,上位 2 色が変わるたびに +#: 文字サイズがちらつくため,**強調の有無によらず固定**の小さいサイズを行ごとに与える. +#: 値は「スコア表示付きの強調行でも縮小なしで収まる最大サイズ」の実測値: +#: "Hot flush / feverish" と "Blood slightly thick" が 36 px, +#: 最長の "Cold / fatigue / anemia" が 30 px(31 px では 337 px となり超過する) +DESCRIPTION_FONT_SIZE_LONG = 36 +DESCRIPTION_FONT_SIZE_LONGEST = 30 +#: 説明文を縮めてよい下限(強調行は右側にスコア欄がある分だけ幅が狭いため. +#: 上記のとおり通常は発動しないが,フォント差のある環境向けの安全網として残す) DESCRIPTION_MIN_FONT_SIZE = 28 #: 説明文(症状)の太さ.既定よりわずかに太くしつつ,太さより字の大きさで読みやすさを #: 稼ぐ方針として Medium に留める(DemiBold は太すぎ・Bold は色名より目立つ.説明文のみに適用) @@ -205,11 +215,13 @@ key: 色のキー(`core.svm.COLOR_KEYS` と同じ). name: 表示する色名. description: 色名の右に表示する説明. + description_font_size: 説明文のサイズ(px).長い説明の行だけ小さくする. """ key: str name: str description: str + description_font_size: int = DESCRIPTION_FONT_SIZE @dataclass(frozen=True) @@ -223,7 +235,8 @@ notes: 注記(2 行). rows: 5 色の行(`DISPLAY_ORDER` と同じ並び). switch_button_label: 言語切替ボタンのラベル(切替**先**の言語名). - close_button_label: 閉じるボタンのラベル. + 結果画面は表示専用のため,このラベルを使うのはメイン画面「結果画面の操作」の + 言語切替ボタン(`switch_button_label()`). """ window_title: str @@ -232,7 +245,6 @@ notes: tuple[str, ...] rows: tuple[ColorRowText, ...] switch_button_label: str - close_button_label: str #: 言語別の表示リソース(旧実装の日本語背景画像と `BuildEnglishBackground()` の文言) @@ -256,7 +268,6 @@ ColorRowText("pale", "淡白紅", "冷え・だるさ・貧血"), ), switch_button_label="English", - close_button_label="閉じる", ), Language.ENGLISH: LanguageTexts( window_title="TIAS Tongue Color Analysis - Result", @@ -266,15 +277,30 @@ "* This result does not diagnose any disease.", "* Displayed image is brightness-corrected.", ), + # 説明文が長い 3 行だけサイズを下げる(値の根拠は `DESCRIPTION_FONT_SIZE_LONG` 参照) rows=( ColorRowText("pink", "Pink", "Normal"), - ColorRowText("crimson", "Red", "Hot flush / feverish"), - ColorRowText("dark_red", "Dark red", "Blood slightly thick"), + ColorRowText( + "crimson", + "Red", + "Hot flush / feverish", + description_font_size=DESCRIPTION_FONT_SIZE_LONG, + ), + ColorRowText( + "dark_red", + "Dark red", + "Blood slightly thick", + description_font_size=DESCRIPTION_FONT_SIZE_LONG, + ), ColorRowText("purple", "Purple", "Blood is thick"), - ColorRowText("pale", "Pale red", "Cold / fatigue / anemia"), + ColorRowText( + "pale", + "Pale red", + "Cold / fatigue / anemia", + description_font_size=DESCRIPTION_FONT_SIZE_LONGEST, + ), ), switch_button_label="日本語", - close_button_label="Close", ), } @@ -293,6 +319,7 @@ swatch: 色名の表示色(RGB). highlighted: 上位 2 色(スコアと下線を描く行)なら True. score_text: 強調行に表示するスコア文字列.非強調行は None. + description_font_size: 説明文のサイズ(px).強調の有無によらず一定. """ key: str @@ -301,6 +328,7 @@ swatch: tuple[int, int, int] highlighted: bool score_text: str | None + description_font_size: int = DESCRIPTION_FONT_SIZE def texts_for(language: Language) -> LanguageTexts: @@ -358,6 +386,7 @@ swatch=COLOR_SWATCHES[key], highlighted=highlighted, score_text=format_score(result.percentages[key]) if highlighted else None, + description_font_size=row_text.description_font_size, ) ) return tuple(rows) @@ -584,7 +613,11 @@ def _draw_rows(painter: QPainter, rows: tuple[ResultRow, ...]) -> None: - """5 色の行(色名・説明)と,上位 2 色のスコア・下線を描く.""" + """5 色の行(色名・説明)と,上位 2 色のスコア・下線を描く. + + 説明文のサイズは行ごと(`ResultRow.description_font_size`)に決まっており, + 強調の有無で変わらない. + """ for index, row in enumerate(rows): baseline = row_baseline(index) # 色名だけは旧背景画像に合わせて黒縁取りを付ける(日英とも同じ扱い) @@ -602,7 +635,7 @@ (DESCRIPTION_X, baseline), f": {row.description}", DESCRIPTION_COLOR, - DESCRIPTION_FONT_SIZE, + row.description_font_size, weight=DESCRIPTION_FONT_WEIGHT, max_width=( DESCRIPTION_MAX_WIDTH_HIGHLIGHTED if row.highlighted else DESCRIPTION_MAX_WIDTH @@ -765,7 +798,10 @@ # --- 画面構築 --- def _build_ui(self) -> None: - """ウィジェットを組み立てる(左: 診断画像/右: 結果パネル + ボタン). + """ウィジェットを組み立てる(左: 診断画像/右: 結果パネル). + + 操作ボタンは持たない(患者側モニタに操作 UI を出さないため,言語切替と + 「閉じる」はメイン画面側に置く). 結果パネルは 644x644 固定描画のため,サブモニタへ最大化したときは周囲が 余白になる.余白が偏らないよう,左右と上下に伸縮スペースを入れて内容を @@ -780,40 +816,33 @@ self._image_label.setFixedSize(self._image_pixmap.size()) layout.addWidget(self._image_label, 0, Qt.AlignmentFlag.AlignVCenter) - right = QVBoxLayout() - right.addStretch(1) self._panel_label = QLabel(self) self._panel_label.setFixedSize(PANEL_SIZE, PANEL_SIZE) - right.addWidget(self._panel_label, 0, Qt.AlignmentFlag.AlignHCenter) - - self._language_button = QPushButton(self) - self._language_button.clicked.connect(self.toggle_language) - self._close_button = QPushButton(self) - self._close_button.clicked.connect(self.close) - buttons = QHBoxLayout() - buttons.addWidget(self._language_button) - buttons.addStretch(1) - buttons.addWidget(self._close_button) - right.addLayout(buttons) - right.addStretch(1) - - layout.addLayout(right, 0) + layout.addWidget(self._panel_label, 0, Qt.AlignmentFlag.AlignVCenter) layout.addStretch(1) # --- 表示の更新 --- - def toggle_language(self) -> None: - """日本語 ⇔ 英語を切り替える(SPEC_01「言語切替ボタン」).""" - self._language = next_language(self._language) + def set_language(self, language: Language) -> None: + """表示言語を設定して描き直す(メイン画面の言語切替ボタンから呼ぶ). + + Args: + language: 表示する言語.現在と同じなら何もしない. + """ + if language is self._language: + return + self._language = language logger.info("結果画面の言語を切り替えました: %s", self._language.value) self._apply_language() + def toggle_language(self) -> None: + """日本語 ⇔ 英語を切り替える(言語状態はメイン画面が持つため主に内部・試験用).""" + self.set_language(next_language(self._language)) + def _apply_language(self) -> None: - """現在の言語でタイトル・ボタン・結果パネルを描き直す.""" + """現在の言語でタイトルと結果パネルを描き直す.""" texts = texts_for(self._language) self.setWindowTitle(texts.window_title) - self._language_button.setText(texts.switch_button_label) - self._close_button.setText(texts.close_button_label) rows = build_result_rows(self._result, self._language) self._panel_label.setPixmap(render_result_panel(rows, texts, self._result.failed)) diff --git a/tests/gui/test_main_window_logic.py b/tests/gui/test_main_window_logic.py index 2e7f9d0..6922e70 100644 --- a/tests/gui/test_main_window_logic.py +++ b/tests/gui/test_main_window_logic.py @@ -8,6 +8,8 @@ - 右クリックで全クリックを取り消す - [舌色診断] は 1 点以上クリックで有効化し,診断(ROI 平均色 → Lab → SVM)を実行する - 入力ガードの文言(変換行列未読込・画像未読込・クリック 0 点) +- 結果画面(患者側モニタ)は表示専用のため,言語切替と「結果画面を閉じる」はメイン画面が持つ + (言語は開いていない間もメイン画面が保持し,次に開く結果画面へ引き継ぐ) ※ testing.md の方針により GUI(QWidget)そのものの自動テストは書かない. 本ファイルは画面部品に依存しない関数・状態クラスのみを対象とする. @@ -39,6 +41,7 @@ TRAINING_DATA_ENV_VAR, ImageLoadError, InputGuardError, + MainWindow, MainWindowState, check_extension, crop_center, @@ -48,6 +51,7 @@ load_image_file, run_diagnosis, ) +from tiasjudge.gui.result_window import Language, next_language, switch_button_label # --- ヘルパー --- @@ -463,3 +467,129 @@ image = make_image(CROP_WIDTH, CROP_HEIGHT) with pytest.raises(ValueError): run_diagnosis(image, [], linear_coefficients(), training_data) + + +# --- 結果画面の操作(言語切替・閉じる)--- + + +class FakeResultWindow: + """開いている結果画面の代役(`set_language` / `close` のみ). + + 実際の結果画面は `close` すると `closed` シグナル経由でメイン画面の一覧から + 取り除かれるため,スタブでも同じ振る舞い(自分を一覧から外す)を再現する. + """ + + def __init__(self, owner: ResultControlHost) -> None: + self._owner = owner + self.languages: list[Language] = [] + self.closed = False + + def set_language(self, language: Language) -> None: + self.languages.append(language) + + def close(self) -> None: + self.closed = True + if self in self._owner._result_windows: + self._owner._result_windows.remove(self) + + +class ResultControlHost: + """`MainWindow` の結果画面操作ロジックだけを取り出したスタブ. + + `MainWindow` を構築すると QWidget を作ることになる(testing.md「GUI の自動テストは + 書かない」)ため,実メソッドを未束縛のまま借りて状態遷移のみを検証する. + """ + + result_language = MainWindow.result_language + toggle_result_language = MainWindow.toggle_result_language + close_result_windows = MainWindow.close_result_windows + + def __init__(self, window_count: int = 0, language: Language = Language.JAPANESE) -> None: + self._result_language = language + self._result_windows: list[FakeResultWindow] = [ + FakeResultWindow(self) for _ in range(window_count) + ] + self.control_updates = 0 + + def _update_controls(self) -> None: + """ボタンの有効・無効とラベルの更新(実装では画面部品を触る)の代役.""" + self.control_updates += 1 + + +class TestResultLanguage: + """結果画面の表示言語をメイン画面が保持する.""" + + @pytest.mark.parametrize("language", list(Language)) + def test_property_exposes_the_current_language(self, language: Language) -> None: + assert ResultControlHost(language=language).result_language is language + + @pytest.mark.parametrize("language", list(Language)) + def test_toggle_switches_the_language(self, language: Language) -> None: + host = ResultControlHost(language=language) + host.toggle_result_language() + assert host.result_language is next_language(language) + + def test_toggle_without_open_windows_is_allowed(self) -> None: + """結果画面が 1 枚も開いていなくても切り替えられる(次に開く画面の言語を決める).""" + host = ResultControlHost(window_count=0) + host.toggle_result_language() + assert host.result_language is Language.ENGLISH + + def test_toggle_applies_to_every_open_window(self) -> None: + host = ResultControlHost(window_count=3) + host.toggle_result_language() + for window in host._result_windows: + assert window.languages == [Language.ENGLISH] + + def test_toggling_twice_restores_the_language(self) -> None: + host = ResultControlHost(window_count=1) + host.toggle_result_language() + host.toggle_result_language() + assert host.result_language is Language.JAPANESE + assert host._result_windows[0].languages == [Language.ENGLISH, Language.JAPANESE] + + def test_toggle_refreshes_the_controls(self) -> None: + """ボタンのラベル(切替先の言語名)を更新するため,毎回 `_update_controls` を呼ぶ.""" + host = ResultControlHost(window_count=1) + host.toggle_result_language() + assert host.control_updates == 1 + + def test_button_label_shows_the_language_to_switch_to(self) -> None: + """ラベルは切替**先**の言語名(SPEC_01「言語切替ボタン」.メイン画面に移設).""" + host = ResultControlHost() + assert switch_button_label(host.result_language) == "English" + host.toggle_result_language() + assert switch_button_label(host.result_language) == "日本語" + + def test_language_is_not_part_of_the_diagnosis_state(self) -> None: + """言語は表示の関心事なので `MainWindowState`(診断入力の状態)には持たせない.""" + state = MainWindowState() + assert not hasattr(state, "language") + assert not hasattr(state, "result_language") + + +class TestCloseResultWindows: + """「結果画面を閉じる」(患者側モニタの画面をメイン画面から閉じる).""" + + def test_closes_every_open_window(self) -> None: + host = ResultControlHost(window_count=3) + windows = list(host._result_windows) + host.close_result_windows() + assert all(window.closed for window in windows) + + def test_leaves_no_window_open(self) -> None: + """閉じる途中で一覧が縮んでも取りこぼさない(複製を回している).""" + host = ResultControlHost(window_count=3) + host.close_result_windows() + assert host._result_windows == [] + + def test_without_open_windows_does_nothing(self) -> None: + host = ResultControlHost(window_count=0) + host.close_result_windows() + assert host._result_windows == [] + + def test_does_not_change_the_language(self) -> None: + """閉じても言語は保持する(次に開く画面へ引き継ぐ).""" + host = ResultControlHost(window_count=2, language=Language.ENGLISH) + host.close_result_windows() + assert host.result_language is Language.ENGLISH diff --git a/tests/gui/test_result_window_logic.py b/tests/gui/test_result_window_logic.py index d189112..b59a626 100644 --- a/tests/gui/test_result_window_logic.py +++ b/tests/gui/test_result_window_logic.py @@ -4,7 +4,9 @@ - 診断画像はクロップ後の元画像を 1/2 縮小表示する - 結果パネルは 5 色の行(色名 + 説明)を一覧表示し,**上位 2 色のみ**スコア(%)と下線を描く -- 言語切替ボタンは日本語 ⇔ 英語を切り替え,ラベルは切替**先**の言語名 +- 結果画面は表示専用(操作ボタンを持たない).言語切替・閉じるはメイン画面側で操作し, + 結果画面は `set_language()` で表示言語を差し替える(ラベルは切替**先**の言語名) +- 説明文のサイズは行ごとに決まり,強調(上位 2 色)の有無では変わらない - 診断失敗時は「Diagnosis failed / Please try again.」を表示する - 注記は「本結果は疾患の診断を行うものではない」「表示画像は明るさ補正されている」の 2 件 - 2 画面運用(施術者側にメイン画面・患者側に結果画面)では,メイン画面とは別のモニタが @@ -20,6 +22,7 @@ from collections.abc import Iterator from contextlib import contextmanager +from dataclasses import replace from pathlib import Path import numpy as np @@ -37,6 +40,8 @@ AFFILIATION_POSITION, COLOR_SWATCHES, DESCRIPTION_FONT_SIZE, + DESCRIPTION_FONT_SIZE_LONG, + DESCRIPTION_FONT_SIZE_LONGEST, DESCRIPTION_FONT_WEIGHT, DESCRIPTION_MAX_WIDTH, DESCRIPTION_MAX_WIDTH_HIGHLIGHTED, @@ -72,8 +77,10 @@ TEXT_MARGIN, UNDERLINE_WIDTH, UNDERLINE_X_RANGE, + ColorRowText, Language, ResultRow, + ResultWindow, _fit_font, _set_font, build_result_rows, @@ -259,7 +266,6 @@ assert texts.window_title assert texts.heading assert texts.switch_button_label - assert texts.close_button_label assert all(row.name and row.description for row in texts.rows) def test_english_names_and_descriptions_match_spec_table(self) -> None: @@ -401,6 +407,83 @@ assert NOTE_POSITIONS[1][1] < ROW_FIRST_Y - NAME_FONT_SIZE +# --- 説明文のサイズ(行ごとに固定.英語の長い行だけ小さくする) --- + +#: 英語の各行に期待する説明文サイズ(result_window.py の定数コメントにある実測値の回帰固定). +#: 40 px のままだと強調行の上限幅 `DESCRIPTION_MAX_WIDTH_HIGHLIGHTED` に収まらない +#: 3 行だけ小さくしている. +EXPECTED_ENGLISH_DESCRIPTION_FONT_SIZES = { + "pink": DESCRIPTION_FONT_SIZE, # "Normal" + "crimson": DESCRIPTION_FONT_SIZE_LONG, # "Hot flush / feverish" + "dark_red": DESCRIPTION_FONT_SIZE_LONG, # "Blood slightly thick" + "purple": DESCRIPTION_FONT_SIZE, # "Blood is thick" + "pale": DESCRIPTION_FONT_SIZE_LONGEST, # "Cold / fatigue / anemia" +} + + +class TestDescriptionFontSizeConstants: + """行ごとの説明文サイズの定数. + + ※ 「その幅に収まる最大サイズ」はインストール済みフォントに依存するため, + ここでは実測値からの意図しない逸脱を防ぐ回帰固定と,定数同士の関係だけを見る + (見た目は人間が確認する.testing.md「GUI の自動テストは書かない」). + """ + + def test_long_sizes_match_the_measured_values(self) -> None: + assert DESCRIPTION_FONT_SIZE == 40 + assert DESCRIPTION_FONT_SIZE_LONG == 36 + assert DESCRIPTION_FONT_SIZE_LONGEST == 30 + + def test_sizes_are_ordered_by_text_length(self) -> None: + """長い行ほど小さい(最長 < 長い < 基準).""" + assert DESCRIPTION_FONT_SIZE_LONGEST < DESCRIPTION_FONT_SIZE_LONG < DESCRIPTION_FONT_SIZE + + def test_smallest_size_does_not_trigger_dynamic_shrinking(self) -> None: + """最小の行サイズでも動的縮小の下限を下回らない(下限に張り付かせない).""" + assert DESCRIPTION_FONT_SIZE_LONGEST >= DESCRIPTION_MIN_FONT_SIZE + + def test_row_text_defaults_to_the_base_size(self) -> None: + """`ColorRowText` の既定は基準サイズ(指定しない行は 40 px).""" + assert ColorRowText("pink", "淡紅", "正常").description_font_size == DESCRIPTION_FONT_SIZE + + def test_result_row_defaults_to_the_base_size(self) -> None: + row = ResultRow( + key="pink", + name="淡紅", + description="正常", + swatch=COLOR_SWATCHES["pink"], + highlighted=False, + score_text=None, + ) + assert row.description_font_size == DESCRIPTION_FONT_SIZE + + +class TestLanguageDescriptionFontSizes: + """言語リソース(`LANGUAGE_TEXTS`)が持つ行ごとのサイズ.""" + + def test_japanese_rows_all_use_the_base_size(self) -> None: + """日本語は文字数が少なく全行 40 px で収まる.""" + for row in texts_for(Language.JAPANESE).rows: + assert row.description_font_size == DESCRIPTION_FONT_SIZE, row.key + + def test_english_rows_match_the_measured_sizes(self) -> None: + actual = {row.key: row.description_font_size for row in texts_for(Language.ENGLISH).rows} + assert actual == EXPECTED_ENGLISH_DESCRIPTION_FONT_SIZES + + @pytest.mark.parametrize("language", list(Language)) + def test_sizes_are_within_the_allowed_range(self, language: Language) -> None: + """どの行も下限〜基準の範囲に収まる(基準より大きくしない).""" + for row in texts_for(language).rows: + assert DESCRIPTION_MIN_FONT_SIZE <= row.description_font_size <= DESCRIPTION_FONT_SIZE + + @pytest.mark.parametrize("language", list(Language)) + def test_longer_descriptions_are_never_larger(self, language: Language) -> None: + """説明が長い行のサイズが短い行より大きいことはない(サイズ指定の取り違え検出).""" + rows = sorted(texts_for(language).rows, key=lambda row: len(row.description)) + sizes = [row.description_font_size for row in rows] + assert sizes == sorted(sizes, reverse=True) + + # --- フォント設定(_set_font / _fit_font) --- @@ -519,6 +602,102 @@ assert texts_for(language) is LANGUAGE_TEXTS[language] +class LanguageStateStub: + """`ResultWindow` の言語切替ロジックだけを取り出したスタブ. + + `ResultWindow` を構築すると QWidget を作ることになる(testing.md「GUI の自動テストは + 書かない」)ため,実メソッドを未束縛のまま借りて,状態遷移と再描画回数だけを検証する. + """ + + set_language = ResultWindow.set_language + toggle_language = ResultWindow.toggle_language + + def __init__(self, language: Language) -> None: + self._language = language + self.redraw_count = 0 + + def _apply_language(self) -> None: + """`ResultWindow._apply_language`(タイトルとパネルの描き直し)の代役.""" + self.redraw_count += 1 + + +class TestSetLanguage: + """`ResultWindow.set_language`(メイン画面の言語切替ボタンから呼ばれる公開 API).""" + + @pytest.mark.parametrize("language", list(Language)) + def test_changing_the_language_redraws_once(self, language: Language) -> None: + window = LanguageStateStub(language) + target = next_language(language) + + window.set_language(target) + + assert window._language is target + assert window.redraw_count == 1 + + @pytest.mark.parametrize("language", list(Language)) + def test_same_language_is_a_no_op(self, language: Language) -> None: + """同じ言語を指定しても描き直さない(無駄な再描画を避ける).""" + window = LanguageStateStub(language) + + window.set_language(language) + + assert window._language is language + assert window.redraw_count == 0 + + @pytest.mark.parametrize("language", list(Language)) + def test_repeated_calls_with_the_same_language_redraw_once(self, language: Language) -> None: + window = LanguageStateStub(language) + target = next_language(language) + + window.set_language(target) + window.set_language(target) + + assert window.redraw_count == 1 + + @pytest.mark.parametrize("language", list(Language)) + def test_toggle_language_switches_to_the_other_language(self, language: Language) -> None: + """`toggle_language` は `set_language(next_language(...))` の薄いラッパ.""" + window = LanguageStateStub(language) + + window.toggle_language() + + assert window._language is next_language(language) + assert window.redraw_count == 1 + + @pytest.mark.parametrize("language", list(Language)) + def test_toggling_twice_restores_the_original_language(self, language: Language) -> None: + window = LanguageStateStub(language) + + window.toggle_language() + window.toggle_language() + + assert window._language is language + assert window.redraw_count == 2 + + +class TestResultWindowIsDisplayOnly: + """結果画面(患者側モニタ)は操作 UI を持たない.""" + + def test_exposes_set_language_as_public_api(self) -> None: + assert callable(ResultWindow.set_language) + assert callable(ResultWindow.toggle_language) + assert isinstance(ResultWindow.language, property) + + def test_module_does_not_use_push_buttons(self) -> None: + """言語切替・閉じるボタンはメイン画面側にあるため,ボタンを import しない.""" + import tiasjudge.gui.result_window as result_window + + assert not hasattr(result_window, "QPushButton") + + def test_source_creates_no_buttons(self) -> None: + """描画コードにボタン生成が残っていない(表示専用の回帰固定).""" + import inspect + + import tiasjudge.gui.result_window as result_window + + assert "QPushButton(" not in inspect.getsource(result_window) + + # --- is_highlighted / format_score / row_baseline --- @@ -617,6 +796,46 @@ for row in build_result_rows(sample_result, language): assert (row.name, row.description) == expected[row.key] + @pytest.mark.parametrize("language", list(Language)) + def test_copies_the_description_font_size_of_each_row( + self, sample_result: DiagnosisResult, language: Language + ) -> None: + """行ごとの説明文サイズを言語リソースからそのまま転写する.""" + expected = {row.key: row.description_font_size for row in texts_for(language).rows} + for row in build_result_rows(sample_result, language): + assert row.description_font_size == expected[row.key], row.key + + def test_english_description_font_sizes_are_fixed(self, sample_result: DiagnosisResult) -> None: + """英語は長い 3 行だけ小さい(40/36/36/40/30 の回帰固定).""" + rows = build_result_rows(sample_result, Language.ENGLISH) + actual = {row.key: row.description_font_size for row in rows} + assert actual == EXPECTED_ENGLISH_DESCRIPTION_FONT_SIZES + + def test_japanese_description_font_sizes_are_uniform( + self, sample_result: DiagnosisResult + ) -> None: + rows = build_result_rows(sample_result, Language.JAPANESE) + assert {row.description_font_size for row in rows} == {DESCRIPTION_FONT_SIZE} + + @pytest.mark.parametrize("language", list(Language)) + @pytest.mark.parametrize( + "top_labels", + [("pink", "purple"), ("pale", "crimson"), ("dark_red", "pale"), ("crimson", "dark_red")], + ) + def test_description_font_size_does_not_depend_on_highlighting( + self, language: Language, top_labels: tuple[str, str] + ) -> None: + """強調(上位 2 色)の有無でサイズが変わらない. + + 上位 2 色が変わるたびに文字サイズが変わると,診断のたびに見た目がちらつくため, + サイズは行ごとに固定する(実装コメント `DESCRIPTION_FONT_SIZE_LONG` 参照). + """ + result = make_result(SAMPLE_PERCENTAGES, top_labels) + rows = build_result_rows(result, language) + expected = {row.key: row.description_font_size for row in texts_for(language).rows} + for row in rows: + assert row.description_font_size == expected[row.key], (row.key, row.highlighted) + def test_swatch_matches_the_color_table(self, sample_result: DiagnosisResult) -> None: for row in build_result_rows(sample_result, Language.ENGLISH): assert row.swatch == COLOR_SWATCHES[row.key] @@ -787,6 +1006,37 @@ assert not pixmap.isNull() assert (pixmap.width(), pixmap.height()) == (PANEL_SIZE, PANEL_SIZE) + def test_uses_the_per_row_description_font_size( + self, gui_app: QApplication, sample_result: DiagnosisResult + ) -> None: + """`ResultRow.description_font_size` が描画に反映される(固定値を無視しない). + + ※ 文字の見た目ではなく「サイズを変えると絵が変わる」ことだけを見る. + フォントが無く幅が測れない環境では検証できないため読み飛ばす. + """ + texts = texts_for(Language.ENGLISH) + rows = build_result_rows(sample_result, Language.ENGLISH) + with painter_on_panel() as painter: + _set_font(painter, DESCRIPTION_FONT_SIZE) + if painter.fontMetrics().horizontalAdvance(rows[0].description) == 0: + pytest.skip("フォントメトリクスが取得できない環境") + + base = render_result_panel(rows, texts, failed=False).toImage() + shrunk_rows = tuple( + replace(row, description_font_size=DESCRIPTION_MIN_FONT_SIZE) for row in rows + ) + shrunk = render_result_panel(shrunk_rows, texts, failed=False).toImage() + assert base != shrunk + + def test_renders_every_row_font_size_without_shrinking_out_of_range( + self, gui_app: QApplication, sample_result: DiagnosisResult + ) -> None: + """行ごとのサイズを指定しても描画が成立する(強調行・非強調行の両方).""" + for language in Language: + rows = build_result_rows(sample_result, language) + pixmap = render_result_panel(rows, texts_for(language), failed=False) + assert not pixmap.isNull() + def test_renders_with_empty_texts(self, gui_app: QApplication) -> None: """空文字でも描画が成立する(縁取りのパス生成が空になるケース).""" rows = tuple( diff --git a/tests/test_main.py b/tests/test_main.py index a4c09ab..97852f7 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -6,6 +6,8 @@ - `--check`: 画面を構築して即終了する(既存の起動確認は `test_package_skeleton.py`) - `--preview-result`: 結果画面だけをダミー結果で開く - `--failed`: `--preview-result` と併用し,診断失敗パターン(旧実装の失敗時スコア組)で開く +- `--english`: `--preview-result` と併用し,英語表示で開く(結果画面は表示専用になり + 画面内で言語を切り替えられないため,プレビューの言語はコマンドラインで指定する) ※ testing.md の方針により GUI(QWidget)そのものの自動テストは書かない.ここで画面を 生成するのは `--check` による**起動可能性の確認**に限る(Qt は conftest.py で offscreen). @@ -38,13 +40,16 @@ # --- 引数解析 --- +OPTION_FLAGS = ("check", "preview_result", "failed", "english") + + class TestArgumentParser: - """`--check` / `--preview-result` / `--failed` の 3 オプション(ENV_04).""" + """`--check` / `--preview-result` / `--failed` / `--english` の 4 オプション(ENV_04).""" def test_no_options_by_default(self) -> None: arguments = _build_parser().parse_args([]) - flags = (arguments.check, arguments.preview_result, arguments.failed) - assert flags == (False, False, False) + flags = tuple(getattr(arguments, name) for name in OPTION_FLAGS) + assert flags == (False, False, False, False) @pytest.mark.parametrize( ("argv", "expected"), @@ -53,11 +58,20 @@ (["--preview-result"], ("preview_result",)), (["--preview-result", "--failed"], ("preview_result", "failed")), (["--failed", "--preview-result", "--check"], ("preview_result", "failed", "check")), + (["--preview-result", "--english"], ("preview_result", "english")), + ( + ["--preview-result", "--english", "--check"], + ("preview_result", "english", "check"), + ), + ( + ["--preview-result", "--english", "--failed"], + ("preview_result", "english", "failed"), + ), ], ) def test_flags_are_parsed(self, argv: list[str], expected: tuple[str, ...]) -> None: arguments = _build_parser().parse_args(argv) - for name in ("check", "preview_result", "failed"): + for name in OPTION_FLAGS: assert getattr(arguments, name) is (name in expected), name def test_unknown_option_is_rejected(self) -> None: @@ -66,7 +80,7 @@ _build_parser().parse_args(["--unknown"]) assert error.value.code == 2 - @pytest.mark.parametrize("option", ["--check", "--preview-result", "--failed"]) + @pytest.mark.parametrize("option", ["--check", "--preview-result", "--failed", "--english"]) def test_help_lists_the_option(self, option: str) -> None: assert option in _build_parser().format_help() @@ -181,6 +195,8 @@ ["--check"], ["--preview-result", "--check"], ["--preview-result", "--failed", "--check"], + ["--preview-result", "--english", "--check"], + ["--preview-result", "--english", "--failed", "--check"], ], ) def test_check_exits_successfully(self, argv: list[str]) -> None: @@ -196,11 +212,23 @@ monkeypatch.setattr(main_window, "MainWindow", fail) assert main(["--preview-result", "--check"]) == 0 + @pytest.mark.parametrize( + ("options", "expected_language"), + [([], Language.JAPANESE), (["--english"], Language.ENGLISH)], + ids=["default_japanese", "english_option"], + ) @pytest.mark.parametrize("failed", [False, True]) - def test_preview_opens_the_result_window_in_japanese( - self, monkeypatch: pytest.MonkeyPatch, failed: bool + def test_preview_opens_the_result_window_with_the_requested_language( + self, + monkeypatch: pytest.MonkeyPatch, + failed: bool, + options: list[str], + expected_language: Language, ) -> None: - """結果画面にダミー結果・ダミー画像・日本語が渡される(ENV_04「デバッグ用」).""" + """結果画面にダミー結果・ダミー画像・指定言語が渡される(ENV_04「デバッグ用」). + + 既定は日本語,`--english` を付けると英語で開く(`--failed` との併用も可). + """ import tiasjudge.gui.result_window as result_window captured: dict[str, object] = {} @@ -213,10 +241,10 @@ captured["closed"] = True monkeypatch.setattr(result_window, "ResultWindow", FakeResultWindow) - argv = ["--preview-result", "--check"] + (["--failed"] if failed else []) + argv = ["--preview-result", "--check", *options] + (["--failed"] if failed else []) assert main(argv) == 0 - assert captured["language"] is Language.JAPANESE + assert captured["language"] is expected_language assert captured["closed"] is True result = captured["result"] assert result.failed is failed # type: ignore[union-attr] @@ -338,3 +366,24 @@ assert _run_preview(app, failed=True, check=False) == 3 # type: ignore[arg-type] assert fake_result_window["language"] is Language.JAPANESE assert fake_result_window["result"].failed is True # type: ignore[union-attr] + + def test_language_defaults_to_japanese(self, fake_result_window: dict[str, object]) -> None: + """`english` を省略した呼び出しは日本語(既定値).""" + app = TestRunPreview.FakeApp([object()]) + assert _run_preview(app, failed=False, check=True) == 0 # type: ignore[arg-type] + assert fake_result_window["language"] is Language.JAPANESE + + @pytest.mark.parametrize( + ("english", "expected_language"), + [(False, Language.JAPANESE), (True, Language.ENGLISH)], + ) + def test_english_option_selects_the_language( + self, + fake_result_window: dict[str, object], + english: bool, + expected_language: Language, + ) -> None: + """結果画面は表示専用のため,プレビューの言語は `--english` で決まる.""" + app = TestRunPreview.FakeApp([object()]) + assert _run_preview(app, failed=False, english=english, check=True) == 0 # type: ignore[arg-type] + assert fake_result_window["language"] is expected_language