244 lines
9.7 KiB
TypeScript
244 lines
9.7 KiB
TypeScript
// 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 — see e.g. download.cy.ts, which gets
|
|
// this for free via a real import.
|
|
export {}
|
|
|
|
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}`)
|
|
|
|
// Viewer column-label display toggle (`?labels=true`): clients surfacing DC in
|
|
// SAS Visual Analytics want column LABELs shown instead of NAMEs. Default
|
|
// behavior (no param) must stay unchanged; the right-click context menu's
|
|
// "Show labels"/"Show names" item is the in-app affordance, and it drives the
|
|
// same URL param so state is shareable/refreshable. See mock data in
|
|
// sas/mocks/sasjs/services/public/viewdata.js: MPE_X_TEST's SOME_CHAR/SOME_DATE
|
|
// have LABELs that differ from NAME, and SOME_NUM has a blank LABEL (fallback
|
|
// to NAME) — this spec exercises exactly that fixture.
|
|
context('viewer column labels toggle tests: ', function () {
|
|
this.beforeAll(() => {
|
|
cy.visit(`${hostUrl}/SASLogon/logout`)
|
|
// forceLicenceKey=true: without it, loginAndUpdateValidKey() only
|
|
// generates key data and logs back out if no valid license already
|
|
// exists — it doesn't actually apply one (see commands.ts). Several
|
|
// specs call it with no argument and rely on an *earlier* spec in the
|
|
// same CI run having already forced a valid license onto the shared
|
|
// server first (csv.cy.ts, excel.cy.ts, etc.). This spec shouldn't
|
|
// depend on run order, so force it explicitly.
|
|
cy.loginAndUpdateValidKey(true)
|
|
})
|
|
|
|
this.beforeEach(() => {
|
|
// No re-login here: beforeAll's loginAndUpdateValidKey() already
|
|
// authenticated the session, and it persists across tests in this file
|
|
// (see filtering.cy.ts/csv.cy.ts for the same pattern). Re-typing into
|
|
// the login form would fail — it's hidden once already logged in.
|
|
cy.visit(hostUrl + appLocation)
|
|
|
|
// Visit 'home' first, matching filtering.cy.ts/csv.cy.ts/download.cy.ts —
|
|
// going straight to 'view/data' skips whatever startup-data fetch 'home'
|
|
// triggers, which passed locally (fast round-trips) but left the
|
|
// nav-tree empty in CI's slower environment.
|
|
visitPage('home')
|
|
visitPage('view/data')
|
|
})
|
|
|
|
it('1 | default (no param): headers show NAMEs', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
|
|
cy.get('#hotTable .ht_clone_top .htCore thead tr', {
|
|
timeout: longerCommandTimeout
|
|
}).should(($headerRow) => {
|
|
expect($headerRow[0].innerHTML).to.include('SOME_CHAR')
|
|
expect($headerRow[0].innerHTML).to.not.include('Some Character Column')
|
|
})
|
|
})
|
|
|
|
it('2 | ?labels=true: headers show LABELs, blank LABEL falls back to NAME', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
appendUrlParam('labels=true')
|
|
|
|
cy.get('#hotTable .ht_clone_top .htCore thead tr', {
|
|
timeout: longerCommandTimeout
|
|
}).should(($headerRow) => {
|
|
const html = $headerRow[0].innerHTML
|
|
expect(html).to.include('Some Character Column')
|
|
expect(html).to.include('Some Date')
|
|
// SOME_NUM has a blank LABEL in the mock -> falls back to NAME
|
|
expect(html).to.include('SOME_NUM')
|
|
})
|
|
})
|
|
|
|
it('3 | context menu "Show labels"/"Show names" toggles headers and the URL', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
|
|
toggleColumnLabelsFromContextMenu('Show labels')
|
|
|
|
cy.url().should('include', 'labels=true')
|
|
cy.get('#hotTable .ht_clone_top .htCore thead tr', {
|
|
timeout: longerCommandTimeout
|
|
}).should(($headerRow) => {
|
|
expect($headerRow[0].innerHTML).to.include('Some Character Column')
|
|
})
|
|
|
|
toggleColumnLabelsFromContextMenu('Show names')
|
|
|
|
cy.url().should('not.include', 'labels=true')
|
|
cy.get('#hotTable .ht_clone_top .htCore thead tr', {
|
|
timeout: longerCommandTimeout
|
|
}).should(($headerRow) => {
|
|
expect($headerRow[0].innerHTML).to.include('SOME_CHAR')
|
|
})
|
|
})
|
|
|
|
it('4 | ?embed=va&labels=true: labels shown with embed chrome hidden', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
appendUrlParam('embed=va&labels=true')
|
|
|
|
cy.get('header.app-header').should('not.exist')
|
|
cy.get('#hotTable .ht_clone_top .htCore thead tr', {
|
|
timeout: longerCommandTimeout
|
|
}).should(($headerRow) => {
|
|
expect($headerRow[0].innerHTML).to.include('Some Character Column')
|
|
})
|
|
})
|
|
|
|
it('5 | info dropdown: first line is NAME (in both toggle states)', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
|
|
// No assertion on the item's label text here: `info` has a custom
|
|
// `renderer`, so Handsontable never renders its `name` ("test info") as
|
|
// text at all — the renderer's own output (checked below) is the only
|
|
// real content.
|
|
openColumnDropdown('SOME_CHAR')
|
|
cy.get('.htDropdownMenu').should(($menu) => {
|
|
expect($menu.text()).to.match(/NAME: SOME_CHAR/)
|
|
})
|
|
cy.get('body').click(0, 0) // close menu
|
|
|
|
appendUrlParam('labels=true')
|
|
|
|
openColumnDropdown('Some Character Column')
|
|
cy.get('.htDropdownMenu').should(($menu) => {
|
|
// NAME is always shown first, even while headers display as LABEL
|
|
expect($menu.text()).to.match(/NAME: SOME_CHAR/)
|
|
})
|
|
})
|
|
})
|
|
|
|
const visitPage = (url: string) => {
|
|
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
|
}
|
|
|
|
// Re-visits the current route with extra hash-query params appended, then
|
|
// forces a real reload. Under hash routing (`useHash: true`), changing only
|
|
// the URL's hash fragment is a same-document navigation — Angular Router
|
|
// still reacts to it (which is enough for `useLabels`, read reactively from
|
|
// `route.queryParams`), but `embed` is parsed once from `window.location.hash`
|
|
// at app bootstrap (app.component.ts), so `?embed=va` only takes effect after
|
|
// an actual reload.
|
|
const appendUrlParam = (param: string) => {
|
|
cy.url().then((url) => {
|
|
const separator = url.includes('?') ? '&' : '?'
|
|
cy.visit(`${url}${separator}${param}`)
|
|
cy.reload()
|
|
cy.get('.app-loading', { timeout: longerCommandTimeout }).should(
|
|
'not.exist'
|
|
)
|
|
})
|
|
}
|
|
|
|
const openColumnDropdown = (headerText: string) => {
|
|
// Handsontable renders the sticky/frozen header via a separate clone pane
|
|
// (.ht_clone_top) for scroll behavior; the header row inside the main
|
|
// .ht_master pane is kept visibility:hidden (replaced visually by the
|
|
// clone). .ht_clone_top is the real, interactive one — confirmed via
|
|
// `document.querySelector('#hotTable .ht_clone_top thead').innerText` in
|
|
// a live browser session.
|
|
cy.get('#hotTable .ht_clone_top .htCore thead button.changeType', {
|
|
timeout: longerCommandTimeout
|
|
})
|
|
.parents('th')
|
|
.filter((_, th) => Cypress.$(th).text().includes(headerText))
|
|
.last()
|
|
.as('targetHeader')
|
|
|
|
// Click the header text first to select the column — the `info` item's
|
|
// renderer reads hot.getSelected() to decide which column to describe,
|
|
// so without an active selection it always shows "No info found".
|
|
cy.get('@targetHeader').click()
|
|
cy.get('@targetHeader').find('button.changeType').click({ force: true })
|
|
}
|
|
|
|
const toggleColumnLabelsFromContextMenu = (
|
|
menuItemText: 'Show labels' | 'Show names'
|
|
) => {
|
|
cy.get('#hotTable .ht_master.handsontable .htCore tbody tr td', {
|
|
timeout: longerCommandTimeout
|
|
})
|
|
.first()
|
|
// force: true — the first body row can sit under an overlay clone's
|
|
// header/sort-icon layer, which fails Cypress's actionability check
|
|
// even though the cell is the real rightclick target underneath.
|
|
.rightclick({ force: true })
|
|
|
|
cy.get('.htContextMenu').contains(menuItemText).click()
|
|
|
|
cy.get('.app-loading', { timeout: longerCommandTimeout }).should('not.exist')
|
|
}
|
|
|
|
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(() => {
|
|
// Small settle wait: right after a fresh visit/reload, the tree
|
|
// component can still be re-rendering, causing this node to be
|
|
// found then swapped out mid-click ("disappeared from the page").
|
|
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
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// Selecting the table triggers async SPA routing + a viewdata fetch;
|
|
// wait for the grid to actually render before any subsequent action
|
|
// (reading the URL, right-clicking, opening a header dropdown) — without
|
|
// this, callers can act while still on the intermediate library-only
|
|
// route or a not-yet-rendered grid. Waiting specifically for a header's
|
|
// dropdown button (not just the <tr> shell) confirms Handsontable has
|
|
// finished populating header cell contents, not just their DOM rows.
|
|
cy.get('#hotTable .ht_clone_top .htCore thead button.changeType', {
|
|
timeout: longerCommandTimeout
|
|
}).should('exist')
|
|
}
|