import { Callbacks } from 'cypress/types/jquery/index' const username = Cypress.env('username') const password = Cypress.env('password') const hostUrl = Cypress.env('hosturl') const appLocation = Cypress.env('appLocation') const longerCommandTimeout = Cypress.env('longerCommandTimeout') const serverType = Cypress.env('serverType') const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`) const fixturePath = 'excels/' context('excel tests: ', function () { this.beforeAll(() => { cy.visit(`${hostUrl}/SASLogon/logout`) cy.loginAndUpdateValidKey(true) }) this.beforeEach(() => { cy.visit(hostUrl + appLocation) visitPage('home') colorLog( `TEST START ---> ${ Cypress.mocha.getRunner().suite.ctx.currentTest.title }`, '#3498DB' ) }) it('1 | Uploads regular Excel file', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') attachExcelFile('regular_excel.xlsx', () => { submitExcel() rejectExcel(done) }) }) it('2 | Uploads Excel with data on the 7th tab', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') attachExcelFile('7th_tab_excel.xlsx', () => { submitExcel() rejectExcel(done) }) }) it('3 | Uploads Excel with missing columns (should fail)', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') attachExcelFile('missing_columns_excel.xlsx', () => { cy.get('.abortMsg', { timeout: longerCommandTimeout }) .should('exist') .then((elements: any) => { if (elements[0]) { if (elements[0].innerText.toLowerCase().includes('missing')) done() } }) }) }) it('4 | Uploads Excel with formulas', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary') attachExcelFile('formulas_excel.xlsx', () => { checkResultOfFormulaUpload(done) }) }) it('5 | Uploads Excel with no data rows', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') attachExcelFile('nodata_rows_excel.xlsx', () => { cy.get('.abortMsg', { timeout: longerCommandTimeout }) .should('exist') .then((elements: any) => { if (elements[0]) { if ( elements[0].innerText .toLowerCase() .includes('no relevant data found') ) done() } }) }) }) it('6 | Uploads Excel with a table that is surrounded by other data', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') attachExcelFile('surrounded_data_excel.xlsx', () => { submitExcel() rejectExcel(done) }) }) it('7 | Uploads Excel with a extra columns in the middle', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') attachExcelFile('extra_column_excel.xlsx', () => { submitExcel() rejectExcel(done) }) }) it('8 | Uploads Excel with a duplicate column', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') attachExcelFile('duplicate_column_excel.xlsx', () => { submitExcel() rejectExcel(done) }) }) it('9 | Uploads Excel with a duplicate row', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') attachExcelFile('duplicate_row_excel.xlsx', () => { submitExcel(() => { cy.get('.duplicate-keys-modal', { timeout: longerCommandTimeout }) .should('exist') .then((elements: any) => { if (elements[0]) { if (elements[0].innerText.toLowerCase().includes('duplicates')) done() } }) }) }) }) it('10 | Uploads Excel with a mixed content', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') attachExcelFile('mixed_content_excel.xlsx', () => { submitExcel(() => { cy.get('.modal-body').then((modalBody: any) => { if ( modalBody[0].innerHTML .toLowerCase() .includes(`invalid values are present`) ) { done() } }) }) }) }) it('11 | Uploads Excel with a blank columns', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') attachExcelFile('blank_columns_excel.xlsx', () => { cy.get('.abortMsg', { timeout: longerCommandTimeout }) .should('exist') .then((elements: any) => { if (elements[0]) { if (elements[0].innerText.toLowerCase().includes('missing')) done() } }) }) }) it('12 | Uploads Excel xls extension', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') attachExcelFile('regular_excel_xls.xls', () => { submitExcel() rejectExcel(done) }) }) // For some strange reason this file breaks cypress. When uploaded manually in DC it is working. // it('13 | Uploads Excel xlsm extension', (done) => { // openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') // attachExcelFile('regular_excel_macro.xlsm', () => { // submitExcel() // rejectExcel(done) // }) // }) it('14 | Uploads Excel with composite primary key', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary') attachExcelFile('MPE_DATADICTIONARY_composite_keys.xlsx', () => { submitExcel() rejectExcel(done) }) }) it('15 | Uploads Excel with missing row (empty table)', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary') attachExcelFile('MPE_DATADICTIONARY_missing_row.xlsx', () => { cy.get('.abortMsg', { timeout: longerCommandTimeout }) .should('exist') .then((elements: any) => { if (elements[0]) { if ( elements[0].innerText .toLowerCase() .includes('no relevant data found') ) done() } }) }) }) it('16 | Uploads Excel with merged cells', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary') attachExcelFile('MPE_DATADICTIONARY_merged_cells.xlsx', () => { submitExcel() rejectExcel(done) }) }) it('17 | Check uploaded values from excel with xls extension', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') attachExcelFile('regular_excel_xls.xls', () => { checkResultOfXLSUpload(done) }) }) it('18 | Uploads Excel with missing row (empty table)', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') attachExcelFile('blank_column_with_header.xlsx', () => { cy.get('.btn-upload-preview', { timeout: 60000 }) .should('be.visible') .then(() => { cy.get('#hotTable', { timeout: 30000 }) .find('div.ht_master.handsontable') .find('div.wtHolder') .find('div.wtHider') .find('div.wtSpreader') .find('table.htCore') .find('tbody') .then((data) => { let allEmpty = true for (let col = 0; col < data[0].children.length; col++) { const cell: any = data[0].children[col].children[5] if (cell.innerText !== '') { allEmpty = false break } } if (allEmpty) done() }) }) }) }) it('19 | Uploads Excel with data on random sheet surrounded with all empty cells', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') attachExcelFile('surrounded_data_all_cells_empty_excel.xlsx', () => { checkResultOfXLSUpload(done) }) }) it('20 | Uploads Excel with data surrounded with empty cells ', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') attachExcelFile('surrounded_data_empty_cells_excel.xlsx', () => { checkResultOfXLSUpload(done) }) }) it('21 | Uploads regular Excel file with first row marked for Delete (yes)', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') attachExcelFile('regular_excel_with_delete.xlsx', () => { cy.get('.btn-upload-preview', { timeout: 60000 }) .should('be.visible') .then(() => { cy.get('#hotTable', { timeout: 30000 }) .find('div.ht_master.handsontable') .find('div.wtHolder') .find('div.wtHider') .find('div.wtSpreader') .find('table.htCore') .find('tbody') .then((data: JQuery) => { const firstRowFirstCol: Partial = data[0].children[0].children[1] if ( firstRowFirstCol.innerText && !firstRowFirstCol.innerText.toLowerCase().includes('yes') ) { done('Delete? column from file not applied') } }) .then(() => { submitExcel() rejectExcel(done) }) }) }) }) it('22 | Uploads password protected Excel and unlocks with correct password', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') cy.get('.buttonBar button:last-child') .should('exist') .click() .then(() => { cy.get('input[type="file"]#file-upload') .attachFile(`/${fixturePath}/regular_excel_password.xlsx`) .then(() => { // Wait for password modal to appear cy.get('#filePasswordInput', { timeout: 10000 }) .should('be.visible') .type('123123') // Click Unlock button cy.get('.btn.btn-success-outline').should('not.be.disabled').click() // Click away the overlay cy.get('.modal-footer .btn.btn-primary', { timeout: 5000 }).click() // Verify file loads successfully cy.get('.btn-upload-preview', { timeout: 60000 }) .should('be.visible') .then(() => { submitExcel() rejectExcel(done) }) }) }) }) it('23 | Uploads password protected Excel and handles wrong password', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') cy.get('.buttonBar button:last-child') .should('exist') .click() .then(() => { cy.get('input[type="file"]#file-upload') .attachFile(`/${fixturePath}/regular_excel_password.xlsx`) .then(() => { // First attempt: Enter wrong password cy.get('#filePasswordInput', { timeout: 10000 }) .should('be.visible') .type('wrongpassword') cy.get('.btn.btn-success-outline').should('not.be.disabled').click() // Verify error message appears cy.get('.modal-footer .color-red', { timeout: 10000 }) .should('be.visible') .should('contain', "Sorry that didn't work, try again.") // Modal should still be open for retry cy.get('#filePasswordInput') .should('be.visible') .clear() .type('123123') // Second attempt: Enter correct password cy.get('.btn.btn-success-outline').should('not.be.disabled').click() // Click away the overlay cy.get('.modal-footer .btn.btn-primary', { timeout: 5000 }).click() // Verify file loads successfully cy.get('.btn-upload-preview', { timeout: 60000 }) .should('be.visible') .then(() => { submitExcel() rejectExcel(done) }) }) }) }) it('24 | Uploads Excel with leading whitespace in header row (should still succeed)', (done) => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') // Every header except the first has a leading space, matching a // real-world defect from copy-pasting between spreadsheets/systems. // Header matching must tolerate // this rather than reporting the columns missing and aborting. attachExcelFile('leading_whitespace_header_excel.xlsx', () => { submitExcel() rejectExcel(done) }) }) it('25 | Keeps decimals when the upload preview becomes a manual edit', () => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') // Entering manual edit mode discards the source file but keeps the values // parsed from it. SOME_NUM holds 0.901045951946194 in the first data row // of the fixture, so a truncated or zeroed cell fails the assertion. attachExcelFile('regular_excel.xlsx', () => { getFirstRowCells().then((previewCells) => { expect(previewCells).to.include(decimalSomeNum) cy.get('.btn-upload-preview.btn-primary').click() cy.get('.modal-footer .btn-success-outline').click() getFirstRowCells().then((editCells) => { expect(editCells).to.include(decimalSomeNum) }) }) }) }) it('26 | Discarding an upload that fails validation clears the red cells', () => { openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') // The loaded table's row count is what the grid must return to - the // preview replaces the data source with the parsed file. cy.get('h4.editor-title', { timeout: longerCommandTimeout }) .invoke('text') .should('match', /\(\d+ rows?/) .then((titleBefore) => { const rowsBefore = titleBefore.match(/\(([\d,]+) rows?/)?.[1] attachExcelFile('mixed_content_excel.xlsx', () => { // The preview paints the cells that fail validation red - the state // the user is looking at when they decide to abandon the file. cy.get('#hotTable .htInvalid', { timeout: longerCommandTimeout }).should('have.length.greaterThan', 0) cy.get('.buttonBar .btn-upload-preview.btn-outline-danger').click() cy.get('clr-modal .modal-footer .btn-success-outline').click() // Back to the loaded table, with no red cells left over. cancelEdit // re-runs the HOT validation after restoring the data; without that // the invalid cell meta survives the restore and the grid stays red. cy.get('h4.editor-title', { timeout: longerCommandTimeout }).should( 'contain.text', `(${rowsBefore} rows` ) cy.get('#hotTable .htInvalid').should('not.exist') // And the page is back in its normal read-only state, not the // preview's button bar. cy.get('.btnCtrl .btn-primary', { timeout: longerCommandTimeout }).should('contain.text', 'Edit') }) }) }) // Large files break Cypress // it ('? | Uploads Excel with size of 5MB', (done) => { // attachExcelFile('5mb_excel.xlsx', () => { // submitExcel(); // rejectExcel(done); // }); // }) // it ('? | Uploads Excel with size of 15MB', (done) => { // attachExcelFile('15mb_excel.xlsx', () => { // submitExcel(); // rejectExcel(done); // }); // }) //Large files tests end this.afterEach(() => { colorLog(`TEST END -------------`, '#3498DB') }) }) const openTableFromTree = (libNameIncludes: string, tablename: string) => { cy.get('.app-loading', { timeout: longerCommandTimeout }) .should('not.exist') .then(() => { cy.get('.nav-tree clr-tree > clr-tree-node', { timeout: longerCommandTimeout }).then((treeNodes: any) => { let viyaLib for (let node of treeNodes) { if (node.innerText.toLowerCase().includes(libNameIncludes)) { viyaLib = node break } } cy.get(viyaLib).within(() => { cy.get('.clr-tree-node-content-container > button').click() cy.get('.clr-treenode-link').then((innerNodes: any) => { for (let innerNode of innerNodes) { if (innerNode.innerText.toLowerCase().includes(tablename)) { innerNode.click() break } } }) }) }) }) } const attachExcelFile = (excelFilename: string, callback?: any) => { cy.get('.buttonBar button:last-child') .should('exist') .click() .then(() => { cy.get('input[type="file"]#file-upload') .attachFile(`/${fixturePath}/${excelFilename}`) .then(() => { cy.get('.clr-abort-modal .modal-title').then((modalTitle) => { if (!modalTitle[0].innerHTML.includes('Abort Message')) { cy.get('.modal-footer .btn.btn-primary').then((modalBtn) => { modalBtn.click() if (callback) callback() }) } else { if (callback) callback() } }) }) }) } const decimalSomeNum = '0.901045951946194' // The row's cell texts, without depending on column positions: the preview // grid renders row-status and delete cells in an order that varies. const getFirstRowCells = () => cy .get('#hotTable', { timeout: longerCommandTimeout }) .find('div.ht_master.handsontable tbody tr') .eq(0) .then(($row) => Array.from($row[0].children).map((cell: any) => cell.innerText.trim()) ) const submitExcel = (callback?: any) => { cy.get('.buttonBar button.preview-submit', { timeout: longerCommandTimeout }) .click() .then(() => { if (callback) callback() }) } const rejectExcel = (callback?: any) => { cy.get('button', { timeout: longerCommandTimeout }) .should('contain', 'Approve') .then((allButtons: any) => { for (let approvalButton of allButtons) { if (approvalButton.innerText.toLowerCase().includes('approve')) { approvalButton.click() break } } cy.get('button.btn-danger') .should('exist') .should('not.be.disabled') .click() .then(() => { cy.get('.modal-footer button.btn-success-outline') .click() .then(() => { cy.get('app-history') .should('exist') .then(() => { if (callback) callback() }) }) }) }) } const acceptExcel = (callback?: any) => { cy.get('button', { timeout: longerCommandTimeout }) .should('contain', 'Approve') .then((allButtons: any) => { for (let approvalButton of allButtons) { if (approvalButton.innerText.toLowerCase().includes('approve')) { approvalButton.click() break } } cy.get('#acceptBtn') .should('exist') .should('not.be.disabled') .click() .then(() => { if (callback) { callback() } }) }) } const checkResultOfFormulaUpload = (callback?: any) => { cy.get('#hotTable', { timeout: longerCommandTimeout }) .find('div.ht_master.handsontable') .find('div.wtHolder') .find('div.wtHider') .find('div.wtSpreader') .find('table.htCore') .find('tbody') .then((data) => { const cell: any = data[0].children[0].children[5] expect(cell.innerText).to.equal('2') if (callback) callback() }) } const checkResultOfXLSUpload = (callback?: any) => { // Config-default width — wide enough that the date/datetime/time columns are // not virtualized away (the old 1280 width hid the trailing time columns). cy.viewport(1600, 900) cy.get('#hotTable', { timeout: 30000 }) .find('div.ht_master.handsontable') .find('div.wtHolder') .find('div.wtHider') .find('div.wtSpreader') .find('table.htCore') .find('tbody') .then((data) => { let cell: any = data[0].children[0].children[2] expect(cell.innerText).to.equal('0') cell = data[0].children[0].children[3] expect(cell.innerText).to.equal('this is dummy data changed in excel') cell = data[0].children[0].children[4] expect(cell.innerText).to.equal('▼\nOption 1') cell = data[0].children[0].children[5] expect(cell.innerText).to.equal('42') // Cells render the raw stored ISO string verbatim (locale-independent; // see registerIntlCellTypes). date -> YYYY-MM-DD, datetime -> // YYYY-MM-DD HH:mm:ss, time -> HH:mm:ss. cell = data[0].children[0].children[6] expect(cell.innerText).to.equal('1960-02-12') cell = data[0].children[0].children[7] expect(cell.innerText).to.equal('1960-01-01 00:00:42') cell = data[0].children[0].children[8] expect(cell.innerText).to.equal('00:00:42') if (callback) callback() }) cy.get('#hotTable', { timeout: 30000 }) .find('div.ht_master.handsontable') .find('div.wtHolder') .scrollTo('right') .find('div.wtHider') .find('div.wtSpreader') .find('table.htCore') .find('tbody') .then((data) => { let cell: any = data[0].children[0].children[1] cell = data[0].children[0].children[9] expect(cell.innerText).to.equal('44') if (callback) callback() }) } const visitPage = (url: string) => { cy.visit(`${hostUrl}${appLocation}/#/${url}`) } const colorLog = (msg: string, color: string) => { console.log('%c' + msg, 'color:' + color + ';font-weight:bold;') }