PlayerWords

Atividade de adivinhação de vocabulário para o Moodle — documentação completa.

English | Português

View on GitHub Download .zip

🧪 Testes Automatizados

O PlayerWords inclui uma suíte PHPUnit cobrindo lógica de negócio, consultas ao repositório, web services e conformidade com a Privacy API, além de uma suíte Behat cobrindo o jogo, a integração com o PlayerHUD e os relatórios de ponta a ponta num navegador real. Todo push de CI executa a matriz completa (Moodle 4.5 → 5.x, PostgreSQL e MariaDB).

PHPUnit — Testes Centrais

Arquivo de teste Casos O que é coberto
backup_restore_test.php 6 Duplicar uma atividade copia suas palavras, renomeia a cópia, reconstrói o cache do curso, e não cria um item de nota duplicado — teste de regressão para a ausência de prepare_activity_structure(); uma referência a item do PlayerHUD sobrevive intacta a um “Duplicar atividade” no mesmo curso (o bloco nunca faz parte desse backup mais estreito); um backup/restauração de curso completo pra um curso novo remapeia a referência pro id novo do item, via o mapeamento playerhud_item que o próprio passo de restauração do block_playerhud registra; uma referência a item de outro curso é descartada em vez de manter apontando pro curso errado — contra o backup_controller/restore_controller real, não um atalho simulado; um backup/restauração de curso completo preserva os modos de pontuação de nota/ranking e tanto score quanto rankingpoints de uma tentativa terminada
cross_instance_security_test.php 4 Estado de sessão, busca de palavra por id, registros de tentativa e a consulta do “registro de tentativas” nunca vazam entre duas instâncias diferentes da atividade, mesmo para o mesmo estudante no mesmo curso
lib_grant_potential_test.php 6 O callback playerhud_grant_potential descoberto pelo teto de “Total XP no jogo” do próprio PlayerHUD: vazio pra uma instância de bloco não reconhecida, pra uma atividade sem item de recompensa configurado, e pra uma atividade ilimitada (reflete a mesma regra antifarm da concessão real); uma atividade limitada retorna uma linha no mesmo formato das entradas de item/missão do próprio PlayerHUD (qtd × rodadas máximas × xp do item); um item de recompensa pertencente à instância de bloco de outro curso não contribui nada; duas atividades limitadas no mesmo curso contribuem cada uma com sua própria linha
lib_reset_userdata_test.php 4 “Redefinir curso” apaga tentativas e reseta notas só quando a opção está marcada, só para o curso alvo, e o padrão do formulário vem marcado
lib_supports_test.php 2 Flags de feature conhecidas retornam o valor documentado e uma desconhecida retorna null; FEATURE_MOD_OTHERPURPOSE (só existe a partir do Moodle 5.1) é checada quando a constante realmente existe na branch que roda o teste
completion/custom_completion_test.php 7 Regra de conclusão customizada (“exigir tentativas”): incompleta abaixo do limite, completa no limite, regra não reportada como disponível quando desabilitada, nomes de regra definidos, descrição inclui a quantidade exigida, ordem de exibição, uma reserva ainda pendente (rodada iniciada mas não terminada) não conta para o limite
privacy/provider_test.php 30 Declaração de metadados (incluindo a preferência de usuário “já viu a introdução”, que vale para o site inteiro) e, para playerwords_attempts, uma correspondência exata do conjunto de campos contra o schema real da tabela; contextos por tentativas; contextos por palavras adicionadas, ambos ignorando uma colisão de instance-id em course_modules com outro tipo de módulo; listar usuários no contexto (no-op para contexto que não é de módulo, e para a mesma colisão); exportar dados do usuário (no-op para lista de contextos vazia, para contexto que não é de módulo, e para um contexto cujo instance-id não resolve); export_user_preferences não relata nada para um usuário que nunca disparou a preferência de introdução vista, e exporta corretamente (componente, nome, descrição) uma vez que disparou; excluir dados do usuário em um único e em múltiplos contextos, no-op para lista de contextos vazia, contexto que não é de módulo, e instance-id não resolvível; excluir dados de todos os usuários num contexto (sem afetar outra atividade; no-op para contexto que não é de módulo e para instance-id não resolvível); excluir dados de uma lista de usuários (no-op para contexto que não é de módulo, instance-id não resolvível, e lista de usuários vazia); export_user_data em múltiplos contextos retorna dados corretamente segregados por contexto, sem misturar atividades; a contagem de leituras no banco fica limitada em vez de escalar linearmente com o número de contextos aprovados, protegendo contra regressão a busca em lote de tentativas/palavras
lib_update_grades_test.php 2 A nota obsoleta de um estudante é realmente limpa (não só deixada intocada) assim que sua última tentativa é removida, então grade_item::has_grades() volta corretamente a false — a correção que permite a exclusão de tentativas no relatório destravar de fato as configurações do mod_form presas a ter nota; um estudante com outras tentativas restantes depois de uma exclusão tem a nota recalculada a partir do que sobrou
mod_form_test.php 3 A trava do Modo de pontuação do ranking reage a dados reais de tentativa, não só ao item de nota: trava assim que existe qualquer tentativa finalizada mesmo numa atividade totalmente sem avaliação, permanece destravada numa atividade recém-criada sem tentativa nenhuma, e não é afetada por uma reserva ainda em aberto (pendente)
lib_grade_item_update_test.php 2 Uma nota de aprovação configurada é realmente aplicada ao item de nota do gradebook, em vez de ser silenciosamente descartada pelo grade_update() do core; passar 'reset' limpa todas as notas registradas contra o item sem excluir o item em si
Subtotal 66  

Testes de Lógica de Negócio (tests/local/)

Arquivo de teste Casos O que é coberto
ai_word_generator_test.php 17 Parsing da resposta de IA (wrapper words/legado concepts, lista nua, cerca de código markdown removida, JSON malformado/não-array, dica cai para definition, entradas não-array ignoradas) e validação de termo como entrada não confiável (palavra única alfabética aceita; termo vazio, multi-palavra e não-alfabético rejeitados; um termo mais longo que a coluna da palavra é rejeitado, e um exatamente no limite ainda é aceito) — tudo via reflection, sem chamada real de IA; o prompt de geração lista as palavras já existentes no banco a evitar quando há alguma, e omite essa seção por completo quando o banco está vazio
attempts_history_service_test.php 23 Registro de tentativas e nota atual do próprio estudante: vazio sem rodadas terminadas; exclui uma reserva ainda pendente; linhas exibidas da mais recente para a mais antiga, enquanto o cálculo da nota usa ordem crescente; a nota computada bate com playerwords_calculate_user_grade() para o método configurado; resumo de nota oculto em atividade não avaliada; texto da palavra cai do conceito para a palavra crua; tempo usado formatado como m:ss; a coluna de pontos no ranking aparece, formatada com 2 casas decimais, quando o ranking está ligado, e é omitida por completo quando está desligado. Relatório de todos os estudantes (get_all_history): tentativas terminadas de todos os estudantes, com o nome de cada um anexado, da mais recente para a mais antiga por padrão; exclui quem pode gerenciar a atividade, tanto das linhas do relatório quanto do dropdown de filtro; o filtro studentid restringe às linhas de um único estudante; a ordenação só aceita colunas da lista permitida, caindo para data em vez de gerar erro numa chave desconhecida; a paginação retorna fatias distintas do conjunto total de resultados; o relatório e o dropdown de filtro de estudante respeitam $CFG->fullnamedisplay e moodle/site:viewfullnames, escondendo o sobrenome de um visualizador sem a capability; delete_attempts() remove a linha e dispara attempt_deleted com o objectid/relateduserid/other corretos, é um no-op para um attemptid de outra instância, é bloqueado pelo SEPARATEGROUPS de excluir um estudante fora do grupo de quem visualiza, e uma chamada em lote sobre dois estudantes retorna os userids afetados deduplicados
gameplay_service_test.php 20 Algoritmo de feedback por letra em 9 combinações de chute/alvo (correto, ausente, presente, letras duplicadas, esgotamento do pool); cálculo de nota para vitória, derrota e notas decimais no modo Binário; modo Linear tanto pro cálculo da nota quanto do ranking (nota cheia nas duas primeiras tentativas, escala proporcionalmente a partir da terceira, fração positiva não-zero na última tentativa permitida, degenera pra nota cheia em toda tentativa quando max_attempts é 2 ou menos, zero quando não completou)
hud_service_test.php 28 Delega pra API \block_playerhud\local\external_items do block_playerhud em toda operação de item, validando pertencimento contra a instância de bloco do próprio chamador em vez de ler as tabelas do PlayerHUD direto: localização do bloco PlayerHUD entre cursos; se o próprio plugin block_playerhud está instalado no site (bate com a checagem real de class_exists); disponibilidade por curso (verdadeiro com instância do bloco, falso sem instância, ignora instância de outro curso); resolução de nome de item (vazio pra item de outra instância de bloco); listagem de itens; consumo de itens (fundos insuficientes, sucesso, ordem FIFO, curto-circuito com quantidade zero, dispensado — não bloqueado — pra item de outra instância de bloco); concessão de itens (linhas de inventário com source='playerwords' mais XP concedido, XP retido quando a chamada sinaliza origem sem limite, item sem XP nunca altera XP em nenhum dos casos, item inexistente, item de instância estranha e quantidade zero são todos no-op); os quatro métodos de custo/concessão de item retornam seus valores neutros documentados em vez de fatalizar quando o block_playerhud não está instalado
intro_service_test.php 5 A preferência de usuário “já viu a introdução”, válida para o site inteiro: falsa por padrão; vira verdadeira e permanece assim depois de marcada (idempotente); isolada por usuário — marcar um usuário nunca afeta outro; o nome da preferência leva o prefixo Frankenstyle do plugin, o contrato do qual tanto o Privacy Provider quanto a limpeza por prefixo do db/uninstall.php dependem
ranking_service_test.php 9 Ranking vazio; ordenação decrescente por pontuação; truncamento top-5 com linha de “outsider” para o usuário atual fora do top; SEPARATEGROUPS filtra para o grupo do próprio estudante; uma reserva ainda pendente (rodada em andamento ou abandonada sem terminar) é excluída do ranking; um usuário que pode gerenciar a atividade (professor editor) nunca aparece no ranking, mesmo com tentativas próprias; o nome exibido respeita $CFG->fullnamedisplay, escondendo o sobrenome de um visualizador sem moodle/site:viewfullnames e mostrando-o por completo pra quem tem a capability
round_presenter_test.php 46 Renderização das linhas da grade; texto de cooldown; mensagens de feedback (desistiu/tempo esgotado/perdeu/venceu, variando pelas tentativas usadas); contexto de ranking; contexto de resultado da rodada (em branco até terminar, revela ao terminar, cooldown reflete mudança posterior de configuração); saldo/custo em item do PlayerHUD no lobby (exibido/oculto pelo estado da rodada, início desabilitado abaixo da quantidade exigida, habilitado quando o saldo cobre), informação de temporizador no lobby; saldo/custo em item do PlayerHUD no botão de dica do painel (exibido/oculto pelo estado de revelação, dica desabilitada abaixo da quantidade exigida, habilitada quando o saldo cobre), o botão de dica fica oculto por completo quando as dicas estão desativadas na atividade, mesmo pra uma palavra que tem dica configurada, temporizador permanece zerado antes da rodada iniciar; resumo de nota até agora (ausente antes de terminar, ausente quando não avaliada, mostra método e nota computada ao terminar, ignora uma tentativa ainda pendente); linha de informação do método de avaliação no lobby (exibida quando relevante, oculta em atividade de rodada única, oculta quando não avaliada); a tecla Ç do teclado só aparece quando o próprio banco de palavras da atividade precisa dela; contador de rodadas jogadas exibido no lobby e no resultado da rodada, usando o símbolo de infinito numa atividade ilimitada e o limite configurado nos demais casos; o rótulo de item concedido pelo PlayerHUD só aparece numa vitória de verdade com item configurado, em branco numa derrota ou quando não configurado; ranking_scoring_explanation() embute a string de detalhe Binário/Linear correspondente ao modo configurado na atividade
round_service_test.php 61 Transições de estado da rodada: palavra sorteada e round_started disparado, e uma rodada finalizada cuja linha de tentativa um professor já excluiu pelo relatório reseta para um estado novo e não finalizado em vez de deixar um resultado obsoleto na tela pra sempre, enquanto o estado de uma rodada ainda em andamento permanece intocado; envio de chute (errado, correto, sem tentativas, após terminar, tamanho incompatível, palavra alvo vazia, caracteres não-alfabéticos, termina quando o tempo esgota no meio do chute mesmo dentro da janela de tolerância do próprio round_expired()); desistência e timeout, cada um rejeitado tanto pra palavra armada mas não iniciada quanto pra nenhuma palavra ativa; nova rodada (reseta o estado, e é um no-op seguro sem sessão prévia); aviso de restrição (limite de rodadas atingido, cooldown ativo, sem restrição); count_rounds_played restrito à instância e ao usuário; cálculo de cooldown (desabilitado, sem tentativas ainda, expirado, reflete mudança posterior de configuração); ensure_round_state retorna imediatamente quando já terminada, recupera sorteando palavra nova após a anterior ser removida no meio da rodada, e retroalimenta a flag roundstarted numa sessão anterior a ela; start_round reserva uma linha de tentativa; finish_round completa a reserva em vez de duplicá-la, insere um registro novo pra uma sessão legada sem reserva a completar, e atualiza o estado de conclusão automática numa vitória de verdade; uma rodada abandonada continua contando para max_rounds; a reserva obsoleta é descartada quando a palavra é removida no meio da rodada; uma rodada vencida concede o item do PlayerHUD configurado com XP quando max_rounds é limitado, concede sem XP quando ilimitado, e uma rodada perdida nunca concede; reveal_hint é rejeitado ao já ter terminado e ao ainda não ter iniciado, e um custo de dica apontando pra um item só desabilitado continua bloqueando quando o saldo está curto; um custo de rodada ou dica apontando pra um item excluído, ou de outro curso, é dispensado em vez de travar o estudante pra sempre; um custo de rodada apontando pra um item só desabilitado (não excluído) continua bloqueando corretamente quando o saldo está curto — desabilitar é reversível, então o custo nunca é dispensado por causa disso
view_page_service_test.php 22 Ramificações de montagem de página: lobby fresco, palavra sorteada persiste entre chamadas, rodada terminada calcula cooldown real, aviso de restrição exibido quando o limite de rodadas é atingido; as URLs de ajuda/registro de tentativas do toolbar sempre estão presentes; a ação de desistir só aparece durante uma rodada ativa; a explicação de critério de desempate do ranking some da ajuda quando o professor desliga o ranking; a explicação do PlayerHUD na ajuda aparece assim que qualquer um entre custo de rodada, custo de dica ou recompensa por vitória está configurado; a ajuda sempre traz a dica apontando de volta pro ícone do toolbar. Introdução automática: verdadeira na primeiríssima carga de página de um usuário em qualquer atividade, e falsa dali em diante — inclusive numa atividade diferente, provando que o escopo é do site inteiro, não por instância — reproduzida igual nas ramificações de lobby fresco, rodada terminada e aviso de restrição. Status do banco de palavras: quem pode gerenciar a atividade vê a contagem de palavras ativas mesmo sem nenhum problema, e, só quando o banco realmente tem alguma, um aviso nomeado de palavra inativa (fora do intervalo de comprimento ou com caractere inválido); um estudante não vê nenhum dos dois; a ramificação de aviso de restrição reporta a rodada como finalizada para que o badge do cronômetro no cabeçalho fique oculto em vez de mostrar um “0” perdido
word_normalizer_test.php 16 Normalização sem acentuação em 8 combinações de diacríticos; is_valid_charset aceita só letras (inclusive acentuadas) e rejeita dígito, espaço, hífen, apóstrofo e string vazia, em 8 casos
words_repository_test.php 65 Seleção de palavra (pool vazio, exclusão de não aprovados/muito curtos/muito longos/caracteres não-letra, modo aleatório, determinismo e ciclagem da sequência compartilhada, evita a palavra excluída quando há alternativa, permite-a de volta quando é a única candidata); get_last_played_word_id (0 sem rodadas terminadas, ignora uma reserva pendente); inserção de palavra manual e por IA, esta última pulando uma palavra já presente no banco; checagem de duplicata word_exists (correspondência case-insensitive, sem correspondência, restrita à instância, corresponde independente da fonte, ignora o próprio id ao renomear); busca, atualização e exclusão de palavra restritas à instância dona; exclusão e aprovação em lote; listagem de palavras recentes, paginada, com join do nome do glossário; sincronização de glossário (divisão de conceito multi-palavra, filtro de stopwords configuráveis, atualização de dica em re-sincronização sem duplicar, limpeza de órfãos quando uma entrada desaparece, modo glossaryid = 0 cobrindo todos os glossários do curso, pula um conceito cujo texto já pertence a uma palavra manual/IA sem alterar a dica dela, pula um token mais longo que a coluna da palavra em vez de abortar toda a sincronização, e — com glossary_split_concepts desligado — um conceito de várias palavras pulado por completo em vez de dividido, com um conceito irmão de uma palavra só não sendo afetado, e que esse é o padrão da instância quando o ajuste nunca é definido explicitamente); a tecla Ç do teclado virtual só é oferecida quando uma palavra aprovada realmente contém uma, restrito à própria atividade e ignorando palavras não aprovadas. get_fragmented_concepts relata um conceito de glossário dividido em várias palavras irmãs, exclui um conceito de palavra única, ignora palavras manuais/IA mesmo quando uma coincidentemente compartilha o texto com o próprio conceito, e é restrito à própria instância. get_inactive_words fica vazio quando não há nada errado, relata uma palavra fora do intervalo de comprimento atual e uma palavra com caractere inválido (cada uma marcada com o motivo), e ignora palavras não aprovadas. get_draw_counts fica ausente — não zero — para uma palavra nunca sorteada, soma toda tentativa independente do resultado, e é restrito à própria instância. count_glossary_candidates (a mesma prévia usada antes mesmo de a atividade existir) conta só candidatas dentro do intervalo pedido pra um glossário; glossaryid = 0 cobre todos os glossários do curso; dois conceitos que tokenizam pra mesma palavra só contam uma vez; um id de glossário de outro curso nunca é contado, mesmo sendo um id real; um curso sem nenhum glossário conta zero sem erro; zero para um conceito de várias palavras quando splitconcepts é passado como falso; get_recent_words cai pro padrão seguro de ordenação (id/DESC) quando recebe coluna ou direção fora da lista permitida, em vez de confiar no chamador
Subtotal 312  

Testes de Web Services (tests/external/)

Arquivo de teste Casos O que é coberto
count_eligible_words_test.php 6 Conta só palavras aprovadas do banco cujo comprimento cai dentro do intervalo pedido; exclui palavras não aprovadas e fora do intervalo; restrito à própria instância da atividade; exige a capability mod/playerwords:addinstance (rejeita um estudante)
count_glossary_candidates_test.php 5 Conta palavras candidatas de um glossário específico dentro do intervalo de comprimento pedido; uma palavra fora do intervalo é excluída; uma stopword passada diretamente do formulário de configurações (ainda sem instância salva) remove o token correspondente antes da contagem; o parâmetro splitconcepts, vindo direto do estado da própria caixa de seleção no formulário de configurações, zera a contribuição de um conceito de várias palavras quando falso; exige a capability mod/playerwords:addinstance (rejeita um estudante)
end_round_test.php 6 Desistência termina a rodada; timeout termina a rodada; um valor inválido de reason é rejeitado; a capability mod/playerwords:view é exigida
new_round_test.php 5 Nova rodada sorteia palavra nova; bloqueado quando o limite de rodadas já foi atingido; a capability mod/playerwords:view é exigida; bloqueado enquanto uma rodada está genuinamente em andamento, fechando a brecha onde um cliente podia descartar uma rodada perdedora antes de ser registrada; bloqueado enquanto uma palavra está apenas armada no lobby (ainda não iniciada), fechando uma brecha de re-sorteio grátis por comprimento/dificuldade da palavra
reveal_hint_test.php 8 Dica é revelada; revelar duas vezes é idempotente; rejeitado após a rodada terminar; rejeitado para a atividade inteira quando as dicas estão desativadas, mesmo com a palavra sorteada tendo uma dica configurada; a capability mod/playerwords:view é exigida; saldo insuficiente de item do PlayerHUD (item real, válido) bloqueia a revelação; um custo apontando pra um item excluído é dispensado em vez disso
start_round_test.php 7 Cronômetro da rodada inicia; rejeitado quando já iniciado; a capability mod/playerwords:view é exigida; saldo insuficiente de item do PlayerHUD (item real, válido) bloqueia o início; um custo apontando pra um item excluído é dispensado em vez disso
submit_guess_test.php 10 Um chute errado nunca revela a palavra; um chute correto revela só quando termina; um chute perdedor também revela; a capability mod/playerwords:view é exigida; timeleft reflete os segundos restantes durante a rodada; timeleft fica congelado no momento em que a rodada terminou, não no relógio real; um total de ranking fracionado sobrevive à limpeza de retorno da API externa, contra a chamada real da webservice
Subtotal 47  
Total Geral 425  
vendor/bin/phpunit --testsuite mod_playerwords

Cobertura de linhas por classe (PHPUnit + Xdebug):

Classe Cobertura de linhas
completion\custom_completion 100%
event\attempt_deleted 46%
external\count_eligible_words 100%
external\count_glossary_candidates 100%
external\end_round 100%
external\new_round 100%
external\reveal_hint 98%
external\start_round 100%
external\submit_guess 100%
local\ai_word_generator 36%
local\attempts_history_service 98%
local\gameplay_service 95%
local\hud_service 91%
local\intro_service 100%
local\ranking_service 98%
local\round_presenter 98%
local\round_service 99%
local\view_page_service 97%
local\word_normalizer 100%
local\words_repository 99%
privacy\provider 100%
Geral 90%

A maioria das outras classes event/*.php não aparece na lista — o Moodle só as carrega sob demanda quando o evento correspondente realmente dispara, então a instrumentação nunca chega a vê-las. attempt_deleted é a exceção, já que attempts_history_service_test.php genuinamente a constrói e dispara: seus 46% cobrem init() e validate_data() (exercitados por toda chamada de create()/trigger()), enquanto get_name(), get_description() e get_objectid_mapping() ficam sem cobertura — eles só rodam ao renderizar de fato uma entrada de log ou mapear uma restauração, nenhum dos quais uma simples asserção de “o evento carrega o objectid/relateduserid certos” exercita.

hud_service carrega um gap deliberado: get_available_quantity(), get_item_name(), consume_items() e grant_items() carregam cada uma uma checagem if (!self::is_installed()) que retorna um valor neutro — o caminho de fallback para um site onde o block_playerhud nunca foi instalado ou já foi removido. Todo ambiente de dev e CI deste plugin instala o block_playerhud junto, então esses quatro ramos só são exercitados por test_item_methods_return_neutral_values_when_not_installed() num site que genuinamente não tem o bloco; aqui o teste fica skipped. Os próprios métodos de ai_word_generator que chamam IA de verdade (call_ai(), call_core_ai(), generate_and_save()…) são o outro gap deliberado: cobri-los exigiria mockar as chamadas HTTP reais do local_aihub/core_ai, julgado não valer a pena por ora.

Behat — Testes de Ponta a Ponta

O PlayerWords também inclui uma suíte Behat que conduz o jogo numa sessão de navegador real, cobrindo o jogo em si, a integração com o PlayerHUD, os relatórios voltados ao professor e a barra de ferramentas/modais — áreas que um teste unitário PHPUnit não consegue exercitar (interface dirigida por JavaScript, navegação real de página).

Arquivo de feature Cenários O que é coberto
mod_playerwords_smoke.feature 1 O lobby carrega e uma rodada pode ser iniciada — a verificação básica sobre a qual o resto da suíte se apoia
mod_playerwords_gameplay.feature 7 Vencer uma rodada na primeira tentativa esconde o selo do cronômetro; perder uma rodada ao esgotar todas as tentativas; as setas do teclado movem o foco entre as caixas de uma linha de chute sem alterar nenhum valor; desistir de uma rodada ativa pede confirmação; uma rodada termina automaticamente quando seu cronômetro se esgota; atingir o limite de rodadas oculta a ação de nova rodada em vez de um beco sem saída; um intervalo configurado mostra uma contagem regressiva em vez do botão de nova rodada
mod_playerwords_playerhud.feature 4 O lobby bloqueia o início de uma rodada até o estudante poder pagar o custo do item configurado; revelar a dica pede confirmação e saldo suficiente; uma rodada inicia e a dica é revelada de graça quando o item configurado não existe mais; vencer uma rodada concede o item configurado do PlayerHUD
mod_playerwords_reports.feature 5 Um estudante vê só o próprio histórico de tentativas, nunca o de outro estudante; o relatório de todos os estudantes do professor pagina além de 30 linhas, ordena ao clicar num cabeçalho de coluna e filtra para um único estudante; a página de ranking mostra o top 5 mais a linha do usuário atual quando ele fica fora dele
mod_playerwords_settings.feature 4 O modo de pontuação e o número de tentativas travam assim que existe uma nota real; adicionar uma palavra manual já existente no banco, ou com um caractere que o jogo não consegue usar, é rejeitado; um item do PlayerHUD que não existe mais permanece selecionado no formulário de configurações em vez de resetar silenciosamente
mod_playerwords_toolbar.feature 9 O ícone de gerenciar palavras e o aviso de palavras inativas só aparecem para quem gerencia a atividade; o ícone de ranking só aparece quando o ranking está ativado; o ícone de desistir só aparece durante uma rodada ativa; o botão de dica só aparece quando as dicas estão ativadas na atividade; o modal de ajuda mostra seus parágrafos opcionais só quando relevantes, e os oculta caso contrário; o modal “Como jogar” abre automaticamente na primeiríssima visita de um jogador, uma única vez; cancelar a confirmação de desistência deixa a rodada intocada
Subtotal 30  
vendor/bin/behat --config public/behat.yml --profile=chrome --tags @mod_playerwords