// Marks this file as an ES module (rather than a global script) so its // top-level consts don't collide, under the TS type-checker, with the same // names declared in other spec files. // // RUN ORDER MATTERS, and build.yaml reflects it: this spec must run after // csv-limited.cy.ts (which asserts the free tier and so needs the first slot, // before any spec applies a licence key) and before every spec that submits a // changeset. Test 2 reads the approval queue, which is paged oldest first, and // a changeset submitted by an earlier spec pushes this one off the first page. export {} const hostUrl = Cypress.env('hosturl') const appLocation = Cypress.env('appLocation') const longerCommandTimeout = Cypress.env('longerCommandTimeout') context('hook program tests: ', function () { this.beforeAll(() => { cy.loginAndUpdateValidKey(true) }) this.beforeEach(() => { cy.visit(hostUrl + appLocation) visitPage('home') }) // TESTDATA.DEMO_MIRROR is an empty mirror of TESTDATA.DEMO_ORDERS, registered // with a PRE_EDIT_HOOK and a POST_EDIT_HOOK (services/hooks/demo_mirror_*.js). // The mock services run those hooks the way the SAS backend %includes the // real hook programs - see dcMockUtils.mockHookSource. it('1 | PRE_EDIT_HOOK: an empty mirror displays the live rows of its target table', () => { openTableFromTree('testdata', 'demo_mirror') // The mirror holds no rows of its own: everything on screen comes from the // pre-edit hook, which loads TESTDATA.DEMO_ORDERS. cy.get('#hotTable .ht_master tbody tr', { timeout: longerCommandTimeout }).should('have.length', 4) cy.get('#hotTable .ht_master tbody tr') .first() .should('contain.text', 'Acme Corp') cy.get('#hotTable .ht_master tbody tr') .last() .should('contain.text', 'Delta Systems') // The grid is validated against the MIRROR's own rule set (AMOUNT <= 2000), // not the real table's (AMOUNT <= 100000) - that is the point of routing // edits through a mirror. clickOnEdit(() => { editCell(0, 'AMOUNT', '5000') }) cy.get('#hotTable .ht_master tbody tr') .first() .find('td.htInvalid') .should('exist') }) it('2 | POST_EDIT_HOOK: a change submitted against the mirror is raised against the target', () => { openTableFromTree('testdata', 'demo_mirror') clickOnEdit(() => { editCell(0, 'AMOUNT', '750') }) submitTable(() => { cy.get('textarea.submit-reason', { timeout: longerCommandTimeout }).type( 'hook routing test' ) submitTableMessage() }) // A successful submit sends the app to the staged-data page; wait for that // before navigating away, or the app's own redirect wins the race. cy.url({ timeout: longerCommandTimeout }).should('include', '/stage/') // The post-edit hook re-points the changeset at TESTDATA.DEMO_ORDERS, so the // approval queue names the real table rather than the mirror. Search the // whole grid rather than one row: the queue also holds changesets from // earlier specs in the same run, so position is not a stable anchor. visitPage('review/approve') cy.get('clr-datagrid clr-dg-row', { timeout: longerCommandTimeout }).should( ($rows) => { const texts = Array.from($rows).map((row: any) => row.innerText.trim().replace(/\s+/g, ' ') ) expect( texts.some((text) => text.includes('TESTDATA.DEMO_ORDERS')), `no approval row names TESTDATA.DEMO_ORDERS - rows: ${texts .slice(0, 5) .join(' | ') .slice(0, 400)}` ).to.be.true } ) }) }) // ─── helpers (declared at the bottom, per the other specs) ─────────────────── const visitPage = (url: string) => { cy.visit(`${hostUrl}${appLocation}/#/${url}`) } 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.wait(300) cy.get( '.clr-tree-node-content-container .clr-treenode-content p' ).click() cy.get('.clr-treenode-link').then((innerNodes: any) => { for (let innerNode of innerNodes) { if (innerNode.innerText.toLowerCase().includes(tablename)) { innerNode.click() break } } }) }) }) }) cy.get('#hotTable .ht_clone_top .htCore thead button.changeType', { timeout: longerCommandTimeout }).should('exist') } const clickOnEdit = (callback?: any) => { cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout }) .click() .then(() => { if (callback) callback() }) } // Edits a single cell, locating the column by its header label. Handsontable // renders the frozen header in a separate clone pane (.ht_clone_top) - the // header row inside .ht_master is kept visibility:hidden - so the labels are // read from the clone. const editCell = (rowIndex: number, colName: string, value: string) => { cy.get('#hotTable .ht_clone_top .htCore thead th', { timeout: longerCommandTimeout }).then((headers: any) => { let colIndex = -1 for (let i = 0; i < headers.length; i++) { if ( String(headers[i].innerText || '') .trim() .toUpperCase() === colName.toUpperCase() ) { colIndex = i break } } expect(colIndex, `column ${colName} present`).to.be.greaterThan(-1) cy.get('#hotTable .ht_master tbody tr') .eq(rowIndex) .then((row: any) => { cy.get(row[0].childNodes[colIndex]) .dblclick({ force: true }) .then(() => { cy.focused() .clear() .type(value + '{enter}') }) }) }) } const submitTable = (callback?: any) => { cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout }) .click() .then(() => { if (callback) callback() }) } const submitTableMessage = (callback?: any) => { cy.get('.modal-footer .btn.btn-sm.btn-success-outline', { timeout: longerCommandTimeout }) .click() .then(() => { if (callback) callback() }) }