from PySide6.QtCore import Qt
from PySide6.QtGui import QImage, QTextCursor, QTextImageFormat
from PySide6.QtTest import QTest
from bouquin.editor import Editor
import re
def _move_cursor_to_first_image(editor: Editor) -> QTextImageFormat | None:
c = editor.textCursor()
c.movePosition(QTextCursor.Start)
while True:
c2 = QTextCursor(c)
c2.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, 1)
if c2.position() == c.position():
break
fmt = c2.charFormat()
if fmt.isImageFormat():
editor.setTextCursor(c2)
return QTextImageFormat(fmt)
c.movePosition(QTextCursor.Right)
return None
def _fmt_at(editor: Editor, pos: int):
c = editor.textCursor()
c.setPosition(pos)
c.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, 1)
return c.charFormat()
def test_space_breaks_link_anchor_and_styling(qtbot):
e = Editor()
e.resize(600, 300)
e.show()
qtbot.waitExposed(e)
# Type a URL, which should be linkified (anchor + underline + blue)
url = "https://mig5.net"
QTest.keyClicks(e, url)
qtbot.waitUntil(lambda: e.toPlainText() == url)
# Sanity: characters within the URL are anchors
for i in range(len(url)):
assert _fmt_at(e, i).isAnchor()
# Hit Space – Editor.keyPressEvent() should call _break_anchor_for_next_char()
QTest.keyClick(e, Qt.Key_Space)
# Type some normal text; it must not inherit the link formatting
tail = "this is a test"
QTest.keyClicks(e, tail)
qtbot.waitUntil(lambda: e.toPlainText().endswith(tail))
txt = e.toPlainText()
# Find where our 'tail' starts
start = txt.index(tail)
end = start + len(tail)
# None of the trailing characters should be part of an anchor or visually underlined
for i in range(start, end):
fmt = _fmt_at(e, i)
assert not fmt.isAnchor(), f"char {i} unexpectedly still has an anchor"
assert not fmt.fontUnderline(), f"char {i} unexpectedly still underlined"
# Optional: ensure the HTML only wraps the URL in , not the trailing text
html = e.document().toHtml()
assert re.search(
r']*href="https?://mig5\.net"[^>]*>(?:]*>)?https?://mig5\.net(?:)?\s+this is a test',
html,
re.S,
), html
assert "this is a test" not in html
def test_embed_qimage_saved_as_data_url(qtbot):
e = Editor()
e.resize(600, 400)
qtbot.addWidget(e)
e.show()
qtbot.waitExposed(e)
img = QImage(60, 40, QImage.Format_ARGB32)
img.fill(0xFF336699)
e._insert_qimage_at_cursor(img, autoscale=False)
html = e.to_html_with_embedded_images()
assert "data:image/png;base64," in html
def test_insert_images_autoscale_and_fit(qtbot, tmp_path):
# Create a very wide image so autoscale triggers
big = QImage(2000, 800, QImage.Format_ARGB32)
big.fill(0xFF00FF00)
big_path = tmp_path / "big.png"
big.save(str(big_path))
e = Editor()
e.resize(420, 300) # known viewport width
qtbot.addWidget(e)
e.show()
qtbot.waitExposed(e)
e.insert_images([str(big_path)], autoscale=True)
# Cursor lands after the image + a blank block; helper will select the image char
fmt = _move_cursor_to_first_image(e)
assert fmt is not None
# After autoscale, width should be <= ~92% of viewport
max_w = int(e.viewport().width() * 0.92)
assert fmt.width() <= max_w + 1 # allow off-by-1 from rounding
# Now exercise "fit to editor width"
e._fit_image_to_editor_width()
_tc, fmt2, _orig = e._image_info_at_cursor()
assert fmt2 is not None
assert abs(fmt2.width() - max_w) <= 1
def test_linkify_trims_trailing_punctuation(qtbot):
e = Editor()
qtbot.addWidget(e)
e.show()
qtbot.waitExposed(e)
e.setPlainText("See (https://example.com).")
# Wait until linkification runs (connected to textChanged)
qtbot.waitUntil(lambda: 'href="' in e.document().toHtml())
html = e.document().toHtml()
# Anchor should *not* include the closing ')'
assert 'href="https://example.com"' in html
assert 'href="https://example.com)."' not in html
def test_space_does_not_bleed_anchor_format(qtbot):
e = Editor()
qtbot.addWidget(e)
e.show()
qtbot.waitExposed(e)
e.setPlainText("https://a.example")
qtbot.waitUntil(lambda: 'href="' in e.document().toHtml())
c = e.textCursor()
c.movePosition(QTextCursor.End)
e.setTextCursor(c)
# Press Space; keyPressEvent should break the anchor for the next char
QTest.keyClick(e, Qt.Key_Space)
assert e.currentCharFormat().isAnchor() is False
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 = Editor()
qtbot.addWidget(e)
e.show()
qtbot.waitExposed(e)
e.setPlainText("code")
c = e.textCursor()
c.select(QTextCursor.BlockUnderCursor)
e.setTextCursor(c)
e.apply_code()
# Put caret at end of the code block, then Enter to create an empty line *inside* the frame
c = e.textCursor()
c.movePosition(QTextCursor.EndOfBlock)
e.setTextCursor(c)
QTest.keyClick(e, Qt.Key_Return)
# Ensure we are on an empty block *inside* the code frame
qtbot.waitUntil(
lambda: e._find_code_frame(e.textCursor()) is not None
and e.textCursor().block().length() == 1
)
# Second Enter should jump *out* of the frame
QTest.keyClick(e, Qt.Key_Return)
# qtbot.waitUntil(lambda: e._find_code_frame(e.textCursor()) is None)