Skip to content

This project is a nuxt 3 starter project. It implements Vitest, Storybook, ESLint (flat config, stylistic), VeeValidate, Pinia, Puppeteer, SonarQube, TypeScript. This project compatible with nuxt 3.14

Notifications You must be signed in to change notification settings

N-Laboratory/nuxt3-starter-guide-example

Repository files navigation

Nuxt 3 Starter Guide

Twitter: N-LAB

日本語版はこちら

This project is a template nuxt 3 project.

The minimum required functions are implemented as a template project and the essentials are explained. This project also implement unit testing, E2E testing, and analyzing source code by SonarQube.

This project implement the following.

  • Vitest (unit test)
  • EsLint (Flat Config and Stylistic)
  • Migrate to Flat Config and Stylistic
  • VeeValidate
  • Navigation guard
  • Pinia
  • Storybook
  • Puppeteer (E2E test)
  • SonarQube
  • TypeScript

Contents

  1. Create New Project
  2. Typescript Setup
  3. EsLint Flat Config Setup
  4. EsLint Stylistic Setup
  5. Migrate To Flat Config And Stylistic
  6. Vitest Setup
  7. VeeValidate Setup
  8. VeeValidate Testing
  9. Navigation guard
  10. Pinia Setup
  11. Pinia Testing
  12. Data Fetching
  13. Storybook Setup
  14. E2E Testing By Puppeteer
  15. Analyzing source code by SonarQube

Run below command to create a new nuxt 3 project.

npx nuxi@latest init <project-name>

If you want to change source directory, add the following to nuxt.config.ts.

// nuxt.config.ts
export default defineNuxtConfig({
  srcDir: 'src/'
});

Install

npm install

Usage

npm run dev

You can access http://localhost:3000 to use this application.

You may experience issues with the latest vue-tsc and vite-plugin-checker, used internally when type checking. For now, you may need to stay on v1 of vue-tsc. For more details, please see https://nuxt.com/docs/guide/concepts/typescript#type-checking

npm install --save-dev vue-tsc@^1 typescript

Add typescript to nuxt.config.ts.

// nuxt.config.ts
export default defineNuxtConfig({
  // For more about all the available options, please see https://nuxt.com/docs/api/nuxt-config#typescript
  typescript: {
    // Enable type-checking at build time
    typeCheck: true
  },
});

If you had installed vue-tsc v2, an error will occur like below. vuejs/language-tools#3969

The only solution currently is to downgrade vue-tsc v1 or try following.

// nuxt.config.ts
export default defineNuxtConfig({
  typescript: {
    // Change typeCheck to false to avoid above error.
    typeCheck: false
  },
});

EsLint Flat Config Setup

The flat config format is the future of ESLint and is designed to be more flexible and project-aware.

npm install --save-dev @nuxt/eslint eslint

Add @nuxt/eslint to modules in nuxt.config.ts.

export default defineNuxtConfig({
  modules: [
    '@nuxt/eslint'
  ],
})

Create eslint.config.mjs in root directory and add the following. For more about all the available options, please see https://eslint.org/docs/latest/use/configure/configuration-files

import withNuxt from './.nuxt/eslint.config.mjs'

export default withNuxt(
  {
    // An array of glob patterns indicating the files that the configuration object should apply to.
    files: ['**/*.ts', '**/*.tsx'],
    // An array of glob patterns indicating the files that the configuration object should not apply to.
    // Use ignores instead of --ignore-path.
    ignores: ["**/*.config.ts"],
    // An object containing the configured rules. When files or ignores are specified, these rule configurations are only available to the matching files.
    rules: {
      'no-console': 'off'
    }
  },
  {
    files: ['**/*.vue',],
    rules: {
      'no-console': 'error'
    }
  }
)

Ignores has the following defined by default.

https://github.com/nuxt/eslint/blob/main/packages/eslint-config/src/flat/configs/ignores.ts

import type { Linter } from 'eslint'

export default function ignores(): Linter.FlatConfig[] {
  return [
    {
      ignores: [
        '**/dist',
        '**/node_modules',
        '**/.nuxt',
        '**/.output',
        '**/.vercel',
        '**/.netlify',
      ],
    },
  ]
}

Add the following item to scripts in package.json.

{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
  }
}

Use below command to run ESLint.

# run ESLint
npm run lint

# run ESLint + fix code
npm run lint:fix

VS Code setup

ESLint v9.x support was added in the ESLint VS Code extension (vscode-eslint) v3.0.10.

In versions of vscode-eslint prior to v3.0.10, the new configuration system is not enabled by default. To enable support for the new configuration files, edit your .vscode/settings.json file and add the following:

{
  "eslint.experimental.useFlatConfig": true
}

ESLint Stylistic Setup

npm install --save-dev @nuxt/eslint eslint

Nuxt integrate with ESLint Stylistic directly.

Similar to the ESLint Module, you can opt-in by setting stylistic to true in the features module options.

export default defineNuxtConfig({
  modules: [
    // Add '@nuxt/eslint'
    '@nuxt/eslint'
  ],
  eslint: {
    config: {
      stylistic: true
    }
  }
})

You can also customize the rules.

For more about all the available options, please see https://eslint.style/guide/config-presets#configuration-factory

export default defineNuxtConfig({
  modules: [
    '@nuxt/eslint'
  ],
  eslint: {
    config: {
      stylistic: {
        indent: 2,
        quotes: 'single',
        semi: false,
      },
    }
  }
})

Automatically format code on save with ESLint in VSCode

Add the following to .vscode/setting.json

{
  "editor.formatOnSave": false,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  },
}

Migrate To Flat Config And Stylistic

This guide provides an overview of how you can migrate your ESLint configuration file from eslintrc and prettier to ESLint flat config and ESLint stylistic. For more details, please see https://eslint.org/docs/latest/use/configure/migration-guide

First, install Nuxt ESLint.

npm install --save-dev @nuxt/eslint eslint

Migrate from eslintrc to ESLint flat config

Delete @nuxtjs/eslint-config-typescript.

npm uninstall @nuxtjs/eslint-config-typescript

Delete @nuxtjs/eslint-config-typescript in package.json.

"devDependencies": {
- "@nuxtjs/eslint-config-typescript": "^12.1.0",
},

Delete .eslintrc file.

- {
-   "extends": [
-     "@nuxtjs/eslint-config-typescript"
-   ],
-   "rules": {
-     "no-console": "off"
-   }
- }

Create eslint.config.mjs file in you root directory.

// eslint.config.mjs
import withNuxt from './.nuxt/eslint.config.mjs'

export default withNuxt(
  {
    files: ['**/*.js', '**/*.ts', '**/*.vue'],
    // Use ignores instead of --ignore-path.
    ignores: ['**/*.log*', '.cache/**'],
    rules: {
      'no-console': 'off',
    },
  },
)

Modify npm scripts like below.

"scripts": {
- lint: "eslint --ext \".js,.ts,.vue\" --ignore-path .gitignore .",
+ lint: "eslint .",
},

Migrate from prettier to ESLint stylistic

Delete prettier and eslint-config-prettier, eslint-plugin-prettier.

npm uninstall prettier eslint-config-prettier eslint-plugin-prettier

Delete prettier and eslint-config-prettier, eslint-plugin-prettier in package.json.

"devDependencies": {
- "eslint-plugin-prettier": "^5.1.0",
- "eslint-config-prettier": "^8.3.0",
- "prettier": "^2.5.1",
},

Delete .prettierrc file.

- {
-   "indent": 2,
-   "quotes": 'single',
-   "semi": false
- }

Add rules to nuxt.config.ts

// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@nuxt/eslint'
  ],
  eslint: {
    config: {
      stylistic: {
        indent: 2,
        quotes: 'single',
        semi: false,
      },
    }
  }
})

Delete prettier commands from npm scripts in package.json.

Vitest Setup

# install Vitest
npm install --save-dev vitest @testing-library/vue happy-dom

Create vitest.config.ts in root directory and add the following to vitest.config.ts.

// vitest.config.ts
import path from 'path'
import { defineConfig } from 'vitest/config'
import Vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'

export default defineConfig({
  resolve: {
    alias: {
      '~': path.resolve(__dirname, './src'),
      '@': path.resolve(__dirname, './src')
    }
  },
  test: {
    globals: true,
    environment: 'happy-dom'
  }
})

Add the following to package.json.

{
  "config": {
    "path": "./src/tests/unitTest/pages/index.spec.ts"
  },
  "scripts": {
    "test:all": "vitest",
    "test:linux": "vitest $npm_package_config_path",
    "test:win": "vitest %npm_package_config_path%"
  },
}

Auto import configure

Use below plugin because vitest does not import function/components that auto import by Nuxt.

npm install --save-dev unplugin-auto-import
npm install --save-dev unplugin-vue-components

Add plugins to vitest.config.ts

// vitest.config.ts
import AutoImportFunctions from 'unplugin-auto-import/vite'
import AutoImportComponents from 'unplugin-vue-components/vite'

export default defineConfig({
  plugins: [
    Vue(),
    // Set plugin name you want to import. You can set preset name.
    // https://github.com/antfu/unplugin-auto-import/tree/main/src/presets
    AutoImportFunctions ({ imports: [
      'vue',
      'vee-validate',
      'vue-router',
      'pinia',
    ], dts: 'auto-imports.d.ts' }),
    AutoImportComponents({
      dirs: ['src/components'],
      dts: '.nuxt/components.d.ts',
    }),
  ],
})

You can set custom plugin like this.

// vitest.config.ts
AutoImport({
  imports: [
    {
      "nuxt/app": [
        "foo"
      ]
    }
  ]
})

Collect coverage

npm install --save-dev @vitest/coverage-v8 vitest-sonar-reporter

Add the following to vitest.config.ts.

// vitest.config.ts
export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      include: ['src/**/*.{vue,js,ts}'],
      all: true,
      reporter: ['html', 'clover', 'text']
    },
    root: '.',
    reporters: ['verbose', 'vitest-sonar-reporter'],
    outputFile: 'test-report.xml'
  }
});

Add --coverage to the following item in package.json.

{
  "scripts": {
    "test:all": "vitest --coverage",
    "test:linux": "vitest --coverage $npm_package_config_path",
    "test:win": "vitest --coverage %npm_package_config_path%"
  },
}

Add index.vue to pages directory.

// index.vue
<template>
  <h1>
    Pages/index.vue
  </h1>
</template>

Here is a test code of index.vue.

import { describe, expect, test } from 'vitest'
import { render, screen } from '@testing-library/vue'
import Index from './pages/index.vue'

describe('Index', () => {
  test('should render page title', () => {
    // Arrange
    render(Index)
    const title = screen.getByText('Pages/index.vue')

    // Assert
    expect(title).toBeTruthy()
  })
})

Run below command to run test.

# run all tests
npm run test:all

You can also run each test file. Set test file path to config:path in package.json.

{
  "config": {
    "path": "./src/tests/unitTest/pages/index.spec.ts"
  },
}
# Run test file defined config:path in package.json (Mac/Linux)
npm run test:linux

# Run test file defined config:path in package.json (Windows)
npm run test:win
# install VeeValidate
npm install --save-dev vee-validate @vee-validate/i18n @vee-validate/rules

Create vee-validate-plugin.ts in plugins directory and add the following to vee-validate-plugin.ts.

// vee-validate-plugin.ts
import { localize } from '@vee-validate/i18n'
import en from '@vee-validate/i18n/dist/locale/en.json'
import { all } from '@vee-validate/rules'
import { defineRule, configure } from 'vee-validate'
import { defineNuxtPlugin } from '#app'

export default defineNuxtPlugin((_nuxtApp) => {
  configure({
    generateMessage: localize({
      en,
    }),
  })

  // import vee-validate all rules
  Object.entries(all).forEach(([name, rule]) => {
    defineRule(name, rule)
  })
})

Form validation

In vee-validate4, there are two ways to implement validation.

  • implement validation in script setup
  • implement validation in html

Implement validation in script setup

If you implement validation in script setup, use useForm/useField.

<script lang="ts" setup>
import { useForm, useField } from 'vee-validate'

// Form configuration. See the following for more details.
// https://vee-validate.logaretm.com/v4/api/use-form/#api-reference
const { handleSubmit, errors, isSubmitting, meta } = useForm({
  // Set validation rule
  validationSchema: {
    email: 'required|email'
  }
})

// Field configuration
const { value: email } = useField<string>('email')

// If you click submit button, this function called.
const foo = () => {
  console.log(email.value)
}

// If you click submit button, this function called and also run validation check at the same time.
const foo = handleSubmit(() => {
  console.log(email.value)
})
</script>

<template>
  <input v-model="email" type="text" name="email">

  <!-- Show error message -->
  <span v-if="errors.email">{{ errors.email }}</span>

  <!-- When all field value is valid, meta.valid return true. -->
  <button type="button" :disabled="!meta.valid" @click="foo">Submit</button>

  <!-- If the form submission function is being run, isSubmitting return true. -->
  <button type="button" :disabled="isSubmitting" @click="foo">Submit</button>
</template>

Implement validation in html

If you implement validation in html, use Form/Field components.

<script lang="ts" setup>
import { Form, Field, ErrorMessage } from 'vee-validate'

// If you click submit button, this function called.
const foo = (values: Record<string, any>) => {
  console.log(values.email)
}
</script>

<template>
  <!-- Form configuration. See the following for more details -->
  <!-- https://vee-validate.logaretm.com/v4/api/use-form/#api-reference -->
  <Form v-slot="{ meta, isSubmitting }" @submit="foo">
    <Field rules="required|email" name="email" as="input" type="text" />

    <!-- Show error message -->
    <ErrorMessage name="email" />

    <!-- When all field value is valid, meta.valid return true. -->
    <button :disabled="!meta.valid">Submit</button>

    <!-- If the form submission function is being run, isSubmitting return true. -->
    <button :disabled="isSubmitting">Submit</button>
  </Form>
</template>

VeeValidate Testing

To import vee-validate configuration, add setupFiles to vitest.config.ts.

// vitest.config.ts
export default defineConfig({
  test: {
    // Import below file when run test.
    setupFiles: './src/tests/unitTest/setup.ts'
  }
})

Create setup.ts in src/tests/unitTest and add the following to setup.ts.

// setup.ts
import { localize } from '@vee-validate/i18n'
import en from '@vee-validate/i18n/dist/locale/en.json'
import { all } from '@vee-validate/rules'
import { defineRule, configure } from 'vee-validate'
import { vi } from 'vitest'
import flushPromises from 'flush-promises'

// vee-validate setup
configure({
  generateMessage: localize({
    en,
  }),
})

// import vee-validate all rules
Object.entries(all).forEach(([name, rule]) => {
  defineRule(name, rule)
})

Here is a sample test code of form validation. It tests email format.

See the following for more details.

// form.vue
<script lang="ts" setup>
import { Form, Field, ErrorMessage } from 'vee-validate'

const foo = (values: Record<string, any>) => {
  console.log(values.email)
}
</script>

<template>
  <Form v-slot="{ meta, isSubmitting }" @submit="foo">
    <Field rules="required|email" name="email" as="input" type="text" placeholder="email" />
    <ErrorMessage name="email" />
    <button :disabled="!meta.valid">Submit</button>
  </Form>
</template>
// form.spec.ts
import { expect, test } from 'vitest'
import { render, screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import Form from './pages/form.vue'

test('should error message display', async () => {
  // Arrange
  const user = userEvent.setup()
  render(Form)

  // Act
  await user.type(screen.getByPlaceholderText('email'), 'abc{Tab}')

  // Assert
  expect(screen.getByText('The email field must be a valid email')).toBeTruthy()
})

You can implement redirect function in middleware directory.

The file have different functions by setting the the following file name.

  • redirect.ts (anonymous (or inline) route middleware, which are defined directly in the pages where they are used.)
// foo.vue
<script setup>
definePageMeta({
  middleware: ["redirect"]
})
</script>
  • redirect.global.ts (automatically run on every route change)

Here is a sample code. See this for more details.

// redirect.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
  // If you access /
  if (to.path === '/') {
    // Redirect to login
    return navigateTo('login')
  }
})

See this for test implementation .

Pinia Setup

# install Pinia
npm install pinia @pinia/nuxt

If you're using npm, you might encounter an ERESOLVE unable to resolve dependency tree error. In that case, add the the following to your package.json:

{
  "overrides": {
    "vue": "latest"
  }
}

If you see below error message, fix override:vue like below.

npm ERR! Invalid comparator: latest
{
  "overrides": {
    "vue": "3.4.30"
  }
}

Add the following to nuxt.config.ts

// nuxt.config.ts
export default defineNuxtConfig({
    modules: [
        ['@pinia/nuxt',
            {
                autoImports: [
                  // Import defineStore
                  'defineStore'
                ]
                // If you use vuex at the same time, add the following
                // disableVuex: false
            }
        ]
    ]
});

Store implementation

Create user.ts in store directory and add the following to user.ts.

// user.ts
// If you add defineStore to autoImports in nuxt.config.ts, you don't need to import below
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    // User definition and initialization
    user: { email: '', password: '' }
  }),
  actions: {
    // Update use info
    setUserInfo (email: string, password: string) {
      this.user.email = email
      this.user.password = password
    }
  }
})

Here is a sample code using store from vue file.

// store.vue
<script lang="ts" setup>
import { useUserStore } from '../store/user'

// Use store
const store = useUserStore()

// Get email from store user info
const email = store.user.email

// Get password from store user info
const password = store.user.password

// Update store user info
store.setUserInfo("new email", "new password")
</script>

Pinia Testing

# install @pinia/testing
npm install --save-dev @pinia/testing

When run test file using pinia, the following error occurs.

getActivePinia was called with no active Pinia. Did you forget to install pinia?

To avoid this error, call setActivePinia function in beforeEach.

import { beforeEach, describe, expect, test } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '../../../store/user'

const initialUser = {
  email: '',
  password: '',
}
const updatedUser = {
  email: 'new email',
  password: 'new password',
}

describe('Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  test('store user info should be initial state', () => {
    // Arrange
    const store = useUserStore()

    // Assert
    expect(store.user).toEqual(initialUser)
  })

  test('if you call setUserInfo(), store user info should update', () => {
    // Arrange
    const store = useUserStore()

    // Act
    store.setUserInfo(updatedUser.email, updatedUser.password)

    // Assert
    expect(store.user).toEqual(updatedUser)
  })
})

You can set the initial state of all of your stores when creating a testing pinia by passing an initialState. See this for more details.

<script lang="ts" setup>
import { useUserStore } from '../store/user'

const store = useUserStore()
const email = store.user.email
const password = store.user.password
</script>

<template>
  <div>
    <p>Email: {{ email }}</p>
    <p>Password: {{ password }}</p>
  </div>
</template>
import { beforeEach, expect, test } from 'vitest'
import { render, screen } from '@testing-library/vue'
import { setActivePinia, createPinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import Foo from './pages/index.vue'

beforeEach(() => {
  setActivePinia(createPinia())
})

test('store user info should set the initial value', () => {
  // Arrange
  render(Foo, {
    global: {
      plugins: [
        createTestingPinia({
          initialState: {
            user: { user: { email: '[email protected]', password: 'test' } },
          },
        }),
      ],
    },
  })

  // Assert
  expect(screen.getByText('Email: [email protected]')).toBeTruthy()
  expect(screen.getByText('Password: test')).toBeTruthy()
})

Nuxt provides useFetch instead of axios. It handles data fetching within your application. See this for more details.

// api.vue
<script lang="ts" setup>
const { data: bar } = await useFetch('/api/v1/foo')
</script>

<template>
  Result: {{ bar }}
</template>

Storybook Setup

Install Storybook

npx storybook@latest init --type vue3 --builder vite

Add the following to scripts in package.json

"scripts": {
  "storybook": "storybook dev -p 6006",
},

NOTE: In Storybook 7, It would automatically add frameworks-specific Vite plugins, e.g. @vitejs/plugin-react if not installed. In Storybook 8 those plugins have to be added explicitly in the user's vite.config.ts:

// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [vue()],
});

Without the above configurration, the follwing error will occur.

 [vite] Internal server error: Failed to parse source for import analysis because the content contains invalid JS syntax. Install @vitejs/plugin-vue to handle .vue files.

Create the new vue file and new story like this.

// pages/index.vue
<template>
  <div>
    Pages/index.vue
  </div>
</template>
// pages/index.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3'
import Index from './index.vue'

type Story = StoryObj<typeof Index>
const meta: Meta<typeof Index> = {
  title: 'Index',
}

export const Default: Story = {
  render: () => ({
    components: { Index },
    template: '<Index />',
  }),
}

export default meta

Run the following command to start storybook, and then you can access http://localhost:6006/

npm run storybook

Install @nuxtjs/storybook dependency to your project.

npx nuxi@latest module add storybook

After installation this library, the following command will start nuxt and Storybook at the same time.

npm run dev

Add the following to modules in nuxt.config.ts.

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/storybook'],
})

You can edit the storybook configuration with the storybook property in nuxt.config.ts.

Add the following to nuxt.config.ts. See more options.

// nuxt.config.ts
export default defineNuxtConfig({
  storybook: {
    host: 'http://localhost',
    port: 6006,
  },
})

Import configuration

If you use an alias in a vue file, an error will occur like below when storybook is running.

TypeError: Failed to fetch dynamically imported module:
// foo.vue
import Foo from '~/components/Foo.vue'

Add an alias to viteFinal in .storybook/main.ts to avoid above error.

import type { StorybookConfig } from "@storybook/vue3-vite";
import path from "path";

const config: StorybookConfig = {
  // add this
  viteFinal: async (config) => {
    if (config?.resolve?.alias) {
      config.resolve.alias = {
        ...config.resolve.alias,
        '@': path.resolve(__dirname, '../src'),
        '~': path.resolve(__dirname, '../src'),
      }
    }
    return config
  },
};

Nuxt auto import configuration

Storybook cannot import functions that are automatically imports by nuxt (e.g. ref, computed, and so on.).

Install the following library to import nuxt auto-imports functions in storybook.

  • unplugin-auto-import
npm install --save-dev unplugin-auto-import

Add the following to viteFinal in .storybook/main.ts

import AutoImportFunctions from "unplugin-auto-import/vite";

const config: StorybookConfig = {
  viteFinal: async (config) => {
    if (config?.plugins) {
      // add this
      config.plugins.push(
        AutoImportFunctions ({ imports: [
          'vue',
          'vee-validate',
          'vue-router',
          'pinia',
        ], dts: '.storybook/auto-imports.d.ts' }),
      )
    }
    return config
  },
}

Storybook cannot import components that are automatically imports by nuxt.

Install the following library to import nuxt auto-imports components in storybook.

  • unplugin-vue-components
npm install --save-dev unplugin-vue-components

Add the following to viteFinal in .storybook/main.ts

import AutoImportComponents from 'unplugin-vue-components/vite'

const config: StorybookConfig = {
  viteFinal: async (config) => {
    if (config?.plugins) {
      // add this
      config.plugins.push(
        AutoImportComponents({
          dirs: ['src/components'],
          dts: '.storybook/components.d.ts',
        }),
      )
    }
    return config
  },
}

Using Pinia in Storybook

Storybook cannot use pinia by default. The following error will occur when using pinia in vue file.

"getActivePinia()" was called but there was no active Pinia. Are you trying to use a store before calling "app.use(pinia)"?

To aboid this, add the follwing to .storybook/preview.ts.

// .storybook/preview.ts
import { type Preview, setup } from '@storybook/vue3'
import { type App } from 'vue'
import { createPinia } from 'pinia'

const pinia = createPinia()

setup((app: App) => {
  app.use(pinia)
})

If you want to set initial state in store, add the follwing to each story in storybook.

import type { Meta, StoryObj } from '@storybook/vue3'
import Index from './index.vue'
import { useUserStore } from '~/store/user'

type Story = StoryObj<typeof Index>

const meta: Meta<typeof Index> = {
  title: 'Index',
}

export const Default: Story = {
  render: () => ({
    setup() {
      // add this
      const store = useUserStore()
      store.user.email = '[email protected]'
      store.user.password = 'foobar'
    },
    components: { Index },
    template: '<Indesx />',
  }),
}

export default meta

Using Vee-Validate in Storybook

Storybook cannot use Vee-Validate by default. The following error will occur when using Vee-Validate in vue file.

Error: No such validator 'XXXX' exists.

To aboid this, add the follwing to .storybook/preview.ts.

// .storybook/preview.ts
import { localize } from '@vee-validate/i18n'
import en from '@vee-validate/i18n/dist/locale/en.json'
import { all } from '@vee-validate/rules'
import { defineRule, configure } from 'vee-validate'

configure({
  generateMessage: localize({ en }),
})

Object.entries(all).forEach(([name, rule]) => {
  // import all validation-rules
  defineRule(name, rule)
})

Mocking API Request in Storybook

Use msw to mock Rest and GraphQL requests right inside your story in storybook. With msw-storybook-addon, you can easily mock your APIs, making that process much simpler.

npm install --save-dev msw msw-storybook-addon
npx msw init public/

Enable MSW in Storybook by initializing MSW and providing the MSW decorator in ./storybook/preview.js

// .storybook\preview.ts
import { initialize, mswLoader } from 'msw-storybook-addon'

// Initialize MSW
initialize()

const preview: Preview = {
  // Provide the MSW addon loader globally
  loaders: [mswLoader],
}

export default preview

Then ensure the staticDirs property in your Storybook configuration will include the generated service worker file (in /public, by default).

// .storybook\main.ts
const config: StorybookConfig = {
  staticDirs: ['../public'],
}
export default config

Here is an example uses the fetch API to make network requests.

// index.vue
<script lang="ts" setup>
import { useFetch } from '@vueuse/core'

const uuid = ref('')
const handleClick = async () => {
  const { data } = await useFetch('https://httpbin.org/uuid').json()
  uuid.value = data.value.uuid
}
</script>

<template>
  <div>
    <input type="submit" value="Get uuid" @click="handleClick">
    <p>UUID = {{ uuid }}</p>
  </div>
</template>
// index.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3'
import { http, HttpResponse } from 'msw'
import Index from './index.vue'

type Story = StoryObj<typeof Index>

const meta: Meta<typeof Index> = {
  title: 'Index',
}

export const Default: Story = {
  render: () => ({
    components: { Index },
    template: '<Index />',
  }),
  parameters: {
    msw: {
      handlers: [
        http.get('https://httpbin.org/uuid', () => {
          return HttpResponse.json({
            uuid: 'test uuid',
          })
        }),
      ],
    },
  },
}

export default meta

msw-storybook-addon starts MSW with default configuration. If you want to configure it, you can pass options to the initialize function. They are the StartOptions from setupWorker. A common example is to configure the onUnhandledRequest behavior, as MSW logs a warning in case there are requests which were not handled. If you want MSW to bypass unhandled requests and not do anything:

// preview.ts
import { initialize } from 'msw-storybook-addon';

initialize({
  onUnhandledRequest: 'bypass'
})

Run interaction testing inside Storybook

Storybook's test addon allows you to test your components directly inside Storybook. It does this by using a Vitest plugin to transform your stories into Vitest tests using portable stories.

Before installing, make sure your project meets the following requirements:

  • Storybook ≥ 8.4
  • A Storybook framework that uses Vite (e.g. vue3-vite), or the Storybook Next.js framework
  • Vitest ≥ 2.1

Run the following command to install and configure the addon, which contains the plugin to run your stories as tests using Vitest:

npx storybook add @storybook/experimental-addon-test

That add command will install and register the test addon. It will also inspect your project's Vite and Vitest setup, and install and configure them with sensible defaults, if necessary. Make sure the following ts file have been created.

// vitest.workspace.ts
import path from 'path'
import { defineWorkspace } from 'vitest/config'
import { storybookTest } from '@storybook/experimental-addon-test/vitest-plugin'
import { storybookVuePlugin } from '@storybook/vue3-vite/vite-plugin'
import AutoImportFunctions from 'unplugin-auto-import/vite'
import AutoImportComponents from 'unplugin-vue-components/vite'

export default defineWorkspace([
  'vitest.config.ts',
  {
    extends: 'vite.config.ts',
    plugins: [
      storybookTest({ configDir: '.storybook' }),
      storybookVuePlugin(),
      // Import nuxt-auto-imports functions
      AutoImportFunctions ({ imports: [
        'vue',
        'vee-validate',
        'vue-router',
        'pinia',
      ], dts: '.storybook/auto-imports.d.ts',
      }),
      // Import nuxt-auto-imports components
      AutoImportComponents({
        dirs: ['src/components'],
        dts: '.storybook/components.d.ts',
      }),
    ],
    resolve: {
      alias: {
        '~': path.resolve(__dirname, './src'),
        '@': path.resolve(__dirname, './src'),
      },
    },
    test: {
      name: 'storybook',
      browser: {
        enabled: true,
        headless: true,
        name: 'chromium',
        provider: 'playwright',
      },
      include: ['**/*.stories.?(m)[jt]s?(x)'],
      setupFiles: ['.storybook/vitest.setup.ts'],
    },
  },
])

Add the follwing to scripts in package.json. --project=storybook will run tests only stories.ts.

  "scripts": {
    "test:storybook": "vitest --project=storybook",
  },

Here is an example.

// index.vue
<script lang="ts" setup>
import { useFetch } from '@vueuse/core'

const uuid = ref('')
const handleClick = async () => {
  const { data } = await useFetch('https://httpbin.org/uuid').json()
  uuid.value = data.value.uuid
}
</script>

<template>
  <div>
    <input type="submit" value="Get uuid" @click="handleClick">
    <p>UUID = {{ uuid }}</p>
  </div>
</template>
// index.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3'
import { http, HttpResponse } from 'msw'
import { within, userEvent } from '@storybook/test'
import Index from './index.vue'

type Story = StoryObj<typeof Index>

const meta: Meta<typeof Index> = {
  title: 'Index',
}
export default meta

export const GetUuid: Story = {
  render: () => ({
    components: { Index },
    template: '<Index />',
  }),
  parameters: {
    msw: {
      handlers: [
        http.get('https://httpbin.org/uuid', () => {
          return HttpResponse.json({
            uuid: 'test uuid',
          })
        }),
      ],
    },
  },
  play: async ({ canvasElement }) => {
    // Arrange
    const canvas = within(canvasElement)

    // Act
    await userEvent.click(await canvas.findByText('Get uuid'))

    // Assert
    await expect(canvas.getByText('UUID = test uuid')).toBeInTheDocument()
  },
}

Run the following command to run tests.

npm run test:storybook

E2E Testing By Puppeteer

Most things that you can do manually in the browser can be done using Puppeteer as E2E testing.

# install Puppeteer
npm install --save-dev puppeteer
<script lang="ts" setup>
import { Form, Field } from 'vee-validate'
</script>

<template>
  <Form v-slot="{ meta, isSubmitting }">
    <Field
      rules="required|email"
      name="email"
      as="input"
      type="text"
    />
    <Field
      rules="required"
      name="password"
      as="input"
      type="text"
    />
    <button
      :disabled="isSubmitting || !meta.valid"
      data-testid="submit-btn"
    >
      Submit
    </button>
  </Form>
</template>

Here is a sample E2E testing code. It tests submit button state.

import { afterAll, beforeAll, describe, expect, test } from 'vitest'
import { launch, PuppeteerLaunchOptions } from 'puppeteer'
import type { Browser, Page } from 'puppeteer'

// Set browser launch option. See the following for more details.
// https://pptr.dev/api/puppeteer.browserlaunchargumentoptions
const options: PuppeteerLaunchOptions = {
  headless: false,
  slowMo: 75,
  defaultViewport: {
    width: 1280,
    height: 1024
  },
  devtools: true,
  args: ['--window-size=1680,1024']
}

describe('E2E', () => {
  let browser: Browser
  let page: Page

  beforeAll(async () => {
    browser = await launch(options)
    page = await browser.newPage()
  })

  afterAll(async () => {
    await browser.close()
  })

   test('1-If you input a valid value, submit button should enable', async () => {
      try {
        // Arrange
        await page.goto('http://localhost:3000/foo')

        // Act
        // Input email
        await page.type('input[name="email"]', '[email protected]')

        // Input password
        await page.type('input[name="password"]', 'foo')

        // Get submit button state. inactive → true, active → false
        const isDisabled = await page.$eval(
          '[data-testid="submit-btn"]',
          element => (element as HTMLButtonElement).disabled
        )

        // Take a screenshot
        await page.screenshot({
          path: './src/tests/e2eTest/evidence/pages/foo/test-01.png',
          fullPage: true
        })

        // Assert
        expect(isDisabled).toBe(false)
      } catch (e) {
        console.error(e)
        expect(e).toBeUndefined()
      }
    }, 60000)
})

To run E2E testing, add the test file path to config:path in package.json.

{
  "config": {
    "path": "./src/tests/e2eTest/spec/foo.spec.ts"
  },
}
# run application server
npm run dev

# run E2E testing
npm run test:e2e

Analyzing source code by SonarQube

SonarQube is a self-managed, automatic code review tool that systematically helps you deliver clean code.

# install SonarQube tools
npm install --save-dev sonarqube-scanner vitest-sonar-reporter

Add the following to vitest.config.ts.

  • add lcov to reporter
  • add reporters and outputFile to test
// vitest.config.ts
export default defineConfig({
  test: {
    coverage: {
      // To collect coverage by SonarQube, add lcov.
      reporter: ['html', 'clover', 'text', 'lcov']
    },
    // To analyze your test code by SonarQube, output test report file
    reporters: ['verbose', 'vitest-sonar-reporter'],
    outputFile: 'test-report.xml',
  }
})

Create sonar-project.properties in root directory and add the following to sonar-project.properties. See this for more details.

sonar.projectKey=nuxt3-starter-guide
sonar.projectName=nuxt3-starter-guide
sonar.sources=src
sonar.tests=src/tests/
sonar.test.inclusions=src/tests/**/*.spec.ts
sonar.exclusions=**/*plugins*/**, src/tests/**/*.spec.ts, src/tests/**/setup.ts
sonar.testExecutionReportPaths=test-report.xml
sonar.javascript.file.suffixes=.js,.jsx
sonar.typescript.file.suffixes=.ts,.tsx,.vue
sonar.typescript.lcov.reportPaths=coverage/lcov.info
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.host.url=http://localhost:9000
sonar.token=sqp_XXXXXXXXXXXXXXXXXXXXXX

Create a SonarQube project

Make sure you have installed SonarQube (v10.7) on your development machine. Run SonarQube server as localhost:9000 before do the following.

To create a SonarQube project, do the following.

  1. Access the following url. http://localhost:9000/projects/create

  2. Click [Create a local project]

  3. Input nuxt3-starter-guide in Project display name and Project key. Click [Next]

  4. Select [Use the global setting] and click [Create project]

  5. Click [Locally]

  6. Click [Generate] and then generate project token

Analyze your source code

Add project token to sonar.token in sonar-project.properties. See this for more details of token.

sonar.token=sqp_XXXXXXXXXXXXXXXXXXXXXX

Add the following to scripts in package.json.

{
  "scripts": {
    "sonar": "sonar-scanner"
  },
}

Run below command to run SonarQube analysis.

# run all tests
npm run test:all

# run SonarQube analysis
npm run sonar

You can access the following url to show result.

http://localhost:9000/dashboard?id=nuxt3-starter-guide

About

This project is a nuxt 3 starter project. It implements Vitest, Storybook, ESLint (flat config, stylistic), VeeValidate, Pinia, Puppeteer, SonarQube, TypeScript. This project compatible with nuxt 3.14

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published