A derived variable may need to reference one defined before it (a label that concatenates a derived region, for example), so the order has to be configured rather than assumed. - MPE_DERIVED: new VARIABLE_ORDER column (ddl, makedatamodel, migration). - mpe_derived: statements are generated in VARIABLE_ORDER then name sequence. VARIABLE_ORDER is required and must be a positive integer. A reference to a derived variable with an equal or higher order is rejected, because SAS would otherwise treat it as uninitialised and return a missing value with no error; references to lower-order variables are allowed. - mock: same ordering, each variable compiled against the physical columns plus those defined earlier, so the frontend can exercise a chained variable. - makedata: VARIABLE_ORDER on both demo rows plus a chained REGION_LABEL. - cypress: asserts the chained value in the grid. - tests: the getdata test seeds a second variable built on the first. - docs: VARIABLE_ORDER documented, with the ordered rule replacing the self-contained one.
163 lines
5.2 KiB
TypeScript
163 lines
5.2 KiB
TypeScript
export {}
|
|
//
|
|
// Derived (computed) variables are configured per table in MPE_DERIVED and
|
|
// created at read time, ahead of the filter, so that they can be filtered on.
|
|
//
|
|
// TESTDATA.DERIVED_DEMO carries two of them, neither of which is a physical
|
|
// column of the table:
|
|
// REGION ifc(STATE in ('CA','OR','WA'),'West','East')
|
|
// LINE_VALUE QTY * PRICE
|
|
//
|
|
// The mock implements the derivation in getdata.js / getcolvals.js (see
|
|
// mpeDerived in dcMockUtils.js), so the EDIT screen can be exercised without a
|
|
// SAS server. The real evaluation is done by SAS.
|
|
|
|
const hostUrl = Cypress.env('hosturl')
|
|
const appLocation = Cypress.env('appLocation')
|
|
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
|
|
|
context('derived variables: ', function () {
|
|
this.beforeAll(() => {
|
|
cy.visit(`${hostUrl}/SASLogon/logout`, { timeout: longerCommandTimeout })
|
|
cy.loginAndUpdateValidKey(true)
|
|
})
|
|
|
|
this.beforeEach(() => {
|
|
cy.viewport(1440, 900)
|
|
bootApp()
|
|
})
|
|
|
|
it('1 | derived columns are shown in the EDIT grid with computed values', () => {
|
|
openEditor('TESTDATA.DERIVED_DEMO')
|
|
|
|
getCellByHeaderAndRow(0, 'REGION').should('have.text', 'West')
|
|
getCellByHeaderAndRow(1, 'REGION').should('have.text', 'East')
|
|
getCellByHeaderAndRow(0, 'LINE_VALUE').should('have.text', '21')
|
|
getCellByHeaderAndRow(1, 'LINE_VALUE').should('have.text', '99')
|
|
// REGION_LABEL is built on REGION, so it only resolves if the derived
|
|
// statements run in VARIABLE_ORDER sequence
|
|
getCellByHeaderAndRow(0, 'REGION_LABEL').should('have.text', 'West-CA')
|
|
getCellByHeaderAndRow(1, 'REGION_LABEL').should('have.text', 'East-NY')
|
|
|
|
cy.screenshot('derived-1-grid', { capture: 'viewport' })
|
|
})
|
|
|
|
it('2 | filters on a character derived column', () => {
|
|
openEditor('TESTDATA.DERIVED_DEMO')
|
|
|
|
openFilterPopup()
|
|
setFilterVariable('REGION')
|
|
setFilterValue('West')
|
|
submitFilter()
|
|
|
|
cy.get('.infoBar-text').should('contain.text', "REGION = 'West'")
|
|
cy.get('.ht_master .htCore tbody tr', {
|
|
timeout: longerCommandTimeout
|
|
}).should('have.length', 2)
|
|
getCellByHeaderAndRow(1, 'REGION').should('have.text', 'West')
|
|
|
|
cy.screenshot('derived-2-filter-region', { capture: 'viewport' })
|
|
})
|
|
|
|
it('3 | filters on a numeric derived column', () => {
|
|
openEditor('TESTDATA.DERIVED_DEMO')
|
|
|
|
openFilterPopup()
|
|
setFilterVariable('LINE_VALUE')
|
|
setFilterOperator('>')
|
|
setFilterValue('50')
|
|
submitFilter()
|
|
|
|
cy.get('.ht_master .htCore tbody tr', {
|
|
timeout: longerCommandTimeout
|
|
}).should('have.length', 1)
|
|
getCellByHeaderAndRow(0, 'LINE_VALUE').should('have.text', '99')
|
|
|
|
cy.screenshot('derived-3-filter-value', { capture: 'viewport' })
|
|
})
|
|
})
|
|
|
|
/**
|
|
* Cypress clears cookies between tests, which drops the SASjs Server session
|
|
* and lands the app back on the evaluation agreement card. Accept it when it is
|
|
* shown - a no-op when the session survived.
|
|
*/
|
|
const bootApp = () => {
|
|
cy.visit(hostUrl, { timeout: longerCommandTimeout })
|
|
cy.get('body').then(($body: any) => {
|
|
if ($body.find('#TCS input[type="checkbox"]').length) {
|
|
cy.get('#TCS input[type="checkbox"]').check({ force: true })
|
|
cy.wait(4000)
|
|
}
|
|
})
|
|
}
|
|
|
|
/** Opens a table straight in the EDIT screen and waits for the grid. */
|
|
const openEditor = (libMem: string) => {
|
|
cy.visit(`${hostUrl}${appLocation}/#/editor/${libMem}`, {
|
|
timeout: longerCommandTimeout
|
|
})
|
|
cy.get('.ht_clone_top .htCore thead tr th', {
|
|
timeout: longerCommandTimeout
|
|
}).should('exist')
|
|
cy.wait(2500)
|
|
}
|
|
|
|
const openFilterPopup = () => {
|
|
cy.contains('button.btnView', 'Filter', {
|
|
timeout: longerCommandTimeout
|
|
}).click()
|
|
cy.get('.filter-modal', { timeout: longerCommandTimeout }).should(
|
|
'be.visible'
|
|
)
|
|
}
|
|
|
|
/**
|
|
* The soft-select inputs drop a transparent .overlay click-catcher over the
|
|
* modal while their suggestion list is open, so they need force.
|
|
*/
|
|
const setFilterVariable = (column: string) => {
|
|
cy.get('#vals_var_id0_0').clear({ force: true }).type(column, { force: true })
|
|
cy.get('#datalist_vals_var_id0_0 option').contains(column).click({
|
|
force: true
|
|
})
|
|
cy.get('#vals_var_id0_0').trigger('keyup', { key: 'Escape', force: true })
|
|
cy.wait(400)
|
|
}
|
|
|
|
const setFilterValue = (value: string) => {
|
|
cy.get('#vals_0_0').clear({ force: true }).type(value, { force: true })
|
|
cy.get('#vals_0_0').trigger('keyup', { key: 'Escape', force: true })
|
|
cy.wait(400)
|
|
}
|
|
|
|
const setFilterOperator = (operator: string) => {
|
|
cy.get('.filter-modal .operator-col select').first().select(operator)
|
|
cy.wait(500)
|
|
}
|
|
|
|
const submitFilter = () => {
|
|
cy.contains('.filter-modal button', 'Ok').click()
|
|
cy.get('.app-loading', { timeout: longerCommandTimeout }).should('not.exist')
|
|
cy.wait(2500)
|
|
}
|
|
|
|
const getCellByHeaderAndRow = (rowIndex: number, headerText: string) => {
|
|
return cy
|
|
.get('.ht_clone_top .htCore thead tr th')
|
|
.should(($ths) => {
|
|
const texts = [...$ths].map((th) => (th as HTMLElement).innerText.trim())
|
|
expect(texts).to.include(headerText)
|
|
})
|
|
.then(($ths) => {
|
|
const index = [...$ths].findIndex(
|
|
(th) => (th as HTMLElement).innerText.trim() === headerText
|
|
)
|
|
|
|
return cy
|
|
.get('.ht_master tbody tr')
|
|
.then((rows: any) => rows[rowIndex].childNodes[index])
|
|
.then((cell) => cy.get(cell))
|
|
})
|
|
}
|