Two new DQ rule types apply regular expressions to cell values: HARDREGEX blocks submission on a non-matching value (same path as the existing CASE/MINVAL/MAXVAL rules); SOFTREGEX is display-only — a non-matching value gets a yellow warning cell but can still submit, so it's wired as a grid renderer rather than a validator, and mirrored in the edit-record modal (which has no grid renderer to hook into) via DcValidator.failsSoftRegex. Both rules exempt blank and SAS special missing values, and fail open on a malformed pattern rather than blocking every submission on that column. HARDREGEX takes precedence when both rules apply to the same column, so a failing value renders red/blocked, never yellow.
366 lines
11 KiB
TypeScript
366 lines
11 KiB
TypeScript
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_general/'
|
|
|
|
context('editor tests: ', function () {
|
|
this.beforeAll(() => {
|
|
cy.visit(`${hostUrl}/SASLogon/logout`)
|
|
cy.loginAndUpdateValidKey()
|
|
})
|
|
|
|
this.beforeEach(() => {
|
|
cy.visit(hostUrl + appLocation)
|
|
|
|
visitPage('home')
|
|
})
|
|
|
|
it('1 | Submits duplicate primary keys', (done) => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary')
|
|
|
|
attachExcelFile('MPE_DATADICTIONARY_duplicate_keys.xlsx', () => {
|
|
clickOnUploadPreview(() => {
|
|
confirmEditPreviewFile(() => {
|
|
submitTable(() => {
|
|
cy.get('.modal-body').then((modalBody: any) => {
|
|
if (modalBody[0].innerText.includes(`Duplicates found:`)) {
|
|
done()
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('2 | Submits null cells which must not be null', (done) => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
cy.get('.ht_master tbody tr').then((rows: any) => {
|
|
cy.get(rows[1].childNodes[2])
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused()
|
|
.clear()
|
|
.type('{enter}')
|
|
.then(() => {
|
|
submitTable(() => {
|
|
cy.get('.modal-body').then((modalBody: any) => {
|
|
if (
|
|
modalBody[0].innerHTML
|
|
.toLowerCase()
|
|
.includes(`invalid values are present`)
|
|
) {
|
|
done()
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('3 | Gets basic dynamic cell validation', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
cy.get('.ht_master tbody tr').then((rows: any) => {
|
|
cy.get(rows[1].childNodes[5])
|
|
.click({ force: true })
|
|
.then(($td) => {
|
|
cy.get('.htAutocompleteArrow', { withinSubject: $td }).should(
|
|
'exist'
|
|
)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('4 | Gets advanced dynamic cell validation', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_tables')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
cy.get('.ht_master tbody tr').then((rows: any) => {
|
|
cy.get(rows[1].childNodes[3])
|
|
.click({ force: true })
|
|
.then(($td) => {
|
|
cy.get('.htAutocompleteArrow', { withinSubject: $td }).should(
|
|
'exist'
|
|
)
|
|
cy.get('.htAutocompleteArrow', {
|
|
withinSubject: rows[1].childNodes[7]
|
|
}).should('exist')
|
|
cy.get('.htAutocompleteArrow', {
|
|
withinSubject: rows[1].childNodes[8]
|
|
}).should('exist')
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// REGEX_HARD_COL/REGEX_SOFT_COL only exist un-stripped on MPE_X_NEW (see
|
|
// RULE_DEMO_COLS in the getdata.js mock) — MPE_X_TEST stays clean to
|
|
// match the excel fixtures.
|
|
it('5 | Blocks submission of a value that fails HARDREGEX', (done) => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
scrollGridRight()
|
|
|
|
getCellByHeaderAndRow(1, 'REGEX_HARD_COL')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused()
|
|
.clear()
|
|
.type('not-an-email{enter}')
|
|
.then(() => {
|
|
submitTable(() => {
|
|
cy.get('.modal-body').then((modalBody: any) => {
|
|
if (
|
|
modalBody[0].innerHTML
|
|
.toLowerCase()
|
|
.includes(`invalid values are present`)
|
|
) {
|
|
done()
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('6 | Warns (but still submits) a value that fails SOFTREGEX', (done) => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
scrollGridRight()
|
|
|
|
getCellByHeaderAndRow(1, 'REGEX_SOFT_COL')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused()
|
|
.clear()
|
|
.type('not a postcode{enter}')
|
|
.then(() => {
|
|
getCellByHeaderAndRow(1, 'REGEX_SOFT_COL').should(
|
|
'have.class',
|
|
'dc-warning-cell'
|
|
)
|
|
|
|
submitTable(() => {
|
|
// Validation passed despite the SOFTREGEX warning: the
|
|
// confirm-submit modal's Submit button is enabled
|
|
// (validationDone === 1), not the "invalid values" abort.
|
|
cy.get('#submitBtn', { timeout: longerCommandTimeout })
|
|
.should('exist')
|
|
.should('not.be.disabled')
|
|
.then(() => done())
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// Handsontable virtualizes columns — with 17 columns on MPE_X_NEW, only
|
|
// the ones in the current viewport actually exist in the DOM. Scroll all
|
|
// the way right so REGEX_HARD_COL/REGEX_SOFT_COL (the last two) render at
|
|
// all. Same technique already used in excel.cy.ts. Must be called (and
|
|
// re-settle) before any header/body query below, since scrolling replaces
|
|
// the previously-rendered column nodes.
|
|
const scrollGridRight = () => {
|
|
return cy
|
|
.get('#hotTable')
|
|
.find('div.ht_master.handsontable')
|
|
.find('div.wtHolder')
|
|
.scrollTo('right')
|
|
}
|
|
|
|
// Locates a body cell by its column's header text rather than a hardcoded
|
|
// childNodes index. Handsontable's hiddenColumns plugin (e.g. HIDDEN_COL,
|
|
// used by several demo columns ahead of REGEX_HARD_COL/REGEX_SOFT_COL in
|
|
// COLHEADERS) removes a hidden column's cells from BOTH the header row and
|
|
// every body row identically, so the header row's th index always lines up
|
|
// with the same-position childNode in any body row — this sidesteps having
|
|
// to know how many hidden columns precede the target. Re-queries both the
|
|
// header and body row fresh each call (rather than accepting a pre-fetched
|
|
// row) since virtualization/scrolling can replace either pane's nodes.
|
|
//
|
|
// The `.should()` retries until headerText actually shows up: Handsontable
|
|
// re-renders virtualized columns asynchronously after a scroll event, on
|
|
// its own schedule outside Cypress's command queue, so scrollTo() settling
|
|
// does not mean the target column has been rendered yet.
|
|
const getCellByHeaderAndRow = (rowIndex: number, headerText: string) => {
|
|
return cy
|
|
.get('.ht_clone_top .htCore thead tr th')
|
|
.should(($ths) => {
|
|
const texts = [...$ths].map((th) => th.innerText.trim())
|
|
expect(texts).to.include(headerText)
|
|
})
|
|
.then(($ths) => {
|
|
const index = [...$ths].findIndex(
|
|
(th) => th.innerText.trim() === headerText
|
|
)
|
|
|
|
return cy
|
|
.get('.ht_master tbody tr')
|
|
.then((rows: any) => rows[rowIndex].childNodes[index])
|
|
.then((cell) => cy.get(cell))
|
|
})
|
|
}
|
|
|
|
const clickOnEdit = (callback?: any) => {
|
|
cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout })
|
|
.click()
|
|
.then(() => {
|
|
if (callback) callback()
|
|
})
|
|
}
|
|
|
|
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')
|
|
.click()
|
|
.then(() => {
|
|
cy.get('input[type="file"]#file-upload')
|
|
.attachFile(`/${fixturePath}/${excelFilename}`)
|
|
.then(() => {
|
|
cy.get('.modal-footer .btn.btn-primary').then((modalBtn) => {
|
|
modalBtn.click()
|
|
if (callback) callback()
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
const clickOnUploadPreview = (callback?: any) => {
|
|
cy.get('.buttonBar button.btn-primary.btn-upload-preview')
|
|
.click()
|
|
.then(() => {
|
|
if (callback) callback()
|
|
})
|
|
}
|
|
|
|
const confirmEditPreviewFile = (callback?: any) => {
|
|
cy.get('.modal-footer button.btn-success-outline')
|
|
.click()
|
|
.then(() => {
|
|
if (callback) callback()
|
|
})
|
|
}
|
|
|
|
const submitTable = (callback?: any) => {
|
|
cy.get('.btnCtrl button.btn-primary')
|
|
.click()
|
|
.then(() => {
|
|
if (callback) callback()
|
|
})
|
|
}
|
|
|
|
const submitTableMessage = (callback?: any) => {
|
|
cy.get('.modal-footer .btn.btn-sm.btn-success-outline')
|
|
.click()
|
|
.then(() => {
|
|
if (callback) callback()
|
|
})
|
|
}
|
|
|
|
const submitExcel = (callback?: any) => {
|
|
cy.get('.buttonBar button.preview-submit')
|
|
.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 visitPage = (url: string) => {
|
|
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
|
}
|