{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/jvns-ca/2026-05-02-testing-vue-components-in-the-browser/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "Hello! One of my long term projects on here is [figuring out how to write frontend Javascript without using Node](https://jvns.ca/#javascript) or any other server JS runtime.\n\nOne issue I run into a lot in my frontend JS projects is that I don’t know how to write tests for them. I’ve tried to use Playwright in the past, but it felt slow and unwieldy to be starting these new browser processes all the time, and it involved some Node code to orchestrate the tests.\n\nThe result is that I just don’t test my frontend code which doesn’t feel great. Usually I don’t update my projects much either so it doesn’t come up that much, but it would be nice to be able to make changes with more confidence! So a way to do frontend testing that I like has been on my wishlist for a long time.\n\n### [idea: just run the tests in the browser tab](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#idea-just-run-the-tests-in-the-browser-tab)\n\nAlex Chan wrote a great post a while back called [Testing JavaScript without a (third-party) framework](https://alexwlchan.net/2023/testing-javascript-without-a-framework/) in response to one of my previous posts in this series that explained how to write a tiny unit-testing framework that runs in a page in browser.\n\nI loved this post at the time, but it only talked about unit testing and I wanted to write end-to-end integration tests for my Vue components, and I didn’t know how to do that.\n\nSo when I was talking to [Marco](https://bsky.app/profile/polotek.bsky.social) the other day and he said something like “you know, you can just run tests for your Vue components in the browser”, I thought “hey, I should try that again!!!”\n\nI just did all of this yesterday so certainly there’s a lot to improve but I wanted to write down a few things I noticed about the process before I forget.\n\nThis was a bit tricky for me because the Vue site usually assumes that you’re using Node as part of your build process in some way (there’s a lot of “step 1: `npm install THING`), and I didn’t want to use Node/Deno/etc. But it turned out to not be too complicated.\n\nThe project I’m going to talk about testing is this [zine feedback site I wrote in 2023](https://jvns.ca/blog/2023/03/31/zine-feedback-site/).\n\n### [the test framework: QUnit](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#the-test-framework-qunit)\n\nI used [QUnit](https://qunitjs.com/). It worked great but I don’t have anything interesting to say about how it works so I’ll leave it at that. I think that Alex’s “write your own test framework” approach would have worked too. I followed [these directions](https://qunitjs.com/browser/).\n\nI did appreciate that QUnit has a “rerun test” button that will only rerun 1 test. Because there are so many network requests in my tests, having a way to run just 1 test makes it a lot less confusing to debug the test.\n\n### [step 1: set up the component for testing](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#step-1-set-up-the-component-for-testing)\n\nThe first thing I needed to do was get my Vue components set up in the test environment.\n\nI changed my main app to put all my components in `window._components`, kind of like this:\n\n```\nconst components = {\n  'Feedback': FeedbackComponent,\n  ...\n}\nwindow._components = components;\n```\n\nThen I was able to write a `mountComponent` function which does basically exactly the same thing my normal main app does (render a tiny template with the component I want to use). The only differences are:\n\n1. I can optionally pass some some extra data to use as its props.\n2. It mounts the component to a temporary invisible div which will get removed from the DOM after the test is done. The div is positioned off the page (`position: absolute; top: -10000, ...`) so you can’t see it.\n\nHere’s what using the `mountComponent` function looks like:\n\n```\nconst {div} = mountComponent(\n  '<Page :feedbacks=\"feedbacks\" id=2 />',\n  {feedbacks: [testFeedback]},\n);\n```\n\nand here’s the code for it:\n\n```\nfunction mountComponent(template, data) {\n  const app = Vue.createApp({\n    template: template,\n    data: () => data,\n  })\n  for (const [c, v] of Object.entries(window._components)) {\n    app.component(c, v);\n  }\n  const div = document.getElementById('qunit-fixture')\n             .appendChild(document.createElement('div'));\n  return div;\n}\n```\n\nThe result is a div where I can programmatically click, fill in form data, check that the right content appears, etc.\n\n### [step 2: add some fixture data](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#step-2-add-some-fixture-data)\n\nBecause I was writing end-to-end integration tests to make sure my client JS worked properly with my server, I needed to have some test data in my database. So I wrote ~25 lines of SQL to set up some test data in my database, and added an endpoint to my dev server to run the SQL to reset the test data to a known state.\n\n```\nasync function reset() {\n    return fetch('/api/reset_test_data', {method: \"POST\"})\n}\n```\n\nThen I just run `await reset()` at the beginning of any test that needs the test data.\n\nMy `reset()` function actually doesn’t always totally reset everything which is kind of bad, but it was workable to start with and can always be improved.\n\n### [step 3: a basic test](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#step-3-a-basic-test)\n\nHere’s what a basic test looks like! Basically we’re rendering the div and make sure it contains some approximately correct data.\n\n```\nQUnit.test('renders feedback content', async function (assert) {\n  const {div} = mountComponent(\n    '<Page :feedbacks=\"feedbacks\" id=2 image=2 page_hash=2 />',\n    {feedbacks: [testFeedback]},\n  );\n  assert.ok(div.textContent.includes('loved this section'));\n})\n```\n\nThose are all the basic pieces! Now here are a few issues I ran into along the way\n\n### [waiting for parts of the page to render](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#waiting-for-parts-of-the-page-to-render)\n\nI have a lot of network requests in my tests, and it takes time for them to finish and for the Vue code to do what it has to do with the results and update the DOM.\n\nI think we all learned a long time ago that putting random `sleep()` calls in your tests and hoping that the timings are right is slow and flaky and extremely frustrating, so I needed a different way.\n\nAs far as I can tell the normal way to deal with this is to figure out a way to tell from the DOM whether it’s okay to proceed or not. Like “if this button is visible, we can “.\n\nSo I wrote a little `waitFor()` function that polls every 20ms to see if a condition has finished yet. It times out after 2 seconds.\n\nHere’s what using it looks like:\n\n```\nQUnit.test(\"click item\", async function (assert) {\n  const {div} = mountComponent(\n    '<Feedback zine_id=\"test123\" image_width=\"800px\" />',\n    {});\n  const item = await waitFor(() => div.querySelector('.feedback-item'));\n  item.click();\n  // rest of test goes here... \n})\n```\n\nIt looks like there are a lot of implementations of this concept out there and they’re all better thought-through than mine. (from a quick Google: [qunit-wait-for](https://www.npmjs.com/package/qunit-wait-for), [playwright expect.poll](https://playwright.dev/docs/testing-library#replacing-waitfor))\n\n### [figuring out the right thing to wait for is not straightforward](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#figuring-out-the-right-thing-to-wait-for-is-not-straightforward)\n\nIn some cases I *thought* I’d identified the right thing to wait for in the DOM (“just wait for this textarea to appear!’) but it turned out that because of some internal details of how my program works, actually I needed to wait for something else later on which was hard to pin down.\n\nI ended up changing one of my components to add some random value to the DOM when it was finished an important action (like `data-this-thing-is-ready=true`) which didn’t feel great.\n\nMy best guess is that the right way to fix this kind of test issue is a refactor that also makes the app more reliable for the users: if there’s an element in the DOM that isn’t actually ready for the user to interact with, maybe I shouldn’t be displaying it yet!\n\n### [adding some CSS classes to identify things (but is that right?)](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#adding-some-css-classes-to-identify-things-but-is-that-right)\n\nI ended up adding a few classes to HTML elements that I needed to find in the tests, either because I needed to click on them or wait for them to appear in the DOM.\n\nI might want to change this approach later - frontend testing frameworks seem to suggest avoiding using CSS classes and instead using something like [getByRole](https://playwright.dev/docs/api/class-framelocator#frame-locator-get-by-role) or as a last resort something like a [data-testid](https://testing-library.com/docs/dom-testing-library/intro/). Feels like there’s a way to make the app more accessible and easier to test at the same time.\n\n### [filling out forms is tricky](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#filling-out-forms-is-tricky)\n\nTo fill out a form, I can’t just set the `value`, I also need to dispatch an event to tell Vue that the element has changed. For example, `checkbox` and `textarea` need different kinds of events.\n\n```\ntextarea.value = 'banana banana banana';\ntextarea.dispatchEvent(new Event('input'));\n```\n\n```\ncheckbox.checked = true;\ncheckbox.dispatchEvent(new Event('change'));\n```\n\nThis is kind of annoying and it made me realize why I might want to use some kind of UI testing library, for example:\n\n- Testing Library’s [example of filling out a form](https://testing-library.com/docs/example-react-formik) looks extremely different from what I’m doing\n- Vue Test Utils: their [section on form handling](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/content/post/2026-05-02-testing-javascript-in-the-browser.markdown) looks like it simplifies this a lot.\n\n### [test coverage](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#test-coverage)\n\nI want to have an idea of what my test coverage was, and it turns out that Chrome actually has a built-in [code coverage](https://developer.chrome.com/docs/devtools/coverage) feature for JS and CSS!\n\nMy JS is bundled into a file called `bundle.js` with esbuild, so I could just look at `bundle.js` and see which lines weren’t covered.\n\nThe process was a little finicky: I had to turn off sourcemaps in the Chrome devtools to get this to work, and there’s a specific not super obvious series of actions I have to do in order to see the coverage data.\n\n### [this was so fun!](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#this-was-so-fun)\n\nAs usual with these posts I’ve never really worked as a frontend or backend developer (other than for myself!) and I feel like I’m constantly learning how to do super basic tasks.\n\nI really had a blast doing this. My frontend projects always feel so fragile because they’re untested, and maybe one day I’ll have a test suite I’m confident in!\n\nSome things I’m still thinking about:\n\n- While writing this post I found this frontend testing library called [Testing Library](https://testing-library.com/) that has a lot of guidelines for how to write tests that are very different from my initial ideas. I experimented with rewriting everything to use Testing Library and it felt pretty good, so we’ll see how that goes. They distribute a `.umd.js` file that works without Node.\n- I’m not sure how I feel about not having a way to run these tests on the command line at all. Maybe there’s a simple way to work primarily in the browser but have an way to run them in CI too if I want?\n",
  "authors": [],
  "captured_at": "2026-09-19T21:30:23.395188495+00:00",
  "category": "programming",
  "content_html": "<p>Hello! One of my long term projects on here is <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://jvns.ca/#javascript\">figuring out how to write frontend Javascript without using Node</a> or any other server JS runtime.</p>\n<p>One issue I run into a lot in my frontend JS projects is that I don’t know how to write tests for them. I’ve tried to use Playwright in the past, but it felt slow and unwieldy to be starting these new browser processes all the time, and it involved some Node code to orchestrate the tests.</p>\n<p>The result is that I just don’t test my frontend code which doesn’t feel great. Usually I don’t update my projects much either so it doesn’t come up that much, but it would be nice to be able to make changes with more confidence! So a way to do frontend testing that I like has been on my wishlist for a long time.</p>\n<h3><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#idea-just-run-the-tests-in-the-browser-tab\">idea: just run the tests in the browser tab</a></h3>\n<p>Alex Chan wrote a great post a while back called <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://alexwlchan.net/2023/testing-javascript-without-a-framework/\">Testing JavaScript without a (third-party) framework</a> in response to one of my previous posts in this series that explained how to write a tiny unit-testing framework that runs in a page in browser.</p>\n<p>I loved this post at the time, but it only talked about unit testing and I wanted to write end-to-end integration tests for my Vue components, and I didn’t know how to do that.</p>\n<p>So when I was talking to <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://bsky.app/profile/polotek.bsky.social\">Marco</a> the other day and he said something like “you know, you can just run tests for your Vue components in the browser”, I thought “hey, I should try that again!!!”</p>\n<p>I just did all of this yesterday so certainly there’s a lot to improve but I wanted to write down a few things I noticed about the process before I forget.</p>\n<p>This was a bit tricky for me because the Vue site usually assumes that you’re using Node as part of your build process in some way (there’s a lot of “step 1: <code>npm install THING</code>), and I didn’t want to use Node/Deno/etc. But it turned out to not be too complicated.</p>\n<p>The project I’m going to talk about testing is this <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://jvns.ca/blog/2023/03/31/zine-feedback-site/\">zine feedback site I wrote in 2023</a>.</p>\n<h3><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#the-test-framework-qunit\">the test framework: QUnit</a></h3>\n<p>I used <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://qunitjs.com/\">QUnit</a>. It worked great but I don’t have anything interesting to say about how it works so I’ll leave it at that. I think that Alex’s “write your own test framework” approach would have worked too. I followed <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://qunitjs.com/browser/\">these directions</a>.</p>\n<p>I did appreciate that QUnit has a “rerun test” button that will only rerun 1 test. Because there are so many network requests in my tests, having a way to run just 1 test makes it a lot less confusing to debug the test.</p>\n<h3><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#step-1-set-up-the-component-for-testing\">step 1: set up the component for testing</a></h3>\n<p>The first thing I needed to do was get my Vue components set up in the test environment.</p>\n<p>I changed my main app to put all my components in <code>window._components</code>, kind of like this:</p>\n<pre><code><span class=\"code-snippet\">const components = {\n  &#39;Feedback&#39;: FeedbackComponent,\n  ...\n}\nwindow._components = components;\n</span></code></pre>\n<p>Then I was able to write a <code>mountComponent</code> function which does basically exactly the same thing my normal main app does (render a tiny template with the component I want to use). The only differences are:</p>\n<ol>\n<li>I can optionally pass some some extra data to use as its props.</li>\n<li>It mounts the component to a temporary invisible div which will get removed from the DOM after the test is done. The div is positioned off the page (<code>position: absolute; top: -10000, ...</code>) so you can’t see it.</li>\n</ol>\n<p>Here’s what using the <code>mountComponent</code> function looks like:</p>\n<pre><code><span class=\"code-snippet\">const {div} = mountComponent(\n  &#39;&lt;Page :feedbacks=&quot;feedbacks&quot; id=2 /&gt;&#39;,\n  {feedbacks: [testFeedback]},\n);\n</span></code></pre>\n<p>and here’s the code for it:</p>\n<pre><code><span class=\"code-snippet\" data-language=\"JavaScript\"><span class=\"syntax-source syntax-js\"><span class=\"syntax-meta syntax-function syntax-declaration syntax-js\"><span class=\"syntax-storage syntax-type syntax-function syntax-js\">function</span> <span class=\"syntax-entity syntax-name syntax-function syntax-js\">mountComponent</span><span class=\"syntax-punctuation syntax-section syntax-group syntax-begin syntax-js\">(</span><span class=\"syntax-variable syntax-parameter syntax-function syntax-js\">template</span><span class=\"syntax-punctuation syntax-separator syntax-parameter syntax-function syntax-js\">,</span> <span class=\"syntax-variable syntax-parameter syntax-function syntax-js\">data</span><span class=\"syntax-punctuation syntax-section syntax-group syntax-end syntax-js\">)</span><span class=\"syntax-meta syntax-function syntax-declaration syntax-js\"> </span></span><span class=\"syntax-meta syntax-block syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span>\n  <span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">app</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-variable syntax-other syntax-object syntax-js\">Vue</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">createApp</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-meta syntax-object-literal syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span>\n    <span class=\"syntax-meta syntax-object-literal syntax-key syntax-js\">template</span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-js\">:</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">template</span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span>\n    <span class=\"syntax-meta syntax-function syntax-declaration syntax-js\"><span class=\"syntax-meta syntax-object-literal syntax-key syntax-js\"><span class=\"syntax-entity syntax-name syntax-function syntax-js\">data</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-js\">:</span> </span><span class=\"syntax-meta syntax-function syntax-declaration syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-begin syntax-js\">(</span><span class=\"syntax-punctuation syntax-section syntax-group syntax-end syntax-js\">)</span><span class=\"syntax-meta syntax-function syntax-declaration syntax-js\"> </span><span class=\"syntax-storage syntax-type syntax-function syntax-arrow syntax-js\">=&gt;</span></span> <span class=\"syntax-meta syntax-block syntax-js\"><span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">data</span></span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span>\n  <span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span>\n  <span class=\"syntax-meta syntax-for syntax-js\"><span class=\"syntax-keyword syntax-control syntax-loop syntax-js\">for</span> <span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-meta syntax-sequence syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-js\">[</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">c</span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">v</span><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-js\">]</span></span> <span class=\"syntax-keyword syntax-operator syntax-js\">of</span> <span class=\"syntax-support syntax-class syntax-builtin syntax-js\">Object</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">entries</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-support syntax-type syntax-object syntax-dom syntax-js\">window</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-property syntax-object syntax-js\">_components</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span> <span class=\"syntax-meta syntax-block syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span>\n    <span class=\"syntax-variable syntax-other syntax-object syntax-js\">app</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">component</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">c</span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">v</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n  </span><span class=\"syntax-meta syntax-block syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span></span>\n  <span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">div</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-support syntax-type syntax-object syntax-dom syntax-js\">document</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-support syntax-function syntax-dom syntax-js\">getElementById</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>qunit-fixture<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span>\n             <span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-support syntax-function syntax-dom syntax-js\">appendChild</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-support syntax-type syntax-object syntax-dom syntax-js\">document</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-support syntax-function syntax-dom syntax-js\">createElement</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>div<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n  <span class=\"syntax-keyword syntax-control syntax-flow syntax-js\">return</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">div</span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n<span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span>\n</span></span></code></pre>\n<p>The result is a div where I can programmatically click, fill in form data, check that the right content appears, etc.</p>\n<h3><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#step-2-add-some-fixture-data\">step 2: add some fixture data</a></h3>\n<p>Because I was writing end-to-end integration tests to make sure my client JS worked properly with my server, I needed to have some test data in my database. So I wrote ~25 lines of SQL to set up some test data in my database, and added an endpoint to my dev server to run the SQL to reset the test data to a known state.</p>\n<pre><code><span class=\"code-snippet\">async function reset() {\n    return fetch(&#39;/api/reset_test_data&#39;, {method: &quot;POST&quot;})\n}\n</span></code></pre>\n<p>Then I just run <code>await reset()</code> at the beginning of any test that needs the test data.</p>\n<p>My <code>reset()</code> function actually doesn’t always totally reset everything which is kind of bad, but it was workable to start with and can always be improved.</p>\n<h3><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#step-3-a-basic-test\">step 3: a basic test</a></h3>\n<p>Here’s what a basic test looks like! Basically we’re rendering the div and make sure it contains some approximately correct data.</p>\n<pre><code><span class=\"code-snippet\" data-language=\"JavaScript\"><span class=\"syntax-source syntax-js\"><span class=\"syntax-variable syntax-other syntax-object syntax-js\">QUnit</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">test</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>renders feedback content<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span> <span class=\"syntax-meta syntax-function syntax-declaration syntax-js\"><span class=\"syntax-storage syntax-type syntax-js\">async</span> <span class=\"syntax-storage syntax-type syntax-function syntax-js\">function</span> <span class=\"syntax-punctuation syntax-section syntax-group syntax-begin syntax-js\">(</span><span class=\"syntax-variable syntax-parameter syntax-function syntax-js\">assert</span><span class=\"syntax-punctuation syntax-section syntax-group syntax-end syntax-js\">)</span><span class=\"syntax-meta syntax-function syntax-declaration syntax-js\"> </span></span><span class=\"syntax-meta syntax-block syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span>\n  <span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-meta syntax-object-literal syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">div</span><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-meta syntax-function-call syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">mountComponent</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span>\n    <span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>&lt;Page :feedbacks=&quot;feedbacks&quot; id=2 image=2 page_hash=2 /&gt;<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span>\n    <span class=\"syntax-meta syntax-object-literal syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span><span class=\"syntax-meta syntax-object-literal syntax-key syntax-js\">feedbacks</span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-js\">:</span> <span class=\"syntax-meta syntax-sequence syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-js\">[</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">testFeedback</span><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-js\">]</span></span><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span>\n  </span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n  <span class=\"syntax-variable syntax-other syntax-object syntax-js\">assert</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">ok</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-variable syntax-other syntax-object syntax-js\">div</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-property syntax-object syntax-js\">textContent</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">includes</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>loved this section<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n<span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span>\n</span></span></code></pre>\n<p>Those are all the basic pieces! Now here are a few issues I ran into along the way</p>\n<h3><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#waiting-for-parts-of-the-page-to-render\">waiting for parts of the page to render</a></h3>\n<p>I have a lot of network requests in my tests, and it takes time for them to finish and for the Vue code to do what it has to do with the results and update the DOM.</p>\n<p>I think we all learned a long time ago that putting random <code>sleep()</code> calls in your tests and hoping that the timings are right is slow and flaky and extremely frustrating, so I needed a different way.</p>\n<p>As far as I can tell the normal way to deal with this is to figure out a way to tell from the DOM whether it’s okay to proceed or not. Like “if this button is visible, we can “.</p>\n<p>So I wrote a little <code>waitFor()</code> function that polls every 20ms to see if a condition has finished yet. It times out after 2 seconds.</p>\n<p>Here’s what using it looks like:</p>\n<pre><code><span class=\"code-snippet\" data-language=\"JavaScript\"><span class=\"syntax-source syntax-js\"><span class=\"syntax-variable syntax-other syntax-object syntax-js\">QUnit</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">test</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-string syntax-quoted syntax-double syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&quot;</span>click item<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&quot;</span></span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span> <span class=\"syntax-meta syntax-function syntax-declaration syntax-js\"><span class=\"syntax-storage syntax-type syntax-js\">async</span> <span class=\"syntax-storage syntax-type syntax-function syntax-js\">function</span> <span class=\"syntax-punctuation syntax-section syntax-group syntax-begin syntax-js\">(</span><span class=\"syntax-variable syntax-parameter syntax-function syntax-js\">assert</span><span class=\"syntax-punctuation syntax-section syntax-group syntax-end syntax-js\">)</span><span class=\"syntax-meta syntax-function syntax-declaration syntax-js\"> </span></span><span class=\"syntax-meta syntax-block syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span>\n  <span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-meta syntax-object-literal syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">div</span><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-meta syntax-function-call syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">mountComponent</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span>\n    <span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>&lt;Feedback zine_id=&quot;test123&quot; image_width=&quot;800px&quot; /&gt;<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span>\n    <span class=\"syntax-meta syntax-object-literal syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n  <span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">item</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">await</span> <span class=\"syntax-meta syntax-function-call syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">waitFor</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-meta syntax-function syntax-declaration syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-begin syntax-js\">(</span><span class=\"syntax-punctuation syntax-section syntax-group syntax-end syntax-js\">)</span><span class=\"syntax-meta syntax-function syntax-declaration syntax-js\"> </span><span class=\"syntax-storage syntax-type syntax-function syntax-arrow syntax-js\">=&gt;</span></span> <span class=\"syntax-meta syntax-block syntax-js\"><span class=\"syntax-variable syntax-other syntax-object syntax-js\">div</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">querySelector</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>.feedback-item<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n  <span class=\"syntax-variable syntax-other syntax-object syntax-js\">item</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-support syntax-function syntax-dom syntax-js\">click</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n  <span class=\"syntax-comment syntax-line syntax-double-slash syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-js\">//</span> rest of test goes here... \n</span><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span>\n</span></span></code></pre>\n<p>It looks like there are a lot of implementations of this concept out there and they’re all better thought-through than mine. (from a quick Google: <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.npmjs.com/package/qunit-wait-for\">qunit-wait-for</a>, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://playwright.dev/docs/testing-library#replacing-waitfor\">playwright expect.poll</a>)</p>\n<h3><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#figuring-out-the-right-thing-to-wait-for-is-not-straightforward\">figuring out the right thing to wait for is not straightforward</a></h3>\n<p>In some cases I <em>thought</em> I’d identified the right thing to wait for in the DOM (“just wait for this textarea to appear!’) but it turned out that because of some internal details of how my program works, actually I needed to wait for something else later on which was hard to pin down.</p>\n<p>I ended up changing one of my components to add some random value to the DOM when it was finished an important action (like <code>data-this-thing-is-ready=true</code>) which didn’t feel great.</p>\n<p>My best guess is that the right way to fix this kind of test issue is a refactor that also makes the app more reliable for the users: if there’s an element in the DOM that isn’t actually ready for the user to interact with, maybe I shouldn’t be displaying it yet!</p>\n<h3><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#adding-some-css-classes-to-identify-things-but-is-that-right\">adding some CSS classes to identify things (but is that right?)</a></h3>\n<p>I ended up adding a few classes to HTML elements that I needed to find in the tests, either because I needed to click on them or wait for them to appear in the DOM.</p>\n<p>I might want to change this approach later - frontend testing frameworks seem to suggest avoiding using CSS classes and instead using something like <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://playwright.dev/docs/api/class-framelocator#frame-locator-get-by-role\">getByRole</a> or as a last resort something like a <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://testing-library.com/docs/dom-testing-library/intro/\">data-testid</a>. Feels like there’s a way to make the app more accessible and easier to test at the same time.</p>\n<h3><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#filling-out-forms-is-tricky\">filling out forms is tricky</a></h3>\n<p>To fill out a form, I can’t just set the <code>value</code>, I also need to dispatch an event to tell Vue that the element has changed. For example, <code>checkbox</code> and <code>textarea</code> need different kinds of events.</p>\n<pre><code><span class=\"code-snippet\">textarea.value = &#39;banana banana banana&#39;;\ntextarea.dispatchEvent(new Event(&#39;input&#39;));\n</span></code></pre>\n<pre><code><span class=\"code-snippet\">checkbox.checked = true;\ncheckbox.dispatchEvent(new Event(&#39;change&#39;));\n</span></code></pre>\n<p>This is kind of annoying and it made me realize why I might want to use some kind of UI testing library, for example:</p>\n<ul>\n<li>Testing Library’s <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://testing-library.com/docs/example-react-formik\">example of filling out a form</a> looks extremely different from what I’m doing</li>\n<li>Vue Test Utils: their <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/content/post/2026-05-02-testing-javascript-in-the-browser.markdown\">section on form handling</a> looks like it simplifies this a lot.</li>\n</ul>\n<h3><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#test-coverage\">test coverage</a></h3>\n<p>I want to have an idea of what my test coverage was, and it turns out that Chrome actually has a built-in <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.chrome.com/docs/devtools/coverage\">code coverage</a> feature for JS and CSS!</p>\n<p>My JS is bundled into a file called <code>bundle.js</code> with esbuild, so I could just look at <code>bundle.js</code> and see which lines weren’t covered.</p>\n<p>The process was a little finicky: I had to turn off sourcemaps in the Chrome devtools to get this to work, and there’s a specific not super obvious series of actions I have to do in order to see the coverage data.</p>\n<h3><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#this-was-so-fun\">this was so fun!</a></h3>\n<p>As usual with these posts I’ve never really worked as a frontend or backend developer (other than for myself!) and I feel like I’m constantly learning how to do super basic tasks.</p>\n<p>I really had a blast doing this. My frontend projects always feel so fragile because they’re untested, and maybe one day I’ll have a test suite I’m confident in!</p>\n<p>Some things I’m still thinking about:</p>\n<ul>\n<li>While writing this post I found this frontend testing library called <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://testing-library.com/\">Testing Library</a> that has a lot of guidelines for how to write tests that are very different from my initial ideas. I experimented with rewriting everything to use Testing Library and it felt pretty good, so we’ll see how that goes. They distribute a <code>.umd.js</code> file that works without Node.</li>\n<li>I’m not sure how I feel about not having a way to run these tests on the command line at all. Maybe there’s a simple way to work primarily in the browser but have an way to run them in CI too if I want?</li>\n</ul>\n",
  "content_markdown": "Hello! One of my long term projects on here is [figuring out how to write frontend Javascript without using Node](https://jvns.ca/#javascript) or any other server JS runtime.\n\nOne issue I run into a lot in my frontend JS projects is that I don’t know how to write tests for them. I’ve tried to use Playwright in the past, but it felt slow and unwieldy to be starting these new browser processes all the time, and it involved some Node code to orchestrate the tests.\n\nThe result is that I just don’t test my frontend code which doesn’t feel great. Usually I don’t update my projects much either so it doesn’t come up that much, but it would be nice to be able to make changes with more confidence! So a way to do frontend testing that I like has been on my wishlist for a long time.\n\n### [idea: just run the tests in the browser tab](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#idea-just-run-the-tests-in-the-browser-tab)\n\nAlex Chan wrote a great post a while back called [Testing JavaScript without a (third-party) framework](https://alexwlchan.net/2023/testing-javascript-without-a-framework/) in response to one of my previous posts in this series that explained how to write a tiny unit-testing framework that runs in a page in browser.\n\nI loved this post at the time, but it only talked about unit testing and I wanted to write end-to-end integration tests for my Vue components, and I didn’t know how to do that.\n\nSo when I was talking to [Marco](https://bsky.app/profile/polotek.bsky.social) the other day and he said something like “you know, you can just run tests for your Vue components in the browser”, I thought “hey, I should try that again!!!”\n\nI just did all of this yesterday so certainly there’s a lot to improve but I wanted to write down a few things I noticed about the process before I forget.\n\nThis was a bit tricky for me because the Vue site usually assumes that you’re using Node as part of your build process in some way (there’s a lot of “step 1: `npm install THING`), and I didn’t want to use Node/Deno/etc. But it turned out to not be too complicated.\n\nThe project I’m going to talk about testing is this [zine feedback site I wrote in 2023](https://jvns.ca/blog/2023/03/31/zine-feedback-site/).\n\n### [the test framework: QUnit](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#the-test-framework-qunit)\n\nI used [QUnit](https://qunitjs.com/). It worked great but I don’t have anything interesting to say about how it works so I’ll leave it at that. I think that Alex’s “write your own test framework” approach would have worked too. I followed [these directions](https://qunitjs.com/browser/).\n\nI did appreciate that QUnit has a “rerun test” button that will only rerun 1 test. Because there are so many network requests in my tests, having a way to run just 1 test makes it a lot less confusing to debug the test.\n\n### [step 1: set up the component for testing](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#step-1-set-up-the-component-for-testing)\n\nThe first thing I needed to do was get my Vue components set up in the test environment.\n\nI changed my main app to put all my components in `window._components`, kind of like this:\n\n```\nconst components = {\n  'Feedback': FeedbackComponent,\n  ...\n}\nwindow._components = components;\n```\n\nThen I was able to write a `mountComponent` function which does basically exactly the same thing my normal main app does (render a tiny template with the component I want to use). The only differences are:\n\n1. I can optionally pass some some extra data to use as its props.\n2. It mounts the component to a temporary invisible div which will get removed from the DOM after the test is done. The div is positioned off the page (`position: absolute; top: -10000, ...`) so you can’t see it.\n\nHere’s what using the `mountComponent` function looks like:\n\n```\nconst {div} = mountComponent(\n  '<Page :feedbacks=\"feedbacks\" id=2 />',\n  {feedbacks: [testFeedback]},\n);\n```\n\nand here’s the code for it:\n\n```\nfunction mountComponent(template, data) {\n  const app = Vue.createApp({\n    template: template,\n    data: () => data,\n  })\n  for (const [c, v] of Object.entries(window._components)) {\n    app.component(c, v);\n  }\n  const div = document.getElementById('qunit-fixture')\n             .appendChild(document.createElement('div'));\n  return div;\n}\n```\n\nThe result is a div where I can programmatically click, fill in form data, check that the right content appears, etc.\n\n### [step 2: add some fixture data](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#step-2-add-some-fixture-data)\n\nBecause I was writing end-to-end integration tests to make sure my client JS worked properly with my server, I needed to have some test data in my database. So I wrote ~25 lines of SQL to set up some test data in my database, and added an endpoint to my dev server to run the SQL to reset the test data to a known state.\n\n```\nasync function reset() {\n    return fetch('/api/reset_test_data', {method: \"POST\"})\n}\n```\n\nThen I just run `await reset()` at the beginning of any test that needs the test data.\n\nMy `reset()` function actually doesn’t always totally reset everything which is kind of bad, but it was workable to start with and can always be improved.\n\n### [step 3: a basic test](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#step-3-a-basic-test)\n\nHere’s what a basic test looks like! Basically we’re rendering the div and make sure it contains some approximately correct data.\n\n```\nQUnit.test('renders feedback content', async function (assert) {\n  const {div} = mountComponent(\n    '<Page :feedbacks=\"feedbacks\" id=2 image=2 page_hash=2 />',\n    {feedbacks: [testFeedback]},\n  );\n  assert.ok(div.textContent.includes('loved this section'));\n})\n```\n\nThose are all the basic pieces! Now here are a few issues I ran into along the way\n\n### [waiting for parts of the page to render](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#waiting-for-parts-of-the-page-to-render)\n\nI have a lot of network requests in my tests, and it takes time for them to finish and for the Vue code to do what it has to do with the results and update the DOM.\n\nI think we all learned a long time ago that putting random `sleep()` calls in your tests and hoping that the timings are right is slow and flaky and extremely frustrating, so I needed a different way.\n\nAs far as I can tell the normal way to deal with this is to figure out a way to tell from the DOM whether it’s okay to proceed or not. Like “if this button is visible, we can “.\n\nSo I wrote a little `waitFor()` function that polls every 20ms to see if a condition has finished yet. It times out after 2 seconds.\n\nHere’s what using it looks like:\n\n```\nQUnit.test(\"click item\", async function (assert) {\n  const {div} = mountComponent(\n    '<Feedback zine_id=\"test123\" image_width=\"800px\" />',\n    {});\n  const item = await waitFor(() => div.querySelector('.feedback-item'));\n  item.click();\n  // rest of test goes here... \n})\n```\n\nIt looks like there are a lot of implementations of this concept out there and they’re all better thought-through than mine. (from a quick Google: [qunit-wait-for](https://www.npmjs.com/package/qunit-wait-for), [playwright expect.poll](https://playwright.dev/docs/testing-library#replacing-waitfor))\n\n### [figuring out the right thing to wait for is not straightforward](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#figuring-out-the-right-thing-to-wait-for-is-not-straightforward)\n\nIn some cases I *thought* I’d identified the right thing to wait for in the DOM (“just wait for this textarea to appear!’) but it turned out that because of some internal details of how my program works, actually I needed to wait for something else later on which was hard to pin down.\n\nI ended up changing one of my components to add some random value to the DOM when it was finished an important action (like `data-this-thing-is-ready=true`) which didn’t feel great.\n\nMy best guess is that the right way to fix this kind of test issue is a refactor that also makes the app more reliable for the users: if there’s an element in the DOM that isn’t actually ready for the user to interact with, maybe I shouldn’t be displaying it yet!\n\n### [adding some CSS classes to identify things (but is that right?)](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#adding-some-css-classes-to-identify-things-but-is-that-right)\n\nI ended up adding a few classes to HTML elements that I needed to find in the tests, either because I needed to click on them or wait for them to appear in the DOM.\n\nI might want to change this approach later - frontend testing frameworks seem to suggest avoiding using CSS classes and instead using something like [getByRole](https://playwright.dev/docs/api/class-framelocator#frame-locator-get-by-role) or as a last resort something like a [data-testid](https://testing-library.com/docs/dom-testing-library/intro/). Feels like there’s a way to make the app more accessible and easier to test at the same time.\n\n### [filling out forms is tricky](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#filling-out-forms-is-tricky)\n\nTo fill out a form, I can’t just set the `value`, I also need to dispatch an event to tell Vue that the element has changed. For example, `checkbox` and `textarea` need different kinds of events.\n\n```\ntextarea.value = 'banana banana banana';\ntextarea.dispatchEvent(new Event('input'));\n```\n\n```\ncheckbox.checked = true;\ncheckbox.dispatchEvent(new Event('change'));\n```\n\nThis is kind of annoying and it made me realize why I might want to use some kind of UI testing library, for example:\n\n- Testing Library’s [example of filling out a form](https://testing-library.com/docs/example-react-formik) looks extremely different from what I’m doing\n- Vue Test Utils: their [section on form handling](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/content/post/2026-05-02-testing-javascript-in-the-browser.markdown) looks like it simplifies this a lot.\n\n### [test coverage](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#test-coverage)\n\nI want to have an idea of what my test coverage was, and it turns out that Chrome actually has a built-in [code coverage](https://developer.chrome.com/docs/devtools/coverage) feature for JS and CSS!\n\nMy JS is bundled into a file called `bundle.js` with esbuild, so I could just look at `bundle.js` and see which lines weren’t covered.\n\nThe process was a little finicky: I had to turn off sourcemaps in the Chrome devtools to get this to work, and there’s a specific not super obvious series of actions I have to do in order to see the coverage data.\n\n### [this was so fun!](https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#this-was-so-fun)\n\nAs usual with these posts I’ve never really worked as a frontend or backend developer (other than for myself!) and I feel like I’m constantly learning how to do super basic tasks.\n\nI really had a blast doing this. My frontend projects always feel so fragile because they’re untested, and maybe one day I’ll have a test suite I’m confident in!\n\nSome things I’m still thinking about:\n\n- While writing this post I found this frontend testing library called [Testing Library](https://testing-library.com/) that has a lot of guidelines for how to write tests that are very different from my initial ideas. I experimented with rewriting everything to use Testing Library and it felt pretty good, so we’ll see how that goes. They distribute a `.umd.js` file that works without Node.\n- I’m not sure how I feel about not having a way to run these tests on the command line at all. Maybe there’s a simple way to work primarily in the browser but have an way to run them in CI too if I want?\n",
  "dateCreated": "2026-09-19T21:30:23.395188495+00:00",
  "date_created": "2026-05-02T00:00:00+00:00",
  "date_modified": "2026-05-02T00:00:00+00:00",
  "date_published": "2026-05-02T00:00:00+00:00",
  "description": "Hello! One of my long term projects on here is figuring out how to write frontend Javascript without using Node or any other server JS runtime. One issue I run into a lot in my frontend JS projects is that I don’t know how to write tests…",
  "external_url": "https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/",
  "git": {
    "commit": "ebb0941c5c06a2ab2377342d607535262a3e969e",
    "path": "items/jvns-ca/2026/05/2026-05-02-testing-vue-components-in-the-browser.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/ebb0941c5c06a2ab2377342d607535262a3e969e/items/jvns-ca/2026/05/2026-05-02-testing-vue-components-in-the-browser.md"
  },
  "id": "urn:aggr:item:ba0256e658a06abb3af18997aafbad693de1223c",
  "isBasedOn": {
    "@id": "https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/"
  },
  "mainEntity": {
    "@id": "https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/jvns-ca/2026-05-02-testing-vue-components-in-the-browser/#webpage"
    },
    "dateModified": "2026-05-02T00:00:00+00:00",
    "datePublished": "2026-05-02T00:00:00+00:00",
    "headline": "Testing Vue components in the browser",
    "timeRequired": "PT8M",
    "url": "https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/",
    "wordCount": 1716
  },
  "name": "Archived snapshot: Testing Vue components in the browser",
  "preview": null,
  "reading_minutes": 8,
  "replicated_at": null,
  "source": "jvns-ca",
  "source_name": "Julia Evans",
  "summary": null,
  "tags": [],
  "temporalCoverage": "2026-09-19T21:30:23.395188495+00:00",
  "title": "Testing Vue components in the browser",
  "url": "https://aggr.aymericbeaumet.com/items/jvns-ca/2026-05-02-testing-vue-components-in-the-browser/",
  "word_count": 1716
}