From f97d75c33e6bf36f749f6ca6767df313fa626066 Mon Sep 17 00:00:00 2001 From: "M.Graczyk" Date: Wed, 27 Sep 2023 08:57:57 +0200 Subject: [PATCH] Playwright e2e setup (#4168) * Add Plawright POC * removed unused variable * removed example file * Remove TS issues * CI url fix * Add artefacts config * basic setup finished * all create product type tests refactored and single from create product * trigger tsc type checks on test folder before commits * cr fixes * removed PW workflow for now, add notification to cypress tests about delete when migration is done, cr fix interface naming * wraped combobox on taxes and category components with box with data test id * git ignore fix --------- Co-authored-by: wojteknowacki --- .gitignore | 3 + .husky/pre-commit | 1 + .../productTypes/createProductType.js | 6 +- cypress/e2e/products/createProduct.js | 6 +- package-lock.json | 72 ++++++++++ package.json | 6 +- playwright.config.ts | 39 ++++++ playwright/api/basics.ts | 40 ++++++ playwright/data/common-locators.ts | 4 + playwright/data/url.ts | 36 +++++ .../pages/dialogs/product-create-dialog.ts | 28 ++++ playwright/pages/home-page.ts | 16 +++ playwright/pages/login-page.ts | 34 +++++ playwright/pages/metadata-seo-page.ts | 105 +++++++++++++++ playwright/pages/product-list-page.ts | 27 ++++ playwright/pages/product-page.ts | 124 ++++++++++++++++++ playwright/pages/product-type-list-page.ts | 21 +++ playwright/pages/product-type-page.ts | 56 ++++++++ .../pages/right-side-details-section.ts | 66 ++++++++++ playwright/tests/auth.setup.ts | 22 ++++ playwright/tests/orders.spec.ts | 14 ++ playwright/tests/product-types.spec.ts | 33 +++++ playwright/tests/product.spec.ts | 26 ++++ playwright/tsconfig.json | 18 +++ .../ProductOrganization.tsx | 54 ++++---- .../components/ProductTaxes/ProductTaxes.tsx | 43 +++--- tsconfig.json | 2 +- 27 files changed, 846 insertions(+), 56 deletions(-) create mode 100644 playwright.config.ts create mode 100644 playwright/api/basics.ts create mode 100644 playwright/data/common-locators.ts create mode 100644 playwright/data/url.ts create mode 100644 playwright/pages/dialogs/product-create-dialog.ts create mode 100644 playwright/pages/home-page.ts create mode 100644 playwright/pages/login-page.ts create mode 100644 playwright/pages/metadata-seo-page.ts create mode 100644 playwright/pages/product-list-page.ts create mode 100644 playwright/pages/product-page.ts create mode 100644 playwright/pages/product-type-list-page.ts create mode 100644 playwright/pages/product-type-page.ts create mode 100644 playwright/pages/right-side-details-section.ts create mode 100644 playwright/tests/auth.setup.ts create mode 100644 playwright/tests/orders.spec.ts create mode 100644 playwright/tests/product-types.spec.ts create mode 100644 playwright/tests/product.spec.ts create mode 100644 playwright/tsconfig.json diff --git a/.gitignore b/.gitignore index 1fc4b21ba06..41826d7cf5a 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,6 @@ django-queries-results.html !.env.template !.dependency-cruiser.js !.featureFlags +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/.husky/pre-commit b/.husky/pre-commit index d24fdfc601b..c931384d792 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -2,3 +2,4 @@ . "$(dirname -- "$0")/_/husky.sh" npx lint-staged +npm run check-types-playwright diff --git a/cypress/e2e/configuration/productTypes/createProductType.js b/cypress/e2e/configuration/productTypes/createProductType.js index b4c2fbbb53a..a41944b416d 100644 --- a/cypress/e2e/configuration/productTypes/createProductType.js +++ b/cypress/e2e/configuration/productTypes/createProductType.js @@ -17,7 +17,7 @@ describe("As an admin I want to create product types", () => { }); it( - "should be able to create product type without shipping required. TC: SALEOR_1501", + "should be able to create product type without shipping required. TC: SALEOR_1501 - migration in progress - to delete when done", { tags: ["@productType", "@allEnv", "@stable", "@oldRelease", "@critical"], }, @@ -37,7 +37,7 @@ describe("As an admin I want to create product types", () => { ); it( - "should be able to create product type with shipping required. TC: SALEOR_1502", + "should be able to create product type with shipping required. TC: SALEOR_1502 - migration in progress - to delete when done", { tags: ["@productType", "@allEnv", "@stable", "@critical"] }, () => { const name = `${startsWith}${faker.datatype.number()}`; @@ -57,7 +57,7 @@ describe("As an admin I want to create product types", () => { ); it( - "should be able to create product type with gift card kind. TC: SALEOR_1510", + "should be able to create product type with gift card kind. TC: SALEOR_1510 - migration in progress - to delete when done", { tags: ["@productType", "@allEnv", "@stable", "@critical"] }, () => { const name = `${startsWith}${faker.datatype.number()}`; diff --git a/cypress/e2e/products/createProduct.js b/cypress/e2e/products/createProduct.js index 138857ce741..eba1d37c033 100644 --- a/cypress/e2e/products/createProduct.js +++ b/cypress/e2e/products/createProduct.js @@ -3,9 +3,7 @@ import faker from "faker"; -import { - PRODUCT_DETAILS, -} from "../../elements/catalog/products/product-details"; +import { PRODUCT_DETAILS } from "../../elements/catalog/products/product-details"; import { PRODUCTS_LIST } from "../../elements/catalog/products/products-list"; import { BUTTON_SELECTORS } from "../../elements/shared/button-selectors"; import { urlList } from "../../fixtures/urlList"; @@ -65,7 +63,7 @@ describe("As an admin I should be able to create product", () => { }); it( - "should be able to create product with variants as an admin. SALEOR_2701", + "should be able to create product with variants as an admin. SALEOR_2701 - migration in progress - to delete when done", { tags: ["@products", "@allEnv", "@critical", "@stable", "@oldRelease"] }, () => { const randomName = `${startsWith}${faker.datatype.number()}`; diff --git a/package-lock.json b/package-lock.json index a76d11473a1..29c464ab642 100644 --- a/package-lock.json +++ b/package-lock.json @@ -117,6 +117,7 @@ "@graphql-codegen/typescript-apollo-client-helpers": "^2.1.10", "@graphql-codegen/typescript-operations": "^2.2.4", "@graphql-codegen/typescript-react-apollo": "^3.2.5", + "@playwright/test": "^1.37.1", "@saleor/app-sdk": "0.43.0", "@sentry/cli": "^2.20.6", "@swc/jest": "^0.2.26", @@ -7054,6 +7055,39 @@ "node": ">=10.12.0" } }, + "node_modules/@playwright/test": { + "version": "1.37.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.37.1.tgz", + "integrity": "sha512-bq9zTli3vWJo8S3LwB91U0qDNQDpEXnw7knhxLM0nwDvexQAwx9tO8iKDZSqqneVq+URd/WIoz+BALMqUTgdSg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "playwright-core": "1.37.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/@playwright/test/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, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/@radix-ui/number": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", @@ -29469,6 +29503,18 @@ "node": ">=6" } }, + "node_modules/playwright-core": { + "version": "1.37.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.37.1.tgz", + "integrity": "sha512-17EuQxlSIYCmEMwzMqusJ2ztDgJePjrbttaefgdsiqeLWidjYz9BxXaTaZWxH1J95SHGk6tjE+dwgWILJoUZfA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/please-upgrade-node": { "version": "3.2.0", "license": "MIT", @@ -41348,6 +41394,26 @@ "webcrypto-core": "^1.7.7" } }, + "@playwright/test": { + "version": "1.37.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.37.1.tgz", + "integrity": "sha512-bq9zTli3vWJo8S3LwB91U0qDNQDpEXnw7knhxLM0nwDvexQAwx9tO8iKDZSqqneVq+URd/WIoz+BALMqUTgdSg==", + "dev": true, + "requires": { + "@types/node": "*", + "fsevents": "2.3.2", + "playwright-core": "1.37.1" + }, + "dependencies": { + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + } + } + }, "@radix-ui/number": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", @@ -56899,6 +56965,12 @@ "find-up": "^3.0.0" } }, + "playwright-core": { + "version": "1.37.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.37.1.tgz", + "integrity": "sha512-17EuQxlSIYCmEMwzMqusJ2ztDgJePjrbttaefgdsiqeLWidjYz9BxXaTaZWxH1J95SHGk6tjE+dwgWILJoUZfA==", + "dev": true + }, "please-upgrade-node": { "version": "3.2.0", "optional": true, diff --git a/package.json b/package.json index ba92a8abf02..88a92cb07f6 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,7 @@ "@graphql-codegen/typescript-apollo-client-helpers": "^2.1.10", "@graphql-codegen/typescript-operations": "^2.2.4", "@graphql-codegen/typescript-react-apollo": "^3.2.5", + "@playwright/test": "^1.37.1", "@saleor/app-sdk": "0.43.0", "@sentry/cli": "^2.20.6", "@swc/jest": "^0.2.26", @@ -315,6 +316,7 @@ "prebuild": "npm run build-types", "check-strict-null-errors": "tsc --noEmit --strictNullChecks | node scripts/count-strict-null-check-errors.js", "check-types": "tsc --noEmit && tsc-strict", + "check-types-playwright": "tsc --noEmit --project playwright/tsconfig.json", "extract-json-messages": "formatjs extract 'src/**/*.{ts,tsx}' --out-file locale/defaultMessages.json --format scripts/formatter.js", "extract-messages": "npm run extract-json-messages", "fetch-schema": "graphql-codegen --config ./fetch-schema.yml", @@ -352,7 +354,9 @@ "qa:move-screenshots": "find cypress/reports/mochareports -type d -name \"*.js\" -exec mv {} cypress/reports/mochareports \\;", "qa:move-videos": "find cypress/videos -type f -name \"*.js.mp4\" -exec mv {} cypress/reports/mochareports/videos \\;", "qa:artifact-move-screenshots": "find cypress/reports/*/mochareports -type d -name \"*.js\" -exec mv {} cypress/reports/mochareports \\;", - "qa:artifact-move-videos": "find cypress/reports/*/mochareports/videos -type f -name \"*.js.mp4\" -exec mv {} cypress/reports/mochareports/videos \\;" + "qa:artifact-move-videos": "find cypress/reports/*/mochareports/videos -type f -name \"*.js.mp4\" -exec mv {} cypress/reports/mochareports/videos \\;", + "qa:basic-regression": "npx playwright test --grep @basic-regression", + "qa:full-regression": "npx playwright test" }, "description": "![Saleor Dashboard](https://user-images.githubusercontent.com/44495184/185379472-2a204c0b-9b7a-4a3e-93c0-2cb85205ed5e.png)" } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000000..7dc832b462b --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,39 @@ +import dotenv from "dotenv"; + +import { defineConfig, devices } from "@playwright/test"; + +dotenv.config(); + +export default defineConfig({ + testDir: "playwright/tests", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: "html", + // webServer: { + // command: "npm run dev", + // url: "http://localhost:9000/", + // reuseExistingServer: !process.env.CI, + // }, + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + baseURL: process.env.CYPRESS_baseUrl, + trace: "on-first-retry", + screenshot: "only-on-failure", + testIdAttribute: "data-test-id", + video: process.env.CI ? "retain-on-failure" : "off", + }, + + /* Configure projects for major browsers */ + projects: [ + { name: "setup", testMatch: /.*\.setup\.ts/ }, + + { + // if new project added make sure to add dependency as below + dependencies: ["setup"], + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], +}); diff --git a/playwright/api/basics.ts b/playwright/api/basics.ts new file mode 100644 index 00000000000..6406c170b43 --- /dev/null +++ b/playwright/api/basics.ts @@ -0,0 +1,40 @@ +import { APIResponse, request } from "@playwright/test"; + +const URL = process.env.API_URI || ""; +interface Data { + query: string; +} + +interface User { + email: string; + password: string; +} +export class GraphQLService { + async example( + user: User, + authorization: string = "auth", + ): Promise { + const headers = { Authorization: `Bearer ${authorization}` }; + + const api = await request.newContext(); + const query = `mutation TokenAuth{ + tokenCreate(email: "${user.email}", password: "${user.password}") { + token + refreshToken + errors: errors { + code + field + message + } + user { + id + } + } + }`; + const data: Data = { + query: query, + }; + const loginResponse = await api.post(URL, { data, headers }); + return loginResponse; + } +} diff --git a/playwright/data/common-locators.ts b/playwright/data/common-locators.ts new file mode 100644 index 00000000000..23c80c2967f --- /dev/null +++ b/playwright/data/common-locators.ts @@ -0,0 +1,4 @@ +export const LOCATORS = { + successBanner: '[data-test-type="success"]', + dataGridTable: "[data-testid='data-grid-canvas']", +}; diff --git a/playwright/data/url.ts b/playwright/data/url.ts new file mode 100644 index 00000000000..39ae9c30672 --- /dev/null +++ b/playwright/data/url.ts @@ -0,0 +1,36 @@ +export const URL_LIST = { + addProduct: "products/add", + addPageType: "pages/add?page-type-id=", + apps: "custom-apps/", + attributes: "attributes/", + categories: "categories/", + channels: "channels/", + collections: "collections/", + configuration: "configuration/", + customers: "customers/", + dashboard: "dashboard/", + draftOrders: "orders/drafts/", + giftCards: "gift-cards/", + homePage: "/", + newPassword: "new-password/", + navigation: "navigation/", + orders: "orders/", + pages: "pages/", + pageTypes: "page-types/", + permissionsGroups: "permission-groups/", + plugins: "plugins/", + products: "products/", + productTypes: "product-types/", + productTypesAdd: "product-types/add", + sales: "discounts/sales/", + shippingMethods: "shipping/", + siteSettings: "site-settings/", + staffMembers: "staff/", + stripeApiPaymentMethods: "https://api.stripe.com/v1/payment_methods", + translations: "translations/", + variants: "variant/", + vouchers: "discounts/vouchers/", + variant: "variant/", + warehouses: "warehouses/", + webhooksAndEvents: "custom-apps/", +}; diff --git a/playwright/pages/dialogs/product-create-dialog.ts b/playwright/pages/dialogs/product-create-dialog.ts new file mode 100644 index 00000000000..1cb56857692 --- /dev/null +++ b/playwright/pages/dialogs/product-create-dialog.ts @@ -0,0 +1,28 @@ +import type { + Locator, + Page, +} from "@playwright/test"; + +export class ProductCreateDialog { + readonly page: Page; + readonly dialogProductTypeInput: Locator; + readonly promptedOptions: Locator; + readonly confirmButton: Locator; + constructor(page: Page) { + this.page = page; + this.dialogProductTypeInput = page.locator( + "[data-test-id='dialog-product-type'] input", + ); + this.promptedOptions = page.getByTestId( + "single-autocomplete-select-option", + ); + this.confirmButton = page.getByTestId("submit"); + } + async selectProductTypeWithVariants() { + await this.dialogProductTypeInput.fill("beer"); + await this.promptedOptions.filter({ hasText: "Beer" }).click(); + } + async clickConfirmButton() { + await this.confirmButton.click(); + } +} diff --git a/playwright/pages/home-page.ts b/playwright/pages/home-page.ts new file mode 100644 index 00000000000..c56190e7ff3 --- /dev/null +++ b/playwright/pages/home-page.ts @@ -0,0 +1,16 @@ +import type { + Locator, + Page, +} from "@playwright/test"; + +export class HomePage { + readonly page: Page; + readonly welcomeMessage: Locator; + constructor(page: Page) { + this.page = page; + this.welcomeMessage = page.getByTestId("home-header"); + } + async goto() { + await this.page.goto("/"); + } +} diff --git a/playwright/pages/login-page.ts b/playwright/pages/login-page.ts new file mode 100644 index 00000000000..89850080016 --- /dev/null +++ b/playwright/pages/login-page.ts @@ -0,0 +1,34 @@ +import type { + Locator, + Page, +} from "@playwright/test"; + +export class LoginPage { + readonly page: Page; + readonly emailInput: Locator; + readonly passwordInput: Locator; + readonly signInButton: Locator; + constructor(page: Page) { + this.page = page; + this.emailInput = page.getByTestId("email"); + this.passwordInput = page.getByTestId("password"); + this.signInButton = page.getByTestId("submit"); + } + async typeEmail(email: string) { + await this.emailInput.type(email); + } + async typePassword(password: string) { + await this.passwordInput.type(password); + } + async clickSignInButton() { + await this.signInButton.click(); + } + async goto() { + const CYPRESS_baseUrl = process.env.CYPRESS_baseUrl; + const loginPageUrl = + CYPRESS_baseUrl === "http://localhost:9000/" + ? "http://localhost:9000/" + : "/dashboard"; + await this.page.goto(loginPageUrl); + } +} diff --git a/playwright/pages/metadata-seo-page.ts b/playwright/pages/metadata-seo-page.ts new file mode 100644 index 00000000000..3e92c8ae9bd --- /dev/null +++ b/playwright/pages/metadata-seo-page.ts @@ -0,0 +1,105 @@ +import * as faker from "faker"; + +import type { Locator, Page } from "@playwright/test"; + +const seoSlugName = `e2e-seoSlug-${faker.datatype.number()}`; +const metaDataName = `e2e-metaDataName-${faker.datatype.number()}`; +const metaDataValue = `e2e-metaDataValue-${faker.datatype.number()}`; +const privateMetaDataName = `e2e-privateMetaDataName-${faker.datatype.number()}`; +const privateMetaDataValue = `e2e-privateMetaDataValue-${faker.datatype.number()}`; +const seoEngineTitle = `e2e-seoSlugTitle-${faker.datatype.number()}`; +const seoDescriptionText = `e2e-seoSlugDescription-${faker.datatype.number()}`; + +export class MetadataSeoPage { + readonly page: Page; + + readonly productNameInput: Locator; + readonly editSeoSettings: Locator; + readonly slugInput: Locator; + readonly seoTitleInput: Locator; + readonly seoDescriptionInput: Locator; + readonly expandMetadataButton: Locator; + readonly metadataForm: Locator; + readonly addMetaButton: Locator; + readonly addPrivateMetaButton: Locator; + readonly metaDataNameInput: Locator; + readonly privateMetaDataNameInput: Locator; + readonly metadataValueField: Locator; + readonly privateMetadataValueField: Locator; + readonly metaExpandButton: Locator; + readonly metaDeletedButton: Locator; + readonly privateMetaSection: Locator; + readonly publicMetaSection: Locator; + readonly fulfillmentMetaSection: Locator; + + constructor(page: Page) { + this.page = page; + this.productNameInput = page.locator("[name='name']"); + this.editSeoSettings = page.getByTestId("edit-seo"); + this.slugInput = page.locator("[name='slug']"); + this.seoTitleInput = page.locator("[name='seoTitle']"); + this.seoDescriptionInput = page.locator("[name='seoDescription']"); + this.expandMetadataButton = page.getByTestId("expand"); + this.metadataForm = page.locator("[data-test-id='metadata-editor']"); + this.addMetaButton = page + .locator("[data-test-is-private='false']") + .getByTestId("add-field"); + this.addPrivateMetaButton = page + .locator("[data-test-is-private='true']") + .getByTestId("add-field"); + this.metaDataNameInput = page + .locator("[data-test-is-private='false']") + .locator("[name*='name']"); + this.privateMetaDataNameInput = page + .locator("[data-test-is-private='true']") + .locator("[name*='name']"); + this.metadataValueField = page + .locator("[data-test-is-private='false']") + .locator("[name*='value']"); + this.privateMetadataValueField = page + .locator("[data-test-is-private='true']") + .locator("[name*='value']"); + this.metaExpandButton = page.getByTestId("expand"); + this.metaDeletedButton = page.getByTestId("delete-field-0"); + this.privateMetaSection = page.locator("[data-test-is-private='true']"); + this.publicMetaSection = page.locator("[data-test-is-private='false']"); + this.fulfillmentMetaSection = page.getByTestId("fulfilled-order-section"); + } + + async expandAndAddAllMetadata( + metaName = metaDataName, + metaValue = metaDataValue, + privateMetaName = privateMetaDataName, + privateMetaValue = privateMetaDataValue, + ) { + await this.clickMetadataSectionExpandButton(); + await this.addMetaButton.click(); + await this.metaDataNameInput.fill(metaName); + await this.metadataValueField.fill(metaValue); + await this.clickPrivateMetadataSectionExpandButton(); + await this.addPrivateMetaButton.click(); + await this.privateMetaDataNameInput.fill(privateMetaName); + await this.privateMetadataValueField.fill(privateMetaValue); + } + + async fillSeoSection( + seoSlug = seoSlugName, + seoTitleEngine = seoEngineTitle, + seoDescription = seoDescriptionText, + ) { + await this.clickSeoSectionEditButton(); + await this.slugInput.fill(seoSlug); + await this.seoTitleInput.fill(seoTitleEngine); + await this.seoDescriptionInput.fill(seoDescription); + } + + async clickSeoSectionEditButton() { + await this.editSeoSettings.click(); + } + async clickMetadataSectionExpandButton() { + await this.expandMetadataButton.first().click(); + } + async clickPrivateMetadataSectionExpandButton() { + await this.expandMetadataButton.last().click(); + } +} diff --git a/playwright/pages/product-list-page.ts b/playwright/pages/product-list-page.ts new file mode 100644 index 00000000000..2daa07fb614 --- /dev/null +++ b/playwright/pages/product-list-page.ts @@ -0,0 +1,27 @@ +import { URL_LIST } from "@data/url"; +import type { + Locator, + Page, +} from "@playwright/test"; + +export class ProductListPage { + readonly page: Page; + readonly productsNames: Locator; + readonly createProductButton: Locator; + readonly searchProducts: Locator; + + constructor(page: Page) { + this.page = page; + this.productsNames = page.getByTestId("name"); + this.createProductButton = page.getByTestId("add-product"); + this.searchProducts = page.locator("[placeholder='Search Products...']"); + } + + async clickCreateProductButton() { + await this.createProductButton.click(); + } + + async goto() { + await this.page.goto(URL_LIST.products); + } +} diff --git a/playwright/pages/product-page.ts b/playwright/pages/product-page.ts new file mode 100644 index 00000000000..b80c2e4073f --- /dev/null +++ b/playwright/pages/product-page.ts @@ -0,0 +1,124 @@ +import * as faker from "faker"; + +import { LOCATORS } from "@data/common-locators"; +import { MetadataSeoPage } from "@pages/metadata-seo-page"; +import { RightSideDetailsPage } from "@pages/right-side-details-section"; +import type { Locator, Page } from "@playwright/test"; +import { expect } from "@playwright/test"; + +const productName = `e2e-productName-${faker.datatype.number()}`; +const productDescription = `e2e-productDescription-${faker.datatype.number()}`; +const productRating = `9`; + +export class ProductPage { + readonly page: Page; + + readonly productNameInput: Locator; + readonly productTypeInput: Locator; + readonly categoryInput: Locator; + readonly categoryItem: Locator; + readonly collectionInput: Locator; + readonly autocompleteDropdown: Locator; + readonly firstCategoryItem: Locator; + readonly visibleRadioBtn: Locator; + readonly channelAvailabilityItem: Locator; + readonly addVariantButton: Locator; + readonly descriptionInput: Locator; + readonly ratingInput: Locator; + readonly variantRow: Locator; + readonly variantPrice: Locator; + readonly collectionRemoveButtons: Locator; + readonly addWarehouseButton: Locator; + readonly warehouseOption: Locator; + readonly stockInput: Locator; + readonly costPriceInput: Locator; + readonly sellingPriceInput: Locator; + readonly productImage: Locator; + readonly uploadImageButton: Locator; + readonly uploadSavedImagesButton: Locator; + readonly uploadMediaUrlButton: Locator; + readonly saveUploadUrlButton: Locator; + readonly editVariant: Locator; + readonly saveButton: Locator; + readonly firstRowDataGrid: Locator; + readonly productUpdateFormSection: Locator; + metadataSeoPage: MetadataSeoPage; + rightSideDetailsPage: RightSideDetailsPage; + + constructor(page: Page) { + this.page = page; + this.metadataSeoPage = new MetadataSeoPage(page); + this.rightSideDetailsPage = new RightSideDetailsPage(page); + this.productNameInput = page.locator("[name='name']"); + this.productTypeInput = page.getByTestId("product-type"); + this.saveButton = page.getByTestId("button-bar-confirm"); + this.categoryInput = page.getByTestId("category"); + this.categoryItem = page.getByTestId("single-autocomplete-select-option"); + this.collectionInput = page.getByTestId("collections"); + this.autocompleteDropdown = page.getByTestId("autocomplete-dropdown"); + this.descriptionInput = page + .getByTestId("rich-text-editor-description") + .locator("[contenteditable]"); + this.variantRow = page.getByTestId("product-variant-row"); + this.variantPrice = page.getByTestId("price"); + this.collectionRemoveButtons = page.getByTestId("collection-remove"); + this.addWarehouseButton = page.getByTestId("add-warehouse"); + this.stockInput = page.getByTestId("stock-input"); + this.productImage = page.getByTestId("product-image"); + this.uploadImageButton = page.getByTestId("button-upload-image"); + this.uploadSavedImagesButton = page.getByTestId("upload-images"); + this.uploadMediaUrlButton = page.getByTestId("upload-media-url"); + this.saveUploadUrlButton = page.getByTestId("upload-url-button"); + this.editVariant = page.getByTestId("row-action-button"); + this.productUpdateFormSection = page.getByTestId("product-update-form"); + this.firstCategoryItem = page.locator("#downshift-0-item-0"); + this.visibleRadioBtn = page.locator("[name='isPublished']"); + this.channelAvailabilityItem = page.locator( + "[data-test-id*='channel-availability-item']", + ); + this.addVariantButton = page.locator( + "[data-test-id*='button-add-variant']", + ); + this.ratingInput = page.locator("[name='rating']"); + this.warehouseOption = page.locator("[role='menuitem']"); + this.costPriceInput = page.locator("[name*='costPrice']"); + this.sellingPriceInput = page.locator("[name*='channel-price']"); + this.firstRowDataGrid = page.locator("[data-testid='glide-cell-1-0']"); + } + + async addSeo() { + await this.metadataSeoPage.fillSeoSection(); + } + async selectFirstCategory() { + await this.rightSideDetailsPage.selectFirstCategory(); + } + async selectFirstTaxOption() { + await this.rightSideDetailsPage.selectFirstTax(); + } + async addAllMetaData() { + await this.metadataSeoPage.expandAndAddAllMetadata(); + } + + async typeNameDescAndRating() { + await this.typeProductName(); + await this.typeProductRating(); + await this.typeProductDescription(); + } + + async typeProductName(name = productName) { + await this.productNameInput.fill(name); + } + async typeProductDescription(description = productDescription) { + await this.descriptionInput.type(description); + } + async typeProductRating(rating = productRating) { + await this.ratingInput.fill(rating); + } + + async clickSaveButton() { + await this.saveButton.click(); + } + async expectSuccessBanner() { + await expect(this.page.locator(LOCATORS.successBanner)).toBeVisible(); + } +} diff --git a/playwright/pages/product-type-list-page.ts b/playwright/pages/product-type-list-page.ts new file mode 100644 index 00000000000..9f6e55fd5d3 --- /dev/null +++ b/playwright/pages/product-type-list-page.ts @@ -0,0 +1,21 @@ +import { URL_LIST } from "@data/url"; +import type { + Locator, + Page, +} from "@playwright/test"; + +export class ProductTypeListPage { + readonly page: Page; + readonly addProductTypeButton: Locator; + constructor(page: Page) { + this.page = page; + this.addProductTypeButton = page.getByTestId("add-product-type"); + } + async goto() { + await this.page.goto(URL_LIST.productTypes); + } + + async clickCreateProductTypeButton() { + await this.addProductTypeButton.click(); + } +} diff --git a/playwright/pages/product-type-page.ts b/playwright/pages/product-type-page.ts new file mode 100644 index 00000000000..c45e47fa8d8 --- /dev/null +++ b/playwright/pages/product-type-page.ts @@ -0,0 +1,56 @@ +import { LOCATORS } from "@data/common-locators"; +import { URL_LIST } from "@data/url"; +import type { Locator, Page } from "@playwright/test"; +import { expect } from "@playwright/test"; + +export class ProductTypePage { + readonly page: Page; + readonly nameInput: Locator; + readonly isShippingRequired: Locator; + readonly assignProductAttributeButton: Locator; + readonly hasVariantsButton: Locator; + readonly shippingWeightInput: Locator; + readonly giftCardKindCheckbox: Locator; + readonly variantSelectionCheckbox: Locator; + readonly saveButton: Locator; + readonly notificationSuccess: Locator; + + constructor(page: Page) { + this.page = page; + this.notificationSuccess = page.getByTestId("notification-message"); + this.nameInput = page.locator("[name='name']"); + this.isShippingRequired = page.locator("[name='isShippingRequired']"); + this.assignProductAttributeButton = page.getByTestId( + "assign-products-attributes", + ); + this.hasVariantsButton = page.locator("[name='hasVariants']"); + this.shippingWeightInput = page.locator("[name='weight']"); + this.giftCardKindCheckbox = page.getByTestId( + "product-type-kind-option-GIFT_CARD", + ); + this.variantSelectionCheckbox = page.getByTestId( + "variant-selection-checkbox", + ); + this.saveButton = page.getByTestId("button-bar-confirm"); + } + + async typeProductTypeName(name: string) { + await this.nameInput.fill(name); + } + async makeProductShippableWithWeight(weight: string = "10") { + await this.isShippingRequired.click(); + await this.shippingWeightInput.fill(weight); + } + async clickSaveButton() { + await this.saveButton.click(); + } + async selectGiftCardButton() { + await this.giftCardKindCheckbox.click(); + } + async goto() { + await this.page.goto(URL_LIST.productTypesAdd); + } + async expectSuccessBanner() { + await expect(this.page.locator(LOCATORS.successBanner)).toBeVisible(); + } +} diff --git a/playwright/pages/right-side-details-section.ts b/playwright/pages/right-side-details-section.ts new file mode 100644 index 00000000000..4b2468fa4d8 --- /dev/null +++ b/playwright/pages/right-side-details-section.ts @@ -0,0 +1,66 @@ +import type { + Locator, + Page, +} from "@playwright/test"; + +export class RightSideDetailsPage { + readonly page: Page; + readonly manageChannelsButton: Locator; + readonly assignedChannels: Locator; + readonly publishedRadioButtons: Locator; + readonly availableForPurchaseRadioButtons: Locator; + readonly radioButtonsValueTrue: Locator; + readonly radioButtonsValueFalse: Locator; + readonly visibleInListingsButton: Locator; + readonly availableChannel: Locator; + readonly categoryInput: Locator; + readonly taxInput: Locator; + readonly categoryItem: Locator; + readonly collectionInput: Locator; + readonly autocompleteDropdown: Locator; + readonly categorySelectOption: Locator; + readonly taxSelectOption: Locator; + readonly collectionSelectOption: Locator; + + constructor(page: Page) { + this.page = page; + this.categorySelectOption = page.locator("[data-test-id*='select-option']"); + this.taxSelectOption = page.locator("[data-test-id*='select-option']"); + this.collectionSelectOption = page.getByTestId( + "multi-autocomplete-select-option", + ); + this.categoryInput = page.getByTestId("category"); + this.taxInput = page.getByTestId("taxes"); + this.categoryItem = page.getByTestId("single-autocomplete-select-option"); + this.collectionInput = page.getByTestId("collections"); + this.autocompleteDropdown = page.getByTestId("autocomplete-dropdown"); + + this.manageChannelsButton = page.getByTestId( + "channels-availability-manage-button", + ); + this.assignedChannels = page.getByTestId("channel-availability-item"); + this.publishedRadioButtons = page.locator("[name*='isPublished'] > "); + this.availableForPurchaseRadioButtons = page.locator( + "[id*='isAvailableForPurchase']", + ); + this.radioButtonsValueTrue = page.locator("[value='true']"); + this.radioButtonsValueFalse = page.locator("[value='false']"); + this.visibleInListingsButton = page.locator("[id*='visibleInListings']"); + this.availableChannel = page.locator( + "[data-test-id*='channel-availability-item']", + ); + } + + async selectFirstCategory() { + await this.categoryInput.click(); + await this.categorySelectOption.first().click(); + } + async selectFirstTax() { + await this.taxInput.click(); + await this.taxSelectOption.first().click(); + } + async selectFirstCollection() { + await this.collectionInput.click(); + await this.collectionSelectOption.first().click(); + } +} diff --git a/playwright/tests/auth.setup.ts b/playwright/tests/auth.setup.ts new file mode 100644 index 00000000000..76d1dc339d1 --- /dev/null +++ b/playwright/tests/auth.setup.ts @@ -0,0 +1,22 @@ +import { HomePage } from "@pages/home-page"; +import { LoginPage } from "@pages/login-page"; +import { + expect, + test as setup, +} from "@playwright/test"; + +const adminFile = "playwright/.auth/admin.json"; + +setup("authenticate as admin", async ({ page }) => { + const loginPage = new LoginPage(page); + await loginPage.goto(); + await loginPage.typeEmail(process.env.CYPRESS_USER_NAME!); + await loginPage.typePassword(process.env.CYPRESS_USER_PASSWORD!); + await loginPage.clickSignInButton(); + + const homePage = new HomePage(page); + await expect(homePage.welcomeMessage).toContainText("Hello there,"); + + // End of authentication steps. + await page.context().storageState({ path: adminFile }); +}); diff --git a/playwright/tests/orders.spec.ts b/playwright/tests/orders.spec.ts new file mode 100644 index 00000000000..24aef0b8a23 --- /dev/null +++ b/playwright/tests/orders.spec.ts @@ -0,0 +1,14 @@ +import { URL_LIST } from "@data/url"; +import { HomePage } from "@pages/home-page"; +import { + expect, + test, +} from "@playwright/test"; + +test.use({ storageState: "playwright/.auth/admin.json" }); + +test("Orders", async ({ page }) => { + await page.goto(URL_LIST.orders); + const homePage = new HomePage(page); + await expect(page.getByTestId("create-order-button")).toBeVisible(); +}); diff --git a/playwright/tests/product-types.spec.ts b/playwright/tests/product-types.spec.ts new file mode 100644 index 00000000000..957f5a1537f --- /dev/null +++ b/playwright/tests/product-types.spec.ts @@ -0,0 +1,33 @@ +import * as faker from "faker"; + +import { ProductTypeListPage } from "@pages/product-type-list-page"; +import { ProductTypePage } from "@pages/product-type-page"; +import { test } from "@playwright/test"; + +test.use({ storageState: "playwright/.auth/admin.json" }); +const name = `e2e-product-type-${faker.datatype.number()}`; + +test("TC: SALEOR_1 Create basic product type @basic-regression @product-type", async ({ + page, +}) => { + const productTypeListPage = new ProductTypeListPage(page); + const productTypeAddPage = new ProductTypePage(page); + + await productTypeListPage.goto(); + await productTypeListPage.clickCreateProductTypeButton(); + await productTypeAddPage.typeProductTypeName(name); + await productTypeAddPage.makeProductShippableWithWeight(); + await productTypeAddPage.clickSaveButton(); + await productTypeAddPage.expectSuccessBanner(); +}); +test("TC: SALEOR_2 Create gift card product type @basic-regression @product-type", async ({ + page, +}) => { + const productTypeAddPage = new ProductTypePage(page); + + await productTypeAddPage.goto(); + await productTypeAddPage.typeProductTypeName(name); + await productTypeAddPage.selectGiftCardButton(); + await productTypeAddPage.clickSaveButton(); + await productTypeAddPage.expectSuccessBanner(); +}); diff --git a/playwright/tests/product.spec.ts b/playwright/tests/product.spec.ts new file mode 100644 index 00000000000..6e0847ce2d6 --- /dev/null +++ b/playwright/tests/product.spec.ts @@ -0,0 +1,26 @@ +import { ProductCreateDialog } from "@pages/dialogs/product-create-dialog"; +import { ProductListPage } from "@pages/product-list-page"; +import { ProductPage } from "@pages/product-page"; +import { test } from "@playwright/test"; + +test.use({ storageState: "playwright/.auth/admin.json" }); + +test("TC: SALEOR_3 Create basic product with variants @basic-regression @product-type", async ({ + page, +}) => { + const productListPage = new ProductListPage(page); + const productCreateDialog = new ProductCreateDialog(page); + const productPage = new ProductPage(page); + + await productListPage.goto(); + await productListPage.clickCreateProductButton(); + await productCreateDialog.selectProductTypeWithVariants(); + await productCreateDialog.clickConfirmButton(); + await productPage.typeNameDescAndRating(); + await productPage.addSeo(); + await productPage.addAllMetaData(); + await productPage.selectFirstCategory(); + await productPage.selectFirstTaxOption(); + await productPage.clickSaveButton(); + await productPage.expectSuccessBanner(); +}); diff --git a/playwright/tsconfig.json b/playwright/tsconfig.json new file mode 100644 index 00000000000..18239bb62e7 --- /dev/null +++ b/playwright/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "esModuleInterop": true, + "lib": ["es2020", "dom", "esnext"], + "skipLibCheck": true, + "target": "ES2020", + "module": "es2020", + "moduleResolution": "node", + "strict": true, + "resolveJsonModule": true, + "paths": { + "@pages/*": ["./pages/*"], + "@api/*": ["./api/*"], + "@data/*": ["./data/*"] + } + } +} diff --git a/src/products/components/ProductOrganization/ProductOrganization.tsx b/src/products/components/ProductOrganization/ProductOrganization.tsx index f61cafb17c3..496dcfc9caf 100644 --- a/src/products/components/ProductOrganization/ProductOrganization.tsx +++ b/src/products/components/ProductOrganization/ProductOrganization.tsx @@ -137,33 +137,33 @@ export const ProductOrganization: React.FC< )} - - + + + = props => { {intl.formatMessage(sectionNames.taxes)} - ({ - label: choice.name, - value: choice.id, - }))} - fetchOptions={() => {}} - value={ - value - ? { - value, - label: taxClassDisplayName, - } - : null - } - name="taxClassId" - label={intl.formatMessage(taxesMessages.taxClass)} - onChange={onChange} - fetchMore={onFetchMore} - /> + + ({ + label: choice.name, + value: choice.id, + }))} + fetchOptions={() => {}} + value={ + value + ? { + value, + label: taxClassDisplayName, + } + : null + } + name="taxClassId" + label={intl.formatMessage(taxesMessages.taxClass)} + onChange={onChange} + fetchMore={onFetchMore} + /> + ); diff --git a/tsconfig.json b/tsconfig.json index ef9c40ef5dc..cdddaa28f62 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,5 +23,5 @@ "@test/*": ["testUtils/*"] } }, - "exclude": ["node_modules", "cypress"] + "exclude": ["node_modules", "cypress", "playwright"] }