refactor: migrate to cypress component test
This commit is contained in:
parent
7c14a84dea
commit
fbc1985de2
|
|
@ -16,6 +16,7 @@
|
|||
"rules": {
|
||||
"no-undef": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-void": "off",
|
||||
"comma-dangle": ["warn", "always-multiline"],
|
||||
"func-call-spacing": "off",
|
||||
"prefer-const": "off",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
{
|
||||
"projectId": "j7s91r",
|
||||
"baseUrl": "http://localhost:3000"
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"componentFolder": "src",
|
||||
"testFiles": "**/*.spec.ts?(x)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,21 @@
|
|||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
const path = require('path')
|
||||
const { startDevServer } = require('@cypress/vite-dev-server')
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
on('dev-server:start', (options) => {
|
||||
return startDevServer({
|
||||
options,
|
||||
viteConfig: {
|
||||
configFile: path.resolve(__dirname, '..', '..', 'vite.config.js'),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return config
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,5 @@
|
|||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
import '@testing-library/cypress/add-commands'
|
||||
|
|
|
|||
48
package.json
48
package.json
|
|
@ -9,10 +9,10 @@
|
|||
"lint:script": "eslint \"{src/**/*.{ts,vue},cypress/**/*.js}\"",
|
||||
"lint:tsc": "vue-tsc --noEmit",
|
||||
"lint": "concurrently 'yarn build' 'yarn lint:tsc' 'yarn lint:script'",
|
||||
"test:unit": "jest",
|
||||
"test:unit": "cypress run-ct",
|
||||
"test:e2e": "yarn build && concurrently -k \"yarn serve\" \"cypress run -c baseUrl=http://localhost:5000\"",
|
||||
"test:e2e:ci": "cypress run -C cypress.prod.json",
|
||||
"test": "yarn test:e2e"
|
||||
"test": "test:unit && yarn test:e2e"
|
||||
},
|
||||
"dependencies": {
|
||||
"@harlem/core": "^1.3.2",
|
||||
|
|
@ -23,14 +23,13 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/vue": "^6.4.2",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@cypress/vite-dev-server": "^2.0.7",
|
||||
"@cypress/vue": "^3.0.3",
|
||||
"@testing-library/cypress": "^8.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.1",
|
||||
"@typescript-eslint/parser": "^4.29.1",
|
||||
"@vitejs/plugin-vue": "^1.4.0",
|
||||
"@vue/compiler-sfc": "^3.2.2",
|
||||
"babel-jest": "^27.0.6",
|
||||
"concurrently": "^6.2.1",
|
||||
"cypress": "^8.2.0",
|
||||
"eslint": "^7.32.0",
|
||||
|
|
@ -41,15 +40,11 @@
|
|||
"eslint-plugin-promise": "^4.3.1",
|
||||
"eslint-plugin-vue": "^7.16.0",
|
||||
"husky": "^4.3.8",
|
||||
"jest": "^27.0.6",
|
||||
"jsdom": "^17.0.0",
|
||||
"lint-staged": "^11.1.2",
|
||||
"rollup-plugin-analyzer": "^4.0.0",
|
||||
"ts-jest": "^27.0.4",
|
||||
"typescript": "^4.3.5",
|
||||
"vite": "^2.4.4",
|
||||
"vue-tsc": "^0.2.2",
|
||||
"vue3-jest": "^27.0.0-alpha.2"
|
||||
"vue-tsc": "^0.2.2"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
|
@ -60,36 +55,5 @@
|
|||
"lint-staged": {
|
||||
"src/**/*.{ts,vue}": "eslint --fix",
|
||||
"cypress/**/*.js": "eslint --fix"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "ts-jest",
|
||||
"globals": {
|
||||
"ts-jest": {}
|
||||
},
|
||||
"testEnvironment": "jsdom",
|
||||
"transform": {
|
||||
"^.+\\.vue$": "vue3-jest",
|
||||
"^.+\\js$": "babel-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.{ts,vue}",
|
||||
"!<rootDir>/src/config.ts"
|
||||
],
|
||||
"moduleFileExtensions": [
|
||||
"vue",
|
||||
"ts",
|
||||
"js",
|
||||
"json",
|
||||
"node"
|
||||
],
|
||||
"testMatch": [
|
||||
"<rootDir>/src/**/*.spec.ts"
|
||||
],
|
||||
"modulePaths": [
|
||||
"<rootDir>"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/src/setup-test.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,10 @@
|
|||
import { render } from '@testing-library/vue'
|
||||
import registerGlobalComponents from 'src/plugins/global-components'
|
||||
import { router } from 'src/router'
|
||||
import { mount } from '@cypress/vue'
|
||||
import AppFooter from './AppFooter.vue'
|
||||
|
||||
describe('# AppFooter', () => {
|
||||
beforeEach(async () => {
|
||||
await router.push('/')
|
||||
})
|
||||
|
||||
it('should render correctly', () => {
|
||||
const { container } = render(AppFooter, {
|
||||
global: { plugins: [registerGlobalComponents, router] },
|
||||
})
|
||||
mount(AppFooter)
|
||||
|
||||
expect(container).toBeInTheDocument()
|
||||
cy.contains('Real world app')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { fireEvent, render, waitFor } from '@testing-library/vue'
|
||||
import { mount } from '@cypress/vue'
|
||||
import { router } from 'src/router'
|
||||
import AppLink from './AppLink.vue'
|
||||
|
||||
|
|
@ -8,20 +8,16 @@ describe('# AppLink', () => {
|
|||
})
|
||||
|
||||
it('should redirect to another page when click the link', async () => {
|
||||
// given
|
||||
const { container, getByRole } = render(AppLink, {
|
||||
mount(AppLink, {
|
||||
global: { plugins: [router] },
|
||||
props: { name: 'tag', params: { tag: 'foo' } },
|
||||
slots: { default: 'Go to Foo tag' },
|
||||
})
|
||||
|
||||
expect(container).toHaveTextContent('Go to Foo tag')
|
||||
cy.contains('Go to Foo tag')
|
||||
|
||||
// when
|
||||
const linkElement = getByRole('link', { name: 'tag' })
|
||||
await fireEvent.click(linkElement)
|
||||
|
||||
// then
|
||||
await waitFor(() => expect(linkElement).toHaveClass('router-link-active'))
|
||||
cy.findByRole('link', { name: 'tag' })
|
||||
.click()
|
||||
.should('have.class', 'router-link-active')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { render } from '@testing-library/vue'
|
||||
import { mount } from '@cypress/vue'
|
||||
import registerGlobalComponents from 'src/plugins/global-components'
|
||||
import { router } from 'src/router'
|
||||
import { updateUser, user } from 'src/store/user'
|
||||
|
|
@ -10,30 +10,39 @@ describe('# AppNavigation', () => {
|
|||
await router.push('/')
|
||||
})
|
||||
|
||||
it('should render Sign in and Sign up when user not logged', () => {
|
||||
const { container } = render(AppNavigation, {
|
||||
global: { plugins: [registerGlobalComponents, router] },
|
||||
})
|
||||
|
||||
expect(container.querySelectorAll('.nav-item')).toHaveLength(3)
|
||||
expect(container.textContent).toContain('Home')
|
||||
expect(container.textContent).toContain('Sign in')
|
||||
expect(container.textContent).toContain('Sign up')
|
||||
afterEach(() => {
|
||||
Cypress.vueWrapper.unmount()
|
||||
})
|
||||
|
||||
it('should render xxx when user logged', () => {
|
||||
updateUser({ id: 1, username: 'foo', email: '', token: '', bio: undefined, image: undefined })
|
||||
const { container } = render(AppNavigation, {
|
||||
it('should render Sign in and Sign up when user not logged', () => {
|
||||
mount(AppNavigation, {
|
||||
global: {
|
||||
plugins: [registerGlobalComponents, router],
|
||||
mocks: { $store: user },
|
||||
},
|
||||
})
|
||||
|
||||
expect(container.querySelectorAll('.nav-item')).toHaveLength(4)
|
||||
expect(container.textContent).toContain('Home')
|
||||
expect(container.textContent).toContain('New Post')
|
||||
expect(container.textContent).toContain('Settings')
|
||||
expect(container.textContent).toContain('foo')
|
||||
cy.get('.nav-item')
|
||||
.should('have.length', 3)
|
||||
.should('contain.text', 'Home')
|
||||
.should('contain.text', 'Sign in')
|
||||
.should('contain.text', 'Sign up')
|
||||
})
|
||||
|
||||
it('should render xxx when user logged', () => {
|
||||
updateUser({ id: 1, username: 'foo', email: '', token: '', bio: undefined, image: undefined })
|
||||
mount(AppNavigation, {
|
||||
global: {
|
||||
plugins: [registerGlobalComponents, router],
|
||||
mocks: { $store: user },
|
||||
},
|
||||
})
|
||||
|
||||
cy.get('.nav-item')
|
||||
.should('have.length', 4)
|
||||
.should('contain.text', 'Home')
|
||||
.should('contain.text', 'New Post')
|
||||
.should('contain.text', 'Settings')
|
||||
.should('contain.text', 'foo')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import { fireEvent, render } from '@testing-library/vue'
|
||||
import { mount } from '@cypress/vue'
|
||||
import AppPagination from './AppPagination.vue'
|
||||
|
||||
describe('# AppPagination', () => {
|
||||
it('should highlight current active page', () => {
|
||||
const { container } = render(AppPagination, {
|
||||
mount(AppPagination, {
|
||||
props: { page: 1, count: 15 },
|
||||
})
|
||||
|
||||
const pageItems = container.querySelectorAll('.page-item')
|
||||
expect(pageItems).toHaveLength(2)
|
||||
expect(pageItems[0]).toHaveClass('active')
|
||||
cy.get('.page-item')
|
||||
.should('have.length', 2)
|
||||
.its(0)
|
||||
.should('have.class', 'active')
|
||||
})
|
||||
|
||||
it('should call onPageChange when click a page item', async () => {
|
||||
const { getByRole, emitted } = render(AppPagination, {
|
||||
mount(AppPagination, {
|
||||
props: { page: 1, count: 15 },
|
||||
})
|
||||
|
||||
await fireEvent.click(getByRole('link', { name: 'Go to page 2' }))
|
||||
cy.findByRole('link', { name: 'Go to page 2' }).click()
|
||||
|
||||
const events = emitted()
|
||||
expect(events['page-change']).toHaveLength(1)
|
||||
expect(events['page-change']?.[0]).toEqual([2])
|
||||
const events = Cypress.vueWrapper.emitted()
|
||||
cy.wrap(events).should('have.property', 'page-change', [2])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
role="link"
|
||||
:aria-label="`Go to page ${pageNumber}`"
|
||||
class="page-link"
|
||||
href="javascript:"
|
||||
@click="onPageChange(pageNumber)"
|
||||
>{{ pageNumber }}</a>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,10 @@
|
|||
import { render } from '@testing-library/vue'
|
||||
import { mount } from '@cypress/vue'
|
||||
import registerGlobalComponents from 'src/plugins/global-components'
|
||||
import { router } from 'src/router'
|
||||
import { getArticle } from 'src/services/article/getArticle'
|
||||
import asyncComponentWrapper from 'src/utils/test/async-component-wrapper'
|
||||
import fixtures from 'src/utils/test/fixtures'
|
||||
import ArticleDetail from './ArticleDetail.vue'
|
||||
|
||||
jest.mock('src/services/article/getArticle')
|
||||
|
||||
describe.skip('# ArticleDetail', () => {
|
||||
const mockGetArticle = getArticle as jest.MockedFunction<typeof getArticle>
|
||||
|
||||
beforeEach(async () => {
|
||||
await router.push({
|
||||
name: 'article',
|
||||
|
|
@ -19,29 +13,27 @@ describe.skip('# ArticleDetail', () => {
|
|||
})
|
||||
|
||||
it('should render markdown body correctly', async () => {
|
||||
mockGetArticle.mockResolvedValue({ ...fixtures.article, body: fixtures.markdown })
|
||||
const { container } = render(asyncComponentWrapper(ArticleDetail), {
|
||||
cy.intercept('/articles/:slug', { ...fixtures.article, body: fixtures.markdown })
|
||||
mount(ArticleDetail, {
|
||||
global: { plugins: [registerGlobalComponents, router] },
|
||||
})
|
||||
|
||||
expect(container.querySelector('.article-content')).toMatchSnapshot()
|
||||
cy.get('.article-content').should('have.html', '')
|
||||
})
|
||||
|
||||
it('should render markdown (zh-CN) body correctly', async () => {
|
||||
mockGetArticle.mockResolvedValue({ ...fixtures.article, body: fixtures.markdownCN })
|
||||
const { container } = render(asyncComponentWrapper(ArticleDetail), {
|
||||
mount(ArticleDetail, {
|
||||
global: { plugins: [registerGlobalComponents, router] },
|
||||
})
|
||||
|
||||
expect(container.querySelector('.article-content')).toMatchSnapshot()
|
||||
cy.get('.article-content').should('have.html', '')
|
||||
})
|
||||
|
||||
it('should filter the xss content in Markdown body', async () => {
|
||||
mockGetArticle.mockResolvedValue({ ...fixtures.article, body: fixtures.markdownXss })
|
||||
const { container } = render(asyncComponentWrapper(ArticleDetail), {
|
||||
mount(ArticleDetail, {
|
||||
global: { plugins: [registerGlobalComponents, router] },
|
||||
})
|
||||
|
||||
expect(container.querySelector('.article-content')?.textContent).not.toContain('alert')
|
||||
cy.get('.article-content').should('have.html', '')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { fireEvent, render } from '@testing-library/vue'
|
||||
import { mount } from '@cypress/vue'
|
||||
import registerGlobalComponents from 'src/plugins/global-components'
|
||||
import { router } from 'src/router'
|
||||
import fixtures from 'src/utils/test/fixtures'
|
||||
|
|
@ -10,37 +10,34 @@ describe('# ArticleDetailComment', () => {
|
|||
})
|
||||
|
||||
it('should render correctly', () => {
|
||||
const { container, queryByRole } = render(ArticleDetailComment, {
|
||||
mount(ArticleDetailComment, {
|
||||
global: { plugins: [registerGlobalComponents, router] },
|
||||
props: { comment: fixtures.comment },
|
||||
})
|
||||
|
||||
expect(container.querySelector('.card-text')).toHaveTextContent('Comment body')
|
||||
expect(container.querySelector('.date-posted')).toHaveTextContent('1/1/2020')
|
||||
expect(queryByRole('button', { name: 'Delete comment' })).toBeNull()
|
||||
cy.get('.card-text').should('have.text', 'Comment body')
|
||||
cy.get('.date-posted').should('have.text', '01/01/2020')
|
||||
cy.findByRole('button', { name: 'Delete comment' }).should('not.exist')
|
||||
})
|
||||
|
||||
it('should delete comment button when comment author is same user', () => {
|
||||
const { getByRole } = render(ArticleDetailComment, {
|
||||
global: { plugins: [registerGlobalComponents, router] },
|
||||
props: {
|
||||
comment: fixtures.comment,
|
||||
username: fixtures.author.username,
|
||||
},
|
||||
})
|
||||
|
||||
expect(getByRole('button', { name: 'Delete comment' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should emit remove comment when click remove comment button', async () => {
|
||||
const { getByRole, emitted } = render(ArticleDetailComment, {
|
||||
mount(ArticleDetailComment, {
|
||||
global: { plugins: [registerGlobalComponents, router] },
|
||||
props: { comment: fixtures.comment, username: fixtures.author.username },
|
||||
})
|
||||
|
||||
await fireEvent.click(getByRole('button', { name: 'Delete comment' }))
|
||||
cy.findByRole('button', { name: 'Delete comment' }).should('exist')
|
||||
})
|
||||
|
||||
const events = emitted()
|
||||
expect(events['remove-comment']).toHaveLength(1)
|
||||
it('should emit remove comment when click remove comment button', async () => {
|
||||
mount(ArticleDetailComment, {
|
||||
global: { plugins: [registerGlobalComponents, router] },
|
||||
props: { comment: fixtures.comment, username: fixtures.author.username },
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Delete comment' }).click()
|
||||
|
||||
const events = Cypress.vueWrapper.emitted()
|
||||
cy.wrap(events).should('have.property', 'remove-comment')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -54,7 +54,5 @@ const emit = defineEmits<{
|
|||
(e: 'remove-comment'): boolean
|
||||
}>()
|
||||
|
||||
const showRemove = $computed(() => (
|
||||
props.username !== undefined && props.username === props.comment.author.username
|
||||
))
|
||||
const showRemove = $computed(() => (props.username !== undefined && props.username === props.comment.author.username))
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,11 @@
|
|||
import { render, waitFor } from '@testing-library/vue'
|
||||
import { mount } from '@cypress/vue'
|
||||
import registerGlobalComponents from 'src/plugins/global-components'
|
||||
import { router } from 'src/router'
|
||||
import { getCommentsByArticle } from 'src/services/comment/getComments'
|
||||
import { deleteComment } from 'src/services/comment/postComment'
|
||||
import asyncComponentWrapper from 'src/utils/test/async-component-wrapper'
|
||||
import fixtures from 'src/utils/test/fixtures'
|
||||
import ArticleDetailComments from './ArticleDetailComments.vue'
|
||||
|
||||
jest.mock('src/services/comment/getComments')
|
||||
jest.mock('src/services/comment/postComment')
|
||||
|
||||
describe('# ArticleDetailComments', () => {
|
||||
const mockGetCommentsByArticle = getCommentsByArticle as jest.MockedFunction<typeof getCommentsByArticle>
|
||||
const mockDeleteComment = deleteComment as jest.MockedFunction<typeof deleteComment>
|
||||
|
||||
beforeEach(async () => {
|
||||
mockGetCommentsByArticle.mockResolvedValue([fixtures.comment])
|
||||
await router.push({
|
||||
name: 'article',
|
||||
params: { slug: fixtures.article.slug },
|
||||
|
|
@ -23,42 +13,48 @@ describe('# ArticleDetailComments', () => {
|
|||
})
|
||||
|
||||
it('should render correctly', async () => {
|
||||
const { container } = render(asyncComponentWrapper(ArticleDetailComments), {
|
||||
global: { plugins: [registerGlobalComponents, router] },
|
||||
cy.intercept('GET', '/api/articles/*/comments', { body: { comments: [fixtures.comment] } }).as('retrieveComments')
|
||||
|
||||
mount(ArticleDetailComments, {
|
||||
global: {
|
||||
plugins: [registerGlobalComponents, router],
|
||||
},
|
||||
})
|
||||
|
||||
expect(mockGetCommentsByArticle).toBeCalledWith('article-foo')
|
||||
expect(container).toBeInTheDocument()
|
||||
cy.wait('@retrieveComments').its('request.url').should('contain', 'article-foo')
|
||||
})
|
||||
|
||||
// TODO: resolve the Cypress.vue is undefined
|
||||
it.skip('should display new comment when post new comment', async () => {
|
||||
cy.intercept('GET', '/api/articles/*/comments', { body: { comments: [fixtures.comment] } }).as('retrieveComments')
|
||||
|
||||
// given
|
||||
const { container } = render(asyncComponentWrapper(ArticleDetailComments), {
|
||||
mount(ArticleDetailComments, {
|
||||
global: { plugins: [registerGlobalComponents, router] },
|
||||
})
|
||||
|
||||
await waitFor(() => expect(mockGetCommentsByArticle).toBeCalled())
|
||||
expect(container.querySelectorAll('.card')).toHaveLength(1)
|
||||
cy.wait('@retrieveComments')
|
||||
cy.get('.card').should('have.length', 1)
|
||||
|
||||
// when
|
||||
// wrapper.findComponent(ArticleDetailCommentsForm).vm.$emit('add-comment', fixtures.comment2)
|
||||
// await nextTick()
|
||||
Cypress.vue.$emit('add-comment', fixtures.comment2)
|
||||
|
||||
// then
|
||||
expect(container.querySelectorAll('.card')).toHaveLength(2)
|
||||
cy.get('.card').should('have.length', 2)
|
||||
})
|
||||
|
||||
it.skip('should call remove comment service when click delete button', async () => {
|
||||
cy.intercept('DELETE', '/api/articles/*/comments', { statusCode: 200 }).as('deleteComment')
|
||||
|
||||
// given
|
||||
render(asyncComponentWrapper(ArticleDetailComments), {
|
||||
mount(ArticleDetailComments, {
|
||||
global: { plugins: [registerGlobalComponents, router] },
|
||||
})
|
||||
await waitFor(() => expect(mockGetCommentsByArticle).toBeCalled())
|
||||
|
||||
// when
|
||||
// wrapper.findComponent(ArticleDetailComment).vm.$emit('remove-comment')
|
||||
Cypress.vue.$emit('remove-comment')
|
||||
|
||||
// then
|
||||
expect(mockDeleteComment).toBeCalledWith('article-foo', 1)
|
||||
cy.wait('@deleteComment')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
import { getCommentsByArticle } from 'src/services/comment/getComments'
|
||||
import { deleteComment } from 'src/services/comment/postComment'
|
||||
import { user } from 'src/store/user'
|
||||
import { onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import ArticleDetailComment from './ArticleDetailComment.vue'
|
||||
import ArticleDetailCommentsForm from './ArticleDetailCommentsForm.vue'
|
||||
|
|
@ -37,5 +38,7 @@ const removeComment = async (commentId: number) => {
|
|||
comments = comments.filter(c => c.id !== commentId)
|
||||
}
|
||||
|
||||
comments = await getCommentsByArticle(slug)
|
||||
onMounted(async () => {
|
||||
comments = await getCommentsByArticle(slug)
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,53 +1,43 @@
|
|||
import { fireEvent, render } from '@testing-library/vue'
|
||||
import { mount } from '@cypress/vue'
|
||||
import ArticleDetailCommentsForm from 'src/components/ArticleDetailCommentsForm.vue'
|
||||
import { useProfile } from 'src/composable/useProfile'
|
||||
import registerGlobalComponents from 'src/plugins/global-components'
|
||||
import { router } from 'src/router'
|
||||
import { postComment } from 'src/services/comment/postComment'
|
||||
import fixtures from 'src/utils/test/fixtures'
|
||||
|
||||
jest.mock('src/composable/useProfile')
|
||||
jest.mock('src/services/comment/postComment')
|
||||
|
||||
describe('# ArticleDetailCommentsForm', () => {
|
||||
const mockUseProfile = useProfile as jest.MockedFunction<typeof useProfile>
|
||||
const mockPostComment = postComment as jest.MockedFunction<typeof postComment>
|
||||
|
||||
beforeEach(async () => {
|
||||
await router.push({ name: 'article', params: { slug: fixtures.article.slug } })
|
||||
mockPostComment.mockResolvedValue(fixtures.comment2)
|
||||
mockUseProfile.mockReturnValue({
|
||||
profile: fixtures.author,
|
||||
updateProfile: jest.fn(),
|
||||
})
|
||||
})
|
||||
|
||||
it('should display sign in button when user not logged', () => {
|
||||
mockUseProfile.mockReturnValue({ profile: null, updateProfile: jest.fn() })
|
||||
const { container } = render(ArticleDetailCommentsForm, {
|
||||
cy.intercept('POST', '/api/articles/*/comments', fixtures.comment2).as('createComment')
|
||||
|
||||
mount(ArticleDetailCommentsForm, {
|
||||
global: { plugins: [registerGlobalComponents, router] },
|
||||
props: { articleSlug: fixtures.article.slug },
|
||||
})
|
||||
|
||||
expect(container.textContent).toContain('add comments on this article')
|
||||
cy.contains('add comments on this article')
|
||||
})
|
||||
|
||||
it('should display form when user logged', async () => {
|
||||
cy.intercept('POST', '/api/articles/*/comments', { statusCode: 200 }).as('createComment')
|
||||
|
||||
// given
|
||||
const { getByRole, emitted } = render(ArticleDetailCommentsForm, {
|
||||
mount(ArticleDetailCommentsForm, {
|
||||
global: { plugins: [registerGlobalComponents, router] },
|
||||
props: { articleSlug: fixtures.article.slug },
|
||||
})
|
||||
|
||||
// when
|
||||
const inputElement = getByRole('textbox', { name: 'Write comment' })
|
||||
await fireEvent.update(inputElement, 'some texts...')
|
||||
await fireEvent.click(getByRole('button', { name: 'Submit' }))
|
||||
cy.findByRole('textbox', { name: 'Write comment' })
|
||||
.type('some texts...')
|
||||
cy.findByRole('button', { name: 'Submit' })
|
||||
|
||||
// then
|
||||
expect(mockPostComment).toBeCalledWith('article-foo', 'some texts...')
|
||||
cy.get('@createComment').should('be.calledWith', 'article-foo', 'some texts...')
|
||||
|
||||
const { submit } = emitted()
|
||||
expect(submit).toHaveLength(1)
|
||||
const events = Cypress.vueWrapper.emitted()
|
||||
cy.wrap(events).should('have.property', 'submit')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ const emit = defineEmits<{
|
|||
}>()
|
||||
|
||||
const username = $computed(() => checkAuthorization(user) ? user.value.username : '')
|
||||
const { profile } = useProfile({ username })
|
||||
const { profile } = useProfile({ username: $raw(username) })
|
||||
|
||||
let comment = $ref('')
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,10 @@
|
|||
import { fireEvent, render } from '@testing-library/vue'
|
||||
import { GlobalMountOptions } from '@vue/test-utils/dist/types'
|
||||
import { mount } from '@cypress/vue'
|
||||
import registerGlobalComponents from 'src/plugins/global-components'
|
||||
import { router } from 'src/router'
|
||||
import { deleteArticle } from 'src/services/article/deleteArticle'
|
||||
import { deleteFavoriteArticle, postFavoriteArticle } from 'src/services/article/favoriteArticle'
|
||||
import { deleteFollowProfile, postFollowProfile } from 'src/services/profile/followProfile'
|
||||
import { updateUser, user } from 'src/store/user'
|
||||
import fixtures from 'src/utils/test/fixtures'
|
||||
import ArticleDetailMeta from './ArticleDetailMeta.vue'
|
||||
|
||||
jest.mock('src/services/article/deleteArticle')
|
||||
jest.mock('src/services/profile/followProfile')
|
||||
jest.mock('src/services/article/favoriteArticle')
|
||||
|
||||
const globalMountOptions: GlobalMountOptions = {
|
||||
plugins: [registerGlobalComponents, router],
|
||||
mocks: { $store: user },
|
||||
}
|
||||
|
||||
describe('# ArticleDetailMeta', () => {
|
||||
const editButton = 'Edit article'
|
||||
const deleteButton = 'Delete article'
|
||||
|
|
@ -26,109 +13,133 @@ describe('# ArticleDetailMeta', () => {
|
|||
const favoriteButton = 'Favorite article'
|
||||
const unfavoriteButton = 'Unfavorite article'
|
||||
|
||||
const mockDeleteArticle = deleteArticle as jest.MockedFunction<typeof deleteArticle>
|
||||
const mockFollowUser = postFollowProfile as jest.MockedFunction<typeof postFollowProfile>
|
||||
const mockUnfollowUser = deleteFollowProfile as jest.MockedFunction<typeof deleteFollowProfile>
|
||||
const mockFavoriteArticle = postFavoriteArticle as jest.MockedFunction<typeof postFavoriteArticle>
|
||||
const mockUnfavoriteArticle = deleteFavoriteArticle as jest.MockedFunction<typeof deleteFavoriteArticle>
|
||||
|
||||
beforeEach(async () => {
|
||||
mockFollowUser.mockResolvedValue({ isOk: () => true } as any)
|
||||
mockUnfollowUser.mockResolvedValue({ isOk: () => true } as any)
|
||||
mockFavoriteArticle.mockResolvedValue({ isOk: () => true, value: fixtures.article } as any)
|
||||
mockUnfavoriteArticle.mockResolvedValue({ isOk: () => true, value: fixtures.article } as any)
|
||||
updateUser(fixtures.user)
|
||||
await router.push({ name: 'article', params: { slug: fixtures.article.slug } })
|
||||
updateUser(fixtures.user)
|
||||
})
|
||||
|
||||
it('should display edit button when user is author', () => {
|
||||
const { queryByRole } = render(ArticleDetailMeta, {
|
||||
global: globalMountOptions,
|
||||
mount(ArticleDetailMeta, {
|
||||
global: {
|
||||
plugins: [registerGlobalComponents, router],
|
||||
mocks: { $store: user },
|
||||
},
|
||||
props: { article: fixtures.article },
|
||||
})
|
||||
|
||||
expect(queryByRole('link', { name: editButton })).toBeInTheDocument()
|
||||
expect(queryByRole('button', { name: followButton })).not.toBeInTheDocument()
|
||||
cy.findByRole('link', { name: editButton }).should('exist')
|
||||
cy.findByRole('button', { name: followButton }).should('not.exist')
|
||||
})
|
||||
|
||||
it('should display follow button when user not author', () => {
|
||||
updateUser({ ...fixtures.user, username: 'user2' })
|
||||
const { queryByRole } = render(ArticleDetailMeta, {
|
||||
global: globalMountOptions,
|
||||
mount(ArticleDetailMeta, {
|
||||
global: {
|
||||
plugins: [registerGlobalComponents, router],
|
||||
mocks: { $store: user },
|
||||
},
|
||||
props: { article: fixtures.article },
|
||||
})
|
||||
|
||||
expect(queryByRole('link', { name: editButton })).not.toBeInTheDocument()
|
||||
expect(queryByRole('button', { name: followButton })).toBeInTheDocument()
|
||||
cy.findByRole('link', { name: editButton }).should('not.exist')
|
||||
cy.findByRole('button', { name: followButton }).should('exist')
|
||||
})
|
||||
|
||||
it('should not display follow button and edit button when user not logged', () => {
|
||||
updateUser(null)
|
||||
const { queryByRole } = render(ArticleDetailMeta, {
|
||||
global: globalMountOptions,
|
||||
mount(ArticleDetailMeta, {
|
||||
global: {
|
||||
plugins: [registerGlobalComponents, router],
|
||||
mocks: { $store: user },
|
||||
},
|
||||
props: { article: fixtures.article },
|
||||
})
|
||||
|
||||
expect(queryByRole('button', { name: editButton })).not.toBeInTheDocument()
|
||||
expect(queryByRole('button', { name: followButton })).not.toBeInTheDocument()
|
||||
cy.findByRole('button', { name: editButton }).should('not.exist')
|
||||
cy.findByRole('button', { name: followButton }).should('not.exist')
|
||||
})
|
||||
|
||||
it('should call delete article service when click delete button', async () => {
|
||||
const { getByRole } = render(ArticleDetailMeta, {
|
||||
global: globalMountOptions,
|
||||
cy.intercept('DELETE', '/api/articles/*', { statusCode: 200 }).as('deleteArticle')
|
||||
|
||||
mount(ArticleDetailMeta, {
|
||||
global: {
|
||||
plugins: [registerGlobalComponents, router],
|
||||
mocks: { $store: user },
|
||||
},
|
||||
props: { article: fixtures.article },
|
||||
})
|
||||
|
||||
await fireEvent.click(getByRole('button', { name: deleteButton }))
|
||||
cy.findByRole('button', { name: deleteButton }).click()
|
||||
|
||||
expect(mockDeleteArticle).toBeCalledWith('article-foo')
|
||||
cy.wait('@deleteArticle').its('request.url').should('contain', 'article-foo')
|
||||
})
|
||||
|
||||
it('should call follow service when click follow button', async () => {
|
||||
cy.intercept('POST', '/api/profiles/*/follow', { statusCode: 200 }).as('followProfile')
|
||||
|
||||
updateUser({ ...fixtures.user, username: 'user2' })
|
||||
const { getByRole } = render(ArticleDetailMeta, {
|
||||
global: globalMountOptions,
|
||||
mount(ArticleDetailMeta, {
|
||||
global: {
|
||||
plugins: [registerGlobalComponents, router],
|
||||
mocks: { $store: user },
|
||||
},
|
||||
props: { article: fixtures.article },
|
||||
})
|
||||
|
||||
await fireEvent.click(getByRole('button', { name: followButton }))
|
||||
cy.findByRole('button', { name: followButton }).click()
|
||||
|
||||
expect(mockFollowUser).toBeCalledWith('Author name')
|
||||
cy.wait('@followProfile').its('request.url').should('contain', 'Author%20name')
|
||||
})
|
||||
|
||||
it('should call unfollow service when click follow button and not followed author', async () => {
|
||||
cy.intercept('DELETE', '/api/profiles/*/follow', { statusCode: 200 }).as('unfollowProfile')
|
||||
|
||||
updateUser({ ...fixtures.user, username: 'user2' })
|
||||
const { getByRole } = render(ArticleDetailMeta, {
|
||||
global: globalMountOptions,
|
||||
mount(ArticleDetailMeta, {
|
||||
global: {
|
||||
plugins: [registerGlobalComponents, router],
|
||||
mocks: { $store: user },
|
||||
},
|
||||
props: { article: { ...fixtures.article, author: { ...fixtures.article.author, following: true } } },
|
||||
})
|
||||
|
||||
await fireEvent.click(getByRole('button', { name: unfollowButton }))
|
||||
cy.findByRole('button', { name: unfollowButton }).click()
|
||||
|
||||
expect(mockUnfollowUser).toBeCalledWith('Author name')
|
||||
cy.wait('@unfollowProfile').its('request.url').should('contain', 'Author')
|
||||
})
|
||||
|
||||
it('should call favorite article service when click favorite button', async () => {
|
||||
cy.intercept('POST', '/api/articles/*/favorite', { statusCode: 200 }).as('favoriteArticle')
|
||||
|
||||
updateUser({ ...fixtures.user, username: 'user2' })
|
||||
const { getByRole } = render(ArticleDetailMeta, {
|
||||
global: globalMountOptions,
|
||||
mount(ArticleDetailMeta, {
|
||||
global: {
|
||||
plugins: [registerGlobalComponents, router],
|
||||
mocks: { $store: user },
|
||||
},
|
||||
props: { article: { ...fixtures.article, favorited: false } },
|
||||
})
|
||||
|
||||
await fireEvent.click(getByRole('button', { name: favoriteButton }))
|
||||
cy.findByRole('button', { name: favoriteButton }).click()
|
||||
|
||||
expect(mockFavoriteArticle).toBeCalledWith('article-foo')
|
||||
cy.wait('@favoriteArticle').its('request.url').should('contain', 'article-foo')
|
||||
})
|
||||
|
||||
it('should call favorite article service when click unfavorite button', async () => {
|
||||
cy.intercept('DELETE', '/api/articles/*/favorite', { statusCode: 200 }).as('unfavoriteArticle')
|
||||
|
||||
updateUser({ ...fixtures.user, username: 'user2' })
|
||||
const { getByRole } = render(ArticleDetailMeta, {
|
||||
global: globalMountOptions,
|
||||
mount(ArticleDetailMeta, {
|
||||
global: {
|
||||
plugins: [registerGlobalComponents, router],
|
||||
mocks: { $store: user },
|
||||
},
|
||||
props: { article: { ...fixtures.article, favorited: true } },
|
||||
})
|
||||
|
||||
await fireEvent.click(getByRole('button', { name: unfavoriteButton }))
|
||||
cy.findByRole('button', { name: unfavoriteButton }).click()
|
||||
|
||||
expect(mockUnfavoriteArticle).toBeCalledWith('article-foo')
|
||||
cy.wait('@unfavoriteArticle').its('request.url').should('contain', 'article-foo')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,32 +1,28 @@
|
|||
import { render } from '@testing-library/vue'
|
||||
import { mount } from '@cypress/vue'
|
||||
import { GlobalMountOptions } from '@vue/test-utils/dist/types'
|
||||
import ArticlesList from 'src/components/ArticlesList.vue'
|
||||
import registerGlobalComponents from 'src/plugins/global-components'
|
||||
import { router } from 'src/router'
|
||||
import { getArticles } from 'src/services/article/getArticles'
|
||||
import fixtures from 'src/utils/test/fixtures'
|
||||
import asyncComponentWrapper from '../utils/test/async-component-wrapper'
|
||||
|
||||
jest.mock('src/services/article/getArticles')
|
||||
|
||||
describe('# ArticlesList', () => {
|
||||
const globalMountOptions: GlobalMountOptions = {
|
||||
plugins: [registerGlobalComponents, router],
|
||||
}
|
||||
|
||||
const mockFetchArticles = getArticles as jest.MockedFunction<typeof getArticles>
|
||||
|
||||
beforeEach(async () => {
|
||||
mockFetchArticles.mockResolvedValue({ articles: [fixtures.article], articlesCount: 1 })
|
||||
await router.push('/')
|
||||
})
|
||||
|
||||
it('should render correctly', async () => {
|
||||
const wrapper = render(asyncComponentWrapper(ArticlesList), {
|
||||
cy.intercept('GET', '/api/articles?*', { body: { articles: [fixtures.article], articlesCount: 1 } })
|
||||
.as('mockRequest')
|
||||
|
||||
mount(ArticlesList, {
|
||||
global: globalMountOptions,
|
||||
})
|
||||
|
||||
expect(wrapper).toBeTruthy()
|
||||
expect(mockFetchArticles).toBeCalledTimes(1)
|
||||
cy.wait('@mockRequest')
|
||||
cy.contains('Article foo')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { useArticles } from 'src/composable/useArticles'
|
||||
import { onMounted } from 'vue'
|
||||
import AppPagination from './AppPagination.vue'
|
||||
import ArticlesListArticlePreview from './ArticlesListArticlePreview.vue'
|
||||
import ArticlesListNavigation from './ArticlesListNavigation.vue'
|
||||
|
|
@ -51,5 +52,7 @@ const {
|
|||
username,
|
||||
} = useArticles()
|
||||
|
||||
await fetchArticles()
|
||||
onMounted(async () => {
|
||||
await fetchArticles()
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,32 +1,27 @@
|
|||
import { fireEvent, render } from '@testing-library/vue'
|
||||
import { mount } from '@cypress/vue'
|
||||
import ArticlesListArticlePreview from 'src/components/ArticlesListArticlePreview.vue'
|
||||
import registerGlobalComponents from 'src/plugins/global-components'
|
||||
import { router } from 'src/router'
|
||||
import fixtures from 'src/utils/test/fixtures'
|
||||
|
||||
const mockFavoriteArticle = jest.fn()
|
||||
jest.mock('src/composable/useFavoriteArticle', () => ({
|
||||
useFavoriteArticle: () => ({
|
||||
favoriteProcessGoing: false,
|
||||
favoriteArticle: mockFavoriteArticle,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('# ArticlesListArticlePreview', () => {
|
||||
const favoriteButton = 'Favorite article'
|
||||
|
||||
beforeEach(async () => {
|
||||
await router.push({ name: 'article', params: { slug: fixtures.article.slug } })
|
||||
})
|
||||
|
||||
it('should call favorite method when click favorite button', async () => {
|
||||
const { getByRole } = render(ArticlesListArticlePreview, {
|
||||
cy.intercept('POST', '/api/articles/*/favorite', { body: { article: fixtures.articleAfterFavorite } })
|
||||
.as('mockFavorite')
|
||||
|
||||
mount(ArticlesListArticlePreview, {
|
||||
global: { plugins: [registerGlobalComponents, router] },
|
||||
props: { article: fixtures.article },
|
||||
})
|
||||
|
||||
await fireEvent.click(getByRole('button', { name: favoriteButton }))
|
||||
cy.findByRole('button', { name: favoriteButton }).click()
|
||||
|
||||
expect(mockFavoriteArticle).toBeCalledTimes(1)
|
||||
cy.wait('@mockFavorite')
|
||||
|
||||
// TODO: Cypress.vueWrapper is undefined
|
||||
// const events = Cypress.vueWrapper.emitted()
|
||||
// cy.wrap(events).should('have.property', 'update', fixtures.articleAfterFavorite)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { render } from '@testing-library/vue'
|
||||
import { mount } from '@cypress/vue'
|
||||
import { GlobalMountOptions } from '@vue/test-utils/dist/types'
|
||||
import ArticlesListNavigation from 'src/components/ArticlesListNavigation.vue'
|
||||
import registerGlobalComponents from 'src/plugins/global-components'
|
||||
|
|
@ -18,26 +18,26 @@ describe('# ArticlesListNavigation', () => {
|
|||
})
|
||||
|
||||
it('should render global feed item when passed global feed prop', () => {
|
||||
const { container } = render(ArticlesListNavigation, {
|
||||
mount(ArticlesListNavigation, {
|
||||
global: globalMountOptions,
|
||||
props: { tag: '', username: '', useGlobalFeed: true },
|
||||
})
|
||||
|
||||
const items = container.querySelectorAll('.nav-item')
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0].textContent).toContain('Global Feed')
|
||||
cy.get('.nav-item')
|
||||
.should('have.length', 1)
|
||||
.should('contain.text', 'Global Feed')
|
||||
})
|
||||
|
||||
it('should render full item', () => {
|
||||
const { container } = render(ArticlesListNavigation, {
|
||||
mount(ArticlesListNavigation, {
|
||||
global: globalMountOptions,
|
||||
props: { tag: 'foo', username: '', useGlobalFeed: true, useMyFeed: true, useTagFeed: true },
|
||||
})
|
||||
|
||||
const items = container.querySelectorAll('.nav-item')
|
||||
expect(items).toHaveLength(3)
|
||||
expect(items[0].textContent).toContain('Global Feed')
|
||||
expect(items[1].textContent).toContain('Your Feed')
|
||||
expect(items[2].textContent).toContain('foo')
|
||||
cy.get('.nav-item')
|
||||
.should('have.length', 3)
|
||||
.should('contain.text', 'Global Feed')
|
||||
.should('contain.text', 'Your Feed')
|
||||
.should('contain.text', 'foo')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,29 +1,17 @@
|
|||
import { render } from '@testing-library/vue'
|
||||
import { mount } from '@cypress/vue'
|
||||
import PopularTags from 'src/components/PopularTags.vue'
|
||||
import { useTags } from 'src/composable/useTags'
|
||||
import registerGlobalComponents from 'src/plugins/global-components'
|
||||
import { router } from 'src/router'
|
||||
import { ref } from 'vue'
|
||||
|
||||
jest.mock('src/composable/useTags')
|
||||
|
||||
describe('# PopularTags', () => {
|
||||
const mockUseTags = useTags as jest.MockedFunction<typeof useTags>
|
||||
it('should render correctly', async () => {
|
||||
cy.intercept('GET', '/api/tags', { body: { tags: ['foo', 'bar'] } })
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockFetchTags = jest.fn()
|
||||
mockUseTags.mockReturnValue({
|
||||
tags: ref(['foo', 'bar']),
|
||||
fetchTags: mockFetchTags,
|
||||
})
|
||||
await router.push('/')
|
||||
})
|
||||
|
||||
it.skip('should render correctly', async () => {
|
||||
const { container } = render(PopularTags, {
|
||||
mount(PopularTags, {
|
||||
global: { plugins: [registerGlobalComponents, router] },
|
||||
})
|
||||
|
||||
expect(container.querySelectorAll('.tag-pill')).toHaveLength(2)
|
||||
cy.get('.tag-pill')
|
||||
.should('have.length', 2)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -16,8 +16,11 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { useTags } from 'src/composable/useTags'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const { tags, fetchTags } = useTags()
|
||||
|
||||
await fetchTags()
|
||||
onMounted(async () => {
|
||||
await fetchTags()
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,26 +1,27 @@
|
|||
import { getProfile } from 'src/services/profile/getProfile'
|
||||
import { watch } from 'vue'
|
||||
import { watch, ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
interface UseProfileProps {
|
||||
username: string
|
||||
username: Ref<string>
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type
|
||||
export function useProfile ({ username }: UseProfileProps) {
|
||||
let profile = $ref<Profile | null>(null)
|
||||
const profile = ref<Profile | null>(null)
|
||||
|
||||
async function fetchProfile (): Promise<void> {
|
||||
updateProfile(null)
|
||||
if (!username) return
|
||||
const profileData = await getProfile(username)
|
||||
const profileData = await getProfile(username.value)
|
||||
updateProfile(profileData)
|
||||
}
|
||||
|
||||
function updateProfile (profileData: Profile | null): void {
|
||||
profile = profileData
|
||||
profile.value = profileData
|
||||
}
|
||||
|
||||
watch($raw(username), fetchProfile, { immediate: true })
|
||||
watch(username, fetchProfile, { immediate: true })
|
||||
|
||||
return {
|
||||
profile,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { ref } from 'vue'
|
|||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type
|
||||
export function useTags () {
|
||||
const tags = ref<string[]>([])
|
||||
let tags = ref<string[]>([])
|
||||
|
||||
async function fetchTags (): Promise<void> {
|
||||
tags.value = []
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { render } from '@testing-library/vue'
|
||||
import { mount } from '@cypress/vue'
|
||||
import { router } from 'src/router'
|
||||
import Article from './Article.vue'
|
||||
|
||||
|
|
@ -8,10 +8,10 @@ describe('# Article', () => {
|
|||
})
|
||||
|
||||
it('should render correctly', () => {
|
||||
const { container } = render(Article, {
|
||||
mount(Article, {
|
||||
global: { plugins: [router] },
|
||||
})
|
||||
|
||||
expect(container.textContent).toContain('Article is downloading')
|
||||
cy.contains('Article is downloading')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -71,15 +71,14 @@ import ArticlesList from 'src/components/ArticlesList.vue'
|
|||
import { useFollow } from 'src/composable/useFollowProfile'
|
||||
import { useProfile } from 'src/composable/useProfile'
|
||||
import { checkAuthorization, user } from 'src/store/user'
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const username = $computed<string>(() => route.params.username as string)
|
||||
|
||||
const { profile, updateProfile } = useProfile({ username })
|
||||
const { profile, updateProfile } = useProfile({ username: $raw(username) })
|
||||
|
||||
const following = $computed<boolean>(() => profile?.following ?? false)
|
||||
const following = $computed<boolean>(() => profile.value?.following ?? false)
|
||||
const { followProcessGoing, toggleFollow } = useFollow({
|
||||
following,
|
||||
username,
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
import 'jest'
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
jest.spyOn(window.Storage.prototype, 'getItem').mockReturnValue('')
|
||||
jest.spyOn(window.Storage.prototype, 'setItem').mockImplementation()
|
||||
jest.mock('src/config', () => ({
|
||||
CONFIG: {
|
||||
API_HOST: '',
|
||||
},
|
||||
}))
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
global.fetch = jest.fn().mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
|
@ -7,36 +7,34 @@ describe('# Create async process', function () {
|
|||
it('should expect active as Vue Ref type', function () {
|
||||
const { active } = createAsyncProcess(someProcess)
|
||||
|
||||
expect(isRef(active)).toBe(true)
|
||||
cy.wrap(isRef(active)).should('be.true')
|
||||
})
|
||||
|
||||
it('should correctly test active functionality', async function () {
|
||||
const { active, run } = createAsyncProcess(someProcess)
|
||||
|
||||
expect(active.value).toBe(false)
|
||||
cy.wrap(active.value).should('be.false')
|
||||
|
||||
const promise = run()
|
||||
void run()
|
||||
|
||||
expect(active.value).toBe(true)
|
||||
cy.wrap(active.value).should('be.true')
|
||||
|
||||
await promise
|
||||
|
||||
expect(active.value).toBe(false)
|
||||
cy.wrap(active.value).should('be.false')
|
||||
})
|
||||
|
||||
it('should expect run as a function', function () {
|
||||
const { run } = createAsyncProcess(someProcess)
|
||||
|
||||
expect(run).toBeInstanceOf(Function)
|
||||
cy.wrap(run).should('be.instanceOf', Function)
|
||||
})
|
||||
|
||||
it('should expect original function called with correct params and return correct data', async function () {
|
||||
const someProcess = jest.fn().mockImplementation(a => Promise.resolve({ a, b: null }))
|
||||
const someProcess = cy.stub().callsFake(a => Promise.resolve({ a, b: null }))
|
||||
const { run } = createAsyncProcess(someProcess)
|
||||
|
||||
const result = await run(null)
|
||||
|
||||
expect(someProcess).toBeCalledWith(null)
|
||||
expect(result).toEqual({ a: null, b: null })
|
||||
cy.wrap(someProcess).should('be.calledWith', null)
|
||||
cy.wrap(result).should('eq', { a: null, b: null })
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@ describe('# Date filters', function () {
|
|||
const dateString = '2019-01-01 00:00:00'
|
||||
const result = dateFilter(dateString)
|
||||
|
||||
expect(result).toMatchInlineSnapshot('"January 1"')
|
||||
cy.wrap(result).should('equal', 'January 1')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ describe('# mapAuthorizationResponse', function () {
|
|||
|
||||
const result = mapAuthorizationResponse<Partial<Response>>(response)
|
||||
|
||||
expect(isEither(result)).toBe(true)
|
||||
expect(result.isOk()).toBe(true)
|
||||
expect(result.value).toEqual(RESPONSE)
|
||||
cy.wrap(isEither(result)).should('be.true')
|
||||
cy.wrap(result.isOk()).should('be.true')
|
||||
cy.wrap(result.value).should('equal', RESPONSE)
|
||||
})
|
||||
|
||||
it('should return Either with AuthorizationError and failed Response', function () {
|
||||
|
|
@ -24,9 +24,9 @@ describe('# mapAuthorizationResponse', function () {
|
|||
|
||||
const result = mapAuthorizationResponse<Partial<Response>>(response)
|
||||
|
||||
expect(isEither(result)).toBe(true)
|
||||
expect(result.isFail()).toBe(true)
|
||||
expect(result.value).toBeInstanceOf(AuthorizationError)
|
||||
cy.wrap(isEither(result)).should('be.true')
|
||||
cy.wrap(result.isFail()).should('be.true')
|
||||
cy.wrap(result.value).should('be.instanceOf', AuthorizationError)
|
||||
})
|
||||
|
||||
it('should throw NetworkError when Response is failed with status != 401', function () {
|
||||
|
|
@ -35,7 +35,7 @@ describe('# mapAuthorizationResponse', function () {
|
|||
|
||||
expect(() => {
|
||||
mapAuthorizationResponse<Partial<Response>>(response)
|
||||
}).toThrowError(NetworkError)
|
||||
}).to.throw()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -48,9 +48,9 @@ describe('# mapValidationResponse', function () {
|
|||
|
||||
const result = mapValidationResponse<ValidationErrors, Partial<Response>>(response)
|
||||
|
||||
expect(isEither(result)).toBe(true)
|
||||
expect(result.isOk()).toBe(true)
|
||||
expect(result.value).toEqual(RESPONSE)
|
||||
cy.wrap(isEither(result)).should('be.true')
|
||||
cy.wrap(result.isOk()).should('be.true')
|
||||
cy.wrap(result.value).should('equal', RESPONSE)
|
||||
})
|
||||
|
||||
it('should return Either with ValidationError and failed Response', async function () {
|
||||
|
|
@ -59,10 +59,12 @@ describe('# mapValidationResponse', function () {
|
|||
|
||||
const result = mapValidationResponse<ValidationErrors, Partial<Response>>(response)
|
||||
|
||||
expect(isEither(result)).toBe(true)
|
||||
expect(result.isFail()).toBe(true)
|
||||
expect(result.value).toBeInstanceOf(ValidationError)
|
||||
expect(result.isFail() && await result.value.getErrors()).toEqual((await RESPONSE.json()).errors)
|
||||
cy.wrap(isEither(result)).should('be.true')
|
||||
cy.wrap(result.isFail()).should('be.true')
|
||||
cy.wrap(result.value).should('be.instanceOf', ValidationError)
|
||||
const errors = result.isFail() && result.value.getErrors()
|
||||
const expectErrors = (await RESPONSE.json()).errors
|
||||
cy.wrap(errors).should('equal', expectErrors)
|
||||
})
|
||||
|
||||
it('should throw NetworkError when Response is failed with status != 422', function () {
|
||||
|
|
@ -71,6 +73,6 @@ describe('# mapValidationResponse', function () {
|
|||
|
||||
expect(() => {
|
||||
mapValidationResponse<ValidationErrors, Partial<Response>>(response)
|
||||
}).toThrowError(NetworkError)
|
||||
}).to.throw()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@ describe('# params2query', () => {
|
|||
|
||||
const result = params2query(params)
|
||||
|
||||
expect(result).toEqual('foo=bar&foo2=bar2')
|
||||
expect(result).to.equal('foo=bar&foo2=bar2')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,257 +0,0 @@
|
|||
import FetchRequest, { FetchRequestOptions } from 'src/utils/request'
|
||||
import { Either, fail, isEither, success } from 'src/utils/either'
|
||||
|
||||
import params2query from 'src/utils/params-to-query'
|
||||
import mockFetch from 'src/utils/test/mock-fetch'
|
||||
import wrapTests from 'src/utils/test/wrap-tests'
|
||||
|
||||
import { NetworkError } from 'src/types/error'
|
||||
|
||||
beforeEach(() => {
|
||||
mockFetch({ type: 'body' })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const PREFIX = '/prefix'
|
||||
const SUB_PREFIX = '/sub-prefix'
|
||||
const PATH = '/path'
|
||||
const PARAMS = { q1: 'q1', q2: 'q2' }
|
||||
|
||||
type SafeMethod = 'get' | 'delete' | 'checkableGet' | 'checkableDelete'
|
||||
type UnsafeMethod = 'post' | 'put' | 'patch' | 'checkablePost' | 'checkablePut' | 'checkablePatch'
|
||||
type Method = SafeMethod | UnsafeMethod
|
||||
|
||||
type CheckableSafeMethod = 'checkableGet' | 'checkableDelete'
|
||||
type CheckableUnsafeMethod = 'checkablePost' | 'checkablePut' | 'checkablePatch'
|
||||
type CheckableMethod = CheckableSafeMethod | CheckableUnsafeMethod
|
||||
|
||||
function isSafe (method: Method): method is SafeMethod {
|
||||
return ['get', 'delete'].includes(method)
|
||||
}
|
||||
function isCheckableSafe (method: CheckableMethod): method is CheckableSafeMethod {
|
||||
return ['checkableGet', 'checkableDelete'].includes(method)
|
||||
}
|
||||
function isCheckable (method: CheckableMethod | Method): method is CheckableMethod {
|
||||
return ['checkableGet', 'checkableDelete', 'checkablePost', 'checkablePut', 'checkablePatch'].includes(method)
|
||||
}
|
||||
|
||||
async function triggerMethod<T = unknown> (request: FetchRequest, method: Method | CheckableMethod, options?: Partial<FetchRequestOptions>): Promise<T | Either<NetworkError, T>> {
|
||||
if (isCheckable(method)) {
|
||||
let response: Either<NetworkError, T>
|
||||
if (isCheckableSafe(method)) response = await request[method]<T>(PATH, options)
|
||||
else response = await request[method]<T>(PATH, {}, options)
|
||||
return response.isOk() ? success(response.value) : fail(response.value)
|
||||
} else {
|
||||
let body: T
|
||||
if (isSafe(method)) body = await request[method]<T>(PATH, options)
|
||||
else body = await request[method]<T>(PATH, {}, options)
|
||||
return body
|
||||
}
|
||||
}
|
||||
|
||||
function forCorrectMethods (task: string, fn: (method: Method) => Promise<void>): void {
|
||||
wrapTests<Method>({
|
||||
task,
|
||||
fn,
|
||||
list: ['get', 'delete', 'post', 'put', 'patch'],
|
||||
testName: method => `for method: ${method}`,
|
||||
})
|
||||
}
|
||||
|
||||
function forCheckableMethods (task: string, fn: (method: CheckableMethod) => Promise<void>): void {
|
||||
wrapTests<CheckableMethod>({
|
||||
task,
|
||||
fn,
|
||||
list: ['checkableGet', 'checkableDelete', 'checkablePost', 'checkablePut', 'checkablePatch'],
|
||||
testName: method => `for method: ${method}`,
|
||||
})
|
||||
}
|
||||
|
||||
function forAllMethods (task: string, fn: (method: Method | CheckableMethod) => Promise<void>): void {
|
||||
forCheckableMethods(task, fn)
|
||||
forCorrectMethods(task, fn)
|
||||
}
|
||||
|
||||
forAllMethods('# Should be implemented', async (method) => {
|
||||
const request = new FetchRequest()
|
||||
|
||||
await triggerMethod(request, method)
|
||||
|
||||
expect(global.fetch).toBeCalledWith(PATH, expect.objectContaining({
|
||||
method: method.replace('checkable', '').toUpperCase(),
|
||||
}))
|
||||
})
|
||||
|
||||
describe('# Should implement prefix', () => {
|
||||
forAllMethods('should implement global prefix', async (method) => {
|
||||
const request = new FetchRequest({ prefix: PREFIX })
|
||||
|
||||
await triggerMethod(request, method)
|
||||
|
||||
expect(global.fetch).toBeCalledWith(`${PREFIX}${PATH}`, expect.any(Object))
|
||||
})
|
||||
|
||||
forAllMethods('should implement local prefix', async (method) => {
|
||||
const request = new FetchRequest()
|
||||
|
||||
await triggerMethod(request, method, { prefix: SUB_PREFIX })
|
||||
|
||||
expect(global.fetch).toBeCalledWith(`${SUB_PREFIX}${PATH}`, expect.any(Object))
|
||||
})
|
||||
|
||||
forAllMethods('should implement global + local prefix', async (method) => {
|
||||
const request = new FetchRequest({ prefix: PREFIX })
|
||||
|
||||
await triggerMethod(request, method, { prefix: SUB_PREFIX })
|
||||
|
||||
expect(global.fetch).toBeCalledWith(`${SUB_PREFIX}${PATH}`, expect.any(Object))
|
||||
})
|
||||
})
|
||||
|
||||
describe('# Should convert query object to query string in request url', () => {
|
||||
forAllMethods('should implement global query', async (method) => {
|
||||
const request = new FetchRequest({ params: PARAMS })
|
||||
|
||||
await triggerMethod(request, method)
|
||||
|
||||
expect(global.fetch).toBeCalledWith(`${PATH}?${params2query(PARAMS)}`, expect.any(Object))
|
||||
})
|
||||
|
||||
forAllMethods('should implement local query', async (method) => {
|
||||
const request = new FetchRequest()
|
||||
|
||||
await triggerMethod(request, method, { params: PARAMS })
|
||||
|
||||
expect(global.fetch).toBeCalledWith(`${PATH}?${params2query(PARAMS)}`, expect.any(Object))
|
||||
})
|
||||
|
||||
forAllMethods('should implement global + local query', async (method) => {
|
||||
const options = { params: { q1: 'q1', q2: 'q2' } }
|
||||
const localOptions = { params: { q1: 'q11', q3: 'q3' } }
|
||||
const expectedOptions = { params: { q1: 'q11', q2: 'q2', q3: 'q3' } }
|
||||
const request = new FetchRequest(options)
|
||||
|
||||
await triggerMethod(request, method, localOptions)
|
||||
|
||||
expect(global.fetch).toBeCalledWith(`${PATH}?${params2query(expectedOptions.params)}`, expect.any(Object))
|
||||
})
|
||||
})
|
||||
|
||||
describe('# Should work with headers', function () {
|
||||
forAllMethods('should add headers', async function (method) {
|
||||
const options = { headers: { h1: 'h1', h2: 'h2' } }
|
||||
const request = new FetchRequest(options)
|
||||
|
||||
await triggerMethod(request, method)
|
||||
|
||||
expect(global.fetch).toBeCalledWith(PATH, expect.objectContaining(options))
|
||||
})
|
||||
|
||||
forAllMethods('should merge headers', async function (method) {
|
||||
const options = { headers: { h1: 'h1', h2: 'h2' } }
|
||||
const localOptions = { headers: { h1: 'h11', h3: 'h3' } }
|
||||
const expectedOptions = { headers: { h1: 'h11', h2: 'h2', h3: 'h3' } }
|
||||
const request = new FetchRequest(options)
|
||||
|
||||
await triggerMethod(request, method, localOptions)
|
||||
|
||||
expect(global.fetch).toBeCalledWith(PATH, expect.objectContaining(expectedOptions))
|
||||
})
|
||||
})
|
||||
|
||||
forCorrectMethods('# Should converted correct response body to json', async function (method) {
|
||||
const DATA = { foo: 'bar' }
|
||||
mockFetch({ type: 'body', ...DATA })
|
||||
const request = new FetchRequest()
|
||||
|
||||
const body = await triggerMethod(request, method)
|
||||
|
||||
expect(body).toMatchObject(DATA)
|
||||
})
|
||||
|
||||
forCheckableMethods('# Should converted checkable response to Either<NetworkError, DATA_TYPE>', async function (method) {
|
||||
const DATA = { foo: 'bar' }
|
||||
interface DATA_TYPE { foo: 'bar' }
|
||||
mockFetch({ type: 'body', ...DATA })
|
||||
const request = new FetchRequest()
|
||||
|
||||
const result = await triggerMethod<DATA_TYPE>(request, method)
|
||||
|
||||
const resultIsEither = isEither<unknown, DATA_TYPE>(result)
|
||||
const resultIsOk = isEither<unknown, DATA_TYPE>(result) && result.isOk()
|
||||
const resultValue = isEither<unknown, DATA_TYPE>(result) && result.isOk() ? result.value : null
|
||||
|
||||
expect(resultIsEither).toBe(true)
|
||||
expect(resultIsOk).toBe(true)
|
||||
expect(resultValue).toMatchObject(DATA)
|
||||
})
|
||||
|
||||
forCorrectMethods('# Should throw NetworkError if correct request is not OK', async function (method) {
|
||||
mockFetch({
|
||||
type: 'full',
|
||||
ok: false,
|
||||
status: 400,
|
||||
statusText: 'Bad request',
|
||||
json: async () => ({}),
|
||||
})
|
||||
|
||||
const request = new FetchRequest()
|
||||
const result = triggerMethod(request, method)
|
||||
|
||||
await expect(result).rejects.toBeInstanceOf(NetworkError)
|
||||
})
|
||||
|
||||
forCheckableMethods('# Should return Either<NetworkError, DATA_TYPE> if checkable request is not OK', async function (method) {
|
||||
mockFetch({
|
||||
type: 'full',
|
||||
ok: false,
|
||||
status: 400,
|
||||
statusText: 'Bad request',
|
||||
json: async () => ({}),
|
||||
})
|
||||
|
||||
const request = new FetchRequest()
|
||||
const result = await triggerMethod(request, method)
|
||||
|
||||
const resultIsEither = isEither<NetworkError, unknown>(result)
|
||||
const resultIsNotOk = isEither<NetworkError, unknown>(result) && result.isFail()
|
||||
const resultValue = isEither<NetworkError, unknown>(result) && result.isFail() ? result.value : null
|
||||
|
||||
expect(resultIsEither).toBe(true)
|
||||
expect(resultIsNotOk).toBe(true)
|
||||
expect(resultValue).toBeInstanceOf(NetworkError)
|
||||
})
|
||||
|
||||
describe('# Authorization header', function () {
|
||||
const TOKEN = 'token'
|
||||
const OPTIONS = { headers: { Authorization: `Token ${TOKEN}` } }
|
||||
|
||||
forAllMethods('should add authorization header', async function (method) {
|
||||
const request = new FetchRequest()
|
||||
request.setAuthorizationHeader(TOKEN)
|
||||
|
||||
await triggerMethod(request, method)
|
||||
|
||||
expect(global.fetch).toBeCalledWith(PATH, expect.objectContaining(OPTIONS))
|
||||
})
|
||||
|
||||
forAllMethods('should remove authorization header', async function (method) {
|
||||
const request = new FetchRequest(OPTIONS)
|
||||
|
||||
await triggerMethod(request, method)
|
||||
|
||||
expect(global.fetch).toBeCalledTimes(1)
|
||||
expect(global.fetch).toBeCalledWith(PATH, expect.objectContaining(OPTIONS))
|
||||
|
||||
request.deleteAuthorizationHeader()
|
||||
await triggerMethod(request, method)
|
||||
|
||||
expect(global.fetch).toBeCalledTimes(2)
|
||||
expect(global.fetch).toBeCalledWith(PATH, expect.objectContaining({
|
||||
headers: {},
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import mockLocalStorage from './test/mock-local-storage'
|
||||
|
||||
import Storage from './storage'
|
||||
|
||||
describe('# storage', function () {
|
||||
const DATA = { foo: 'bar' }
|
||||
const KEY = 'key'
|
||||
|
||||
const storage = new Storage<typeof DATA>(KEY)
|
||||
|
||||
describe('# GET', function () {
|
||||
it('should be called with correct key', function () {
|
||||
const fn = mockLocalStorage('getItem')
|
||||
|
||||
storage.get()
|
||||
|
||||
expect(fn).toBeCalledWith(KEY)
|
||||
})
|
||||
|
||||
it('should get an object given valid local storage item', function () {
|
||||
mockLocalStorage('getItem', DATA)
|
||||
|
||||
const result = storage.get()
|
||||
|
||||
expect(result).toMatchObject(DATA)
|
||||
})
|
||||
|
||||
it('should get null if invalid storage item given', function () {
|
||||
mockLocalStorage('getItem', '{invalid value}', false)
|
||||
|
||||
expect(() => {
|
||||
const result = storage.get()
|
||||
expect(result).toBeNull()
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('# SET', () => {
|
||||
it('should be called with correct key and value', function () {
|
||||
const fn = mockLocalStorage('setItem')
|
||||
|
||||
storage.set(DATA)
|
||||
|
||||
expect(fn).toBeCalledWith(KEY, JSON.stringify(DATA))
|
||||
})
|
||||
})
|
||||
|
||||
describe('# REMOVE', () => {
|
||||
it('should be called with correct key', function () {
|
||||
const fn = mockLocalStorage('removeItem')
|
||||
|
||||
storage.remove()
|
||||
|
||||
expect(fn).toBeCalledWith(KEY)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -27,6 +27,12 @@ This is **Strong** content.`,
|
|||
updatedAt: '2020-01-01T00:00:00Z',
|
||||
}
|
||||
|
||||
const articleAfterFavorite: Article = {
|
||||
...article,
|
||||
favorited: true,
|
||||
favoritesCount: 1,
|
||||
}
|
||||
|
||||
const comment: ArticleComment = {
|
||||
id: 1,
|
||||
author,
|
||||
|
|
@ -51,6 +57,7 @@ export default {
|
|||
author,
|
||||
user,
|
||||
article,
|
||||
articleAfterFavorite,
|
||||
comment,
|
||||
comment2,
|
||||
markdown,
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
interface FetchResponseBody {
|
||||
type: 'body'
|
||||
}
|
||||
interface FetchResponseFull {
|
||||
type: 'full'
|
||||
ok: boolean
|
||||
status: number
|
||||
statusText: string
|
||||
json: () => Promise<unknown>
|
||||
}
|
||||
|
||||
export default function mockFetch (data: FetchResponseBody | FetchResponseFull): void {
|
||||
let response
|
||||
const { type, ...body } = data
|
||||
|
||||
if (type === 'body') {
|
||||
response = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => body,
|
||||
}
|
||||
} else {
|
||||
response = body
|
||||
}
|
||||
|
||||
global.fetch = jest.fn().mockResolvedValue(response)
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
|
||||
type LocalStorageKey = 'getItem' | 'setItem' | 'removeItem'
|
||||
|
||||
export default function mockLocalStorage<T> (key: LocalStorageKey, data?: T, stringify = true): jest.Mock {
|
||||
const fn = jest.fn().mockReturnValue(stringify ? JSON.stringify(data) : data)
|
||||
// use __proto__ because jsdom bug: https://github.com/facebook/jest/issues/6798#issuecomment-412871616
|
||||
// eslint-disable-next-line no-proto
|
||||
global.localStorage.__proto__[key] = fn
|
||||
return fn
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noUnusedLocals": true,
|
||||
"types": ["cypress", "@testing-library/cypress"]
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
|
|
|
|||
Loading…
Reference in New Issue