[{"data":1,"prerenderedAt":1308},["ShallowReactive",2],{"i-simple-icons:github":3,"i-lucide:arrow-up-right":8,"i-lucide:loader":10,"post-pt-br-custom-element-vitest-silent-failures":12,"translations-blog-pt-br-custom-element-vitest-silent-failures":1289,"i-material-symbols:arrow-back":1297,"i-lucide:languages":1299,"i-lucide:hash":1301,"i-logos:typescript-icon":1303,"i-material-symbols:content-copy-outline":1306},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":7},0,24,false,"\u003Cpath fill=\"currentColor\" d=\"M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":9},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M7 7h10v10M7 17L17 7\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":11},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 2v4m4.2 1.8l2.9-2.9M18 12h4m-5.8 4.2l2.9 2.9M12 18v4m-7.1-2.9l2.9-2.9M2 12h4M4.9 4.9l2.9 2.9\"\u002F>",{"id":13,"title":14,"body":15,"date":1280,"description":1281,"draft":6,"extension":1282,"machineTranslated":194,"meta":1283,"navigation":194,"path":1284,"seo":1285,"stem":1286,"translationKey":1287,"__hash__":1288},"blog\u002Fpt-br\u002Fblog\u002Fcustom-element-vitest-silent-failures.md","Por que seus testes de Custom Elements falham silenciosamente no Vitest",{"type":16,"value":17,"toc":1274},"minimark",[18,37,42,45,226,229,334,349,352,356,363,386,389,496,517,520,522,526,539,658,668,753,771,788,804,810,1061,1068,1125,1127,1131,1134,1178,1258,1270],[19,20,21,22,32,33,36],"p",{},"Recentemente construí o ",[23,24,28],"a",{"href":25,"rel":26},"https:\u002F\u002Fgithub.com\u002Farthurvasconcelos\u002Fa11y-hud",[27],"nofollow",[29,30,31],"code",{},"a11y-hud",", um overlay de acessibilidade agnóstico de framework que roda auditorias do axe-core dentro de uma aplicação web em execução. O núcleo dele é um Custom Element ",[29,34,35],{},"\u003Ca11y-hud>"," com Shadow DOM. Escrever testes unitários para ele no Vitest com jsdom acabou sendo mais interessante do que eu esperava, e não no bom sentido. Estas são as duas falhas que me custaram mais tempo.",[38,39,41],"h2",{"id":40},"a-configuração","A configuração",[19,43,44],{},"O elemento se registra como efeito colateral quando seu módulo carrega:",[46,47,52],"pre",{"className":48,"code":49,"language":50,"meta":51,"style":51},"language-typescript shiki shiki-themes material-theme-lighter github-light tokyo-night","export class A11yHudElement extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: \"open\" });\n  }\n  connectedCallback() {\n    \u002F\u002F sets data-theme, renders the panel, starts the scan...\n  }\n}\n\ncustomElements.define(\"a11y-hud\", A11yHudElement);\n","typescript","",[29,53,54,83,94,108,154,160,171,178,183,189,196],{"__ignoreMap":51},[55,56,59,63,67,71,75,79],"span",{"class":57,"line":58},"line",1,[55,60,62],{"class":61},"sjbMc","export",[55,64,66],{"class":65},"s5-vi"," class",[55,68,70],{"class":69},"soe2T"," A11yHudElement",[55,72,74],{"class":73},"sLNfz"," extends",[55,76,78],{"class":77},"sst2a"," HTMLElement",[55,80,82],{"class":81},"s9td2"," {\n",[55,84,86,89,92],{"class":57,"line":85},2,[55,87,88],{"class":65},"  constructor",[55,90,91],{"class":81},"()",[55,93,82],{"class":81},[55,95,97,101,104],{"class":57,"line":96},3,[55,98,100],{"class":99},"s2LeO","    super",[55,102,91],{"class":103},"sLT5_",[55,105,107],{"class":106},"scI-1",";\n",[55,109,111,115,118,122,125,128,132,135,139,143,146,149,152],{"class":57,"line":110},4,[55,112,114],{"class":113},"sFcAS","    this",[55,116,117],{"class":106},".",[55,119,121],{"class":120},"stLRJ","attachShadow",[55,123,124],{"class":103},"(",[55,126,127],{"class":81},"{",[55,129,131],{"class":130},"sqlqt"," mode",[55,133,134],{"class":106},":",[55,136,138],{"class":137},"sby7X"," \"",[55,140,142],{"class":141},"smbRM","open",[55,144,145],{"class":137},"\"",[55,147,148],{"class":81}," }",[55,150,151],{"class":103},")",[55,153,107],{"class":106},[55,155,157],{"class":57,"line":156},5,[55,158,159],{"class":81},"  }\n",[55,161,163,167,169],{"class":57,"line":162},6,[55,164,166],{"class":165},"sYrai","  connectedCallback",[55,168,91],{"class":81},[55,170,82],{"class":81},[55,172,174],{"class":57,"line":173},7,[55,175,177],{"class":176},"s5acS","    \u002F\u002F sets data-theme, renders the panel, starts the scan...\n",[55,179,181],{"class":57,"line":180},8,[55,182,159],{"class":81},[55,184,186],{"class":57,"line":185},9,[55,187,188],{"class":81},"}\n",[55,190,192],{"class":57,"line":191},10,[55,193,195],{"emptyLinePlaceholder":194},true,"\n",[55,197,199,203,205,208,211,213,215,217,220,222,224],{"class":57,"line":198},11,[55,200,202],{"class":201},"swiMj","customElements",[55,204,117],{"class":106},[55,206,207],{"class":120},"define",[55,209,124],{"class":210},"sOEgH",[55,212,145],{"class":137},[55,214,31],{"class":141},[55,216,145],{"class":137},[55,218,219],{"class":106},",",[55,221,70],{"class":201},[55,223,151],{"class":210},[55,225,107],{"class":106},[19,227,228],{},"No arquivo de teste:",[46,230,232],{"className":48,"code":231,"language":50,"meta":51,"style":51},"import { A11yHudElement } from \".\u002Felement.js\";\n\nit(\"registers as a custom element\", () => {\n  expect(customElements.get(\"a11y-hud\")).toBeDefined(); \u002F\u002F undefined\n});\n",[29,233,234,259,263,287,325],{"__ignoreMap":51},[55,235,236,239,242,245,247,250,252,255,257],{"class":57,"line":58},[55,237,238],{"class":61},"import",[55,240,241],{"class":81}," {",[55,243,70],{"class":244},"sO8C4",[55,246,148],{"class":81},[55,248,249],{"class":61}," from",[55,251,138],{"class":137},[55,253,254],{"class":141},".\u002Felement.js",[55,256,145],{"class":137},[55,258,107],{"class":106},[55,260,261],{"class":57,"line":85},[55,262,195],{"emptyLinePlaceholder":194},[55,264,265,268,270,272,275,277,279,282,285],{"class":57,"line":96},[55,266,267],{"class":120},"it",[55,269,124],{"class":210},[55,271,145],{"class":137},[55,273,274],{"class":141},"registers as a custom element",[55,276,145],{"class":137},[55,278,219],{"class":106},[55,280,281],{"class":81}," ()",[55,283,284],{"class":65}," =>",[55,286,82],{"class":81},[55,288,289,292,294,296,298,301,303,305,307,309,312,314,317,319,322],{"class":57,"line":110},[55,290,291],{"class":120},"  expect",[55,293,124],{"class":103},[55,295,202],{"class":201},[55,297,117],{"class":106},[55,299,300],{"class":120},"get",[55,302,124],{"class":103},[55,304,145],{"class":137},[55,306,31],{"class":141},[55,308,145],{"class":137},[55,310,311],{"class":103},"))",[55,313,117],{"class":106},[55,315,316],{"class":120},"toBeDefined",[55,318,91],{"class":103},[55,320,321],{"class":106},";",[55,323,324],{"class":176}," \u002F\u002F undefined\n",[55,326,327,330,332],{"class":57,"line":156},[55,328,329],{"class":81},"}",[55,331,151],{"class":210},[55,333,107],{"class":106},[19,335,336,337,340,341,344,345,348],{},"Este teste falha. ",[29,338,339],{},"customElements.get(\"a11y-hud\")"," retorna ",[29,342,343],{},"undefined",", como se ",[29,346,347],{},"customElements.define"," nunca tivesse sido chamado. Nenhum erro de import, nenhum erro de módulo, nada no console. A linha de import está bem ali. A chamada de define simplesmente não aconteceu.",[350,351],"hr",{},[38,353,355],{"id":354},"problema-1-o-esbuild-descarta-um-import-que-você-nunca-usa","Problema 1: o esbuild descarta um import que você nunca usa",[19,357,358,359,362],{},"O Vitest entrega arquivos TypeScript ao esbuild, que os transpila um por vez, sem informação de tipos. Isso importa aqui, porque o esbuild não tem como saber se ",[29,360,361],{},"A11yHudElement"," é uma classe ou uma interface. A regra do próprio TypeScript é que um import usado apenas como tipo é apagado da saída, e o esbuild copia essa regra da única forma que consegue: se um nome importado nunca é usado como valor no arquivo, o import inteiro é removido.",[19,364,365,366,368,369,371,372,374,375,378,379,381,382,385],{},"No teste acima, ",[29,367,361],{}," é importado mas nunca referenciado. O bloco ",[29,370,267],{}," só chama ",[29,373,339],{},". Então o esbuild trata o import como somente-tipo e o apaga. A saída é um arquivo sem import nenhum, ",[29,376,377],{},"element.ts"," nunca carrega e ",[29,380,347],{}," nunca roda. Você pode ver isso por conta própria passando o teste pelo ",[29,383,384],{},"esbuild --loader=ts",": a linha de import simplesmente some.",[19,387,388],{},"A correção que escolhi é uma linha:",[46,390,392],{"className":48,"code":391,"language":50,"meta":51,"style":51},"import { A11yHudElement } from \".\u002Felement.js\";\n\nvoid A11yHudElement; \u002F\u002F uses the import as a value, so esbuild keeps it\n\nit(\"registers as a custom element\", () => {\n  expect(customElements.get(\"a11y-hud\")).toBeDefined(); \u002F\u002F defined\n});\n",[29,393,394,414,418,431,435,455,488],{"__ignoreMap":51},[55,395,396,398,400,402,404,406,408,410,412],{"class":57,"line":58},[55,397,238],{"class":61},[55,399,241],{"class":81},[55,401,70],{"class":244},[55,403,148],{"class":81},[55,405,249],{"class":61},[55,407,138],{"class":137},[55,409,254],{"class":141},[55,411,145],{"class":137},[55,413,107],{"class":106},[55,415,416],{"class":57,"line":85},[55,417,195],{"emptyLinePlaceholder":194},[55,419,420,424,426,428],{"class":57,"line":96},[55,421,423],{"class":422},"s4Nsh","void",[55,425,70],{"class":201},[55,427,321],{"class":106},[55,429,430],{"class":176}," \u002F\u002F uses the import as a value, so esbuild keeps it\n",[55,432,433],{"class":57,"line":110},[55,434,195],{"emptyLinePlaceholder":194},[55,436,437,439,441,443,445,447,449,451,453],{"class":57,"line":156},[55,438,267],{"class":120},[55,440,124],{"class":210},[55,442,145],{"class":137},[55,444,274],{"class":141},[55,446,145],{"class":137},[55,448,219],{"class":106},[55,450,281],{"class":81},[55,452,284],{"class":65},[55,454,82],{"class":81},[55,456,457,459,461,463,465,467,469,471,473,475,477,479,481,483,485],{"class":57,"line":162},[55,458,291],{"class":120},[55,460,124],{"class":103},[55,462,202],{"class":201},[55,464,117],{"class":106},[55,466,300],{"class":120},[55,468,124],{"class":103},[55,470,145],{"class":137},[55,472,31],{"class":141},[55,474,145],{"class":137},[55,476,311],{"class":103},[55,478,117],{"class":106},[55,480,316],{"class":120},[55,482,91],{"class":103},[55,484,321],{"class":106},[55,486,487],{"class":176}," \u002F\u002F defined\n",[55,489,490,492,494],{"class":57,"line":173},[55,491,329],{"class":81},[55,493,151],{"class":210},[55,495,107],{"class":106},[19,497,498,501,502,505,506,509,510,513,514,516],{},[29,499,500],{},"void expr"," avalia a expressão e descarta o resultado, o que basta para tornar o import um import de valor. Há duas alternativas mais limpas, dependendo do gosto. Um import puro de efeito colateral, ",[29,503,504],{},"import \".\u002Felement.js\"",", diz exatamente o que você quer dizer e não pode ser eliminado. Ou defina ",[29,507,508],{},"verbatimModuleSyntax: true"," no seu tsconfig: o esbuild respeita isso e para de descartar imports, e o TypeScript então te obriga a escrever ",[29,511,512],{},"import type"," para os que são só tipo. Eu escolheria essa opção num projeto novo. Num projeto existente com muitos imports mistos, a linha com ",[29,515,423],{}," é a opção pragmática.",[19,518,519],{},"Isso não morde no build de produção, porque nada ali importa a classe sem usá-la. Só aparece nos testes, onde importar um módulo puramente pelo efeito colateral de registro é algo normal de se fazer.",[350,521],{},[38,523,525],{"id":524},"problema-2-o-jsdom-reporta-erros-de-ciclo-de-vida-num-lugar-onde-você-não-está-olhando","Problema 2: o jsdom reporta erros de ciclo de vida num lugar onde você não está olhando",[19,527,528,529,532,533,536,537,134],{},"Com o primeiro problema resolvido, o elemento se registrava, mas vários testes que dependiam do ",[29,530,531],{},"connectedCallback"," ainda estouravam o tempo limite. Especificamente, testes que esperavam ",[29,534,535],{},"el.dataset.theme"," ser definido ficavam parados pelos 1000 ms inteiros e falhavam. Aqui está uma versão simplificada do ",[29,538,531],{},[46,540,542],{"className":48,"code":541,"language":50,"meta":51,"style":51},"connectedCallback() {\n  this._applyResolvedTheme(); \u002F\u002F sets dataset.theme\n  this._render();\n  void this._runScan();\n}\n\nprivate _applyResolvedTheme() {\n  this.dataset.theme = resolveTheme(this._theme);\n}\n",[29,543,544,552,569,582,599,603,607,619,654],{"__ignoreMap":51},[55,545,546,548,550],{"class":57,"line":58},[55,547,531],{"class":120},[55,549,91],{"class":210},[55,551,82],{"class":81},[55,553,554,557,559,562,564,566],{"class":57,"line":85},[55,555,556],{"class":113},"  this",[55,558,117],{"class":106},[55,560,561],{"class":120},"_applyResolvedTheme",[55,563,91],{"class":103},[55,565,321],{"class":106},[55,567,568],{"class":176}," \u002F\u002F sets dataset.theme\n",[55,570,571,573,575,578,580],{"class":57,"line":96},[55,572,556],{"class":113},[55,574,117],{"class":106},[55,576,577],{"class":120},"_render",[55,579,91],{"class":103},[55,581,107],{"class":106},[55,583,584,587,590,592,595,597],{"class":57,"line":110},[55,585,586],{"class":422},"  void",[55,588,589],{"class":113}," this",[55,591,117],{"class":106},[55,593,594],{"class":120},"_runScan",[55,596,91],{"class":103},[55,598,107],{"class":106},[55,600,601],{"class":57,"line":156},[55,602,188],{"class":81},[55,604,605],{"class":57,"line":162},[55,606,195],{"emptyLinePlaceholder":194},[55,608,609,612,615,617],{"class":57,"line":173},[55,610,611],{"class":201},"private",[55,613,614],{"class":120}," _applyResolvedTheme",[55,616,91],{"class":210},[55,618,82],{"class":81},[55,620,621,623,625,628,630,634,637,640,642,645,647,650,652],{"class":57,"line":180},[55,622,556],{"class":113},[55,624,117],{"class":106},[55,626,627],{"class":201},"dataset",[55,629,117],{"class":106},[55,631,633],{"class":632},"s-jro","theme",[55,635,636],{"class":422}," =",[55,638,639],{"class":120}," resolveTheme",[55,641,124],{"class":103},[55,643,644],{"class":113},"this",[55,646,117],{"class":106},[55,648,649],{"class":632},"_theme",[55,651,151],{"class":103},[55,653,107],{"class":106},[55,655,656],{"class":57,"line":185},[55,657,188],{"class":81},[19,659,660,661,664,665,134],{},"E ",[29,662,663],{},"resolveTheme"," chama ",[29,666,667],{},"matchMedia",[46,669,671],{"className":48,"code":670,"language":50,"meta":51,"style":51},"export function resolveTheme(theme: Theme): ResolvedTheme {\n  if (matchMedia(\"(prefers-contrast: more)\").matches) return \"high-contrast\";\n  \u002F\u002F ...\n}\n",[29,672,673,701,744,749],{"__ignoreMap":51},[55,674,675,677,680,682,684,687,689,692,694,696,699],{"class":57,"line":58},[55,676,62],{"class":61},[55,678,679],{"class":65}," function",[55,681,639],{"class":120},[55,683,124],{"class":81},[55,685,633],{"class":686},"sGWo7",[55,688,134],{"class":422},[55,690,691],{"class":69}," Theme",[55,693,151],{"class":81},[55,695,134],{"class":422},[55,697,698],{"class":69}," ResolvedTheme",[55,700,82],{"class":81},[55,702,703,707,710,712,714,716,719,721,723,725,728,731,735,737,740,742],{"class":57,"line":85},[55,704,706],{"class":705},"sNxNA","  if",[55,708,709],{"class":103}," (",[55,711,667],{"class":120},[55,713,124],{"class":103},[55,715,145],{"class":137},[55,717,718],{"class":141},"(prefers-contrast: more)",[55,720,145],{"class":137},[55,722,151],{"class":103},[55,724,117],{"class":106},[55,726,727],{"class":632},"matches",[55,729,730],{"class":103},") ",[55,732,734],{"class":733},"smhDj","return",[55,736,138],{"class":137},[55,738,739],{"class":141},"high-contrast",[55,741,145],{"class":137},[55,743,107],{"class":106},[55,745,746],{"class":57,"line":96},[55,747,748],{"class":176},"  \u002F\u002F ...\n",[55,750,751],{"class":57,"line":110},[55,752,188],{"class":81},[19,754,755,756,759,760,763,764,766,767,770],{},"O jsdom não implementa ",[29,757,758],{},"window.matchMedia",". Chamá-lo lança ",[29,761,762],{},"TypeError: matchMedia is not a function",". Então ",[29,765,561],{}," lança a exceção, ",[29,768,769],{},"dataset.theme"," nunca é definido e o teste espera até estourar o tempo.",[19,772,773,774,777,778,780,781,783,784,787],{},"A parte que torna isso difícil de depurar é onde o erro vai parar. ",[29,775,776],{},"document.body.appendChild(el)"," não lança nada. O teste não falha na linha que causou o problema. Ele falha por timeout, com uma mensagem sobre ",[29,779,769],{}," ainda estar undefined, o que aponta para a lógica de tema e não para o ",[29,782,667],{},". O ",[29,785,786],{},"TypeError"," até é impresso, mas pelo Vitest, como um bloco \"Unhandled Error\" depois da execução inteira, sem nenhuma ligação com o teste que estourou o tempo. E se o arquivo de teste que você está olhando por acaso não faz asserções sobre nada a jusante da exceção, ele passa, com o erro parado no fim da saída como um aviso vago sobre falsos positivos.",[19,789,790,791,796,797,799,800,803],{},"Esse é o comportamento conforme a especificação. A ",[23,792,795],{"href":793,"rel":794},"https:\u002F\u002Fhtml.spec.whatwg.org\u002Fmultipage\u002Fcustom-elements.html#invoke-custom-element-reactions",[27],"especificação de Custom Elements"," diz que uma exceção lançada de um callback de ciclo de vida (",[29,798,531],{},", ",[29,801,802],{},"attributeChangedCallback"," e assim por diante) deve ser reportada, não propagada para o que a disparou. O jsdom faz exatamente isso: captura o erro e o reporta do mesmo jeito que um navegador reporta uma exceção não tratada de um manipulador de evento. Num navegador essa é a decisão certa, porque um elemento quebrado não deveria derrubar a página. Num teste, significa que o stack trace e a asserção que falhou são duas coisas diferentes em dois lugares diferentes.",[19,805,806,807,809],{},"A correção é fazer um stub de ",[29,808,758],{}," num arquivo de setup do Vitest, que roda uma vez antes de qualquer arquivo de teste:",[46,811,814],{"className":48,"code":812,"filename":813,"language":50,"meta":51,"style":51},"if (!window.matchMedia) {\n  Object.defineProperty(window, \"matchMedia\", {\n    value: (query: string): MediaQueryList => ({\n      matches: false,\n      media: query,\n      onchange: null,\n      addListener: () => {},\n      removeListener: () => {},\n      addEventListener: () => {},\n      removeEventListener: () => {},\n      dispatchEvent: () => false,\n    }),\n    configurable: true,\n    writable: true,\n  });\n}\n","src\u002Ftest-setup.ts",[29,815,816,838,864,896,910,922,935,951,966,981,996,1011,1021,1034,1046,1056],{"__ignoreMap":51},[55,817,818,821,823,827,830,832,834,836],{"class":57,"line":58},[55,819,820],{"class":705},"if",[55,822,709],{"class":210},[55,824,826],{"class":825},"sJNXy","!",[55,828,829],{"class":201},"window",[55,831,117],{"class":106},[55,833,667],{"class":632},[55,835,151],{"class":210},[55,837,82],{"class":81},[55,839,840,843,845,848,850,852,854,856,858,860,862],{"class":57,"line":85},[55,841,842],{"class":201},"  Object",[55,844,117],{"class":106},[55,846,847],{"class":120},"defineProperty",[55,849,124],{"class":103},[55,851,829],{"class":201},[55,853,219],{"class":106},[55,855,138],{"class":137},[55,857,667],{"class":141},[55,859,145],{"class":137},[55,861,219],{"class":106},[55,863,82],{"class":81},[55,865,866,869,871,873,876,878,882,884,886,889,891,893],{"class":57,"line":96},[55,867,868],{"class":120},"    value",[55,870,134],{"class":106},[55,872,709],{"class":81},[55,874,875],{"class":686},"query",[55,877,134],{"class":422},[55,879,881],{"class":880},"sMMjR"," string",[55,883,151],{"class":81},[55,885,134],{"class":422},[55,887,888],{"class":69}," MediaQueryList",[55,890,284],{"class":65},[55,892,709],{"class":103},[55,894,895],{"class":81},"{\n",[55,897,898,901,903,907],{"class":57,"line":110},[55,899,900],{"class":130},"      matches",[55,902,134],{"class":106},[55,904,906],{"class":905},"swdek"," false",[55,908,909],{"class":106},",\n",[55,911,912,915,917,920],{"class":57,"line":156},[55,913,914],{"class":130},"      media",[55,916,134],{"class":106},[55,918,919],{"class":201}," query",[55,921,909],{"class":106},[55,923,924,927,929,933],{"class":57,"line":162},[55,925,926],{"class":130},"      onchange",[55,928,134],{"class":106},[55,930,932],{"class":931},"s-rx0"," null",[55,934,909],{"class":106},[55,936,937,940,942,944,946,949],{"class":57,"line":173},[55,938,939],{"class":120},"      addListener",[55,941,134],{"class":106},[55,943,281],{"class":81},[55,945,284],{"class":65},[55,947,948],{"class":81}," {}",[55,950,909],{"class":106},[55,952,953,956,958,960,962,964],{"class":57,"line":180},[55,954,955],{"class":120},"      removeListener",[55,957,134],{"class":106},[55,959,281],{"class":81},[55,961,284],{"class":65},[55,963,948],{"class":81},[55,965,909],{"class":106},[55,967,968,971,973,975,977,979],{"class":57,"line":185},[55,969,970],{"class":120},"      addEventListener",[55,972,134],{"class":106},[55,974,281],{"class":81},[55,976,284],{"class":65},[55,978,948],{"class":81},[55,980,909],{"class":106},[55,982,983,986,988,990,992,994],{"class":57,"line":191},[55,984,985],{"class":120},"      removeEventListener",[55,987,134],{"class":106},[55,989,281],{"class":81},[55,991,284],{"class":65},[55,993,948],{"class":81},[55,995,909],{"class":106},[55,997,998,1001,1003,1005,1007,1009],{"class":57,"line":198},[55,999,1000],{"class":120},"      dispatchEvent",[55,1002,134],{"class":106},[55,1004,281],{"class":81},[55,1006,284],{"class":65},[55,1008,906],{"class":905},[55,1010,909],{"class":106},[55,1012,1014,1017,1019],{"class":57,"line":1013},12,[55,1015,1016],{"class":81},"    }",[55,1018,151],{"class":103},[55,1020,909],{"class":106},[55,1022,1024,1027,1029,1032],{"class":57,"line":1023},13,[55,1025,1026],{"class":130},"    configurable",[55,1028,134],{"class":106},[55,1030,1031],{"class":905}," true",[55,1033,909],{"class":106},[55,1035,1037,1040,1042,1044],{"class":57,"line":1036},14,[55,1038,1039],{"class":130},"    writable",[55,1041,134],{"class":106},[55,1043,1031],{"class":905},[55,1045,909],{"class":106},[55,1047,1049,1052,1054],{"class":57,"line":1048},15,[55,1050,1051],{"class":81},"  }",[55,1053,151],{"class":103},[55,1055,107],{"class":106},[55,1057,1059],{"class":57,"line":1058},16,[55,1060,188],{"class":81},[19,1062,1063,1064,1067],{},"Já que você está lá, faça um stub de ",[29,1065,1066],{},"scrollIntoView"," também. O jsdom também não implementa esse, e ele vai aparecer como o mesmo tipo de erro não tratado e desconectado no momento em que um teste exercitar um fluxo de destaque ou rolagem:",[46,1069,1071],{"className":48,"code":1070,"language":50,"meta":51,"style":51},"if (!Element.prototype.scrollIntoView) {\n  Element.prototype.scrollIntoView = () => {};\n}\n",[29,1072,1073,1098,1121],{"__ignoreMap":51},[55,1074,1075,1077,1079,1081,1084,1086,1090,1092,1094,1096],{"class":57,"line":58},[55,1076,820],{"class":705},[55,1078,709],{"class":210},[55,1080,826],{"class":825},[55,1082,1083],{"class":880},"Element",[55,1085,117],{"class":106},[55,1087,1089],{"class":1088},"sjRl0","prototype",[55,1091,117],{"class":106},[55,1093,1066],{"class":632},[55,1095,151],{"class":210},[55,1097,82],{"class":81},[55,1099,1100,1103,1105,1107,1109,1111,1113,1115,1117,1119],{"class":57,"line":85},[55,1101,1102],{"class":880},"  Element",[55,1104,117],{"class":106},[55,1106,1089],{"class":1088},[55,1108,117],{"class":106},[55,1110,1066],{"class":120},[55,1112,636],{"class":422},[55,1114,281],{"class":81},[55,1116,284],{"class":65},[55,1118,948],{"class":81},[55,1120,107],{"class":106},[55,1122,1123],{"class":57,"line":96},[55,1124,188],{"class":81},[350,1126],{},[38,1128,1130],{"id":1129},"o-checklist","O checklist",[19,1132,1133],{},"Se você está testando Custom Elements no Vitest com jsdom, faça estas três coisas de antemão e pule a sessão de depuração:",[1135,1136,1137,1156,1168],"ol",{},[1138,1139,1140,1144,1145,1148,1149,1152,1153,1155],"li",{},[1141,1142,1143],"strong",{},"Garanta que o módulo do elemento realmente carrega."," Ou ",[29,1146,1147],{},"void MyElement"," depois do import, ou um ",[29,1150,1151],{},"import \".\u002Fmy-element.js\""," puro, ou ",[29,1154,508],{}," no tsconfig. Sem um desses, o esbuild descarta um import que nunca vê sendo usado como valor.",[1138,1157,1158,1163,1164,1167],{},[1141,1159,1160,1161],{},"Faça um stub de ",[29,1162,758],{}," no seu ",[29,1165,1166],{},"setupFiles",". Qualquer callback de ciclo de vida que toque em media queries vai lançar uma exceção, e ela não vai cair no teste que depende dele.",[1138,1169,1170,1163,1175,1177],{},[1141,1171,1160,1172],{},[29,1173,1174],{},"Element.prototype.scrollIntoView",[29,1176,1166],{},", pelo mesmo motivo.",[46,1179,1182],{"className":48,"code":1180,"filename":1181,"language":50,"meta":51,"style":51},"export default defineConfig({\n  test: {\n    environment: \"jsdom\",\n    setupFiles: [\"src\u002Ftest-setup.ts\"],\n  },\n});\n","vitest.config.ts",[29,1183,1184,1198,1207,1223,1244,1250],{"__ignoreMap":51},[55,1185,1186,1188,1191,1194,1196],{"class":57,"line":58},[55,1187,62],{"class":61},[55,1189,1190],{"class":61}," default",[55,1192,1193],{"class":120}," defineConfig",[55,1195,124],{"class":210},[55,1197,895],{"class":81},[55,1199,1200,1203,1205],{"class":57,"line":85},[55,1201,1202],{"class":130},"  test",[55,1204,134],{"class":106},[55,1206,82],{"class":81},[55,1208,1209,1212,1214,1216,1219,1221],{"class":57,"line":96},[55,1210,1211],{"class":130},"    environment",[55,1213,134],{"class":106},[55,1215,138],{"class":137},[55,1217,1218],{"class":141},"jsdom",[55,1220,145],{"class":137},[55,1222,909],{"class":106},[55,1224,1225,1228,1230,1233,1235,1237,1239,1242],{"class":57,"line":110},[55,1226,1227],{"class":130},"    setupFiles",[55,1229,134],{"class":106},[55,1231,1232],{"class":210}," [",[55,1234,145],{"class":137},[55,1236,813],{"class":141},[55,1238,145],{"class":137},[55,1240,1241],{"class":210},"]",[55,1243,909],{"class":106},[55,1245,1246,1248],{"class":57,"line":156},[55,1247,1051],{"class":81},[55,1249,909],{"class":106},[55,1251,1252,1254,1256],{"class":57,"line":162},[55,1253,329],{"class":81},[55,1255,151],{"class":210},[55,1257,107],{"class":106},[19,1259,1260,1261,340,1264,1266,1267,1269],{},"Os dois problemas se somam. O esbuild removendo um import não deixa rastro nenhum, e o jsdom reportando erros de ciclo de vida fora de banda deixa um rastro que não aponta para o teste que quebrou. Depois que você conhece os dois modos de falha, o caminho de depuração é curto. Antes de conhecê-los, os dois parecem iguais vistos de fora: ",[29,1262,1263],{},"customElements.get(\"my-element\")",[29,1265,343],{},", ou ",[29,1268,769],{}," nunca aparece, e nada na saída ao lado do teste que falhou te diz por quê.",[1271,1272,1273],"style",{},"html pre.shiki code .sjbMc, html code.shiki .sjbMc{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#7DCFFF;--shiki-dark-font-style:inherit}html pre.shiki code .s5-vi, html code.shiki .s5-vi{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#BB9AF7}html pre.shiki code .soe2T, html code.shiki .soe2T{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#C0CAF5}html pre.shiki code .sLNfz, html code.shiki .sLNfz{--shiki-light:#9C3EDA;--shiki-light-font-style:inherit;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#9D7CD8;--shiki-dark-font-style:italic}html pre.shiki code .sst2a, html code.shiki .sst2a{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#BB9AF7}html pre.shiki code .s9td2, html code.shiki .s9td2{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#9ABDF5}html pre.shiki code .s2LeO, html code.shiki .s2LeO{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#F7768E}html pre.shiki code .sLT5_, html code.shiki .sLT5_{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#9ABDF5}html pre.shiki code .scI-1, html code.shiki .scI-1{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#89DDFF}html pre.shiki code .sFcAS, html code.shiki .sFcAS{--shiki-light:#39ADB5;--shiki-default:#005CC5;--shiki-dark:#F7768E}html pre.shiki code .stLRJ, html code.shiki .stLRJ{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#7AA2F7}html pre.shiki code .sqlqt, html code.shiki .sqlqt{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#73DACA}html pre.shiki code .sby7X, html code.shiki .sby7X{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#89DDFF}html pre.shiki code .smbRM, html code.shiki .smbRM{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECE6A}html pre.shiki code .sYrai, html code.shiki .sYrai{--shiki-light:#E53935;--shiki-default:#6F42C1;--shiki-dark:#7AA2F7}html pre.shiki code .s5acS, html code.shiki .s5acS{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#51597D;--shiki-dark-font-style:italic}html pre.shiki code .swiMj, html code.shiki .swiMj{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#C0CAF5}html pre.shiki code .sOEgH, html code.shiki .sOEgH{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#9ABDF5}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sO8C4, html code.shiki .sO8C4{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#0DB9D7}html pre.shiki code .s4Nsh, html code.shiki .s4Nsh{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#89DDFF}html pre.shiki code .s-jro, html code.shiki .s-jro{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#7DCFFF}html pre.shiki code .sGWo7, html code.shiki .sGWo7{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#E0AF68;--shiki-dark-font-style:inherit}html pre.shiki code .sNxNA, html code.shiki .sNxNA{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#BB9AF7;--shiki-dark-font-style:inherit}html pre.shiki code .smhDj, html code.shiki .smhDj{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#BB9AF7;--shiki-dark-font-style:italic}html pre.shiki code .sJNXy, html code.shiki .sJNXy{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#BB9AF7}html pre.shiki code .sMMjR, html code.shiki .sMMjR{--shiki-light:#E2931D;--shiki-default:#005CC5;--shiki-dark:#0DB9D7}html pre.shiki code .swdek, html code.shiki .swdek{--shiki-light:#FF5370;--shiki-default:#005CC5;--shiki-dark:#FF9E64}html pre.shiki code .s-rx0, html code.shiki .s-rx0{--shiki-light:#39ADB5;--shiki-default:#005CC5;--shiki-dark:#FF9E64}html pre.shiki code .sjRl0, html code.shiki .sjRl0{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#7DCFFF}",{"title":51,"searchDepth":85,"depth":85,"links":1275},[1276,1277,1278,1279],{"id":40,"depth":85,"text":41},{"id":354,"depth":85,"text":355},{"id":524,"depth":85,"text":525},{"id":1129,"depth":85,"text":1130},"2026-05-03","Duas pegadinhas nada óbvias que fazem testes de Custom Elements parecerem corretos enquanto estão completamente quebrados, e como corrigi-las.","md",{},"\u002Fpt-br\u002Fblog\u002Fcustom-element-vitest-silent-failures",{"title":14,"description":1281},"pt-br\u002Fblog\u002Fcustom-element-vitest-silent-failures","custom-element-vitest-silent-failures","EKolbXikAeOrn10hVLBFVPQr7AfdANkUIMNgYcc_qZ0",[1290,1294],{"path":1291,"name":1292,"to":1293},"\u002Fen-gb\u002Fblog\u002Fcustom-element-vitest-silent-failures","English","\u002Fblog\u002Fcustom-element-vitest-silent-failures",{"path":1295,"name":1296,"to":1295},"\u002Fsv\u002Fblog\u002Fcustom-element-vitest-silent-failures","Svenska",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":1298},"\u003Cpath fill=\"currentColor\" d=\"m7.825 13l5.6 5.6L12 20l-8-8l8-8l1.425 1.4l-5.6 5.6H20v2z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":1300},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m5 8l6 6m-7 0l6-6l2-3M2 5h12M7 2h1m14 20l-5-10l-5 10m2-4h6\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":1302},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 9h16M4 15h16M10 3L8 21m8-18l-2 18\"\u002F>",{"left":4,"top":4,"width":1304,"height":1304,"rotate":4,"vFlip":6,"hFlip":6,"body":1305},256,"\u003Cpath fill=\"#3178c6\" d=\"M20 0h216c11.046 0 20 8.954 20 20v216c0 11.046-8.954 20-20 20H20c-11.046 0-20-8.954-20-20V20C0 8.954 8.954 0 20 0\"\u002F>\u003Cpath fill=\"#fff\" d=\"M150.518 200.475v27.62q6.738 3.453 15.938 5.179T185.849 235q9.934 0 18.874-1.899t15.678-6.257q6.738-4.359 10.669-11.394q3.93-7.033 3.93-17.391q0-7.51-2.246-13.163a30.8 30.8 0 0 0-6.479-10.055q-4.232-4.402-10.149-7.898t-13.347-6.602q-5.442-2.245-9.761-4.359t-7.342-4.316q-3.024-2.2-4.665-4.661t-1.641-5.567q0-2.848 1.468-5.135q1.469-2.288 4.147-3.927t6.565-2.547q3.887-.906 8.638-.906q3.456 0 7.299.518q3.844.517 7.732 1.597a54 54 0 0 1 7.558 2.719a41.7 41.7 0 0 1 6.781 3.797v-25.807q-6.306-2.417-13.778-3.582T198.633 107q-9.847 0-18.658 2.115q-8.811 2.114-15.506 6.602q-6.694 4.49-10.582 11.437Q150 134.102 150 143.769q0 12.342 7.127 21.06t21.638 14.759a292 292 0 0 1 10.625 4.575q4.924 2.244 8.509 4.66t5.658 5.265t2.073 6.474a9.9 9.9 0 0 1-1.296 4.963q-1.295 2.287-3.93 3.97t-6.565 2.632t-9.2.95q-8.983 0-17.794-3.151t-16.327-9.451m-46.036-68.733H140V109H41v22.742h35.345V233h28.137z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":1307},"\u003Cpath fill=\"currentColor\" d=\"M9 18q-.825 0-1.412-.587T7 16V4q0-.825.588-1.412T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.587 1.413T18 18zm0-2h9V4H9zm-4 6q-.825 0-1.412-.587T3 20V6h2v14h11v2zm4-6V4z\"\u002F>",1792918925008]