Autor: José Anderson (@DessimA)
LinkedIn: /in/dessim
Website: dessima.pages.dev
- Visão Geral
- Arquitetura de Software
- Estrutura de Pastas
- Módulos e Responsabilidades
- Telas e Fluxo de Navegação
- Regras de Negócio
- Requisitos Funcionais
- Requisitos Não Funcionais
- Protocolo JSON
- Persistência de Dados
- Design System
- Internacionalização (i18n)
- Monitoramento
- Qualidade de Código (Lint)
- Testes Automatizados
- CI/CD
- Contribuição
O QuizLab é uma Single Page Application (SPA) desenvolvida em Vanilla JavaScript (ES6+), construída com Vite e zero dependências em runtime. A aplicação permite criar, importar, gerenciar e responder simulados de múltipla escolha diretamente no navegador.
Toda a lógica de negócio, validação, estado e persistência ocorre no lado do cliente (client-side), sem necessidade de servidor ou backend. A aplicação funciona offline após o primeiro carregamento graças a um Service Worker (PWA).
Pilares do projeto:
- Zero Runtime Dependencies nenhuma biblioteca externa, framework ou bundler em produção.
- Client-Side First lógica e persistência vivem inteiramente no navegador.
- Modularidade cada responsabilidade é isolada em um módulo IIFE próprio, exposto via
window. - DRY lógica de renderização e validação reutilizável em toda a base de código.
- i18n Nativo todos os textos da interface centralizados em um repositório único.
O projeto possui uma arquitetura de duas camadas:
-
Camada de Build (Vite + ESM): Os arquivos são escritos como módulos ES6 (
import/export). O Vite faz o bundle e code-splitting, gerando múltiplos entry points:index.html(app principal),docs.html(documentação) etroubleshooting.html(ajuda). -
Camada de Runtime (IIFE Singletons): Em runtime, cada módulo é um objeto Singleton encapsulado em IIFE (Immediately Invoked Function Expression) e exposto no escopo global via
window, simulando namespaces. Isso garante zero dependências entre módulos em produção.
O padrão de comunicação central é o Event Delegation: um único listener no document intercepta todos os eventos, roteia pelo atributo data-action do elemento HTML e executa o handler registrado no app.js. Isso elimina addEventListener espalhados pelo código e desacopla completamente o HTML da lógica JS.
graph TD
DOM[DOM Events] --> Delegator[EventDelegator]
Delegator -- "data-action route" --> App[app.js / App class]
App -- "init / reset / confirm" --> Engine[QuizEngine]
App -- "wizard / export" --> Creator[CreatorManager]
App -- "CRUD / search / bulk" --> Lib[LibraryManager]
App -- "change screen" --> Screen[ScreenManager]
Engine -- "getState()" --> Screen
Engine -- "getState()" --> Renderer[QuizRenderer]
Engine -- "saveStatsToLibrary()" --> Storage[StorageManager]
Creator -- "validateQuiz()" --> Validator[Validator]
Lib -- "get / set / canStore()" --> Storage
App -- "show()" --> Toast[ToastSystem]
App -- "confirm() / alert() / custom()" --> Modal[ModalManager]
Storage <--> LS[(LocalStorage)]
| Camada | Responsabilidade |
|---|---|
| Core | Configurações globais, persistência, validação |
| Components | Componentes de UI reutilizáveis sem estado de negócio |
| Features | Lógica de negócio de cada funcionalidade |
| UI | Renderização de telas e roteamento de eventos |
| Data | Internacionalização e repositório de textos |
sequenceDiagram
participant HTML as index.html
participant Vite as Vite Bundle
participant Entry as js/entries/index.js
participant App as js/app.js (App class)
participant Modules as Módulos IIFE
HTML->>Vite: <script type="module" src="js/entries/index.js">
Vite->>Entry: Carrega entry point
Entry->>Entry: Importa datadog-rum.js, version.js, config.js
Entry->>Entry: Importa todos os módulos IIFE
Entry->>Entry: Importa TEXTS e I18n
Entry->>App: Importa app.js
Entry->>window: Expõe TEXTS globalmente
Entry->>I18n: I18n.init(TEXTS)
Note over App: DOMContentLoaded
App->>I18n: I18n.apply()
App->>Modules: ThemeManager.init()
App->>Modules: ToastSystem.init()
App->>Modules: StorageManager.init()
App->>Modules: ModalManager.init()
App->>Modules: EventDelegator.init()
App->>App: _setupEventHandlers()
App->>Modules: Adiciona quiz demo se biblioteca vazia
quizlab/
├── index.html # Entry point principal (SPA)
├── docs.html # Documentação do usuário (com i18n)
├── troubleshooting.html # Página de ajuda e suporte (com i18n)
├── styles.css # Estilos globais com variáveis CSS
├── sw.js # Service Worker (PWA)
├── vite.config.js # Configuração do Vite (3 entry points)
├── package.json
├── CHANGES.md # Changelog detalhado
├── .env.example # Exemplo de variáveis de ambiente (Datadog)
│
├── js/
│ ├── entries/
│ │ ├── index.js # Entry point principal (Vite)
│ │ ├── docs.js # Entry point docs.html (com i18n)
│ │ └── troubleshooting.js # Entry point troubleshooting.html
│ ├── app.js # Classe App orquestradora
│ ├── datadog-rum.js # Monitoramento Datadog RUM
│ ├── core/
│ │ ├── version.js # Constante APP_VERSION
│ │ ├── config.js # CONFIG e Utils (constantes, enums, helpers)
│ │ ├── storage-manager.js # Facade para o localStorage
│ │ └── validator.js # Validação de schema JSON
│ ├── components/
│ │ ├── exam-nav-map.js # Painel de mapa de questões (Modo Exame)
│ │ ├── icon-system.js # Injeção de SVG inline
│ │ ├── theme-manager.js # Toggle dark/light com persistência
│ │ ├── modal-manager.js # Modais de confirmação, alerta e custom
│ │ ├── toast-system.js # Notificações flutuantes
│ │ └── focus-trap.js # Acessibilidade: foco dentro de modais
│ ├── features/
│ │ ├── quiz-engine.js # Estado do quiz, lógica de resposta e timer
│ │ ├── creator-manager.js # Wizard de criação e drag & drop
│ │ ├── library-manager.js # Renderização, busca, seleção em massa
│ │ ├── review-manager.js # Tela de revisão, resultado e rastreamento
│ │ ├── review-quiz-builder.js # Montagem do quiz temporário de revisão
│ │ └── file-handler.js # Leitura, parse e importação de JSONs
│ └── ui/
│ ├── screen-manager.js # Troca de telas e retomada de sessão
│ ├── quiz-renderer.js # Renderização das questões no DOM
│ └── event-delegator.js # Listener único e roteamento data-action
│
├── data/
│ ├── texts.js # Repositório central de textos da UI e docs
│ └── i18n.js # Motor de internacionalização
│
├── tests/
│ ├── setup/
│ │ ├── environment.js # Polyfills de DOM para Node.js
│ │ └── loader.js # Carregador de módulos IIFE em Node
│ ├── unit/
│ │ ├── quiz-engine.test.js # Engine + timer + exam mode (49 testes)
│ │ ├── validator.test.js # Schema validation
│ │ ├── storage-manager.test.js
│ │ ├── file-handler.test.js
│ │ ├── p1-p2-fixes.test.js # Regressão: sessão órfã, debounce, CSS
│ │ └── security-fixes.test.js # Regressão: XSS, módulos reais
│ └── integration/
│ └── quiz-flow.test.js # Fluxo completo estudo + exame
│
├── e2e/ # Testes End-to-End (Playwright)
│ ├── package.json # "type": "module" para o diretório
│ ├── helpers.js # Seed de biblioteca e fluxos reutilizáveis
│ ├── fixtures/ # JSONs determinísticos para os testes
│ ├── quiz-flow.spec.js # Modo estudo: iniciar, responder, resultado
│ ├── exam-mode.spec.js # Modo exame: timer, sem feedback, setas
│ ├── library.spec.js # Import, busca, ordenação, exclusão
│ ├── creator.spec.js # Criação de simulado do zero
│ ├── resume.spec.js # Retomada de sessão salva
│ └── shuffle.spec.js # Embaralhamento de questões/alternativas
│
├── playwright.config.js # Configuração do Playwright (webServer Vite)
├── dist/ # Saída do build (Vite)
├── assets/ # Imagens e favicon
│
└── .github/
└── workflows/
├── ci.yml # Testes + E2E em PRs para main e develop
└── deploy.yml # Testes em push para main (produção)
Define a constante APP_VERSION como fonte única de verdade da versão da aplicação. É carregado primeiro pelo entry point, garantindo que o nome do cache do Service Worker seja sempre atualizado junto com a versão.
Centraliza todas as constantes, enums e funções utilitárias puras da aplicação.
CONFIG.STORAGE // chaves do localStorage
CONFIG.LIMITS // quotas de armazenamento, histórico
CONFIG.TIMINGS // autosave (30s), toast (4s), debounce (300ms), timer (120s/questão)
CONFIG.QUESTION_TYPES // 'unica' | 'multipla'
CONFIG.QUIZ_MODES // 'study' | 'exam'
CONFIG.ELEMENTS // IDs das telas no DOM
Utils.formatTime(seconds) // formata MM:SS
Utils.plural(count, word) // pluraliza palavra
Utils.truncate(text) // corta com reticênciasLimites de armazenamento relevantes:
| Constante | Valor | Descrição |
|---|---|---|
STORAGE_SAFE_QUOTA_BYTES |
4 MB | Quota fixa medida no localStorage |
STORAGE_WARN_THRESHOLD |
0.70 | Barra amarela a partir de 70% de uso |
STORAGE_BLOCK_THRESHOLD |
0.85 | Bloqueio de novas importações a partir de 85% |
MAX_HISTORY_ENTRIES |
10 | Partidas mantidas no histórico por simulado |
MAX_WRONG_PER_QUIZ |
65 | Máx. de questões erradas rastreadas por simulado |
Facade para o localStorage. Todas as operações de leitura e escrita passam por aqui, centralizando a serialização JSON e o tratamento de erros.
Métodos relevantes:
getById(id)atalho paragetLibrary().find(), evita repetição nos consumidores.replaceInLibrary(id, data)substitui conteúdo e atualizaquestionsCountem uma única escrita.updateLibraryMeta(id, updates)merge parcial dos metadados sem tocar nas questões.removeManyFromLibrary(ids)exclui um array de IDs em uma única escrita, limpando sessão órfã se necessário.updateQuizStats(id, stats)registra resultado, acumula histórico (limitado aMAX_HISTORY_ENTRIES) e recalcula média.saveWrongQuestions(id, list)thin wrapper emupdateLibraryMetapara persistirwrongQuestions.getAggregatedWrong(quizIds?)agregawrongQuestionsde todos ou de IDs selecionados.getStorageStats()síncrono. Mede o uso real dolocalStorageviaBlob.sizepor chave. Retorna{ usage, quota, percent }.canStore(data)síncrono. Projeta o uso pós-adição e bloqueia se>= STORAGE_BLOCK_THRESHOLD. Retorna{ allowed, reason?, stats }.
Valida estruturas de dados em dois contextos:
validateQuiz(data)valida um objeto JSON completo antes de importar ou exportar. Retorna{ valid: Boolean, errors: String[] }.isQuestionCardValid(card)valida um card de questão no DOM do criador em tempo real.
Classe App que orquestra a inicialização de todos os módulos. Verifica a disponibilidade de cada manager (com retry até 3s), configura handlers de eventos, gerencia auto-save do rascunho, atalhos de teclado (Ctrl+S, Escape), eventos de timer (quizlab:timer-tick, quizlab:timer-expired) e adiciona um simulado demo na primeira visita.
Responsabilidades principais: inicializar módulos na ordem correta, registrar todos os handlers data-action via EventDelegator.registerMultiple(), gerenciar modal de opções do quiz, e coordenar o fluxo de exportação.
O "cérebro" da aplicação. Não toca no DOM. Mantém e expõe o estado completo do quiz em andamento.
Responsabilidades: inicializar e restaurar sessão, selecionar alternativas (única/múltipla), confirmar respostas, navegar entre questões, marcar questões com flag, controlar o timer via setInterval e emitir eventos customizados (quizlab:timer-tick, quizlab:timer-expired, quizlab:state-changed, quizlab:answer-changed).
Modo Exame: gerencia o estado answersFinalized que só é ativado ao chamar finalize() (manual ou por expiração do timer). Antes da finalização, select() permite alterações livres na resposta da questão atual. getPendingReview() retorna questões não respondidas e/ou marcadas com flag. getExamProgress() retorna contagens de progresso para a UI. finalize() calcula o score total e marca questionAnswered[] para todas as questões.
Gerencia o wizard de criação e edição de simulados: construir cards de questão no DOM, reordenar por drag & drop, validar cada card em tempo real, serializar o objeto final e acionar preview/exportação.
Renderiza e gerencia a tela de biblioteca com CRUD, busca, seleção em massa, abas (Simulados/Revisão), indicador de armazenamento circular SVG, painel de revisão de erros e badge de erros.
Gerencia a tela de revisão pré-finalização e a tela de resultado. Renderiza gabarito completo, seção de questões marcadas, histórico de tentativas e persiste o banco de erros.
Monta o objeto de quiz temporário para a sessão de revisão de erros a partir do array agregado de wrongQuestions.
Pipeline de importação de arquivos JSON. Suporta importação única e em massa (até 50 arquivos, 2MB cada), com classificação em salvos, ignorados (idênticos), conflitos e falhas.
Controla a troca de telas, inicialização de quiz, restauração de sessão, barra de timer do Modo Exame e overlay de carregamento.
Constrói o HTML da questão atual a partir do estado do QuizEngine e injeta no DOM.
Registra um único listener por tipo de evento no document. Ao capturar um evento, sobe a árvore DOM buscando o atributo data-action, data-oninput ou data-onchange e executa o handler correspondente registrado via register() ou registerMultiple().
Componente IIFE que gerencia o painel de mapa de questões do Modo Exame. Cria um overlay modal com grade de células numeradas, cada uma com estado visual:
- Cinza: não visitada / sem resposta
- Azul/primary: pelo menos uma alternativa selecionada
- Amarelo/warning: marcada com flag para revisão
- Borda destacada: questão atual
Fechamento via Escape ou clique fora. Atualização dinâmica via evento quizlab:state-changed.
Injeção de ícones SVG inline. Usa WeakSet para impedir injeção duplicada. Aguarda document.fonts.load() antes de renderizar.
Notificações flutuantes com auto-dismiss (4s), tipos info, success e error.
Gerencia modais de confirmação (confirm), alerta (alert) e conteúdo dinâmico (custom). Suporta múltiplos modais identificados por ID.
Prende o foco dentro de modais abertos para navegação por teclado.
Alterna data-theme no <html>, persiste no localStorage e atualiza ícone e aria-label.
Repositório centralizado de todos os textos visíveis da interface, organizado por contexto (app, header, landing, library, import, editor, quiz, result, creator, options, feedback, review, modal, confirm, export, toast, stats, footer). Nenhuma string hardcoded deve existir nos módulos.
Motor de internacionalização que carrega os textos de texts.js e os aplica nos elementos HTML via atributos data-i18n, data-i18n-placeholder, data-i18n-value e data-i18n-badge. Métodos públicos: init(), t() (tradução por caminho), apply() (aplica em toda a página), applyTo() (aplica em container específico), format() (interpolação de variáveis).
flowchart TD
Landing([Landing Page]) --> Upload
Upload[Upload Screen] -->|Importar JSON| Validate{Validar JSON}
Validate -->|Inválido| Upload
Validate -->|Único válido| Options
Validate -->|Múltiplos válidos| Batch[Batch Report Modal]
Batch --> Library
Upload -->|Acessar Biblioteca| Library[Library Screen]
Upload -->|Criar Simulado| Creator[Creator Screen]
Library -->|Iniciar| Options[Quiz Options Modal]
Library -->|Retomar sessão salva| Quiz
Library -->|Editar| Creator
Library -->|Excluir em massa| Library
Creator -->|Exportar / Salvar| Library
Options -->|Confirmar modo e embaralhamento| Quiz[Quiz Screen]
Quiz -->|Última questão respondida| Review[Review Screen]
Review -->|Questões pendentes| Quiz
Review -->|Finalizar| Result[Result Screen]
Quiz -->|Timer expirado - Modo Exame| Result
Result --> Upload
Telas registradas em CONFIG.ELEMENTS:
| ID | Descrição |
|---|---|
landingPage |
Apresentação inicial com CTAs e funcionalidades |
uploadScreen |
Hub principal: importar, biblioteca, criar |
quizScreen |
Resolução das questões |
reviewScreen |
Revisão de questões pendentes antes de finalizar |
resultScreen |
Resultado final com estatísticas e histórico |
libraryScreen |
Grade de simulados salvos com seleção em massa |
creatorScreen |
Wizard de criação e edição |
- Somente arquivos
.jsonsão aceitos (validação de extensão e MIME type). - Limite de 50 arquivos por vez e 2MB por arquivo, com timeout de 10s.
- O arquivo passa pela validação completa de schema antes de qualquer ação.
- Se o simulado já existe na biblioteca com o mesmo nome e mesmo conteúdo (verificado via hash dos IDs das questões), o arquivo importado é ignorado silenciosamente.
- Se o nome coincide mas o conteúdo difere, o usuário é consultado para substituir ou não (fluxo de arquivo único). Em importação em massa, conflitos são reportados e devem ser resolvidos individualmente.
- A importação é bloqueada quando o uso do
localStorageatinge 85% da quota segura de 4 MB.
- Capacidade determinada dinamicamente pela quota do
localStorage(quota segura: 4 MB). - Indicador circular SVG no cabeçalho da tela exibe o percentual de uso em tempo real.
- Modo seleção em massa permite selecionar múltiplos cards e excluí-los em uma única operação.
- Cada item armazena metadados de desempenho: média de acertos, número de partidas, data do último acesso e histórico das últimas 10 partidas.
- Ao excluir um simulado, a sessão ativa associada é removida automaticamente do
localStorage.
Ao iniciar um simulado, o usuário escolhe:
- Modo Estudo: feedback visual imediato após confirmar cada resposta. Progresso salvo automaticamente para retomada futura.
- Modo Exame: sem feedback durante a resolução. Timer decrescente em barra dedicada (
#examTimerBar) com animação de pulso nos últimos 60 segundos. Resultado somente ao finalizar ou quando o tempo esgotar (calculado comoquestões × 120 segundos, ou valor customizado).
O usuário pode optar por embaralhar a ordem das questões, a ordem das alternativas, ou ambas, via algoritmo Fisher-Yates.
Calculado automaticamente (questões × 120 segundos). O QuizEngine gerencia o intervalo e emite eventos customizados sem tocar no DOM. A camada de UI consome esses eventos para atualizar #timerDisplay e #examTimerBar.
Se o usuário fechar o navegador durante um Modo Estudo, o progresso é detectado ao acessar a biblioteca. O card do simulado exibe um botão "Retomar" ao lado de "Iniciar". Um hash de sessão detecta se o quiz foi modificado desde o salvamento.
Salvo automaticamente a cada 30 segundos (e também com Ctrl+S). Restaurado na próxima abertura do criador.
- Ocorre apenas para simulados salvos na Biblioteca Local.
- Ao finalizar uma partida, questões incorretas são persistidas. Questões acertadas são removidas do banco.
- Limite de 65 questões por simulado. Ao atingir, questões com menor
errorCountsão descartadas. - O quiz de revisão é temporário: não possui
libraryIde não é salvo na biblioteca.
Fluxo de Revisão de Erros:
sequenceDiagram
participant LM as LibraryManager
participant SM as StorageManager
participant RQB as ReviewQuizBuilder
participant SC as ScreenManager
participant QE as QuizEngine
participant RM as ReviewManager
LM->>SM: getAggregatedWrong(selectedIds)
SM-->>LM: wrongQuestions[]
LM->>RQB: build(wrongQuestions, qty)
RQB-->>LM: quizObj com _reviewSources
LM->>SC: loadQuiz(quizObj, null, options)
SC->>QE: init(quizObj)
Note over QE: Sessão temporária (sem libraryId)
QE-->>SC: state pronto
SC->>SC: change(quizScreen)
Note over RM: Ao finalizar...
RM->>RM: _extractAndSaveWrongQuestions()
RM->>SM: updateLibraryMeta(sourceId, ...) × N origens
RF01 Importação de JSON único
RF02 Importação em massa (até 50 arquivos)
RF03 Validação de schema
RF04 Biblioteca de simulados com CRUD
RF05 Indicador de armazenamento (circular SVG com thresholds 70%/85%)
RF06 Criador de simulados com drag & drop e rascunho automático
RF07 Modo Estudo (feedback imediato + salvamento automático)
RF08 Modo Exame (timer decrescente + barra dedicada)
RF09 Questões de única e múltipla escolha
RF10 Embaralhamento (questões e/ou alternativas)
RF11 Marcação de questões (flag)
RF12 Navegação livre entre questões via grade
RF13 Revisão antes de finalizar
RF14 Retomada de sessão salva (com hash de integridade)
RF15 Histórico de desempenho (últimas 10 partidas)
RF16 Tema claro e escuro (com persistência)
RF17 Onboarding de primeira visita
RF18 Rascunho automático do criador (30s + Ctrl+S)
RF19 Rastreamento de erros (com limite e deduplicação)
RF20 Aba Revisão na Biblioteca com badge numérico
RF21 Listagem de fontes de revisão com seleção individual
RF22 Controle de quantidade via slider
RF23 Quiz de revisão temporário
RF24 Atualização pós-revisão (remove acertos do banco)
RF25 Internacionalização da interface (i18n)
RF26 Relatório de desempenho por tema (pontos fortes/fracos)
RF27 Sugestão de tags no criador (24 tags padrão)
RF28 Exportação com opção de salvar na biblioteca
RF29 Edição de respostas durante o Modo Exame (alteração livre até finalização)
RF30 Mapa de questões (ExamNavMap) com grade visual e estados (respondida/marcada/atual)
RF31 Revisão pré-finalização obrigatória no Modo Exame
RF32 Proteção contra saída acidental (beforeunload) durante exame ativo
RNF01 Zero dependências em runtime (Vanilla JS) RNF02 Funciona Offline (PWA com Service Worker, Cache First para fontes, Network First para assets) RNF03 Persistência Client-Side (localStorage) RNF04 Guard Dinâmico de Cota (medição real via Blob.size, quota segura de 4 MB) RNF05 Responsividade (mobile < 480px, tablet, desktop ≥ 1024px) RNF06 Performance de Eventos (Event Delegation único no document) RNF07 Integridade de Estado (SRP: engine não toca no DOM, storage é acesso único ao localStorage) RNF08 Acessibilidade (focus trap em modais, aria-live para feedback/timer, contraste 4.5:1+, suporte a prefers-contrast e prefers-reduced-motion) RNF09 Segurança (textContent substitui innerHTML, Utils.escapeHtml(), validação de tipos, sanitização, timeout em leitura de arquivos) RNF10 Build tooling com Vite (code-splitting, 3 entry points, substituição de import.meta.env) RNF11 Content Security Policy (CSP) via meta tag em todas as páginas RNF12 Sanitização de XSS em todos os pontos de entrada de dados do usuário
Formato esperado para importação e exportação:
{
"nomeSimulado": "string (obrigatório)",
"descricao": "string (opcional)",
"tags": ["string"],
"tempoLimiteMinutos": 60,
"questoes": [
{
"id": "string | number (obrigatório)",
"enunciado": "string (obrigatório)",
"tipo": "unica | multipla (obrigatório)",
"tags": ["string (obrigatório: 1 a 2 temas predominantes)"],
"alternativas": [
{ "id": "string (obrigatório)", "texto": "string (obrigatório)" }
],
"respostasCorretas": ["id_da_alternativa"],
"explicacao": "string (opcional): pode citar alternativas por {{id}}"
}
]
}Regras de validação:
nomeSimulado,questoessão obrigatórios.- Cada questão deve ter
id,enunciado,tipo,alternativas(mínimo 2) erespostasCorretas(mínimo 1). tagspor questão é obrigatório: array com 1 a 2 strings (temas predominantes), sem itens vazios nem duplicadas. São usadas no resultado final para o relatório de desempenho por tema (matriz, lacunas críticas, gaps e plano de estudo).- Para
tipo: "unica",respostasCorretasdeve conter exatamente 1 elemento. - Todos os IDs em
respostasCorretasdevem existir no arrayalternativasda questão. explicacaoé opcional e pode citar as alternativas por referências dinâmicas{{id}}(ex.:A resposta é '{{c}}'). O QuizLab substitui o placeholder pela letra exibida na ordem atual, então a explicação acompanha o embaralhamento de questões e alternativas (padrão usado nos bancos "Revisão de Véspera"). Referências a IDs inexistentes são preservadas intactas para facilitar a detecção de erros, e o validator rejeita refs a IDs que não existem.- Todos os campos string são validados contra tipos primitivos.
tempoLimiteMinutosé opcional; se ausente, o timer do Modo Exame é calculado automaticamente (questões × 120 segundos).
Todas as chaves do localStorage são centralizadas em CONFIG.STORAGE:
| Chave | Conteúdo |
|---|---|
quizlab_library |
Array de simulados com metadados |
quizlab_session |
Estado da sessão ativa (Modo Estudo) |
quizlab_draft |
Rascunho do criador (autosave a cada 30s, restaurado ao abrir) |
quizlab_theme |
Preferência de tema (dark / light) |
Estrutura de metadados por item da biblioteca:
{
"id": "uuid",
"data": { ... },
"meta": {
"timesPlayed": 5,
"lastPlayed": 1700000000000,
"averageScore": 72,
"history": [
{ "playedAt": 1700000000000, "score": 80, "correct": 8, "total": 10 }
],
"wrongQuestions": [
{ "sourceQuizId": "uuid", "sourceQuizName": "string", "questao": { ... }, "errorCount": 1 }
]
}
}O design system é definido inteiramente em variáveis CSS no :root de styles.css,
organizadas em três camadas (primitivo → semântico → componente), seguindo o
Design System Coringa v2. Nenhum valor de cor, espaçamento ou tipografia deve ser
hardcoded nos módulos.
Primitivos: --roxo-950/900/800/700/500/300/100, --verde-700/500/300,
--cinza-800/600/400/200, --branco, --preto-absoluto, --vermelho-500/700,
--amarelo-500, --teal-500/700/900, --azul-500/700.
Semânticos:
- Ação (verde, racionado a CTAs e destaques):
--primary,--primary-dark,--primary-glow,--on-accent(texto sobre fundo verde) - Marca (roxo):
--brand,--brand-strong,--brand-glow - Estado:
--success/--success-strong(teal),--error/--error-bg/--danger-solid(vermelho),--warning(amarelo),--info/--info-glow(azul) - Superfícies:
--bg,--bg-glass,--surface,--surface-light,--code-bg,--code-text - Texto e borda:
--text,--text-main,--text-secondary,--text-muted,--border,--border-glass,--overlay
Tipografia: --font (Inter, corpo), --font-display (Fraunces, títulos de
destaque com eixo WONK) e --font-mono (JetBrains Mono).
Elevação e movimento: --elevation-1/2/3 (a 3 inclui glow temático e é usada
em modais), --shadow-sm/md/lg, --duration-fast/base/slow/dramatic,
--ease-standard/dramatic.
Forma e assinatura: --radius-sm/md/lg/pill/full; o recorte assimétrico
(assinatura do Coringa) é aplicado ao CTA principal do hero, à carta da
landing, ao badge e a selos de canto em modais e na barra do quiz. A
personalidade v2.1 adiciona glow ambiente no fundo, grão sutil (SVG noise) e
losangos arlequim como divisores.
(.btn-hero.btn-primary::before com clip-path), preservando o :focus-visible.
Acessibilidade: :focus-visible global com contorno verde de 2px; estados de
erro/sucesso/informação usam o par fundo -700/texto branco verificado;
prefers-reduced-motion desliga animações; prefers-contrast: more reforça as
cores.
Todos os textos visíveis ao usuário estão centralizados em data/texts.js e aplicados via data/i18n.js.
Atributos HTML disponíveis:
| Atributo | Efeito |
|---|---|
data-i18n="caminho.chave" |
Define o textContent do elemento |
data-i18n-placeholder="caminho.chave" |
Define o placeholder do input |
data-i18n-value="caminho.chave" |
Define o textContent (para options) |
data-i18n-badge="caminho.chave" |
Define o textContent (para badges) |
// Exemplo de uso programático
I18n.t('library.title') // 'Minha Biblioteca'
I18n.format('import.batchReport.saved', { count: 5 }) // '5 importado(s) com sucesso.'O QuizLab utiliza Datadog RUM (Real User Monitoring) para métricas de performance e erros em produção. A configuração é feita via variáveis de ambiente injetadas pelo Vite:
DATADOG_APPLICATION_ID=
DATADOG_CLIENT_TOKEN=
DATADOG_SERVICE=quizlab
DATADOG_ENV=production
DATADOG_VERSION=1.6.3
Copie .env.example para .env e preencha os valores para habilitar o monitoramento em desenvolvimento.
O projeto usa ESLint (flat config), um verificador de política de texto e um detector de dead code para manter a qualidade e a consistência do código:
npm run lint # ESLint + política de texto + dead code (falha se houver problemas)
npm run lint:js # apenas ESLint
npm run lint:fix # corrige automaticamente o que for possível (eslint --fix)
npm run lint:text # apenas a política anti-travessão
npm run lint:deadcode # apenas a verificação de dead code- ESLint (
eslint.config.mjs): baseeslint:recommended, globais do navegador e do app (padrão IIFE que expõe módulos nowindow), testes em CommonJS e scripts de ferramentas em Node.console.*é permitido por design (debug do projeto). - Política de texto (
scripts/check-no-em-dash.js): o travessão em-dash (U+2014) e o escape equivalente em JavaScript são proibidos em todo o projeto (código, comentários, testes, documentação, CSS, JSON e workflows). Use hífen simples" - ". - Dead code (
scripts/check-deadcode.js): detecta métodos de módulos globais (window.X = X) nunca usados, classes CSS definidas emstyles.cssmas nunca referenciadas (HTML/JS/data) e chaves i18n (folhas do objetoTEXTSemdata/texts.js) nunca usadas viaTEXTS.caminho.chave, atributosdata-i18n*ouI18n.t()/format(). Métodos privados (prefixo_) são ignorados por convenção;DDActions(scaffold de telemetria) é exceção explícita no script.
O lint roda no CI (npm run lint) e deve passar localmente antes de qualquer Pull Request.
O projeto usa husky para rodar o lint automaticamente antes de cada commit. Após
npm install, o hook é instalado sozinho e bloqueia commits com lint sujo ou travessão
em-dash. Para pular o hook em um caso pontual: git commit --no-verify.
O projeto usa dois níveis de teste: unitários/integração com o runner nativo do
Node.js (node:test, sem dependências) e end-to-end com Playwright (Chromium)
rodando contra o app real servido pelo Vite.
# Servidor de desenvolvimento (Vite)
npm run dev
# Build de produção
npm run build
# Preview do build
npm run preview
# Testes
npm test # todos os testes
npm run test:unit # apenas unitários
npm run test:integration # apenas integraçãoCobertura:
quiz-engine.test.jsinit, select (única/múltipla), confirm, navigate, flag, reset, shuffling.validator.test.jsschema válido, campos obrigatórios, tipos inválidos, alternativas.storage-manager.test.jsCRUD, replaceInLibrary, updateLibraryMeta, removeManyFromLibrary, histórico, getStorageStats (quota fixa 4 MB), canStore, saveWrongQuestions, getAggregatedWrong.file-handler.test.js_findDuplicate,_handleSingle,_handleBatch(salvos, skipped, conflito, cheio, redirect),handleMultiple(filtro de extensão).p1-p2-fixes.test.jsregressão: sessão órfã após deletar simulado, modal repetido, timer invisível no Modo Exame.security-fixes.test.jsregressão: XSS via explicações/alternativas, validação de arquivo (limite 2MB/50 arquivos/timeout 10s), sanitização de descrições.quiz-flow.test.jsfluxo completo: adicionar → iniciar → responder → salvar stats → acumulação de média.explanation-utils.test.jsreferências{{id}}na explicação resolvendo para a letra exibida na ordem atual (incluindo embaralhamento).
Os testes rodam em Node.js >= 21 sem DOM real. O ambiente é simulado via polyfills em tests/setup/environment.js e os módulos IIFE são carregados via tests/setup/loader.js.
Os testes E2E validam os fluxos reais no navegador: landing, biblioteca, quiz
(estudo e exame), criador, importação e retomada de sessão. O Playwright inicia o
servidor do Vite automaticamente na porta 4173 (configurado em
playwright.config.js).
# Instala o browser Chromium (uma vez)
npm run test:e2e:install
# Executa os testes E2E (inicia o Vite sozinho)
npm run test:e2e
# Executa com janela visível (debug)
npm run test:e2e:headedDetalhes de implementação:
- Os testes semeiam o
localStorage(quizlab_library) antes do app carregar viapage.addInitScript(helpers eme2e/helpers.js), garantindo determinismo sem depender do simulado demo. - Fixtures JSON determinísticos ficam em
e2e/fixtures/. - Os seletores usam
data-action(e não textos visíveis) para não depender do catálogoTEXTS. - O diretório
e2e/tempackage.jsonpróprio com"type": "module"para permitir ESM sem afetar o restante do projeto (CommonJS). - O teste de embaralhamento (
shuffle.spec.js) fixaMath.randomviaaddInitScript(mockDeterministicRandom) para tornar a permutação do Fisher-Yates determinística e livre de flakiness.
Em Linux, se o Chromium falhar com libnspr4.so: cannot open shared object file,
instale as dependências de sistema:
npx playwright install-deps chromiumci.yml executa em Pull Requests para main e develop e pushes para develop:
- Checkout do código
- Setup Node.js 22
npm cinpm run lintnpm run test:unitnpm run test:integration- Job separado
e2e:npx playwright install --with-deps chromium+npm run test:e2e(relatório em artefato se falhar)
deploy.yml executa em push para main (produção):
- Checkout do código
- Setup Node.js 22
npm cinpm run lintnpm test(suite completa)
Leia o CONTRIBUTING.md para entender os padrões de arquitetura (ESM + IIFE, Event Delegation), convenções de commit (Conventional Commits) e o checklist de qualidade antes de enviar um Pull Request.
Documentação gerada com base na versão v1.6.3.