From ac6a48308ffb00312e2675bf466bccd7f06d42fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Ry=C5=A1ka?= Date: Mon, 18 Nov 2024 14:13:43 +0100 Subject: [PATCH 1/3] E2E tests with associated ci.yml changes --- .github/workflows/ci.yml | 27 ++- test/Playwright/.gitignore | 5 + test/Playwright/README.md | 62 ++++++ test/Playwright/package-lock.json | 97 ++++++++++ test/Playwright/package.json | 14 ++ test/Playwright/pageObjects/BasePage.ts | 36 ++++ test/Playwright/pageObjects/CheckoutPage.ts | 61 ++++++ test/Playwright/playwright.config.ts | 91 +++++++++ test/Playwright/tests/order.spec.ts | 200 ++++++++++++++++++++ 9 files changed, 592 insertions(+), 1 deletion(-) create mode 100644 test/Playwright/.gitignore create mode 100644 test/Playwright/README.md create mode 100644 test/Playwright/package-lock.json create mode 100644 test/Playwright/package.json create mode 100644 test/Playwright/pageObjects/BasePage.ts create mode 100644 test/Playwright/pageObjects/CheckoutPage.ts create mode 100644 test/Playwright/playwright.config.ts create mode 100644 test/Playwright/tests/order.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1b487b..b0ef5bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,7 @@ jobs: CMSSHOPIFYCONFIG__SHOPIFYURL: ${{ secrets.SHOPIFY_URL }} CMSSHOPIFYCONFIG__STOREFRONTAPIKEY: ${{ secrets.STOREFRONT_API_KEY }} CMSSHOPIFYCONFIG__STOREFRONTAPIVERSION: ${{ secrets.STOREFRONT_API_VERSION }} + CMSSHOPIFYCONFIG__STOREPASSWORD: ${{ secrets.SHOPIFY_STORE_PASSWORD }} steps: - uses: actions/checkout@v4 @@ -57,6 +58,18 @@ jobs: with: global-json-file: global.json + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: lts/* + + - name: Install NPM + run: | + cd test/Playwright + npm ci + npx playwright install --with-deps + cd ../.. + - name: Install dependencies run: | dotnet restore ` @@ -170,9 +183,21 @@ jobs: exit 1 } - # TODO: Run the E2E tests + # Sleep for finishing initialization + Start-Sleep -Seconds 10 + + # Run the E2E tests + cd test/Playwright + npx playwright test + cd ../.. # Stop the background ASP.NET Core application Receive-Job -Name ${{ env.PROJECT_NAME }} Stop-Job -Name ${{ env.PROJECT_NAME }} Remove-Job -Name ${{ env.PROJECT_NAME }} + + - uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: ./test/Playwright/playwright-report/ + retention-days: 30 diff --git a/test/Playwright/.gitignore b/test/Playwright/.gitignore new file mode 100644 index 0000000..68c5d18 --- /dev/null +++ b/test/Playwright/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/test/Playwright/README.md b/test/Playwright/README.md new file mode 100644 index 0000000..5e2dd23 --- /dev/null +++ b/test/Playwright/README.md @@ -0,0 +1,62 @@ +# Playwright E2E tests + +## Description + +E2E tests are implemented using Playwright - an open-source, NodeJS-based framework for web testing and automation. + +To prevent false-positives, tests are configured to run maximum of 2 times based on the result of the 1st test. Following combinations apply. + +- test PASSES the 1st run -> Test **PASSED** +- test FAILS the 1st run, retries and PASSES the 2nd run -> Test **PASSED** with **FLAKY** signature +- test FAILS the 1st, retries and FAILS the 2nd run -> Test **FAILED** + +## Install dependencies + +`npm i` + +`npx playwright install --with-deps` + +## Run tests + +Following env variables are expected to be set + +- ASPNETCORE_URLS = Base url of the app +- CMSSHOPIFYCONFIG\_\_ADMINAPIKEY = Shopify API access token +- CMSSHOPIFYCONFIG\_\_SHOPIFYURL = Shopify store URL +- CMSSHOPIFYCONFIG\_\_STOREPASSWORD = Shopify store password + +Run the tests with command + +`npx playwright test` + +## Test results + +Test results are generated whether the tests pass or fail. + +#### HTML + +HTML test result can be opened using + +`npx playwright show-report` + +#### JUnit + +Test results are generated also in JUnit format, for possible integration with other reporting systems. XML file can be found in `test-results/e2e-junit-results.xml` + +#### Artifacts + +In case of a test failure, following artifacts are created + +- Screenshot of the last application state (point of failure) - generates on every fail +- Video of full test run - generates on every fail +- Trace - generates on first retry + +Trace contains enhanced information to ease debugging failures. It can be opened with Trace Viewer (GUI tool) locally or using hosted variant by Playwright where it is possible to upload trace files using drag and drop. + +To open local Trace Viewer use + +`npx playwright show-trace path/to/trace.zip` + +Hosted variant by Playwright can be found at + +[trace.playwright.dev](https://trace.playwright.dev/) diff --git a/test/Playwright/package-lock.json b/test/Playwright/package-lock.json new file mode 100644 index 0000000..e24e4cf --- /dev/null +++ b/test/Playwright/package-lock.json @@ -0,0 +1,97 @@ +{ + "name": "playwright", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "playwright", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.48.2", + "@types/node": "^22.9.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz", + "integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.48.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz", + "integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.48.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz", + "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/test/Playwright/package.json b/test/Playwright/package.json new file mode 100644 index 0000000..ac4d7f8 --- /dev/null +++ b/test/Playwright/package.json @@ -0,0 +1,14 @@ +{ + "name": "playwright", + "version": "1.0.0", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@playwright/test": "^1.48.2", + "@types/node": "^22.9.0" + } +} diff --git a/test/Playwright/pageObjects/BasePage.ts b/test/Playwright/pageObjects/BasePage.ts new file mode 100644 index 0000000..dcd8c54 --- /dev/null +++ b/test/Playwright/pageObjects/BasePage.ts @@ -0,0 +1,36 @@ +import { type Locator, type Page } from "@playwright/test"; + +export class BasePage { + readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + async goto(url: string) { + await this.page.goto(url); + await this.fullyLoadPage(); + } + async fullyLoadPage() { + await this.page.evaluate(() => + document.querySelectorAll("img[loading=lazy]").forEach((img) => img.setAttribute("loading", "eager")) + ); + + await this.page.evaluate(async () => { + await new Promise((resolve) => { + let totalHeight = 0; + const distance = 200; + const timer = setInterval(() => { + var scrollHeight = document.body.scrollHeight; + window.scrollBy(0, distance); + totalHeight += distance; + if (totalHeight >= scrollHeight - window.innerHeight) { + clearInterval(timer); + resolve(void 0); + } + }, 70); + }); + window.scrollTo(0, 0); + }); + } +} diff --git a/test/Playwright/pageObjects/CheckoutPage.ts b/test/Playwright/pageObjects/CheckoutPage.ts new file mode 100644 index 0000000..148e74a --- /dev/null +++ b/test/Playwright/pageObjects/CheckoutPage.ts @@ -0,0 +1,61 @@ +import { type Locator, type Page } from "@playwright/test"; +import { BasePage } from "./BasePage"; + +export class CheckoutPage extends BasePage { + readonly $firstName: Locator; + readonly $lastName: Locator; + readonly $email: Locator; + readonly $address: Locator; + readonly $city: Locator; + readonly $psc: Locator; + readonly $country: Locator; + readonly $shippingMethods: Locator; + readonly $cardNumber: Locator; + readonly $cardExpirationDate: Locator; + readonly $cardSecurityCode: Locator; + readonly $payBtn: Locator; + + constructor(page: Page) { + super(page); + + this.$firstName = page.locator('#TextField0[name="firstName"]'); + this.$lastName = page.locator('#TextField1[name="lastName"]'); + this.$email = page.locator('input[name="email"]'); + this.$address = page.locator('#TextField2[name="address1"]'); + this.$city = page.locator('#TextField5[name="city"]'); + this.$psc = page.locator('#TextField4[name="postalCode"]'); + this.$country = page.locator('select[name="countryCode"]'); + this.$shippingMethods = page.locator("#shipping_methods"); + this.$cardNumber = page + .frameLocator('iframe[title="Field container for: Card number"]') + .getByPlaceholder("Card number"); + this.$cardExpirationDate = page + .frameLocator('iframe[title="Field container for: Expiration date (MM / YY)"]') + .getByPlaceholder("Expiration date (MM / YY)"); + this.$cardSecurityCode = page + .frameLocator('iframe[title="Field container for: Security code"]') + .getByPlaceholder("Security code"); + this.$payBtn = page.locator("#checkout-pay-button"); + } + + async fillCustomerDetails(details) { + await this.$firstName.fill(details.firstName); + await this.$lastName.fill(details.lastName); + await this.$email.fill(details.email); + await this.$address.fill(details.address); + await this.$psc.fill(details.psc); + await this.$city.fill(details.city); + await this.$country.selectOption({ label: details.country }); + } + async selectShipping(shipping) { + await this.$shippingMethods.locator("label", { hasText: shipping }).click(); + } + async fillPayment(payment) { + await this.$cardNumber.fill(payment.cardNumber); + await this.$cardExpirationDate.fill(payment.cardExpirationDate); + await this.$cardSecurityCode.fill(payment.cardSecurityCode); + } + async confirmOrder() { + await this.$payBtn.click(); + } +} diff --git a/test/Playwright/playwright.config.ts b/test/Playwright/playwright.config.ts new file mode 100644 index 0000000..d0b3b0b --- /dev/null +++ b/test/Playwright/playwright.config.ts @@ -0,0 +1,91 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ + +const XBYK_STORE_URL = process.env.ASPNETCORE_URLS; + +export default defineConfig({ + testDir: "./tests", + snapshotPathTemplate: `{testDir}/screenshots/{projectName}/{testFilePath}/{arg}{ext}`, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + retries: 1, + /* Opt out of parallel tests on CI. */ + workers: 4, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [["list"], ["html", { open: "never" }], ["junit", { outputFile: "test-results/e2e-junit-results.xml" }]], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + screenshot: "only-on-failure", + video: "retain-on-failure", + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + }, + timeout: 60 * 1000, // Maximum time a test can run + expect: { + timeout: 10000, // Maximum time expect() should wait for the condition to be met. + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + baseURL: XBYK_STORE_URL, // + ignoreHTTPSErrors: true, + }, + }, + + // { + // name: "firefox", + // use: { ...devices["Desktop Firefox"], baseURL: "https://localhost:14066/", ignoreHTTPSErrors: true }, + // }, + + // { + // name: "webkit", + // use: { ...devices["Desktop Safari"], baseURL: "https://localhost:14066/", ignoreHTTPSErrors: true }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/test/Playwright/tests/order.spec.ts b/test/Playwright/tests/order.spec.ts new file mode 100644 index 0000000..667d052 --- /dev/null +++ b/test/Playwright/tests/order.spec.ts @@ -0,0 +1,200 @@ +import { test, expect, APIRequestContext } from "@playwright/test"; +import { CheckoutPage } from "../pageObjects/CheckoutPage"; +import { BasePage } from "../pageObjects/BasePage"; + +const SHOPIFY_ACCESS_TOKEN = process.env.CMSSHOPIFYCONFIG__ADMINAPIKEY; +const SHOPIFY_URL = process.env.CMSSHOPIFYCONFIG__SHOPIFYURL; +const SHOPIFY_STORE_PASSWORD = process.env.CMSSHOPIFYCONFIG__STOREPASSWORD; + +test.describe("Orders", () => { + let basePage: BasePage; + let apiContext: APIRequestContext; + + test.beforeAll(async ({ playwright }) => { + if (!SHOPIFY_ACCESS_TOKEN || !SHOPIFY_URL || !SHOPIFY_STORE_PASSWORD) { + throw new Error("Invalid env variable"); + } + apiContext = await playwright.request.newContext({ + baseURL: `${SHOPIFY_URL}/admin/api/2024-10/`, + extraHTTPHeaders: { + "X-Shopify-Access-Token": SHOPIFY_ACCESS_TOKEN, + }, + }); + }); + + test.afterAll(async ({}) => { + await apiContext.dispose(); + }); + + test.beforeEach(async ({ page }) => { + basePage = new BasePage(page); + await basePage.goto("/store"); + }); + + test(`Complete order with updated quantity`, async ({ page }) => { + const testData = { + customer: { + firstName: "Testfirstname", + lastName: "Testlastname", + email: "autotest@test.cz", + address: "Botanická 42/99", + city: "Brno", + psc: "602 00", + country: "Czechia", + }, + shipping: "Standard", + payment: { + cardNumber: "1", + cardExpirationDate: "12/27", + cardSecurityCode: "123", + }, + }; + test.info().annotations.push({ + type: "Info", + description: JSON.stringify(testData), + }); + + await test.step("Select category 'Brewing kits' from store page", async () => { + await page.locator(".store-menu-list li", { hasText: "Brewing kits" }).click(); + await expect(page).toHaveURL("/store/brewing-kits"); + }); + await test.step(`Select product 'Aeropress'`, async () => { + await page.locator(".product-tile", { hasText: "Aeropress" }).click(); + await expect(page.locator(".product-detail", { hasText: "Aeropress" })).toBeVisible(); // expecting to be on product page of Coffee Plunger + }); + await test.step("Add product to cart", async () => { + await page.locator("button", { hasText: "Add to cart" }).click(); + }); + await test.step("Go to shopping cart", async () => { + await page.goto("/shopping-cart"); + }); + await test.step("Update quantity", async () => { + await page.locator('input[name="Quantity"]').fill("2"); + await page.locator('input[name="cartOperation"][value="Update"]').click(); + await expect(page.locator(".cart-item-info__price")).toContainText(`1884 Kč`); + await expect(page.locator(".cart-total")).toContainText(`1884 Kč`); + }); + await test.step("Go to checkout", async () => { + await page.locator("a", { hasText: "Go to shopify checkout page" }).click(); + }); + await test.step("Login to shopify store", async () => { + if (!SHOPIFY_STORE_PASSWORD) { + throw new Error("Invalid env variable SHOPIFY_STORE_PASSWORD"); + } + await page.locator("#password").fill(SHOPIFY_STORE_PASSWORD); + await page.locator('button[type="submit"]').click(); + await page.goto("/shopping-cart"); + await page.locator("a", { hasText: "Go to shopify checkout page" }).click(); + }); + + const checkoutPage = new CheckoutPage(page); + + await test.step("Fill customer details, shipping and payment information", async () => { + await checkoutPage.fillCustomerDetails(testData.customer); + await checkoutPage.selectShipping(testData.shipping); + await checkoutPage.fillPayment(testData.payment); + }); + await test.step("Check calculated price", async () => { + await expect(page.locator('[role="row"]', { hasText: /Total/ })).toContainText("Kč 2,334.00"); // 2x Aeropress + shipping (450czk) + }); + + let orderId: string; + + await test.step("Confirm order and check thank you page", async () => { + await checkoutPage.confirmOrder(); + await expect(page.locator("#checkout-main")).toContainText("Thank you"); + await expect(page.locator('[role="row"]', { hasText: /Total/ })).toContainText("Kč 2,334.00"); + await page.waitForTimeout(3000); // explicit wait for shopify to process + await page.locator("a", { hasText: "Continue shopping" }).click(); + await expect(page).toHaveURL(/\/thank-you\?orderId=[0-9]*/); + await expect(page.locator(".thank-you-content")).toContainText("Thank You"); + orderId = await page.url().split("orderId=")[1]; + }); + + await test.step("Check order in shopify API", async () => { + const response = await apiContext.get(`./orders/${orderId}.json`, { + maxRedirects: 0, + }); + test.info().annotations.push({ + type: "Received HTTP code of shopify order", + description: response.status() + " " + response.statusText(), + }); + expect(response.ok()).toBeTruthy(); + const apiOrder = await response.json(); + expect(apiOrder).toEqual( + expect.objectContaining({ + order: expect.objectContaining({ + contact_email: testData.customer.email, + billing_address: expect.objectContaining({ + first_name: testData.customer.firstName, + address1: testData.customer.address, + phone: null, + city: testData.customer.city, + zip: testData.customer.psc, + country: "Czech Republic", + last_name: testData.customer.lastName, + country_code: "CZ", + }), + shipping_address: expect.objectContaining({ + first_name: testData.customer.firstName, + address1: testData.customer.address, + phone: null, + city: testData.customer.city, + zip: testData.customer.psc, + country: "Czech Republic", + last_name: testData.customer.lastName, + country_code: "CZ", + }), + current_total_price_set: expect.objectContaining({ + presentment_money: { + amount: "2334.00", + currency_code: "CZK", + }, + }), + total_price_set: expect.objectContaining({ + presentment_money: { + amount: "2334.00", + currency_code: "CZK", + }, + }), + line_items: [ + expect.objectContaining({ + gift_card: false, + grams: 700, + name: "Aeropress", + price_set: expect.objectContaining({ + presentment_money: { + amount: "942.00", + currency_code: "CZK", + }, + }), + quantity: 2, + sku: "SK1U", + taxable: true, + title: "Aeropress", + total_discount: "0.00", + total_discount_set: expect.objectContaining({ + presentment_money: { + amount: "0.00", + currency_code: "CZK", + }, + }), + }), + ], + shipping_lines: [ + expect.objectContaining({ + code: "Standard", + price_set: expect.objectContaining({ + presentment_money: { + amount: "450.00", + currency_code: "CZK", + }, + }), + }), + ], + }), + }) + ); + }); + }); +}); From fbe7f852ffe940ca73f71fbc30f43cf8e2ccf71d Mon Sep 17 00:00:00 2001 From: andrejrblue <167293373+andrejrblue@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:39:33 +0100 Subject: [PATCH 2/3] Update ci.yml upload artifacts set to always --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0ef5bc..c19b2b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -199,5 +199,6 @@ jobs: - uses: actions/upload-artifact@v4 with: name: playwright-report + if: ${{ always() }} path: ./test/Playwright/playwright-report/ retention-days: 30 From 4955d78ad26b5d405e26a821a8760e6aeb2eefef Mon Sep 17 00:00:00 2001 From: andrejrblue <167293373+andrejrblue@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:44:11 +0100 Subject: [PATCH 3/3] Update ci.yml upload artifacts set to "if not cancelled" --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c19b2b2..eee9b7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -197,8 +197,8 @@ jobs: Remove-Job -Name ${{ env.PROJECT_NAME }} - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} with: name: playwright-report - if: ${{ always() }} path: ./test/Playwright/playwright-report/ retention-days: 30