chore: migrate to vitest
This commit is contained in:
parent
bb96c96d3b
commit
9dc3e0bd9c
19
package.json
19
package.json
|
|
@ -8,41 +8,46 @@
|
|||
"build": "vite build",
|
||||
"serve": "vite preview --port 4137",
|
||||
"lint:script": "eslint \"{src/**/*.{ts,vue},cypress/**/*.ts}\"",
|
||||
"lint:tsc": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
||||
"lint:tsc": "vue-tsc --noEmit",
|
||||
"lint": "concurrently \"npm run lint:tsc\" \"npm run lint:script\"",
|
||||
"test:unit": "echo \"TODO: migrate to vitest\"",
|
||||
"test:unit:ci": "echo \"TODO: migrate to vitest\"",
|
||||
"test:unit": "vitest run --coverage",
|
||||
"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\"",
|
||||
"test:e2e:prod": "cypress run --e2e -c baseUrl=https://vue3-realworld-example-app-mutoe.vercel.app",
|
||||
"test": "npm run test:unit:ci && npm run test:e2e:ci",
|
||||
"test": "npm run test:unit && npm run test:e2e:ci",
|
||||
"generate:api": "curl -sL https://raw.githubusercontent.com/gothinkster/realworld/main/api/openapi.yml -o ./src/services/openapi.yml && sta -p ./src/services/openapi.yml -o ./src/services -n api.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"insane": "^2.6.2",
|
||||
"marked": "^4.2.5",
|
||||
"pinia": "^2.0.28",
|
||||
"vue": "^3.2.45",
|
||||
"pinia": "^2.1.6",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mutoe/eslint-config-preset-vue": "~2.1.2",
|
||||
"@pinia/testing": "^0.0.14",
|
||||
"@pinia/testing": "^0.1.3",
|
||||
"@testing-library/cypress": "^8.0.7",
|
||||
"@testing-library/vue": "^7.0.0",
|
||||
"@types/marked": "^4.0.8",
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"@vitest/coverage-v8": "^0.34.3",
|
||||
"concurrently": "^7.6.0",
|
||||
"cypress": "^11.2.0",
|
||||
"eslint": "^8.31.0",
|
||||
"eslint-plugin-chai-friendly": "^0.7.2",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"happy-dom": "^10.11.2",
|
||||
"husky": "^8.0.2",
|
||||
"lint-staged": "^13.1.0",
|
||||
"msw": "^1.3.0",
|
||||
"rollup-plugin-analyzer": "^4.0.0",
|
||||
"swagger-typescript-api": "^12.0.2",
|
||||
"typescript": "~5.0.4",
|
||||
"vite": "^4.4.9",
|
||||
"vitest": "^0.34.3",
|
||||
"vitest-dom": "^0.1.0",
|
||||
"vue-tsc": "^1.8.8"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
|
|
|||
1630
pnpm-lock.yaml
1630
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -1,9 +1,12 @@
|
|||
import AppFooter from 'src/components/AppFooter.vue'
|
||||
import { render } from '@testing-library/vue'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { renderOptions } from 'src/utils/test/test.utils'
|
||||
import AppFooter from './AppFooter.vue'
|
||||
|
||||
describe('# AppFooter', () => {
|
||||
it('should render correctly', () => {
|
||||
cy.mount(AppFooter)
|
||||
const { container } = render(AppFooter, renderOptions())
|
||||
|
||||
cy.contains('Real world app')
|
||||
expect(container).toHaveTextContent('Real world app')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
import { fireEvent, render, waitFor } from '@testing-library/vue'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { renderOptions } from 'src/utils/test/test.utils.ts'
|
||||
import AppLink from './AppLink.vue'
|
||||
|
||||
describe('# AppLink', () => {
|
||||
it('should redirect to another page when click the link', () => {
|
||||
// given
|
||||
cy.mount(AppLink, {
|
||||
it('should redirect to another page when click the link', async () => {
|
||||
const { container, getByRole } = render(AppLink, renderOptions({
|
||||
props: { name: 'tag', params: { tag: 'foo' } },
|
||||
slots: { default: () => 'Go to Foo tag' },
|
||||
})
|
||||
}))
|
||||
|
||||
cy.contains('Go to Foo tag')
|
||||
expect(container).toHaveTextContent('Go to Foo tag')
|
||||
await fireEvent.click(getByRole('link', { name: 'tag' }))
|
||||
|
||||
cy.contains('tag').click()
|
||||
|
||||
// await waitFor(() => expect(linkElement).toHaveClass('router-link-active'))
|
||||
await waitFor(() => expect(getByRole('link', { name: 'tag' })).toHaveClass('router-link-active'))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,32 +1,27 @@
|
|||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useUserStore } from 'src/store/user'
|
||||
import { render } from '@testing-library/vue'
|
||||
import { expect, describe, it } from 'vitest'
|
||||
import { renderOptions } from 'src/utils/test/test.utils.ts'
|
||||
import AppNavigation from './AppNavigation.vue'
|
||||
|
||||
describe('# AppNavigation', () => {
|
||||
beforeEach(async () => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
it('should render Sign in and Sign up when user not logged', () => {
|
||||
const userStore = useUserStore()
|
||||
userStore.updateUser(null)
|
||||
cy.mount(AppNavigation)
|
||||
const { getByRole } = render(AppNavigation, renderOptions())
|
||||
|
||||
cy.get('.nav-item').should('have.length', 3)
|
||||
cy.contains('Home')
|
||||
cy.contains('Sign in')
|
||||
cy.contains('Sign up')
|
||||
expect(getByRole('link', { name: 'Home' })).toHaveTextContent('Home')
|
||||
expect(getByRole('link', { name: 'Sign in' })).toHaveTextContent('Sign in')
|
||||
expect(getByRole('link', { name: 'Sign up' })).toHaveTextContent('Sign up')
|
||||
})
|
||||
|
||||
it('should render xxx when user logged', () => {
|
||||
const userStore = useUserStore()
|
||||
userStore.updateUser({ username: 'foo', email: '', token: '', bio: '', image: '' })
|
||||
cy.mount(AppNavigation)
|
||||
const { getByRole } = render(AppNavigation, renderOptions({
|
||||
initialState: {
|
||||
user: { user: { username: 'username', email: '', token: '', bio: '', image: '' } },
|
||||
},
|
||||
}))
|
||||
|
||||
cy.get('.nav-item').should('have.length', 4)
|
||||
cy.contains('Home')
|
||||
cy.contains('New Post')
|
||||
cy.contains('Settings')
|
||||
cy.contains('foo')
|
||||
expect(getByRole('link', { name: 'Home' })).toHaveTextContent('Home')
|
||||
expect(getByRole('link', { name: 'New Post' })).toHaveTextContent('New Post')
|
||||
expect(getByRole('link', { name: 'Settings' })).toHaveTextContent('Settings')
|
||||
expect(getByRole('link', { name: 'username' })).toHaveTextContent('username')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
active-class="active"
|
||||
:name="link.name"
|
||||
:params="link.params"
|
||||
:aria-label="link.title"
|
||||
>
|
||||
<i
|
||||
v-if="link.icon"
|
||||
|
|
|
|||
|
|
@ -1,25 +1,26 @@
|
|||
import { mount } from 'cypress/vue'
|
||||
import { fireEvent, render } from '@testing-library/vue'
|
||||
import { renderOptions } from 'src/utils/test/test.utils.ts'
|
||||
import { describe, it, vi, expect } from 'vitest'
|
||||
import AppPagination from './AppPagination.vue'
|
||||
|
||||
describe('# AppPagination', () => {
|
||||
it('should highlight current active page', () => {
|
||||
cy.mount(AppPagination, {
|
||||
const { getByRole } = render(AppPagination, renderOptions({
|
||||
props: { page: 1, count: 15 },
|
||||
})
|
||||
}))
|
||||
|
||||
cy.get('.page-item').should('have.length', 2)
|
||||
.eq(0).should('have.class', 'active')
|
||||
expect(getByRole('link', { name: 'Go to page 1' }).parentNode).toHaveClass('active')
|
||||
expect(getByRole('link', { name: 'Go to page 2' }).parentNode).not.toHaveClass('active')
|
||||
})
|
||||
|
||||
it('should call onPageChange when click a page item', () => {
|
||||
const onPageChange = cy.spy().as('onPageChange')
|
||||
mount(AppPagination, {
|
||||
it('should call onPageChange when click a page item', async () => {
|
||||
const onPageChange = vi.fn()
|
||||
const { getByRole } = render(AppPagination, renderOptions({
|
||||
props: { page: 1, count: 15, onPageChange },
|
||||
})
|
||||
}))
|
||||
|
||||
cy.findByRole('link', { name: 'Go to page 2' })
|
||||
.click()
|
||||
await fireEvent.click(getByRole('link', { name: 'Go to page 2' }))
|
||||
|
||||
cy.get('@onPageChange').should('have.been.calledWith', 2)
|
||||
expect(onPageChange).toHaveBeenCalledWith(2)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,51 +1,40 @@
|
|||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { render } from '@testing-library/vue'
|
||||
import fixtures from 'src/utils/test/fixtures'
|
||||
import { asyncWrapper, createTestRouter } from 'src/utils/test/test.utils'
|
||||
import { asyncWrapper, renderOptions, setupMockServer } from 'src/utils/test/test.utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import ArticleDetail from './ArticleDetail.vue'
|
||||
|
||||
describe('# ArticleDetail', () => {
|
||||
const router = createTestRouter()
|
||||
const AsyncArticleDetail = asyncWrapper(ArticleDetail)
|
||||
const server = setupMockServer(
|
||||
['/api/articles/markdown', { article: { ...fixtures.article, body: fixtures.markdown } }],
|
||||
['/api/articles/markdown-cn', { article: { ...fixtures.article, body: fixtures.markdownCN } }],
|
||||
['/api/articles/markdown-xss', { article: { ...fixtures.article, body: fixtures.markdownXss } }],
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
cy.wrap(router.push({ name: 'article', params: { slug: fixtures.article.slug } }))
|
||||
it('should render markdown body correctly', async () => {
|
||||
const { container } = render(asyncWrapper(ArticleDetail), await renderOptions({
|
||||
initialRoute: { name: 'article', params: { slug: 'markdown' } },
|
||||
}))
|
||||
await server.waitForRequest('GET', '/api/articles/markdown')
|
||||
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should render markdown body correctly', () => {
|
||||
cy.fixture('article.json').then((res) => {
|
||||
res.article.body = fixtures.markdown
|
||||
cy.intercept('/api/articles/*', res).as('getArticle')
|
||||
})
|
||||
it('should render markdown (zh-CN) body correctly', async () => {
|
||||
const { container } = render(asyncWrapper(ArticleDetail), await renderOptions({
|
||||
initialRoute: { name: 'article', params: { slug: 'markdown-cn' } },
|
||||
}))
|
||||
await server.waitForRequest('GET', '/api/articles/markdown-cn')
|
||||
|
||||
cy.mount(AsyncArticleDetail, { router })
|
||||
cy.wait('@getArticle')
|
||||
|
||||
cy.get('.article-content').should('contain.text', 'h1 Heading 8-)')
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
// TODO: the markdown content should do the unit test for the markdown renderer
|
||||
it.skip('should render markdown (zh-CN) body correctly', () => {
|
||||
cy.fixture('article.json').then((body) => {
|
||||
body.article.body = fixtures.markdownCN
|
||||
cy.intercept('/api/articles/*', body).as('getArticle')
|
||||
})
|
||||
it('should filter the xss content in Markdown body', async () => {
|
||||
const { container } = render(asyncWrapper(ArticleDetail), await renderOptions({
|
||||
initialRoute: { name: 'article', params: { slug: 'markdown-xss' } },
|
||||
}))
|
||||
await server.waitForRequest('GET', '/api/articles/markdown-xss')
|
||||
|
||||
cy.mount(AsyncArticleDetail, { router })
|
||||
cy.wait('@getArticle')
|
||||
|
||||
cy.get('.article-content').should('have.text', fixtures.markdownCN)
|
||||
})
|
||||
|
||||
it.skip('should filter the xss content in Markdown body', () => {
|
||||
cy.fixture('article.json').then((body) => {
|
||||
body.article.body = fixtures.markdownXss
|
||||
cy.intercept('/api/articles/*', body).as('getArticle')
|
||||
})
|
||||
|
||||
cy.mount(AsyncArticleDetail, { router })
|
||||
cy.wait('@getArticle')
|
||||
|
||||
cy.get('.article-content').should('have.text', fixtures.markdownXss)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,40 +1,43 @@
|
|||
import { fireEvent, render } from '@testing-library/vue'
|
||||
import fixtures from 'src/utils/test/fixtures'
|
||||
import { renderOptions } from 'src/utils/test/test.utils.ts'
|
||||
import { vi, describe, it, expect } from 'vitest'
|
||||
import ArticleDetailComment from './ArticleDetailComment.vue'
|
||||
|
||||
describe('# ArticleDetailComment', () => {
|
||||
it('should render correctly', () => {
|
||||
cy.mount(ArticleDetailComment, {
|
||||
const { container, queryByRole } = render(ArticleDetailComment, renderOptions({
|
||||
props: { comment: fixtures.comment },
|
||||
})
|
||||
}))
|
||||
|
||||
cy.get('.card-text').should('have.text', 'Comment body')
|
||||
cy.get('.date-posted').should('have.text', '1/1/2020')
|
||||
cy.findByRole('button', { name: 'Delete comment' }).should('not.exist')
|
||||
expect(container).toHaveTextContent('Comment body')
|
||||
expect(container).toHaveTextContent('1/1/2020')
|
||||
expect(queryByRole('button', { name: 'Delete comment' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should delete comment button when comment author is same user', () => {
|
||||
cy.mount(ArticleDetailComment, {
|
||||
const { getByRole } = render(ArticleDetailComment, renderOptions({
|
||||
props: {
|
||||
comment: fixtures.comment,
|
||||
username: fixtures.author.username,
|
||||
},
|
||||
})
|
||||
}))
|
||||
|
||||
cy.findByRole('button', { name: 'Delete comment' })
|
||||
expect(getByRole('button', { name: 'Delete comment' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should emit remove comment when click remove comment button', () => {
|
||||
const onRemoveComment = cy.spy().as('onRemoveComment')
|
||||
cy.mount(ArticleDetailComment, {
|
||||
const onRemoveComment = vi.fn()
|
||||
const { getByRole } = render(ArticleDetailComment, renderOptions({
|
||||
props: {
|
||||
comment: fixtures.comment,
|
||||
username: fixtures.author.username,
|
||||
onRemoveComment,
|
||||
},
|
||||
})
|
||||
}))
|
||||
|
||||
cy.findByRole('button', { name: 'Delete comment' }).click()
|
||||
fireEvent.click(getByRole('button', { name: 'Delete comment' }))
|
||||
|
||||
cy.get('@onRemoveComment').should('have.been.called')
|
||||
expect(onRemoveComment).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
{{ comment.author.username }}
|
||||
</AppLink>
|
||||
|
||||
<span class="date-posted">{{ (new Date(comment.createdAt)).toLocaleDateString() }}</span>
|
||||
<span class="date-posted">{{ (new Date(comment.createdAt)).toLocaleDateString('en-US') }}</span>
|
||||
|
||||
<span class="mod-options">
|
||||
<i
|
||||
|
|
|
|||
|
|
@ -1,61 +1,55 @@
|
|||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useUserStore } from 'src/store/user'
|
||||
import { fireEvent, render } from '@testing-library/vue'
|
||||
import fixtures from 'src/utils/test/fixtures'
|
||||
import { asyncWrapper, createTestRouter } from 'src/utils/test/test.utils'
|
||||
import { asyncWrapper, renderOptions, setupMockServer } from 'src/utils/test/test.utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import ArticleDetailComments from './ArticleDetailComments.vue'
|
||||
|
||||
describe('# ArticleDetailComments', () => {
|
||||
// const mockDeleteComment = deleteComment as jest.MockedFunction<typeof deleteComment>
|
||||
const AsyncArticleDetailComments = asyncWrapper(ArticleDetailComments)
|
||||
const router = createTestRouter()
|
||||
setActivePinia(createPinia())
|
||||
const userStore = useUserStore()
|
||||
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', '/api/profiles/*', { profile: fixtures.author }).as('getProfile')
|
||||
cy.intercept('GET', '/api/articles/*/comments', { comments: [fixtures.comment] }).as('getCommentsByArticle')
|
||||
cy.intercept('POST', '/api/articles/*/comments', { comment: fixtures.comment2 }).as('postCommentsByArticle')
|
||||
cy.wrap(router.push({ name: 'article', params: { slug: fixtures.article.slug } }))
|
||||
const server = setupMockServer(
|
||||
['GET', '/api/profiles/*', { profile: fixtures.author }],
|
||||
['GET', '/api/articles/*/comments', { comments: [fixtures.comment] }],
|
||||
['POST', '/api/articles/*/comments', { comment: fixtures.comment2 }],
|
||||
['DELETE', '/api/articles/*/comments/*'],
|
||||
)
|
||||
|
||||
it('should render correctly', async () => {
|
||||
const { container } = render(asyncWrapper(ArticleDetailComments), await renderOptions({
|
||||
initialRoute: { name: 'article', params: { slug: fixtures.article.slug } },
|
||||
initialState: { user: { user: null } },
|
||||
}))
|
||||
|
||||
await server.waitForRequest('GET', '/api/articles/article-foo/comments')
|
||||
|
||||
expect(container).toHaveTextContent('Comment body')
|
||||
})
|
||||
|
||||
it('should render correctly', () => {
|
||||
cy.mount(AsyncArticleDetailComments, { router })
|
||||
it('should display new comment when post new comment', async () => {
|
||||
const { container, getByRole } = render(asyncWrapper(ArticleDetailComments), await renderOptions({
|
||||
initialRoute: { name: 'article', params: { slug: fixtures.article.slug } },
|
||||
initialState: { user: { user: fixtures.user } },
|
||||
}))
|
||||
await server.waitForRequest('GET', '/api/articles/*/comments')
|
||||
expect(container).toHaveTextContent('Comment body')
|
||||
|
||||
cy.wait('@getCommentsByArticle')
|
||||
.its('request.url')
|
||||
.should('contain', '/api/articles/article-foo/comments')
|
||||
await fireEvent.update(getByRole('textbox', { name: 'Write comment' }), fixtures.comment2.body)
|
||||
await fireEvent.click(getByRole('button', { name: 'Submit' }))
|
||||
|
||||
cy.contains(fixtures.comment.body)
|
||||
await server.waitForRequest('POST', '/api/articles/*/comments')
|
||||
expect(container).toHaveTextContent(fixtures.comment2.body)
|
||||
})
|
||||
|
||||
it('should display new comment when post new comment', () => {
|
||||
// given
|
||||
userStore.updateUser(fixtures.user)
|
||||
cy.mount(AsyncArticleDetailComments, { router })
|
||||
cy.wait('@getProfile')
|
||||
cy.wait('@getCommentsByArticle')
|
||||
cy.contains(fixtures.comment.body)
|
||||
it('should call remove comment service when click delete button', async () => {
|
||||
const { container, getByRole } = render(asyncWrapper(ArticleDetailComments), await renderOptions({
|
||||
initialRoute: { name: 'article', params: { slug: fixtures.article.slug } },
|
||||
initialState: { user: { user: fixtures.user } },
|
||||
}))
|
||||
await server.waitForRequest('GET', '/api/articles/article-foo/comments')
|
||||
|
||||
// when
|
||||
cy.findByRole('textbox', { name: 'Write comment' }).type(fixtures.comment2.body)
|
||||
cy.findByRole('button', { name: 'Submit' }).click()
|
||||
await fireEvent.click(getByRole('button', { name: 'Delete comment' }))
|
||||
|
||||
// then
|
||||
cy.contains(fixtures.comment2.body)
|
||||
})
|
||||
|
||||
it.only('should call remove comment service when click delete button', () => {
|
||||
// given
|
||||
cy.intercept('DELETE', '/api/articles/*/comments/*', { status: 200 }).as('deleteComment')
|
||||
userStore.updateUser(fixtures.user)
|
||||
cy.mount(AsyncArticleDetailComments, { router })
|
||||
cy.wait('@getCommentsByArticle')
|
||||
|
||||
// when
|
||||
cy.findByRole('button', { name: 'Delete comment' }).click()
|
||||
|
||||
// then
|
||||
cy.wait('@deleteComment')
|
||||
cy.contains(fixtures.comment.body).should('not.exist')
|
||||
await server.waitForRequest('DELETE', '/api/articles/article-foo/comments/*')
|
||||
expect(container).not.toHaveTextContent(fixtures.comment.body)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,37 +1,34 @@
|
|||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { fireEvent, render } from '@testing-library/vue'
|
||||
import ArticleDetailCommentsForm from 'src/components/ArticleDetailCommentsForm.vue'
|
||||
import { useUserStore } from 'src/store/user'
|
||||
import fixtures from 'src/utils/test/fixtures'
|
||||
import { renderOptions, setupMockServer } from 'src/utils/test/test.utils.ts'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('# ArticleDetailCommentsForm', () => {
|
||||
setActivePinia(createPinia())
|
||||
const userStore = useUserStore()
|
||||
|
||||
beforeEach(() => {
|
||||
cy.intercept('/api/profiles/*', { profile: fixtures.author }).as('getProfile')
|
||||
cy.intercept('POST', '/api/articles/*/comments', { comment: { body: 'some texts...' } }).as('postComment')
|
||||
userStore.updateUser(fixtures.user)
|
||||
})
|
||||
const server = setupMockServer(
|
||||
['POST', '/api/articles/*/comments', { comment: { body: 'some texts...' } }],
|
||||
)
|
||||
|
||||
it('should display sign in button when user not logged', () => {
|
||||
userStore.updateUser(null)
|
||||
cy.mount(ArticleDetailCommentsForm, {
|
||||
const { container } = render(ArticleDetailCommentsForm, renderOptions({
|
||||
initialState: { user: { user: null } },
|
||||
props: { articleSlug: fixtures.article.slug },
|
||||
})
|
||||
}))
|
||||
|
||||
cy.contains('add comments on this article')
|
||||
expect(container).toHaveTextContent('add comments on this article')
|
||||
})
|
||||
|
||||
it('should display form when user logged', () => {
|
||||
cy.mount(ArticleDetailCommentsForm, {
|
||||
it('should display form when user logged', async () => {
|
||||
server.use(['GET', '/api/profiles/*', { profile: fixtures.author }])
|
||||
const { getByRole } = render(ArticleDetailCommentsForm, renderOptions({
|
||||
initialState: { user: { user: fixtures.user } },
|
||||
props: { articleSlug: fixtures.article.slug },
|
||||
})
|
||||
}))
|
||||
await server.waitForRequest('GET', '/api/profiles/*')
|
||||
|
||||
cy.findByRole('textbox', { name: 'Write comment' }).type('some texts...')
|
||||
cy.findByRole('button', { name: 'Submit' }).click()
|
||||
await fireEvent.update(getByRole('textbox', { name: 'Write comment' }), 'some texts...')
|
||||
await fireEvent.click(getByRole('button', { name: 'Submit' }))
|
||||
|
||||
cy.wait('@postComment')
|
||||
.its('request.body')
|
||||
.should('deep.equal', { comment: { body: 'some texts...' } })
|
||||
await server.waitForRequest('POST', '/api/articles/*/comments')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { fireEvent, render } from '@testing-library/vue'
|
||||
import type { Profile } from 'src/services/api'
|
||||
import { useUserStore } from 'src/store/user'
|
||||
import fixtures from 'src/utils/test/fixtures'
|
||||
import { renderOptions, setupMockServer } from 'src/utils/test/test.utils.ts'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import ArticleDetailMeta from './ArticleDetailMeta.vue'
|
||||
|
||||
const editButton = 'Edit article'
|
||||
|
|
@ -12,119 +13,105 @@ const favoriteButton = 'Favorite article'
|
|||
const unfavoriteButton = 'Unfavorite article'
|
||||
|
||||
describe('# ArticleDetailMeta', () => {
|
||||
setActivePinia(createPinia())
|
||||
const userStore = useUserStore()
|
||||
|
||||
beforeEach(() => {
|
||||
userStore.updateUser(fixtures.user)
|
||||
})
|
||||
const server = setupMockServer()
|
||||
|
||||
it('should display edit button when user is author', () => {
|
||||
cy.mount(ArticleDetailMeta, {
|
||||
const { getByRole, queryByRole } = render(ArticleDetailMeta, renderOptions({
|
||||
initialState: { user: { user: fixtures.user } },
|
||||
props: { article: fixtures.article },
|
||||
})
|
||||
}))
|
||||
|
||||
cy.findByRole('link', { name: editButton })
|
||||
cy.findByRole('button', { name: followButton }).should('not.exist')
|
||||
expect(getByRole('link', { name: editButton })).toBeInTheDocument()
|
||||
expect(queryByRole('button', { name: followButton })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display follow button when user not author', () => {
|
||||
userStore.updateUser({ ...fixtures.user, username: 'user2' })
|
||||
cy.mount(ArticleDetailMeta, {
|
||||
const { getByRole, queryByRole } = render(ArticleDetailMeta, renderOptions({
|
||||
initialState: { user: { user: { ...fixtures.user, username: 'user2' } } },
|
||||
props: { article: fixtures.article },
|
||||
})
|
||||
}))
|
||||
|
||||
cy.findByRole('link', { name: editButton }).should('not.exist')
|
||||
cy.findByRole('button', { name: followButton })
|
||||
expect(getByRole('button', { name: followButton })).toBeInTheDocument()
|
||||
expect(queryByRole('link', { name: editButton })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not display follow button and edit button when user not logged', () => {
|
||||
userStore.updateUser(null)
|
||||
cy.mount(ArticleDetailMeta, {
|
||||
const { queryByRole } = render(ArticleDetailMeta, renderOptions({
|
||||
initialState: { user: { user: null } },
|
||||
props: { article: fixtures.article },
|
||||
})
|
||||
}))
|
||||
|
||||
cy.findByRole('link', { name: editButton }).should('not.exist')
|
||||
cy.findByRole('button', { name: followButton }).should('not.exist')
|
||||
expect(queryByRole('button', { name: editButton })).not.toBeInTheDocument()
|
||||
expect(queryByRole('button', { name: followButton })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call delete article service when click delete button', () => {
|
||||
cy.intercept('DELETE', '/api/articles/*', { status: 200 }).as('deleteArticle')
|
||||
cy.mount(ArticleDetailMeta, {
|
||||
it('should call delete article service when click delete button', async () => {
|
||||
server.use(['DELETE', '/api/articles/*'])
|
||||
const { getByRole } = render(ArticleDetailMeta, renderOptions({
|
||||
initialState: { user: { user: fixtures.user } },
|
||||
props: { article: fixtures.article },
|
||||
})
|
||||
}))
|
||||
|
||||
cy.findByRole('button', { name: deleteButton }).click()
|
||||
await fireEvent.click(getByRole('button', { name: deleteButton }))
|
||||
|
||||
cy.wait('@deleteArticle')
|
||||
.its('request.url')
|
||||
.should('contain', '/api/articles/article-foo')
|
||||
await server.waitForRequest('DELETE', '/api/articles/*')
|
||||
})
|
||||
|
||||
it('should call follow service when click follow button', () => {
|
||||
it('should call follow service when click follow button', async () => {
|
||||
const newProfile: Profile = { ...fixtures.user, following: true }
|
||||
cy.intercept('POST', '/api/profiles/*/follow', { profile: newProfile }).as('followUser')
|
||||
userStore.updateUser({ ...fixtures.user, username: 'user2' })
|
||||
const onUpdate = cy.spy().as('onUpdate')
|
||||
cy.mount(ArticleDetailMeta, {
|
||||
server.use(['POST', '/api/profiles/*/follow', { profile: newProfile }])
|
||||
const onUpdate = vi.fn()
|
||||
const { getByRole } = render(ArticleDetailMeta, renderOptions({
|
||||
initialState: { user: { user: { ...fixtures.user, username: 'user2' } } },
|
||||
props: { article: fixtures.article, onUpdate },
|
||||
})
|
||||
}))
|
||||
|
||||
cy.findByRole('button', { name: followButton }).click()
|
||||
await fireEvent.click(getByRole('button', { name: followButton }))
|
||||
|
||||
cy.get('@followUser')
|
||||
.its('request.url')
|
||||
.should('contain', '/api/profiles/Author%20name/follow')
|
||||
|
||||
cy.get('@onUpdate').should('be.calledWith', { ...fixtures.article, author: newProfile })
|
||||
await server.waitForRequest('POST', '/api/profiles/*/follow')
|
||||
expect(onUpdate).toHaveBeenCalledWith({ ...fixtures.article, author: newProfile })
|
||||
})
|
||||
|
||||
it('should call unfollow service when click follow button and not followed author', () => {
|
||||
it('should call unfollow service when click follow button and not followed author', async () => {
|
||||
const newProfile: Profile = { ...fixtures.user, following: false }
|
||||
cy.intercept('DELETE', '/api/profiles/*/follow', { profile: newProfile }).as('unfollowUser')
|
||||
userStore.updateUser({ ...fixtures.user, username: 'user2' })
|
||||
const onUpdate = cy.spy().as('onUpdate')
|
||||
cy.mount(ArticleDetailMeta, {
|
||||
server.use(['DELETE', '/api/profiles/*/follow', { profile: newProfile }])
|
||||
const onUpdate = vi.fn()
|
||||
const { getByRole } = render(ArticleDetailMeta, renderOptions({
|
||||
initialState: { user: { user: { ...fixtures.user, username: 'user2' } } },
|
||||
props: {
|
||||
article: { ...fixtures.article, author: { ...fixtures.article.author, following: true } },
|
||||
onUpdate,
|
||||
},
|
||||
})
|
||||
}))
|
||||
|
||||
cy.findByRole('button', { name: unfollowButton }).click()
|
||||
await fireEvent.click(getByRole('button', { name: unfollowButton }))
|
||||
|
||||
cy.wait('@unfollowUser')
|
||||
.its('request.url')
|
||||
.should('contain', '/api/profiles/Author%20name/follow')
|
||||
await server.waitForRequest('DELETE', '/api/profiles/*/follow')
|
||||
|
||||
cy.get('@onUpdate').should('be.calledWith', { ...fixtures.article, author: newProfile })
|
||||
expect(onUpdate).toHaveBeenCalledWith({ ...fixtures.article, author: newProfile })
|
||||
})
|
||||
|
||||
it('should call favorite article service when click favorite button', () => {
|
||||
cy.intercept('POST', '/api/articles/*/favorite', { status: 200 }).as('favoriteArticle')
|
||||
userStore.updateUser({ ...fixtures.user, username: 'user2' })
|
||||
cy.mount(ArticleDetailMeta, {
|
||||
it('should call favorite article service when click favorite button', async () => {
|
||||
server.use(['POST', '/api/articles/*/favorite', { article: { ...fixtures.article, favorited: true } }])
|
||||
const { getByRole } = render(ArticleDetailMeta, renderOptions({
|
||||
initialState: { user: { user: { ...fixtures.user, username: 'user2' } } },
|
||||
props: { article: { ...fixtures.article, favorited: false } },
|
||||
})
|
||||
}))
|
||||
|
||||
cy.findByRole('button', { name: favoriteButton }).click()
|
||||
await fireEvent.click(getByRole('button', { name: favoriteButton }))
|
||||
|
||||
cy.wait('@favoriteArticle')
|
||||
.its('request.url')
|
||||
.should('contain', '/api/articles/article-foo/favorite')
|
||||
await server.waitForRequest('POST', '/api/articles/*/favorite')
|
||||
})
|
||||
|
||||
it('should call favorite article service when click unfavorite button', () => {
|
||||
cy.intercept('DELETE', '/api/articles/*/favorite', { status: 200 }).as('unfavoriteArticle')
|
||||
userStore.updateUser({ ...fixtures.user, username: 'user2' })
|
||||
cy.mount(ArticleDetailMeta, {
|
||||
it('should call favorite article service when click unfavorite button', async () => {
|
||||
server.use(['DELETE', '/api/articles/*/favorite', { article: { ...fixtures.article, favorited: false } }])
|
||||
const { getByRole } = render(ArticleDetailMeta, renderOptions({
|
||||
initialState: { user: { user: { ...fixtures.user, username: 'user2' } } },
|
||||
props: { article: { ...fixtures.article, favorited: true } },
|
||||
})
|
||||
}))
|
||||
|
||||
cy.findByRole('button', { name: unfavoriteButton }).click()
|
||||
await fireEvent.click(getByRole('button', { name: unfavoriteButton }))
|
||||
|
||||
cy.wait('@unfavoriteArticle')
|
||||
.its('request.url')
|
||||
.should('contain', '/api/articles/article-foo/favorite')
|
||||
await server.waitForRequest('DELETE', '/api/articles/*/favorite')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,23 +1,21 @@
|
|||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { render } from '@testing-library/vue'
|
||||
import ArticlesList from 'src/components/ArticlesList.vue'
|
||||
import fixtures from 'src/utils/test/fixtures'
|
||||
import { asyncWrapper } from 'src/utils/test/test.utils'
|
||||
import { asyncWrapper, renderOptions, setupMockServer } from 'src/utils/test/test.utils'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
describe('# ArticlesList', () => {
|
||||
const AsyncArticlesList = asyncWrapper(ArticlesList)
|
||||
setActivePinia(createPinia())
|
||||
const server = setupMockServer(
|
||||
['GET', '/api/articles*', { articles: [fixtures.article], articlesCount: 1 }],
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', '/api/articles*', { articles: [fixtures.article], articlesCount: 1 }).as('getArticles')
|
||||
})
|
||||
it('should render correctly', async () => {
|
||||
const { container } = render(asyncWrapper(ArticlesList), renderOptions())
|
||||
|
||||
it('should render correctly', () => {
|
||||
cy.mount(AsyncArticlesList)
|
||||
await server.waitForRequest('GET', '/api/articles*')
|
||||
|
||||
cy.wait('@getArticles')
|
||||
|
||||
cy.contains(fixtures.article.title)
|
||||
cy.contains('Article description')
|
||||
cy.contains(fixtures.article.author.username)
|
||||
expect(container).toHaveTextContent(fixtures.article.title)
|
||||
expect(container).toHaveTextContent('Article description')
|
||||
expect(container).toHaveTextContent(fixtures.article.author.username)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
import { fireEvent, render } from '@testing-library/vue'
|
||||
import ArticlesListArticlePreview from 'src/components/ArticlesListArticlePreview.vue'
|
||||
import fixtures from 'src/utils/test/fixtures'
|
||||
import { renderOptions, setupMockServer } from 'src/utils/test/test.utils.ts'
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
const favoriteButton = 'Favorite article'
|
||||
|
||||
describe('# ArticlesListArticlePreview', () => {
|
||||
it('should call favorite method when click favorite button', () => {
|
||||
cy.intercept('POST', '/api/articles/*/favorite', { status: 200 }).as('favoriteArticle')
|
||||
cy.mount(ArticlesListArticlePreview, {
|
||||
const server = setupMockServer()
|
||||
|
||||
it('should call favorite method when click favorite button', async () => {
|
||||
server.use(['POST', '/api/articles/*/favorite', { article: { ...fixtures.article, favorited: true } }])
|
||||
const { getByRole } = render(ArticlesListArticlePreview, renderOptions({
|
||||
props: { article: fixtures.article },
|
||||
})
|
||||
}))
|
||||
|
||||
cy.findByRole('button', { name: favoriteButton }).click()
|
||||
await fireEvent.click(getByRole('button', { name: favoriteButton }))
|
||||
|
||||
cy.wait('@favoriteArticle')
|
||||
await server.waitForRequest('POST', '/api/articles/*/favorite')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,37 +1,27 @@
|
|||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { render } from '@testing-library/vue'
|
||||
import ArticlesListNavigation from 'src/components/ArticlesListNavigation.vue'
|
||||
import { useUserStore } from 'src/store/user'
|
||||
import fixtures from 'src/utils/test/fixtures'
|
||||
import { renderOptions } from 'src/utils/test/test.utils.ts'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('# ArticlesListNavigation', () => {
|
||||
setActivePinia(createPinia())
|
||||
const userStore = useUserStore()
|
||||
|
||||
beforeEach(async () => {
|
||||
userStore.updateUser(fixtures.user)
|
||||
})
|
||||
|
||||
it('should render global feed item when passed global feed prop', () => {
|
||||
cy.mount(ArticlesListNavigation, {
|
||||
const { container } = render(ArticlesListNavigation, renderOptions({
|
||||
initialState: { user: { user: fixtures.user } },
|
||||
props: { tag: '', username: '', useGlobalFeed: true },
|
||||
})
|
||||
}))
|
||||
|
||||
cy.get('.nav-item')
|
||||
.should('have.length', 1)
|
||||
.contains('Global Feed')
|
||||
expect(container).toHaveTextContent('Global Feed')
|
||||
})
|
||||
|
||||
it('should render full item', () => {
|
||||
cy.mount(ArticlesListNavigation, {
|
||||
const { container } = render(ArticlesListNavigation, renderOptions({
|
||||
initialState: { user: { user: fixtures.user } },
|
||||
props: { tag: 'foo', username: '', useGlobalFeed: true, useMyFeed: true, useTagFeed: true },
|
||||
})
|
||||
}))
|
||||
|
||||
cy.get('.nav-item')
|
||||
.should('have.length', 3)
|
||||
.should(elements => {
|
||||
expect(elements).to.contain('Global Feed')
|
||||
expect(elements).to.contain('Your Feed')
|
||||
expect(elements).to.contain('foo')
|
||||
})
|
||||
expect(container).toHaveTextContent('Global Feed')
|
||||
expect(container).toHaveTextContent('Your Feed')
|
||||
expect(container).toHaveTextContent('foo')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
import { render } from '@testing-library/vue'
|
||||
import PopularTags from 'src/components/PopularTags.vue'
|
||||
import { asyncWrapper } from 'src/utils/test/test.utils'
|
||||
import { asyncWrapper, renderOptions, setupMockServer } from 'src/utils/test/test.utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('# PopularTags', () => {
|
||||
const AsyncPopularTags = asyncWrapper(PopularTags)
|
||||
const server = setupMockServer(
|
||||
['GET', '/api/tags', { tags: ['tag1', 'tag2'] }],
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', '/api/tags', { tags: ['foo', 'bar'] }).as('getTags')
|
||||
})
|
||||
it('should render correctly', async () => {
|
||||
const { getAllByRole } = render(asyncWrapper(PopularTags), renderOptions())
|
||||
|
||||
it('should render correctly', () => {
|
||||
cy.mount(AsyncPopularTags)
|
||||
await server.waitForRequest('GET', '/api/tags')
|
||||
|
||||
cy.get('.tag-pill')
|
||||
.should('have.length', 2)
|
||||
.contains('foo')
|
||||
expect(getAllByRole('link')).toHaveLength(2)
|
||||
expect(getAllByRole('link')[0]).toHaveTextContent('tag1')
|
||||
expect(getAllByRole('link')[1]).toHaveTextContent('tag2')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
:key="tag"
|
||||
name="tag"
|
||||
:params="{tag}"
|
||||
:aria-label="tag"
|
||||
class="tag-pill tag-default"
|
||||
>
|
||||
{{ tag }}
|
||||
|
|
@ -16,6 +17,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { useTags } from 'src/composable/useTags'
|
||||
|
||||
const { tags, fetchTags } = useTags()
|
||||
|
||||
await fetchTags()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,7 +1,8 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
/* eslint-disable */
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +1,31 @@
|
|||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { createTestRouter } from 'src/utils/test/test.utils'
|
||||
import { render } from '@testing-library/vue'
|
||||
import fixtures from 'src/utils/test/fixtures.ts'
|
||||
import { renderOptions, setupMockServer } from 'src/utils/test/test.utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import Article from './Article.vue'
|
||||
|
||||
describe('# Article', () => {
|
||||
const router = createTestRouter()
|
||||
const server = setupMockServer(
|
||||
['GET', '/api/articles/foo', { article: fixtures.article }],
|
||||
['GET', '/api/articles/foo/comments', { comments: fixtures.articleComments }],
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
cy.wrap(router.push({ name: 'article', params: { slug: 'foo' } }))
|
||||
cy.intercept('GET', '/api/articles/foo', { fixture: 'article.json' }).as('getArticle')
|
||||
cy.intercept('GET', '/api/articles/foo/comments', { fixture: 'article-comments.json' }).as('getComments')
|
||||
})
|
||||
it('should render correctly', async () => {
|
||||
const { container } = render(Article, await renderOptions({
|
||||
initialRoute: { name: 'article', params: { slug: 'foo' } },
|
||||
}))
|
||||
|
||||
it('should render correctly', () => {
|
||||
cy.mount(Article, { router })
|
||||
expect(container).toHaveTextContent('Article is downloading')
|
||||
expect(container).toHaveTextContent('Comments are downloading')
|
||||
|
||||
cy.contains('Article is downloading')
|
||||
cy.contains('Comments are downloading')
|
||||
await server.waitForRequest('GET', '/api/articles/foo')
|
||||
|
||||
cy.wait('@getArticle')
|
||||
|
||||
cy.contains('Article title')
|
||||
cy.contains('Before starting a new implementation')
|
||||
expect(container).toHaveTextContent(fixtures.article.title)
|
||||
expect(container).toHaveTextContent('Article body')
|
||||
expect(container).toHaveTextContent(fixtures.article.author.username)
|
||||
expect(container).toHaveTextContent(fixtures.articleComments[0].body)
|
||||
expect(container).toHaveTextContent(fixtures.articleComments[0].author.username)
|
||||
expect(container).toHaveTextContent(fixtures.articleComments[1].body)
|
||||
expect(container).toHaveTextContent(fixtures.articleComments[1].author.username)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/triple-slash-reference */
|
||||
/// <reference types="vitest-dom/extend-expect" />
|
||||
import 'vitest-dom/extend-expect'
|
||||
|
||||
// https://github.com/mswjs/msw/issues/1415#issuecomment-1650562700
|
||||
location.href = 'https://api.realworld.io/'
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import { dateFilter } from 'src/utils/filters'
|
||||
import { expect, describe, it } from 'vitest'
|
||||
|
||||
describe('# Date filters', () => {
|
||||
it('should format date correctly', () => {
|
||||
const dateString = '2019-01-01 00:00:00'
|
||||
const result = dateFilter(dateString)
|
||||
|
||||
expect(result).to.equal('January 1')
|
||||
expect(result).toBe('January 1')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import params2query from './params-to-query'
|
||||
|
||||
describe('# params2query', () => {
|
||||
|
|
@ -9,6 +10,6 @@ describe('# params2query', () => {
|
|||
|
||||
const result = params2query(params)
|
||||
|
||||
expect(result).to.equal('foo=bar&foo2=bar2')
|
||||
expect(result).toEqual('foo=bar&foo2=bar2')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { describe, it, beforeAll, expect } from 'vitest'
|
||||
import Storage from './storage'
|
||||
|
||||
describe('# Storage', () => {
|
||||
|
|
@ -6,23 +7,23 @@ describe('# Storage', () => {
|
|||
|
||||
const storage = new Storage<typeof DATA>(KEY)
|
||||
|
||||
before(() => {
|
||||
beforeAll(() => {
|
||||
storage.remove()
|
||||
})
|
||||
|
||||
it('should be called with correct key', () => {
|
||||
expect(storage.get()).to.be.null
|
||||
expect(storage.get()).toBeNull()
|
||||
})
|
||||
|
||||
it('should be set value correctly', () => {
|
||||
storage.set(DATA)
|
||||
|
||||
expect(storage.get()).to.deep.equal(DATA)
|
||||
expect(storage.get()).toEqual(DATA)
|
||||
})
|
||||
|
||||
it('should be remove correctly', () => {
|
||||
storage.remove()
|
||||
|
||||
expect(storage.get()).to.be.null
|
||||
expect(storage.get()).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,19 +1,198 @@
|
|||
/* eslint-disable unicorn/no-nested-ternary */
|
||||
import type { MockedRequest, DefaultBodyType } from 'msw'
|
||||
import { matchRequestUrl, rest } from 'msw'
|
||||
import type { SetupServer } from 'msw/node'
|
||||
import { setupServer } from 'msw/node'
|
||||
import { routes } from 'src/router'
|
||||
import type { DefineComponent } from 'vue'
|
||||
import { afterAll, afterEach, beforeAll } from 'vitest'
|
||||
import { defineComponent, h, Suspense } from 'vue'
|
||||
import type { Router } from 'vue-router'
|
||||
import type { RouteLocationRaw, Router } from 'vue-router'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import type { RenderOptions } from '@testing-library/vue'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import AppLink from 'src/components/AppLink.vue'
|
||||
|
||||
export const createTestRouter = (): Router => createRouter({
|
||||
export const createTestRouter = (base?: string): Router => createRouter({
|
||||
routes,
|
||||
history: createMemoryHistory(),
|
||||
history: createMemoryHistory(base),
|
||||
})
|
||||
|
||||
type AsyncWrapper = (...args: Parameters<typeof h>) => DefineComponent
|
||||
export const asyncWrapper: AsyncWrapper = (...args) => defineComponent({
|
||||
render () {
|
||||
return h(Suspense, null, {
|
||||
default: h(...args),
|
||||
interface RenderOptionsArgs {
|
||||
props: Record<string, unknown>
|
||||
slots: Record<string, (...args: unknown[]) => unknown>
|
||||
initialRoute: RouteLocationRaw
|
||||
initialState: Record<string, unknown>
|
||||
}
|
||||
|
||||
const scheduler = typeof setImmediate === 'function' ? setImmediate : setTimeout
|
||||
|
||||
export function flushPromises (): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
scheduler(resolve, 0)
|
||||
})
|
||||
}
|
||||
|
||||
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 result = {
|
||||
props: args.props,
|
||||
slots: args.slots,
|
||||
global: {
|
||||
plugins: [
|
||||
router,
|
||||
createTestingPinia({
|
||||
initialState: {
|
||||
user: { user: null },
|
||||
...args.initialState,
|
||||
},
|
||||
})],
|
||||
components: { AppLink },
|
||||
},
|
||||
}
|
||||
|
||||
const { initialRoute } = args
|
||||
|
||||
if (!initialRoute) return result
|
||||
|
||||
return new Promise((resolve) => {
|
||||
router.replace(initialRoute).then(() => resolve(result))
|
||||
})
|
||||
}
|
||||
|
||||
export function asyncWrapper (component: ReturnType<typeof defineComponent>, props?: Record<string, unknown>): ReturnType<typeof defineComponent> {
|
||||
return defineComponent({
|
||||
render () {
|
||||
return h(
|
||||
'div',
|
||||
{ id: 'root' },
|
||||
h(Suspense, null, {
|
||||
default () {
|
||||
return h(component, props)
|
||||
},
|
||||
fallback: h('div', 'Loading...'),
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function waitForServerRequest (server: SetupServer, method: string, url: string, flush = true) {
|
||||
let requestId = ''
|
||||
let request: MockedRequest
|
||||
|
||||
const result = await new Promise<MockedRequest>((resolve, reject) => {
|
||||
server.events.on('request:start', (req) => {
|
||||
const matchesMethod = req.method.toLowerCase() === method.toLowerCase()
|
||||
const matchesUrl = matchRequestUrl(req.url, url).matches
|
||||
if (matchesMethod && matchesUrl) {
|
||||
requestId = req.id
|
||||
request = req
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
server.events.on('response:mocked', (_res, reqId) => {
|
||||
if (reqId === requestId) resolve(request)
|
||||
})
|
||||
|
||||
server.events.on('request:unhandled', (req) => {
|
||||
if (req.id === requestId) reject(new Error(`The ${req.method} ${req.url.href} request was unhandled.`))
|
||||
})
|
||||
})
|
||||
flush && await flushPromises()
|
||||
return result
|
||||
}
|
||||
|
||||
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'all' | 'ALL'
|
||||
|
||||
type Listener =
|
||||
| [HttpMethod, string, number, object]
|
||||
| [HttpMethod, string, number]
|
||||
| [HttpMethod, string, object]
|
||||
| [string, number, object]
|
||||
| [HttpMethod, string]
|
||||
| [string, object]
|
||||
| [string]
|
||||
|
||||
/**
|
||||
* Sets up a mock server with provided listeners.
|
||||
*
|
||||
* @example
|
||||
* const server = setupMockServer(
|
||||
* ['/api/articles/markdown', { article }],
|
||||
* ['/api/articles/markdown', 200, { article }],
|
||||
* ['GET', '/api/articles/markdown', { article }],
|
||||
* ['GET', '/api/articles/markdown', 200, { article }],
|
||||
* ['DELETE', '/api/articles/comment'],
|
||||
* ['DELETE', '/api/articles/comment', 204],
|
||||
* )
|
||||
*
|
||||
* it('...', async () => {
|
||||
* await server.waitForRequest('/api/articles/markdown')
|
||||
* await server.waitForRequest('GET', '/api/articles/markdown')
|
||||
* })
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export function setupMockServer (...listeners: Listener[]) {
|
||||
const parseArgs = (args: Listener): [string, string, number, (object | null)] => {
|
||||
if (args.length === 4) return args
|
||||
if (args.length === 3) {
|
||||
if (typeof args[1] === 'number') return ['all', args[0], args[1], args[2] as object] // ['all', path, 200, object]
|
||||
if (typeof args[2] === 'number') return [args[0], args[1], args[2], null] // [method, path, status, null]
|
||||
return [args[0], args[1], 200, args[2]] // [method, path, 200, object]
|
||||
}
|
||||
if (args.length === 2) {
|
||||
if (typeof args[1] === 'string') return [args[0], args[1], 200, null]
|
||||
return ['all', args[0], 200, args[1]]
|
||||
}
|
||||
return ['all', args[0], 200, null]
|
||||
}
|
||||
|
||||
const server = setupServer(
|
||||
...listeners.map((args) => {
|
||||
let [method, path, status, response] = parseArgs(args)
|
||||
method = method.toLowerCase()
|
||||
return rest[method as 'all'](`${import.meta.env.VITE_API_HOST}${path}`, (_req, res, ctx) => {
|
||||
return res(ctx.status(status), ctx.json(response))
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
beforeAll(() => void server.listen())
|
||||
afterEach(() => void server.resetHandlers())
|
||||
afterAll(() => void server.close())
|
||||
|
||||
async function waitForRequest (path: string): Promise<MockedRequest<DefaultBodyType>>
|
||||
async function waitForRequest (path: string, flush: boolean): Promise<MockedRequest<DefaultBodyType>>
|
||||
async function waitForRequest (method: HttpMethod, path: string): Promise<MockedRequest<DefaultBodyType>>
|
||||
async function waitForRequest (method: HttpMethod, path: string, flush: boolean): Promise<MockedRequest<DefaultBodyType>>
|
||||
async function waitForRequest (...args: [string] | [string, boolean] | [HttpMethod, string] | [HttpMethod, string, boolean]): Promise<MockedRequest<DefaultBodyType>> {
|
||||
const [method, path, flush] = args.length === 1
|
||||
? ['all', args[0]] // ['all', path]
|
||||
: args.length === 2 && typeof args[1] === 'boolean'
|
||||
? ['all', args[0], args[1]] // ['all', path, flush]
|
||||
: args.length === 2
|
||||
? [args[0], args[1]] // [method, path]
|
||||
: args // [method, path, flush]
|
||||
return waitForServerRequest(server, method, path, flush)
|
||||
}
|
||||
|
||||
const originalUse = server.use.bind(server)
|
||||
|
||||
function use (...listeners: Listener[]) {
|
||||
originalUse(
|
||||
...listeners.map((args) => {
|
||||
let [method, path, status, response] = parseArgs(args)
|
||||
method = method.toLowerCase()
|
||||
return rest[method as 'all'](`${import.meta.env.VITE_API_HOST}${path}`, (_req, res, ctx) => {
|
||||
return res(ctx.status(status), ctx.json(response))
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return Object.assign(server, { waitForRequest, use })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
|
||||
interface WrapTestsProps <Item> {
|
||||
task: string
|
||||
list: Item[]
|
||||
fn: (item: Item) => void
|
||||
only?: boolean
|
||||
testName?: (item: Item, index: number) => string
|
||||
}
|
||||
|
||||
function wrapTests<Item> ({ task, list, fn, testName, only = false }: WrapTestsProps<Item>): void {
|
||||
// @ts-ignore
|
||||
const descFn = only ? context.only : context
|
||||
|
||||
descFn(task, () => {
|
||||
for (const [index, item] of list.entries()) {
|
||||
const name = testName !== undefined ? testName(item, index) : ''
|
||||
// @ts-ignore
|
||||
it(name, () => fn(item))
|
||||
}
|
||||
})
|
||||
}
|
||||
wrapTests.only = function <Item> ({ task, list, fn, testName }: WrapTestsProps<Item>): ReturnType<typeof wrapTests> {
|
||||
wrapTests({ task, list, fn, testName, only: true })
|
||||
}
|
||||
|
||||
export default wrapTests
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { isRef } from 'vue'
|
||||
import useAsync from 'src/utils/use-async'
|
||||
import { vi, expect, it, describe } from 'vitest'
|
||||
|
||||
describe('# Create async process', () => {
|
||||
const someProcess = (): Promise<null> => Promise.resolve(null)
|
||||
|
|
@ -7,39 +8,35 @@ describe('# Create async process', () => {
|
|||
it('should expect active as Vue Ref type', () => {
|
||||
const { active } = useAsync(someProcess)
|
||||
|
||||
expect(isRef(active)).to.be.true
|
||||
expect(isRef(active)).toBe(true)
|
||||
})
|
||||
|
||||
it('should correctly test active functionality', async () => {
|
||||
const { active, run } = useAsync(someProcess)
|
||||
|
||||
expect(active.value).to.be.false
|
||||
expect(active.value).toBe(false)
|
||||
|
||||
const promise = run()
|
||||
|
||||
expect(active.value).to.be.true
|
||||
expect(active.value).toBe(true)
|
||||
|
||||
await promise
|
||||
|
||||
expect(active.value).to.be.false
|
||||
expect(active.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should expect run as a function', () => {
|
||||
const { run } = useAsync(someProcess)
|
||||
|
||||
expect(run).to.be.instanceof(Function)
|
||||
expect(run).toBeInstanceOf(Function)
|
||||
})
|
||||
|
||||
it('should expect original function called with correct params and return correct data', () => {
|
||||
const someProcess = cy.stub().returns(Promise.resolve({ a: 1, b: null }))
|
||||
it('should expect original function called with correct params and return correct data', async () => {
|
||||
const someProcess = vi.fn().mockResolvedValue({ a: 1, b: null })
|
||||
const { run } = useAsync(someProcess)
|
||||
|
||||
cy.wrap(run(null))
|
||||
.its('a')
|
||||
.should('to.be', 1)
|
||||
.its('b')
|
||||
.should('to.be', null)
|
||||
|
||||
expect(someProcess).to.be.calledWith(null)
|
||||
const result = await run(null)
|
||||
expect(result).toEqual({ a: 1, b: null })
|
||||
expect(someProcess).toBeCalledWith(null)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
/// <reference types="vitest" />
|
||||
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
|
|
@ -14,4 +16,12 @@ export default defineConfig({
|
|||
vue(),
|
||||
analyzer({ summaryOnly: true }),
|
||||
],
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
setupFiles: './src/setupTests.ts',
|
||||
globals: true,
|
||||
snapshotFormat: {
|
||||
escapeString: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue