2261 lines
64 KiB
Python
2261 lines
64 KiB
Python
import base64
|
|
import pytest
|
|
|
|
from PySide6.QtCore import Qt, QPoint
|
|
from PySide6.QtGui import (
|
|
QImage,
|
|
QColor,
|
|
QKeyEvent,
|
|
QTextCursor,
|
|
QTextDocument,
|
|
QFont,
|
|
QTextCharFormat,
|
|
)
|
|
from PySide6.QtWidgets import QApplication, QTextEdit
|
|
|
|
from bouquin.markdown_editor import MarkdownEditor
|
|
from bouquin.markdown_highlighter import MarkdownHighlighter
|
|
from bouquin.theme import ThemeManager, ThemeConfig, Theme
|
|
|
|
|
|
def _today():
|
|
from datetime import date
|
|
|
|
return date.today().isoformat()
|
|
|
|
|
|
def text(editor) -> str:
|
|
return editor.toPlainText()
|
|
|
|
|
|
def lines_keep(editor):
|
|
"""Split preserving a trailing empty line if the text ends with '\\n'."""
|
|
return text(editor).split("\n")
|
|
|
|
|
|
def press_backtick(qtbot, widget, n=1):
|
|
"""Send physical backtick key events (avoid IME/dead-key issues)."""
|
|
for _ in range(n):
|
|
qtbot.keyClick(widget, Qt.Key_QuoteLeft)
|
|
|
|
|
|
@pytest.fixture
|
|
def editor(app, qtbot):
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
ed = MarkdownEditor(themes)
|
|
qtbot.addWidget(ed)
|
|
ed.show()
|
|
qtbot.waitExposed(ed)
|
|
ed.setFocus()
|
|
return ed
|
|
|
|
|
|
@pytest.fixture
|
|
def editor_hello(app):
|
|
tm = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
e = MarkdownEditor(tm)
|
|
e.setPlainText("hello")
|
|
e.moveCursor(QTextCursor.MoveOperation.End)
|
|
return e
|
|
|
|
|
|
def test_from_and_to_markdown_roundtrip(editor):
|
|
md = "# Title\n\nThis is **bold** and _italic_ and ~~strike~~.\n\n- [ ] task\n- [x] done\n\n```\ncode\n```"
|
|
editor.from_markdown(md)
|
|
out = editor.to_markdown()
|
|
assert "Title" in out and "task" in out and "code" in out
|
|
|
|
|
|
def test_apply_styles_and_headings(editor, qtbot):
|
|
editor.from_markdown("hello world")
|
|
editor.selectAll()
|
|
editor.apply_weight()
|
|
editor.apply_italic()
|
|
editor.apply_strikethrough()
|
|
editor.apply_heading(24)
|
|
md = editor.to_markdown()
|
|
assert "**" in md and "*~~~~*" in md
|
|
|
|
|
|
def test_toggle_lists_and_checkboxes(editor):
|
|
editor.from_markdown("item one\nitem two\n")
|
|
editor.toggle_bullets()
|
|
assert "- " in editor.to_markdown()
|
|
editor.toggle_numbers()
|
|
assert "1. " in editor.to_markdown()
|
|
editor.toggle_checkboxes()
|
|
md = editor.to_markdown()
|
|
assert "- [ ]" in md or "- [x]" in md
|
|
|
|
|
|
def test_insert_image_from_path(editor, tmp_path):
|
|
img = tmp_path / "pic.png"
|
|
qimg = QImage(2, 2, QImage.Format_RGBA8888)
|
|
qimg.fill(QColor(255, 0, 0))
|
|
assert qimg.save(str(img)) # ensure a valid PNG on disk
|
|
|
|
editor.insert_image_from_path(img)
|
|
md = editor.to_markdown()
|
|
# Accept either "image/png" or older "image/image/png" prefix
|
|
assert "data:image/png;base64" in md or "data:image/image/png;base64" in md
|
|
|
|
|
|
def test_checkbox_toggle_by_click(editor, qtbot):
|
|
# Load a markdown checkbox
|
|
editor.from_markdown("- [ ] task here")
|
|
# Verify display token present
|
|
display = editor.toPlainText()
|
|
assert "☐" in display
|
|
|
|
# Click on the first character region to toggle
|
|
c = editor.textCursor()
|
|
c.movePosition(QTextCursor.StartOfBlock)
|
|
editor.setTextCursor(c)
|
|
r = editor.cursorRect()
|
|
center = r.center()
|
|
pos = QPoint(r.left() + 2, center.y())
|
|
qtbot.mouseClick(editor.viewport(), Qt.LeftButton, pos=pos)
|
|
|
|
# Should have toggled to checked icon
|
|
display2 = editor.toPlainText()
|
|
assert "☑" in display2
|
|
|
|
|
|
def test_apply_heading_levels(editor, qtbot):
|
|
editor.setPlainText("hello")
|
|
editor.selectAll()
|
|
# H2
|
|
editor.apply_heading(18)
|
|
assert editor.toPlainText().startswith("## ")
|
|
# H3
|
|
editor.selectAll()
|
|
editor.apply_heading(14)
|
|
assert editor.toPlainText().startswith("### ")
|
|
# Normal (no heading)
|
|
editor.selectAll()
|
|
editor.apply_heading(12)
|
|
assert not editor.toPlainText().startswith("#")
|
|
|
|
|
|
def test_enter_on_nonempty_list_continues(qtbot, editor):
|
|
qtbot.addWidget(editor)
|
|
editor.show()
|
|
editor.from_markdown("- item")
|
|
c = editor.textCursor()
|
|
c.movePosition(QTextCursor.End)
|
|
editor.setTextCursor(c)
|
|
|
|
ev = QKeyEvent(QKeyEvent.KeyPress, Qt.Key_Return, Qt.NoModifier, "\n")
|
|
editor.keyPressEvent(ev)
|
|
txt = editor.toPlainText()
|
|
assert "\n\u2022 " in txt
|
|
|
|
|
|
def test_enter_on_empty_list_marks_empty(qtbot, editor):
|
|
qtbot.addWidget(editor)
|
|
editor.show()
|
|
editor.from_markdown("- ")
|
|
c = editor.textCursor()
|
|
c.movePosition(QTextCursor.End)
|
|
editor.setTextCursor(c)
|
|
|
|
ev = QKeyEvent(QKeyEvent.KeyPress, Qt.Key_Return, Qt.NoModifier, "\n")
|
|
editor.keyPressEvent(ev)
|
|
assert editor.toPlainText().startswith("\u2022 \n")
|
|
|
|
|
|
def test_triple_backtick_autoexpands(editor, qtbot):
|
|
editor.from_markdown("")
|
|
press_backtick(qtbot, editor, 2)
|
|
press_backtick(qtbot, editor, 1) # triggers expansion
|
|
qtbot.wait(0)
|
|
|
|
t = text(editor)
|
|
assert t.count("```") == 2
|
|
assert t.startswith("```\n\n```")
|
|
assert t.endswith("\n")
|
|
# caret is on the blank line inside the block
|
|
assert editor.textCursor().blockNumber() == 1
|
|
assert lines_keep(editor)[1] == ""
|
|
|
|
|
|
def test_toolbar_inserts_block_on_own_lines(editor, qtbot):
|
|
editor.from_markdown("hello")
|
|
editor.moveCursor(QTextCursor.End)
|
|
editor.apply_code() # </> action inserts fenced code block
|
|
qtbot.wait(0)
|
|
|
|
t = text(editor)
|
|
assert "hello```" not in t # never inline
|
|
assert t.startswith("hello\n```")
|
|
assert t.endswith("```\n")
|
|
# caret inside block (blank line)
|
|
assert editor.textCursor().blockNumber() == 2
|
|
assert lines_keep(editor)[2] == ""
|
|
|
|
|
|
def test_toolbar_inside_block_does_not_insert_inline_fences(editor, qtbot):
|
|
editor.from_markdown("")
|
|
editor.apply_code() # create a block (caret now on blank line inside)
|
|
qtbot.wait(0)
|
|
|
|
pos_before = editor.textCursor().position()
|
|
t_before = text(editor)
|
|
|
|
editor.apply_code() # pressing </> inside should be a no-op
|
|
qtbot.wait(0)
|
|
|
|
assert text(editor) == t_before
|
|
assert editor.textCursor().position() == pos_before
|
|
|
|
|
|
def test_toolbar_on_opening_fence_jumps_inside(editor, qtbot):
|
|
editor.from_markdown("")
|
|
editor.apply_code()
|
|
qtbot.wait(0)
|
|
|
|
# Go to opening fence (line 0)
|
|
editor.moveCursor(QTextCursor.Start)
|
|
editor.apply_code() # should jump inside the block
|
|
qtbot.wait(0)
|
|
|
|
assert editor.textCursor().blockNumber() == 1
|
|
assert lines_keep(editor)[1] == ""
|
|
|
|
|
|
def test_toolbar_on_closing_fence_jumps_out(editor, qtbot):
|
|
editor.from_markdown("")
|
|
editor.apply_code()
|
|
qtbot.wait(0)
|
|
|
|
# Go to closing fence line (template: 0 fence, 1 blank, 2 fence, 3 blank-after)
|
|
editor.moveCursor(QTextCursor.End) # blank-after
|
|
editor.moveCursor(QTextCursor.Up) # closing fence
|
|
editor.moveCursor(QTextCursor.StartOfLine)
|
|
|
|
editor.apply_code() # jump to the line after the fence
|
|
qtbot.wait(0)
|
|
|
|
# Now on the blank line after the block
|
|
assert editor.textCursor().block().text() == ""
|
|
assert editor.textCursor().block().previous().text().strip() == "```"
|
|
|
|
|
|
def test_down_escapes_from_last_code_line(editor, qtbot):
|
|
editor.from_markdown("```\nLINE\n```\n")
|
|
# Put caret at end of "LINE"
|
|
editor.moveCursor(QTextCursor.Start)
|
|
editor.moveCursor(QTextCursor.Down) # "LINE"
|
|
editor.moveCursor(QTextCursor.EndOfLine)
|
|
|
|
qtbot.keyPress(editor, Qt.Key_Down) # hop after closing fence
|
|
qtbot.wait(0)
|
|
|
|
# caret now on the blank line after the fence
|
|
assert editor.textCursor().block().text() == ""
|
|
assert editor.textCursor().block().previous().text().strip() == "```"
|
|
|
|
|
|
def test_down_on_closing_fence_at_eof_creates_line(editor, qtbot):
|
|
editor.from_markdown("```\ncode\n```") # no trailing newline
|
|
# caret on closing fence line
|
|
editor.moveCursor(QTextCursor.End)
|
|
editor.moveCursor(QTextCursor.StartOfLine)
|
|
|
|
qtbot.keyPress(editor, Qt.Key_Down) # should append newline and move there
|
|
qtbot.wait(0)
|
|
|
|
# Do NOT use splitlines() here—preserve trailing blank line
|
|
assert text(editor).endswith("\n")
|
|
assert editor.textCursor().block().text() == "" # on the new blank line
|
|
assert editor.textCursor().block().previous().text().strip() == "```"
|
|
|
|
|
|
def test_no_orphan_two_backticks_lines_after_edits(editor, qtbot):
|
|
editor.from_markdown("")
|
|
# create a block via typing
|
|
press_backtick(qtbot, editor, 3)
|
|
qtbot.keyClicks(editor, "x")
|
|
qtbot.keyPress(editor, Qt.Key_Down) # escape
|
|
editor.apply_code() # add second block via toolbar
|
|
qtbot.wait(0)
|
|
|
|
# ensure there are no stray "``" lines
|
|
assert not any(ln.strip() == "``" for ln in lines_keep(editor))
|
|
|
|
|
|
def _fmt_at(block, pos):
|
|
"""Return a *copy* of the char format at pos so it doesn't dangle."""
|
|
layout = block.layout()
|
|
for fr in list(layout.formats()):
|
|
if fr.start <= pos < fr.start + fr.length:
|
|
return QTextCharFormat(fr.format)
|
|
return None
|
|
|
|
|
|
@pytest.fixture
|
|
def highlighter(app):
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
hl = MarkdownHighlighter(doc, themes)
|
|
return doc, hl
|
|
|
|
|
|
def test_headings_and_inline_styles(highlighter):
|
|
doc, hl = highlighter
|
|
doc.setPlainText("# H1\n## H2\n### H3\n***b+i*** **b** *i* __b__ _i_\n")
|
|
hl.rehighlight()
|
|
|
|
# H1: '#' markers hidden (very small size), text bold/larger
|
|
b0 = doc.findBlockByNumber(0)
|
|
fmt_marker = _fmt_at(b0, 0)
|
|
assert fmt_marker is not None
|
|
assert fmt_marker.fontPointSize() <= 0.2 # marker hidden
|
|
|
|
fmt_h1_text = _fmt_at(b0, 2)
|
|
assert fmt_h1_text is not None
|
|
assert fmt_h1_text.fontWeight() == QFont.Weight.Bold
|
|
|
|
# Bold-italic precedence
|
|
b3 = doc.findBlockByNumber(3)
|
|
line = b3.text()
|
|
triple = "***b+i***"
|
|
start = line.find(triple)
|
|
assert start != -1
|
|
pos_inside = start + 3 # skip the *** markers, land on 'b'
|
|
f_bi_inner = _fmt_at(b3, pos_inside)
|
|
assert f_bi_inner is not None
|
|
assert f_bi_inner.fontWeight() == QFont.Weight.Bold and f_bi_inner.fontItalic()
|
|
|
|
# Bold without triples
|
|
f_b = _fmt_at(b3, b3.text().find("**b**") + 2)
|
|
assert f_b.fontWeight() == QFont.Weight.Bold
|
|
|
|
# Italic without bold
|
|
f_i = _fmt_at(b3, b3.text().rfind("_i_") + 1)
|
|
assert f_i.fontItalic()
|
|
|
|
|
|
def test_code_blocks_inline_code_and_strike_overlay(highlighter):
|
|
doc, hl = highlighter
|
|
doc.setPlainText("```\n**B**\n```\nX ~~**boom**~~ Y `code`\n")
|
|
hl.rehighlight()
|
|
|
|
# Fence and inner lines use code block format
|
|
fence = doc.findBlockByNumber(0)
|
|
inner = doc.findBlockByNumber(1)
|
|
|
|
fmt_fence = _fmt_at(fence, 0)
|
|
fmt_inner = _fmt_at(inner, 0)
|
|
assert fmt_fence is not None and fmt_inner is not None
|
|
|
|
# check key properties
|
|
assert fmt_inner.fontFixedPitch() or fmt_inner.font().styleHint() == QFont.Monospace
|
|
assert fmt_inner.background() == hl.code_block_format.background()
|
|
|
|
# Inline code uses fixed pitch and hides the backticks
|
|
inline = doc.findBlockByNumber(3)
|
|
start = inline.text().find("`code`")
|
|
|
|
fmt_inline_char = _fmt_at(inline, start + 1)
|
|
fmt_inline_tick = _fmt_at(inline, start)
|
|
assert fmt_inline_char is not None and fmt_inline_tick is not None
|
|
assert fmt_inline_char.fontFixedPitch()
|
|
assert fmt_inline_tick.fontPointSize() <= 0.2 # backtick hidden
|
|
|
|
boom_pos = inline.text().find("boom")
|
|
fmt_boom = _fmt_at(inline, boom_pos)
|
|
assert fmt_boom is not None
|
|
assert fmt_boom.fontStrikeOut() and fmt_boom.fontWeight() == QFont.Weight.Bold
|
|
|
|
|
|
def test_theme_change_rehighlight(highlighter):
|
|
doc, hl = highlighter
|
|
hl._on_theme_changed()
|
|
doc.setPlainText("`x`")
|
|
hl.rehighlight()
|
|
b = doc.firstBlock()
|
|
fmt = _fmt_at(b, 1)
|
|
assert fmt is not None and fmt.fontFixedPitch()
|
|
|
|
|
|
@pytest.fixture
|
|
def hl_light(app):
|
|
# Light theme path
|
|
tm = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
hl = MarkdownHighlighter(doc, tm)
|
|
return doc, hl
|
|
|
|
|
|
@pytest.fixture
|
|
def hl_light_edit(app, qtbot):
|
|
tm = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
edit = QTextEdit() # <-- give the doc a layout
|
|
edit.setDocument(doc)
|
|
qtbot.addWidget(edit)
|
|
edit.show()
|
|
qtbot.wait(10) # let Qt build the layouts
|
|
hl = MarkdownHighlighter(doc, tm)
|
|
return doc, hl, edit
|
|
|
|
|
|
def fmt(doc, block_no, pos):
|
|
"""Return the QTextCharFormat at character position `pos` in the given block."""
|
|
b = doc.findBlockByNumber(block_no)
|
|
it = b.begin()
|
|
off = 0
|
|
while not it.atEnd():
|
|
frag = it.fragment()
|
|
length = frag.length() # includes chars in this fragment
|
|
if off + length > pos:
|
|
return frag.charFormat()
|
|
off += length
|
|
it = it.next()
|
|
# Fallback (shouldn't happen in our tests)
|
|
cf = QTextCharFormat()
|
|
return cf
|
|
|
|
|
|
def test_light_palette_specific_colors(hl_light_edit, qtbot):
|
|
doc, hl, edit = hl_light_edit
|
|
doc.setPlainText("```\ncode\n```")
|
|
hl.rehighlight()
|
|
# the second block ("code") is the one inside the fenced block
|
|
b_code = doc.firstBlock().next()
|
|
fmt = _fmt_at(b_code, 0)
|
|
assert fmt is not None and fmt.background().style() != 0
|
|
|
|
|
|
def test_code_block_light_colors(hl_light):
|
|
"""Ensure code block colors use the light palette (covers 74-75)."""
|
|
doc, hl = hl_light
|
|
doc.setPlainText("```\ncode\n```")
|
|
hl.rehighlight()
|
|
# Background is a light gray and text is dark/black-ish in light theme
|
|
bg = hl.code_block_format.background().color()
|
|
fg = hl.code_block_format.foreground().color()
|
|
assert bg.red() >= 240 and bg.green() >= 240 and bg.blue() >= 240
|
|
assert fg.red() < 40 and fg.green() < 40 and fg.blue() < 40
|
|
|
|
|
|
def test_end_guard_skips_italic_followed_by_marker(hl_light):
|
|
"""
|
|
Triggers the end-following guard for italic e.g. '*i**'.
|
|
"""
|
|
doc, hl = hl_light
|
|
doc.setPlainText("*i**")
|
|
hl.rehighlight()
|
|
# The 'i' should not get italic due to the guard (closing '*' followed by '*')
|
|
f = fmt(doc, 0, 1)
|
|
assert not f.fontItalic()
|
|
|
|
|
|
def test_char_rect_at_edges_and_click_checkbox(editor, qtbot):
|
|
"""
|
|
Exercises char_rect_at()-style logic and checkbox toggle via click
|
|
to push coverage on geometry-dependent paths.
|
|
"""
|
|
editor.from_markdown("- [ ] task")
|
|
c = editor.textCursor()
|
|
c.movePosition(QTextCursor.StartOfBlock)
|
|
editor.setTextCursor(c)
|
|
r = editor.cursorRect()
|
|
qtbot.mouseClick(editor.viewport(), Qt.LeftButton, pos=r.center())
|
|
assert "☑" in editor.toPlainText()
|
|
|
|
|
|
def test_heading_apply_levels_and_inline_styles(editor):
|
|
editor.setPlainText("hello")
|
|
editor.selectAll()
|
|
editor.apply_heading(18) # H2
|
|
assert editor.toPlainText().startswith("## ")
|
|
editor.selectAll()
|
|
editor.apply_heading(12) # normal
|
|
assert not editor.toPlainText().startswith("#")
|
|
|
|
# Bold/italic/strike together to nudge style branches
|
|
editor.setPlainText("hi")
|
|
editor.selectAll()
|
|
editor.apply_weight()
|
|
editor.apply_italic()
|
|
editor.apply_strikethrough()
|
|
md = editor.to_markdown()
|
|
assert "**" in md and "*" in md and "~~" in md
|
|
|
|
|
|
def test_insert_image_and_markdown_roundtrip(editor, tmp_path):
|
|
img = tmp_path / "p.png"
|
|
qimg = QImage(2, 2, QImage.Format_RGBA8888)
|
|
qimg.fill(QColor(255, 0, 0))
|
|
assert qimg.save(str(img))
|
|
editor.insert_image_from_path(img)
|
|
# At least a replacement char shows in the plain-text view
|
|
assert "\ufffc" in editor.toPlainText()
|
|
# And markdown contains a data: URI
|
|
assert "data:image" in editor.to_markdown()
|
|
|
|
|
|
def test_apply_italic_and_strike(editor):
|
|
# Italic: insert markers with no selection and place caret in between
|
|
editor.setPlainText("x")
|
|
editor.moveCursor(QTextCursor.MoveOperation.End)
|
|
editor.apply_italic()
|
|
assert editor.toPlainText().endswith("x**")
|
|
assert editor.textCursor().position() == len(editor.toPlainText()) - 1
|
|
|
|
# With selection toggling
|
|
editor.setPlainText("*y*")
|
|
c = editor.textCursor()
|
|
c.movePosition(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.MoveAnchor)
|
|
c.movePosition(QTextCursor.MoveOperation.Start, QTextCursor.MoveMode.KeepAnchor)
|
|
editor.setTextCursor(c)
|
|
editor.apply_italic()
|
|
assert editor.toPlainText() == "y"
|
|
|
|
# Strike: no selection case inserts placeholder and moves caret
|
|
editor.setPlainText("z")
|
|
editor.moveCursor(QTextCursor.MoveOperation.End)
|
|
editor.apply_strikethrough()
|
|
assert editor.toPlainText().endswith("z~~~~")
|
|
assert editor.textCursor().position() == len(editor.toPlainText()) - 2
|
|
|
|
|
|
def test_apply_code_inline_block_navigation(editor):
|
|
# Selection case -> fenced block around selection
|
|
editor.setPlainText("code")
|
|
c = editor.textCursor()
|
|
c.select(QTextCursor.SelectionType.Document)
|
|
editor.setTextCursor(c)
|
|
editor.apply_code()
|
|
assert "```\ncode\n```\n" in editor.toPlainText()
|
|
|
|
# No selection, at EOF with no following block -> creates block and extra newline path
|
|
editor.setPlainText("before")
|
|
editor.moveCursor(QTextCursor.MoveOperation.End)
|
|
editor.apply_code()
|
|
t = editor.toPlainText()
|
|
assert t.endswith("before\n```\n\n```\n")
|
|
# Caret should be inside the code block blank line
|
|
assert editor.textCursor().position() == len("before\n") + 4
|
|
|
|
|
|
def test_insert_image_from_path_invalid_returns(editor_hello, tmp_path):
|
|
# Non-existent path should just return (early exit)
|
|
bad = tmp_path / "missing.png"
|
|
editor_hello.insert_image_from_path(bad)
|
|
# Nothing new added
|
|
assert editor_hello.toPlainText() == "hello"
|
|
|
|
|
|
# ============================================================================
|
|
# setDocument Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_markdown_editor_set_document(app):
|
|
"""Test setting a new document on the editor"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
|
|
# Create a new document
|
|
new_doc = QTextDocument()
|
|
new_doc.setPlainText("New document content")
|
|
|
|
# Set the document
|
|
editor.setDocument(new_doc)
|
|
|
|
# Verify document was set
|
|
assert editor.document() == new_doc
|
|
assert "New document content" in editor.toPlainText()
|
|
|
|
|
|
def test_markdown_editor_set_document_with_highlighter(app):
|
|
"""Test setting document preserves highlighter"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
|
|
# Ensure highlighter exists
|
|
assert hasattr(editor, "highlighter")
|
|
|
|
# Create and set new document
|
|
new_doc = QTextDocument()
|
|
new_doc.setPlainText("# Heading")
|
|
editor.setDocument(new_doc)
|
|
|
|
# Highlighter should be attached to new document
|
|
assert editor.highlighter.document() == new_doc
|
|
|
|
|
|
# ============================================================================
|
|
# showEvent Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_markdown_editor_show_event(app, qtbot):
|
|
"""Test showEvent triggers code block background update"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.from_markdown("```python\ncode\n```")
|
|
|
|
# Show the editor
|
|
editor.show()
|
|
qtbot.waitExposed(editor)
|
|
|
|
# Process events to let QTimer.singleShot fire
|
|
QApplication.processEvents()
|
|
|
|
# Editor should be visible
|
|
assert editor.isVisible()
|
|
|
|
|
|
# ============================================================================
|
|
# Checkbox Transformation Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_markdown_editor_transform_unchecked_checkbox(app, qtbot):
|
|
"""Test transforming - [ ] to unchecked checkbox"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.show()
|
|
qtbot.waitExposed(editor)
|
|
|
|
# Type checkbox markdown
|
|
editor.insertPlainText("- [ ] Task")
|
|
|
|
# Process events to let transformation happen
|
|
QApplication.processEvents()
|
|
|
|
# Should contain checkbox character
|
|
text = editor.toPlainText()
|
|
assert editor._CHECK_UNCHECKED_DISPLAY in text
|
|
|
|
|
|
def test_markdown_editor_transform_checked_checkbox(app, qtbot):
|
|
"""Test transforming - [x] to checked checkbox"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.show()
|
|
qtbot.waitExposed(editor)
|
|
|
|
# Type checked checkbox markdown
|
|
editor.insertPlainText("- [x] Done")
|
|
|
|
# Process events
|
|
QApplication.processEvents()
|
|
|
|
# Should contain checked checkbox character
|
|
text = editor.toPlainText()
|
|
assert editor._CHECK_CHECKED_DISPLAY in text
|
|
|
|
|
|
def test_markdown_editor_transform_todo(app, qtbot):
|
|
"""Test transforming TODO to unchecked checkbox"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.show()
|
|
qtbot.waitExposed(editor)
|
|
|
|
# Type TODO
|
|
editor.insertPlainText("TODO: Important task")
|
|
|
|
# Process events
|
|
QApplication.processEvents()
|
|
|
|
# Should contain checkbox and no TODO
|
|
text = editor.toPlainText()
|
|
assert editor._CHECK_UNCHECKED_DISPLAY in text
|
|
|
|
|
|
def test_markdown_editor_transform_todo_with_indent(app, qtbot):
|
|
"""Test transforming indented TODO"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.show()
|
|
qtbot.waitExposed(editor)
|
|
|
|
# Type indented TODO
|
|
editor.insertPlainText(" TODO: Indented task")
|
|
|
|
# Process events
|
|
QApplication.processEvents()
|
|
|
|
# Should handle indented TODO
|
|
text = editor.toPlainText()
|
|
assert editor._CHECK_UNCHECKED_DISPLAY in text
|
|
|
|
|
|
def test_markdown_editor_transform_todo_with_colon(app, qtbot):
|
|
"""Test transforming TODO: with colon"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.show()
|
|
qtbot.waitExposed(editor)
|
|
|
|
# Type TODO with colon
|
|
editor.insertPlainText("TODO: Task with colon")
|
|
|
|
# Process events
|
|
QApplication.processEvents()
|
|
|
|
text = editor.toPlainText()
|
|
assert editor._CHECK_UNCHECKED_DISPLAY in text
|
|
|
|
|
|
def test_markdown_editor_transform_todo_with_dash(app, qtbot):
|
|
"""Test transforming TODO- with dash"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.show()
|
|
qtbot.waitExposed(editor)
|
|
|
|
# Type TODO with dash
|
|
editor.insertPlainText("TODO- Task with dash")
|
|
|
|
# Process events
|
|
QApplication.processEvents()
|
|
|
|
text = editor.toPlainText()
|
|
assert editor._CHECK_UNCHECKED_DISPLAY in text
|
|
|
|
|
|
def test_markdown_editor_no_transform_when_updating(app):
|
|
"""Test that transformation doesn't happen when _updating flag is set"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
|
|
# Set updating flag
|
|
editor._updating = True
|
|
|
|
# Try to insert checkbox markdown
|
|
editor.insertPlainText("- [ ] Task")
|
|
|
|
# Should NOT transform since _updating is True
|
|
# This tests the early return in _on_text_changed
|
|
assert editor._updating
|
|
|
|
|
|
# ============================================================================
|
|
# Code Block Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_markdown_editor_is_inside_code_block(app):
|
|
"""Test detecting if cursor is inside code block"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.from_markdown("```python\ncode here\n```\noutside")
|
|
|
|
# Move cursor to inside code block
|
|
cursor = editor.textCursor()
|
|
cursor.setPosition(10) # Inside the code block
|
|
editor.setTextCursor(cursor)
|
|
|
|
block = cursor.block()
|
|
# Test the method exists and can be called
|
|
result = editor._is_inside_code_block(block)
|
|
assert isinstance(result, bool)
|
|
|
|
|
|
def test_markdown_editor_code_block_spacing(app):
|
|
"""Test code block spacing application"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.from_markdown("```python\nline1\nline2\n```")
|
|
|
|
# Apply code block spacing
|
|
editor._apply_code_block_spacing()
|
|
|
|
# Should complete without error
|
|
assert True
|
|
|
|
|
|
def test_markdown_editor_update_code_block_backgrounds(app):
|
|
"""Test updating code block backgrounds"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.from_markdown("```python\ncode\n```")
|
|
|
|
# Update backgrounds
|
|
editor._update_code_block_row_backgrounds()
|
|
|
|
# Should complete without error
|
|
assert True
|
|
|
|
|
|
# ============================================================================
|
|
# Image Insertion Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_markdown_editor_insert_image_from_path(app, tmp_path):
|
|
"""Test inserting image from file path"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
|
|
# Create a real PNG image (1x1 pixel)
|
|
# PNG file signature + minimal valid PNG data
|
|
png_data = (
|
|
b"\x89PNG\r\n\x1a\n" # PNG signature
|
|
b"\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
|
|
b"\x08\x06\x00\x00\x00\x1f\x15\xc4\x89" # IHDR chunk
|
|
b"\x00\x00\x00\nIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01"
|
|
b"\r\n-\xb4" # IDAT chunk
|
|
b"\x00\x00\x00\x00IEND\xaeB`\x82" # IEND chunk
|
|
)
|
|
image_path = tmp_path / "test.png"
|
|
image_path.write_bytes(png_data)
|
|
|
|
# Insert image
|
|
editor.insert_image_from_path(image_path)
|
|
|
|
# Check that document has content (image + newline)
|
|
# Images don't show in toPlainText() but affect document structure
|
|
doc = editor.document()
|
|
assert doc.characterCount() > 1 # Should have image char + newline
|
|
|
|
|
|
# ============================================================================
|
|
# Formatting Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_markdown_editor_toggle_bold_empty_selection(app):
|
|
"""Test toggling bold with no selection"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.insertPlainText("text")
|
|
|
|
# Move cursor to middle of text (no selection)
|
|
cursor = editor.textCursor()
|
|
cursor.setPosition(2)
|
|
editor.setTextCursor(cursor)
|
|
|
|
# Toggle bold (inserts ** markers with cursor between them)
|
|
editor.apply_weight()
|
|
|
|
# Should have inserted bold markers
|
|
text = editor.toPlainText()
|
|
assert "**" in text
|
|
|
|
# Should handle empty selection
|
|
assert True
|
|
|
|
|
|
def test_markdown_editor_toggle_italic_empty_selection(app):
|
|
"""Test toggling italic with no selection"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.insertPlainText("text")
|
|
|
|
# Move cursor to middle (no selection)
|
|
cursor = editor.textCursor()
|
|
cursor.setPosition(2)
|
|
editor.setTextCursor(cursor)
|
|
|
|
# Toggle italic
|
|
editor.apply_italic()
|
|
|
|
# Should handle empty selection
|
|
assert True
|
|
|
|
|
|
def test_markdown_editor_toggle_strikethrough_empty_selection(app):
|
|
"""Test toggling strikethrough with no selection"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.insertPlainText("text")
|
|
|
|
cursor = editor.textCursor()
|
|
cursor.setPosition(2)
|
|
editor.setTextCursor(cursor)
|
|
|
|
editor.apply_strikethrough()
|
|
|
|
assert True
|
|
|
|
|
|
def test_markdown_editor_toggle_code_empty_selection(app):
|
|
"""Test toggling code with no selection"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.insertPlainText("text")
|
|
|
|
cursor = editor.textCursor()
|
|
cursor.setPosition(2)
|
|
editor.setTextCursor(cursor)
|
|
|
|
editor.apply_code()
|
|
|
|
assert True
|
|
|
|
|
|
# ============================================================================
|
|
# Heading Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_markdown_editor_set_heading_various_levels(app):
|
|
"""Test setting different heading levels"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
|
|
for level in [14, 18, 24]:
|
|
editor.clear()
|
|
editor.insertPlainText("Heading text")
|
|
|
|
# Select all
|
|
cursor = editor.textCursor()
|
|
cursor.select(QTextCursor.Document)
|
|
editor.setTextCursor(cursor)
|
|
|
|
# Set heading level
|
|
editor.apply_heading(level)
|
|
|
|
# Should have heading markdown
|
|
text = editor.toPlainText()
|
|
assert "#" in text
|
|
|
|
|
|
def test_markdown_editor_set_heading_zero_removes_heading(app):
|
|
"""Test setting heading level 0 removes heading"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.from_markdown("# Heading")
|
|
|
|
# Select heading
|
|
cursor = editor.textCursor()
|
|
cursor.select(QTextCursor.Document)
|
|
editor.setTextCursor(cursor)
|
|
|
|
# Set to level 0 (remove heading)
|
|
editor.apply_heading(0)
|
|
|
|
# Should not have heading markers
|
|
text = editor.toPlainText()
|
|
assert not text.startswith("#")
|
|
|
|
|
|
# ============================================================================
|
|
# List Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_markdown_editor_toggle_list_bullet(app):
|
|
"""Test toggling bullet list"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.insertPlainText("Item 1\nItem 2")
|
|
|
|
# Select all
|
|
cursor = editor.textCursor()
|
|
cursor.select(QTextCursor.Document)
|
|
editor.setTextCursor(cursor)
|
|
|
|
# Toggle bullet list
|
|
editor.toggle_bullets()
|
|
|
|
# Should have bullet markers
|
|
text = editor.toPlainText()
|
|
assert "•" in text or "-" in text
|
|
|
|
|
|
def test_markdown_editor_toggle_list_ordered(app):
|
|
"""Test toggling ordered list"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.insertPlainText("Item 1\nItem 2")
|
|
|
|
cursor = editor.textCursor()
|
|
cursor.select(QTextCursor.Document)
|
|
editor.setTextCursor(cursor)
|
|
|
|
editor.toggle_numbers()
|
|
|
|
text = editor.toPlainText()
|
|
assert "1" in text or "2" in text
|
|
|
|
|
|
# ============================================================================
|
|
# Code Block Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_markdown_editor_apply_code_selected_text(app):
|
|
"""Test toggling code block with selected text"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.insertPlainText("def hello():\n print('hi')")
|
|
|
|
# Select all
|
|
cursor = editor.textCursor()
|
|
cursor.select(QTextCursor.Document)
|
|
editor.setTextCursor(cursor)
|
|
|
|
# Toggle code block
|
|
editor.apply_code()
|
|
|
|
# Should have code fence
|
|
text = editor.toPlainText()
|
|
assert "```" in text
|
|
|
|
|
|
def test_markdown_editor_apply_code_remove(app):
|
|
"""Test removing code block"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.from_markdown("```python\ncode\n```")
|
|
|
|
# Select all
|
|
cursor = editor.textCursor()
|
|
cursor.select(QTextCursor.Document)
|
|
editor.setTextCursor(cursor)
|
|
|
|
# Toggle off
|
|
editor.apply_code()
|
|
|
|
# Code fences should be reduced/removed
|
|
editor.toPlainText()
|
|
# May still have ``` but different structure
|
|
assert True # Just verify no crash
|
|
|
|
|
|
# ============================================================================
|
|
# Checkbox Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_markdown_editor_insert_checkbox_unchecked(app):
|
|
"""Test inserting unchecked checkbox"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
|
|
editor.toggle_checkboxes()
|
|
|
|
text = editor.toPlainText()
|
|
assert editor._CHECK_UNCHECKED_DISPLAY in text
|
|
|
|
|
|
# ============================================================================
|
|
# Toggle Checkboxes Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_markdown_editor_toggle_checkboxes_none_selected(app):
|
|
"""Test toggling checkboxes with no selection"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.from_markdown("☐ Task 1\n☐ Task 2")
|
|
|
|
# No selection, just cursor
|
|
editor.toggle_checkboxes()
|
|
|
|
# Should handle gracefully
|
|
assert True
|
|
|
|
|
|
def test_markdown_editor_toggle_checkboxes_mixed(app):
|
|
"""Test toggling mixed checked/unchecked checkboxes"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.from_markdown("☐ Task 1\n☑ Task 2\n☐ Task 3")
|
|
|
|
# Select all
|
|
cursor = editor.textCursor()
|
|
cursor.select(QTextCursor.Document)
|
|
editor.setTextCursor(cursor)
|
|
|
|
# Toggle
|
|
editor.toggle_checkboxes()
|
|
|
|
# Should toggle all
|
|
text = editor.toPlainText()
|
|
assert (
|
|
editor._CHECK_CHECKED_DISPLAY in text or editor._CHECK_UNCHECKED_DISPLAY in text
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Markdown Conversion Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_markdown_editor_to_markdown_with_checkboxes(app):
|
|
"""Test converting to markdown preserves checkboxes"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.from_markdown("- [ ] Task 1\n- [x] Task 2")
|
|
|
|
md = editor.to_markdown()
|
|
|
|
# Should have checkbox markdown
|
|
assert "[ ]" in md or "[x]" in md
|
|
|
|
|
|
def test_markdown_editor_from_markdown_with_images(app):
|
|
"""Test loading markdown with images"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
|
|
md_with_image = "# Title\n\n\n\nText"
|
|
editor.from_markdown(md_with_image)
|
|
|
|
# Should load without error
|
|
text = editor.toPlainText()
|
|
assert "Title" in text
|
|
|
|
|
|
def test_markdown_editor_from_markdown_with_links(app):
|
|
"""Test loading markdown with links"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
|
|
md_with_link = "[Click here](https://example.com)"
|
|
editor.from_markdown(md_with_link)
|
|
|
|
text = editor.toPlainText()
|
|
assert "Click here" in text
|
|
|
|
|
|
# ============================================================================
|
|
# Selection and Cursor Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_markdown_editor_select_word_under_cursor(app):
|
|
"""Test selecting word under cursor"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.insertPlainText("Hello world test")
|
|
|
|
# Move cursor to middle of word
|
|
cursor = editor.textCursor()
|
|
cursor.setPosition(7) # Middle of "world"
|
|
editor.setTextCursor(cursor)
|
|
|
|
# Select word (via double-click or other mechanism)
|
|
cursor.select(QTextCursor.WordUnderCursor)
|
|
editor.setTextCursor(cursor)
|
|
|
|
assert cursor.hasSelection()
|
|
|
|
|
|
def test_markdown_editor_get_selected_blocks(app):
|
|
"""Test getting selected blocks"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.insertPlainText("Line 1\nLine 2\nLine 3")
|
|
|
|
# Select multiple lines
|
|
cursor = editor.textCursor()
|
|
cursor.setPosition(0)
|
|
cursor.setPosition(14, QTextCursor.KeepAnchor)
|
|
editor.setTextCursor(cursor)
|
|
|
|
# Should have selection
|
|
assert cursor.hasSelection()
|
|
|
|
|
|
# ============================================================================
|
|
# Key Event Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_markdown_editor_key_press_tab(app):
|
|
"""Test tab key handling"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.show()
|
|
|
|
# Create tab key event
|
|
event = QKeyEvent(QKeyEvent.KeyPress, Qt.Key_Tab, Qt.NoModifier)
|
|
|
|
# Send event
|
|
editor.keyPressEvent(event)
|
|
|
|
# Should insert tab or spaces
|
|
text = editor.toPlainText()
|
|
assert len(text) > 0 or text == "" # Tab or spaces inserted
|
|
|
|
|
|
def test_markdown_editor_key_press_return_in_list(app):
|
|
"""Test return key in list"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.from_markdown("- Item 1")
|
|
|
|
# Move cursor to end
|
|
cursor = editor.textCursor()
|
|
cursor.movePosition(QTextCursor.End)
|
|
editor.setTextCursor(cursor)
|
|
|
|
# Press return
|
|
event = QKeyEvent(QKeyEvent.KeyPress, Qt.Key_Return, Qt.NoModifier)
|
|
editor.keyPressEvent(event)
|
|
|
|
# Should create new list item
|
|
text = editor.toPlainText()
|
|
assert "Item 1" in text
|
|
|
|
|
|
# ============================================================================
|
|
# Link Handling Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_markdown_editor_anchor_at_cursor(app):
|
|
"""Test getting anchor at cursor position"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.from_markdown("[link](https://example.com)")
|
|
|
|
# Move cursor over link
|
|
cursor = editor.textCursor()
|
|
cursor.setPosition(2)
|
|
editor.setTextCursor(cursor)
|
|
|
|
# Get anchor (if any)
|
|
anchor = cursor.charFormat().anchorHref()
|
|
|
|
# May or may not have anchor depending on rendering
|
|
assert isinstance(anchor, str)
|
|
|
|
|
|
def test_markdown_editor_mouse_move_over_link(app):
|
|
"""Test mouse movement over link"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(themes)
|
|
editor.from_markdown("[link](https://example.com)")
|
|
editor.show()
|
|
|
|
# Simulate mouse move
|
|
# This tests viewport event handling
|
|
assert True # Just verify no crash
|
|
|
|
|
|
# ============================================================================
|
|
# Theme Mode Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_markdown_highlighter_light_mode(app):
|
|
"""Test highlighter in light mode"""
|
|
doc = QTextDocument()
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
|
|
# Check that light mode colors are set
|
|
bg = highlighter.code_block_format.background().color()
|
|
assert bg.isValid()
|
|
# Check it's a light color (high RGB values, close to 245)
|
|
assert bg.red() > 240 and bg.green() > 240 and bg.blue() > 240
|
|
|
|
fg = highlighter.code_block_format.foreground().color()
|
|
assert fg.isValid()
|
|
# Check it's a dark color for text
|
|
assert fg.red() < 50 and fg.green() < 50 and fg.blue() < 50
|
|
|
|
|
|
def test_markdown_highlighter_dark_mode(app):
|
|
"""Test highlighter in dark mode"""
|
|
doc = QTextDocument()
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.DARK))
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
|
|
# Check that dark mode uses palette colors
|
|
bg = highlighter.code_block_format.background().color()
|
|
fg = highlighter.code_block_format.foreground().color()
|
|
|
|
assert bg.isValid()
|
|
assert fg.isValid()
|
|
|
|
|
|
# ============================================================================
|
|
# Highlighting Pattern Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_markdown_highlighter_triple_backtick_code(app):
|
|
"""Test highlighting triple backtick code blocks"""
|
|
doc = QTextDocument()
|
|
doc.setPlainText("```python\ndef hello():\n pass\n```")
|
|
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
|
|
# Force rehighlight
|
|
highlighter.rehighlight()
|
|
|
|
# Should complete without errors
|
|
assert True
|
|
|
|
|
|
def test_markdown_highlighter_inline_code(app):
|
|
"""Test highlighting inline code with backticks"""
|
|
doc = QTextDocument()
|
|
doc.setPlainText("Here is `inline code` in text")
|
|
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
highlighter.rehighlight()
|
|
|
|
assert True
|
|
|
|
|
|
def test_markdown_highlighter_bold_text(app):
|
|
"""Test highlighting bold text"""
|
|
doc = QTextDocument()
|
|
doc.setPlainText("This is **bold** text")
|
|
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
highlighter.rehighlight()
|
|
|
|
assert True
|
|
|
|
|
|
def test_markdown_highlighter_italic_text(app):
|
|
"""Test highlighting italic text"""
|
|
doc = QTextDocument()
|
|
doc.setPlainText("This is *italic* text")
|
|
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
highlighter.rehighlight()
|
|
|
|
assert True
|
|
|
|
|
|
def test_markdown_highlighter_headings(app):
|
|
"""Test highlighting various heading levels"""
|
|
doc = QTextDocument()
|
|
doc.setPlainText("# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6")
|
|
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
highlighter.rehighlight()
|
|
|
|
assert True
|
|
|
|
|
|
def test_markdown_highlighter_links(app):
|
|
"""Test highlighting markdown links"""
|
|
doc = QTextDocument()
|
|
doc.setPlainText("[link text](https://example.com)")
|
|
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
highlighter.rehighlight()
|
|
|
|
assert True
|
|
|
|
|
|
def test_markdown_highlighter_images(app):
|
|
"""Test highlighting markdown images"""
|
|
doc = QTextDocument()
|
|
doc.setPlainText("")
|
|
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
highlighter.rehighlight()
|
|
|
|
assert True
|
|
|
|
|
|
def test_markdown_highlighter_blockquotes(app):
|
|
"""Test highlighting blockquotes"""
|
|
doc = QTextDocument()
|
|
doc.setPlainText("> This is a quote\n> Second line")
|
|
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
highlighter.rehighlight()
|
|
|
|
assert True
|
|
|
|
|
|
def test_markdown_highlighter_lists(app):
|
|
"""Test highlighting lists"""
|
|
doc = QTextDocument()
|
|
doc.setPlainText("- Item 1\n- Item 2\n- Item 3")
|
|
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
highlighter.rehighlight()
|
|
|
|
assert True
|
|
|
|
|
|
def test_markdown_highlighter_ordered_lists(app):
|
|
"""Test highlighting ordered lists"""
|
|
doc = QTextDocument()
|
|
doc.setPlainText("1. First\n2. Second\n3. Third")
|
|
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
highlighter.rehighlight()
|
|
|
|
assert True
|
|
|
|
|
|
def test_markdown_highlighter_horizontal_rules(app):
|
|
"""Test highlighting horizontal rules"""
|
|
doc = QTextDocument()
|
|
doc.setPlainText("Text above\n\n---\n\nText below")
|
|
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
highlighter.rehighlight()
|
|
|
|
assert True
|
|
|
|
|
|
def test_markdown_highlighter_strikethrough(app):
|
|
"""Test highlighting strikethrough text"""
|
|
doc = QTextDocument()
|
|
doc.setPlainText("This is ~~strikethrough~~ text")
|
|
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
highlighter.rehighlight()
|
|
|
|
assert True
|
|
|
|
|
|
def test_markdown_highlighter_mixed_formatting(app):
|
|
"""Test highlighting mixed markdown formatting"""
|
|
doc = QTextDocument()
|
|
doc.setPlainText(
|
|
"# Title\n\nThis is **bold** and *italic* with `code`.\n\n- List item\n- Another item"
|
|
)
|
|
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
highlighter.rehighlight()
|
|
|
|
assert True
|
|
|
|
|
|
def test_markdown_highlighter_switch_dark_mode(app):
|
|
"""Test that dark mode uses different colors than light mode"""
|
|
doc = QTextDocument()
|
|
doc.setPlainText("# Test")
|
|
|
|
# Create light mode highlighter
|
|
themes_light = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
highlighter_light = MarkdownHighlighter(doc, themes_light)
|
|
light_bg = highlighter_light.code_block_format.background().color()
|
|
|
|
# Create dark mode highlighter with new document (to avoid conflicts)
|
|
doc2 = QTextDocument()
|
|
doc2.setPlainText("# Test")
|
|
themes_dark = ThemeManager(app, ThemeConfig(theme=Theme.DARK))
|
|
highlighter_dark = MarkdownHighlighter(doc2, themes_dark)
|
|
dark_bg = highlighter_dark.code_block_format.background().color()
|
|
|
|
# In light mode, background should be light (high RGB values)
|
|
# In dark mode, background should be darker (lower RGB values)
|
|
# Note: actual values depend on system palette and theme settings
|
|
assert light_bg.isValid()
|
|
assert dark_bg.isValid()
|
|
|
|
# At least one of these should be true (depending on system theme):
|
|
# - Light is lighter than dark, OR
|
|
# - Both are set to valid colors (if system theme overrides)
|
|
is_light_lighter = (
|
|
light_bg.red() + light_bg.green() + light_bg.blue()
|
|
> dark_bg.red() + dark_bg.green() + dark_bg.blue()
|
|
)
|
|
both_valid = light_bg.isValid() and dark_bg.isValid()
|
|
|
|
assert is_light_lighter or both_valid # At least colors are being set
|
|
|
|
|
|
# ============================================================================
|
|
# MarkdownHighlighter Tests - Missing Coverage
|
|
# ============================================================================
|
|
|
|
|
|
def test_markdown_highlighter_code_block_detection(qtbot, app):
|
|
"""Test code block detection and highlighting."""
|
|
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
highlighter = MarkdownHighlighter(doc, theme_manager)
|
|
|
|
# Set text with code block
|
|
text = """
|
|
Some text
|
|
```python
|
|
def hello():
|
|
pass
|
|
```
|
|
More text
|
|
"""
|
|
doc.setPlainText(text)
|
|
|
|
# The highlighter should process the text
|
|
# Just ensure no crash
|
|
assert highlighter is not None
|
|
|
|
|
|
def test_markdown_highlighter_headers(qtbot, app):
|
|
"""Test header highlighting."""
|
|
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
highlighter = MarkdownHighlighter(doc, theme_manager)
|
|
|
|
text = """
|
|
# Header 1
|
|
## Header 2
|
|
### Header 3
|
|
Normal text
|
|
"""
|
|
doc.setPlainText(text)
|
|
|
|
assert highlighter is not None
|
|
|
|
|
|
def test_markdown_highlighter_emphasis(qtbot, app):
|
|
"""Test emphasis highlighting."""
|
|
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
highlighter = MarkdownHighlighter(doc, theme_manager)
|
|
|
|
text = "**bold** and *italic* and ***both***"
|
|
doc.setPlainText(text)
|
|
|
|
assert highlighter is not None
|
|
|
|
|
|
def test_markdown_highlighter_horizontal_rule(qtbot, app):
|
|
"""Test horizontal rule highlighting."""
|
|
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
highlighter = MarkdownHighlighter(doc, theme_manager)
|
|
|
|
text = """
|
|
Text above
|
|
---
|
|
Text below
|
|
***
|
|
More text
|
|
___
|
|
End
|
|
"""
|
|
doc.setPlainText(text)
|
|
|
|
assert highlighter is not None
|
|
|
|
|
|
def test_markdown_highlighter_complex_document(qtbot, app):
|
|
"""Test highlighting a complex document with mixed elements."""
|
|
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
highlighter = MarkdownHighlighter(doc, theme_manager)
|
|
|
|
text = """
|
|
# Main Title
|
|
|
|
This is a paragraph with **bold** and *italic* text.
|
|
|
|
## Code Example
|
|
|
|
Here's some `inline code` and a block:
|
|
|
|
```python
|
|
def fibonacci(n):
|
|
if n <= 1:
|
|
return n
|
|
return fibonacci(n-1) + fibonacci(n-2)
|
|
```
|
|
|
|
## Lists
|
|
|
|
- Item with *emphasis*
|
|
- Another item with **bold**
|
|
- [A link](https://example.com)
|
|
|
|
> A blockquote with **formatted** text
|
|
> Second line
|
|
|
|
---
|
|
|
|
### Final Section
|
|
|
|
~~Strikethrough~~ and normal text.
|
|
"""
|
|
doc.setPlainText(text)
|
|
|
|
# Should handle complex document
|
|
assert highlighter is not None
|
|
|
|
|
|
def test_markdown_highlighter_empty_document(qtbot, app):
|
|
"""Test highlighting empty document."""
|
|
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
highlighter = MarkdownHighlighter(doc, theme_manager)
|
|
|
|
doc.setPlainText("")
|
|
|
|
assert highlighter is not None
|
|
|
|
|
|
def test_markdown_highlighter_update_on_text_change(qtbot, app):
|
|
"""Test that highlighter updates when text changes."""
|
|
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
highlighter = MarkdownHighlighter(doc, theme_manager)
|
|
|
|
doc.setPlainText("Initial text")
|
|
doc.setPlainText("# Header text")
|
|
doc.setPlainText("**Bold text**")
|
|
|
|
# Should handle updates
|
|
assert highlighter is not None
|
|
|
|
|
|
def test_markdown_highlighter_nested_emphasis(qtbot, app):
|
|
"""Test nested emphasis patterns."""
|
|
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
highlighter = MarkdownHighlighter(doc, theme_manager)
|
|
|
|
text = "This has **bold with *italic* inside** and more"
|
|
doc.setPlainText(text)
|
|
|
|
assert highlighter is not None
|
|
|
|
|
|
def test_markdown_highlighter_unclosed_code_block(qtbot, app):
|
|
"""Test handling of unclosed code block."""
|
|
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
highlighter = MarkdownHighlighter(doc, theme_manager)
|
|
|
|
text = """
|
|
```python
|
|
def hello():
|
|
print("world")
|
|
"""
|
|
doc.setPlainText(text)
|
|
|
|
# Should handle gracefully
|
|
assert highlighter is not None
|
|
|
|
|
|
def test_markdown_highlighter_special_characters(qtbot, app):
|
|
"""Test handling special characters in markdown."""
|
|
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
highlighter = MarkdownHighlighter(doc, theme_manager)
|
|
|
|
text = """
|
|
Special chars: < > & " '
|
|
Escaped: \\* \\_ \\`
|
|
Unicode: 你好 café résumé
|
|
"""
|
|
doc.setPlainText(text)
|
|
|
|
assert highlighter is not None
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"markdown_line",
|
|
[
|
|
"- [ ] Task", # checkbox
|
|
"- Task", # bullet
|
|
"1. Task", # numbered
|
|
],
|
|
)
|
|
def test_home_on_list_line_moves_to_text_start(qtbot, editor, markdown_line):
|
|
"""Home on a list line should jump to just after the list marker."""
|
|
editor.from_markdown(markdown_line)
|
|
|
|
# Put caret at end of the line
|
|
cursor = editor.textCursor()
|
|
cursor.movePosition(QTextCursor.End)
|
|
editor.setTextCursor(cursor)
|
|
|
|
# Press Home (no modifiers)
|
|
qtbot.keyPress(editor, Qt.Key_Home)
|
|
qtbot.wait(0)
|
|
|
|
c = editor.textCursor()
|
|
block = c.block()
|
|
line = block.text()
|
|
pos_in_block = c.position() - block.position()
|
|
|
|
# The first character of the user text is the 'T' in "Task"
|
|
logical_start = line.index("Task")
|
|
|
|
assert not c.hasSelection()
|
|
assert pos_in_block == logical_start
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"markdown_line",
|
|
[
|
|
"- [ ] Task", # checkbox
|
|
"- Task", # bullet
|
|
"1. Task", # numbered
|
|
],
|
|
)
|
|
def test_shift_home_on_list_line_selects_text_after_marker(
|
|
qtbot, editor, markdown_line
|
|
):
|
|
"""
|
|
Shift+Home from the end of a list line should select the text after the marker,
|
|
not the marker itself.
|
|
"""
|
|
editor.from_markdown(markdown_line)
|
|
|
|
# Put caret at end of the line
|
|
cursor = editor.textCursor()
|
|
cursor.movePosition(QTextCursor.End)
|
|
editor.setTextCursor(cursor)
|
|
|
|
# Shift+Home: extend selection back to "logical home"
|
|
qtbot.keyPress(editor, Qt.Key_Home, Qt.ShiftModifier)
|
|
qtbot.wait(0)
|
|
|
|
c = editor.textCursor()
|
|
block = c.block()
|
|
line = block.text()
|
|
block_start = block.position()
|
|
|
|
logical_start = line.index("Task")
|
|
expected_start = block_start + logical_start
|
|
expected_end = block_start + len(line)
|
|
|
|
assert c.hasSelection()
|
|
assert c.selectionStart() == expected_start
|
|
assert c.selectionEnd() == expected_end
|
|
# Selected text is exactly the user-visible text, not the marker
|
|
assert c.selectedText() == line[logical_start:]
|
|
|
|
|
|
def test_up_from_below_checkbox_moves_to_text_start(qtbot, editor):
|
|
"""
|
|
Up from the line below a checkbox should land to the right of the checkbox,
|
|
where the text starts, not to the left of the marker.
|
|
"""
|
|
editor.from_markdown("- [ ] Task\nSecond line")
|
|
|
|
# Put caret somewhere on the second line (end of document is fine)
|
|
cursor = editor.textCursor()
|
|
cursor.movePosition(QTextCursor.End)
|
|
editor.setTextCursor(cursor)
|
|
|
|
# Press Up to move to the checkbox line
|
|
qtbot.keyPress(editor, Qt.Key_Up)
|
|
qtbot.wait(0)
|
|
|
|
c = editor.textCursor()
|
|
block = c.block()
|
|
line = block.text()
|
|
pos_in_block = c.position() - block.position()
|
|
|
|
logical_start = line.index("Task")
|
|
assert pos_in_block >= logical_start
|
|
|
|
|
|
def test_backspace_on_empty_checkbox_removes_marker(qtbot, editor):
|
|
"""
|
|
When a checkbox line has no text after the marker, Backspace at/after the
|
|
text position should delete the marker itself, leaving a plain empty line.
|
|
"""
|
|
editor.from_markdown("- [ ] ")
|
|
|
|
# Put caret at end of the checkbox line (after the marker)
|
|
cursor = editor.textCursor()
|
|
cursor.movePosition(QTextCursor.End)
|
|
editor.setTextCursor(cursor)
|
|
|
|
qtbot.keyPress(editor, Qt.Key_Backspace)
|
|
qtbot.wait(0)
|
|
|
|
first_block = editor.document().firstBlock()
|
|
# Marker should be gone
|
|
assert first_block.text() == ""
|
|
assert editor._CHECK_UNCHECKED_DISPLAY not in editor.toPlainText()
|
|
|
|
|
|
def test_insert_alarm_marker_on_checkbox_line_does_not_merge_lines(editor, qtbot):
|
|
# Two checkbox lines
|
|
editor.from_markdown("- [ ] Test\n- [ ] Foobar")
|
|
|
|
# Move caret to second line ("Foobar")
|
|
cursor = editor.textCursor()
|
|
cursor.movePosition(QTextCursor.Start)
|
|
cursor.movePosition(QTextCursor.Down)
|
|
editor.setTextCursor(cursor)
|
|
|
|
# Add an alarm to the second line
|
|
editor.insert_alarm_marker("16:54")
|
|
qtbot.wait(0)
|
|
|
|
lines = lines_keep(editor)
|
|
|
|
# Still two separate lines
|
|
assert len(lines) == 2
|
|
|
|
# First line unchanged (no alarm)
|
|
assert "Test" in lines[0]
|
|
assert "⏰" not in lines[0]
|
|
|
|
# Second line has the alarm marker
|
|
assert "Foobar" in lines[1]
|
|
assert "⏰ 16:54" in lines[1]
|
|
|
|
|
|
def test_render_images_with_corrupted_data(qtbot, app):
|
|
"""Test rendering images with corrupted data that creates null QImage"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(theme_manager=themes)
|
|
qtbot.addWidget(editor)
|
|
editor.show()
|
|
|
|
# Create some binary data that will decode but not form a valid image
|
|
corrupted_data = base64.b64encode(b"not an image file").decode("utf-8")
|
|
markdown = f""
|
|
|
|
editor.from_markdown(markdown)
|
|
qtbot.wait(50)
|
|
|
|
# Should still work without crashing
|
|
text = editor.to_markdown()
|
|
assert len(text) >= 0
|
|
|
|
|
|
def test_insert_alarm_marker(qtbot, app):
|
|
"""Test inserting alarm markers"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(theme_manager=themes)
|
|
qtbot.addWidget(editor)
|
|
editor.show()
|
|
|
|
# Insert alarm marker
|
|
editor.insert_alarm_marker("14:30")
|
|
qtbot.wait(50)
|
|
|
|
content = editor.to_markdown()
|
|
assert "14:30" in content or "⏰" in content
|
|
|
|
|
|
def test_editor_with_tables(qtbot, app):
|
|
"""Test editor with markdown tables"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(theme_manager=themes)
|
|
qtbot.addWidget(editor)
|
|
editor.show()
|
|
|
|
table_markdown = """
|
|
| Header 1 | Header 2 |
|
|
|----------|----------|
|
|
| Cell 1 | Cell 2 |
|
|
| Cell 3 | Cell 4 |
|
|
"""
|
|
editor.from_markdown(table_markdown)
|
|
qtbot.wait(50)
|
|
|
|
result = editor.to_markdown()
|
|
assert "Header 1" in result or "|" in result
|
|
|
|
|
|
def test_editor_with_code_blocks(qtbot, app):
|
|
"""Test editor with code blocks"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(theme_manager=themes)
|
|
qtbot.addWidget(editor)
|
|
editor.show()
|
|
|
|
code_markdown = """
|
|
Some text
|
|
|
|
```python
|
|
def hello():
|
|
print("world")
|
|
```
|
|
|
|
More text
|
|
"""
|
|
editor.from_markdown(code_markdown)
|
|
qtbot.wait(50)
|
|
|
|
result = editor.to_markdown()
|
|
assert "def hello" in result or "python" in result
|
|
|
|
|
|
def test_editor_undo_redo(qtbot, app):
|
|
"""Test undo/redo functionality"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(theme_manager=themes)
|
|
qtbot.addWidget(editor)
|
|
editor.show()
|
|
|
|
# Type some text
|
|
editor.from_markdown("Initial text")
|
|
qtbot.wait(50)
|
|
|
|
# Add more text
|
|
editor.insertPlainText(" additional")
|
|
qtbot.wait(50)
|
|
|
|
# Undo
|
|
editor.undo()
|
|
qtbot.wait(50)
|
|
|
|
# Redo
|
|
editor.redo()
|
|
qtbot.wait(50)
|
|
|
|
assert len(editor.to_markdown()) > 0
|
|
|
|
|
|
def test_editor_cut_copy_paste(qtbot, app):
|
|
"""Test cut/copy/paste operations"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(theme_manager=themes)
|
|
qtbot.addWidget(editor)
|
|
editor.show()
|
|
|
|
editor.from_markdown("Test content for copy")
|
|
qtbot.wait(50)
|
|
|
|
# Select all
|
|
editor.selectAll()
|
|
|
|
# Copy
|
|
editor.copy()
|
|
qtbot.wait(50)
|
|
|
|
# Move to end and paste
|
|
cursor = editor.textCursor()
|
|
cursor.movePosition(QTextCursor.End)
|
|
editor.setTextCursor(cursor)
|
|
|
|
editor.paste()
|
|
qtbot.wait(50)
|
|
|
|
# Should have content twice (or clipboard might be empty in test env)
|
|
assert len(editor.to_markdown()) > 0
|
|
|
|
|
|
def test_editor_with_blockquotes(qtbot, app):
|
|
"""Test editor with blockquotes"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(theme_manager=themes)
|
|
qtbot.addWidget(editor)
|
|
editor.show()
|
|
|
|
quote_markdown = """
|
|
Normal text
|
|
|
|
> This is a quote
|
|
> With multiple lines
|
|
|
|
More normal text
|
|
"""
|
|
editor.from_markdown(quote_markdown)
|
|
qtbot.wait(50)
|
|
|
|
result = editor.to_markdown()
|
|
assert ">" in result or "quote" in result
|
|
|
|
|
|
def test_editor_with_horizontal_rules(qtbot, app):
|
|
"""Test editor with horizontal rules"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(theme_manager=themes)
|
|
qtbot.addWidget(editor)
|
|
editor.show()
|
|
|
|
hr_markdown = """
|
|
Section 1
|
|
|
|
---
|
|
|
|
Section 2
|
|
"""
|
|
editor.from_markdown(hr_markdown)
|
|
qtbot.wait(50)
|
|
|
|
result = editor.to_markdown()
|
|
assert "Section" in result
|
|
|
|
|
|
def test_editor_with_mixed_content(qtbot, app):
|
|
"""Test editor with mixed markdown content"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(theme_manager=themes)
|
|
qtbot.addWidget(editor)
|
|
editor.show()
|
|
|
|
mixed_markdown = """
|
|
# Heading
|
|
|
|
This is **bold** and *italic* text.
|
|
|
|
- [ ] Todo item
|
|
- [x] Completed item
|
|
|
|
```python
|
|
code()
|
|
```
|
|
|
|
[Link](https://example.com)
|
|
|
|
> Quote
|
|
|
|
| Table | Header |
|
|
|-------|--------|
|
|
| A | B |
|
|
"""
|
|
editor.from_markdown(mixed_markdown)
|
|
qtbot.wait(50)
|
|
|
|
result = editor.to_markdown()
|
|
# Should contain various markdown elements
|
|
assert len(result) > 50
|
|
|
|
|
|
def test_editor_insert_text_at_cursor(qtbot, app):
|
|
"""Test inserting text at cursor position"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(theme_manager=themes)
|
|
qtbot.addWidget(editor)
|
|
editor.show()
|
|
|
|
editor.from_markdown("Start Middle End")
|
|
qtbot.wait(50)
|
|
|
|
# Move cursor to middle
|
|
cursor = editor.textCursor()
|
|
cursor.setPosition(6)
|
|
editor.setTextCursor(cursor)
|
|
|
|
# Insert text
|
|
editor.insertPlainText("INSERTED ")
|
|
qtbot.wait(50)
|
|
|
|
result = editor.to_markdown()
|
|
assert "INSERTED" in result
|
|
|
|
|
|
def test_editor_delete_operations(qtbot, app):
|
|
"""Test delete operations"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
editor = MarkdownEditor(theme_manager=themes)
|
|
qtbot.addWidget(editor)
|
|
editor.show()
|
|
|
|
editor.from_markdown("Text to delete")
|
|
qtbot.wait(50)
|
|
|
|
# Select some text and delete
|
|
cursor = editor.textCursor()
|
|
cursor.setPosition(0)
|
|
cursor.setPosition(4, QTextCursor.KeepAnchor)
|
|
editor.setTextCursor(cursor)
|
|
|
|
cursor.removeSelectedText()
|
|
qtbot.wait(50)
|
|
|
|
result = editor.to_markdown()
|
|
assert "Text" not in result or len(result) < 15
|
|
|
|
|
|
def test_markdown_highlighter_dark_theme(qtbot, app):
|
|
"""Test markdown highlighter with dark theme - covers lines 74-75"""
|
|
# Create theme manager with dark theme
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.DARK))
|
|
|
|
# Create a text document
|
|
doc = QTextDocument()
|
|
|
|
# Create highlighter with dark theme
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
|
|
# Set some markdown text
|
|
doc.setPlainText("# Heading\n\nSome **bold** text\n\n```python\ncode\n```")
|
|
|
|
# The highlighter should work with dark theme
|
|
assert highlighter is not None
|
|
assert highlighter.code_block_format is not None
|
|
|
|
|
|
def test_markdown_highlighter_light_theme(qtbot, app):
|
|
"""Test markdown highlighter with light theme"""
|
|
# Create theme manager with light theme
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
|
|
# Create a text document
|
|
doc = QTextDocument()
|
|
|
|
# Create highlighter with light theme
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
|
|
# Set some markdown text
|
|
doc.setPlainText("# Heading\n\nSome **bold** text")
|
|
|
|
# The highlighter should work with light theme
|
|
assert highlighter is not None
|
|
assert highlighter.code_block_format is not None
|
|
|
|
|
|
def test_markdown_highlighter_system_dark_theme(qtbot, app, monkeypatch):
|
|
"""Test markdown highlighter with system dark theme"""
|
|
# Create theme manager with system theme
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.SYSTEM))
|
|
|
|
# Mock the system to be dark
|
|
monkeypatch.setattr(themes, "_is_system_dark", True)
|
|
|
|
# Create a text document
|
|
doc = QTextDocument()
|
|
|
|
# Create highlighter
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
|
|
# Set some markdown text
|
|
doc.setPlainText("# Dark Theme Heading\n\n**Bold text**")
|
|
|
|
# The highlighter should use dark theme colors
|
|
assert highlighter is not None
|
|
|
|
|
|
def test_markdown_highlighter_with_headings(qtbot, app):
|
|
"""Test highlighting various heading levels"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
|
|
markdown = """
|
|
# H1 Heading
|
|
## H2 Heading
|
|
### H3 Heading
|
|
#### H4 Heading
|
|
##### H5 Heading
|
|
###### H6 Heading
|
|
"""
|
|
doc.setPlainText(markdown)
|
|
|
|
# Should highlight all headings
|
|
assert highlighter.h1_format is not None
|
|
assert highlighter.h2_format is not None
|
|
|
|
|
|
def test_markdown_highlighter_with_emphasis(qtbot, app):
|
|
"""Test highlighting bold and italic"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
|
|
markdown = """
|
|
**Bold text**
|
|
*Italic text*
|
|
***Bold and italic***
|
|
__Also bold__
|
|
_Also italic_
|
|
"""
|
|
doc.setPlainText(markdown)
|
|
|
|
# Should have emphasis formats
|
|
assert highlighter is not None
|
|
|
|
|
|
def test_markdown_highlighter_with_code(qtbot, app):
|
|
"""Test highlighting inline code and code blocks"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
|
|
markdown = """
|
|
Inline `code` here.
|
|
|
|
```python
|
|
def hello():
|
|
print("world")
|
|
```
|
|
|
|
More text.
|
|
"""
|
|
doc.setPlainText(markdown)
|
|
|
|
# Should highlight code
|
|
assert highlighter.code_block_format is not None
|
|
|
|
|
|
def test_markdown_highlighter_with_links(qtbot, app):
|
|
"""Test highlighting links"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
|
|
markdown = """
|
|
[Link text](https://example.com)
|
|
<https://auto-link.com>
|
|
"""
|
|
doc.setPlainText(markdown)
|
|
|
|
# Should have link format
|
|
assert highlighter is not None
|
|
|
|
|
|
def test_markdown_highlighter_with_lists(qtbot, app):
|
|
"""Test highlighting lists"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
|
|
markdown = """
|
|
- Unordered item 1
|
|
- Unordered item 2
|
|
|
|
1. Ordered item 1
|
|
2. Ordered item 2
|
|
|
|
- [ ] Unchecked task
|
|
- [x] Checked task
|
|
"""
|
|
doc.setPlainText(markdown)
|
|
|
|
# Should highlight lists
|
|
assert highlighter is not None
|
|
|
|
|
|
def test_markdown_highlighter_with_blockquotes(qtbot, app):
|
|
"""Test highlighting blockquotes"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
|
|
markdown = """
|
|
> This is a quote
|
|
> With multiple lines
|
|
"""
|
|
doc.setPlainText(markdown)
|
|
|
|
# Should highlight quotes
|
|
assert highlighter is not None
|
|
|
|
|
|
def test_markdown_highlighter_theme_change(qtbot, app):
|
|
"""Test changing theme after creation"""
|
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
|
doc = QTextDocument()
|
|
highlighter = MarkdownHighlighter(doc, themes)
|
|
|
|
markdown = "# Heading\n\n**Bold**"
|
|
doc.setPlainText(markdown)
|
|
|
|
# Change to dark theme
|
|
themes.apply(Theme.DARK)
|
|
qtbot.wait(50)
|
|
|
|
# Highlighter should update
|
|
# We can't directly test the visual change, but verify it doesn't crash
|
|
assert highlighter is not None
|