Merge pull request #313 from oakkitten/anki50
Fix tests and some of the code to work with Anki 2.1.50
This commit is contained in:
commit
fe8b221a93
9
.github/workflows/main.yml
vendored
9
.github/workflows/main.yml
vendored
@ -12,8 +12,15 @@ jobs:
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y pyqt5-dev-tools xvfb
|
sudo apt-get install -y pyqt5-dev-tools xvfb
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python 3.8
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.8
|
||||||
|
|
||||||
|
- name: Setup Python 3.9
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.9
|
||||||
|
|
||||||
- name: Install tox
|
- name: Install tox
|
||||||
run: pip install tox
|
run: pip install tox
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
|||||||
AnkiConnect.zip
|
AnkiConnect.zip
|
||||||
meta.json
|
meta.json
|
||||||
.idea/
|
.idea/
|
||||||
|
.tox/
|
||||||
|
@ -13,6 +13,13 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import aqt
|
||||||
|
|
||||||
|
anki_version = tuple(int(segment) for segment in aqt.appVersion.split("."))
|
||||||
|
|
||||||
|
if anki_version < (2, 1, 45):
|
||||||
|
raise Exception("Minimum Anki version supported: 2.1.45")
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import glob
|
import glob
|
||||||
import hashlib
|
import hashlib
|
||||||
@ -20,34 +27,23 @@ import inspect
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import random
|
import platform
|
||||||
import re
|
import re
|
||||||
import string
|
|
||||||
import time
|
import time
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
|
||||||
from PyQt5 import QtCore
|
|
||||||
from PyQt5.QtCore import QTimer
|
|
||||||
from PyQt5.QtWidgets import QMessageBox, QCheckBox
|
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
import anki.exporting
|
import anki.exporting
|
||||||
import anki.storage
|
import anki.storage
|
||||||
import aqt
|
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
from anki.consts import MODEL_CLOZE
|
from anki.consts import MODEL_CLOZE
|
||||||
|
|
||||||
from anki.exporting import AnkiPackageExporter
|
from anki.exporting import AnkiPackageExporter
|
||||||
from anki.importing import AnkiPackageImporter
|
from anki.importing import AnkiPackageImporter
|
||||||
from anki.notes import Note
|
from anki.notes import Note
|
||||||
|
from anki.errors import NotFoundError
|
||||||
|
from aqt.qt import Qt, QTimer, QMessageBox, QCheckBox
|
||||||
|
|
||||||
from .edit import Edit
|
from .edit import Edit
|
||||||
|
|
||||||
try:
|
|
||||||
from anki.rsbackend import NotFoundError
|
|
||||||
except:
|
|
||||||
NotFoundError = Exception
|
|
||||||
|
|
||||||
from . import web, util
|
from . import web, util
|
||||||
|
|
||||||
|
|
||||||
@ -56,8 +52,6 @@ from . import web, util
|
|||||||
#
|
#
|
||||||
|
|
||||||
class AnkiConnect:
|
class AnkiConnect:
|
||||||
_anki21_version = int(aqt.appVersion.split('.')[-1])
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.log = None
|
self.log = None
|
||||||
self.timer = None
|
self.timer = None
|
||||||
@ -84,11 +78,7 @@ class AnkiConnect:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def save_model(self, models, ankiModel):
|
def save_model(self, models, ankiModel):
|
||||||
if self._anki21_version < 45:
|
models.update_dict(ankiModel)
|
||||||
models.save(ankiModel, True)
|
|
||||||
models.flush()
|
|
||||||
else:
|
|
||||||
models.update_dict(ankiModel)
|
|
||||||
|
|
||||||
def logEvent(self, name, data):
|
def logEvent(self, name, data):
|
||||||
if self.log is not None:
|
if self.log is not None:
|
||||||
@ -391,7 +381,7 @@ class AnkiConnect:
|
|||||||
msg.setStandardButtons(QMessageBox.Yes|QMessageBox.No)
|
msg.setStandardButtons(QMessageBox.Yes|QMessageBox.No)
|
||||||
msg.setDefaultButton(QMessageBox.No)
|
msg.setDefaultButton(QMessageBox.No)
|
||||||
msg.setCheckBox(QCheckBox(text='Ignore further requests from "{}"'.format(origin), parent=msg))
|
msg.setCheckBox(QCheckBox(text='Ignore further requests from "{}"'.format(origin), parent=msg))
|
||||||
msg.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
|
msg.setWindowFlags(Qt.WindowStaysOnTopHint)
|
||||||
pressedButton = msg.exec_()
|
pressedButton = msg.exec_()
|
||||||
|
|
||||||
if pressedButton == QMessageBox.Yes:
|
if pressedButton == QMessageBox.Yes:
|
||||||
@ -547,9 +537,8 @@ class AnkiConnect:
|
|||||||
# however, since 62c23c6816adf912776b9378c008a52bb50b2e8d (2.1.45)
|
# however, since 62c23c6816adf912776b9378c008a52bb50b2e8d (2.1.45)
|
||||||
# passing cardsToo to `rem` (long deprecated) won't raise an error!
|
# passing cardsToo to `rem` (long deprecated) won't raise an error!
|
||||||
# this is dangerous, so let's raise our own exception
|
# this is dangerous, so let's raise our own exception
|
||||||
if self._anki21_version >= 28:
|
raise Exception("Since Anki 2.1.28 it's not possible "
|
||||||
raise Exception("Since Anki 2.1.28 it's not possible "
|
"to delete decks without deleting cards as well")
|
||||||
"to delete decks without deleting cards as well")
|
|
||||||
try:
|
try:
|
||||||
self.startEditing()
|
self.startEditing()
|
||||||
decks = filter(lambda d: d in self.deckNames(), decks)
|
decks = filter(lambda d: d in self.deckNames(), decks)
|
||||||
@ -1425,9 +1414,7 @@ class AnkiConnect:
|
|||||||
if savedMid:
|
if savedMid:
|
||||||
deck['mid'] = savedMid
|
deck['mid'] = savedMid
|
||||||
|
|
||||||
addCards.editor.note = ankiNote
|
addCards.editor.set_note(ankiNote)
|
||||||
addCards.editor.loadNote()
|
|
||||||
addCards.editor.updateTags()
|
|
||||||
|
|
||||||
addCards.activateWindow()
|
addCards.activateWindow()
|
||||||
|
|
||||||
@ -1633,6 +1620,9 @@ class AnkiConnect:
|
|||||||
# when run inside Anki, `__name__` would be either numeric,
|
# when run inside Anki, `__name__` would be either numeric,
|
||||||
# or, if installed via `link.sh`, `AnkiConnectDev`
|
# or, if installed via `link.sh`, `AnkiConnectDev`
|
||||||
if __name__ != "plugin":
|
if __name__ != "plugin":
|
||||||
|
if platform.system() == "Windows" and anki_version == (2, 1, 50):
|
||||||
|
util.patch_anki_2_1_50_having_null_stdout_on_windows()
|
||||||
|
|
||||||
Edit.register_with_anki()
|
Edit.register_with_anki()
|
||||||
|
|
||||||
ac = AnkiConnect()
|
ac = AnkiConnect()
|
||||||
|
102
plugin/edit.py
102
plugin/edit.py
@ -1,5 +1,6 @@
|
|||||||
import aqt
|
import aqt
|
||||||
import aqt.editor
|
import aqt.editor
|
||||||
|
import aqt.browser.previewer
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
from aqt.qt import QDialog, Qt, QKeySequence, QShortcut
|
from aqt.qt import QDialog, Qt, QKeySequence, QShortcut
|
||||||
from aqt.utils import disable_help_button, restoreGeom, saveGeom, tooltip
|
from aqt.utils import disable_help_button, restoreGeom, saveGeom, tooltip
|
||||||
@ -7,6 +8,8 @@ from anki.errors import NotFoundError
|
|||||||
from anki.consts import QUEUE_TYPE_SUSPENDED
|
from anki.consts import QUEUE_TYPE_SUSPENDED
|
||||||
from anki.utils import ids2str
|
from anki.utils import ids2str
|
||||||
|
|
||||||
|
from . import anki_version
|
||||||
|
|
||||||
|
|
||||||
# Edit dialog. Like Edit Current, but:
|
# Edit dialog. Like Edit Current, but:
|
||||||
# * has a Preview button to preview the cards for the note
|
# * has a Preview button to preview the cards for the note
|
||||||
@ -184,7 +187,7 @@ class Edit(aqt.editcurrent.EditCurrent):
|
|||||||
# upon a request to open the dialog via `aqt.dialogs.open()`,
|
# upon a request to open the dialog via `aqt.dialogs.open()`,
|
||||||
# the manager will call either the constructor or the `reopen` method
|
# the manager will call either the constructor or the `reopen` method
|
||||||
def __init__(self, note):
|
def __init__(self, note):
|
||||||
QDialog.__init__(self, None, Qt.Window)
|
QDialog.__init__(self, None, Qt.WindowType.Window)
|
||||||
aqt.mw.garbage_collect_on_dialog_finish(self)
|
aqt.mw.garbage_collect_on_dialog_finish(self)
|
||||||
self.form = aqt.forms.editcurrent.Ui_Dialog()
|
self.form = aqt.forms.editcurrent.Ui_Dialog()
|
||||||
self.form.setupUi(self)
|
self.form.setupUi(self)
|
||||||
@ -225,8 +228,6 @@ class Edit(aqt.editcurrent.EditCurrent):
|
|||||||
if changes.note_text and handler is not self.editor:
|
if changes.note_text and handler is not self.editor:
|
||||||
self.reload_notes_after_user_action_elsewhere()
|
self.reload_notes_after_user_action_elsewhere()
|
||||||
|
|
||||||
# adjusting buttons right after initializing doesn't have any effect;
|
|
||||||
# this seems to do the trick
|
|
||||||
def editor_did_load_note(self, _editor):
|
def editor_did_load_note(self, _editor):
|
||||||
self.enable_disable_next_and_previous_buttons()
|
self.enable_disable_next_and_previous_buttons()
|
||||||
|
|
||||||
@ -304,29 +305,66 @@ class Edit(aqt.editcurrent.EditCurrent):
|
|||||||
gui_hooks.editor_did_init.append(self.add_preview_button)
|
gui_hooks.editor_did_init.append(self.add_preview_button)
|
||||||
gui_hooks.editor_did_init_buttons.append(self.add_right_hand_side_buttons)
|
gui_hooks.editor_did_init_buttons.append(self.add_right_hand_side_buttons)
|
||||||
|
|
||||||
self.editor = aqt.editor.Editor(aqt.mw, self.form.fieldsArea, self)
|
# on Anki 2.1.50, browser mode makes the Preview button visible
|
||||||
|
extra_kwargs = {} if anki_version < (2, 1, 50) else {
|
||||||
|
"editor_mode": aqt.editor.EditorMode.BROWSER
|
||||||
|
}
|
||||||
|
|
||||||
|
self.editor = aqt.editor.Editor(aqt.mw, self.form.fieldsArea, self,
|
||||||
|
**extra_kwargs)
|
||||||
|
|
||||||
gui_hooks.editor_did_init_buttons.remove(self.add_right_hand_side_buttons)
|
gui_hooks.editor_did_init_buttons.remove(self.add_right_hand_side_buttons)
|
||||||
gui_hooks.editor_did_init.remove(self.add_preview_button)
|
gui_hooks.editor_did_init.remove(self.add_preview_button)
|
||||||
|
|
||||||
# taken from `setupEditor` of browser.py
|
# * on Anki < 2.1.50, make the button via js (`setupEditor` of browser.py);
|
||||||
# PreviewButton calls pycmd `preview`, which is hardcoded.
|
# also, make a copy of _links so that opening Anki's browser does not
|
||||||
# copying _links is needed so that opening Anki's browser does not
|
# screw them up as they are apparently shared between instances?!
|
||||||
# screw them up as they are apparently shared between instances?!
|
# the last part seems to have been fixed in Anki 2.1.50
|
||||||
|
# * on Anki 2.1.50, the button is created by setting editor mode,
|
||||||
|
# see above; so we only need to add the link.
|
||||||
def add_preview_button(self, editor):
|
def add_preview_button(self, editor):
|
||||||
QShortcut(QKeySequence("Ctrl+Shift+P"), self, self.show_preview)
|
QShortcut(QKeySequence("Ctrl+Shift+P"), self, self.show_preview)
|
||||||
|
|
||||||
editor._links = editor._links.copy()
|
if anki_version < (2, 1, 50):
|
||||||
editor._links["preview"] = self.show_preview
|
editor._links = editor._links.copy()
|
||||||
editor.web.eval("""
|
editor.web.eval("""
|
||||||
$editorToolbar.then(({notetypeButtons}) =>
|
$editorToolbar.then(({notetypeButtons}) =>
|
||||||
notetypeButtons.appendButton(
|
notetypeButtons.appendButton(
|
||||||
{component: editorToolbar.PreviewButton, id: 'preview'}
|
{component: editorToolbar.PreviewButton, id: 'preview'}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
editor._links["preview"] = lambda _editor: self.show_preview() and None
|
||||||
|
|
||||||
|
# * on Anki < 2.1.50, button style is okay-ish from get-go,
|
||||||
|
# except when disabled; adding class `btn` fixes that;
|
||||||
|
# * on Anki 2.1.50, buttons have weird font size and are square';
|
||||||
|
# the style below makes them in line with left-hand side buttons
|
||||||
def add_right_hand_side_buttons(self, buttons, editor):
|
def add_right_hand_side_buttons(self, buttons, editor):
|
||||||
|
if anki_version < (2, 1, 50):
|
||||||
|
extra_button_class = "btn"
|
||||||
|
else:
|
||||||
|
extra_button_class = "anki-connect-button"
|
||||||
|
editor.web.eval("""
|
||||||
|
(function(){
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.innerHTML = `
|
||||||
|
.anki-connect-button {
|
||||||
|
white-space: nowrap;
|
||||||
|
width: auto;
|
||||||
|
padding: 0 2px;
|
||||||
|
font-size: var(--base-font-size);
|
||||||
|
}
|
||||||
|
.anki-connect-button:disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: .4;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
})();
|
||||||
|
""")
|
||||||
|
|
||||||
def add(cmd, function, label, tip, keys):
|
def add(cmd, function, label, tip, keys):
|
||||||
button_html = editor.addButton(
|
button_html = editor.addButton(
|
||||||
icon=None,
|
icon=None,
|
||||||
@ -338,30 +376,34 @@ class Edit(aqt.editcurrent.EditCurrent):
|
|||||||
keys=keys,
|
keys=keys,
|
||||||
)
|
)
|
||||||
|
|
||||||
# adding class `btn` properly styles buttons when disabled
|
button_html = button_html.replace('class="',
|
||||||
button_html = button_html.replace('class="', 'class="btn ')
|
f'class="{extra_button_class} ')
|
||||||
buttons.append(button_html)
|
buttons.append(button_html)
|
||||||
|
|
||||||
add("browse", self.show_browser, "Browse", "Browse", "Ctrl+F")
|
add("browse", self.show_browser, "Browse", "Browse", "Ctrl+F")
|
||||||
add("previous", self.show_previous, "<", "Previous", "Alt+Left")
|
add("previous", self.show_previous, "<", "Previous", "Alt+Left")
|
||||||
add("next", self.show_next, ">", "Next", "Alt+Right")
|
add("next", self.show_next, ">", "Next", "Alt+Right")
|
||||||
|
|
||||||
|
def run_javascript_after_toolbar_ready(self, js):
|
||||||
|
js = f"setTimeout(function() {{ {js} }}, 1)"
|
||||||
|
if anki_version < (2, 1, 50):
|
||||||
|
js = f'$editorToolbar.then(({{ toolbar }}) => {js})'
|
||||||
|
else:
|
||||||
|
js = f'require("anki/ui").loaded.then(() => {js})'
|
||||||
|
self.editor.web.eval(js)
|
||||||
|
|
||||||
def enable_disable_next_and_previous_buttons(self):
|
def enable_disable_next_and_previous_buttons(self):
|
||||||
def to_js(boolean):
|
def to_js(boolean):
|
||||||
return "true" if boolean else "false"
|
return "true" if boolean else "false"
|
||||||
|
|
||||||
disable_previous = to_js(not(history.has_note_to_left_of(self.note)))
|
disable_previous = not(history.has_note_to_left_of(self.note))
|
||||||
disable_next = to_js(not(history.has_note_to_right_of(self.note)))
|
disable_next = not(history.has_note_to_right_of(self.note))
|
||||||
|
|
||||||
self.editor.web.eval(f"""
|
self.run_javascript_after_toolbar_ready(f"""
|
||||||
$editorToolbar.then(({{ toolbar }}) => {{
|
document.getElementById("{DOMAIN_PREFIX}previous")
|
||||||
setTimeout(function() {{
|
.disabled = {to_js(disable_previous)};
|
||||||
document.getElementById("{DOMAIN_PREFIX}previous")
|
document.getElementById("{DOMAIN_PREFIX}next")
|
||||||
.disabled = {disable_previous};
|
.disabled = {to_js(disable_next)};
|
||||||
document.getElementById("{DOMAIN_PREFIX}next")
|
|
||||||
.disabled = {disable_next};
|
|
||||||
}}, 1);
|
|
||||||
}});
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
import anki.sync
|
import anki.sync
|
||||||
@ -83,3 +84,10 @@ def setting(key):
|
|||||||
return aqt.mw.addonManager.getConfig(__name__).get(key, DEFAULT_CONFIG[key])
|
return aqt.mw.addonManager.getConfig(__name__).get(key, DEFAULT_CONFIG[key])
|
||||||
except:
|
except:
|
||||||
raise Exception('setting {} not found'.format(key))
|
raise Exception('setting {} not found'.format(key))
|
||||||
|
|
||||||
|
|
||||||
|
# see https://github.com/FooSoft/anki-connect/issues/308
|
||||||
|
# fixed in https://github.com/ankitects/anki/commit/0b2a226d
|
||||||
|
def patch_anki_2_1_50_having_null_stdout_on_windows():
|
||||||
|
if sys.stdout is None:
|
||||||
|
sys.stdout = open(os.devnull, "w", encoding="utf8")
|
||||||
|
@ -5,15 +5,20 @@ from dataclasses import dataclass
|
|||||||
|
|
||||||
import aqt.operations.note
|
import aqt.operations.note
|
||||||
import pytest
|
import pytest
|
||||||
from PyQt5 import QtTest
|
import anki.collection
|
||||||
from _pytest.monkeypatch import MonkeyPatch # noqa
|
from _pytest.monkeypatch import MonkeyPatch # noqa
|
||||||
from pytest_anki._launch import anki_running, temporary_user # noqa
|
from pytest_anki._launch import anki_running, temporary_user # noqa
|
||||||
from waitress import wasyncore
|
from waitress import wasyncore
|
||||||
|
|
||||||
from plugin import AnkiConnect
|
from plugin import AnkiConnect, anki_version
|
||||||
from plugin.edit import Edit
|
from plugin.edit import Edit
|
||||||
from plugin.util import DEFAULT_CONFIG
|
from plugin.util import DEFAULT_CONFIG
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PyQt6 import QtTest
|
||||||
|
except ImportError:
|
||||||
|
from PyQt5 import QtTest
|
||||||
|
|
||||||
|
|
||||||
ac = AnkiConnect()
|
ac = AnkiConnect()
|
||||||
|
|
||||||
@ -77,22 +82,33 @@ def waitress_patched_to_prevent_it_from_dying():
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def anki_patched_to_prevent_backups():
|
||||||
|
with MonkeyPatch().context() as monkey:
|
||||||
|
if anki_version < (2, 1, 50):
|
||||||
|
monkey.setitem(aqt.profiles.profileConf, "numBackups", 0)
|
||||||
|
else:
|
||||||
|
monkey.setattr(anki.collection.Collection, "create_backup",
|
||||||
|
lambda *args, **kwargs: True)
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def empty_anki_session_started():
|
def empty_anki_session_started():
|
||||||
with waitress_patched_to_prevent_it_from_dying():
|
with waitress_patched_to_prevent_it_from_dying():
|
||||||
with anki_running(
|
with anki_patched_to_prevent_backups():
|
||||||
qtbot=None, # noqa
|
with anki_running(
|
||||||
enable_web_debugging=False,
|
qtbot=None, # noqa
|
||||||
profile_name="test_user",
|
enable_web_debugging=False,
|
||||||
) as session:
|
profile_name="test_user",
|
||||||
yield session
|
) as session:
|
||||||
|
yield session
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def profile_created_and_loaded(session):
|
def profile_created_and_loaded(session):
|
||||||
with temporary_user(session.base, "test_user", "en_US"):
|
with temporary_user(session.base, "test_user", "en_US"):
|
||||||
with session.profile_loaded():
|
with session.profile_loaded():
|
||||||
aqt.mw.pm.profile["numBackups"] = 0 # don't try to do backups
|
|
||||||
yield session
|
yield session
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,10 +30,6 @@ def test_deleteDeck(setup):
|
|||||||
assert {*before} - {*after} == {"test_deck"}
|
assert {*before} - {*after} == {"test_deck"}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
|
||||||
condition=ac._anki21_version < 28,
|
|
||||||
reason=f"Not applicable to Anki < 2.1.28"
|
|
||||||
)
|
|
||||||
def test_deleteDeck_must_be_called_with_cardsToo_set_to_True_on_later_api(setup):
|
def test_deleteDeck_must_be_called_with_cardsToo_set_to_True_on_later_api(setup):
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
ac.deleteDecks(decks=["test_deck"])
|
ac.deleteDecks(decks=["test_deck"])
|
||||||
|
@ -1,8 +1,63 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import aqt.operations.note
|
import aqt.operations.note
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from conftest import get_dialog_instance
|
from conftest import get_dialog_instance, wait_until
|
||||||
from plugin.edit import Edit, DecentPreviewer, history
|
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):
|
def test_edit_dialog_opens(setup):
|
||||||
@ -99,6 +154,30 @@ class TestPreviewDialog:
|
|||||||
assert dialog._should_enable_next() is next_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:
|
class TestHistory:
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
|
import pytest
|
||||||
|
|
||||||
from conftest import ac, anki_connect_config_loaded, \
|
from conftest import ac, anki_connect_config_loaded, \
|
||||||
set_up_test_deck_and_test_model_and_two_notes, \
|
set_up_test_deck_and_test_model_and_two_notes, \
|
||||||
@ -33,6 +36,19 @@ class TestProfiles:
|
|||||||
|
|
||||||
|
|
||||||
class TestExportImport:
|
class TestExportImport:
|
||||||
|
# since Anki 2.1.50, exporting media for some wild reason
|
||||||
|
# will change the current working directory, which then gets removed.
|
||||||
|
# see `exporting.py`, ctrl-f `os.chdir(self.mediaDir)`
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def current_working_directory_preserved(self):
|
||||||
|
cwd = os.getcwd()
|
||||||
|
yield
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.getcwd()
|
||||||
|
except FileNotFoundError:
|
||||||
|
os.chdir(cwd)
|
||||||
|
|
||||||
def test_exportPackage(self, session_with_profile_loaded, setup):
|
def test_exportPackage(self, session_with_profile_loaded, setup):
|
||||||
filename = session_with_profile_loaded.base + "/export.apkg"
|
filename = session_with_profile_loaded.base + "/export.apkg"
|
||||||
ac.exportPackage(deck="test_deck", path=filename)
|
ac.exportPackage(deck="test_deck", path=filename)
|
||||||
|
17
tox.ini
17
tox.ini
@ -40,25 +40,26 @@
|
|||||||
# LIBGL_ALWAYS_INDIRECT=1
|
# LIBGL_ALWAYS_INDIRECT=1
|
||||||
# QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu"
|
# QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu"
|
||||||
# QT_DEBUG_PLUGINS=1
|
# QT_DEBUG_PLUGINS=1
|
||||||
# ANKIDEV=true
|
# ANKIDEV=1
|
||||||
|
|
||||||
[tox]
|
[tox]
|
||||||
minversion = 3.24
|
minversion = 3.24
|
||||||
skipsdist = true
|
skipsdist = true
|
||||||
skip_install = true
|
skip_install = true
|
||||||
envlist = py38-anki{45,46,47,48,49}
|
envlist = py38-anki{45,46,47,48,49},py39-anki{50qt5,50qt6}
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands =
|
commands =
|
||||||
xvfb-run python -m pytest {posargs}
|
env HOME={envtmpdir}/home xvfb-run python -m pytest {posargs}
|
||||||
setenv =
|
setenv =
|
||||||
HOME={envdir}/home
|
anki50qt6: DISABLE_QT5_COMPAT=1
|
||||||
allowlist_externals =
|
allowlist_externals =
|
||||||
|
env
|
||||||
xvfb-run
|
xvfb-run
|
||||||
deps =
|
deps =
|
||||||
pytest==7.1.1
|
pytest==7.1.1
|
||||||
pytest-forked==1.4.0
|
pytest-forked==1.4.0
|
||||||
pytest-anki @ git+https://github.com/oakkitten/pytest-anki.git@17d19043
|
pytest-anki @ git+https://github.com/oakkitten/pytest-anki.git@a0d27aa5
|
||||||
|
|
||||||
anki45: anki==2.1.45
|
anki45: anki==2.1.45
|
||||||
anki45: aqt==2.1.45
|
anki45: aqt==2.1.45
|
||||||
@ -74,3 +75,9 @@ deps =
|
|||||||
|
|
||||||
anki49: anki==2.1.49
|
anki49: anki==2.1.49
|
||||||
anki49: aqt==2.1.49
|
anki49: aqt==2.1.49
|
||||||
|
|
||||||
|
anki50qt5: anki==2.1.50
|
||||||
|
anki50qt5: aqt[qt5]==2.1.50
|
||||||
|
|
||||||
|
anki50qt6: anki==2.1.50
|
||||||
|
anki50qt6: aqt[qt6]==2.1.50
|
||||||
|
Loading…
Reference in New Issue
Block a user