前端项目添加 打包脚本 部署脚本 生成改动日志脚本

打包脚本

package.json 添加

1
2
3
// ..
"zip": "zip -r dist-$(git rev-parse --abbrev-ref HEAD)-$npm_package_name-$npm_package_version-$(date +'%Y_%m_%d').zip dist/ && open .",
"bz": "npm run build && npm run zip"

然后运行 npm run bz 就能直接打开打包好的zip包目录

阅读更多

cypress测试框架基础用法

E2E库 Cypress

端到端End to End (E2E)测试,将系统作为一个整体的测试方法

准备工作

首先有一个前端项目的打包文件,可以启动的后端服务,来做自动化e2e测试

安装配置

1
npm install --save-dev cypress # 将 Cypress 安装到前端 ,作为开发依赖项

配置package.json 添加脚本 cypress:opentest:e2e

1
2
3
4
5
6
7
8
9
10
11
12
13
{
// ..
"scripts": {
"start": "webpack-dev-server --open --mode development",
"start-prod": "node app.js",
"test": "jest",
"eslint": "eslint './**/*.{js,jsx}'",
"cypress:open": "cypress open",
"test:e2e": "cypress run",
"build": "webpack --mode production"
},
// ..
}

后端服务也可以添加启动指令 "start-prod": "node app.js"

app.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
const express = require('express')
const app = express()

// get the port from env variable
const PORT = process.env.PORT || 5001

app.use(express.static('dist'))

app.listen(PORT, () => {
// eslint-disable-next-line
console.log(`server started on port ${PORT}`, `http://localhost:${PORT}`)
})

ps: 对于 const express = require('express')的eslint报错,可以在.eslintrc.js中添加配置

1
2
3
4
5
6

'env': {
// ..
'node': true,
'commonjs': true,
},

.gitignore 也可把视频添加忽略 cypress/videos

首次启动

首次启动cypress 项目根目录会多一个 cypress 文件夹和一个配置文件 cypress.config.js

1
npm run cypress:open

cypress.config.js:

1
2
3
4
5
6
7
8
9
10
11
12
// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
e2e: {
// eslint-disable-next-line
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
})

目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  my-proj git:(main) ✗ tree -L 2 
.
├── README.md
├── app.js
├── cypress
│ ├── e2e
│ ├── fixtures
│ ├── screenshots
│ └── support
├── cypress.config.js
├── dist
│ ├── bundle.js
│ ├── bundle.js.LICENSE.txt
│ └── index.html
# ..

第一次Cypress会在 integration/examples 目录中创建测试示例 可以删了examples文件夹

cy 导致的 eslint 风格报错

报错大概这样

1
2
3
4
5
6
7
8
9
10
11
12
13
 *  正在执行任务: npm run eslint 


> my-proj@1.0.0 eslint
> eslint './**/*.{js,jsx}'

Warning: React version not specified in eslint-plugin-react settings. See https://github.com/jsx-eslint/eslint-plugin-react#configuration .

/Users/weimo/mine/code/my-proj/cypress/e2e/app.cy.js
3:5 error 'cy' is not defined no-undef
4:5 error 'cy' is not defined no-undef
5:5 error 'cy' is not defined no-undef
# ..

我们可以通过安装eslint-plugin-cypress作为开发依赖项来摆脱这个报错

1
npm install eslint-plugin-cypress --save-dev

改变 .eslintrc.js中的配置 (vscode安装了eslint拓展需要修改setting.json)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module.exports = {
"env": {
"browser": true,
"es6": true,
"jest/globals": true,
"cypress/globals": true
},
"extends": [
// ...
],
"parserOptions": {
// ...
},
"plugins": [
"react", "jest", "cypress"
],
"rules": {
// ...
}
}

编写运行测试

添加文件 cypress/e2e/app.cy.js:

1
2
3
4
5
6
7
describe('Pokedex', function() {
it('front page can be opened 主页应该能打开', function() {
cy.visit('http://localhost:5001')
cy.contains('ivysaur') // 代码是全小写的,页面由css控制显示为首字大写,所以这里小写
cy.contains('Pokémon and Pokémon character names are trademarks of Nintendo.')
})
})

启动测试

启动服务 运行目前的测试是测试5001端口

可以运行 npm run cypress:open 后使用交互界面看结果:

Cypress demo

也可以运行 npm run test:e2e 后在控制台看到结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
正在执行任务: npm run test:e2e 


> my-proj@1.0.0 test:e2e
> cypress run


DevTools listening on ws://127.0.0.1:55305/devtools/browser/25c805c0-5b4b-4250-9c9e-6cc43f8184a8
2024-06-20 17:58:23.038 Cypress[4588:4056456] WARNING: Secure coding is not enabled for restorable state! Enable secure coding by implementing NSApplicationDelegate.applicationSupportsSecureRestorableState: and returning YES.
Couldn't find tsconfig.json. tsconfig-paths will be skipped

====================================================================================================

(Run Starting)

┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Cypress: 13.12.0 │
│ Browser: Electron 118 (headless) │
│ Node Version: v16.20.2 (/Users/weimo/.nvm/versions/node/v16.20.2/bin/node) │
│ Specs: 1 found (app.cy.js) │
│ Searched: cypress/e2e/**/*.cy.{js,jsx,ts,tsx} │
└────────────────────────────────────────────────────────────────────────────────────────────────┘


────────────────────────────────────────────────────────────────────────────────────────────────────

Running: app.cy.js (1 of 1)


Pokedex
1) front page can be opened 主页应该能打开

详情页测试
✓ 主页有charmander小火龙入口 (273ms)
✓ 点击小火龙详情页应该显示详情 (1012ms)
✓ 进入小火龙详情页后,能够进入前一个详情页 (1514ms)
✓ 进入小火龙详情页后,能够进入下一个详情页 (912ms)
✓ 进入小火龙详情页后,能够返回主页 (855ms)
# ..
(Screenshots)

- /Users/weimo/mine/code/my-proj/cypress/screenshots/app.cy.js/Pok (1280x720)
edex -- front page can be opened 主页应该能打开 (failed).png


====================================================================================================

(Run Finished)


Spec Tests Passing Failing Pending Skipped
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ✖ app.cy.js 01:05 6 5 1 - - │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
✖ 1 of 1 failed (100%) 01:05 6 5 1 - -


* 终端进程“/bin/zsh '-i', '-c', 'npm run test:e2e'”已终止,退出代码: 1。
* 终端将被任务重用,按任意键关闭。

一个测试示例文件

文档 https://docs.cypress.io/

查找内容 cy.contains() cy.get() cy.find() cy.should() 点击按钮 检测数据变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
describe('blog app',function () {

beforeEach(function () {
cy.request('POST','http://localhost:3003/api/testing/reset')
const user = {
name: 'DASB',
username: 'sb',
password: '123456'
}
cy.request('POST','http://localhost:3003/api/users', user)
cy.visit('http://localhost:3000')
})


it('can open site 可以打开网页',function () {
cy.visit('http://localhost:3000')
cy.contains('blogs')
})

it('and its have login form 显示登录表单',function () {
cy.contains('username')
cy.contains('password')
})

describe('login 登录测试',function () {

it('can not log in with incorrect credentials 错误信息不可以登录',function () {
cy.get('#username-login').type('dasb')
cy.get('#password-login').type('dasb')
cy.get('#login-button').click()

cy.contains('error')
})

it('can log in with correct info 正确信息可以登录',function () {
cy.get('#username-login').type('sb')
cy.get('#password-login').type('123456')
cy.get('#login-button').click()

cy.contains('success')
})

})

describe('When logged in', function() {
beforeEach(function () {
cy.login({ username: 'sb', password: '123456' })
})

it('A blog can be created', function() {
cy.contains('create a new blog').click()
cy.get('#title').type('标题')
cy.get('#author').type('作者')
cy.get('#url').type('链接')
cy.get('#likes').type('666')
cy.contains('submit').click()
cy.contains('标题 作者')
})

it('open show details and click like!', function () {

cy.createBlog({ title:'Test', author:'T est',url:'hppp:%%test@tset.com',likes:999 })

cy.contains('Test').find('button:first').as('theShowDetailButton') // 找到show detail按钮
// 然后我们点击按钮,检查上面的文本是否改变
cy.get('@theShowDetailButton').click()
cy.get('@theShowDetailButton').should('contain', 'hide detail')

cy.get('#add-likes-button').as('theLikeButton') // 找到show detail按钮
cy.contains(999)
cy.get('@theLikeButton').click()
cy.contains(1000)

})


it('blog can be deleted',function () {

cy.createBlog({ title:'this', author:'will',url:'remove',likes:0 })

cy.get('#show-detail-button').as('theShowDetailButton')
cy.get('@theShowDetailButton').click()
cy.contains('this')
cy.contains('will')
cy.contains('remove')

cy.get('#delete-button').click()
cy.contains('delete blog')
cy.should('not.contain','this')
cy.should('not.contain','will')
cy.should('not.contain','remove')

})

describe('the order is correct order by likes',function () {

beforeEach(function () {

cy.createBlog({ title:'first', author:'hehe',url:'haha',likes:0 })
cy.createBlog({ title:'second', author:'hehe',url:'xxxx',likes:666 })
cy.createBlog({ title:'third', author:'hehe',url:'xxxx',likes:888 })
cy.createBlog({ title:'fourth', author:'hehe',url:'xxxx',likes:888 })
cy.createBlog({ title:'fifth', author:'hehe',url:'xxxx',likes:111 })
cy.createBlog({ title:'sixth', author:'will',url:'remove',likes:1001 })
})

it('have six blogs in correct order',function () {

// open all details
cy.get('#blog-list div').find('button:first').click({ multiple: true })

// blogList should in correct order
cy.get('#blog-list div')
.first().should('contain','sixth').should('contain',1001)
.next().should('contain','third').should('contain',888)
.next().should('contain','fourth').should('contain',888)
.next().should('contain','second').should('contain',666)
.next().should('contain','fifth').should('contain',111)
.next().should('contain','first').should('contain',0)

})

it('click fourth blog like! button, then the order change',function () {
// open all details
cy.get('#blog-list div').find('button:first').click({ multiple: true })

cy.get('#blog-list div')
.first()
.next()
.next().should('contain','fourth').should('contain',888).find('#add-likes-button').click()

cy.get('#blog-list div')
.first()
.next()
.next().should('contain','fourth').should('contain',889)
})

// describe('click like on fourth blog and check order',function () {

// beforeEach('click like',function () {

// cy.get('#blog-list div').find('button:first').click({ multiple: true })

// cy.get('#blog-list div')
// .first()
// .next()
// .next().should('contain','fourth').should('contain',888).find('#add-likes-button').click()
// })

// it('the order in correct',function () {
// cy.get('#blog-list div')
// .first()
// .next().should('contain','fourth').should('contain',889)

// })
// })

})
})
})

:src动态导入图片路径

对于webpack

1
const img = require(`./img/${name}.png`)
1
<img :src="img" />

或者直接在html中使用

1
2
<img :src="require(`./img/${name}.png`)" />
<img :src="require(`@/img/${name}.png`)" />

对于vite

1
2
// ..
activeIcon: new URL('@/assets/icon/intelligent-contro.png', import.meta.url).href,
1
2
3
4
5
<img
:src="item.activeIcon"
height="40"
alt=""
>

前端开发切换环境

本质就是判断是生产环境还是开发环境,或者测试环境的切换,需要对的变量进行修改

一般通过环境变量、配置文件、代理来实现切换

阅读更多