"""メイン画面の QWidget 非依存ロジックの検証(SPEC_01「メイン画面」・PLAN_02 ステップ 6).
仕様上の要件は以下(SPEC_01).
- 変換行列は `.csv`,画像は `.png` のみ受け付け,他拡張子はエラー表示
- 画像は読込時に中央基準で 1024×1280 にクロップする
- 画像上の左クリックで 51×51 px の ROI を追加(最大 4 点.5 点目はエラー),赤枠を描画
- 右クリックで全クリックを取り消す
- [舌色診断] は 1 点以上クリックで有効化し,診断(ROI 平均色 → Lab → SVM)を実行する
- 入力ガードの文言(変換行列未読込・画像未読込・クリック 0 点)
- 結果画面(患者側モニタ)は表示専用のため,言語切替と「結果画面を閉じる」はメイン画面が持つ
(言語は開いていない間もメイン画面が保持し,次に開く結果画面へ引き継ぐ)
※ testing.md の方針により GUI(QWidget)そのものの自動テストは書かない.
本ファイルは画面部品に依存しない関数・状態クラスのみを対象とする.
"""
from __future__ import annotations
from pathlib import Path
import cv2
import numpy as np
import pytest
from tiasjudge.core import roi
from tiasjudge.core.color import rgb_to_lab
from tiasjudge.core.csv_io import TrainingData, load_training_data
from tiasjudge.core.svm import COLOR_KEYS, diagnose
from tiasjudge.gui.main_window import (
CROP_HEIGHT,
CROP_WIDTH,
DISPLAY_DIVISOR,
IMAGE_EXTENSION,
MATRIX_EXTENSION,
MESSAGE_CLICK_REQUIRED,
MESSAGE_IMAGE_REQUIRED,
MESSAGE_TRANSFORM_REQUIRED,
ROI_RECT_COLOR,
ROI_RECT_THICKNESS,
TRAINING_DATA_ENV_VAR,
ImageLoadError,
InputGuardError,
MainWindow,
MainWindowState,
check_extension,
crop_center,
default_training_data_path,
draw_roi_markers,
is_roi_inside_image,
load_image_file,
run_diagnosis,
)
from tiasjudge.gui.result_window import Language, next_language, switch_button_label
# --- ヘルパー ---
def make_image(width: int, height: int, bgr: tuple[int, int, int] = (10, 20, 30)) -> np.ndarray:
"""指定サイズの単色 BGR 画像を作る."""
image = np.empty((height, width, 3), dtype=np.uint8)
image[:, :] = bgr
return image
def make_ramp_image(width: int, height: int) -> np.ndarray:
"""B = x 座標,G = y 座標(256 で巡回)の画像を作る.クロップ位置を解析的に検証できる."""
image = np.zeros((height, width, 3), dtype=np.uint8)
image[:, :, 0] = (np.arange(width) % 256).astype(np.uint8)[np.newaxis, :]
image[:, :, 1] = (np.arange(height) % 256).astype(np.uint8)[:, np.newaxis]
return image
def write_png(path: Path, image: np.ndarray) -> Path:
"""PNG を書き出す(非 ASCII パス対応のため imencode + write_bytes)."""
success, buffer = cv2.imencode(".png", image)
assert success
path.write_bytes(buffer.tobytes())
return path
def linear_coefficients() -> np.ndarray:
"""1 次項のみの合成変換行列(sRGB→XYZ 相当)を作る(SPEC_02 (1) の形式に合わせた 17×3)."""
coefficients = np.zeros((17, 3), dtype=np.float64)
coefficients[0] = (0.4124, 0.2126, 0.0193)
coefficients[1] = (0.3576, 0.7152, 0.1192)
coefficients[2] = (0.1805, 0.0722, 0.9505)
return coefficients * (100.0 / 255.0)
@pytest.fixture(scope="module")
def training_data(data_csv_path: Path) -> TrainingData:
"""同梱学習データ(`data/data.csv`)を読み込む."""
return load_training_data(data_csv_path)
# --- 定数(SPEC_01) ---
def test_crop_size_is_1024_by_1280() -> None:
"""クロップ後サイズは 1024×1280(SPEC_01「画像選択」)."""
assert (CROP_WIDTH, CROP_HEIGHT) == (1024, 1280)
def test_display_is_half_scale() -> None:
"""画像表示は 1/2 縮小(SPEC_01「画像表示」)."""
assert DISPLAY_DIVISOR == 2
def test_accepted_extensions() -> None:
"""変換行列は .csv,画像は .png(SPEC_01)."""
assert MATRIX_EXTENSION == ".csv"
assert IMAGE_EXTENSION == ".png"
def test_guard_messages_follow_spec() -> None:
"""入力ガードの文言は SPEC_01「入力ガード(エラー表示)」どおり."""
assert MESSAGE_TRANSFORM_REQUIRED == "変換行列を読み込んでください"
assert MESSAGE_IMAGE_REQUIRED == "画像を読み込んでください"
assert "1 点以上 4 点以下クリックしてください" in MESSAGE_CLICK_REQUIRED
# --- check_extension(SPEC_01「変換行列選択」「画像選択」) ---
class TestCheckExtension:
"""拡張子の検証."""
@pytest.mark.parametrize("name", ["matrix.csv", "MATRIX.CSV", "a.b.Csv"])
def test_accepts_expected_extension_case_insensitively(self, name: str) -> None:
check_extension(name, MATRIX_EXTENSION, "message")
@pytest.mark.parametrize("name", ["matrix.txt", "matrix.png", "matrix", "csv"])
def test_rejects_other_extensions(self, name: str) -> None:
with pytest.raises(InputGuardError, match="message"):
check_extension(name, MATRIX_EXTENSION, "message")
def test_accepts_path_object(self, tmp_path: Path) -> None:
check_extension(tmp_path / "画像.PNG", IMAGE_EXTENSION, "message")
# --- crop_center(SPEC_01「画像選択」) ---
class TestCropCenter:
"""中央基準 1024×1280 クロップ."""
def test_large_image_is_cropped_to_target_size(self) -> None:
cropped = crop_center(make_image(2000, 3000))
assert cropped.shape == (CROP_HEIGHT, CROP_WIDTH, 3)
def test_crop_is_taken_from_the_center(self) -> None:
"""切り出し位置が中央(左上オフセット = (幅-1024)//2, (高さ-1280)//2)であること."""
width, height = 2000, 3000
cropped = crop_center(make_ramp_image(width, height))
x_offset = (width - CROP_WIDTH) // 2
y_offset = (height - CROP_HEIGHT) // 2
assert int(cropped[0, 0, 0]) == x_offset % 256
assert int(cropped[0, 0, 1]) == y_offset % 256
assert int(cropped[-1, -1, 0]) == (x_offset + CROP_WIDTH - 1) % 256
assert int(cropped[-1, -1, 1]) == (y_offset + CROP_HEIGHT - 1) % 256
def test_exact_size_image_is_unchanged(self) -> None:
image = make_ramp_image(CROP_WIDTH, CROP_HEIGHT)
cropped = crop_center(image)
assert cropped.shape == (CROP_HEIGHT, CROP_WIDTH, 3)
assert np.array_equal(cropped, image)
@pytest.mark.parametrize(
("width", "height"),
[(500, 600), (2000, 600), (500, 3000), (1, 1)],
)
def test_smaller_image_is_kept_within_bounds(self, width: int, height: int) -> None:
"""目標より小さい辺はそのまま(はみ出さない)."""
cropped = crop_center(make_image(width, height))
assert cropped.shape == (min(height, CROP_HEIGHT), min(width, CROP_WIDTH), 3)
def test_returns_independent_copy(self) -> None:
"""クロップ結果を書き換えても元画像は変わらない(描画で元画像を壊さないため)."""
image = make_image(2000, 3000, (1, 2, 3))
cropped = crop_center(image)
cropped[:, :] = (255, 255, 255)
assert np.array_equal(image[0, 0], np.array([1, 2, 3], dtype=np.uint8))
# --- load_image_file ---
class TestLoadImageFile:
"""PNG 読込(クロップ込み・日本語パス対応・異常系)."""
def test_loads_and_crops_png(self, tmp_path: Path) -> None:
path = write_png(tmp_path / "tongue.png", make_ramp_image(2000, 3000))
image = load_image_file(path)
assert image.shape == (CROP_HEIGHT, CROP_WIDTH, 3)
def test_loads_png_with_japanese_path(self, tmp_path: Path) -> None:
"""非 ASCII パスでも読み込める(cv2.imread の制約を回避していること)."""
directory = tmp_path / "舌画像フォルダ"
directory.mkdir()
path = write_png(directory / "舌_01.png", make_image(1024, 1280, (5, 6, 7)))
image = load_image_file(path)
assert image.shape == (CROP_HEIGHT, CROP_WIDTH, 3)
assert tuple(int(value) for value in image[0, 0]) == (5, 6, 7)
def test_missing_file_raises_image_load_error(self, tmp_path: Path) -> None:
with pytest.raises(ImageLoadError):
load_image_file(tmp_path / "no_such_file.png")
def test_corrupt_file_raises_image_load_error(self, tmp_path: Path) -> None:
path = tmp_path / "broken.png"
path.write_bytes(b"this is not a png")
with pytest.raises(ImageLoadError):
load_image_file(path)
def test_empty_file_raises_image_load_error(self, tmp_path: Path) -> None:
path = tmp_path / "empty.png"
path.write_bytes(b"")
with pytest.raises(ImageLoadError):
load_image_file(path)
# --- draw_roi_markers(SPEC_01「ROI 指定」) ---
class TestDrawRoiMarkers:
"""クリック位置への赤枠描画."""
def test_does_not_modify_the_source_image(self) -> None:
image = make_image(200, 200, (0, 0, 0))
original = image.copy()
draw_roi_markers(image, [(100, 100)])
assert np.array_equal(image, original)
def test_no_points_leaves_image_as_is(self) -> None:
image = make_image(200, 200, (0, 0, 0))
assert np.array_equal(draw_roi_markers(image, []), image)
def test_rectangle_is_centered_on_the_click_point(self) -> None:
"""枠は (x±25, y±25) に描かれる(ROI 51×51 に対応)."""
image = make_image(200, 200, (0, 0, 0))
annotated = draw_roi_markers(image, [(100, 100)])
for corner in ((75, 75), (125, 75), (75, 125), (125, 125)):
x, y = corner
assert tuple(int(value) for value in annotated[y, x]) == ROI_RECT_COLOR
# 枠の内側(中心)は塗られない
assert tuple(int(value) for value in annotated[100, 100]) == (0, 0, 0)
assert roi.ROI_OFFSET == 25
def test_rectangle_color_and_thickness_follow_legacy(self) -> None:
"""枠色は BGR (0, 0, 200)・太さ 3(旧実装踏襲)."""
assert ROI_RECT_COLOR == (0, 0, 200)
assert ROI_RECT_THICKNESS == 3
image = make_image(200, 200, (0, 0, 0))
annotated = draw_roi_markers(image, [(100, 100)])
# 上辺は太さぶんの帯になる(正確なラスタライズは OpenCV の実装依存なので,
# 枠線上とその近傍が塗られ,離れた位置は塗られないことを確認する)
for y in (74, 75, 76):
assert tuple(int(v) for v in annotated[y, 100]) == ROI_RECT_COLOR
assert tuple(int(v) for v in annotated[70, 100]) == (0, 0, 0)
assert tuple(int(v) for v in annotated[80, 100]) == (0, 0, 0)
def test_draws_all_points(self) -> None:
image = make_image(300, 300, (0, 0, 0))
annotated = draw_roi_markers(image, [(50, 50), (200, 200)])
assert tuple(int(v) for v in annotated[25, 25]) == ROI_RECT_COLOR
assert tuple(int(v) for v in annotated[175, 175]) == ROI_RECT_COLOR
# --- is_roi_inside_image ---
class TestIsRoiInsideImage:
"""ROI が画像内に収まるかの境界判定(旧実装のクラッシュ箇所のガード)."""
@pytest.mark.parametrize("point", [(25, 25), (74, 74), (25, 74), (50, 50)])
def test_inside_points(self, point: tuple[int, int]) -> None:
assert is_roi_inside_image(make_image(100, 100), point) is True
@pytest.mark.parametrize("point", [(24, 25), (25, 24), (75, 74), (74, 75), (0, 0), (99, 99)])
def test_outside_points(self, point: tuple[int, int]) -> None:
assert is_roi_inside_image(make_image(100, 100), point) is False
def test_boundary_matches_core_roi_behaviour(self) -> None:
"""判定が True の点では `core.roi` が例外を出さず,False の点では出す."""
image = make_image(100, 100)
roi.roi_mean_bgr(image, (25, 25))
roi.roi_mean_bgr(image, (74, 74))
with pytest.raises(ValueError):
roi.roi_mean_bgr(image, (24, 25))
with pytest.raises(ValueError):
roi.roi_mean_bgr(image, (75, 74))
# --- MainWindowState(SPEC_01「入力ガード」「ROI 指定」「リセット」) ---
@pytest.fixture
def loaded_state() -> MainWindowState:
"""変換行列・画像を読み込み済みの状態を作る."""
state = MainWindowState()
state.set_transform(Path("matrix.csv"), linear_coefficients())
state.set_image(Path("tongue.png"), make_image(CROP_WIDTH, CROP_HEIGHT))
return state
class TestMainWindowStateGuards:
"""入力ガードの遷移."""
def test_initial_state_requires_transform_matrix(self) -> None:
state = MainWindowState()
with pytest.raises(InputGuardError, match=MESSAGE_TRANSFORM_REQUIRED):
state.ensure_clickable()
def test_click_before_image_requires_image(self) -> None:
state = MainWindowState()
state.set_transform(Path("matrix.csv"), linear_coefficients())
with pytest.raises(InputGuardError, match=MESSAGE_IMAGE_REQUIRED):
state.add_point((100, 100))
assert state.point_count == 0
def test_click_before_transform_reports_transform_first(self) -> None:
"""変換行列・画像の両方が未読込なら,先に変換行列を案内する(SPEC_01 の順序)."""
state = MainWindowState()
with pytest.raises(InputGuardError, match=MESSAGE_TRANSFORM_REQUIRED):
state.add_point((100, 100))
def test_diagnose_with_zero_clicks_is_rejected(self, loaded_state: MainWindowState) -> None:
assert loaded_state.can_diagnose is False
with pytest.raises(InputGuardError, match="1 点以上 4 点以下"):
loaded_state.ensure_diagnosable()
def test_diagnose_is_enabled_after_one_click(self, loaded_state: MainWindowState) -> None:
loaded_state.add_point((100, 100))
assert loaded_state.can_diagnose is True
loaded_state.ensure_diagnosable()
class TestMainWindowStatePoints:
"""クリック点の追加・上限・リセット."""
def test_accepts_up_to_four_points(self, loaded_state: MainWindowState) -> None:
points = [(100, 100), (200, 200), (300, 300), (400, 400)]
for point in points:
loaded_state.add_point(point)
assert loaded_state.points == points
assert loaded_state.point_count == roi.MAX_ROI_POINTS == 4
def test_fifth_point_is_rejected_and_not_counted(self, loaded_state: MainWindowState) -> None:
"""5 点目はエラー(SPEC_01).旧実装と異なりクリック数も増やさない."""
for index in range(roi.MAX_ROI_POINTS):
loaded_state.add_point((100 + index * 60, 100))
with pytest.raises(InputGuardError):
loaded_state.add_point((500, 500))
assert loaded_state.point_count == roi.MAX_ROI_POINTS
def test_point_whose_roi_leaves_the_image_is_rejected(
self, loaded_state: MainWindowState
) -> None:
with pytest.raises(InputGuardError):
loaded_state.add_point((0, 0))
assert loaded_state.point_count == 0
def test_clear_points_resets_all_clicks(self, loaded_state: MainWindowState) -> None:
"""右クリックのリセット(SPEC_01「リセット」).旧実装と異なり点数も 0 に戻る."""
loaded_state.add_point((100, 100))
loaded_state.add_point((200, 200))
loaded_state.clear_points()
assert loaded_state.point_count == 0
assert loaded_state.can_diagnose is False
# リセット後は再び 4 点までクリックできる
for index in range(roi.MAX_ROI_POINTS):
loaded_state.add_point((100 + index * 60, 100))
assert loaded_state.point_count == roi.MAX_ROI_POINTS
def test_loading_a_new_image_clears_points(self, loaded_state: MainWindowState) -> None:
loaded_state.add_point((100, 100))
loaded_state.set_image(Path("other.png"), make_image(CROP_WIDTH, CROP_HEIGHT))
assert loaded_state.point_count == 0
def test_clear_image_disables_clicking(self, loaded_state: MainWindowState) -> None:
"""[診断続行] 相当.画像とクリックが戻り,変換行列は保持される."""
loaded_state.add_point((100, 100))
loaded_state.clear_image()
assert loaded_state.has_image is False
assert loaded_state.has_transform is True
assert loaded_state.point_count == 0
with pytest.raises(InputGuardError, match=MESSAGE_IMAGE_REQUIRED):
loaded_state.ensure_clickable()
def test_clear_transform_requires_reloading_the_matrix(
self, loaded_state: MainWindowState
) -> None:
"""[診断続行] で「同じ変換行列を使わない」を選んだ場合."""
loaded_state.clear_transform()
assert loaded_state.has_transform is False
with pytest.raises(InputGuardError, match=MESSAGE_TRANSFORM_REQUIRED):
loaded_state.ensure_clickable()
# --- default_training_data_path ---
class TestDefaultTrainingDataPath:
"""学習データの既定パス解決(絶対パスのハードコード禁止)."""
def test_environment_variable_takes_precedence(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
override = tmp_path / "custom_data.csv"
monkeypatch.setenv(TRAINING_DATA_ENV_VAR, str(override))
assert default_training_data_path() == override
def test_falls_back_to_bundled_data_csv(
self, monkeypatch: pytest.MonkeyPatch, data_csv_path: Path
) -> None:
monkeypatch.delenv(TRAINING_DATA_ENV_VAR, raising=False)
resolved = default_training_data_path()
assert resolved.is_file()
assert resolved.resolve() == data_csv_path.resolve()
# --- run_diagnosis(SPEC_02「処理パイプライン」の結合) ---
class TestRunDiagnosis:
"""ROI 平均色 → Lab → SVM のパイプライン."""
def test_returns_diagnosis_result_for_a_single_point(self, training_data: TrainingData) -> None:
image = make_image(CROP_WIDTH, CROP_HEIGHT, (110, 110, 180)) # BGR
result = run_diagnosis(image, [(100, 100)], linear_coefficients(), training_data)
assert set(result.percentages) == set(COLOR_KEYS)
assert all(0 <= value <= 100 for value in result.percentages.values())
assert len(result.top_labels) == 2
assert result.lab == rgb_to_lab((180.0, 110.0, 110.0), linear_coefficients())
def test_matches_composing_core_functions(self, training_data: TrainingData) -> None:
"""`core` を直接組み合わせた場合と同じ結果になる(gui は計算を持たない)."""
image = make_ramp_image(CROP_WIDTH, CROP_HEIGHT)
points = [(100, 100), (300, 400)]
coefficients = linear_coefficients()
result = run_diagnosis(image, points, coefficients, training_data)
expected = diagnose(rgb_to_lab(roi.mean_rgb(image, points), coefficients), training_data)
assert result.lab == expected.lab
assert result.percentages == expected.percentages
assert result.top_labels == expected.top_labels
assert result.failed == expected.failed
def test_multiple_points_average_the_rois(self, training_data: TrainingData) -> None:
"""複数点は各 ROI 平均の平均を使う(SPEC_02 (0))."""
image = make_image(CROP_WIDTH, CROP_HEIGHT, (100, 100, 100))
image[0:200, 0:200] = (200, 200, 200)
points = [(100, 100), (600, 600)]
result = run_diagnosis(image, points, linear_coefficients(), training_data)
assert result.lab == rgb_to_lab((150.0, 150.0, 150.0), linear_coefficients())
def test_point_outside_the_image_raises_value_error(self, training_data: TrainingData) -> None:
"""GUI 側でガードされる想定だが,`core` が明示的な例外を出すこと."""
image = make_image(CROP_WIDTH, CROP_HEIGHT)
with pytest.raises(ValueError):
run_diagnosis(image, [(0, 0)], linear_coefficients(), training_data)
def test_no_points_raises_value_error(self, training_data: TrainingData) -> None:
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