diff --git a/src/layout/Summary/SummaryComponent.tsx b/src/layout/Summary/SummaryComponent.tsx index 856fe44e53..d49124a153 100644 --- a/src/layout/Summary/SummaryComponent.tsx +++ b/src/layout/Summary/SummaryComponent.tsx @@ -189,7 +189,10 @@ const SummaryComponentInner = React.forwardRef(function ( onChangeClick={onChangeClick} changeText={langAsString('form_filler.summary_item_change')} targetBaseComponentId={targetBaseComponentId} - overrides={{ largeGroup, display, pageBreak, grid, excludedChildren }} + // Intentionally not passing the pageBreak override here, as that will cause the property to + // be inherited forever in repeating groups, causing every component inside to be on a separate page. + // Passing `grid` here would make every Summary render use the grid settings for the topmost component + overrides={{ largeGroup, display, excludedChildren }} RenderSummary={RenderSummary} /> ) : ( diff --git a/test/e2e/integration/frontend-test/pdf.ts b/test/e2e/integration/frontend-test/pdf.ts index ca5dc6fe22..0784920ad5 100644 --- a/test/e2e/integration/frontend-test/pdf.ts +++ b/test/e2e/integration/frontend-test/pdf.ts @@ -230,6 +230,75 @@ describe('PDF', () => { }); }); + it('should generate PDF for group step (using Summary1 pdfLayout)', () => { + cy.intercept('GET', '**/api/layoutsettings/group', (req) => { + req.on('response', (res) => { + const body = JSON.parse(res.body) as ILayoutSettings; + body.pages.pdfLayoutName = 'summary'; // Forces PDF engine to use the 'summary' page as the PDF page + res.send(body); + }); + }).as('settings'); + + cy.goto('group'); + cy.findByRole('checkbox', { name: /liten/i }).check(); + cy.findByRole('checkbox', { name: /middels/i }).check(); + cy.findByRole('checkbox', { name: /stor/i }).check(); + cy.findByRole('checkbox', { name: /svær/i }).check(); + cy.findByRole('checkbox', { name: /enorm/i }).check(); + + cy.gotoNavPage('repeating'); + cy.findByRole('checkbox', { name: /ja/i }).check(); + + cy.interceptLayout('group', (component) => { + if (component.type === 'RepeatingGroup' && component.id === 'mainGroup') { + component.pageBreak = { + breakBefore: 'always', + breakAfter: 'auto', + }; + } + }); + + cy.testPdf({ + freeze: false, + snapshotName: 'group-custom-summary1', + callback: () => { + // Regression test for https://github.com/Altinn/app-frontend-react/issues/3745 + cy.expectPageBreaks(6); + }, + }); + }); + + it('should generate PDF for group step (using Summary2 automatic PDF)', () => { + cy.setFeatureToggle('betaPDFenabled', true); + cy.goto('group'); + cy.findByRole('checkbox', { name: /liten/i }).check(); + cy.findByRole('checkbox', { name: /middels/i }).check(); + cy.findByRole('checkbox', { name: /stor/i }).check(); + cy.findByRole('checkbox', { name: /svær/i }).check(); + cy.findByRole('checkbox', { name: /enorm/i }).check(); + + cy.gotoNavPage('repeating'); + cy.findByRole('checkbox', { name: /ja/i }).check(); + + cy.interceptLayout('group', (component) => { + if (component.type === 'RepeatingGroup' && component.id === 'mainGroup') { + component.pageBreak = { + breakBefore: 'always', + breakAfter: 'auto', + }; + } + }); + + cy.testPdf({ + freeze: false, + snapshotName: 'group-custom-summary2', + callback: () => { + // Summary2 doesn't do page-breaks per row, only for the component itself + cy.expectPageBreaks(1); + }, + }); + }); + it('should generate PDF for likert step', () => { cy.goto('likert'); cy.findByRole('table', { name: likertPage.optionalTableTitle }).within(() => { diff --git a/test/e2e/integration/multiple-datamodels-test/saving.ts b/test/e2e/integration/multiple-datamodels-test/saving.ts index ec765fbe06..17b3bc4349 100644 --- a/test/e2e/integration/multiple-datamodels-test/saving.ts +++ b/test/e2e/integration/multiple-datamodels-test/saving.ts @@ -11,7 +11,7 @@ describe('saving multiple data models', () => { // same time. cy.intercept('PATCH', '**/data*').as('saveFormData'); cy.startAppInstance(appFrontend.apps.multipleDatamodelsTest); - cy.setCookie('FEATURE_saveOnBlur', 'false'); + cy.setFeatureToggle('saveOnBlur', false); }); it('Calls save on individual data models', () => { diff --git a/test/e2e/support/custom.ts b/test/e2e/support/custom.ts index 15674b5f3c..d4a9c2bfa4 100644 --- a/test/e2e/support/custom.ts +++ b/test/e2e/support/custom.ts @@ -12,6 +12,7 @@ import type { ResponseFuzzing, Size, SnapshotOptions, SnapshotViewport } from 't import { breakpoints } from 'src/hooks/useDeviceWidths'; import { getInstanceIdRegExp } from 'src/utils/instanceIdRegExp'; import type { LayoutContextValue } from 'src/features/form/layout/LayoutsContext'; +import type { IFeatureToggles } from 'src/features/toggles'; import type { ILayoutFile } from 'src/layout/common.generated'; import JQueryWithSelector = Cypress.JQueryWithSelector; @@ -635,6 +636,7 @@ Cypress.Commands.add( snapshotName = false, beforeReload, callback, + freeze = true, returnToForm = false, enableResponseFuzzing = false, buildUrl = buildPdfUrl, @@ -685,19 +687,24 @@ Cypress.Commands.add( cy.viewport(794, 1123); cy.get('body').invoke('css', 'margin', '0.75in'); - // Stops timers which helps in 'freezing' the page in its current state, makes it easier to see when data is missing - cy.clock(); - - cy.then(() => { - const timeout = setTimeout(() => { - throw 'PDF callback failed, print was not ready when #readyForPrint appeared'; - }, 0); - // Verify that generic elements that should be hidden are not present + if (freeze) { + // Stops timers which helps in 'freezing' the page in its current state, makes it easier to see when data is missing + cy.clock(); + + cy.then(() => { + const timeout = setTimeout(() => { + throw 'PDF callback failed, print was not ready when #readyForPrint appeared'; + }, 0); + // Verify that generic elements that should be hidden are not present + cy.findAllByRole('button').should('not.exist'); + // Run tests from callback + callback(); + cy.then(() => clearTimeout(timeout)); + }); + } else { cy.findAllByRole('button').should('not.exist'); - // Run tests from callback callback(); - cy.then(() => clearTimeout(timeout)); - }); + } // Disable response fuzzing and re-enable caching cy.get('@responseFuzzing').invoke('disable'); @@ -987,3 +994,20 @@ Cypress.Commands.add('openNavGroup', (groupName, pageName, subformName) => { }); } }); + +Cypress.Commands.add('expectPageBreaks', (expectedCount: number) => { + cy.window().should((win) => { + if (!win.matchMedia('print').matches) { + throw new Error('expectPageBreaks can only be called when media is in print mode'); + } + const allElements = Array.from(win.document.querySelectorAll('*')); + const breakBeforeCount = allElements.filter((e) => win.getComputedStyle(e).breakBefore === 'page').length; + const breakAfterCount = allElements.filter((e) => win.getComputedStyle(e).breakAfter === 'page').length; + const pageCount = breakBeforeCount + breakAfterCount; + expect(pageCount).to.equal(expectedCount); + }); +}); + +Cypress.Commands.add('setFeatureToggle', (toggleName: IFeatureToggles, value: boolean) => { + cy.setCookie(`FEATURE_${toggleName}`, value.toString()); +}); diff --git a/test/e2e/support/global.ts b/test/e2e/support/global.ts index 8a362bf755..54ac6acf80 100644 --- a/test/e2e/support/global.ts +++ b/test/e2e/support/global.ts @@ -4,6 +4,7 @@ import type { ConsoleMessage } from 'cypress-fail-on-console-error'; import type { CyUser, TenorUser } from 'test/e2e/support/auth'; +import type { IFeatureToggles } from 'src/features/toggles'; import type { BackendValidationIssue, BackendValidationIssuesWithSource } from 'src/features/validation'; import type { ILayoutSets } from 'src/layout/common.generated'; import type { CompExternal, ILayoutCollection, ILayouts } from 'src/layout/layout'; @@ -33,6 +34,7 @@ export type StartAppInstanceOptions = { export interface TestPdfOptions { snapshotName?: string; beforeReload?: () => void; + freeze?: boolean; callback: () => void; returnToForm?: boolean; enableResponseFuzzing?: boolean; @@ -325,6 +327,16 @@ declare global { openNavGroup(groupName: RegExp, pageName?: RegExp, subformName?: RegExp): Chainable; + /** + * Assert the approximate number of pages in a printout by counting CSS break-before and break-after page properties + */ + expectPageBreaks(expectedCount: number): Chainable; + + /** + * Set a feature toggle value via cookie + */ + setFeatureToggle(toggleName: IFeatureToggles, value: boolean): Chainable; + ignoreConsoleMessages(consoleMessages: ConsoleMessage[]): Chainable; } }