From 74c38e16416dcd8be20faa66d9f282a6438522b5 Mon Sep 17 00:00:00 2001 From: YuryShkoda Date: Mon, 3 Aug 2026 09:28:52 +0300 Subject: [PATCH] feat(stage): add Formatted/Unformatted toggle to the staging page - Mirrors the review page's toggle: getstagetable.sas now also webouts fmt_stagetable (SAS-formatted), selectFormattedRows() picks between it and the raw stagetable, falling back to raw if fmt data is absent - Adds stage.cy.ts e2e coverage and wires it into both build.yaml and release.yaml's cypress --spec lists (previously out of sync with each other on viewbox.cy.ts too) --- .gitea/workflows/build.yaml | 2 +- .gitea/workflows/release.yaml | 2 +- client/cypress/e2e/stage.cy.ts | 61 +++++++++++++++++++ .../utils/select-formatted-rows.spec.ts | 20 ++++++ .../app/shared/utils/select-formatted-rows.ts | 13 ++++ client/src/app/stage/stage.component.html | 16 ++++- client/src/app/stage/stage.component.scss | 24 ++++++++ client/src/app/stage/stage.component.ts | 21 ++++++- client/src/app/stage/stage.module.ts | 2 + .../sasjs/services/auditors/getstagetable.js | 4 ++ .../sasjs/services/auditors/getstagetable.js | 4 ++ sas/sasjs/services/auditors/getstagetable.sas | 3 + 12 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 client/cypress/e2e/stage.cy.ts create mode 100644 client/src/app/shared/utils/select-formatted-rows.spec.ts create mode 100644 client/src/app/shared/utils/select-formatted-rows.ts diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index cb57d4c..9b949c4 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -146,7 +146,7 @@ jobs: # Start frontend and run cypress # timeout 1800: SIGTERM after 30 min so Cypress can flush video/screenshots # before the outer timeout-minutes hard-kills the step (avoids silent multi-hour hangs) - npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && timeout 1800 npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts,cypress/e2e/viewer-labels.cy.ts,cypress/e2e/viewbox.cy.ts" + npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && timeout 1800 npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts,cypress/e2e/viewer-labels.cy.ts,cypress/e2e/viewbox.cy.ts,cypress/e2e/stage.cy.ts" - name: Zip Cypress videos if: always() diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index 9c9dab3..f8778a0 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -143,7 +143,7 @@ jobs: replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts cat ./cypress.config.ts # Start frontend and run cypress - npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts,cypress/e2e/viewer-labels.cy.ts" + npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts,cypress/e2e/viewer-labels.cy.ts,cypress/e2e/viewbox.cy.ts,cypress/e2e/stage.cy.ts" - name: Zip Cypress videos if: always() diff --git a/client/cypress/e2e/stage.cy.ts b/client/cypress/e2e/stage.cy.ts new file mode 100644 index 0000000..531c83a --- /dev/null +++ b/client/cypress/e2e/stage.cy.ts @@ -0,0 +1,61 @@ +const hostUrl = Cypress.env('hosturl') +const appLocation = Cypress.env('appLocation') +const longerCommandTimeout = Cypress.env('longerCommandTimeout') + +context('stage tests: ', function () { + this.beforeAll(() => { + cy.visit(`${hostUrl}/SASLogon/logout`) + cy.loginAndUpdateValidKey() + }) + + this.beforeEach(() => { + cy.visit(hostUrl + appLocation) + + visitPage('stage/DC20221007T122326121_612316_7259') + }) + + // getstagetable's mock ignores the table_id param entirely, so any id in + // the URL resolves to the same fixture row - no need to submit a real + // table first just to reach this page. + it('1 | Formatted/Unformatted toggle switches between fmt_stagetable and stagetable, defaulting to formatted', () => { + cy.get('.app-loading', { timeout: longerCommandTimeout }).should( + 'not.exist' + ) + + getCellByHeaderAndRow(0, 'SOME_DATE').should('have.text', '12FEB1960') + + cy.get('.formatted-values-toggle').click() + + getCellByHeaderAndRow(0, 'SOME_DATE').should('have.text', '42') + + // Toggling back reverts to the formatted view. + cy.get('.formatted-values-toggle').click() + + getCellByHeaderAndRow(0, 'SOME_DATE').should('have.text', '12FEB1960') + }) +}) + +const visitPage = (url: string) => { + cy.visit(`${hostUrl}${appLocation}/#/${url}`) +} + +// Locates a body cell by its column's header text rather than a hardcoded +// childNodes index - same helper as editor.cy.ts's own. +const getCellByHeaderAndRow = (rowIndex: number, headerText: string) => { + return cy + .get('.ht_clone_top .htCore thead tr th') + .should(($ths) => { + const texts = [...$ths].map((th) => th.innerText.trim()) + expect(texts).to.include(headerText) + }) + .then(($ths) => { + const index = [...$ths].findIndex( + (th) => th.innerText.trim() === headerText + ) + + return cy + .get('.ht_master tbody tr') + .then((rows: any) => rows[rowIndex].childNodes[index]) + .then((cell) => cy.get(cell)) + }) +} diff --git a/client/src/app/shared/utils/select-formatted-rows.spec.ts b/client/src/app/shared/utils/select-formatted-rows.spec.ts new file mode 100644 index 0000000..da966be --- /dev/null +++ b/client/src/app/shared/utils/select-formatted-rows.spec.ts @@ -0,0 +1,20 @@ +import { selectFormattedRows } from './select-formatted-rows' + +describe('selectFormattedRows', () => { + const rawRows = [{ SOME_NUM: 42, SOME_DATE: 42 }] + const formattedRows = [{ SOME_NUM: '42', SOME_DATE: '11FEB1960' }] + + it('returns the formatted rows when showFormatted is true and formatted rows exist', () => { + expect(selectFormattedRows(rawRows, formattedRows, true)).toBe( + formattedRows + ) + }) + + it('returns the raw rows when showFormatted is false, even if formatted rows exist', () => { + expect(selectFormattedRows(rawRows, formattedRows, false)).toBe(rawRows) + }) + + it('falls back to the raw rows when showFormatted is true but no formatted rows were provided', () => { + expect(selectFormattedRows(rawRows, undefined, true)).toBe(rawRows) + }) +}) diff --git a/client/src/app/shared/utils/select-formatted-rows.ts b/client/src/app/shared/utils/select-formatted-rows.ts new file mode 100644 index 0000000..bcda676 --- /dev/null +++ b/client/src/app/shared/utils/select-formatted-rows.ts @@ -0,0 +1,13 @@ +/** + * Picks which row set to render - raw or SAS-formatted - the same + * formatted/unformatted toggle already used on the review page, extracted + * so it isn't duplicated inline. Falls back to raw rows if the backend + * hasn't sent a formatted variant (e.g. an older service response). + */ +export function selectFormattedRows( + rawRows: any[], + formattedRows: any[] | undefined, + showFormatted: boolean +): any[] { + return showFormatted && formattedRows ? formattedRows : rawRows +} diff --git a/client/src/app/stage/stage.component.html b/client/src/app/stage/stage.component.html index 7eac67a..3e5f1d8 100644 --- a/client/src/app/stage/stage.component.html +++ b/client/src/app/stage/stage.component.html @@ -66,7 +66,7 @@
Actions
-
+
+ + + + + + @if (tableDetails?.['ALLOW_RESTORE'] === 'YES') {