[{"data":1,"prerenderedAt":1308},["ShallowReactive",2],{"post-pt-br-custom-element-vitest-silent-failures":3,"i-simple-icons:github":1281,"i-lucide:arrow-up-right":1285,"i-lucide:loader":1287,"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},{"id":4,"title":5,"body":6,"date":1271,"description":1272,"draft":1273,"extension":1274,"machineTranslated":185,"meta":1275,"navigation":185,"path":1276,"seo":1277,"stem":1278,"translationKey":1279,"__hash__":1280},"blog\u002Fpt-br\u002Fblog\u002Fcustom-element-vitest-silent-failures.md","Por que seus testes de Custom Elements falham silenciosamente no Vitest",{"type":7,"value":8,"toc":1265},"minimark",[9,28,33,36,217,220,325,340,343,347,354,377,380,487,508,511,513,517,530,649,659,744,762,779,795,801,1052,1059,1116,1118,1122,1125,1169,1249,1261],[10,11,12,13,23,24,27],"p",{},"Recentemente construí o ",[14,15,19],"a",{"href":16,"rel":17},"https:\u002F\u002Fgithub.com\u002Farthurvasconcelos\u002Fa11y-hud",[18],"nofollow",[20,21,22],"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 ",[20,25,26],{},"\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.",[29,30,32],"h2",{"id":31},"a-configuração","A configuração",[10,34,35],{},"O elemento se registra como efeito colateral quando seu módulo carrega:",[37,38,43],"pre",{"className":39,"code":40,"language":41,"meta":42,"style":42},"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","",[20,44,45,74,85,99,145,151,162,169,174,180,187],{"__ignoreMap":42},[46,47,50,54,58,62,66,70],"span",{"class":48,"line":49},"line",1,[46,51,53],{"class":52},"sjbMc","export",[46,55,57],{"class":56},"s5-vi"," class",[46,59,61],{"class":60},"soe2T"," A11yHudElement",[46,63,65],{"class":64},"sLNfz"," extends",[46,67,69],{"class":68},"sst2a"," HTMLElement",[46,71,73],{"class":72},"s9td2"," {\n",[46,75,77,80,83],{"class":48,"line":76},2,[46,78,79],{"class":56},"  constructor",[46,81,82],{"class":72},"()",[46,84,73],{"class":72},[46,86,88,92,95],{"class":48,"line":87},3,[46,89,91],{"class":90},"s2LeO","    super",[46,93,82],{"class":94},"sLT5_",[46,96,98],{"class":97},"scI-1",";\n",[46,100,102,106,109,113,116,119,123,126,130,134,137,140,143],{"class":48,"line":101},4,[46,103,105],{"class":104},"sFcAS","    this",[46,107,108],{"class":97},".",[46,110,112],{"class":111},"stLRJ","attachShadow",[46,114,115],{"class":94},"(",[46,117,118],{"class":72},"{",[46,120,122],{"class":121},"sqlqt"," mode",[46,124,125],{"class":97},":",[46,127,129],{"class":128},"sby7X"," \"",[46,131,133],{"class":132},"smbRM","open",[46,135,136],{"class":128},"\"",[46,138,139],{"class":72}," }",[46,141,142],{"class":94},")",[46,144,98],{"class":97},[46,146,148],{"class":48,"line":147},5,[46,149,150],{"class":72},"  }\n",[46,152,154,158,160],{"class":48,"line":153},6,[46,155,157],{"class":156},"sYrai","  connectedCallback",[46,159,82],{"class":72},[46,161,73],{"class":72},[46,163,165],{"class":48,"line":164},7,[46,166,168],{"class":167},"s5acS","    \u002F\u002F sets data-theme, renders the panel, starts the scan...\n",[46,170,172],{"class":48,"line":171},8,[46,173,150],{"class":72},[46,175,177],{"class":48,"line":176},9,[46,178,179],{"class":72},"}\n",[46,181,183],{"class":48,"line":182},10,[46,184,186],{"emptyLinePlaceholder":185},true,"\n",[46,188,190,194,196,199,202,204,206,208,211,213,215],{"class":48,"line":189},11,[46,191,193],{"class":192},"swiMj","customElements",[46,195,108],{"class":97},[46,197,198],{"class":111},"define",[46,200,115],{"class":201},"sOEgH",[46,203,136],{"class":128},[46,205,22],{"class":132},[46,207,136],{"class":128},[46,209,210],{"class":97},",",[46,212,61],{"class":192},[46,214,142],{"class":201},[46,216,98],{"class":97},[10,218,219],{},"No arquivo de teste:",[37,221,223],{"className":39,"code":222,"language":41,"meta":42,"style":42},"import { A11yHudElement } from \".\u002Felement.js\";\n\nit(\"registers as a custom element\", () => {\n  expect(customElements.get(\"a11y-hud\")).toBeDefined(); \u002F\u002F undefined\n});\n",[20,224,225,250,254,278,316],{"__ignoreMap":42},[46,226,227,230,233,236,238,241,243,246,248],{"class":48,"line":49},[46,228,229],{"class":52},"import",[46,231,232],{"class":72}," {",[46,234,61],{"class":235},"sO8C4",[46,237,139],{"class":72},[46,239,240],{"class":52}," from",[46,242,129],{"class":128},[46,244,245],{"class":132},".\u002Felement.js",[46,247,136],{"class":128},[46,249,98],{"class":97},[46,251,252],{"class":48,"line":76},[46,253,186],{"emptyLinePlaceholder":185},[46,255,256,259,261,263,266,268,270,273,276],{"class":48,"line":87},[46,257,258],{"class":111},"it",[46,260,115],{"class":201},[46,262,136],{"class":128},[46,264,265],{"class":132},"registers as a custom element",[46,267,136],{"class":128},[46,269,210],{"class":97},[46,271,272],{"class":72}," ()",[46,274,275],{"class":56}," =>",[46,277,73],{"class":72},[46,279,280,283,285,287,289,292,294,296,298,300,303,305,308,310,313],{"class":48,"line":101},[46,281,282],{"class":111},"  expect",[46,284,115],{"class":94},[46,286,193],{"class":192},[46,288,108],{"class":97},[46,290,291],{"class":111},"get",[46,293,115],{"class":94},[46,295,136],{"class":128},[46,297,22],{"class":132},[46,299,136],{"class":128},[46,301,302],{"class":94},"))",[46,304,108],{"class":97},[46,306,307],{"class":111},"toBeDefined",[46,309,82],{"class":94},[46,311,312],{"class":97},";",[46,314,315],{"class":167}," \u002F\u002F undefined\n",[46,317,318,321,323],{"class":48,"line":147},[46,319,320],{"class":72},"}",[46,322,142],{"class":201},[46,324,98],{"class":97},[10,326,327,328,331,332,335,336,339],{},"Este teste falha. ",[20,329,330],{},"customElements.get(\"a11y-hud\")"," retorna ",[20,333,334],{},"undefined",", como se ",[20,337,338],{},"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.",[341,342],"hr",{},[29,344,346],{"id":345},"problema-1-o-esbuild-descarta-um-import-que-você-nunca-usa","Problema 1: o esbuild descarta um import que você nunca usa",[10,348,349,350,353],{},"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 ",[20,351,352],{},"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.",[10,355,356,357,359,360,362,363,365,366,369,370,372,373,376],{},"No teste acima, ",[20,358,352],{}," é importado mas nunca referenciado. O bloco ",[20,361,258],{}," só chama ",[20,364,330],{},". Então o esbuild trata o import como somente-tipo e o apaga. A saída é um arquivo sem import nenhum, ",[20,367,368],{},"element.ts"," nunca carrega e ",[20,371,338],{}," nunca roda. Você pode ver isso por conta própria passando o teste pelo ",[20,374,375],{},"esbuild --loader=ts",": a linha de import simplesmente some.",[10,378,379],{},"A correção que escolhi é uma linha:",[37,381,383],{"className":39,"code":382,"language":41,"meta":42,"style":42},"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",[20,384,385,405,409,422,426,446,479],{"__ignoreMap":42},[46,386,387,389,391,393,395,397,399,401,403],{"class":48,"line":49},[46,388,229],{"class":52},[46,390,232],{"class":72},[46,392,61],{"class":235},[46,394,139],{"class":72},[46,396,240],{"class":52},[46,398,129],{"class":128},[46,400,245],{"class":132},[46,402,136],{"class":128},[46,404,98],{"class":97},[46,406,407],{"class":48,"line":76},[46,408,186],{"emptyLinePlaceholder":185},[46,410,411,415,417,419],{"class":48,"line":87},[46,412,414],{"class":413},"s4Nsh","void",[46,416,61],{"class":192},[46,418,312],{"class":97},[46,420,421],{"class":167}," \u002F\u002F uses the import as a value, so esbuild keeps it\n",[46,423,424],{"class":48,"line":101},[46,425,186],{"emptyLinePlaceholder":185},[46,427,428,430,432,434,436,438,440,442,444],{"class":48,"line":147},[46,429,258],{"class":111},[46,431,115],{"class":201},[46,433,136],{"class":128},[46,435,265],{"class":132},[46,437,136],{"class":128},[46,439,210],{"class":97},[46,441,272],{"class":72},[46,443,275],{"class":56},[46,445,73],{"class":72},[46,447,448,450,452,454,456,458,460,462,464,466,468,470,472,474,476],{"class":48,"line":153},[46,449,282],{"class":111},[46,451,115],{"class":94},[46,453,193],{"class":192},[46,455,108],{"class":97},[46,457,291],{"class":111},[46,459,115],{"class":94},[46,461,136],{"class":128},[46,463,22],{"class":132},[46,465,136],{"class":128},[46,467,302],{"class":94},[46,469,108],{"class":97},[46,471,307],{"class":111},[46,473,82],{"class":94},[46,475,312],{"class":97},[46,477,478],{"class":167}," \u002F\u002F defined\n",[46,480,481,483,485],{"class":48,"line":164},[46,482,320],{"class":72},[46,484,142],{"class":201},[46,486,98],{"class":97},[10,488,489,492,493,496,497,500,501,504,505,507],{},[20,490,491],{},"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, ",[20,494,495],{},"import \".\u002Felement.js\"",", diz exatamente o que você quer dizer e não pode ser eliminado. Ou defina ",[20,498,499],{},"verbatimModuleSyntax: true"," no seu tsconfig: o esbuild respeita isso e para de descartar imports, e o TypeScript então te obriga a escrever ",[20,502,503],{},"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 ",[20,506,414],{}," é a opção pragmática.",[10,509,510],{},"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.",[341,512],{},[29,514,516],{"id":515},"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",[10,518,519,520,523,524,527,528,125],{},"Com o primeiro problema resolvido, o elemento se registrava, mas vários testes que dependiam do ",[20,521,522],{},"connectedCallback"," ainda estouravam o tempo limite. Especificamente, testes que esperavam ",[20,525,526],{},"el.dataset.theme"," ser definido ficavam parados pelos 1000 ms inteiros e falhavam. Aqui está uma versão simplificada do ",[20,529,522],{},[37,531,533],{"className":39,"code":532,"language":41,"meta":42,"style":42},"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",[20,534,535,543,560,573,590,594,598,610,645],{"__ignoreMap":42},[46,536,537,539,541],{"class":48,"line":49},[46,538,522],{"class":111},[46,540,82],{"class":201},[46,542,73],{"class":72},[46,544,545,548,550,553,555,557],{"class":48,"line":76},[46,546,547],{"class":104},"  this",[46,549,108],{"class":97},[46,551,552],{"class":111},"_applyResolvedTheme",[46,554,82],{"class":94},[46,556,312],{"class":97},[46,558,559],{"class":167}," \u002F\u002F sets dataset.theme\n",[46,561,562,564,566,569,571],{"class":48,"line":87},[46,563,547],{"class":104},[46,565,108],{"class":97},[46,567,568],{"class":111},"_render",[46,570,82],{"class":94},[46,572,98],{"class":97},[46,574,575,578,581,583,586,588],{"class":48,"line":101},[46,576,577],{"class":413},"  void",[46,579,580],{"class":104}," this",[46,582,108],{"class":97},[46,584,585],{"class":111},"_runScan",[46,587,82],{"class":94},[46,589,98],{"class":97},[46,591,592],{"class":48,"line":147},[46,593,179],{"class":72},[46,595,596],{"class":48,"line":153},[46,597,186],{"emptyLinePlaceholder":185},[46,599,600,603,606,608],{"class":48,"line":164},[46,601,602],{"class":192},"private",[46,604,605],{"class":111}," _applyResolvedTheme",[46,607,82],{"class":201},[46,609,73],{"class":72},[46,611,612,614,616,619,621,625,628,631,633,636,638,641,643],{"class":48,"line":171},[46,613,547],{"class":104},[46,615,108],{"class":97},[46,617,618],{"class":192},"dataset",[46,620,108],{"class":97},[46,622,624],{"class":623},"s-jro","theme",[46,626,627],{"class":413}," =",[46,629,630],{"class":111}," resolveTheme",[46,632,115],{"class":94},[46,634,635],{"class":104},"this",[46,637,108],{"class":97},[46,639,640],{"class":623},"_theme",[46,642,142],{"class":94},[46,644,98],{"class":97},[46,646,647],{"class":48,"line":176},[46,648,179],{"class":72},[10,650,651,652,655,656,125],{},"E ",[20,653,654],{},"resolveTheme"," chama ",[20,657,658],{},"matchMedia",[37,660,662],{"className":39,"code":661,"language":41,"meta":42,"style":42},"export function resolveTheme(theme: Theme): ResolvedTheme {\n  if (matchMedia(\"(prefers-contrast: more)\").matches) return \"high-contrast\";\n  \u002F\u002F ...\n}\n",[20,663,664,692,735,740],{"__ignoreMap":42},[46,665,666,668,671,673,675,678,680,683,685,687,690],{"class":48,"line":49},[46,667,53],{"class":52},[46,669,670],{"class":56}," function",[46,672,630],{"class":111},[46,674,115],{"class":72},[46,676,624],{"class":677},"sGWo7",[46,679,125],{"class":413},[46,681,682],{"class":60}," Theme",[46,684,142],{"class":72},[46,686,125],{"class":413},[46,688,689],{"class":60}," ResolvedTheme",[46,691,73],{"class":72},[46,693,694,698,701,703,705,707,710,712,714,716,719,722,726,728,731,733],{"class":48,"line":76},[46,695,697],{"class":696},"sNxNA","  if",[46,699,700],{"class":94}," (",[46,702,658],{"class":111},[46,704,115],{"class":94},[46,706,136],{"class":128},[46,708,709],{"class":132},"(prefers-contrast: more)",[46,711,136],{"class":128},[46,713,142],{"class":94},[46,715,108],{"class":97},[46,717,718],{"class":623},"matches",[46,720,721],{"class":94},") ",[46,723,725],{"class":724},"smhDj","return",[46,727,129],{"class":128},[46,729,730],{"class":132},"high-contrast",[46,732,136],{"class":128},[46,734,98],{"class":97},[46,736,737],{"class":48,"line":87},[46,738,739],{"class":167},"  \u002F\u002F ...\n",[46,741,742],{"class":48,"line":101},[46,743,179],{"class":72},[10,745,746,747,750,751,754,755,757,758,761],{},"O jsdom não implementa ",[20,748,749],{},"window.matchMedia",". Chamá-lo lança ",[20,752,753],{},"TypeError: matchMedia is not a function",". Então ",[20,756,552],{}," lança a exceção, ",[20,759,760],{},"dataset.theme"," nunca é definido e o teste espera até estourar o tempo.",[10,763,764,765,768,769,771,772,774,775,778],{},"A parte que torna isso difícil de depurar é onde o erro vai parar. ",[20,766,767],{},"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 ",[20,770,760],{}," ainda estar undefined, o que aponta para a lógica de tema e não para o ",[20,773,658],{},". O ",[20,776,777],{},"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.",[10,780,781,782,787,788,790,791,794],{},"Esse é o comportamento conforme a especificação. A ",[14,783,786],{"href":784,"rel":785},"https:\u002F\u002Fhtml.spec.whatwg.org\u002Fmultipage\u002Fcustom-elements.html#invoke-custom-element-reactions",[18],"especificação de Custom Elements"," diz que uma exceção lançada de um callback de ciclo de vida (",[20,789,522],{},", ",[20,792,793],{},"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.",[10,796,797,798,800],{},"A correção é fazer um stub de ",[20,799,749],{}," num arquivo de setup do Vitest, que roda uma vez antes de qualquer arquivo de teste:",[37,802,805],{"className":39,"code":803,"filename":804,"language":41,"meta":42,"style":42},"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",[20,806,807,829,855,887,901,913,926,942,957,972,987,1002,1012,1025,1037,1047],{"__ignoreMap":42},[46,808,809,812,814,818,821,823,825,827],{"class":48,"line":49},[46,810,811],{"class":696},"if",[46,813,700],{"class":201},[46,815,817],{"class":816},"sJNXy","!",[46,819,820],{"class":192},"window",[46,822,108],{"class":97},[46,824,658],{"class":623},[46,826,142],{"class":201},[46,828,73],{"class":72},[46,830,831,834,836,839,841,843,845,847,849,851,853],{"class":48,"line":76},[46,832,833],{"class":192},"  Object",[46,835,108],{"class":97},[46,837,838],{"class":111},"defineProperty",[46,840,115],{"class":94},[46,842,820],{"class":192},[46,844,210],{"class":97},[46,846,129],{"class":128},[46,848,658],{"class":132},[46,850,136],{"class":128},[46,852,210],{"class":97},[46,854,73],{"class":72},[46,856,857,860,862,864,867,869,873,875,877,880,882,884],{"class":48,"line":87},[46,858,859],{"class":111},"    value",[46,861,125],{"class":97},[46,863,700],{"class":72},[46,865,866],{"class":677},"query",[46,868,125],{"class":413},[46,870,872],{"class":871},"sMMjR"," string",[46,874,142],{"class":72},[46,876,125],{"class":413},[46,878,879],{"class":60}," MediaQueryList",[46,881,275],{"class":56},[46,883,700],{"class":94},[46,885,886],{"class":72},"{\n",[46,888,889,892,894,898],{"class":48,"line":101},[46,890,891],{"class":121},"      matches",[46,893,125],{"class":97},[46,895,897],{"class":896},"swdek"," false",[46,899,900],{"class":97},",\n",[46,902,903,906,908,911],{"class":48,"line":147},[46,904,905],{"class":121},"      media",[46,907,125],{"class":97},[46,909,910],{"class":192}," query",[46,912,900],{"class":97},[46,914,915,918,920,924],{"class":48,"line":153},[46,916,917],{"class":121},"      onchange",[46,919,125],{"class":97},[46,921,923],{"class":922},"s-rx0"," null",[46,925,900],{"class":97},[46,927,928,931,933,935,937,940],{"class":48,"line":164},[46,929,930],{"class":111},"      addListener",[46,932,125],{"class":97},[46,934,272],{"class":72},[46,936,275],{"class":56},[46,938,939],{"class":72}," {}",[46,941,900],{"class":97},[46,943,944,947,949,951,953,955],{"class":48,"line":171},[46,945,946],{"class":111},"      removeListener",[46,948,125],{"class":97},[46,950,272],{"class":72},[46,952,275],{"class":56},[46,954,939],{"class":72},[46,956,900],{"class":97},[46,958,959,962,964,966,968,970],{"class":48,"line":176},[46,960,961],{"class":111},"      addEventListener",[46,963,125],{"class":97},[46,965,272],{"class":72},[46,967,275],{"class":56},[46,969,939],{"class":72},[46,971,900],{"class":97},[46,973,974,977,979,981,983,985],{"class":48,"line":182},[46,975,976],{"class":111},"      removeEventListener",[46,978,125],{"class":97},[46,980,272],{"class":72},[46,982,275],{"class":56},[46,984,939],{"class":72},[46,986,900],{"class":97},[46,988,989,992,994,996,998,1000],{"class":48,"line":189},[46,990,991],{"class":111},"      dispatchEvent",[46,993,125],{"class":97},[46,995,272],{"class":72},[46,997,275],{"class":56},[46,999,897],{"class":896},[46,1001,900],{"class":97},[46,1003,1005,1008,1010],{"class":48,"line":1004},12,[46,1006,1007],{"class":72},"    }",[46,1009,142],{"class":94},[46,1011,900],{"class":97},[46,1013,1015,1018,1020,1023],{"class":48,"line":1014},13,[46,1016,1017],{"class":121},"    configurable",[46,1019,125],{"class":97},[46,1021,1022],{"class":896}," true",[46,1024,900],{"class":97},[46,1026,1028,1031,1033,1035],{"class":48,"line":1027},14,[46,1029,1030],{"class":121},"    writable",[46,1032,125],{"class":97},[46,1034,1022],{"class":896},[46,1036,900],{"class":97},[46,1038,1040,1043,1045],{"class":48,"line":1039},15,[46,1041,1042],{"class":72},"  }",[46,1044,142],{"class":94},[46,1046,98],{"class":97},[46,1048,1050],{"class":48,"line":1049},16,[46,1051,179],{"class":72},[10,1053,1054,1055,1058],{},"Já que você está lá, faça um stub de ",[20,1056,1057],{},"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:",[37,1060,1062],{"className":39,"code":1061,"language":41,"meta":42,"style":42},"if (!Element.prototype.scrollIntoView) {\n  Element.prototype.scrollIntoView = () => {};\n}\n",[20,1063,1064,1089,1112],{"__ignoreMap":42},[46,1065,1066,1068,1070,1072,1075,1077,1081,1083,1085,1087],{"class":48,"line":49},[46,1067,811],{"class":696},[46,1069,700],{"class":201},[46,1071,817],{"class":816},[46,1073,1074],{"class":871},"Element",[46,1076,108],{"class":97},[46,1078,1080],{"class":1079},"sjRl0","prototype",[46,1082,108],{"class":97},[46,1084,1057],{"class":623},[46,1086,142],{"class":201},[46,1088,73],{"class":72},[46,1090,1091,1094,1096,1098,1100,1102,1104,1106,1108,1110],{"class":48,"line":76},[46,1092,1093],{"class":871},"  Element",[46,1095,108],{"class":97},[46,1097,1080],{"class":1079},[46,1099,108],{"class":97},[46,1101,1057],{"class":111},[46,1103,627],{"class":413},[46,1105,272],{"class":72},[46,1107,275],{"class":56},[46,1109,939],{"class":72},[46,1111,98],{"class":97},[46,1113,1114],{"class":48,"line":87},[46,1115,179],{"class":72},[341,1117],{},[29,1119,1121],{"id":1120},"o-checklist","O checklist",[10,1123,1124],{},"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:",[1126,1127,1128,1147,1159],"ol",{},[1129,1130,1131,1135,1136,1139,1140,1143,1144,1146],"li",{},[1132,1133,1134],"strong",{},"Garanta que o módulo do elemento realmente carrega."," Ou ",[20,1137,1138],{},"void MyElement"," depois do import, ou um ",[20,1141,1142],{},"import \".\u002Fmy-element.js\""," puro, ou ",[20,1145,499],{}," no tsconfig. Sem um desses, o esbuild descarta um import que nunca vê sendo usado como valor.",[1129,1148,1149,1154,1155,1158],{},[1132,1150,1151,1152],{},"Faça um stub de ",[20,1153,749],{}," no seu ",[20,1156,1157],{},"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.",[1129,1160,1161,1154,1166,1168],{},[1132,1162,1151,1163],{},[20,1164,1165],{},"Element.prototype.scrollIntoView",[20,1167,1157],{},", pelo mesmo motivo.",[37,1170,1173],{"className":39,"code":1171,"filename":1172,"language":41,"meta":42,"style":42},"export default defineConfig({\n  test: {\n    environment: \"jsdom\",\n    setupFiles: [\"src\u002Ftest-setup.ts\"],\n  },\n});\n","vitest.config.ts",[20,1174,1175,1189,1198,1214,1235,1241],{"__ignoreMap":42},[46,1176,1177,1179,1182,1185,1187],{"class":48,"line":49},[46,1178,53],{"class":52},[46,1180,1181],{"class":52}," default",[46,1183,1184],{"class":111}," defineConfig",[46,1186,115],{"class":201},[46,1188,886],{"class":72},[46,1190,1191,1194,1196],{"class":48,"line":76},[46,1192,1193],{"class":121},"  test",[46,1195,125],{"class":97},[46,1197,73],{"class":72},[46,1199,1200,1203,1205,1207,1210,1212],{"class":48,"line":87},[46,1201,1202],{"class":121},"    environment",[46,1204,125],{"class":97},[46,1206,129],{"class":128},[46,1208,1209],{"class":132},"jsdom",[46,1211,136],{"class":128},[46,1213,900],{"class":97},[46,1215,1216,1219,1221,1224,1226,1228,1230,1233],{"class":48,"line":101},[46,1217,1218],{"class":121},"    setupFiles",[46,1220,125],{"class":97},[46,1222,1223],{"class":201}," [",[46,1225,136],{"class":128},[46,1227,804],{"class":132},[46,1229,136],{"class":128},[46,1231,1232],{"class":201},"]",[46,1234,900],{"class":97},[46,1236,1237,1239],{"class":48,"line":147},[46,1238,1042],{"class":72},[46,1240,900],{"class":97},[46,1242,1243,1245,1247],{"class":48,"line":153},[46,1244,320],{"class":72},[46,1246,142],{"class":201},[46,1248,98],{"class":97},[10,1250,1251,1252,331,1255,1257,1258,1260],{},"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: ",[20,1253,1254],{},"customElements.get(\"my-element\")",[20,1256,334],{},", ou ",[20,1259,760],{}," nunca aparece, e nada na saída ao lado do teste que falhou te diz por quê.",[1262,1263,1264],"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":42,"searchDepth":76,"depth":76,"links":1266},[1267,1268,1269,1270],{"id":31,"depth":76,"text":32},{"id":345,"depth":76,"text":346},{"id":515,"depth":76,"text":516},{"id":1120,"depth":76,"text":1121},"2026-05-03","Duas pegadinhas nada óbvias que fazem testes de Custom Elements parecerem corretos enquanto estão completamente quebrados, e como corrigi-las.",false,"md",{},"\u002Fpt-br\u002Fblog\u002Fcustom-element-vitest-silent-failures",{"title":5,"description":1272},"pt-br\u002Fblog\u002Fcustom-element-vitest-silent-failures","custom-element-vitest-silent-failures","EKolbXikAeOrn10hVLBFVPQr7AfdANkUIMNgYcc_qZ0",{"left":1282,"top":1282,"width":1283,"height":1283,"rotate":1282,"vFlip":1273,"hFlip":1273,"body":1284},0,24,"\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":1282,"top":1282,"width":1283,"height":1283,"rotate":1282,"vFlip":1273,"hFlip":1273,"body":1286},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M7 7h10v10M7 17L17 7\"\u002F>",{"left":1282,"top":1282,"width":1283,"height":1283,"rotate":1282,"vFlip":1273,"hFlip":1273,"body":1288},"\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>",[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":1282,"top":1282,"width":1283,"height":1283,"rotate":1282,"vFlip":1273,"hFlip":1273,"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":1282,"top":1282,"width":1283,"height":1283,"rotate":1282,"vFlip":1273,"hFlip":1273,"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":1282,"top":1282,"width":1283,"height":1283,"rotate":1282,"vFlip":1273,"hFlip":1273,"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":1282,"top":1282,"width":1304,"height":1304,"rotate":1282,"vFlip":1273,"hFlip":1273,"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":1282,"top":1282,"width":1283,"height":1283,"rotate":1282,"vFlip":1273,"hFlip":1273,"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>",1792626870820]