diff --git a/.github/workflows/cypress-ci.yaml b/.github/workflows/cypress-ci.yaml new file mode 100644 index 000000000..340849817 --- /dev/null +++ b/.github/workflows/cypress-ci.yaml @@ -0,0 +1,42 @@ +name: Cypress Tests +on: + workflow_dispatch: + inputs: + pr-number: + description: Pull request number + required: false + type: string + +jobs: + cypress-run: + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: cache + uses: actions/cache@v4 + with: + path: "tests/functional" + key: node-modules-${{ hashFiles('tests/functional/package.json') }} + + + - name: cypress install + uses: cypress-io/github-action@v6 + with: + working-directory: '${{ github.workspace }}/tests/functional' + env: + CYPRESS_keycloakUsername: ${{secrets.keycloakUsername}} + CYPRESS_keycloakPassword: ${{secrets.keycloakPassword}} + CYPRESS_depEnv: ${{ github.event.inputs.pr-number }} + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: '${{ github.workspace }}/tests/functional/screenshots' + diff --git a/.gitignore b/.gitignore index f3ab92d0e..c3fe41ab2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,8 @@ build coverage dist **/src/formio -**/cypress/**/screenshots **/cypress/**/videos +screenshots node_modules # Ignore only top-level package-lock.json diff --git a/app/app.js b/app/app.js index e2e202789..007d4c69a 100644 --- a/app/app.js +++ b/app/app.js @@ -75,7 +75,7 @@ apiRouter.get('/api', (_req, res) => { // Host API endpoints apiRouter.use(config.get('server.apiPath'), v1Router); app.use(config.get('server.basePath'), apiRouter); -app.use(middleware.dataErrors); +app.use(middleware.errorHandler); // Host the static frontend assets const staticFilesPath = config.get('frontend.basePath'); diff --git a/app/package-lock.json b/app/package-lock.json index 1faf6c92f..c84bf15aa 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -11,12 +11,11 @@ "dependencies": { "@json2csv/node": "^6.1.3", "@json2csv/transforms": "^6.1.3", - "api-problem": "^7.0.4", + "api-problem": "^9.0.2", "aws-sdk": "^2.1376.0", "axios": "^0.28.1", "axios-oauth-client": "^1.5.0", "axios-token-interceptor": "^0.2.0", - "body-parser": "^1.20.2", "bytes": "^3.1.2", "compression": "^1.7.4", "config": "^3.3.9", @@ -26,7 +25,6 @@ "express-rate-limit": "^7.2.0", "express-winston": "^4.2.0", "falsey": "^1.0.0", - "fast-json-patch": "^3.1.1", "fs-extra": "^10.1.0", "handlebars": "^4.7.8", "jose": "^5.2.2", @@ -1984,9 +1982,9 @@ } }, "node_modules/api-problem": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/api-problem/-/api-problem-7.0.4.tgz", - "integrity": "sha512-8f/Dg1LY26YSMzhguscyJWw2qVafdczyK82+X7zcnv73iN1mh4KmVEm2w7xAL3ttpWddPE7lTKMRFdidH/dFYA==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/api-problem/-/api-problem-9.0.2.tgz", + "integrity": "sha512-Xr4TyFkyvTEkgL8zUhyoqeK2Oxx2GQaFIPNiuhVRfop34gNl5r5gk1jYMsQhtPWSpwavROVweNIyL677ibE4rw==", "engines": { "node": ">=6" }, @@ -5967,11 +5965,6 @@ "node": ">= 6" } }, - "node_modules/fast-json-patch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", - "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==" - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -12639,9 +12632,9 @@ } }, "api-problem": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/api-problem/-/api-problem-7.0.4.tgz", - "integrity": "sha512-8f/Dg1LY26YSMzhguscyJWw2qVafdczyK82+X7zcnv73iN1mh4KmVEm2w7xAL3ttpWddPE7lTKMRFdidH/dFYA==" + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/api-problem/-/api-problem-9.0.2.tgz", + "integrity": "sha512-Xr4TyFkyvTEkgL8zUhyoqeK2Oxx2GQaFIPNiuhVRfop34gNl5r5gk1jYMsQhtPWSpwavROVweNIyL677ibE4rw==" }, "append-field": { "version": "1.0.0", @@ -15658,11 +15651,6 @@ } } }, - "fast-json-patch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", - "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==" - }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", diff --git a/app/package.json b/app/package.json index adc34c6b2..a18da46ab 100644 --- a/app/package.json +++ b/app/package.json @@ -49,12 +49,11 @@ "dependencies": { "@json2csv/node": "^6.1.3", "@json2csv/transforms": "^6.1.3", - "api-problem": "^7.0.4", + "api-problem": "^9.0.2", "aws-sdk": "^2.1376.0", "axios": "^0.28.1", "axios-oauth-client": "^1.5.0", "axios-token-interceptor": "^0.2.0", - "body-parser": "^1.20.2", "bytes": "^3.1.2", "compression": "^1.7.4", "config": "^3.3.9", @@ -64,7 +63,6 @@ "express-rate-limit": "^7.2.0", "express-winston": "^4.2.0", "falsey": "^1.0.0", - "fast-json-patch": "^3.1.1", "fs-extra": "^10.1.0", "handlebars": "^4.7.8", "jose": "^5.2.2", diff --git a/app/src/forms/common/middleware/dataErrors.js b/app/src/forms/common/middleware/dataErrors.js deleted file mode 100644 index 593906ec6..000000000 --- a/app/src/forms/common/middleware/dataErrors.js +++ /dev/null @@ -1,36 +0,0 @@ -const Problem = require('api-problem'); -const Objection = require('objection'); - -module.exports.dataErrors = async (err, _req, res, next) => { - let error = err; - if (err instanceof Objection.DataError) { - error = new Problem(422, { - detail: 'Sorry... the database does not like the data you provided :(', - }); - } else if (err instanceof Objection.NotFoundError) { - error = new Problem(404, { - detail: "Sorry... we still haven't found what you're looking for :(", - }); - } else if (err instanceof Objection.UniqueViolationError) { - error = new Problem(422, { - detail: 'Unique Validation Error', - }); - } else if (err instanceof Objection.ValidationError) { - error = new Problem(422, { - detail: 'Validation Error', - errors: err.data, - }); - } - - if (error instanceof Problem && error.status !== 500) { - // Handle here when not an internal error. These are mostly from buggy - // systems using API Keys, but could also be from frontend bugs. Save the - // ERROR level logs (below) for only the things that need investigation. - error.send(res); - } else { - // HTTP 500 Problems and all other exceptions should be handled by the error - // handler in app.js. It will log them at the ERROR level and include a full - // stack trace. - next(error); - } -}; diff --git a/app/src/forms/common/middleware/errorHandler.js b/app/src/forms/common/middleware/errorHandler.js new file mode 100644 index 000000000..084aa213d --- /dev/null +++ b/app/src/forms/common/middleware/errorHandler.js @@ -0,0 +1,77 @@ +const Problem = require('api-problem'); +const Objection = require('objection'); + +/** + * Given a subclass of DBError will create and throw the corresponding Problem. + * If the error is of an unknown type it will not be converted. + * + * @param {DBError} error the error to convert to a Problem. + * @returns nothing + */ +const _throwObjectionProblem = (error) => { + if (error instanceof Objection.DataError) { + throw new Problem(422, { + detail: 'Sorry... the database does not like the data you provided :(', + }); + } + + if (error instanceof Objection.NotFoundError) { + throw new Problem(404, { + detail: "Sorry... we still haven't found what you're looking for :(", + }); + } + + if (error instanceof Objection.ConstraintViolationError) { + throw new Problem(422, { + detail: 'Constraint Violation Error', + }); + } + + if (error instanceof Objection.ValidationError) { + throw new Problem(422, { + detail: 'Validation Error', + errors: error.data, + }); + } +}; + +/** + * Send an error response for all errors except 500s, which are handled by the + * code in app.js. + * + * @param {*} err the Error that occurred. + * @param {*} _req the Express object representing the HTTP request - unused. + * @param {*} res the Express object representing the HTTP response. + * @param {*} next the Express chaining function. + * @returns nothing + */ +module.exports.errorHandler = async (err, _req, res, next) => { + try { + if (err instanceof Objection.DBError || err instanceof Objection.NotFoundError || err instanceof Objection.ValidationError) { + _throwObjectionProblem(err); + } + + // Express throws Errors that are not Problems, but do have an HTTP status + // code. For example, 400 is thrown when POST bodies are malformed JSON. + if (!(err instanceof Problem) && (err.status || err.statusCode)) { + throw new Problem(err.status || err.statusCode, { + detail: err.message, + }); + } + + // Not sure what it is, so also handle it in the catch block. + throw err; + } catch (error) { + if (error instanceof Problem && error.status !== 500) { + // Handle here when not an internal error. These are mostly from buggy + // systems using API Keys, but could also be from frontend bugs. Note that + // this does not log the error (see below). + error.send(res); + } else { + // HTTP 500 Problems and all other exceptions should be handled by the + // error handler in app.js. It will log them at the ERROR level and + // include a full stack trace. + next(error); + } + } +}; diff --git a/app/src/forms/common/middleware/index.js b/app/src/forms/common/middleware/index.js index 7cb497cd3..e953aaa3b 100644 --- a/app/src/forms/common/middleware/index.js +++ b/app/src/forms/common/middleware/index.js @@ -1,4 +1,4 @@ module.exports = { - ...require('./dataErrors'), + ...require('./errorHandler'), ...require('./rateLimiter'), }; diff --git a/app/tests/unit/forms/common/middleware/dataErrors.spec.js b/app/tests/unit/forms/common/middleware/dataErrors.spec.js deleted file mode 100644 index eb4b479ee..000000000 --- a/app/tests/unit/forms/common/middleware/dataErrors.spec.js +++ /dev/null @@ -1,88 +0,0 @@ -const { getMockRes } = require('@jest-mock/express'); -const Problem = require('api-problem'); -const Objection = require('objection'); - -const middleware = require('../../../../../src/forms/common/middleware'); - -describe('test data errors middleware', () => { - it('should handle an objection data error', () => { - const error = new Objection.DataError({ - nativeError: { message: 'This is a DataError' }, - }); - const { res } = getMockRes(); - const next = jest.fn(); - - middleware.dataErrors(error, {}, res, next); - - expect(next).not.toBeCalled(); - expect(res.end).toBeCalledWith(expect.stringContaining('422')); - }); - - it('should handle an objection not found error', () => { - const error = new Objection.NotFoundError({ - nativeError: { message: 'This is a NotFoundError' }, - }); - const { res } = getMockRes(); - const next = jest.fn(); - - middleware.dataErrors(error, {}, res, next); - - expect(next).not.toBeCalled(); - expect(res.end).toBeCalledWith(expect.stringContaining('404')); - }); - - it('should handle an objection unique violation error', () => { - const error = new Objection.UniqueViolationError({ - nativeError: { message: 'This is a UniqueViolationError' }, - }); - const { res } = getMockRes(); - const next = jest.fn(); - - middleware.dataErrors(error, {}, res, next); - - expect(next).not.toBeCalled(); - expect(res.end).toBeCalledWith(expect.stringContaining('422')); - }); - - it('should handle an objection validation error', () => { - const error = new Objection.ValidationError({ - nativeError: { message: 'This is a ValidationError' }, - }); - const { res } = getMockRes(); - const next = jest.fn(); - - middleware.dataErrors(error, {}, res, next); - - expect(next).not.toBeCalled(); - expect(res.end).toBeCalledWith(expect.stringContaining('422')); - }); - - it('should handle any non-500 Problems', () => { - const error = new Problem(429); - const { res } = getMockRes(); - const next = jest.fn(); - - middleware.dataErrors(error, {}, res, next); - - expect(next).not.toBeCalled(); - expect(res.end).toBeCalledWith(expect.stringContaining('429')); - }); - - it('should pass through any 500s', () => { - const error = new Problem(500); - const next = jest.fn(); - - middleware.dataErrors(error, {}, {}, next); - - expect(next).toBeCalledWith(error); - }); - - it('should pass through any Errors', () => { - const error = new Error(); - const next = jest.fn(); - - middleware.dataErrors(error, {}, {}, next); - - expect(next).toBeCalledWith(error); - }); -}); diff --git a/app/tests/unit/forms/common/middleware/errorHandler.spec.js b/app/tests/unit/forms/common/middleware/errorHandler.spec.js new file mode 100644 index 000000000..508d1df29 --- /dev/null +++ b/app/tests/unit/forms/common/middleware/errorHandler.spec.js @@ -0,0 +1,120 @@ +const { getMockRes } = require('@jest-mock/express'); +const Problem = require('api-problem'); +const Objection = require('objection'); + +const middleware = require('../../../../../src/forms/common/middleware'); + +describe('test error handler middleware', () => { + it('should handle an objection data error', () => { + const error = new Objection.DataError({ + nativeError: { message: 'This is a DataError' }, + }); + const { res, next } = getMockRes(); + + middleware.errorHandler(error, {}, res, next); + + expect(next).not.toBeCalled(); + expect(res.end).toBeCalledWith(expect.stringContaining('422')); + }); + + it('should handle an objection not found error', () => { + const error = new Objection.NotFoundError({ + nativeError: { message: 'This is a NotFoundError' }, + }); + const { res, next } = getMockRes(); + + middleware.errorHandler(error, {}, res, next); + + expect(next).not.toBeCalled(); + expect(res.end).toBeCalledWith(expect.stringContaining('404')); + }); + + it('should handle an objection unique violation error', () => { + const error = new Objection.UniqueViolationError({ + nativeError: { message: 'This is a UniqueViolationError' }, + }); + const { res, next } = getMockRes(); + + middleware.errorHandler(error, {}, res, next); + + expect(next).not.toBeCalled(); + expect(res.end).toBeCalledWith(expect.stringContaining('422')); + }); + + it('should handle an objection validation error', () => { + const error = new Objection.ValidationError({ + nativeError: { message: 'This is a ValidationError' }, + }); + const { res, next } = getMockRes(); + + middleware.errorHandler(error, {}, res, next); + + expect(next).not.toBeCalled(); + expect(res.end).toBeCalledWith(expect.stringContaining('422')); + }); + + it('should handle non-problem errors with a status', () => { + const error = new Error('This is a 400 status.'); + error.status = 400; + const { res, next } = getMockRes(); + + middleware.errorHandler(error, {}, res, next); + + expect(next).not.toBeCalled(); + expect(res.end).toBeCalledWith(expect.stringContaining('400')); + expect(res.end).toBeCalledWith(expect.stringContaining('This is a 400 status.')); + }); + + it('should handle non-problem errors with a status code', () => { + const error = new Error('This is a 400 status code.'); + error.statusCode = 400; + const { res, next } = getMockRes(); + + middleware.errorHandler(error, {}, res, next); + + expect(next).not.toBeCalled(); + expect(res.end).toBeCalledWith(expect.stringContaining('400')); + expect(res.end).toBeCalledWith(expect.stringContaining('This is a 400 status code.')); + }); + + it('should handle any non-500 Problems', () => { + const error = new Problem(429); + const { res, next } = getMockRes(); + + middleware.errorHandler(error, {}, res, next); + + expect(next).not.toBeCalled(); + expect(res.end).toBeCalledWith(expect.stringContaining('429')); + }); + + it('should pass through unknown objection errors', () => { + const error = new Objection.DBError({ + nativeError: { + message: 'This base class is never actually instantiated', + }, + }); + const { res, next } = getMockRes(); + + middleware.errorHandler(error, {}, res, next); + + expect(next).toBeCalledWith(error); + }); + + it('should pass through any 500s', () => { + const error = new Problem(500); + const { next } = getMockRes(); + + middleware.errorHandler(error, {}, {}, next); + + expect(next).toBeCalledWith(error); + }); + + it('should pass through any Errors without statuses', () => { + const error = new Error(); + const { next } = getMockRes(); + + middleware.errorHandler(error, {}, {}, next); + + expect(next).toBeCalledWith(error); + }); +}); diff --git a/tests/functional/cypress/e2e/about.cy.js b/tests/functional/cypress/e2e/about.cy.js index bb5d49ed7..e1d2f4013 100644 --- a/tests/functional/cypress/e2e/about.cy.js +++ b/tests/functional/cypress/e2e/about.cy.js @@ -2,9 +2,23 @@ const depEnv = Cypress.env('depEnv'); const baseUrl = Cypress.env('baseUrl'); + describe('Application About Page', () => { it('Visits the app about page', () => { - cy.visit(`/${depEnv}`); - cy.contains('h1', 'Create, publish forms, and receive submissions with the Common Hosted Forms Service.'); + + if(depEnv=="") + { + + cy.visit(`/app`); + cy.contains('Create, publish forms, and receive submissions with the Common Hosted Forms Service.').should('be.visible'); + } + else + { + + cy.visit(`/${depEnv}`); + cy.contains('Create, publish forms, and receive submissions with the Common Hosted Forms Service.').should('be.visible'); + cy.get('[data-test="base-auth-btn"] > .v-btn > .v-btn__content > span').click(); + } + }); }); diff --git a/tests/functional/cypress/e2e/form-design-advanceddata.cy.js b/tests/functional/cypress/e2e/form-design-advanceddata.cy.js index e6231a942..c119a06e2 100644 --- a/tests/functional/cypress/e2e/form-design-advanceddata.cy.js +++ b/tests/functional/cypress/e2e/form-design-advanceddata.cy.js @@ -84,75 +84,49 @@ it('Checks the Container component', () => { var pretty=JSON.stringify({ "label": "Applicant Details", - "customClass": "bg-primary", - "reorder": false, - "addAnotherPosition": "bottom", - "layoutFixed": false, - "enableRowGroups": false, - "initEmpty": false, - "tableView": false, - "key": "dataGrid", - "type": "datagrid", - "input": true, - "components": [ + "key": "dataGrid", + "type": "datagrid", + "input": true, + "components": [ { "label": "Children", "key": "children", "type": "datagrid", "input": true, - "validate": { - "minLength": 3, - "maxLength": 6 - }, + + "components": [ - { - "label": "First Name", - "key": "firstName", - "type": "textfield", - "input": true, - "tableView": true, + { + "label": "First Name", + "key": "firstName", + "type": "textfield", + "input": true, + "tableView": true }, - - { - "label": "Last Name", - "key": "lastName", - "type": "textfield", - "input": true, - "tableView": true - }, - { - "label": "Gender", - "key": "gender", - "type": "select", - "input": true, - data: { - values: [ - { - "value": "male", - "label": "Male" - }, - { - "value": "female", - "label": "Female" - }, - { - "value": "other", - "label": "Other" - } - ] + { + "label": "Gender", + "key": "gender", + "type": "select", + "input": true, + data: { + values: [ + { + "value": "male", + "label": "Male" }, - - } - + { + "value": "female", + "label": "Female" + } + ] + }, + + } ] - - } - - ] - - - - }) + } + ] + +}) cy.get('div.ace_content').type(pretty,{ parseSpecialCharSequences: false }); cy.get('button').contains('Save').click(); diff --git a/tests/functional/cypress/e2e/form-design-advancedfield.cy.js b/tests/functional/cypress/e2e/form-design-advancedfield.cy.js new file mode 100644 index 000000000..552fe8854 --- /dev/null +++ b/tests/functional/cypress/e2e/form-design-advancedfield.cy.js @@ -0,0 +1,324 @@ +import 'cypress-keycloak-commands'; +import 'cypress-drag-drop'; +import { formsettings } from '../support/login.js'; + +const depEnv = Cypress.env('depEnv'); + + +Cypress.Commands.add('waitForLoad', () => { + const loaderTimeout = 60000; + + cy.get('.nprogress-busy', { timeout: loaderTimeout }).should('not.exist'); +}); +describe('Form Designer', () => { + + beforeEach(()=>{ + + + cy.on('uncaught:exception', (err, runnable) => { + // Form.io throws an uncaught exception for missing projectid + // Cypress catches it as undefined: undefined so we can't get the text + console.log(err); + return false; + }); + }); + it('Visits the form settings page', () => { + + + cy.viewport(1000, 1100); + cy.waitForLoad(); + formsettings(); + + }); + it('Checks the simpleradioadvanced', () => { + + cy.viewport(1000, 1800); + cy.waitForLoad(); + + // Form design page with advanced Fields components + cy.get('button').contains('Advanced Fields').click(); + cy.get('button').contains('Advanced Fields').click(); + cy.waitForLoad(); + cy.get('div.formio-builder-form').then($el => { + const coords = $el[0].getBoundingClientRect(); + cy.waitForLoad(); + cy.get('[data-type="simpleradioadvanced"]') + + + .trigger('mousedown', { which: 1}, { force: true }) + .trigger('mousemove', coords.x, -850, { force: true }) + //.trigger('mousemove', coords.y, +100, { force: true }) + .trigger('mouseup', { force: true }); + cy.waitForLoad(); + cy.get(':nth-child(2) > .nav-link').click(); + cy.get(':nth-child(2) > .nav-link').click(); + //cy.get('[href="#data"]').click(); + + cy.get('input[name="data[values][0][label]"]').type('Canadian'); + cy.get('input[name="data[values][0][value]"]').type('1'); + + cy.waitForLoad(); + + cy.get('button').contains('Save').click(); + }); + + }); + it('Checks the simpletextareaadvanced', () => { + + cy.viewport(1000, 1800); + cy.waitForLoad(); + cy.get('div.formio-builder-form').then($el => { + const coords = $el[0].getBoundingClientRect(); + cy.get('[data-type="simpletextareaadvanced"]') + .trigger('mousedown', { which: 1}, { force: true }) + .trigger('mousemove', coords.x, -400, { force: true }) + //.trigger('mousemove', coords.y, +100, { force: true }) + .trigger('mouseup', { force: true }); + cy.get('input[name="data[customClass]"]').type('bg-primary'); + cy.waitForLoad(); + + cy.get('button').contains('Save').click(); + + + }); + }); + it('Checks the simpleurladvanced', () => { + cy.viewport(1000, 1800); + cy.waitForLoad(); + cy.get('div.formio-builder-form').then($el => { + const coords = $el[0].getBoundingClientRect(); + cy.get('[data-type="simpleurladvanced"]') + .trigger('mousedown', { which: 1}, { force: true }) + .trigger('mousemove', coords.x, -250, { force: true }) + //.trigger('mousemove', coords.y, +100, { force: true }) + .trigger('mouseup', { force: true }); + cy.get('input[name="data[prefix]"]').type('https://'); + + cy.get('input[name="data[suffix]"]').type('.com'); + + cy.waitForLoad(); + + cy.get('button').contains('Save').click(); + + + }); + }); + it('Checks the simpleselectboxesadvanced', () => { + cy.viewport(1000, 1800); + cy.waitForLoad(); + cy.get('div.formio-builder-form').then($el => { + const coords = $el[0].getBoundingClientRect(); + cy.get('[data-type="simpleselectboxesadvanced"]') + .trigger('mousedown', { which: 1}, { force: true }) + .trigger('mousemove', coords.x, -750, { force: true }) + + .trigger('mouseup', { force: true }); + cy.get(':nth-child(2) > .nav-link').click(); + cy.get(':nth-child(2) > .nav-link').click(); + cy.get('input[name="data[values][0][label]"]').type('Eligible'); + cy.get('input[name="data[values][0][value]"]').type('1'); + + + + cy.waitForLoad(); + + cy.get('button').contains('Save').click(); + + + }); + }); + + it('Checks the simpletagsadvanced', () => { + + cy.viewport(1000, 1800); + cy.waitForLoad(); + cy.get('div.formio-builder-form').then($el => { + const coords = $el[0].getBoundingClientRect(); + cy.get('[data-type="simpletagsadvanced"]') + .trigger('mousedown', { which: 1}, { force: true }) + .trigger('mousemove', coords.x, -250, { force: true }) + //.trigger('mousemove', coords.y, +100, { force: true }) + .trigger('mouseup', { force: true }); + cy.waitForLoad(); + + cy.get('button').contains('Save').click(); + + + }); + }); + + it('Checks the simplefile', () => { + cy.viewport(1000, 1800); + cy.waitForLoad(); + cy.get('button').contains('BC Government').click(); + cy.get('div.formio-builder-form').then($el => { + const coords = $el[0].getBoundingClientRect(); + cy.get('[data-type="simplefile"]') + .trigger('mousedown', { which: 1}, { force: true }) + .trigger('mousemove', coords.x, -150, { force: true }) + //.trigger('mousemove', coords.y, +100, { force: true }) + .trigger('mouseup', { force: true }); + cy.waitForLoad(); + + cy.get('button').contains('Save').click(); + + + }); + }); + + it('Checks the orgbook', () => { + + cy.viewport(1000, 1800); + cy.waitForLoad(); + cy.get('div.formio-builder-form').then($el => { + const coords = $el[0].getBoundingClientRect(); + cy.get('[data-type="orgbook"]') + .trigger('mousedown', { which: 1}, { force: true }) + .trigger('mousemove', coords.x, -30, { force: true }) + //.trigger('mousemove', coords.y, +100, { force: true }) + .trigger('mouseup', { force: true }); + cy.waitForLoad(); + + cy.get('button').contains('Save').click(); + + + }); + + }); + + it('Checks the bcaddress', () => { + cy.viewport(1000, 1800); + cy.waitForLoad(); + cy.get('div.formio-builder-form').then($el => { + const coords = $el[0].getBoundingClientRect(); + cy.get('[data-type="bcaddress"]') + .trigger('mousedown', { which: 1}, { force: true }) + .trigger('mousemove', coords.x, +20, { force: true }) + //.trigger('mousemove', coords.y, +100, { force: true }) + .trigger('mouseup', { force: true }); + cy.waitForLoad(); + + cy.get('button').contains('Save').click(); + + + }); + + }); + + it('Verify submission', () => { + cy.viewport(1000, 1800); + cy.waitForLoad(); + cy.waitForLoad(); + cy.intercept('GET', `/${depEnv}/api/v1/forms/*`).as('getForm'); + // Form saving + let savedButton = cy.get('[data-cy=saveButton]'); + expect(savedButton).to.not.be.null; + savedButton.trigger('click'); + cy.waitForLoad(); + + + // Go to My forms + cy.wait('@getForm').then(()=>{ + let userFormsLinks = cy.get('[data-cy=userFormsLinks]'); + expect(userFormsLinks).to.not.be.null; + userFormsLinks.trigger('click'); + }); + // Filter the newly created form + cy.location('search').then(search => { + //let pathName = fullUrl.pathname + let arr = search.split('='); + let arrayValues = arr[1].split('&'); + cy.log(arrayValues[0]); + //cy.log(arrayValues[1]); + //cy.log(arrayValues[2]); + cy.visit(`/${depEnv}/form/manage?f=${arrayValues[0]}`); + cy.waitForLoad(); + }) + + //Publish the form + cy.get('.v-label > span').click(); + + cy.get('span').contains('Publish Version 1'); + + cy.contains('Continue').should('be.visible'); + cy.contains('Continue').trigger('click'); + + //Share link verification + let shareFormButton = cy.get('[data-cy=shareFormButton]'); + expect(shareFormButton).to.not.be.null; + shareFormButton.trigger('click').then(()=>{ + //let shareFormLinkButton = cy.get('[data-cy=shareFormLinkButtonss]'); + let shareFormLinkButton=cy.get('.mx-2'); + expect(shareFormLinkButton).to.not.be.null; + shareFormLinkButton.trigger('click'); + + //Close form share window + cy.get('.v-card-actions > .v-btn > .v-btn__content > span').click(); + }); + cy.location('search').then(search => { + //let pathName = fullUrl.pathname + let arr = search.split('='); + let arrayValues = arr[1].split('&'); + cy.visit(`/${depEnv}/form/submit?f=${arrayValues[0]}`); + cy.waitForLoad(); + }) + cy.waitForLoad(); + // for print option verification + cy.get(':nth-child(2) > .d-print-none > :nth-child(1) > .v-btn').should('be.visible'); + cy.get('.mdi-printer').should('be.visible'); + cy.get('.mdi-content-save').should('be.visible'); + cy.waitForLoad(); + // Check registered business address + + cy.waitForLoad(); + cy.waitForLoad(); + cy.waitForLoad(); + cy.waitForLoad(); + cy.waitForLoad(); + cy.waitForLoad(); + cy.get('input[type="radio"]').click(); + cy.get('input[type="checkbox"]').click(); + + cy.get('div').find('textarea').type('some text'); + cy.get('input[name="data[bcaddress]"').type('goldstream'); + cy.get('input[name="data[simpleurladvanced]"').type('www.google'); + cy.get('.choices__inner').click(); + cy.get('.choices__inner').type('hello'); + cy.get('label').contains('Registered Business Name').click(); + cy.waitForLoad(); + cy.get('input[placeholder="Type to search"]').type("Thrifty Foods"); + cy.contains('THRIFTY FOODS').click(); + cy.get('input[name="data[bcaddress]"').click(); + cy.get('input[name="data[bcaddress]"').type('2260 Sooke'); + cy.get('.browse').should('have.attr', 'ref').and('include', 'fileBrowse'); + cy.get('.browse').should('have.attr', 'href').and('include', '#'); + cy.get('.browse').click(); + let fileUploadInputField = cy.get('input[type=file]'); + cy.get('input[type=file]').should('not.to.be.null'); + fileUploadInputField.attachFile('add1.png'); + cy.waitForLoad(); + cy.waitForLoad(); + //verify file uploads to object storage + + cy.get('.col-md-9 > a').should('have.attr', 'ref').and('include', 'fileLink'); + cy.get('div.col-md-2').contains('61.48 kB'); + + //form submission + cy.get('button').contains('Submit').click(); + cy.waitForLoad(); + cy.get('button').contains('Submit').click(); + // verify the components after submission + cy.get('span').contains('Canadian').should('be.visible'); + cy.get('span').contains('Eligible').should('be.visible'); + cy.get('.choices__inner > .choices__list > .choices__item').contains('hello'); + cy.get('.col-md-9 > a').contains('add1.png'); + cy.get('.ui > .choices__list > .choices__item').contains('THRIFTY FOODS'); + + + + }); + + + + +}); diff --git a/tests/functional/cypress/e2e/form-design-basicfields.cy.js b/tests/functional/cypress/e2e/form-design-basicfields.cy.js index faf875687..228d0dfd3 100644 --- a/tests/functional/cypress/e2e/form-design-basicfields.cy.js +++ b/tests/functional/cypress/e2e/form-design-basicfields.cy.js @@ -205,7 +205,7 @@ describe('Form Designer', () => { cy.get('span.btn').contains('Date / Time') .trigger('mousedown', { which: 1}, { force: true }) - .trigger('mousemove', coords.x, -70, { force: true }) + .trigger('mousemove', coords.x, -50, { force: true }) .trigger('mouseup', { force: true }); //cy.get('p').contains('Multi-line Text Component'); cy.get('button').contains('Save').click(); @@ -218,7 +218,7 @@ describe('Form Designer', () => { // Form Editing it('Form Edit', () => { - cy.viewport(1000, 1100); + cy.viewport(1000, 1800); cy.intercept('GET', `/${depEnv}/api/v1/forms/*`).as('getForm'); let savedButton = cy.get('[data-cy=saveButton]'); expect(savedButton).to.not.be.null; @@ -231,16 +231,16 @@ describe('Form Designer', () => { //Adding another component + cy.get('button').contains('Basic Fields').click(); cy.get('button').contains('Basic Fields').click(); cy.get('div.formio-builder-form').then($el => { const coords = $el[0].getBoundingClientRect(); - cy.get('span.btn').contains('Number') + cy.get('span.btn').contains('Text Field') .trigger('mousedown', { which: 1}, { force: true }) .trigger('mousemove', coords.x, -50, { force: true }) .trigger('mouseup', { force: true }); - //cy.get('p').contains('Multi-line Text Component'); - cy.get('input[name="data[label]"]').clear().type('ID Number'); + cy.get('button').contains('Save').click(); }); cy.get('[ref=removeComponent]').then($el => { @@ -274,14 +274,14 @@ describe('Form Designer', () => { //Form preview cy.visit(`/${depEnv}/form/preview?f=${dval[0]}&d=${arrayValues[0]}`); cy.waitForLoad(); - cy.get('label').contains('Last Name'); - cy.get('label').contains('First Name'); - cy.get('input[name="data[simplecheckbox1]"]').should('be.visible'); - cy.get('label').contains('Select all skills'); - cy.get('input[name="data[simplephonenumber1]').should('be.visible'); - cy.get('input[name="data[simpledatetime]').should('be.visible'); - cy.get('input[name="data[simpleemail]').should('be.visible'); - cy.get('input[name="data[simplenumber1]').should('be.visible'); + cy.get('label').contains('Last Name').should('be.visible'); + cy.get('label').contains('First Name').should('be.visible'); + cy.get('label').contains('Applying for self').should('be.visible'); + cy.get('label').contains('Select all skills').should('be.visible'); + cy.get('label').contains('Phone Number').should('be.visible'); + cy.get('label').contains('Date / Time').should('be.visible'); + cy.get('label').contains('Email').should('be.visible'); + //cy.get('label').contains('Number').should('be.visible'); cy.get('label').contains('Select Gender'); diff --git a/tests/functional/cypress/e2e/form-settings.cy.js b/tests/functional/cypress/e2e/form-settings.cy.js deleted file mode 100644 index 2e16eb730..000000000 --- a/tests/functional/cypress/e2e/form-settings.cy.js +++ /dev/null @@ -1,160 +0,0 @@ -import 'cypress-keycloak-commands'; -import 'cypress-drag-drop'; - -const depEnv = Cypress.env('depEnv'); - - -Cypress.Commands.add('waitForLoad', () => { - const loaderTimeout = 60000; - - cy.get('.nprogress-busy', { timeout: loaderTimeout }).should('not.exist'); -}); - - - -describe('Form Designer', () => { - - beforeEach(()=>{ - - cy.viewport(1000, 1800); - cy.waitForLoad(); - cy.kcLogout(); - cy.kcLogin("user"); - - cy.on('uncaught:exception', (err, runnable) => { - // Form.io throws an uncaught exception for missing projectid - // Cypress catches it as undefined: undefined so we can't get the text - console.log(err); - return false; - }); - }); - -// Verifying fields in the form settings page - it('Visits the form settings page', () => { - - cy.visit(`/${depEnv}/form/create`); - cy.location('pathname').should('eq', `/${depEnv}/form/create`); - cy.contains('h1', 'Form Settings'); - cy.get('.v-row > :nth-child(1) > .v-card > .v-card-title > span').contains('Form Title'); - - let title="title" + Math.random().toString(16).slice(2); - - - cy.get('#input-15').type(title); - cy.get('#input-17').type('test description'); - cy.get('#input-22').click(); - cy.get('.v-selection-control-group > .v-card').should('be.visible'); - cy.get('#input-23').click(); - //cy.get('.v-selection-control-group > .v-card').should('not.be.visible'); - //cy.get('#input-91').should('be.visible'); - cy.get('.v-row > .v-input > .v-input__control > .v-selection-control-group > :nth-child(1) > .v-label > span').contains('IDIR'); - cy.get('span').contains('Basic BCeID'); - - cy.get(':nth-child(2) > .v-card > .v-card-text > .v-input--error > :nth-child(2)').contains('Please select 1 log-in type'); - //cy.get('#input-92').should('be.visible'); - //cy.get('#input-93').should('be.visible'); - - - cy.get('#input-24').click(); - - - cy.get('#checkbox-25').click(); - cy.get('#checkbox-28').click(); - cy.get('#checkbox-38').click(); - cy.get('#checkbox-50').click(); - cy.get('#input-88').click(); - cy.get('#input-88').type('abc@gmail.com'); - - - - cy.get('#input-54').click(); - cy.contains("Citizens' Services (CITZ)").click(); - - cy.get('#input-58').click(); - - - cy.get('.v-list').should('contain','Applications that will be evaluated followed'); - cy.get('.v-list').should('contain','Collection of Datasets, data submission'); - cy.get('.v-list').should('contain','Registrations or Sign up - no evaluation'); - cy.get('.v-list').should('contain','Reporting usually on a repeating schedule or event driven like follow-ups'); - cy.get('.v-list').should('contain','Feedback Form to determine satisfaction, agreement, likelihood, or other qualitative questions'); - cy.contains('Reporting usually on a repeating schedule or event driven like follow-ups').click(); - cy.get('#input-64').click(); - cy.get('#input-70').click(); - cy.get('.mt-3 > .mdi-help-circle-outline').should('be.visible') - cy.get('.mt-3 > .mdi-help-circle-outline').click(); - cy.get('.d-flex > .v-input > .v-input__control > .v-field > .v-field__field > .v-field__input').click(); - cy.get('.d-flex > .v-input > .v-input__control > .v-field > .v-field__field > .v-field__input').type('test label'); - cy.get('#checkbox-76').click(); - cy.get('button').contains('Continue').click(); - - - // Form design page with simple textbox components - - - let textFields = ["First Name", "Middle Name", "Last Name"]; - - for(let i=0; i { - const bounds = $el[0].getBoundingClientRect(); - cy.get('span.btn').contains('Text Field') - .trigger('mousedown', { which: 1}, { force: true }) - .trigger('mousemove', bounds.x, -50, { force: true }) - .trigger('mouseup', { force: true }); - cy.get('p').contains('Text Field Component'); - cy.get('input[name="data[label]"]').clear().type(textFields[i]); - cy.get('button').contains('Save').click(); - }); - } - cy.intercept('GET', `/${depEnv}/api/v1/forms/*`).as('getForm'); - // Form saving - let savedButton = cy.get('[data-cy=saveButton]'); - expect(savedButton).to.not.be.null; - savedButton.trigger('click'); - cy.waitForLoad(); - - - // Go to My forms - cy.wait('@getForm').then(()=>{ - let userFormsLinks = cy.get('[data-cy=userFormsLinks]'); - expect(userFormsLinks).to.not.be.null; - userFormsLinks.trigger('click'); - }); - // Filter the newly created form - cy.location('search').then(search => { - //let pathName = fullUrl.pathname - let arr = search.split('='); - let arrayValues = arr[1].split('&'); - cy.log(arrayValues[0]); - //cy.log(arrayValues[1]); - //cy.log(arrayValues[2]); - cy.visit(`/${depEnv}/form/manage?f=${arrayValues[0]}`); - cy.waitForLoad(); - }) - - //Publish the form - cy.get('.v-label > span').click(); - - cy.get('span').contains('Publish Version 1'); - - cy.contains('Continue').should('be.visible'); - cy.contains('Continue').trigger('click'); - //Share link verification - let shareFormButton = cy.get('[data-cy=shareFormButton]'); - expect(shareFormButton).to.not.be.null; - shareFormButton.trigger('click').then(()=>{ - //let shareFormLinkButton = cy.get('[data-cy=shareFormLinkButtonss]'); - let shareFormLinkButton=cy.get('.mx-2'); - expect(shareFormLinkButton).to.not.be.null; - shareFormLinkButton.trigger('click'); - cy.get('.mx-2 > .v-btn').click(); - }); - - - - - - }); - -}); \ No newline at end of file diff --git a/tests/functional/cypress/e2e/form-simple-form-publish.cy.js b/tests/functional/cypress/e2e/form-simple-form-publish.cy.js new file mode 100644 index 000000000..88ab713e3 --- /dev/null +++ b/tests/functional/cypress/e2e/form-simple-form-publish.cy.js @@ -0,0 +1,109 @@ +import 'cypress-keycloak-commands'; +import 'cypress-drag-drop'; +import { formsettings } from '../support/login.js'; + +const depEnv = Cypress.env('depEnv'); + + +Cypress.Commands.add('waitForLoad', () => { + const loaderTimeout = 60000; + + cy.get('.nprogress-busy', { timeout: loaderTimeout }).should('not.exist'); +}); + + + +describe('Form Designer', () => { + + beforeEach(()=>{ + + + + cy.on('uncaught:exception', (err, runnable) => { + // Form.io throws an uncaught exception for missing projectid + // Cypress catches it as undefined: undefined so we can't get the text + console.log(err); + return false; + }); + }); + it('Visits the form settings page', () => { + + + cy.viewport(1000, 1100); + cy.waitForLoad(); + + formsettings(); + + + }); +// Verifying fields in the form settings page + it('Checks simplebcaddress and form submission', () => { + cy.viewport(1000, 1100); + cy.waitForLoad(); + + cy.get('button').contains('BC Government').click(); + cy.get('div.formio-builder-form').then($el => { + const coords = $el[0].getBoundingClientRect(); + cy.get('[data-key="simplebcaddress"]') + .trigger('mousedown', { which: 1}, { force: true }) + .trigger('mousemove', coords.x, -550, { force: true }) + //.trigger('mousemove', coords.y, +100, { force: true }) + .trigger('mouseup', { force: true }); + cy.waitForLoad(); + //cy.get('input[name="data[label]"]').type('s'); + cy.get('button').contains('Save').click(); + //cy.get('.btn-success').click(); + + + }); + cy.intercept('GET', `/${depEnv}/api/v1/forms/*`).as('getForm'); + // Form saving + let savedButton = cy.get('[data-cy=saveButton]'); + expect(savedButton).to.not.be.null; + savedButton.trigger('click'); + cy.waitForLoad(); + + + // Go to My forms + cy.wait('@getForm').then(()=>{ + let userFormsLinks = cy.get('[data-cy=userFormsLinks]'); + expect(userFormsLinks).to.not.be.null; + userFormsLinks.trigger('click'); + }); + // Filter the newly created form + cy.location('search').then(search => { + //let pathName = fullUrl.pathname + let arr = search.split('='); + let arrayValues = arr[1].split('&'); + cy.log(arrayValues[0]); + //cy.log(arrayValues[1]); + //cy.log(arrayValues[2]); + cy.visit(`/${depEnv}/form/manage?f=${arrayValues[0]}`); + cy.waitForLoad(); + }) + + //Publish the form + cy.get('.v-label > span').click(); + + cy.get('span').contains('Publish Version 1'); + + cy.contains('Continue').should('be.visible'); + cy.contains('Continue').trigger('click'); + //Share link verification + let shareFormButton = cy.get('[data-cy=shareFormButton]'); + expect(shareFormButton).to.not.be.null; + shareFormButton.trigger('click').then(()=>{ + //let shareFormLinkButton = cy.get('[data-cy=shareFormLinkButtonss]'); + let shareFormLinkButton=cy.get('.mx-2'); + expect(shareFormLinkButton).to.not.be.null; + shareFormLinkButton.trigger('click'); + cy.get('.mx-2 > .v-btn').click(); + }); + + + + + + }); + +}); \ No newline at end of file diff --git a/tests/functional/cypress/e2e/kitchen-sink.cy.js b/tests/functional/cypress/e2e/kitchen-sink.cy.js index aa1c9ee6c..bc9f4112f 100644 --- a/tests/functional/cypress/e2e/kitchen-sink.cy.js +++ b/tests/functional/cypress/e2e/kitchen-sink.cy.js @@ -45,7 +45,6 @@ const data = { // function helperTwoColumn() { cy.contains('span', 'Layout & Static Content').click(); - //cy.get('[href="#2Column"]').click(); cy.contains('span', 'Layout & Static Content').click(); cy.get('[href="#2Column"]').click(); @@ -76,9 +75,6 @@ function helperThreeColumn() { cy.contains('Select List in Fieldset 1').click(); - - - cy.get('[data-value="2"]').click(); } @@ -167,16 +163,13 @@ function helperFormFields() { cy.get('#day1-year').type('2021'); // time1 - - cy.get('[type="time"]').type('11:30'); - } -// + // Tests -// + describe('Kitchen Sink Example Form', () => { beforeEach(() => { // Form Load @@ -208,7 +201,7 @@ describe('Kitchen Sink Example Form', () => { // Visit Page cy.visit(`/${depEnv}/form/submit?f=${formId}`); - cy.wait(['@formOptions', '@formVersion']); + //cy.wait(['@formOptions', '@formVersion']); cy.location('pathname').should('eq', `/${depEnv}/form/submit`); cy.location('search').should('eq', `?f=${formId}`); }); diff --git a/tests/functional/cypress/support/login.js b/tests/functional/cypress/support/login.js index 36ad34089..6ae23b5b7 100644 --- a/tests/functional/cypress/support/login.js +++ b/tests/functional/cypress/support/login.js @@ -1,16 +1,21 @@ export function formsettings(){ - - - - const depEnv = Cypress.env('depEnv'); const username=Cypress.env('keycloakUsername'); const password=Cypress.env('keycloakPassword'); - cy.visit(`/${depEnv}`); + if(depEnv=="app") + { + cy.visit(`https://chefs-dev.apps.silver.devops.gov.bc.ca/app`); + } + else + { + cy.visit(`/${depEnv}`); + + } + cy.get('[data-test="base-auth-btn"] > .v-btn > .v-btn__content > span').click(); cy.get('[data-test="idir"]').click(); @@ -37,8 +42,8 @@ export function formsettings(){ cy.get('#checkbox-28').click(); cy.get('#checkbox-38').click(); cy.get('#checkbox-54').click(); - cy.get('#input-92').click(); - cy.get('#input-92').type('abc@gmail.com'); + cy.get('#input-91').click(); + cy.get('#input-91').type('abc@gmail.com'); cy.get('#input-58').click(); cy.contains("Citizens' Services (CITZ)").click(); cy.get('#input-62').click(); @@ -56,9 +61,4 @@ export function formsettings(){ cy.get('.d-flex > .v-input > .v-input__control > .v-field > .v-field__field > .v-field__input').type('test label'); cy.get('#checkbox-80').click(); cy.get('button').contains('Continue').click(); - - - - - } \ No newline at end of file diff --git a/tests/functional/package.json b/tests/functional/package.json index efbaf9969..42a84dc71 100644 --- a/tests/functional/package.json +++ b/tests/functional/package.json @@ -22,7 +22,6 @@ "license": "Apache-2.0", "dependencies": { "cypress-drag-drop": "^1.1.1", - "cypress-real-events": "^1.12.0", "date-fns": "^2.26.0", "har-validator": "^5.1.5" },