test: add settings page tests
This commit is contained in:
parent
9dc3e0bd9c
commit
fbc6b8b0c3
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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-->>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-->>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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
|
@ -45,6 +45,8 @@
|
|||
>
|
||||
<i
|
||||
class="ion-close-round"
|
||||
role="button"
|
||||
:aria-label="`Remove tag: ${tag}`"
|
||||
@click="removeTag(tag)"
|
||||
/>
|
||||
{{ tag }}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
<form
|
||||
ref="formRef"
|
||||
aria-label="Login form"
|
||||
@submit.prevent="login"
|
||||
>
|
||||
<fieldset
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
<form
|
||||
ref="formRef"
|
||||
aria-label="Registration form"
|
||||
@submit.prevent="register"
|
||||
>
|
||||
<fieldset class="form-group">
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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: '',
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue