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') }) }) }) }) }) it('8 | Dragging a viewbox by its corner handle resizes it', () => { const viewbox_table = 'mpe_audit' openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') cy.get('.viewbox-open').click() openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table]) cy.contains('.open-viewbox', viewbox_table, { matchCase: false, timeout: longerCommandTimeout }) // The Viewboxes picker modal is still open at this point, its backdrop // sitting above .viewboxes-container - a real click (unlike cy.find(), // which doesn't care what's visually on top) would land on the modal // instead of the handle underneath it, silently no-opping the drag. closeViewboxModal() // Re-query by the title text rather than caching a reference, since // the viewbox can still be re-rendering its content right after // opening. cy.contains('.viewboxes-container .viewbox .table-title', viewbox_table, { matchCase: false, timeout: longerCommandTimeout }) .parents('.viewbox') .first() .as('viewboxEl') // tableOnClick() calls snapToGrid() asynchronously right after its data // load resolves, which overwrites width/height/x/y for every open // viewbox via the [style.width.px]/[style.height.px] bindings - totally // independent of any drag. The table title above renders immediately // (before that data load finishes), so capturing initialRect right // after it appears races snapToGrid: the box can still silently resize // itself out from under the test between capturing initialRect and the // drag. Waiting for the table's own header cells (populated only once // that same data load - and so snapToGrid - has completed) closes that // race, same signal checkColumns() uses elsewhere in this file. cy.get('@viewboxEl').find('.ht_master.handsontable thead tr th', { timeout: longerCommandTimeout }) cy.get('@viewboxEl').then(($box) => { const initialRect = $box[0].getBoundingClientRect() // Chained off one query, not re-queried between each step, so // cypress-real-events keeps operating on the same subject throughout. // // Dragging INWARD (negative offsets, shrinking) rather than outward: // a freshly opened viewbox starts flush against the right edge of the // viewport (x: window.innerWidth - defaultConfig.width, so // box.right === innerWidth exactly). realMouseMove's target // coordinate is computed as an absolute point on the page, so dragging // the corner outward/rightward to grow the box would target a point // past the viewport's right edge, outside the Cypress AUT iframe // entirely - Chrome never delivers a move event for a point outside // the iframe. Shrinking stays well within the box's own footprint // (500x300 default, 200x200 min per .viewbox's CSS), so the target // coordinate is always safely on-screen - same resize() code path, // just exercised in the direction that's actually simulatable. cy.get('@viewboxEl') .find('.dragHandle.corner') .realMouseDown({ button: 'left', position: 'center' }) .realMouseMove(-100, -80, { position: 'topLeft' }) .realMouseUp() // A generous threshold, not an exact pixel match - real rendering has // its own small offsets (see setHandleTransform's "fine tune" +5). // The point is confirming the box actually resizes at all in response // to the drag. cy.get('@viewboxEl').should(($box) => { const rect = $box[0].getBoundingClientRect() expect(rect.width, 'width after resize').to.be.lessThan( initialRect.width - 30 ) expect(rect.height, 'height after resize').to.be.lessThan( initialRect.height - 30 ) }) }) }) it('9 | Dragging a viewbox by its right-edge handle resizes width only', () => { const viewbox_table = 'mpe_audit' openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') cy.get('.viewbox-open').click() openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table]) cy.contains('.open-viewbox', viewbox_table, { matchCase: false, timeout: longerCommandTimeout }) closeViewboxModal() cy.contains('.viewboxes-container .viewbox .table-title', viewbox_table, { matchCase: false, timeout: longerCommandTimeout }) .parents('.viewbox') .first() .as('viewboxEl') // See test 8's comment: snapToGrid() resizes every open viewbox // asynchronously right after its data load resolves, racing initialRect // capture if that happens right after the title (which renders before // the load finishes) appears. Wait for the table's own header cells // first, since those only populate once that same load has completed. cy.get('@viewboxEl').find('.ht_master.handsontable thead tr th', { timeout: longerCommandTimeout }) cy.get('@viewboxEl').then(($box) => { const initialRect = $box[0].getBoundingClientRect() // Chained off one query - see test 8's comment on why re-querying // between down/move/up silently drops the move. Dragging INWARD // (negative offset, shrinking) rather than outward - see test 8's // comment: a freshly opened viewbox starts flush against the // viewport's right edge, so growing rightward would target a point // past the AUT iframe's edge, which Chrome never delivers a move // event for. cy.get('@viewboxEl') .find('.dragHandle.right') .realMouseDown({ button: 'left', position: 'center' }) .realMouseMove(-100, 0, { position: 'topLeft' }) .realMouseUp() cy.get('@viewboxEl').should(($box) => { const rect = $box[0].getBoundingClientRect() expect(rect.width, 'width after resize').to.be.lessThan( initialRect.width - 30 ) // This handle must only affect width - height (and the box's own // left edge) stay put. expect(rect.height, 'height unchanged').to.be.closeTo( initialRect.height, 5 ) expect(rect.left, 'left edge unchanged').to.be.closeTo( initialRect.left, 5 ) }) }) }) it('10 | Dragging a viewbox by its bottom-left corner handle resizes it and moves its left edge', () => { const viewbox_table = 'mpe_audit' openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') cy.get('.viewbox-open').click() openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table]) cy.contains('.open-viewbox', viewbox_table, { matchCase: false, timeout: longerCommandTimeout }) closeViewboxModal() cy.contains('.viewboxes-container .viewbox .table-title', viewbox_table, { matchCase: false, timeout: longerCommandTimeout }) .parents('.viewbox') .first() .as('viewboxEl') // See test 8's comment: wait for snapToGrid()'s async resize to settle // before capturing initialRect, or the box can silently change size out // from under the test. cy.get('@viewboxEl').find('.ht_master.handsontable thead tr th', { timeout: longerCommandTimeout }) cy.get('@viewboxEl').then(($box) => { const initialRect = $box[0].getBoundingClientRect() // Chained off one query - see test 8's comment on why re-querying // between down/move/up silently drops the move. Dragging this // handle right+down (only positive offsets are meaningful for // realMouseMove's position-anchored coordinates) shrinks width // while moving the box's own left edge right by the same amount, // and grows height - unlike every other handle here, this one is // expected to move the box (viewbox.x) as well as resize it, which // is the more error-prone half of this handler to cover. cy.get('@viewboxEl') .find('.dragHandle.corner-left') .realMouseDown({ button: 'left', position: 'center' }) .realMouseMove(60, 80, { position: 'topLeft' }) .realMouseUp() cy.get('@viewboxEl').should(($box) => { const rect = $box[0].getBoundingClientRect() expect(rect.width, 'width after resize').to.be.lessThan( initialRect.width - 30 ) expect(rect.left, 'left edge moved right').to.be.greaterThan( initialRect.left + 30 ) expect(rect.height, 'height after resize').to.be.greaterThan( initialRect.height + 30 ) }) }) }) it('11 | Dragging a viewbox by its left-edge handle resizes width and moves its left edge, leaving height alone', () => { const viewbox_table = 'mpe_audit' openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') cy.get('.viewbox-open').click() openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table]) cy.contains('.open-viewbox', viewbox_table, { matchCase: false, timeout: longerCommandTimeout }) closeViewboxModal() cy.contains('.viewboxes-container .viewbox .table-title', viewbox_table, { matchCase: false, timeout: longerCommandTimeout }) .parents('.viewbox') .first() .as('viewboxEl') // See test 8's comment: wait for snapToGrid()'s async resize to settle // before capturing initialRect, or the box can silently change size out // from under the test. cy.get('@viewboxEl').find('.ht_master.handsontable thead tr th', { timeout: longerCommandTimeout }) cy.get('@viewboxEl').then(($box) => { const initialRect = $box[0].getBoundingClientRect() // Chained off one query - see test 8's comment on why re-querying // between down/move/up silently drops the move. Dragging this // handle right (only positive offsets are meaningful for // realMouseMove's position-anchored coordinates) shrinks width // while moving the box's own left edge right by the same amount - // same viewbox.x-moving behavior as the bottom-left corner, minus // the height change. cy.get('@viewboxEl') .find('.dragHandle.left') .realMouseDown({ button: 'left', position: 'center' }) .realMouseMove(60, 0, { position: 'topLeft' }) .realMouseUp() cy.get('@viewboxEl').should(($box) => { const rect = $box[0].getBoundingClientRect() expect(rect.width, 'width after resize').to.be.lessThan( initialRect.width - 30 ) expect(rect.left, 'left edge moved right').to.be.greaterThan( initialRect.left + 30 ) expect(rect.height, 'height unchanged').to.be.closeTo( initialRect.height, 5 ) }) }) }) it('12 | Dragging a viewbox by its bottom-edge handle resizes height only', () => { const viewbox_table = 'mpe_audit' openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') cy.get('.viewbox-open').click() openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table]) cy.contains('.open-viewbox', viewbox_table, { matchCase: false, timeout: longerCommandTimeout }) closeViewboxModal() cy.contains('.viewboxes-container .viewbox .table-title', viewbox_table, { matchCase: false, timeout: longerCommandTimeout }) .parents('.viewbox') .first() .as('viewboxEl') // See test 8's comment: wait for snapToGrid()'s async resize to settle // before capturing initialRect, or the box can silently change size out // from under the test. cy.get('@viewboxEl').find('.ht_master.handsontable thead tr th', { timeout: longerCommandTimeout }) cy.get('@viewboxEl').then(($box) => { const initialRect = $box[0].getBoundingClientRect() // Chained off one query - see test 8's comment on why re-querying // between down/move/up silently drops the move. cy.get('@viewboxEl') .find('.dragHandle.bottom') .realMouseDown({ button: 'left', position: 'center' }) .realMouseMove(0, 100, { position: 'topLeft' }) .realMouseUp() cy.get('@viewboxEl').should(($box) => { const rect = $box[0].getBoundingClientRect() expect(rect.height, 'height after resize').to.be.greaterThan( initialRect.height + 30 ) // This handle must only affect height - width (and the box's own // left edge) stay put. expect(rect.width, 'width unchanged').to.be.closeTo( initialRect.width, 5 ) expect(rect.left, 'left edge unchanged').to.be.closeTo( initialRect.left, 5 ) }) }) }) }) 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