Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ case "$platform" in
test_startup_fresh_workspace.py
test_ui_audit_regressions.py
test_web_adapter.py
test_web_adapter_figure_export.py
)
;;
esac
Expand Down
3 changes: 2 additions & 1 deletion tests/test_verify_dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,9 @@ def test_sdist_must_carry_the_reviewed_build_inputs(
verify_dist.verify_sdist(
_sdist(tmp_path, current_package_files, build_inputs=build_inputs)
)
# Byte edits that do not depend on the checkout's line ends.
reopened = {
"setup.py": (b'"chemvas.bootstrap.web_drafts",\n', b""),
"setup.py": (b'"chemvas.bootstrap.web_drafts",', b""),
"pyproject.toml": (b'"assets/icon/*.png"]', b'"assets/icon/*.png", "web/*"]'),
}[name]
build_inputs = _build_inputs()
Expand Down
57 changes: 54 additions & 3 deletions tests/test_web_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,30 @@ def test_browser_state_and_svg_contracts():
assert result.returncode == 0, result.stdout + result.stderr


@pytest.mark.parametrize("newline", ["\n", "\r\n"], ids=["lf", "crlf"])
def test_browser_contracts_hold_for_either_checkout_line_ending(tmp_path, newline):
"""Git checks the page sources out with LF or CRLF; the same checks hold."""
node = shutil.which("node")
assert node is not None, (
"The browser adapter checks require Node.js 20+ (runtime has no Node dependency)."
)
suite = tmp_path / "tests/web_adapter.test.mjs"
copies = {ROOT / "tests/web_adapter.test.mjs": suite}
for source in (ROOT / "app/chemvas/web").iterdir():
if source.is_file():
copies[source] = tmp_path / "app/chemvas/web" / source.name
for source, target in copies.items():
text = source.read_bytes().decode("utf-8").replace("\r\n", "\n")
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(text.replace("\n", newline).encode("utf-8"))
app = (tmp_path / "app/chemvas/web/app.mjs").read_bytes()
assert app.count(b"\r\n") == (app.count(b"\n") if newline == "\r\n" else 0)
result = subprocess.run(
[node, "--test", str(suite)], capture_output=True, text=True, check=False
)
assert result.returncode == 0, result.stdout + result.stderr


def test_existing_desktop_history_owns_browser_undo_and_rejected_edits():
from chemvas.ui.canvas.canvas_history_service import CanvasHistoryService
from chemvas.ui.molecule.structure_geometry_logic import (
Expand Down Expand Up @@ -7617,7 +7641,14 @@ def test_browser_mark_rendering_matches_native(
):
from decimal import Decimal

from PyQt6.QtGui import QFont, QFontMetricsF, QPainterPath, QTextDocument
from PyQt6.QtGui import (
QFont,
QFontMetricsF,
QPainterPath,
QTextDocument,
QTextLayout,
QTransform,
)

source = new_document()
source["state"]["settings"]["bond_length_px"] = length
Expand Down Expand Up @@ -7678,13 +7709,33 @@ def test_browser_mark_rendering_matches_native(
assert mark["radius"] * 2 == item.path().boundingRect().width()
assert mark["stroke"] == item.pen().widthF()
else:
# Lay each browser run out the way this native item paints: typographic
# runs by addText's design metrics, plain text through QTextDocument's
# device metrics, which can advance native Windows glyphs differently.
path = QPainterPath()
for run in mark["runs"]:
font = QFont(spec["family"])
font.setPointSizeF(run["size"])
path.addText(run["x"], run["y"], font, run["text"])
if item._layout is not None:
path.addText(run["x"], run["y"], font, run["text"])
continue
layout = QTextLayout(run["text"], font)
layout.beginLayout()
line = layout.createLine()
layout.endLayout()
for glyphs in layout.glyphRuns():
for glyph, position in zip(
glyphs.glyphIndexes(), glyphs.positions(), strict=True
):
baseline = run["y"] + (position.y() - line.ascent())
origin = QTransform.fromTranslate(run["x"] + position.x(), baseline)
path.addPath(origin.map(glyphs.rawFont().pathForGlyph(glyph)))
# Qt places glyphs in 1/64 px units, so the sides may round one unit
# apart. Rect sides then come from a few double sums and differences
# below 256 px; allow their last-place error and nothing more.
assert path.boundingRect().getRect() == pytest.approx(
item.mapToScene(item.glyph_path()).boundingRect().getRect(), abs=1 / 64
item.mapToScene(item.glyph_path()).boundingRect().getRect(),
abs=1 / 64 + 4 * math.ulp(256.0),
)
# An attached fixture has no matching atom annotation; the desktop edits it
# all the same and only resynchronizes annotations when marks change.
Expand Down
62 changes: 44 additions & 18 deletions tests/test_web_adapter_figure_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,46 @@
desktop's offscreen export writes for the accepted document, refuses what the
native owner refuses, and leaves the session as it was. The server runs that
export in a short-lived child process; this test compares its reply with the
same export run in-process.
same export run in a separate oracle process. That process starts the way the
desktop does, because this test process's Qt platform and default font are the
test runner's, not the product's.
"""

from __future__ import annotations

import os
import json
import subprocess
import sys
from copy import deepcopy
from pathlib import Path
from xml.etree import ElementTree as ET

import pytest

os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")

from PyQt6.QtWidgets import QApplication

from chemvas.bootstrap.document_cli_shared import offscreen_canvas
from chemvas.bootstrap.web_adapter import BrowserSession, edit_document, new_document
from chemvas.domain.document import extract_document_state

ROOT = Path(__file__).resolve().parents[1]
SVG_ROOT = "{http://www.w3.org/2000/svg}svg"


@pytest.fixture(scope="module", autouse=True)
def application():
app = QApplication.instance() or QApplication([])
yield app
# The desktop's offscreen export on the host's export platform at 96 DPI. As in
# bootstrap/application.py, the application pins AA_Use96Dpi and exists before
# the canvas, so it keeps its own default font (Segoe UI on native Windows).
NATIVE_EXPORT = """
import json, os, sys
sys.path.insert(0, sys.argv[1])
from chemvas.bootstrap.document_cli_shared import offscreen_canvas, qt_platform
from chemvas.domain.document import extract_document_state
os.environ["QT_QPA_PLATFORM"] = qt_platform()
if sys.platform == "win32":
os.environ["QT_FONT_DPI"] = "96"
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication
QApplication.setAttribute(Qt.ApplicationAttribute.AA_Use96Dpi)
application = QApplication(["chemvas-web-figure-export-oracle"])
with open(sys.argv[2], encoding="utf-8") as stream:
state = extract_document_state(json.load(stream))
with offscreen_canvas(state, command="web-figure-export-oracle") as (_canvas, service):
service.export_figure(sys.argv[3], fmt="svg", scope="sheet")
"""


def unchanged(session):
Expand All @@ -46,11 +60,23 @@ def unchanged(session):

def native_svg(source, tmp_path):
"""The desktop's whole-canvas plain SVG for ``source``, from its offscreen owner."""
document = tmp_path / "source.json"
document.write_text(json.dumps(source), encoding="utf-8")
path = tmp_path / "native.svg"
with offscreen_canvas(
extract_document_state(source), command="web-figure-export-oracle"
) as (_canvas, service):
service.export_figure(str(path), fmt="svg", scope="sheet")
result = subprocess.run(
[
sys.executable,
"-c",
NATIVE_EXPORT,
str(ROOT / "app"),
str(document),
str(path),
],
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, result.stderr
return path.read_text(encoding="utf-8")


Expand Down
Loading
Loading