Merge branch 'master' into script-setup

This commit is contained in:
mutoe 2021-02-28 22:52:11 +08:00
commit bf37587cbb
23 changed files with 507 additions and 479 deletions

View File

@ -26,7 +26,8 @@
"files": [ "src/**/*.spec.ts" ],
"rules": {
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "off"
"@typescript-eslint/no-unnecessary-type-assertion": "off",
"@typescript-eslint/no-explicit-any": "off"
}
}
]

View File

@ -36,6 +36,7 @@ For more information on how to this works with other frontends/backends, head ov
- [x] [Script setup sugar](https://github.com/vuejs/rfcs/blob/sfc-improvements/active-rfcs/0000-sfc-script-setup.md)
- [ ] [Script ref sugar](https://github.com/vuejs/rfcs/blob/ref-sugar/active-rfcs/0000-ref-sugar.md)
- [x] Unit test ([Vue Testing Library](https://testing-library.com/docs/vue-testing-library/intro))
- [ ] Vetur Tools [![Vetur#2296](https://img.shields.io/github/issues/detail/state/vuejs/vetur/2296?label=vetur%232296&logo=github&style=flat-square)](https://github.com/vuejs/vetur/issues/2296)
> _[Why we have the second branch?](https://github.com/mutoe/vue3-realworld-example-app/commit/c0c983dba08cb31fc96bbc3eb7f15faf469d0624#commitcomment-47600736)_

7
cypress/constants.js Normal file
View File

@ -0,0 +1,7 @@
export const ROUTES = {
HOME: '#/',
LOGIN: '#/login',
REGISTER: '#/register',
SETTINGS: '#/settings',
ARTICLE: '#/article/article-title',
}

View File

@ -1,19 +1,19 @@
{
"article": {
"title": "статья",
"slug": "-45l18w",
"body": "уцауцауц",
"createdAt": "2020-11-01T14:59:39.404Z",
"updatedAt": "2020-11-01T14:59:39.404Z",
"tagList": [],
"description": "цуцуа",
"author": {
"username": "sdfsdfsadsdfsdf",
"bio": null,
"image": "https://static.productionready.io/images/smiley-cyrus.jpg",
"following": false
},
"favorited": false,
"favoritesCount": 0
}
}
"article": {
"title": "Article title",
"slug": "article-title",
"body": "# Article body\n\nThis is **Strong** text",
"createdAt": "2020-11-01T14:59:39.404Z",
"updatedAt": "2020-11-01T14:59:39.404Z",
"tagList": [],
"description": "this is descripion",
"author": {
"username": "plumrx",
"bio": null,
"image": "https://static.productionready.io/images/smiley-cyrus.jpg",
"following": false
},
"favorited": true,
"favoritesCount": 0
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
{
"profile": {
"username": "ma0dong",
"bio": null,
"image": "https://static.productionready.io/images/smiley-cyrus.jpg",
"following": false
}
}

View File

@ -1,12 +0,0 @@
{
"user": {
"id": 122907,
"email": "plumrx@qq.com",
"createdAt": "2020-11-04T14:43:31.622Z",
"updatedAt": "2020-11-04T14:43:31.628Z",
"username": "plumrx",
"bio": null,
"image": null,
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MTIyOTA3LCJ1c2VybmFtZSI6InBsdW1yeDIiLCJleHAiOjE2MDk2ODUwMTF9.A-gAjGYC9u7_mwjNK61HqeR3jwrFPN-UDvUnMjyJfBo"
}
}

View File

@ -0,0 +1,12 @@
{
"user": {
"id": 71965,
"email": "plumrx@example.com",
"createdAt": "2019-10-22T03:22:54.038Z",
"updatedAt": "2020-10-20T15:02:49.629Z",
"username": "plumrx",
"bio": "aaa22",
"image": null,
"token": "fake_token"
}
}

View File

@ -1,17 +0,0 @@
{
"loginPass": {
"username": "plumrx1",
"email": "plumrx1@qq.com",
"password": "12345678"
},
"loginFailed": {
"username": "plumrx2",
"email": "plumrx2@qq.com",
"password": "12345678"
},
"registered":{
"username": "plumrx",
"email": "plumrx@qq.com",
"password": "12345678"
}
}

View File

@ -0,0 +1,63 @@
import { ROUTES } from '../constants'
describe('Article', () => {
beforeEach(() => {
cy.intercept('GET', /articles\/\S+$/, { fixture: 'article.json' })
cy.intercept('GET', /articles\?/, { fixture: 'articles.json' })
cy.intercept('GET', /tags$/, { fixture: 'articles_of_tag.json' })
cy.intercept('GET', /profiles\/\S+/, { fixture: 'profile.json' })
cy.intercept('DELETE', /articles\/\S+$/, { statusCode: 200, body: {} }).as('deleteArticle')
})
describe('post article', () => {
before(() => {
cy.login()
cy.visit('/')
})
it('jump to post detail page when submit create article form', () => {
cy.intercept('POST', /articles$/, { fixture: 'article.json' })
cy.get('[href="#/article/create"]').click()
cy.get('[placeholder="Article Title"]').type('Title')
cy.get('[placeholder="What\'s this article about?"]').type('content')
cy.get('[placeholder="Write your article (in markdown)"]').type('## test')
cy.get('[placeholder="Enter tags"]').type('butt')
cy.get('[type="submit"]').click()
cy.url()
.should('contain', 'article/article-title')
cy.get('.container > h1')
.should('contain', 'Article title')
})
it('should render markdown correctly', () => {
cy.get('.article-content').within(() => {
cy.get('h1')
.should('contain', 'Article body')
cy.get('strong')
.should('contain', 'Strong')
})
})
})
describe('delete article', () => {
before(() => {
cy.login()
cy.visit(ROUTES.ARTICLE)
})
it('delete article', () => {
cy.get('.article-actions button.btn-outline-danger')
.contains('Delete Article')
.click()
cy.wait('@deleteArticle')
cy.get('.home-page p')
.should('contain', 'A place to share your knowledge.')
})
})
})

View File

@ -0,0 +1,85 @@
import { ROUTES } from '../constants'
describe('Auth', () => {
describe('Login and logout', () => {
it('should login success when submit a valid login form', () => {
cy.login()
cy.url().should('match', /\/#\/$/)
})
it('should logout when click logout button', () => {
cy.get(`[href="${ROUTES.SETTINGS}"]`).click()
cy.get('button.btn-outline-danger')
.contains('logout')
.click()
cy.get('ul.navbar-nav')
.should('contain', 'Sign in')
.should('contain', 'Sign up')
})
it('should display error when submit an invalid form (password not match)', () => {
cy.intercept('POST', /users\/login/, {
statusCode: 422,
body: { errors: { 'email or password': ['is invalid'] } },
})
cy.visit(ROUTES.LOGIN)
cy.get('[type="email"]').type('foo@example.com')
cy.get('[type="password"]').type('12345678')
cy.get('[type="submit"]').click()
cy.contains('email or password is invalid')
})
it('should display format error without API call when submit an invalid format', () => {
cy.intercept('POST', /users\/login/).as('loginRequest')
cy.visit(ROUTES.LOGIN)
cy.get('[type="email"]').type('foo')
cy.get('[type="password"]').type('123456')
cy.get('[type="submit"]').click()
cy.get('form').then(([$el]) => {
cy.wrap($el.checkValidity()).should('to.be', false)
})
})
})
describe('Register', () => {
it('should call register API and jump to home page when submit a valid form', () => {
cy.intercept('POST', /users$/, { fixture: 'user.json' }).as('registerRequest')
cy.visit(ROUTES.REGISTER)
cy.get('[placeholder="Your Name"]').type('foo')
cy.get('[placeholder="Email"]').type('foo@example.com')
cy.get('[placeholder="Password"]').type('12345678')
cy.get('[type="submit"]').click()
cy.wait('@registerRequest')
cy.url().should('match', /\/#\/$/)
})
it('should display error message when submit the form that username already exist', () => {
cy.intercept('POST', /users$/, {
statusCode: 422,
body: { errors: { email: ['has already been taken'], username: ['has already been taken'] } },
}).as('registerRequest')
cy.visit(ROUTES.REGISTER)
cy.get('[placeholder="Your Name"]').type('foo')
cy.get('[placeholder="Email"]').type('foo@example.com')
cy.get('[placeholder="Password"]').type('12345678')
cy.get('[type="submit"]').click()
cy.wait('@registerRequest')
cy.contains('email has already been taken')
cy.contains('username has already been taken')
})
})
})

View File

@ -0,0 +1,30 @@
import { ROUTES } from '../constants'
describe('Favorite', () => {
beforeEach(() => {
cy.intercept('GET', /articles\?/, { fixture: 'articles.json' }).as('getArticles')
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')
})
it('should jump to login page when click favorite article button given user not logged', () => {
cy.intercept('POST', /articles\/\S+\/favorite$/, { statusCode: 401, body: {} }).as('favoriteArticle')
cy.visit(ROUTES.HOME)
cy.get('i.ion-heart:first').click()
cy.url().should('contain', 'login')
})
it('should call favorite api and highlight favorite button when click favorite button', () => {
cy.intercept('POST', /articles\/\S+\/favorite$/, { fixture: 'article.json' }).as('favoriteArticle')
cy.login()
cy.visit(ROUTES.HOME)
// like articles
cy.get('i.ion-heart:first').click()
cy.wait('@favoriteArticle')
cy.get('.article-meta:first button')
.should('have.class', 'btn-primary')
})
})

View File

@ -0,0 +1,39 @@
import { ROUTES } from '../constants'
describe.only('Follow', () => {
beforeEach(() => {
cy.intercept('GET', /articles\?/, { fixture: 'articles.json' }).as('getArticles')
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')
cy.fixture('article.json').then(article => {
article.article.author.username = 'foo'
cy.intercept('GET', /articles\/\S+/, { statusCode: 200, body: article }).as('getArticle')
})
})
it('should not display follow button when user not logged', () => {
cy.visit(ROUTES.ARTICLE)
cy.get('body').should('contain', ' to add comments on this article. ')
cy.get('.article-meta button.space:first')
.should('not.contain.text', 'Follow')
})
it('should call follow user api when click follow user button', () => {
cy.fixture('profile.json').then(profile => {
profile.profile.following = true
cy.intercept('POST', /profiles\/\S+\/follow/, { statusCode: 200, body: profile }).as('followUser')
})
cy.login()
cy.visit(ROUTES.ARTICLE)
// follow author
cy.get('.article-meta button.btn-outline-secondary')
.contains('Follow')
.click()
cy.wait('@followUser')
cy.get('.article-actions button.btn-outline-secondary:last')
.should('contain', 'Unfollow')
})
})

View File

@ -1,26 +1,27 @@
describe('View the homepage', () => {
beforeEach(() => {
cy.intercept('GET', /articles\?tag=butt/, { fixture: 'article_of_tag' }).as('article_of_tag')
cy.intercept('GET', /articles/, { fixture: 'articles.json' }).as('getArticles')
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')
import { ROUTES } from '../constants'
cy.visit('/')
describe('Homepage', () => {
beforeEach(() => {
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')
})
it('should can access home page', () => {
cy.visit(ROUTES.HOME)
cy.wait('@getArticles')
cy.get('h1.logo-font')
.should('contain.text', 'conduit')
})
it('should highlight Global Feed when home page loaded', () => {
cy.get('.feed-toggle > .nav')
cy.get('.articles-toggle > .nav')
.contains('Global Feed')
.should('have.class', 'active')
})
it('should display article when page loaded', () => {
cy.wait('@getArticles')
cy.get('.article-preview:first')
.find('h1')
.should('contain.text', 'abc123')
@ -35,24 +36,31 @@ describe('View the homepage', () => {
.contains(' to add comments on this article. ')
})
it('should highlight Home nav-item when page load', () => {
it('should highlight Home nav-item top menu bar when page load', () => {
cy.visit(ROUTES.HOME)
cy.wait('@getArticles')
cy.get('ul.nav.navbar-nav.pull-xs-right a.nav-link')
.contains('Home')
.should('have.class', 'active')
})
it.only('turn up the page', () => {
cy.wait('@getArticles')
it('should jump to next page when click page 2 in pagination', () => {
cy.get('ul.pagination li.page-item:nth-child(2) a.page-link')
.click()
cy.wait('@getArticles')
.then(console.log)
.its('request.url')
.should('contain', 'limit=10&offset=10')
})
// it('',()=>{
it('should display popular tags in home page', () => {
cy.visit(ROUTES.HOME)
cy.wait('@getTags')
// })
cy.contains('Popular Tags')
.parent('.sidebar')
.find('.tag-pill')
.should('have.length', 8)
})
})

View File

@ -1,73 +0,0 @@
describe('test for like-follow', () => {
beforeEach(() => {
cy.intercept('GET', /articles\?tag=butt/, { fixture: 'article_of_tag' }).as('article_of_tag')
cy.intercept('GET', /articles\?/, { fixture: 'articles.json' }).as('getArticles')
cy.intercept('GET', /articles\//, { fixture: 'article.json' }).as('getArticle')
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')
cy.visit('/')
})
it('no-login:like articles', () => {
cy.get('i.ion-heart:first')
.click()
cy.url()
.should('contain', 'login')
cy.get('h1.text-xs-center')
.should('contain.text', 'Sign in')
})
it('login:like articles', () => {
// login
cy.fixture('users.json').then(users => {
cy.login(users.loginPass)
})
// like articles
cy.get('i.ion-heart:first')
.click()
cy.get('.article-meta:first button')
.should('have.class', 'btn-primary')
})
it('no-login:follow author', () => {
cy.get('.article-preview:first')
.click()
cy.url()
.should('contain', 'article')
cy.get('body')
.should('contain', ' to add comments on this article. ')
cy.get('.article-meta button.space:first')
.should('contain.text', ' Follow')
cy.get('.article-meta button.space:first')
.click()
cy.url()
.should('contain', 'login')
})
it('login: follow author', () => {
// login
cy.fixture('users.json').then(users => {
cy.login(users.loginPass)
})
// click article
cy.get('a.preview-link:first span')
.contains('Read more...')
.click()
// follow author
cy.get('.article-meta button.btn-outline-secondary')
.should('contain', 'Follow')
cy.get('.article-meta button.btn-outline-secondary:first')
.click()
cy.get('.article-actions button.btn-outline-secondary:last')
.should('contain', 'Unfollow')
})
})

View File

@ -1,27 +0,0 @@
describe('test for login', () => {
beforeEach(() => {
cy.intercept('GET', /articles\?tag=butt/, { fixture: 'article_of_tag' }).as('article_of_tag')
cy.intercept('GET', /articles\?/, { fixture: 'articles.json' }).as('getArticles')
cy.intercept('GET', /articles\//, { fixture: 'article.json' }).as('getArticle')
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')
cy.visit('/')
})
it('login and logout in home page', () => {
// login
cy.fixture('users.json').then(users => {
cy.login(users.loginPass)
})
// logout
cy.get('[href="#/settings"]')
.click()
cy.get('button.btn-outline-danger')
.contains('logout')
.click()
cy.get('ul.navbar-nav')
.should('contain', ' Sign in')
.should('contain', ' Sign up')
})
})

View File

@ -1,42 +0,0 @@
describe('test for login', () => {
before(() => {
cy.visit('/')
// login
cy.fixture('users.json').then(users => {
cy.login(users.loginPass)
})
})
it('post article', () => {
// post article
cy.get('[href="#/article/create"]')
.click()
cy.get('[placeholder="Article Title"]')
.type('tilte')
cy.get('[placeholder="What\'s this article about?"]')
.type('content')
cy.get('[placeholder="Write your article (in markdown)"]')
.type('## test')
cy.get('[placeholder="Enter tags"]')
.type('butt')
cy.get('[type="submit"]')
.click()
cy.get('.article-page .banner h1')
.should('contain', 'tilte')
})
it('delete article', () => {
// delete article
cy.get('.article-actions button.btn-outline-danger')
.click()
cy.get('.home-page p')
.should('contain', 'A place to share your knowledge.')
})
})

View File

@ -1,20 +0,0 @@
describe('test for register', () => {
beforeEach(() => {
cy.intercept('POST', /users/, { fixture: 'register.json' }).as('register')
cy.intercept('GET', /articles\?tag=butt/, { fixture: 'article_of_tag' }).as('article_of_tag')
cy.intercept('GET', /articles\?/, { fixture: 'articles.json' }).as('getArticles')
cy.intercept('GET', /articles\//, { fixture: 'article.json' }).as('getArticle')
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')
cy.visit('/')
})
it('ligin in home page', () => {
// click logup button in home page
cy.fixture('users.json').then(users => {
cy.register(users.registered)
cy.get('.navbar')
.should('contain', 'plumrx')
})
})
})

View File

@ -1,13 +1,14 @@
describe('test for tag', () => {
beforeEach(() => {
cy.intercept('GET', /articles\?tag=butt/, { fixture: 'article_of_tag' }).as('article_of_tag')
cy.intercept('GET', /articles/, { fixture: 'articles.json' }).as('getArticles')
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')
import { ROUTES } from '../constants'
cy.visit('/')
describe('Tag', () => {
beforeEach(() => {
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')
})
it('it should display correct tags when page loaded', () => {
it('should display correct tags when page loaded', () => {
cy.visit(ROUTES.HOME)
cy.wait('@getTags')
cy.get('div.tag-list')
@ -20,9 +21,8 @@ describe('test for tag', () => {
})
it('should show right articles of tag', () => {
// 点击最后一个tag
cy.get('a.tag-pill.tag-default:last')
.click()
cy.get('a.tag-pill.tag-default:last').click()
cy.wait('@getArticlesOfTag')
cy.get('a.tag-pill.tag-default:last')
.should('have.class', 'router-link-active')
@ -31,7 +31,6 @@ describe('test for tag', () => {
cy.get('a.tag-pill.tag-default:last').invoke('text').then(tag => {
const path = `#/tag/${tag}`
// check URL
cy.url()
.should('include', path)
@ -40,14 +39,4 @@ describe('test for tag', () => {
.should('contain', tag)
})
})
it('check articles tag including butt', () => {
// 点击最后一个tag
cy.get('a.tag-pill.tag-default:last')
.click()
// 文章标签元素
cy.get('.article-preview ul.tag-list')
.should('have.length', 9)
})
})

View File

@ -7,55 +7,22 @@
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
Cypress.Commands.add('login', (user) => {
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.get('li.nav-item a.nav-link')
.contains(' Sign in')
.click()
cy.visit(ROUTES.LOGIN)
cy.get('[type="email"]')
.type(user.email)
cy.get('[type="password"]')
.type(user.password)
cy.get('[type="submit"]')
.contains(' Sign in ')
.click()
cy.get('[type="email"]').type('foo@example.com')
cy.get('[type="password"]').type('12345678')
cy.get('[type="submit"]').contains('Sign in').click()
cy.get('h1.logo-font')
.should('contain', ' conduit ')
cy.get('li.nav-item:last')
.should('contain.text', user.username)
})
Cypress.Commands.add('register', (user) => {
// click sign up button in home page
cy.get('li.nav-item a.nav-link')
.contains(' Sign up')
.click()
// []属性选择器
cy.get('[placeholder="Your Name"]')
.type(user.username)
cy.get('[placeholder="Email"]')
.type(user.email)
cy.get('[placeholder="Password"]')
.type(user.password)
cy.get('[type="submit"]')
.click()
.should('contain.text', username)
})

9
cypress/support/index.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
// in cypress/support/index.d.ts
// load type definitions that come with Cypress module
/// <reference types="cypress" />
declare namespace Cypress {
interface Chainable {
login(): void
}
}

View File

@ -5,7 +5,7 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"lint:script": "eslint \"{src,cypress}/**/*.{js,ts,vue}\"",
"lint:script": "eslint \"{src/**/*.{ts,vue},cypress/**/*.js}\"",
"lint:vti": "vti diagnostics",
"lint": "concurrently 'yarn tsc' 'yarn lint:script' 'yarn lint:vti'",
"test:unit": "jest --coverage",