[{"data":1,"prerenderedAt":1265},["ShallowReactive",2],{"i-simple-icons:github":3,"i-lucide:arrow-up-right":8,"i-lucide:loader":10,"post-en-gb-custom-element-vitest-silent-failures":12,"translations-custom-element-vitest-silent-failures":1262,"i-material-symbols:arrow-back":1263},{"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":1253,"description":1254,"draft":6,"extension":1255,"meta":1256,"navigation":185,"path":1257,"seo":1258,"stem":1259,"translationKey":1260,"__hash__":1261},"blog\u002Fen-gb\u002Fblog\u002Fcustom-element-vitest-silent-failures.md","Why your Custom Element tests fail silently in Vitest",{"type":16,"value":17,"toc":1247},"minimark",[18,37,42,45,218,221,326,341,344,348,351,369,375,480,486,493,495,499,512,631,641,725,743,754,777,780,793,1043,1050,1107,1109,1113,1116,1151,1234,1243],[19,20,21,22,32,33,36],"p",{},"I recently built ",[23,24,28],"a",{"href":25,"rel":26},"https:\u002F\u002Fgithub.com\u002Farthurvasconcelos\u002Fa11y-hud",[27],"nofollow",[29,30,31],"code",{},"a11y-hud",", a framework-agnostic accessibility overlay that runs axe-core audits inside a running web app. The core of it is a ",[29,34,35],{},"\u003Ca11y-hud>"," Custom Element with a Shadow DOM. Writing unit tests for it in Vitest with jsdom turned out to be much more interesting than I expected — in the bad way. Here are the two failures that cost me the most time.",[38,39,41],"h2",{"id":40},"the-setup","The setup",[19,43,44],{},"The element registers itself as a side effect when its module loads:",[46,47,52],"pre",{"className":48,"code":49,"language":50,"meta":51,"style":51},"language-typescript shiki shiki-themes 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,107,146,152,162,169,174,180,187],{"__ignoreMap":51},[55,56,59,63,67,71,75,79],"span",{"class":57,"line":58},"line",1,[55,60,62],{"class":61},"sthd0","export",[55,64,66],{"class":65},"stqjZ"," class",[55,68,70],{"class":69},"seCP5"," A11yHudElement",[55,72,74],{"class":73},"sIYdW"," extends",[55,76,78],{"class":77},"s0EQU"," HTMLElement",[55,80,82],{"class":81},"svadF"," {\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,103],{"class":57,"line":96},3,[55,98,100],{"class":99},"shyUM","    super",[55,102,91],{"class":81},[55,104,106],{"class":105},"sBGXs",";\n",[55,108,110,113,116,120,123,127,130,134,138,141,144],{"class":57,"line":109},4,[55,111,112],{"class":99},"    this",[55,114,115],{"class":105},".",[55,117,119],{"class":118},"sP8dw","attachShadow",[55,121,122],{"class":81},"({ ",[55,124,126],{"class":125},"sUesm","mode",[55,128,129],{"class":105},":",[55,131,133],{"class":132},"szk3v"," \"",[55,135,137],{"class":136},"sHZvL","open",[55,139,140],{"class":132},"\"",[55,142,143],{"class":81}," })",[55,145,106],{"class":105},[55,147,149],{"class":57,"line":148},5,[55,150,151],{"class":81},"  }\n",[55,153,155,158,160],{"class":57,"line":154},6,[55,156,157],{"class":118},"  connectedCallback",[55,159,91],{"class":81},[55,161,82],{"class":81},[55,163,165],{"class":57,"line":164},7,[55,166,168],{"class":167},"sxfWu","    \u002F\u002F sets data-theme, renders the panel, starts the scan...\n",[55,170,172],{"class":57,"line":171},8,[55,173,151],{"class":81},[55,175,177],{"class":57,"line":176},9,[55,178,179],{"class":81},"}\n",[55,181,183],{"class":57,"line":182},10,[55,184,186],{"emptyLinePlaceholder":185},true,"\n",[55,188,190,194,196,199,202,204,206,208,211,213,216],{"class":57,"line":189},11,[55,191,193],{"class":192},"sNbpT","customElements",[55,195,115],{"class":105},[55,197,198],{"class":118},"define",[55,200,201],{"class":81},"(",[55,203,140],{"class":132},[55,205,31],{"class":136},[55,207,140],{"class":132},[55,209,210],{"class":105},",",[55,212,70],{"class":192},[55,214,215],{"class":81},")",[55,217,106],{"class":105},[19,219,220],{},"In the test file:",[46,222,224],{"className":48,"code":223,"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,225,226,253,257,281,319],{"__ignoreMap":51},[55,227,228,231,234,238,241,244,246,249,251],{"class":57,"line":58},[55,229,230],{"class":61},"import",[55,232,233],{"class":81}," { ",[55,235,237],{"class":236},"s-zMA","A11yHudElement",[55,239,240],{"class":81}," }",[55,242,243],{"class":61}," from",[55,245,133],{"class":132},[55,247,248],{"class":136},".\u002Felement.js",[55,250,140],{"class":132},[55,252,106],{"class":105},[55,254,255],{"class":57,"line":85},[55,256,186],{"emptyLinePlaceholder":185},[55,258,259,262,264,266,269,271,273,276,279],{"class":57,"line":96},[55,260,261],{"class":118},"it",[55,263,201],{"class":81},[55,265,140],{"class":132},[55,267,268],{"class":136},"registers as a custom element",[55,270,140],{"class":132},[55,272,210],{"class":105},[55,274,275],{"class":81}," ()",[55,277,278],{"class":65}," =>",[55,280,82],{"class":81},[55,282,283,286,288,290,292,295,297,299,301,303,306,308,311,313,316],{"class":57,"line":109},[55,284,285],{"class":118},"  expect",[55,287,201],{"class":81},[55,289,193],{"class":192},[55,291,115],{"class":105},[55,293,294],{"class":118},"get",[55,296,201],{"class":81},[55,298,140],{"class":132},[55,300,31],{"class":136},[55,302,140],{"class":132},[55,304,305],{"class":81},"))",[55,307,115],{"class":105},[55,309,310],{"class":118},"toBeDefined",[55,312,91],{"class":81},[55,314,315],{"class":105},";",[55,317,318],{"class":167}," \u002F\u002F ❌ undefined\n",[55,320,321,324],{"class":57,"line":148},[55,322,323],{"class":81},"})",[55,325,106],{"class":105},[19,327,328,329,332,333,336,337,340],{},"This test fails. ",[29,330,331],{},"customElements.get(\"a11y-hud\")"," returns ",[29,334,335],{},"undefined"," — as if ",[29,338,339],{},"customElements.define"," was never called. But there's no import error, no module error, nothing in the console. The import succeeded. The define call just… didn't happen.",[342,343],"hr",{},[38,345,347],{"id":346},"problem-1-esbuild-defers-module-execution-when-you-dont-use-the-export","Problem 1: esbuild defers module execution when you don't use the export",[19,349,350],{},"Vitest uses esbuild to transform TypeScript. When it sees a named export that is imported but never referenced in the test body, it can skip executing that module's side effects entirely.",[19,352,353,354,356,357,359,360,362,363,365,366,368],{},"In the test above, ",[29,355,237],{}," is imported but never used. The ",[29,358,261],{}," block only calls ",[29,361,331],{}," — it doesn't touch ",[29,364,237],{}," directly. From esbuild's perspective, the import is dead code. The module is never executed. ",[29,367,339],{}," never runs.",[19,370,371],{},[372,373,374],"strong",{},"The fix is one line:",[46,376,378],{"className":48,"code":377,"language":50,"meta":51,"style":51},"import { A11yHudElement } from \".\u002Felement.js\";\n\nvoid A11yHudElement; \u002F\u002F forces the module to execute\n\nit(\"registers as a custom element\", () => {\n  expect(customElements.get(\"a11y-hud\")).toBeDefined(); \u002F\u002F ✅\n});\n",[29,379,380,400,404,417,421,441,474],{"__ignoreMap":51},[55,381,382,384,386,388,390,392,394,396,398],{"class":57,"line":58},[55,383,230],{"class":61},[55,385,233],{"class":81},[55,387,237],{"class":236},[55,389,240],{"class":81},[55,391,243],{"class":61},[55,393,133],{"class":132},[55,395,248],{"class":136},[55,397,140],{"class":132},[55,399,106],{"class":105},[55,401,402],{"class":57,"line":85},[55,403,186],{"emptyLinePlaceholder":185},[55,405,406,410,412,414],{"class":57,"line":96},[55,407,409],{"class":408},"solFm","void",[55,411,70],{"class":192},[55,413,315],{"class":105},[55,415,416],{"class":167}," \u002F\u002F forces the module to execute\n",[55,418,419],{"class":57,"line":109},[55,420,186],{"emptyLinePlaceholder":185},[55,422,423,425,427,429,431,433,435,437,439],{"class":57,"line":148},[55,424,261],{"class":118},[55,426,201],{"class":81},[55,428,140],{"class":132},[55,430,268],{"class":136},[55,432,140],{"class":132},[55,434,210],{"class":105},[55,436,275],{"class":81},[55,438,278],{"class":65},[55,440,82],{"class":81},[55,442,443,445,447,449,451,453,455,457,459,461,463,465,467,469,471],{"class":57,"line":154},[55,444,285],{"class":118},[55,446,201],{"class":81},[55,448,193],{"class":192},[55,450,115],{"class":105},[55,452,294],{"class":118},[55,454,201],{"class":81},[55,456,140],{"class":132},[55,458,31],{"class":136},[55,460,140],{"class":132},[55,462,305],{"class":81},[55,464,115],{"class":105},[55,466,310],{"class":118},[55,468,91],{"class":81},[55,470,315],{"class":105},[55,472,473],{"class":167}," \u002F\u002F ✅\n",[55,475,476,478],{"class":57,"line":164},[55,477,323],{"class":81},[55,479,106],{"class":105},[19,481,482,485],{},[29,483,484],{},"void expr"," evaluates the expression and discards the result. It's enough to tell esbuild that the import is live, so the module executes, the side effect runs, and the element is registered.",[19,487,488,489,492],{},"This only happens in Vitest's test environment. At build time, tsup's ",[29,490,491],{},"text"," loader and bundler configuration treat the module correctly. It's purely a Vitest\u002Fesbuild interaction.",[342,494],{},[38,496,498],{"id":497},"problem-2-jsdom-silently-swallows-lifecycle-callback-errors","Problem 2: jsdom silently swallows lifecycle callback errors",[19,500,501,502,505,506,509,510,129],{},"With the first problem fixed, the element registered — but several tests that relied on ",[29,503,504],{},"connectedCallback"," behaviour were still timing out. Specifically, tests that waited for ",[29,507,508],{},"el.dataset.theme"," to be defined would sit there for the full 1000 ms and fail. Here's a simplified version of ",[29,511,504],{},[46,513,515],{"className":48,"code":514,"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,516,517,525,542,555,572,576,580,592,627],{"__ignoreMap":51},[55,518,519,521,523],{"class":57,"line":58},[55,520,504],{"class":118},[55,522,91],{"class":81},[55,524,82],{"class":81},[55,526,527,530,532,535,537,539],{"class":57,"line":85},[55,528,529],{"class":99},"  this",[55,531,115],{"class":105},[55,533,534],{"class":118},"_applyResolvedTheme",[55,536,91],{"class":81},[55,538,315],{"class":105},[55,540,541],{"class":167}," \u002F\u002F sets dataset.theme\n",[55,543,544,546,548,551,553],{"class":57,"line":96},[55,545,529],{"class":99},[55,547,115],{"class":105},[55,549,550],{"class":118},"_render",[55,552,91],{"class":81},[55,554,106],{"class":105},[55,556,557,560,563,565,568,570],{"class":57,"line":109},[55,558,559],{"class":408},"  void",[55,561,562],{"class":99}," this",[55,564,115],{"class":105},[55,566,567],{"class":118},"_runScan",[55,569,91],{"class":81},[55,571,106],{"class":105},[55,573,574],{"class":57,"line":148},[55,575,179],{"class":81},[55,577,578],{"class":57,"line":154},[55,579,186],{"emptyLinePlaceholder":185},[55,581,582,585,588,590],{"class":57,"line":164},[55,583,584],{"class":192},"private",[55,586,587],{"class":118}," _applyResolvedTheme",[55,589,91],{"class":81},[55,591,82],{"class":81},[55,593,594,596,598,601,603,607,610,613,615,618,620,623,625],{"class":57,"line":171},[55,595,529],{"class":99},[55,597,115],{"class":105},[55,599,600],{"class":192},"dataset",[55,602,115],{"class":105},[55,604,606],{"class":605},"sZREb","theme",[55,608,609],{"class":408}," =",[55,611,612],{"class":118}," resolveTheme",[55,614,201],{"class":81},[55,616,617],{"class":99},"this",[55,619,115],{"class":105},[55,621,622],{"class":605},"_theme",[55,624,215],{"class":81},[55,626,106],{"class":105},[55,628,629],{"class":57,"line":176},[55,630,179],{"class":81},[19,632,633,634,637,638,129],{},"And ",[29,635,636],{},"resolveTheme"," calls ",[29,639,640],{},"matchMedia",[46,642,644],{"className":48,"code":643,"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,645,646,674,716,721],{"__ignoreMap":51},[55,647,648,650,653,655,657,660,662,665,667,669,672],{"class":57,"line":58},[55,649,62],{"class":61},[55,651,652],{"class":65}," function",[55,654,612],{"class":118},[55,656,201],{"class":81},[55,658,606],{"class":659},"sfwPi",[55,661,129],{"class":408},[55,663,664],{"class":69}," Theme",[55,666,215],{"class":81},[55,668,129],{"class":408},[55,670,671],{"class":69}," ResolvedTheme",[55,673,82],{"class":81},[55,675,676,679,682,684,686,688,691,693,695,697,700,703,707,709,712,714],{"class":57,"line":85},[55,677,678],{"class":65},"  if",[55,680,681],{"class":81}," (",[55,683,640],{"class":118},[55,685,201],{"class":81},[55,687,140],{"class":132},[55,689,690],{"class":136},"(prefers-contrast: more)",[55,692,140],{"class":132},[55,694,215],{"class":81},[55,696,115],{"class":105},[55,698,699],{"class":605},"matches",[55,701,702],{"class":81},") ",[55,704,706],{"class":705},"sEVbI","return",[55,708,133],{"class":132},[55,710,711],{"class":136},"high-contrast",[55,713,140],{"class":132},[55,715,106],{"class":105},[55,717,718],{"class":57,"line":96},[55,719,720],{"class":167},"  \u002F\u002F ...\n",[55,722,723],{"class":57,"line":109},[55,724,179],{"class":81},[19,726,727,728,731,732,735,736,738,739,742],{},"jsdom does not implement ",[29,729,730],{},"window.matchMedia",". Calling it throws a ",[29,733,734],{},"TypeError: matchMedia is not a function",". So ",[29,737,534],{}," throws, ",[29,740,741],{},"dataset.theme"," is never set, and the test waits forever.",[19,744,745,746,749,750,753],{},"But here's the subtle part: ",[372,747,748],{},"there is no error in the test output",". No ",[29,751,752],{},"TypeError",", no stack trace, nothing. The test just times out.",[19,755,756,757,762,763,765,766,769,770,773,774,776],{},"The reason is that this is actually spec-compliant behaviour. The ",[23,758,761],{"href":759,"rel":760},"https:\u002F\u002Fhtml.spec.whatwg.org\u002Fmultipage\u002Fcustom-elements.html#concept-upgrade-an-element",[27],"Custom Elements spec"," requires that errors thrown inside lifecycle callbacks (",[29,764,504],{},", ",[29,767,768],{},"attributeChangedCallback",", etc.) are caught and reported as uncaught exceptions rather than propagated to the caller. jsdom follows this spec, which means ",[29,771,772],{},"document.body.appendChild(el)"," does not throw even when ",[29,775,504],{}," throws internally. The error is swallowed.",[19,778,779],{},"This is the right behaviour in a browser — you don't want a single misbehaving element to bring down the whole page. But in tests, it makes Custom Element debugging surprisingly hard.",[19,781,782,785,786,788,789,792],{},[372,783,784],{},"The fix:"," stub ",[29,787,730],{}," in the test setup file. Since Vitest supports a ",[29,790,791],{},"setupFiles"," option, this runs once before any tests:",[46,794,796],{"className":48,"code":795,"language":50,"meta":51,"style":51},"\u002F\u002F src\u002Ftest-setup.ts\nif (!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",[29,797,798,803,824,850,880,894,906,918,936,951,966,981,997,1005,1018,1030,1038],{"__ignoreMap":51},[55,799,800],{"class":57,"line":58},[55,801,802],{"class":167},"\u002F\u002F src\u002Ftest-setup.ts\n",[55,804,805,808,810,813,816,818,820,822],{"class":57,"line":85},[55,806,807],{"class":65},"if",[55,809,681],{"class":81},[55,811,812],{"class":65},"!",[55,814,815],{"class":192},"window",[55,817,115],{"class":105},[55,819,640],{"class":605},[55,821,215],{"class":81},[55,823,82],{"class":81},[55,825,826,829,831,834,836,838,840,842,844,846,848],{"class":57,"line":96},[55,827,828],{"class":192},"  Object",[55,830,115],{"class":105},[55,832,833],{"class":118},"defineProperty",[55,835,201],{"class":81},[55,837,815],{"class":192},[55,839,210],{"class":105},[55,841,133],{"class":132},[55,843,640],{"class":136},[55,845,140],{"class":132},[55,847,210],{"class":105},[55,849,82],{"class":81},[55,851,852,855,857,859,862,864,868,870,872,875,877],{"class":57,"line":109},[55,853,854],{"class":118},"    value",[55,856,129],{"class":105},[55,858,681],{"class":81},[55,860,861],{"class":659},"query",[55,863,129],{"class":408},[55,865,867],{"class":866},"sdEiP"," string",[55,869,215],{"class":81},[55,871,129],{"class":408},[55,873,874],{"class":69}," MediaQueryList",[55,876,278],{"class":65},[55,878,879],{"class":81}," ({\n",[55,881,882,885,887,891],{"class":57,"line":148},[55,883,884],{"class":125},"      matches",[55,886,129],{"class":105},[55,888,890],{"class":889},"sDiRO"," false",[55,892,893],{"class":105},",\n",[55,895,896,899,901,904],{"class":57,"line":154},[55,897,898],{"class":125},"      media",[55,900,129],{"class":105},[55,902,903],{"class":192}," query",[55,905,893],{"class":105},[55,907,908,911,913,916],{"class":57,"line":164},[55,909,910],{"class":125},"      onchange",[55,912,129],{"class":105},[55,914,915],{"class":889}," null",[55,917,893],{"class":105},[55,919,920,923,925,928,931,934],{"class":57,"line":171},[55,921,922],{"class":118},"      addListener",[55,924,129],{"class":105},[55,926,927],{"class":81}," () ",[55,929,930],{"class":65},"=>",[55,932,933],{"class":81}," {}",[55,935,893],{"class":105},[55,937,938,941,943,945,947,949],{"class":57,"line":176},[55,939,940],{"class":118},"      removeListener",[55,942,129],{"class":105},[55,944,927],{"class":81},[55,946,930],{"class":65},[55,948,933],{"class":81},[55,950,893],{"class":105},[55,952,953,956,958,960,962,964],{"class":57,"line":182},[55,954,955],{"class":118},"      addEventListener",[55,957,129],{"class":105},[55,959,927],{"class":81},[55,961,930],{"class":65},[55,963,933],{"class":81},[55,965,893],{"class":105},[55,967,968,971,973,975,977,979],{"class":57,"line":189},[55,969,970],{"class":118},"      removeEventListener",[55,972,129],{"class":105},[55,974,927],{"class":81},[55,976,930],{"class":65},[55,978,933],{"class":81},[55,980,893],{"class":105},[55,982,984,987,989,991,993,995],{"class":57,"line":983},12,[55,985,986],{"class":118},"      dispatchEvent",[55,988,129],{"class":105},[55,990,927],{"class":81},[55,992,930],{"class":65},[55,994,890],{"class":889},[55,996,893],{"class":105},[55,998,1000,1003],{"class":57,"line":999},13,[55,1001,1002],{"class":81},"    })",[55,1004,893],{"class":105},[55,1006,1008,1011,1013,1016],{"class":57,"line":1007},14,[55,1009,1010],{"class":125},"    configurable",[55,1012,129],{"class":105},[55,1014,1015],{"class":889}," true",[55,1017,893],{"class":105},[55,1019,1021,1024,1026,1028],{"class":57,"line":1020},15,[55,1022,1023],{"class":125},"    writable",[55,1025,129],{"class":105},[55,1027,1015],{"class":889},[55,1029,893],{"class":105},[55,1031,1033,1036],{"class":57,"line":1032},16,[55,1034,1035],{"class":81},"  })",[55,1037,106],{"class":105},[55,1039,1041],{"class":57,"line":1040},17,[55,1042,179],{"class":81},[19,1044,1045,1046,1049],{},"While you're in the setup file, also stub ",[29,1047,1048],{},"scrollIntoView"," — jsdom doesn't implement that either, and it will surface as an unhandled error the moment any test exercises a highlight\u002Fscroll flow:",[46,1051,1053],{"className":48,"code":1052,"language":50,"meta":51,"style":51},"if (!Element.prototype.scrollIntoView) {\n  Element.prototype.scrollIntoView = () => {};\n}\n",[29,1054,1055,1080,1103],{"__ignoreMap":51},[55,1056,1057,1059,1061,1063,1066,1068,1072,1074,1076,1078],{"class":57,"line":58},[55,1058,807],{"class":65},[55,1060,681],{"class":81},[55,1062,812],{"class":65},[55,1064,1065],{"class":866},"Element",[55,1067,115],{"class":105},[55,1069,1071],{"class":1070},"szlED","prototype",[55,1073,115],{"class":105},[55,1075,1048],{"class":605},[55,1077,215],{"class":81},[55,1079,82],{"class":81},[55,1081,1082,1085,1087,1089,1091,1093,1095,1097,1099,1101],{"class":57,"line":85},[55,1083,1084],{"class":866},"  Element",[55,1086,115],{"class":105},[55,1088,1071],{"class":1070},[55,1090,115],{"class":105},[55,1092,1048],{"class":118},[55,1094,609],{"class":408},[55,1096,927],{"class":81},[55,1098,930],{"class":65},[55,1100,933],{"class":81},[55,1102,106],{"class":105},[55,1104,1105],{"class":57,"line":96},[55,1106,179],{"class":81},[342,1108],{},[38,1110,1112],{"id":1111},"the-checklist","The checklist",[19,1114,1115],{},"If you're testing Custom Elements in Vitest with jsdom, do these three things upfront and save yourself the debugging session:",[1117,1118,1119,1130,1141],"ol",{},[1120,1121,1122,1125,1126,1129],"li",{},[372,1123,1124],{},"Access every imported element class"," with ",[29,1127,1128],{},"void MyElement"," at the top of each test file that imports it. Without this, esbuild may not execute the module.",[1120,1131,1132,1137,1138,1140],{},[372,1133,1134,1135],{},"Stub ",[29,1136,730],{}," in your ",[29,1139,791],{},". Any lifecycle callback that touches media queries will throw silently if this isn't present.",[1120,1142,1143,1137,1148,1150],{},[372,1144,1134,1145],{},[29,1146,1147],{},"Element.prototype.scrollIntoView",[29,1149,791],{},". jsdom doesn't implement it and the error will appear at an inconvenient time.",[46,1152,1154],{"className":48,"code":1153,"language":50,"meta":51,"style":51},"\u002F\u002F vitest.config.ts\nexport default defineConfig({\n  test: {\n    environment: \"jsdom\",\n    setupFiles: [\"src\u002Ftest-setup.ts\"],\n  },\n});\n",[29,1155,1156,1161,1174,1183,1199,1221,1228],{"__ignoreMap":51},[55,1157,1158],{"class":57,"line":58},[55,1159,1160],{"class":167},"\u002F\u002F vitest.config.ts\n",[55,1162,1163,1165,1168,1171],{"class":57,"line":85},[55,1164,62],{"class":61},[55,1166,1167],{"class":61}," default",[55,1169,1170],{"class":118}," defineConfig",[55,1172,1173],{"class":81},"({\n",[55,1175,1176,1179,1181],{"class":57,"line":96},[55,1177,1178],{"class":125},"  test",[55,1180,129],{"class":105},[55,1182,82],{"class":81},[55,1184,1185,1188,1190,1192,1195,1197],{"class":57,"line":109},[55,1186,1187],{"class":125},"    environment",[55,1189,129],{"class":105},[55,1191,133],{"class":132},[55,1193,1194],{"class":136},"jsdom",[55,1196,140],{"class":132},[55,1198,893],{"class":105},[55,1200,1201,1204,1206,1209,1211,1214,1216,1219],{"class":57,"line":148},[55,1202,1203],{"class":125},"    setupFiles",[55,1205,129],{"class":105},[55,1207,1208],{"class":81}," [",[55,1210,140],{"class":132},[55,1212,1213],{"class":136},"src\u002Ftest-setup.ts",[55,1215,140],{"class":132},[55,1217,1218],{"class":81},"]",[55,1220,893],{"class":105},[55,1222,1223,1226],{"class":57,"line":154},[55,1224,1225],{"class":81},"  }",[55,1227,893],{"class":105},[55,1229,1230,1232],{"class":57,"line":164},[55,1231,323],{"class":81},[55,1233,106],{"class":105},[19,1235,1236,1237,332,1240,1242],{},"The combination of esbuild deferring execution and jsdom silently catching lifecycle errors makes Custom Element test failures look completely different from what they actually are. Once you know these two failure modes, the debugging path is straightforward — but before you know them, both failures look like the same thing: ",[29,1238,1239],{},"customElements.get(\"my-element\")",[29,1241,335],{}," and nothing in the output tells you why.",[1244,1245,1246],"style",{},"html pre.shiki code .sthd0, html code.shiki .sthd0{--shiki-default:#D73A49;--shiki-dark:#7DCFFF}html pre.shiki code .stqjZ, html code.shiki .stqjZ{--shiki-default:#D73A49;--shiki-dark:#BB9AF7}html pre.shiki code .seCP5, html code.shiki .seCP5{--shiki-default:#6F42C1;--shiki-dark:#C0CAF5}html pre.shiki code .sIYdW, html code.shiki .sIYdW{--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#9D7CD8;--shiki-dark-font-style:italic}html pre.shiki code .s0EQU, html code.shiki .s0EQU{--shiki-default:#6F42C1;--shiki-dark:#BB9AF7}html pre.shiki code .svadF, html code.shiki .svadF{--shiki-default:#24292E;--shiki-dark:#9ABDF5}html pre.shiki code .shyUM, html code.shiki .shyUM{--shiki-default:#005CC5;--shiki-dark:#F7768E}html pre.shiki code .sBGXs, html code.shiki .sBGXs{--shiki-default:#24292E;--shiki-dark:#89DDFF}html pre.shiki code .sP8dw, html code.shiki .sP8dw{--shiki-default:#6F42C1;--shiki-dark:#7AA2F7}html pre.shiki code .sUesm, html code.shiki .sUesm{--shiki-default:#24292E;--shiki-dark:#73DACA}html pre.shiki code .szk3v, html code.shiki .szk3v{--shiki-default:#032F62;--shiki-dark:#89DDFF}html pre.shiki code .sHZvL, html code.shiki .sHZvL{--shiki-default:#032F62;--shiki-dark:#9ECE6A}html pre.shiki code .sxfWu, html code.shiki .sxfWu{--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#51597D;--shiki-dark-font-style:italic}html pre.shiki code .sNbpT, html code.shiki .sNbpT{--shiki-default:#24292E;--shiki-dark:#C0CAF5}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 .s-zMA, html code.shiki .s-zMA{--shiki-default:#24292E;--shiki-dark:#0DB9D7}html pre.shiki code .solFm, html code.shiki .solFm{--shiki-default:#D73A49;--shiki-dark:#89DDFF}html pre.shiki code .sZREb, html code.shiki .sZREb{--shiki-default:#24292E;--shiki-dark:#7DCFFF}html pre.shiki code .sfwPi, html code.shiki .sfwPi{--shiki-default:#E36209;--shiki-dark:#E0AF68}html pre.shiki code .sEVbI, html code.shiki .sEVbI{--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#BB9AF7;--shiki-dark-font-style:italic}html pre.shiki code .sdEiP, html code.shiki .sdEiP{--shiki-default:#005CC5;--shiki-dark:#0DB9D7}html pre.shiki code .sDiRO, html code.shiki .sDiRO{--shiki-default:#005CC5;--shiki-dark:#FF9E64}html pre.shiki code .szlED, html code.shiki .szlED{--shiki-default:#005CC5;--shiki-dark:#7DCFFF}",{"title":51,"searchDepth":85,"depth":85,"links":1248},[1249,1250,1251,1252],{"id":40,"depth":85,"text":41},{"id":346,"depth":85,"text":347},{"id":497,"depth":85,"text":498},{"id":1111,"depth":85,"text":1112},"2026-05-03","Two non-obvious gotchas that make Custom Element tests look correct while being completely broken — and how to fix them.","md",{},"\u002Fen-gb\u002Fblog\u002Fcustom-element-vitest-silent-failures",{"title":14,"description":1254},"en-gb\u002Fblog\u002Fcustom-element-vitest-silent-failures","custom-element-vitest-silent-failures","jCMMK3ck5jaDaC69ydcP_mYL1XvOlcmA-sL_RkTAZY0",[],{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":1264},"\u003Cpath fill=\"currentColor\" d=\"m7.825 13l5.6 5.6L12 20l-8-8l8-8l1.425 1.4l-5.6 5.6H20v2z\"\u002F>",1777927588016]