Files
dc/client/cypress/e2e/deploy-folder-picker.cy.ts
T
dc e8cdc37a05
Build / Build-and-ng-test (pull_request) Successful in 5m10s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m34s
Build / Build-and-test-development (pull_request) Successful in 32m58s
test(deploy): match the picker's requests with regexps, and sign in on a secured estate
The spec failed on CI: the browse button never appeared. Two causes, both
found by reproducing the CI estate locally (seeded drive):

- The intercept pattern '**/drive/folder**' does not match the request the
  app makes ('/SASjsApi/drive/folder?_folderPath=...') in this Cypress
  version, so the app saw the real drive, loaded the startup service and
  never routed to the deploy screen. Both intercepts are now regexps, which
  match deterministically. Verified on the seeded estate: the app routes to
  #/deploy and the agreement card renders.

- A secured estate shows a login form first. The boot helper now waits for
  one of the states it can act on (a visible login form, the agreement card,
  or the deploy screen), signs in when the form is visible, then waits for
  the deploy screen. The login form stays in the DOM hidden on other routes,
  so visibility - not presence - decides.
2026-11-05 15:10:51 +00:00

254 lines
7.6 KiB
TypeScript

// Marks this file as an ES module so its top-level consts don't collide with
// other spec files under the TS type-checker.
export {}
//
// The deploy screen's folder picker. The estate the suite runs against is
// seeded, which hides the deploy screen (makedata has removed itself) and
// refuses DIRLIST / WRITECHECK once the settings file exists - so this spec
// reports the admin folder as unconfigured to land on the deploy screen, and
// answers the two probes itself.
const hostUrl = Cypress.env('hosturl')
const username = Cypress.env('username')
const password = Cypress.env('password')
/** The folder rows the mocked DIRLIST replies with. */
const FOLDERS = [
{
FILEPATH: '/tmp/DataController/alpha',
FILENAME: 'alpha',
FILE_OR_FOLDER: 'folder',
LEVEL: 1,
MSG: ' '
},
{
FILEPATH: '/tmp/DataController/beta',
FILENAME: 'beta',
FILE_OR_FOLDER: 'folder',
LEVEL: 1,
MSG: ' '
},
{
FILEPATH: '/tmp/DataController/shut',
FILENAME: 'shut',
FILE_OR_FOLDER: 'locked',
LEVEL: 1,
MSG: 'Permission denied'
}
]
const dirResponse = () => ({
dirinfo: [
{
PARENT: '/tmp/DataController',
SEP: '/',
TRUNCATED: 'N',
PLATFORM: 'UNIX',
DEPTH: 1
}
],
dirlist: FOLDERS
})
/**
* The picker's probes, answered with a fixed listing. A probe that reaches the
* real mock would refuse: the estate is configured, and the gate in the mock
* makedata answers 500 for DIRLIST after the settings file exists.
*/
const stubProbes = () => {
cy.intercept('POST', /\/SASjsApi\/stp\/execute/, (req) => {
let program = ''
try {
program = decodeURIComponent(
(req.url.match(/_program=([^&]+)/) || [])[1] || ''
)
} catch (e) {
program = (req.url.match(/_program=([^&]+)/) || [])[1] || ''
}
if (!program.includes('admin/makedata')) {
req.continue()
return
}
const body =
typeof req.body === 'string' ? req.body : JSON.stringify(req.body)
if (body.includes('DIRLIST')) {
req.reply(dirResponse())
return
}
if (body.includes('WRITECHECK')) {
req.reply({
checkwrite: [
{
PATH: '/tmp/DataController',
PATH_EXISTS: 'Y',
IS_FOLDER: 'Y',
WRITEABLE: 'Y',
MSG: ' '
}
]
})
return
}
req.continue()
})
}
/**
* Boots the app onto the deploy screen. The admin folder is reported as
* unconfigured (makedata present), so the app routes there instead of loading
* the startup service. Readiness is the picker's Browse button appearing -
* never a fixed wait, which turns a slow runner into an intermittent failure.
*/
const bootDeployScreen = () => {
cy.intercept('GET', /\/SASjsApi\/drive\/folder/, (req) => {
let url = req.url
try {
url = decodeURIComponent(req.url)
} catch (e) {
url = req.url
}
if (url.includes('services/admin')) {
req.reply({ files: ['makedata.js'], folders: [] })
return
}
req.continue()
})
stubProbes()
cy.visit(hostUrl, { timeout: 60000 })
// Wait for the page to reach a state we can act on: a visible login form,
// the evaluation agreement, or the deploy screen. A hidden login form stays
// in the DOM on some routes, so it only counts when it is visible.
const loginVisible = ($body: any) => {
const userInput = $body.find('input.username')[0]
return !!userInput && !Cypress.dom.isHidden(userInput)
}
const deployState = ($body: any) =>
$body.find('#TCS input[type="checkbox"]').length +
$body.find('#dcDirectory-browse').length
cy.get('body').should(($body) => {
expect(
loginVisible($body) || deployState($body) > 0,
'the app rendered a state to act on'
).to.be.true
})
cy.get('body').then(($body) => {
if (loginVisible($body)) {
cy.get('input.username').type(username)
cy.get('input.password').type(password)
cy.get('.login-group button').click()
}
})
// Accept the evaluation agreement when it is shown. Wait for the page to
// reach one of the two post-boot states first - the agreement card or the
// deploy screen - so the checkbox is only looked for once it can exist.
cy.get('body', { timeout: 60000 }).should(($body) => {
expect(
deployState($body),
'the deploy screen or its agreement card rendered'
).to.be.greaterThan(0)
})
cy.get('body').then(($body) => {
if ($body.find('#TCS input[type="checkbox"]').length) {
cy.get('#TCS input[type="checkbox"]').check({ force: true })
}
})
cy.get('#dcDirectory-browse', { timeout: 60000 }).should('exist')
}
context('deploy folder picker: ', function () {
it('1 | lists folders below the picker row, not over it', () => {
bootDeployScreen()
cy.get('#dcDirectory-browse').click()
cy.get('#dcDirectory-browser', { timeout: 30000 }).should('exist')
// The geometry the overlay bug broke, asserted on the live DOM: the row
// contains everything it rendered, and the panel starts below it.
cy.get('.dc-location-input').then(($row) => {
const row = $row[0].getBoundingClientRect()
const input = $row.find('input')[0].getBoundingClientRect()
expect(input.top).to.be.at.least(row.top - 0.5)
expect(input.bottom).to.be.at.most(row.bottom + 0.5)
const buttons = $row.find('button')
for (const button of buttons) {
const rect = button.getBoundingClientRect()
expect(
rect.bottom,
`${button.textContent} sits inside the picker row`
).to.be.at.most(row.bottom + 0.5)
}
})
cy.get('#dcDirectory-browser').then(($panel) => {
const panel = $panel[0].getBoundingClientRect()
cy.get('.dc-location-input').then(($row) => {
const row = $row[0].getBoundingClientRect()
expect(
panel.top,
'the folder panel starts below the picker row'
).to.be.at.least(row.bottom - 0.5)
})
// The panel is usable, not just positioned: the listing arrived and the
// rows are inside the panel.
const rows = $panel.find('.dc-folder')
expect(rows.length, 'folders listed').to.equal(3)
for (const row of rows) {
const rect = row.getBoundingClientRect()
expect(rect.top).to.be.at.least(panel.top - 0.5)
expect(rect.bottom).to.be.at.most(panel.bottom + 0.5)
}
expect($panel[0].textContent).to.contain('alpha')
expect($panel[0].textContent).to.contain('beta')
expect($panel[0].textContent).to.contain('Permission denied')
})
})
it('2 | validates the chosen folder and keeps the row layout', () => {
bootDeployScreen()
// Set the path explicitly. The field may arrive pre-filled from SYSSCPL,
// and a blank one would make validate() short-circuit with "Type or pick a
// folder first" - a failed assertion that says nothing about the layout.
cy.get('#dcDirectory').clear().type('/tmp/DataController')
// Validate answers below the row as well, and reports the writeable
// folder - the verdict must not overlay the buttons either.
cy.get('#dcDirectory-validate').click()
cy.get('#dcDirectory-validate-result', { timeout: 30000 }).then(
($verdict) => {
const verdict = $verdict[0].getBoundingClientRect()
cy.get('.dc-location-input').then(($row) => {
const row = $row[0].getBoundingClientRect()
expect(
verdict.top,
'the write check verdict starts below the picker row'
).to.be.at.least(row.bottom - 0.5)
})
expect($verdict[0].textContent).to.contain('is writeable')
}
)
})
})