implemented cypress e2e testing

This commit is contained in:
Carlos Jimenez Ruiz 2022-03-24 14:12:14 +01:00
parent 601cdc0908
commit ecdb785abb
26 changed files with 2945 additions and 7 deletions

View File

@ -65,5 +65,16 @@ module.exports = {
// allow debugger during development only // allow debugger during development only
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
} },
overrides: [
{
files: ['**/*.spec.{js,ts}'],
extends: [
// Add Cypress-specific lint rules, globals and Cypress plugin
// See https://github.com/cypress-io/eslint-plugin-cypress#rules
'plugin:cypress/recommended',
],
},
],
} }

10
.vscode/settings.json vendored
View File

@ -12,5 +12,13 @@
"typescript", "typescript",
"vue" "vue"
], ],
"jest.jestCommandLine": "jest" "jest.jestCommandLine": "jest",
"json.schemas": [
{
"fileMatch": [
"cypress.json"
],
"url": "https://on.cypress.io/cypress.schema.json"
}
]
} }

15
cypress.json Normal file
View File

@ -0,0 +1,15 @@
{
"baseUrl": "http://localhost:8080/",
"fixturesFolder": "test/cypress/fixtures",
"integrationFolder": "test/cypress/integration",
"pluginsFile": "test/cypress/plugins/index.js",
"screenshotsFolder": "test/cypress/screenshots",
"supportFile": "test/cypress/support/index.js",
"videosFolder": "test/cypress/videos",
"video": true,
"component": {
"componentFolder": "src",
"testFiles": "**/*.spec.js",
"supportFile": "test/cypress/support/unit.js"
}
}

2381
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,13 +9,13 @@
"lint": "eslint --ext .js,.vue ./", "lint": "eslint --ext .js,.vue ./",
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore", "format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
"test": "echo \"See package.json => scripts for available tests.\" && exit 0", "test": "echo \"See package.json => scripts for available tests.\" && exit 0",
"test:unit": "jest --updateSnapshot", "test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci", "test:unit:ci": "jest --ci",
"test:unit:coverage": "jest --coverage", "test:unit:coverage": "jest --coverage",
"test:unit:watch": "jest --watch",
"test:unit:watchAll": "jest --watchAll",
"serve:test:coverage": "quasar serve test/jest/coverage/lcov-report/ --port 8788", "serve:test:coverage": "quasar serve test/jest/coverage/lcov-report/ --port 8788",
"concurrently:dev:jest": "concurrently \"quasar dev\" \"jest --watch\"" "concurrently:dev:jest": "concurrently \"quasar dev\" \"jest --watch\"",
"test:e2e": "cross-env E2E_TEST=true start-test \"quasar dev\" http-get://localhost:8080 \"cypress open\"",
"test:e2e:ci": "cross-env E2E_TEST=true start-test \"quasar dev\" http-get://localhost:8080 \"cypress run\""
}, },
"dependencies": { "dependencies": {
"@quasar/extras": "^1.0.0", "@quasar/extras": "^1.0.0",
@ -29,13 +29,15 @@
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.13.14", "@babel/eslint-parser": "^7.13.14",
"@quasar/app-webpack": "^3.0.0", "@quasar/app-webpack": "^3.0.0",
"@quasar/quasar-app-extension-testing-e2e-cypress": "^4.0.1",
"@quasar/quasar-app-extension-testing-unit-jest": "^3.0.0-alpha.9", "@quasar/quasar-app-extension-testing-unit-jest": "^3.0.0-alpha.9",
"eslint": "^8.10.0", "eslint": "^8.10.0",
"eslint-config-prettier": "^8.1.0", "eslint-config-prettier": "^8.1.0",
"eslint-plugin-jest": "^25.2.2",
"eslint-plugin-vue": "^8.5.0", "eslint-plugin-vue": "^8.5.0",
"eslint-webpack-plugin": "^3.1.1", "eslint-webpack-plugin": "^3.1.1",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"eslint-plugin-jest": "^25.2.2" "eslint-plugin-cypress": "^2.11.3"
}, },
"browserslist": [ "browserslist": [
"last 10 Chrome versions", "last 10 Chrome versions",

View File

@ -4,5 +4,10 @@
"options": [ "options": [
"scripts" "scripts"
] ]
},
"@quasar/testing-e2e-cypress": {
"options": [
"scripts"
]
} }
} }

View File

@ -1,5 +1,11 @@
{ {
"unit-jest": { "unit-jest": {
"runnerCommand": "jest --ci" "runnerCommand": "jest --ci"
},
"e2e-cypress": {
"runnerCommand": "cross-env E2E_TEST=true start-test \"quasar dev\" http-get://localhost:8080 \"cypress run\""
},
"unit-cypress": {
"runnerCommand": "cypress run-ct"
} }
} }

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>
import { defineComponent } from 'vue';
export default defineComponent({
name: 'QuasarButton',
emits: ['test'],
});
</script>

View File

@ -0,0 +1,71 @@
<template>
<!-- notice dialogRef here -->
<q-dialog ref="dialogRef" @hide="onDialogHide" data-cy="dialog">
<q-card class="q-dialog-plugin">
<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>
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,33 @@
<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>
import { ref, defineComponent } from 'vue';
export default defineComponent({
name: 'QuasarDrawer',
setup() {
const showDrawer = ref(true);
return {
showDrawer,
};
},
});
</script>

View File

@ -0,0 +1,21 @@
<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>
import { defineComponent } from 'vue';
export default defineComponent({
name: 'QuasarPageSticky',
props: {
title: {
type: String,
required: true,
},
},
});
</script>

View File

@ -0,0 +1,28 @@
<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>
import { ref, defineComponent } from 'vue';
export default defineComponent({
name: 'QuasarTooltip',
setup() {
const showTooltip = ref(true);
return {
showTooltip,
};
},
});
</script>

View File

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

View File

@ -0,0 +1,24 @@
import { mount } from '@cypress/vue';
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';
mount(DialogWrapper, {
props: {
component: QuasarDialog,
componentProps: {
message,
},
},
});
cy.dataCy('dialog').should('exist').should('contain', message);
});
it('should close a dialog when clikcing ok', () => {
// The dialog is still visible from the previous test
cy.dataCy('dialog').should('exist').dataCy('ok-button').click();
cy.dataCy('dialog').should('not.exist');
});
});

View File

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

View File

@ -0,0 +1,22 @@
import { mount } from '@cypress/vue';
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', () => {
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,11 @@
import { mount } from '@cypress/vue';
import QuasarTooltip from '../QuasarTooltip.vue';
describe('QuasarTooltip', () => {
it('should show a tooltip', () => {
mount(QuasarTooltip);
cy.dataCy('button').trigger('mouseover');
cy.dataCy('tooltip').contains('Here I am!');
});
});

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

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

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,40 @@
/// <reference types="cypress" />
// 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', 'Quasar');
});
});
// ** 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');
// });
// });

View File

@ -0,0 +1,31 @@
/// <reference types="cypress" />
/* eslint-env node */
// ***********************************************************
// 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)
// cypress/plugins/index.js
// const {injectDevServer} = require('@quasar/quasar-app-extension-testing-e2e-cypress/cct-dev-server');
// /**
// * @type {Cypress.PluginConfig}
// */
module.exports = async (on, config) => {
// // Enable component testing, you can safely remove this
// // if you don't plan to use Cypress for unit tests
// if (config.testingType === 'component') {
// await injectDevServer(on, config);
// }
return config;
};

View File

@ -0,0 +1,30 @@
// ***********************************************
// 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 is 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,16 @@
// ***********************************************************
// This example support/index.js 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';

View File

@ -0,0 +1,46 @@
// ***********************************************************
// This example support/unit.js is processed and
// loaded automatically before your unit 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';
// Change this if you have a different entrypoint for the main scss.
import 'src/css/app.scss';
// Quasar styles
import 'quasar/src/css/index.sass';
// 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 { config } from '@vue/test-utils';
import { Dialog } from 'quasar';
// 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,26 @@
<script>
import { defineComponent } from 'vue';
import { Dialog } from 'quasar';
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>
import { defineComponent } from 'vue';
export default defineComponent({
name: 'LayoutContainer',
inheritAttrs: false,
props: {
component: {
type: Object,
required: true,
},
},
});
</script>