diff --git a/docs/frontend/browser-results.json b/docs/frontend/browser-results.json index 41ff899..a508f77 100644 --- a/docs/frontend/browser-results.json +++ b/docs/frontend/browser-results.json @@ -18,5 +18,5 @@ "No uncaught browser errors, API/external requests, microphone or camera requests" ], "errors": [], - "requests": 56 + "requests": 54 } \ No newline at end of file diff --git a/docs/frontend/recordings/hel825-phone-nav-esc.webm b/docs/frontend/recordings/hel825-phone-nav-esc.webm new file mode 100644 index 0000000..50fb514 Binary files /dev/null and b/docs/frontend/recordings/hel825-phone-nav-esc.webm differ diff --git a/docs/frontend/recordings/hel825-wide-nav-dialog-esc.webm b/docs/frontend/recordings/hel825-wide-nav-dialog-esc.webm new file mode 100644 index 0000000..6995135 Binary files /dev/null and b/docs/frontend/recordings/hel825-wide-nav-dialog-esc.webm differ diff --git a/docs/rework/appearance-fog.png b/docs/rework/appearance-fog.png index 70a5e3c..7bb14f7 100644 Binary files a/docs/rework/appearance-fog.png and b/docs/rework/appearance-fog.png differ diff --git a/docs/rework/backup-fog.png b/docs/rework/backup-fog.png index e4c8756..bff09c3 100644 Binary files a/docs/rework/backup-fog.png and b/docs/rework/backup-fog.png differ diff --git a/docs/rework/birth-calendar-help-moon.png b/docs/rework/birth-calendar-help-moon.png index c6dce27..657dac5 100644 Binary files a/docs/rework/birth-calendar-help-moon.png and b/docs/rework/birth-calendar-help-moon.png differ diff --git a/docs/rework/birth-preview-moon.png b/docs/rework/birth-preview-moon.png index 67aadc3..2ca00bf 100644 Binary files a/docs/rework/birth-preview-moon.png and b/docs/rework/birth-preview-moon.png differ diff --git a/docs/rework/browser-results.json b/docs/rework/browser-results.json index 24665d3..fdce497 100644 --- a/docs/rework/browser-results.json +++ b/docs/rework/browser-results.json @@ -25,8 +25,8 @@ "reader": { "top": 762, "bottom": 792, - "left": 596.5, - "right": 843.5, + "left": 596.484375, + "right": 843.515625, "height": 30 }, "composer": { @@ -75,8 +75,8 @@ "reader": { "top": 762, "bottom": 792, - "left": 596.5, - "right": 843.5, + "left": 596.484375, + "right": 843.515625, "height": 30 }, "composer": { @@ -511,7 +511,7 @@ "reader": { "top": 252, "bottom": 282, - "left": 359.5, + "left": 359.484375, "right": 484.5, "height": 30 }, @@ -554,7 +554,7 @@ "reader": { "top": 252, "bottom": 282, - "left": 359.5, + "left": 359.484375, "right": 484.5, "height": 30 }, @@ -586,9 +586,9 @@ ], "motion": { "inked_lines": { - "a7926846-51f6-4d1a-8057-565c2350c71d:1:0": 1 + "6fc4f8ff-218a-4c0a-933c-7982fef596b6:1:0": 1 }, - "slow_frames_new_turn_to_reply_s": 2.32 + "slow_frames_new_turn_to_reply_s": 2.31 }, "console_errors": [], "limits": [ diff --git a/docs/rework/city-fog.png b/docs/rework/city-fog.png index 631c2c2..6f1baa4 100644 Binary files a/docs/rework/city-fog.png and b/docs/rework/city-fog.png differ diff --git a/docs/rework/compact-first-press-390-420-moon.png b/docs/rework/compact-first-press-390-420-moon.png index 60422e5..e8ea483 100644 Binary files a/docs/rework/compact-first-press-390-420-moon.png and b/docs/rework/compact-first-press-390-420-moon.png differ diff --git a/docs/rework/compact-first-press-390-430-moon.png b/docs/rework/compact-first-press-390-430-moon.png index b0d04c7..a5ae1c9 100644 Binary files a/docs/rework/compact-first-press-390-430-moon.png and b/docs/rework/compact-first-press-390-430-moon.png differ diff --git a/docs/rework/demo-voice-390.png b/docs/rework/demo-voice-390.png index a660b32..bdc35e9 100644 Binary files a/docs/rework/demo-voice-390.png and b/docs/rework/demo-voice-390.png differ diff --git a/docs/rework/existing-birth-fog.png b/docs/rework/existing-birth-fog.png index ba08cc7..0ef32fb 100644 Binary files a/docs/rework/existing-birth-fog.png and b/docs/rework/existing-birth-fog.png differ diff --git a/docs/rework/existing-growth-fog.png b/docs/rework/existing-growth-fog.png index c8bf17a..0f706a1 100644 Binary files a/docs/rework/existing-growth-fog.png and b/docs/rework/existing-growth-fog.png differ diff --git a/docs/rework/full-history-fog.png b/docs/rework/full-history-fog.png index ceaf3e0..81d105e 100644 Binary files a/docs/rework/full-history-fog.png and b/docs/rework/full-history-fog.png differ diff --git a/docs/rework/full-history-moon.png b/docs/rework/full-history-moon.png index 947e1b8..a9892da 100644 Binary files a/docs/rework/full-history-moon.png and b/docs/rework/full-history-moon.png differ diff --git a/docs/rework/life-empty-fog.png b/docs/rework/life-empty-fog.png index 8e29daf..0d40266 100644 Binary files a/docs/rework/life-empty-fog.png and b/docs/rework/life-empty-fog.png differ diff --git a/docs/rework/live-gateway/desktop-history-moon.png b/docs/rework/live-gateway/desktop-history-moon.png index 1c543fa..d285e4a 100644 Binary files a/docs/rework/live-gateway/desktop-history-moon.png and b/docs/rework/live-gateway/desktop-history-moon.png differ diff --git a/docs/rework/live-gateway/desktop-model-error.png b/docs/rework/live-gateway/desktop-model-error.png index 4518b90..491674b 100644 Binary files a/docs/rework/live-gateway/desktop-model-error.png and b/docs/rework/live-gateway/desktop-model-error.png differ diff --git a/docs/rework/live-gateway/desktop-preview-moon.png b/docs/rework/live-gateway/desktop-preview-moon.png index 56495bd..b4aad26 100644 Binary files a/docs/rework/live-gateway/desktop-preview-moon.png and b/docs/rework/live-gateway/desktop-preview-moon.png differ diff --git a/docs/rework/live-gateway/desktop-real-chat-moon.png b/docs/rework/live-gateway/desktop-real-chat-moon.png index 4ef7d03..281d265 100644 Binary files a/docs/rework/live-gateway/desktop-real-chat-moon.png and b/docs/rework/live-gateway/desktop-real-chat-moon.png differ diff --git a/docs/rework/live-gateway/mobile-chat-fog.png b/docs/rework/live-gateway/mobile-chat-fog.png index f61b89f..aa85132 100644 Binary files a/docs/rework/live-gateway/mobile-chat-fog.png and b/docs/rework/live-gateway/mobile-chat-fog.png differ diff --git a/docs/rework/live-gateway/mobile-history-moon.png b/docs/rework/live-gateway/mobile-history-moon.png index 3f5b54f..f646983 100644 Binary files a/docs/rework/live-gateway/mobile-history-moon.png and b/docs/rework/live-gateway/mobile-history-moon.png differ diff --git a/docs/rework/live-gateway/mobile-relation-fog.png b/docs/rework/live-gateway/mobile-relation-fog.png index 641ca5c..3a3150b 100644 Binary files a/docs/rework/live-gateway/mobile-relation-fog.png and b/docs/rework/live-gateway/mobile-relation-fog.png differ diff --git a/docs/rework/long-line-360-640-fog.png b/docs/rework/long-line-360-640-fog.png index dd0784a..2a657e4 100644 Binary files a/docs/rework/long-line-360-640-fog.png and b/docs/rework/long-line-360-640-fog.png differ diff --git a/docs/rework/long-line-360-640-moon.png b/docs/rework/long-line-360-640-moon.png index e5e71a1..75a8671 100644 Binary files a/docs/rework/long-line-360-640-moon.png and b/docs/rework/long-line-360-640-moon.png differ diff --git a/docs/rework/long-line-390-420-fog.png b/docs/rework/long-line-390-420-fog.png index 3010b74..af212c4 100644 Binary files a/docs/rework/long-line-390-420-fog.png and b/docs/rework/long-line-390-420-fog.png differ diff --git a/docs/rework/long-line-390-420-moon.png b/docs/rework/long-line-390-420-moon.png index 5d69509..53f2a1e 100644 Binary files a/docs/rework/long-line-390-420-moon.png and b/docs/rework/long-line-390-420-moon.png differ diff --git a/docs/rework/long-line-390-844-fog.png b/docs/rework/long-line-390-844-fog.png index 8ead6ab..3804e2c 100644 Binary files a/docs/rework/long-line-390-844-fog.png and b/docs/rework/long-line-390-844-fog.png differ diff --git a/docs/rework/long-line-390-844-moon.png b/docs/rework/long-line-390-844-moon.png index 8e48ea3..40552ac 100644 Binary files a/docs/rework/long-line-390-844-moon.png and b/docs/rework/long-line-390-844-moon.png differ diff --git a/docs/rework/memory-groups-empty-fog.png b/docs/rework/memory-groups-empty-fog.png index 9719ffa..a018abc 100644 Binary files a/docs/rework/memory-groups-empty-fog.png and b/docs/rework/memory-groups-empty-fog.png differ diff --git a/docs/rework/services-fog.png b/docs/rework/services-fog.png index fd65287..9df7c86 100644 Binary files a/docs/rework/services-fog.png and b/docs/rework/services-fog.png differ diff --git a/web/tests/browser_check.py b/web/tests/browser_check.py index 3d311a6..34934d6 100644 --- a/web/tests/browser_check.py +++ b/web/tests/browser_check.py @@ -97,7 +97,7 @@ try: click(page, '读到了书里的那片海', exact=False) expect(page.get_by_role('heading', name='读到了书里的那片海')).to_be_visible() shot(page, f'{width}-days-{theme}') - click(page, '留一盏小灯') + click(page, '留一盏小灯', exact=False) expect(page.get_by_role('heading', name='留一盏小灯')).to_be_visible() click(page, '读到了书里的那片海', exact=False) click(page, '在交谈里看这段') diff --git a/web/tests/hel825_shot.py b/web/tests/hel825_shot.py new file mode 100644 index 0000000..7c38e1d --- /dev/null +++ b/web/tests/hel825_shot.py @@ -0,0 +1,235 @@ +"""HEL-825 review evidence: named viewports x both themes, confirm dialogs, +error states, floating tag, plus two short screen recordings.""" +import functools +import http.server +import threading +from pathlib import Path + +from playwright.sync_api import sync_playwright, expect + +ROOT = Path(__file__).resolve().parents[1] +OUT = ROOT.parents[0] / 'docs' / 'frontend' / 'screenshots' / 'hel825' +OUT.mkdir(parents=True, exist_ok=True) +VID = ROOT.parents[0] / 'docs' / 'frontend' / 'recordings' +VID.mkdir(parents=True, exist_ok=True) + + +class Quiet(http.server.SimpleHTTPRequestHandler): + def log_message(self, *_): + pass + + +server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), functools.partial(Quiet, directory=str(ROOT / 'dist'))) +threading.Thread(target=server.serve_forever, daemon=True).start() +url = f'http://127.0.0.1:{server.server_port}' +passed = [] +errors = [] + + +def log_pass(text): + passed.append(text) + print('PASS', text, flush=True) + + +with sync_playwright() as pw: + browser = pw.chromium.launch() + + def fresh(width, height, record=None): + ctx = browser.new_context(viewport={'width': width, 'height': height}, record_video_dir=(str(VID) if record else None), + record_video_size={'width': min(width, 1280), 'height': min(height, 800)} if record else None) + page = ctx.new_page() + page.set_default_timeout(9000) + page.on('pageerror', lambda e: errors.append(str(e))) + return ctx, page + + def setup_demo(page, width): + page.goto(url + '/?demo=1') + page.get_by_role('button', name='下一步').click() + page.get_by_label('她的名字', exact=True).fill('小满') + page.get_by_label('出生日期(公历)', exact=True).fill('2000-10-18') + page.get_by_role('button', name='下一步').click() + page.get_by_role('button', name='下一步').click() + page.get_by_role('button', name='朋友', exact=True).click() + page.get_by_role('button', name='下一步').click() + page.get_by_role('checkbox').check() + page.get_by_role('button', name='确认创建', exact=True).click() + page.get_by_role('button', name='去看看她').click() + page.locator('.demo > button').click() + page.get_by_role('button', name='载入三天记录').click() + page.locator('.demo > button').click() + + def nav(page, name): + page.get_by_role('navigation', name='主导航').get_by_role('button', name=name, exact=True).click() + page.wait_for_timeout(450) + if name == '设置' and page.viewport_size['width'] <= 720 and page.locator('.pane.open.has-detail').count(): + page.locator('.pane.open .back').click() + + def shot(page, name): + page.screenshot(path=str(OUT / f'{name}.png'), full_page=False) + + # --- 1600x1000 and 1024 wide: full sweep both themes --- + for width, height in [(1600, 1000), (1024, 1000)]: + ctx, page = fresh(width, height) + setup_demo(page, width) + for theme, label in [('moon', '靛蓝月光'), ('fog', '蓝紫雾光')]: + page.locator('.demo > button').click() + page.get_by_role('button', name=label, exact=True).click() + page.wait_for_timeout(400) + shot(page, f'{width}x{height}-home-{theme}') + # her-now floating tag under role on wide, above panel title <=1100 + nav(page, '日子') + if page.get_by_role('tab', name='她的日子').get_attribute('aria-selected') != 'true': + page.get_by_role('tab', name='她的日子').click() + page.wait_for_timeout(300) + shot(page, f'{width}x{height}-days-{theme}') + page.get_by_role('button', name='读到了书里的那片海').click() + expect(page.get_by_role('heading', name='读到了书里的那片海')).to_be_visible() + shot(page, f'{width}x{height}-days-detail-{theme}') + page.get_by_role('tab', name='我们的回忆').click() + page.wait_for_timeout(300) + shot(page, f'{width}x{height}-memory-{theme}') + page.get_by_role('button', name='留一盏小灯', exact=False).first.click() + expect(page.get_by_role('heading', name='留一盏小灯')).to_be_visible() + shot(page, f'{width}x{height}-memory-detail-{theme}') + # settings + relation confirm dialog + nav(page, '设置') + page.get_by_role('navigation', name='设置分区').get_by_role('button', name='关系', exact=True).click() + page.get_by_label('她怎么称呼你', exact=True).fill('小伙伴') + page.get_by_role('group', name='你们的关系').get_by_role('button', name='恋人', exact=True).click() + page.get_by_role('button', name='保存').click() + expect(page.get_by_role('dialog')).to_contain_text('记忆与信任都保留') + shot(page, f'{width}x{height}-relation-confirm-{theme}') + page.get_by_role('dialog').get_by_role('button', name='确认', exact=True).click() + expect(page.get_by_role('status').filter(has_text='已保存在本机演示')).to_be_visible() + # reset relation back to friend so the next theme loop has a changeable draft + page.get_by_role('group', name='你们的关系').get_by_role('button', name='朋友', exact=True).click() + page.get_by_role('button', name='保存').click() + page.get_by_role('dialog').get_by_role('button', name='确认', exact=True).click() + expect(page.get_by_role('status').filter(has_text='已保存在本机演示')).to_be_visible() + page.wait_for_timeout(400) + shot(page, f'{width}x{height}-settings-{theme}') + # read-fail retry card via demo failure switch + nav(page, '日子') + if page.get_by_role('tab', name='她的日子').get_attribute('aria-selected') != 'true': + page.get_by_role('tab', name='她的日子').click() + page.wait_for_timeout(300) + page.locator('.demo > button').click() + page.get_by_role('button', name='下次操作模拟失败').click() + page.locator('.demo > button').click() + page.reload() + nav(page, '日子') + page.wait_for_timeout(600) + if page.get_by_role('button', name='再试一次').count(): + shot(page, f'{width}x{height}-read-fail-{theme}') + log_pass(f'{width}x{height} {theme}: read-failure card with retry captured') + else: + page.locator('.demo > button').click() + page.get_by_role('button', name='载入三天记录').click() + page.locator('.demo > button').click() + # restore data for next theme loop + page.locator('.demo > button').click() + page.get_by_role('button', name='载入三天记录').click() + page.locator('.demo > button').click() + ctx.close() + log_pass(f'{width}x{height}: home/days/detail/memory/settings/confirm both themes') + + # --- phone viewports --- + for width, height in [(390, 844), (320, 568)]: + ctx, page = fresh(width, height) + setup_demo(page, width) + for theme, label in [('moon', '靛蓝月光'), ('fog', '蓝紫雾光')]: + page.locator('.demo > button').click() + page.get_by_role('button', name=label, exact=True).click() + page.wait_for_timeout(400) + shot(page, f'{width}x{height}-home-{theme}') + nav(page, '日子') + if page.locator('.pane.open.has-detail').count(): + page.locator('.pane.open .back').click() + shot(page, f'{width}x{height}-days-{theme}') + page.get_by_role('button', name='读到了书里的那片海').click() + expect(page.get_by_role('heading', name='读到了书里的那片海')).to_be_visible() + shot(page, f'{width}x{height}-days-detail-{theme}') + nav(page, '设置') + page.get_by_role('navigation', name='设置分区').get_by_role('button', name='她', exact=True).click() + page.wait_for_timeout(300) + shot(page, f'{width}x{height}-settings-her-{theme}') + ctx.close() + log_pass(f'{width}x{height}: home/days/detail/settings both themes') + + # --- 390x430 keyboard-equivalent: composer focused geometry --- + ctx, page = fresh(390, 430) + setup_demo(page, 390) + for theme, label in [('moon', '靛蓝月光'), ('fog', '蓝紫雾光')]: + page.locator('.demo > button').click() + page.get_by_role('button', name=label, exact=True).click() + page.wait_for_timeout(400) + nav(page, '交谈') + composer = page.get_by_role('textbox', name='想和她说的话') + composer.click() + page.wait_for_timeout(500) + shot(page, f'390x430-keyboard-{theme}') + expect(composer).to_be_in_viewport() + ctx.close() + log_pass('390x430 keyboard-equivalent: composer focused, in viewport, both themes') + + # --- recordings: wide nav/dialog/esc, phone nav/back --- + ctx, page = fresh(1600, 1000, record=True) + setup_demo(page, 1600) + page.wait_for_timeout(600) + nav(page, '日子') + page.get_by_role('button', name='读到了书里的那片海').click() + page.wait_for_timeout(700) + page.get_by_role('button', name='在交谈里看这段').click() + page.wait_for_timeout(900) + page.keyboard.press('Escape') + page.wait_for_timeout(600) + page.get_by_role('button', name='结束交谈 · 回到日子').click() + page.wait_for_timeout(600) + nav(page, '设置') + page.get_by_role('navigation', name='设置分区').get_by_role('button', name='关系', exact=True).click() + page.get_by_label('她怎么称呼你', exact=True).fill('小伙伴') + page.get_by_role('group', name='你们的关系').get_by_role('button', name='恋人', exact=True).click() + page.get_by_role('button', name='保存').click() + page.wait_for_timeout(500) + page.get_by_role('dialog').get_by_role('button', name='确认', exact=True).click() + page.wait_for_timeout(700) + nav(page, '此刻') + page.wait_for_timeout(800) + video = page.video.path() + ctx.close() + Path(VID / 'hel825-wide-nav-dialog-esc.webm') + if video: + import shutil + shutil.move(video, VID / 'hel825-wide-nav-dialog-esc.webm') + log_pass('wide recording: days->chat jump, Esc, back, relation confirm, nav') + + ctx, page = fresh(390, 844, record=True) + setup_demo(page, 390) + page.wait_for_timeout(600) + nav(page, '日子') + page.get_by_role('button', name='读到了书里的那片海').click() + page.wait_for_timeout(700) + page.keyboard.press('Escape') + page.wait_for_timeout(600) + nav(page, '交谈') + composer = page.get_by_role('textbox', name='想和她说的话') + composer.click() + page.wait_for_timeout(800) + composer.fill('你好呀') + page.get_by_role('button', name='发送', exact=True).click() + page.wait_for_timeout(1200) + nav(page, '此刻') + page.wait_for_timeout(700) + video = page.video.path() + ctx.close() + if video: + import shutil + shutil.move(video, VID / 'hel825-phone-nav-esc.webm') + log_pass('phone recording: days detail, Esc back, talk send, nav') + + browser.close() + +server.shutdown() +print('ERRORS:', errors if errors else 'none') +if errors: + raise SystemExit(1) diff --git a/web/tests/hel825_tag_probe.py b/web/tests/hel825_tag_probe.py new file mode 100644 index 0000000..3d4d845 --- /dev/null +++ b/web/tests/hel825_tag_probe.py @@ -0,0 +1,69 @@ +"""HEL-825: geometry probe for the 她此刻 floating tag (no image reading needed). +Rule: with the days panel open, >1100px wide the tag sits under the role stage; +<=1100px it sits above the panel title.""" +import functools +import http.server +import threading +from pathlib import Path + +from playwright.sync_api import sync_playwright + +ROOT = Path(__file__).resolve().parents[1] + + +class Quiet(http.server.SimpleHTTPRequestHandler): + def log_message(self, *_): + pass + + +server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), functools.partial(Quiet, directory=str(ROOT / 'dist'))) +threading.Thread(target=server.serve_forever, daemon=True).start() +url = f'http://127.0.0.1:{server.server_port}' + +with sync_playwright() as pw: + browser = pw.chromium.launch() + for width, expect_mode in [(1600, 'under-role'), (1440, 'under-role'), (1101, 'under-role'), (1100, 'above-title'), (1024, 'above-title'), (390, 'above-title')]: + ctx = browser.new_context(viewport={'width': width, 'height': 1000 if width > 800 else 844}) + page = ctx.new_page() + page.set_default_timeout(9000) + page.goto(url + '/?demo=1') + page.get_by_role('button', name='下一步').click() + page.get_by_label('她的名字', exact=True).fill('小满') + page.get_by_label('出生日期(公历)', exact=True).fill('2000-10-18') + page.get_by_role('button', name='下一步').click() + page.get_by_role('button', name='下一步').click() + page.get_by_role('button', name='朋友', exact=True).click() + page.get_by_role('button', name='下一步').click() + page.get_by_role('checkbox').check() + page.get_by_role('button', name='确认创建', exact=True).click() + page.get_by_role('button', name='去看看她').click() + page.locator('.demo > button').click() + page.get_by_role('button', name='载入三天记录').click() + page.locator('.demo > button').click() + page.get_by_role('navigation', name='主导航').get_by_role('button', name='日子', exact=True).click() + page.wait_for_timeout(900) + info = page.evaluate("""() => { + const chip = document.querySelector('.now-chip'); + const pane = document.querySelector('.pane.read'); + const stage = document.querySelector('.stage') || document.querySelector('.who'); + const title = pane ? pane.querySelector('h2') : null; + const near = pane ? pane.querySelector('.k-near') : null; + const r = el => { if (!el) return null; const b = el.getBoundingClientRect(); return {top: b.top, bottom: b.bottom, left: b.left, right: b.right, text: (el.textContent||'').trim().slice(0,40), display: el ? getComputedStyle(el).display : null}; }; + return {chip: r(chip), pane: r(pane), stage: r(stage), title: r(title), near: r(near)}; + }""") + chip, stage, title, near = info['chip'], info['stage'], info['title'], info['near'] + ok = False + detail = '' + if expect_mode == 'under-role' and chip and stage: + ok = chip['display'] != 'none' and chip['top'] >= stage['bottom'] - 6 + detail = f"chip_top={chip['top']:.0f} chip_display={chip['display']} stage_bottom={stage['bottom']:.0f}" + elif expect_mode == 'above-title' and title and near: + ok = near['display'] != 'none' and near['bottom'] <= title['top'] + 6 and near['text'].startswith('她此刻') + detail = f"near_bottom={near['bottom']:.0f} title_top={title['top']:.0f} near={near['text'][:24]}" + print(('PASS' if ok else 'FAIL'), width, expect_mode, detail, flush=True) + if not ok: + raise SystemExit(1) + ctx.close() + browser.close() +server.shutdown() +print('ALL GEOMETRY OK')