Files
carinha-control-panel/tests/test_painel_web_browser.py
T
julioandClaude Opus 5.5 673dea62e8 feat(web): migra Movimento com homem-morto e trava de hardware
- Controle: opt-in de movimento fisico, botao "segure para mover"
  (mouse, toque e teclado), sliders das articulacoes tambem so movem
  enquanto pressionados; velocidade e interpolacao ficam no servidor
- sequencias prontas e posturas salvas: previa so na tela ou execucao
  segurando o botao ate o fim; soltar interrompe na hora
- posturas com nome inline (sem prompt), exclusao confirmada inline,
  exportar/importar; posturas aparecem na paleta (Ctrl+K)
- Calibracao: testes de fim de curso por canal, marcar limites e tabela
  editavel com validacao antes de salvar
- Diagnostico (antigo Servos): ao vivo limitado a 10/s, bloqueado quando
  o braco esta com os motores
- desenho esquematico do braco em SVG com previa animada
- servidor: evento 'motores' tambem ao ligar canais; renovar no alvo
  devolve movendo=false para a sequencia avancar
- aba escondida ou E-Stop soltam o braco e desativam o movimento

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:56:48 -03:00

204 lines
9.6 KiB
Python

"""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)