380 lines
16 KiB
Python
380 lines
16 KiB
Python
"""Три вкладки страницы профиля: стратегии, список сайтов и текст профиля.
|
||
|
||
Каждая вкладка про одно. Вкладку стратегий целиком занимает их список,
|
||
свои записи списка стоят раньше встроенных, а на вкладке текста нет ничего,
|
||
кроме редактора.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import unittest
|
||
from types import SimpleNamespace
|
||
from unittest.mock import Mock
|
||
|
||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||
|
||
from PyQt6.QtWidgets import QApplication, QPlainTextEdit
|
||
|
||
from profile.state import ProfileListFileEditorState
|
||
from profile.strategy_state import ProfileStrategyState
|
||
from profile.ui.profile_list_file_tab import SIDE_BY_SIDE_MIN_WIDTH, ProfileListFileTab
|
||
from profile.ui.profile_raw_text_tab import ProfileRawTextTab
|
||
from profile.ui.profile_setup_page import ProfileSetupPageBase
|
||
from ui.navigation.history import ScreenState
|
||
|
||
|
||
def _worker_stub(*_args, **_kwargs):
|
||
return None
|
||
|
||
|
||
class _TabsCase(unittest.TestCase):
|
||
@classmethod
|
||
def setUpClass(cls) -> None:
|
||
cls.app = QApplication.instance() or QApplication([])
|
||
|
||
def tearDown(self) -> None:
|
||
self.app.closeAllWindows()
|
||
self.app.processEvents()
|
||
|
||
def _page(self) -> ProfileSetupPageBase:
|
||
page = ProfileSetupPageBase(
|
||
create_profile_setup_load_worker=_worker_stub,
|
||
create_profile_list_file_load_worker=_worker_stub,
|
||
create_profile_list_file_save_worker=_worker_stub,
|
||
create_profile_list_file_validation_worker=_worker_stub,
|
||
create_profile_settings_save_worker=_worker_stub,
|
||
create_profile_raw_text_save_worker=_worker_stub,
|
||
create_profile_enabled_save_worker=_worker_stub,
|
||
create_profile_user_update_worker=_worker_stub,
|
||
create_profile_user_delete_worker=_worker_stub,
|
||
create_profile_strategy_apply_worker=_worker_stub,
|
||
create_profile_strategy_feedback_save_worker=_worker_stub,
|
||
open_profiles=lambda: None,
|
||
open_root=lambda: None,
|
||
on_profile_changed=lambda: None,
|
||
)
|
||
self.addCleanup(page.deleteLater)
|
||
return page
|
||
|
||
def _payload(self, *, rating: str = "", favorite: bool = False, strategy_id: str = "tls_fake"):
|
||
return SimpleNamespace(
|
||
item=SimpleNamespace(in_preset=True, enabled=True, strategy_id=strategy_id, strategy_name="General ALT2 1.9.9"),
|
||
current_strategy_state=ProfileStrategyState(rating=rating, favorite=favorite),
|
||
)
|
||
|
||
|
||
class StrategiesTabTests(_TabsCase):
|
||
def test_list_takes_the_whole_tab_and_other_tabs_stay_unbuilt(self) -> None:
|
||
page = self._page()
|
||
|
||
# Под списком нет кнопок: оценка и избранное живут в меню стратегии.
|
||
self.assertIs(page._strategy_stack.widget(0), page._strategy_list)
|
||
self.assertFalse(hasattr(page, "_strategy_feedback_bar"))
|
||
self.assertFalse(page._raw_tab_built)
|
||
self.assertFalse(page._editor_tab_built)
|
||
|
||
def test_rating_from_the_menu_is_saved_for_the_clicked_strategy(self) -> None:
|
||
page = self._page()
|
||
page._payload = self._payload(strategy_id="tls_fake")
|
||
page._profile_key = "profile:0"
|
||
page._loading = False
|
||
page._request_strategy_feedback_save = Mock()
|
||
|
||
# Оценили не ту стратегию, что выбрана для профиля.
|
||
page._strategy_list.strategy_rating_requested.emit("multisplit_1", "notwork")
|
||
page._strategy_list.strategy_favorite_requested.emit("multisplit_1", True)
|
||
|
||
self.assertEqual(
|
||
page._request_strategy_feedback_save.call_args_list[0].args,
|
||
({"strategy_id": "multisplit_1", "rating": "notwork", "favorite": None},),
|
||
)
|
||
self.assertEqual(
|
||
page._request_strategy_feedback_save.call_args_list[1].args,
|
||
({"strategy_id": "multisplit_1", "rating": None, "favorite": True},),
|
||
)
|
||
|
||
def test_nothing_is_saved_while_the_profile_is_loading(self) -> None:
|
||
page = self._page()
|
||
page._payload = self._payload()
|
||
page._profile_key = "profile:0"
|
||
page._loading = True
|
||
page._request_strategy_feedback_save = Mock()
|
||
|
||
page._strategy_list.strategy_rating_requested.emit("tls_fake", "work")
|
||
|
||
page._request_strategy_feedback_save.assert_not_called()
|
||
|
||
|
||
class ListFileTabTests(_TabsCase):
|
||
def _tab(self, width: int) -> ProfileListFileTab:
|
||
tab = ProfileListFileTab()
|
||
self.addCleanup(tab.deleteLater)
|
||
tab.resize(width, 600)
|
||
tab.show()
|
||
self.app.processEvents()
|
||
return tab
|
||
|
||
def test_wide_tab_puts_user_entries_and_base_side_by_side(self) -> None:
|
||
tab = self._tab(SIDE_BY_SIDE_MIN_WIDTH + 200)
|
||
|
||
self.assertTrue(tab.side_by_side())
|
||
self.assertEqual(tab.user_pane.geometry().top(), tab.base_pane.geometry().top())
|
||
self.assertLess(tab.user_pane.geometry().left(), tab.base_pane.geometry().left())
|
||
# Своим записям отдано больше места, чем базе.
|
||
self.assertGreater(tab.user_pane.width(), tab.base_pane.width())
|
||
|
||
def test_narrow_tab_stacks_them_with_user_entries_first(self) -> None:
|
||
tab = self._tab(SIDE_BY_SIDE_MIN_WIDTH - 200)
|
||
|
||
self.assertFalse(tab.side_by_side())
|
||
self.assertLess(tab.user_pane.geometry().top(), tab.base_pane.geometry().top())
|
||
|
||
def test_layout_follows_the_width(self) -> None:
|
||
tab = self._tab(SIDE_BY_SIDE_MIN_WIDTH - 200)
|
||
|
||
tab.resize(SIDE_BY_SIDE_MIN_WIDTH + 200, 600)
|
||
self.app.processEvents()
|
||
|
||
self.assertTrue(tab.side_by_side())
|
||
|
||
def test_only_user_entries_can_be_edited(self) -> None:
|
||
tab = self._tab(900)
|
||
|
||
self.assertFalse(tab.user_text.isReadOnly())
|
||
self.assertTrue(tab.base_text.isReadOnly())
|
||
|
||
def test_page_names_both_fields_by_their_files(self) -> None:
|
||
page = self._page()
|
||
page._ensure_editor_tab_built()
|
||
|
||
page._apply_list_file_editor_state(
|
||
ProfileListFileEditorState(
|
||
kind="hostlist",
|
||
display_path="lists/googlevideo.txt",
|
||
base_text="googlevideo.com",
|
||
user_text="example.com",
|
||
base_display_path="lists/base/googlevideo.txt",
|
||
user_display_path="lists/user/googlevideo.txt",
|
||
editable=True,
|
||
base_entries_count=1,
|
||
user_entries_count=1,
|
||
)
|
||
)
|
||
|
||
self.assertEqual(page._list_file_user_title.text(), "Ваши записи: lists/user/googlevideo.txt")
|
||
self.assertEqual(
|
||
page._list_file_base_title.text(),
|
||
"Встроенные записи (только просмотр): lists/base/googlevideo.txt",
|
||
)
|
||
self.assertFalse(page._list_file_base_pane.isHidden())
|
||
self.assertEqual(page._list_file_text.toPlainText(), "example.com")
|
||
# Отдельной строки с именем файла над полями нет: оно уже в подписях.
|
||
self.assertFalse(hasattr(page, "_list_file_title"))
|
||
|
||
def test_list_that_cannot_be_edited_is_shown_without_the_base_field(self) -> None:
|
||
page = self._page()
|
||
page._ensure_editor_tab_built()
|
||
|
||
page._apply_list_file_editor_state(
|
||
ProfileListFileEditorState(
|
||
kind="hostlist",
|
||
display_path="lists/service.txt",
|
||
text="service.example",
|
||
editable=False,
|
||
error_text="Это служебный список.",
|
||
)
|
||
)
|
||
|
||
self.assertTrue(page._list_file_base_pane.isHidden())
|
||
self.assertTrue(page._list_file_text.isReadOnly())
|
||
self.assertEqual(page._list_file_text.toPlainText(), "service.example")
|
||
|
||
|
||
class RawTextTabTests(_TabsCase):
|
||
def test_tab_holds_one_editor_and_the_save_button(self) -> None:
|
||
tab = ProfileRawTextTab()
|
||
self.addCleanup(tab.deleteLater)
|
||
|
||
self.assertEqual(tab.findChildren(QPlainTextEdit), [tab.text])
|
||
self.assertEqual(tab.save_button.text(), "Сохранить текст профиля")
|
||
# Редактор растягивается на всю высоту вкладки.
|
||
self.assertGreater(tab.text.maximumHeight(), 10000)
|
||
|
||
|
||
def test_page_fills_the_editor_from_the_profile(self) -> None:
|
||
page = self._page()
|
||
page._payload = SimpleNamespace(
|
||
item=SimpleNamespace(in_preset=True, enabled=True, strategy_id="tls_fake", strategy_name="TLS"),
|
||
raw_profile_text="--filter-tcp=443\n--lua-desync=fake",
|
||
preset_preamble_text="",
|
||
)
|
||
|
||
page._switch_strategy_tab(2)
|
||
|
||
self.assertTrue(page._raw_tab_built)
|
||
self.assertEqual(page._raw_profile_text.toPlainText(), "--filter-tcp=443\n--lua-desync=fake")
|
||
self.assertFalse(page._raw_profile_text.isReadOnly())
|
||
self.assertTrue(page._raw_profile_save_button.isEnabled())
|
||
self.assertEqual(page._strategy_stack.currentIndex(), 2)
|
||
|
||
|
||
class WindowHistoryScreenTests(_TabsCase):
|
||
"""Кнопка «назад» окна возвращает к тому же профилю, разделу и подробностям стратегии."""
|
||
|
||
def _loaded_page(self) -> ProfileSetupPageBase:
|
||
page = self._page()
|
||
page._profile_key = "uid:youtube"
|
||
page._payload = SimpleNamespace(item=SimpleNamespace(display_name="YouTube"))
|
||
entries = {
|
||
"fake-1": SimpleNamespace(name="Alpha v1", args="--lua-desync=fake:blob=x", visual=None, payload_scopes=(), old_name=""),
|
||
}
|
||
page._strategy_list.set_rows(entries=entries, states={}, current_strategy_id="fake-1", open_group_token="uid:youtube")
|
||
page._rebuild_breadcrumb()
|
||
return page
|
||
|
||
def test_screen_names_profile_section_and_strategy_details(self) -> None:
|
||
page = self._loaded_page()
|
||
changes = []
|
||
page.navigation_screen_changed.connect(lambda: changes.append(page.navigation_screen().key))
|
||
|
||
self.assertEqual(page.navigation_screen().key, "uid:youtube|strategies|")
|
||
self.assertEqual(page.navigation_screen().title, "YouTube")
|
||
|
||
page._strategy_list.show_details("fake-1")
|
||
self.assertEqual(page.navigation_screen().key, "uid:youtube|strategies|fake-1")
|
||
self.assertEqual(page.navigation_screen().title, "YouTube → Alpha v1")
|
||
|
||
page._open_section("raw")
|
||
self.assertEqual(page.navigation_screen().key, "uid:youtube|raw|")
|
||
self.assertEqual(page.navigation_screen().title, "YouTube → Текст профиля")
|
||
self.assertEqual(changes[-1], "uid:youtube|raw|")
|
||
self.assertIn("uid:youtube|strategies|fake-1", changes)
|
||
|
||
def test_restore_reopens_strategy_details_and_section(self) -> None:
|
||
page = self._loaded_page()
|
||
page.show_profile = Mock()
|
||
page._strategy_list.show_details("fake-1")
|
||
details = page.navigation_screen()
|
||
page._open_section("raw")
|
||
raw = page.navigation_screen()
|
||
page._open_section("strategies")
|
||
|
||
self.assertTrue(page.restore_navigation_screen(details))
|
||
page.show_profile.assert_called_with("uid:youtube")
|
||
self.assertTrue(page._strategy_list.details_open())
|
||
self.assertEqual(page.navigation_screen().key, details.key)
|
||
|
||
self.assertTrue(page.restore_navigation_screen(raw))
|
||
self.assertEqual(page._strategy_stack.currentIndex(), 2)
|
||
self.assertFalse(page._strategy_list.details_open())
|
||
self.assertEqual(page.navigation_screen().key, raw.key)
|
||
|
||
def test_restore_of_another_profile_waits_for_its_data(self) -> None:
|
||
page = self._loaded_page()
|
||
page._strategy_list.show_details("fake-1")
|
||
details = page.navigation_screen()
|
||
page._strategy_list.close_details()
|
||
|
||
# Человек ушёл в другой профиль, а «назад» ведёт к подробностям стратегии прежнего.
|
||
page._profile_key = "uid:discord"
|
||
|
||
def load_other_profile(profile_key: str) -> None:
|
||
page._profile_key = profile_key
|
||
page._payload = None
|
||
|
||
page.show_profile = load_other_profile
|
||
|
||
self.assertTrue(page.restore_navigation_screen(details))
|
||
# Данные ещё не пришли, но экран уже считается тем самым: журнал не выбросит запись.
|
||
self.assertEqual(page.navigation_screen().key, details.key)
|
||
self.assertFalse(page._strategy_list.details_open())
|
||
|
||
page._payload = SimpleNamespace(item=SimpleNamespace(display_name="YouTube"))
|
||
page._apply_navigation_screen(*page._pending_navigation_screen)
|
||
|
||
self.assertTrue(page._strategy_list.details_open())
|
||
self.assertIsNone(page._pending_navigation_screen)
|
||
self.assertEqual(page.navigation_screen().key, details.key)
|
||
|
||
def test_new_reference_of_the_same_profile_renames_its_screens(self) -> None:
|
||
page = self._loaded_page()
|
||
page._open_section("raw")
|
||
before = page.navigation_screen()
|
||
renamed = []
|
||
page.navigation_screens_renamed.connect(lambda old, new: renamed.append((old, new)))
|
||
|
||
# Правка имени или условий выдала профилю новую ссылку.
|
||
page._profile_key = "uid:youtube-2"
|
||
|
||
self.assertEqual(renamed, [("uid:youtube", "uid:youtube-2")])
|
||
after = page.rename_navigation_screen(before, "uid:youtube", "uid:youtube-2")
|
||
self.assertEqual(after, page.navigation_screen())
|
||
self.assertEqual(after.payload, ("uid:youtube-2", "raw", ""))
|
||
# Экран другого профиля переименование не трогает.
|
||
other = ScreenState(key="uid:discord|raw|", payload=("uid:discord", "raw", ""))
|
||
self.assertIs(page.rename_navigation_screen(other, "uid:youtube", "uid:youtube-2"), other)
|
||
|
||
def test_opening_another_profile_is_not_a_rename(self) -> None:
|
||
page = self._loaded_page()
|
||
renamed = []
|
||
page.navigation_screens_renamed.connect(lambda old, new: renamed.append((old, new)))
|
||
page.reload_current_profile = Mock()
|
||
|
||
page.show_profile("uid:discord")
|
||
|
||
self.assertEqual(page._profile_key, "uid:discord")
|
||
self.assertEqual(renamed, [])
|
||
|
||
def test_opening_another_profile_is_a_step_and_its_data_is_a_correction(self) -> None:
|
||
page = self._loaded_page()
|
||
steps, corrections = [], []
|
||
page.navigation_screen_changed.connect(lambda: steps.append(page.navigation_screen().key))
|
||
page.navigation_screen_corrected.connect(lambda: corrections.append(page.navigation_screen().key))
|
||
page.reload_current_profile = Mock()
|
||
|
||
page.show_profile("uid:discord")
|
||
self.assertEqual(steps, ["uid:discord|strategies|"])
|
||
|
||
# Пришли данные профиля, и страница перестроилась: это не шаг человека.
|
||
page._loading = True
|
||
page._rebuild_breadcrumb()
|
||
page._loading = False
|
||
self.assertEqual(steps, ["uid:discord|strategies|"])
|
||
self.assertEqual(corrections, ["uid:discord|strategies|"])
|
||
|
||
def test_another_profile_opens_on_strategies_not_on_the_previous_section(self) -> None:
|
||
page = self._loaded_page()
|
||
page.reload_current_profile = Mock()
|
||
page._open_section("raw")
|
||
|
||
page.show_profile("uid:discord")
|
||
|
||
self.assertEqual(page._current_section(), "strategies")
|
||
self.assertEqual(page.navigation_screen().key, "uid:discord|strategies|")
|
||
|
||
def test_missing_profile_leaves_the_window_history(self) -> None:
|
||
from profile.ui.profile_setup_payload_controller import ProfileSetupPayloadController
|
||
|
||
page = self._loaded_page()
|
||
page._pending_navigation_screen = ("raw", "")
|
||
gone = []
|
||
page.navigation_screen_unavailable.connect(lambda: gone.append(True))
|
||
|
||
ProfileSetupPayloadController(page)._report_profile_missing()
|
||
|
||
self.assertEqual(gone, [True])
|
||
self.assertIsNone(page._pending_navigation_screen)
|
||
|
||
def test_user_choice_while_loading_cancels_the_pending_screen(self) -> None:
|
||
page = self._loaded_page()
|
||
page._pending_navigation_screen = ("strategies", "fake-1")
|
||
|
||
page._open_section("raw")
|
||
|
||
self.assertIsNone(page._pending_navigation_screen)
|
||
self.assertEqual(page.navigation_screen().key, "uid:youtube|raw|")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|