- Conversa: chat empurrado por SSE, indicador de "pensando", gesto da resposta com repetir, voz com aviso claro quando o contexto nao e seguro e leitura em voz alta so de respostas novas - Memoria unificada: lembrancas da conversa + rotina/preferencias em abas; apagar tudo com confirmacao inline (sem confirm nativo) - Reacoes: biblioteca com miniaturas estaticas (sem iframe por cartao), busca, mostrar no celular, feedback, editar em <dialog>, duplicar, ativar e excluir com Desfazer no toast; criar com IA; diario; grade de gestos com estrela para favoritos; uma unica previa lateral - treinamento recarrega por evento SSE ao terminar, sem polling - corrige icones encolhidos em botoes .pequeno.icone Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
178 lines
8.2 KiB
Python
178 lines
8.2 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)
|
|
|
|
# 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, celular 400px, tema')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main(sys.argv[1] if len(sys.argv) > 1 else None)
|