"""Real Qt/browser rendering with synthetic queues; no customer messages.""" import json from pathlib import Path from types import SimpleNamespace from unittest import TestCase, mock from PySide6.QtWidgets import QApplication, QMainWindow from PySide6.QtCore import QEventLoop, QTimer import wechat_gui_qt as qt from review_state import pending_attention_counts import test_desktop_review_visibility as review_tests class AttentionBadgeTests(TestCase): @classmethod def setUpClass(cls): cls.app = QApplication.instance() or QApplication([]) def setUp(self): review_tests.DesktopReviewTest.setUp(self) def test_counts_are_disjoint_and_count_conversations_not_messages(self): states = {'review': {'awaiting_review': True, 'reply_text': '合成草稿', 'last_lines': ['a'] * 50}, 'human': {'awaiting_review': True, 'manual_required': True}, 'uncertain': {'awaiting_review': True, 'send_state': 'uncertain'}, 'ordinary': {'stage': 'queued'}, 'bad': None} self.assertEqual(pending_attention_counts(states), {'review': 1, 'human': 2, 'total': 3}) self.assertEqual(pending_attention_counts({}), {'review': 0, 'human': 0, 'total': 0}) def test_queue_refresh_updates_both_native_entrances_under_unrelated_filter(self): sidebar = qt.Sidebar(); self.addCleanup(sidebar.deleteLater) self.page.attentionChanged.connect(sidebar.set_attention_counts) review_tests.DesktopReviewTest.seed(self) self.page._set_task_filter('等待中') self.assertEqual([b.attention_count for b in sidebar.nav_buttons[:2]], [3, 3]) self.assertIn('待审核 1,需人工 2', sidebar.nav_buttons[0].toolTip()) self.assertEqual(sidebar.nav_buttons[2].attention_count, 0) self.path.write_text('{}', encoding='utf-8') self.page._load_pending_states() self.assertEqual([b.attention_count for b in sidebar.nav_buttons[:2]], [0, 0]) def test_hidden_queue_refreshes_counts_without_rebuilding_widgets(self): review_tests.DesktopReviewTest.seed(self) host = SimpleNamespace(_console_view='automation', queue_page=self.page, stack=SimpleNamespace(currentWidget=lambda: object()), _schedule_console_push=mock.Mock()) with mock.patch.object(self.page, 'refresh_data') as full: qt.MainWindow._refresh_queue_page(host) full.assert_not_called(); host._schedule_console_push.assert_called_once() self.assertEqual(pending_attention_counts(self.page.pending_states)['total'], 3) def test_native_badge_scales_and_preserves_exact_accessible_count(self): button = qt.SidebarNavButton('robot', '自动回复'); self.addCleanup(button.deleteLater) for scale in (1, 2, 3): with mock.patch.object(qt, '_UI_SCALE', scale): button.resize(100 * scale, 77 * scale) button.set_attention_counts(120, 3) image = button.grab().toImage() red = sum(1 for y in range(image.height()) for x in range(image.width()) if image.pixelColor(x,y).red() > 190 and image.pixelColor(x,y).green() < 90) self.assertGreater(red, 30) self.assertIn('123', button.accessibleName()) def test_web_badges_update_while_editing_and_hide_when_resolved(self): owner = QMainWindow(); owner._push_console_state = mock.Mock() shell = qt.ConsoleShell(owner); owner.setCentralWidget(shell); owner.resize(1440,1000); owner.show() self.addCleanup(owner.deleteLater); self.addCleanup(owner.close) if not shell.page_ready: loaded=[]; loop=QEventLoop() def finished(ok): loaded.append(bool(ok)); loop.quit() shell.loadFinished.connect(finished); QTimer.singleShot(10000,loop.quit); loop.exec() shell.loadFinished.disconnect(finished); self.assertEqual(loaded,[True]) def evaluate(script): values=[]; loop=QEventLoop() def completed(value): values.append(value); loop.quit() shell.page().runJavaScript(script,completed); QTimer.singleShot(5000,loop.quit); loop.exec() self.assertEqual(len(values),1); return values[0] payload = {'view':'system','demo':False,'ready':True,'system':{'scale':'自动'}, 'attention': {'review':1,'human':2,'total':3}} evaluate('window.applyState(' + json.dumps(payload) + '); fit();') loop=QEventLoop(); QTimer.singleShot(250,loop.quit); loop.exec() query = '''JSON.stringify(Array.from(document.querySelectorAll('.nav-attention-badge')).map(b=>({ count:b.textContent, hidden:getComputedStyle(b).display==='none', title:b.closest('button').title, red:getComputedStyle(b).backgroundColor, inside:b.getBoundingClientRect().right<=b.closest('button').getBoundingClientRect().right, rect:[b.getBoundingClientRect().right,b.closest('button').getBoundingClientRect().right] })))''' badges = json.loads(evaluate(query)) self.assertEqual(len(badges),2) self.assertTrue(all(b['count']=='3' and not b['hidden'] and b['inside'] for b in badges), badges) self.assertTrue(all(b['red']=='rgb(230, 55, 79)' for b in badges)) evaluate("document.querySelector('#sys-server-url').focus(); document.querySelector('#sys-server-url').value='synthetic-edit';") evaluate("window.applyState({view:'system',attention:{review:120,human:3,total:123}});") self.assertTrue(all(b['count']=='99+' and '123' in b['title'] for b in json.loads(evaluate(query)))) self.assertEqual(evaluate("document.querySelector('#sys-server-url').value"), 'synthetic-edit') evaluate("window.applyState({view:'system',attention:{review:0,human:0,total:0}});") self.assertTrue(all(b['hidden'] for b in json.loads(evaluate(query)))) # Capture only this synthetic profile for visual verification. evaluate("document.querySelector('#sys-server-url').blur(); window.applyState({view:'auto',people:[],task:null,metrics:{review:'12',human:'2'},attention:{review:12,human:2,total:14}});") image_path = Path('C:/kefu/deploy/review-badges-20260917/badge-preview.png') loop=QEventLoop(); QTimer.singleShot(150,loop.quit); loop.exec() shell.grab().save(str(image_path))