diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index b6480ea03e3..3ca8efe7dac 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -763,12 +763,12 @@ "process_editor.configuration_panel.edit_policy_alert_message": "Du må ha tilgangsregler som dekker alle oppgaver. Gå til Tilganger for å sjekke om du har en regel som dekker denne oppgaven. Hvis du ikke har en regel for oppgaven, kan du enten lage en ny regel eller inkludere denne oppgaven i en regel som allerede finnes.", "process_editor.configuration_panel.edit_policy_open_policy_editor_button": "Gå til Tilganger", "process_editor.configuration_panel.edit_policy_open_policy_editor_heading": "Åpne Tilganger for å redigere tilgangsregler", - "process_editor.configuration_panel_actions_action_label": "Handling {{ actionIndex }}: {{ actionName }}", + "process_editor.configuration_panel_actions_action_label": "Handling {{actionIndex}}: {{actionName}}", "process_editor.configuration_panel_actions_action_type_help_text": "Hjelpetekst for valg av handlingstype", "process_editor.configuration_panel_actions_add_new": "Legg til ny handling", "process_editor.configuration_panel_actions_combobox_description": "Velg en predefinert handling eller definer din egen ved å skrive inn navnet som fritekst i feltet", "process_editor.configuration_panel_actions_custom_action": "Skriv en egendefinert handling", - "process_editor.configuration_panel_actions_delete_action": "Slett {{ actionName }}-handlingen", + "process_editor.configuration_panel_actions_delete_action": "Slett {{actionName}}-handlingen", "process_editor.configuration_panel_actions_set_server_action_info": "Handlingen skal utføres uten å endre status på prosessen. Dette alternativet er kun tilgjengelig for egendefinerte handlinger.", "process_editor.configuration_panel_actions_set_server_action_label": "Handlingen skal ikke påvirke prosessen", "process_editor.configuration_panel_actions_title": "Handlinger", @@ -824,7 +824,7 @@ "process_editor.configuration_panel_select_data_model": "Velg en datamodell", "process_editor.configuration_panel_set_data_model": "Datamodell:", "process_editor.configuration_panel_set_data_model_link": "Legg til datamodell", - "process_editor.configuration_panel_set_data_types_to_sign": "Datyper som skal signeres:", + "process_editor.configuration_panel_set_data_types_to_sign": "Datatyper som skal signeres:", "process_editor.configuration_panel_signing_task": "Oppgave: Signering", "process_editor.configuration_view_panel_id_label": "ID:", "process_editor.configuration_view_panel_name_label": "Navn: ", @@ -848,7 +848,7 @@ "process_editor.sync_error_layout_sets_data_type": "En feil oppsto under synkronisering av datatype i filen 'layoutsets.json'. Vennligst forsikre deg om at 'layoutsets.json' kun inneholder gyldig JSON-struktur og prøv igjen.", "process_editor.sync_error_layout_sets_task_id": "En feil oppsto under synkronisering av oppgave-ID i filen 'layoutsets.json'. Vennligst forsikre deg om at 'layoutsets.json' kun inneholder gyldig JSON-struktur og prøv igjen.", "process_editor.sync_error_policy_file_task_id": "En feil oppsto under synkronisering av oppgave-ID i filen 'policy.json'. Vennligst forsikre deg om at 'policy.json' kun inneholder gyldig JSON-struktur og prøv igjen.", - "process_editor.too_old_version_helptext_content": "Du har nå versjon {{ version }} av app-biblioteket vårt.\n\nVi lanserer muligheten til å redigere prosessen sammen med versjon 8 av biblioteket. Når du har oppgradert til versjon 8, får du funksjonalitet for å redigere prosessen.\n\nFør det kan du bare se prosessen og eventuelle oppsett som er knyttet til den.", + "process_editor.too_old_version_helptext_content": "Du har nå versjon {{version}} av app-biblioteket vårt.\n\nVi lanserer muligheten til å redigere prosessen sammen med versjon 8 av biblioteket. Når du har oppgradert til versjon 8, får du funksjonalitet for å redigere prosessen.\n\nFør det kan du bare se prosessen og eventuelle oppsett som er knyttet til den.", "process_editor.too_old_version_helptext_title": "Informasjon om hvorfor prosessen ikke kan redigeres", "process_editor.too_old_version_title": "Prosessen kan ikke redigeres", "process_editor.unknown_heading_error_message": "Obs, noe gikk galt!", diff --git a/frontend/packages/process-editor/src/bpmnProviders/SupportedPaletteProvider.js b/frontend/packages/process-editor/src/bpmnProviders/SupportedPaletteProvider.js index 62c359405a5..2d217ed88a9 100644 --- a/frontend/packages/process-editor/src/bpmnProviders/SupportedPaletteProvider.js +++ b/frontend/packages/process-editor/src/bpmnProviders/SupportedPaletteProvider.js @@ -163,6 +163,7 @@ class SupportedPaletteProvider { className: 'bpmn-icon-task-generic bpmn-icon-data-task', title: translate('Create Altinn Data Task'), action: { + click: createCustomTask('data'), dragstart: createCustomTask('data'), }, }, @@ -171,6 +172,7 @@ class SupportedPaletteProvider { title: translate('Create Altinn Feedback Task'), className: 'bpmn-icon-task-generic bpmn-icon-feedback-task', action: { + click: createCustomTask('feedback'), dragstart: createCustomTask('feedback'), }, }, @@ -179,6 +181,7 @@ class SupportedPaletteProvider { className: 'bpmn-icon-task-generic bpmn-icon-signing-task', title: translate('Create Altinn signing Task'), action: { + click: createCustomSigningTask(), dragstart: createCustomSigningTask(), }, }, @@ -187,6 +190,7 @@ class SupportedPaletteProvider { className: 'bpmn-icon-task-generic bpmn-icon-confirmation-task', title: translate('Create Altinn Confirm Task'), action: { + click: createCustomConfirmationTask(), dragstart: createCustomConfirmationTask(), }, }, @@ -195,6 +199,7 @@ class SupportedPaletteProvider { className: `bpmn-icon-task-generic ${shouldDisplayFeature('displayPaymentTaskProcessEditor') ? 'bpmn-icon-payment-task' : 'payment-is-hidden-based-on-feature-toggle'}`, title: translate('Payment'), action: { + click: createCustomPaymentTask(), dragstart: createCustomPaymentTask(), }, }, diff --git a/frontend/packages/process-editor/src/components/Canvas/BPMNEditor/BPMNEditor.module.css b/frontend/packages/process-editor/src/components/Canvas/BPMNEditor/BPMNEditor.module.css index a439a965053..84394056058 100644 --- a/frontend/packages/process-editor/src/components/Canvas/BPMNEditor/BPMNEditor.module.css +++ b/frontend/packages/process-editor/src/components/Canvas/BPMNEditor/BPMNEditor.module.css @@ -1,14 +1,3 @@ -.container { - visibility: hidden; -} - -.spinner { - display: flex; - flex: 1; - justify-content: center; - align-content: center; -} - .editorContainer { border: 1px solid var(--fds-semantic-border-neutral-default); border-radius: 5px; diff --git a/frontend/packages/process-editor/src/components/Canvas/BPMNEditor/BPMNEditor.test.tsx b/frontend/packages/process-editor/src/components/Canvas/BPMNEditor/BPMNEditor.test.tsx index 3a9a6a3f80d..4d632932795 100644 --- a/frontend/packages/process-editor/src/components/Canvas/BPMNEditor/BPMNEditor.test.tsx +++ b/frontend/packages/process-editor/src/components/Canvas/BPMNEditor/BPMNEditor.test.tsx @@ -13,11 +13,6 @@ jest.mock('../../../hooks/useBpmnEditor', () => ({ describe('BPMNEditor', () => { afterEach(jest.clearAllMocks); - it('render spinner when pendingApiOperations is true', () => { - renderBpmnEditor({ pendingApiOperations: true }); - - screen.getByText(textMock('process_editor.loading')); - }); it('does not render spinner when pendingApiOperations is false', () => { renderBpmnEditor({ pendingApiOperations: false }); diff --git a/frontend/packages/process-editor/src/components/Canvas/BPMNEditor/BPMNEditor.tsx b/frontend/packages/process-editor/src/components/Canvas/BPMNEditor/BPMNEditor.tsx index a93484c9aa1..6a3b9bc2a06 100644 --- a/frontend/packages/process-editor/src/components/Canvas/BPMNEditor/BPMNEditor.tsx +++ b/frontend/packages/process-editor/src/components/Canvas/BPMNEditor/BPMNEditor.tsx @@ -1,28 +1,10 @@ import React from 'react'; import classes from './BPMNEditor.module.css'; import { useBpmnEditor } from '../../../hooks/useBpmnEditor'; - import './BPMNEditor.css'; -import { useBpmnApiContext } from '../../../contexts/BpmnApiContext'; -import { StudioSpinner } from '@studio/components'; -import { useTranslation } from 'react-i18next'; export const BPMNEditor = (): React.ReactElement => { - const { t } = useTranslation(); const { canvasRef } = useBpmnEditor(); - const { pendingApiOperations } = useBpmnApiContext(); - return ( - <> - {pendingApiOperations && ( -
- -
- )} -
- - ); + return
; }; diff --git a/frontend/packages/process-editor/src/components/Canvas/Canvas.tsx b/frontend/packages/process-editor/src/components/Canvas/Canvas.tsx index f78777025eb..b851ba92693 100644 --- a/frontend/packages/process-editor/src/components/Canvas/Canvas.tsx +++ b/frontend/packages/process-editor/src/components/Canvas/Canvas.tsx @@ -11,14 +11,12 @@ import { BPMNEditor } from './BPMNEditor'; import { VersionHelpText } from './VersionHelpText'; export const Canvas = (): React.ReactElement => { - const { isEditAllowed, bpmnXml } = useBpmnContext(); + const { isEditAllowed } = useBpmnContext(); return ( <> {!isEditAllowed && } -
- {isEditAllowed ? : } -
+
{isEditAllowed ? : }
); }; diff --git a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigContent/EditActions/EditActions.tsx b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigContent/EditActions/EditActions.tsx index 83d7ffb1fa7..7364d1e84d5 100644 --- a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigContent/EditActions/EditActions.tsx +++ b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigContent/EditActions/EditActions.tsx @@ -30,7 +30,7 @@ export const EditActions = () => { <> {actionElements.map((actionElement: ModdleElement, index: number) => ( { + const elementSelector = `${type}[data-element-id="${id}"]`; + await this.page.waitForSelector(elementSelector); + return elementSelector; + } +} diff --git a/frontend/testing/playwright/pages/DataModelPage.ts b/frontend/testing/playwright/pages/DataModelPage.ts index 2a49e4c7049..ca30b27f387 100644 --- a/frontend/testing/playwright/pages/DataModelPage.ts +++ b/frontend/testing/playwright/pages/DataModelPage.ts @@ -113,11 +113,10 @@ export class DataModelPage extends BasePage { } public async checkThatSuccessAlertIsVisibleOnScreen(): Promise { - await this.page - .getByRole('alert', { - name: this.textMock('schema_editor.data_model_generation_success_message'), - }) - .isVisible(); + const alert = this.page.getByText( + this.textMock('schema_editor.data_model_generation_success_message'), + ); + await expect(alert).toBeVisible(); } public async checkThatDataModelOptionExists(option: string): Promise { diff --git a/frontend/testing/playwright/pages/GiteaPage.ts b/frontend/testing/playwright/pages/GiteaPage.ts index e9e62a6b687..f18f06b712b 100644 --- a/frontend/testing/playwright/pages/GiteaPage.ts +++ b/frontend/testing/playwright/pages/GiteaPage.ts @@ -2,6 +2,8 @@ import type { LanguageCode } from '../enum/LanguageCode'; import { BasePage } from '../helpers/BasePage'; import type { Environment } from '../helpers/StudioEnvironment'; import type { Page } from '@playwright/test'; +import { type BpmnTaskType } from '../types/BpmnTaskType'; +import { expect } from '@playwright/test'; // Since this page is Gitea's page, it's not using the nb/en.json files, which are used in the frontend. const giteaPageTexts: Record = { @@ -12,6 +14,8 @@ const giteaPageTexts: Record = { dataModelBindings: 'dataModelBindings', config: 'config', texts: 'texts', + process: 'process', + applicationmetadata: 'applicationmetadata', }; export class GiteaPage extends BasePage { @@ -105,4 +109,106 @@ export class GiteaPage extends BasePage { public async verifyTextIdAndValue(id: string, value: string): Promise { await this.page.getByText(`"id": "${id}", "value": "${value}"`, { exact: true }).isVisible(); } + + public async clickOnProcessFilesButton(): Promise { + await this.page.getByRole('link', { name: giteaPageTexts['process'], exact: true }).click(); + } + + public async clickOnProcessBpmnFile(): Promise { + await this.page.getByRole('link', { name: `${giteaPageTexts['process']}.bpmn` }).click(); + } + + public async verifyThatTheNewTaskIsVisible(id: string, task: BpmnTaskType): Promise { + const text = this.page.getByText(``); + await expect(text).toBeVisible(); + } + + public async verifyThatTheNewTaskIsHidden(id: string, task: BpmnTaskType): Promise { + await this.page.getByText(``).isHidden(); + } + + public async verifySequenceFlowDirection(fromId: string, toId: string): Promise { + const firstPartOfText = this.page.getByText('`); + await expect(secondPartOfText).toBeVisible(); + } + + public async clickOnApplicationMetadataFile(): Promise { + await this.page + .getByRole('link', { name: `${giteaPageTexts['applicationmetadata']}.json` }) + .click(); + } + + public async verifyIdInDataModel(id: string, dataModel: string): Promise { + const text = ` + "id": "${dataModel}", + "allowedContentTypes": [ + "application/xml" + ], + "appLogic": { + "autoCreate": true, + "classRef": "Altinn.App.Models.${dataModel}.${dataModel}", + "allowAnonymousOnStateless": false, + "autoDeleteOnProcessEnd": false + }, + "taskId": "${id}", + "maxCount": 1, + "minCount": 1, + "enablePdfCreation": true, + "enableFileScan": false, + "validationErrorOnPendingFileScan": false, + "enabledFileAnalysers": [], + "enabledFileValidators": [] + `; + const textLocator = this.page.getByText(text); + expect(textLocator).toBeVisible(); + } + + public async verifyThatActionIsVisible(action: string): Promise { + await this.page.getByText(`${action}`).isVisible(); + } + + public async verifyThatActionIsHidden(action: string): Promise { + await this.page.getByText(`${action}`).isHidden(); + } + + public async verifyThatTaskIsHidden(task: string): Promise { + await this.page.getByText(`${task}`).isHidden(); + } + + public async verifyThatTaskIsVisible(task: string): Promise { + await this.page.getByText(`${task}`).isVisible(); + } + + public async verifyThatDataTypeToSignIsHidden(dataTypeToSign: string): Promise { + const text = ` + + + ${dataTypeToSign} + + + `; + await this.page.getByText(text).isHidden(); + } + + public async verifyThatDataTypeToSignIsVisible(dataTypeToSign: string): Promise { + const text = ` + + + ${dataTypeToSign} + + + `; + await this.page.getByText(text).isVisible(); + } + + public async verifyThatCustomReceiptIsNotVisible(): Promise { + await this.page.getByText('"taskId": "CustomReceipt"').isHidden(); + } + + public async verifyThatCustomReceiptIsVisible(): Promise { + await this.page.getByText('"taskId": "CustomReceipt"').isVisible(); + } } diff --git a/frontend/testing/playwright/pages/ProcessEditorPage.ts b/frontend/testing/playwright/pages/ProcessEditorPage.ts index 11eb5c5db8a..8c06dacaf3f 100644 --- a/frontend/testing/playwright/pages/ProcessEditorPage.ts +++ b/frontend/testing/playwright/pages/ProcessEditorPage.ts @@ -1,6 +1,10 @@ import { BasePage } from '../helpers/BasePage'; import type { Environment } from '../helpers/StudioEnvironment'; import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; +import { type BpmnTaskType } from '../types/BpmnTaskType'; + +const connectionArrowText: string = 'Connect using Sequence/MessageFlow or Association'; export class ProcessEditorPage extends BasePage { constructor(page: Page, environment?: Environment) { @@ -14,4 +18,471 @@ export class ProcessEditorPage extends BasePage { public async verifyProcessEditorPage(): Promise { await this.page.waitForURL(this.getRoute('editorProcess')); } + + public async clickOnTaskInBpmnEditor(elementSelector: string): Promise { + await this.page.click(elementSelector); + } + + public async waitForInitialTaskHeaderToBeVisible(): Promise { + const heading = this.page.getByRole('heading', { + name: this.textMock('process_editor.configuration_panel_data_task'), + }); + + await expect(heading).toBeVisible(); + } + + public async clickOnDataModelButton(): Promise { + await this.page + .getByRole('button', { + name: this.textMock('process_editor.configuration_panel_set_data_model'), + }) + .click(); + } + + public async waitForDataModelComboboxToBeVisible(): Promise { + const combobox = this.page.getByRole('combobox', { + name: this.textMock('process_editor.configuration_panel_set_data_model'), + }); + await expect(combobox).toBeVisible(); + } + + public async clickOnDeleteDataModel(): Promise { + await this.page + .getByRole('button', { + name: this.textMock('general.delete'), + }) + .click(); + } + + public async waitForAddDataModelButtonToBeVisible(): Promise { + const button = this.page.getByRole('button', { + name: this.textMock('process_editor.configuration_panel_set_data_model_link'), + }); + await expect(button).toBeVisible(); + } + + public async clickOnAddDataModel(): Promise { + await this.page + .getByRole('button', { + name: this.textMock('process_editor.configuration_panel_set_data_model_link'), + }) + .click(); + } + + public async clickOnDataModelCombobox(): Promise { + await this.page + .getByRole('combobox', { + name: this.textMock('process_editor.configuration_panel_set_data_model'), + }) + .click(); + } + + public async clickOnDataModelOption(option: string): Promise { + await this.page.getByRole('option', { name: option }).click(); + } + + public async waitForDataModelButtonToBeVisible(): Promise { + const button = this.page.getByRole('button', { + name: this.textMock('process_editor.configuration_panel_set_data_model'), + }); + await expect(button).toBeVisible(); + } + + public async verifyDataModelButtonTextIsSelectedDataModel(option: string): Promise { + await this.page + .getByRole('button', { + name: this.textMock('process_editor.configuration_panel_set_data_model') + option, + }) + .isVisible(); + } + + public async verifyThatAddNewDataModelButtonIsHidden(): Promise { + await this.page + .getByRole('button', { + name: this.textMock('process_editor.configuration_panel_set_data_model_link'), + }) + .isHidden(); + } + + public async clickOnActionsAccordion(): Promise { + await this.page + .getByRole('button', { + name: this.textMock('process_editor.configuration_panel_actions_title'), + }) + .click(); + } + + public async waitForAddActionsButtonToBeVisible(): Promise { + const button = this.page.getByRole('button', { + name: this.textMock('process_editor.configuration_panel_actions_add_new'), + }); + await expect(button).toBeVisible(); + } + + public async clickAddActionsButton(): Promise { + await this.page + .getByRole('button', { + name: this.textMock('process_editor.configuration_panel_actions_add_new'), + }) + .click(); + } + + public async waitForActionComboboxTitleToBeVisible( + actionIndex: string, + actionName?: string, + ): Promise { + const combobox = this.page.getByRole('combobox', { + name: this.textMock('process_editor.configuration_panel_actions_action_label', { + actionIndex, + actionName: actionName ?? '', + }), + }); + await expect(combobox).toBeVisible(); + } + + public async clickOnActionCombobox(actionIndex: string, actionName?: string): Promise { + await this.page + .getByRole('combobox', { + name: this.textMock('process_editor.configuration_panel_actions_action_label', { + actionIndex, + actionName: actionName ?? '', + }), + }) + .click(); + } + + public async clickOnActionOption(action: string): Promise { + await this.page.getByRole('option', { name: action }).click(); + } + + public async removeFocusFromActionCombobox( + actionIndex: string, + actionName?: string, + ): Promise { + await this.page + .getByRole('combobox', { + name: this.textMock('process_editor.configuration_panel_actions_action_label', { + actionIndex, + actionName: actionName ?? '', + }), + }) + .blur(); + } + + public async clickOnSaveActionButton(): Promise { + await this.page + .getByRole('button', { + name: this.textMock('general.save'), + }) + .click(); + } + + public async waitForActionButtonToBeVisible( + actionIndex: string, + actionName?: string, + ): Promise { + const button = this.page.getByRole('button', { + name: this.textMock('process_editor.configuration_panel_actions_action_label', { + actionIndex, + actionName: actionName ?? '', + }), + }); + await expect(button).toBeVisible(); + } + + public async typeValueInActionCombobox( + customText: string, + actionIndex: string, + actionName?: string, + ): Promise { + await this.page + .getByRole('combobox', { + name: this.textMock('process_editor.configuration_panel_actions_action_label', { + actionIndex, + actionName: actionName ?? '', + }), + }) + .fill(customText); + } + + public async verifyThatCustomActionTextIsVisible(): Promise { + const text = this.page.getByText( + this.textMock('process_editor.configuration_panel_actions_custom_action'), + ); + await expect(text).toBeVisible(); + } + + public async clickOnPolicyAccordion(): Promise { + await this.page + .getByRole('button', { + name: this.textMock('process_editor.configuration_panel_policy_title'), + }) + .click(); + } + + public async waitForNavigateToPolicyButtonIsVisible(): Promise { + const button = this.page.getByRole('button', { + name: this.textMock( + 'process_editor.configuration_panel.edit_policy_open_policy_editor_button', + ), + }); + await expect(button).toBeVisible(); + } + + public async clickOnNavigateToPolicyEditorButton(): Promise { + await this.page + .getByRole('button', { + name: this.textMock( + 'process_editor.configuration_panel.edit_policy_open_policy_editor_button', + ), + }) + .click(); + } + + public async waitForPolicyEditorModalTabToBeVisible(): Promise { + const heading = this.page.getByRole('heading', { + name: this.textMock('policy_editor.rules'), + level: 2, + }); + await expect(heading).toBeVisible(); + } + + public async dragTaskInToBpmnEditor( + task: BpmnTaskType, + dropElementSelector: string, + extraDistanceX?: number, + extraDistanceY?: number, + ) { + const boundingBox = await this.page.locator(dropElementSelector).boundingBox(); + const targetX = boundingBox.width / 2 + (extraDistanceX ?? 0); + const targetY = boundingBox.y + boundingBox.height / 2 + (extraDistanceY ?? 0); + + const title = `Create Altinn ${task} task`; + await this.startDragElement(title); + await this.stopDragElement(targetX, targetY); + } + + public async waitForTaskToBeVisibleInConfigPanel(task: BpmnTaskType): Promise { + const text = this.page.getByText(`Navn: Altinn ${task} task`); + await expect(text).toBeVisible(); + } + + public async getTaskIdFromOpenNewlyAddedTask(): Promise { + const selector = 'text=ID: Activity_'; + await this.page.waitForSelector(selector); + return await this.getFullIdFromButtonSelector(selector); + } + + public async clickOnTaskIdEditButton(id: string): Promise { + await this.page + .getByText(`${this.textMock('process_editor.configuration_panel_id_label')} ${id}`) + .click(); + } + + public async waitForEditIdInputFieldToBeVisible(): Promise { + const inputField = this.page.getByRole('textbox', { + name: this.textMock('process_editor.configuration_panel_change_task_id'), + }); + await expect(inputField).toBeVisible(); + } + + public async emptyIdInputfield(): Promise { + await this.page + .getByRole('textbox', { + name: this.textMock('process_editor.configuration_panel_change_task_id'), + }) + .clear(); + } + + public async writeNewId(id: string): Promise { + await this.page + .getByRole('textbox', { + name: this.textMock('process_editor.configuration_panel_change_task_id'), + }) + .fill(id); + } + + public async waitForTextBoxToHaveValue(id: string): Promise { + const textBox = this.page.getByRole('textbox', { + name: this.textMock('process_editor.configuration_panel_change_task_id'), + }); + await expect(textBox).toHaveValue(id); + } + + public async saveNewId(): Promise { + await this.page + .getByRole('textbox', { + name: this.textMock('process_editor.configuration_panel_change_task_id'), + }) + .blur(); + } + + public async waitForNewTaskIdButtonToBeVisible(id: string): Promise { + const button = this.page.getByText( + `${this.textMock('process_editor.configuration_panel_id_label')} ${id}`, + ); + await expect(button).toBeVisible(); + } + + public async verifyThatThereAreNoDataModelsAvailable(): Promise { + const noDataModelMessage = this.page.getByText( + this.textMock('process_editor.configuration_panel_no_data_model_to_select'), + ); + await expect(noDataModelMessage).toBeVisible(); + } + + public async pressEscapeOnKeyboard(): Promise { + await this.page.keyboard.press('Escape'); + } + + public async clickOnConnectionArrow(): Promise { + await this.page.getByTitle(connectionArrowText).click(); + } + + public async verifyThatPolicyEditorIsOpen(): Promise { + const heading = this.page.getByRole('heading', { + name: this.textMock('policy_editor.rules'), + level: 2, + }); + await expect(heading).toBeVisible(); + } + public async closePolicyEditor(): Promise { + await this.page + .getByRole('button', { + name: this.textMock('settings_modal.close_button_label'), + }) + .click(); + } + + public async verifyThatPolicyEditorIsClosed(): Promise { + const heading = this.page.getByRole('heading', { + name: this.textMock('policy_editor.rules'), + level: 2, + }); + await expect(heading).toBeHidden(); + } + + public async clickDataTypesToSignCombobox(): Promise { + await this.page + .getByRole('combobox', { + name: this.textMock('process_editor.configuration_panel_set_data_types_to_sign'), + }) + .click(); + } + + public async clickOnDataTypesToSignOption(option: string): Promise { + await this.page.getByRole('option', { name: option }).click(); + } + + public async waitForDataTypeToSignButtonToBeVisible(option: string): Promise { + const button = this.page.getByLabel(this.textMock('general.delete_item', { item: option })); + await expect(button).toBeVisible(); + } + + public async waitForEndEventHeaderToBeVisible(): Promise { + const heading = this.page.getByRole('heading', { + name: this.textMock('process_editor.configuration_panel_end_event'), + }); + + await expect(heading).toBeVisible(); + } + + public async clickOnReceiptAccordion(): Promise { + await this.page + .getByRole('button', { + name: this.textMock('process_editor.configuration_panel_custom_receipt_accordion_header'), + }) + .click(); + } + + public async waitForCreateCustomReceiptButtonToBeVisible(): Promise { + const text = this.page.getByRole('button', { + name: this.textMock( + 'process_editor.configuration_panel_custom_receipt_create_your_own_button', + ), + }); + await expect(text).toBeVisible(); + } + + public async clickOnCreateCustomReceipt(): Promise { + await this.page + .getByRole('button', { + name: this.textMock( + 'process_editor.configuration_panel_custom_receipt_create_your_own_button', + ), + }) + .click(); + } + + public async waitForLayoutTextfieldToBeVisible(): Promise { + const textbox = this.page.getByRole('textbox', { + name: this.textMock('process_editor.configuration_panel_custom_receipt_textfield_label'), + }); + await expect(textbox).toBeVisible(); + } + + public async writeLayoutSetId(layoutSetId: string): Promise { + await this.page + .getByRole('textbox', { + name: this.textMock('process_editor.configuration_panel_custom_receipt_textfield_label'), + }) + .fill(layoutSetId); + } + + public async clickOnAddDataModelCombobox(): Promise { + await this.page + .getByRole('combobox', { + name: this.textMock( + 'process_editor.configuration_panel_custom_receipt_select_data_model_label', + ), + }) + .click(); + } + + public async waitForSaveNewCustomReceiptButtonToBeVisible(): Promise { + const button = this.page.getByRole('button', { + name: this.textMock('process_editor.configuration_panel_custom_receipt_create_button'), + }); + await expect(button).toBeVisible(); + } + + public async clickOnSaveNewCustomReceiptButton(): Promise { + await this.page + .getByRole('button', { + name: this.textMock('process_editor.configuration_panel_custom_receipt_create_button'), + }) + .click(); + } + + public async waitForEditLayoutSetIdButtonToBeVisible(): Promise { + const button = this.page.getByRole('button', { + name: this.textMock('process_editor.configuration_panel_custom_receipt_textfield_label'), + }); + await expect(button).toBeVisible(); + } + + /** + * + * Helper methods below this + * + */ + + private async startDragElement(title: string): Promise { + await this.page.getByTitle(title).hover(); + await this.page.mouse.down(); + } + + private async stopDragElement(xPosition: number, yPosition: number): Promise { + const numberOfMouseMoveEvents: number = 20; + await this.page.mouse.move(xPosition, yPosition, { steps: numberOfMouseMoveEvents }); + await this.page.mouse.up(); + } + + private async getFullIdFromButtonSelector(selector: string): Promise { + const button = this.page.locator(selector); + const fullText = await button.textContent(); + const extractedText = fullText.match(/ID: (Activity_\w+)/); + const fullId: string = extractedText[1]; + return fullId; + } } diff --git a/frontend/testing/playwright/playwright.config.ts b/frontend/testing/playwright/playwright.config.ts index db3d1018f02..66919c36800 100644 --- a/frontend/testing/playwright/playwright.config.ts +++ b/frontend/testing/playwright/playwright.config.ts @@ -118,6 +118,18 @@ export default defineConfig({ headless: true, }, }, + { + name: TestNames.PROCESS_EDITOR, + dependencies: [TestNames.SETUP], + testDir: './tests/process-editor/', + testMatch: '*.spec.ts', + use: { + ...devices['Desktop Chrome'], + storageState: '.playwright/auth/user.json', + testAppName: AppNames.PROCESS_EDITOR_APP, + headless: true, + }, + }, { name: TestNames.LOGOUT_AND_INVALID_LOGIN_ONLY, // Add ALL other test names here to make sure that the log out test is the last test to be executed @@ -131,6 +143,7 @@ export default defineConfig({ TestNames.UI_EDITOR, TestNames.SETTINGS_MODAL, TestNames.TEXT_EDITOR, + TestNames.PROCESS_EDITOR, ...Object.values(TestNames).filter( (testName) => testName !== TestNames.LOGOUT_AND_INVALID_LOGIN_ONLY, ), diff --git a/frontend/testing/playwright/tests/process-editor/process-editor.spec.ts b/frontend/testing/playwright/tests/process-editor/process-editor.spec.ts new file mode 100644 index 00000000000..f11eae522f0 --- /dev/null +++ b/frontend/testing/playwright/tests/process-editor/process-editor.spec.ts @@ -0,0 +1,363 @@ +import { test } from '../../extenders/testExtend'; +import { expect } from '@playwright/test'; +import type { Page } from '@playwright/test'; +import { Gitea } from '../../helpers/Gitea'; +import { DesignerApi } from '../../helpers/DesignerApi'; +import type { StorageState } from '../../types/StorageState'; +import { ProcessEditorPage } from '../../pages/ProcessEditorPage'; +import { BpmnJSQuery } from '../../helpers/BpmnJSQuery'; +import { Header } from '../../components/Header'; +import { DataModelPage } from '../../pages/DataModelPage'; +import { GiteaPage } from '../../pages/GiteaPage'; +import { type BpmnTaskType } from '../../types/BpmnTaskType'; + +// This line must be there to ensure that the tests do not run in parallell, and +// that the before all call is being executed before we start the tests +test.describe.configure({ mode: 'serial' }); + +test.beforeAll(async ({ testAppName, request, storageState }) => { + const designerApi = new DesignerApi({ app: testAppName }); + const response = await designerApi.createApp(request, storageState as StorageState); + expect(response.ok()).toBeTruthy(); +}); + +test.afterAll(async ({ request, testAppName }) => { + const gitea = new Gitea(); + const response = await request.delete(gitea.getDeleteAppEndpoint({ app: testAppName })); + expect(response.ok()).toBeTruthy(); +}); + +const setupAndVerifyProcessEditorPage = async ( + page: Page, + testAppName: string, +): Promise => { + const processEditorPage = new ProcessEditorPage(page, { app: testAppName }); + await processEditorPage.loadProcessEditorPage(); + await processEditorPage.verifyProcessEditorPage(); + return processEditorPage; +}; + +test('That it is possible to add and remove datamodel, and add actions to the default task in the process editor', async ({ + page, + testAppName, +}): Promise => { + const processEditorPage = await setupAndVerifyProcessEditorPage(page, testAppName); + const bpmnJSQuery = new BpmnJSQuery(page); + const header = new Header(page, { app: testAppName }); + const giteaPage = new GiteaPage(page, { app: testAppName }); + + const initialTaskDataElementIdSelector: string = await bpmnJSQuery.getTaskByIdAndType( + 'Task_1', + 'g', + ); + await processEditorPage.clickOnTaskInBpmnEditor(initialTaskDataElementIdSelector); + await processEditorPage.waitForInitialTaskHeaderToBeVisible(); + + // --------------------- Add and delete datamodel --------------------- + await processEditorPage.clickOnDataModelButton(); + await processEditorPage.waitForDataModelComboboxToBeVisible(); + + await processEditorPage.clickOnDeleteDataModel(); + await processEditorPage.waitForAddDataModelButtonToBeVisible(); + + await processEditorPage.clickOnAddDataModel(); + await processEditorPage.waitForDataModelComboboxToBeVisible(); + + const dataModelName: string = 'model'; + await processEditorPage.clickOnDataModelCombobox(); + await processEditorPage.clickOnDataModelOption(dataModelName); + await processEditorPage.waitForDataModelButtonToBeVisible(); + + await processEditorPage.verifyDataModelButtonTextIsSelectedDataModel(dataModelName); + await processEditorPage.verifyThatAddNewDataModelButtonIsHidden(); + + // --------------------- Add actions --------------------- + await processEditorPage.clickOnActionsAccordion(); + await processEditorPage.waitForAddActionsButtonToBeVisible(); + + const actionIndex1: string = '1'; + await processEditorPage.clickAddActionsButton(); + await processEditorPage.waitForActionComboboxTitleToBeVisible(actionIndex1); + + const actionOptionWrite: string = 'write'; + await processEditorPage.clickOnActionCombobox(actionIndex1); + await processEditorPage.clickOnActionOption(actionOptionWrite); + await processEditorPage.removeFocusFromActionCombobox(actionIndex1); + await processEditorPage.clickOnSaveActionButton(); + await processEditorPage.waitForActionButtonToBeVisible(actionIndex1, actionOptionWrite); + + // --------------------- Verify policy editor --------------------- + await processEditorPage.clickOnPolicyAccordion(); + await processEditorPage.waitForNavigateToPolicyButtonIsVisible(); + await processEditorPage.clickOnNavigateToPolicyEditorButton(); + + await processEditorPage.verifyThatPolicyEditorIsOpen(); + await processEditorPage.closePolicyEditor(); + await processEditorPage.verifyThatPolicyEditorIsClosed(); + + // --------------------- Check that files are uploaded to Gitea --------------------- + await goToGiteaAndNavigateToProcessBpmnFile(header, giteaPage); + + const numberOfPagesBackToAltinnStudio: number = 5; + await giteaPage.goBackNPages(numberOfPagesBackToAltinnStudio); + + await processEditorPage.verifyProcessEditorPage(); + await commitAndPushToGitea(header); + + await goToGiteaAndNavigateToProcessBpmnFile(header, giteaPage); + await giteaPage.verifyThatActionIsVisible(actionOptionWrite); +}); + +test('That it is possible to add a new task to the process editor, configure some of its data', async ({ + page, + testAppName, +}): Promise => { + const processEditorPage = await setupAndVerifyProcessEditorPage(page, testAppName); + const bpmnJSQuery = new BpmnJSQuery(page); + const header = new Header(page, { app: testAppName }); + const dataModelPage = new DataModelPage(page, { app: testAppName }); + const giteaPage = new GiteaPage(page, { app: testAppName }); + + // --------------------- Drag new task into the editor --------------------- + const svgSelector = await bpmnJSQuery.getTaskByIdAndType('SingleDataTask', 'svg'); + const dataTask: BpmnTaskType = 'data'; + await processEditorPage.dragTaskInToBpmnEditor(dataTask, svgSelector); + await processEditorPage.waitForTaskToBeVisibleInConfigPanel(dataTask); + const randomGeneratedId = await processEditorPage.getTaskIdFromOpenNewlyAddedTask(); + + // --------------------- Edit the id --------------------- + const newId: string = 'my_new_id'; + await editRandomGeneratedId(processEditorPage, randomGeneratedId, newId); + + // --------------------- Add new data model --------------------- + await processEditorPage.clickOnAddDataModel(); + await processEditorPage.waitForDataModelComboboxToBeVisible(); + await processEditorPage.clickOnDataModelCombobox(); + await processEditorPage.verifyThatThereAreNoDataModelsAvailable(); + await processEditorPage.pressEscapeOnKeyboard(); + + const newDataModel: string = 'testDataModel'; + await navigateToDataModelAndCreateNewDataModel( + dataModelPage, + processEditorPage, + header, + newDataModel, + ); + const newTaskSelector: string = await bpmnJSQuery.getTaskByIdAndType(newId, 'g'); + await processEditorPage.clickOnTaskInBpmnEditor(newTaskSelector); + + await processEditorPage.clickOnAddDataModel(); + await processEditorPage.waitForDataModelComboboxToBeVisible(); + await processEditorPage.clickOnDataModelCombobox(); + await processEditorPage.clickOnDataModelOption(newDataModel); + await processEditorPage.waitForDataModelButtonToBeVisible(); + await processEditorPage.verifyDataModelButtonTextIsSelectedDataModel(newDataModel); + + // --------------------- Connect the task to the process --------------------- + await processEditorPage.clickOnConnectionArrow(); + + const initialId: string = 'Task_1'; + const initialTaskSelector: string = await bpmnJSQuery.getTaskByIdAndType(initialId, 'g'); + await processEditorPage.clickOnTaskInBpmnEditor(initialTaskSelector); + + // --------------------- Check that files are uploaded to Gitea --------------------- + await goToGiteaAndNavigateToProcessBpmnFile(header, giteaPage); + await giteaPage.verifyThatTheNewTaskIsHidden(newId, dataTask); + + const numberOfPagesBackToAltinnStudio: number = 5; + await giteaPage.goBackNPages(numberOfPagesBackToAltinnStudio); + + await processEditorPage.verifyProcessEditorPage(); + await commitAndPushToGitea(header); + + await goToGiteaAndNavigateToProcessBpmnFile(header, giteaPage); + await giteaPage.verifyThatTheNewTaskIsVisible(newId, dataTask); + + await giteaPage.verifySequenceFlowDirection(newId, initialId); + const numblerBackToConfig: number = 2; + await giteaPage.goBackNPages(numblerBackToConfig); + await giteaPage.clickOnApplicationMetadataFile(); + await giteaPage.verifyIdInDataModel(newId, newDataModel); +}); + +test('That it is possible to add a new signing task, and update the datatypes to sign', async ({ + page, + testAppName, +}): Promise => { + const processEditorPage = await setupAndVerifyProcessEditorPage(page, testAppName); + const bpmnJSQuery = new BpmnJSQuery(page); + const header = new Header(page, { app: testAppName }); + const giteaPage = new GiteaPage(page, { app: testAppName }); + + // --------------------- Drag new task into the editor --------------------- + const svgSelector = await bpmnJSQuery.getTaskByIdAndType('SingleDataTask', 'svg'); + const signingTask: BpmnTaskType = 'signing'; + + const extraMovingDistanceX: number = -120; + const extraMovingDistanceY: number = 0; + await processEditorPage.dragTaskInToBpmnEditor( + signingTask, + svgSelector, + extraMovingDistanceX, + extraMovingDistanceY, + ); + await processEditorPage.waitForTaskToBeVisibleInConfigPanel(signingTask); + const randomGeneratedId = await processEditorPage.getTaskIdFromOpenNewlyAddedTask(); + + // --------------------- Edit the id --------------------- + const newId: string = 'signing_id'; + await editRandomGeneratedId(processEditorPage, randomGeneratedId, newId); + + // --------------------- Add data types to sign --------------------- + await processEditorPage.clickDataTypesToSignCombobox(); + const dataTypeToSign: string = 'ref-data-as-pdf'; + await processEditorPage.clickOnDataTypesToSignOption(dataTypeToSign); + await processEditorPage.waitForDataTypeToSignButtonToBeVisible(dataTypeToSign); + await processEditorPage.pressEscapeOnKeyboard(); + + // --------------------- Verify correct actions --------------------- + await processEditorPage.clickOnActionsAccordion(); + + const actionIndex1: string = '1'; + const actionIndex2: string = '2'; + const actionOptionSign: string = 'sign'; + const actionOptionReject: string = 'reject'; + await processEditorPage.waitForActionButtonToBeVisible(actionIndex1, actionOptionSign); + await processEditorPage.waitForActionButtonToBeVisible(actionIndex2, actionOptionReject); + + // --------------------- Check that files are uploaded to Gitea --------------------- + await goToGiteaAndNavigateToProcessBpmnFile(header, giteaPage); + await giteaPage.verifyThatTaskIsHidden(signingTask); + await giteaPage.verifyThatActionIsHidden(actionOptionSign); + await giteaPage.verifyThatActionIsHidden(actionOptionReject); + await giteaPage.verifyThatDataTypeToSignIsHidden(dataTypeToSign); + + const numberOfPagesBackToAltinnStudio: number = 5; + await giteaPage.goBackNPages(numberOfPagesBackToAltinnStudio); + + await processEditorPage.verifyProcessEditorPage(); + await commitAndPushToGitea(header); + + await giteaPage.verifyThatTaskIsVisible(signingTask); + await giteaPage.verifyThatActionIsVisible(actionOptionSign); + await giteaPage.verifyThatActionIsVisible(actionOptionReject); + await giteaPage.verifyThatDataTypeToSignIsVisible(signingTask); +}); + +test('That it is possible to create a custom receipt', async ({ page, testAppName }) => { + const processEditorPage = await setupAndVerifyProcessEditorPage(page, testAppName); + const dataModelPage = new DataModelPage(page, { app: testAppName }); + const bpmnJSQuery = new BpmnJSQuery(page); + const header = new Header(page, { app: testAppName }); + const giteaPage = new GiteaPage(page, { app: testAppName }); + + // --------------------- Create new data model --------------------- + const newDataModel: string = 'newDataModel'; + await navigateToDataModelAndCreateNewDataModel( + dataModelPage, + processEditorPage, + header, + newDataModel, + ); + + // --------------------- Add layout set id and data model id to the receipt --------------------- + const endEvent: string = await bpmnJSQuery.getTaskByIdAndType('EndEvent_1', 'g'); + await processEditorPage.clickOnTaskInBpmnEditor(endEvent); + await processEditorPage.waitForEndEventHeaderToBeVisible(); + + await processEditorPage.clickOnReceiptAccordion(); + await processEditorPage.waitForCreateCustomReceiptButtonToBeVisible(); + + await processEditorPage.clickOnCreateCustomReceipt(); + await processEditorPage.waitForLayoutTextfieldToBeVisible(); + + const newLayoutSetId: string = 'layoutSetId'; + await processEditorPage.writeLayoutSetId(newLayoutSetId); + await processEditorPage.clickOnAddDataModelCombobox(); + await processEditorPage.clickOnDataModelOption(newDataModel); + await processEditorPage.pressEscapeOnKeyboard(); + + await processEditorPage.waitForSaveNewCustomReceiptButtonToBeVisible(); + await processEditorPage.clickOnSaveNewCustomReceiptButton(); + await processEditorPage.waitForEditLayoutSetIdButtonToBeVisible(); + + // --------------------- Check that files are uploaded to Gitea --------------------- + await goToGiteaAndNavigateToApplicationMetadataFile(header, giteaPage); + await giteaPage.verifyThatCustomReceiptIsNotVisible(); + const numberOfPagesBackToAltinnStudio: number = 4; + await giteaPage.goBackNPages(numberOfPagesBackToAltinnStudio); + + await processEditorPage.verifyProcessEditorPage(); + await commitAndPushToGitea(header); + + await goToGiteaAndNavigateToApplicationMetadataFile(header, giteaPage); + await giteaPage.verifyThatCustomReceiptIsVisible(); +}); + +// --------------------- Helper Functions --------------------- +const editRandomGeneratedId = async ( + processEditorPage: ProcessEditorPage, + randomGeneratedId: string, + newId: string, +): Promise => { + await processEditorPage.clickOnTaskIdEditButton(randomGeneratedId); + await processEditorPage.waitForEditIdInputFieldToBeVisible(); + await processEditorPage.emptyIdInputfield(); + await processEditorPage.writeNewId(newId); + await processEditorPage.waitForTextBoxToHaveValue(newId); + await processEditorPage.saveNewId(); + await processEditorPage.waitForNewTaskIdButtonToBeVisible(newId); +}; + +const goToGiteaAndNavigateToProcessBpmnFile = async ( + header: Header, + giteaPage: GiteaPage, +): Promise => { + await header.clickOnThreeDotsMenu(); + await header.clickOnGoToGiteaRepository(); + + await giteaPage.verifyGiteaPage(); + await giteaPage.clickOnAppFilesButton(); + await giteaPage.clickOnConfigFilesButton(); + await giteaPage.clickOnProcessFilesButton(); + await giteaPage.clickOnProcessBpmnFile(); +}; + +const commitAndPushToGitea = async (header: Header): Promise => { + await header.clickOnUploadLocalChangesButton(); + await header.clickOnValidateChanges(); + await header.checkThatUploadSuccessMessageIsVisible(); +}; + +const navigateToDataModelAndCreateNewDataModel = async ( + dataModelPage: DataModelPage, + processEditorPage: ProcessEditorPage, + header: Header, + newDataModelName: string, +): Promise => { + await header.clickOnNavigateToPageInTopMenuHeader('data_model'); + await dataModelPage.verifyDataModelPage(); + await dataModelPage.clickOnCreateNewDataModelButton(); + await dataModelPage.typeDataModelName(newDataModelName); + await dataModelPage.clickOnCreateModelButton(); + await dataModelPage.waitForDataModelToAppear(newDataModelName); + await dataModelPage.clickOnGenerateDataModelButton(); + await dataModelPage.checkThatSuccessAlertIsVisibleOnScreen(); + await dataModelPage.waitForSuccessAlertToDisappear(); + + await header.clickOnNavigateToPageInTopMenuHeader('process_editor'); + await processEditorPage.verifyProcessEditorPage(); +}; + +const goToGiteaAndNavigateToApplicationMetadataFile = async ( + header: Header, + giteaPage: GiteaPage, +): Promise => { + await header.clickOnThreeDotsMenu(); + await header.clickOnGoToGiteaRepository(); + + await giteaPage.verifyGiteaPage(); + await giteaPage.clickOnAppFilesButton(); + await giteaPage.clickOnConfigFilesButton(); + await giteaPage.clickOnApplicationMetadataFile(); +}; diff --git a/frontend/testing/playwright/types/BpmnTaskType.ts b/frontend/testing/playwright/types/BpmnTaskType.ts new file mode 100644 index 00000000000..484f00b5f36 --- /dev/null +++ b/frontend/testing/playwright/types/BpmnTaskType.ts @@ -0,0 +1 @@ +export type BpmnTaskType = 'data' | 'feedback' | 'signing' | 'confirm';