@ -0,0 +1,16 @@ |
|||
module.exports = { |
|||
root: true, |
|||
env: { |
|||
node: true, |
|||
}, |
|||
extends: ['plugin:vue/vue3-essential', '@vue/standard', '@vue/typescript/recommended'], |
|||
parserOptions: { |
|||
ecmaVersion: 2020, |
|||
}, |
|||
rules: { |
|||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', |
|||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', |
|||
'space-before-function-paren': ['error', 'never'], |
|||
'comma-dangle': ['error', 'only-multiline'], |
|||
}, |
|||
} |
@ -0,0 +1,33 @@ |
|||
.DS_Store |
|||
node_modules |
|||
dist |
|||
dist-ssr |
|||
|
|||
/tests/e2e/videos/ |
|||
/tests/e2e/screenshots/ |
|||
/instrumented |
|||
/coverage |
|||
.nyc_output |
|||
|
|||
# local env files |
|||
.env |
|||
.env.local |
|||
.env.*.local |
|||
|
|||
# Log files |
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
|||
pnpm-debug.log* |
|||
|
|||
# Editor directories and files |
|||
.idea |
|||
.vscode |
|||
*.suo |
|||
*.ntvs* |
|||
*.njsproj |
|||
*.sln |
|||
*.sw? |
|||
|
|||
*.local |
|||
LOCAL_NOTES.md |
@ -0,0 +1,38 @@ |
|||
image: node:14 |
|||
|
|||
stages: |
|||
- test |
|||
|
|||
variables: |
|||
npm_config_cache: '$CI_PROJECT_DIR/.npm' |
|||
CYPRESS_CACHE_FOLDER: '$CI_PROJECT_DIR/cache/Cypress' |
|||
YARN_CACHE_FOLDER: '/root/.cache/yarn' |
|||
|
|||
cache: |
|||
paths: |
|||
- /root/.cache/ |
|||
- cache/Cypress |
|||
- $CI_PROJECT_DIR/node_modules/ |
|||
key: ${CI_COMMIT_REF_SLUG} |
|||
|
|||
test: |
|||
stage: test |
|||
image: |
|||
name: cypress/included:7.0.0 |
|||
entrypoint: [''] |
|||
script: |
|||
# Uncomment for debugging |
|||
# - export |
|||
- yarn cache dir |
|||
- cd -- "$CI_PROJECT_DIR" |
|||
- yarn --frozen-lockfile |
|||
- yarn start |
|||
- yarn test:ci |
|||
coverage: '/Lines\s+:\s(\d+.?\d+)%/' |
|||
artifacts: |
|||
paths: |
|||
- '$CI_PROJECT_DIR/tests/e2e/videos/*.mp4' |
|||
- '$CI_PROJECT_DIR/coverage' |
|||
expire_in: 2 weeks |
|||
when: on_failure |
|||
timeout: 15m |
@ -0,0 +1,7 @@ |
|||
{ |
|||
"trailingComma": "es5", |
|||
"tabWidth": 2, |
|||
"semi": false, |
|||
"singleQuote": true, |
|||
"printWidth": 100 |
|||
} |
@ -0,0 +1,109 @@ |
|||
# Changelog |
|||
|
|||
## 1.1.2 (2021-04-08) |
|||
- Fix: fixed Cypress version in GitLab CI conf. |
|||
|
|||
## 1.1.1 (2021-04-07) |
|||
- Chore: bumped deps. |
|||
|
|||
## 1.1.0 (2021-03-16) |
|||
- Feat: added [Tailwind JIT compiler](https://github.com/tailwindlabs/tailwindcss-jit). |
|||
- Chore: upgraded Vite to 2.1. |
|||
- Chore: bumped deps. |
|||
|
|||
## 1.0.2 (2021-03-10) |
|||
- Chore: bumped deps. |
|||
|
|||
## 1.0.1 (2021-03-01) |
|||
- Chore: bumped deps. |
|||
|
|||
## 1.0.0 (2021-02-22) |
|||
|
|||
- Feat: added an example of `_APP_VERSION` using version from `package.json` |
|||
- Docs: tweaked README. |
|||
- Chore: upgraded Tailwind from PostCSS 7 compatibility build to latest PostCSS 8. |
|||
- Chore: upgraded Vite from beta to latest 2.0. |
|||
- Chore: changed dependabot interval to monthly. |
|||
|
|||
## 0.9.0 (2021-02-08) |
|||
|
|||
- Feat: added a fully typed example of Vuex 4 store (It's much easier to strip out if you don't need it than to build manually every time you do.) |
|||
- Feat: added `@tailwindcss/line-clamp` plugin. |
|||
- Feat: added `_APP_VERSION` constant that can be defined build time. |
|||
- Refactor: moved to the new [script setup syntax](https://github.com/vuejs/rfcs/pull/227). (At the time of this change Vetur doesn't understand this syntax yet so you might want to use Volar instead.) |
|||
- Chore: bumped deps. |
|||
|
|||
## 0.8.0 (2021-01-28) |
|||
|
|||
- Feat: added an alias for referring `<project_root>/src` with just `@`. |
|||
- Chore: bumped deps. |
|||
|
|||
## 0.7.0 (2021-01-21) |
|||
|
|||
- Feat: upgraded Cypress and GitLab Cypress image to 6.3.0. |
|||
- Feat: added [postcss-nesting](https://github.com/jonathantneal/postcss-nesting) plugin. |
|||
- Chore: bumped deps. |
|||
|
|||
## 0.6.2 (2021-01-18) |
|||
|
|||
- Chore: bumped deps. |
|||
|
|||
## 0.6.1 (2021-01-08) |
|||
|
|||
- Fix: moved non-browser packages to dev dependencies. |
|||
- Chore: bumped deps. |
|||
|
|||
## 0.6.0 (2021-01-07) |
|||
|
|||
- Feat: upgraded Vite to 2.0 Beta. |
|||
- Fix: fixed GitHub actions. |
|||
|
|||
## 0.5.3 (2020-12-28) |
|||
|
|||
- Chore: bumped deps. |
|||
|
|||
## 0.5.2 (2020-12-21) |
|||
|
|||
- Chore: bumped deps. |
|||
|
|||
## 0.5.1 (2020-12-15) |
|||
|
|||
- Chore: bumped deps. |
|||
|
|||
## 0.5.0 (2020-12-07) |
|||
|
|||
- Feat: added `vite.config.js`. |
|||
- Chore: bumped deps. |
|||
- Fix: repackaged TailwindCSS compatibility version as per new docs. |
|||
|
|||
## 0.4.2 (2020-11-23) |
|||
|
|||
- Chore: bumped deps. |
|||
|
|||
## 0.4.1 (2020-11-19) |
|||
|
|||
- Fix: fixed Tailwind package. |
|||
|
|||
## 0.4.0 (2020-11-18) |
|||
|
|||
- Upgraded to Tailwind 2.0 |
|||
- Added `@tailwindcss/aspect-ratio` and `@tailwindcss/forms` plugins |
|||
|
|||
## 0.3.0 (2020-11-17) |
|||
|
|||
- Added pre-configured GitLab CI, moved project to GitLab |
|||
- Renamed master branch to trunk |
|||
- Bumped deps |
|||
|
|||
## 0.2.0 (2020-10-24) |
|||
|
|||
- Added commented out tailwind UI config |
|||
|
|||
## 0.1.0 (2020-09-28) |
|||
|
|||
- Added changelog |
|||
- Added Cypress.io tests |
|||
|
|||
## 0.0.0 (2020-09-15) |
|||
|
|||
- Initial version |
@ -0,0 +1,21 @@ |
|||
MIT License |
|||
|
|||
Copyright (c) 2020 Ville Säävuori |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in all |
|||
copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
SOFTWARE. |
@ -0,0 +1,60 @@ |
|||
# Vite Typescript + Tailwind Starter |
|||
|
|||
Simple, opinionated, and **production-ready** project template for Vite. |
|||
|
|||
See [changelog](./CHANGES.md) for latest changes. |
|||
|
|||
## Features |
|||
|
|||
- Vue 3 |
|||
- Vuex 4 store (fully typed) |
|||
- TypeScript |
|||
- Tailwind CSS w/ JIT compiler + following plugins preinstalled |
|||
- `@tailwindcss/aspect-ratio` |
|||
- `@tailwindcss/line-clamp` |
|||
- `@tailwindcss/typography` |
|||
- `@tailwindcss/forms` |
|||
- PostCSS 8 w/ `postcss-nesting` plugin |
|||
- Eslint |
|||
- Prettier |
|||
- Alias `@` to `<project_root>/src` |
|||
- Define `_APP_VERSION` from `package.json` version at build time |
|||
- Using new `script setup` syntax (see [Vue rfc #227](https://github.com/vuejs/rfcs/pull/227)) |
|||
- Cypress.io e2e tests (configured similarly to `vue-cli`) |
|||
- GitHub workflows |
|||
- Dependabot |
|||
- Automated e2e tests |
|||
- GitLab CI |
|||
## Project setup and usage |
|||
|
|||
Install dependencies: |
|||
|
|||
``` |
|||
yarn |
|||
``` |
|||
|
|||
Run development server: |
|||
|
|||
``` |
|||
yarn dev |
|||
``` |
|||
|
|||
Open Cypress test runner: |
|||
|
|||
``` |
|||
yarn test |
|||
``` |
|||
|
|||
Run Cypress tests in headless mode: |
|||
|
|||
``` |
|||
yarn test:ci |
|||
``` |
|||
|
|||
## Contributing |
|||
|
|||
Contributions are welcome! Please follow the [code of conduct](https://www.contributor-covenant.org/version/2/0/code_of_conduct/) when interacting with others. |
|||
|
|||
--- |
|||
|
|||
[Follow @uninen](https://twitter.com/uninen) on Twitter |
@ -0,0 +1,9 @@ |
|||
{ |
|||
"baseUrl": "http://localhost:3000", |
|||
"fixturesFolder": "tests/e2e/fixtures", |
|||
"integrationFolder": "tests/e2e/integration", |
|||
"pluginsFile": "tests/e2e/plugins/index.ts", |
|||
"screenshotsFolder": "tests/e2e/screenshots", |
|||
"supportFile": "tests/e2e/support/index.ts", |
|||
"videosFolder": "tests/e2e/videos" |
|||
} |
@ -0,0 +1,28 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8" /> |
|||
<link rel="icon" href="/favicon.ico" /> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|||
<title>Vite Typescript + Tailwind Starter</title> |
|||
<script type="text/javascript"> |
|||
!(function(doc, win) { |
|||
var docEle = doc.documentElement, //获取html元素 |
|||
event = "onorientationchange" in window ? "orientationchange" : "resize", //判断是屏幕旋转还是resize; |
|||
fn = function() { |
|||
var width = docEle.clientWidth; |
|||
width && (docEle.style.fontSize = (width / 1080) + "px"); //设置html的fontSize,随着event的改变而改变。 |
|||
}; |
|||
|
|||
win.addEventListener(event, fn, false); |
|||
doc.addEventListener("DOMContentLoaded", fn, false); |
|||
|
|||
}(document, window)); |
|||
</script> |
|||
</head> |
|||
<body class="w-screen h-screen min-h-0"> |
|||
<!-- style="overflow: hidden;" --> |
|||
<div id="app" class="flex w-screen h-screen min-h-0"></div> |
|||
<script type="module" src="/src/main.ts"></script> |
|||
</body> |
|||
</html> |
@ -0,0 +1,40 @@ |
|||
{ |
|||
"name": "vite-ts-tailwind-starter", |
|||
"version": "1.1.2", |
|||
"scripts": { |
|||
"dev": "vite", |
|||
"build": "vite build", |
|||
"start": "yarn dev & wait-on tcp:3000 -v", |
|||
"test": "cypress open", |
|||
"test:ci": "cypress run --headless" |
|||
}, |
|||
"dependencies": { |
|||
"echarts": "^5.1.1", |
|||
"vue": "~3.0", |
|||
"vuex": "~4.0" |
|||
}, |
|||
"devDependencies": { |
|||
"@tailwindcss/aspect-ratio": "~0.2", |
|||
"@tailwindcss/forms": "~0.3", |
|||
"@tailwindcss/line-clamp": "~0.2", |
|||
"@tailwindcss/typography": "~0.4", |
|||
"@typescript-eslint/eslint-plugin": "~4", |
|||
"@typescript-eslint/parser": "~4", |
|||
"@vitejs/plugin-vue": "~1.2", |
|||
"@vue/compiler-sfc": "~3.0", |
|||
"@vue/eslint-config-standard": "~6.0", |
|||
"@vue/eslint-config-typescript": "~7.0", |
|||
"@vuedx/typecheck": "~0.6", |
|||
"@vuedx/typescript-plugin-vue": "~0.6", |
|||
"autoprefixer": "~10.2", |
|||
"cypress": "^7.0", |
|||
"eslint": "~7", |
|||
"eslint-plugin-vue": "~7", |
|||
"postcss": "~8.2", |
|||
"postcss-nesting": "~7.0", |
|||
"tailwindcss": "~2.1", |
|||
"typescript": "~4.2", |
|||
"vite": "~2.1", |
|||
"wait-on": "~5.3" |
|||
} |
|||
} |
@ -0,0 +1,7 @@ |
|||
module.exports = { |
|||
plugins: { |
|||
tailwindcss: {}, |
|||
autoprefixer: {}, |
|||
'postcss-nesting': {}, |
|||
}, |
|||
} |
After Width: | Height: | Size: 4.2 KiB |
@ -0,0 +1,16 @@ |
|||
<template> |
|||
<div class="justify-center flex flex-1 bg"> |
|||
<HelloWorld class="flex-1" msg="Hello Vue 3 + Vite + Tailwind CSS" /> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import HelloWorld from './components/HelloWorld.vue' |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.bg { |
|||
background: url('./assets/bg.png') center no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
</style> |
After Width: | Height: | Size: 76 KiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 82 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 134 KiB |
After Width: | Height: | Size: 1.8 MiB |
After Width: | Height: | Size: 1.6 KiB |
@ -0,0 +1,103 @@ |
|||
<template> |
|||
<div class="flex flex-col"> |
|||
<!-- 标题 --> |
|||
<div class="title"> |
|||
<Toolbar /> |
|||
</div> |
|||
<div class="con flex-1 flex flex-col"> |
|||
<div class="floor1 flex justify-between"> |
|||
<!-- 温度 --> |
|||
<Temperature /> |
|||
<!-- 湿度 --> |
|||
<Humidity /> |
|||
</div> |
|||
<div class="flex-1"> |
|||
<iframe></iframe> |
|||
</div> |
|||
<div class="floor3 flex justify-between"> |
|||
<!-- 吞土量实时统计 --> |
|||
<Amount /> |
|||
<!-- 仓库热力图 --> |
|||
<Heat /> |
|||
</div> |
|||
<!-- 曲线对比图 --> |
|||
<div class="floor4"> |
|||
<Curve /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { defineComponent } from 'vue' |
|||
import Toolbar from './toolbar.vue' |
|||
import Temperature from './temperature.vue' |
|||
import Humidity from './humidity.vue' |
|||
import Amount from './amount.vue' |
|||
import Heat from './heat.vue' |
|||
import Curve from './curve.vue' |
|||
|
|||
export default defineComponent({ |
|||
components: { |
|||
Toolbar, |
|||
Temperature, |
|||
Humidity, |
|||
Amount, |
|||
Heat, |
|||
Curve, |
|||
}, |
|||
data() { |
|||
return {} |
|||
}, |
|||
methods: {}, |
|||
}) |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.con { |
|||
margin-left: 20rem; |
|||
margin-right: 20rem; |
|||
} |
|||
|
|||
.title { |
|||
width: 100%; |
|||
height: 65rem; |
|||
text-align: center; |
|||
background: url('../assets/top.png') center no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
|
|||
.floor1 { |
|||
width: 100%; |
|||
height: 390rem; |
|||
margin-top: 22rem; |
|||
} |
|||
|
|||
.floor3 { |
|||
width: 100%; |
|||
height: 390rem; |
|||
margin-bottom: 22rem; |
|||
} |
|||
|
|||
.floor4 { |
|||
width: 100%; |
|||
height: 390rem; |
|||
margin-bottom: 22rem; |
|||
} |
|||
</style> |
|||
|
|||
<style> |
|||
.box-bg { |
|||
width: 497rem; |
|||
height: 392rem; |
|||
background: url('../assets/box1.png') center no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
|
|||
.box2-bg { |
|||
width: 100%; |
|||
height: 392rem; |
|||
background: url('../assets/box2.png') center no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
</style> |
@ -0,0 +1,20 @@ |
|||
<template> |
|||
<div class="box-bg">吞土量</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { defineComponent } from 'vue' |
|||
|
|||
export default defineComponent({ |
|||
setup() { |
|||
return {} |
|||
}, |
|||
}) |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.temperature { |
|||
width: 1440rem; |
|||
height: 1110rem; |
|||
} |
|||
</style> |
@ -0,0 +1,20 @@ |
|||
<template> |
|||
<div class="box2-bg">曲线对比图</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { defineComponent } from 'vue' |
|||
|
|||
export default defineComponent({ |
|||
setup() { |
|||
return {} |
|||
}, |
|||
}) |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.temperature { |
|||
width: 100%; |
|||
height: 1110rem; |
|||
} |
|||
</style> |
@ -0,0 +1,20 @@ |
|||
<template> |
|||
<div class="box-bg">仓库热力图</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { defineComponent } from 'vue' |
|||
|
|||
export default defineComponent({ |
|||
setup() { |
|||
return {} |
|||
}, |
|||
}) |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.temperature { |
|||
width: 1440rem; |
|||
height: 1110rem; |
|||
} |
|||
</style> |
@ -0,0 +1,50 @@ |
|||
<template> |
|||
<div class="box-bg"> |
|||
<div :style="{ width: '300px', height: '300px' }" ref="humidity"></div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { ref } from 'vue' |
|||
import * as echarts from 'echarts' |
|||
|
|||
export default { |
|||
name: 'Home', |
|||
setup() { |
|||
const humidity = ref<HTMLElement>() //也可以用const humidity = ref<any>(); |
|||
|
|||
console.log(humidity) |
|||
const myCharts = ref<any>() |
|||
setTimeout(() => { |
|||
// 绘制图表 |
|||
myCharts.value = echarts.init(humidity.value!) |
|||
myCharts.value.setOption({ |
|||
title: { text: '总用户量' }, |
|||
tooltip: {}, |
|||
xAxis: { |
|||
data: ['12-3', '12-4', '12-5', '12-6', '12-7', '12-8'], |
|||
}, |
|||
yAxis: {}, |
|||
series: [ |
|||
{ |
|||
name: '用户量', |
|||
type: 'line', |
|||
data: [5, 20, 36, 10, 10, 20], |
|||
}, |
|||
], |
|||
}) |
|||
}, 10) |
|||
|
|||
return { |
|||
humidity, |
|||
} |
|||
}, |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.temperature { |
|||
width: 1440rem; |
|||
height: 1110rem; |
|||
} |
|||
</style> |
@ -0,0 +1,60 @@ |
|||
<template> |
|||
<div class="box-bg"> |
|||
<div :style="{ width: '92%', height: '92%',margin: '8% 3% auto 5%' }" ref="temperature"></div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { ref } from 'vue' |
|||
import * as echarts from 'echarts' |
|||
|
|||
export default { |
|||
name: 'Home', |
|||
setup() { |
|||
const temperature = ref<HTMLElement>() //也可以用const temperature = ref<any>(); |
|||
|
|||
console.log(temperature) |
|||
const myCharts = ref<any>() |
|||
setTimeout(() => { |
|||
// 绘制图表 |
|||
myCharts.value = echarts.init(temperature.value!) |
|||
myCharts.value.setOption({ |
|||
// echarts 换内容 |
|||
title: { |
|||
text: '温度曲线图', |
|||
textStyle: { |
|||
color: '#03D7FA', |
|||
fontWeight: 'normal', |
|||
fontSize: '16rem', |
|||
}, |
|||
}, |
|||
xAxis: { |
|||
type: 'category', |
|||
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], |
|||
}, |
|||
yAxis: { |
|||
type: 'value', |
|||
}, |
|||
series: [ |
|||
{ |
|||
data: [820, 932, 901, 934, 1290, 1330, 1320], |
|||
type: 'line', |
|||
smooth: true, |
|||
}, |
|||
], |
|||
}) |
|||
}, 10) |
|||
|
|||
return { |
|||
temperature, |
|||
} |
|||
}, |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.temperature { |
|||
width: 1440rem; |
|||
height: 1110rem; |
|||
} |
|||
</style> |
@ -0,0 +1,49 @@ |
|||
<template> |
|||
<div class="flex flex-row flex-center pl-6 py-2" style="line-height: 65rem;"> |
|||
<div class="left-text flex flex-row"> |
|||
<img class="wheater" src="../assets/yin.png" /> |
|||
<div class="flex flex-col text-white ml-3"> |
|||
<div>10%</div> |
|||
<div>27℃</div> |
|||
</div> |
|||
</div> |
|||
<div class="flex-1 titleName" style="line-height: 65rem;">现海园区电子看板</div> |
|||
<div class="left-text flex flex-row"> |
|||
<div class="time">2021.04.27 20:00:00</div> |
|||
<img class="wheater" src="../assets/huo.png" /> |
|||
<img class="wheater" src="../assets/shui.png" /> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { defineComponent } from 'vue' |
|||
|
|||
export default defineComponent({ |
|||
setup() { |
|||
return {} |
|||
}, |
|||
}) |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.temperature { |
|||
width: 1440rem; |
|||
height: 1110rem; |
|||
} |
|||
|
|||
.titleName { |
|||
font-size: 30rem; |
|||
font-weight: bold; |
|||
color: #fff; |
|||
} |
|||
|
|||
.left-text { |
|||
width: 320rem; |
|||
} |
|||
|
|||
.wheater { |
|||
width: 28px; |
|||
height: 22rem; |
|||
} |
|||
</style> |
@ -0,0 +1,5 @@ |
|||
@tailwind base; |
|||
|
|||
@tailwind components; |
|||
|
|||
@tailwind utilities; |
@ -0,0 +1,10 @@ |
|||
import { createApp } from 'vue' |
|||
import App from './App.vue' |
|||
import './index.css' |
|||
import * as echarts from 'echarts' |
|||
|
|||
|
|||
const app = createApp(App) |
|||
app.config.globalProperties.$echarts = echarts |
|||
|
|||
createApp(App).mount('#app') |
@ -0,0 +1,7 @@ |
|||
declare module '*.vue' { |
|||
import { Component } from 'vue' |
|||
const _default: Component |
|||
export default _default |
|||
} |
|||
|
|||
declare const _APP_VERSION: string |
@ -0,0 +1,25 @@ |
|||
import { ActionTree, ActionContext } from 'vuex' |
|||
|
|||
import { State } from './state' |
|||
import { Mutations, Mutation } from './mutations' |
|||
|
|||
export enum Action { |
|||
initApp = 'initApp', |
|||
} |
|||
|
|||
type AugmentedActionContext = { |
|||
commit<K extends keyof Mutations>( |
|||
key: K, |
|||
payload?: Parameters<Mutations[K]>[1] |
|||
): ReturnType<Mutations[K]> |
|||
} & Omit<ActionContext<State, State>, 'commit'> |
|||
|
|||
export interface Actions { |
|||
[Action.initApp]({ state, commit, dispatch }: AugmentedActionContext): void |
|||
} |
|||
|
|||
export const actions: ActionTree<State, State> & Actions = { |
|||
[Action.initApp]({ state, commit, dispatch }) { |
|||
console.log('app inited!') |
|||
}, |
|||
} |
@ -0,0 +1,12 @@ |
|||
import { GetterTree } from 'vuex' |
|||
import { State } from './state' |
|||
|
|||
export type Getters = { |
|||
isReady(state: State): boolean |
|||
} |
|||
|
|||
export const getters: GetterTree<State, State> & Getters = { |
|||
isReady(state) { |
|||
return !state.isInitialized |
|||
}, |
|||
} |
@ -0,0 +1,42 @@ |
|||
/* eslint-disable @typescript-eslint/no-extra-semi */ |
|||
import { InjectionKey } from 'vue' |
|||
import { createStore, Store as VuexStore, CommitOptions, DispatchOptions } from 'vuex' |
|||
|
|||
import { mutations, Mutations, Mutation } from './mutations' |
|||
import { actions, Actions, Action } from './actions' |
|||
import { getters, Getters } from './getters' |
|||
import { state } from './state' |
|||
import type { State } from './state' |
|||
|
|||
export const key: InjectionKey<VuexStore<State>> = Symbol() |
|||
|
|||
export const store = createStore({ |
|||
state, |
|||
getters, |
|||
mutations, |
|||
actions, |
|||
}) |
|||
|
|||
export type Store = Omit<VuexStore<State>, 'getters' | 'commit' | 'dispatch'> & { |
|||
commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>( |
|||
key: K, |
|||
payload: P, |
|||
options?: CommitOptions |
|||
): ReturnType<Mutations[K]> |
|||
} & { |
|||
dispatch<K extends keyof Actions>( |
|||
key: K, |
|||
payload?: Parameters<Actions[K]>[1], |
|||
options?: DispatchOptions |
|||
): ReturnType<Actions[K]> |
|||
} & { |
|||
getters: { |
|||
[K in keyof Getters]: ReturnType<Getters[K]> |
|||
} |
|||
} |
|||
|
|||
export function useStore(): Store { |
|||
return store as Store |
|||
} |
|||
|
|||
export { State, Mutation, Action } |
@ -0,0 +1,16 @@ |
|||
import { MutationTree } from 'vuex' |
|||
import { State } from './state' |
|||
|
|||
export enum Mutation { |
|||
INCREMENT = 'INCREMENT', |
|||
} |
|||
|
|||
export type Mutations<S = State> = { |
|||
[Mutation.INCREMENT](state: S, payload: number): void |
|||
} |
|||
|
|||
export const mutations: MutationTree<State> & Mutations = { |
|||
[Mutation.INCREMENT](state: State, payload: number = 1) { |
|||
state.count += payload |
|||
}, |
|||
} |
@ -0,0 +1,15 @@ |
|||
export interface State { |
|||
debug: boolean |
|||
version: string |
|||
isInitialized: boolean |
|||
count: number |
|||
} |
|||
|
|||
const versionString = import.meta.env.MODE === 'development' ? _APP_VERSION + '-dev' : _APP_VERSION |
|||
|
|||
export const state: State = { |
|||
debug: import.meta.env.MODE === 'development', |
|||
version: versionString, |
|||
isInitialized: false, |
|||
count: 0, |
|||
} |
@ -0,0 +1,23 @@ |
|||
const defaultTheme = require('tailwindcss/defaultTheme') |
|||
|
|||
module.exports = { |
|||
mode: 'jit', |
|||
purge: { |
|||
enabled: process.env.NODE_ENV === 'production', |
|||
content: ['./index.html', './src/**/*.{vue,ts}'], |
|||
}, |
|||
// theme: {
|
|||
// extend: {
|
|||
// // here's how to extend fonts if needed
|
|||
// fontFamily: {
|
|||
// sans: [...defaultTheme.fontFamily.sans],
|
|||
// },
|
|||
// },
|
|||
// },
|
|||
plugins: [ |
|||
require('@tailwindcss/aspect-ratio'), |
|||
require('@tailwindcss/line-clamp'), |
|||
require('@tailwindcss/typography'), |
|||
require('@tailwindcss/forms'), |
|||
], |
|||
} |
@ -0,0 +1,299 @@ |
|||
/// <reference types="cypress" />
|
|||
|
|||
context('Actions', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/commands/actions') |
|||
}) |
|||
|
|||
// https://on.cypress.io/interacting-with-elements
|
|||
|
|||
it('.type() - type into a DOM element', () => { |
|||
// https://on.cypress.io/type
|
|||
cy.get('.action-email') |
|||
.type('fake@email.com').should('have.value', 'fake@email.com') |
|||
|
|||
// .type() with special character sequences
|
|||
.type('{leftarrow}{rightarrow}{uparrow}{downarrow}') |
|||
.type('{del}{selectall}{backspace}') |
|||
|
|||
// .type() with key modifiers
|
|||
.type('{alt}{option}') //these are equivalent
|
|||
.type('{ctrl}{control}') //these are equivalent
|
|||
.type('{meta}{command}{cmd}') //these are equivalent
|
|||
.type('{shift}') |
|||
|
|||
// Delay each keypress by 0.1 sec
|
|||
.type('slow.typing@email.com', { delay: 100 }) |
|||
.should('have.value', 'slow.typing@email.com') |
|||
|
|||
cy.get('.action-disabled') |
|||
// Ignore error checking prior to type
|
|||
// like whether the input is visible or disabled
|
|||
.type('disabled error checking', { force: true }) |
|||
.should('have.value', 'disabled error checking') |
|||
}) |
|||
|
|||
it('.focus() - focus on a DOM element', () => { |
|||
// https://on.cypress.io/focus
|
|||
cy.get('.action-focus').focus() |
|||
.should('have.class', 'focus') |
|||
.prev().should('have.attr', 'style', 'color: orange;') |
|||
}) |
|||
|
|||
it('.blur() - blur off a DOM element', () => { |
|||
// https://on.cypress.io/blur
|
|||
cy.get('.action-blur').type('About to blur').blur() |
|||
.should('have.class', 'error') |
|||
.prev().should('have.attr', 'style', 'color: red;') |
|||
}) |
|||
|
|||
it('.clear() - clears an input or textarea element', () => { |
|||
// https://on.cypress.io/clear
|
|||
cy.get('.action-clear').type('Clear this text') |
|||
.should('have.value', 'Clear this text') |
|||
.clear() |
|||
.should('have.value', '') |
|||
}) |
|||
|
|||
it('.submit() - submit a form', () => { |
|||
// https://on.cypress.io/submit
|
|||
cy.get('.action-form') |
|||
.find('[type="text"]').type('HALFOFF') |
|||
|
|||
cy.get('.action-form').submit() |
|||
.next().should('contain', 'Your form has been submitted!') |
|||
}) |
|||
|
|||
it('.click() - click on a DOM element', () => { |
|||
// https://on.cypress.io/click
|
|||
cy.get('.action-btn').click() |
|||
|
|||
// You can click on 9 specific positions of an element:
|
|||
// -----------------------------------
|
|||
// | topLeft top topRight |
|
|||
// | |
|
|||
// | |
|
|||
// | |
|
|||
// | left center right |
|
|||
// | |
|
|||
// | |
|
|||
// | |
|
|||
// | bottomLeft bottom bottomRight |
|
|||
// -----------------------------------
|
|||
|
|||
// clicking in the center of the element is the default
|
|||
cy.get('#action-canvas').click() |
|||
|
|||
cy.get('#action-canvas').click('topLeft') |
|||
cy.get('#action-canvas').click('top') |
|||
cy.get('#action-canvas').click('topRight') |
|||
cy.get('#action-canvas').click('left') |
|||
cy.get('#action-canvas').click('right') |
|||
cy.get('#action-canvas').click('bottomLeft') |
|||
cy.get('#action-canvas').click('bottom') |
|||
cy.get('#action-canvas').click('bottomRight') |
|||
|
|||
// .click() accepts an x and y coordinate
|
|||
// that controls where the click occurs :)
|
|||
|
|||
cy.get('#action-canvas') |
|||
.click(80, 75) // click 80px on x coord and 75px on y coord
|
|||
.click(170, 75) |
|||
.click(80, 165) |
|||
.click(100, 185) |
|||
.click(125, 190) |
|||
.click(150, 185) |
|||
.click(170, 165) |
|||
|
|||
// click multiple elements by passing multiple: true
|
|||
cy.get('.action-labels>.label').click({ multiple: true }) |
|||
|
|||
// Ignore error checking prior to clicking
|
|||
cy.get('.action-opacity>.btn').click({ force: true }) |
|||
}) |
|||
|
|||
it('.dblclick() - double click on a DOM element', () => { |
|||
// https://on.cypress.io/dblclick
|
|||
|
|||
// Our app has a listener on 'dblclick' event in our 'scripts.js'
|
|||
// that hides the div and shows an input on double click
|
|||
cy.get('.action-div').dblclick().should('not.be.visible') |
|||
cy.get('.action-input-hidden').should('be.visible') |
|||
}) |
|||
|
|||
it('.rightclick() - right click on a DOM element', () => { |
|||
// https://on.cypress.io/rightclick
|
|||
|
|||
// Our app has a listener on 'contextmenu' event in our 'scripts.js'
|
|||
// that hides the div and shows an input on right click
|
|||
cy.get('.rightclick-action-div').rightclick().should('not.be.visible') |
|||
cy.get('.rightclick-action-input-hidden').should('be.visible') |
|||
}) |
|||
|
|||
it('.check() - check a checkbox or radio element', () => { |
|||
// https://on.cypress.io/check
|
|||
|
|||
// By default, .check() will check all
|
|||
// matching checkbox or radio elements in succession, one after another
|
|||
cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]') |
|||
.check().should('be.checked') |
|||
|
|||
cy.get('.action-radios [type="radio"]').not('[disabled]') |
|||
.check().should('be.checked') |
|||
|
|||
// .check() accepts a value argument
|
|||
cy.get('.action-radios [type="radio"]') |
|||
.check('radio1').should('be.checked') |
|||
|
|||
// .check() accepts an array of values
|
|||
cy.get('.action-multiple-checkboxes [type="checkbox"]') |
|||
.check(['checkbox1', 'checkbox2']).should('be.checked') |
|||
|
|||
// Ignore error checking prior to checking
|
|||
cy.get('.action-checkboxes [disabled]') |
|||
.check({ force: true }).should('be.checked') |
|||
|
|||
cy.get('.action-radios [type="radio"]') |
|||
.check('radio3', { force: true }).should('be.checked') |
|||
}) |
|||
|
|||
it('.uncheck() - uncheck a checkbox element', () => { |
|||
// https://on.cypress.io/uncheck
|
|||
|
|||
// By default, .uncheck() will uncheck all matching
|
|||
// checkbox elements in succession, one after another
|
|||
cy.get('.action-check [type="checkbox"]') |
|||
.not('[disabled]') |
|||
.uncheck().should('not.be.checked') |
|||
|
|||
// .uncheck() accepts a value argument
|
|||
cy.get('.action-check [type="checkbox"]') |
|||
.check('checkbox1') |
|||
.uncheck('checkbox1').should('not.be.checked') |
|||
|
|||
// .uncheck() accepts an array of values
|
|||
cy.get('.action-check [type="checkbox"]') |
|||
.check(['checkbox1', 'checkbox3']) |
|||
.uncheck(['checkbox1', 'checkbox3']).should('not.be.checked') |
|||
|
|||
// Ignore error checking prior to unchecking
|
|||
cy.get('.action-check [disabled]') |
|||
.uncheck({ force: true }).should('not.be.checked') |
|||
}) |
|||
|
|||
it('.select() - select an option in a <select> element', () => { |
|||
// https://on.cypress.io/select
|
|||
|
|||
// at first, no option should be selected
|
|||
cy.get('.action-select') |
|||
.should('have.value', '--Select a fruit--') |
|||
|
|||
// Select option(s) with matching text content
|
|||
cy.get('.action-select').select('apples') |
|||
// confirm the apples were selected
|
|||
// note that each value starts with "fr-" in our HTML
|
|||
cy.get('.action-select').should('have.value', 'fr-apples') |
|||
|
|||
cy.get('.action-select-multiple') |
|||
.select(['apples', 'oranges', 'bananas']) |
|||
// when getting multiple values, invoke "val" method first
|
|||
.invoke('val') |
|||
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas']) |
|||
|
|||
// Select option(s) with matching value
|
|||
cy.get('.action-select').select('fr-bananas') |
|||
// can attach an assertion right away to the element
|
|||
.should('have.value', 'fr-bananas') |
|||
|
|||
cy.get('.action-select-multiple') |
|||
.select(['fr-apples', 'fr-oranges', 'fr-bananas']) |
|||
.invoke('val') |
|||
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas']) |
|||
|
|||
// assert the selected values include oranges
|
|||
cy.get('.action-select-multiple') |
|||
.invoke('val').should('include', 'fr-oranges') |
|||
}) |
|||
|
|||
it('.scrollIntoView() - scroll an element into view', () => { |
|||
// https://on.cypress.io/scrollintoview
|
|||
|
|||
// normally all of these buttons are hidden,
|
|||
// because they're not within
|
|||
// the viewable area of their parent
|
|||
// (we need to scroll to see them)
|
|||
cy.get('#scroll-horizontal button') |
|||
.should('not.be.visible') |
|||
|
|||
// scroll the button into view, as if the user had scrolled
|
|||
cy.get('#scroll-horizontal button').scrollIntoView() |
|||
.should('be.visible') |
|||
|
|||
cy.get('#scroll-vertical button') |
|||
.should('not.be.visible') |
|||
|
|||
// Cypress handles the scroll direction needed
|
|||
cy.get('#scroll-vertical button').scrollIntoView() |
|||
.should('be.visible') |
|||
|
|||
cy.get('#scroll-both button') |
|||
.should('not.be.visible') |
|||
|
|||
// Cypress knows to scroll to the right and down
|
|||
cy.get('#scroll-both button').scrollIntoView() |
|||
.should('be.visible') |
|||
}) |
|||
|
|||
it('.trigger() - trigger an event on a DOM element', () => { |
|||
// https://on.cypress.io/trigger
|
|||
|
|||
// To interact with a range input (slider)
|
|||
// we need to set its value & trigger the
|
|||
// event to signal it changed
|
|||
|
|||
// Here, we invoke jQuery's val() method to set
|
|||
// the value and trigger the 'change' event
|
|||
cy.get('.trigger-input-range') |
|||
.invoke('val', 25) |
|||
.trigger('change') |
|||
.get('input[type=range]').siblings('p') |
|||
.should('have.text', '25') |
|||
}) |
|||
|
|||
it('cy.scrollTo() - scroll the window or element to a position', () => { |
|||
// https://on.cypress.io/scrollTo
|
|||
|
|||
// You can scroll to 9 specific positions of an element:
|
|||
// -----------------------------------
|
|||
// | topLeft top topRight |
|
|||
// | |
|
|||
// | |
|
|||
// | |
|
|||
// | left center right |
|
|||
// | |
|
|||
// | |
|
|||
// | |
|
|||
// | bottomLeft bottom bottomRight |
|
|||
// -----------------------------------
|
|||
|
|||
// if you chain .scrollTo() off of cy, we will
|
|||
// scroll the entire window
|
|||
cy.scrollTo('bottom') |
|||
|
|||
cy.get('#scrollable-horizontal').scrollTo('right') |
|||
|
|||
// or you can scroll to a specific coordinate:
|
|||
// (x axis, y axis) in pixels
|
|||
cy.get('#scrollable-vertical').scrollTo(250, 250) |
|||
|
|||
// or you can scroll to a specific percentage
|
|||
// of the (width, height) of the element
|
|||
cy.get('#scrollable-both').scrollTo('75%', '25%') |
|||
|
|||
// control the easing of the scroll (default is 'swing')
|
|||
cy.get('#scrollable-vertical').scrollTo('center', { easing: 'linear' }) |
|||
|
|||
// control the duration of the scroll (in ms)
|
|||
cy.get('#scrollable-both').scrollTo('center', { duration: 2000 }) |
|||
}) |
|||
}) |
@ -0,0 +1,40 @@ |
|||
/// <reference types="cypress" />
|
|||
|
|||
context('Aliasing', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/commands/aliasing') |
|||
}) |
|||
|
|||
it('.as() - alias a DOM element for later use', () => { |
|||
// https://on.cypress.io/as
|
|||
|
|||
// Alias a DOM element for use later
|
|||
// We don't have to traverse to the element
|
|||
// later in our code, we reference it with @
|
|||
|
|||
cy.get('.as-table').find('tbody>tr') |
|||
.first().find('td').first() |
|||
.find('button').as('firstBtn') |
|||
|
|||
// when we reference the alias, we place an
|
|||
// @ in front of its name
|
|||
cy.get('@firstBtn').click() |
|||
|
|||
cy.get('@firstBtn') |
|||
.should('have.class', 'btn-success') |
|||
.and('contain', 'Changed') |
|||
}) |
|||
|
|||
it('.as() - alias a route for later use', () => { |
|||
// Alias the route to wait for its response
|
|||
cy.server() |
|||
cy.route('GET', 'comments/*').as('getComment') |
|||
|
|||
// we have code that gets a comment when
|
|||
// the button is clicked in scripts.js
|
|||
cy.get('.network-btn').click() |
|||
|
|||
// https://on.cypress.io/wait
|
|||
cy.wait('@getComment').its('status').should('eq', 200) |
|||
}) |
|||
}) |
@ -0,0 +1,177 @@ |
|||
/// <reference types="cypress" />
|
|||
|
|||
context('Assertions', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/commands/assertions') |
|||
}) |
|||
|
|||
describe('Implicit Assertions', () => { |
|||
it('.should() - make an assertion about the current subject', () => { |
|||
// https://on.cypress.io/should
|
|||
cy.get('.assertion-table') |
|||
.find('tbody tr:last') |
|||
.should('have.class', 'success') |
|||
.find('td') |
|||
.first() |
|||
// checking the text of the <td> element in various ways
|
|||
.should('have.text', 'Column content') |
|||
.should('contain', 'Column content') |
|||
.should('have.html', 'Column content') |
|||
// chai-jquery uses "is()" to check if element matches selector
|
|||
.should('match', 'td') |
|||
// to match text content against a regular expression
|
|||
// first need to invoke jQuery method text()
|
|||
// and then match using regular expression
|
|||
.invoke('text') |
|||
.should('match', /column content/i) |
|||
|
|||
// a better way to check element's text content against a regular expression
|
|||
// is to use "cy.contains"
|
|||
// https://on.cypress.io/contains
|
|||
cy.get('.assertion-table') |
|||
.find('tbody tr:last') |
|||
// finds first <td> element with text content matching regular expression
|
|||
.contains('td', /column content/i) |
|||
.should('be.visible') |
|||
|
|||
// for more information about asserting element's text
|
|||
// see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents
|
|||
}) |
|||
|
|||
it('.and() - chain multiple assertions together', () => { |
|||
// https://on.cypress.io/and
|
|||
cy.get('.assertions-link') |
|||
.should('have.class', 'active') |
|||
.and('have.attr', 'href') |
|||
.and('include', 'cypress.io') |
|||
}) |
|||
}) |
|||
|
|||
describe('Explicit Assertions', () => { |
|||
// https://on.cypress.io/assertions
|
|||
it('expect - make an assertion about a specified subject', () => { |
|||
// We can use Chai's BDD style assertions
|
|||
expect(true).to.be.true |
|||
const o = { foo: 'bar' } |
|||
|
|||
expect(o).to.equal(o) |
|||
expect(o).to.deep.equal({ foo: 'bar' }) |
|||
// matching text using regular expression
|
|||
expect('FooBar').to.match(/bar$/i) |
|||
}) |
|||
|
|||
it('pass your own callback function to should()', () => { |
|||
// Pass a function to should that can have any number
|
|||
// of explicit assertions within it.
|
|||
// The ".should(cb)" function will be retried
|
|||
// automatically until it passes all your explicit assertions or times out.
|
|||
cy.get('.assertions-p') |
|||
.find('p') |
|||
.should(($p) => { |
|||
// https://on.cypress.io/$
|
|||
// return an array of texts from all of the p's
|
|||
// @ts-ignore TS6133 unused variable
|
|||
const texts = $p.map((i, el) => Cypress.$(el).text()) |
|||
|
|||
// jquery map returns jquery object
|
|||
// and .get() convert this to simple array
|
|||
const paragraphs = texts.get() |
|||
|
|||
// array should have length of 3
|
|||
expect(paragraphs, 'has 3 paragraphs').to.have.length(3) |
|||
|
|||
// use second argument to expect(...) to provide clear
|
|||
// message with each assertion
|
|||
expect(paragraphs, 'has expected text in each paragraph').to.deep.eq([ |
|||
'Some text from first p', |
|||
'More text from second p', |
|||
'And even more text from third p', |
|||
]) |
|||
}) |
|||
}) |
|||
|
|||
it('finds element by class name regex', () => { |
|||
cy.get('.docs-header') |
|||
.find('div') |
|||
// .should(cb) callback function will be retried
|
|||
.should(($div) => { |
|||
expect($div).to.have.length(1) |
|||
|
|||
const className = $div[0].className |
|||
|
|||
expect(className).to.match(/heading-/) |
|||
}) |
|||
// .then(cb) callback is not retried,
|
|||
// it either passes or fails
|
|||
.then(($div) => { |
|||
expect($div, 'text content').to.have.text('Introduction') |
|||
}) |
|||
}) |
|||
|
|||
it('can throw any error', () => { |
|||
cy.get('.docs-header') |
|||
.find('div') |
|||
.should(($div) => { |
|||
if ($div.length !== 1) { |
|||
// you can throw your own errors
|
|||
throw new Error('Did not find 1 element') |
|||
} |
|||
|
|||
const className = $div[0].className |
|||
|
|||
if (!className.match(/heading-/)) { |
|||
throw new Error(`Could not find class "heading-" in ${className}`) |
|||
} |
|||
}) |
|||
}) |
|||
|
|||
it('matches unknown text between two elements', () => { |
|||
/** |
|||
* Text from the first element. |
|||
* @type {string} |
|||
*/ |
|||
let text |
|||
|
|||
/** |
|||
* Normalizes passed text, |
|||
* useful before comparing text with spaces and different capitalization. |
|||
* @param {string} s Text to normalize |
|||
*/ |
|||
const normalizeText = (s) => s.replace(/\s/g, '').toLowerCase() |
|||
|
|||
cy.get('.two-elements') |
|||
.find('.first') |
|||
.then(($first) => { |
|||
// save text from the first element
|
|||
text = normalizeText($first.text()) |
|||
}) |
|||
|
|||
cy.get('.two-elements') |
|||
.find('.second') |
|||
.should(($div) => { |
|||
// we can massage text before comparing
|
|||
const secondText = normalizeText($div.text()) |
|||
|
|||
expect(secondText, 'second text').to.equal(text) |
|||
}) |
|||
}) |
|||
|
|||
it('assert - assert shape of an object', () => { |
|||
const person = { |
|||
name: 'Joe', |
|||
age: 20, |
|||
} |
|||
|
|||
assert.isObject(person, 'value is object') |
|||
}) |
|||
|
|||
it('retries the should callback until assertions pass', () => { |
|||
cy.get('#random-number') |
|||
.should(($div) => { |
|||
const n = parseFloat($div.text()) |
|||
|
|||
expect(n).to.be.gte(1).and.be.lte(10) |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
@ -0,0 +1,97 @@ |
|||
/// <reference types="cypress" />
|
|||
|
|||
context('Connectors', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/commands/connectors') |
|||
}) |
|||
|
|||
it('.each() - iterate over an array of elements', () => { |
|||
// https://on.cypress.io/each
|
|||
cy.get('.connectors-each-ul>li') |
|||
.each(($el, index, $list) => { |
|||
console.log($el, index, $list) |
|||
}) |
|||
}) |
|||
|
|||
it('.its() - get properties on the current subject', () => { |
|||
// https://on.cypress.io/its
|
|||
cy.get('.connectors-its-ul>li') |
|||
// calls the 'length' property yielding that value
|
|||
.its('length') |
|||
.should('be.gt', 2) |
|||
}) |
|||
|
|||
it('.invoke() - invoke a function on the current subject', () => { |
|||
// our div is hidden in our script.js
|
|||
// $('.connectors-div').hide()
|
|||
|
|||
// https://on.cypress.io/invoke
|
|||
cy.get('.connectors-div').should('be.hidden') |
|||
// call the jquery method 'show' on the 'div.container'
|
|||
.invoke('show') |
|||
.should('be.visible') |
|||
}) |
|||
|
|||
it('.spread() - spread an array as individual args to callback function', () => { |
|||
// https://on.cypress.io/spread
|
|||
const arr = ['foo', 'bar', 'baz'] |
|||
|
|||
cy.wrap(arr).spread((foo, bar, baz) => { |
|||
expect(foo).to.eq('foo') |
|||
expect(bar).to.eq('bar') |
|||
expect(baz).to.eq('baz') |
|||
}) |
|||
}) |
|||
|
|||
describe('.then()', () => { |
|||
it('invokes a callback function with the current subject', () => { |
|||
// https://on.cypress.io/then
|
|||
cy.get('.connectors-list > li') |
|||
.then(($lis) => { |
|||
expect($lis, '3 items').to.have.length(3) |
|||
expect($lis.eq(0), 'first item').to.contain('Walk the dog') |
|||
expect($lis.eq(1), 'second item').to.contain('Feed the cat') |
|||
expect($lis.eq(2), 'third item').to.contain('Write JavaScript') |
|||
}) |
|||
}) |
|||
|
|||
it('yields the returned value to the next command', () => { |
|||
cy.wrap(1) |
|||
.then((num) => { |
|||
expect(num).to.equal(1) |
|||
|
|||
return 2 |
|||
}) |
|||
.then((num) => { |
|||
expect(num).to.equal(2) |
|||
}) |
|||
}) |
|||
|
|||
it('yields the original subject without return', () => { |
|||
cy.wrap(1) |
|||
.then((num) => { |
|||
expect(num).to.equal(1) |
|||
// note that nothing is returned from this callback
|
|||
}) |
|||
.then((num) => { |
|||
// this callback receives the original unchanged value 1
|
|||
expect(num).to.equal(1) |
|||
}) |
|||
}) |
|||
|
|||
it('yields the value yielded by the last Cypress command inside', () => { |
|||
cy.wrap(1) |
|||
.then((num) => { |
|||
expect(num).to.equal(1) |
|||
// note how we run a Cypress command
|
|||
// the result yielded by this Cypress command
|
|||
// will be passed to the second ".then"
|
|||
cy.wrap(2) |
|||
}) |
|||
.then((num) => { |
|||
// this callback receives the value yielded by "cy.wrap(2)"
|
|||
expect(num).to.equal(2) |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
@ -0,0 +1,77 @@ |
|||
/// <reference types="cypress" />
|
|||
|
|||
context('Cookies', () => { |
|||
beforeEach(() => { |
|||
Cypress.Cookies.debug(true) |
|||
|
|||
cy.visit('https://example.cypress.io/commands/cookies') |
|||
|
|||
// clear cookies again after visiting to remove
|
|||
// any 3rd party cookies picked up such as cloudflare
|
|||
cy.clearCookies() |
|||
}) |
|||
|
|||
it('cy.getCookie() - get a browser cookie', () => { |
|||
// https://on.cypress.io/getcookie
|
|||
cy.get('#getCookie .set-a-cookie').click() |
|||
|
|||
// cy.getCookie() yields a cookie object
|
|||
cy.getCookie('token').should('have.property', 'value', '123ABC') |
|||
}) |
|||
|
|||
it('cy.getCookies() - get browser cookies', () => { |
|||
// https://on.cypress.io/getcookies
|
|||
cy.getCookies().should('be.empty') |
|||
|
|||
cy.get('#getCookies .set-a-cookie').click() |
|||
|
|||
// cy.getCookies() yields an array of cookies
|
|||
cy.getCookies().should('have.length', 1).should((cookies) => { |
|||
// each cookie has these properties
|
|||
expect(cookies[0]).to.have.property('name', 'token') |
|||
expect(cookies[0]).to.have.property('value', '123ABC') |
|||
expect(cookies[0]).to.have.property('httpOnly', false) |
|||
expect(cookies[0]).to.have.property('secure', false) |
|||
expect(cookies[0]).to.have.property('domain') |
|||
expect(cookies[0]).to.have.property('path') |
|||
}) |
|||
}) |
|||
|
|||
it('cy.setCookie() - set a browser cookie', () => { |
|||
// https://on.cypress.io/setcookie
|
|||
cy.getCookies().should('be.empty') |
|||
|
|||
cy.setCookie('foo', 'bar') |
|||
|
|||
// cy.getCookie() yields a cookie object
|
|||
cy.getCookie('foo').should('have.property', 'value', 'bar') |
|||
}) |
|||
|
|||
it('cy.clearCookie() - clear a browser cookie', () => { |
|||
// https://on.cypress.io/clearcookie
|
|||
cy.getCookie('token').should('be.null') |
|||
|
|||
cy.get('#clearCookie .set-a-cookie').click() |
|||
|
|||
cy.getCookie('token').should('have.property', 'value', '123ABC') |
|||
|
|||
// cy.clearCookies() yields null
|
|||
cy.clearCookie('token').should('be.null') |
|||
|
|||
cy.getCookie('token').should('be.null') |
|||
}) |
|||
|
|||
it('cy.clearCookies() - clear browser cookies', () => { |
|||
// https://on.cypress.io/clearcookies
|
|||
cy.getCookies().should('be.empty') |
|||
|
|||
cy.get('#clearCookies .set-a-cookie').click() |
|||
|
|||
cy.getCookies().should('have.length', 1) |
|||
|
|||
// cy.clearCookies() yields null
|
|||
cy.clearCookies() |
|||
|
|||
cy.getCookies().should('be.empty') |
|||
}) |
|||
}) |
@ -0,0 +1,222 @@ |
|||
/// <reference types="cypress" />
|
|||
|
|||
context('Cypress.Commands', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/cypress-api') |
|||
}) |
|||
|
|||
// https://on.cypress.io/custom-commands
|
|||
|
|||
it('.add() - create a custom command', () => { |
|||
Cypress.Commands.add('console', { |
|||
prevSubject: true, |
|||
}, (subject, method) => { |
|||
// the previous subject is automatically received
|
|||
// and the commands arguments are shifted
|
|||
|
|||
// allow us to change the console method used
|
|||
method = method || 'log' |
|||
|
|||
// log the subject to the console
|
|||
// @ts-ignore TS7017
|
|||
console[method]('The subject is', subject) |
|||
|
|||
// whatever we return becomes the new subject
|
|||
// we don't want to change the subject so
|
|||
// we return whatever was passed in
|
|||
return subject |
|||
}) |
|||
|
|||
// @ts-ignore TS2339
|
|||
cy.get('button').console('info').then(($button) => { |
|||
// subject is still $button
|
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
context('Cypress.Cookies', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/cypress-api') |
|||
}) |
|||
|
|||
// https://on.cypress.io/cookies
|
|||
it('.debug() - enable or disable debugging', () => { |
|||
Cypress.Cookies.debug(true) |
|||
|
|||
// Cypress will now log in the console when
|
|||
// cookies are set or cleared
|
|||
cy.setCookie('fakeCookie', '123ABC') |
|||
cy.clearCookie('fakeCookie') |
|||
cy.setCookie('fakeCookie', '123ABC') |
|||
cy.clearCookie('fakeCookie') |
|||
cy.setCookie('fakeCookie', '123ABC') |
|||
}) |
|||
|
|||
it('.preserveOnce() - preserve cookies by key', () => { |
|||
// normally cookies are reset after each test
|
|||
cy.getCookie('fakeCookie').should('not.be.ok') |
|||
|
|||
// preserving a cookie will not clear it when
|
|||
// the next test starts
|
|||
cy.setCookie('lastCookie', '789XYZ') |
|||
Cypress.Cookies.preserveOnce('lastCookie') |
|||
}) |
|||
|
|||
it('.defaults() - set defaults for all cookies', () => { |
|||
if (Number(Cypress.version.charAt(0)) < 5) return |
|||
|
|||
// now any cookie with the name 'session_id' will
|
|||
// not be cleared before each new test runs
|
|||
Cypress.Cookies.defaults({ |
|||
// @ts-ignore
|
|||
preserve: 'session_id', |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
context('Cypress.Server', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/cypress-api') |
|||
}) |
|||
|
|||
// Permanently override server options for
|
|||
// all instances of cy.server()
|
|||
|
|||
// https://on.cypress.io/cypress-server
|
|||
it('.defaults() - change default config of server', () => { |
|||
Cypress.Server.defaults({ |
|||
delay: 0, |
|||
force404: false, |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
context('Cypress.arch', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/cypress-api') |
|||
}) |
|||
|
|||
it('Get CPU architecture name of underlying OS', () => { |
|||
// https://on.cypress.io/arch
|
|||
expect(Cypress.arch).to.exist |
|||
}) |
|||
}) |
|||
|
|||
context('Cypress.config()', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/cypress-api') |
|||
}) |
|||
|
|||
it('Get and set configuration options', () => { |
|||
// https://on.cypress.io/config
|
|||
let myConfig = Cypress.config() |
|||
|
|||
expect(myConfig).to.have.property('animationDistanceThreshold', 5) |
|||
expect(myConfig).to.have.property('baseUrl', null) |
|||
expect(myConfig).to.have.property('defaultCommandTimeout', 4000) |
|||
expect(myConfig).to.have.property('requestTimeout', 5000) |
|||
expect(myConfig).to.have.property('responseTimeout', 30000) |
|||
expect(myConfig).to.have.property('viewportHeight', 660) |
|||
expect(myConfig).to.have.property('viewportWidth', 1000) |
|||
expect(myConfig).to.have.property('pageLoadTimeout', 60000) |
|||
expect(myConfig).to.have.property('waitForAnimations', true) |
|||
|
|||
expect(Cypress.config('pageLoadTimeout')).to.eq(60000) |
|||
|
|||
// this will change the config for the rest of your tests!
|
|||
Cypress.config('pageLoadTimeout', 20000) |
|||
|
|||
expect(Cypress.config('pageLoadTimeout')).to.eq(20000) |
|||
|
|||
Cypress.config('pageLoadTimeout', 60000) |
|||
}) |
|||
}) |
|||
|
|||
context('Cypress.dom', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/cypress-api') |
|||
}) |
|||
|
|||
// https://on.cypress.io/dom
|
|||
it('.isHidden() - determine if a DOM element is hidden', () => { |
|||
let hiddenP = Cypress.$('.dom-p p.hidden').get(0) |
|||
let visibleP = Cypress.$('.dom-p p.visible').get(0) |
|||
|
|||
// our first paragraph has css class 'hidden'
|
|||
expect(Cypress.dom.isHidden(hiddenP)).to.be.true |
|||
expect(Cypress.dom.isHidden(visibleP)).to.be.false |
|||
}) |
|||
}) |
|||
|
|||
context('Cypress.env()', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/cypress-api') |
|||
}) |
|||
|
|||
// We can set environment variables for highly dynamic values
|
|||
|
|||
// https://on.cypress.io/environment-variables
|
|||
it('Get environment variables', () => { |
|||
// https://on.cypress.io/env
|
|||
// set multiple environment variables
|
|||
Cypress.env({ |
|||
host: 'veronica.dev.local', |
|||
api_server: 'http://localhost:8888/v1/', |
|||
}) |
|||
|
|||
// get environment variable
|
|||
expect(Cypress.env('host')).to.eq('veronica.dev.local') |
|||
|
|||
// set environment variable
|
|||
Cypress.env('api_server', 'http://localhost:8888/v2/') |
|||
expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/') |
|||
|
|||
// get all environment variable
|
|||
expect(Cypress.env()).to.have.property('host', 'veronica.dev.local') |
|||
expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/') |
|||
}) |
|||
}) |
|||
|
|||
context('Cypress.log', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/cypress-api') |
|||
}) |
|||
|
|||
it('Control what is printed to the Command Log', () => { |
|||
// https://on.cypress.io/cypress-log
|
|||
}) |
|||
}) |
|||
|
|||
context('Cypress.platform', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/cypress-api') |
|||
}) |
|||
|
|||
it('Get underlying OS name', () => { |
|||
// https://on.cypress.io/platform
|
|||
expect(Cypress.platform).to.be.exist |
|||
}) |
|||
}) |
|||
|
|||
context('Cypress.version', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/cypress-api') |
|||
}) |
|||
|
|||
it('Get current version of Cypress being run', () => { |
|||
// https://on.cypress.io/version
|
|||
expect(Cypress.version).to.be.exist |
|||
}) |
|||
}) |
|||
|
|||
context('Cypress.spec', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/cypress-api') |
|||
}) |
|||
|
|||
it('Get current spec information', () => { |
|||
// https://on.cypress.io/spec
|
|||
// wrap the object so we can inspect it easily by clicking in the command log
|
|||
cy.wrap(Cypress.spec).should('include.keys', ['name', 'relative', 'absolute']) |
|||
}) |
|||
}) |
@ -0,0 +1,115 @@ |
|||
/// <reference types="cypress" />
|
|||
|
|||
/// JSON fixture file can be loaded directly using
|
|||
// the built-in JavaScript bundler
|
|||
// @ts-ignore
|
|||
const requiredExample = require('../../fixtures/example') |
|||
|
|||
context('Files', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/commands/files') |
|||
}) |
|||
|
|||
beforeEach(() => { |
|||
// load example.json fixture file and store
|
|||
// in the test context object
|
|||
cy.fixture('example.json').as('example') |
|||
}) |
|||
|
|||
it('cy.fixture() - load a fixture', () => { |
|||
// https://on.cypress.io/fixture
|
|||
|
|||
// Instead of writing a response inline you can
|
|||
// use a fixture file's content.
|
|||
|
|||
cy.server() |
|||
cy.fixture('example.json').as('comment') |
|||
// when application makes an Ajax request matching "GET comments/*"
|
|||
// Cypress will intercept it and reply with object
|
|||
// from the "comment" alias
|
|||
cy.route('GET', 'comments/*', '@comment').as('getComment') |
|||
|
|||
// we have code that gets a comment when
|
|||
// the button is clicked in scripts.js
|
|||
cy.get('.fixture-btn').click() |
|||
|
|||
cy.wait('@getComment').its('responseBody') |
|||
.should('have.property', 'name') |
|||
.and('include', 'Using fixtures to represent data') |
|||
|
|||
// you can also just write the fixture in the route
|
|||
cy.route('GET', 'comments/*', 'fixture:example.json').as('getComment') |
|||
|
|||
// we have code that gets a comment when
|
|||
// the button is clicked in scripts.js
|
|||
cy.get('.fixture-btn').click() |
|||
|
|||
cy.wait('@getComment').its('responseBody') |
|||
.should('have.property', 'name') |
|||
.and('include', 'Using fixtures to represent data') |
|||
|
|||
// or write fx to represent fixture
|
|||
// by default it assumes it's .json
|
|||
cy.route('GET', 'comments/*', 'fx:example').as('getComment') |
|||
|
|||
// we have code that gets a comment when
|
|||
// the button is clicked in scripts.js
|
|||
cy.get('.fixture-btn').click() |
|||
|
|||
cy.wait('@getComment').its('responseBody') |
|||
.should('have.property', 'name') |
|||
.and('include', 'Using fixtures to represent data') |
|||
}) |
|||
|
|||
it('cy.fixture() or require - load a fixture', function () { |
|||
// we are inside the "function () { ... }"
|
|||
// callback and can use test context object "this"
|
|||
// "this.example" was loaded in "beforeEach" function callback
|
|||
expect(this.example, 'fixture in the test context') |
|||
.to.deep.equal(requiredExample) |
|||
|
|||
// or use "cy.wrap" and "should('deep.equal', ...)" assertion
|
|||
// @ts-ignore
|
|||
cy.wrap(this.example, 'fixture vs require') |
|||
.should('deep.equal', requiredExample) |
|||
}) |
|||
|
|||
it('cy.readFile() - read file contents', () => { |
|||
// https://on.cypress.io/readfile
|
|||
|
|||
// You can read a file and yield its contents
|
|||
// The filePath is relative to your project's root.
|
|||
cy.readFile('cypress.json').then((json) => { |
|||
expect(json).to.be.an('object') |
|||
}) |
|||
}) |
|||
|
|||
it('cy.writeFile() - write to a file', () => { |
|||
// https://on.cypress.io/writefile
|
|||
|
|||
// You can write to a file
|
|||
|
|||
// Use a response from a request to automatically
|
|||
// generate a fixture file for use later
|
|||
cy.request('https://jsonplaceholder.cypress.io/users') |
|||
.then((response) => { |
|||
cy.writeFile('cypress/fixtures/users.json', response.body) |
|||
}) |
|||
|
|||
cy.fixture('users').should((users) => { |
|||
expect(users[0].name).to.exist |
|||
}) |
|||
|
|||
// JavaScript arrays and objects are stringified
|
|||
// and formatted into text.
|
|||
cy.writeFile('cypress/fixtures/profile.json', { |
|||
id: 8739, |
|||
name: 'Jane', |
|||
email: 'jane@example.com', |
|||
}) |
|||
|
|||
cy.fixture('profile').should((profile) => { |
|||
expect(profile.name).to.eq('Jane') |
|||
}) |
|||
}) |
|||
}) |
@ -0,0 +1,52 @@ |
|||
/// <reference types="cypress" />
|
|||
|
|||
context('Local Storage', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/commands/local-storage') |
|||
}) |
|||
// Although local storage is automatically cleared
|
|||
// in between tests to maintain a clean state
|
|||
// sometimes we need to clear the local storage manually
|
|||
|
|||
it('cy.clearLocalStorage() - clear all data in local storage', () => { |
|||
// https://on.cypress.io/clearlocalstorage
|
|||
cy.get('.ls-btn').click().should(() => { |
|||
expect(localStorage.getItem('prop1')).to.eq('red') |
|||
expect(localStorage.getItem('prop2')).to.eq('blue') |
|||
expect(localStorage.getItem('prop3')).to.eq('magenta') |
|||
}) |
|||
|
|||
// clearLocalStorage() yields the localStorage object
|
|||
cy.clearLocalStorage().should((ls) => { |
|||
expect(ls.getItem('prop1')).to.be.null |
|||
expect(ls.getItem('prop2')).to.be.null |
|||
expect(ls.getItem('prop3')).to.be.null |
|||
}) |
|||
|
|||
// Clear key matching string in Local Storage
|
|||
cy.get('.ls-btn').click().should(() => { |
|||
expect(localStorage.getItem('prop1')).to.eq('red') |
|||
expect(localStorage.getItem('prop2')).to.eq('blue') |
|||
expect(localStorage.getItem('prop3')).to.eq('magenta') |
|||
}) |
|||
|
|||
cy.clearLocalStorage('prop1').should((ls) => { |
|||
expect(ls.getItem('prop1')).to.be.null |
|||
expect(ls.getItem('prop2')).to.eq('blue') |
|||
expect(ls.getItem('prop3')).to.eq('magenta') |
|||
}) |
|||
|
|||
// Clear keys matching regex in Local Storage
|
|||
cy.get('.ls-btn').click().should(() => { |
|||
expect(localStorage.getItem('prop1')).to.eq('red') |
|||
expect(localStorage.getItem('prop2')).to.eq('blue') |
|||
expect(localStorage.getItem('prop3')).to.eq('magenta') |
|||
}) |
|||
|
|||
cy.clearLocalStorage(/prop1|2/).should((ls) => { |
|||
expect(ls.getItem('prop1')).to.be.null |
|||
expect(ls.getItem('prop2')).to.be.null |
|||
expect(ls.getItem('prop3')).to.eq('magenta') |
|||
}) |
|||
}) |
|||
}) |
@ -0,0 +1,32 @@ |
|||
/// <reference types="cypress" />
|
|||
|
|||
context('Location', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/commands/location') |
|||
}) |
|||
|
|||
it('cy.hash() - get the current URL hash', () => { |
|||
// https://on.cypress.io/hash
|
|||
cy.hash().should('be.empty') |
|||
}) |
|||
|
|||
it('cy.location() - get window.location', () => { |
|||
// https://on.cypress.io/location
|
|||
cy.location().should((location) => { |
|||
expect(location.hash).to.be.empty |
|||
expect(location.href).to.eq('https://example.cypress.io/commands/location') |
|||
expect(location.host).to.eq('example.cypress.io') |
|||
expect(location.hostname).to.eq('example.cypress.io') |
|||
expect(location.origin).to.eq('https://example.cypress.io') |
|||
expect(location.pathname).to.eq('/commands/location') |
|||
expect(location.port).to.eq('') |
|||
expect(location.protocol).to.eq('https:') |
|||
expect(location.search).to.be.empty |
|||
}) |
|||
}) |
|||
|
|||
it('cy.url() - get the current URL', () => { |
|||
// https://on.cypress.io/url
|
|||
cy.url().should('eq', 'https://example.cypress.io/commands/location') |
|||
}) |
|||
}) |
@ -0,0 +1,104 @@ |
|||
/// <reference types="cypress" />
|
|||
|
|||
context('Misc', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/commands/misc') |
|||
}) |
|||
|
|||
it('.end() - end the command chain', () => { |
|||
// https://on.cypress.io/end
|
|||
|
|||
// cy.end is useful when you want to end a chain of commands
|
|||
// and force Cypress to re-query from the root element
|
|||
cy.get('.misc-table').within(() => { |
|||
// ends the current chain and yields null
|
|||
cy.contains('Cheryl').click().end() |
|||
|
|||
// queries the entire table again
|
|||
cy.contains('Charles').click() |
|||
}) |
|||
}) |
|||
|
|||
it('cy.exec() - execute a system command', () => { |
|||
// execute a system command.
|
|||
// so you can take actions necessary for
|
|||
// your test outside the scope of Cypress.
|
|||
// https://on.cypress.io/exec
|
|||
|
|||
// we can use Cypress.platform string to
|
|||
// select appropriate command
|
|||
// https://on.cypress/io/platform
|
|||
cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`) |
|||
|
|||
// on CircleCI Windows build machines we have a failure to run bash shell
|
|||
// https://github.com/cypress-io/cypress/issues/5169
|
|||
// so skip some of the tests by passing flag "--env circle=true"
|
|||
const isCircleOnWindows = Cypress.platform === 'win32' && Cypress.env('circle') |
|||
|
|||
if (isCircleOnWindows) { |
|||
cy.log('Skipping test on CircleCI') |
|||
|
|||
return |
|||
} |
|||
|
|||
// cy.exec problem on Shippable CI
|
|||
// https://github.com/cypress-io/cypress/issues/6718
|
|||
const isShippable = Cypress.platform === 'linux' && Cypress.env('shippable') |
|||
|
|||
if (isShippable) { |
|||
cy.log('Skipping test on ShippableCI') |
|||
|
|||
return |
|||
} |
|||
|
|||
cy.exec('echo Jane Lane') |
|||
.its('stdout').should('contain', 'Jane Lane') |
|||
|
|||
if (Cypress.platform === 'win32') { |
|||
cy.exec('print cypress.json') |
|||
.its('stderr').should('be.empty') |
|||
} else { |
|||
cy.exec('cat cypress.json') |
|||
.its('stderr').should('be.empty') |
|||
|
|||
cy.exec('pwd') |
|||
.its('code').should('eq', 0) |
|||
} |
|||
}) |
|||
|
|||
it('cy.focused() - get the DOM element that has focus', () => { |
|||
// https://on.cypress.io/focused
|
|||
cy.get('.misc-form').find('#name').click() |
|||
cy.focused().should('have.id', 'name') |
|||
|
|||
cy.get('.misc-form').find('#description').click() |
|||
cy.focused().should('have.id', 'description') |
|||
}) |
|||
|
|||
context('Cypress.Screenshot', function () { |
|||
it('cy.screenshot() - take a screenshot', () => { |
|||
// https://on.cypress.io/screenshot
|
|||
cy.screenshot('my-image') |
|||
}) |
|||
|
|||
it('Cypress.Screenshot.defaults() - change default config of screenshots', function () { |
|||
Cypress.Screenshot.defaults({ |
|||
blackout: ['.foo'], |
|||
capture: 'viewport', |
|||
clip: { x: 0, y: 0, width: 200, height: 200 }, |
|||
scale: false, |
|||
disableTimersAndAnimations: true, |
|||
screenshotOnRunFailure: true, |
|||
onBeforeScreenshot () { }, |
|||
onAfterScreenshot () { }, |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
it('cy.wrap() - wrap an object', () => { |
|||
// https://on.cypress.io/wrap
|
|||
cy.wrap({ foo: 'bar' }) |
|||
.should('have.property', 'foo') |
|||
.and('include', 'bar') |
|||
}) |
|||
}) |
@ -0,0 +1,56 @@ |
|||
/// <reference types="cypress" />
|
|||
|
|||
context('Navigation', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io') |
|||
cy.get('.navbar-nav').contains('Commands').click() |
|||
cy.get('.dropdown-menu').contains('Navigation').click() |
|||
}) |
|||
|
|||
it('cy.go() - go back or forward in the browser\'s history', () => { |
|||
// https://on.cypress.io/go
|
|||
|
|||
cy.location('pathname').should('include', 'navigation') |
|||
|
|||
cy.go('back') |
|||
cy.location('pathname').should('not.include', 'navigation') |
|||
|
|||
cy.go('forward') |
|||
cy.location('pathname').should('include', 'navigation') |
|||
|
|||
// clicking back
|
|||
cy.go(-1) |
|||
cy.location('pathname').should('not.include', 'navigation') |
|||
|
|||
// clicking forward
|
|||
cy.go(1) |
|||
cy.location('pathname').should('include', 'navigation') |
|||
}) |
|||
|
|||
it('cy.reload() - reload the page', () => { |
|||
// https://on.cypress.io/reload
|
|||
cy.reload() |
|||
|
|||
// reload the page without using the cache
|
|||
cy.reload(true) |
|||
}) |
|||
|
|||
it('cy.visit() - visit a remote url', () => { |
|||
// https://on.cypress.io/visit
|
|||
|
|||
// Visit any sub-domain of your current domain
|
|||
|
|||
// Pass options to the visit
|
|||
cy.visit('https://example.cypress.io/commands/navigation', { |
|||
timeout: 50000, // increase total time for the visit to resolve
|
|||
onBeforeLoad (contentWindow) { |
|||
// contentWindow is the remote page's window object
|
|||
expect(typeof contentWindow === 'object').to.be.true |
|||
}, |
|||
onLoad (contentWindow) { |
|||
// contentWindow is the remote page's window object
|
|||
expect(typeof contentWindow === 'object').to.be.true |
|||
}, |
|||
}) |
|||
}) |
|||
}) |
@ -0,0 +1,205 @@ |
|||
/// <reference types="cypress" />
|
|||
|
|||
context('Network Requests', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/commands/network-requests') |
|||
}) |
|||
|
|||
// Manage AJAX / XHR requests in your app
|
|||
|
|||
it('cy.server() - control behavior of network requests and responses', () => { |
|||
// https://on.cypress.io/server
|
|||
|
|||
cy.server().should((server) => { |
|||
// the default options on server
|
|||
// you can override any of these options
|
|||
expect(server.delay).to.eq(0) |
|||
expect(server.method).to.eq('GET') |
|||
expect(server.status).to.eq(200) |
|||
expect(server.headers).to.be.null |
|||
expect(server.response).to.be.null |
|||
expect(server.onRequest).to.be.undefined |
|||
expect(server.onResponse).to.be.undefined |
|||
expect(server.onAbort).to.be.undefined |
|||
|
|||
// These options control the server behavior
|
|||
// affecting all requests
|
|||
|
|||
// pass false to disable existing route stubs
|
|||
expect(server.enable).to.be.true |
|||
// forces requests that don't match your routes to 404
|
|||
expect(server.force404).to.be.false |
|||
|
|||
if (Number(Cypress.version.charAt(0)) >= 5) { |
|||
// ignores requests from ever being logged or stubbed
|
|||
// @ts-ignore
|
|||
expect(server.ignore).to.be.a('function') |
|||
} |
|||
}) |
|||
|
|||
cy.server({ |
|||
method: 'POST', |
|||
delay: 1000, |
|||
status: 422, |
|||
response: {}, |
|||
}) |
|||
|
|||
// any route commands will now inherit the above options
|
|||
// from the server. anything we pass specifically
|
|||
// to route will override the defaults though.
|
|||
}) |
|||
|
|||
it('cy.request() - make an XHR request', () => { |
|||
// https://on.cypress.io/request
|
|||
cy.request('https://jsonplaceholder.cypress.io/comments') |
|||
.should((response) => { |
|||
expect(response.status).to.eq(200) |
|||
// the server sometimes gets an extra comment posted from another machine
|
|||
// which gets returned as 1 extra object
|
|||
expect(response.body).to.have.property('length').and.be.oneOf([500, 501]) |
|||
expect(response).to.have.property('headers') |
|||
expect(response).to.have.property('duration') |
|||
}) |
|||
}) |
|||
|
|||
it('cy.request() - verify response using BDD syntax', () => { |
|||
cy.request('https://jsonplaceholder.cypress.io/comments') |
|||
.then((response) => { |
|||
// https://on.cypress.io/assertions
|
|||
expect(response).property('status').to.equal(200) |
|||
expect(response).property('body').to.have.property('length').and.be.oneOf([500, 501]) |
|||
expect(response).to.include.keys('headers', 'duration') |
|||
}) |
|||
}) |
|||
|
|||
it('cy.request() with query parameters', () => { |
|||
// will execute request
|
|||
// https://jsonplaceholder.cypress.io/comments?postId=1&id=3
|
|||
cy.request({ |
|||
url: 'https://jsonplaceholder.cypress.io/comments', |
|||
qs: { |
|||
postId: 1, |
|||
id: 3, |
|||
}, |
|||
}) |
|||
.its('body') |
|||
.should('be.an', 'array') |
|||
.and('have.length', 1) |
|||
.its('0') // yields first element of the array
|
|||
.should('contain', { |
|||
postId: 1, |
|||
id: 3, |
|||
}) |
|||
}) |
|||
|
|||
it('cy.request() - pass result to the second request', () => { |
|||
// first, let's find out the userId of the first user we have
|
|||
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1') |
|||
.its('body') // yields the response object
|
|||
.its('0') // yields the first element of the returned list
|
|||
// the above two commands its('body').its('0')
|
|||
// can be written as its('body.0')
|
|||
// if you do not care about TypeScript checks
|
|||
.then((user) => { |
|||
expect(user).property('id').to.be.a('number') |
|||
// make a new post on behalf of the user
|
|||
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', { |
|||
userId: user.id, |
|||
title: 'Cypress Test Runner', |
|||
body: 'Fast, easy and reliable testing for anything that runs in a browser.', |
|||
}) |
|||
}) |
|||
// note that the value here is the returned value of the 2nd request
|
|||
// which is the new post object
|
|||
.then((response) => { |
|||
expect(response).property('status').to.equal(201) // new entity created
|
|||
expect(response).property('body').to.contain({ |
|||
title: 'Cypress Test Runner', |
|||
}) |
|||
|
|||
// we don't know the exact post id - only that it will be > 100
|
|||
// since JSONPlaceholder has built-in 100 posts
|
|||
expect(response.body).property('id').to.be.a('number') |
|||
.and.to.be.gt(100) |
|||
|
|||
// we don't know the user id here - since it was in above closure
|
|||
// so in this test just confirm that the property is there
|
|||
expect(response.body).property('userId').to.be.a('number') |
|||
}) |
|||
}) |
|||
|
|||
it('cy.request() - save response in the shared test context', () => { |
|||
// https://on.cypress.io/variables-and-aliases
|
|||
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1') |
|||
.its('body').its('0') // yields the first element of the returned list
|
|||
.as('user') // saves the object in the test context
|
|||
.then(function () { |
|||
// NOTE 👀
|
|||
// By the time this callback runs the "as('user')" command
|
|||
// has saved the user object in the test context.
|
|||
// To access the test context we need to use
|
|||
// the "function () { ... }" callback form,
|
|||
// otherwise "this" points at a wrong or undefined object!
|
|||
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', { |
|||
userId: this.user.id, |
|||
title: 'Cypress Test Runner', |
|||
body: 'Fast, easy and reliable testing for anything that runs in a browser.', |
|||
}) |
|||
.its('body').as('post') // save the new post from the response
|
|||
}) |
|||
.then(function () { |
|||
// When this callback runs, both "cy.request" API commands have finished
|
|||
// and the test context has "user" and "post" objects set.
|
|||
// Let's verify them.
|
|||
expect(this.post, 'post has the right user id').property('userId').to.equal(this.user.id) |
|||
}) |
|||
}) |
|||
|
|||
it('cy.route() - route responses to matching requests', () => { |
|||
// https://on.cypress.io/route
|
|||
|
|||
let message = 'whoa, this comment does not exist' |
|||
|
|||
cy.server() |
|||
|
|||
// Listen to GET to comments/1
|
|||
cy.route('GET', 'comments/*').as('getComment') |
|||
|
|||
// we have code that gets a comment when
|
|||
// the button is clicked in scripts.js
|
|||
cy.get('.network-btn').click() |
|||
|
|||
// https://on.cypress.io/wait
|
|||
cy.wait('@getComment').its('status').should('eq', 200) |
|||
|
|||
// Listen to POST to comments
|
|||
cy.route('POST', '/comments').as('postComment') |
|||
|
|||
// we have code that posts a comment when
|
|||
// the button is clicked in scripts.js
|
|||
cy.get('.network-post').click() |
|||
cy.wait('@postComment').should((xhr) => { |
|||
expect(xhr.requestBody).to.include('email') |
|||
expect(xhr.requestHeaders).to.have.property('Content-Type') |
|||
expect(xhr.responseBody).to.have.property('name', 'Using POST in cy.route()') |
|||
}) |
|||
|
|||
// Stub a response to PUT comments/ ****
|
|||
cy.route({ |
|||
method: 'PUT', |
|||
url: 'comments/*', |
|||
status: 404, |
|||
response: { error: message }, |
|||
delay: 500, |
|||
}).as('putComment') |
|||
|
|||
// we have code that puts a comment when
|
|||
// the button is clicked in scripts.js
|
|||
cy.get('.network-put').click() |
|||
|
|||
cy.wait('@putComment') |
|||
|
|||
// our 404 statusCode logic in scripts.js executed
|
|||
cy.get('.network-put-comment').should('contain', message) |
|||
}) |
|||
}) |
@ -0,0 +1,114 @@ |
|||
/// <reference types="cypress" />
|
|||
|
|||
context('Querying', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/commands/querying') |
|||
}) |
|||
|
|||
// The most commonly used query is 'cy.get()', you can
|
|||
// think of this like the '$' in jQuery
|
|||
|
|||
it('cy.get() - query DOM elements', () => { |
|||
// https://on.cypress.io/get
|
|||
|
|||
cy.get('#query-btn').should('contain', 'Button') |
|||
|
|||
cy.get('.query-btn').should('contain', 'Button') |
|||
|
|||
cy.get('#querying .well>button:first').should('contain', 'Button') |
|||
// ↲
|
|||
// Use CSS selectors just like jQuery
|
|||
|
|||
cy.get('[data-test-id="test-example"]').should('have.class', 'example') |
|||
|
|||
// 'cy.get()' yields jQuery object, you can get its attribute
|
|||
// by invoking `.attr()` method
|
|||
cy.get('[data-test-id="test-example"]') |
|||
.invoke('attr', 'data-test-id') |
|||
.should('equal', 'test-example') |
|||
|
|||
// or you can get element's CSS property
|
|||
cy.get('[data-test-id="test-example"]') |
|||
.invoke('css', 'position') |
|||
.should('equal', 'static') |
|||
|
|||
// or use assertions directly during 'cy.get()'
|
|||
// https://on.cypress.io/assertions
|
|||
cy.get('[data-test-id="test-example"]') |
|||
.should('have.attr', 'data-test-id', 'test-example') |
|||
.and('have.css', 'position', 'static') |
|||
}) |
|||
|
|||
it('cy.contains() - query DOM elements with matching content', () => { |
|||
// https://on.cypress.io/contains
|
|||
cy.get('.query-list') |
|||
.contains('bananas') |
|||
.should('have.class', 'third') |
|||
|
|||
// we can pass a regexp to `.contains()`
|
|||
cy.get('.query-list') |
|||
.contains(/^b\w+/) |
|||
.should('have.class', 'third') |
|||
|
|||
cy.get('.query-list') |
|||
.contains('apples') |
|||
.should('have.class', 'first') |
|||
|
|||
// passing a selector to contains will
|
|||
// yield the selector containing the text
|
|||
cy.get('#querying') |
|||
.contains('ul', 'oranges') |
|||
.should('have.class', 'query-list') |
|||
|
|||
cy.get('.query-button') |
|||
.contains('Save Form') |
|||
.should('have.class', 'btn') |
|||
}) |
|||
|
|||
it('.within() - query DOM elements within a specific element', () => { |
|||
// https://on.cypress.io/within
|
|||
cy.get('.query-form').within(() => { |
|||
cy.get('input:first').should('have.attr', 'placeholder', 'Email') |
|||
cy.get('input:last').should('have.attr', 'placeholder', 'Password') |
|||
}) |
|||
}) |
|||
|
|||
it('cy.root() - query the root DOM element', () => { |
|||
// https://on.cypress.io/root
|
|||
|
|||
// By default, root is the document
|
|||
cy.root().should('match', 'html') |
|||
|
|||
cy.get('.query-ul').within(() => { |
|||
// In this within, the root is now the ul DOM element
|
|||
cy.root().should('have.class', 'query-ul') |
|||
}) |
|||
}) |
|||
|
|||
it('best practices - selecting elements', () => { |
|||
// https://on.cypress.io/best-practices#Selecting-Elements
|
|||
cy.get('[data-cy=best-practices-selecting-elements]').within(() => { |
|||
// Worst - too generic, no context
|
|||
cy.get('button').click() |
|||
|
|||
// Bad. Coupled to styling. Highly subject to change.
|
|||
cy.get('.btn.btn-large').click() |
|||
|
|||
// Average. Coupled to the `name` attribute which has HTML semantics.
|
|||
cy.get('[name=submission]').click() |
|||
|
|||
// Better. But still coupled to styling or JS event listeners.
|
|||
cy.get('#main').click() |
|||
|
|||
// Slightly better. Uses an ID but also ensures the element
|
|||
// has an ARIA role attribute
|
|||
cy.get('#main[role=button]').click() |
|||
|
|||
// Much better. But still coupled to text content that may change.
|
|||
cy.contains('Submit').click() |
|||
|
|||
// Best. Insulated from all changes.
|
|||
cy.get('[data-cy=submit]').click() |
|||
}) |
|||
}) |
|||
}) |
@ -0,0 +1,205 @@ |
|||
/// <reference types="cypress" />
|
|||
// remove no check once Cypress.sinon is typed
|
|||
// https://github.com/cypress-io/cypress/issues/6720
|
|||
|
|||
context('Spies, Stubs, and Clock', () => { |
|||
it('cy.spy() - wrap a method in a spy', () => { |
|||
// https://on.cypress.io/spy
|
|||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') |
|||
|
|||
const obj = { |
|||
foo () {}, |
|||
} |
|||
|
|||
const spy = cy.spy(obj, 'foo').as('anyArgs') |
|||
|
|||
obj.foo() |
|||
|
|||
expect(spy).to.be.called |
|||
}) |
|||
|
|||
it('cy.spy() retries until assertions pass', () => { |
|||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') |
|||
|
|||
const obj = { |
|||
/** |
|||
* Prints the argument passed |
|||
* @param x {any} |
|||
*/ |
|||
foo (x) { |
|||
console.log('obj.foo called with', x) |
|||
}, |
|||
} |
|||
|
|||
cy.spy(obj, 'foo').as('foo') |
|||
|
|||
setTimeout(() => { |
|||
obj.foo('first') |
|||
}, 500) |
|||
|
|||
setTimeout(() => { |
|||
obj.foo('second') |
|||
}, 2500) |
|||
|
|||
cy.get('@foo').should('have.been.calledTwice') |
|||
}) |
|||
|
|||
it('cy.stub() - create a stub and/or replace a function with stub', () => { |
|||
// https://on.cypress.io/stub
|
|||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') |
|||
|
|||
const obj = { |
|||
/** |
|||
* prints both arguments to the console |
|||
* @param a {string} |
|||
* @param b {string} |
|||
*/ |
|||
foo (a, b) { |
|||
console.log('a', a, 'b', b) |
|||
}, |
|||
} |
|||
|
|||
const stub = cy.stub(obj, 'foo').as('foo') |
|||
|
|||
obj.foo('foo', 'bar') |
|||
|
|||
expect(stub).to.be.called |
|||
}) |
|||
|
|||
it('cy.clock() - control time in the browser', () => { |
|||
// https://on.cypress.io/clock
|
|||
|
|||
// create the date in UTC so its always the same
|
|||
// no matter what local timezone the browser is running in
|
|||
const now = new Date(Date.UTC(2017, 2, 14)).getTime() |
|||
|
|||
cy.clock(now) |
|||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') |
|||
cy.get('#clock-div').click() |
|||
.should('have.text', '1489449600') |
|||
}) |
|||
|
|||
it('cy.tick() - move time in the browser', () => { |
|||
// https://on.cypress.io/tick
|
|||
|
|||
// create the date in UTC so its always the same
|
|||
// no matter what local timezone the browser is running in
|
|||
const now = new Date(Date.UTC(2017, 2, 14)).getTime() |
|||
|
|||
cy.clock(now) |
|||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') |
|||
cy.get('#tick-div').click() |
|||
.should('have.text', '1489449600') |
|||
|
|||
cy.tick(10000) // 10 seconds passed
|
|||
cy.get('#tick-div').click() |
|||
.should('have.text', '1489449610') |
|||
}) |
|||
|
|||
it('cy.stub() matches depending on arguments', () => { |
|||
// see all possible matchers at
|
|||
// https://sinonjs.org/releases/latest/matchers/
|
|||
const greeter = { |
|||
/** |
|||
* Greets a person |
|||
* @param {string} name |
|||
*/ |
|||
greet (name) { |
|||
return `Hello, ${name}!` |
|||
}, |
|||
} |
|||
|
|||
cy.stub(greeter, 'greet') |
|||
.callThrough() // if you want non-matched calls to call the real method
|
|||
.withArgs(Cypress.sinon.match.string).returns('Hi') |
|||
.withArgs(Cypress.sinon.match.number).throws(new Error('Invalid name')) |
|||
|
|||
expect(greeter.greet('World')).to.equal('Hi') |
|||
// @ts-ignore
|
|||
expect(() => greeter.greet(42)).to.throw('Invalid name') |
|||
expect(greeter.greet).to.have.been.calledTwice |
|||
|
|||
// non-matched calls goes the actual method
|
|||
// @ts-ignore
|
|||
expect(greeter.greet()).to.equal('Hello, undefined!') |
|||
}) |
|||
|
|||
it('matches call arguments using Sinon matchers', () => { |
|||
// see all possible matchers at
|
|||
// https://sinonjs.org/releases/latest/matchers/
|
|||
const calculator = { |
|||
/** |
|||
* returns the sum of two arguments |
|||
* @param a {number} |
|||
* @param b {number} |
|||
*/ |
|||
add (a, b) { |
|||
return a + b |
|||
}, |
|||
} |
|||
|
|||
const spy = cy.spy(calculator, 'add').as('add') |
|||
|
|||
expect(calculator.add(2, 3)).to.equal(5) |
|||
|
|||
// if we want to assert the exact values used during the call
|
|||
expect(spy).to.be.calledWith(2, 3) |
|||
|
|||
// let's confirm "add" method was called with two numbers
|
|||
expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number) |
|||
|
|||
// alternatively, provide the value to match
|
|||
expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3)) |
|||
|
|||
// match any value
|
|||
expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3) |
|||
|
|||
// match any value from a list
|
|||
expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3) |
|||
|
|||
/** |
|||
* Returns true if the given number is event |
|||
* @param {number} x |
|||
*/ |
|||
const isEven = (x) => x % 2 === 0 |
|||
|
|||
// expect the value to pass a custom predicate function
|
|||
// the second argument to "sinon.match(predicate, message)" is
|
|||
// shown if the predicate does not pass and assertion fails
|
|||
expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, 'isEven'), 3) |
|||
|
|||
/** |
|||
* Returns a function that checks if a given number is larger than the limit |
|||
* @param {number} limit |
|||
* @returns {(x: number) => boolean} |
|||
*/ |
|||
const isGreaterThan = (limit) => (x) => x > limit |
|||
|
|||
/** |
|||
* Returns a function that checks if a given number is less than the limit |
|||
* @param {number} limit |
|||
* @returns {(x: number) => boolean} |
|||
*/ |
|||
const isLessThan = (limit) => (x) => x < limit |
|||
|
|||
// you can combine several matchers using "and", "or"
|
|||
expect(spy).to.be.calledWith( |
|||
Cypress.sinon.match.number, |
|||
Cypress.sinon.match(isGreaterThan(2), '> 2').and(Cypress.sinon.match(isLessThan(4), '< 4')), |
|||
) |
|||
|
|||
expect(spy).to.be.calledWith( |
|||
Cypress.sinon.match.number, |
|||
Cypress.sinon.match(isGreaterThan(200), '> 200').or(Cypress.sinon.match(3)), |
|||
) |
|||
|
|||
// matchers can be used from BDD assertions
|
|||
cy.get('@add').should('have.been.calledWith', |
|||
Cypress.sinon.match.number, Cypress.sinon.match(3)) |
|||
|
|||
// you can alias matchers for shorter test code
|
|||
const { match: M } = Cypress.sinon |
|||
|
|||
cy.get('@add').should('have.been.calledWith', M.number, M(3)) |
|||
}) |
|||
}) |
@ -0,0 +1,121 @@ |
|||
/// <reference types="cypress" />
|
|||
|
|||
context('Traversal', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/commands/traversal') |
|||
}) |
|||
|
|||
it('.children() - get child DOM elements', () => { |
|||
// https://on.cypress.io/children
|
|||
cy.get('.traversal-breadcrumb') |
|||
.children('.active') |
|||
.should('contain', 'Data') |
|||
}) |
|||
|
|||
it('.closest() - get closest ancestor DOM element', () => { |
|||
// https://on.cypress.io/closest
|
|||
cy.get('.traversal-badge') |
|||
.closest('ul') |
|||
.should('have.class', 'list-group') |
|||
}) |
|||
|
|||
it('.eq() - get a DOM element at a specific index', () => { |
|||
// https://on.cypress.io/eq
|
|||
cy.get('.traversal-list>li') |
|||
.eq(1).should('contain', 'siamese') |
|||
}) |
|||
|
|||
it('.filter() - get DOM elements that match the selector', () => { |
|||
// https://on.cypress.io/filter
|
|||
cy.get('.traversal-nav>li') |
|||
.filter('.active').should('contain', 'About') |
|||
}) |
|||
|
|||
it('.find() - get descendant DOM elements of the selector', () => { |
|||
// https://on.cypress.io/find
|
|||
cy.get('.traversal-pagination') |
|||
.find('li').find('a') |
|||
.should('have.length', 7) |
|||
}) |
|||
|
|||
it('.first() - get first DOM element', () => { |
|||
// https://on.cypress.io/first
|
|||
cy.get('.traversal-table td') |
|||
.first().should('contain', '1') |
|||
}) |
|||
|
|||
it('.last() - get last DOM element', () => { |
|||
// https://on.cypress.io/last
|
|||
cy.get('.traversal-buttons .btn') |
|||
.last().should('contain', 'Submit') |
|||
}) |
|||
|
|||
it('.next() - get next sibling DOM element', () => { |
|||
// https://on.cypress.io/next
|
|||
cy.get('.traversal-ul') |
|||
.contains('apples').next().should('contain', 'oranges') |
|||
}) |
|||
|
|||
it('.nextAll() - get all next sibling DOM elements', () => { |
|||
// https://on.cypress.io/nextall
|
|||
cy.get('.traversal-next-all') |
|||
.contains('oranges') |
|||
.nextAll().should('have.length', 3) |
|||
}) |
|||
|
|||
it('.nextUntil() - get next sibling DOM elements until next el', () => { |
|||
// https://on.cypress.io/nextuntil
|
|||
cy.get('#veggies') |
|||
.nextUntil('#nuts').should('have.length', 3) |
|||
}) |
|||
|
|||
it('.not() - remove DOM elements from set of DOM elements', () => { |
|||
// https://on.cypress.io/not
|
|||
cy.get('.traversal-disabled .btn') |
|||
.not('[disabled]').should('not.contain', 'Disabled') |
|||
}) |
|||
|
|||
it('.parent() - get parent DOM element from DOM elements', () => { |
|||
// https://on.cypress.io/parent
|
|||
cy.get('.traversal-mark') |
|||
.parent().should('contain', 'Morbi leo risus') |
|||
}) |
|||
|
|||
it('.parents() - get parent DOM elements from DOM elements', () => { |
|||
// https://on.cypress.io/parents
|
|||
cy.get('.traversal-cite') |
|||
.parents().should('match', 'blockquote') |
|||
}) |
|||
|
|||
it('.parentsUntil() - get parent DOM elements from DOM elements until el', () => { |
|||
// https://on.cypress.io/parentsuntil
|
|||
cy.get('.clothes-nav') |
|||
.find('.active') |
|||
.parentsUntil('.clothes-nav') |
|||
.should('have.length', 2) |
|||
}) |
|||
|
|||
it('.prev() - get previous sibling DOM element', () => { |
|||
// https://on.cypress.io/prev
|
|||
cy.get('.birds').find('.active') |
|||
.prev().should('contain', 'Lorikeets') |
|||
}) |
|||
|
|||
it('.prevAll() - get all previous sibling DOM elements', () => { |
|||
// https://on.cypress.io/prevAll
|
|||
cy.get('.fruits-list').find('.third') |
|||
.prevAll().should('have.length', 2) |
|||
}) |
|||
|
|||
it('.prevUntil() - get all previous sibling DOM elements until el', () => { |
|||
// https://on.cypress.io/prevUntil
|
|||
cy.get('.foods-list').find('#nuts') |
|||
.prevUntil('#veggies').should('have.length', 3) |
|||
}) |
|||
|
|||
it('.siblings() - get all sibling DOM elements', () => { |
|||
// https://on.cypress.io/siblings
|
|||
cy.get('.traversal-pills .active') |
|||
.siblings().should('have.length', 2) |
|||
}) |
|||
}) |
@ -0,0 +1,136 @@ |
|||
/// <reference types="cypress" />
|
|||
|
|||
context('Utilities', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/utilities') |
|||
}) |
|||
|
|||
it('Cypress._ - call a lodash method', () => { |
|||
// https://on.cypress.io/_
|
|||
cy.request('https://jsonplaceholder.cypress.io/users') |
|||
.then((response) => { |
|||
let ids = Cypress._.chain(response.body).map('id').take(3).value() |
|||
|
|||
expect(ids).to.deep.eq([1, 2, 3]) |
|||
}) |
|||
}) |
|||
|
|||
it('Cypress.$ - call a jQuery method', () => { |
|||
// https://on.cypress.io/$
|
|||
let $li = Cypress.$('.utility-jquery li:first') |
|||
|
|||
cy.wrap($li) |
|||
.should('not.have.class', 'active') |
|||
.click() |
|||
.should('have.class', 'active') |
|||
}) |
|||
|
|||
it('Cypress.Blob - blob utilities and base64 string conversion', () => { |
|||
// https://on.cypress.io/blob
|
|||
cy.get('.utility-blob').then(($div) => { |
|||
// https://github.com/nolanlawson/blob-util#imgSrcToDataURL
|
|||
// get the dataUrl string for the javascript-logo
|
|||
return Cypress.Blob.imgSrcToDataURL('https://example.cypress.io/assets/img/javascript-logo.png', undefined, 'anonymous') |
|||
.then((dataUrl) => { |
|||
// create an <img> element and set its src to the dataUrl
|
|||
let img = Cypress.$('<img />', { src: dataUrl }) |
|||
|
|||
// need to explicitly return cy here since we are initially returning
|
|||
// the Cypress.Blob.imgSrcToDataURL promise to our test
|
|||
// append the image
|
|||
$div.append(img) |
|||
|
|||
cy.get('.utility-blob img').click() |
|||
.should('have.attr', 'src', dataUrl) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
it('Cypress.minimatch - test out glob patterns against strings', () => { |
|||
// https://on.cypress.io/minimatch
|
|||
let matching = Cypress.minimatch('/users/1/comments', '/users/*/comments', { |
|||
matchBase: true, |
|||
}) |
|||
|
|||
expect(matching, 'matching wildcard').to.be.true |
|||
|
|||
matching = Cypress.minimatch('/users/1/comments/2', '/users/*/comments', { |
|||
matchBase: true, |
|||
}) |
|||
|
|||
expect(matching, 'comments').to.be.false |
|||
|
|||
// ** matches against all downstream path segments
|
|||
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/**', { |
|||
matchBase: true, |
|||
}) |
|||
|
|||
expect(matching, 'comments').to.be.true |
|||
|
|||
// whereas * matches only the next path segment
|
|||
|
|||
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/*', { |
|||
matchBase: false, |
|||
}) |
|||
|
|||
expect(matching, 'comments').to.be.false |
|||
}) |
|||
|
|||
it('Cypress.moment() - format or parse dates using a moment method', () => { |
|||
// https://on.cypress.io/moment
|
|||
const time = Cypress.moment('2014-04-25T19:38:53.196Z').utc().format('h:mm A') |
|||
|
|||
expect(time).to.be.a('string') |
|||
|
|||
cy.get('.utility-moment').contains('3:38 PM') |
|||
.should('have.class', 'badge') |
|||
|
|||
// the time in the element should be between 3pm and 5pm
|
|||
const start = Cypress.moment('3:00 PM', 'LT') |
|||
const end = Cypress.moment('5:00 PM', 'LT') |
|||
|
|||
cy.get('.utility-moment .badge') |
|||
.should(($el) => { |
|||
// parse American time like "3:38 PM"
|
|||
const m = Cypress.moment($el.text().trim(), 'LT') |
|||
|
|||
// display hours + minutes + AM|PM
|
|||
const f = 'h:mm A' |
|||
|
|||
expect(m.isBetween(start, end), |
|||
`${m.format(f)} should be between ${start.format(f)} and ${end.format(f)}`).to.be.true |
|||
}) |
|||
}) |
|||
|
|||
it('Cypress.Promise - instantiate a bluebird promise', () => { |
|||
// https://on.cypress.io/promise
|
|||
let waited = false |
|||
|
|||
/** |
|||
* @return Bluebird<string> |
|||
*/ |
|||
function waitOneSecond () { |
|||
// return a promise that resolves after 1 second
|
|||
// @ts-ignore TS2351 (new Cypress.Promise)
|
|||
return new Cypress.Promise((resolve, reject) => { |
|||
setTimeout(() => { |
|||
// set waited to true
|
|||
waited = true |
|||
|
|||
// resolve with 'foo' string
|
|||
resolve('foo') |
|||
}, 1000) |
|||
}) |
|||
} |
|||
|
|||
cy.then(() => { |
|||
// return a promise to cy.then() that
|
|||
// is awaited until it resolves
|
|||
// @ts-ignore TS7006
|
|||
return waitOneSecond().then((str) => { |
|||
expect(str).to.eq('foo') |
|||
expect(waited).to.be.true |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
@ -0,0 +1,59 @@ |
|||
/// <reference types="cypress" />
|
|||
|
|||
context('Viewport', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/commands/viewport') |
|||
}) |
|||
|
|||
it('cy.viewport() - set the viewport size and dimension', () => { |
|||
// https://on.cypress.io/viewport
|
|||
|
|||
cy.get('#navbar').should('be.visible') |
|||
cy.viewport(320, 480) |
|||
|
|||
// the navbar should have collapse since our screen is smaller
|
|||
cy.get('#navbar').should('not.be.visible') |
|||
cy.get('.navbar-toggle').should('be.visible').click() |
|||
cy.get('.nav').find('a').should('be.visible') |
|||
|
|||
// lets see what our app looks like on a super large screen
|
|||
cy.viewport(2999, 2999) |
|||
|
|||
// cy.viewport() accepts a set of preset sizes
|
|||
// to easily set the screen to a device's width and height
|
|||
|
|||
// We added a cy.wait() between each viewport change so you can see
|
|||
// the change otherwise it is a little too fast to see :)
|
|||
|
|||
cy.viewport('macbook-15') |
|||
cy.wait(200) |
|||
cy.viewport('macbook-13') |
|||
cy.wait(200) |
|||
cy.viewport('macbook-11') |
|||
cy.wait(200) |
|||
cy.viewport('ipad-2') |
|||
cy.wait(200) |
|||
cy.viewport('ipad-mini') |
|||
cy.wait(200) |
|||
cy.viewport('iphone-6+') |
|||
cy.wait(200) |
|||
cy.viewport('iphone-6') |
|||
cy.wait(200) |
|||
cy.viewport('iphone-5') |
|||
cy.wait(200) |
|||
cy.viewport('iphone-4') |
|||
cy.wait(200) |
|||
cy.viewport('iphone-3') |
|||
cy.wait(200) |
|||
|
|||
// cy.viewport() accepts an orientation for all presets
|
|||
// the default orientation is 'portrait'
|
|||
cy.viewport('ipad-2', 'portrait') |
|||
cy.wait(200) |
|||
cy.viewport('iphone-4', 'landscape') |
|||
cy.wait(200) |
|||
|
|||
// The viewport will be reset back to the default dimensions
|
|||
// in between tests (the default can be set in cypress.json)
|
|||
}) |
|||
}) |
@ -0,0 +1,33 @@ |
|||
/// <reference types="cypress" />
|
|||
|
|||
context('Waiting', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/commands/waiting') |
|||
}) |
|||
// BE CAREFUL of adding unnecessary wait times.
|
|||
// https://on.cypress.io/best-practices#Unnecessary-Waiting
|
|||
|
|||
// https://on.cypress.io/wait
|
|||
it('cy.wait() - wait for a specific amount of time', () => { |
|||
cy.get('.wait-input1').type('Wait 1000ms after typing') |
|||
cy.wait(1000) |
|||
cy.get('.wait-input2').type('Wait 1000ms after typing') |
|||
cy.wait(1000) |
|||
cy.get('.wait-input3').type('Wait 1000ms after typing') |
|||
cy.wait(1000) |
|||
}) |
|||
|
|||
it('cy.wait() - wait for a specific route', () => { |
|||
cy.server() |
|||
|
|||
// Listen to GET to comments/1
|
|||
cy.route('GET', 'comments/*').as('getComment') |
|||
|
|||
// we have code that gets a comment when
|
|||
// the button is clicked in scripts.js
|
|||
cy.get('.network-btn').click() |
|||
|
|||
// wait for GET comments/1
|
|||
cy.wait('@getComment').its('status').should('eq', 200) |
|||
}) |
|||
}) |
@ -0,0 +1,22 @@ |
|||
/// <reference types="cypress" />
|
|||
|
|||
context('Window', () => { |
|||
beforeEach(() => { |
|||
cy.visit('https://example.cypress.io/commands/window') |
|||
}) |
|||
|
|||
it('cy.window() - get the global window object', () => { |
|||
// https://on.cypress.io/window
|
|||
cy.window().should('have.property', 'top') |
|||
}) |
|||
|
|||
it('cy.document() - get the document object', () => { |
|||
// https://on.cypress.io/document
|
|||
cy.document().should('have.property', 'charset').and('eq', 'UTF-8') |
|||
}) |
|||
|
|||
it('cy.title() - get the title', () => { |
|||
// https://on.cypress.io/title
|
|||
cy.title().should('include', 'Kitchen Sink') |
|||
}) |
|||
}) |
@ -0,0 +1,5 @@ |
|||
{ |
|||
"name": "Using fixtures to represent data", |
|||
"email": "hello@cypress.io", |
|||
"body": "Fixtures are a great way to mock data for responses to routes" |
|||
} |
@ -0,0 +1,9 @@ |
|||
// https://docs.cypress.io/api/introduction/api.html
|
|||
|
|||
describe('Homepage', () => { |
|||
it('Shows correct text', () => { |
|||
cy.visit('/') |
|||
|
|||
cy.contains('h1', 'Hello Vue 3 + Vite + Tailwind CSS') |
|||
}) |
|||
}) |
@ -0,0 +1,21 @@ |
|||
/// <reference types="cypress" />
|
|||
// ***********************************************************
|
|||
// 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
|
|||
} |
@ -0,0 +1,25 @@ |
|||
// ***********************************************
|
|||
// 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
|
|||
// ***********************************************
|
|||
//
|
|||
//
|
|||
// -- 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) => { ... })
|
@ -0,0 +1,34 @@ |
|||
/// <reference types="cypress" />
|
|||
// ***********************************************************
|
|||
// 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' |
|||
|
|||
Cypress.on('window:before:load', (win) => { |
|||
win.handleFromCypress = function (request) { |
|||
return fetch(request.url, { |
|||
method: request.method, |
|||
headers: request.requestHeaders, |
|||
body: request.requestBody, |
|||
}).then((res) => { |
|||
const content = |
|||
res.headers.map['content-type'] === 'application/json' ? res.json() : res.text() |
|||
return new Promise((resolve) => { |
|||
content.then((body) => resolve([res.status, res.headers, body])) |
|||
}) |
|||
}) |
|||
} |
|||
}) |
@ -0,0 +1,18 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"target": "esnext", |
|||
"module": "esnext", |
|||
"moduleResolution": "node", |
|||
"strict": true, |
|||
"jsx": "preserve", |
|||
"sourceMap": false, |
|||
"lib": ["esnext", "dom"], |
|||
"types": ["vite/client"], |
|||
"plugins": [{ "name": "@vuedx/typescript-plugin-vue" }], |
|||
"paths": { |
|||
"@/*": ["./src/*"] |
|||
} |
|||
}, |
|||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"], |
|||
"exclude": ["node_modules", "dist", "public", "tests"] |
|||
} |
@ -0,0 +1,16 @@ |
|||
import * as path from 'path' |
|||
import { defineConfig } from 'vite' |
|||
import vue from '@vitejs/plugin-vue' |
|||
import pkg from './package.json' |
|||
|
|||
export default defineConfig({ |
|||
plugins: [vue()], |
|||
resolve: { |
|||
alias: { |
|||
'@': path.resolve(__dirname, './src'), |
|||
}, |
|||
}, |
|||
define: { |
|||
_APP_VERSION: JSON.stringify(pkg.version), |
|||
}, |
|||
}) |