test: add settings page tests

This commit is contained in:
mutoe 2023-09-07 22:30:10 +08:00 committed by Dongsen
parent 4e0c2955bd
commit fc9ffe1bc9
19 changed files with 471 additions and 41 deletions

View File

@ -10,7 +10,7 @@
"lint:script": "eslint \"{src/**/*.{ts,vue},cypress/**/*.ts}\"",
"lint:tsc": "vue-tsc --noEmit",
"lint": "concurrently \"npm run lint:tsc\" \"npm run lint:script\"",
"test:unit": "vitest run --coverage",
"test:unit": "vitest run",
"test:e2e": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress open --e2e -c baseUrl=http://localhost:4137\"",
"test:e2e:local": "cypress open --e2e -c baseUrl=http://localhost:5173",
"test:e2e:ci": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress run --e2e -c baseUrl=http://localhost:4137\"",
@ -29,6 +29,7 @@
"@mutoe/eslint-config-preset-vue": "~2.1.2",
"@pinia/testing": "^0.1.3",
"@testing-library/cypress": "^8.0.7",
"@testing-library/user-event": "^14.4.3",
"@testing-library/vue": "^7.0.0",
"@types/marked": "^4.0.8",
"@vitejs/plugin-vue": "^4.3.4",

View File

@ -31,6 +31,9 @@ devDependencies:
'@testing-library/cypress':
specifier: ^8.0.7
version: 8.0.7(cypress@11.2.0)
'@testing-library/user-event':
specifier: ^14.4.3
version: 14.4.3(@testing-library/dom@9.3.1)
'@testing-library/vue':
specifier: ^7.0.0
version: 7.0.0(@vue/compiler-sfc@3.3.4)(vue@3.3.4)
@ -658,6 +661,15 @@ packages:
pretty-format: 27.5.1
dev: true
/@testing-library/user-event@14.4.3(@testing-library/dom@9.3.1):
resolution: {integrity: sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==}
engines: {node: '>=12', npm: '>=6'}
peerDependencies:
'@testing-library/dom': '>=7.21.4'
dependencies:
'@testing-library/dom': 9.3.1
dev: true
/@testing-library/vue@7.0.0(@vue/compiler-sfc@3.3.4)(vue@3.3.4):
resolution: {integrity: sha512-JU/q93HGo2qdm1dCgWymkeQlfpC0/0/DBZ2nAHgEAsVZxX11xVIxT7gbXdI7HACQpUbsUWt1zABGU075Fzt9XQ==}
engines: {node: '>=14'}

View File

@ -1,8 +1,8 @@
import { render } from '@testing-library/vue'
import ArticlesList from 'src/components/ArticlesList.vue'
import fixtures from 'src/utils/test/fixtures'
import { asyncWrapper, renderOptions, setupMockServer } from 'src/utils/test/test.utils'
import { describe, it, expect } from 'vitest'
import ArticlesList from './ArticlesList.vue'
describe('# ArticlesList', () => {
const server = setupMockServer(

View File

@ -23,11 +23,11 @@ exports[`# ArticleDetail > should filter the xss content in Markdown body 1`] =
aria-label="profile"
class=""
data-v-b9ba5d49=""
href="/profile/Author%20name"
href="/profile/mutoe"
>
<img
alt="Author name"
alt="mutoe"
data-v-b9ba5d49=""
src=""
/>
@ -41,10 +41,10 @@ exports[`# ArticleDetail > should filter the xss content in Markdown body 1`] =
aria-label="profile"
class="author"
data-v-b9ba5d49=""
href="/profile/Author%20name"
href="/profile/mutoe"
>
Author name
mutoe
</a>
<span
@ -120,11 +120,11 @@ exports[`# ArticleDetail > should filter the xss content in Markdown body 1`] =
aria-label="profile"
class=""
data-v-b9ba5d49=""
href="/profile/Author%20name"
href="/profile/mutoe"
>
<img
alt="Author name"
alt="mutoe"
data-v-b9ba5d49=""
src=""
/>
@ -138,10 +138,10 @@ exports[`# ArticleDetail > should filter the xss content in Markdown body 1`] =
aria-label="profile"
class="author"
data-v-b9ba5d49=""
href="/profile/Author%20name"
href="/profile/mutoe"
>
Author name
mutoe
</a>
<span
@ -202,11 +202,11 @@ exports[`# ArticleDetail > should render markdown (zh-CN) body correctly 1`] = `
aria-label="profile"
class=""
data-v-b9ba5d49=""
href="/profile/Author%20name"
href="/profile/mutoe"
>
<img
alt="Author name"
alt="mutoe"
data-v-b9ba5d49=""
src=""
/>
@ -220,10 +220,10 @@ exports[`# ArticleDetail > should render markdown (zh-CN) body correctly 1`] = `
aria-label="profile"
class="author"
data-v-b9ba5d49=""
href="/profile/Author%20name"
href="/profile/mutoe"
>
Author name
mutoe
</a>
<span
@ -1539,11 +1539,11 @@ D--&gt;&gt;A: Dashed open arrow
aria-label="profile"
class=""
data-v-b9ba5d49=""
href="/profile/Author%20name"
href="/profile/mutoe"
>
<img
alt="Author name"
alt="mutoe"
data-v-b9ba5d49=""
src=""
/>
@ -1557,10 +1557,10 @@ D--&gt;&gt;A: Dashed open arrow
aria-label="profile"
class="author"
data-v-b9ba5d49=""
href="/profile/Author%20name"
href="/profile/mutoe"
>
Author name
mutoe
</a>
<span
@ -1621,11 +1621,11 @@ exports[`# ArticleDetail > should render markdown body correctly 1`] = `
aria-label="profile"
class=""
data-v-b9ba5d49=""
href="/profile/Author%20name"
href="/profile/mutoe"
>
<img
alt="Author name"
alt="mutoe"
data-v-b9ba5d49=""
src=""
/>
@ -1639,10 +1639,10 @@ exports[`# ArticleDetail > should render markdown body correctly 1`] = `
aria-label="profile"
class="author"
data-v-b9ba5d49=""
href="/profile/Author%20name"
href="/profile/mutoe"
>
Author name
mutoe
</a>
<span
@ -2674,11 +2674,11 @@ Third paragraph of definition 2.
aria-label="profile"
class=""
data-v-b9ba5d49=""
href="/profile/Author%20name"
href="/profile/mutoe"
>
<img
alt="Author name"
alt="mutoe"
data-v-b9ba5d49=""
src=""
/>
@ -2692,10 +2692,10 @@ Third paragraph of definition 2.
aria-label="profile"
class="author"
data-v-b9ba5d49=""
href="/profile/Author%20name"
href="/profile/mutoe"
>
Author name
mutoe
</a>
<span

View File

@ -0,0 +1,116 @@
import userEvent from '@testing-library/user-event'
import { fireEvent, render } from '@testing-library/vue'
import { router } from 'src/router.ts'
import fixtures from 'src/utils/test/fixtures.ts'
import { renderOptions, setupMockServer } from 'src/utils/test/test.utils.ts'
import { describe, expect, it, vi } from 'vitest'
import EditArticle from './EditArticle.vue'
describe('# EditArticle page', () => {
const server = setupMockServer()
it('should call create api when fill form and click submit button', async () => {
server.use(['POST', '/api/articles', { article: { ...fixtures.article, slug: 'article-title' } }])
vi.spyOn(router, 'push')
const { getByRole, getByPlaceholderText } = render(EditArticle, await renderOptions({
router,
initialRoute: '/articles',
}))
await fireEvent.update(getByPlaceholderText('Article Title'), 'Article Title')
await fireEvent.update(getByPlaceholderText("What's this article about?"), 'Article descriptions')
await fireEvent.update(getByPlaceholderText('Write your article (in markdown)'), 'this is **article body**.')
await userEvent.type(getByPlaceholderText('Enter tags'), 'tag1{Enter}tag2{Enter}')
await fireEvent.click(getByRole('button', { name: 'Publish Article' }))
const mockedRequest = await server.waitForRequest('POST', '/api/articles')
expect(router.push).toHaveBeenCalledWith({ name: 'article', params: { slug: 'article-title' } })
expect(await mockedRequest.json()).toMatchInlineSnapshot(`
{
"article": {
"body": "this is **article body**.",
"description": "Article descriptions",
"tagList": [
"tag1",
"tag2",
],
"title": "Article Title",
},
}
`)
})
it('should call update api when click submit button and in editing', async () => {
server.use(
['GET', '/api/articles/*', { article: fixtures.article }],
['PUT', '/api/articles/*', { article: fixtures.article }],
)
vi.spyOn(router, 'push')
const { getByRole, getByPlaceholderText } = render(EditArticle, await renderOptions({
router,
initialRoute: { name: 'article', params: { slug: 'article-foo' } },
}))
await server.waitForRequest('GET', '/api/articles/*')
await userEvent.type(getByPlaceholderText('Enter tags'), 'tag1{Enter}tag2{Enter}')
await fireEvent.click(getByRole('button', { name: 'Publish Article' }))
const mockedRequest = await server.waitForRequest('PUT', '/api/articles/article-foo')
expect(router.push).toHaveBeenCalledWith({ name: 'article', params: { slug: 'article-foo' } })
expect(await mockedRequest.json()).toMatchInlineSnapshot(`
{
"article": {
"body": "# Article body
This is **Strong** content.",
"description": "Article description",
"tagList": [
"foo",
"tag1",
"tag2",
],
"title": "Article foo",
},
}
`)
})
it('should can remove tag when lick remove tag button', async () => {
server.use(
['GET', '/api/articles/*', { article: fixtures.article }],
['PUT', '/api/articles/*', { article: fixtures.article }],
)
const { getByRole, getByPlaceholderText } = render(EditArticle, await renderOptions({
router,
initialRoute: { name: 'article', params: { slug: 'article-foo' } },
}))
await server.waitForRequest('GET', '/api/articles/*')
await userEvent.type(getByPlaceholderText('Enter tags'), 'tag1{Enter}tag2{Enter}')
await userEvent.click(getByRole('button', { name: 'Remove tag: tag1' }))
await fireEvent.click(getByRole('button', { name: 'Publish Article' }))
const mockedRequest = await server.waitForRequest('PUT', '/api/articles/article-foo')
expect(await mockedRequest.json()).toMatchInlineSnapshot(`
{
"article": {
"body": "# Article body
This is **Strong** content.",
"description": "Article description",
"tagList": [
"foo",
"tag2",
],
"title": "Article foo",
},
}
`)
})
})

View File

@ -45,6 +45,8 @@
>
<i
class="ion-close-round"
role="button"
:aria-label="`Remove tag: ${tag}`"
@click="removeTag(tag)"
/>
{{ tag }}

66
src/pages/Login.spec.ts Normal file
View File

@ -0,0 +1,66 @@
import { fireEvent, render } from '@testing-library/vue'
import { useUserStore } from 'src/store/user.ts'
import fixtures from 'src/utils/test/fixtures.ts'
import { createTestRouter, renderOptions, setupMockServer } from 'src/utils/test/test.utils.ts'
import { describe, expect, it, vi } from 'vitest'
import Login from './Login.vue'
describe('# Login page', () => {
const server = setupMockServer()
it('should call login api when fill form and click submit button', async () => {
const router = createTestRouter()
server.use(['POST', '/api/users/login', { user: fixtures.user }])
const { getByRole, getByPlaceholderText } = render(Login, renderOptions({
router,
}))
const store = useUserStore()
await fireEvent.update(getByPlaceholderText('Email'), 'email@email.com')
await fireEvent.update(getByPlaceholderText('Password'), 'password')
await fireEvent.click(getByRole('button', { name: 'Sign in' }))
const mockedRequest = await server.waitForRequest('POST', '/api/users/login')
expect(router.currentRoute.value.path).toBe('/')
expect(store.updateUser).toHaveBeenCalledWith(fixtures.user)
expect(await mockedRequest.json()).toMatchInlineSnapshot(`
{
"user": {
"email": "email@email.com",
"password": "password",
},
}
`)
})
it('should display error message when api returned some errors', async () => {
server.use(['POST', '/api/users/login', 400, { errors: { password: ['is invalid'] } }])
const { container, getByRole, getByPlaceholderText } = render(Login, renderOptions())
await fireEvent.update(getByPlaceholderText('Email'), 'email@email.com')
await fireEvent.update(getByPlaceholderText('Password'), 'password')
await fireEvent.click(getByRole('button', { name: 'Sign in' }))
await server.waitForRequest('POST', '/api/users/login')
expect(container).toHaveTextContent('password is invalid')
})
it('should not trigger api call when user submit a invalid form', async () => {
const { getByRole, getByPlaceholderText } = render(Login, renderOptions())
const formElement = getByRole('form', { name: 'Login form' }) as HTMLFormElement
vi.spyOn(formElement, 'checkValidity')
expect(getByRole('button', { name: 'Sign in' })).toHaveProperty('disabled', true)
await fireEvent.update(getByPlaceholderText('Email'), 'email')
await fireEvent.update(getByPlaceholderText('Password'), 'password')
await fireEvent.click(getByRole('button', { name: 'Sign in' }))
expect(formElement.checkValidity).toHaveBeenCalled()
})
})

View File

@ -23,6 +23,7 @@
<form
ref="formRef"
aria-label="Login form"
@submit.prevent="login"
>
<fieldset

57
src/pages/Profile.spec.ts Normal file
View File

@ -0,0 +1,57 @@
import { fireEvent, render } from '@testing-library/vue'
import Profile from 'src/pages/Profile.vue'
import { router } from 'src/router.ts'
import fixtures from 'src/utils/test/fixtures.ts'
import { asyncWrapper, createTestRouter, flushPromises, renderOptions, setupMockServer } from 'src/utils/test/test.utils.ts'
import { describe, it, expect, vi } from 'vitest'
describe('# Profile page', () => {
const server = setupMockServer(
['GET', '/api/profiles/*', { profile: fixtures.user }],
['GET', '/api/articles', { articles: [fixtures.article], articlesCount: 1 }],
)
it('should display user info', async () => {
const router = createTestRouter()
const { container } = render(asyncWrapper(Profile), await renderOptions({
router,
initialState: { user: { user: null } },
initialRoute: '/profile/mutoe',
}))
await flushPromises()
expect(container).toHaveTextContent('mutoe')
})
it('should display edit button when author logged', async () => {
vi.spyOn(router, 'push')
const { getByRole } = render(asyncWrapper(Profile), await renderOptions({
router,
initialState: { user: { user: fixtures.user } },
initialRoute: '/profile/mutoe',
}))
await flushPromises()
await fireEvent.click(getByRole('link', { name: 'Edit profile settings' }))
expect(router.push).toHaveBeenCalledWith({ name: 'settings', params: {} })
})
it('should jump to login page when click follow user', async () => {
server.use(['POST', '/api/profiles/*/follow', { profile: fixtures.user }])
vi.spyOn(router, 'push')
const { getByRole } = render(asyncWrapper(Profile), await renderOptions({
router,
initialState: { user: { user: null } },
initialRoute: '/profile/mutoe',
}))
await flushPromises()
await fireEvent.click(getByRole('button', { name: 'Follow mutoe' }))
await server.waitForRequest('POST', '/api/profiles/*/follow')
})
})

View File

@ -27,6 +27,7 @@
v-if="showEdit"
class="btn btn-sm btn-outline-secondary action-btn"
name="settings"
aria-label="Edit profile settings"
>
<i class="ion-gear-a space" />
Edit profile settings
@ -71,7 +72,7 @@ import ArticlesList from 'src/components/ArticlesList.vue'
import { useFollow } from 'src/composable/useFollowProfile'
import { useProfile } from 'src/composable/useProfile'
import type { Profile } from 'src/services/api'
import { isAuthorized, useUserStore } from 'src/store/user'
import { useUserStore } from 'src/store/user'
import { computed } from 'vue'
import { useRoute } from 'vue-router'
@ -86,9 +87,9 @@ const { followProcessGoing, toggleFollow } = useFollow({
onUpdate: (newProfileData: Profile) => updateProfile(newProfileData),
})
const { user } = storeToRefs(useUserStore())
const { user, isAuthorized } = storeToRefs(useUserStore())
const showEdit = computed<boolean>(() => isAuthorized() && user.value?.username === username.value)
const showEdit = computed<boolean>(() => isAuthorized && user.value?.username === username.value)
const showFollow = computed<boolean>(() => user.value?.username !== username.value)
</script>

View File

@ -0,0 +1,72 @@
import { fireEvent, render } from '@testing-library/vue'
import { createTestRouter, renderOptions, setupMockServer } from 'src/utils/test/test.utils.ts'
import { describe, expect, it, vi } from 'vitest'
import Register from './Register.vue'
describe('# Register form', () => {
const server = setupMockServer()
it('should call register api when fill form and click submit button', async () => {
const router = createTestRouter()
server.use(['POST', '/api/users'])
const { getByRole, getByPlaceholderText } = render(Register, renderOptions({
router,
}))
await fireEvent.update(getByPlaceholderText('Your Name'), 'username')
await fireEvent.update(getByPlaceholderText('Email'), 'email@email.com')
await fireEvent.update(getByPlaceholderText('Password'), 'password')
await fireEvent.click(getByRole('button', { name: 'Sign up' }))
const mockedRequest = await server.waitForRequest('POST', '/api/users')
expect(router.currentRoute.value.path).toBe('/')
expect(await mockedRequest.json()).toMatchInlineSnapshot(`
{
"user": {
"email": "email@email.com",
"password": "password",
"username": "username",
},
}
`)
})
it('should display error message when api returned some errors', async () => {
server.use(['POST', '/api/users', 400, {
errors: {
email: ['is invalid'],
username: ['is already taken'],
},
}])
const { container, getByRole, getByPlaceholderText } = render(Register, renderOptions())
await fireEvent.update(getByPlaceholderText('Your Name'), 'username')
await fireEvent.update(getByPlaceholderText('Email'), 'email@email.com')
await fireEvent.update(getByPlaceholderText('Password'), 'password')
await fireEvent.click(getByRole('button', { name: 'Sign up' }))
await server.waitForRequest('POST', '/api/users')
expect(container).toHaveTextContent('email is invalid')
expect(container).toHaveTextContent('username is already taken')
})
it('should not trigger api call when user submit a invalid form', async () => {
const { getByRole, getByPlaceholderText } = render(Register, renderOptions())
const formElement = getByRole('form', { name: 'Registration form' }) as HTMLFormElement
vi.spyOn(formElement, 'checkValidity')
expect(getByRole('button', { name: 'Sign up' })).toHaveProperty('disabled', true)
await fireEvent.update(getByPlaceholderText('Your Name'), 'username')
await fireEvent.update(getByPlaceholderText('Email'), 'email')
await fireEvent.update(getByPlaceholderText('Password'), 'password')
await fireEvent.click(getByRole('button', { name: 'Sign up' }))
expect(formElement.checkValidity).toHaveBeenCalled()
})
})

View File

@ -23,6 +23,7 @@
<form
ref="formRef"
aria-label="Registration form"
@submit.prevent="register"
>
<fieldset class="form-group">

View File

@ -0,0 +1,83 @@
import { fireEvent, render, waitFor } from '@testing-library/vue'
import { router } from 'src/router.ts'
import { useUserStore } from 'src/store/user.ts'
import fixtures from 'src/utils/test/fixtures.ts'
import { renderOptions, setupMockServer } from 'src/utils/test/test.utils.ts'
import { describe, expect, it, vi } from 'vitest'
import Settings from './Settings.vue'
describe('# Settings Page', () => {
const server = setupMockServer()
it('should render correctly', async () => {
const { container } = render(Settings, renderOptions({
initialState: { user: { user: fixtures.user } },
}))
expect(container).toHaveTextContent('Your Settings')
})
it('should jump to login page when user not logged', async () => {
vi.spyOn(router, 'push')
render(Settings, await renderOptions({
router,
initialState: { user: { user: null } },
initialRoute: '/settings',
}))
await waitFor(() => expect(router.push).toBeCalled())
})
it('should jump to home page and clear logged state when click logout button', async () => {
vi.spyOn(router, 'push')
const { getByRole } = render(Settings, await renderOptions({
router,
initialState: { user: { user: fixtures.user } },
initialRoute: '/settings',
}))
const store = useUserStore()
await fireEvent.click(getByRole('button', { name: 'Logout' }))
expect(store.isAuthorized).toBe(false)
expect(router.push).toHaveBeenCalledWith({ name: 'global-feed' })
})
it('should not trigger update api when user click submit directly', async () => {
const { getByRole } = render(Settings, await renderOptions({
router,
initialState: { user: { user: fixtures.user } },
initialRoute: '/settings',
}))
expect(getByRole('button', { name: 'Update Settings' })).toHaveProperty('disabled')
})
it('should submit new settings when submit form', async () => {
vi.spyOn(router, 'push')
server.use(['PUT', '/api/user', { user: { ...fixtures.user, username: 'new username' } }])
const { getByRole, getByPlaceholderText } = render(Settings, await renderOptions({
router,
initialState: { user: { user: fixtures.user } },
initialRoute: '/settings',
}))
await fireEvent.update(getByPlaceholderText('Your name'), 'new username')
await fireEvent.update(getByPlaceholderText('New password'), 'new password')
await fireEvent.click(getByRole('button', { name: 'Update Settings' }))
const mockedRequest = await server.waitForRequest('PUT', '/api/user')
expect(router.push).toHaveBeenCalledWith({ name: 'profile', params: { username: 'new username' } })
expect(await mockedRequest.json()).toMatchInlineSnapshot(`
{
"user": {
"bio": "Author bio",
"email": "foo@example.com",
"image": "",
"password": "new password",
"username": "new username",
},
}
`)
})
})

View File

@ -46,7 +46,7 @@
v-model="form.password"
type="password"
class="form-control form-control-lg"
placeholder="New Password"
placeholder="New password"
>
</fieldset>
<button
@ -63,6 +63,7 @@
<button
class="btn btn-outline-danger"
aria-label="Logout"
@click="onLogout"
>
Or click here to logout.
@ -107,10 +108,10 @@ onMounted(async () => {
const isButtonDisabled = computed(() =>
form.image === userStore.user?.image &&
form.username === userStore.user?.username &&
form.bio === userStore.user?.bio &&
form.email === userStore.user?.email &&
!form.password,
form.username === userStore.user?.username &&
form.bio === userStore.user?.bio &&
form.email === userStore.user?.email &&
!form.password,
)
</script>

View File

@ -82,9 +82,6 @@ export const router = createRouter({
export function routerPush (name: AppRouteNames, params?: RouteParams): ReturnType<typeof router.push> {
return params !== undefined
? router.push({
name,
params,
})
? router.push({ name, params })
: router.push({ name })
}

View File

@ -10,7 +10,7 @@ export const isAuthorized = (): boolean => !!userStorage.get()
export const useUserStore = defineStore('user', () => {
const user = ref(userStorage.get())
const isAuthorized = computed(() => user.value !== null)
const isAuthorized = computed(() => !!user.value)
function updateUser (userData?: User | null) {
if (userData === undefined || userData === null) {

View File

@ -1,7 +1,7 @@
import type { Comment, Article, Profile, User } from 'src/services/api'
const author: Profile = {
username: 'Author name',
username: 'mutoe',
bio: 'Author bio',
following: false,
image: '',

View File

@ -20,8 +20,12 @@ export const createTestRouter = (base?: string): Router => createRouter({
interface RenderOptionsArgs {
props: Record<string, unknown>
slots: Record<string, (...args: unknown[]) => unknown>
router?: Router
initialRoute: RouteLocationRaw
initialState: Record<string, unknown>
stubActions: boolean
}
const scheduler = typeof setImmediate === 'function' ? setImmediate : setTimeout
@ -36,7 +40,7 @@ export function renderOptions (): RenderOptions
export function renderOptions (args: Partial<Omit<RenderOptionsArgs, 'initialRoute'>>): RenderOptions
export async function renderOptions (args: (Partial<RenderOptionsArgs> & {initialRoute: RouteLocationRaw})): Promise<RenderOptions>
export function renderOptions (args: Partial<RenderOptionsArgs> = {}): RenderOptions | Promise<RenderOptions> {
const router = createTestRouter()
const router = args.router || createTestRouter()
const result = {
props: args.props,
@ -49,6 +53,7 @@ export function renderOptions (args: Partial<RenderOptionsArgs> = {}): RenderOpt
user: { user: null },
...args.initialState,
},
stubActions: args.stubActions ?? false,
})],
components: { AppLink },
},

View File

@ -23,5 +23,20 @@ export default defineConfig({
snapshotFormat: {
escapeString: false,
},
coverage: {
enabled: true,
provider: 'v8',
include: [
'src',
],
exclude: [
'src/*.{ts,vue}',
'src/services/api.ts',
'src/setupTests.ts',
'src/utils/test',
'**/*.d.ts',
],
all: true,
},
},
})