nuxt / test-utils

🧪 Test utilities for Nuxt
http://nuxt.com/docs/getting-started/testing
MIT License
322 stars 83 forks source link

add more info about migrating to nuxt-vitest #510

Open VegasChickiChicki opened 1 year ago

VegasChickiChicki commented 1 year ago

Error message:

TypeError: Package import specifier "#app/entry" is not defined in package C:\Users\dodaa\WebstormProjects\nuxt-3-template\node_modules\vitest-environment-nuxt\package.json imported from C:\Users\dodaa\WebstormProjects\nuxt-3-template\node_modules\vitest-environment-nuxt\dist\index.mjs ❯ new NodeError node:internal/errors:387:5 ❯ throwImportNotDefined node:internal/modules/esm/resolve:354:9 ❯ packageImportsResolve node:internal/modules/esm/resolve:737:3 ❯ moduleResolve node:internal/modules/esm/resolve:895:21 ❯ defaultResolve node:internal/modules/esm/resolve:1115:11 ❯ nextResolve node:internal/modules/esm/loader:163:28 ❯ ESMLoader.resolve node:internal/modules/esm/loader:837:30 ❯ ESMLoader.getModuleJob node:internal/modules/esm/loader:424:18 ❯ ESMLoader.import node:internal/modules/esm/loader:521:22 ❯ importModuleDynamically node:internal/modules/esm/translators:110:35

Package file:

 {
  "name": "nuxt-app",
  "private": true,
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "start": "nuxt start",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare",
    "eslint": "eslint . --ext js --ext vue",
    "eslint:fix": "eslint . --ext js --ext vue --fix",
    "stylelint": "npx stylelint \"**/*.{css,scss,vue}\"",
    "stylelint:fix": "npx stylelint \"**/*.{css,scss,vue}\" --fix",
    "storybook": "storybook dev -p 6006",
    "storybook:build": "storybook build -s ./public,./assets",
    "test:unit": "vitest --config ./vitest.config.js"
  },
  "devDependencies": {
    "@nuxt/test-utils": "^3.5.1",
    "@nuxtjs/eslint-module": "^4.0.2",
    "@storybook/addon-essentials": "^7.1.0-alpha.39",
    "@storybook/vue3": "^7.1.0-alpha.39",
    "@storybook/vue3-vite": "^7.1.0-alpha.39",
    "@types/node": "^18",
    "@vitejs/plugin-vue": "^4.2.3",
    "@vue/test-utils": "^2.3.2",
    "eslint": "^8.42.0",
    "eslint-config-prettier": "^8.8.0",
    "eslint-plugin-storybook": "^0.6.12",
    "eslint-plugin-vue": "^9.10.0",
    "jsdom": "^22.1.0",
    "nuxt": "^3.6.0",
    "nuxt-vitest": "^0.8.5",
    "sass": "^1.62.1",
    "sass-loader": "^13.2.2",
    "storybook": "^7.1.0-alpha.39",
    "stylelint": "^15.4.0",
    "stylelint-config-prettier-scss": "^1.0.0",
    "stylelint-config-recommended-vue": "^1.4.0",
    "stylelint-config-standard-scss": "^9.0.0",
    "stylelint-order": "^6.0.3",
    "stylelint-scss": "^5.0.1",
    "typescript": "^5.1.3",
    "vite": "^4.3.9",
    "vite-tsconfig-paths": "^4.2.0",
    "vitest": "^0.30.1"
  },
  "dependencies": {
    "@vuelidate/core": "^2.0.2",
    "@vuelidate/validators": "^2.0.2",
    "axios": "^1.4.0",
    "jwt-decode": "^3.1.2",
    "maska": "^2.1.9",
    "nuxt-icons": "^3.2.1",
    "vuex": "^4.1.0"
  },
  "overrides": {
    "vue": "latest"
  }
}

test file:

// @vitest-environment nuxt
import { describe, it } from 'vitest';
import { mount } from '@vue/test-utils';
import { vMaska } from 'maska';
import VPhoneForm from './v-phone-form.vue';

const phoneValue = '+71002003040';
const phoneIncorrectValue = 'bad value';

describe('v-phone-form', () => {
    it('renders the component correctly', () => {
        const wrapper = mount(VPhoneForm, {
            global: {
                directives: {
                    maska: vMaska
                }
            }
        });
        const phoneForm = wrapper.find('[data-testid="phone-form"]');
        const inputPhoneInput = wrapper.find('[data-testid="input-text-input"]');
        const submitButton = wrapper.find('[data-testid="phone-form-submit-button"]');

        expect(phoneForm.exists()).toBeTruthy();
        expect(inputPhoneInput.exists()).toBeTruthy();
        expect(submitButton.exists()).toBeTruthy();
    });

    it('shows error text when phone is incorrect', async () => {
        const wrapper = mount(VPhoneForm, {
            global: {
                directives: {
                    maska: vMaska
                }
            }
        });
        const submitButton = wrapper.find('[data-testid="phone-form-submit-button"]');
        const inputPhone = wrapper.find('[data-testid="input-text"]');

        await submitButton.trigger('submit');

        expect(inputPhone.classes()).toContain('input-text--error');
    });

    it('hides error text when phone is entered correctly', async () => {
        const wrapper = mount(VPhoneForm, {
            global: {
                directives: {
                    maska: vMaska
                }
            }
        });
        const inputPhone = wrapper.find('[data-testid="input-text"]');
        const inputPhoneInput = wrapper.find('[data-testid="input-text-input"]');
        const inputPhoneHelperTextContent = wrapper.find('[data-testid="input-text-helper-text-content"]');
        const submitButton = wrapper.find('[data-testid="phone-form-submit-button"]');

        await submitButton.trigger('submit');
        await inputPhoneInput.setValue(phoneValue);

        expect(inputPhone.classes()).not.toContain('input-text--error');
        expect(inputPhoneHelperTextContent.exists()).toBeFalsy();
    });

    it('checks phone input when entering a non-number value', async () => {
        const wrapper = mount(VPhoneForm, {
            global: {
                directives: {
                    maska: vMaska
                }
            }
        });
        const inputPhoneInput = wrapper.find('[data-testid="input-text-input"]');

        await inputPhoneInput.setValue(phoneIncorrectValue);

        expect(inputPhoneInput.element.value).not.toBe(phoneIncorrectValue);
    });

    it('emits a "check-phone" event when the form is submitted', async () => {
        const wrapper = mount(VPhoneForm, {
            global: {
                directives: {
                    maska: vMaska
                }
            }
        });
        const inputPhoneInput = wrapper.find('[data-testid="input-text-input"]');
        const submitButton = wrapper.find('[data-testid="phone-form-submit-button"]');

        await inputPhoneInput.setValue(phoneValue);
        await submitButton.trigger('submit');
        await wrapper.vm.$nextTick();

        expect(wrapper.emitted()).toHaveProperty('check-phone');
        expect(wrapper.emitted()['check-phone'][0]).toEqual([true]);
    });
});
danielroe commented 1 year ago

Would you provide a sandbox please? What's your vitest.config?

VegasChickiChicki commented 1 year ago

Would you provide a sandbox please? What's your vitest.config?

My vitest.config:

import { resolve } from 'path';
import vue from '@vitejs/plugin-vue';

const customElements = ['nuxt-icon'];

export default {
    plugins: [
        vue({
            template: {
                compilerOptions: {
                    isCustomElement: (tag) => customElements.includes(tag)
                }
            }
        })
    ],
    test: {
        globals: true,
        environment: 'jsdom'
    },
    resolve: {
        alias: {
            '~': resolve(__dirname, '.'),
            '~/': resolve(__dirname, './'),
            '@': resolve(__dirname, '.'),
            '@/': resolve(__dirname, './')
        }
    }
};
danielroe commented 1 year ago

Would you instead use the configuration in the installation guide?

CleanShot 2023-06-24 at 22 19 47@2x

https://github.com/danielroe/nuxt-vitest#installation

You shouldn't need almost any of your configuration.

VegasChickiChicki commented 1 year ago

Would you instead use the configuration in the installation guide?

CleanShot 2023-06-24 at 22 19 47@2x

https://github.com/danielroe/nuxt-vitest#installation

You shouldn't need almost any of your configuration.

I fix this. Thanks! But i have this warns in console:

stderr | components/v-page-header/v-page-header.test.js > v-page-header > renders with page title
[Vue warn]: Component <Anonymous>: setup function returned a promise, but no <Suspense> boundary was found in the parent component tree. A component with async setup() must be nested in a <Suspense> in order to be rendered.
  at <NuxtIcon name="alert-star" >
  at <VPageHeader title="page title" ref="VTU_COMPONENT" >
  at <VTUROOT>

stderr | unknown test
[Vue Router warn]: No match found for location with path "blank"
[Vue Router warn]: No match found for location with path "blank"
[Vue Router warn]: No match found for location with path "blank"
[Vue Router warn]: No match found for location with path "/blank"

stderr | unknown test
<empty line>
stdout | unknown test
<Suspense> is an experimental feature and its API will likely change.

I also noticed that I get errors due to this construction:

plugins: [
        vue({
            template: {
                compilerOptions: {
                    isCustomElement: (tag) => customElements.includes(tag)
                }
            }
        })
    ],

I needed it when I tried to run and got an error related to the nuxt-icon library. Error message:

Vitest caught 1 unhandled error during the test run. This might cause false positive tests. Resolve unhandled errors to make sure your tests are not affected.

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Unhandled Error ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
SyntaxError: At least one <template> or <script> is required in a single file component.
 ❯ Object.parse$2 [as parse] node_modules/@vue/compiler-sfc/dist/compiler-sfc.cjs.js:1275:7
 ❯ createDescriptor node_modules/@vitejs/plugin-vue/dist/index.mjs:70:43
 ❯ transformMain node_modules/@vitejs/plugin-vue/dist/index.mjs:2273:34
 ❯ TransformContext.transform node_modules/@vitejs/plugin-vue/dist/index.mjs:2794:16
 ❯ Object.transform node_modules/vite/dist/node/chunks/dep-e8f070e8.js:42919:44
 ❯ loadAndTransform node_modules/vite/dist/node/chunks/dep-e8f070e8.js:53385:29

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: {
  "id": "C:/Users/dodaa/WebstormProjects/nuxt-3-template/pages/index/index.vue",
  "plugin": "vite:vue",
  "pluginCode": "import { withAsyncContext as _withAsyncContext } from 'vue'
import VPageHeader from '~/components/v-page-header/v-page-header.vue';

const _sfc_main = {
  __name: 'index',
  async setup(__props, { expose: __expose }) {
  __expose();

let __temp, __restore

const { $store } = useNuxtApp();

definePageMeta({
        layout: 'default',
        layoutTransition: {
                name: 'ease-opacity',
                mode: 'out-in'
        },
        pageTransition: {
                name: 'ease-opacity',
                mode: 'out-in'
        }
});

;(
  ([__temp,__restore] = _withAsyncContext(() => $store.dispatch('user/getUserData'))),
  await __temp,
  __restore()
);

const __returned__ = { $store, VPageHeader }
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}

}
import { createVNode as _createVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = { class: "main-page" }

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("main", _hoisted_1, [
    _createVNode($setup["VPageHeader"], { title: 'Main page' })
  ]))
}

import _export_sfc from 'plugin-vue:export-helper'
export default /*#__PURE__*/_export_sfc(_sfc_main, [['render',_sfc_render],['__file',"C:/Users/dodaa/WebstormProjects/nuxt-3-template/pages/index/index.vue"]])",
}
danielroe commented 1 year ago

You should remove the vue plugin - Nuxt vitest already does that for y ou.

VegasChickiChicki commented 1 year ago

You should remove the vue plugin - Nuxt vitest already does that for y ou.

Yes, i do it. But this point is not explicitly specified in the documentation. The documentation is written in such a way that I thought that the vitest config should simply be passed to the defineVitestConfig function, it does not explicitly state what can or cannot be added there (at least in the examples).

VegasChickiChicki commented 1 year ago

In general, everything works, but there is still a question about the warnings in the console.

stderr | components/v-page-header/v-page-header.test.js > v-page-header > renders with page title
[Vue warn]: Component <Anonymous>: setup function returned a promise, but no <Suspense> boundary was found in the parent component tree. A component with async setup() must be nested in a <Suspense> in order to be rendered.
  at <NuxtIcon name="alert-star" >
  at <VPageHeader title="page title" ref="VTU_COMPONENT" >
  at <VTUROOT>

stderr | unknown test
[Vue Router warn]: No match found for location with path "blank"
[Vue Router warn]: No match found for location with path "blank"
[Vue Router warn]: No match found for location with path "blank"
[Vue Router warn]: No match found for location with path "/blank"

stderr | unknown test
<empty line>
stdout | unknown test
<Suspense> is an experimental feature and its API will likely change.
danielroe commented 1 year ago

I think we should improve the docs to help people migrating from an existing setup like yours.

For the warnings , you should use mountSuspended for testing components with an async setup (or that rely on Nuxt context).