diff --git a/test/README.md b/test/README.md index fde7b82fc9..f71cfc3397 100755 --- a/test/README.md +++ b/test/README.md @@ -47,7 +47,7 @@ npx cypress run --env environment=tt02 -s 'test/e2e/integration/*/*.ts' - [ttd/signering-brukerstyrt](https://altinn.studio/repos/ttd/signering-brukerstyrt) - [ttd/signing-test](https://dev.altinn.studio/repos/ttd/signing-test) - [ttd/stateless-app](https://dev.altinn.studio/repos/ttd/stateless-app) -- [ttd/subform-test](https://dev.altinn.studio/repos/ttd/subform-test) +- [ttd/subform-test](https://altinn.studio/repos/ttd/subform-test) 3. Start the app you want to test: diff --git a/test/e2e/config/tt02.json b/test/e2e/config/tt02.json index ac4902042f..72637175d0 100644 --- a/test/e2e/config/tt02.json +++ b/test/e2e/config/tt02.json @@ -28,8 +28,6 @@ "selfIdentifiedFullName": "olemartinorg", "selfIdentifiedFirstName": "olemartinorg", "selfIdentifiedUserName": "olemartinorg", - "selfIdentifiedUserPwd": "qkwcaDU9qsaA3iL5bgKGMLpfYdf3vzpb", - - "signingPartyId": "51826033" + "selfIdentifiedUserPwd": "qkwcaDU9qsaA3iL5bgKGMLpfYdf3vzpb" } } diff --git a/test/e2e/integration/anonymous-stateless-app/anonymous.ts b/test/e2e/integration/anonymous-stateless-app/anonymous.ts index ed58c08d32..19b9a0ab96 100644 --- a/test/e2e/integration/anonymous-stateless-app/anonymous.ts +++ b/test/e2e/integration/anonymous-stateless-app/anonymous.ts @@ -6,7 +6,7 @@ const appFrontend = new AppFrontend(); describe('Anonymous (stateless)', () => { beforeEach(() => { cy.intercept('**/api/layoutsettings/stateless').as('getLayoutStateless'); - cy.startAppInstance(appFrontend.apps.anonymousStateless, { cyUser: null }); + cy.startAppInstance(appFrontend.apps.anonymousStateless, { cyUser: null, tenorUser: null }); cy.wait('@getLayoutStateless'); cy.get(appFrontend.stateless.name).should('exist').and('be.visible'); }); diff --git a/test/e2e/integration/anonymous-stateless-app/auto-save-behavior.ts b/test/e2e/integration/anonymous-stateless-app/auto-save-behavior.ts index 0dae6fbdde..4a93f49dda 100644 --- a/test/e2e/integration/anonymous-stateless-app/auto-save-behavior.ts +++ b/test/e2e/integration/anonymous-stateless-app/auto-save-behavior.ts @@ -9,7 +9,7 @@ describe('Auto save behavior', () => { postFormDataCounter++; }).as('putFormData'); cy.interceptLayoutSetsUiSettings({ autoSaveBehavior: 'onChangeFormData' }); - cy.startAppInstance(appFrontend.apps.anonymousStateless, { cyUser: null }); + cy.startAppInstance(appFrontend.apps.anonymousStateless, { cyUser: null, tenorUser: null }); cy.get(appFrontend.stateless.name).type('Per'); cy.wait('@putFormData').then(() => { @@ -30,7 +30,7 @@ describe('Auto save behavior', () => { postFormDataCounter++; }).as('putFormData'); cy.interceptLayoutSetsUiSettings({ autoSaveBehavior: 'onChangePage' }); - cy.startAppInstance(appFrontend.apps.anonymousStateless, { cyUser: null }); + cy.startAppInstance(appFrontend.apps.anonymousStateless, { cyUser: null, tenorUser: null }); cy.get(appFrontend.stateless.name).type('Per'); // Doing a hard wait to be sure no request is sent to backend diff --git a/test/e2e/integration/anonymous-stateless-app/options.ts b/test/e2e/integration/anonymous-stateless-app/options.ts index 536bd214e9..969f32cdb8 100644 --- a/test/e2e/integration/anonymous-stateless-app/options.ts +++ b/test/e2e/integration/anonymous-stateless-app/options.ts @@ -5,7 +5,7 @@ const appFrontend = new AppFrontend(); describe('Anonymous (stateless) - Options', () => { it('should support fetching option list and changing its value', () => { cy.intercept('**/api/layoutsettings/stateless').as('getLayoutStateless'); - cy.startAppInstance(appFrontend.apps.anonymousStateless, { cyUser: null }); + cy.startAppInstance(appFrontend.apps.anonymousStateless, { cyUser: null, tenorUser: null }); cy.wait('@getLayoutStateless'); const dropdownComponent = appFrontend.stateless.dropdown; diff --git a/test/e2e/integration/anonymous-stateless-app/validation.ts b/test/e2e/integration/anonymous-stateless-app/validation.ts index 5dd62f2fca..598b878547 100644 --- a/test/e2e/integration/anonymous-stateless-app/validation.ts +++ b/test/e2e/integration/anonymous-stateless-app/validation.ts @@ -5,7 +5,7 @@ const appFrontend = new AppFrontend(); describe('Validation in anonymous stateless app', () => { it('Should show validation message for missing name', () => { - cy.startAppInstance(appFrontend.apps.anonymousStateless, { cyUser: null }); + cy.startAppInstance(appFrontend.apps.anonymousStateless, { cyUser: null, tenorUser: null }); cy.get(appFrontend.stateless.name).should('exist').and('be.visible'); cy.get(appFrontend.stateless.name).invoke('val').should('be.empty'); cy.get(appFrontend.navButtons).contains('button', 'next').click(); diff --git a/test/e2e/integration/frontend-test/accordion.ts b/test/e2e/integration/frontend-test/accordion.ts index 57a9c6ffa2..cf99681579 100644 --- a/test/e2e/integration/frontend-test/accordion.ts +++ b/test/e2e/integration/frontend-test/accordion.ts @@ -4,7 +4,7 @@ describe('Accordion', () => { const accordionContent = /in horas tendebat resumptis/i; - cy.findByText(accordionContent).should('not.exist'); + cy.findByText(accordionContent).should('not.be.visible'); cy.findByRole('button', { name: /mer informasjon vedrørende navneendring/i }).click(); cy.findByText(accordionContent).should('be.visible'); }); diff --git a/test/e2e/integration/frontend-test/auto-save-behavior.ts b/test/e2e/integration/frontend-test/auto-save-behavior.ts index 8d65702ca6..f62e2d5cb2 100644 --- a/test/e2e/integration/frontend-test/auto-save-behavior.ts +++ b/test/e2e/integration/frontend-test/auto-save-behavior.ts @@ -14,10 +14,10 @@ describe('Auto save behavior', () => { cy.goto('group'); cy.findByRole('checkbox', { name: appFrontend.group.prefill.liten }).check(); - cy.get('@saveFormData.all').should('have.length', 1); + cy.get('@saveFormData.all').should('have.length', 2); cy.findByRole('button', { name: 'Neste' }).clickAndGone(); - cy.get('@saveFormData.all').should('have.length', 2); // The row has a fiels with preselectedOptionIndex + cy.get('@saveFormData.all').should('have.length', 3); // The row has a fiels with preselectedOptionIndex cy.findByRole('button', { name: 'Forrige' }).clickAndGone(); // Doing an extra wait to be sure no request is sent to backend @@ -25,7 +25,7 @@ describe('Auto save behavior', () => { cy.waitForNetworkIdle(100); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(100); - cy.get('@saveFormData.all').should('have.length', 2); + cy.get('@saveFormData.all').should('have.length', 3); }); it('onChangePage: Should not save form when interacting with form element(checkbox), but should save on navigating between pages', () => { @@ -37,11 +37,11 @@ describe('Auto save behavior', () => { // Doing a hard wait to be sure no request is sent to backend // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(1000); - cy.get('@saveFormData.all').should('have.length', 0); + cy.get('@saveFormData.all').should('have.length', 1); // At this point we've saved the prefill value (1). cy.findByRole('button', { name: 'Neste' }).clickAndGone(); - cy.get('@saveFormData.all').should('have.length', 1); + cy.get('@saveFormData.all').should('have.length', 2); // Clicking the back button does not save anything, because we didn't // change anything in the form data worth saving @@ -55,34 +55,34 @@ describe('Auto save behavior', () => { // At some point when we got the reply back we saw that one of those new rows should have a preselectedOptionIndex, // so that gets set and saved as well during a page navigation (2). - cy.get('@saveFormData.all').should('have.length', 2); + cy.get('@saveFormData.all').should('have.length', 3); cy.get(appFrontend.group.showGroupToContinue).findByRole('checkbox', { name: 'Ja' }).check(); cy.get(appFrontend.group.mainGroup).should('be.visible'); // We have now clicked 'Ja' to show the repeating group (3) cy.findByRole('button', { name: 'Forrige' }).clickAndGone(); - cy.get('@saveFormData.all').should('have.length', 3); + cy.get('@saveFormData.all').should('have.length', 4); // NavigationBar cy.findByRole('checkbox', { name: appFrontend.group.prefill.middels }).check(); // Now we've added 'middels' (4) cy.gotoNavPage('repeating'); - cy.get('@saveFormData.all').should('have.length', 4); + cy.get('@saveFormData.all').should('have.length', 5); // Icon previous button cy.get(appFrontend.group.showGroupToContinue).findByRole('checkbox', { name: 'Ja' }).uncheck(); // Now we've unchecked 'ja' and added a preselectedOptionIndex for the new row (5) cy.findByRole('button', { name: 'Forrige' }).clickAndGone(); - cy.get('@saveFormData.all').should('have.length', 5); + cy.get('@saveFormData.all').should('have.length', 6); // Doing a hard wait to be sure no request is sent to backend // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(1000); cy.waitUntilSaved(); - cy.get('@saveFormData.all').should('have.length', 5); + cy.get('@saveFormData.all').should('have.length', 6); }); (['current', 'all'] as const).forEach((pages) => { diff --git a/test/e2e/integration/frontend-test/instantiation.ts b/test/e2e/integration/frontend-test/instantiation.ts index d7487f120b..1f5cbdef2b 100644 --- a/test/e2e/integration/frontend-test/instantiation.ts +++ b/test/e2e/integration/frontend-test/instantiation.ts @@ -2,6 +2,7 @@ import { v4 as uuidv4 } from 'uuid'; import { AppFrontend } from 'test/e2e/pageobjects/app-frontend'; import { cyMockResponses } from 'test/e2e/pageobjects/party-mocks'; +import { Tenor } from 'test/e2e/support/users'; import type { IncomingApplicationMetadata } from 'src/features/applicationMetadata/types'; import type { InstantiationValidationResult } from 'src/features/instantiate/InstantiationValidation'; @@ -14,14 +15,14 @@ describe('Instantiation', () => { const invalidParty = Cypress.env('type') === 'localtest' ? /950474084/ // Localtest: Oslos Vakreste borettslag - : /310732001/; // TT02: Søvnig Impulsiv Tiger AS + : /314277961/; // TT02: Offisiell Virtuell Tiger AS it('should show an error message when going directly to instantiation', () => { cyMockResponses({ doNotPromptForParty: false, onEntryShow: 'new-instance', }); - cy.startAppInstance(appFrontend.apps.frontendTest, { cyUser: 'manager' }); + cy.startAppInstance(appFrontend.apps.frontendTest, { cyUser: 'manager', tenorUser: Tenor.users.snaalDugnad }); cy.findByRole('button', { name: invalidParty }).click(); cy.findByText('Du kan ikke starte denne tjenesten').should('be.visible'); @@ -37,7 +38,7 @@ describe('Instantiation', () => { { id: 'def456', lastChanged: '2023-01-02T00:00:00.000Z', lastChangedBy: 'user' }, ], }); - cy.startAppInstance(appFrontend.apps.frontendTest, { cyUser: 'manager' }); + cy.startAppInstance(appFrontend.apps.frontendTest, { cyUser: 'manager', tenorUser: Tenor.users.snaalDugnad }); cy.findByRole('button', { name: invalidParty }).click(); cy.findByText('Du har allerede startet å fylle ut dette skjemaet.').should('be.visible'); diff --git a/test/e2e/integration/frontend-test/party-selection.ts b/test/e2e/integration/frontend-test/party-selection.ts index d0bb9ffa74..592da562de 100644 --- a/test/e2e/integration/frontend-test/party-selection.ts +++ b/test/e2e/integration/frontend-test/party-selection.ts @@ -2,6 +2,8 @@ import texts from 'test/e2e/fixtures/texts.json'; import { AppFrontend } from 'test/e2e/pageobjects/app-frontend'; import { cyMockResponses, CyPartyMocks, removeAllButKeepOrg } from 'test/e2e/pageobjects/party-mocks'; import { cyUserCredentials } from 'test/e2e/support/auth'; +import { Tenor } from 'test/e2e/support/users'; +import type { CyUser } from 'test/e2e/support/auth'; import type { IParty } from 'src/types/shared'; @@ -10,7 +12,8 @@ const appFrontend = new AppFrontend(); // Org numbers the accountant test user only represents as accountant — no instantiate rights, so // instantiation returns 403. const NonInstantiableOrgForAccountant = { - AldrendeOppstemtTigerAS: '213611372', + DdgFitness: '897069650', + StabilSmulApe: '313252698', } as const; describe('Party selection', () => { @@ -123,44 +126,38 @@ describe('Party selection', () => { }); }); - [false].forEach((doNotPromptForParty) => { - it(`${ - doNotPromptForParty ? 'Does not prompt' : 'Prompts' - } for party when doNotPromptForParty = ${doNotPromptForParty}, on instantiation with multiple possible parties`, () => { - cyMockResponses({ - allowedToInstantiate: (parties) => [...parties, CyPartyMocks.ExamplePerson1], - doNotPromptForParty, - }); - cy.startAppInstance(appFrontend.apps.frontendTest); + it(`Prompts for party when doNotPromptForParty = false, on instantiation with multiple possible parties`, () => { + cyMockResponses({ + allowedToInstantiate: (parties) => [...parties, CyPartyMocks.ExamplePerson1], + doNotPromptForParty: false, + }); + cy.startAppInstance(appFrontend.apps.frontendTest, { tenorUser: Tenor.users.humanAndrefiolin }); - if (!doNotPromptForParty) { - cy.get(appFrontend.partySelection.appHeader).should('be.visible'); - cy.get('[id^="party-"]').should('be.visible'); - cy.findByRole('heading', { name: 'Hvorfor ser jeg dette?' }).should('be.visible'); - cy.findByRole('heading', { name: 'Hvorfor ser jeg dette?' }) - .siblings('p') - .first() - .should( - 'contain.text', - 'Du kan endre profilinnstillingene dine for å ikke bli spurt om aktør hver gang du starter utfylling av et nytt skjema.', - ); - cy.findByRole('heading', { name: 'Appen for test av app frontend' }).should('not.exist'); + cy.get(appFrontend.partySelection.appHeader).should('be.visible'); + cy.get('[id^="party-"]').should('be.visible'); + cy.findByRole('heading', { name: 'Hvorfor ser jeg dette?' }).should('be.visible'); + cy.findByRole('heading', { name: 'Hvorfor ser jeg dette?' }) + .siblings('p') + .first() + .should( + 'contain.text', + 'Du kan endre profilinnstillingene dine for å ikke bli spurt om aktør hver gang du starter utfylling av et nytt skjema.', + ); + cy.findByRole('heading', { name: 'Appen for test av app frontend' }).should('not.exist'); - cy.visualTesting('reportee-selection'); + cy.visualTesting('reportee-selection'); - cy.get('[id^="party-"]').eq(0).click(); - } + clickValidParty('default'); - cy.get(appFrontend.appHeader).should('be.visible'); - cy.findByRole('heading', { name: 'Appen for test av app frontend' }).should('be.visible'); - cy.get('[id^="party-"]').should('not.exist'); + cy.get(appFrontend.appHeader).should('be.visible'); + cy.findByRole('heading', { name: 'Appen for test av app frontend' }).should('be.visible'); + cy.get('[id^="party-"]').should('not.exist'); - // Test that it goes straight in when accessing an existing instance - cy.reloadAndWait(); + // Test that it goes straight in when accessing an existing instance + cy.reloadAndWait(); - cy.findByRole('heading', { name: 'Appen for test av app frontend' }).should('be.visible'); - cy.get('[id^="party-"]').should('not.exist'); - }); + cy.findByRole('heading', { name: 'Appen for test av app frontend' }).should('be.visible'); + cy.get('[id^="party-"]').should('not.exist'); }); [true, false].forEach((doNotPromptForParty) => { @@ -186,7 +183,11 @@ describe('Party selection', () => { (req) => { req.on('response', (res) => { const parties = res.body as IParty[]; - correctParty = parties[0]; // parties.find((party: IParty) => party.partyId == partyId); + if (Cypress.env('type') === 'localtest') { + correctParty = parties.find((party) => party.name === cyUserCredentials.default.displayName); + } else { + correctParty = parties.find((party) => party.name === Tenor.users.humanAndrefiolin.name.toUpperCase()); + } if (!correctParty) { throw new Error(`No parties returned from api`); } @@ -212,7 +213,10 @@ describe('Party selection', () => { }, ); - cy.startAppInstance(appFrontend.apps.frontendTest, { cyUser: 'default' }); + cy.startAppInstance(appFrontend.apps.frontendTest, { + cyUser: 'default', + tenorUser: Tenor.users.humanAndrefiolin, + }); cy.get(appFrontend.appHeader).should('be.visible'); cy.get('[id^="party-"]').should('not.exist'); @@ -230,7 +234,10 @@ describe('Party selection', () => { appPromptForPartyOverride, allowedToInstantiate: (parties) => [...parties, CyPartyMocks.ExamplePerson1], }); - cy.startAppInstance(appFrontend.apps.frontendTest, { cyUser: 'default' }); + cy.startAppInstance(appFrontend.apps.frontendTest, { + cyUser: 'default', + tenorUser: Tenor.users.humanAndrefiolin, + }); if (appPromptForPartyOverride === 'always') { cy.get(appFrontend.partySelection.appHeader).should('be.visible'); @@ -242,7 +249,7 @@ describe('Party selection', () => { .should('contain.text', 'Denne appen er satt opp til å alltid spørre om aktør.'); cy.findByRole('heading', { name: 'Appen for test av app frontend' }).should('not.exist'); - cy.get('[id^="party-"]').eq(0).click(); + clickValidParty('default'); } cy.get(appFrontend.appHeader).should('be.visible'); @@ -254,18 +261,29 @@ describe('Party selection', () => { it('Should be possible to select another party if instantiation fails, and go back to party selection and instantiate again', () => { cy.allowFailureOnEnd(); - const user = cyUserCredentials.accountant.firstName; // Pin a specific org by number instead of taking the first one — the tt02 party list order is // not stable, and an org the user *can* instantiate for may otherwise end up first. cyMockResponses({ - allowedToInstantiate: (parties) => + allowedToInstantiate: (parties) => { + const userToKeep = + Cypress.env('type') === 'localtest' + ? cyUserCredentials.accountant.displayName + : Tenor.users.humanAndrefiolin.name.toUpperCase(); + const orgToKeep = + Cypress.env('type') === 'localtest' + ? NonInstantiableOrgForAccountant.DdgFitness + : NonInstantiableOrgForAccountant.StabilSmulApe; // Removing all other users as well, since one of the users are not allowed to instantiate on tt02 - removeAllButKeepOrg(parties, NonInstantiableOrgForAccountant.AldrendeOppstemtTigerAS).filter( - (party) => party.orgNumber || party.name.includes(user), - ), + return removeAllButKeepOrg(parties, orgToKeep).filter( + (party) => party.orgNumber || party.name.includes(userToKeep), + ); + }, doNotPromptForParty: false, }); - cy.startAppInstance(appFrontend.apps.frontendTest, { cyUser: 'accountant' }); + cy.startAppInstance(appFrontend.apps.frontendTest, { + cyUser: 'accountant', + tenorUser: Tenor.users.humanAndrefiolin, + }); // Select the organisation. This is not allowed to instantiate in this app, so it will throw an error. cy.findAllByText(/org\.nr\. \d+/) @@ -282,10 +300,7 @@ describe('Party selection', () => { // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); - // The person on the other hand is allowed to instantiate - cy.findAllByText(/personnr\. \d+/) - .first() - .click(); + clickValidParty('accountant'); cy.findByRole('heading', { name: 'Appen for test av app frontend' }).should('be.visible'); // To make sure this instance is different from the next, we navigate to the next process step in this one @@ -308,3 +323,11 @@ describe('Party selection', () => { cy.findByRole('heading', { name: 'Appen for test av app frontend' }).should('be.visible'); }); }); + +function clickValidParty(cyUser: CyUser) { + if (Cypress.env('type') === 'localtest') { + cy.findByText(cyUserCredentials[cyUser].displayName).click(); + } else { + cy.findByText(Tenor.users.humanAndrefiolin.name.toUpperCase()).click(); + } +} diff --git a/test/e2e/integration/frontend-test/pdf.ts b/test/e2e/integration/frontend-test/pdf.ts index 47c2929c0c..ea853c1cd5 100644 --- a/test/e2e/integration/frontend-test/pdf.ts +++ b/test/e2e/integration/frontend-test/pdf.ts @@ -459,7 +459,7 @@ describe('PDF', () => { // Used to cause a crash, @see https://github.com/Altinn/app-frontend-react/pull/2019 it('Grid in Group should display correctly', { retries: 0 }, () => { - cy.intercept('GET', '**/layouts/**', (req) => { + cy.intercept('GET', '**/layouts/changename', (req) => { req.on('response', (res) => { const body: ILayoutCollection = JSON.parse(res.body); res.send({ diff --git a/test/e2e/integration/frontend-test/prefill.ts b/test/e2e/integration/frontend-test/prefill.ts index c1081e414e..1c8255be98 100644 --- a/test/e2e/integration/frontend-test/prefill.ts +++ b/test/e2e/integration/frontend-test/prefill.ts @@ -1,10 +1,15 @@ import { AppFrontend } from 'test/e2e/pageobjects/app-frontend'; +import { Tenor } from 'test/e2e/support/users'; const appFrontend = new AppFrontend(); describe('Prefill', () => { it('Check Prefill from register and readonly input', () => { - const userFullName = Cypress.env('defaultFullName'); + const userFullName = + Cypress.env('type') === 'localtest' + ? Cypress.env('defaultFullName') + : Tenor.users.saligBlomsterplante.reverseName.toUpperCase(); + cy.goto('changename'); cy.get(appFrontend.changeOfName.currentName).then((name) => { cy.wrap(name).and('have.value', userFullName).and('have.attr', 'readonly'); diff --git a/test/e2e/integration/frontend-test/print-button.ts b/test/e2e/integration/frontend-test/print-button.ts deleted file mode 100644 index ba6d68e8de..0000000000 --- a/test/e2e/integration/frontend-test/print-button.ts +++ /dev/null @@ -1,15 +0,0 @@ -describe('Print button', () => { - it('check that print button is present, and window.print is called', () => { - cy.goto('changename'); - - cy.window().then((win) => { - const printStub = cy.stub(win, 'print'); - // eslint-disable-next-line cypress/unsafe-to-chain-command - cy.contains('button', 'Print / Lagre PDF') - .click() - .then(() => { - expect(printStub).to.be.called; - }); - }); - }); -}); diff --git a/test/e2e/integration/frontend-test/self-identified-user.ts b/test/e2e/integration/frontend-test/self-identified-user.ts index ce623b2054..1e108eb86f 100644 --- a/test/e2e/integration/frontend-test/self-identified-user.ts +++ b/test/e2e/integration/frontend-test/self-identified-user.ts @@ -2,16 +2,20 @@ import { CyHttpMessages } from 'cypress/types/net-stubbing'; import { AppFrontend } from 'test/e2e/pageobjects/app-frontend'; import IncomingHttpResponse = CyHttpMessages.IncomingHttpResponse; +import { Tenor } from 'test/e2e/support/users'; + import type { IncomingApplicationMetadata } from 'src/features/applicationMetadata/types'; const appFrontend = new AppFrontend(); +// TODO: We should enable these tests again when we find out how to log in to tt02 with a self-identified user. It broke when sunsetting A2. + describe('Self identified user', () => { - it('should be able to log in and create an instance', () => { + it.skip('should be able to log in and create an instance', () => { testSelfIdentifiedUser(); }); - it('should be able to log in and create an instance when only persons are allowed', () => { + it.skip('should be able to log in and create an instance when only persons are allowed', () => { cy.intercept('GET', '**/api/v1/applicationmetadata', (req) => { req.on('response', (res: IncomingHttpResponse) => { res.body.partyTypesAllowed = { @@ -34,5 +38,6 @@ function testSelfIdentifiedUser() { cy.findByRole('link', { name: /tilbake til innboks/i }).should('be.visible'); cy.findByRole('heading', { name: /Appen for test av app frontend/i }).should('exist'); - cy.assertUser('selfIdentified'); + // TODO: We probably have to do something else here to make the Tenor/tt02 variant work + cy.assertUser('selfIdentified', Tenor.users.dypsindigLoddsnor); } diff --git a/test/e2e/integration/signering-brukerstyrt/signing.ts b/test/e2e/integration/signering-brukerstyrt/signing.ts index f863a4c4d7..c5b58fa695 100644 --- a/test/e2e/integration/signering-brukerstyrt/signing.ts +++ b/test/e2e/integration/signering-brukerstyrt/signing.ts @@ -1,40 +1,17 @@ import { AppFrontend } from 'test/e2e/pageobjects/app-frontend'; -import { type TenorOrg, type TenorUser } from 'test/e2e/support/auth'; +import { Tenor } from 'test/e2e/support/users'; import { reverseName } from 'test/e2e/support/utils'; const appFrontend = new AppFrontend(); -const sivilisertAvansertIsbjoernSA: TenorOrg = { - name: 'Sivilisert Avansert Isbjørn SA', - orgNr: '312405091', -}; - -const tenorUsers: Record = { - humanAndrefiolin: { - name: 'Human Andrefiolin', - ssn: '09876298713', - role: 'CEO', - }, - varsomDiameter: { - name: 'Varsom Diameter', - ssn: '03835698199', - role: 'Chairman', - }, - standhaftigBjornunge: { - name: 'Standhaftig Bjørnunge', - ssn: '23849199013', - }, - snaalDugnad: { - name: 'Snål Dugnad', - ssn: '10928198958', - }, -} as const; - describe('Signing', () => { it('should allow signing by a specified signee and on behalf of a company', () => { + cy.preventPartySelection(); + // Step 1: Log in as the initial user cy.startAppInstance(appFrontend.apps.signeringBrukerstyrt, { - tenorUser: tenorUsers.humanAndrefiolin, + cyUser: null, + tenorUser: Tenor.users.humanAndrefiolin, authenticationLevel: '2', }); @@ -49,12 +26,12 @@ describe('Signing', () => { // Person: Human Andrefiolin cy.findByRole('button', { name: /legg til person/i }).click(); - cy.findByRole('textbox', { name: /fødselsnummer/i }).type(tenorUsers.humanAndrefiolin.ssn); - cy.findByRole('textbox', { name: /navn/i }).type(tenorUsers.humanAndrefiolin.name.split(' ')[1]); + cy.findByRole('textbox', { name: /fødselsnummer/i }).type(Tenor.users.humanAndrefiolin.ssn); + cy.findByRole('textbox', { name: /navn/i }).type(Tenor.users.humanAndrefiolin.name.split(' ')[1]); cy.findByRole('button', { name: /hent opplysninger/i }).click(); cy.waitUntilSaved(); - cy.findByRole('textbox', { name: /navn/i }).should('have.value', tenorUsers.humanAndrefiolin.name.toUpperCase()); + cy.findByRole('textbox', { name: /navn/i }).should('have.value', Tenor.users.humanAndrefiolin.name.toUpperCase()); cy.findByRole('textbox', { name: /adresse/i }).type('Testveien 1'); cy.findByRole('textbox', { name: /postnr/i }).type('0244'); @@ -65,14 +42,14 @@ describe('Signing', () => { //Person: Standhaftig Bjørnunge cy.findByRole('button', { name: /legg til person/i }).click(); - cy.findByRole('textbox', { name: /fødselsnummer/i }).type(tenorUsers.standhaftigBjornunge.ssn); - cy.findByRole('textbox', { name: /navn/i }).type(tenorUsers.standhaftigBjornunge.name.split(' ')[1]); + cy.findByRole('textbox', { name: /fødselsnummer/i }).type(Tenor.users.standhaftigBjornunge.ssn); + cy.findByRole('textbox', { name: /navn/i }).type(Tenor.users.standhaftigBjornunge.name.split(' ')[1]); cy.findByRole('button', { name: /hent opplysninger/i }).click(); cy.waitUntilSaved(); cy.findByRole('textbox', { name: /navn/i }).should( 'have.value', - tenorUsers.standhaftigBjornunge.name.toUpperCase(), + Tenor.users.standhaftigBjornunge.name.toUpperCase(), ); cy.findByRole('textbox', { name: /adresse/i }).type('Testveien 2'); @@ -84,7 +61,7 @@ describe('Signing', () => { // Virksomhet: Sivilisert Avansert Isbjørn SA cy.findByRole('button', { name: /legg til virksomhet/i }).click(); - cy.findByRole('textbox', { name: /organisasjonsnummer/i }).type(sivilisertAvansertIsbjoernSA.orgNr); + cy.findByRole('textbox', { name: /organisasjonsnummer/i }).type(Tenor.orgs.sivilisertAvansertIsbjoernSA.orgNr); cy.findByRole('button', { name: /hent opplysninger/i }).click(); cy.findByTestId('group-edit-container').within(() => { cy.findByRole('button', { name: /lagre og lukk/i }).click(); @@ -98,8 +75,8 @@ describe('Signing', () => { cy.findByRole('button', { name: /neste/i }).click(); // Styre - cy.findByRole('textbox', { name: /fødselsnummer/i }).type(tenorUsers.varsomDiameter.ssn); - cy.findByRole('textbox', { name: /etternavn/i }).type(tenorUsers.varsomDiameter.name.split(' ')[1]); + cy.findByRole('textbox', { name: /fødselsnummer/i }).type(Tenor.users.varsomDiameter.ssn); + cy.findByRole('textbox', { name: /etternavn/i }).type(Tenor.users.varsomDiameter.name.split(' ')[1]); cy.findByRole('button', { name: /hent opplysninger/i }).click(); cy.findByRole('radio', { @@ -115,17 +92,20 @@ describe('Signing', () => { name: /personer som skal signere personer som skal signere beskrivelse/i, }).within(() => { cy.findByRole('row', { - name: new RegExp(`${sivilisertAvansertIsbjoernSA.name} (venter på signering|varsling mislyktes)`, 'i'), + name: new RegExp( + `${Tenor.orgs.sivilisertAvansertIsbjoernSA.name} (venter på signering|varsling mislyktes)`, + 'i', + ), }); cy.findByRole('row', { name: new RegExp( - `(${tenorUsers.humanAndrefiolin.name}|${reverseName(tenorUsers.humanAndrefiolin.name)}) (venter på signering|varsling mislyktes)`, + `(${Tenor.users.humanAndrefiolin.name}|${reverseName(Tenor.users.humanAndrefiolin.name)}) (venter på signering|varsling mislyktes)`, 'i', ), }); cy.findByRole('row', { name: new RegExp( - `(${tenorUsers.standhaftigBjornunge.name}|${reverseName(tenorUsers.standhaftigBjornunge.name)}) (venter på signering|varsling mislyktes)`, + `(${Tenor.users.standhaftigBjornunge.name}|${reverseName(Tenor.users.standhaftigBjornunge.name)}) (venter på signering|varsling mislyktes)`, 'i', ), }); @@ -147,7 +127,7 @@ describe('Signing', () => { }).within(() => { cy.findByRole('row', { name: new RegExp( - `(${tenorUsers.humanAndrefiolin.name}|${reverseName(tenorUsers.humanAndrefiolin.name)})`, + `(${Tenor.users.humanAndrefiolin.name}|${reverseName(Tenor.users.humanAndrefiolin.name)})`, 'i', ), }).within(() => { @@ -155,7 +135,7 @@ describe('Signing', () => { }); }); - cy.findByText(new RegExp(`du signerer på vegne av ${sivilisertAvansertIsbjoernSA.name}`, 'i')); + cy.findByText(new RegExp(`du signerer på vegne av ${Tenor.orgs.sivilisertAvansertIsbjoernSA.name}`, 'i')); cy.findByRole('checkbox', { name: /jeg bekrefter at informasjonen og dokumentene er korrekte/i }).click(); cy.findByRole('button', { name: 'Signer' }).click(); @@ -165,7 +145,7 @@ describe('Signing', () => { cy.hash().then((hash) => { cy.startAppInstance(appFrontend.apps.signeringBrukerstyrt, { - tenorUser: tenorUsers.humanAndrefiolin, + tenorUser: Tenor.users.humanAndrefiolin, authenticationLevel: '2', urlSuffix: `/${hash}`, }); diff --git a/test/e2e/integration/stateless-app/party-selection.ts b/test/e2e/integration/stateless-app/party-selection.ts index bf763967a7..71a25c8564 100644 --- a/test/e2e/integration/stateless-app/party-selection.ts +++ b/test/e2e/integration/stateless-app/party-selection.ts @@ -1,12 +1,17 @@ import { AppFrontend } from 'test/e2e/pageobjects/app-frontend'; import { cyMockResponses, removeAllButOneOrg } from 'test/e2e/pageobjects/party-mocks'; import { cyUserCredentials } from 'test/e2e/support/auth'; +import { Tenor } from 'test/e2e/support/users'; const appFrontend = new AppFrontend(); describe('Stateless party selection', () => { it('should show party selection before starting instance', () => { - const user = cyUserCredentials.accountant.firstName; + const user = + Cypress.env('type') === 'localtest' + ? cyUserCredentials.accountant.firstName + : Tenor.users.humanAndrefiolin.name.toUpperCase(); + cyMockResponses({ partyTypesAllowed: { person: true, @@ -20,7 +25,7 @@ describe('Stateless party selection', () => { doNotPromptForParty: false, }); - cy.startAppInstance(appFrontend.apps.stateless, { cyUser: 'accountant' }); + cy.startAppInstance(appFrontend.apps.stateless, { cyUser: 'accountant', tenorUser: Tenor.users.humanAndrefiolin }); cy.get(appFrontend.partySelection.appHeader).should('be.visible'); cy.findByText(/ikke bli spurt om aktør hver gang/).should('be.visible'); diff --git a/test/e2e/integration/stateless-app/receipt.ts b/test/e2e/integration/stateless-app/receipt.ts index 12a18bc4cc..76420f5c94 100644 --- a/test/e2e/integration/stateless-app/receipt.ts +++ b/test/e2e/integration/stateless-app/receipt.ts @@ -1,5 +1,6 @@ import texts from 'test/e2e/fixtures/texts.json'; import { AppFrontend } from 'test/e2e/pageobjects/app-frontend'; +import { Tenor } from 'test/e2e/support/users'; const appFrontend = new AppFrontend(); @@ -31,7 +32,10 @@ describe('Receipt', () => { cy.get(appFrontend.feedback).should('contain.text', 'Firmanavn: Foo bar AS'); cy.get(appFrontend.feedback).should('contain.text', 'Org.nr: 12345678901'); - const userFirstName = Cypress.env('defaultFirstName'); + const userFirstName = + Cypress.env('type') === 'localtest' + ? Cypress.env('defaultFirstName') + : Tenor.users.saligBlomsterplante.firstName.toUpperCase(); cy.get(appFrontend.feedback).should('contain.text', `Navn: ${userFirstName}`); cy.get(appFrontend.feedback).should('contain.text', 'ID: 1364'); diff --git a/test/e2e/integration/stateless-app/stateless.ts b/test/e2e/integration/stateless-app/stateless.ts index 781cd13d09..1c54c48717 100644 --- a/test/e2e/integration/stateless-app/stateless.ts +++ b/test/e2e/integration/stateless-app/stateless.ts @@ -1,5 +1,6 @@ import texts from 'test/e2e/fixtures/texts.json'; import { AppFrontend } from 'test/e2e/pageobjects/app-frontend'; +import { Tenor } from 'test/e2e/support/users'; const appFrontend = new AppFrontend(); @@ -42,7 +43,11 @@ describe('Stateless', () => { }); it('is possible to start app instance from stateless app', () => { - const userFirstName = Cypress.env('defaultFirstName'); + const userFirstName = + Cypress.env('type') === 'localtest' + ? Cypress.env('defaultFirstName') + : Tenor.users.saligBlomsterplante.firstName.toUpperCase(); + cy.startStatefulFromStateless(); cy.findByRole('textbox', { name: /navn/i }).should('have.value', userFirstName); cy.findByRole('textbox', { name: /id/i }).should('have.value', '1364'); diff --git a/test/e2e/pageobjects/app-frontend.ts b/test/e2e/pageobjects/app-frontend.ts index 22ad96db77..d6d7df8f67 100644 --- a/test/e2e/pageobjects/app-frontend.ts +++ b/test/e2e/pageobjects/app-frontend.ts @@ -35,7 +35,7 @@ export class AppFrontend { /** @see https://dev.altinn.studio/repos/ttd/stateless-app */ stateless: 'stateless-app', - /** @see https://dev.altinn.studio/repos/ttd/subform-test */ + /** @see https://altinn.studio/repos/ttd/subform-test */ subformTest: 'subform-test', }; diff --git a/test/e2e/pageobjects/party-mocks.ts b/test/e2e/pageobjects/party-mocks.ts index e9db896bba..6d5c9cb3a8 100644 --- a/test/e2e/pageobjects/party-mocks.ts +++ b/test/e2e/pageobjects/party-mocks.ts @@ -188,7 +188,7 @@ export function removeAllButKeepOrg(parties: IParty[], orgNumber: string): IPart // present, e.g. in docker/podman/localtest environments that have entirely different test data. const org = parties.find((party) => party.partyTypeName === PartyType.Organisation && party.orgNumber === orgNumber); if (!org) { - return removeAllButOneOrg(parties); + throw new Error(`Could not find organisation with orgNumber ${orgNumber} in party list`); } const persons = parties.filter((party) => party.partyTypeName === PartyType.Person); return [org, ...persons]; diff --git a/test/e2e/support/apps/signing-test/signing-login.ts b/test/e2e/support/apps/signing-test/signing-login.ts index 642a058f23..a3e3c5eec5 100644 --- a/test/e2e/support/apps/signing-test/signing-login.ts +++ b/test/e2e/support/apps/signing-test/signing-login.ts @@ -1,71 +1,33 @@ import { AppFrontend } from 'test/e2e/pageobjects/app-frontend'; -import type { CyUser } from 'test/e2e/support/auth'; +import { Tenor } from 'test/e2e/support/users'; +import type { TenorUser } from 'test/e2e/support/users'; -import type { IParty } from 'src/types/shared'; +type User = 'manager' | 'accountant' | 'auditor'; -export function signingTestLogin(user: CyUser) { +const tenorUserMapping: Record = { + manager: Tenor.users.raffinertFilm, + accountant: Tenor.users.beskjedenGitar, + auditor: Tenor.users.dypsindigLoddsnor, +}; + +export function signingTestLogin(user: User) { const appFrontend = new AppFrontend(); cy.waitUntilSaved(); cy.url().then((url) => { const instanceSuffix = new URL(url).hash; - const partyId = Cypress.env('signingPartyId'); - - if (partyId) { - // Intercepting party list to only return the party we want to use. This will be automatically used by - // app-frontend when it starts. - let correctParty: IParty | undefined = undefined; + cy.log('Instance suffix:', instanceSuffix || 'none'); - // The /parties request and /current request happen in parallel, so we need - // to await the first request in order to use its value in the second intercept. - let resolveParties: () => void; - const partiesPromise = new Promise((res) => { - resolveParties = res; - }); - - cy.intercept( - { - method: 'GET', - url: `**/api/v1/parties?allowedtoinstantiatefilter=true`, - times: 1, - }, - (req) => { - req.on('response', (res) => { - const parties = res.body as IParty[]; - correctParty = parties.find((party: IParty) => party.partyId == partyId); - if (!correctParty) { - throw new Error(`Could not find party with id ${partyId}`); - } - res.send([correctParty]); - resolveParties(); - }); - }, - ); - cy.intercept( - { - method: 'GET', - url: `**/api/authorization/parties/current?returnPartyObject=true`, - times: 1, - }, - (req) => { - req.on('response', async (res) => { - await partiesPromise; - if (!correctParty) { - throw new Error(`Could not find party with id ${partyId}`); - } - res.send(correctParty); - }); - }, - ); - } + const tenorUser = tenorUserMapping[user]; - cy.startAppInstance(appFrontend.apps.signingTest, { cyUser: user, urlSuffix: instanceSuffix }); + cy.startAppInstance(appFrontend.apps.signingTest, { cyUser: user, urlSuffix: instanceSuffix, tenorUser }); - if (Cypress.env('type') === 'production-like' && instanceSuffix) { - // We need to reload after re-logging in on tt02 when we already had a session, because the startAppInstance - // command will not reload the page if we already are visiting the correct URL. - cy.reloadAndWait(); + if (!instanceSuffix && Cypress.env('type') !== 'localtest') { + const org = Tenor.orgs.overflodigSlemTigerAS; + cy.findByText('Hvem vil du sende inn for?').should('be.visible'); + cy.findByRole('textbox', { name: 'Søk etter aktør' }).type(org.name); + cy.findByRole('button', { name: new RegExp(`org.nr. ${org.orgNr}`) }).click(); } - cy.assertUser(user); + cy.assertUser(user, tenorUser); }); } diff --git a/test/e2e/support/auth.ts b/test/e2e/support/auth.ts index f0b2a2f437..38729af801 100644 --- a/test/e2e/support/auth.ts +++ b/test/e2e/support/auth.ts @@ -1,6 +1,7 @@ import type { CyHttpMessages, RouteHandler } from 'cypress/types/net-stubbing'; -import type { IncomingApplicationMetadata } from 'src/features/applicationMetadata/types'; +import type { TenorLoginParams, TenorUser } from 'test/e2e/support/users'; + import type { IProcess, ITask } from 'src/types/shared'; export type CyUser = 'default' | 'manager' | 'accountant' | 'auditor' | 'selfIdentified'; @@ -54,10 +55,30 @@ export const cyUserCredentials: { [K in CyUser]: UserInfo } = { export const getDisplayName = (user: CyUser) => cyUserCredentials[user].displayName; export const getLocalPartyId = (user: CyUser) => cyUserCredentials[user].localPartyId; -Cypress.Commands.add('assertUser', (user: CyUser) => { - cy.get('[data-testid=AppHeader]').should('contain.text', getDisplayName(user)); +Cypress.Commands.add('assertUser', (user: CyUser, tenorUser: TenorUser) => { + if (Cypress.env('type') === 'localtest') { + cy.get('[data-testid=AppHeader]').should('contain.text', getDisplayName(user)); + } else { + cy.get('[data-testid=AppHeader]').should('contain.text', tenorUser.reverseName.toUpperCase()); + } }); +const emptyPageHtml = ` +

Empty page loaded, proceeding to app

+ +`; + type MinimalTask = Pick; function getPermissions(format: string): MinimalTask { const permissions: MinimalTask = { @@ -129,7 +150,7 @@ export function cyUserLogin({ cyUser, authenticationLevel }: CyUserLoginParams) return loginSelfIdentifiedTt02Login(userName, userPassword); } - return cyUserTt02Login(userName, userPassword); + throw new Error(`Login not implemented for user: ${cyUser}`); } type LocalLoginParams = @@ -174,7 +195,7 @@ function localLogin({ authenticationLevel, ...rest }: LocalLoginParams) { cy.intercept({ method: 'POST', url: '/Home/LogInTestUser', times: 1 }, (req) => { req.on('response', (res) => { expect(res.statusCode).to.eq(302); - res.send(200, '

Empty page loaded, proceeding to app

'); + res.send(200, emptyPageHtml); }); }).as('login'); @@ -183,6 +204,7 @@ function localLogin({ authenticationLevel, ...rest }: LocalLoginParams) { } function loginSelfIdentifiedTt02Login(user: string, pwd: string) { + // TODO: This does not work after A2 sunset, we'll need to find another way to login with a self-identified user in tests const loginUrl = 'https://tt02.altinn.no/ui/Authentication/SelfIdentified'; cy.visit(loginUrl); cy.findByRole('textbox', { name: /Brukernavn/i }).type(user); @@ -197,7 +219,7 @@ function loginSelfIdentifiedTt02Login(user: string, pwd: string) { (req) => { req.on('response', (res) => { expect(res.statusCode).to.eq(302); - res.send(200, '

Empty page loaded, proceeding to app

'); + res.send(200, emptyPageHtml); }); }, ).as('login'); @@ -206,28 +228,6 @@ function loginSelfIdentifiedTt02Login(user: string, pwd: string) { cy.findByRole('heading', { name: 'Empty page loaded, proceeding to app' }).should('exist'); } -function cyUserTt02Login(user: string, pwd: string) { - cy.request({ - method: 'POST', - url: `${Cypress.config('baseUrl')}/api/authentication/authenticatewithpassword`, - headers: { - 'Content-Type': 'application/hal+json', - }, - body: JSON.stringify({ - UserName: user, - UserPassword: pwd, - }), - }).as('login'); - waitForLogin(); -} - -function waitForLogin() { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - cy.get('@login').should((r: any) => { - expect(r?.response?.statusCode ?? r?.status).to.eq(200); - }); -} - /************************ * * TENOR AUTHENTICATION @@ -238,36 +238,10 @@ function waitForLogin() { * ************************/ -export type TenorOrg = { - name: string; - orgNr: string; -}; - -export type TenorUser = { - name: string; - ssn: string; - role?: string; - orgs?: string[]; -}; - export type AppResponseRef = { current: ((res: CyHttpMessages.IncomingHttpResponse) => void) | undefined }; -type TenorLoginParams = { - appName: string; - tenorUser: TenorUser; - authenticationLevel: string; -}; - export function tenorUserLogin(props: TenorLoginParams) { cy.log(`Logging in as Tenor user: ${props.tenorUser.name}`); - cy.intercept('**/api/v1/applicationmetadata', (req) => { - req.reply((res) => { - const body = res.body as IncomingApplicationMetadata; - - res.headers['cache-control'] = 'no-store'; - body.promptForParty = 'never'; - }); - }); if (Cypress.env('type') === 'localtest') { return localLogin({ displayName: props.tenorUser.name, ...props }); @@ -277,11 +251,22 @@ export function tenorUserLogin(props: TenorLoginParams) { } function tenorTt02Login({ appName, tenorUser }: Omit) { - cy.visit(`https://ttd.apps.${Cypress.config('baseUrl')?.slice(8)}/ttd/${appName}`); - - cy.findByRole('link', { name: /testid lag din egen testbruker/i }).click(); - cy.findByRole('textbox', { name: /personidentifikator \(syntetisk\)/i }).clear(); - cy.findByRole('textbox', { name: /personidentifikator \(syntetisk\)/i }).type(tenorUser.ssn); + // This page was made to have an endpoint serving text/html for Cypress to set the correct origin before logging in + // via Tenor (as that happens on another origin). If we just visited the app directly, Cypress would notice the + // redirect and think the login page was the app itself, and some things would break (like accessing window.Cypress). + const appOrigin = `https://ttd.apps.${Cypress.config('baseUrl')?.slice(8)}`; + const appUrl = `${appOrigin}/ttd/${appName}`; + cy.visit(`${appUrl}/login.html`); + cy.location('origin').should('eq', appOrigin); + cy.get('h2').should('have.text', 'Placeholder page for Cypress to set origin before logging in via Tenor'); + cy.get('a').click(); + + cy.origin('https://login.test.idporten.no', () => { + cy.get('a[href="/authorize/testid1"]').click(); + }); + cy.origin('https://testid.test.idporten.no', { args: tenorUser }, (tenorUser) => { + cy.get('input[name=pid]').type(tenorUser.ssn); + }); cy.get('@appResponse').then((ref) => { ref.current = (res) => { @@ -315,10 +300,13 @@ function tenorTt02Login({ appName, tenorUser }: OmitEmpty page loaded, proceeding to app'); + res.send(200, emptyPageHtml); }; }); - cy.findByRole('button', { name: /autentiser/i }).click(); + cy.origin('https://testid.test.idporten.no', () => { + cy.get('button[type=submit]').click(); + }); + cy.findByRole('heading', { name: 'Empty page loaded, proceeding to app' }).should('exist'); } diff --git a/test/e2e/support/custom.ts b/test/e2e/support/custom.ts index a939937382..71cd097d21 100644 --- a/test/e2e/support/custom.ts +++ b/test/e2e/support/custom.ts @@ -11,6 +11,7 @@ import type { ResponseFuzzing, Size, SnapshotOptions, SnapshotViewport } from 't import { breakpoints } from 'src/hooks/useDeviceWidths'; import { getInstanceIdRegExp } from 'src/utils/instanceIdRegExp'; +import type { IncomingApplicationMetadata } from 'src/features/applicationMetadata/types'; import type { LayoutContextValue } from 'src/features/form/layout/LayoutsContext'; import type { IFeatureToggles } from 'src/features/toggles'; import type { ILayoutFile } from 'src/layout/common.generated'; @@ -642,6 +643,12 @@ Cypress.Commands.add('directSnapshot', (snapshotName, { width, minHeight }, rese } }); +/** + * After the navigation rewrite where we now add the current task ID to the URL, this test is only realistic if + * we remove the task and page from the URL before rendering the PDF. This is because the real PDF generator + * won't know about the task and page, and will load this URL and assume the app will figure out how to display + * the current task as a PDF. + */ function buildPdfUrl(href: string): string { const regex = getInstanceIdRegExp(); const instanceId = regex.exec(href)?.[1]; @@ -672,7 +679,7 @@ Cypress.Commands.add( cy.getCurrentViewportSize().as('testPdfViewportSize'); // Make sure instantiation is completed before we get the url - cy.location('hash', { log: false }).should('contain', '#/instance/').as('hashBeforePdf'); + cy.location('href', { log: false }).should('contain', '/instance/').as('urlBeforePdf'); // Make sure we blur any selected component before reload to trigger save cy.get('body').click({ log: false }); @@ -689,18 +696,11 @@ Cypress.Commands.add( cy.log('Testing PDF'); - // Build PDF url and visit cy.window({ log: false }).then((win) => { - const visitUrl = buildUrl(win.location.href); - - // Visit this first so that we don't just re-route in the active react app - win.location.href = 'about:blank'; - - // After the navigation rewrite where we now add the current task ID to the URL, this test is only realistic if - // we remove the task and page from the URL before rendering the PDF. This is because the real PDF generator - // won't know about the task and page, and will load this URL and assume the app will figure out how to display - // the current task as a PDF. - cy.visit(visitUrl); + // A regular cy.visit() would not work here, as it would just trigger a hash-change + const url = buildUrl(win.location.href); + cy.visit(`${win.location.protocol}//${win.location.host}${win.location.pathname}/login.html`); + cy.visit(url); }); // Wait for readyForPrint, after this everything should be rendered so using timeout: 0 @@ -753,9 +753,10 @@ Cypress.Commands.add( }); cy.get('body').invoke('css', 'margin', ''); - cy.get('@hashBeforePdf').then((hashBeforePdf) => { + cy.get('@urlBeforePdf').then((urlBeforePdf) => { cy.window().then((win) => { - win.location.hash = hashBeforePdf.toString(); + cy.visit(`${win.location.protocol}//${win.location.host}${win.location.pathname}/login.html`); + cy.visit(urlBeforePdf.toString()); }); }); @@ -1041,3 +1042,12 @@ Cypress.Commands.add('expectPageBreaks', (expectedCount: number) => { Cypress.Commands.add('setFeatureToggle', (toggleName: IFeatureToggles, value: boolean) => { cy.setCookie(`FEATURE_${toggleName}`, value.toString()); }); + +Cypress.Commands.add('preventPartySelection', () => { + cy.intercept('**/api/v1/applicationmetadata', (req) => { + req.reply((res) => { + const body = res.body as IncomingApplicationMetadata; + body.promptForParty = 'never'; + }); + }).as('preventPartySelection'); +}); diff --git a/test/e2e/support/global.ts b/test/e2e/support/global.ts index 264aea543c..9fc03818ef 100644 --- a/test/e2e/support/global.ts +++ b/test/e2e/support/global.ts @@ -2,7 +2,8 @@ import type JQuery from 'cypress/types/jquery'; import type { RouteMatcher } from 'cypress/types/net-stubbing'; import type { ConsoleMessage } from 'cypress-fail-on-console-error'; -import type { CyUser, TenorUser } from 'test/e2e/support/auth'; +import type { CyUser } from 'test/e2e/support/auth'; +import type { TenorUser } from 'test/e2e/support/users'; import type { IFeatureToggles } from 'src/features/toggles'; import type { BackendValidationIssue, BackendValidationIssuesWithSource } from 'src/features/validation'; @@ -10,8 +11,8 @@ import type { ILayoutSets } from 'src/layout/common.generated'; import type { CompExternal, ILayoutCollection, ILayouts } from 'src/layout/layout'; import type { LooseAutocomplete } from 'src/types'; -export type FrontendTestTask = 'message' | 'changename' | 'group' | 'likert' | 'datalist' | 'confirm'; -export type FillableFrontendTasks = Exclude; +export type FrontendTestTask = 'message' | 'changename' | 'group' | 'likert' | 'datalist'; +export type FillableFrontendTasks = Exclude; export type StartAppInstanceOptions = { // User to log in as @@ -22,11 +23,6 @@ export type StartAppInstanceOptions = { authenticationLevel?: '0' | '1' | '2'; - // JavaScript code to evaluate before starting the app instance (evaluates in the browser, in context of the app). - // The code runs inside an async function, and if it ends with a return value, that value will assumed to be a - // URL that the app page should be navigated to. - evaluateBefore?: string; - // You can add a URL suffix if you need, for example to start a specific instance urlSuffix?: string; }; @@ -54,7 +50,7 @@ declare global { /** * Quickly go to a certain task in the app */ - goto(target: FrontendTestTask, options?: StartAppInstanceOptions): Chainable; + goto(target: FrontendTestTask): Chainable; /** * In 'ttd/frontend-test' we're using a pattern of initially hidden pages to expand with new test cases. @@ -96,6 +92,11 @@ declare global { */ waitForLoad(): Chainable; + /** + * Intercept application metadata and prevent party selection + */ + preventPartySelection(): Chainable; + /** * Start an app instance based on the environment selected * @example cy.startAppInstance('appName') @@ -168,7 +169,7 @@ declare global { iframeCustom(): Chainable; - assertUser(user: CyUser): Chainable; + assertUser(user: CyUser, tenorUser: TenorUser): Chainable; interceptPermissions(): Chainable; setPermissions(permissionFormat: string): void; diff --git a/test/e2e/support/navigation.ts b/test/e2e/support/navigation.ts index 2cd00fcfda..fa67998033 100644 --- a/test/e2e/support/navigation.ts +++ b/test/e2e/support/navigation.ts @@ -1,75 +1,19 @@ import { AppFrontend } from 'test/e2e/pageobjects/app-frontend'; -import { getLocalPartyId } from 'test/e2e/support/auth'; -import { getTargetUrl } from 'test/e2e/support/start-app-instance'; -import type { FrontendTestTask, StartAppInstanceOptions } from 'test/e2e/support/global'; +import type { FrontendTestTask } from 'test/e2e/support/global'; const appFrontend = new AppFrontend(); -const taskMapping: { [key in FrontendTestTask]: string | undefined } = { - message: undefined, - changename: 'Task_2', - group: 'Task_3', - likert: 'Task_4', - datalist: 'Task_5', - confirm: undefined, -}; - -interface Context { - baseUrl: string; -} - -interface ExtraOutput { - code: string; - urlSuffix?: string; -} - -type Extras = (context: Context) => ExtraOutput; - -/** - * This function generates javascript code to move through the instance to the desired task as quickly as possible, - * using the gateway that lets you skip directly to a task. - * - * When using the goto() function, this code is injected into the app before the app itself runs, and creates - * the instance and skips to the correct task before loading app-frontend-react. - */ -function generateEvalString(target: FrontendTestTask, extra?: Extras): string { - const baseUrl = getTargetUrl(appFrontend.apps.frontendTest); - const fullPartyId = getLocalPartyId('default'); - - // Hard-coded partyId for now, since I couldn't figure out a way to get it from the app. - // This is the one used on tt02. - const partyId = fullPartyId ? fullPartyId.split('.')[1] : 50085642; - - const instantiateUrl = `${baseUrl}/instances?instanceOwnerPartyId=${partyId}`; - const createInstance = ` - const partyId = ${JSON.stringify(partyId)}; - const xsrfCookie = document.cookie.split('; ').find((row) => row.startsWith('XSRF-TOKEN=')).split('=')[1]; - const headers = { 'Content-Type': 'application/json', 'X-XSRF-TOKEN': xsrfCookie }; - const instanceCreated = await fetch(${JSON.stringify(instantiateUrl)}, { method: 'POST', headers }); - var instance = await instanceCreated.json(); - const instanceId = instance.id; - var instanceData = instance.data; - `; +function initAndGoto(nextTask?: string) { + cy.intercept('**/active', []).as('noActiveInstances'); + cy.startAppInstance(appFrontend.apps.frontendTest); + cy.get('#finishedLoading').should('exist'); - const targetTask = taskMapping[target]; - if (!targetTask) { - throw new Error(`No task mapping for task ${target}. You probably cannot skip to ${target} directly.`); + if (nextTask) { + cy.findByRole('button', { name: 'Velg neste steg' }).click(); + cy.findByRole('radio', { name: nextTask }).click(); + cy.findByRole('button', { name: 'Gå til' }).click(); + cy.get('#finishedLoading').should('exist'); } - - const taskData = { GwTargetTask: targetTask }; - const skipToTask = ` - var taskData = ${JSON.stringify(taskData)}; - var dataId = instanceData.find((data) => data.dataType === 'message').id; - await fetch('${baseUrl}/instances/' + instanceId + '/data/' + dataId, { method: 'PUT', headers, body: JSON.stringify(taskData) }); - await fetch('${baseUrl}/instances/' + instanceId + '/process/next', { method: 'PUT', headers }); - `; - - const extras = extra ? extra({ baseUrl }) : { code: '' }; - const returnInstanceUrl = ` - return '${baseUrl}/#/instance/' + instanceId + '${extras.urlSuffix || ''}'; - `; - - return createInstance + skipToTask + extras.code + returnInstanceUrl; } /** @@ -77,83 +21,28 @@ function generateEvalString(target: FrontendTestTask, extra?: Extras): string { * These should always complete the task fully, i.e. end the task and move to the next one after it. * It never generates a PDF for the previous tasks. */ -const gotoFunctions: { [key in FrontendTestTask]: (extra?: Extras, startOptions?: StartAppInstanceOptions) => void } = { - message: (extra?: Extras, startOptions?: StartAppInstanceOptions) => { - cy.intercept('**/active', []).as('noActiveInstances'); - if (extra) { - throw new Error('Extra not supported for message navigator'); - } - cy.startAppInstance(appFrontend.apps.frontendTest, startOptions); - cy.findByRole('link', { name: /tilbake til innboks/i }).should('be.visible'); - }, - changename: (extra?: Extras, startOptions?: StartAppInstanceOptions) => { - cy.startAppInstance(appFrontend.apps.frontendTest, { - ...startOptions, - evaluateBefore: generateEvalString('changename', extra), - }); - }, - group: (extra?: Extras, startOptions?: StartAppInstanceOptions) => { - if (extra) { - throw new Error('Extra not supported for group navigator'); - } - cy.startAppInstance(appFrontend.apps.frontendTest, { - ...startOptions, - evaluateBefore: generateEvalString('group'), - }); - }, - likert: (extra?: Extras, startOptions?: StartAppInstanceOptions) => { - if (extra) { - throw new Error('Extra not supported for likert navigator'); - } - cy.startAppInstance(appFrontend.apps.frontendTest, { - ...startOptions, - evaluateBefore: generateEvalString('likert'), - }); - }, - datalist: (extra?: Extras, startOptions?: StartAppInstanceOptions) => { - if (extra) { - throw new Error('Extra not supported for datalist navigator'); - } - cy.startAppInstance(appFrontend.apps.frontendTest, { - ...startOptions, - evaluateBefore: generateEvalString('datalist'), - }); - }, - confirm: (extra?: Extras, startOptions?: StartAppInstanceOptions) => { - if (extra) { - throw new Error('Extra not supported for confirm navigator'); - } - cy.startAppInstance(appFrontend.apps.frontendTest, { - ...startOptions, - evaluateBefore: generateEvalString('datalist', ({ baseUrl }) => ({ - code: `await fetch('${baseUrl}/instances/' + instanceId + '/process/next', { method: 'PUT', headers });`, - })), - }); - }, +const gotoFunctions: { [key in FrontendTestTask]: () => void } = { + message: () => initAndGoto(), + changename: () => initAndGoto('Endring av navn (Task_2)'), + group: () => initAndGoto('Repeterende grupper (Task_3)'), + likert: () => initAndGoto('Likert (Task_4)'), + datalist: () => initAndGoto('List (Task_5)'), }; -Cypress.Commands.add('goto', (task, options) => { - gotoFunctions[task](undefined, options); +Cypress.Commands.add('goto', (task) => { + gotoFunctions[task](); cy.findByRole('progressbar').should('not.exist'); }); Cypress.Commands.add('gotoHiddenPage', (target) => { - gotoFunctions.changename(({ baseUrl }) => ({ - urlSuffix: `/Task_2/${target}`, - code: [ - `var instanceDataNew = await fetch('${baseUrl}/instances/' + instanceId, { headers }).then((res) => res.json());`, - `var changeNameModel = instanceDataNew.data.find((data) => data.dataType === 'ServiceModel-test');`, - `var dataModel = await fetch('${baseUrl}/instances/' + instanceId + '/data/' + changeNameModel.id, { headers }).then((res) => res.json());`, - `dataModel['NyttNavn-grp-9313'] = { - 'NyttNavn-grp-9314': { - 'PersonFornavnNytt-datadef-34758': { value: 'a' }, - 'PersonBekrefterNyttNavn': { value: 'Ja' }, - } - }`, - `dataModel.ChooseExtraPages = "${target}";`, - `await fetch('${baseUrl}/instances/' + instanceId + '/data/' + changeNameModel.id, - { method: 'PUT', headers, body: JSON.stringify(dataModel) });`, - ].join('\n'), - })); + gotoFunctions.changename(); cy.findByRole('progressbar').should('not.exist'); + cy.get(appFrontend.changeOfName.newFirstName).type('a'); + cy.findByRole('tab', { name: /nytt etternavn/i }).click(); + cy.get(appFrontend.changeOfName.newLastName).type('a'); + cy.get(appFrontend.changeOfName.confirmChangeName) + .findByRole('checkbox', { name: /Ja[a-z, ]*/ }) + .check(); + cy.get(`[type=checkbox][name=choose-extra][value=${target}]`).check(); + cy.gotoNavPage(target); }); diff --git a/test/e2e/support/start-app-instance.ts b/test/e2e/support/start-app-instance.ts index 8e59d9390f..5909509692 100644 --- a/test/e2e/support/start-app-instance.ts +++ b/test/e2e/support/start-app-instance.ts @@ -2,13 +2,13 @@ import dotenv from 'dotenv'; import escapeRegex from 'escape-string-regexp'; import { cyUserLogin, tenorUserLogin } from 'test/e2e/support/auth'; +import { Tenor } from 'test/e2e/support/users'; import type { AppResponseRef } from 'test/e2e/support/auth'; Cypress.Commands.add('startAppInstance', function (appName, options) { const { cyUser = 'default', - tenorUser = null, - evaluateBefore, + tenorUser = Tenor.users.saligBlomsterplante, urlSuffix = '', authenticationLevel = '1', } = options || {}; @@ -86,7 +86,6 @@ Cypress.Commands.add('startAppInstance', function (appName, options) { // https://docs.percy.io/docs/debugging-sdks#asset-discovery cy.get('@appResponse').then((ref) => { cy.intercept({ url: targetUrl }, (req) => { - const cookies = req.headers['cookie'] || ''; req.on('response', (res) => { if (typeof res.body === 'string' || res.statusCode === 200) { if (ref.current) { @@ -94,11 +93,6 @@ Cypress.Commands.add('startAppInstance', function (appName, options) { return; } - if (evaluateBefore && !cookies.includes('cy-evaluated-js=true')) { - res.body = generateHtmlToEval(evaluateBefore); - return; - } - const source = /https?:\/\/.*?\/altinn-app-frontend\./g; const target = `http://${targetHost}/altinn-app-frontend.`; res.body = res.body.replace(source, target); @@ -125,7 +119,9 @@ Cypress.Commands.add('startAppInstance', function (appName, options) { cy.clearCookies({ domain: 'platform.tt02.altinn.no' }); } - if (tenorUser) { + if (tenorUser && cyUser && Cypress.env('type') === 'localtest') { + cyUserLogin({ cyUser, authenticationLevel }); + } else if (tenorUser) { tenorUserLogin({ appName, tenorUser, authenticationLevel }); } else if (cyUser) { cyUserLogin({ cyUser, authenticationLevel }); @@ -154,31 +150,3 @@ export function getTargetUrl(appName: string) { ? `${Cypress.config('baseUrl')}/ttd/${appName}` : `https://ttd.apps.${Cypress.config('baseUrl')?.slice(8)}/ttd/${appName}`; } - -function generateHtmlToEval(javascript: string) { - return ` - - - Evaluating JavaScript before starting app - - - -
- - - `.trim(); -} diff --git a/test/e2e/support/users.ts b/test/e2e/support/users.ts new file mode 100644 index 0000000000..60df56833d --- /dev/null +++ b/test/e2e/support/users.ts @@ -0,0 +1,109 @@ +export type TenorOrg = { + name: string; + orgNr: string; +}; + +export type TenorRoleType = 'Manager' | 'Chairman' | 'Accountant' | 'Auditor'; + +export type TenorRole = { + type: TenorRoleType; + forOrg: keyof typeof tenorOrgs; +}; + +export type TenorUser = { + firstName: string; + lastName: string; + name: string; + reverseName: string; + ssn: string; + role?: TenorRole; +}; + +export type TenorLoginParams = { + appName: string; + tenorUser: TenorUser; + authenticationLevel: string; +}; + +const tenorOrgs = { + sivilisertAvansertIsbjoernSA: { + name: 'Sivilisert Avansert Isbjørn SA', + orgNr: '312405091', + }, + overflodigSlemTigerAS: { + name: 'Overflødig Slem Tiger AS', + orgNr: '310926833', + }, +} as const; + +function name(first: string, last: string): { name: string; reverseName: string; firstName: string; lastName: string } { + return { name: `${first} ${last}`, reverseName: `${last} ${first}`, firstName: first, lastName: last }; +} + +const tenorUsers = { + saligBlomsterplante: { + ...name('Salig', 'Blomsterplante'), + ssn: '20920448276', + }, + humanAndrefiolin: { + ...name('Human', 'Andrefiolin'), + ssn: '09876298713', + role: { + type: 'Manager', + forOrg: 'sivilisertAvansertIsbjoernSA', + }, + }, + varsomDiameter: { + ...name('Varsom', 'Diameter'), + ssn: '03835698199', + role: { + type: 'Chairman', + forOrg: 'sivilisertAvansertIsbjoernSA', + }, + }, + standhaftigBjornunge: { + ...name('Standhaftig', 'Bjørnunge'), + ssn: '23849199013', + }, + snaalDugnad: { + ...name('Snål', 'Dugnad'), + ssn: '10928198958', + }, + raffinertFilm: { + ...name('Raffinert', 'Film'), + ssn: '28826898781', + role: { + type: 'Manager', + forOrg: 'overflodigSlemTigerAS', + }, + }, + akustiskGaranti: { + ...name('Akustisk', 'Garanti'), + ssn: '04845698703', + role: { + type: 'Chairman', + forOrg: 'overflodigSlemTigerAS', + }, + }, + beskjedenGitar: { + ...name('Beskjeden', 'Gitar'), + ssn: '15893148970', + role: { + type: 'Accountant', + forOrg: 'overflodigSlemTigerAS', + }, + }, + dypsindigLoddsnor: { + ...name('Dypsindig', 'Loddsnor'), + ssn: '12887498871', + role: { + type: 'Auditor', + forOrg: 'overflodigSlemTigerAS', + }, + }, +} as const; + +export const Tenor = { + users: tenorUsers satisfies Record, + orgs: tenorOrgs satisfies Record, +};