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

3
.gitignore vendored
View file

@ -35,3 +35,6 @@ yarn-error.log*
# local .env files
.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.
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
```bash
yarn
@ -14,7 +16,7 @@ yarn
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
yarn lint
```
@ -24,5 +26,24 @@ yarn lint
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
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,
"scripts": {
"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",
"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": {
"@electron/remote": "^2.0.10",
@ -24,18 +31,27 @@
"devDependencies": {
"@intlify/vite-plugin-vue-i18n": "^3.3.1",
"@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",
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",
"@vitest/ui": "^0.15.0",
"@vue/test-utils": "^2.0.0",
"autoprefixer": "^10.4.2",
"cypress": "^12.2.0",
"electron": "^25.5.0",
"eslint": "^8.10.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-import": "^2.19.1",
"eslint-plugin-n": "^15.0.0",
"eslint-plugin-promise": "^6.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": {
"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-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>
</template>

View file

@ -2,7 +2,11 @@
<div>
<p>{{ title }}</p>
<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 }}
</li>
</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