anki-connect/tests/test_edit.py
oakkitten 056e722187 Edit dialog: fix editor buttons on Anki 2.1.50
Also add a few tests for the buttons to make sure
that they get actually added and enabled/disabled.
2022-04-26 15:22:06 +01:00

254 lines
9.1 KiB
Python

from dataclasses import dataclass
from unittest.mock import MagicMock
import aqt.operations.note
import pytest
from conftest import get_dialog_instance, wait_until
from plugin.edit import Edit, DecentPreviewer, history, DOMAIN_PREFIX
NOTHING = object()
class Value:
def __init__(self):
self.value = NOTHING
def set(self, value):
self.value = value
def has_been_set(self):
return self.value is not NOTHING
@dataclass
class JavascriptDialogButtonManipulator:
dialog: ...
def eval_js(self, js):
evaluation_result = Value()
self.dialog.editor.web.evalWithCallback(js, evaluation_result.set)
wait_until(evaluation_result.has_been_set)
return evaluation_result.value
def wait_until_toolbar_buttons_are_ready(self):
ready_flag = Value()
self.dialog.editor._links["set_ready_flag"] = ready_flag.set # noqa
self.dialog.run_javascript_after_toolbar_ready("pycmd('set_ready_flag');")
wait_until(ready_flag.has_been_set)
# preview button doesn't have an id, so find by label
def click_preview_button(self):
self.eval_js("""
document.evaluate("//button[text()='Preview']", document)
.iterateNext()
.click()
""")
def click_button(self, button_id):
self.eval_js(f"""
document.getElementById("{DOMAIN_PREFIX}{button_id}").click()
""")
def is_button_disabled(self, button_id):
return self.eval_js(f"""
document.getElementById("{DOMAIN_PREFIX}{button_id}").disabled
""")
##############################################################################
def test_edit_dialog_opens(setup):
Edit.open_dialog_and_show_note_with_id(setup.note1_id)
def test_edit_dialog_opens_only_once(setup):
dialog1 = Edit.open_dialog_and_show_note_with_id(setup.note1_id)
dialog2 = Edit.open_dialog_and_show_note_with_id(setup.note1_id)
assert dialog1 is dialog2
def test_edit_dialog_fails_to_open_with_invalid_note(setup):
with pytest.raises(Exception):
Edit.open_dialog_and_show_note_with_id(123)
class TestBrowser:
@staticmethod
def get_selected_card_ids():
return get_dialog_instance("Browser").table.get_selected_card_ids()
def test_dialog_opens(self, setup):
dialog = Edit.open_dialog_and_show_note_with_id(setup.note1_id)
dialog.show_browser()
def test_selects_cards_of_last_note(self, setup):
Edit.open_dialog_and_show_note_with_id(setup.note1_id)
Edit.open_dialog_and_show_note_with_id(setup.note2_id).show_browser()
assert {*self.get_selected_card_ids()} == {*setup.note2_card_ids}
def test_selects_cards_of_note_before_last_after_previous_button_pressed(self, setup):
Edit.open_dialog_and_show_note_with_id(setup.note1_id)
dialog = Edit.open_dialog_and_show_note_with_id(setup.note2_id)
def verify_that_the_table_shows_note2_cards_then_note1_cards():
get_dialog_instance("Browser").table.select_all()
assert {*self.get_selected_card_ids()[:2]} == {*setup.note2_card_ids}
assert {*self.get_selected_card_ids()[2:]} == {*setup.note1_card_ids}
dialog.show_previous()
dialog.show_browser()
assert {*self.get_selected_card_ids()} == {*setup.note1_card_ids}
verify_that_the_table_shows_note2_cards_then_note1_cards()
dialog.show_next()
dialog.show_browser()
assert {*self.get_selected_card_ids()} == {*setup.note2_card_ids}
verify_that_the_table_shows_note2_cards_then_note1_cards()
class TestPreviewDialog:
def test_opens(self, setup):
edit_dialog = Edit.open_dialog_and_show_note_with_id(setup.note1_id)
edit_dialog.show_preview()
@pytest.fixture
def dialog(self, setup):
edit_dialog = Edit.open_dialog_and_show_note_with_id(setup.note1_id)
preview_dialog: DecentPreviewer = edit_dialog.show_preview()
def press_next_button(times=0):
for _ in range(times):
preview_dialog._last_render = 0 # render without delay
preview_dialog._on_next()
preview_dialog.press_next_button = press_next_button
yield preview_dialog
@pytest.mark.parametrize(
"next_button_presses, current_card, "
"showing_question_only, previous_enabled, next_enabled",
[
pytest.param(0, 0, True, False, True,
id="next button pressed 0 times; first card, question"),
pytest.param(1, 0, False, True, True,
id="next button pressed 1 time; first card, answer"),
pytest.param(2, 1, True, True, True,
id="next button pressed 2 times; second card, question"),
pytest.param(3, 1, False, True, False,
id="next button pressed 3 times; second card, answer"),
pytest.param(4, 1, False, True, False,
id="next button pressed 4 times; second card still, answer"),
]
)
def test_navigation(self, dialog, next_button_presses, current_card,
showing_question_only, previous_enabled, next_enabled):
dialog.press_next_button(times=next_button_presses)
assert dialog.adapter.current == current_card
assert dialog.showing_question_and_can_show_answer() is showing_question_only
assert dialog._should_enable_prev() is previous_enabled
assert dialog._should_enable_next() is next_enabled
class TestButtons:
@pytest.fixture
def manipulator(self, setup):
dialog = Edit.open_dialog_and_show_note_with_id(setup.note1_id)
return JavascriptDialogButtonManipulator(dialog)
def test_preview_button_can_be_clicked(self, manipulator, monkeypatch):
monkeypatch.setattr(manipulator.dialog, "show_preview", MagicMock())
manipulator.wait_until_toolbar_buttons_are_ready()
manipulator.click_preview_button()
wait_until(lambda: manipulator.dialog.show_preview.call_count == 1)
def test_addon_buttons_can_be_clicked(self, manipulator):
manipulator.wait_until_toolbar_buttons_are_ready()
manipulator.click_button(button_id="browse")
wait_until(lambda: get_dialog_instance("Browser") is not None)
def test_addon_buttons_get_disabled_enabled(self, setup, manipulator):
Edit.open_dialog_and_show_note_with_id(setup.note2_id)
manipulator.wait_until_toolbar_buttons_are_ready()
assert manipulator.is_button_disabled("previous") is False
assert manipulator.is_button_disabled("next") is True
class TestHistory:
@pytest.fixture(autouse=True)
def cleanup(self):
history.note_ids = []
def test_single_note(self, setup):
assert history.note_ids == []
Edit.open_dialog_and_show_note_with_id(setup.note1_id)
assert history.note_ids == [setup.note1_id]
def test_two_notes(self, setup):
Edit.open_dialog_and_show_note_with_id(setup.note1_id)
Edit.open_dialog_and_show_note_with_id(setup.note2_id)
assert history.note_ids == [setup.note1_id, setup.note2_id]
def test_old_note_reopened(self, setup):
Edit.open_dialog_and_show_note_with_id(setup.note1_id)
Edit.open_dialog_and_show_note_with_id(setup.note2_id)
Edit.open_dialog_and_show_note_with_id(setup.note1_id)
assert history.note_ids == [setup.note2_id, setup.note1_id]
def test_navigation(self, setup):
dialog = Edit.open_dialog_and_show_note_with_id(setup.note1_id)
Edit.open_dialog_and_show_note_with_id(setup.note2_id)
dialog.show_previous()
assert dialog.note.id == setup.note1_id
dialog.show_previous()
assert dialog.note.id == setup.note1_id
dialog.show_next()
assert dialog.note.id == setup.note2_id
dialog.show_next()
assert dialog.note.id == setup.note2_id
class TestNoteDeletionElsewhere:
@pytest.fixture
def delete_note(self, run_background_tasks_on_main_thread):
"""
Yields a function that accepts a single note id and deletes the note,
running the required hooks in sync
"""
return (
lambda note_id: aqt.operations.note
.remove_notes(parent=None, note_ids=[note_id]) # noqa
.run_in_background()
)
@staticmethod
def edit_dialog_is_open():
return aqt.dialogs._dialogs[Edit.dialog_registry_tag][1] is not None # noqa
@pytest.fixture
def dialog(self, setup):
Edit.open_dialog_and_show_note_with_id(setup.note1_id)
yield Edit.open_dialog_and_show_note_with_id(setup.note2_id)
def test_one_of_the_history_notes_is_deleted_and_dialog_stays(self,
setup, dialog, delete_note):
assert dialog.note.id == setup.note2_id
delete_note(setup.note2_id)
assert self.edit_dialog_is_open()
assert dialog.note.id == setup.note1_id
def test_all_of_the_history_notes_are_deleted_and_dialog_closes(self,
setup, dialog, delete_note):
delete_note(setup.note1_id)
delete_note(setup.note2_id)
assert not self.edit_dialog_is_open()