Fix focusing on editor after leaving the app and returning. More code coverage and removing obsolete bits of code
This commit is contained in:
parent
74177f2cd3
commit
aad1ba5d7d
16 changed files with 264 additions and 100 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
# 0.1.11
|
# 0.1.11
|
||||||
|
|
||||||
* Add missing export extensions to export_by_extension
|
* Add missing export extensions to export_by_extension
|
||||||
|
* Fix focusing on editor after leaving the app and returning
|
||||||
|
* More code coverage and removing obsolete bits of code
|
||||||
|
|
||||||
# 0.1.10.2
|
# 0.1.10.2
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -257,61 +257,25 @@ class DBManager:
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
def get_version(
|
def get_version(self, *, version_id: int) -> dict | None:
|
||||||
self,
|
|
||||||
*,
|
|
||||||
date_iso: str | None = None,
|
|
||||||
version_no: int | None = None,
|
|
||||||
version_id: int | None = None,
|
|
||||||
) -> dict | None:
|
|
||||||
"""
|
"""
|
||||||
Fetch a specific version by (date, version_no) OR by version_id.
|
Fetch a specific version by version_id.
|
||||||
Returns a dict with keys: id, date, version_no, created_at, note, content.
|
Returns a dict with keys: id, date, version_no, created_at, note, content.
|
||||||
"""
|
"""
|
||||||
cur = self.conn.cursor()
|
cur = self.conn.cursor()
|
||||||
if version_id is not None:
|
|
||||||
row = cur.execute(
|
row = cur.execute(
|
||||||
"SELECT id, date, version_no, created_at, note, content "
|
"SELECT id, date, version_no, created_at, note, content "
|
||||||
"FROM versions WHERE id=?;",
|
"FROM versions WHERE id=?;",
|
||||||
(version_id,),
|
(version_id,),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
else:
|
|
||||||
if date_iso is None or version_no is None:
|
|
||||||
raise ValueError(
|
|
||||||
"Provide either version_id OR (date_iso and version_no)"
|
|
||||||
)
|
|
||||||
row = cur.execute(
|
|
||||||
"SELECT id, date, version_no, created_at, note, content "
|
|
||||||
"FROM versions WHERE date=? AND version_no=?;",
|
|
||||||
(date_iso, version_no),
|
|
||||||
).fetchone()
|
|
||||||
return dict(row) if row else None
|
return dict(row) if row else None
|
||||||
|
|
||||||
def revert_to_version(
|
def revert_to_version(self, date_iso: str, version_id: int) -> None:
|
||||||
self,
|
|
||||||
date_iso: str,
|
|
||||||
*,
|
|
||||||
version_no: int | None = None,
|
|
||||||
version_id: int | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Point the page head (pages.current_version_id) to an existing version.
|
Point the page head (pages.current_version_id) to an existing version.
|
||||||
"""
|
"""
|
||||||
if self.conn is None:
|
|
||||||
raise RuntimeError("Database is not connected")
|
|
||||||
cur = self.conn.cursor()
|
cur = self.conn.cursor()
|
||||||
|
|
||||||
if version_id is None:
|
|
||||||
if version_no is None:
|
|
||||||
raise ValueError("Provide version_no or version_id")
|
|
||||||
row = cur.execute(
|
|
||||||
"SELECT id FROM versions WHERE date=? AND version_no=?;",
|
|
||||||
(date_iso, version_no),
|
|
||||||
).fetchone()
|
|
||||||
if row is None:
|
|
||||||
raise ValueError("Version not found for this date")
|
|
||||||
version_id = int(row["id"])
|
|
||||||
else:
|
|
||||||
# Ensure that version_id belongs to the given date
|
# Ensure that version_id belongs to the given date
|
||||||
row = cur.execute(
|
row = cur.execute(
|
||||||
"SELECT date FROM versions WHERE id=?;", (version_id,)
|
"SELECT date FROM versions WHERE id=?;", (version_id,)
|
||||||
|
|
@ -341,18 +305,13 @@ class DBManager:
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [(r[0], r[1]) for r in rows]
|
return [(r[0], r[1]) for r in rows]
|
||||||
|
|
||||||
def export_json(
|
def export_json(self, entries: Sequence[Entry], file_path: str) -> None:
|
||||||
self, entries: Sequence[Entry], file_path: str, pretty: bool = True
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Export to json.
|
Export to json.
|
||||||
"""
|
"""
|
||||||
data = [{"date": d, "content": c} for d, c in entries]
|
data = [{"date": d, "content": c} for d, c in entries]
|
||||||
with open(file_path, "w", encoding="utf-8") as f:
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
if pretty:
|
|
||||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
else:
|
|
||||||
json.dump(data, f, ensure_ascii=False, separators=(",", ":"))
|
|
||||||
|
|
||||||
def export_csv(self, entries: Sequence[Entry], file_path: str) -> None:
|
def export_csv(self, entries: Sequence[Entry], file_path: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -500,7 +459,7 @@ class DBManager:
|
||||||
elif ext in {".sql", ".sqlite"}:
|
elif ext in {".sql", ".sqlite"}:
|
||||||
self.export_sql(file_path)
|
self.export_sql(file_path)
|
||||||
elif ext == ".md":
|
elif ext == ".md":
|
||||||
self.export_markdown(file_path)
|
self.export_markdown(entries, file_path)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported extension: {ext}")
|
raise ValueError(f"Unsupported extension: {ext}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -140,10 +140,8 @@ class Editor(QTextEdit):
|
||||||
bc.setPosition(b.position() + b.length())
|
bc.setPosition(b.position() + b.length())
|
||||||
return blocks > 0 and (codeish / blocks) >= 0.6
|
return blocks > 0 and (codeish / blocks) >= 0.6
|
||||||
|
|
||||||
def _nearest_code_frame(self, cursor=None, tolerant: bool = False):
|
def _nearest_code_frame(self, cursor, tolerant: bool = False):
|
||||||
"""Walk up parents from the cursor and return the first code frame."""
|
"""Walk up parents from the cursor and return the first code frame."""
|
||||||
if cursor is None:
|
|
||||||
cursor = self.textCursor()
|
|
||||||
f = cursor.currentFrame()
|
f = cursor.currentFrame()
|
||||||
while f:
|
while f:
|
||||||
if self._is_code_frame(f, tolerant=tolerant):
|
if self._is_code_frame(f, tolerant=tolerant):
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ from PySide6.QtGui import (
|
||||||
QGuiApplication,
|
QGuiApplication,
|
||||||
QPalette,
|
QPalette,
|
||||||
QTextCharFormat,
|
QTextCharFormat,
|
||||||
|
QTextCursor,
|
||||||
QTextListFormat,
|
QTextListFormat,
|
||||||
)
|
)
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
|
|
@ -146,6 +147,16 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
QApplication.instance().installEventFilter(self)
|
QApplication.instance().installEventFilter(self)
|
||||||
|
|
||||||
|
# Focus on the editor
|
||||||
|
self.setFocusPolicy(Qt.StrongFocus)
|
||||||
|
self.editor.setFocusPolicy(Qt.StrongFocus)
|
||||||
|
self.toolBar.setFocusPolicy(Qt.NoFocus)
|
||||||
|
for w in self.toolBar.findChildren(QWidget):
|
||||||
|
w.setFocusPolicy(Qt.NoFocus)
|
||||||
|
QGuiApplication.instance().applicationStateChanged.connect(
|
||||||
|
self._on_app_state_changed
|
||||||
|
)
|
||||||
|
|
||||||
# Status bar for feedback
|
# Status bar for feedback
|
||||||
self.statusBar().showMessage("Ready", 800)
|
self.statusBar().showMessage("Ready", 800)
|
||||||
|
|
||||||
|
|
@ -481,7 +492,8 @@ class MainWindow(QMainWindow):
|
||||||
# Inject the extra_data before the closing </body></html>
|
# Inject the extra_data before the closing </body></html>
|
||||||
modified = re.sub(r"(<\/body><\/html>)", extra_data_html + r"\1", text)
|
modified = re.sub(r"(<\/body><\/html>)", extra_data_html + r"\1", text)
|
||||||
text = modified
|
text = modified
|
||||||
self.editor.setHtml(text)
|
# Force a save now so we don't lose it.
|
||||||
|
self._set_editor_html_preserve_view(text)
|
||||||
self._dirty = True
|
self._dirty = True
|
||||||
self._save_date(date_iso, True)
|
self._save_date(date_iso, True)
|
||||||
|
|
||||||
|
|
@ -489,9 +501,7 @@ class MainWindow(QMainWindow):
|
||||||
QMessageBox.critical(self, "Read Error", str(e))
|
QMessageBox.critical(self, "Read Error", str(e))
|
||||||
return
|
return
|
||||||
|
|
||||||
self.editor.blockSignals(True)
|
self._set_editor_html_preserve_view(text)
|
||||||
self.editor.setHtml(text)
|
|
||||||
self.editor.blockSignals(False)
|
|
||||||
|
|
||||||
self._dirty = False
|
self._dirty = False
|
||||||
# track which date the editor currently represents
|
# track which date the editor currently represents
|
||||||
|
|
@ -850,6 +860,8 @@ If you want an encrypted backup, choose Backup instead of Export.
|
||||||
def eventFilter(self, obj, event):
|
def eventFilter(self, obj, event):
|
||||||
if event.type() == QEvent.KeyPress and not self._locked:
|
if event.type() == QEvent.KeyPress and not self._locked:
|
||||||
self._idle_timer.start()
|
self._idle_timer.start()
|
||||||
|
if event.type() in (QEvent.ApplicationActivate, QEvent.WindowActivate):
|
||||||
|
QTimer.singleShot(0, self._focus_editor_now)
|
||||||
return super().eventFilter(obj, event)
|
return super().eventFilter(obj, event)
|
||||||
|
|
||||||
def _enter_lock(self):
|
def _enter_lock(self):
|
||||||
|
|
@ -891,6 +903,7 @@ If you want an encrypted backup, choose Backup instead of Export.
|
||||||
if tb:
|
if tb:
|
||||||
tb.setEnabled(True)
|
tb.setEnabled(True)
|
||||||
self._idle_timer.start()
|
self._idle_timer.start()
|
||||||
|
QTimer.singleShot(0, self._focus_editor_now)
|
||||||
|
|
||||||
# ----------------- Close handlers ----------------- #
|
# ----------------- Close handlers ----------------- #
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
|
|
@ -906,3 +919,61 @@ If you want an encrypted backup, choose Backup instead of Export.
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
super().closeEvent(event)
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
# ----------------- Below logic helps focus the editor ----------------- #
|
||||||
|
|
||||||
|
def _focus_editor_now(self):
|
||||||
|
"""Give focus to the editor and ensure the caret is visible."""
|
||||||
|
if getattr(self, "_locked", False):
|
||||||
|
return
|
||||||
|
if not self.isActiveWindow():
|
||||||
|
return
|
||||||
|
# Belt-and-suspenders: do it now and once more on the next tick
|
||||||
|
self.editor.setFocus(Qt.ActiveWindowFocusReason)
|
||||||
|
self.editor.ensureCursorVisible()
|
||||||
|
QTimer.singleShot(
|
||||||
|
0,
|
||||||
|
lambda: (
|
||||||
|
self.editor.setFocus(Qt.ActiveWindowFocusReason),
|
||||||
|
self.editor.ensureCursorVisible(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_app_state_changed(self, state):
|
||||||
|
# Called on macOS/Wayland/Windows when the whole app re-activates
|
||||||
|
if state == Qt.ApplicationActive and self.isActiveWindow():
|
||||||
|
QTimer.singleShot(0, self._focus_editor_now)
|
||||||
|
|
||||||
|
def changeEvent(self, ev):
|
||||||
|
# Called on some platforms when the window's activation state flips
|
||||||
|
super().changeEvent(ev)
|
||||||
|
if ev.type() == QEvent.ActivationChange and self.isActiveWindow():
|
||||||
|
QTimer.singleShot(0, self._focus_editor_now)
|
||||||
|
|
||||||
|
def _set_editor_html_preserve_view(self, html: str):
|
||||||
|
ed = self.editor
|
||||||
|
|
||||||
|
# Save caret/selection and scroll
|
||||||
|
cur = ed.textCursor()
|
||||||
|
old_pos, old_anchor = cur.position(), cur.anchor()
|
||||||
|
v = ed.verticalScrollBar().value()
|
||||||
|
h = ed.horizontalScrollBar().value()
|
||||||
|
|
||||||
|
# Only touch the doc if it actually changed
|
||||||
|
ed.blockSignals(True)
|
||||||
|
if ed.toHtml() != html:
|
||||||
|
ed.setHtml(html)
|
||||||
|
ed.blockSignals(False)
|
||||||
|
|
||||||
|
# Restore scroll first
|
||||||
|
ed.verticalScrollBar().setValue(v)
|
||||||
|
ed.horizontalScrollBar().setValue(h)
|
||||||
|
|
||||||
|
# Restore caret/selection
|
||||||
|
cur = ed.textCursor()
|
||||||
|
cur.setPosition(old_anchor)
|
||||||
|
mode = (
|
||||||
|
QTextCursor.KeepAnchor if old_anchor != old_pos else QTextCursor.MoveAnchor
|
||||||
|
)
|
||||||
|
cur.setPosition(old_pos, mode)
|
||||||
|
ed.setTextCursor(cur)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
@ -74,25 +73,14 @@ def test_revert_to_version_by_number_and_id_and_errors(cfg: DBConfig):
|
||||||
ver2_id, ver2_no = mgr.save_new_version("2025-01-04", "<p>v2</p>", note="edit")
|
ver2_id, ver2_no = mgr.save_new_version("2025-01-04", "<p>v2</p>", note="edit")
|
||||||
assert ver1_no == 1 and ver2_no == 2
|
assert ver1_no == 1 and ver2_no == 2
|
||||||
|
|
||||||
# Revert using version_no (exercises branch where version_id is None)
|
# Revert using version_id
|
||||||
mgr.revert_to_version(date_iso="2025-01-04", version_no=1, version_id=None)
|
|
||||||
cur = mgr.conn.cursor()
|
|
||||||
head = cur.execute(
|
|
||||||
"SELECT current_version_id FROM pages WHERE date=?", ("2025-01-04",)
|
|
||||||
).fetchone()[0]
|
|
||||||
assert head == ver1_id
|
|
||||||
|
|
||||||
# Revert using version_id directly should also work
|
|
||||||
mgr.revert_to_version(date_iso="2025-01-04", version_id=ver2_id)
|
mgr.revert_to_version(date_iso="2025-01-04", version_id=ver2_id)
|
||||||
|
cur = mgr.conn.cursor()
|
||||||
head2 = cur.execute(
|
head2 = cur.execute(
|
||||||
"SELECT current_version_id FROM pages WHERE date=?", ("2025-01-04",)
|
"SELECT current_version_id FROM pages WHERE date=?", ("2025-01-04",)
|
||||||
).fetchone()[0]
|
).fetchone()[0]
|
||||||
assert head2 == ver2_id
|
assert head2 == ver2_id
|
||||||
|
|
||||||
# Error: version not found for date (non-existent version_no)
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
mgr.revert_to_version(date_iso="2025-01-04", version_no=99)
|
|
||||||
|
|
||||||
# Error: version_id belongs to a different date
|
# Error: version_id belongs to a different date
|
||||||
other_id, _ = mgr.save_new_version("2025-01-05", "<p>other</p>")
|
other_id, _ = mgr.save_new_version("2025-01-05", "<p>other</p>")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ def test_export_by_extension_and_unknown(tmp_path):
|
||||||
import types
|
import types
|
||||||
|
|
||||||
mgr.get_all_entries = types.MethodType(lambda self: entries, mgr)
|
mgr.get_all_entries = types.MethodType(lambda self: entries, mgr)
|
||||||
for ext in [".json", ".csv", ".txt", ".html"]:
|
for ext in [".json", ".csv", ".txt", ".html", ".md"]:
|
||||||
path = tmp_path / f"route{ext}"
|
path = tmp_path / f"route{ext}"
|
||||||
mgr.export_by_extension(str(path))
|
mgr.export_by_extension(str(path))
|
||||||
assert path.exists()
|
assert path.exists()
|
||||||
|
|
|
||||||
|
|
@ -145,10 +145,6 @@ def test_linkify_trims_trailing_punctuation(qtbot):
|
||||||
|
|
||||||
|
|
||||||
def test_code_block_enter_exits_on_empty_line(qtbot):
|
def test_code_block_enter_exits_on_empty_line(qtbot):
|
||||||
from PySide6.QtCore import Qt
|
|
||||||
from PySide6.QtGui import QTextCursor
|
|
||||||
from PySide6.QtTest import QTest
|
|
||||||
from bouquin.editor import Editor
|
|
||||||
|
|
||||||
e = _mk_editor()
|
e = _mk_editor()
|
||||||
qtbot.addWidget(e)
|
qtbot.addWidget(e)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import base64
|
import base64
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from PySide6.QtCore import Qt, QMimeData, QByteArray
|
from PySide6.QtCore import Qt, QMimeData, QByteArray
|
||||||
from PySide6.QtGui import QImage, QPixmap, QKeyEvent, QTextCursor
|
from PySide6.QtGui import QImage, QTextCursor
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
from PySide6.QtTest import QTest
|
from PySide6.QtTest import QTest
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import base64
|
from PySide6.QtCore import QUrl
|
||||||
from pathlib import Path
|
|
||||||
from PySide6.QtCore import QUrl, QByteArray
|
|
||||||
from PySide6.QtGui import QImage, QTextCursor, QTextImageFormat, QColor
|
from PySide6.QtGui import QImage, QTextCursor, QTextImageFormat, QColor
|
||||||
from bouquin.theme import ThemeManager
|
from bouquin.theme import ThemeManager
|
||||||
from bouquin.editor import Editor
|
from bouquin.editor import Editor
|
||||||
|
|
|
||||||
136
tests/test_editor_more.py
Normal file
136
tests/test_editor_more.py
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
from PySide6.QtCore import Qt, QEvent, QUrl, QObject, Slot
|
||||||
|
from PySide6.QtGui import QImage, QMouseEvent, QTextCursor
|
||||||
|
from PySide6.QtTest import QTest
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
|
from bouquin.editor import Editor
|
||||||
|
from bouquin.theme import ThemeManager, ThemeConfig
|
||||||
|
|
||||||
|
|
||||||
|
def _mk_editor() -> Editor:
|
||||||
|
app = QApplication.instance()
|
||||||
|
tm = ThemeManager(app, ThemeConfig())
|
||||||
|
e = Editor(tm)
|
||||||
|
e.resize(700, 400)
|
||||||
|
e.show()
|
||||||
|
return e
|
||||||
|
|
||||||
|
|
||||||
|
def _point_for_char(e: Editor, pos: int):
|
||||||
|
c = e.textCursor()
|
||||||
|
c.setPosition(pos)
|
||||||
|
r = e.cursorRect(c)
|
||||||
|
return r.center()
|
||||||
|
|
||||||
|
|
||||||
|
def test_trim_url_and_linkify_and_ctrl_mouse(qtbot):
|
||||||
|
e = _mk_editor()
|
||||||
|
qtbot.addWidget(e)
|
||||||
|
assert e._trim_url_end("https://ex.com)") == "https://ex.com"
|
||||||
|
assert e._trim_url_end("www.mysite.org]") == "www.mysite.org"
|
||||||
|
|
||||||
|
url = "https://example.org/path"
|
||||||
|
QTest.keyClicks(e, url)
|
||||||
|
qtbot.waitUntil(lambda: url in e.toPlainText())
|
||||||
|
|
||||||
|
p = _point_for_char(e, 0)
|
||||||
|
move = QMouseEvent(
|
||||||
|
QEvent.MouseMove, p, Qt.NoButton, Qt.NoButton, Qt.ControlModifier
|
||||||
|
)
|
||||||
|
e.mouseMoveEvent(move)
|
||||||
|
assert e.viewport().cursor().shape() == Qt.PointingHandCursor
|
||||||
|
|
||||||
|
opened = {}
|
||||||
|
|
||||||
|
class Catcher(QObject):
|
||||||
|
@Slot(QUrl)
|
||||||
|
def handle(self, u: QUrl):
|
||||||
|
opened["u"] = u.toString()
|
||||||
|
|
||||||
|
from PySide6.QtGui import QDesktopServices
|
||||||
|
|
||||||
|
catcher = Catcher()
|
||||||
|
QDesktopServices.setUrlHandler("https", catcher, "handle")
|
||||||
|
try:
|
||||||
|
rel = QMouseEvent(
|
||||||
|
QEvent.MouseButtonRelease,
|
||||||
|
p,
|
||||||
|
Qt.LeftButton,
|
||||||
|
Qt.LeftButton,
|
||||||
|
Qt.ControlModifier,
|
||||||
|
)
|
||||||
|
e.mouseReleaseEvent(rel)
|
||||||
|
got_signal = []
|
||||||
|
e.linkActivated.connect(lambda href: got_signal.append(href))
|
||||||
|
e.mouseReleaseEvent(rel)
|
||||||
|
assert opened or got_signal
|
||||||
|
finally:
|
||||||
|
QDesktopServices.unsetUrlHandler("https")
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_images_and_image_helpers(qtbot, tmp_path):
|
||||||
|
e = _mk_editor()
|
||||||
|
qtbot.addWidget(e)
|
||||||
|
|
||||||
|
# No image under cursor yet (412 guard)
|
||||||
|
tc, fmt, orig = e._image_info_at_cursor()
|
||||||
|
assert tc is None and fmt is None and orig is None
|
||||||
|
|
||||||
|
# Insert a real image file (574–584 path)
|
||||||
|
img_path = tmp_path / "tiny.png"
|
||||||
|
img = QImage(4, 4, QImage.Format_ARGB32)
|
||||||
|
img.fill(0xFF336699)
|
||||||
|
assert img.save(str(img_path), "PNG")
|
||||||
|
e.insert_images([str(img_path)], autoscale=False)
|
||||||
|
assert "<img" in e.toHtml()
|
||||||
|
|
||||||
|
# Guards when not on an image (453, 464)
|
||||||
|
e._scale_image_at_cursor(1.1)
|
||||||
|
e._fit_image_to_editor_width()
|
||||||
|
|
||||||
|
|
||||||
|
def test_checkbox_click_and_enter_continuation(qtbot):
|
||||||
|
e = _mk_editor()
|
||||||
|
qtbot.addWidget(e)
|
||||||
|
e.setPlainText("☐ task one")
|
||||||
|
|
||||||
|
# Need it visible for mouse coords
|
||||||
|
e.resize(600, 300)
|
||||||
|
e.show()
|
||||||
|
qtbot.waitExposed(e)
|
||||||
|
|
||||||
|
# Click on the checkbox glyph to toggle (605–614)
|
||||||
|
start_point = _point_for_char(e, 0)
|
||||||
|
press = QMouseEvent(
|
||||||
|
QEvent.MouseButtonPress,
|
||||||
|
start_point,
|
||||||
|
Qt.LeftButton,
|
||||||
|
Qt.LeftButton,
|
||||||
|
Qt.NoModifier,
|
||||||
|
)
|
||||||
|
e.mousePressEvent(press)
|
||||||
|
assert e.toPlainText().startswith("☑ ")
|
||||||
|
|
||||||
|
# Press Enter at end -> new line with fresh checkbox (680–684)
|
||||||
|
c = e.textCursor()
|
||||||
|
c.movePosition(QTextCursor.End)
|
||||||
|
e.setTextCursor(c)
|
||||||
|
QTest.keyClick(e, Qt.Key_Return)
|
||||||
|
lines = e.toPlainText().splitlines()
|
||||||
|
assert len(lines) >= 2 and lines[1].startswith("☐ ")
|
||||||
|
|
||||||
|
|
||||||
|
def test_heading_and_lists_toggle_remove(qtbot):
|
||||||
|
e = _mk_editor()
|
||||||
|
qtbot.addWidget(e)
|
||||||
|
e.setPlainText("para")
|
||||||
|
|
||||||
|
# "Normal" path is size=0 (904…)
|
||||||
|
e.apply_heading(0)
|
||||||
|
|
||||||
|
# bullets twice -> second call removes (945–946)
|
||||||
|
e.toggle_bullets()
|
||||||
|
e.toggle_bullets()
|
||||||
|
# numbers twice -> second call removes (955–956)
|
||||||
|
e.toggle_numbers()
|
||||||
|
e.toggle_numbers()
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import runpy
|
import runpy
|
||||||
import types
|
import types
|
||||||
import sys
|
import sys
|
||||||
import builtins
|
|
||||||
|
|
||||||
|
|
||||||
def test_dunder_main_executes_without_launching_qt(monkeypatch):
|
def test_dunder_main_executes_without_launching_qt(monkeypatch):
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
import os
|
from PySide6.QtCore import QDate
|
||||||
from datetime import date, timedelta
|
|
||||||
from pathlib import Path
|
|
||||||
from PySide6.QtCore import QDate, QByteArray
|
|
||||||
from bouquin.theme import ThemeManager
|
from bouquin.theme import ThemeManager
|
||||||
from bouquin.main_window import MainWindow
|
from bouquin.main_window import MainWindow
|
||||||
from bouquin.settings import save_db_config
|
from bouquin.settings import save_db_config
|
||||||
|
|
|
||||||
15
tests/test_search_edgecase.py
Normal file
15
tests/test_search_edgecase.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
from bouquin.search import Search as SearchWidget
|
||||||
|
|
||||||
|
|
||||||
|
class DummyDB:
|
||||||
|
def search_entries(self, q):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_html_snippet_no_match_triggers_start_window(qtbot):
|
||||||
|
w = SearchWidget(db=DummyDB())
|
||||||
|
qtbot.addWidget(w)
|
||||||
|
html = "<p>" + ("x" * 300) + "</p>" # long text, no token present
|
||||||
|
frag, left, right = w._make_html_snippet(html, "notfound", radius=10, maxlen=80)
|
||||||
|
assert frag != ""
|
||||||
|
assert left is False and right is True
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
@ -47,7 +45,6 @@ def test_search_exception_path_closed_db_triggers_quiet_handling(app, fresh_db,
|
||||||
# Typing should not raise; exception path returns empty results
|
# Typing should not raise; exception path returns empty results
|
||||||
w._search("anything")
|
w._search("anything")
|
||||||
assert w.results.isHidden() # remains hidden because there are no rows
|
assert w.results.isHidden() # remains hidden because there are no rows
|
||||||
# Also, the "resultDatesChanged" signal should emit an empty list (coverage on that branch)
|
|
||||||
|
|
||||||
|
|
||||||
def test_make_html_snippet_ellipses_both_sides(app, fresh_db):
|
def test_make_html_snippet_ellipses_both_sides(app, fresh_db):
|
||||||
|
|
@ -59,3 +56,15 @@ def test_make_html_snippet_ellipses_both_sides(app, fresh_db):
|
||||||
assert snippet # non-empty
|
assert snippet # non-empty
|
||||||
assert left_ell is True
|
assert left_ell is True
|
||||||
assert right_ell is True
|
assert right_ell is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_results_middle(app, fresh_db, qtbot):
|
||||||
|
w = Search(fresh_db)
|
||||||
|
w.show()
|
||||||
|
qtbot.addWidget(w)
|
||||||
|
# Choose a query so that the first match sits well inside a long string,
|
||||||
|
# forcing both left and right ellipses.
|
||||||
|
assert fresh_db.connect()
|
||||||
|
|
||||||
|
w._search("middle")
|
||||||
|
assert w.results.isVisible()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import pytest
|
import pytest
|
||||||
from PySide6.QtWidgets import QWidget
|
|
||||||
from bouquin.search import Search
|
from bouquin.search import Search
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import types
|
|
||||||
import pytest
|
import pytest
|
||||||
from PySide6.QtCore import Qt
|
|
||||||
from PySide6.QtWidgets import QApplication, QDialog, QWidget
|
from PySide6.QtWidgets import QApplication, QDialog, QWidget
|
||||||
|
|
||||||
from bouquin.db import DBConfig, DBManager
|
from bouquin.db import DBConfig, DBManager
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue