[{"data":1,"prerenderedAt":3061},["ShallowReactive",2],{"post-testing-a-nuxt-monorepo-without-losing-your-mind":3},{"id":4,"title":5,"body":6,"brief":3050,"createdAt":3051,"description":3052,"extension":3053,"meta":3054,"navigation":291,"path":3055,"readTime":3056,"seo":3057,"slug":3058,"stem":3059,"__hash__":3060},"blog\u002Fblog\u002Ftesting-a-nuxt-monorepo-without-losing-your-mind.md","Testing a Nuxt Monorepo Without Losing Your Mind",{"type":7,"value":8,"toc":3048},"minimark",[9,13,16,19,22,28,31,34,88,99,102,107,110,159,166,171,174,177,201,207,1333,1338,1370,1375,1390,1817,1823,1828,1837,1843,2156,2161,2177,2180,2185,2203,2206,2209,2215,2225,2230,2233,2236,2242,2247,2250,2253,2260,2266,2954,2958,2961,2998,3005,3008,3013,3016,3036,3039,3044],[10,11,12],"p",{},"Testing a standard, single-package app is pretty straightforward. But a Nuxt monorepo? That can get messy very fast.",[10,14,15],{},"When you're dealing with multiple shared packages, utilities, auto-imports, and all that framework-specific magic, it’s easy to end up with test suites that are either completely shallow or just annoyingly brittle. I recently had to set up the tests for a monorepo I maintain, and to be honest, I just wanted a setup that made sense. I needed something easy to run, easy to understand, and solid enough to give me actual confidence as the codebase grows.",[10,17,18],{},"My plan was simple, just nail the foundation first. Unit tests for the logic, component tests for the UI, and Playwright for the end-to-end stuff.",[10,20,21],{},"Here’s a walkthrough of the setup I ended up building with Vitest and Playwright.",[10,23,24],{},[25,26,27],"strong",{},"The Lay of the Land",[10,29,30],{},"Before I start throwing configs around, let's talk about the shape of the codebase I'm working with.",[10,32,33],{},"I'm running a pnpm workspace. The codebase is split pretty traditionally, looking something like this:",[35,36,41],"pre",{"className":37,"code":38,"language":39,"meta":40,"style":40},"language-plaintext shiki shiki-themes vitesse-light vitesse-black","├── apps\u002F                 # Where the actual Nuxt applications live\n├── packages\u002F\n│   └── ui-layer\u002F         # Shared UI components consumed by the apps\n├── package.json\n├── pnpm-workspace.yaml\n├── playwright.config.mts\n└── vitest.config.ts\n","plaintext","",[42,43,44,52,58,64,70,76,82],"code",{"__ignoreMap":40},[45,46,49],"span",{"class":47,"line":48},"line",1,[45,50,51],{},"├── apps\u002F                 # Where the actual Nuxt applications live\n",[45,53,55],{"class":47,"line":54},2,[45,56,57],{},"├── packages\u002F\n",[45,59,61],{"class":47,"line":60},3,[45,62,63],{},"│   └── ui-layer\u002F         # Shared UI components consumed by the apps\n",[45,65,67],{"class":47,"line":66},4,[45,68,69],{},"├── package.json\n",[45,71,73],{"class":47,"line":72},5,[45,74,75],{},"├── pnpm-workspace.yaml\n",[45,77,79],{"class":47,"line":78},6,[45,80,81],{},"├── playwright.config.mts\n",[45,83,85],{"class":47,"line":84},7,[45,86,87],{},"└── vitest.config.ts\n",[10,89,90,91,94,95,98],{},"The most important guys here are the ",[42,92,93],{},"vitest.config.ts"," and ",[42,96,97],{},"playwright.config.mts",". And do you notice where they are at? Right at the root of the repo.",[10,100,101],{},"I didn't want an approach where I had to scatter separate test configurations inside every single app and package folders. I wanted it to be from the top-down. A centralised config makes it way easier to run everything at once, keeps the tooling dependencies clean and ensures no part of the codebase is doing weird things in its own isolated test environment. It basically just guarantees consistent behaviour across the board.",[10,103,104],{},[25,105,106],{},"The Gear",[10,108,109],{},"Let's not bore ourselves with a step-by-step installation guide like you see in most posts. You know how to use your package manger. But for this setup to work, here is the tooling l installed at the root of my workspace:",[111,112,113,120,126,136,141,147,153],"ul",{},[114,115,116,119],"li",{},[42,117,118],{},"vitest"," The core runner.",[114,121,122,125],{},[42,123,124],{},"@nuxt\u002Ftest-utils"," Nuxt's official testing utility (crucial for the component side of things).",[114,127,128,131,132,135],{},[42,129,130],{},"@vue\u002Ftest-utils"," & ",[42,133,134],{},"happy-dom"," For mounting and interacting with the UI in Vitest.",[114,137,138],{},[42,139,140],{},"@testing-library\u002Fvue",[114,142,143,146],{},[42,144,145],{},"@vitejs\u002Fplugin-vue"," Needed so Vitest knows what to do with ⁠.vue⁠ files when running outside of the Nuxt environment.",[114,148,149,152],{},[42,150,151],{},"@playwright\u002Ftest"," For the end-to-end stuff.",[114,154,155,158],{},[42,156,157],{},"@vitest\u002Fcoverage-v8"," For when you want to see those coverage metrics.",[10,160,161,162,165],{},"Just make sure these are installed as ",[42,163,164],{},"devDependencies⁠"," at your monorepo root, not in the individual apps.",[10,167,168],{},[25,169,170],{},"Phase 1: The Root Vitest Config (One Config to Rule Them All)",[10,172,173],{},"Here is where the magic happens, as mentioned earlier, I'm setting up in the root, so I wrote one single dynamic config to rule all the apps and packages.",[10,175,176],{},"The idea here is strict separation of concerns. I split the test runner into two distinct project types for every single workspace:",[178,179,180,187],"ol",{},[114,181,182,183,186],{},"Unit Tests (",[42,184,185],{},"*.unit.spec.ts","): Runs purely in Node. Super fast, meant for business logic, utilities and composables.",[114,188,189,190,193,194,196,197,200],{},"Component Tests (",[42,191,192],{},"*.component.spec.ts","): Runs in ",[42,195,134],{}," with the Vue plugin enabled. Meant for testing actual ",[42,198,199],{},".vue"," files and UI behaviour.",[10,202,203,204,206],{},"Here is the ",[42,205,93],{}," in its full glory lol:",[35,208,212],{"className":209,"code":210,"language":211,"meta":40,"style":40},"language-typescript shiki shiki-themes vitesse-light vitesse-black","import vue from \"@vitejs\u002Fplugin-vue\";\nimport { resolve } from \"node:path\";\nimport { defineConfig } from \"vitest\u002Fconfig\";\n\nconst workspaceRoot = process.cwd();\nconst uiLayerRoot = resolve(workspaceRoot, \"packages\u002Fui-layer\");\n\n\u002F\u002F nuxt loads the layer config while resolving each runtime project.\nconst workspaces = [\n  { name: \"admin\", root: resolve(workspaceRoot, \"apps\u002Fadmin\") },\n  { name: \"distributor\", root: resolve(workspaceRoot, \"apps\u002Fdistributor\") },\n  { name: \"provider\", root: resolve(workspaceRoot, \"apps\u002Fprovider\") },\n  { name: \"ui-layer\", root: uiLayerRoot },\n];\n\nexport default defineConfig(() => {\n  const unitProjects = workspaces.map(({ name, root }) => ({\n    extends: true as const,\n    root,\n    resolve: {\n      alias: {\n        \"@mca\u002Fui-layer\": uiLayerRoot,\n        \"@\": root,\n        \"~\": root,\n      },\n    },\n    test: {\n      name: `unit-${name}`,\n      include: [\"**\u002F*.unit.spec.ts\"],\n      environment: \"node\" as const,\n      setupFiles: [resolve(workspaceRoot, \"test\u002Fsetup.ts\")],\n    },\n  }));\n\n  const componentProjects = workspaces.map(({ name, root }) => ({\n    extends: true as const,\n    root,\n    plugins: [vue({})],\n    resolve: {\n      alias: {\n        \"@mca\u002Fui-layer\": uiLayerRoot,\n        \"@\": root,\n        \"~\": root,\n      },\n    },\n    test: {\n      name: `component-${name}`,\n      include: [\"**\u002F*.component.spec.ts\"],\n      environment: \"happy-dom\" as const,\n      setupFiles: [resolve(workspaceRoot, \"test\u002Fsetup.ts\")],\n    },\n  }));\n\n  return {\n    test: {\n      globals: true,\n      clearMocks: true,\n      restoreMocks: true,\n      projects: [...unitProjects, ...componentProjects],\n      coverage: {\n        provider: \"v8\" as const,\n        reporter: [\"text\", \"html\", \"lcov\"],\n        reportsDirectory: \".\u002Fcoverage\",\n        exclude: [\n          \"**\u002F*.unit.spec.ts\", \n          \"**\u002F*.component.spec.ts\", \n          \"**\u002F*.d.ts\", \n          \"**\u002F.nuxt\u002F**\", \n          \"**\u002F.output\u002F**\", \n          \"**\u002Fnode_modules\u002F**\", \n          \"**\u002F*.config.*\", \n          \"test\u002F**\"\n        ],\n      },\n    },\n  };\n});\n","typescript",[42,213,214,241,265,287,293,318,347,351,358,371,418,457,496,523,529,534,554,592,612,620,629,637,654,670,686,692,698,706,732,751,772,798,803,809,814,844,859,866,880,887,894,909,924,939,944,949,956,978,994,1013,1036,1041,1046,1051,1059,1066,1078,1090,1102,1120,1128,1149,1184,1201,1210,1223,1234,1246,1258,1270,1282,1294,1305,1311,1316,1321,1327],{"__ignoreMap":40},[45,215,216,220,224,227,231,234,237],{"class":47,"line":48},[45,217,219],{"class":218},"sTPum","import",[45,221,223],{"class":222},"s9nN2"," vue",[45,225,226],{"class":218}," from",[45,228,230],{"class":229},"scnC2"," \"",[45,232,145],{"class":233},"spP0B",[45,235,236],{"class":229},"\"",[45,238,240],{"class":239},"stPdD",";\n",[45,242,243,245,248,251,254,256,258,261,263],{"class":47,"line":54},[45,244,219],{"class":218},[45,246,247],{"class":239}," {",[45,249,250],{"class":222}," resolve",[45,252,253],{"class":239}," }",[45,255,226],{"class":218},[45,257,230],{"class":229},[45,259,260],{"class":233},"node:path",[45,262,236],{"class":229},[45,264,240],{"class":239},[45,266,267,269,271,274,276,278,280,283,285],{"class":47,"line":60},[45,268,219],{"class":218},[45,270,247],{"class":239},[45,272,273],{"class":222}," defineConfig",[45,275,253],{"class":239},[45,277,226],{"class":218},[45,279,230],{"class":229},[45,281,282],{"class":233},"vitest\u002Fconfig",[45,284,236],{"class":229},[45,286,240],{"class":239},[45,288,289],{"class":47,"line":66},[45,290,292],{"emptyLinePlaceholder":291},true,"\n",[45,294,295,299,302,305,308,311,315],{"class":47,"line":72},[45,296,298],{"class":297},"s5TCs","const ",[45,300,301],{"class":222},"workspaceRoot",[45,303,304],{"class":239}," =",[45,306,307],{"class":222}," process",[45,309,310],{"class":239},".",[45,312,314],{"class":313},"s_xSY","cwd",[45,316,317],{"class":239},"();\n",[45,319,320,322,325,327,329,332,334,337,339,342,344],{"class":47,"line":78},[45,321,298],{"class":297},[45,323,324],{"class":222},"uiLayerRoot",[45,326,304],{"class":239},[45,328,250],{"class":313},[45,330,331],{"class":239},"(",[45,333,301],{"class":222},[45,335,336],{"class":239},",",[45,338,230],{"class":229},[45,340,341],{"class":233},"packages\u002Fui-layer",[45,343,236],{"class":229},[45,345,346],{"class":239},");\n",[45,348,349],{"class":47,"line":84},[45,350,292],{"emptyLinePlaceholder":291},[45,352,354],{"class":47,"line":353},8,[45,355,357],{"class":356},"snYqZ","\u002F\u002F nuxt loads the layer config while resolving each runtime project.\n",[45,359,361,363,366,368],{"class":47,"line":360},9,[45,362,298],{"class":297},[45,364,365],{"class":222},"workspaces",[45,367,304],{"class":239},[45,369,370],{"class":239}," [\n",[45,372,374,377,381,384,386,389,391,394,397,399,402,404,406,408,410,413,415],{"class":47,"line":373},10,[45,375,376],{"class":239},"  { ",[45,378,380],{"class":379},"sHLBJ","name",[45,382,383],{"class":239},": ",[45,385,236],{"class":229},[45,387,388],{"class":233},"admin",[45,390,236],{"class":229},[45,392,393],{"class":239},", ",[45,395,396],{"class":379},"root",[45,398,383],{"class":239},[45,400,401],{"class":313},"resolve",[45,403,331],{"class":239},[45,405,301],{"class":222},[45,407,393],{"class":239},[45,409,236],{"class":229},[45,411,412],{"class":233},"apps\u002Fadmin",[45,414,236],{"class":229},[45,416,417],{"class":239},") },\n",[45,419,421,423,425,427,429,432,434,436,438,440,442,444,446,448,450,453,455],{"class":47,"line":420},11,[45,422,376],{"class":239},[45,424,380],{"class":379},[45,426,383],{"class":239},[45,428,236],{"class":229},[45,430,431],{"class":233},"distributor",[45,433,236],{"class":229},[45,435,393],{"class":239},[45,437,396],{"class":379},[45,439,383],{"class":239},[45,441,401],{"class":313},[45,443,331],{"class":239},[45,445,301],{"class":222},[45,447,393],{"class":239},[45,449,236],{"class":229},[45,451,452],{"class":233},"apps\u002Fdistributor",[45,454,236],{"class":229},[45,456,417],{"class":239},[45,458,460,462,464,466,468,471,473,475,477,479,481,483,485,487,489,492,494],{"class":47,"line":459},12,[45,461,376],{"class":239},[45,463,380],{"class":379},[45,465,383],{"class":239},[45,467,236],{"class":229},[45,469,470],{"class":233},"provider",[45,472,236],{"class":229},[45,474,393],{"class":239},[45,476,396],{"class":379},[45,478,383],{"class":239},[45,480,401],{"class":313},[45,482,331],{"class":239},[45,484,301],{"class":222},[45,486,393],{"class":239},[45,488,236],{"class":229},[45,490,491],{"class":233},"apps\u002Fprovider",[45,493,236],{"class":229},[45,495,417],{"class":239},[45,497,499,501,503,505,507,510,512,514,516,518,520],{"class":47,"line":498},13,[45,500,376],{"class":239},[45,502,380],{"class":379},[45,504,383],{"class":239},[45,506,236],{"class":229},[45,508,509],{"class":233},"ui-layer",[45,511,236],{"class":229},[45,513,393],{"class":239},[45,515,396],{"class":379},[45,517,383],{"class":239},[45,519,324],{"class":222},[45,521,522],{"class":239}," },\n",[45,524,526],{"class":47,"line":525},14,[45,527,528],{"class":239},"];\n",[45,530,532],{"class":47,"line":531},15,[45,533,292],{"emptyLinePlaceholder":291},[45,535,537,540,543,545,548,551],{"class":47,"line":536},16,[45,538,539],{"class":218},"export",[45,541,542],{"class":218}," default",[45,544,273],{"class":313},[45,546,547],{"class":239},"(()",[45,549,550],{"class":239}," =>",[45,552,553],{"class":239}," {\n",[45,555,557,560,563,565,568,570,573,576,579,581,584,587,589],{"class":47,"line":556},17,[45,558,559],{"class":297},"  const ",[45,561,562],{"class":222},"unitProjects",[45,564,304],{"class":239},[45,566,567],{"class":222}," workspaces",[45,569,310],{"class":239},[45,571,572],{"class":313},"map",[45,574,575],{"class":239},"(({",[45,577,578],{"class":222}," name",[45,580,336],{"class":239},[45,582,583],{"class":222}," root",[45,585,586],{"class":239}," })",[45,588,550],{"class":239},[45,590,591],{"class":239}," ({\n",[45,593,595,598,600,603,606,609],{"class":47,"line":594},18,[45,596,597],{"class":379},"    extends",[45,599,383],{"class":239},[45,601,602],{"class":218},"true",[45,604,605],{"class":218}," as",[45,607,608],{"class":297}," const",[45,610,611],{"class":239},",\n",[45,613,615,618],{"class":47,"line":614},19,[45,616,617],{"class":222},"    root",[45,619,611],{"class":239},[45,621,623,626],{"class":47,"line":622},20,[45,624,625],{"class":379},"    resolve",[45,627,628],{"class":239},": {\n",[45,630,632,635],{"class":47,"line":631},21,[45,633,634],{"class":379},"      alias",[45,636,628],{"class":239},[45,638,640,643,646,648,650,652],{"class":47,"line":639},22,[45,641,642],{"class":229},"        \"",[45,644,645],{"class":233},"@mca\u002Fui-layer",[45,647,236],{"class":229},[45,649,383],{"class":239},[45,651,324],{"class":222},[45,653,611],{"class":239},[45,655,657,659,662,664,666,668],{"class":47,"line":656},23,[45,658,642],{"class":229},[45,660,661],{"class":233},"@",[45,663,236],{"class":229},[45,665,383],{"class":239},[45,667,396],{"class":222},[45,669,611],{"class":239},[45,671,673,675,678,680,682,684],{"class":47,"line":672},24,[45,674,642],{"class":229},[45,676,677],{"class":233},"~",[45,679,236],{"class":229},[45,681,383],{"class":239},[45,683,396],{"class":222},[45,685,611],{"class":239},[45,687,689],{"class":47,"line":688},25,[45,690,691],{"class":239},"      },\n",[45,693,695],{"class":47,"line":694},26,[45,696,697],{"class":239},"    },\n",[45,699,701,704],{"class":47,"line":700},27,[45,702,703],{"class":379},"    test",[45,705,628],{"class":239},[45,707,709,712,714,717,720,723,725,728,730],{"class":47,"line":708},28,[45,710,711],{"class":379},"      name",[45,713,383],{"class":239},[45,715,716],{"class":229},"`",[45,718,719],{"class":233},"unit-",[45,721,722],{"class":218},"${",[45,724,380],{"class":233},[45,726,727],{"class":218},"}",[45,729,716],{"class":229},[45,731,611],{"class":239},[45,733,735,738,741,743,746,748],{"class":47,"line":734},29,[45,736,737],{"class":379},"      include",[45,739,740],{"class":239},": [",[45,742,236],{"class":229},[45,744,745],{"class":233},"**\u002F*.unit.spec.ts",[45,747,236],{"class":229},[45,749,750],{"class":239},"],\n",[45,752,754,757,759,761,764,766,768,770],{"class":47,"line":753},30,[45,755,756],{"class":379},"      environment",[45,758,383],{"class":239},[45,760,236],{"class":229},[45,762,763],{"class":233},"node",[45,765,236],{"class":229},[45,767,605],{"class":218},[45,769,608],{"class":297},[45,771,611],{"class":239},[45,773,775,778,780,782,784,786,788,790,793,795],{"class":47,"line":774},31,[45,776,777],{"class":379},"      setupFiles",[45,779,740],{"class":239},[45,781,401],{"class":313},[45,783,331],{"class":239},[45,785,301],{"class":222},[45,787,393],{"class":239},[45,789,236],{"class":229},[45,791,792],{"class":233},"test\u002Fsetup.ts",[45,794,236],{"class":229},[45,796,797],{"class":239},")],\n",[45,799,801],{"class":47,"line":800},32,[45,802,697],{"class":239},[45,804,806],{"class":47,"line":805},33,[45,807,808],{"class":239},"  }));\n",[45,810,812],{"class":47,"line":811},34,[45,813,292],{"emptyLinePlaceholder":291},[45,815,817,819,822,824,826,828,830,832,834,836,838,840,842],{"class":47,"line":816},35,[45,818,559],{"class":297},[45,820,821],{"class":222},"componentProjects",[45,823,304],{"class":239},[45,825,567],{"class":222},[45,827,310],{"class":239},[45,829,572],{"class":313},[45,831,575],{"class":239},[45,833,578],{"class":222},[45,835,336],{"class":239},[45,837,583],{"class":222},[45,839,586],{"class":239},[45,841,550],{"class":239},[45,843,591],{"class":239},[45,845,847,849,851,853,855,857],{"class":47,"line":846},36,[45,848,597],{"class":379},[45,850,383],{"class":239},[45,852,602],{"class":218},[45,854,605],{"class":218},[45,856,608],{"class":297},[45,858,611],{"class":239},[45,860,862,864],{"class":47,"line":861},37,[45,863,617],{"class":222},[45,865,611],{"class":239},[45,867,869,872,874,877],{"class":47,"line":868},38,[45,870,871],{"class":379},"    plugins",[45,873,740],{"class":239},[45,875,876],{"class":313},"vue",[45,878,879],{"class":239},"({})],\n",[45,881,883,885],{"class":47,"line":882},39,[45,884,625],{"class":379},[45,886,628],{"class":239},[45,888,890,892],{"class":47,"line":889},40,[45,891,634],{"class":379},[45,893,628],{"class":239},[45,895,897,899,901,903,905,907],{"class":47,"line":896},41,[45,898,642],{"class":229},[45,900,645],{"class":233},[45,902,236],{"class":229},[45,904,383],{"class":239},[45,906,324],{"class":222},[45,908,611],{"class":239},[45,910,912,914,916,918,920,922],{"class":47,"line":911},42,[45,913,642],{"class":229},[45,915,661],{"class":233},[45,917,236],{"class":229},[45,919,383],{"class":239},[45,921,396],{"class":222},[45,923,611],{"class":239},[45,925,927,929,931,933,935,937],{"class":47,"line":926},43,[45,928,642],{"class":229},[45,930,677],{"class":233},[45,932,236],{"class":229},[45,934,383],{"class":239},[45,936,396],{"class":222},[45,938,611],{"class":239},[45,940,942],{"class":47,"line":941},44,[45,943,691],{"class":239},[45,945,947],{"class":47,"line":946},45,[45,948,697],{"class":239},[45,950,952,954],{"class":47,"line":951},46,[45,953,703],{"class":379},[45,955,628],{"class":239},[45,957,959,961,963,965,968,970,972,974,976],{"class":47,"line":958},47,[45,960,711],{"class":379},[45,962,383],{"class":239},[45,964,716],{"class":229},[45,966,967],{"class":233},"component-",[45,969,722],{"class":218},[45,971,380],{"class":233},[45,973,727],{"class":218},[45,975,716],{"class":229},[45,977,611],{"class":239},[45,979,981,983,985,987,990,992],{"class":47,"line":980},48,[45,982,737],{"class":379},[45,984,740],{"class":239},[45,986,236],{"class":229},[45,988,989],{"class":233},"**\u002F*.component.spec.ts",[45,991,236],{"class":229},[45,993,750],{"class":239},[45,995,997,999,1001,1003,1005,1007,1009,1011],{"class":47,"line":996},49,[45,998,756],{"class":379},[45,1000,383],{"class":239},[45,1002,236],{"class":229},[45,1004,134],{"class":233},[45,1006,236],{"class":229},[45,1008,605],{"class":218},[45,1010,608],{"class":297},[45,1012,611],{"class":239},[45,1014,1016,1018,1020,1022,1024,1026,1028,1030,1032,1034],{"class":47,"line":1015},50,[45,1017,777],{"class":379},[45,1019,740],{"class":239},[45,1021,401],{"class":313},[45,1023,331],{"class":239},[45,1025,301],{"class":222},[45,1027,393],{"class":239},[45,1029,236],{"class":229},[45,1031,792],{"class":233},[45,1033,236],{"class":229},[45,1035,797],{"class":239},[45,1037,1039],{"class":47,"line":1038},51,[45,1040,697],{"class":239},[45,1042,1044],{"class":47,"line":1043},52,[45,1045,808],{"class":239},[45,1047,1049],{"class":47,"line":1048},53,[45,1050,292],{"emptyLinePlaceholder":291},[45,1052,1054,1057],{"class":47,"line":1053},54,[45,1055,1056],{"class":218},"  return",[45,1058,553],{"class":239},[45,1060,1062,1064],{"class":47,"line":1061},55,[45,1063,703],{"class":379},[45,1065,628],{"class":239},[45,1067,1069,1072,1074,1076],{"class":47,"line":1068},56,[45,1070,1071],{"class":379},"      globals",[45,1073,383],{"class":239},[45,1075,602],{"class":218},[45,1077,611],{"class":239},[45,1079,1081,1084,1086,1088],{"class":47,"line":1080},57,[45,1082,1083],{"class":379},"      clearMocks",[45,1085,383],{"class":239},[45,1087,602],{"class":218},[45,1089,611],{"class":239},[45,1091,1093,1096,1098,1100],{"class":47,"line":1092},58,[45,1094,1095],{"class":379},"      restoreMocks",[45,1097,383],{"class":239},[45,1099,602],{"class":218},[45,1101,611],{"class":239},[45,1103,1105,1108,1111,1113,1116,1118],{"class":47,"line":1104},59,[45,1106,1107],{"class":379},"      projects",[45,1109,1110],{"class":239},": [...",[45,1112,562],{"class":222},[45,1114,1115],{"class":239},", ...",[45,1117,821],{"class":222},[45,1119,750],{"class":239},[45,1121,1123,1126],{"class":47,"line":1122},60,[45,1124,1125],{"class":379},"      coverage",[45,1127,628],{"class":239},[45,1129,1131,1134,1136,1138,1141,1143,1145,1147],{"class":47,"line":1130},61,[45,1132,1133],{"class":379},"        provider",[45,1135,383],{"class":239},[45,1137,236],{"class":229},[45,1139,1140],{"class":233},"v8",[45,1142,236],{"class":229},[45,1144,605],{"class":218},[45,1146,608],{"class":297},[45,1148,611],{"class":239},[45,1150,1152,1155,1157,1159,1162,1164,1166,1168,1171,1173,1175,1177,1180,1182],{"class":47,"line":1151},62,[45,1153,1154],{"class":379},"        reporter",[45,1156,740],{"class":239},[45,1158,236],{"class":229},[45,1160,1161],{"class":233},"text",[45,1163,236],{"class":229},[45,1165,393],{"class":239},[45,1167,236],{"class":229},[45,1169,1170],{"class":233},"html",[45,1172,236],{"class":229},[45,1174,393],{"class":239},[45,1176,236],{"class":229},[45,1178,1179],{"class":233},"lcov",[45,1181,236],{"class":229},[45,1183,750],{"class":239},[45,1185,1187,1190,1192,1194,1197,1199],{"class":47,"line":1186},63,[45,1188,1189],{"class":379},"        reportsDirectory",[45,1191,383],{"class":239},[45,1193,236],{"class":229},[45,1195,1196],{"class":233},".\u002Fcoverage",[45,1198,236],{"class":229},[45,1200,611],{"class":239},[45,1202,1204,1207],{"class":47,"line":1203},64,[45,1205,1206],{"class":379},"        exclude",[45,1208,1209],{"class":239},": [\n",[45,1211,1213,1216,1218,1220],{"class":47,"line":1212},65,[45,1214,1215],{"class":229},"          \"",[45,1217,745],{"class":233},[45,1219,236],{"class":229},[45,1221,1222],{"class":239},", \n",[45,1224,1226,1228,1230,1232],{"class":47,"line":1225},66,[45,1227,1215],{"class":229},[45,1229,989],{"class":233},[45,1231,236],{"class":229},[45,1233,1222],{"class":239},[45,1235,1237,1239,1242,1244],{"class":47,"line":1236},67,[45,1238,1215],{"class":229},[45,1240,1241],{"class":233},"**\u002F*.d.ts",[45,1243,236],{"class":229},[45,1245,1222],{"class":239},[45,1247,1249,1251,1254,1256],{"class":47,"line":1248},68,[45,1250,1215],{"class":229},[45,1252,1253],{"class":233},"**\u002F.nuxt\u002F**",[45,1255,236],{"class":229},[45,1257,1222],{"class":239},[45,1259,1261,1263,1266,1268],{"class":47,"line":1260},69,[45,1262,1215],{"class":229},[45,1264,1265],{"class":233},"**\u002F.output\u002F**",[45,1267,236],{"class":229},[45,1269,1222],{"class":239},[45,1271,1273,1275,1278,1280],{"class":47,"line":1272},70,[45,1274,1215],{"class":229},[45,1276,1277],{"class":233},"**\u002Fnode_modules\u002F**",[45,1279,236],{"class":229},[45,1281,1222],{"class":239},[45,1283,1285,1287,1290,1292],{"class":47,"line":1284},71,[45,1286,1215],{"class":229},[45,1288,1289],{"class":233},"**\u002F*.config.*",[45,1291,236],{"class":229},[45,1293,1222],{"class":239},[45,1295,1297,1299,1302],{"class":47,"line":1296},72,[45,1298,1215],{"class":229},[45,1300,1301],{"class":233},"test\u002F**",[45,1303,1304],{"class":229},"\"\n",[45,1306,1308],{"class":47,"line":1307},73,[45,1309,1310],{"class":239},"        ],\n",[45,1312,1314],{"class":47,"line":1313},74,[45,1315,691],{"class":239},[45,1317,1319],{"class":47,"line":1318},75,[45,1320,697],{"class":239},[45,1322,1324],{"class":47,"line":1323},76,[45,1325,1326],{"class":239},"  };\n",[45,1328,1330],{"class":47,"line":1329},77,[45,1331,1332],{"class":239},"});\n",[10,1334,1335],{},[25,1336,1337],{},"Why this works so well",[178,1339,1340,1350],{},[114,1341,1342,1343,1345,1346,1349],{},"No DOM overhead for pure logic: Notice how the ",[42,1344,562],{}," use ",[42,1347,1348],{},"environment: \"node\""," and doesn't load the Vue plugin? If I'm testing a utility function that formats a currency string, I don't need to boot up a simulated browser environment. It keeps the unit tests blazing fast.",[114,1351,1352,1353,1355,1356,1358,1359,1362,1363,1366,1367,1369],{},"Handling Cross-Workspace Aliasing: When you test outside of the Nuxt context, Vitest has no idea what ",[42,1354,677],{},"⁠ or ",[42,1357,661],{},"⁠ means. By explicitly mapping ",[42,1360,1361],{},"@mca\u002Fui-layer⁠"," and the Nuxt aliases (",[42,1364,1365],{},"⁠@⁠,"," ",[42,1368,677],{},"⁠) dynamically to each workspace's root, imports resolve seamlessly without Nuxt having to hold our hand.",[10,1371,1372],{},[25,1373,1374],{},"Pro-Tip: The Setup File",[10,1376,1377,1378,1381,1382,1385,1386,1389],{},"You might notice this line ",[42,1379,1380],{},"setupFiles: [resolve(workspaceRoot, \"test\u002Fsetup.ts\")]"," in the config. Because my unit tests runs in Node, they don't have access to browser APIs. I use this to dynamically polyfill things like ",[42,1383,1384],{},"localStorage⁠"," only when the environment needs it, without overwriting ",[42,1387,1388],{},"happy-dom⁠'s"," native implementation.",[35,1391,1393],{"className":209,"code":1392,"language":211,"meta":40,"style":40},"import { vi } from \"vitest\";\n\nconst createStorage = () => {\n  const values = new Map\u003Cstring, string>();\n\n  return {\n    getItem: (key: string) => values.get(key) ?? null,\n    setItem: (key: string, value: string) => values.set(key, String(value)),\n    removeItem: (key: string) => values.delete(key),\n    clear: () => values.clear(),\n    key: (index: number) => Array.from(values.keys())[index] ?? null,\n    get length() {\n      return values.size;\n    },\n  };\n};\n\nfor (const key of [\"localStorage\", \"sessionStorage\"] as const) {\n  if (typeof globalThis[key]?.getItem !== \"function\") {\n    vi.stubGlobal(key, createStorage());\n  }\n}\n",[42,1394,1395,1416,1420,1436,1466,1470,1476,1516,1563,1592,1610,1658,1669,1684,1688,1692,1697,1701,1747,1785,1807,1812],{"__ignoreMap":40},[45,1396,1397,1399,1401,1404,1406,1408,1410,1412,1414],{"class":47,"line":48},[45,1398,219],{"class":218},[45,1400,247],{"class":239},[45,1402,1403],{"class":222}," vi",[45,1405,253],{"class":239},[45,1407,226],{"class":218},[45,1409,230],{"class":229},[45,1411,118],{"class":233},[45,1413,236],{"class":229},[45,1415,240],{"class":239},[45,1417,1418],{"class":47,"line":54},[45,1419,292],{"emptyLinePlaceholder":291},[45,1421,1422,1424,1427,1429,1432,1434],{"class":47,"line":60},[45,1423,298],{"class":297},[45,1425,1426],{"class":313},"createStorage",[45,1428,304],{"class":239},[45,1430,1431],{"class":239}," ()",[45,1433,550],{"class":239},[45,1435,553],{"class":239},[45,1437,1438,1440,1443,1445,1448,1451,1454,1458,1460,1463],{"class":47,"line":66},[45,1439,559],{"class":297},[45,1441,1442],{"class":222},"values",[45,1444,304],{"class":239},[45,1446,1447],{"class":297}," new ",[45,1449,1450],{"class":313},"Map",[45,1452,1453],{"class":239},"\u003C",[45,1455,1457],{"class":1456},"s_NWU","string",[45,1459,336],{"class":239},[45,1461,1462],{"class":1456}," string",[45,1464,1465],{"class":239},">();\n",[45,1467,1468],{"class":47,"line":72},[45,1469,292],{"emptyLinePlaceholder":291},[45,1471,1472,1474],{"class":47,"line":78},[45,1473,1056],{"class":218},[45,1475,553],{"class":239},[45,1477,1478,1481,1484,1487,1489,1491,1494,1496,1498,1501,1503,1505,1508,1511,1514],{"class":47,"line":84},[45,1479,1480],{"class":313},"    getItem",[45,1482,1483],{"class":239},": (",[45,1485,1486],{"class":222},"key",[45,1488,383],{"class":239},[45,1490,1457],{"class":1456},[45,1492,1493],{"class":239},") => ",[45,1495,1442],{"class":222},[45,1497,310],{"class":239},[45,1499,1500],{"class":313},"get",[45,1502,331],{"class":239},[45,1504,1486],{"class":222},[45,1506,1507],{"class":239},") ",[45,1509,1510],{"class":297},"??",[45,1512,1513],{"class":297}," null",[45,1515,611],{"class":239},[45,1517,1518,1521,1523,1525,1527,1529,1531,1534,1536,1538,1540,1542,1544,1547,1549,1551,1553,1556,1558,1560],{"class":47,"line":353},[45,1519,1520],{"class":313},"    setItem",[45,1522,1483],{"class":239},[45,1524,1486],{"class":222},[45,1526,383],{"class":239},[45,1528,1457],{"class":1456},[45,1530,393],{"class":239},[45,1532,1533],{"class":222},"value",[45,1535,383],{"class":239},[45,1537,1457],{"class":1456},[45,1539,1493],{"class":239},[45,1541,1442],{"class":222},[45,1543,310],{"class":239},[45,1545,1546],{"class":313},"set",[45,1548,331],{"class":239},[45,1550,1486],{"class":222},[45,1552,393],{"class":239},[45,1554,1555],{"class":313},"String",[45,1557,331],{"class":239},[45,1559,1533],{"class":222},[45,1561,1562],{"class":239},")),\n",[45,1564,1565,1568,1570,1572,1574,1576,1578,1580,1582,1585,1587,1589],{"class":47,"line":360},[45,1566,1567],{"class":313},"    removeItem",[45,1569,1483],{"class":239},[45,1571,1486],{"class":222},[45,1573,383],{"class":239},[45,1575,1457],{"class":1456},[45,1577,1493],{"class":239},[45,1579,1442],{"class":222},[45,1581,310],{"class":239},[45,1583,1584],{"class":313},"delete",[45,1586,331],{"class":239},[45,1588,1486],{"class":222},[45,1590,1591],{"class":239},"),\n",[45,1593,1594,1597,1600,1602,1604,1607],{"class":47,"line":373},[45,1595,1596],{"class":313},"    clear",[45,1598,1599],{"class":239},": () => ",[45,1601,1442],{"class":222},[45,1603,310],{"class":239},[45,1605,1606],{"class":313},"clear",[45,1608,1609],{"class":239},"(),\n",[45,1611,1612,1615,1617,1620,1622,1625,1627,1630,1632,1635,1637,1639,1641,1644,1647,1649,1652,1654,1656],{"class":47,"line":420},[45,1613,1614],{"class":313},"    key",[45,1616,1483],{"class":239},[45,1618,1619],{"class":222},"index",[45,1621,383],{"class":239},[45,1623,1624],{"class":1456},"number",[45,1626,1493],{"class":239},[45,1628,1629],{"class":222},"Array",[45,1631,310],{"class":239},[45,1633,1634],{"class":313},"from",[45,1636,331],{"class":239},[45,1638,1442],{"class":222},[45,1640,310],{"class":239},[45,1642,1643],{"class":313},"keys",[45,1645,1646],{"class":239},"())[",[45,1648,1619],{"class":222},[45,1650,1651],{"class":239},"] ",[45,1653,1510],{"class":297},[45,1655,1513],{"class":297},[45,1657,611],{"class":239},[45,1659,1660,1663,1666],{"class":47,"line":459},[45,1661,1662],{"class":297},"    get",[45,1664,1665],{"class":313}," length",[45,1667,1668],{"class":239},"() {\n",[45,1670,1671,1674,1677,1679,1682],{"class":47,"line":498},[45,1672,1673],{"class":218},"      return",[45,1675,1676],{"class":222}," values",[45,1678,310],{"class":239},[45,1680,1681],{"class":222},"size",[45,1683,240],{"class":239},[45,1685,1686],{"class":47,"line":525},[45,1687,697],{"class":239},[45,1689,1690],{"class":47,"line":531},[45,1691,1326],{"class":239},[45,1693,1694],{"class":47,"line":536},[45,1695,1696],{"class":239},"};\n",[45,1698,1699],{"class":47,"line":556},[45,1700,292],{"emptyLinePlaceholder":291},[45,1702,1703,1706,1709,1711,1713,1716,1719,1721,1724,1726,1728,1730,1733,1735,1738,1740,1742,1745],{"class":47,"line":594},[45,1704,1705],{"class":218},"for",[45,1707,1708],{"class":239}," (",[45,1710,298],{"class":297},[45,1712,1486],{"class":222},[45,1714,1715],{"class":297}," of",[45,1717,1718],{"class":239}," [",[45,1720,236],{"class":229},[45,1722,1723],{"class":233},"localStorage",[45,1725,236],{"class":229},[45,1727,336],{"class":239},[45,1729,230],{"class":229},[45,1731,1732],{"class":233},"sessionStorage",[45,1734,236],{"class":229},[45,1736,1737],{"class":239},"]",[45,1739,605],{"class":218},[45,1741,608],{"class":297},[45,1743,1744],{"class":239},")",[45,1746,553],{"class":239},[45,1748,1749,1752,1754,1757,1760,1763,1765,1768,1771,1774,1776,1779,1781,1783],{"class":47,"line":614},[45,1750,1751],{"class":218},"  if",[45,1753,1708],{"class":239},[45,1755,1756],{"class":297},"typeof",[45,1758,1759],{"class":222}," globalThis",[45,1761,1762],{"class":239},"[",[45,1764,1486],{"class":222},[45,1766,1767],{"class":239},"]?.",[45,1769,1770],{"class":222},"getItem",[45,1772,1773],{"class":297}," !==",[45,1775,230],{"class":229},[45,1777,1778],{"class":233},"function",[45,1780,236],{"class":229},[45,1782,1744],{"class":239},[45,1784,553],{"class":239},[45,1786,1787,1790,1792,1795,1797,1799,1801,1804],{"class":47,"line":622},[45,1788,1789],{"class":222},"    vi",[45,1791,310],{"class":239},[45,1793,1794],{"class":313},"stubGlobal",[45,1796,331],{"class":239},[45,1798,1486],{"class":222},[45,1800,336],{"class":239},[45,1802,1803],{"class":313}," createStorage",[45,1805,1806],{"class":239},"());\n",[45,1808,1809],{"class":47,"line":631},[45,1810,1811],{"class":239},"  }\n",[45,1813,1814],{"class":47,"line":639},[45,1815,1816],{"class":239},"}\n",[10,1818,1819],{},[1820,1821,1822],"em",{},"This simple check makes the setup file completely polymorphic across both environments.",[10,1824,1825],{},[25,1826,1827],{},"Phase 2: UI Components",[10,1829,1830,1831,1833,1834,1836],{},"Testing Vue components in a monorepo could get messy when you try to boot up Nuxt just to test a simple button. But because I separated my ",[42,1832,821],{}," to run in ",[42,1835,134],{}," in the Vitest config, I can test my shared UI layer instantly.",[10,1838,1839,1840,1842],{},"Let's look at a component from the ",[42,1841,341],{}," folder. Here is a standard test for a Badge.vue component:",[35,1844,1846],{"className":209,"code":1845,"language":211,"meta":40,"style":40},"import { render, screen } from \"@testing-library\u002Fvue\";\nimport { describe, expect, it } from \"vitest\";\nimport Badge from \".\u002Fbadge.vue\";\n\ndescribe(\"Badge\", () => {\n  it(\"renders its label from props\", async () => {\n    render(Badge, { props: { label: \"Active\" } });\n\n    \u002F\u002F Testing what the user actually sees\n    expect(screen.getByText(\"Active\")).toBeInTheDocument();\n  });\n\n  it(\"renders a named label slot when provided\", async () => {\n    render(Badge, {\n      slots: { label: \"Custom label\" },\n    });\n\n    expect(screen.getByText(\"Custom label\")).toBeInTheDocument();\n  });\n});\n",[42,1847,1848,1874,1905,1923,1927,1949,1974,2009,2013,2018,2049,2054,2058,2081,2093,2113,2118,2122,2148,2152],{"__ignoreMap":40},[45,1849,1850,1852,1854,1857,1859,1862,1864,1866,1868,1870,1872],{"class":47,"line":48},[45,1851,219],{"class":218},[45,1853,247],{"class":239},[45,1855,1856],{"class":222}," render",[45,1858,336],{"class":239},[45,1860,1861],{"class":222}," screen",[45,1863,253],{"class":239},[45,1865,226],{"class":218},[45,1867,230],{"class":229},[45,1869,140],{"class":233},[45,1871,236],{"class":229},[45,1873,240],{"class":239},[45,1875,1876,1878,1880,1883,1885,1888,1890,1893,1895,1897,1899,1901,1903],{"class":47,"line":54},[45,1877,219],{"class":218},[45,1879,247],{"class":239},[45,1881,1882],{"class":222}," describe",[45,1884,336],{"class":239},[45,1886,1887],{"class":222}," expect",[45,1889,336],{"class":239},[45,1891,1892],{"class":222}," it",[45,1894,253],{"class":239},[45,1896,226],{"class":218},[45,1898,230],{"class":229},[45,1900,118],{"class":233},[45,1902,236],{"class":229},[45,1904,240],{"class":239},[45,1906,1907,1909,1912,1914,1916,1919,1921],{"class":47,"line":60},[45,1908,219],{"class":218},[45,1910,1911],{"class":222}," Badge",[45,1913,226],{"class":218},[45,1915,230],{"class":229},[45,1917,1918],{"class":233},".\u002Fbadge.vue",[45,1920,236],{"class":229},[45,1922,240],{"class":239},[45,1924,1925],{"class":47,"line":66},[45,1926,292],{"emptyLinePlaceholder":291},[45,1928,1929,1932,1934,1936,1939,1941,1943,1945,1947],{"class":47,"line":72},[45,1930,1931],{"class":313},"describe",[45,1933,331],{"class":239},[45,1935,236],{"class":229},[45,1937,1938],{"class":233},"Badge",[45,1940,236],{"class":229},[45,1942,336],{"class":239},[45,1944,1431],{"class":239},[45,1946,550],{"class":239},[45,1948,553],{"class":239},[45,1950,1951,1954,1956,1958,1961,1963,1965,1968,1970,1972],{"class":47,"line":78},[45,1952,1953],{"class":313},"  it",[45,1955,331],{"class":239},[45,1957,236],{"class":229},[45,1959,1960],{"class":233},"renders its label from props",[45,1962,236],{"class":229},[45,1964,336],{"class":239},[45,1966,1967],{"class":297}," async",[45,1969,1431],{"class":239},[45,1971,550],{"class":239},[45,1973,553],{"class":239},[45,1975,1976,1979,1981,1983,1985,1988,1991,1994,1997,1999,2001,2004,2006],{"class":47,"line":84},[45,1977,1978],{"class":313},"    render",[45,1980,331],{"class":239},[45,1982,1938],{"class":222},[45,1984,336],{"class":239},[45,1986,1987],{"class":239}," { ",[45,1989,1990],{"class":379},"props",[45,1992,1993],{"class":239},": { ",[45,1995,1996],{"class":379},"label",[45,1998,383],{"class":239},[45,2000,236],{"class":229},[45,2002,2003],{"class":233},"Active",[45,2005,236],{"class":229},[45,2007,2008],{"class":239}," } });\n",[45,2010,2011],{"class":47,"line":353},[45,2012,292],{"emptyLinePlaceholder":291},[45,2014,2015],{"class":47,"line":360},[45,2016,2017],{"class":356},"    \u002F\u002F Testing what the user actually sees\n",[45,2019,2020,2023,2025,2028,2030,2033,2035,2037,2039,2041,2044,2047],{"class":47,"line":373},[45,2021,2022],{"class":313},"    expect",[45,2024,331],{"class":239},[45,2026,2027],{"class":222},"screen",[45,2029,310],{"class":239},[45,2031,2032],{"class":313},"getByText",[45,2034,331],{"class":239},[45,2036,236],{"class":229},[45,2038,2003],{"class":233},[45,2040,236],{"class":229},[45,2042,2043],{"class":239},")).",[45,2045,2046],{"class":313},"toBeInTheDocument",[45,2048,317],{"class":239},[45,2050,2051],{"class":47,"line":420},[45,2052,2053],{"class":239},"  });\n",[45,2055,2056],{"class":47,"line":459},[45,2057,292],{"emptyLinePlaceholder":291},[45,2059,2060,2062,2064,2066,2069,2071,2073,2075,2077,2079],{"class":47,"line":498},[45,2061,1953],{"class":313},[45,2063,331],{"class":239},[45,2065,236],{"class":229},[45,2067,2068],{"class":233},"renders a named label slot when provided",[45,2070,236],{"class":229},[45,2072,336],{"class":239},[45,2074,1967],{"class":297},[45,2076,1431],{"class":239},[45,2078,550],{"class":239},[45,2080,553],{"class":239},[45,2082,2083,2085,2087,2089,2091],{"class":47,"line":525},[45,2084,1978],{"class":313},[45,2086,331],{"class":239},[45,2088,1938],{"class":222},[45,2090,336],{"class":239},[45,2092,553],{"class":239},[45,2094,2095,2098,2100,2102,2104,2106,2109,2111],{"class":47,"line":531},[45,2096,2097],{"class":379},"      slots",[45,2099,1993],{"class":239},[45,2101,1996],{"class":379},[45,2103,383],{"class":239},[45,2105,236],{"class":229},[45,2107,2108],{"class":233},"Custom label",[45,2110,236],{"class":229},[45,2112,522],{"class":239},[45,2114,2115],{"class":47,"line":536},[45,2116,2117],{"class":239},"    });\n",[45,2119,2120],{"class":47,"line":556},[45,2121,292],{"emptyLinePlaceholder":291},[45,2123,2124,2126,2128,2130,2132,2134,2136,2138,2140,2142,2144,2146],{"class":47,"line":594},[45,2125,2022],{"class":313},[45,2127,331],{"class":239},[45,2129,2027],{"class":222},[45,2131,310],{"class":239},[45,2133,2032],{"class":313},[45,2135,331],{"class":239},[45,2137,236],{"class":229},[45,2139,2108],{"class":233},[45,2141,236],{"class":229},[45,2143,2043],{"class":239},[45,2145,2046],{"class":313},[45,2147,317],{"class":239},[45,2149,2150],{"class":47,"line":614},[45,2151,2053],{"class":239},[45,2153,2154],{"class":47,"line":622},[45,2155,1332],{"class":239},[10,2157,2158],{},[25,2159,2160],{},"The Beauty of Isolation",[10,2162,2163,2164,2167,2168,2170,2171,2173,2174,2176],{},"Look at the imports. Notice what's missing? ",[25,2165,2166],{},"Nuxt."," This is a pure Vue component test using ",[42,2169,140],{},". Because this component lives in my shared ",[42,2172,509],{}," package and relies on standard Vue props and slots, it is completely framework-agnostic. The Vitest config mounts it in ",[42,2175,134],{},", verifies the DOM output, and gets out of the way fast.",[10,2178,2179],{},"By choosing to test these presentational components in isolation and focusing strictly on what the user actually sees on the screen, I avoid dealing with high maintenance tests altogether..",[10,2181,2182],{},[25,2183,2184],{},"What about Nuxt's auto-imports?",[10,2186,2187,2188,2191,2192,2195,2196,2199,2200],{},"If you’re looking at that ",[42,2189,2190],{},"vitest.config.ts&#x60; and thinking, *\"How does this handle components that actually use Nuxt stuffs like*&#x20;","useFetch",[42,2193,2194],{},"*,*&#x20;","useRoute",[42,2197,2198],{},"*, or*&#x20;","useRuntimeConfig`",[1820,2201,2202],{},"?\"",[10,2204,2205],{},"The honest answer? It doesn't.",[10,2207,2208],{},"I really just don’t want to test those components in Vitest. And honestly, I don't think you should either.",[10,2210,2211,2212,310],{},"Trying to component-test \"smart\" components that are tightly coupled to frameworks could be a trap. The moment you try to run them in Vitest, you'll be having ",[42,2213,2214],{},"ReferenceError: useFetch is not defined",[10,2216,2217,2218,2221,2222,2224],{},"To fix that, you have to mock the router. Then mock the fetch interceptors. Then mock the whole Nuxt context. By the time you write 50 lines of setup code just to get a single button to render, ",[25,2219,2220],{},"you aren't testing your component anymore, you are just testing your mocks."," If Nuxt changes how ",[42,2223,2191],{}," behaves under the hood in a minor update, your tests will instantly break, even if the actual app runs perfectly fine in the browser. It’s just a massive waste of time in my opinion.",[10,2226,2227],{},[25,2228,2229],{},"Enter Playwright",[10,2231,2232],{},"This is why my setup is split. I let Vitest do what it’s actually good at: testing pure business logic and the dumb UI components in my shared layer. It does this in milliseconds without any overhead.",[10,2234,2235],{},"But for the framework-heavy components? That is a job for Playwright.",[10,2237,2238,2239,2241],{},"Playwright doesn't care about mocking ",[42,2240,2191],{}," because it actually boots up the real Nuxt server in a real browser. You test the app exactly how a user interacts with it, and you don't have to write a single mock to do it.",[10,2243,2244],{},[25,2245,2246],{},"Phase 3: The Playwright Setup",[10,2248,2249],{},"So if I'm not testing Nuxt heavy components in Vitest, how do I actually test them?",[10,2251,2252],{},"I run them in a real browser using Playwright.",[10,2254,2255,2256,2259],{},"The codebase has multiple apps sitting in the ",[42,2257,2258],{},"apps\u002F"," directory. I'm definitely not going to be opening three terminal tabs to manually boot up dev servers every time I want to run E2E tests.",[10,2261,2262,2263,2265],{},"Instead, I just let the root ",[42,2264,97],{}," handle the orchestration.",[35,2267,2269],{"className":209,"code":2268,"language":211,"meta":40,"style":40},"import { fileURLToPath } from \"node:url\";\nimport { defineConfig, devices } from \"@playwright\u002Ftest\";\nimport type { ConfigOptions } from \"@nuxt\u002Ftest-utils\u002Fplaywright\";\n\nexport default defineConfig\u003CConfigOptions>({\n  testDir: \".\u002Ftests\u002Fe2e\",\n  testMatch: \"**\u002F*.spec.ts\",\n  fullyParallel: true,\n  forbidOnly: !!process.env.CI,\n  retries: process.env.CI ? 2 : 0,\n  reporter: process.env.CI ? \"github\" : \"list\",\n  outputDir: \".\u002Ftest-results\u002Fplaywright\",\n  use: {\n    ...devices[\"Desktop Chrome\"],\n    baseURL: \"http:\u002F\u002Flocalhost:3000\",\n    trace: \"on-first-retry\",\n    screenshot: \"only-on-failure\",\n    video: \"retain-on-failure\",\n  },\n  projects: [\n    {\n      name: \"admin-chromium\",\n      testMatch: \"admin\u002F**\u002F*.spec.ts\",\n      use: {\n        ...devices[\"Desktop Chrome\"],\n        nuxt: { rootDir: fileURLToPath(new URL(\".\u002Fapps\u002Fadmin\", import.meta.url)) },\n      },\n    },\n    {\n      name: \"distributor-chromium\",\n      testMatch: \"distributor\u002F**\u002F*.spec.ts\",\n      use: {\n        ...devices[\"Desktop Chrome\"],\n        nuxt: { rootDir: fileURLToPath(new URL(\".\u002Fapps\u002Fdistributor\", import.meta.url)) },\n      },\n    },\n    {\n      name: \"provider-chromium\",\n      testMatch: \"provider\u002F**\u002F*.spec.ts\",\n      use: {\n        ...devices[\"Desktop Chrome\"],\n        nuxt: { rootDir: fileURLToPath(new URL(\".\u002Fapps\u002Fprovider\", import.meta.url)) },\n      },\n    },\n  ],\n});\n",[42,2270,2271,2293,2318,2343,2347,2363,2379,2395,2406,2431,2463,2500,2516,2523,2542,2558,2574,2590,2606,2611,2618,2623,2638,2654,2661,2678,2727,2731,2735,2739,2754,2769,2775,2791,2832,2836,2840,2844,2859,2874,2880,2896,2937,2941,2945,2950],{"__ignoreMap":40},[45,2272,2273,2275,2277,2280,2282,2284,2286,2289,2291],{"class":47,"line":48},[45,2274,219],{"class":218},[45,2276,247],{"class":239},[45,2278,2279],{"class":222}," fileURLToPath",[45,2281,253],{"class":239},[45,2283,226],{"class":218},[45,2285,230],{"class":229},[45,2287,2288],{"class":233},"node:url",[45,2290,236],{"class":229},[45,2292,240],{"class":239},[45,2294,2295,2297,2299,2301,2303,2306,2308,2310,2312,2314,2316],{"class":47,"line":54},[45,2296,219],{"class":218},[45,2298,247],{"class":239},[45,2300,273],{"class":222},[45,2302,336],{"class":239},[45,2304,2305],{"class":222}," devices",[45,2307,253],{"class":239},[45,2309,226],{"class":218},[45,2311,230],{"class":229},[45,2313,151],{"class":233},[45,2315,236],{"class":229},[45,2317,240],{"class":239},[45,2319,2320,2322,2325,2327,2330,2332,2334,2336,2339,2341],{"class":47,"line":60},[45,2321,219],{"class":218},[45,2323,2324],{"class":218}," type",[45,2326,247],{"class":239},[45,2328,2329],{"class":222}," ConfigOptions",[45,2331,253],{"class":239},[45,2333,226],{"class":218},[45,2335,230],{"class":229},[45,2337,2338],{"class":233},"@nuxt\u002Ftest-utils\u002Fplaywright",[45,2340,236],{"class":229},[45,2342,240],{"class":239},[45,2344,2345],{"class":47,"line":66},[45,2346,292],{"emptyLinePlaceholder":291},[45,2348,2349,2351,2353,2355,2357,2360],{"class":47,"line":72},[45,2350,539],{"class":218},[45,2352,542],{"class":218},[45,2354,273],{"class":313},[45,2356,1453],{"class":239},[45,2358,2359],{"class":1456},"ConfigOptions",[45,2361,2362],{"class":239},">({\n",[45,2364,2365,2368,2370,2372,2375,2377],{"class":47,"line":78},[45,2366,2367],{"class":379},"  testDir",[45,2369,383],{"class":239},[45,2371,236],{"class":229},[45,2373,2374],{"class":233},".\u002Ftests\u002Fe2e",[45,2376,236],{"class":229},[45,2378,611],{"class":239},[45,2380,2381,2384,2386,2388,2391,2393],{"class":47,"line":84},[45,2382,2383],{"class":379},"  testMatch",[45,2385,383],{"class":239},[45,2387,236],{"class":229},[45,2389,2390],{"class":233},"**\u002F*.spec.ts",[45,2392,236],{"class":229},[45,2394,611],{"class":239},[45,2396,2397,2400,2402,2404],{"class":47,"line":353},[45,2398,2399],{"class":379},"  fullyParallel",[45,2401,383],{"class":239},[45,2403,602],{"class":218},[45,2405,611],{"class":239},[45,2407,2408,2411,2413,2416,2419,2421,2424,2426,2429],{"class":47,"line":360},[45,2409,2410],{"class":379},"  forbidOnly",[45,2412,383],{"class":239},[45,2414,2415],{"class":297},"!!",[45,2417,2418],{"class":222},"process",[45,2420,310],{"class":239},[45,2422,2423],{"class":222},"env",[45,2425,310],{"class":239},[45,2427,2428],{"class":222},"CI",[45,2430,611],{"class":239},[45,2432,2433,2436,2438,2440,2442,2444,2446,2448,2451,2455,2458,2461],{"class":47,"line":373},[45,2434,2435],{"class":379},"  retries",[45,2437,383],{"class":239},[45,2439,2418],{"class":222},[45,2441,310],{"class":239},[45,2443,2423],{"class":222},[45,2445,310],{"class":239},[45,2447,2428],{"class":222},[45,2449,2450],{"class":297}," ?",[45,2452,2454],{"class":2453},"sqbOQ"," 2",[45,2456,2457],{"class":297}," :",[45,2459,2460],{"class":2453}," 0",[45,2462,611],{"class":239},[45,2464,2465,2468,2470,2472,2474,2476,2478,2480,2482,2484,2487,2489,2491,2493,2496,2498],{"class":47,"line":420},[45,2466,2467],{"class":379},"  reporter",[45,2469,383],{"class":239},[45,2471,2418],{"class":222},[45,2473,310],{"class":239},[45,2475,2423],{"class":222},[45,2477,310],{"class":239},[45,2479,2428],{"class":222},[45,2481,2450],{"class":297},[45,2483,230],{"class":229},[45,2485,2486],{"class":233},"github",[45,2488,236],{"class":229},[45,2490,2457],{"class":297},[45,2492,230],{"class":229},[45,2494,2495],{"class":233},"list",[45,2497,236],{"class":229},[45,2499,611],{"class":239},[45,2501,2502,2505,2507,2509,2512,2514],{"class":47,"line":459},[45,2503,2504],{"class":379},"  outputDir",[45,2506,383],{"class":239},[45,2508,236],{"class":229},[45,2510,2511],{"class":233},".\u002Ftest-results\u002Fplaywright",[45,2513,236],{"class":229},[45,2515,611],{"class":239},[45,2517,2518,2521],{"class":47,"line":498},[45,2519,2520],{"class":379},"  use",[45,2522,628],{"class":239},[45,2524,2525,2528,2531,2533,2535,2538,2540],{"class":47,"line":525},[45,2526,2527],{"class":239},"    ...",[45,2529,2530],{"class":222},"devices",[45,2532,1762],{"class":239},[45,2534,236],{"class":229},[45,2536,2537],{"class":233},"Desktop Chrome",[45,2539,236],{"class":229},[45,2541,750],{"class":239},[45,2543,2544,2547,2549,2551,2554,2556],{"class":47,"line":531},[45,2545,2546],{"class":379},"    baseURL",[45,2548,383],{"class":239},[45,2550,236],{"class":229},[45,2552,2553],{"class":233},"http:\u002F\u002Flocalhost:3000",[45,2555,236],{"class":229},[45,2557,611],{"class":239},[45,2559,2560,2563,2565,2567,2570,2572],{"class":47,"line":536},[45,2561,2562],{"class":379},"    trace",[45,2564,383],{"class":239},[45,2566,236],{"class":229},[45,2568,2569],{"class":233},"on-first-retry",[45,2571,236],{"class":229},[45,2573,611],{"class":239},[45,2575,2576,2579,2581,2583,2586,2588],{"class":47,"line":556},[45,2577,2578],{"class":379},"    screenshot",[45,2580,383],{"class":239},[45,2582,236],{"class":229},[45,2584,2585],{"class":233},"only-on-failure",[45,2587,236],{"class":229},[45,2589,611],{"class":239},[45,2591,2592,2595,2597,2599,2602,2604],{"class":47,"line":594},[45,2593,2594],{"class":379},"    video",[45,2596,383],{"class":239},[45,2598,236],{"class":229},[45,2600,2601],{"class":233},"retain-on-failure",[45,2603,236],{"class":229},[45,2605,611],{"class":239},[45,2607,2608],{"class":47,"line":614},[45,2609,2610],{"class":239},"  },\n",[45,2612,2613,2616],{"class":47,"line":622},[45,2614,2615],{"class":379},"  projects",[45,2617,1209],{"class":239},[45,2619,2620],{"class":47,"line":631},[45,2621,2622],{"class":239},"    {\n",[45,2624,2625,2627,2629,2631,2634,2636],{"class":47,"line":639},[45,2626,711],{"class":379},[45,2628,383],{"class":239},[45,2630,236],{"class":229},[45,2632,2633],{"class":233},"admin-chromium",[45,2635,236],{"class":229},[45,2637,611],{"class":239},[45,2639,2640,2643,2645,2647,2650,2652],{"class":47,"line":656},[45,2641,2642],{"class":379},"      testMatch",[45,2644,383],{"class":239},[45,2646,236],{"class":229},[45,2648,2649],{"class":233},"admin\u002F**\u002F*.spec.ts",[45,2651,236],{"class":229},[45,2653,611],{"class":239},[45,2655,2656,2659],{"class":47,"line":672},[45,2657,2658],{"class":379},"      use",[45,2660,628],{"class":239},[45,2662,2663,2666,2668,2670,2672,2674,2676],{"class":47,"line":688},[45,2664,2665],{"class":239},"        ...",[45,2667,2530],{"class":222},[45,2669,1762],{"class":239},[45,2671,236],{"class":229},[45,2673,2537],{"class":233},[45,2675,236],{"class":229},[45,2677,750],{"class":239},[45,2679,2680,2683,2685,2688,2690,2693,2695,2698,2701,2703,2705,2708,2710,2712,2714,2716,2719,2721,2724],{"class":47,"line":694},[45,2681,2682],{"class":379},"        nuxt",[45,2684,1993],{"class":239},[45,2686,2687],{"class":379},"rootDir",[45,2689,383],{"class":239},[45,2691,2692],{"class":313},"fileURLToPath",[45,2694,331],{"class":239},[45,2696,2697],{"class":297},"new",[45,2699,2700],{"class":313}," URL",[45,2702,331],{"class":239},[45,2704,236],{"class":229},[45,2706,2707],{"class":233},".\u002Fapps\u002Fadmin",[45,2709,236],{"class":229},[45,2711,393],{"class":239},[45,2713,219],{"class":218},[45,2715,310],{"class":239},[45,2717,2718],{"class":379},"meta",[45,2720,310],{"class":239},[45,2722,2723],{"class":222},"url",[45,2725,2726],{"class":239},")) },\n",[45,2728,2729],{"class":47,"line":700},[45,2730,691],{"class":239},[45,2732,2733],{"class":47,"line":708},[45,2734,697],{"class":239},[45,2736,2737],{"class":47,"line":734},[45,2738,2622],{"class":239},[45,2740,2741,2743,2745,2747,2750,2752],{"class":47,"line":753},[45,2742,711],{"class":379},[45,2744,383],{"class":239},[45,2746,236],{"class":229},[45,2748,2749],{"class":233},"distributor-chromium",[45,2751,236],{"class":229},[45,2753,611],{"class":239},[45,2755,2756,2758,2760,2762,2765,2767],{"class":47,"line":774},[45,2757,2642],{"class":379},[45,2759,383],{"class":239},[45,2761,236],{"class":229},[45,2763,2764],{"class":233},"distributor\u002F**\u002F*.spec.ts",[45,2766,236],{"class":229},[45,2768,611],{"class":239},[45,2770,2771,2773],{"class":47,"line":800},[45,2772,2658],{"class":379},[45,2774,628],{"class":239},[45,2776,2777,2779,2781,2783,2785,2787,2789],{"class":47,"line":805},[45,2778,2665],{"class":239},[45,2780,2530],{"class":222},[45,2782,1762],{"class":239},[45,2784,236],{"class":229},[45,2786,2537],{"class":233},[45,2788,236],{"class":229},[45,2790,750],{"class":239},[45,2792,2793,2795,2797,2799,2801,2803,2805,2807,2809,2811,2813,2816,2818,2820,2822,2824,2826,2828,2830],{"class":47,"line":811},[45,2794,2682],{"class":379},[45,2796,1993],{"class":239},[45,2798,2687],{"class":379},[45,2800,383],{"class":239},[45,2802,2692],{"class":313},[45,2804,331],{"class":239},[45,2806,2697],{"class":297},[45,2808,2700],{"class":313},[45,2810,331],{"class":239},[45,2812,236],{"class":229},[45,2814,2815],{"class":233},".\u002Fapps\u002Fdistributor",[45,2817,236],{"class":229},[45,2819,393],{"class":239},[45,2821,219],{"class":218},[45,2823,310],{"class":239},[45,2825,2718],{"class":379},[45,2827,310],{"class":239},[45,2829,2723],{"class":222},[45,2831,2726],{"class":239},[45,2833,2834],{"class":47,"line":816},[45,2835,691],{"class":239},[45,2837,2838],{"class":47,"line":846},[45,2839,697],{"class":239},[45,2841,2842],{"class":47,"line":861},[45,2843,2622],{"class":239},[45,2845,2846,2848,2850,2852,2855,2857],{"class":47,"line":868},[45,2847,711],{"class":379},[45,2849,383],{"class":239},[45,2851,236],{"class":229},[45,2853,2854],{"class":233},"provider-chromium",[45,2856,236],{"class":229},[45,2858,611],{"class":239},[45,2860,2861,2863,2865,2867,2870,2872],{"class":47,"line":882},[45,2862,2642],{"class":379},[45,2864,383],{"class":239},[45,2866,236],{"class":229},[45,2868,2869],{"class":233},"provider\u002F**\u002F*.spec.ts",[45,2871,236],{"class":229},[45,2873,611],{"class":239},[45,2875,2876,2878],{"class":47,"line":889},[45,2877,2658],{"class":379},[45,2879,628],{"class":239},[45,2881,2882,2884,2886,2888,2890,2892,2894],{"class":47,"line":896},[45,2883,2665],{"class":239},[45,2885,2530],{"class":222},[45,2887,1762],{"class":239},[45,2889,236],{"class":229},[45,2891,2537],{"class":233},[45,2893,236],{"class":229},[45,2895,750],{"class":239},[45,2897,2898,2900,2902,2904,2906,2908,2910,2912,2914,2916,2918,2921,2923,2925,2927,2929,2931,2933,2935],{"class":47,"line":911},[45,2899,2682],{"class":379},[45,2901,1993],{"class":239},[45,2903,2687],{"class":379},[45,2905,383],{"class":239},[45,2907,2692],{"class":313},[45,2909,331],{"class":239},[45,2911,2697],{"class":297},[45,2913,2700],{"class":313},[45,2915,331],{"class":239},[45,2917,236],{"class":229},[45,2919,2920],{"class":233},".\u002Fapps\u002Fprovider",[45,2922,236],{"class":229},[45,2924,393],{"class":239},[45,2926,219],{"class":218},[45,2928,310],{"class":239},[45,2930,2718],{"class":379},[45,2932,310],{"class":239},[45,2934,2723],{"class":222},[45,2936,2726],{"class":239},[45,2938,2939],{"class":47,"line":926},[45,2940,691],{"class":239},[45,2942,2943],{"class":47,"line":941},[45,2944,697],{"class":239},[45,2946,2947],{"class":47,"line":946},[45,2948,2949],{"class":239},"  ],\n",[45,2951,2952],{"class":47,"line":951},[45,2953,1332],{"class":239},[10,2955,2956],{},[25,2957,1337],{},[10,2959,2960],{},"There are two details in this config that save me from doing manual setup:",[178,2962,2963,2986],{},[114,2964,2965,1366,2968,1366,2970,2973,2974,2977,2978,2981,2982,2985],{},[25,2966,2967],{},"The",[42,2969,2338],{},[25,2971,2972],{},"Integration:"," Instead of standard Playwright types, I typed the config with ",[42,2975,2976],{},"\u003CConfigOptions>"," from Nuxt's official test utilities. This exposes the ",[42,2979,2980],{},"nuxt"," key in the ",[42,2983,2984],{},"use"," block.",[114,2987,2988,2991,2992,2995,2996,310],{},[25,2989,2990],{},"Project-Scoped Roots:"," In the ",[42,2993,2994],{},"projects"," array, I explicitly pass the path to each app's directory using ",[42,2997,2692],{},[10,2999,3000,3001,3004],{},"Because of this, Playwright is smart. When I run ",[42,3002,3003],{},"npx playwright test",", it looks at the project, maps it to the correct directory in my monorepo, automatically builds and boots the local Nuxt server for that specific app, runs the tests inside a real browser, and shuts the server down when it's finished.",[10,3006,3007],{},"I don't have to manage any local ports, I don't have to coordinate server lifecycles in my CI\u002FCD pipeline, and I don't have to manually mock a single API call. It just works.",[10,3009,3010],{},[25,3011,3012],{},"The Pragmatic Monorepo",[10,3014,3015],{},"Setting up tests in a monorepo usually feels like fighting against your tools. But by letting each tool do exactly what it was built for, testing actually becomes invisible.",[111,3017,3018,3024,3030],{},[114,3019,3020,3023],{},[25,3021,3022],{},"Vitest"," handles the pure logic and presentational UI components in milliseconds.",[114,3025,3026,3029],{},[25,3027,3028],{},"Playwright"," boots up the Nuxt apps and tests them like a real user.",[114,3031,3032,3035],{},[25,3033,3034],{},"The Root Configs"," act as the orchestrators, meaning I don't have to jump between a terminal tabs to get confidence in my code.",[10,3037,3038],{},"I don't think I need 100% test coverage on every single layer. I just need tests that are fast, reliable, and actually catch bugs without forcing me to write 50 lines of mocks.",[10,3040,3041],{},[25,3042,3043],{},"At what point do you give up on unit testing a component and just let E2E handle it? Do you mock out your framework utilities in Vitest, or push them straight to Playwright? Let me know what you think.",[3045,3046,3047],"style",{},"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 .sTPum, html code.shiki .sTPum{--shiki-default:#1E754F;--shiki-dark:#4D9375}html pre.shiki code .s9nN2, html code.shiki .s9nN2{--shiki-default:#B07D48;--shiki-dark:#BD976A}html pre.shiki code .scnC2, html code.shiki .scnC2{--shiki-default:#B5695977;--shiki-dark:#C98A7D77}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html pre.shiki code .stPdD, html code.shiki .stPdD{--shiki-default:#999999;--shiki-dark:#444444}html pre.shiki code .s5TCs, html code.shiki .s5TCs{--shiki-default:#AB5959;--shiki-dark:#CB7676}html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}html pre.shiki code .sHLBJ, html code.shiki .sHLBJ{--shiki-default:#998418;--shiki-dark:#B8A965}html pre.shiki code .s_NWU, html code.shiki .s_NWU{--shiki-default:#2E8F82;--shiki-dark:#5DA994}html pre.shiki code .sqbOQ, html code.shiki .sqbOQ{--shiki-default:#2F798A;--shiki-dark:#4C9A91}",{"title":40,"searchDepth":54,"depth":54,"links":3049},[],"Testing a standard, single-package app is pretty straightforward. But a Nuxt monorepo? That can get messy very fast. When you're dealing with multiple shared packages...","2026-07-15T12:12:54.476Z","Centralised configs, isolated component tests, and reliable E2E with Vitest & Playwright, without the mocking headache.","md",{},"\u002Fblog\u002Ftesting-a-nuxt-monorepo-without-losing-your-mind","10",{"title":5,"description":3052},"testing-a-nuxt-monorepo-without-losing-your-mind","blog\u002Ftesting-a-nuxt-monorepo-without-losing-your-mind","4r0EbT5OlDM-d7vyWyoTcmNXIRybms2qio1RMPQ15JM",1784360092461]