Files
dc/client/cypress/e2e/excel-multi-load.cy.ts
T
dc db99b5868f test(cypress): clear the session instead of visiting the logout route
The specs opened with `cy.visit(`${hostUrl}/SASLogon/logout`)`. Against a dev
server that only navigated the single-page app: every path answered index.html,
so nothing was logged out. With the app served by the server it talks to, the
route answers 403 - `desktopRestrict` in @sasjs/server refuses it by design, and
desktop is that server's default mode - so the visit failed the before hook and
every spec aborted on its first line.

`cy.clearSession()` clears cookies and localStorage, which is what the visit was
standing in for, and needs nothing from the server. The MODE=desktop line and its
comment go with it: the server already defaults to desktop, so the line changed
nothing and the comment described a mode the job never ran in.

Verified locally against a 4.14.0 estate with the build streamed to
/AppStream/clickme and the mock services seeded: viewer-labels 5/5 (the spec that
failed in CI), filtering 10/10.

Also in this push, three review findings on the harness itself:

- resolve-browser.sh searches ${DC_PATH:-$PATH} through its own loop, so the
  test pins the whole decision to fixtures instead of inheriting a chromium from
  the host; case 2 was passing here and failing on any machine with one.
- the Cypress step requires the exported path (`${CYPRESS_BROWSER:?...}`) rather
  than falling back to a browser name, so a missing export fails with a message
  naming the step that should have set it.
- the step asserts the app is served at /AppStream/clickme before the suite
  starts, because a deploy that streamed no frontend would otherwise surface as
  twenty-six navigation failures.
2026-11-08 00:32:33 +00:00

368 lines
13 KiB
TypeScript

import { Callbacks } from 'cypress/types/jquery/index'
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_multi_load/'
const library = 'DC996664'
const mpeXTestTable = 'MPE_X_TEST'
const mpeTablesTable = 'MPE_TABLES'
context('excel multi load tests: ', function () {
this.beforeAll(() => {
cy.clearSession()
cy.loginAndUpdateValidKey(true)
})
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
visitPage('home/multi-load')
colorLog(
`TEST START ---> ${
Cypress.mocha.getRunner().suite.ctx.currentTest.title
}`,
'#3498DB'
)
})
it('1 | Uploads Excel file with multiple sheets, 3 sheets including data, 2 sheets matched with dataset', (done) => {
attachExcelFile('multi_load_test_2.xlsx', () => {
checkHotUserDatasetTable(
'hotTableUserDataset',
[
[library, mpeXTestTable],
[library, mpeTablesTable]
],
() => {
cy.get('#continue-btn')
.trigger('click')
.then(() => {
checkIfTreeHasTables(
[`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`],
undefined,
(includes: boolean) => {
if (includes) {
// MPE_TABLES is left out of the submission, and the pane
// says why - as a sheet that could not be read or as one
// with no rows, depending on what the sheet utility
// reported. Either way it is named.
cy.get('#file-extraction-failures, #file-empty-sheets', {
timeout: longerCommandTimeout
})
.should('be.visible')
.and('contain.text', mpeTablesTable)
// MPE_TABLES sheet does not have data so 1 error image must be shown
hasErrorTables(1, (valid: boolean) => {
if (valid) done()
})
}
}
)
})
}
)
})
})
it('2 | Uploads Excel file with multiple sheets, 2 sheets matched with dataset, 1 matched sheet does not have data', (done) => {
attachExcelFile('multi_load_test_1.xlsx', () => {
checkHotUserDatasetTable(
'hotTableUserDataset',
[
[library, mpeXTestTable],
[library, mpeTablesTable]
],
() => {
cy.get('#continue-btn')
.trigger('click')
.then(() => {
checkIfTreeHasTables(
[`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`],
`${library}.${mpeXTestTable}`,
(includes: boolean) => {
if (includes) {
cy.get('#hotTable')
.should('be.visible')
.then(() => {
checkHotUserDatasetTable(
'hotTable',
[
['No', '1', 'more dummy data'],
[
'No',
'1',
'It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told:'
],
[
'No',
'1',
'if you can fill the unforgiving minute'
]
],
() => {
submitTables()
hasSuccessSubmits(2, (valid: boolean) => {
if (valid) done()
})
}
)
})
}
}
)
})
}
)
})
})
it('3 | Uploads Excel file with multiple sheets, 1 sheets has 2 tables', (done) => {
attachExcelFile('multi_load_test_1.xlsx', () => {
checkHotUserDatasetTable(
'hotTableUserDataset',
[
[library, mpeXTestTable],
[library, mpeTablesTable]
],
() => {
cy.get('#continue-btn')
.trigger('click')
.then(() => {
checkIfTreeHasTables(
[`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`],
`${library}.${mpeXTestTable}`,
(includes: boolean) => {
if (includes) {
cy.get('#hotTable')
.should('be.visible')
.then(() => {
checkHotUserDatasetTable(
'hotTable',
[
['No', '1', 'more dummy data'],
[
'No',
'1',
'It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told:'
],
[
'No',
'1',
'if you can fill the unforgiving minute'
]
],
() => {
clickOnTreeNode('DC996664.MPE_TABLES', () => {
cy.wait(1000).then(() => {
cy.get('#hotTable')
.should('be.visible')
.then(() => {
checkHotUserDatasetTable(
'hotTable',
[
[
'No',
'DC914286',
'MPE_COLUMN_LEVEL_SECURITY'
],
['No', 'DC914286', 'MPE_XLMAP_INFO'],
['No', 'DC914286', 'MPE_XLMAP_RULES']
],
() => {
submitTables()
hasSuccessSubmits(
2,
(valid: boolean) => {
if (valid) done()
}
)
}
)
})
})
})
}
)
})
}
}
)
})
}
)
})
})
// The file's own status line: size and stage. The stages are momentary, so
// the dataset lookups are held open to make "Fetching the dataset details"
// assertable (and visible in the recording).
it('4 | Shows the file size and the loading stage while the file is processed', () => {
attachExcelFile('multi_load_test_1.xlsx', () => {
cy.contains('File size:')
.should('be.visible')
.invoke('text')
.should('match', /File size:\s*[\d.]+ MB/)
// The stage line lives in the file pane until the datasets are fetched,
// then in the no-table-selected pane while the sheets are extracted, so
// both ids are asked for - the element that carries it changes.
const STAGE = '#file-loading-stage, #file-extraction-stage'
// The sheet scan is quick; from then on the file waits for the user.
cy.get(STAGE, { timeout: longerCommandTimeout })
.should('be.visible')
.and('contain.text', 'File ready to load')
// The adapter posts to /SASjsApi/stp/execute/?_program=... - a glob on
// 'execute**' does not match the trailing slash, so match on the path.
cy.intercept({ url: /SASjsApi\/stp\/execute/ }, (req) => {
req.on('response', (res) => {
res.setDelay(1500)
})
})
cy.get('#continue-btn').trigger('click')
cy.get(STAGE, { timeout: longerCommandTimeout })
.should('be.visible')
.and('contain.text', 'Fetching the dataset details')
// Once every sheet has been extracted the stage gives way to the hint.
cy.get(STAGE, { timeout: longerCommandTimeout }).should('not.exist')
cy.contains('Please select a dataset on the left').should('be.visible')
})
})
this.afterEach(() => {
colorLog(`TEST END -------------`, '#3498DB')
})
})
const attachExcelFile = (excelFilename: string, callback?: any) => {
cy.get('#browse-file')
.should('exist')
.click()
.then(() => {
cy.get('input[type="file"]#file-upload')
.attachFile(`/${fixturePath}/${excelFilename}`)
.then(() => {
if (callback) callback()
})
})
}
const checkHotUserDatasetTable = (
hotId: string,
dataToContain: any[][],
callback?: () => void
) => {
cy.get(`#${hotId}`, { timeout: longerCommandTimeout })
.find('div.ht_master.handsontable')
.find('div.wtHolder')
.find('div.wtHider')
.find('div.wtSpreader')
.find('table.htCore')
.find('tbody')
.then((data) => {
cy.wait(2000).then(() => {
for (let rowI = 0; rowI < dataToContain.length; rowI++) {
for (let colI = 0; colI < dataToContain[rowI].length; colI++) {
expect(data[0].children[rowI].children[colI]).to.contain(
dataToContain[rowI][colI]
)
}
}
if (callback) callback()
})
})
}
const clickOnTreeNode = (clickOnNode: string, callback?: () => void) => {
cy.get('.nav-tree clr-tree > clr-tree-node').then((treeNodes: any) => {
for (let node of treeNodes) {
if (node.innerText.toUpperCase().trim().includes(clickOnNode)) {
cy.get(node).trigger('click')
if (callback) callback()
}
}
})
}
const checkIfTreeHasTables = (
tables: string[],
clickOnNode?: string,
callback?: (includes: boolean) => void
) => {
cy.get('.nav-tree clr-tree > clr-tree-node').then((treeNodes: any) => {
let datasets = tables
let nodesCorrect = true
let nodeToClick
for (let node of treeNodes) {
if (!datasets.includes(node.innerText.toUpperCase().trim())) {
nodesCorrect = false
}
if (clickOnNode) {
if (node.innerText.toUpperCase().trim().includes(clickOnNode)) {
nodeToClick = node
}
}
}
if (nodeToClick) {
cy.wait(1000)
cy.get(nodeToClick).trigger('click')
}
if (callback) callback(nodesCorrect)
})
}
const submitTables = () => {
cy.get('#submit-all').trigger('click')
cy.get('#submit-tables').trigger('click')
cy.wait(1000)
}
const hasSuccessSubmits = (
expectedNoOfSubmits: number,
callback: (valid: boolean) => void
) => {
cy.get('.nav-tree clr-tree > clr-tree-node cds-icon[status="success"]')
.should('be.visible')
.then(($nodes) => {
callback(expectedNoOfSubmits === $nodes.length)
})
}
const hasErrorTables = (
expectedNoOfErrors: number,
callback: (valid: boolean) => void
) => {
cy.get('.nav-tree clr-tree > clr-tree-node cds-icon[status="danger"]')
.should('be.visible')
.then(($nodes) => {
callback(expectedNoOfErrors === $nodes.length)
})
}
const visitPage = (url: string) => {
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
}
const colorLog = (msg: string, color: string) => {
console.log('%c' + msg, 'color:' + color + ';font-weight:bold;')
}