PlayerCross

Atividade de palavras cruzadas por dedução para o Moodle — documentação completa.

English | Português

View on GitHub Download .zip

🧪 Testes Automatizados

O PlayerCross 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 8 Duplicar uma atividade copia o pool de palavras e reconstrói o cache do curso sem criar um item de nota duplicado; toda coluna do install.xml de playercross_attempts é comparada estaticamente contra os atributos declarados pelo próprio passo de backup, para que uma coluna adicionada depois nunca reverta silenciosamente ao seu padrão na restauração — a proteção adicionada depois que o próprio win_condition já esteve faltando nessa lista; o timemodified da palavra sobrevive ao backup/restore; uma referência a item do PlayerHUD sobrevive intacta a um “Duplicar atividade” no mesmo curso; um backup/restore completo do curso para um curso novo remapeia a referência para o id do novo item; uma referência a item de outro curso é descartada em vez de continuar apontando para o curso errado; score e rankingpoints sobrevivem ao backup/restore numa linha de tentativa, mesmo quando divergem entre si; max_attempts_final_guess, gradescoringmode e rankingscoringmode sobrevivem ao backup/restore da própria atividade
cross_instance_security_test.php 4 O estado de sessão da rodada, buscas de palavra, registros de tentativa e a consulta de histórico de tentativas nunca vazam entre duas instâncias de atividade diferentes, mesmo para o mesmo estudante no mesmo curso
lib_grant_potential_test.php 6 O callback playerhud_grant_potential, usado pela própria estimativa de teto de “XP total no jogo” do PlayerHUD: vazio para uma instância de bloco desconhecida, para uma atividade sem item de recompensa configurado, e para uma atividade ilimitada (espelha a regra antifarming da concessão real); uma atividade limitada retorna uma linha no formato das próprias entradas de detalhamento do PlayerHUD; um item de recompensa pertencente à instância de bloco de outro curso não contribui em nada; duas atividades limitadas no mesmo curso contribuem cada uma com sua própria linha
lib_reset_userdata_test.php 4 A redefinição de curso apaga tentativas e reseta notas somente quando a caixa está marcada, somente para o curso alvo, e o padrão do formulário vem com a caixa marcada
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 6 Regra de conclusão personalizada (“exigir rodadas concluídas”): incompleta abaixo do limite, completa no limite, regra não reportada como disponível quando desativada, nomes de regras definidas, descrição da regra inclui o número exigido, ordem de exibição
privacy/provider_test.php 21 Declaração de metadados; exportação da preferência de usuário “viu a introdução”, em todo o site, tanto ausente quanto definida; contextos por tentativas; contextos por palavras adicionadas; listar usuários no contexto (e no-op para um contexto que não é de módulo); exportar dados do usuário (e no-op para uma contextlist vazia); excluir dados de um único usuário em múltiplos contextos; excluir dados de múltiplos usuários; excluir dados de todos os usuários em um contexto (e no-op para um contexto que não é de módulo); 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 4 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, não é afetada por uma reserva ainda em aberto (pendente), e permanece destravada numa atividade recém-criada sem tentativa nenhuma; a trava do Modo de pontuação da nota (que exige uma nota real) permanece corretamente independente mesmo depois que a trava do ranking já foi acionada
Subtotal 57  

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

Arquivo de teste Casos O que é coberto
ai_word_generator_test.php 19 Parsing da resposta de IA (wrappers words/concepts legado, lista simples, bloco de código markdown removido, JSON malformado/não-array, pista recorre a definition, entradas não-array ignoradas) e validação de termo de entrada não confiável (palavra alfabética única aceita; termos vazios, com múltiplas palavras ou não alfabéticos 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
attempts_history_service_test.php 21 Histórico de tentativas próprio restrito ao usuário informado, mais recente primeiro, e vazio sem nenhuma tentativa; a nota reportada bate com playercross_calculate_user_grade() para o método de avaliação configurado; uma linha recorre à palavra crua quando nenhum conceito foi registrado; o tempo usado é formatado como m:ss; resumo de nota oculto para atividade sem avaliação; relatório de todos os estudantes pagina e recorre a uma coluna de ordenação segura para uma chave desconhecida, filtra por um único estudante, ordena por pontuação crescente e lista todos os estudantes mais recente primeiro; um usuário que gerencia a atividade é excluído tanto do relatório quanto do menu de filtro por estudante; 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; a coluna rankingpoints fica em branco quando o show_ranking da atividade está desligado e mostra um valor independente de score quando está ligado; 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 16 calculate_max_errors() soma o orçamento de erros de cada termo mais o palpite final, e um campo com uma única tentativa contribui zero; a pontuação Binária sempre dá crédito total numa vitória e zero numa derrota; a pontuação Linear com zero erros é sempre exatamente o crédito total, sem nenhuma tolerância; o primeiro erro já reduz a nota, batendo com a tabela de exemplo prático da documentação (1 erro → 92,31); a Linear tem um piso de nota / (max_errors + 1) no orçamento máximo de erros, nunca chegando a zero numa vitória genuinamente concluída; a Linear numa rodada não concluída é sempre zero; um fallback defensivo para crédito total quando qualquer um dos campos de tentativas fica ilimitado (0); os modos de pontuação da nota e do ranking são configuráveis de forma independente na mesma rodada; o bônus fixo de 10% por acerto antecipado; o bônus tem teto na nota nominal para a pontuação mas não tem teto para o ranking; o bônus nunca se aplica numa derrota ou quando inelegível; o construtor da chave de sessão
hud_service_test.php 27 Delega para a própria API de itens do block_playerhud em toda operação, validando a posse contra a instância de bloco do próprio chamador: busca de bloco entre cursos; se o block_playerhud está instalado; disponibilidade no curso (com/sem instância de bloco, ignorando a de outro curso); resolução de nome de item; listagem de itens; consumo de itens (saldo insuficiente, sucesso, ordem FIFO, atalho para quantidade zero, dispensado para item de instância estrangeira); concessão de itens (inventário mais XP concedido, XP suprimido quando sinalizado como ilimitado, itens de XP zero não concedem nada, itens inválidos/de instância estrangeira/quantidade zero são no-ops); resolução de XP em lote para vários itens de uma vez, um item de outra instância de bloco omitido do mapa, uma lista vazia de ids retorna sem consultar, e ids repetidos são resolvidos uma única vez; 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 “viu a introdução”, em todo o site: falsa por padrão; vira verdadeira e permanece (idempotente); isolada por usuário; o nome da preferência é prefixado com o Frankenstyle do plugin
puzzle_builder_test.php 9 Cobertura total dos slots entre tema e termos; uma letra exclusiva dos termos ainda compartilha corretamente seu slot; degradação graciosa para uma letra da frase-mistério não cobrível, e essa degradação pode ser desativada; o texto da frase-mistério vem da própria pista da palavra-tema, nunca do seu conceito; a ortografia acentuada original sobrevive junto à palavra-termo e à pista do tema normalizadas; falha rígida quando o pool de palavras é insuficiente; determinismo do modo de palavra compartilhado; o desempate da seleção gulosa de termos é determinístico
ranking_service_test.php 9 Ranking vazio; ordenação decrescente por pontos de ranking; o ranking é conduzido por rankingpoints, não por score — os dois podem genuinamente divergir (modos de pontuação independentes, bônus por acerto antecipado sem teto), e uma linha com pontuação menor mas ranking maior ainda supera uma com pontuação maior mas ranking menor; truncamento top-5 com linha extra para o usuário atual em posição inferior; SEPARATEGROUPS filtra pelo grupo do estudante, e une a associação deduplicada de todos os grupos aos quais o estudante pertence; um usuário que gerencia a atividade 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 60 Renderização das peças da frase-mistério (respeita os slots revelados, peças ocultas carregam seu número de slot, todas reveladas ao finalizar, agrupadas por palavra); renderização das linhas de termo (palavra não resolvida oculta, revelada ao finalizar a rodada, revelada ao ser resolvida, rótulo de tentativas esgotadas mostrado só quando realmente esgotado, a frase-mistério sempre é mostrada, uma letra compartilhada cruzadamente revelada é refletida, o selo de tentativas restantes de um termo conta regressivamente conforme attemptsused cresce, mostra o glifo de infinito quando ilimitado, e se oculta ao resolver/esgotar/finalizar a rodada); texto do intervalo (inativo/ativo, reflete uma mudança posterior de configuração); mensagem de feedback varia conforme o resultado, incluindo o esgotamento do palpite final; informação de relevância do método de avaliação; resumo da nota até agora (ausente sem item de nota, mostrado ao finalizar); contexto do lobby (custo/saldo do PlayerHUD, pode iniciar com saldo suficiente, informação do cronômetro só quando ativado, contagem de termos desta rodada, nenhum custo mostrado para a conta convidado); contexto do painel de rodada (tempo restante zero antes de começar, oculta revelar enquanto ativo, disponibilidade da dica global, contagem de dicas restantes mostrada, o botão de dica se oculta por completo quando as dicas estão desativadas para a atividade, e separadamente ao atingir o limite configurado de revelações, o botão de dica mostra/omite seu custo do PlayerHUD, pode pagar a dica com saldo suficiente, nenhum custo de dica mostrado para a conta convidado, disponibilidade de cedilha reflete o pool de palavras, o selo de tentativas restantes do palpite final conta regressivamente e se oculta ao resolver a frase, finalguessexhausted carrega um rótulo legível); contexto do resultado da rodada (em branco até finalizar, revela ao finalizar, rótulo de concessão do PlayerHUD mostrado só numa vitória real, omitido numa derrota, e omitido para a conta convidado mesmo numa vitória); ranking_scoring_explanation() embute a string de detalhe Binário/Linear correspondente ao modo configurado na atividade
round_service_test.php 67 Estado padrão da rodada e descarte de estado estruturalmente obsoleto, incluindo estado sem os campos de ortografia de revelação de uma sessão mais antiga, e uma rodada finalizada cuja linha de tentativa um professor já excluiu pelo relatório — resetando 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; construção do puzzle sob demanda, e recusa de sortear um puzzle novo quando max_rounds/intervalo restringe o estudante; revelar uma dica para de funcionar ao atingir o limite configurado por rodada, e é rejeitada de imediato para a atividade inteira quando as dicas estão desativadas; dicas sozinhas podem finalizar e vencer a rodada; envio de palpite de termo (errado incrementa tentativas, correto resolve e revela slots compartilhados); resolver todos os termos sozinho não finaliza a rodada; um palpite final correto sozinho não finaliza a rodada, e resolve automaticamente qualquer termo restante feito inteiramente de letras já compartilhadas; um palpite final errado abaixo do limite de tentativas mantém a rodada aberta; termos-depois-palpite-final e palpite-final-depois-termos finalizam e vencem a rodada; o esgotamento de um termo encerra a rodada como derrota em “ambos obrigatórios”, mas não em “só a frase-mistério”; o esgotamento do palpite final sempre encerra a rodada como derrota, tanto em “ambos obrigatórios” quanto em “só a frase-mistério” — diferente do esgotamento de termo, isso é incondicional ao win_condition já que a frase é sempre obrigatória para vencer; em “só a frase-mistério”, resolver todos os termos sozinho ainda não finaliza a rodada, enquanto o palpite final sozinho vence imediatamente; errorsused conta só os palpites errados, nunca o que finalmente resolve, distinto de attemptsused; o bônus por acerto antecipado é elegível quando a frase é acertada diretamente antes de qualquer termo ser resolvido, e não é elegível assim que um termo é resolvido primeiro, mesmo que a frase seja confirmada depois; finish_round() grava rankingpoints junto com score, e os dois podem divergir; desistência encerra a rodada como derrota e nunca concede o item de vitória; tempo esgotado rejeitado antes do prazo; uma nova rodada reseta o estado; contagem de rodadas jogadas e intervalo; variantes do aviso de restrição (limite de rodadas atingido, intervalo ativo, sem restrição); cálculo do intervalo (desativado, expirado pelo tempo, reflete uma mudança posterior de configuração); os eventos round_started e round_completed disparam no momento certo; vencer concede o item configurado do PlayerHUD com XP quando limitado e sem XP quando ilimitado; iniciar uma rodada ou revelar uma dica dispensa seu custo do PlayerHUD quando o item configurado foi excluído ou pertence a outro curso, mas ainda bloqueia quando o item está apenas desativado e o saldo é insuficiente; enviar um palpite de termo, um palpite final ou revelar uma dica é rejeitado de imediato antes de a rodada realmente ter sido iniciada, fechando o desvio que pularia seu custo configurado do PlayerHUD; a conta convidado nunca é cobrada ao iniciar uma rodada ou revelar uma dica, e uma rodada vencida não deixa nenhuma linha de tentativa, atualização de nota ou concessão do PlayerHUD para trás; revelar uma dica é rejeitado quando a rodada já terminou
view_page_service_test.php 40 Ramificações de montagem da página: lobby recém-iniciado, um puzzle sorteado persiste entre chamadas, uma rodada finalizada calcula um intervalo real, aviso de restrição quando o limite de rodadas é atingido; ação de desistir mostrada só durante uma rodada ativa; URLs da barra de ferramentas sempre presentes, barra de ferramentas de gestor oculta para estudantes e mostrada para professores, e um professor não-editor (só com a capability de relatório) vê o link de relatório mas não o de gerenciar palavras; palavras inativas ocultas para estudantes, mostradas para um gestor, e a contagem de ativas mostrada sozinha quando nada está inativo; link de ranking oculto quando o ranking está desativado; ajuda do PlayerHUD mostrada quando uma recompensa de vitória está configurada; o texto de ajuda da condição de vitória parte de “ambos obrigatórios” e reflete a configuração “só a frase-mistério”; o aviso de perda por termo é mostrado quando as tentativas por termo são limitadas e oculto quando ilimitadas; o aviso de perda pela frase-mistério é mostrado sempre que max_attempts_final_guess está configurado, incondicional ao win_condition, e oculto quando ilimitado; a explicação da fórmula de pontuação é mostrada quando a Linear está ativa para a nota, ou só para o ranking (desde que o ranking esteja visível), e oculta quando os dois são Binário; auto-exibição da introdução sinalizada uma vez no lobby e não se repete numa atividade diferente, e também é sinalizada corretamente nas ramificações de rodada finalizada e aviso de restrição; o contexto de ajuda sempre carrega o ponteiro de dica de revisão; 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 38 Normalização insensível a acentos em 8 combinações de acento/maiúscula-minúscula; is_valid_charset aceita só letras (incluindo acentuadas) e rejeita dígitos, espaços, um hífen, um apóstrofo e uma string vazia, em 8 casos; chars() divide uma palavra normalizada em caracteres individuais em 4 casos sem rasgar sequências multibyte — o motivo pelo qual puzzle_builder::cipher_slots() usa esse método em vez de uma divisão simples por byte; normalize_phrase() divide uma frase em texto livre em tokens de palavra normalizados em 8 casos, tratando dígitos/pontuação/hífen/apóstrofo como separadores e recolhendo espaços extras — a mesma divisão que submit_final_guess() aplica ao palpite de frase-mistério do estudante
words_repository_test.php 67 Candidatas a frase-mistério e a termo respeitam seus próprios intervalos de comprimento independentes, e as candidatas a termo excluem uma palavra não aprovada ou com charset inválido; seleção de tema compartilhada e aleatória (o modo compartilhado cicla pelo pool conforme o número da rodada avança, não só determinístico pra uma rodada fixa), o último id de palavra-tema jogado, pick_theme_word retorna null com o pool vazio, e o id de tema excluído é permitido de volta quando é a única candidata restante; verificações de existência de palavra (insensível a maiúsculas/minúsculas, restrita à instância, ignorando um id excluído, independente da fonte); detecção de palavra com cedilha (presente, ausente, ignora não aprovadas, restrita à própria instância); inserção manual e por IA, busca, atualização e exclusão de palavras (todas restritas à instância dona); exclusão e aprovação em lote; listagem de palavras recentes, incluindo o nome do glossário; sincronização com o Glossário (desativada sem o bit de fonte, conceitos de uma e várias palavras, stopwords configuradas, atualização da pista na ressincronização, remoção de órfãs, escopo a um ou todos os glossários do curso, ignorando palavra pertencente a outra fonte, pulando 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); relato de conceitos fragmentados (conceitos de várias palavras divididos, conceitos de uma palavra excluídos, fontes não-Glossário ignoradas, restrito à própria instância); detecção de palavras inativas (descompasso de comprimento, charset inválido, uma palavra válida só para o papel de tema não é relatada, palavras não aprovadas ignoradas); contagem de sorteios do tema (ausente, somada independente do resultado, restrita à própria instância); contagem de candidatas do Glossário (dentro do intervalo, em todos os glossários do curso, tokens deduplicados, restrita ao próprio curso, zero quando não há glossários, e 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 378  

Testes de Web Services (tests/external/)

Arquivo de teste Casos O que é coberto
count_eligible_theme_words_test.php 5 Conta só pistas aprovadas cujo total de letras cai no intervalo pedido; exclui uma pista acima de um comprimento máximo real (não-zero); exclui palavras não aprovadas; restrito à própria instância de atividade; exige a capability mod/playercross:managewords (rejeita um estudante)
count_eligible_words_test.php 5 Conta só palavras aprovadas do pool cujo comprimento cai no intervalo pedido; exclui palavras não aprovadas e fora do intervalo; restrito à própria instância; exige a capability mod/playercross:managewords
count_glossary_candidates_test.php 5 Conta palavras candidatas para um glossário específico dentro do intervalo de comprimento pedido; exclui palavras fora do intervalo; uma stopword vinda diretamente do formulário de configurações remove o token correspondente antes de contar; o parâmetro splitconcepts, vindo diretamente do estado do próprio checkbox no formulário de configurações, zera a contribuição de um conceito de várias palavras quando falso; exige a capability mod/playercross:addinstance — o único ponto que a mantém, por rodar a partir do próprio formulário de “adicionar atividade”, antes de existir um course module
end_round_test.php 6 Desistência finaliza a rodada; tempo esgotado finaliza a rodada; um valor de reason inválido é rejeitado; a capability mod/playercross:view é exigida
new_round_test.php 5 Uma nova rodada sorteia um puzzle novo; bloqueada quando o limite de rodadas já foi atingido; a capability mod/playercross:view é exigida; bloqueada enquanto uma rodada está genuinamente em andamento, fechando a brecha onde um cliente podia descartar uma rodada perdedora antes de ser registrada; bloqueada enquanto uma palavra-tema está apenas armada no lobby (ainda não iniciada), fechando uma brecha de re-sorteio grátis de dificuldade do puzzle
reveal_hint_test.php 8 Revela mais uma peça; rejeitada quando todos os slots já estão revelados; rejeitada quando o limite configurado de dicas por rodada é atingido; rejeitada para a atividade inteira quando as dicas estão desativadas, mesmo com slots ocultos restantes e sem limite por rodada configurado; a capability mod/playercross:view é exigida; saldo insuficiente de item do PlayerHUD bloqueia a revelação; um custo apontando para um item excluído é dispensado; rejeitada quando a rodada nunca foi realmente iniciada, driblando o custo de dica do PlayerHUD
start_round_test.php 7 A rodada inicia; rejeitada quando já iniciada; a capability mod/playercross:view é exigida; saldo insuficiente de item do PlayerHUD bloqueia o início; um custo apontando para um item excluído é dispensado; bloqueada quando o limite de rodadas já foi atingido, mesmo partindo de uma sessão nova; a conta convidado joga uma demo gratuita sem ser cobrada
submit_term_guess_test.php 7 Um palpite errado de termo nunca vaza a palavra do termo; um termo resolvido comum é sinalizado para um toast, não para o banner de fim de rodada; resolver todos os termos só revela a palavra-tema quando a rodada realmente finaliza; um usuário sem matrícula/capability não pode enviar um palpite; rejeitado quando a rodada nunca foi realmente iniciada, driblando o custo de rodada do PlayerHUD
submit_final_guess_test.php 6 Um palpite final errado nunca vaza a palavra-tema; um palpite final correto sozinho não vence a rodada nem revela a palavra-tema (em “ambos obrigatórios”); resolver todos os termos e depois acertar a frase final vence a rodada e revela a palavra-tema; a conta convidado joga uma demo gratuita até vencer sem persistir tentativa nem concessão do PlayerHUD; rejeitado quando a rodada nunca foi realmente iniciada, driblando o custo de rodada do PlayerHUD; esgotar as tentativas da frase-mistério encerra a rodada e sinaliza finalguessexhausted na resposta do painel, através do dispatch real do web service
Subtotal 56  
Total Geral 491  
vendor/bin/phpunit --testsuite mod_playercross

Cobertura de linhas por classe (PHPUnit + Xdebug):

Classe Cobertura de linhas
completion\custom_completion 100%
event\attempt_deleted 46%
external\count_eligible_theme_words 100%
external\count_eligible_words 100%
external\count_glossary_candidates 100%
external\end_round 100%
external\new_round 100%
external\reveal_hint 100%
external\start_round 100%
external\submit_term_guess 100%
external\submit_final_guess 100%
local\ai_word_generator 37%
local\attempts_history_service 98%
local\gameplay_service 100%
local\hud_service 89%
local\intro_service 100%
local\puzzle_builder 98%
local\ranking_service 98%
local\round_presenter 99%
local\round_service 96%
local\view_page_service 100%
local\word_normalizer 100%
local\words_repository 99%
privacy\provider 95%
Geral 90%

A maioria das classes event/*.php não está listada — o Moodle só as carrega de forma preguiçosa quando o evento correspondente realmente dispara, então a instrumentação nunca as enxerga. 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.

submit_final_guess e submit_term_guess chegaram a 100% assim que os próprios ramos de roundresult_structure()/panel_structure() ganharam cobertura com testes adicionados depois — incluindo os novos campos de resumo de ranking e os de tentativas restantes/esgotamento do palpite final, respectivamente. Os métodos de ai_word_generator que chamam IA de verdade (call_ai(), call_core_ai(), generate_and_save()…) são o único gap deliberado que resta: cobri-los exigiria mockar as chamadas HTTP reais do local_aihub/core_ai, julgado não valer a pena por ora.

hud_service carrega um gap deliberado parecido: get_available_quantity(), get_item_name(), consume_items() e grant_items() cada uma carrega 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.

Behat — Testes de Ponta a Ponta

O PlayerCross 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_playercross_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_playercross_gameplay.feature 15 Vencer uma rodada acertando a frase-mistério diretamente esconde o selo do cronômetro; resolver um termo revela cruzadamente suas letras compartilhadas na frase-mistério; 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; as setas do teclado movem o foco entre as caixas de um termo sem alterar nenhum valor; as setas do teclado movem o foco entre linhas, chegando até a frase-mistério a partir do primeiro termo; o botão de enviar de um termo fica oculto enquanto sua linha está incompleta e aparece assim que cada letra é digitada; um termo pode ser enviado tocando no próprio botão da linha, não só pelo Enter; enviar um termo preserva a digitação em andamento de outro termo ainda aberto; uma rodada recém-iniciada mostra o selo de tentativas restantes configurado tanto para um termo quanto para a frase-mistério; esgotar as tentativas da frase-mistério encerra a rodada como derrota na condição de vitória padrão; esgotar as tentativas da frase-mistério encerra a rodada como derrota também em “só a frase-mistério”; um intervalo configurado mostra uma contagem regressiva em vez do botão de nova rodada
mod_playercross_playerhud.feature 4 O lobby bloqueia o início de uma rodada até o estudante poder pagar o custo do item configurado; revelar uma 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_playercross_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_playercross_settings.feature 5 Número de termos, método de avaliação e os novos campos de pontuação travam assim que existe uma nota real; escolher avaliação Linear com um campo de tentativas ilimitado é rejeitado pelo formulário de configurações; adicionar uma palavra manual já existente no pool, 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_playercross_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 39  
vendor/bin/behat --config public/behat.yml --profile=chrome --tags @mod_playercross