From 097f9c274ae579e6c5a3ffbe213fb40b51bedec2 Mon Sep 17 00:00:00 2001 From: mutoe Date: Sat, 11 Jun 2022 19:28:30 +0800 Subject: [PATCH] refactor: component test migrate to cypress ct --- .eslintrc | 16 - .eslintrc.js | 30 + cypress.config.ts | 14 +- cypress/.eslintrc.js | 18 +- .../e2e/{article.test.js => article.cy.ts} | 4 +- cypress/e2e/{auth.test.js => auth.cy.ts} | 6 +- cypress/{constants.js => e2e/constant.ts} | 0 .../e2e/{favorite.test.js => favorite.cy.ts} | 2 +- cypress/e2e/{follow.test.js => follow.cy.ts} | 2 +- cypress/e2e/{home.test.js => home.cy.ts} | 4 +- cypress/e2e/{tag.test.js => tag.cy.ts} | 4 +- cypress/fixtures/article-comments.json | 28 + cypress/fixtures/article.json | 2 +- ...icles_of_tag.json => articles-of-tag.json} | 0 cypress/plugins/index.js | 21 - cypress/support/commands.js | 27 - cypress/support/commands.ts | 12 + cypress/support/component-index.html | 18 + cypress/support/component.ts | 78 + cypress/support/e2e.js | 20 - cypress/support/e2e.ts | 42 + cypress/support/index.d.ts | 9 - cypress/tsconfig.json | 14 + env.d.ts | 1 + package.json | 59 +- src/components/AppFooter.spec.ts | 15 +- src/components/AppLink.spec.ts | 22 +- src/components/AppLink.vue | 4 +- src/components/AppNavigation.spec.ts | 32 +- src/components/AppPagination.spec.ts | 23 +- src/components/ArticleDetail.spec.ts | 61 +- src/components/ArticleDetail.vue | 8 +- src/components/ArticleDetailComment.spec.ts | 38 +- src/components/ArticleDetailComments.spec.ts | 77 +- .../ArticleDetailCommentsForm.spec.ts | 53 +- src/components/ArticleDetailMeta.spec.ts | 122 +- src/components/ArticlesList.spec.ts | 35 +- .../ArticlesListArticlePreview.spec.ts | 27 +- src/components/ArticlesListArticlePreview.vue | 8 +- src/components/ArticlesListNavigation.spec.ts | 34 +- src/components/PopularTags.spec.ts | 29 +- src/pages/Article.spec.ts | 25 +- src/plugins/marked.ts | 3 +- src/router.ts | 121 +- src/setup-test.ts | 17 - src/types/global-component.d.ts | 1 + src/types/global.d.ts | 4 - src/utils/create-async-process.spec.ts | 23 +- src/utils/filters.spec.ts | 2 +- src/utils/map-checkable-response.spec.ts | 45 +- src/utils/params-to-query.spec.ts | 2 +- src/utils/request.spec.ts | 288 +- src/utils/request.ts | 4 +- src/utils/storage.spec.ts | 55 +- src/utils/test/mock-fetch.ts | 27 - src/utils/test/mock-local-storage.ts | 10 - src/utils/test/render-async.ts | 107 - src/utils/test/test.utils.ts | 19 + src/utils/test/wrap-tests.ts | 6 +- tsconfig.app.json | 13 + tsconfig.config.json | 2 +- tsconfig.cypress-ct.json | 9 + tsconfig.json | 12 +- vite.config.ts | 4 +- yarn.lock | 2454 +---------------- 65 files changed, 944 insertions(+), 3328 deletions(-) delete mode 100644 .eslintrc create mode 100644 .eslintrc.js rename cypress/e2e/{article.test.js => article.cy.ts} (94%) rename cypress/e2e/{auth.test.js => auth.cy.ts} (95%) rename cypress/{constants.js => e2e/constant.ts} (100%) rename cypress/e2e/{favorite.test.js => favorite.cy.ts} (96%) rename cypress/e2e/{follow.test.js => follow.cy.ts} (97%) rename cypress/e2e/{home.test.js => home.cy.ts} (95%) rename cypress/e2e/{tag.test.js => tag.cy.ts} (93%) create mode 100644 cypress/fixtures/article-comments.json rename cypress/fixtures/{articles_of_tag.json => articles-of-tag.json} (100%) delete mode 100644 cypress/plugins/index.js delete mode 100644 cypress/support/commands.js create mode 100644 cypress/support/commands.ts create mode 100644 cypress/support/component-index.html create mode 100644 cypress/support/component.ts delete mode 100644 cypress/support/e2e.js create mode 100644 cypress/support/e2e.ts delete mode 100644 cypress/support/index.d.ts create mode 100644 cypress/tsconfig.json create mode 100644 env.d.ts delete mode 100644 src/setup-test.ts delete mode 100644 src/utils/test/mock-fetch.ts delete mode 100644 src/utils/test/mock-local-storage.ts delete mode 100644 src/utils/test/render-async.ts create mode 100644 src/utils/test/test.utils.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.cypress-ct.json diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index c2f816c..0000000 --- a/.eslintrc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "root": true, - "parser": "vue-eslint-parser", - "parserOptions": { - "parser": "@typescript-eslint/parser", - "project": "./tsconfig.json", - "sourceType": "module", - "extraFileExtensions": [".vue", ".d.ts"] - }, - "extends": [ - "@mutoe/eslint-config-preset-vue" - ], - "rules": { - "@typescript-eslint/explicit-function-return-type": "off" - } -} diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..ecc1f88 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,30 @@ +module.exports = { + root: true, + parser: 'vue-eslint-parser', + parserOptions: { + parser: '@typescript-eslint/parser', + project: ['./tsconfig.app.json', './tsconfig.config.json', './tsconfig.cypress-ct.json'], + sourceType: 'module', + extraFileExtensions: ['.vue'], + }, + extends: [ + '@mutoe/eslint-config-preset-vue', + ], + rules: { + '@typescript-eslint/explicit-function-return-type': 'off', + }, + overrides: [ + { + files: [ + 'src/**/*.{cy,spec}.{js,ts,jsx,tsx}', + ], + extends: [ + 'plugin:cypress/recommended', + ], + rules: { + // `expect(true).to.be.true` is a valid expression + 'no-unused-expressions': 'off', + }, + }, + ], +} diff --git a/cypress.config.ts b/cypress.config.ts index 53f1b06..06e63a6 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -3,12 +3,14 @@ import { defineConfig } from 'cypress' export default defineConfig({ projectId: 'j7s91r', e2e: { - // We've imported your old cypress plugins here. - // You may want to clean this up later by importing these. - setupNodeEvents(on, config) { - return require('./cypress/plugins/index.js')(on, config) - }, - baseUrl: 'http://localhost:4173', specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}', + baseUrl: 'http://localhost:4173', + }, + component: { + specPattern: 'src/**/*.{cy,spec}.{js,ts,jsx,tsx}', + devServer: { + framework: 'vue', + bundler: 'vite', + }, }, }) diff --git a/cypress/.eslintrc.js b/cypress/.eslintrc.js index 2740098..3d9bfcc 100644 --- a/cypress/.eslintrc.js +++ b/cypress/.eslintrc.js @@ -1,12 +1,20 @@ module.exports = { root: true, + parser: '@typescript-eslint/parser', + extends: [ + '@mutoe/eslint-config-preset-ts', + 'plugin:cypress/recommended', + ], env: { 'cypress/globals': true, }, - extends: [ - '@mutoe/eslint-config-preset-basic', - 'plugin:cypress/recommended', + overrides: [ + { + files: ['support/**/*.ts'], + rules: { + '@typescript-eslint/no-namespace': 'off', + '@typescript-eslint/consistent-type-imports': 'error', + }, + }, ], - rules: { - }, } diff --git a/cypress/e2e/article.test.js b/cypress/e2e/article.cy.ts similarity index 94% rename from cypress/e2e/article.test.js rename to cypress/e2e/article.cy.ts index c2dea6c..104450e 100644 --- a/cypress/e2e/article.test.js +++ b/cypress/e2e/article.cy.ts @@ -1,10 +1,10 @@ -import { ROUTES } from '../constants' +import { ROUTES } from './constant' describe('Article', () => { beforeEach(() => { cy.intercept('GET', /articles\?limit/, { fixture: 'articles.json' }) cy.intercept('GET', /articles\/.+/, { fixture: 'article.json' }) - cy.intercept('GET', /tags/, { fixture: 'articles_of_tag.json' }) + cy.intercept('GET', /tags/, { fixture: 'articles-of-tag.json' }) cy.intercept('GET', /profiles\/.+/, { fixture: 'profile.json' }) cy.intercept('DELETE', /articles\/.+/, { statusCode: 200, body: {} }).as('deleteArticle') }) diff --git a/cypress/e2e/auth.test.js b/cypress/e2e/auth.cy.ts similarity index 95% rename from cypress/e2e/auth.test.js rename to cypress/e2e/auth.cy.ts index b15d643..58d9823 100644 --- a/cypress/e2e/auth.test.js +++ b/cypress/e2e/auth.cy.ts @@ -1,4 +1,4 @@ -import { ROUTES } from '../constants' +import { ROUTES } from './constant' describe('Auth', () => { describe('Login and logout', () => { @@ -42,8 +42,8 @@ describe('Auth', () => { cy.get('[type="password"]').type('123456') cy.get('[type="submit"]').click() - cy.get('form').then(([$el]) => { - cy.wrap($el.checkValidity()).should('to.be', false) + cy.get('form').then(($el) => { + cy.wrap($el[0].checkValidity()).should('to.be', false) }) }) diff --git a/cypress/constants.js b/cypress/e2e/constant.ts similarity index 100% rename from cypress/constants.js rename to cypress/e2e/constant.ts diff --git a/cypress/e2e/favorite.test.js b/cypress/e2e/favorite.cy.ts similarity index 96% rename from cypress/e2e/favorite.test.js rename to cypress/e2e/favorite.cy.ts index a6979dc..0d1cc14 100644 --- a/cypress/e2e/favorite.test.js +++ b/cypress/e2e/favorite.cy.ts @@ -1,4 +1,4 @@ -import { ROUTES } from '../constants' +import { ROUTES } from './constant' describe('Favorite', () => { beforeEach(() => { diff --git a/cypress/e2e/follow.test.js b/cypress/e2e/follow.cy.ts similarity index 97% rename from cypress/e2e/follow.test.js rename to cypress/e2e/follow.cy.ts index 15daaaf..2d6f72d 100644 --- a/cypress/e2e/follow.test.js +++ b/cypress/e2e/follow.cy.ts @@ -1,4 +1,4 @@ -import { ROUTES } from '../constants' +import { ROUTES } from './constant' describe.only('Follow', () => { beforeEach(() => { diff --git a/cypress/e2e/home.test.js b/cypress/e2e/home.cy.ts similarity index 95% rename from cypress/e2e/home.test.js rename to cypress/e2e/home.cy.ts index 69dbb1d..e6a682c 100644 --- a/cypress/e2e/home.test.js +++ b/cypress/e2e/home.cy.ts @@ -1,8 +1,8 @@ -import { ROUTES } from '../constants' +import { ROUTES } from './constant' describe('Homepage', () => { beforeEach(() => { - cy.intercept('GET', /articles\?tag=butt/, { fixture: 'articles_of_tag.json' }).as('getArticlesOfTag') + cy.intercept('GET', /articles\?tag=butt/, { fixture: 'articles-of-tag.json' }).as('getArticlesOfTag') cy.intercept('GET', /articles\?limit/, { fixture: 'articles.json' }).as('getArticles') cy.intercept('GET', /articles\/.+/, { fixture: 'article.json' }).as('getArticle') cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags') diff --git a/cypress/e2e/tag.test.js b/cypress/e2e/tag.cy.ts similarity index 93% rename from cypress/e2e/tag.test.js rename to cypress/e2e/tag.cy.ts index d21e467..5832b78 100644 --- a/cypress/e2e/tag.test.js +++ b/cypress/e2e/tag.cy.ts @@ -1,8 +1,8 @@ -import { ROUTES } from '../constants' +import { ROUTES } from './constant' describe('Tag', () => { beforeEach(() => { - cy.intercept('GET', /articles\?tag=butt/, { fixture: 'articles_of_tag.json' }).as('getArticlesOfTag') + cy.intercept('GET', /articles\?tag=butt/, { fixture: 'articles-of-tag.json' }).as('getArticlesOfTag') cy.intercept('GET', /articles\?limit/, { fixture: 'articles.json' }).as('getArticles') cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags') }) diff --git a/cypress/fixtures/article-comments.json b/cypress/fixtures/article-comments.json new file mode 100644 index 0000000..81dab3a --- /dev/null +++ b/cypress/fixtures/article-comments.json @@ -0,0 +1,28 @@ +{ + "comments": [ + { + "id": 5, + "createdAt": "2021-11-24T12:11:08.480Z", + "updatedAt": "2021-11-24T12:11:08.480Z", + "body": "If someone else has started working on an implementation, consider jumping in and helping them! by contacting the author.", + "author": { + "username": "Gerome", + "bio": null, + "image": "https://api.realworld.io/images/demo-avatar.png", + "following": false + } + }, + { + "id": 4, + "createdAt": "2021-11-24T12:11:08.340Z", + "updatedAt": "2021-11-24T12:11:08.340Z", + "body": "Before starting a new implementation, please check if there is any work in progress for the stack you want to work on.", + "author": { + "username": "Gerome", + "bio": null, + "image": "https://api.realworld.io/images/demo-avatar.png", + "following": false + } + } + ] +} diff --git a/cypress/fixtures/article.json b/cypress/fixtures/article.json index bf2ad14..529cd94 100644 --- a/cypress/fixtures/article.json +++ b/cypress/fixtures/article.json @@ -10,7 +10,7 @@ "author": { "username": "plumrx", "bio": null, - "image": "https://static.realworld.io/images/smiley-cyrus.jpg", + "image": "https://api.realworld.io/images/demo-avatar.png", "following": false }, "favorited": true, diff --git a/cypress/fixtures/articles_of_tag.json b/cypress/fixtures/articles-of-tag.json similarity index 100% rename from cypress/fixtures/articles_of_tag.json rename to cypress/fixtures/articles-of-tag.json diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js deleted file mode 100644 index aa9918d..0000000 --- a/cypress/plugins/index.js +++ /dev/null @@ -1,21 +0,0 @@ -/// -// *********************************************************** -// This example plugins/index.js can be used to load plugins -// -// You can change the location of this file or turn off loading -// the plugins file with the 'pluginsFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/plugins-guide -// *********************************************************** - -// This function is called when a project is opened or re-opened (e.g. due to -// the project's config changing) - -/** - * @type {Cypress.PluginConfig} - */ -module.exports = (on, config) => { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config -} diff --git a/cypress/support/commands.js b/cypress/support/commands.js deleted file mode 100644 index 6a09561..0000000 --- a/cypress/support/commands.js +++ /dev/null @@ -1,27 +0,0 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** - -import { ROUTES } from '../constants' - -Cypress.Commands.add('login', (username = 'plumrx') => { - cy.fixture('user.json').then(authResponse => { - authResponse.user.username = username - cy.intercept('POST', /users\/login$/, { statusCode: 200, body: authResponse }) - }) - - // click sign in button in home page - cy.visit(ROUTES.LOGIN) - - cy.get('[type="email"]').type('foo@example.com') - cy.get('[type="password"]').type('12345678') - cy.get('[type="submit"]').contains('Sign in').click() - - cy.url().should('match', /#\/$/) -}) diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts new file mode 100644 index 0000000..3403108 --- /dev/null +++ b/cypress/support/commands.ts @@ -0,0 +1,12 @@ +/// +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** + +import '@testing-library/cypress/add-commands' diff --git a/cypress/support/component-index.html b/cypress/support/component-index.html new file mode 100644 index 0000000..957c9fc --- /dev/null +++ b/cypress/support/component-index.html @@ -0,0 +1,18 @@ + + + + + + + Components App + + + + + + + + +
+ + diff --git a/cypress/support/component.ts b/cypress/support/component.ts new file mode 100644 index 0000000..73fc58d --- /dev/null +++ b/cypress/support/component.ts @@ -0,0 +1,78 @@ +/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types */ +// noinspection ES6PreferShortImport + +// *********************************************************** +// This example support/component.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** +import './commands' + +import type { CyMountOptions } from 'cypress/vue' +import { mount } from 'cypress/vue' +import registerGlobalComponents from 'src/plugins/global-components' +import { routes } from 'src/router' +import type { + DefineComponent, ExtractDefaultPropTypes, MethodOptions, + AllowedComponentProps, ComponentCustomProps, VNodeProps, + ComponentOptionsMixin, + ComputedOptions, + EmitsOptions, ExtractPropTypes, +} from 'vue' +import type { Router } from 'vue-router' +import { createMemoryHistory, createRouter } from 'vue-router' + +type PublicProps = VNodeProps & AllowedComponentProps & ComponentCustomProps +type RouterOptions = {router?: Router} + +type Mount = , EE extends string = string, PP = PublicProps, Props = Readonly>, Defaults = ExtractDefaultPropTypes>( + component: DefineComponent, + options?: CyMountOptions & Omit, D> & RouterOptions, +) => Cypress.Chainable + +type MountParams = Parameters + +declare global { + namespace Cypress { + // noinspection JSUnusedGlobalSymbols + interface Chainable { + mount: Mount + } + } +} + +Cypress.Commands.add('mount', (component: any, options: MountParams[1] = {}) => { + options.global = options.global || {} + options.global.plugins = options.global.plugins || [] + + if (!options.router) { + options.router = createRouter({ + routes, + history: createMemoryHistory(), + }) + } + + options.global.plugins.push({ + install (app) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + app.use(options.router!) + }, + }) + + options.global.plugins.push({ + install (app) { + registerGlobalComponents(app) + }, + }) + + return mount(component, options) +}) diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js deleted file mode 100644 index d68db96..0000000 --- a/cypress/support/e2e.js +++ /dev/null @@ -1,20 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import './commands' - -// Alternatively you can use CommonJS syntax: -// require('./commands') diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts new file mode 100644 index 0000000..8fb5ac7 --- /dev/null +++ b/cypress/support/e2e.ts @@ -0,0 +1,42 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +import './commands' + +import { ROUTES } from 'e2e/constant' + +declare global { + namespace Cypress { + interface Chainable { + login(): void + } + } +} + +Cypress.Commands.add('login', (username = 'plumrx') => { + cy.fixture('user.json').then(authResponse => { + authResponse.user.username = username + cy.intercept('POST', /users\/login$/, { statusCode: 200, body: authResponse }) + }) + + // click sign in button in home page + cy.visit(ROUTES.LOGIN) + + cy.get('[type="email"]').type('foo@example.com') + cy.get('[type="password"]').type('12345678') + cy.get('[type="submit"]').contains('Sign in').click() + + cy.url().should('match', /#\/$/) +}) diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts deleted file mode 100644 index 599128e..0000000 --- a/cypress/support/index.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// in cypress/support/index.d.ts -// load type definitions that come with Cypress module -/// - -declare namespace Cypress { - interface Chainable { - login(): void - } -} diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 0000000..8157a51 --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "@vue/tsconfig/tsconfig.web.json", + "include": ["./**/*.{ts,json}", ".eslintrc.js", "../src/router.ts", "../src/plugins/global-components.ts"], + "compilerOptions": { + "baseUrl": ".", + "isolatedModules": false, + "target": "es5", + "lib": ["es5", "dom"], + "types": ["cypress", "@testing-library/cypress"], + "paths": { + "src/*": ["../src/*"], + } + } +} diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/package.json b/package.json index 2c63e82..3f2fd66 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,13 @@ "dev": "vite", "build": "vite build", "serve": "vite preview --port 4137", - "lint:script": "eslint \"{src/**/*.{ts,vue},cypress/**/*.js}\"", + "lint:script": "eslint \"{src/**/*.{ts,vue},cypress/**/*.ts}\"", "lint:tsc": "vue-tsc --noEmit", "lint": "concurrently \"yarn lint:tsc\" \"yarn lint:script\"", - "test:unit": "jest", - "test:e2e": "yarn build && concurrently -rk -s first \"yarn serve\" \"./node_modules/.bin/cypress run -c baseUrl=http://localhost:4137\"", - "test:e2e:ci": "./node_modules/.bin/cypress run -C cypress.prod.json", + "test:unit": "cypress open --component", + "test:unit:ci": "cypress run --component --quiet --reporter spec", + "test:e2e": "yarn build && concurrently -rk -s first \"yarn serve\" \"cypress run --e2e -c baseUrl=http://localhost:4137\"", + "test:e2e:ci": "cypress run --e2e -C cypress.prod.config.ts", "test": "yarn test:unit && yarn test:e2e" }, "dependencies": { @@ -23,64 +24,26 @@ "vue-router": "^4.0.15" }, "devDependencies": { - "@babel/core": "^7.18.2", + "@cypress/vue": "^3.1.2", "@mutoe/eslint-config-preset-vue": "~1.3.2", - "@testing-library/jest-dom": "^5.16.4", - "@testing-library/vue": "^6.4.2", - "@types/jest": "^28.1.1", + "@pinia/testing": "^0.0.12", + "@testing-library/cypress": "^8.0.3", "@types/marked": "^4.0.3", "@vitejs/plugin-vue": "^2.3.3", - "@vue/compiler-sfc": "^3.2.37", "@vue/tsconfig": "^0.1.3", - "babel-jest": "^28.1.1", "concurrently": "^7.2.1", - "cypress": "^10.0.3", + "cypress": "^10.1.0", "eslint": "^8.17.0", "eslint-plugin-cypress": "^2.12.1", "husky": "^8.0.0", - "jest": "^28.1.1", - "jsdom": "^19.0.0", "lint-staged": "^13.0.0", "rollup-plugin-analyzer": "^4.0.0", - "ts-jest": "^28.0.4", "typescript": "~4.7.2", "vite": "^2.9.10", - "vue-tsc": "^0.37.3", - "vue3-jest": "^27.0.0-alpha.2" + "vue-tsc": "^0.37.3" }, "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": [ - "/src/**/*.{ts,vue}", - "!/src/config.ts" - ], - "moduleFileExtensions": [ - "vue", - "ts", - "js", - "json", - "node" - ], - "testMatch": [ - "/src/**/*.spec.ts" - ], - "modulePaths": [ - "" - ], - "setupFilesAfterEnv": [ - "/src/setup-test.ts" - ] + "cypress/**/*.ts": "eslint --fix" } } diff --git a/src/components/AppFooter.spec.ts b/src/components/AppFooter.spec.ts index 0590c1c..26fa714 100644 --- a/src/components/AppFooter.spec.ts +++ b/src/components/AppFooter.spec.ts @@ -1,18 +1,9 @@ -import { render } from '@testing-library/vue' -import registerGlobalComponents from 'src/plugins/global-components' -import { router } from 'src/router' -import AppFooter from './AppFooter.vue' +import AppFooter from 'src/components/AppFooter.vue' describe('# AppFooter', () => { - beforeEach(async () => { - await router.push('/') - }) - it('should render correctly', () => { - const { container } = render(AppFooter, { - global: { plugins: [registerGlobalComponents, router] }, - }) + cy.mount(AppFooter) - expect(container).toBeInTheDocument() + cy.contains('Real world app') }) }) diff --git a/src/components/AppLink.spec.ts b/src/components/AppLink.spec.ts index 6668832..8b48142 100644 --- a/src/components/AppLink.spec.ts +++ b/src/components/AppLink.spec.ts @@ -1,27 +1,17 @@ -import { fireEvent, render, waitFor } from '@testing-library/vue' -import { router } from 'src/router' import AppLink from './AppLink.vue' describe('# AppLink', () => { - beforeEach(async () => { - await router.push('/') - }) - - it('should redirect to another page when click the link', async () => { + it('should redirect to another page when click the link', () => { // given - const { container, getByRole } = render(AppLink, { - global: { plugins: [router] }, + cy.mount(AppLink, { props: { name: 'tag', params: { tag: 'foo' } }, - slots: { default: 'Go to Foo tag' }, + 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) + cy.contains('tag').click() - // then - await waitFor(() => expect(linkElement).toHaveClass('router-link-active')) + // await waitFor(() => expect(linkElement).toHaveClass('router-link-active')) }) }) diff --git a/src/components/AppLink.vue b/src/components/AppLink.vue index 2027f6a..f239bf2 100644 --- a/src/components/AppLink.vue +++ b/src/components/AppLink.vue @@ -11,12 +11,12 @@ import type { AppRouteNames } from 'src/router' import type { RouteParams } from 'vue-router' -interface Props { +export interface AppLinkProps { name: AppRouteNames params?: Partial } -const props = withDefaults(defineProps(), { +const props = withDefaults(defineProps(), { params: () => ({}), }) diff --git a/src/components/AppNavigation.spec.ts b/src/components/AppNavigation.spec.ts index 2ea2c0a..e9fb730 100644 --- a/src/components/AppNavigation.spec.ts +++ b/src/components/AppNavigation.spec.ts @@ -1,42 +1,32 @@ -import { render } from '@testing-library/vue' import { createPinia, setActivePinia } from 'pinia' -import registerGlobalComponents from 'src/plugins/global-components' -import { router } from 'src/router' import { useUserStore } from 'src/store/user' import AppNavigation from './AppNavigation.vue' describe('# AppNavigation', () => { beforeEach(async () => { setActivePinia(createPinia()) - await router.push('/') }) it('should render Sign in and Sign up when user not logged', () => { const userStore = useUserStore() userStore.updateUser(null) - const { container } = render(AppNavigation, { - global: { plugins: [registerGlobalComponents, router], components: {} }, - }) + cy.mount(AppNavigation) - expect(container.querySelectorAll('.nav-item')).toHaveLength(3) - expect(container.textContent).toContain('Home') - expect(container.textContent).toContain('Sign in') - expect(container.textContent).toContain('Sign up') + cy.get('.nav-item').should('have.length', 3) + cy.contains('Home') + cy.contains('Sign in') + cy.contains('Sign up') }) it('should render xxx when user logged', () => { const userStore = useUserStore() userStore.updateUser({ id: 1, username: 'foo', email: '', token: '', bio: undefined, image: undefined }) - const { container } = render(AppNavigation, { - global: { - plugins: [registerGlobalComponents, router], - }, - }) + cy.mount(AppNavigation) - 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', 4) + cy.contains('Home') + cy.contains('New Post') + cy.contains('Settings') + cy.contains('foo') }) }) diff --git a/src/components/AppPagination.spec.ts b/src/components/AppPagination.spec.ts index 3769a5d..3414bf0 100644 --- a/src/components/AppPagination.spec.ts +++ b/src/components/AppPagination.spec.ts @@ -1,26 +1,25 @@ -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, { + cy.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) + .eq(0).should('have.class', 'active') }) - it('should call onPageChange when click a page item', async () => { - const { getByRole, emitted } = render(AppPagination, { - props: { page: 1, count: 15 }, + it('should call onPageChange when click a page item', () => { + const onPageChange = cy.spy().as('onPageChange') + mount(AppPagination, { + props: { page: 1, count: 15, onPageChange }, }) - 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]) + cy.get('@onPageChange').should('have.been.calledWith', 2) }) }) diff --git a/src/components/ArticleDetail.spec.ts b/src/components/ArticleDetail.spec.ts index a7ffcc4..360d2c2 100644 --- a/src/components/ArticleDetail.spec.ts +++ b/src/components/ArticleDetail.spec.ts @@ -1,46 +1,51 @@ -import registerGlobalComponents from 'src/plugins/global-components' -import { router } from 'src/router' -import { getArticle } from 'src/services/article/getArticle' +import { createPinia, setActivePinia } from 'pinia' import fixtures from 'src/utils/test/fixtures' -import { renderAsync } from '../utils/test/render-async' +import { asyncWrapper, createTestRouter } from 'src/utils/test/test.utils' import ArticleDetail from './ArticleDetail.vue' -jest.mock('src/services/article/getArticle') +describe('# ArticleDetail', () => { + const router = createTestRouter() + const AsyncArticleDetail = asyncWrapper(ArticleDetail) -describe.skip('# ArticleDetail', () => { - const mockGetArticle = getArticle as jest.MockedFunction - - beforeEach(async () => { - await router.push({ - name: 'article', - params: { slug: fixtures.article.slug }, - }) + beforeEach(() => { + setActivePinia(createPinia()) + cy.wrap(router.push({ name: 'article', params: { slug: fixtures.article.slug } })) }) - it('should render markdown body correctly', async () => { - mockGetArticle.mockResolvedValue({ ...fixtures.article, body: fixtures.markdown }) - const { container } = await renderAsync(ArticleDetail, { - global: { plugins: [registerGlobalComponents, router] }, + it('should render markdown body correctly', () => { + cy.fixture('article.json').then((res) => { + res.article.body = fixtures.markdown + cy.intercept('/api/articles/*', res).as('getArticle') }) - expect(container.querySelector('.article-content')).toMatchSnapshot() + cy.mount(AsyncArticleDetail, { router }) + cy.wait('@getArticle') + + cy.get('.article-content').should('contain.text', 'h1 Heading 8-)') }) - it('should render markdown (zh-CN) body correctly', async () => { - mockGetArticle.mockResolvedValue({ ...fixtures.article, body: fixtures.markdownCN }) - const { container } = await renderAsync(ArticleDetail, { - global: { plugins: [registerGlobalComponents, router] }, + // 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') }) - expect(container.querySelector('.article-content')).toMatchSnapshot() + cy.mount(AsyncArticleDetail, { router }) + cy.wait('@getArticle') + + cy.get('.article-content').should('have.text', fixtures.markdownCN) }) - it('should filter the xss content in Markdown body', async () => { - mockGetArticle.mockResolvedValue({ ...fixtures.article, body: fixtures.markdownXss }) - const { container } = await renderAsync(ArticleDetail, { - global: { plugins: [registerGlobalComponents, router] }, + 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') }) - expect(container.querySelector('.article-content')?.textContent).not.toContain('alert') + cy.mount(AsyncArticleDetail, { router }) + cy.wait('@getArticle') + + cy.get('.article-content').should('have.text', fixtures.markdownXss) }) }) diff --git a/src/components/ArticleDetail.vue b/src/components/ArticleDetail.vue index 96dad0a..a510637 100644 --- a/src/components/ArticleDetail.vue +++ b/src/components/ArticleDetail.vue @@ -4,6 +4,7 @@

{{ article.title }}

@@ -12,14 +13,14 @@
- +
- + - +
  • diff --git a/src/components/ArticleDetailComment.spec.ts b/src/components/ArticleDetailComment.spec.ts index 81a4a7e..ec0b5a5 100644 --- a/src/components/ArticleDetailComment.spec.ts +++ b/src/components/ArticleDetailComment.spec.ts @@ -1,46 +1,40 @@ -import { fireEvent, render } from '@testing-library/vue' -import registerGlobalComponents from 'src/plugins/global-components' -import { router } from 'src/router' import fixtures from 'src/utils/test/fixtures' import ArticleDetailComment from './ArticleDetailComment.vue' describe('# ArticleDetailComment', () => { - beforeEach(async () => { - await router.push({ name: 'article', params: { slug: fixtures.article.slug } }) - }) - it('should render correctly', () => { - const { container, queryByRole } = render(ArticleDetailComment, { - global: { plugins: [registerGlobalComponents, router] }, + cy.mount(ArticleDetailComment, { 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', '1/1/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] }, + cy.mount(ArticleDetailComment, { props: { comment: fixtures.comment, username: fixtures.author.username, }, }) - expect(getByRole('button', { name: 'Delete comment' })).toBeInTheDocument() + cy.findByRole('button', { name: 'Delete comment' }) }) - it('should emit remove comment when click remove comment button', async () => { - const { getByRole, emitted } = render(ArticleDetailComment, { - global: { plugins: [registerGlobalComponents, router] }, - props: { comment: fixtures.comment, username: fixtures.author.username }, + it('should emit remove comment when click remove comment button', () => { + const onRemoveComment = cy.spy().as('onRemoveComment') + cy.mount(ArticleDetailComment, { + props: { + comment: fixtures.comment, + username: fixtures.author.username, + onRemoveComment, + }, }) - await fireEvent.click(getByRole('button', { name: 'Delete comment' })) + cy.findByRole('button', { name: 'Delete comment' }).click() - const events = emitted() - expect(events['remove-comment']).toHaveLength(1) + cy.get('@onRemoveComment').should('have.been.called') }) }) diff --git a/src/components/ArticleDetailComments.spec.ts b/src/components/ArticleDetailComments.spec.ts index 99ff9b2..e1a339c 100644 --- a/src/components/ArticleDetailComments.spec.ts +++ b/src/components/ArticleDetailComments.spec.ts @@ -1,64 +1,61 @@ -import { waitFor } from '@testing-library/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 { createPinia, setActivePinia } from 'pinia' +import { useUserStore } from 'src/store/user' import fixtures from 'src/utils/test/fixtures' -import { renderAsync } from '../utils/test/render-async' +import { asyncWrapper, createTestRouter } from 'src/utils/test/test.utils' 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 - const mockDeleteComment = deleteComment as jest.MockedFunction + // const mockDeleteComment = deleteComment as jest.MockedFunction + const AsyncArticleDetailComments = asyncWrapper(ArticleDetailComments) + const router = createTestRouter() + setActivePinia(createPinia()) + const userStore = useUserStore() - beforeEach(async () => { - mockGetCommentsByArticle.mockResolvedValue([fixtures.comment]) - await router.push({ - name: 'article', - params: { slug: fixtures.article.slug }, - }) + 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 } })) }) - it('should render correctly', async () => { - const { container } = await renderAsync(ArticleDetailComments, { - global: { plugins: [registerGlobalComponents, router] }, - }) + it('should render correctly', () => { + cy.mount(AsyncArticleDetailComments, { router }) - expect(mockGetCommentsByArticle).toBeCalledWith('article-foo') - expect(container).toBeInTheDocument() + cy.wait('@getCommentsByArticle') + .its('request.url') + .should('contain', '/api/articles/article-foo/comments') + + cy.contains(fixtures.comment.body) }) - it.skip('should display new comment when post new comment', async () => { + it('should display new comment when post new comment', () => { // given - const { container } = await renderAsync(ArticleDetailComments, { - global: { plugins: [registerGlobalComponents, router] }, - }) - - await waitFor(() => expect(mockGetCommentsByArticle).toBeCalled()) - expect(container.querySelectorAll('.card')).toHaveLength(1) + userStore.updateUser(fixtures.user) + cy.mount(AsyncArticleDetailComments, { router }) + cy.wait('@getProfile') + cy.wait('@getCommentsByArticle') + cy.contains(fixtures.comment.body) // when - // wrapper.findComponent(ArticleDetailCommentsForm).vm.$emit('add-comment', fixtures.comment2) - // await nextTick() + cy.findByRole('textbox', { name: 'Write comment' }).type(fixtures.comment2.body) + cy.findByRole('button', { name: 'Submit' }).click() // then - expect(container.querySelectorAll('.card')).toHaveLength(2) + cy.contains(fixtures.comment2.body) }) - it.skip('should call remove comment service when click delete button', async () => { + it.only('should call remove comment service when click delete button', () => { // given - await renderAsync(ArticleDetailComments, { - global: { plugins: [registerGlobalComponents, router] }, - }) - await waitFor(() => expect(mockGetCommentsByArticle).toBeCalled()) + cy.intercept('DELETE', '/api/articles/*/comments/*', { status: 200 }).as('deleteComment') + userStore.updateUser(fixtures.user) + cy.mount(AsyncArticleDetailComments, { router }) + cy.wait('@getCommentsByArticle') // when - // wrapper.findComponent(ArticleDetailComment).vm.$emit('remove-comment') + cy.findByRole('button', { name: 'Delete comment' }).click() // then - expect(mockDeleteComment).toBeCalledWith('article-foo', 1) + cy.wait('@deleteComment') + cy.contains(fixtures.comment.body).should('not.exist') }) }) diff --git a/src/components/ArticleDetailCommentsForm.spec.ts b/src/components/ArticleDetailCommentsForm.spec.ts index f7dfc35..e0e1866 100644 --- a/src/components/ArticleDetailCommentsForm.spec.ts +++ b/src/components/ArticleDetailCommentsForm.spec.ts @@ -1,54 +1,37 @@ -import { fireEvent, render } from '@testing-library/vue' +import { createPinia, setActivePinia } from 'pinia' 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 { useUserStore } from 'src/store/user' import fixtures from 'src/utils/test/fixtures' -import { ref } from 'vue' - -jest.mock('src/composable/useProfile') -jest.mock('src/services/comment/postComment') describe('# ArticleDetailCommentsForm', () => { - const mockUseProfile = useProfile as jest.MockedFunction - const mockPostComment = postComment as jest.MockedFunction + setActivePinia(createPinia()) + const userStore = useUserStore() - beforeEach(async () => { - await router.push({ name: 'article', params: { slug: fixtures.article.slug } }) - mockPostComment.mockResolvedValue(fixtures.comment2) - mockUseProfile.mockReturnValue({ - profile: ref(fixtures.author), - updateProfile: jest.fn(), - }) + 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) }) it('should display sign in button when user not logged', () => { - mockUseProfile.mockReturnValue({ profile: ref(null), updateProfile: jest.fn() }) - const { container } = render(ArticleDetailCommentsForm, { - global: { plugins: [registerGlobalComponents, router] }, + userStore.updateUser(null) + cy.mount(ArticleDetailCommentsForm, { 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 () => { - // given - const { getByRole, emitted } = render(ArticleDetailCommentsForm, { - global: { plugins: [registerGlobalComponents, router] }, + it('should display form when user logged', () => { + cy.mount(ArticleDetailCommentsForm, { 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' }).click() - // then - expect(mockPostComment).toBeCalledWith('article-foo', 'some texts...') - - const { submit } = emitted() - expect(submit).toHaveLength(1) + cy.wait('@postComment') + .its('request.body') + .should('deep.equal', { comment: { body: 'some texts...' } }) }) }) diff --git a/src/components/ArticleDetailMeta.spec.ts b/src/components/ArticleDetailMeta.spec.ts index b176022..bbb346c 100644 --- a/src/components/ArticleDetailMeta.spec.ts +++ b/src/components/ArticleDetailMeta.spec.ts @@ -1,134 +1,118 @@ -import { fireEvent, render } from '@testing-library/vue' -import type { GlobalMountOptions } from '@vue/test-utils/dist/types' -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 { createPinia, setActivePinia } from 'pinia' import { useUserStore } 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], -} +const editButton = 'Edit article' +const deleteButton = 'Delete article' +const followButton = 'Follow' +const unfollowButton = 'Unfollow' +const favoriteButton = 'Favorite article' +const unfavoriteButton = 'Unfavorite article' describe('# ArticleDetailMeta', () => { - const editButton = 'Edit article' - const deleteButton = 'Delete article' - const followButton = 'Follow' - const unfollowButton = 'Unfollow' - const favoriteButton = 'Favorite article' - const unfavoriteButton = 'Unfavorite article' - - const mockDeleteArticle = deleteArticle as jest.MockedFunction - const mockFollowUser = postFollowProfile as jest.MockedFunction - const mockUnfollowUser = deleteFollowProfile as jest.MockedFunction - const mockFavoriteArticle = postFavoriteArticle as jest.MockedFunction - const mockUnfavoriteArticle = deleteFavoriteArticle as jest.MockedFunction + setActivePinia(createPinia()) const userStore = useUserStore() - 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) + beforeEach(() => { userStore.updateUser(fixtures.user) - await router.push({ name: 'article', params: { slug: fixtures.article.slug } }) }) it('should display edit button when user is author', () => { - const { queryByRole } = render(ArticleDetailMeta, { - global: globalMountOptions, + cy.mount(ArticleDetailMeta, { props: { article: fixtures.article }, }) - expect(queryByRole('link', { name: editButton })).toBeInTheDocument() - expect(queryByRole('button', { name: followButton })).not.toBeInTheDocument() + cy.findByRole('link', { name: editButton }) + cy.findByRole('button', { name: followButton }).should('not.exist') }) it('should display follow button when user not author', () => { userStore.updateUser({ ...fixtures.user, username: 'user2' }) - const { queryByRole } = render(ArticleDetailMeta, { - global: globalMountOptions, + cy.mount(ArticleDetailMeta, { 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 }) }) it('should not display follow button and edit button when user not logged', () => { userStore.updateUser(null) - const { queryByRole } = render(ArticleDetailMeta, { - global: globalMountOptions, + cy.mount(ArticleDetailMeta, { props: { article: fixtures.article }, }) - expect(queryByRole('button', { name: editButton })).not.toBeInTheDocument() - expect(queryByRole('button', { name: followButton })).not.toBeInTheDocument() + cy.findByRole('link', { 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, + it('should call delete article service when click delete button', () => { + cy.intercept('DELETE', '/api/articles/*', { status: 200 }).as('deleteArticle') + cy.mount(ArticleDetailMeta, { 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', '/api/articles/article-foo') }) - it('should call follow service when click follow button', async () => { + it('should call follow service when click follow button', () => { + cy.intercept('POST', '/api/profiles/*/follow', { status: 200 }).as('followUser') userStore.updateUser({ ...fixtures.user, username: 'user2' }) - const { getByRole } = render(ArticleDetailMeta, { - global: globalMountOptions, + cy.mount(ArticleDetailMeta, { props: { article: fixtures.article }, }) - await fireEvent.click(getByRole('button', { name: followButton })) + cy.findByRole('button', { name: followButton }).click() - expect(mockFollowUser).toBeCalledWith('Author name') + cy.get('@followUser') + .its('request.url') + .should('contain', '/api/profiles/Author%20name/follow') }) - it('should call unfollow service when click follow button and not followed author', async () => { + it('should call unfollow service when click follow button and not followed author', () => { + cy.intercept('DELETE', '/api/profiles/*/follow', { status: 200 }).as('unfollowUser') userStore.updateUser({ ...fixtures.user, username: 'user2' }) - const { getByRole } = render(ArticleDetailMeta, { - global: globalMountOptions, + cy.mount(ArticleDetailMeta, { 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('@unfollowUser') + .its('request.url') + .should('contain', '/api/profiles/Author%20name/follow') }) - it('should call favorite article service when click favorite button', async () => { + 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' }) - const { getByRole } = render(ArticleDetailMeta, { - global: globalMountOptions, + cy.mount(ArticleDetailMeta, { 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', '/api/articles/article-foo/favorite') }) - it('should call favorite article service when click unfavorite button', async () => { + 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' }) - const { getByRole } = render(ArticleDetailMeta, { - global: globalMountOptions, + cy.mount(ArticleDetailMeta, { 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', '/api/articles/article-foo/favorite') }) }) diff --git a/src/components/ArticlesList.spec.ts b/src/components/ArticlesList.spec.ts index 93209ff..dfc8f30 100644 --- a/src/components/ArticlesList.spec.ts +++ b/src/components/ArticlesList.spec.ts @@ -1,32 +1,23 @@ -import { waitFor } from '@testing-library/vue' -import type { GlobalMountOptions } from '@vue/test-utils/dist/types' +import { createPinia, setActivePinia } from 'pinia' 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 { renderAsync } from '../utils/test/render-async' - -jest.mock('src/services/article/getArticles') +import { asyncWrapper } from 'src/utils/test/test.utils' describe('# ArticlesList', () => { - const globalMountOptions: GlobalMountOptions = { - plugins: [registerGlobalComponents, router], - } + const AsyncArticlesList = asyncWrapper(ArticlesList) + setActivePinia(createPinia()) - const mockFetchArticles = getArticles as jest.MockedFunction - - beforeEach(async () => { - mockFetchArticles.mockResolvedValue({ articles: [fixtures.article], articlesCount: 1 }) - await router.push('/') + beforeEach(() => { + cy.intercept('GET', '/api/articles*', { articles: [fixtures.article], articlesCount: 1 }).as('getArticles') }) - it('should render correctly', async () => { - const wrapper = renderAsync(ArticlesList, { - global: globalMountOptions, - }) + it('should render correctly', () => { + cy.mount(AsyncArticlesList) - expect(wrapper).toBeTruthy() - await waitFor(() => expect(mockFetchArticles).toBeCalledTimes(1)) + cy.wait('@getArticles') + + cy.contains(fixtures.article.title) + cy.contains('Article description') + cy.contains(fixtures.article.author.username) }) }) diff --git a/src/components/ArticlesListArticlePreview.spec.ts b/src/components/ArticlesListArticlePreview.spec.ts index d990216..517e6c1 100644 --- a/src/components/ArticlesListArticlePreview.spec.ts +++ b/src/components/ArticlesListArticlePreview.spec.ts @@ -1,32 +1,17 @@ -import { fireEvent, render } from '@testing-library/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, - }), -})) +const favoriteButton = 'Favorite article' 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, { - global: { plugins: [registerGlobalComponents, router] }, + it('should call favorite method when click favorite button', () => { + cy.intercept('POST', '/api/articles/*/favorite', { status: 200 }).as('favoriteArticle') + cy.mount(ArticlesListArticlePreview, { props: { article: fixtures.article }, }) - await fireEvent.click(getByRole('button', { name: favoriteButton })) + cy.findByRole('button', { name: favoriteButton }).click() - expect(mockFavoriteArticle).toBeCalledTimes(1) + cy.wait('@favoriteArticle') }) }) diff --git a/src/components/ArticlesListArticlePreview.vue b/src/components/ArticlesListArticlePreview.vue index 6315934..0f99420 100644 --- a/src/components/ArticlesListArticlePreview.vue +++ b/src/components/ArticlesListArticlePreview.vue @@ -3,14 +3,14 @@