Replace one-shot .then()/for-loop matching with retrying .should()/cy.contains() lookups, drop stale cached DOM node references (viyaLib) in favor of live re-queries.
650 lines
19 KiB
TypeScript
650 lines
19 KiB
TypeScript
import { cloneDeep } from 'lodash-es'
|
|
|
|
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}`)
|
|
|
|
context('editor tests: ', function () {
|
|
this.beforeAll(() => {
|
|
cy.visit(`${hostUrl}/SASLogon/logout`)
|
|
cy.loginAndUpdateValidKey(true)
|
|
})
|
|
|
|
this.beforeEach(() => {
|
|
cy.visit(hostUrl + appLocation)
|
|
cy.wait(2000)
|
|
|
|
cy.get('body').then(($body) => {
|
|
const usernameInput = $body.find('input.username')[0]
|
|
|
|
if (usernameInput && !Cypress.dom.isHidden(usernameInput)) {
|
|
cy.get('input.username').type(username)
|
|
cy.get('input.password').type(password)
|
|
|
|
cy.get('.login-group button').click()
|
|
}
|
|
})
|
|
|
|
visitPage('home')
|
|
})
|
|
|
|
it('1 | Add one viewbox', (done) => {
|
|
const viewbox_table = 'mpe_audit'
|
|
const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
|
|
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
|
|
cy.get('.viewbox-open').click()
|
|
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
|
|
|
|
cy.contains('.open-viewbox', viewbox_table, {
|
|
matchCase: false,
|
|
timeout: longerCommandTimeout
|
|
})
|
|
|
|
checkColumns(columns, () => {
|
|
done()
|
|
})
|
|
})
|
|
|
|
it('2 | Add two viewboxes', (done) => {
|
|
const viewboxes = [
|
|
{
|
|
viewbox_table: 'mpe_audit',
|
|
columns: ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
|
|
},
|
|
{
|
|
viewbox_table: 'mpe_alerts',
|
|
columns: [
|
|
'TX_FROM',
|
|
'ALERT_EVENT',
|
|
'ALERT_LIB',
|
|
'ALERT_DS',
|
|
'ALERT_USER'
|
|
]
|
|
}
|
|
]
|
|
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
cy.get('.viewbox-open').click()
|
|
openTableFromViewboxTree(
|
|
libraryToOpenIncludes,
|
|
viewboxes.map((viewbox) => viewbox.viewbox_table)
|
|
)
|
|
viewboxes.forEach((viewbox) => {
|
|
cy.contains('.open-viewbox', viewbox.viewbox_table, {
|
|
matchCase: false,
|
|
timeout: longerCommandTimeout
|
|
})
|
|
})
|
|
|
|
let result = 0
|
|
|
|
viewboxes.forEach((viewbox) => {
|
|
checkColumns(viewbox.columns, () => {
|
|
result++
|
|
|
|
if (result === viewboxes.length) done()
|
|
})
|
|
})
|
|
})
|
|
|
|
it('3 | Add viewbox, add columns', (done) => {
|
|
const viewbox_table = 'mpe_audit'
|
|
const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
|
|
const additionalColumns = ['IS_PK']
|
|
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
|
|
cy.get('.viewbox-open').click()
|
|
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
|
|
|
|
cy.contains('.open-viewbox', viewbox_table, {
|
|
matchCase: false,
|
|
timeout: longerCommandTimeout
|
|
})
|
|
|
|
openViewboxConfig(viewbox_table)
|
|
|
|
removeAllColumns()
|
|
|
|
addColumns(additionalColumns)
|
|
|
|
checkColumns([...columns, ...additionalColumns], () => {
|
|
done()
|
|
})
|
|
})
|
|
|
|
it('4 | Add viewbox, add columns and reorder', (done) => {
|
|
const viewbox_table = 'mpe_audit'
|
|
const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
|
|
const additionalColumns = ['IS_PK', 'MOVE_TYPE']
|
|
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
|
|
cy.get('.viewbox-open').click()
|
|
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
|
|
|
|
cy.contains('.open-viewbox', viewbox_table, {
|
|
matchCase: false,
|
|
timeout: longerCommandTimeout
|
|
})
|
|
|
|
openViewboxConfig(viewbox_table)
|
|
|
|
removeAllColumns()
|
|
|
|
addColumns(additionalColumns, () => {
|
|
cy.wait(1000)
|
|
//reorder
|
|
cy.get('.col-box.column-MOVE_TYPE')
|
|
.realMouseDown({ button: 'left', position: 'center' })
|
|
.realMouseMove(0, 10, { position: 'center' })
|
|
cy.wait(200) // In our case, we wait 200ms cause we have animations which we are sure that take this amount of time
|
|
cy.get('.col-box.column-IS_PK')
|
|
.realMouseMove(0, 0, { position: 'center' })
|
|
.realMouseUp()
|
|
//reorder end
|
|
|
|
cy.wait(500)
|
|
|
|
checkColumns([...columns, ...additionalColumns.reverse()], () => {
|
|
done()
|
|
})
|
|
})
|
|
})
|
|
|
|
it('5 | Add viewbox, add columns, reorder, remove column, add again', (done) => {
|
|
const viewbox_table = 'mpe_audit'
|
|
const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
|
|
const additionalColumns = ['IS_PK', 'MOVE_TYPE']
|
|
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
|
|
cy.get('.viewbox-open').click()
|
|
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
|
|
|
|
cy.contains('.open-viewbox', viewbox_table, {
|
|
matchCase: false,
|
|
timeout: longerCommandTimeout
|
|
})
|
|
|
|
openViewboxConfig(viewbox_table)
|
|
|
|
removeAllColumns()
|
|
|
|
addColumns(additionalColumns, () => {
|
|
cy.wait(1000)
|
|
//reorder
|
|
cy.get('.col-box.column-MOVE_TYPE')
|
|
.realMouseDown({ button: 'left', position: 'center' })
|
|
.realMouseMove(0, 10, { position: 'center' })
|
|
cy.wait(200) // In our case, we wait 200ms cause we have animations which we are sure that take this amount of time
|
|
cy.get('.col-box.column-IS_PK')
|
|
.realMouseMove(0, 0, { position: 'center' })
|
|
.realMouseUp()
|
|
//reorder end
|
|
|
|
cy.wait(500)
|
|
|
|
checkColumns([...columns, ...additionalColumns.reverse()], () => {
|
|
const colToRemove = 'MOVE_TYPE'
|
|
|
|
removeColumn(colToRemove)
|
|
checkColumns(
|
|
[
|
|
...columns,
|
|
...additionalColumns.filter((col) => col !== colToRemove)
|
|
],
|
|
() => {
|
|
addColumns([colToRemove], () => {
|
|
checkColumns([...columns, ...additionalColumns.reverse()], () => {
|
|
done()
|
|
})
|
|
})
|
|
}
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
it('6 | Add viewboxes, reload and check url restored configuration', (done) => {
|
|
const viewboxes = [
|
|
{
|
|
viewbox_table: 'mpe_audit',
|
|
columns: ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM'],
|
|
additionalColumns: ['IS_PK', 'MOVE_TYPE']
|
|
},
|
|
{
|
|
viewbox_table: 'mpe_alerts',
|
|
columns: [
|
|
'TX_FROM',
|
|
'ALERT_EVENT',
|
|
'ALERT_LIB',
|
|
'ALERT_DS',
|
|
'ALERT_USER'
|
|
],
|
|
additionalColumns: ['TX_TO']
|
|
}
|
|
]
|
|
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
|
|
cy.get('.viewbox-open').click()
|
|
openTableFromViewboxTree(libraryToOpenIncludes, [
|
|
viewboxes[0].viewbox_table,
|
|
viewboxes[1].viewbox_table
|
|
])
|
|
|
|
openViewboxConfig(viewboxes[0].viewbox_table)
|
|
|
|
cy.wait(500)
|
|
|
|
removeAllColumns()
|
|
|
|
addColumns(viewboxes[0].additionalColumns, () => {
|
|
cy.wait(1000)
|
|
|
|
if (viewboxes[0].viewbox_table === 'mpe_audit') {
|
|
cy.get('.col-box.column-MOVE_TYPE')
|
|
.realMouseDown({ button: 'left', position: 'center' })
|
|
.realMouseMove(0, 10, { position: 'center' })
|
|
cy.wait(200) // In our case, we wait 200ms cause we have animations which we are sure that take this amount of time
|
|
cy.get('.col-box.column-IS_PK')
|
|
.realMouseMove(0, 0, { position: 'center' })
|
|
.realMouseUp()
|
|
}
|
|
|
|
cy.wait(1000)
|
|
|
|
openViewboxConfig(viewboxes[1].viewbox_table)
|
|
|
|
addColumns(viewboxes[1].additionalColumns, () => {
|
|
cy.wait(1000).reload()
|
|
|
|
let result = 0
|
|
|
|
checkColumns(
|
|
[
|
|
...viewboxes[0].columns,
|
|
...cloneDeep(viewboxes[0].additionalColumns.reverse())
|
|
],
|
|
() => {
|
|
result++
|
|
|
|
if (result === 2) done()
|
|
}
|
|
)
|
|
checkColumns(
|
|
[...viewboxes[1].columns, ...viewboxes[1].additionalColumns],
|
|
() => {
|
|
result++
|
|
|
|
if (result === 2) done()
|
|
}
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
// The base VIEW table and a viewbox on a different table must never
|
|
// share a filter dialog's cached state - each keeps its own entry in
|
|
// the globals filter cache (getFilterObjPath), keyed by viewboxId where
|
|
// one is present. Opened from the VIEW page specifically
|
|
// (rootParam === 'view') - the one branch where this previously broke,
|
|
// since a 'view'-page QueryComponent was always treated as the base
|
|
// table regardless of viewboxId.
|
|
it('7 | Base table filter and viewbox filter stay isolated from each other', () => {
|
|
const viewbox_table = 'mpe_audit'
|
|
|
|
visitPage('view/data')
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
|
|
// Filter the base table.
|
|
openViewFilter()
|
|
setFilterWithValue('SOME_CHAR', 'this is dummy data', 'value', () => {
|
|
cy.get('app-query', { withinSubject: null }).should('not.exist')
|
|
|
|
// Open a viewbox on a *different* table and check its filter dialog
|
|
// does not show the base table's filter.
|
|
openViewViewboxes()
|
|
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
|
|
closeViewboxModal()
|
|
|
|
// cy.contains retries until the viewbox's title actually shows the
|
|
// expected table name (it can render before its data/title loads),
|
|
// then .parents() walks back up to its container - avoids a
|
|
// one-shot .then() racing the title's own async update, which is
|
|
// what made this flaky in the first place.
|
|
cy.contains('.viewboxes-container .viewbox .table-title', viewbox_table, {
|
|
matchCase: false,
|
|
timeout: longerCommandTimeout
|
|
})
|
|
.parents('.viewbox')
|
|
.first()
|
|
.within(() => {
|
|
cy.get('clr-icon[shape="filter"]').click()
|
|
|
|
cy.get('.filter-modal code.language-sql', {
|
|
withinSubject: null
|
|
}).should(($code) => {
|
|
expect($code.text()).to.not.contain('SOME_CHAR')
|
|
})
|
|
|
|
// Reverse direction: filter the viewbox, then confirm
|
|
// re-opening the base table's own filter dialog still shows
|
|
// the base table's filter, not the viewbox's.
|
|
setFilterWithValue('LIBREF', 'sasdemo', 'value', () => {
|
|
cy.get('app-query', { withinSubject: null }).should('not.exist')
|
|
|
|
openViewFilter()
|
|
|
|
cy.get('.filter-modal code.language-sql', {
|
|
withinSubject: null
|
|
}).should(($code) => {
|
|
expect($code.text()).to.contain('SOME_CHAR')
|
|
expect($code.text()).to.not.contain('LIBREF')
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
const removeAllColumns = () => {
|
|
cy.get('.configuration-wrapper clr-icon[shape="trash"]').then(
|
|
(removeNodes) => {
|
|
for (let removeNode of removeNodes) {
|
|
removeNode.click()
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
// Asserts that at least one open viewbox currently shows exactly this
|
|
// column list, in order. Uses a single .should() callback (synchronous
|
|
// jQuery reads only, no nested cy.get()/.then()) so Cypress retries the
|
|
// whole check against a fresh DOM snapshot until it passes or times out -
|
|
// unlike .then(), which only ever sees one snapshot and, combined with an
|
|
// early return on a mismatch, would silently never invoke the caller's
|
|
// callback while Handsontable/the viewbox's data are still loading or
|
|
// re-rendering asynchronously.
|
|
const checkColumns = (columns: string[], callback: () => void) => {
|
|
cy.get('.viewboxes-container .viewbox', { withinSubject: null }).should(
|
|
($viewboxNodes) => {
|
|
const matches = $viewboxNodes.toArray().some((viewboxNode) => {
|
|
// rowHeaders: true (viewboxes.component.ts) adds a leading, unlabeled
|
|
// corner <th> before the actual data columns - skip it, or every
|
|
// comparison is off by one against the row-header cell instead of
|
|
// the first real column.
|
|
const headerCells = Cypress.$(viewboxNode)
|
|
.find('.ht_master.handsontable thead tr th')
|
|
.toArray()
|
|
.slice(1)
|
|
|
|
return columns.every((col, i) =>
|
|
(headerCells[i]?.innerHTML || '')
|
|
.toLowerCase()
|
|
.includes(col.toLowerCase())
|
|
)
|
|
})
|
|
|
|
expect(matches, `a viewbox showing columns: ${columns.join(', ')}`).to.be
|
|
.true
|
|
}
|
|
)
|
|
|
|
cy.then(() => callback())
|
|
}
|
|
|
|
const closeViewboxModal = () => {
|
|
cy.get('app-viewboxes .close', { withinSubject: null }).click()
|
|
}
|
|
|
|
const removeColumn = (column: string) => {
|
|
cy.get(`.col-box.column-${column} clr-icon`, { withinSubject: null }).click()
|
|
}
|
|
|
|
const addColumns = (columns: string[], callback?: () => void) => {
|
|
for (const column of columns) {
|
|
cy.get('.cols-search input', { withinSubject: null }).type(column)
|
|
cy.get('.cols-search .autocomplete-wrapper', { withinSubject: null })
|
|
.first()
|
|
.trigger('keydown', { key: 'ArrowDown' })
|
|
cy.get('.cols-search .autocomplete-wrapper', { withinSubject: null })
|
|
.first()
|
|
.trigger('keydown', { key: 'Enter' })
|
|
|
|
// Wait for the actual column box to render before moving on to the
|
|
// next column (or firing the callback) - selecting it doesn't add it
|
|
// to the DOM synchronously, and cy.get()'s own retry covers that gap
|
|
// instead of relying on a fixed cy.wait().
|
|
cy.get(`.col-box.column-${column}`, { timeout: longerCommandTimeout })
|
|
}
|
|
|
|
if (callback) cy.then(() => callback())
|
|
}
|
|
|
|
// The VIEW page's "options" dropdown (viewer.component.html) - distinct
|
|
// from the editor page's dedicated .viewbox-open button used by the
|
|
// tests above, since test 7 runs on the VIEW page specifically.
|
|
const openViewOptionsDropdown = () => {
|
|
cy.get('.filterSide', { withinSubject: null }).click()
|
|
}
|
|
|
|
const openViewFilter = () => {
|
|
openViewOptionsDropdown()
|
|
cy.get('[clrDropdownItem]', { withinSubject: null })
|
|
.contains('Filter')
|
|
.click()
|
|
}
|
|
|
|
const openViewViewboxes = () => {
|
|
openViewOptionsDropdown()
|
|
cy.get('[clrDropdownItem]', { withinSubject: null })
|
|
.contains('Viewboxes')
|
|
.click()
|
|
}
|
|
|
|
// cy.contains retries until a matching node appears/updates - the open
|
|
// viewbox's own list entry can render before its table-name text has
|
|
// actually loaded, so a one-shot .then() lookup here would race it.
|
|
const openViewboxConfig = (viewbox_tablename: string) => {
|
|
cy.contains('.open-viewbox', viewbox_tablename, {
|
|
matchCase: false,
|
|
timeout: longerCommandTimeout
|
|
}).click()
|
|
}
|
|
|
|
const openTableFromTree = (libNameIncludes: string, tablename: string) => {
|
|
cy.get('.app-loading', { timeout: longerCommandTimeout }).should('not.exist')
|
|
|
|
// Small settle wait: right after a fresh visit (an extra navigation on
|
|
// top of beforeEach's own, e.g. test 7's visitPage('view/data')), the
|
|
// tree component can still be re-rendering, causing a node to be found
|
|
// then swapped out mid-click - same fix already proven in
|
|
// viewer-labels.cy.ts.
|
|
cy.wait(300)
|
|
|
|
const libraryNodeSelector = '.nav-tree clr-tree > clr-tree-node'
|
|
const libraryMatcher = new RegExp(libNameIncludes, 'i')
|
|
|
|
cy.contains(libraryNodeSelector, libraryMatcher, {
|
|
timeout: longerCommandTimeout
|
|
})
|
|
.find('.clr-tree-node-content-container p')
|
|
.click()
|
|
|
|
// Re-query the library node fresh rather than reusing the reference
|
|
// from before the click above - expanding a library can replace its
|
|
// whole DOM subtree once its child tables load (that's what made the
|
|
// earlier cached-reference version of this helper flaky: Cypress
|
|
// detected the click's own side effect detaching the element it was
|
|
// still verifying actionability against).
|
|
cy.contains(libraryNodeSelector, libraryMatcher, {
|
|
timeout: longerCommandTimeout
|
|
}).within(() => {
|
|
cy.contains('.clr-treenode-link', tablename, {
|
|
matchCase: false,
|
|
timeout: longerCommandTimeout
|
|
}).click()
|
|
})
|
|
}
|
|
|
|
const setFilterWithValue = (
|
|
variableValue: string,
|
|
valueString: string,
|
|
valueField: 'value' | 'time' | 'date' | 'datetime' | 'in' | 'between',
|
|
callback?: any
|
|
) => {
|
|
cy.wait(600)
|
|
|
|
cy.focused().type(variableValue)
|
|
cy.wait(100)
|
|
// cy.focused().trigger('input')
|
|
cy.get('.variable-col .autocomplete-wrapper', { withinSubject: null })
|
|
.first()
|
|
.trigger('keydown', { key: 'ArrowDown' })
|
|
cy.get('.variable-col .autocomplete-wrapper', {
|
|
withinSubject: null
|
|
}).trigger('keydown', { key: 'Enter' })
|
|
cy.focused().tab()
|
|
cy.wait(100)
|
|
|
|
if (valueField === 'in') {
|
|
cy.focused().select(valueField.toUpperCase()).trigger('change')
|
|
} else if (valueField === 'between') {
|
|
cy.focused().select(valueField.toUpperCase()).trigger('change')
|
|
} else {
|
|
cy.focused().tab()
|
|
cy.wait(100)
|
|
}
|
|
|
|
switch (valueField) {
|
|
case 'value': {
|
|
cy.focused().type(valueString)
|
|
|
|
break
|
|
}
|
|
case 'time': {
|
|
cy.focused().type(valueString)
|
|
|
|
break
|
|
}
|
|
case 'date': {
|
|
cy.focused().type(valueString)
|
|
cy.focused().tab()
|
|
|
|
break
|
|
}
|
|
case 'datetime': {
|
|
const date = valueString.split(' ')[0]
|
|
const time = valueString.split(' ')[1]
|
|
|
|
cy.focused().type(date)
|
|
cy.focused().tab()
|
|
cy.focused().tab()
|
|
cy.focused().type(time)
|
|
|
|
break
|
|
}
|
|
case 'in': {
|
|
cy.get('.checkbox-vals').then(() => {
|
|
cy.focused().tab()
|
|
cy.focused().click()
|
|
cy.get('.no-values')
|
|
.should('not.exist')
|
|
.then(() => {
|
|
cy.get('clr-checkbox-wrapper input').then((inputs: any) => {
|
|
inputs[0].click()
|
|
cy.get('.in-values-modal .modal-footer button').click()
|
|
|
|
cy.get('.modal-footer .btn-success-outline').click()
|
|
|
|
if (callback) callback()
|
|
})
|
|
})
|
|
})
|
|
|
|
break
|
|
}
|
|
case 'between': {
|
|
cy.focused().tab()
|
|
|
|
const start = valueString.split('-')[0]
|
|
const end = valueString.split('-')[1]
|
|
|
|
cy.focused().type(start)
|
|
cy.focused().tab()
|
|
cy.focused().type(end)
|
|
}
|
|
default: {
|
|
break
|
|
}
|
|
}
|
|
|
|
cy.wait(100)
|
|
cy.focused().tab()
|
|
cy.wait(100)
|
|
cy.focused().tab()
|
|
cy.wait(100)
|
|
cy.focused().tab()
|
|
cy.wait(100)
|
|
cy.focused().tab()
|
|
cy.wait(100)
|
|
cy.focused().click()
|
|
|
|
if (callback) callback()
|
|
}
|
|
|
|
const openTableFromViewboxTree = (
|
|
libNameIncludes: string,
|
|
tablenames: string[]
|
|
) => {
|
|
cy.get('.add-new 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('p')
|
|
.click()
|
|
.then(() => {
|
|
cy.get('.clr-treenode-link').then(async (innerNodes: any) => {
|
|
for (let innerNode of innerNodes) {
|
|
for (let tablename of tablenames) {
|
|
await pause(300)
|
|
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
|
innerNode.click()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
const pause = (ms: number) => {
|
|
return new Promise((resolve, reject) => {
|
|
setTimeout(() => {
|
|
resolve(null)
|
|
}, ms)
|
|
})
|
|
}
|
|
|
|
const visitPage = (url: string) => {
|
|
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
|
}
|