HEL-825 review: link-card accessible-name selector fix (browser_check), evidence scripts, regenerated screenshots/recordings
Co-authored-by: multica-agent <github@multica.ai>
@@ -18,5 +18,5 @@
|
||||
"No uncaught browser errors, API/external requests, microphone or camera requests"
|
||||
],
|
||||
"errors": [],
|
||||
"requests": 56
|
||||
"requests": 54
|
||||
}
|
||||
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
@@ -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": [
|
||||
|
||||
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 230 KiB |
|
Before Width: | Height: | Size: 391 KiB After Width: | Height: | Size: 388 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 388 KiB After Width: | Height: | Size: 433 KiB |
|
Before Width: | Height: | Size: 436 KiB After Width: | Height: | Size: 386 KiB |
|
Before Width: | Height: | Size: 282 KiB After Width: | Height: | Size: 276 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 397 KiB After Width: | Height: | Size: 398 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 302 KiB After Width: | Height: | Size: 302 KiB |
|
Before Width: | Height: | Size: 302 KiB After Width: | Height: | Size: 302 KiB |
|
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 401 KiB After Width: | Height: | Size: 402 KiB |
|
Before Width: | Height: | Size: 399 KiB After Width: | Height: | Size: 399 KiB |
|
Before Width: | Height: | Size: 282 KiB After Width: | Height: | Size: 283 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 136 KiB |
@@ -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, '在交谈里看这段')
|
||||
|
||||
@@ -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)
|
||||
@@ -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')
|
||||