"""Painel novo (web/dist) contra o servidor real: token, SSE, gestos, paleta, E-Stop, ajustes e celular. Requer `npm run build` em web/. Uso: python tests/test_painel_web_browser.py [pasta-para-capturas] """ from playwright.sync_api import sync_playwright, expect from pathlib import Path import importlib.util import os import sys import tempfile import threading ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) os.chdir(ROOT) spec = importlib.util.spec_from_file_location('kiosk_web', ROOT / 'kiosk-server.py') kiosk = importlib.util.module_from_spec(spec) spec.loader.exec_module(kiosk) from braco import Braco from cerebro import Cerebro from conversa import Conversa from servos import ControleServos from treinamento import Treinamento TOKEN = 'token-de-teste-123' class HW: def __init__(self): self.calls = [] def pulse(self, ch, us): self.calls.append((ch, us)) def off(self, ch): self.calls.append((ch, None)) def main(capturas=None): assert (ROOT / 'web/dist/index.html').is_file(), 'Rode npm run build em web/ primeiro' with tempfile.TemporaryDirectory() as tmp: tmp = Path(tmp) kiosk.ARQUIVO_APARENCIA = str(tmp / 'aparencia.json') server = kiosk.Servidor(('127.0.0.1', 0), kiosk.Handler) server.token = TOKEN server.controle = kiosk.ControlePC(tmp / 'app.json') server.servos = ControleServos(tmp / 'servos.json', HW()) server.braco = Braco(tmp / 'braco.json', server.servos) server.cerebro = Cerebro(tmp / 'ia.json', server.controle, kiosk.enfileirar) server.conversa = Conversa(tmp / 'memoria.db', server.cerebro) server.treinamento = Treinamento(tmp / 'treinamento.db', iniciar=False) server.conversa.treinamento = server.treinamento kiosk.ligar_eventos(server) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() try: testar('http://127.0.0.1:%d' % server.server_port, server, capturas) finally: kiosk.EVENTOS.fechar() server.shutdown() server.server_close() server.servos.close() def testar(base, server, capturas): auth = {'Authorization': 'Bearer ' + TOKEN} with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page(viewport={'width': 1440, 'height': 960}) erros = [] page.on('pageerror', lambda e: erros.append(str(e))) page.goto(base + '/') # 1. token expect(page.get_by_role('heading', name='Token do Carinha')).to_be_visible() page.get_by_label('Token').fill(TOKEN) page.get_by_role('button', name='Entrar').click() expect(page.get_by_text('Servidor', exact=True)).to_be_visible(timeout=5000) # 2. telemetria do celular chega por SSE page.request.post(base + '/app/status', headers=auth, data={'dormindo': False, 'bateria': 0.8, 'microfone_ativo': True, 'microfone': 0.3}) expect(page.get_by_text('Celular online · 80%')).to_be_visible(timeout=5000) # 3. gesto pelo dock kiosk.FILA.limpar() page.get_by_role('button', name='Feliz').first.click() expect(page.get_by_text('Expressão: feliz')).to_be_visible(timeout=5000) assert kiosk.FILA.ordens() == ['feliz'], kiosk.FILA.ordens() # 4. atalho numérico e paleta page.locator('body').click(position={'x': 5, 'y': 900}) page.keyboard.press('2') page.wait_for_timeout(300) assert kiosk.FILA.ordens()[-1] == 'coracao', kiosk.FILA.ordens() page.keyboard.press('Control+k') page.get_by_label('Buscar comando').fill('surpreso') page.keyboard.press('Enter') page.wait_for_timeout(300) assert kiosk.FILA.ordens()[-1] == 'surpreso', kiosk.FILA.ordens() if capturas: page.screenshot(path=str(Path(capturas) / 'ao-vivo.png'), full_page=True) # 5. E-Stop real page.get_by_role('button', name='Parar tudo').click() expect(page.get_by_role('button', name='PARADO · Rearmar')).to_be_visible(timeout=5000) assert kiosk.ESTOP['ativo'] and server.servos.travado page.get_by_role('button', name='PARADO · Rearmar').click() expect(page.get_by_role('button', name='Parar tudo')).to_be_visible(timeout=5000) assert not server.servos.travado # 6. ajustes salvam sem travar page.get_by_role('link', name='Ajustes').first.click() page.get_by_label('Buscar ajuste').fill('boca') boca = page.get_by_role('switch', name='Mostrar boca') assert boca.is_checked() boca.locator('xpath=..').click() page.wait_for_timeout(500) assert server.controle.config['boca'] is False if capturas: page.get_by_label('Buscar ajuste').fill('') page.screenshot(path=str(Path(capturas) / 'ajustes.png'), full_page=True) # 6b. conversa: pergunta de hora é respondida pelo relógio (sem IA) e chega por SSE page.get_by_role('link', name='Conversa').first.click() page.get_by_label('Mensagem').fill('Que horas são?') page.get_by_role('button', name='Enviar').click() expect(page.get_by_text('no horário de São Paulo')).to_be_visible(timeout=5000) page.get_by_label('Nova lembrança').fill('Meu nome é Júlio') page.get_by_role('button', name='Adicionar').click() expect(page.get_by_text('Meu nome é Júlio')).to_be_visible(timeout=5000) page.get_by_role('tab', name='Rotina e preferências').click() page.get_by_label('Nova rotina').fill('Treino às 7h') page.get_by_role('button', name='Adicionar').click() expect(page.get_by_text('Treino às 7h')).to_be_visible(timeout=5000) if capturas: page.screenshot(path=str(Path(capturas) / 'conversa.png'), full_page=True) # 6c. reações: criar emoji, mostrar no celular, excluir e desfazer pelo toast page.get_by_role('link', name='Reações').first.click() page.get_by_role('button', name='Novo emoji').click() page.get_by_label('Nome').fill('Ideia brilhante') page.get_by_label('Quando usar').fill('Quando tiver uma ideia') page.get_by_role('radio', name='🚀').click() page.get_by_role('button', name='Salvar').click() cartao = page.locator('li.reacao', has_text='Ideia brilhante') expect(cartao).to_be_visible(timeout=5000) cartao.get_by_role('button', name='Mostrar').click() page.wait_for_timeout(400) assert server.treinamento.expressao['receita']['nome'] == 'Ideia brilhante' cartao.get_by_role('button', name='Excluir').click() expect(cartao).to_have_count(0, timeout=5000) page.get_by_role('button', name='Desfazer').last.click() expect(page.locator('li.reacao', has_text='Ideia brilhante')).to_be_visible(timeout=5000) if capturas: page.screenshot(path=str(Path(capturas) / 'reacoes.png'), full_page=True) # 6d. braço: opt-in, homem-morto e desligamento pelo prazo ao soltar hw = server.servos.hw page.get_by_role('link', name='Movimento').first.click() segurar = page.get_by_role('button', name='Segure para ir ao alvo') expect(segurar).to_be_disabled() page.get_by_role('switch', name='Ativar movimento físico').locator('xpath=..').click() expect(segurar).to_be_enabled() hw.calls.clear() segurar.hover() page.mouse.down() page.wait_for_timeout(700) assert server.servos.active, 'motores deveriam estar ligados enquanto segura' expect(page.get_by_text('Motores ligados')).to_be_visible(timeout=3000) page.mouse.up() page.wait_for_timeout(900) assert not server.servos.active, 'motores deveriam desligar sozinhos após soltar' assert any(us for _, us in hw.calls), hw.calls if capturas: page.screenshot(path=str(Path(capturas) / 'movimento.png'), full_page=True) # E-Stop desativa o movimento físico na hora page.get_by_role('button', name='Parar tudo').click() expect(page.get_by_role('switch', name='Ativar movimento físico')).to_be_disabled(timeout=3000) expect(segurar).to_be_disabled() page.get_by_role('button', name='PARADO · Rearmar').click() expect(page.get_by_role('button', name='Parar tudo')).to_be_visible(timeout=3000) # 7. celular: barra inferior e nada de rolagem horizontal page.set_viewport_size({'width': 400, 'height': 860}) page.get_by_role('link', name='Ao vivo').click() page.wait_for_timeout(300) largura = page.evaluate('document.scrollingElement.scrollWidth') assert largura <= 400, 'rolagem horizontal: %s' % largura if capturas: page.screenshot(path=str(Path(capturas) / 'celular.png'), full_page=True) # 8. tema claro page.set_viewport_size({'width': 1280, 'height': 900}) inicial = page.evaluate("document.documentElement.dataset.theme") outro = 'light' if inicial == 'dark' else 'dark' page.get_by_role('button', name='Usar tema ' + ('claro' if outro == 'light' else 'escuro')).click() assert page.evaluate("document.documentElement.dataset.theme") == outro if capturas: page.screenshot(path=str(Path(capturas) / ('tema-' + outro + '.png'))) assert not erros, erros browser.close() print('OK: token, SSE, dock 1-9, paleta, E-Stop, ajustes otimistas, conversa, memória, reações com desfazer, braço com homem-morto, celular 400px, tema') if __name__ == '__main__': main(sys.argv[1] if len(sys.argv) > 1 else None)