async def test_workspace_button_rename_clicked(qtbot, workspace_fs, core_config, alice_user_info): switch_language(core_config, "en") roles = {alice_user_info.user_id: (WorkspaceRole.OWNER, alice_user_info)} w = WorkspaceButton( workspace_name="Workspace", workspace_fs=workspace_fs, users_roles=roles, is_mounted=True, files=[], ) qtbot.addWidget(w) with qtbot.waitSignal(w.rename_clicked, timeout=500) as blocker: qtbot.mouseClick(w.button_rename, QtCore.Qt.LeftButton) assert blocker.args == [w]
async def _gui_factory( event_bus=None, core_config=core_config, start_arg=None, skip_dialogs=True, throttle_job_no_wait=True, ): # Wait for the backend to run if necessary await running_backend_ready() # First start popup blocks the test # Check version and mountpoint are useless for most tests core_config = core_config.evolve( gui_check_version_at_startup=False, gui_first_launch=False, gui_last_version=parsec_version, mountpoint_enabled=True, gui_language="en", gui_show_confined=False, ) event_bus = event_bus or event_bus_factory() ParsecApp.connected_devices = set() # Language config rely on global var, must reset it for each test ! switch_language(core_config, "en") # Pass minimize_on_close to avoid having test blocked by the closing confirmation prompt main_w = testing_main_window_cls(job_scheduler, job_scheduler.close, event_bus, core_config, minimize_on_close=True) aqtbot.add_widget(main_w) main_w.show_window(skip_dialogs=skip_dialogs) main_w.show_top() windows.append(main_w) main_w.add_instance(start_arg) def right_main_window(): assert ParsecApp.get_main_window() is main_w # For some reasons, the main window from the previous test might # still be around. Simply wait for things to settle down until # our freshly created window is detected as the app main window. await aqtbot.wait_until(right_main_window) return main_w
def test_organization_validator(qtbot, core_config): switch_language(core_config, "en") le = ValidatedLineEdit() le.set_validator(validators.OrganizationIDValidator()) qtbot.add_widget(le) le.show() qtbot.keyClicks(le, "Reynholm") qtbot.wait_until(lambda: le.text() == "Reynholm") assert le.is_input_valid() assert le.property("validity") == QtGui.QValidator.Acceptable qtbot.keyClicks(le, " Industries") qtbot.wait_until(lambda: le.text() == "Reynholm Industries") assert not le.is_input_valid() assert le.property("validity") == QtGui.QValidator.Invalid
async def _gui_factory(event_bus=None, core_config=core_config, start_arg=None): # First start popup blocks the test # Check version and mountpoint are useless for most tests core_config = core_config.evolve( gui_check_version_at_startup=False, gui_first_launch=False, gui_last_version=parsec_version, mountpoint_enabled=True, gui_language="en", ) event_bus = event_bus or EventBus() # Language config rely on global var, must reset it for each test ! switch_language(core_config) def _create_main_window(): # Pass minimize_on_close to avoid having test blocked by the # closing confirmation prompt switch_language(core_config, "en") monkeypatch.setattr( "parsec.core.gui.main_window.list_available_devices", lambda *args, **kwargs: (["a"]), ) main_w = MainWindow(qt_thread_gateway._job_scheduler, event_bus, core_config, minimize_on_close=True) qtbot.add_widget(main_w) main_w.showMaximized() main_w.show_top() windows.append(main_w) main_w.add_instance(start_arg) def right_main_window(): assert ParsecApp.get_main_window() is main_w # For some reasons, the main window from the previous test might # still be around. Simply wait for things to settle down until # our freshly created window is detected as the app main window. qtbot.wait_until(right_main_window) return main_w return await qt_thread_gateway.send_action(_create_main_window)
def _create_main_window(): # Pass minimize_on_close to avoid having test blocked by the # closing confirmation prompt switch_language(core_config, "en") monkeypatch.setattr( "parsec.core.gui.main_window.list_available_devices", lambda *args, **kwargs: (["a"]), ) main_w = MainWindow(qt_thread_gateway._job_scheduler, event_bus, core_config, minimize_on_close=True) qtbot.add_widget(main_w) main_w.showMaximized() main_w.show_top() windows.append(main_w) main_w.add_instance(start_arg) return main_w
def test_email_validator(qtbot, core_config): switch_language(core_config, "en") le = ValidatedLineEdit() le.set_validator(validators.EmailValidator()) qtbot.addWidget(le) le.show() qtbot.keyClicks(le, "maurice") assert not le.is_input_valid() assert le.property("validity") == QtGui.QValidator.Intermediate qtbot.keyClicks(le, "*****@*****.**") assert le.is_input_valid() assert le.property("validity") == QtGui.QValidator.Acceptable qtbot.keyClicks(le, "#") assert not le.is_input_valid() assert le.property("validity") == QtGui.QValidator.Invalid
async def test_workspace_button_shared_by(qtbot, workspace_fs, core_config): switch_language(core_config, "en") w = WorkspaceButton( workspace_name="Workspace", workspace_fs=workspace_fs, is_shared=True, is_creator=False, files=[], ) qtbot.addWidget(w) w.show() assert w.widget_empty.isVisible() is True assert w.widget_files.isVisible() is False assert w.label_owner.isVisible() is False assert w.label_shared.isVisible() is True assert w.name == "Workspace" assert w.label_title.text() == "Workspace"
async def test_workspace_button(qtbot, workspace_fs, core_config): switch_language(core_config, "en") roles = {workspace_fs.device.user_id: WorkspaceRole.OWNER} w = WorkspaceButton( workspace_name="Workspace", workspace_fs=workspace_fs, users_roles=roles, is_mounted=True, files=[], ) qtbot.addWidget(w) w.show() assert w.widget_empty.isVisible() is True assert w.widget_files.isVisible() is False assert w.label_owner.isVisible() is True assert w.label_shared.isVisible() is False assert w.name == "Workspace" assert w.label_title.text().startswith("Workspace") assert w.label_title.toolTip() == "Workspace (private)"
def test_backend_action_addr_validator(qtbot, core_config, action_addr): switch_language(core_config, "en") le = ValidatedLineEdit() le.set_validator(validators.BackendActionAddrValidator()) qtbot.add_widget(le) le.show() qtbot.keyClicks(le, "http://host:1337") qtbot.wait_until(lambda: le.text() == "http://host:1337") assert not le.is_input_valid() assert le.property("validity") == QtGui.QValidator.Intermediate le.setText("") qtbot.wait_until(lambda: le.text() == "") assert not le.is_input_valid() assert le.property("validity") == QtGui.QValidator.Intermediate qtbot.keyClicks(le, action_addr) qtbot.wait_until(lambda: le.text() == action_addr) assert le.is_input_valid() assert le.property("validity") == QtGui.QValidator.Acceptable
async def test_workspace_button(qtbot, workspace_fs, core_config, alice_user_info): switch_language(core_config, "en") roles = {alice_user_info.user_id: (WorkspaceRole.OWNER, alice_user_info)} w = WorkspaceButton.create( workspace_name=EntryName("Workspace"), workspace_fs=workspace_fs, users_roles=roles, is_mounted=True, files=[], ) qtbot.add_widget(w) w.show() assert w.widget_empty.isVisible() is True assert w.widget_files.isVisible() is False assert w.label_owner.isVisible() is True assert w.label_shared.isVisible() is False assert w.name == EntryName("Workspace") assert w.label_title.text().startswith("Workspace") assert w.label_title.toolTip() == "Workspace (private)" assert w.label_role.text() == _("TEXT_WORKSPACE_ROLE_OWNER")
async def test_workspace_button_files(qtbot, workspace_fs, core_config): switch_language(core_config, "en") roles = {workspace_fs.device.user_id: WorkspaceRole.OWNER} w = WorkspaceButton( workspace_name="Workspace", workspace_fs=workspace_fs, users_roles=roles, is_mounted=True, files=["File1.txt", "File2.txt", "Dir1"], ) qtbot.addWidget(w) w.show() assert w.widget_empty.isVisible() is False assert w.widget_files.isVisible() is True assert w.label_owner.isVisible() is True assert w.label_shared.isVisible() is False assert w.name == "Workspace" assert w.file1_name.text() == "File1.txt" assert w.file2_name.text() == "File2.txt" assert w.file3_name.text() == "Dir1" assert w.file4_name.text() == ""
async def test_workspace_button_files(qtbot, workspace_fs, core_config): switch_language(core_config, "en") w = WorkspaceButton( workspace_name="Workspace", workspace_fs=workspace_fs, is_shared=True, is_creator=True, files=["File1.txt", "File2.txt", "Dir1"], ) qtbot.addWidget(w) w.show() assert w.widget_empty.isVisible() is False assert w.widget_files.isVisible() is True assert w.label_owner.isVisible() is True assert w.label_shared.isVisible() is True assert w.name == "Workspace" assert w.label_title.text() == "Workspace (shared wi..." assert w.label_title.toolTip() assert w.file1_name.text() == "File1.txt" assert w.file2_name.text() == "File2.txt" assert w.file3_name.text() == "Dir1" assert w.file4_name.text() == ""
def run_gui(config: CoreConfig, start_arg: str = None, diagnose: bool = False): logger.info("Starting UI") # Needed for High DPI usage of QIcons, otherwise only QImages are well scaled QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) app = QApplication(["-stylesheet"]) app.setOrganizationName("Scille") app.setOrganizationDomain("parsec.cloud") app.setApplicationName("Parsec") QFontDatabase.addApplicationFont(":/fonts/fonts/Roboto-Regular.ttf") f = QFont("Roboto") app.setFont(f) rc = QFile(":/styles/styles/main.css") rc.open(QFile.ReadOnly) content = rc.readAll().data() app.setStyleSheet(str(content, "utf-8")) lang_key = lang.switch_language(config) event_bus = EventBus() with run_trio_thread() as jobs_ctx: win = MainWindow( jobs_ctx=jobs_ctx, event_bus=event_bus, config=config, minimize_on_close=config.gui_tray_enabled and systray_available(), ) result_queue = Queue(maxsize=1) class ThreadSafeNoQtSignal(ThreadSafeQtSignal): def __init__(self): self.qobj = None self.signal_name = "" self.args_types = () def emit(self, *args): pass jobs_ctx.submit_job( ThreadSafeNoQtSignal(), ThreadSafeNoQtSignal(), _start_ipc_server, config, win, start_arg, result_queue, ) if result_queue.get() == "already_running": # Another instance of Parsec already started, nothing more to do return if systray_available(): systray = Systray(parent=win) win.systray_notification.connect(systray.on_systray_notification) systray.on_close.connect(win.close_app) systray.on_show.connect(win.show_top) app.aboutToQuit.connect(before_quit(systray)) if config.gui_tray_enabled: app.setQuitOnLastWindowClosed(False) if config.gui_check_version_at_startup and not diagnose: CheckNewVersion(jobs_ctx=jobs_ctx, event_bus=event_bus, config=config, parent=win) win.showMaximized(skip_dialogs=diagnose) win.show_top() win.new_instance_needed.emit(start_arg) def kill_window(*args): win.close_app(force=True) QApplication.quit() signal.signal(signal.SIGINT, kill_window) # QTimer wakes up the event loop periodically which allows us to close # the window even when it is in background. timer = QTimer() timer.start(1000 if diagnose else 400) timer.timeout.connect(kill_window if diagnose else lambda: None) if diagnose: diagnose_timer = QTimer() diagnose_timer.start(1000) diagnose_timer.timeout.connect(kill_window) if lang_key: event_bus.send("gui.config.changed", gui_language=lang_key) if diagnose: with fail_on_first_exception(kill_window): return app.exec_() else: return app.exec_()
def run_gui(config: CoreConfig, start_arg: str = None, diagnose: bool = False): logger.info("Starting UI") # Needed for High DPI usage of QIcons, otherwise only QImages are well scaled QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) QApplication.setHighDpiScaleFactorRoundingPolicy( Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) app = ParsecApp() app.load_stylesheet() app.load_font() lang_key = lang.switch_language(config) event_bus = EventBus() with run_trio_thread() as jobs_ctx: win = MainWindow( jobs_ctx=jobs_ctx, event_bus=event_bus, config=config, minimize_on_close=config.gui_tray_enabled and systray_available(), ) result_queue = Queue(maxsize=1) class ThreadSafeNoQtSignal(ThreadSafeQtSignal): def __init__(self): self.qobj = None self.signal_name = "" self.args_types = () def emit(self, *args): pass jobs_ctx.submit_job( ThreadSafeNoQtSignal(), ThreadSafeNoQtSignal(), _start_ipc_server, config, win, start_arg, result_queue, ) if result_queue.get() == "already_running": # Another instance of Parsec already started, nothing more to do return if systray_available(): systray = Systray(parent=win) win.systray_notification.connect(systray.on_systray_notification) systray.on_close.connect(win.close_app) systray.on_show.connect(win.show_top) app.aboutToQuit.connect(before_quit(systray)) if config.gui_tray_enabled: app.setQuitOnLastWindowClosed(False) if config.gui_check_version_at_startup and not diagnose: CheckNewVersion(jobs_ctx=jobs_ctx, event_bus=event_bus, config=config, parent=win) win.show_window(skip_dialogs=diagnose, invitation_link=start_arg) win.show_top() win.new_instance_needed.emit(start_arg) def kill_window(*args): win.close_app(force=True) QApplication.quit() signal.signal(signal.SIGINT, kill_window) # QTimer wakes up the event loop periodically which allows us to close # the window even when it is in background. timer = QTimer() timer.start(1000 if diagnose else 400) timer.timeout.connect(kill_window if diagnose else lambda: None) if diagnose: diagnose_timer = QTimer() diagnose_timer.start(1000) diagnose_timer.timeout.connect(kill_window) if lang_key: event_bus.send(CoreEvent.GUI_CONFIG_CHANGED, gui_language=lang_key) if diagnose: with fail_on_first_exception(kill_window): return app.exec_() else: with log_pyqt_exceptions(): return app.exec_()
def test_file_table_sort(qtbot, core_config): switch_language(core_config, "en") w = FileTable(parent=None) qtbot.add_widget(w) w.add_parent_workspace() w.add_folder(EntryName("Dir1"), EntryID.new(), True, False) w.add_file( EntryName("File1.txt"), EntryID.new(), 100, pendulum.datetime(2000, 1, 15), pendulum.datetime(2000, 1, 20), True, False, ) w.add_file( EntryName("AnotherFile.txt"), EntryID.new(), 80, pendulum.datetime(2000, 1, 10), pendulum.datetime(2000, 1, 25), True, False, ) assert w.rowCount() == 4 assert w.item(0, 1).text() == "Workspaces list" assert w.item(1, 1).text() == "Dir1" assert w.item(2, 1).text() == "File1.txt" assert w.item(3, 1).text() == "AnotherFile.txt" # Name w.sortByColumn(1, QtCore.Qt.AscendingOrder) assert w.item(0, 1).text() == "Workspaces list" assert w.item(1, 1).text() == "AnotherFile.txt" assert w.item(2, 1).text() == "Dir1" assert w.item(3, 1).text() == "File1.txt" w.sortByColumn(1, QtCore.Qt.DescendingOrder) assert w.item(0, 1).text() == "Workspaces list" assert w.item(1, 1).text() == "File1.txt" assert w.item(2, 1).text() == "Dir1" assert w.item(3, 1).text() == "AnotherFile.txt" # Created w.sortByColumn(2, QtCore.Qt.AscendingOrder) assert w.item(0, 1).text() == "Workspaces list" assert w.item(1, 1).text() == "Dir1" assert w.item(2, 1).text() == "AnotherFile.txt" assert w.item(3, 1).text() == "File1.txt" w.sortByColumn(2, QtCore.Qt.DescendingOrder) assert w.item(0, 1).text() == "Workspaces list" assert w.item(1, 1).text() == "File1.txt" assert w.item(2, 1).text() == "AnotherFile.txt" assert w.item(3, 1).text() == "Dir1" # Updated w.sortByColumn(3, QtCore.Qt.AscendingOrder) assert w.item(0, 1).text() == "Workspaces list" assert w.item(1, 1).text() == "Dir1" assert w.item(2, 1).text() == "File1.txt" assert w.item(3, 1).text() == "AnotherFile.txt" w.sortByColumn(3, QtCore.Qt.DescendingOrder) assert w.item(0, 1).text() == "Workspaces list" assert w.item(1, 1).text() == "AnotherFile.txt" assert w.item(2, 1).text() == "File1.txt" assert w.item(3, 1).text() == "Dir1" # Size w.sortByColumn(4, QtCore.Qt.AscendingOrder) assert w.item(0, 1).text() == "Workspaces list" assert w.item(1, 1).text() == "Dir1" assert w.item(2, 1).text() == "AnotherFile.txt" assert w.item(3, 1).text() == "File1.txt" w.sortByColumn(4, QtCore.Qt.DescendingOrder) assert w.item(0, 1).text() == "Workspaces list" assert w.item(1, 1).text() == "File1.txt" assert w.item(2, 1).text() == "AnotherFile.txt" assert w.item(3, 1).text() == "Dir1"
async def _run_gui(app: ParsecApp, config: CoreConfig, start_arg: str = None, diagnose: bool = False): app.load_stylesheet() app.load_font() lang_key = lang.switch_language(config) event_bus = EventBus() async with run_trio_job_scheduler() as jobs_ctx: win = MainWindow( jobs_ctx=jobs_ctx, quit_callback=jobs_ctx.close, event_bus=event_bus, config=config, minimize_on_close=config.gui_tray_enabled and systray_available(), ) # Attempt to run an IPC server if Parsec is not already started try: await jobs_ctx.nursery.start(_run_ipc_server, config, win, start_arg) # Another instance of Parsec already started, nothing more to do except IPCServerAlreadyRunning: return # If we are here, it's either the IPC server has successfully started # or it has crashed without being able to communicate with an existing # IPC server. Such case is of course not supposed to happen but if it # does we nevertheless keep the application running as a kind of # failsafe mode (and the crash reason is logged and sent to telemetry). # Systray is not displayed on MacOS, having natively a menu with similar functions. if systray_available() and sys.platform != "darwin": systray = Systray(parent=win) win.systray_notification.connect(systray.on_systray_notification) systray.on_close.connect(win.close_app) systray.on_show.connect(win.show_top) app.aboutToQuit.connect(before_quit(systray)) if config.gui_tray_enabled: app.setQuitOnLastWindowClosed(False) if config.gui_check_version_at_startup and not diagnose: CheckNewVersion(jobs_ctx=jobs_ctx, event_bus=event_bus, config=config, parent=win) win.show_window(skip_dialogs=diagnose) win.show_top() win.new_instance_needed.emit(start_arg) if sys.platform == "darwin": # macFUSE is not bundled with Parsec and must be manually installed by the user # so we detect early such need to provide a help dialogue ;-) # TODO: provide a similar mechanism on Windows&Linux to handle mountpoint runner not available from parsec.core.gui.instance_widget import ensure_macfuse_available_or_show_dialogue ensure_macfuse_available_or_show_dialogue(win) def kill_window(*args): win.close_app(force=True) signal.signal(signal.SIGINT, kill_window) # QTimer wakes up the event loop periodically which allows us to close # the window even when it is in background. timer = QTimer() timer.start(400) timer.timeout.connect(lambda: None) if diagnose: diagnose_timer = QTimer() diagnose_timer.start(1000) diagnose_timer.timeout.connect(kill_window) if lang_key: event_bus.send(CoreEvent.GUI_CONFIG_CHANGED, gui_language=lang_key) with QDialogInProcess.manage_pools(): if diagnose: with fail_on_first_exception(kill_window): await trio.sleep_forever() else: with log_pyqt_exceptions(): await trio.sleep_forever()