DazedTL/tests/test_wolf_wrap_search.py

393 lines
15 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Unit tests for search-first wrap workflow (util/wolfdawn/wrap_search.py)."""
from __future__ import annotations
import json
import os
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
os.chdir(ROOT)
sys.path.insert(0, str(ROOT))
import util.dazedwrap as dazedwrap # noqa: E402
from util.wolfdawn import wrap_search as ws # noqa: E402
RUMOR_SHEET = "├■街の噂MOB"
GATEKEEPER_TEXT = (
"That gatekeeper just randomly hits on any pretty woman he sees. "
"They say he once tried to pick up a horned rabbit."
)
GATEKEEPER_ROW = 26
GATEKEEPER_FIELD = "町のウワサ0"
RUMOR_FIXTURE = {
"file": "DataBase.project",
"kind": "db",
"groups": [
{
"type": 29,
"typeName": RUMOR_SHEET,
"lines": [
{
"row": 25,
"field": 1,
"fieldName": "町のウワサ0",
"source": "別の噂",
"text": "Short rumor that fits fine.",
},
{
"row": GATEKEEPER_ROW,
"field": 1,
"fieldName": GATEKEEPER_FIELD,
"source": "門番",
"text": GATEKEEPER_TEXT,
},
],
}
],
}
class TestWrapSearch(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.translated = Path(self.tmp.name) / "translated"
self.translated.mkdir()
self.work = Path(self.tmp.name) / "wolf_json"
self.work.mkdir()
path = self.translated / "DataBase.project.json"
path.write_text(json.dumps(RUMOR_FIXTURE, ensure_ascii=False, indent=4), encoding="utf-8")
def tearDown(self):
self.tmp.cleanup()
def test_search_and_wrap_db_sheet(self):
hits = ws.search_translated_text("gatekeeper just randomly", self.translated)
self.assertEqual(len(hits), 1)
hit = hits[0]
self.assertEqual(hit.sheet_name, RUMOR_SHEET)
self.assertEqual(hit.row, GATEKEEPER_ROW)
path = self.translated / "DataBase.project.json"
doc = json.loads(path.read_text(encoding="utf-8"))
changed = ws.wrap_overflow_in_sheet(path, doc, RUMOR_SHEET, width=34)
self.assertEqual(changed, 1)
line = ws.locate_line(doc, hit.hit_id)
assert line is not None
self.assertIn("\n", line["text"])
self.assertLessEqual(dazedwrap.max_line_visible_length(line["text"]), 34)
ws.set_sheet_width(self.work, RUMOR_SHEET, 34, json_file="DataBase.project.json")
self.assertEqual(
ws.get_sheet_width(ws.load_wrap_profile(self.work), RUMOR_SHEET), 34
)
class TestNamesSearchAndWrap(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.translated = Path(self.tmp.name) / "translated"
self.translated.mkdir()
self.category = "├■街の噂MOB"
fixture = {
"kind": "names",
"names": [
{
"source": "「メイドがどんどん辞めていく」",
"text": '"Yeah, maids keep quitting one after another."',
"note": self.category,
},
{
"source": "長い",
"text": (
"This is a very long English rumor that should definitely "
"wrap when the width is set to thirty-four characters."
),
"note": self.category,
},
],
}
self.path = self.translated / "names.json"
self.path.write_text(json.dumps(fixture, ensure_ascii=False, indent=4), encoding="utf-8")
def tearDown(self):
self.tmp.cleanup()
def test_search_summary_shows_translated_text(self):
hits = ws.search_translated_text("maids", self.translated)
self.assertEqual(len(hits), 1)
summary = hits[0].summary(40)
self.assertTrue(summary.split("\n", 1)[0].startswith('"Yeah, maids'))
self.assertNotIn("メイド", summary)
path, _doc, line = ws.load_hit_from_id(self.translated, hits[0].hit_id)
assert path is not None and line is not None
self.assertEqual(path.name, "names.json")
self.assertIn("maids", line["text"])
def test_wrap_overflow_in_names_category(self):
doc = json.loads(self.path.read_text(encoding="utf-8"))
changed = ws.wrap_overflow_in_sheet(
self.path, doc, self.category, 34, kind="names",
)
self.assertGreaterEqual(changed, 1)
long_text = json.loads(self.path.read_text(encoding="utf-8"))["names"][1]["text"]
self.assertIn("\n", long_text)
self.assertLessEqual(dazedwrap.max_line_visible_length(long_text), 34)
class TestWrapPreviewHtml(unittest.TestCase):
def test_html_preview_hides_codes_and_simulates_body_font(self):
text = r'That \c[21]\f[20]cafe\c[19]\f[18] over there'
html = ws.format_wrap_preview_html(text, 40, body_font=13)
self.assertNotIn(r"\c[", html)
self.assertNotIn(r"\f[", html)
self.assertIn("cafe", html)
self.assertIn("font-size:14px", html) # 20*13/18
self.assertIn("font-size:13px", html)
self.assertIn(ws.wolf_preview_color(21), html)
info = ws.wrap_preview_info(text, 40)
self.assertLess(info["longest"], len(text))
class TestManualFontAndWrap(unittest.TestCase):
def test_manual_scales_emphasis_wraps_and_skips_short_plain(self):
text = (
r"So they even take worries like this in the "
r"\c[21]\f[20]confessional\c[19]\f[18] booth every day."
)
new_text, changed = ws.apply_manual_font_and_wrap(text, 40, font=14)
self.assertTrue(changed)
self.assertIn(r"\f[16]", new_text)
self.assertIn(r"\f[14]", new_text)
self.assertLessEqual(dazedwrap.max_line_visible_length(new_text), 40)
short, skipped = ws.apply_manual_font_and_wrap(
"Short.", 40, font=14, only_overflow_or_fonts=True
)
self.assertFalse(skipped)
self.assertEqual(short, "Short.")
def test_manual_font_zero_or_none_does_not_add_font(self):
text = "A short line that needs wrapping because it is quite long for width thirty."
for font in (None, 0):
new_text, changed = ws.apply_manual_font_and_wrap(text, 30, font=font)
self.assertTrue(changed)
self.assertNotIn(r"\f[", new_text)
self.assertLessEqual(dazedwrap.max_line_visible_length(new_text), 30)
# Existing fonts stay put when font is disabled.
with_f = r"\f[18]Hello there this is a longer line for wrapping please."
kept, changed = ws.apply_manual_font_and_wrap(with_f, 20, font=None)
self.assertTrue(changed)
self.assertTrue(kept.startswith(r"\f[18]"))
self.assertNotIn(r"\f[14]", kept)
def test_manual_reflows_soft_breaks_that_already_fit(self):
"""Hard \\n lines can each fit width while the preview still reflows."""
text = (
'\\f[13]"I heard there\'s a super hot female\n'
"adventurer who came to town recently!?\n"
"Maybe I'll catch a glimpse of her at the\n"
r'\c[21]\f[14]tavern\c[19]\f[13]!"'
)
# Per-line lengths are under 55, so the old overflow check skipped wrap.
self.assertFalse(ws.line_needs_wrap(text, 55))
new_text, changed = ws.apply_manual_font_and_wrap(text, 55, font=13)
self.assertTrue(changed)
self.assertEqual(new_text, ws.wrap_line_text(text, 55))
self.assertIn("female adventurer who came", new_text.replace("\n", " "))
# Already at the target layout → no-op.
again, changed_again = ws.apply_manual_font_and_wrap(new_text, 55, font=13)
self.assertFalse(changed_again)
self.assertEqual(again, new_text)
def test_wrap_overflow_manual_in_names_category(self):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "names.json"
doc = {
"kind": "names",
"names": [
{
"note": "",
"source": "a",
"text": (
r"So they even take worries like this in the "
r"\c[21]\f[20]confessional\c[19]\f[18] booth."
),
},
{"note": "", "source": "b", "text": "OK"},
{"note": "other", "source": "c", "text": "x" * 80},
],
}
path.write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8")
hit = ws.WrapHit(
json_file="names.json",
kind="names",
sheet_name="",
row=0,
field_name="name",
text=doc["names"][0]["text"],
max_line_len=80,
)
changed = ws.wrap_overflow_manual_in_scope(path, doc, hit, 36, font=14)
self.assertEqual(changed, 1)
saved = json.loads(path.read_text(encoding="utf-8"))
self.assertIn(r"\f[14]", saved["names"][0]["text"])
self.assertEqual(saved["names"][1]["text"], "OK")
self.assertEqual(saved["names"][2]["text"], "x" * 80)
class TestEventScopeWrap(unittest.TestCase):
def test_wrap_all_in_map_scene_only(self):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "Map001.mps.json"
long_a = (
"This is a very long line of dialogue that "
"should wrap at thirty four visible chars."
)
long_b = (
"Another very long line of dialogue that also "
"should wrap at thirty four visible chars."
)
doc = {
"kind": "map",
"file": "Map001.mps",
"scenes": [
{
"event": 1,
"name": "Intro",
"lines": [
{"text": long_a},
{"text": "Short line."},
],
},
{
"event": 2,
"name": "Other",
"lines": [{"text": long_b}],
},
],
}
path.write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8")
hit = ws.WrapHit(
json_file="Map001.mps.json",
kind="map",
sheet_name="Map001.mps",
row=1,
field_name="UI",
text="very long line",
max_line_len=80,
scene_index=0,
line_index=0,
map_file="Map001.mps",
)
stats = ws.scope_stats(doc, hit, 34)
self.assertEqual(stats.total, 2)
self.assertEqual(stats.overflow, 1)
self.assertIn("event 1", stats.label)
self.assertIn("Intro", stats.label)
self.assertEqual(ws.wrap_overflow_in_scope(path, doc, hit, 34), 1)
# Scene 2 must stay untouched.
self.assertEqual(doc["scenes"][1]["lines"][0]["text"], long_b)
self.assertNotEqual(doc["scenes"][0]["lines"][0]["text"], long_a)
class TestFitTextToBox(unittest.TestCase):
"""DB Relayout: wrap + font shrink until soft line count fits."""
RUMOR = (
'\\f[14]"I heard there\'s a super hot female adventurer who\n'
"came to town recently!? Maybe I'll catch a glimpse\n"
'of her at the \\c[21]\\f[15]tavern\\c[19]\\f[14]!"'
)
def test_count_soft_lines(self):
self.assertEqual(ws.count_soft_lines("a\nb\nc"), 3)
self.assertEqual(ws.count_soft_lines("one line"), 1)
self.assertEqual(ws.count_soft_lines(""), 0)
def test_max_lines_zero_wraps_only(self):
fitted, changed = ws.fit_text_to_box(self.RUMOR, 44, max_lines=0)
self.assertTrue(changed)
self.assertEqual(ws.count_soft_lines(fitted), 3)
self.assertIn(r"\f[14]", fitted)
def test_max_lines_shrinks_font_and_fits(self):
fitted, changed = ws.fit_text_to_box(self.RUMOR, 44, max_lines=2)
self.assertTrue(changed)
self.assertLessEqual(ws.count_soft_lines(fitted), 2)
from util.wolfdawn import codes as wolf_codes
self.assertLess(wolf_codes.infer_base_font_size(fitted), 14)
self.assertIn("tavern", fitted)
def test_box_font_from_jp_source_scales_width(self):
"""Native box px from JP source lets more cells fit when \\f shrinks."""
from util.wolfdawn import codes as wolf_codes
fitted, changed = ws.fit_text_to_box(
self.RUMOR, 44, max_lines=2, box_font=18
)
self.assertTrue(changed)
self.assertLessEqual(ws.count_soft_lines(fitted), 2)
# With box_font=18, body can stay larger than when native=14.
self.assertGreaterEqual(wolf_codes.infer_base_font_size(fitted), 12)
fitted2, changed2 = ws.fit_text_to_box(
fitted, 44, max_lines=2, box_font=18
)
self.assertFalse(changed2)
self.assertEqual(fitted2, fitted)
def test_wrap_hit_in_file_honors_max_lines(self):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "DataBase.project.json"
source = (
"「最近町に来た女冒険者が激マブらしいぞ!?\n"
" \\c[21]\\f[20]酒場\\c[19]\\f[18]に行ったら会えるかな!」"
)
doc = {
"kind": "db",
"file": "DataBase.project",
"groups": [
{
"typeName": RUMOR_SHEET,
"lines": [
{
"row": 21,
"field": 25,
"fieldName": GATEKEEPER_FIELD,
"source": source,
"text": self.RUMOR,
}
],
}
],
}
path.write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8")
hit_id = {
"kind": "db",
"sheet_name": RUMOR_SHEET,
"row": 21,
"field_name": GATEKEEPER_FIELD,
}
self.assertTrue(
ws.wrap_hit_in_file(path, doc, hit_id, 44, max_lines=2)
)
line = ws.locate_line(doc, hit_id)
self.assertIsNotNone(line)
self.assertLessEqual(ws.count_soft_lines(str(line["text"])), 2)
from util.wolfdawn import codes as wolf_codes
# JP body is \\f[18]; shrink should land near 12, not floor at 8.
self.assertGreaterEqual(
wolf_codes.infer_base_font_size(str(line["text"])), 12
)
if __name__ == "__main__":
unittest.main()