First commit with working test of all types

This commit is contained in:
Elvanos 2023-08-24 22:40:07 +02:00
parent a3f637d55a
commit a50671c3c1
53 changed files with 3845 additions and 67 deletions

View file

@ -9,7 +9,7 @@ module.exports = {
// `parser: 'vue-eslint-parser'` is already included with any 'plugin:vue/**' config and should be omitted // `parser: 'vue-eslint-parser'` is already included with any 'plugin:vue/**' config and should be omitted
parserOptions: { parserOptions: {
parser: require.resolve('@typescript-eslint/parser'), parser: require.resolve('@typescript-eslint/parser'),
extraFileExtensions: [ '.vue' ] extraFileExtensions: ['.vue']
}, },
env: { env: {
@ -32,11 +32,12 @@ module.exports = {
// but leave only one uncommented! // but leave only one uncommented!
// See https://eslint.vuejs.org/rules/#available-rules // See https://eslint.vuejs.org/rules/#available-rules
'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention) 'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention)
// 'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability) 'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
// 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead) 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
'standard',
'plugin:cypress/recommended'
'standard'
], ],
plugins: [ plugins: [
@ -46,7 +47,7 @@ module.exports = {
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files // https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files
// required to lint *.vue files // required to lint *.vue files
'vue' 'vue'
], ],
globals: { globals: {
@ -64,7 +65,7 @@ module.exports = {
// add your custom rules here // add your custom rules here
rules: { rules: {
// allow async-await // allow async-await
'generator-star-spacing': 'off', 'generator-star-spacing': 'off',
// allow paren-less arrow functions // allow paren-less arrow functions
@ -84,7 +85,7 @@ module.exports = {
// The core 'import/named' rules // The core 'import/named' rules
// does not work with type definitions // does not work with type definitions
'import/named': 'off', 'import/named': 'off',
'prefer-promise-reject-errors': 'off', 'prefer-promise-reject-errors': 'off',
quotes: ['warn', 'single', { avoidEscape: true }], quotes: ['warn', 'single', { avoidEscape: true }],

3
.gitignore vendored
View file

@ -35,3 +35,6 @@ yarn-error.log*
# local .env files # local .env files
.env.local* .env.local*
.nyc_output
coverage/

3
.nycrc Normal file
View file

@ -0,0 +1,3 @@
{
"extends": "@quasar/quasar-app-extension-testing-e2e-cypress/nyc-config-preset"
}

View file

@ -4,6 +4,8 @@ A worldbuilding database manager
Use Yarn 1.22.19 or stuff is gonna bug out. Use Yarn 1.22.19 or stuff is gonna bug out.
Make sure you are running this with Node v16.17.0 ("nvm" is great for these older versions)
## Install the dependencies and set up the project ## Install the dependencies and set up the project
```bash ```bash
yarn yarn
@ -14,7 +16,7 @@ yarn
quasar dev -m electron quasar dev -m electron
``` ```
### Lint the files ### Lint the files manually (you can do this, but like... use an plugin of some kind in your IDE please T_T)
```bash ```bash
yarn lint yarn lint
``` ```
@ -24,5 +26,24 @@ yarn lint
quasar build quasar build
``` ```
### Testing:
##### Unit test - with pretty web-UI
```bash
test:unit:ui
```
##### Unit test - without any UI, fully in a terminal
```bash
test:unit
```
##### Component test - via Cypress, pick Electron on the config screen (I suggest turning on the electron dev window first, the test is a bit buggy sometimes)
```bash
test:component
```
##### e2e test - via Cypress, pick Electron on the config screen (I suggest turning on the electron dev window first, the test is a bit buggy sometimes)
```bash
test:e2e
```
### Customize the configuration ### Customize the configuration
See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js). See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js).

29
cypress.config.ts Normal file
View file

@ -0,0 +1,29 @@
import registerCodeCoverageTasks from '@cypress/code-coverage/task';
import { injectQuasarDevServerConfig } from '@quasar/quasar-app-extension-testing-e2e-cypress/cct-dev-server';
import { defineConfig } from 'cypress';
export default defineConfig({
fixturesFolder: 'test/cypress/fixtures',
screenshotsFolder: 'test/cypress/screenshots',
videosFolder: 'test/cypress/videos',
video: true,
e2e: {
setupNodeEvents(on, config) {
registerCodeCoverageTasks(on, config);
return config;
},
baseUrl: 'http://localhost:9000/',
supportFile: 'test/cypress/support/e2e.ts',
specPattern: 'test/cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
},
component: {
setupNodeEvents(on, config) {
registerCodeCoverageTasks(on, config);
return config;
},
supportFile: 'test/cypress/support/component.ts',
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
indexHtmlFile: 'test/cypress/support/component-index.html',
devServer: injectQuasarDevServerConfig(),
},
});

View file

@ -7,9 +7,16 @@
"private": true, "private": true,
"scripts": { "scripts": {
"lint": "eslint --ext .js,.ts,.vue ./", "lint": "eslint --ext .js,.ts,.vue ./",
"test": "echo \"No test specified\" && exit 0", "test": "echo \"See package.json => scripts for available tests.\" && exit 0",
"dev": "quasar dev", "dev": "quasar dev",
"build": "quasar build" "build": "quasar build",
"test:unit:ui": "vitest --ui",
"test:unit": "vitest",
"test:unit:ci": "vitest run",
"test:e2e": "cross-env NODE_ENV=test start-test \"quasar dev\" http-get://localhost:9000 \"cypress open --e2e\"",
"test:e2e:ci": "cross-env NODE_ENV=test start-test \"quasar dev\" http-get://localhost:9000 \"cypress run --e2e\"",
"test:component": "cross-env NODE_ENV=test cypress open --component",
"test:component:ci": "cross-env NODE_ENV=test cypress run --component"
}, },
"dependencies": { "dependencies": {
"@electron/remote": "^2.0.10", "@electron/remote": "^2.0.10",
@ -24,18 +31,27 @@
"devDependencies": { "devDependencies": {
"@intlify/vite-plugin-vue-i18n": "^3.3.1", "@intlify/vite-plugin-vue-i18n": "^3.3.1",
"@quasar/app-vite": "^1.3.0", "@quasar/app-vite": "^1.3.0",
"@quasar/quasar-app-extension-testing": "^2.1.0",
"@quasar/quasar-app-extension-testing-e2e-cypress": "^5.1.0",
"@quasar/quasar-app-extension-testing-unit-vitest": "^0.1.0",
"@types/node": "^12.20.21", "@types/node": "^12.20.21",
"@typescript-eslint/eslint-plugin": "^5.10.0", "@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0", "@typescript-eslint/parser": "^5.10.0",
"@vitest/ui": "^0.15.0",
"@vue/test-utils": "^2.0.0",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"cypress": "^12.2.0",
"electron": "^25.5.0", "electron": "^25.5.0",
"eslint": "^8.10.0", "eslint": "^8.10.0",
"eslint-config-standard": "^17.0.0", "eslint-config-standard": "^17.0.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-import": "^2.19.1", "eslint-plugin-import": "^2.19.1",
"eslint-plugin-n": "^15.0.0", "eslint-plugin-n": "^15.0.0",
"eslint-plugin-promise": "^6.0.0", "eslint-plugin-promise": "^6.0.0",
"eslint-plugin-vue": "^9.0.0", "eslint-plugin-vue": "^9.0.0",
"typescript": "^4.5.4" "jsdom": "^22.1.0",
"typescript": "^4.5.4",
"vitest": "^0.15.0"
}, },
"engines": { "engines": {
"node": "^18 || ^16 || ^14.19", "node": "^18 || ^16 || ^14.19",

22
quasar.extensions.json Normal file
View file

@ -0,0 +1,22 @@
{
"@quasar/testing": {
"harnesses": [
"unit-vitest@alpha",
"e2e-cypress"
]
},
"@quasar/testing-unit-vitest": {
"options": [
"scripts",
"typescript",
"ui"
]
},
"@quasar/testing-e2e-cypress": {
"options": [
"scripts",
"typescript",
"code-coverage"
]
}
}

11
quasar.testing.json Normal file
View file

@ -0,0 +1,11 @@
{
"unit-vitest": {
"runnerCommand": "vitest run"
},
"e2e-cypress": {
"runnerCommand": "cross-env NODE_ENV=test start-test \"quasar dev\" http-get://localhost:9000 \"cypress run --e2e\""
},
"component-cypress": {
"runnerCommand": "cross-env NODE_ENV=test cypress run --component"
}
}

View file

@ -14,7 +14,9 @@
<q-item-section> <q-item-section>
<q-item-label>{{ title }}</q-item-label> <q-item-label>{{ title }}</q-item-label>
<q-item-label caption>{{ caption }}</q-item-label> <q-item-label caption>
{{ caption }}
</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
</template> </template>

View file

@ -2,7 +2,11 @@
<div> <div>
<p>{{ title }}</p> <p>{{ title }}</p>
<ul> <ul>
<li v-for="todo in todos" :key="todo.id" @click="increment"> <li
v-for="todo in todos"
:key="todo.id"
@click="increment"
>
{{ todo.id }} - {{ todo.content }} {{ todo.id }} - {{ todo.content }}
</li> </li>
</ul> </ul>

View file

@ -0,0 +1,132 @@
<template>
<q-btn-group
flat
class="appWindowButtons bg-dark"
>
<!-- Minimize button-->
<q-btn
flat
:ripple="false"
:class="{'minimize': osSystem === 'darwin'}"
dark
size='sm'
@click="minimizeWindow">
<q-icon name="mdi-window-minimize"></q-icon>
</q-btn>
<!-- MinMax button-->
<q-btn
flat
:ripple="false"
:class="{'minMax': osSystem === 'darwin'}"
dark
size='sm'
>
<q-icon :name="(isMaximized)? 'mdi-window-restore' : 'mdi-window-maximize'"></q-icon>
</q-btn>
<!-- Close button-->
<q-btn
flat
:ripple="false"
dark
size='sm'
:class="[{'close': osSystem === 'darwin'}]"
>
<q-icon name="mdi-window-close"></q-icon>
</q-btn>
</q-btn-group>
</template>
<script lang="ts">
export default {
/****************************************************************/
// Basic component functionality
/****************************************************************/
/**
* Determines if the window is maximed or not
*/
isMaximized = false
/**
* Gets the currently used OS
*/
osSystem = remote.process.platform
/**
* Currently opened window
*/
currentWindow = remote.getCurrentWindow()
/**
* Checks if the window is currently maximized or not
*/
checkIfMaximized () {
this.isMaximized = this.currentWindow.isMaximized()
}
/**
* Minimizes the current window
*/
minimizeWindow () {
this.currentWindow.minimize()
}
/**
* Resizes the window to either smaller or maximized
*/
resizeWindow () {
if (this.currentWindow.isMaximized()) {
this.currentWindow.unmaximize()
}
else {
this.currentWindow.maximize()
}
}
created () {
window.addEventListener("resize", this.checkIfMaximized)
this.checkIfMaximized()
}
destroyed () {
window.addEventListener("resize", this.checkIfMaximized)
}
/****************************************************************/
// Close project dialog
/****************************************************************/
projectCloseCheckDialogTrigger: string | false = false
projectCloseCheckDialogClose () {
this.projectCloseCheckDialogTrigger = false
}
projectCloseCheckDialogAssignUID () {
this.projectCloseCheckDialogTrigger = this.generateUID()
}
}
</script>
<style lang="scss" scoped>
.appWindowButtons {
border-radius: 0;
position: fixed;
right: 0;
top: 0;
height: 40px;
z-index: 99999999;
color: #fff;
-webkit-app-region: no-drag;
}
</style>
<style lang="scss" >
</style>

View file

@ -0,0 +1,19 @@
<template>
<q-btn
data-cy="button"
label="test emit"
color="positive"
rounded
icon="edit"
@click="$emit('test')"
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'QuasarButton',
emits: { test: () => true }
})
</script>

View file

@ -0,0 +1,44 @@
<template>
<q-checkbox
v-model="checked"
data-cy="checkbox"
/>
<q-toggle
v-model="toggled"
data-cy="toggle"
/>
<q-radio
v-model="selected"
val="Value1"
data-cy="radio-1"
>
Value1
</q-radio>
<q-radio
v-model="selected"
val="Value2"
data-cy="radio-2"
>
Value2
</q-radio>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
name: 'QuasarCheckboxAndToggle',
setup () {
const checked = ref()
const toggled = ref()
const selected = ref()
return {
checked,
toggled,
selected
}
}
})
</script>

View file

@ -0,0 +1,16 @@
<template>
<q-card
data-cy="dark-card"
:dark="$q.dark.isActive"
>
{{ $q.dark.isActive ? 'Dark ' : 'Light' }} content
</q-card>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'QuasarDark'
})
</script>

View file

@ -0,0 +1,50 @@
<template>
<q-date
v-model="date"
data-cy="date-picker"
/>
<div>
<q-input
v-model="date"
label="Scegli data"
>
<template #append>
<q-btn
data-cy="open-date-picker-popup-button"
icon="event"
flat
round
@click="dateDialogRef.show()"
/>
</template>
</q-input>
<q-dialog ref="dateDialogRef">
<q-date
v-model="date"
@update:model-value="dateDialogRef.hide()"
/>
</q-dialog>
</div>
<span data-cy="date-value">{{ date }}</span>
</template>
<script lang="ts">
import type { QDialog } from 'quasar'
import type { Ref } from 'vue'
import { defineComponent, ref } from 'vue'
export default defineComponent({
name: 'QuasarDate',
setup () {
const date = ref('')
const dateDialogRef = ref() as Ref<QDialog>
return {
date,
dateDialogRef
}
}
})
</script>

View file

@ -0,0 +1,78 @@
<template>
<!-- notice dialogRef here -->
<q-dialog
ref="dialogRef"
@hide="onDialogHide"
>
<q-card>
<q-card-section>{{ message }}</q-card-section>
<!-- buttons example -->
<q-card-actions align="right">
<q-btn
data-cy="ok-button"
color="primary"
label="OK"
@click="onOKClick"
/>
<q-btn
color="primary"
label="Cancel"
@click="onCancelClick"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script lang="ts">
import { useDialogPluginComponent } from 'quasar'
import { defineComponent } from 'vue'
export default defineComponent({
name: 'QuasarDialog',
props: {
message: {
type: String,
required: true
}
},
// REQUIRED; need to specify some events that your
// component will emit through useDialogPluginComponent()
emits: useDialogPluginComponent.emits,
setup () {
// REQUIRED; must be called inside of setup()
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
useDialogPluginComponent()
// dialogRef - Vue ref to be applied to QDialog
// onDialogHide - Function to be used as handler for @hide on QDialog
// onDialogOK - Function to call to settle dialog with "ok" outcome
// example: onDialogOK() - no payload
// example: onDialogOK({ /*.../* }) - with payload
// onDialogCancel - Function to call to settle dialog with "cancel" outcome
return {
// This is REQUIRED;
// Need to inject these (from useDialogPluginComponent() call)
// into the vue scope for the vue html template
dialogRef,
onDialogHide,
// other methods that we used in our vue html template;
// these are part of our example (so not required)
onOKClick () {
// on OK, it is REQUIRED to
// call onDialogOK (with optional payload)
onDialogOK()
// or with payload: onDialogOK({ ... })
// ...and it will also hide the dialog automatically
},
// we can passthrough onDialogCancel directly
onCancelClick: onDialogCancel
}
}
})
</script>

View file

@ -0,0 +1,40 @@
<template>
<q-drawer
v-model="showDrawer"
show-if-above
:width="200"
:breakpoint="700"
elevated
data-cy="drawer"
class="bg-primary text-white"
>
<q-scroll-area class="fit">
<div class="q-pa-sm">
<div
v-for="n in 50"
:key="n"
>
Drawer {{ n }} / 50
</div>
</div>
<q-btn data-cy="button">
Am I on screen?
</q-btn>
</q-scroll-area>
</q-drawer>
</template>
<script lang="ts">
import { ref, defineComponent } from 'vue'
export default defineComponent({
name: 'QuasarDrawer',
setup () {
const showDrawer = ref(true)
return {
showDrawer
}
}
})
</script>

View file

@ -0,0 +1,31 @@
<template>
<q-btn
data-cy="open-menu-btn"
label="Open menu"
>
<q-menu>
<q-list>
<q-item
v-close-popup
clickable
>
<q-item-section>Item 1</q-item-section>
</q-item>
<q-item
v-close-popup
clickable
>
<q-item-section>Item 2</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'QuasarMenu'
})
</script>

View file

@ -0,0 +1,29 @@
<template>
<q-page-sticky
position="bottom-right"
:offset="[18, 18]"
>
<q-btn
data-cy="button"
rounded
color="accent"
icon="arrow_forward"
>
{{ title }}
</q-btn>
</q-page-sticky>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'QuasarPageSticky',
props: {
title: {
type: String,
required: true
}
}
})
</script>

View file

@ -0,0 +1,54 @@
<template>
<q-select
v-model="selected"
data-cy="select"
label="test options selection"
:options="options"
:loading="loading"
:disable="disable"
/>
<span data-cy="select-value">{{ selected }}</span>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
const syncOptions = ['Option 1', 'Option 2', 'Option 3']
export default defineComponent({
name: 'QuasarSelect',
props: {
loadOptionsAsync: {
type: Boolean,
default: false
},
disable: {
type: Boolean,
default: false
}
},
setup (props) {
const selected = ref()
const loading = ref(false)
const options = ref()
if (props.loadOptionsAsync) {
loading.value = true
setTimeout(() => {
options.value = syncOptions
loading.value = false
}, 2000)
} else {
options.value = syncOptions
}
return {
loading,
selected,
options
}
}
})
</script>

View file

@ -0,0 +1,31 @@
<template>
<q-btn
color="primary"
data-cy="button"
>
Button
<q-tooltip
v-model="showTooltip"
data-cy="tooltip"
class="bg-red"
:offset="[10, 10]"
>
Here I am!
</q-tooltip>
</q-btn>
</template>
<script lang="ts">
import { ref, defineComponent } from 'vue'
export default defineComponent({
name: 'QuasarTooltip',
setup () {
const showTooltip = ref(true)
return {
showTooltip
}
}
})
</script>

View file

@ -0,0 +1,32 @@
<template>
<div>
<span data-cy="model-value">{{ modelValue }}</span>
<button
data-cy="button"
@click="
$emit(
'update:modelValue',
modelValue.length > 0 ? modelValue.substring(1) : ''
)
"
>
Remove first letter
</button>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'QuasarTooltip',
props: {
modelValue: {
type: String,
required: true
}
},
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
emits: { 'update:modelValue': (payload: string) => payload !== undefined }
})
</script>

View file

@ -0,0 +1,43 @@
import QuasarButton from '../QuasarButton.vue'
describe('QuasarButton', () => {
it('renders a message', () => {
const label = 'Hello there'
cy.mount(QuasarButton, {
props: {
label
}
})
cy.dataCy('button').should('contain', label)
})
it('renders another message', () => {
const label = 'Will this work?'
cy.mount(QuasarButton, {
props: {
label
}
})
cy.dataCy('button').should('contain', label)
})
it('should have a `positive` color', () => {
cy.mount(QuasarButton)
cy.dataCy('button')
.should('have.backgroundColor', 'var(--q-positive)')
.should('have.color', 'white')
})
it('should emit `test` upon click', () => {
cy.mount(QuasarButton)
cy.dataCy('button')
cy.dataCy('button').click()
cy.dataCy('button').should(() => {
expect(Cypress.vueWrapper.emitted('test')).to.have.length(1)
})
})
})

View file

@ -0,0 +1,38 @@
import QuasarCheckComponents from '../QuasarCheckComponents.vue'
describe('QuasarCheckbox', () => {
it('can be used with normal Cypress commands', () => {
cy.mount(QuasarCheckComponents)
cy.dataCy('checkbox').check()
cy.dataCy('checkbox').should('be.checked')
cy.dataCy('checkbox').uncheck()
cy.dataCy('checkbox').should('not.be.checked')
})
})
describe('QuasarToggle', () => {
it('can be used with normal Cypress commands', () => {
cy.mount(QuasarCheckComponents)
cy.dataCy('toggle').check()
cy.dataCy('toggle').should('be.checked')
cy.dataCy('toggle').uncheck()
cy.dataCy('toggle').should('not.be.checked')
})
})
describe('QuasarToggle', () => {
it('can be used with normal Cypress commands', () => {
cy.mount(QuasarCheckComponents)
cy.dataCy('radio-1').check()
cy.dataCy('radio-1').should('be.checked')
cy.dataCy('radio-2').check()
cy.dataCy('radio-2').should('be.checked')
cy.dataCy('radio-1').should('not.be.checked')
})
})

View file

@ -0,0 +1,22 @@
import { Dark } from 'quasar'
import QuasarDark from '../QuasarDark.vue'
describe('QuasarDark', () => {
it('changes its color', () => {
cy.mount(QuasarDark)
cy.dataCy('dark-card')
.should('not.have.class', 'q-dark')
.then(() => {
Dark.set(true)
})
cy.dataCy('dark-card')
.should('have.class', 'q-dark')
.then(() => {
Cypress.vueWrapper.vm.$q.dark.set(false)
})
cy.dataCy('dark-card').should('not.have.class', 'q-dark')
})
})

View file

@ -0,0 +1,33 @@
import QuasarDate from '../QuasarDate.vue'
const targetDate = '2023/02/23'
describe('QuasarDate', () => {
it('selects a date by date string', () => {
cy.mount(QuasarDate)
cy.dataCy('date-picker').selectDate(targetDate)
cy.dataCy('date-value').should('have.text', targetDate)
})
it('selects a date by date object', () => {
cy.mount(QuasarDate)
cy.dataCy('date-picker').selectDate(new Date(targetDate))
cy.dataCy('date-value').should('have.text', targetDate)
})
it('selects a date displayed into a popup proxy', () => {
cy.mount(QuasarDate)
cy.dataCy('open-date-picker-popup-button').click()
cy.withinDialog(() => {
cy.get('.q-date').selectDate(targetDate)
})
cy.dataCy('date-value').should('have.text', targetDate)
// When dealing with a nested dialog, or a popup proxy within a dialog,
// add a data-cy on the dialog/popup-proxy containing the QDate and use the `withinDialog` extended signature:
// Example: cy.withinDialog({ dataCy: 'date-picker-popup', fn: () => { cy.get('.q-date').selectDate(targetDate); } })
})
})

View file

@ -0,0 +1,42 @@
import DialogWrapper from 'app/test/cypress/wrappers/DialogWrapper.vue'
import QuasarDialog from '../QuasarDialog.vue'
describe('QuasarDialog', () => {
it('should show a dialog with a message', () => {
const message = 'Hello, I am a dialog'
cy.mount(DialogWrapper, {
props: {
component: QuasarDialog,
componentProps: {
message
}
}
})
cy.withinDialog((el) => {
cy.wrap(el).should('contain', message)
cy.dataCy('ok-button').click()
})
})
it('should keep the dialog open when not dismissed', () => {
const message = 'Hello, I am a dialog'
cy.mount(DialogWrapper, {
props: {
component: QuasarDialog,
componentProps: {
message
}
}
})
// The helper won't check for the dialog to be closed
// when the callback completes
cy.withinDialog({
persistent: true,
fn: (el) => {
cy.wrap(el).should('contain', message)
}
})
})
})

View file

@ -0,0 +1,20 @@
import LayoutContainer from 'app/test/cypress/wrappers/LayoutContainer.vue'
import QuasarDrawer from '../QuasarDrawer.vue'
describe('QuasarDrawer', () => {
it('should show a drawer', () => {
cy.mount(LayoutContainer, {
props: {
component: QuasarDrawer
}
})
cy.dataCy('drawer')
.should('exist')
.dataCy('button')
.should('not.be.visible')
cy.get('.q-scrollarea .scroll')
cy.get('.q-scrollarea .scroll').scrollTo('bottom', { duration: 500 })
cy.get('.q-scrollarea .scroll').dataCy('button')
cy.get('.q-scrollarea .scroll').should('be.visible')
})
})

View file

@ -0,0 +1,21 @@
import QuasarMenu from '../QuasarMenu.vue'
describe('QuasarMenu', () => {
it('click an item by content', () => {
cy.mount(QuasarMenu)
cy.dataCy('open-menu-btn').click()
cy.withinMenu(() => {
cy.get('.q-item').contains('Item 1').click()
})
})
it('click an item by cardinality', () => {
cy.mount(QuasarMenu)
cy.dataCy('open-menu-btn').click()
cy.withinMenu(() => {
cy.get('.q-item').eq(1).click()
})
})
})

View file

@ -0,0 +1,21 @@
import LayoutContainer from 'app/test/cypress/wrappers/LayoutContainer.vue'
import QuasarPageSticky from '../QuasarPageSticky.vue'
describe('QuasarPageSticky', () => {
it('should show a sticky at the bottom-right of the page', () => {
cy.mount(LayoutContainer, {
props: {
component: QuasarPageSticky,
title: 'Test'
}
})
cy.dataCy('button')
.should('be.visible')
.should(($el) => {
const rect = $el[0].getBoundingClientRect()
expect(rect.bottom).to.equal(window.innerHeight - 18)
expect(rect.right).to.equal(window.innerWidth - 18)
})
})
})

View file

@ -0,0 +1,44 @@
import QuasarSelect from '../QuasarSelect.vue'
function dataCySelect (dataCyId: string) {
return cy.dataCy(dataCyId).closest('.q-select')
}
describe('QuasarSelect', () => {
it('makes sure the select is disabled', () => {
cy.mount(QuasarSelect, {
props: { disable: true }
})
// `cy.dataCy('select')` won't work in this case, as it won't get the root q-select element
dataCySelect('select').should('have.attr', 'aria-disabled', 'true')
})
it('selects an option by content', () => {
cy.mount(QuasarSelect)
cy.dataCy('select').select('Option 1')
cy.dataCy('select-value').should('have.text', 'Option 1')
})
it('selects an option by cardinality', () => {
cy.mount(QuasarSelect)
cy.dataCy('select').select(1)
cy.dataCy('select-value').should('have.text', 'Option 2')
})
it('selects an option asynchronously', () => {
cy.mount(QuasarSelect, {
props: {
loadOptionsAsync: true
}
})
// Wait for loading to complete
cy.dataCy('select').get('.q-spinner').should('not.exist')
cy.dataCy('select').select('Option 3')
cy.dataCy('select-value').should('have.text', 'Option 3')
})
})

View file

@ -0,0 +1,10 @@
import QuasarTooltip from '../QuasarTooltip.vue'
describe('QuasarTooltip', () => {
it('should show a tooltip', () => {
cy.mount(QuasarTooltip)
cy.dataCy('button').trigger('mouseover')
cy.dataCy('tooltip').contains('Here I am!')
})
})

View file

@ -0,0 +1,72 @@
import { vModelAdapter } from '@quasar/quasar-app-extension-testing-e2e-cypress'
import { ref } from 'vue'
import VModelComponent from '../VModelComponent.vue'
describe('VModelComponent', () => {
it('should show the value', () => {
const text = 'Quasar'
cy.mount(VModelComponent, {
props: {
modelValue: text
}
})
cy.dataCy('model-value').should('contain', text)
})
it('should call the listener when an update via inner button occurs', () => {
const text = 'Quasar'
const fn = cy.stub()
cy.mount(VModelComponent, {
props: {
modelValue: text,
// This is how Vue internally codifies listeners,
// defining a prop prepended with `on` and camelCased
'onUpdate:modelValue': fn
}
})
cy.dataCy('button')
cy.dataCy('button').click()
cy.dataCy('button').then(() => {
expect(fn).to.be.calledWith('uasar')
})
})
it('should update the value via inner button when not using the helper', () => {
const text = 'Quasar'
cy.mount(VModelComponent, {
props: {
modelValue: text,
'onUpdate:modelValue': (emittedValue: string) =>
Cypress.vueWrapper.setProps({ modelValue: emittedValue })
}
})
cy.dataCy('button').click()
cy.dataCy('model-value').should('contain', 'uasar')
})
it('should update the value via inner button using the helper', () => {
const model = ref('Quasar')
cy.mount(VModelComponent, {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
props: {
...vModelAdapter(model)
}
})
cy.dataCy('button').click()
cy.dataCy('model-value')
.should('contain', 'uasar')
.then(() => {
// You cannot access `model.value` in a synchronous way,
// you need to chain checks on it to a Cypress command or you'll be testing the initial value.
expect(model.value).to.equal('uasar')
})
})
})

View file

@ -0,0 +1,12 @@
import ColorAssertionsComponent from '../color-assertions.vue'
describe('color assertions', () => {
it('works with names, hex codes and CSS variables', () => {
cy.mount(ColorAssertionsComponent)
cy.get('.wrapper')
.should('have.color', 'var(--q-primary)')
.and('have.backgroundColor', 'black')
.and('have.backgroundColor', '#000')
})
})

View file

@ -0,0 +1,16 @@
import DataCyComponent from '../data-cy.vue'
describe('dataCy command', () => {
it('works as a parent command', () => {
cy.mount(DataCyComponent)
cy.dataCy('wrapper').should('exist')
cy.dataCy('paragraph').should('exist').and('contain', 'Test')
})
it('works as a child command', () => {
cy.mount(DataCyComponent)
cy.dataCy('wrapper').dataCy('paragraph').should('exist')
})
})

View file

@ -0,0 +1,5 @@
<template>
<div class="wrapper text-primary bg-black">
Text1
</div>
</template>

View file

@ -0,0 +1,7 @@
<template>
<div data-cy="wrapper">
<p data-cy="paragraph">
Test
</p>
</div>
</template>

2
test/cypress/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
videos/*
screenshots/*

View file

@ -0,0 +1,46 @@
// Use `cy.dataCy` custom command for more robust tests
// See https://docs.cypress.io/guides/references/best-practices.html#Selecting-Elements
// ** This file is an example of how to write Cypress tests, you can safely delete it **
// This test will pass when run against a clean Quasar project
describe('Landing', () => {
beforeEach(() => {
cy.visit('/')
})
it('.should() - assert that <title> is correct', () => {
cy.title().should('include', 'Fantasia Archive')
cy.get('li').first().click()
cy.contains('Clicks on todos: 1').should('exist')
})
})
// ** The following code is an example to show you how to write some tests for your home page **
//
// describe('Home page tests', () => {
// beforeEach(() => {
// cy.visit('/');
// });
// it('has pretty background', () => {
// cy.dataCy('landing-wrapper')
// .should('have.css', 'background')
// .and('match', /(".+(\/img\/background).+\.png)/);
// });
// it('has pretty logo', () => {
// cy.dataCy('landing-wrapper img')
// .should('have.class', 'logo-main')
// .and('have.attr', 'src')
// .and('match', /^(data:image\/svg\+xml).+/);
// });
// it('has very important information', () => {
// cy.dataCy('instruction-wrapper')
// .should('contain', 'SETUP INSTRUCTIONS')
// .and('contain', 'Configure Authentication')
// .and('contain', 'Database Configuration and CRUD operations')
// .and('contain', 'Continuous Integration & Continuous Deployment CI/CD');
// });
// });
// Workaround for Cypress AE + TS + Vite
// See: https://github.com/quasarframework/quasar-testing/issues/262#issuecomment-1154127497
export {}

View file

@ -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"
}

View file

@ -0,0 +1,30 @@
// ***********************************************
// This example commands.ts 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) => { ... })
// DO NOT REMOVE
// Imports Quasar Cypress AE predefined commands
import { registerCommands } from '@quasar/quasar-app-extension-testing-e2e-cypress'
registerCommands()

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>

View file

@ -0,0 +1,51 @@
// ***********************************************************
// This example support/component.ts is processed and
// loaded automatically before your component 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
// 'component.supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
import './commands'
import '@cypress/code-coverage/support'
// Quasar styles
import 'quasar/src/css/index.sass' // Or 'quasar/dist/quasar.prod.css' if no CSS preprocessor is installed
// Change this if you have a different entrypoint for the main scss.
import 'src/css/app.scss' // Or 'src/css/app.css' if no CSS preprocessor is installed
// ICON SETS
// If you use multiple or different icon-sets then the default, be sure to import them here.
import 'quasar/dist/icon-set/material-icons.umd.prod'
import '@quasar/extras/material-icons/material-icons.css'
import { installQuasarPlugin } from '@quasar/quasar-app-extension-testing-e2e-cypress'
import { Dialog } from 'quasar'
// Since Cypress v10 we cannot import `config` directly from VTU as Cypress bundles its own version of it
// See https://github.com/cypress-io/cypress/issues/22611
import { VueTestUtils } from 'cypress/vue'
const { config } = VueTestUtils
// Example to import i18n from boot and use as plugin
// import { i18n } from 'src/boot/i18n';
// You can modify the global config here for all tests or pass in the configuration per test
// For example use the actual i18n instance or mock it
// config.global.plugins.push(i18n);
config.global.mocks = {
$t: () => ''
}
// Overwrite the transition and transition-group stubs which are stubbed by test-utils by default.
// We do want transitions to show when doing visual testing :)
config.global.stubs = {}
installQuasarPlugin({ plugins: { Dialog } })

View file

@ -0,0 +1,17 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your e2e test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
import './commands'
import '@cypress/code-coverage/support'

View file

@ -0,0 +1,26 @@
<script lang="ts">
import { Dialog } from 'quasar'
import { defineComponent } from 'vue'
export default defineComponent({
name: 'DialogWrapper',
props: {
component: {
type: Object,
required: true
},
componentProps: {
type: Object,
default: () => ({})
}
},
setup (props) {
Dialog.create({
component: props.component,
// props forwarded to your custom component
componentProps: props.componentProps
})
}
})
</script>

View file

@ -0,0 +1,20 @@
<template>
<q-layout>
<component :is="component" v-bind="$attrs" />
</q-layout>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'LayoutContainer',
inheritAttrs: false,
props: {
component: {
type: Object,
required: true
}
}
})
</script>

View file

@ -0,0 +1,38 @@
import { installQuasar } from '@quasar/quasar-app-extension-testing-unit-vitest'
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import ExampleComponent from './demo/ExampleComponent.vue'
installQuasar()
describe('example Component', () => {
it('should mount component with todos', () => {
const wrapper = mount(ExampleComponent, {
props: {
title: 'Hello',
meta: {
totalCount: 4
},
todos: [
{ id: 1, content: 'Hallo' },
{ id: 2, content: 'Hoi' }
]
}
})
expect(wrapper.vm.clickCount).toBe(0)
wrapper.find('.q-item').trigger('click')
expect(wrapper.vm.clickCount).toBe(1)
})
it('should mount component without todos', () => {
const wrapper = mount(ExampleComponent, {
props: {
title: 'Hello',
meta: {
totalCount: 4
}
}
})
expect(wrapper.findAll('.q-item')).toHaveLength(0)
})
})

View file

@ -0,0 +1,19 @@
import { installQuasar } from '@quasar/quasar-app-extension-testing-unit-vitest'
import { mount } from '@vue/test-utils'
import { Notify } from 'quasar'
import { describe, expect, it, vi } from 'vitest'
import NotifyComponent from './demo/NotifyComponent.vue'
installQuasar({ plugins: { Notify } })
describe('notify example', () => {
it('should call notify on click', async () => {
expect(NotifyComponent).toBeTruthy()
const wrapper = mount(NotifyComponent, {})
const spy = vi.spyOn(Notify, 'create')
expect(spy).not.toHaveBeenCalled()
wrapper.trigger('click')
expect(spy).toHaveBeenCalled()
})
})

View file

@ -0,0 +1,52 @@
<template>
<div>
<p>{{ title }}</p>
<q-list>
<q-item
v-for="todo in todos"
:key="todo.id"
clickable
@click="increment"
>
{{ todo.id }} - {{ todo.content }}
</q-item>
</q-list>
<p>Count: {{ todoCount }} / {{ meta.totalCount }}</p>
<p>Active: {{ active ? 'yes' : 'no' }}</p>
<p>Clicks on todos: {{ clickCount }}</p>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
interface Todo {
id: number;
content: string;
}
interface Meta {
totalCount: number;
}
const props = withDefaults(
defineProps<{
title: string;
todos?: Todo[];
meta: Meta;
active?: boolean;
}>(),
{
todos: () => []
}
)
const clickCount = ref(0)
function increment () {
clickCount.value += 1
return clickCount.value
}
const todoCount = computed(() => props.todos.length)
</script>

View file

@ -0,0 +1,13 @@
<template>
<q-btn @click="onClick">
Click me!
</q-btn>
</template>
<script lang="ts" setup>
import { Notify } from 'quasar'
function onClick () {
Notify.create('Hello there!')
}
</script>

View file

@ -0,0 +1 @@
// This file will be run before each test file

27
vitest.config.ts Normal file
View file

@ -0,0 +1,27 @@
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
import tsconfigPaths from 'vite-tsconfig-paths'
// https://vitejs.dev/config/
export default defineConfig({
test: {
environment: 'jsdom',
setupFiles: 'test/vitest/setup-file.ts',
include: [
// Matches vitest tests in any subfolder of 'src' or into 'test/vitest/__tests__'
// Matches all files with extension 'js', 'jsx', 'ts' and 'tsx'
'src/**/*.vitest.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
'test/vitest/__tests__/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'
]
},
plugins: [
vue({
template: { transformAssetUrls }
}),
quasar({
sassVariables: 'src/quasar-variables.scss'
}),
tsconfigPaths()
]
})

2451
yarn.lock

File diff suppressed because it is too large Load diff