Skip to content

Computed Properties Not Updating in Tests When Using mountSuspended #1115

@OziOcb

Description

@OziOcb

Environment


  • Operating System: Linux
  • Node Version: v18.20.3
  • Nuxt Version: 3.15.4
  • CLI Version: 3.21.1
  • Nitro Version: 2.10.4
  • Package Manager: [email protected]
  • Builder: -
  • User Config: compatibilityDate, devtools
  • Runtime Modules: -
  • Build Modules: -

Reproduction

Minimal Reproduction - https://stackblitz.com/edit/github-remjha3q?file=app.vue

  • Open app.vue and app.spec.ts files
  • Open a new Terminal and run npm run rest

Describe the bug

When testing a Vue component using mountSuspended from @nuxt/test-utils/runtime, computed properties fail to update reactively when their dependencies change. The same test cases pass when using @vue/test-utils' mount or shallowMount methods.

Current Behavior:

  • Data property foo updates correctly
  • Computed property bar remains stale with its initial value
  • Computed property baz remains stale with its initial value

Expected Behavior:

  • When foo changes to true:
    • foo property should update
    • bar should update to "(bar = true)"
    • baz should update to "baz => (bar = true)"

Workaround:

Using standard @vue/test-utils mounting methods works as expected:

// These alternatives work correctly
return shallowMount(App, {});
// or
return mount(App, {});

Files

package.json

{
  "name": "nuxt-app",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare",
    "test": "vitest"
  },
  "dependencies": {
    "nuxt": "^3.15.4",
    "vue": "latest",
    "vue-router": "latest"
  },
  "devDependencies": {
    "@nuxt/test-utils": "^3.15.4",
    "@vue/test-utils": "^2.4.6",
    "happy-dom": "^16.6.0",
    "jsdom": "^26.0.0",
    "vitest": "^3.0.5"
  }
}

vitest.config.ts

import { defineVitestConfig } from '@nuxt/test-utils/config';

export default defineVitestConfig({
  test: {
    environment: 'nuxt',
    environmentOptions: {
      nuxt: {
        domEnvironment: 'jsdom', // 'happy-dom' (default) or 'jsdom'
      },
    },
    globals: true,
  },
});

app.vue

<template>
  <div>{{ baz }}</div>
</template>

<script lang="ts">
export default defineNuxtComponent({
  name: 'UnitTestsMountSuspendedComputedValuesBug',

  data() {
    return {
      foo: false,
    };
  },

  computed: {
    bar() {
      return this.foo ? '(bar = true)' : '(bar = false)';
    },

    baz() {
      return 'baz => ' + this.bar;
    },
  },
});
</script>

app.spec.ts

import { describe, beforeEach, it, expect } from 'vitest';
import { mountSuspended } from '@nuxt/test-utils/runtime';
import { shallowMount, mount } from '@vue/test-utils';
import App from './app.vue';
import { VueWrapper } from '@vue/test-utils';

let wrapper: VueWrapper<InstanceType<typeof App>>;
const mountComponent = async () => {
  // return shallowMount(App, { // this works as expected
  // return mount(App, { // this works as expected
  return await mountSuspended(App, { // this doesn't work as expected!
    // data() { // pre-setting data while using moutSuspended doesn't work as well! (it works fine with other methods though)
    //   return {
    //     foo: true,
    //   };
    // },
  });
};

describe('AppMountSuspendedComputedValuesBug', () => {
  beforeEach(async () => {
    wrapper = await mountComponent();
  });

  // Toggling foo (data property) works as expected
  describe('foo', () => {
    it('should equal false by default', () => {
      expect(wrapper.vm.foo).toBe(false);
    });

    it('should equal true if changed using wrapper.vm.foo = true', async () => {
      wrapper.vm.foo = true;
      await wrapper.vm.$nextTick();
      expect(wrapper.vm.foo).toBe(true);
    });
  });

  // bar (coumputed value) doesn't update when foo (data property) changes
  describe('bar', () => {
    it('should return "(bar = false)" by default', () => {
      expect(wrapper.vm.bar).toBe('(bar = false)');
    });

    it('should return "(bar = true)" if this.foo === true', async () => {
      wrapper.vm.foo = true;
      await wrapper.vm.$nextTick();
      expect(wrapper.vm.bar).toBe('(bar = true)');
    });
  });

  // Obviously, baz (computed value) won't update becasue bar (computed value) doesn't update
  describe('baz', () => {
    it('should return "baz => (bar = false)" by default', () => {
      expect(wrapper.vm.baz).toBe('baz => (bar = false)');
    });

    it('should return "baz => (bar = true)" if this.foo === true', async () => {
      wrapper.vm.foo = true;
      await wrapper.vm.$nextTick();
      expect(wrapper.vm.baz).toBe('baz => (bar = true)');
    });
  });
});

Additional context

Pre-setting initial data values through the mount options also doesn't work with mountSuspended, while it works fine with other mounting methods.

Logs

RERUN  app.spec.ts x1 

 ❯ app.spec.ts (6 tests | 2 failed) 234ms
   ✓ AppMountSuspendedComputedValuesBug > foo > should equal false by default
   ✓ AppMountSuspendedComputedValuesBug > foo > should equal true if changed using wrapper.vm.foo = true
   ✓ AppMountSuspendedComputedValuesBug > bar > should return "(bar = false)" by default
   × AppMountSuspendedComputedValuesBug > bar > should return "(bar = true)" if this.foo === true 44ms
     → expected '(bar = false)' to be '(bar = true)' // Object.is equality
   ✓ AppMountSuspendedComputedValuesBug > baz > should return "baz => (bar = false)" by default
   × AppMountSuspendedComputedValuesBug > baz > should return "baz => (bar = true)" if this.foo === true 36ms
     → expected 'baz => (bar = false)' to be 'baz => (bar = true)' // Object.is equality

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯

 FAIL  app.spec.ts > AppMountSuspendedComputedValuesBug > bar > should return "(bar = true)" if this.foo === true
AssertionError: expected '(bar = false)' to be '(bar = true)' // Object.is equality

Expected: "(bar = true)"
Received: "(bar = false)"eval app.spec.ts:48:30
     46|       wrapper.vm.foo = true;
     47|       await wrapper.vm.$nextTick();
     48|       expect(wrapper.vm.bar).toBe('(bar = true)');
       |                              ^
     49|     });
     50|   });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯

 FAIL  app.spec.ts > AppMountSuspendedComputedValuesBug > baz > should return "baz => (bar = true)" if this.foo === true
AssertionError: expected 'baz => (bar = false)' to be 'baz => (bar = true)' // Object.is equality

Expected: "baz => (bar = true)"
Received: "baz => (bar = false)"eval app.spec.ts:61:30
     59|       wrapper.vm.foo = true;
     60|       await wrapper.vm.$nextTick();
     61|       expect(wrapper.vm.baz).toBe('baz => (bar = true)');
       |                              ^
     62|     });
     63|   });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯


 Test Files  1 failed (1)
      Tests  2 failed | 4 passed (6)
   Start at  16:28:03
   Duration  730ms

 FAIL  Tests failed. Watching for file changes...
       press h to show help, press q to quit

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions