[{"data":1,"prerenderedAt":1308},["ShallowReactive",2],{"i-simple-icons:github":3,"i-lucide:arrow-up-right":8,"i-lucide:loader":10,"post-sv-custom-element-vitest-silent-failures":12,"translations-blog-sv-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\u002Fsv\u002Fblog\u002Fcustom-element-vitest-silent-failures.md","Varför dina Custom Element-tester misslyckas tyst i 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",{},"Jag byggde nyligen ",[23,24,28],"a",{"href":25,"rel":26},"https:\u002F\u002Fgithub.com\u002Farthurvasconcelos\u002Fa11y-hud",[27],"nofollow",[29,30,31],"code",{},"a11y-hud",", ett ramverksoberoende tillgänglighetsoverlay som kör axe-core-granskningar inuti en körande webbapp. Kärnan är ett Custom Element, ",[29,34,35],{},"\u003Ca11y-hud>",", med Shadow DOM. Att skriva enhetstester för det i Vitest med jsdom visade sig vara mer intressant än jag väntat mig, och inte på ett bra sätt. Det här är de två felen som kostade mig mest tid.",[38,39,41],"h2",{"id":40},"uppsättningen","Uppsättningen",[19,43,44],{},"Elementet registrerar sig själv som en sidoeffekt när dess modul laddas:",[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],{},"I testfilen:",[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],{},"Det här testet misslyckas. ",[29,338,339],{},"customElements.get(\"a11y-hud\")"," returnerar ",[29,342,343],{},"undefined",", som om ",[29,346,347],{},"customElements.define"," aldrig anropats. Inget importfel, inget modulfel, ingenting i konsolen. Importraden står ju där. Define-anropet skedde bara aldrig.",[350,351],"hr",{},[38,353,355],{"id":354},"problem-1-esbuild-tar-bort-en-import-du-aldrig-använder","Problem 1: esbuild tar bort en import du aldrig använder",[19,357,358,359,362],{},"Vitest lämnar TypeScript-filer till esbuild, som transpilerar dem en fil i taget utan typinformation. Det spelar roll här, eftersom esbuild inte har någon möjlighet att veta om ",[29,360,361],{},"A11yHudElement"," är en klass eller ett interface. TypeScripts egen regel är att en import som bara används som typ raderas ur utdata, och esbuild kopierar den regeln på det enda sätt det kan: om ett importerat namn aldrig används som ett värde i filen tas hela importen bort.",[19,364,365,366,368,369,371,372,374,375,378,379,381,382,385],{},"I testet ovan importeras ",[29,367,361],{}," men refereras aldrig. ",[29,370,267],{},"-blocket anropar bara ",[29,373,339],{},". Så esbuild behandlar importen som typ-only och raderar den. Resultatet är en fil utan någon import alls, ",[29,376,377],{},"element.ts"," laddas aldrig och ",[29,380,347],{}," körs aldrig. Du kan se det själv genom att köra testet genom ",[29,383,384],{},"esbuild --loader=ts",": importraden är helt enkelt borta.",[19,387,388],{},"Lösningen jag valde är en rad:",[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"," evaluerar uttrycket och slänger resultatet, vilket räcker för att göra importen till en värdeimport. Det finns två renare alternativ, beroende på smak. En ren sidoeffektsimport, ",[29,503,504],{},"import \".\u002Felement.js\"",", säger exakt vad du menar och kan inte elimineras. Eller sätt ",[29,507,508],{},"verbatimModuleSyntax: true"," i din tsconfig: esbuild respekterar det och slutar ta bort importer, och TypeScript tvingar dig då att skriva ",[29,511,512],{},"import type"," för de som bara är typer. Det skulle jag välja i ett nytt projekt. I ett befintligt projekt med många blandade importer är ",[29,515,423],{},"-raden det pragmatiska valet.",[19,518,519],{},"Det här biter inte i produktionsbygget, eftersom ingenting där importerar klassen utan att använda den. Det dyker bara upp i tester, där det är helt normalt att importera en modul enbart för dess registreringssidoeffekt.",[350,521],{},[38,523,525],{"id":524},"problem-2-jsdom-rapporterar-livscykelfel-någonstans-där-du-inte-tittar","Problem 2: jsdom rapporterar livscykelfel någonstans där du inte tittar",[19,527,528,529,532,533,536,537,134],{},"Med det första problemet löst registrerades elementet, men flera tester som berodde på ",[29,530,531],{},"connectedCallback"," fick fortfarande timeout. Närmare bestämt satt tester som väntade på att ",[29,534,535],{},"el.dataset.theme"," skulle sättas kvar hela 1000 ms och misslyckades. Här är en förenklad version av ",[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],{},"Och ",[29,662,663],{},"resolveTheme"," anropar ",[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],{},"jsdom implementerar inte ",[29,757,758],{},"window.matchMedia",". Att anropa det kastar ",[29,761,762],{},"TypeError: matchMedia is not a function",". Så ",[29,765,561],{}," kastar, ",[29,768,769],{},"dataset.theme"," sätts aldrig och testet väntar tills det får timeout.",[19,772,773,774,777,778,780,781,783,784,787],{},"Det som gör det här svårt att felsöka är var felet hamnar. ",[29,775,776],{},"document.body.appendChild(el)"," kastar inte. Testet misslyckas inte på raden som orsakade problemet. Det misslyckas på en timeout, med ett meddelande om att ",[29,779,769],{}," fortfarande är undefined, vilket pekar dig mot temalogiken snarare än mot ",[29,782,667],{},". ",[29,785,786],{},"TypeError","-felet skrivs visserligen ut, men av Vitest, som ett \"Unhandled Error\"-block efter hela körningen, utan någon koppling tillbaka till testet som fick timeout. Och om testfilen du tittar på råkar inte göra några assertions på något nedströms från kastet, så passerar den, med felet liggande längst ner i utdata som en vag varning om falska positiver.",[19,789,790,791,796,797,799,800,803],{},"Det här är spec-enligt beteende. ",[23,792,795],{"href":793,"rel":794},"https:\u002F\u002Fhtml.spec.whatwg.org\u002Fmultipage\u002Fcustom-elements.html#invoke-custom-element-reactions",[27],"Custom Elements-specifikationen"," säger att ett undantag som kastas från en livscykel-callback (",[29,798,531],{},", ",[29,801,802],{},"attributeChangedCallback"," och så vidare) ska rapporteras, inte propageras till det som utlöste det. jsdom gör exakt det: det fångar felet och rapporterar det på samma sätt som en webbläsare rapporterar ett ofångat undantag från en händelsehanterare. I en webbläsare är det rätt beslut, eftersom ett trasigt element inte ska ta ner hela sidan. I ett test innebär det att stacktracen och den misslyckade assertionen är två olika saker på två olika ställen.",[19,805,806,807,809],{},"Lösningen är att stubba ",[29,808,758],{}," i en Vitest-setupfil, som körs en gång före alla testfiler:",[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],{},"När du ändå är där, stubba även ",[29,1065,1066],{},"scrollIntoView",". jsdom implementerar inte heller den, och den dyker upp som samma sorts frikopplade ohanterade fel så fort ett test går igenom ett highlight- eller scrollflöde:",[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},"checklistan","Checklistan",[19,1132,1133],{},"Om du testar Custom Elements i Vitest med jsdom, gör de här tre sakerna från början och hoppa över felsökningssessionen:",[1135,1136,1137,1156,1168],"ol",{},[1138,1139,1140,1144,1145,1148,1149,1152,1153,1155],"li",{},[1141,1142,1143],"strong",{},"Se till att elementmodulen faktiskt laddas."," Antingen ",[29,1146,1147],{},"void MyElement"," efter importen, en ren ",[29,1150,1151],{},"import \".\u002Fmy-element.js\""," eller ",[29,1154,508],{}," i tsconfig. Utan något av dessa tar esbuild bort en import som det aldrig ser användas som värde.",[1138,1157,1158,1163,1164,1167],{},[1141,1159,1160,1161],{},"Stubba ",[29,1162,758],{}," i dina ",[29,1165,1166],{},"setupFiles",". Varje livscykel-callback som rör media queries kommer att kasta, och kastet landar inte i testet som beror på den.",[1138,1169,1170,1163,1175,1177],{},[1141,1171,1160,1172],{},[29,1173,1174],{},"Element.prototype.scrollIntoView",[29,1176,1166],{},", av samma anledning.",[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],{},"De två problemen förstärker varandra. Att esbuild tar bort en import lämnar inga spår alls, och att jsdom rapporterar livscykelfel vid sidan av lämnar ett spår som inte pekar på testet som gick sönder. När du väl känner till båda felsätten är felsökningsvägen kort. Innan du känner till dem ser båda likadana ut utifrån: ",[29,1262,1263],{},"customElements.get(\"my-element\")",[29,1265,343],{},", eller ",[29,1268,769],{}," dyker aldrig upp, och ingenting i utdata bredvid det misslyckade testet berättar varför.",[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","Två icke-uppenbara fallgropar som får Custom Element-tester att se korrekta ut medan de är helt trasiga, och hur man åtgärdar dem.","md",{},"\u002Fsv\u002Fblog\u002Fcustom-element-vitest-silent-failures",{"title":14,"description":1281},"sv\u002Fblog\u002Fcustom-element-vitest-silent-failures","custom-element-vitest-silent-failures","ugcu3t18g5pcOFyL7wsN8B2KaDFq2Bmop3b_l-i_oUw",[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},"\u002Fpt-br\u002Fblog\u002Fcustom-element-vitest-silent-failures","Português",{"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>",1792918925072]