diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 4554f25..2ed1817 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -2,6 +2,8 @@ name: GitHub Pages on: push: branches: [main] + pull_request: + branches: [main] workflow_dispatch: permissions: contents: read @@ -50,11 +52,20 @@ jobs: if curl --fail --silent "$TEST_URL" > /dev/null; then break; fi sleep 1 done + curl --fail --silent "$TEST_URL" > /dev/null npm run test:e2e + - name: Save browser failure evidence + if: failure() + uses: actions/upload-artifact@v4 + with: + name: browser-test-results + path: test-results/ + if-no-files-found: ignore - uses: actions/upload-pages-artifact@v3 with: path: dist deploy: + if: github.ref == 'refs/heads/main' needs: build runs-on: ubuntu-latest permissions: diff --git a/README.md b/README.md new file mode 100644 index 0000000..acf5035 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# Dancer + +Editor de danças Habbo em AstroJS, TypeScript e [WiredSnippets/wsproom](https://github.com/WiredSnippets/wsproom). Site estático: a edição, reprodução e busca de usuários acontecem no navegador. + +## Executar + +Requer Node.js 22 e Git. No PowerShell com scripts desabilitados, use `npm.cmd` em lugar de `npm`. + +```sh +npm ci +npm run assets:prepare +npm run dev +``` + +A primeira preparação baixa e converte as bibliotecas de avatar com as ferramentas do wsproom. Pode demorar vários minutos. Não baixa furnis. Aguarde a preparação terminar antes de iniciar o servidor de desenvolvimento. + +Os downloads ficam em `.cache/shroom/`; os arquivos publicados em `public/assets/shroom/`. Ambos são ignorados pelo Git. As quatro danças da raiz são exemplos fornecidos com o projeto e permanecem versionadas. + +```sh +npm run assets:check +npm test +npm run typecheck +npm run build +npm run preview +``` + +Com o servidor iniciado, execute `npm run test:e2e` para os testes de navegador. No Windows eles usam o Edge instalado; em Linux execute antes `npx playwright install --with-deps chromium`. `TEST_URL` define outra URL, incluindo o subdiretório de publicação. + +O hook de build do Astro valida hashes, XML, bibliotecas e imagens. Assets ausentes, incompletos ou alterados impedem o build, inclusive ao executar `astro build` diretamente. Os assets são incluídos no artefato estático `dist/`, sem serem commitados. + +Para atualizar o catálogo: `npm run assets:prepare -- --refresh`. A variável `HABBO_EXTERNAL_VARIABLES` permite escolher outra configuração pública de assets. A preparação reutiliza SWFs já baixados da mesma versão do catálogo. + +## Usar + +- Abra uma das quatro danças ou importe XML, `.shroom` ou `.wsproom`. +- Selecione um frame e altere ação, frame do sprite, `dx`, `dy` e `dd` de cada parte. +- Adicione, duplique, remova ou reordene frames. A prévia reflete as edições. +- Use play/pause, o seletor de frame e as setas de rotação. Espaço também alterna a reprodução quando o foco está fora dos campos. +- Cole uma figurestring ou selecione Usuário, informe o nick e escolha o hotel. +- Exporte em XML ou SHROOM. O rascunho também é salvo neste navegador quando o armazenamento local está disponível. + +A busca usa diretamente `https://www.habbo.[hotel]/api/public/users?name=` e lê `figureString`. Depende da disponibilidade e política CORS do hotel. A edição com figurestring continua disponível quando a consulta falha. + +## GitHub Pages + +1. Envie o projeto para seu repositório GitHub, branch `main`. +2. Em **Settings → Pages → Build and deployment**, escolha **GitHub Actions**. +3. Execute o workflow **GitHub Pages** ou envie um commit para `main`. + +O workflow instala dependências pelo lockfile, prepara os assets, executa testes, a checagem obrigatória do build e testes de navegador no resultado estático, e publica o artefato. `site` e `base` são calculados pelo nome do repositório. Não é preciso commitar assets nem criar branch `gh-pages`. + +Localmente, `SITE_URL` e `BASE_PATH` permitem reproduzir a URL de publicação. Exemplo PowerShell: + +```powershell +$env:SITE_URL = 'https://seu-usuario.github.io' +$env:BASE_PATH = '/dancer/' +npm run build +npm run preview +``` + +## Motor e assets + +O motor é `@wiredsnippets/shroom@1.1.0`, com ferramentas de conversão da revisão `eaf266f54d5c8a9eb06174f13a85174e2745e034` de WiredSnippets/wsproom. O adaptador de edição limpa apenas os caches de definição de animação, preservando bibliotecas e texturas; essa integração deve ser revisada ao atualizar o motor. + +Alguns SWFs oficiais contêm entradas de manifest sem símbolo correspondente. A preparação remove apenas referências comprovadamente ausentes no SWF original, mantendo a checagem de falhas de extração. + +Shroom é distribuído sob **LGPL-3.0-or-later**; consulte [COPYING.LESSER](https://github.com/WiredSnippets/wsproom/blob/master/COPYING.LESSER). Os assets Habbo pertencem aos respectivos titulares e não integram o código-fonte deste repositório. diff --git a/docs/superpowers/plans/2026-09-20-dancer.md b/docs/superpowers/plans/2026-09-20-dancer.md index ef28de5..3a14555 100644 --- a/docs/superpowers/plans/2026-09-20-dancer.md +++ b/docs/superpowers/plans/2026-09-20-dancer.md @@ -34,11 +34,11 @@ **Interfaces:** `parseDance(input: Uint8Array | string): DanceDocument`; `toXml(document: DanceDocument): string`; `toShroom(document: DanceDocument): Uint8Array`. Documento contém nome, descrição e frames com partes identificadas por id e atributos action/frame/dx/dy/dd; preservar atributos extras suportados. -- [ ] Criar teste parametrizado com Dance1–4: importar, conferir contagens `[8,8,10,16]`, exportar e reimportar, comparando estrutura e valores negativos. -- [ ] Testar truncamento do container, comprimentos fora dos limites, XML malformado e ausência de frames; executar teste e registrar falha antes da implementação. -- [ ] Implementar codec seguindo o formato de `ShroomAssetBundle` upstream, sem tratar bytes binários como texto integral. -- [ ] Implementar criação, duplicação, remoção e reordenação com frame ativo sempre válido; testar que o último frame não é removido. -- [ ] Executar testes e commitar `feat: add dance document and SHROOM codecs`. +- [x] Criar teste parametrizado com Dance1–4: importar, conferir contagens `[8,8,10,16]`, exportar e reimportar, comparando estrutura e valores negativos. +- [x] Testar truncamento do container, comprimentos fora dos limites, XML malformado e ausência de frames; executar teste e registrar falha antes da implementação. +- [x] Implementar codec seguindo o formato de `ShroomAssetBundle` upstream, sem tratar bytes binários como texto integral. +- [x] Implementar criação, duplicação, remoção e reordenação com frame ativo sempre válido; testar que o último frame não é removido. +- [x] Executar testes e commitar `feat: add dance document and SHROOM codecs`. ## 2. Astro e pipeline de assets @@ -46,14 +46,14 @@ **Interfaces:** preparação produz metadados XML e bibliotecas wsproom em `public/assets/shroom/`; `checkAssets(root: string): Promise` lança erro descritivo ao encontrar ausência ou corrupção. -- [ ] Fixar versão/revisão do wsproom e versões compatíveis de Astro/PixiJS após verificar requisitos de Node. Gerar lockfile. -- [ ] Testar fixtures de assets ausentes, vazios, XML inválido e biblioteca referenciada ausente. Uma pasta vazia deve falhar. -- [ ] Compor download de figuras e conversão upstream individualmente; excluir fluxo genérico que baixa furnis. Validar status HTTP, concluir inventário somente após download e conversão completos. -- [ ] Checar arquivos referenciados pelo figuremap e manifests, incluindo arquivos internos das bibliotecas. Associar inventário à revisão do motor e à origem dos dados. -- [ ] Adicionar `assets:prepare`, `assets:check`, `dev`, `build` e `preview`. Um hook `astro:build:start` aguarda `checkAssets`; falha impede emissão do site. -- [ ] Configurar `output: 'static'`, `site` e `base` por ambiente; buscar arquivos no cliente com `import.meta.env.BASE_URL`. -- [ ] Executar build sem assets e confirmar falha; preparar assets reais e confirmar checagem positiva. Conferir `git ls-files public/assets/shroom .cache/shroom` vazio. -- [ ] Commit `build: configure Astro and mandatory Shroom assets`. +- [x] Fixar versão/revisão do wsproom e versões compatíveis de Astro/PixiJS após verificar requisitos de Node. Gerar lockfile. +- [x] Testar fixtures de assets ausentes, vazios, XML inválido e biblioteca referenciada ausente. Uma pasta vazia deve falhar. +- [x] Compor download de figuras e conversão upstream individualmente; excluir fluxo genérico que baixa furnis. Validar status HTTP, concluir inventário somente após download e conversão completos. +- [x] Checar arquivos referenciados pelo figuremap e manifests, incluindo arquivos internos das bibliotecas. Associar inventário à revisão do motor e à origem dos dados. +- [x] Adicionar `assets:prepare`, `assets:check`, `dev`, `build` e `preview`. Um hook `astro:build:start` aguarda `checkAssets`; falha impede emissão do site. +- [x] Configurar `output: 'static'`, `site` e `base` por ambiente; buscar arquivos no cliente com `import.meta.env.BASE_URL`. +- [x] Executar build sem assets e confirmar falha; preparar assets reais e confirmar checagem positiva. Conferir `git ls-files public/assets/shroom .cache/shroom` vazio. +- [x] Commit `build: configure Astro and mandatory Shroom assets`. ## 3. Motor e reprodução @@ -61,12 +61,12 @@ **Interfaces:** `Preview.setDance(document)`, `Preview.seek(index)`, `Preview.setFigure(figure)`, `Preview.setDirection(direction)`, `Preview.destroy()`. Ticker implementa `IAnimationTicker.subscribe` e `current` upstream, com avanço manual controlado. -- [ ] Testar relógio: pausa mantém frame, seek escolhe frame exato, play dá volta no último frame e elapsed time não depende da taxa de pintura. -- [ ] Injetar ticker em `Shroom`; criar quarto 3×3 sem paredes e avatar em `(1,1,0)`. -- [ ] Fornecer XML editado como dados de efeito ao AvatarLoader. Invalidar caches de efeitos/desenho em edições sem acumular bibliotecas por tecla pressionada. -- [ ] Sincronizar relógio do avatar com seek inclusive depois de carregamento assíncrono ou troca de aparência. -- [ ] Verificar no navegador cada preset, deslocamentos negativos e `dd`, pausa/seek e oito direções. Garantir liberação de ticker, texturas e listeners no descarte. -- [ ] Commit `feat: render editable dances with deterministic playback`. +- [x] Testar relógio: pausa mantém frame, seek escolhe frame exato, play dá volta no último frame e elapsed time não depende da taxa de pintura. +- [x] Injetar ticker em `Shroom`; criar quarto 3×3 sem paredes e avatar em `(1,1,0)`. +- [x] Fornecer XML editado como dados de efeito ao AvatarLoader. Invalidar caches de efeitos/desenho em edições sem acumular bibliotecas por tecla pressionada. +- [x] Sincronizar relógio do avatar com seek inclusive depois de carregamento assíncrono ou troca de aparência. +- [x] Verificar no navegador cada preset, deslocamentos negativos e `dd`, pausa/seek e oito direções. Garantir liberação de ticker, texturas e listeners no descarte. +- [x] Commit `feat: render editable dances with deterministic playback`. ## 4. Editor e aparência @@ -74,26 +74,30 @@ **Interfaces:** `lookupFigure(hotel: Hotel, nick: string, signal?: AbortSignal): Promise`; estado único contém documento, frame ativo, reprodução, figurestring e direção. -- [ ] Testar URL com `URLSearchParams`, hotel inválido, 404, timeout, payload sem figureString e cancelamento. Impedir respostas obsoletas de substituir aparência atual. -- [ ] Construir controles compactos para presets, importação/exportação, frames e tabela de partes. Prévia adjacente com play/pause, rotação e range rotulado. -- [ ] Aplicar figurestring diretamente ou obter `figureString` pela API solicitada. Mostrar erro curto preservando o avatar anterior. -- [ ] Pausar ao selecionar frame; aplicar edições instantaneamente. Implementar persistência local com tratamento de armazenamento indisponível e dados inválidos. -- [ ] Testar falha de importação preservando documento, exclusão do frame ativo ajustando índice e recuperação de rascunho. -- [ ] Inspecionar visualmente desktop e tela estreita; verificar navegação por teclado e rótulos acessíveis, sem títulos decorativos. -- [ ] Commit `feat: add minimal frame editor and hotel avatar lookup`. +- [x] Testar URL com `URLSearchParams`, hotel inválido, 404, timeout, payload sem figureString e cancelamento. Impedir respostas obsoletas de substituir aparência atual. +- [x] Construir controles compactos para presets, importação/exportação, frames e tabela de partes. Prévia adjacente com play/pause, rotação e range rotulado. +- [x] Aplicar figurestring diretamente ou obter `figureString` pela API solicitada. Mostrar erro curto preservando o avatar anterior. +- [x] Pausar ao selecionar frame; aplicar edições instantaneamente. Implementar persistência local com tratamento de armazenamento indisponível e dados inválidos. +- [x] Testar falha de importação preservando documento, exclusão do frame ativo ajustando índice e recuperação de rascunho. +- [x] Inspecionar visualmente desktop e tela estreita; verificar navegação por teclado e rótulos acessíveis, sem títulos decorativos. +- [x] Commit `feat: add minimal frame editor and hotel avatar lookup`. ## 5. GitHub Pages e validação final **Arquivos:** `.github/workflows/pages.yml`, `README.md`, `tests/e2e/editor.spec.ts`. -- [ ] Criar workflow com instalação por lockfile, preparação/cache dos assets, testes, build validado e publicação por artifact do Pages. Usar `contents: read`, `pages: write`, `id-token: write` e environment `github-pages`. -- [ ] Derivar site/base do repositório no workflow, incluindo o caso de repositório `.github.io`. Artefato contém `dist`, não fontes ou cache de SWFs. -- [ ] Testar sob `/dancer/`: carregar presets e assets, play/pause/seek, editar e exportar/reimportar, rotacionar e trocar aparência. -- [ ] Verificar requisição real à API pública no navegador; registrar eventuais bloqueios CORS ou restrições externas em vez de simular sucesso. -- [ ] Documentar comandos de instalação, assets, desenvolvimento, build e ativação do Pages, além da origem/licença do motor. -- [ ] Rodar testes, typecheck e build; conferir arquivos versionados e commits; commit `ci: add GitHub Pages deployment and usage guide`. +- [x] Criar workflow com instalação por lockfile, preparação/cache dos assets, testes, build validado e publicação por artifact do Pages. Usar `contents: read`, `pages: write`, `id-token: write` e environment `github-pages`. +- [x] Derivar site/base do repositório no workflow, incluindo o caso de repositório `.github.io`. Artefato contém `dist`, não fontes ou cache de SWFs. +- [x] Testar sob `/dancer/`: carregar presets e assets, play/pause/seek, editar e exportar/reimportar, rotacionar e trocar aparência. +- [x] Verificar requisição real à API pública no navegador; registrar eventuais bloqueios CORS ou restrições externas em vez de simular sucesso. +- [x] Documentar comandos de instalação, assets, desenvolvimento, build e ativação do Pages, além da origem/licença do motor. +- [x] Rodar testes, typecheck e build; conferir arquivos versionados e commits; commit `ci: add GitHub Pages deployment and usage guide`. - [ ] Se houver remoto autorizado, publicar e conferir workflow e URL. Se continuar sem remoto, informar exatamente o que falta para deploy, sem afirmar publicação concluída. -## Execução proposta +## Execução e verificação -Execução nativa nesta sessão, por etapas, pois o adaptador, os assets e o editor compartilham contratos estreitos. Plano aguardando revisão e escolha do método pelo usuário, conforme a skill writing-plans. +Plano aprovado pelo usuário e executado diretamente nesta sessão, com commits progressivos e revisão independente ao final. As etapas 3 e 4 foram registradas juntas por compartilharem o fluxo de prévia. O usuário integrou as alterações em `Habbianos/dancer`; a primeira execução do Pages falhou nos testes de navegador. A correção salva o rascunho pendente em `pagehide` e verifica a inicialização assíncrona em teste unitário, sem substituir módulos compilados. O workflow também verifica pull requests e preserva traces em caso de falha. + +Evidências: 18 testes unitários, typecheck, três testes de navegador repetidos duas vezes no build de produção sob `/dancer/`; build estático com 3.013 arquivos de assets validados. O teste de navegador cobre edição com comparação de pixels, pausa/seek, quatro presets, oito direções, XML exportado e reimportado, persistência, importação inválida, consulta ao hotel e viewport móvel. A API brasileira também foi consultada sem interceptação no navegador. O build sem assets falhou no hook obrigatório. `git ls-files public/assets/shroom .cache/shroom` retorna vazio. + +Decisões de integração: usar ESM do wsproom para compartilhar uma única instância de PixiJS; converter o índice XML para duas unidades internas do motor; remover referências órfãs de manifests apenas quando também ausentes dos símbolos do SWF original. O adaptador permanece fixado na versão do motor testada. diff --git a/docs/superpowers/specs/2026-09-20-dancer-design.md b/docs/superpowers/specs/2026-09-20-dancer-design.md index 086df2b..1ec6475 100644 --- a/docs/superpowers/specs/2026-09-20-dancer-design.md +++ b/docs/superpowers/specs/2026-09-20-dancer-design.md @@ -41,7 +41,7 @@ Buscar usuários diretamente do navegador em `https://www.habbo.[hotel]/api/publ Configurar `site` e `base` para páginas de projeto e permitir personalização por ambiente. Workflow com checkout, instalação reproduzível, cache de assets associado à revisão do motor/configuração, download/conversão, testes, checagem obrigatória, build, upload do artefato e deploy no environment github-pages. Os assets fazem parte do artefato publicado, nunca dos commits. Não usar branch de distribuição para versionar os arquivos gerados. -Realizar commits progressivos por etapa funcional. Preparar workflow e instruções de ativação do Pages; a execução do deploy depende de um remoto GitHub acessível e configurado, que ainda não existe neste repositório local. +Realizar commits progressivos por etapa funcional. Preparar workflow e instruções de ativação do Pages; o remoto foi configurado pelo usuário como `Habbianos/dancer`. ## Alternativas consideradas @@ -57,4 +57,4 @@ Entregar repositório Git local, commits progressivos, código, lockfile, workfl ## Estado -Revisada com as instruções do usuário sobre AstroJS, GitHub Pages, assets obrigatórios não versionados, API pública e commits progressivos. Código do produto ainda não implementado. +Implementada em AstroJS com editor e renderização no navegador, assets obrigatórios não versionados e workflow GitHub Pages. Revisão independente e testes de regressão concluídos. Alterações integradas pelo usuário em `Habbianos/dancer`. Correção da primeira execução do GitHub Actions validada localmente; publicação remota ainda pendente de verificação. diff --git a/playwright.config.ts b/playwright.config.ts index 9f17ab3..20e828e 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,5 +2,11 @@ import { defineConfig } from '@playwright/test'; export default defineConfig({ testDir: './tests/e2e', timeout: 60000, - use: { baseURL: process.env.TEST_URL ?? 'http://127.0.0.1:4321/', viewport: { width: 1300, height: 850 }, launchOptions: process.platform === 'win32' ? { channel: 'msedge' } : {} }, + use: { + baseURL: process.env.TEST_URL ?? 'http://127.0.0.1:4321/', + viewport: { width: 1300, height: 850 }, + launchOptions: process.platform === 'win32' ? { channel: 'msedge' } : {}, + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + }, }); diff --git a/src/editor/controller.ts b/src/editor/controller.ts index 7b2e249..3d93269 100644 --- a/src/editor/controller.ts +++ b/src/editor/controller.ts @@ -24,14 +24,16 @@ let saveTimer: ReturnType; let renderTimer: ReturnType; function status(message = '') { $('status').textContent = message; } function error(reason: unknown) { status(reason instanceof Error ? reason.message : 'Não foi possível concluir a operação.'); } +function saveDraft() { + clearTimeout(saveTimer); + try { + localStorage.setItem(storageKey, JSON.stringify({ xml: toXml(dance), figure, direction })); + $('saved').textContent = 'Salvo neste navegador'; + } catch { $('saved').textContent = 'Exporte para salvar'; } +} function persist() { clearTimeout(saveTimer); - saveTimer = setTimeout(() => { - try { - localStorage.setItem(storageKey, JSON.stringify({ xml: toXml(dance), figure, direction })); - $('saved').textContent = 'Salvo neste navegador'; - } catch { $('saved').textContent = 'Exporte para salvar'; } - }, 250); + saveTimer = setTimeout(saveDraft, 250); } function pause() { clock.playing = false; syncPlay(); } function syncPlay() { $('play').textContent = clock.playing ? 'Ⅱ' : '▶'; $('play').setAttribute('aria-label', clock.playing ? 'Pausar' : 'Reproduzir'); } @@ -168,7 +170,7 @@ requestAnimationFrame(tick); window.addEventListener('keydown', event => { if (event.code === 'Space' && (event.target === document.body || event.target === $('stage'))) { event.preventDefault(); $('play').click(); } }); -window.addEventListener('pagehide', () => { lookup?.abort(); preview?.destroy(); }); +window.addEventListener('pagehide', () => { saveDraft(); lookup?.abort(); preview?.destroy(); }); async function start() { let restored = false; try { diff --git a/tests/controller.test.ts b/tests/controller.test.ts new file mode 100644 index 0000000..9f0fa55 --- /dev/null +++ b/tests/controller.test.ts @@ -0,0 +1,47 @@ +import { afterEach, beforeEach, expect, it, vi } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { newDance, type DanceDocument } from '../src/dance/document'; +import { parseDance, toXml } from '../src/dance/codec'; + +const state = vi.hoisted(() => ({ factory: vi.fn() })); +vi.mock('../src/preview/renderer', () => ({ createPreview: state.factory })); +const captureListeners = () => vi.spyOn(window, 'addEventListener'); +let listeners: ReturnType; +beforeEach(() => { + vi.resetModules(); vi.useFakeTimers(); + vi.stubGlobal('requestAnimationFrame', () => 0); + const source = readFileSync('src/pages/index.astro', 'utf8'); + document.body.innerHTML = source.match(/([\s\S]*)<\/body>/)![1].replace(/