You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
remove uma pessoa da divisão (removePerson(entry.uid))
Linhas reconferidas em 2026-08-29 — deslocaram ~24 em relação à tabela da #106 (que dizia :122 e :159). Reconfirmar por conteúdo, não por linha (exigência 8): o site 8 é o <Button> do cabeçalho, irmão do <span>Dividir com</span>, com onClick={handleClose}; o site 9 é o <Button> dentro do .map(...) de resolved, com onClick={() => removePerson(entry.uid)}.
Renderizado por components/forms/TransactionForm.tsx (grep confirma o import — exigência 7), que é o formulário central do /registro. Não é código morto.
Evidência
// site 8 — components/forms/transaction/SplitSection.tsx:146-154<Buttontype="button"variant="ghost"size="icon"onClick={handleClose}className="text-text-tertiary hover:text-text-primary"><XclassName="h-4 w-4"/></Button>// site 9 — components/forms/transaction/SplitSection.tsx:183-191<Buttontype="button"variant="ghost"size="icon"onClick={()=>removePerson(entry.uid)}className="mb-0.5 flex-shrink-0 text-text-tertiary hover:text-negative"><XclassName="h-4 w-4"/></Button>
O <svg> não pode servir de nome: lucide-react está pinado em 1.8.0 (package.json:45) e o Icon dessa versão esconde o <svg> quando não recebe filho nem prop de a11y —
// lucide-react@1.8.0 — dist/esm/Icon.js:36 (lido do tarball do registry)
...!children&&!hasA11yProp(rest)&&{"aria-hidden": "true"},
— então a computação do nome do role=button fica sem fonte alguma. Medido na árvore real via CDP (Accessibility.getFullAXTree) na auditoria da #106: role=button name="".
WCAG 4.1.2 Name, Role, Value (nível A).
Falsificação (exigência 2)
O lint não pega.eslint.config.mjs:25-32 escopa jsx-a11y/label-has-associated-control a components/ui/field.tsx e nada mais. CI verde com os dois sites presentes.
Não é decisão deliberada.git log de SplitSection.tsx: 277c172 é repaginação visual; nada sobre rotulagem.
Este é o par mais confusível dos 9 sites, porque os dois botões são visualmente e semanticamente idênticos na árvore: mesmo ícone X, mesmo variant/size, e ambos anunciados apenas como "botão". Quem usa leitor de tela e abriu "Dividir com alguém" encontra, por linha, um "botão" que remove aquela pessoa e, acima, um "botão" que descarta a divisão inteira — sem nada que os distinga. O destino de errar não é simétrico: handleClose joga fora todas as linhas já preenchidas.
Acontece no /registro, o formulário central do produto. Atinge também comando de voz, que não tem alvo nomeado para mirar.
Proposta
Uma linha por site, aditiva, sem mudança visual. Rótulos distintos, que é o ponto inteiro desta fatia:
Rótulo estático e não interpolado com o nome da pessoa (Remover ${entry.personId}): o personId pode estar vazio (linha recém-adicionada, placeholder="Selecionar...") e o nome vem do Combobox, cujo options já são texto decriptado — montar a string ali acopla o rótulo ao estado de seleção sem ganho, e um nome vazio produziria "Remover " truncado. O Combobox irmão, esse sim, já anuncia de quem é a linha.
Rotular o botão, não o ícone.<X aria-label="..." /> desliga o aria-hidden via hasA11yProp, mas põe o nome no <svg> em vez do controle — alvo de clique e nó nomeado divergem, contra os 20 controles do repo que rotulam o <Button>.
Cobertura (exigência 3)
Nenhum teste pega hoje e o lint está verde. Acrescentar um bloco describe a __tests__/unit/a11y-nomes-acessiveis.test.ts, criado pela #126 (se ela ainda não tiver mergeado, criar o arquivo com o mesmo cabeçalho). Varredura de texto-fonte, padrão de row-actions.test.ts (#54) e a11y-estado-selecao.test.ts (#107) — não há @testing-library/react no projeto.
Aqui a asserção genérica falha duas vezes, não uma.SplitSection.tsx tem cinco <Button> (o "Dividir com alguém", os dois X, e mais dois no rodapé), então toMatch(/aria-label/) passa com qualquer um rotulado. E como os dois sites usam <X>, um recorte que pegue só a primeira ocorrência deixa o site 9 descoberto. O gate precisa iterar sobre todas as ocorrências e exigir rótulos distintos:
// todos os <Button> que envolvem um <X>; (?:(?!<Button)[\s\S])*? garante que// cada fatia comece no <Button> mais próximo do ícone, não num anterior.constgatilhos=[...source.matchAll(/<Button\b(?:(?!<Button)[\s\S])*?<X\b/g)].map((m)=>m[0])expect(gatilhos).toHaveLength(2)constrotulos=gatilhos.map((g)=>g.match(/^\s*aria-label="([^"]+)"$/m)?.[1])expect(rotulos.every(Boolean)).toBe(true)expect(newSet(rotulos).size).toBe(2)// rótulos distintos — o defeito é a ambiguidade
toHaveLength(2) é parte do gate, não cerimônia: se o .map ganhar outro X no futuro, o teste falha em vez de cobrir só dois dos três.
Nota sobre a "armadilha da regex" descrita na #106. O corpo da #106 alerta que <(button|Button)\b([^>]*?)> não encontra o site 9, porque o > da arrow function em onClick={() => removePerson(...)} fecha a tag cedo — e conclui que o parser precisa andar caractere a caractere respeitando {} e aspas. Esse alerta vale para o scanner repo-wide (issue separada), não para este gate. O recorte acima não tenta delimitar a tag de abertura: vai do <Button até o <X, atravessando a arrow function sem precisar interpretá-la, e busca o aria-label ancorado em linha própria dentro desse trecho. Não há parser a escrever aqui.
A âncora ^\s*...$ com flag m é a mesma de a11y-estado-selecao.test.ts: sem ela o atributo comentado deixa o teste verde com o bug de volta.
Custo estimado
P — 2 arquivos: components/forms/transaction/SplitSection.tsx + __tests__/unit/a11y-nomes-acessiveis.test.ts.
Fecha parcialmente #106 — usar refs #106, nãocloses (exigência 8).
Fatia (c) de #106. Sites 8 e 9 dos 9. As outras: #126 (DS), #127 e #128 (lápis).
Onde
2 sites, no mesmo arquivo. Ambos
<Button size="icon">cujo único filho é um<X>dolucide-react, semaria-label,aria-labelledby,titleousr-only:components/forms/transaction/SplitSection.tsx:146-154handleClose)components/forms/transaction/SplitSection.tsx:183-191removePerson(entry.uid))Linhas reconferidas em 2026-08-29 — deslocaram ~24 em relação à tabela da #106 (que dizia
:122e:159). Reconfirmar por conteúdo, não por linha (exigência 8): o site 8 é o<Button>do cabeçalho, irmão do<span>Dividir com</span>, comonClick={handleClose}; o site 9 é o<Button>dentro do.map(...)deresolved, comonClick={() => removePerson(entry.uid)}.Renderizado por
components/forms/TransactionForm.tsx(grepconfirma o import — exigência 7), que é o formulário central do/registro. Não é código morto.Evidência
O
<svg>não pode servir de nome:lucide-reactestá pinado em1.8.0(package.json:45) e oIcondessa versão esconde o<svg>quando não recebe filho nem prop de a11y —— então a computação do nome do
role=buttonfica sem fonte alguma. Medido na árvore real via CDP (Accessibility.getFullAXTree) na auditoria da #106:role=button name="".WCAG 4.1.2 Name, Role, Value (nível A).
Falsificação (exigência 2)
eslint.config.mjs:25-32escopajsx-a11y/label-has-associated-controlacomponents/ui/field.tsxe nada mais. CI verde com os dois sites presentes.git logdeSplitSection.tsx:277c172é repaginação visual; nada sobre rotulagem.Fieldvizinho não resolve. As linhas do.mapusam<Field label={idx === 0 ? 'Pessoa' : undefined}>para oComboboxe oCurrencyInput, mas oFieldnomeia campos de formulário e não envolve o<Button>do site 9 — é o escopo da [a11y] Field nunca liga o label ao controle — 94 campos sem associação; três campos de dinheiro no mesmo dialog se anunciam todos como "R$ 0,00" #53, que não alcança botões. Confirmado no arquivo: o<Button>é irmão dos dois<div>deField, não filho.Impacto
Este é o par mais confusível dos 9 sites, porque os dois botões são visualmente e semanticamente idênticos na árvore: mesmo ícone
X, mesmovariant/size, e ambos anunciados apenas como "botão". Quem usa leitor de tela e abriu "Dividir com alguém" encontra, por linha, um "botão" que remove aquela pessoa e, acima, um "botão" que descarta a divisão inteira — sem nada que os distinga. O destino de errar não é simétrico:handleClosejoga fora todas as linhas já preenchidas.Acontece no
/registro, o formulário central do produto. Atinge também comando de voz, que não tem alvo nomeado para mirar.Proposta
Uma linha por site, aditiva, sem mudança visual. Rótulos distintos, que é o ponto inteiro desta fatia:
// site 8 <Button type="button" variant="ghost" size="icon" onClick={handleClose} + aria-label="Fechar divisão" className="text-text-tertiary hover:text-text-primary"> // site 9 <Button type="button" variant="ghost" size="icon" onClick={() => removePerson(entry.uid)} + aria-label="Remover pessoa da divisão" className="mb-0.5 flex-shrink-0 text-text-tertiary hover:text-negative">Rótulo estático e não interpolado com o nome da pessoa (
Remover ${entry.personId}): opersonIdpode estar vazio (linha recém-adicionada,placeholder="Selecionar...") e o nome vem doCombobox, cujooptionsjá são texto decriptado — montar a string ali acopla o rótulo ao estado de seleção sem ganho, e um nome vazio produziria "Remover " truncado. OComboboxirmão, esse sim, já anuncia de quem é a linha.Rotular o botão, não o ícone.
<X aria-label="..." />desliga oaria-hiddenviahasA11yProp, mas põe o nome no<svg>em vez do controle — alvo de clique e nó nomeado divergem, contra os 20 controles do repo que rotulam o<Button>.Cobertura (exigência 3)
Nenhum teste pega hoje e o lint está verde. Acrescentar um bloco
describea__tests__/unit/a11y-nomes-acessiveis.test.ts, criado pela #126 (se ela ainda não tiver mergeado, criar o arquivo com o mesmo cabeçalho). Varredura de texto-fonte, padrão derow-actions.test.ts(#54) ea11y-estado-selecao.test.ts(#107) — não há@testing-library/reactno projeto.Aqui a asserção genérica falha duas vezes, não uma.
SplitSection.tsxtem cinco<Button>(o "Dividir com alguém", os doisX, e mais dois no rodapé), entãotoMatch(/aria-label/)passa com qualquer um rotulado. E como os dois sites usam<X>, um recorte que pegue só a primeira ocorrência deixa o site 9 descoberto. O gate precisa iterar sobre todas as ocorrências e exigir rótulos distintos:toHaveLength(2)é parte do gate, não cerimônia: se o.mapganhar outroXno futuro, o teste falha em vez de cobrir só dois dos três.Nota sobre a "armadilha da regex" descrita na #106. O corpo da #106 alerta que
<(button|Button)\b([^>]*?)>não encontra o site 9, porque o>da arrow function emonClick={() => removePerson(...)}fecha a tag cedo — e conclui que o parser precisa andar caractere a caractere respeitando{}e aspas. Esse alerta vale para o scanner repo-wide (issue separada), não para este gate. O recorte acima não tenta delimitar a tag de abertura: vai do<Buttonaté o<X, atravessando a arrow function sem precisar interpretá-la, e busca oaria-labelancorado em linha própria dentro desse trecho. Não há parser a escrever aqui.A âncora
^\s*...$com flagmé a mesma dea11y-estado-selecao.test.ts: sem ela o atributo comentado deixa o teste verde com o bug de volta.Custo estimado
P — 2 arquivos:
components/forms/transaction/SplitSection.tsx+__tests__/unit/a11y-nomes-acessiveis.test.ts.Fecha parcialmente #106 — usar
refs #106, nãocloses(exigência 8).