From 1b1309d318d00d54d665a8b4738c20c9589a823e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Schultz=20Madsen?= Date: Sun, 5 Apr 2026 09:16:55 +0200 Subject: [PATCH 1/2] feat: migrate WDIO e2e tests to Playwright - Add Playwright page objects for inner/outer resource modals and pages - Add Playwright test specs: a/ (plugin activation), b/ (CRUD tests) - Add playwright.config.ts - Replace WDIO test job with Playwright test job in both CI workflows - Replace Cypress DB configuration with Playwright DB configuration - Remove WDIO file copies from build job Co-Authored-By: Claude Opus 4.6 --- .github/workflows/dotnet-core-master.yml | 82 ++++----- .github/workflows/dotnet-core-pr.yml | 86 +++++----- eform-client/playwright.config.ts | 21 +++ .../OuterInnerResourceInnerResource.page.ts | 155 ++++++++++++++++++ .../OuterInnerResourceModal.page.ts | 87 ++++++++++ .../OuterInnerResourceOuterResource.page.ts | 155 ++++++++++++++++++ .../a/outer-inner-resource-settings.spec.ts | 41 +++++ ...uter-inner-resource-inner-resource.spec.ts | 93 +++++++++++ ...uter-inner-resource-outer-resource.spec.ts | 95 +++++++++++ 9 files changed, 735 insertions(+), 80 deletions(-) create mode 100644 eform-client/playwright.config.ts create mode 100644 eform-client/playwright/e2e/plugins/outer-inner-resource-pn/OuterInnerResourceInnerResource.page.ts create mode 100644 eform-client/playwright/e2e/plugins/outer-inner-resource-pn/OuterInnerResourceModal.page.ts create mode 100644 eform-client/playwright/e2e/plugins/outer-inner-resource-pn/OuterInnerResourceOuterResource.page.ts create mode 100644 eform-client/playwright/e2e/plugins/outer-inner-resource-pn/a/outer-inner-resource-settings.spec.ts create mode 100644 eform-client/playwright/e2e/plugins/outer-inner-resource-pn/b/outer-inner-resource-inner-resource.spec.ts create mode 100644 eform-client/playwright/e2e/plugins/outer-inner-resource-pn/b/outer-inner-resource-outer-resource.spec.ts diff --git a/.github/workflows/dotnet-core-master.yml b/.github/workflows/dotnet-core-master.yml index 626390db..a1860f35 100644 --- a/.github/workflows/dotnet-core-master.yml +++ b/.github/workflows/dotnet-core-master.yml @@ -26,11 +26,6 @@ jobs: - name: Copy dependencies run: | cp -av eform-angular-outer-inner-resource-plugin/eform-client/src/app/plugins/modules/outer-inner-resource-pn eform-angular-frontend/eform-client/src/app/plugins/modules/outer-inner-resource-pn - cp -av eform-angular-outer-inner-resource-plugin/eform-client/e2e/Tests/outer-inner-resource-settings eform-angular-frontend/eform-client/e2e/Tests/outer-inner-resource-settings - cp -av eform-angular-outer-inner-resource-plugin/eform-client/e2e/Tests/outer-inner-resource-general eform-angular-frontend/eform-client/e2e/Tests/outer-inner-resource-general - cp -av eform-angular-outer-inner-resource-plugin/eform-client/e2e/Page\ objects/OuterInnerResource eform-angular-frontend/eform-client/e2e/Page\ objects/OuterInnerResource - cp -av eform-angular-outer-inner-resource-plugin/eform-client/wdio-headless-plugin-step2.conf.ts eform-angular-frontend/eform-client/wdio-headless-plugin-step2.conf.ts - cp -av eform-angular-outer-inner-resource-plugin/eform-client/wdio-plugin-step2.conf.ts eform-angular-frontend/eform-client/wdio-plugin-step2.conf.ts mkdir -p eform-angular-frontend/eFormAPI/eFormAPI.Web/Plugins cd eform-angular-frontend/eform-client && ../../eform-angular-outer-inner-resource-plugin/testinginstallpn.sh - name: Get the version release @@ -51,9 +46,13 @@ jobs: with: name: outer-inner-resource-container path: outer-inner-resource-container.tar - outer-inner-resource-test: + outer-inner-resource-playwright-test: needs: outer-inner-resource-build - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + test: [a,b] steps: - uses: actions/checkout@v3 with: @@ -62,7 +61,7 @@ jobs: id: extract_branch run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" - name: Create docker network - run: docker network create --driver bridge data + run: docker network create --driver bridge --attachable data - name: Start MariaDB run: | docker pull mariadb:10.8 @@ -85,25 +84,14 @@ jobs: repository: microting/eform-angular-frontend ref: ${{ steps.extract_branch.outputs.branch }} path: eform-angular-frontend - - name: Cache node_modules - id: cache - uses: actions/cache@v3 - with: - path: eform-angular-frontend/eform-client/node_modules - key: ${{ runner.os }}-build-${{ hashFiles('eform-angular-frontend/eform-client/package.json') }} - restore-keys: | - ${{ runner.os }}-build- - ${{ runner.os }}- - name: Sleep 15 seconds run: sleep 15 - name: Copy dependencies run: | cp -av eform-angular-outer-inner-resource-plugin/eform-client/src/app/plugins/modules/outer-inner-resource-pn eform-angular-frontend/eform-client/src/app/plugins/modules/outer-inner-resource-pn - cp -av eform-angular-outer-inner-resource-plugin/eform-client/e2e/Tests/outer-inner-resource-settings eform-angular-frontend/eform-client/e2e/Tests/outer-inner-resource-settings - cp -av eform-angular-outer-inner-resource-plugin/eform-client/e2e/Tests/outer-inner-resource-general eform-angular-frontend/eform-client/e2e/Tests/outer-inner-resource-general - cp -av eform-angular-outer-inner-resource-plugin/eform-client/e2e/Page\ objects/OuterInnerResource eform-angular-frontend/eform-client/e2e/Page\ objects/OuterInnerResource - cp -av eform-angular-outer-inner-resource-plugin/eform-client/wdio-headless-plugin-step2.conf.ts eform-angular-frontend/eform-client/wdio-headless-plugin-step2.conf.ts - cp -av eform-angular-outer-inner-resource-plugin/eform-client/wdio-plugin-step2.conf.ts eform-angular-frontend/eform-client/wdio-plugin-step2.conf.ts + mkdir -p eform-angular-frontend/eform-client/playwright/e2e/plugins/ + cp -av eform-angular-outer-inner-resource-plugin/eform-client/playwright/e2e/plugins/outer-inner-resource-pn eform-angular-frontend/eform-client/playwright/e2e/plugins/outer-inner-resource-pn + cp -av eform-angular-outer-inner-resource-plugin/eform-client/playwright.config.ts eform-angular-frontend/eform-client/playwright.config.ts mkdir -p eform-angular-frontend/eFormAPI/eFormAPI.Web/Plugins cd eform-angular-frontend/eform-client && ../../eform-angular-outer-inner-resource-plugin/testinginstallpn.sh - name: Start the newly build Docker container @@ -114,26 +102,34 @@ jobs: - name: Get standard output run: cat docker_run_log - name: Pretest changes to work with Docker container - run: sed -i 's/localhost/mariadbtest/g' eform-angular-frontend/eform-client/e2e/Constants/DatabaseConfigurationConstants.ts + run: sed -i 's/localhost/mariadbtest/g' eform-angular-frontend/eform-client/playwright/e2e/Constants/DatabaseConfigurationConstants.ts - name: yarn install run: cd eform-angular-frontend/eform-client && yarn install - if: steps.cache.outputs.cache-hit != 'true' + - name: Install Playwright browsers + run: cd eform-angular-frontend/eform-client && npx playwright install --with-deps chromium + - name: Wait for app + run: npx wait-on http://localhost:4200 --timeout 120000 - name: DB Configuration - uses: cypress-io/github-action@v4 - with: - start: echo 'hi' - wait-on: "http://localhost:4200" - wait-on-timeout: 120 - browser: chrome - record: false - spec: cypress/e2e/db/* - config-file: cypress.config.ts - working-directory: eform-angular-frontend/eform-client - command-prefix: "--" + run: cd eform-angular-frontend/eform-client && npx playwright test playwright/e2e/Tests/database-configuration/ - name: Change rabbitmq hostname run: docker exec -i mariadbtest mysql -u root --password=secretpassword -e 'update 420_SDK.Settings set Value = "my-rabbit" where Name = "rabbitMqHost"' - - name: Plugin testing - run: cd eform-angular-frontend/eform-client && npm run testheadlessplugin + - name: Get standard output + run: | + cat docker_run_log + result=`cat docker_run_log | grep "Now listening on: http://0.0.0.0:5000" -m 1 | wc -l` + if [ $result -ne 1 ];then exit 1; fi + - name: Enable plugin + if: matrix.test != 'a' + run: | + docker exec -i mariadbtest mysql -u root --password=secretpassword -e 'update 420_Angular.EformPlugins set Status = 2' + docker restart my-container + sleep 15 + - name: Wait for app + run: npx wait-on http://localhost:4200 --timeout 120000 + - name: ${{ matrix.test }} playwright test + run: | + cd eform-angular-frontend/eform-client + npx playwright test playwright/e2e/plugins/outer-inner-resource-pn/${{ matrix.test }}/ - name: Stop the newly build Docker container run: docker stop my-container - name: Get standard output @@ -141,10 +137,16 @@ jobs: cat docker_run_log result=`cat docker_run_log | grep "Now listening on: http://0.0.0.0:5000" -m 1 | wc -l` if [ $result -ne 1 ];then exit 1; fi - - name: The job has failed + - name: Get standard output if: ${{ failure() }} - run: | - cat docker_run_log + run: cat docker_run_log + - name: Archive Playwright report + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report-${{ matrix.test }} + path: eform-angular-frontend/eform-client/playwright-report/ + retention-days: 2 outer-inner-resource-dotnet-test: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/dotnet-core-pr.yml b/.github/workflows/dotnet-core-pr.yml index 49aef0f9..e8d188ec 100644 --- a/.github/workflows/dotnet-core-pr.yml +++ b/.github/workflows/dotnet-core-pr.yml @@ -23,11 +23,6 @@ jobs: - name: Copy dependencies run: | cp -av eform-angular-outer-inner-resource-plugin/eform-client/src/app/plugins/modules/outer-inner-resource-pn eform-angular-frontend/eform-client/src/app/plugins/modules/outer-inner-resource-pn - cp -av eform-angular-outer-inner-resource-plugin/eform-client/e2e/Tests/outer-inner-resource-settings eform-angular-frontend/eform-client/e2e/Tests/outer-inner-resource-settings - cp -av eform-angular-outer-inner-resource-plugin/eform-client/e2e/Tests/outer-inner-resource-general eform-angular-frontend/eform-client/e2e/Tests/outer-inner-resource-general - cp -av eform-angular-outer-inner-resource-plugin/eform-client/e2e/Page\ objects/OuterInnerResource eform-angular-frontend/eform-client/e2e/Page\ objects/OuterInnerResource - cp -av eform-angular-outer-inner-resource-plugin/eform-client/wdio-headless-plugin-step2.conf.ts eform-angular-frontend/eform-client/wdio-headless-plugin-step2.conf.ts - cp -av eform-angular-outer-inner-resource-plugin/eform-client/wdio-plugin-step2.conf.ts eform-angular-frontend/eform-client/wdio-plugin-step2.conf.ts mkdir -p eform-angular-frontend/eFormAPI/eFormAPI.Web/Plugins cd eform-angular-frontend/eform-client && ../../eform-angular-outer-inner-resource-plugin/testinginstallpn.sh - name: Get the version release @@ -48,19 +43,27 @@ jobs: with: name: outer-inner-resource-container path: outer-inner-resource-container.tar - outer-inner-resource-test: + outer-inner-resource-playwright-test: needs: outer-inner-resource-build - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + test: [a,b] steps: - uses: actions/checkout@v3 with: path: eform-angular-outer-inner-resource-plugin - name: Create docker network - run: docker network create --driver bridge data + run: docker network create --driver bridge --attachable data - name: Start MariaDB run: | docker pull mariadb:10.8 docker run --name mariadbtest --network data -e MYSQL_ROOT_PASSWORD=secretpassword -p 3306:3306 -d mariadb:10.8 + - name: Start rabbitmq + run: | + docker pull rabbitmq:latest + docker run -d --hostname my-rabbit --name some-rabbit --network data -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=password rabbitmq:latest - uses: actions/download-artifact@v4 with: name: outer-inner-resource-container @@ -75,25 +78,14 @@ jobs: repository: microting/eform-angular-frontend ref: stable path: eform-angular-frontend - - name: Cache node_modules - id: cache - uses: actions/cache@v3 - with: - path: eform-angular-frontend/eform-client/node_modules - key: ${{ runner.os }}-build-${{ hashFiles('eform-angular-frontend/eform-client/package.json') }} - restore-keys: | - ${{ runner.os }}-build- - ${{ runner.os }}- - name: Sleep 15 seconds run: sleep 15 - name: Copy dependencies run: | cp -av eform-angular-outer-inner-resource-plugin/eform-client/src/app/plugins/modules/outer-inner-resource-pn eform-angular-frontend/eform-client/src/app/plugins/modules/outer-inner-resource-pn - cp -av eform-angular-outer-inner-resource-plugin/eform-client/e2e/Tests/outer-inner-resource-settings eform-angular-frontend/eform-client/e2e/Tests/outer-inner-resource-settings - cp -av eform-angular-outer-inner-resource-plugin/eform-client/e2e/Tests/outer-inner-resource-general eform-angular-frontend/eform-client/e2e/Tests/outer-inner-resource-general - cp -av eform-angular-outer-inner-resource-plugin/eform-client/e2e/Page\ objects/OuterInnerResource eform-angular-frontend/eform-client/e2e/Page\ objects/OuterInnerResource - cp -av eform-angular-outer-inner-resource-plugin/eform-client/wdio-headless-plugin-step2.conf.ts eform-angular-frontend/eform-client/wdio-headless-plugin-step2.conf.ts - cp -av eform-angular-outer-inner-resource-plugin/eform-client/wdio-plugin-step2.conf.ts eform-angular-frontend/eform-client/wdio-plugin-step2.conf.ts + mkdir -p eform-angular-frontend/eform-client/playwright/e2e/plugins/ + cp -av eform-angular-outer-inner-resource-plugin/eform-client/playwright/e2e/plugins/outer-inner-resource-pn eform-angular-frontend/eform-client/playwright/e2e/plugins/outer-inner-resource-pn + cp -av eform-angular-outer-inner-resource-plugin/eform-client/playwright.config.ts eform-angular-frontend/eform-client/playwright.config.ts mkdir -p eform-angular-frontend/eFormAPI/eFormAPI.Web/Plugins cd eform-angular-frontend/eform-client && ../../eform-angular-outer-inner-resource-plugin/testinginstallpn.sh - name: Start the newly build Docker container @@ -104,26 +96,34 @@ jobs: - name: Get standard output run: cat docker_run_log - name: Pretest changes to work with Docker container - run: sed -i 's/localhost/mariadbtest/g' eform-angular-frontend/eform-client/e2e/Constants/DatabaseConfigurationConstants.ts + run: sed -i 's/localhost/mariadbtest/g' eform-angular-frontend/eform-client/playwright/e2e/Constants/DatabaseConfigurationConstants.ts - name: yarn install run: cd eform-angular-frontend/eform-client && yarn install - if: steps.cache.outputs.cache-hit != 'true' + - name: Install Playwright browsers + run: cd eform-angular-frontend/eform-client && npx playwright install --with-deps chromium + - name: Wait for app + run: npx wait-on http://localhost:4200 --timeout 120000 - name: DB Configuration - uses: cypress-io/github-action@v4 - with: - start: echo 'hi' - wait-on: "http://localhost:4200" - wait-on-timeout: 120 - browser: chrome - record: false - spec: cypress/e2e/db/* - config-file: cypress.config.ts - working-directory: eform-angular-frontend/eform-client - command-prefix: "--" + run: cd eform-angular-frontend/eform-client && npx playwright test playwright/e2e/Tests/database-configuration/ - name: Change rabbitmq hostname run: docker exec -i mariadbtest mysql -u root --password=secretpassword -e 'update 420_SDK.Settings set Value = "my-rabbit" where Name = "rabbitMqHost"' - - name: Plugin testing - run: cd eform-angular-frontend/eform-client && npm run testheadlessplugin + - name: Get standard output + run: | + cat docker_run_log + result=`cat docker_run_log | grep "Now listening on: http://0.0.0.0:5000" -m 1 | wc -l` + if [ $result -ne 1 ];then exit 1; fi + - name: Enable plugin + if: matrix.test != 'a' + run: | + docker exec -i mariadbtest mysql -u root --password=secretpassword -e 'update 420_Angular.EformPlugins set Status = 2' + docker restart my-container + sleep 15 + - name: Wait for app + run: npx wait-on http://localhost:4200 --timeout 120000 + - name: ${{ matrix.test }} playwright test + run: | + cd eform-angular-frontend/eform-client + npx playwright test playwright/e2e/plugins/outer-inner-resource-pn/${{ matrix.test }}/ - name: Stop the newly build Docker container run: docker stop my-container - name: Get standard output @@ -131,10 +131,16 @@ jobs: cat docker_run_log result=`cat docker_run_log | grep "Now listening on: http://0.0.0.0:5000" -m 1 | wc -l` if [ $result -ne 1 ];then exit 1; fi - - name: The job has failed + - name: Get standard output if: ${{ failure() }} - run: | - cat docker_run_log + run: cat docker_run_log + - name: Archive Playwright report + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report-${{ matrix.test }} + path: eform-angular-frontend/eform-client/playwright-report/ + retention-days: 2 outer-inner-resource-dotnet-test: runs-on: ubuntu-latest steps: diff --git a/eform-client/playwright.config.ts b/eform-client/playwright.config.ts new file mode 100644 index 00000000..fa40c578 --- /dev/null +++ b/eform-client/playwright.config.ts @@ -0,0 +1,21 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './playwright/e2e', + fullyParallel: false, + workers: 1, + timeout: 120_000, + use: { + baseURL: 'http://localhost:4200', + viewport: { width: 1920, height: 1080 }, + video: 'retain-on-failure', + screenshot: 'only-on-failure', + trace: 'retain-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/eform-client/playwright/e2e/plugins/outer-inner-resource-pn/OuterInnerResourceInnerResource.page.ts b/eform-client/playwright/e2e/plugins/outer-inner-resource-pn/OuterInnerResourceInnerResource.page.ts new file mode 100644 index 00000000..72280162 --- /dev/null +++ b/eform-client/playwright/e2e/plugins/outer-inner-resource-pn/OuterInnerResourceInnerResource.page.ts @@ -0,0 +1,155 @@ +import { Page, Locator } from '@playwright/test'; +import { OuterInnerResourceModalPage } from './OuterInnerResourceModal.page'; + +export class OuterInnerResourceInnerResourcePage { + public modalPage: OuterInnerResourceModalPage; + + constructor(public page: Page) { + this.modalPage = new OuterInnerResourceModalPage(page); + } + + public async rowNum(): Promise { + await this.page.waitForTimeout(500); + return await this.page.locator('tbody > tr').count(); + } + + public outerInnerResourceDropdownMenu(): Locator { + return this.page.locator('#outer-inner-resource-pn'); + } + + public innerResourceMenuPoint(): Locator { + return this.page.locator('#outer-inner-resource-pn-inner-resources'); + } + + public newInnerResourceBtn(): Locator { + return this.page.locator('#newInnerResource'); + } + + async goToInnerResource() { + await this.outerInnerResourceDropdownMenu().waitFor({ state: 'visible', timeout: 20000 }); + await this.outerInnerResourceDropdownMenu().click(); + await this.innerResourceMenuPoint().waitFor({ state: 'visible', timeout: 20000 }); + await this.innerResourceMenuPoint().click(); + await this.newInnerResourceBtn().waitFor({ state: 'visible', timeout: 20000 }); + } + + async getInnerObjectByName(name: string): Promise { + await this.page.waitForTimeout(500); + const count = await this.rowNum(); + for (let i = 1; i <= count; i++) { + const listRowObject = await this.getRowObject(i); + if (listRowObject.name === name) { + return listRowObject; + } + } + return null; + } + + async getRowObject(rowNum: number): Promise { + const obj = new InnerResourceListRowObject(this); + return await obj.getRow(rowNum); + } + + async openCreateModal(name?: string, externalId?: number | string) { + await this.newInnerResourceBtn().click(); + if (name) { + await this.modalPage.innerResourceCreateNameInput().waitFor({ state: 'visible', timeout: 20000 }); + await this.modalPage.innerResourceCreateNameInput().fill(name); + } + if (externalId) { + await this.modalPage.createInnerResourceId().waitFor({ state: 'visible', timeout: 20000 }); + await this.modalPage.createInnerResourceId().fill(externalId.toString()); + } + } + + async closeCreateModal(clickCancel = false) { + if (!clickCancel) { + await this.modalPage.innerResourceCreateSaveBtn().click(); + await this.page.locator('#spinner-animation').waitFor({ state: 'hidden', timeout: 20000 }); + } else { + await this.modalPage.innerResourceCreateCancelBtn().click(); + await this.page.locator('#spinner-animation').waitFor({ state: 'hidden', timeout: 20000 }); + await this.newInnerResourceBtn().waitFor({ state: 'visible', timeout: 20000 }); + } + } + + async createNewInnerResource(name?: string, externalId?: number | string, clickCancel = false) { + await this.openCreateModal(name, externalId); + await this.closeCreateModal(clickCancel); + } +} + +export class InnerResourceListRowObject { + public id: number; + public name: string; + public externalId: number; + public rowLocator: Locator; + private pageObj: OuterInnerResourceInnerResourcePage; + + constructor(pageObj: OuterInnerResourceInnerResourcePage) { + this.pageObj = pageObj; + } + + async getRow(rowNum: number): Promise { + this.rowLocator = this.pageObj.page.locator('tbody > tr').nth(rowNum - 1); + if (await this.rowLocator.count() > 0) { + try { + this.id = +(await this.rowLocator.locator('.mat-column-id').innerText()); + } catch (e) {} + try { + this.name = await this.rowLocator.locator('.mat-column-name').innerText(); + } catch (e) {} + try { + this.externalId = +(await this.rowLocator.locator('.mat-column-externalId').innerText()); + } catch (e) {} + } + return this; + } + + async openDeleteModal() { + await this.rowLocator.locator('.mat-column-actions button').nth(1).click(); + await this.pageObj.modalPage.innerResourceDeleteDeleteBtn().waitFor({ state: 'visible', timeout: 20000 }); + } + + async closeDeleteModal(clickCancel = false) { + if (!clickCancel) { + await this.pageObj.modalPage.innerResourceDeleteDeleteBtn().click(); + await this.pageObj.page.locator('#spinner-animation').waitFor({ state: 'hidden', timeout: 20000 }); + } else { + await this.pageObj.modalPage.innerResourceDeleteCancelBtn().click(); + await this.pageObj.newInnerResourceBtn().waitFor({ state: 'visible', timeout: 20000 }); + } + } + + async delete(clickCancel = false) { + await this.openDeleteModal(); + await this.closeDeleteModal(clickCancel); + } + + async openEditModal(newName?: string, newExternalId?: number | string) { + await this.rowLocator.locator('.mat-column-actions button').nth(0).click(); + if (newName) { + await this.pageObj.modalPage.innerResourceEditName().waitFor({ state: 'visible', timeout: 20000 }); + await this.pageObj.modalPage.innerResourceEditName().fill(newName); + } + if (newExternalId) { + await this.pageObj.modalPage.innerResourceEditExternalIdInput().waitFor({ state: 'visible', timeout: 20000 }); + await this.pageObj.modalPage.innerResourceEditExternalIdInput().fill(newExternalId.toString()); + } + } + + async closeEditModal(clickCancel = false) { + if (!clickCancel) { + await this.pageObj.modalPage.innerResourceEditSaveBtn().click(); + await this.pageObj.page.locator('#spinner-animation').waitFor({ state: 'hidden', timeout: 20000 }); + } else { + await this.pageObj.modalPage.innerResourceEditCancelBtn().click(); + await this.pageObj.newInnerResourceBtn().waitFor({ state: 'visible', timeout: 20000 }); + } + } + + async edit(newName?: string, newExternalId?: number | string, clickCancel = false) { + await this.openEditModal(newName, newExternalId); + await this.closeEditModal(clickCancel); + } +} diff --git a/eform-client/playwright/e2e/plugins/outer-inner-resource-pn/OuterInnerResourceModal.page.ts b/eform-client/playwright/e2e/plugins/outer-inner-resource-pn/OuterInnerResourceModal.page.ts new file mode 100644 index 00000000..a203f462 --- /dev/null +++ b/eform-client/playwright/e2e/plugins/outer-inner-resource-pn/OuterInnerResourceModal.page.ts @@ -0,0 +1,87 @@ +import { Page, Locator } from '@playwright/test'; + +export class OuterInnerResourceModalPage { + constructor(public page: Page) {} + + // Outer Resource - Create + public outerResourceCreateNameInput(): Locator { + return this.page.locator('#createOuterResourceName'); + } + + public outerResourceCreateSaveBtn(): Locator { + return this.page.locator('#outerResourceCreateSaveBtn'); + } + + public outerResourceCreateCancelBtn(): Locator { + return this.page.locator('#outerResourceCreateCancelBtn'); + } + + public createOuterResourceExternalId(): Locator { + return this.page.locator('#createOuterResourceExternalId'); + } + + // Outer Resource - Edit + public outerResourceEditNameInput(): Locator { + return this.page.locator('#updateOuterResourceName'); + } + + public outerResourceEditSaveBtn(): Locator { + return this.page.locator('#outerResourceEditSaveBtn'); + } + + public outerResourceEditCancelBtn(): Locator { + return this.page.locator('#outerResourceEditCancelBtn'); + } + + // Outer Resource - Delete + public outerResourceDeleteDeleteBtn(): Locator { + return this.page.locator('#outerResourceDeleteDeleteBtn'); + } + + public outerResourceDeleteCancelBtn(): Locator { + return this.page.locator('#outerResourceDeleteCancelBtn'); + } + + // Inner Resource - Create + public innerResourceCreateNameInput(): Locator { + return this.page.locator('#createInnerResourceName'); + } + + public innerResourceCreateSaveBtn(): Locator { + return this.page.locator('#innerResourceCreateSaveBtn'); + } + + public innerResourceCreateCancelBtn(): Locator { + return this.page.locator('#innerResourceCreateCancelBtn'); + } + + public createInnerResourceId(): Locator { + return this.page.locator('#createInnerResourceId'); + } + + // Inner Resource - Edit + public innerResourceEditName(): Locator { + return this.page.locator('#updateInnerResourceName'); + } + + public innerResourceEditSaveBtn(): Locator { + return this.page.locator('#innerResourceEditSaveBtn'); + } + + public innerResourceEditCancelBtn(): Locator { + return this.page.locator('#innerResourceEditCancelBtn'); + } + + public innerResourceEditExternalIdInput(): Locator { + return this.page.locator('#updateInnerResourceExternalId'); + } + + // Inner Resource - Delete + public innerResourceDeleteDeleteBtn(): Locator { + return this.page.locator('#innerResourceDeleteDeleteBtn'); + } + + public innerResourceDeleteCancelBtn(): Locator { + return this.page.locator('#innerResourceDeleteCancelBtn'); + } +} diff --git a/eform-client/playwright/e2e/plugins/outer-inner-resource-pn/OuterInnerResourceOuterResource.page.ts b/eform-client/playwright/e2e/plugins/outer-inner-resource-pn/OuterInnerResourceOuterResource.page.ts new file mode 100644 index 00000000..226af605 --- /dev/null +++ b/eform-client/playwright/e2e/plugins/outer-inner-resource-pn/OuterInnerResourceOuterResource.page.ts @@ -0,0 +1,155 @@ +import { Page, Locator } from '@playwright/test'; +import { OuterInnerResourceModalPage } from './OuterInnerResourceModal.page'; + +export class OuterInnerResourceOuterResourcePage { + public modalPage: OuterInnerResourceModalPage; + + constructor(public page: Page) { + this.modalPage = new OuterInnerResourceModalPage(page); + } + + public async rowNum(): Promise { + await this.page.waitForTimeout(500); + return await this.page.locator('tbody > tr').count(); + } + + public outerInnerResourceDropdownMenu(): Locator { + return this.page.locator('#outer-inner-resource-pn'); + } + + public outerResourceMenuPoint(): Locator { + return this.page.locator('#outer-inner-resource-pn-outer-resources'); + } + + public newOuterResourceBtn(): Locator { + return this.page.locator('#newOuterResourceBtn'); + } + + async goToOuterResource() { + await this.outerInnerResourceDropdownMenu().waitFor({ state: 'visible', timeout: 20000 }); + await this.outerInnerResourceDropdownMenu().click(); + await this.outerResourceMenuPoint().waitFor({ state: 'visible', timeout: 20000 }); + await this.outerResourceMenuPoint().click(); + await this.newOuterResourceBtn().waitFor({ state: 'visible', timeout: 20000 }); + } + + async getOuterObjectByName(name: string): Promise { + await this.page.waitForTimeout(500); + const count = await this.rowNum(); + for (let i = 1; i <= count; i++) { + const listRowObject = await this.getRowObject(i); + if (listRowObject.name === name) { + return listRowObject; + } + } + return null; + } + + async getRowObject(rowNum: number): Promise { + const obj = new OuterResourceListRowObject(this); + return await obj.getRow(rowNum); + } + + async openCreateModal(name?: string, externalId?: number | string) { + await this.newOuterResourceBtn().click(); + if (name) { + await this.modalPage.outerResourceCreateNameInput().waitFor({ state: 'visible', timeout: 20000 }); + await this.modalPage.outerResourceCreateNameInput().fill(name); + } + if (externalId) { + await this.modalPage.createOuterResourceExternalId().waitFor({ state: 'visible', timeout: 20000 }); + await this.modalPage.createOuterResourceExternalId().fill(externalId.toString()); + } + } + + async closeCreateModal(clickCancel = false) { + if (!clickCancel) { + await this.modalPage.outerResourceCreateSaveBtn().click(); + await this.page.locator('#spinner-animation').waitFor({ state: 'hidden', timeout: 20000 }); + } else { + await this.modalPage.outerResourceCreateCancelBtn().click(); + await this.page.locator('#spinner-animation').waitFor({ state: 'hidden', timeout: 20000 }); + await this.newOuterResourceBtn().waitFor({ state: 'visible', timeout: 20000 }); + } + } + + async createNewOuterResource(name?: string, externalId?: number | string, clickCancel = false) { + await this.openCreateModal(name, externalId); + await this.closeCreateModal(clickCancel); + } +} + +export class OuterResourceListRowObject { + public id: number; + public name: string; + public externalId: number; + public rowLocator: Locator; + private pageObj: OuterInnerResourceOuterResourcePage; + + constructor(pageObj: OuterInnerResourceOuterResourcePage) { + this.pageObj = pageObj; + } + + async getRow(rowNum: number): Promise { + this.rowLocator = this.pageObj.page.locator('tbody > tr').nth(rowNum - 1); + if (await this.rowLocator.count() > 0) { + try { + this.id = +(await this.rowLocator.locator('.mat-column-id').innerText()); + } catch (e) {} + try { + this.name = await this.rowLocator.locator('.mat-column-name').innerText(); + } catch (e) {} + try { + this.externalId = +(await this.rowLocator.locator('.mat-column-externalId').innerText()); + } catch (e) {} + } + return this; + } + + async openDeleteModal() { + await this.rowLocator.locator('.mat-column-actions button').nth(1).click(); + await this.pageObj.modalPage.outerResourceDeleteDeleteBtn().waitFor({ state: 'visible', timeout: 20000 }); + } + + async closeDeleteModal(clickCancel = false) { + if (!clickCancel) { + await this.pageObj.modalPage.outerResourceDeleteDeleteBtn().click(); + await this.pageObj.page.locator('#spinner-animation').waitFor({ state: 'hidden', timeout: 20000 }); + } else { + await this.pageObj.modalPage.outerResourceDeleteCancelBtn().click(); + await this.pageObj.newOuterResourceBtn().waitFor({ state: 'visible', timeout: 20000 }); + } + } + + async delete(clickCancel = false) { + await this.openDeleteModal(); + await this.closeDeleteModal(clickCancel); + } + + async openEditModal(newName?: string, newExternalId?: number | string) { + await this.rowLocator.locator('.mat-column-actions button').nth(0).click(); + if (newName) { + await this.pageObj.modalPage.outerResourceEditNameInput().waitFor({ state: 'visible', timeout: 20000 }); + await this.pageObj.modalPage.outerResourceEditNameInput().fill(newName); + } + if (newExternalId) { + await this.pageObj.modalPage.createOuterResourceExternalId().waitFor({ state: 'visible', timeout: 20000 }); + await this.pageObj.modalPage.createOuterResourceExternalId().fill(newExternalId.toString()); + } + } + + async closeEditModal(clickCancel = false) { + if (!clickCancel) { + await this.pageObj.modalPage.outerResourceEditSaveBtn().click(); + await this.pageObj.page.locator('#spinner-animation').waitFor({ state: 'hidden', timeout: 20000 }); + } else { + await this.pageObj.modalPage.outerResourceEditCancelBtn().click(); + await this.pageObj.newOuterResourceBtn().waitFor({ state: 'visible', timeout: 20000 }); + } + } + + async edit(newName?: string, newExternalId?: number | string, clickCancel = false) { + await this.openEditModal(newName, newExternalId); + await this.closeEditModal(clickCancel); + } +} diff --git a/eform-client/playwright/e2e/plugins/outer-inner-resource-pn/a/outer-inner-resource-settings.spec.ts b/eform-client/playwright/e2e/plugins/outer-inner-resource-pn/a/outer-inner-resource-settings.spec.ts new file mode 100644 index 00000000..53537c8e --- /dev/null +++ b/eform-client/playwright/e2e/plugins/outer-inner-resource-pn/a/outer-inner-resource-settings.spec.ts @@ -0,0 +1,41 @@ +import { test, expect } from '@playwright/test'; +import { LoginPage } from '../../../Page objects/Login.page'; +import { MyEformsPage } from '../../../Page objects/MyEforms.page'; +import { PluginPage } from '../../../Page objects/Plugin.page'; + +const pluginName = 'Microting Outer Inner Resource plugin'; +let page; + +test.describe('Application settings page - site header section', () => { + test.beforeAll(async ({ browser }) => { + page = await browser.newPage(); + }); + test.afterAll(async () => { + await page.close(); + }); + + test('should go to plugin settings page', async () => { + const loginPage = new LoginPage(page); + const myEformsPage = new MyEformsPage(page); + const pluginPage = new PluginPage(page); + await loginPage.open('/auth'); + await loginPage.login(); + await myEformsPage.Navbar.goToPluginsPage(); + const plugin = await pluginPage.getPluginRowObjByName(pluginName); + expect(plugin).not.toBeNull(); + expect(plugin!.name.trim()).toBe(pluginName); + expect(plugin!.status.trim()).toBe('toggle_off'); + }); + + test('should activate the plugin', async () => { + test.setTimeout(240000); + const pluginPage = new PluginPage(page); + const plugin = await pluginPage.getPluginRowObjByName(pluginName); + expect(plugin).not.toBeNull(); + await plugin!.enableOrDisablePlugin(); + const pluginAfter = await pluginPage.getPluginRowObjByName(pluginName); + expect(pluginAfter).not.toBeNull(); + expect(pluginAfter!.name.trim()).toBe(pluginName); + expect(pluginAfter!.status.trim()).toBe('toggle_on'); + }); +}); diff --git a/eform-client/playwright/e2e/plugins/outer-inner-resource-pn/b/outer-inner-resource-inner-resource.spec.ts b/eform-client/playwright/e2e/plugins/outer-inner-resource-pn/b/outer-inner-resource-inner-resource.spec.ts new file mode 100644 index 00000000..5a80d26a --- /dev/null +++ b/eform-client/playwright/e2e/plugins/outer-inner-resource-pn/b/outer-inner-resource-inner-resource.spec.ts @@ -0,0 +1,93 @@ +import { test, expect } from '@playwright/test'; +import { LoginPage } from '../../../Page objects/Login.page'; +import { OuterInnerResourceInnerResourcePage } from '../OuterInnerResourceInnerResource.page'; +import { OuterInnerResourceModalPage } from '../OuterInnerResourceModal.page'; + +let page; +let innerResourcePage: OuterInnerResourceInnerResourcePage; +let modalPage: OuterInnerResourceModalPage; + +const newNameInnerResource = Math.random().toString(36).substring(7); +const nameForDeleteTest = Math.random().toString(36).substring(7); +const nameForEditTest = Math.random().toString(36).substring(7); + +test.describe('Outer Inner Resource - Inner Resources', () => { + test.beforeAll(async ({ browser }) => { + page = await browser.newPage(); + innerResourcePage = new OuterInnerResourceInnerResourcePage(page); + modalPage = new OuterInnerResourceModalPage(page); + const loginPage = new LoginPage(page); + await loginPage.open('/auth'); + await loginPage.login(); + await innerResourcePage.goToInnerResource(); + }); + + test.afterAll(async () => { + await page.close(); + }); + + // Add tests + test('should not create a new inner resource without name', async () => { + const rowNumBeforeCreate = await innerResourcePage.rowNum(); + await innerResourcePage.openCreateModal(); + expect(await modalPage.innerResourceCreateSaveBtn().isEnabled()).toBe(false); + await innerResourcePage.closeCreateModal(true); + expect(await innerResourcePage.rowNum()).toBe(rowNumBeforeCreate); + }); + + test('should add inner resource with only name', async () => { + const rowNumBeforeCreate = await innerResourcePage.rowNum(); + await innerResourcePage.createNewInnerResource(newNameInnerResource); + expect(await innerResourcePage.rowNum()).toBe(rowNumBeforeCreate + 1); + const listRowObject = await innerResourcePage.getInnerObjectByName(newNameInnerResource); + expect(listRowObject).not.toBeNull(); + expect(listRowObject!.name).toBe(newNameInnerResource); + }); + + test('should clean up after add test', async () => { + const listRowObject = await innerResourcePage.getInnerObjectByName(newNameInnerResource); + expect(listRowObject).not.toBeNull(); + await listRowObject!.delete(); + }); + + // Edit tests + test('should create inner resource for edit test', async () => { + await innerResourcePage.createNewInnerResource(nameForEditTest); + const listRowObject = await innerResourcePage.getInnerObjectByName(nameForEditTest); + expect(listRowObject).not.toBeNull(); + }); + + // TODO: Can't change name - edit test commented out in original + // test('should edit inner resource', async () => { + // ... + // }); + + test('should clean up after edit test', async () => { + const listRowObject = await innerResourcePage.getInnerObjectByName(nameForEditTest); + expect(listRowObject).not.toBeNull(); + await listRowObject!.delete(); + }); + + // Delete tests + test('should create inner resource for delete test', async () => { + await innerResourcePage.createNewInnerResource(nameForDeleteTest); + const listRowObject = await innerResourcePage.getInnerObjectByName(nameForDeleteTest); + expect(listRowObject).not.toBeNull(); + }); + + test('should not delete inner resource when cancelling', async () => { + const rowNumBeforeDelete = await innerResourcePage.rowNum(); + const listRowObject = await innerResourcePage.getInnerObjectByName(nameForDeleteTest); + expect(listRowObject).not.toBeNull(); + await listRowObject!.delete(true); + expect(await innerResourcePage.rowNum()).toBe(rowNumBeforeDelete); + }); + + test('should delete inner resource', async () => { + const rowNumBeforeDelete = await innerResourcePage.rowNum(); + const listRowObject = await innerResourcePage.getInnerObjectByName(nameForDeleteTest); + expect(listRowObject).not.toBeNull(); + await listRowObject!.delete(); + expect(await innerResourcePage.rowNum()).toBe(rowNumBeforeDelete - 1); + }); +}); diff --git a/eform-client/playwright/e2e/plugins/outer-inner-resource-pn/b/outer-inner-resource-outer-resource.spec.ts b/eform-client/playwright/e2e/plugins/outer-inner-resource-pn/b/outer-inner-resource-outer-resource.spec.ts new file mode 100644 index 00000000..0b38b9b4 --- /dev/null +++ b/eform-client/playwright/e2e/plugins/outer-inner-resource-pn/b/outer-inner-resource-outer-resource.spec.ts @@ -0,0 +1,95 @@ +import { test, expect } from '@playwright/test'; +import { LoginPage } from '../../../Page objects/Login.page'; +import { OuterInnerResourceOuterResourcePage } from '../OuterInnerResourceOuterResource.page'; +import { OuterInnerResourceModalPage } from '../OuterInnerResourceModal.page'; + +let page; +let outerResourcePage: OuterInnerResourceOuterResourcePage; +let modalPage: OuterInnerResourceModalPage; + +const newNameOuterResource = Math.random().toString(36).substring(7); +const nameForDeleteTest = Math.random().toString(36).substring(7); +const nameForEditTest = Math.random().toString(36).substring(7); + +test.describe('Outer Inner Resource - Outer Resources', () => { + test.beforeAll(async ({ browser }) => { + page = await browser.newPage(); + outerResourcePage = new OuterInnerResourceOuterResourcePage(page); + modalPage = new OuterInnerResourceModalPage(page); + const loginPage = new LoginPage(page); + await loginPage.open('/auth'); + await loginPage.login(); + await outerResourcePage.goToOuterResource(); + }); + + test.afterAll(async () => { + await page.close(); + }); + + // Add tests + test('should add outer resource with only name', async () => { + const rowNumBeforeCreate = await outerResourcePage.rowNum(); + await outerResourcePage.createNewOuterResource(newNameOuterResource); + expect(await outerResourcePage.rowNum()).toBe(rowNumBeforeCreate + 1); + const listRowObject = await outerResourcePage.getOuterObjectByName(newNameOuterResource); + expect(listRowObject).not.toBeNull(); + expect(listRowObject!.name).toBe(newNameOuterResource); + }); + + test('should not create outer resource without name', async () => { + const rowNumBeforeCreate = await outerResourcePage.rowNum(); + await outerResourcePage.openCreateModal(); + expect(await modalPage.outerResourceCreateSaveBtn().isEnabled()).toBe(false); + await outerResourcePage.closeCreateModal(true); + expect(await outerResourcePage.rowNum()).toBe(rowNumBeforeCreate); + }); + + test('should clean up after add test', async () => { + const listRowObject = await outerResourcePage.getOuterObjectByName(newNameOuterResource); + expect(listRowObject).not.toBeNull(); + await listRowObject!.delete(); + }); + + // Edit tests + test('should create outer resource for edit test', async () => { + await outerResourcePage.createNewOuterResource(nameForEditTest); + const listRowObject = await outerResourcePage.getOuterObjectByName(nameForEditTest); + expect(listRowObject).not.toBeNull(); + }); + + // TODO: Can't change name - edit test commented out in original + // test('should edit outer resource', async () => { + // ... + // }); + + test('should clean up after edit test', async () => { + const rowNumBeforeDelete = await outerResourcePage.rowNum(); + const listRowObject = await outerResourcePage.getOuterObjectByName(nameForEditTest); + expect(listRowObject).not.toBeNull(); + await listRowObject!.delete(); + expect(await outerResourcePage.rowNum()).toBe(rowNumBeforeDelete - 1); + }); + + // Delete tests + test('should create outer resource for delete test', async () => { + await outerResourcePage.createNewOuterResource(nameForDeleteTest); + const listRowObject = await outerResourcePage.getOuterObjectByName(nameForDeleteTest); + expect(listRowObject).not.toBeNull(); + }); + + test('should not delete outer resource when cancelling', async () => { + const rowNumBeforeDelete = await outerResourcePage.rowNum(); + const listRowObject = await outerResourcePage.getOuterObjectByName(nameForDeleteTest); + expect(listRowObject).not.toBeNull(); + await listRowObject!.delete(true); + expect(await outerResourcePage.rowNum()).toBe(rowNumBeforeDelete); + }); + + test('should delete outer resource', async () => { + const rowNumBeforeDelete = await outerResourcePage.rowNum(); + const listRowObject = await outerResourcePage.getOuterObjectByName(nameForDeleteTest); + expect(listRowObject).not.toBeNull(); + await listRowObject!.delete(); + expect(await outerResourcePage.rowNum()).toBe(rowNumBeforeDelete - 1); + }); +}); From f5aee9b9df6adee24925f8c02fc2a97cbad15167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Schultz=20Madsen?= Date: Mon, 6 Apr 2026 07:45:20 +0200 Subject: [PATCH 2/2] fix(ci): update startup log grep for HTTP/2 compatibility Kestrel now uses Http1AndHttp2 protocol mode, which may change the listening URL format. Use wildcard grep pattern to match regardless of protocol scheme, consistent with eform-angular-frontend CI. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/dotnet-core-master.yml | 4 ++-- .github/workflows/dotnet-core-pr.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dotnet-core-master.yml b/.github/workflows/dotnet-core-master.yml index a1860f35..68d751b0 100644 --- a/.github/workflows/dotnet-core-master.yml +++ b/.github/workflows/dotnet-core-master.yml @@ -116,7 +116,7 @@ jobs: - name: Get standard output run: | cat docker_run_log - result=`cat docker_run_log | grep "Now listening on: http://0.0.0.0:5000" -m 1 | wc -l` + result=`cat docker_run_log | grep "Now listening on:.*:5000" -m 1 | wc -l` if [ $result -ne 1 ];then exit 1; fi - name: Enable plugin if: matrix.test != 'a' @@ -135,7 +135,7 @@ jobs: - name: Get standard output run: | cat docker_run_log - result=`cat docker_run_log | grep "Now listening on: http://0.0.0.0:5000" -m 1 | wc -l` + result=`cat docker_run_log | grep "Now listening on:.*:5000" -m 1 | wc -l` if [ $result -ne 1 ];then exit 1; fi - name: Get standard output if: ${{ failure() }} diff --git a/.github/workflows/dotnet-core-pr.yml b/.github/workflows/dotnet-core-pr.yml index e8d188ec..f5b04f22 100644 --- a/.github/workflows/dotnet-core-pr.yml +++ b/.github/workflows/dotnet-core-pr.yml @@ -110,7 +110,7 @@ jobs: - name: Get standard output run: | cat docker_run_log - result=`cat docker_run_log | grep "Now listening on: http://0.0.0.0:5000" -m 1 | wc -l` + result=`cat docker_run_log | grep "Now listening on:.*:5000" -m 1 | wc -l` if [ $result -ne 1 ];then exit 1; fi - name: Enable plugin if: matrix.test != 'a' @@ -129,7 +129,7 @@ jobs: - name: Get standard output run: | cat docker_run_log - result=`cat docker_run_log | grep "Now listening on: http://0.0.0.0:5000" -m 1 | wc -l` + result=`cat docker_run_log | grep "Now listening on:.*:5000" -m 1 | wc -l` if [ $result -ne 1 ];then exit 1; fi - name: Get standard output if: ${{ failure() }}