diff --git a/docs/test_plan.txt b/docs/test_plan.txt new file mode 100644 index 0000000..50586c8 --- /dev/null +++ b/docs/test_plan.txt @@ -0,0 +1,42 @@ +Generally I would like to speak with the PO about the product, with the Team about technology and then before everything +we should describe scope and out-of-scope of automation. + +During this task i discovered bellow activities to be done. + +INCREASE REGRESSION COVERAGE: +UI +1. Cover critical functionalities - related with MVP for current project stage , where all functionalities should be + working and stable to deliver value: +- registration and login flow +- payment flow - needs further investigation and analysis +- editing flow ( in iterations, this part requires additional functionalities mapping to describe more detialed view) +- Customer Service +2. MVP - 1 happy path including selling per one product ( book, cards etc. ) +3. User Profile: +- Settings - editing +- Saved Projects +- Online PhotoBooks +- FAQs +- Orders ( may require further functionalities mapping and exploration ) +4. Blog +5. Cover more edge cases and other functionalities of products +LOAD - if it is not done yet, then it is huge risk to not do it ASAP +it requires to perform load tests on the backend side +- evaluate available technology +- create a POC of evaluated tech +- start analysing backend metrics ( observability ) and based on findings design covering load scenarios +- extend load scenarios if needed +SECURITY +the minimum is: +1. Static Analysis +2. Dynamic analysis +SAST sometimes can be done through the config of CI/CD ( f.ex. gitlab ci scanners ) +may require evaluation of new technology ( f.ex.: Sonar Cloud , Snyk ) + +IMPROVEMENTS +1. critical improvements : +- create user creation factory , common for all test cases with proper environment config - trough API +( including countries grouping, permissions, feature flags etc. ) +2. implement configuration allowing for multi-country testing ( common scenarios, country dedicated etc. ) +3. Observability - start observing the test runs , cycles to spot any trends ( flaky, short, longest etc. ): +- may require to evaluate dedicated technology ( f.ex.: allure ) diff --git a/layouts.ts b/helpers/photosGenerator.ts similarity index 96% rename from layouts.ts rename to helpers/photosGenerator.ts index c61e10e..3eff8cd 100644 --- a/layouts.ts +++ b/helpers/photosGenerator.ts @@ -1,5 +1,5 @@ import { writeFile, mkdir } from "fs/promises"; -import type { Locator, Page } from "@playwright/test"; +import type { Page } from "@playwright/test"; import { toPng } from "jdenticon"; export class PhotoSelectorLayout { diff --git a/package.json b/package.json index 8fa7882..2df2342 100644 --- a/package.json +++ b/package.json @@ -9,5 +9,8 @@ "dependencies": { "@playwright/test": "1.17.2", "jdenticon": "3.1.1" + }, + "devDependencies": { + "@faker-js/faker": "^7.6.0" } } diff --git a/playwright.config.ts b/playwright.config.ts index 8dce33d..86e8540 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,8 +6,10 @@ const config: PlaywrightTestConfig = { // https://playwright.dev/docs/test-configuration timeout: 50000, use: { - //headless: false, + headless: true, trace: "on-first-retry", + screenshot: "only-on-failure", + video: "on-first-retry" }, }; diff --git a/selectors/editor-step-3.ts b/selectors/editor-step-3.ts new file mode 100644 index 0000000..bee204f --- /dev/null +++ b/selectors/editor-step-3.ts @@ -0,0 +1,49 @@ +import {expect, Locator, Page} from '@playwright/test'; + +export class EditorStep3 { + readonly addToBasketBtn: Locator + readonly stepIndicator: Locator + readonly dataPrintSurface: Locator + readonly editingDialogueWindow: Locator + readonly editingDialogueCancel: Locator + readonly editingDialogueConfirm: Locator + readonly editingDialogueTitle: Locator + readonly editingDialogueTitleErr: Locator + readonly campaignForm: Locator + readonly finaliseDialogueWindow: Locator + readonly finaliseDialogueContinue: Locator + readonly finaliseDialogueBack: Locator + + + + constructor(private page: Page) { + this.addToBasketBtn = page.locator("[data-tam=add-to-basket]"); + this.stepIndicator = page.locator("[data-tam=step-indicator]"); + this.dataPrintSurface= page.locator("[data-tam=print-surface]"); + + this.editingDialogueWindow = page.locator("[data-testid=dialog-window]"); + this.editingDialogueCancel = page.locator("[data-tam=cancel-cover-edit]"); + //TODO bellow element has overlay and should be fixed in UI, because some parents layers are hiding given one with + // data-tam attribute. Cannot be clicked directly + this.editingDialogueConfirm = page.locator("[data-tam=confirm-cover-edit]"); + //TODO add data-testid for bellow selector + this.editingDialogueTitle = page.locator("[placeholder=\"Enter a title\"]:required"); + this.editingDialogueTitleErr = page.locator(".status-container"); + this.campaignForm = page.locator("form[campaign-form]"); + this.finaliseDialogueContinue = page.locator("[data-testid=dialog-button][data-tam=dismiss-warning]"); + this.finaliseDialogueBack = page.locator("[data-testid=dialog-button][data-tam=resolve-warning]"); + this.finaliseDialogueWindow = page.locator("[data-testid=dialog-window]"); + + } + + async clickAddToBasketBtn() { + await this.addToBasketBtn.click(); + await expect(this.editingDialogueWindow).toBeVisible(); + } + + async clickConfirmEdtDialogue() { + await this.editingDialogueConfirm.click(); + } + + +} diff --git a/selectors/product-config.ts b/selectors/product-config.ts new file mode 100644 index 0000000..700f830 --- /dev/null +++ b/selectors/product-config.ts @@ -0,0 +1,15 @@ +import {Locator, Page} from '@playwright/test'; + +export class ProductConfig { + readonly yourPhotoBookTitle: Locator + readonly productLoading: Locator + + + + constructor(private page: Page) { + //bellow CSS selector is anitpatern and should be replaced with data-testid + this.yourPhotoBookTitle = page.locator("h1.container-title"); + this.productLoading = page.locator("[data-testid=loading]"); + + } +} diff --git a/selectors/user-registration.ts b/selectors/user-registration.ts new file mode 100644 index 0000000..0601c2c --- /dev/null +++ b/selectors/user-registration.ts @@ -0,0 +1,19 @@ +import {Locator, Page} from '@playwright/test'; + +export class UserRegistration { + readonly emailInput: Locator + readonly passwordInput: Locator + readonly confirmPassInout: Locator + readonly registrationBtn: Locator + + + constructor(private page: Page) { + // bellow locators sounds like cypress dedicated + // should be aligned and greed on one schema , or it is 3rd party responsibility + this.emailInput = page.locator("[data-cy=EmailInput]"); + this.passwordInput = page.locator("[data-cy=PasswordInput]") + this.confirmPassInout = page.locator("[data-cy=ConfirmPasswordInput]") + this.registrationBtn = page.locator("[data-cy=RegisterButton]") + } + +} \ No newline at end of file diff --git a/setup.ts b/setup.ts index 2623cda..3de5174 100644 --- a/setup.ts +++ b/setup.ts @@ -1,20 +1,30 @@ -import { test as base } from "@playwright/test"; +import {test as base} from '@playwright/test'; -export type TestEnvironment = "test" | "acceptance" | "production"; -export type Channel = "bonusprint.co.uk"; -export type ArticleType = "HardCoverPhotoBook"; +enum EnvTypes { + Test = 'test', + Acceptance = 'acceptance', + Production = 'production', +} + +export type TestEnvironment = EnvTypes; +export type Channel = 'bonusprint.co.uk'; +export type ArticleType = 'HardCoverPhotoBook'; export type TestConfig = { testEnvironment: TestEnvironment; getInstantEditorUrl: (channel: Channel, articleType: ArticleType, papId: string) => string; + getLoginRegisterUrl: (path: string) => string; }; export const testEnvironment: TestEnvironment = getAndValidateEnvironment(process.env.TEST_ENV); export const test = base.extend({ testEnvironment: testEnvironment, - getInstantEditorUrl: async ({ testEnvironment }, use) => { + getInstantEditorUrl: async ({testEnvironment}, use) => { await use(getInstantEditorUrl.bind(this, testEnvironment)); }, + getLoginRegisterUrl: async ({testEnvironment}, use) => { + await use(getLoginRegisterUrl.bind(this, testEnvironment)); + }, }); /** @@ -25,13 +35,13 @@ export const test = base.extend({ * @return option */ function getAndValidateEnvironment(option?: string): TestEnvironment { - option = String(option || "").toLowerCase(); + option = String(option || '').toLowerCase(); switch (option) { - case "": - return "test"; - case "test": - case "acceptance": - case "production": + case '': + return EnvTypes.Test; + case EnvTypes.Test: + case EnvTypes.Acceptance: + case EnvTypes.Production: return option as TestEnvironment; default: throw Error(`Unknown environment: ${option}.`); @@ -42,25 +52,38 @@ function getAndValidateEnvironment(option?: string): TestEnvironment { * @return {string} href */ function getInstantEditorUrl(env: TestEnvironment, channel: Channel, articleType: ArticleType, papId: string): string { - const url = new URL("http://localhost/index.html"); + const url = new URL('http://localhost/index.html'); switch (env) { - case "test": + case EnvTypes.Test: url.hostname = `t-dtap.editor.${channel}`; url.pathname = `/instant` + url.pathname; break; - case "acceptance": + case EnvTypes.Acceptance: url.hostname = `a-dtap.editor.${channel}`; url.pathname = `/instant` + url.pathname; break; - case "production": + case EnvTypes.Production: url.hostname = `editor.${channel}`; url.pathname = `/instant` + url.pathname; break; default: throw Error(`Unknown environment: ${env}.`); } - url.searchParams.set("articleType", articleType.toLocaleLowerCase()); - url.searchParams.set("papId", papId.toUpperCase()); - url.searchParams.set("testExecution", "true"); + url.searchParams.set('articleType', articleType.toLocaleLowerCase()); + url.searchParams.set('papId', papId.toUpperCase()); + url.searchParams.set('testExecution', 'true'); return url.href; } + +function getLoginRegisterUrl(env: TestEnvironment, path: string) { + // const url = new URL("http://localhost"); + let fullHostName; + switch (env) { + case EnvTypes.Test: + fullHostName = 'https://t-dtap.login.albelli.com/' + path; + break; + default: + fullHostName = 'https://t-dtap.login.albelli.com/register'; + } + return fullHostName; +} diff --git a/test/auth/account/register/user-registration.spec.ts b/test/auth/account/register/user-registration.spec.ts new file mode 100644 index 0000000..5dad6ce --- /dev/null +++ b/test/auth/account/register/user-registration.spec.ts @@ -0,0 +1,42 @@ +import {test} from '../../../../setup'; +import {UserRegistration} from '../../../../selectors/user-registration'; +import {faker} from '@faker-js/faker'; +import {expect} from '@playwright/test'; + + +test.describe("User registration", () => { + test.beforeEach(async ({ page,getLoginRegisterUrl }) => { + + const registerUrl = getLoginRegisterUrl("register"); + await page.goto(registerUrl); + + }) + test("Given user wants to register himself", async ({ page }) => { + const userRegistrationSelectors = new UserRegistration(page) + + await test.step("When type valid email address", async () => { + await userRegistrationSelectors.emailInput.type(faker.internet.email()) + }); + + await test.step("And type his valid password", async () => { + await userRegistrationSelectors.passwordInput.type('Password!02') + + }); + await test.step("And confirm pass", async () => { + await userRegistrationSelectors.confirmPassInout.type('Password!02') + + }); + + await test.step("Then user is registered", async () => { + await page.route('https://t-dtap.login.albelli.com/api/register', (route) => { + route.fulfill({ + status: 200 + }); + }); + }) + + await test.step("And submit", async () => { + await userRegistrationSelectors.registrationBtn.click() + }); + }); +}); \ No newline at end of file diff --git a/test/products/books/basket/basket-add.spec.ts b/test/products/books/basket/basket-add.spec.ts new file mode 100644 index 0000000..c6c4b52 --- /dev/null +++ b/test/products/books/basket/basket-add.spec.ts @@ -0,0 +1,98 @@ +import { expect } from "@playwright/test"; +import { test } from "../../../../setup"; +import { PhotoSelectorLayout } from "../../../../helpers/photosGenerator"; +import {EditorStep3} from '../../../../selectors/editor-step-3'; +import {ProductConfig} from '../../../../selectors/product-config'; + +test.describe("Photobook Basket - Adding selected photos to Basket, setting Title, Canceling and Confirming", () => { + test.beforeEach(async ({ page,getInstantEditorUrl }) => { + //In next iteration extract it to the separate two functions like: envSetup and photoUpload and make it reusable + let photoSelector = new PhotoSelectorLayout(page); + const editorUrl = getInstantEditorUrl("bonusprint.co.uk", "HardCoverPhotoBook", "PAP_360"); + await page.goto(editorUrl); + await photoSelector.createAndUploadRandomPhotos(1); + await photoSelector.waitForUploadsComplete(); + await photoSelector.clickOnUsePhotos(); + }) + + test("Given Client wants to add selected photos to basket", async ({page}) => { + const editorSelectors = new EditorStep3(page); + + await test.step("When click Add to basket button", async () => { + await editorSelectors.clickAddToBasketBtn(); + }); + + await test.step("Then I can see Editing Dialogue Window", async () => { + await expect(editorSelectors.editingDialogueTitle).toBeVisible() + await expect(editorSelectors.editingDialogueCancel).toBeVisible() + await expect(editorSelectors.editingDialogueConfirm).toBeVisible() + }); + }); + + test("Given Client wants to Confirm Basket without setting Title", async ({page}) => { + let editorSelectors = new EditorStep3(page); + + await test.step("When click Confirm without setting Title", async () => { + await editorSelectors.clickAddToBasketBtn(); + // CSS workaround until element will be fixed in UI + await page.locator('header>span[class="edit-cover-button save-button"]').click({force:true}) + }); + + await test.step("Then Error message is shown", async () => { + await expect(editorSelectors.editingDialogueTitleErr).toBeVisible(); + + }); + }); + + + test("Given Client wants to Confirm Basket, set valid Title and finalise Editing Dialogue", async ({page}) => { + const editorSelectors = new EditorStep3(page); + + await test.step("When Client set a valid Title", async () => { + await editorSelectors.clickAddToBasketBtn(); + await editorSelectors.editingDialogueTitle.type('Valid Title'); + }); + + await test.step("And click Confirm button", async () => { + // CSS workaround until element will be fixed in UI + await page.locator('header>span[class="edit-cover-button save-button"]').click({force:true}) + }); + + await test.step("Then Error message is not shown", async () => { + await expect(editorSelectors.editingDialogueTitleErr).not.toBeVisible(); + }); + //This step is not valid anymore - was shown only once ,and it was satisfaction survey form + // await test.step("And The Feedback Form is visible ", async () => { + // await expect(editorSelectors.campaignForm).toBeVisible(); + // }); + }); + + + test("Given Client wants to finalise his choices and confirm basket", async ({page}) => { + const editorSelectors = new EditorStep3(page); + const productConfig = new ProductConfig(page) + + await test.step("And Client do not want to change anything", async () => { + await editorSelectors.clickAddToBasketBtn(); + await editorSelectors.editingDialogueTitle.type('Valid Title'); + // workaround until element will be fixed in UI + await page.locator('header>span[class="edit-cover-button save-button"]').isVisible() + await page.locator('header>span[class="edit-cover-button save-button"]').click({force:true}) + + }); + + await test.step("When click Add to Basket button", async () => { + await editorSelectors.clickAddToBasketBtn(); + await editorSelectors.finaliseDialogueWindow.isVisible() + }); + + await test.step("And click Continue anyway button", async () => { + await editorSelectors.finaliseDialogueContinue.click() + }); + + await test.step("And product config page is visible ", async () => { + await expect(productConfig.productLoading).not.toBeVisible() + await expect(productConfig.yourPhotoBookTitle).toBeVisible() + }); + }); +}); diff --git a/test/products/books/basket/busket-buy.spec.ts b/test/products/books/basket/busket-buy.spec.ts new file mode 100644 index 0000000..7f63fb8 --- /dev/null +++ b/test/products/books/basket/busket-buy.spec.ts @@ -0,0 +1,14 @@ +import { test } from "../../../../setup"; + +test.describe("Photobook Basket - Adding selected photos to Basket, setting Title, Canceling and Confirming", () => { + + test("Given Client wants to add selected photos to basket", async ({page}) => { + + await test.step("When click Add to basket button", async () => { + }); + + await test.step("Then I can see Editing Dialogue Window", async () => { + }); + }); + +}); diff --git a/test/products/books/basket/product-config/product-config.spec.ts b/test/products/books/basket/product-config/product-config.spec.ts new file mode 100644 index 0000000..e0896f2 --- /dev/null +++ b/test/products/books/basket/product-config/product-config.spec.ts @@ -0,0 +1,18 @@ +import { test } from "../../../../../setup"; + + +test.describe("Product Configuration - ", () => { + + test("Given Client wants to add selected photos to basket", async ({page}) => { + + await test.step("When click Add to basket button", async () => { + + }); + + await test.step("Then I can see Editing Dialogue Window", async () => { + + }); + }); + + +}); diff --git a/test/photobook.spec.ts b/test/products/books/photobook.spec.ts similarity index 82% rename from test/photobook.spec.ts rename to test/products/books/photobook.spec.ts index 13af9d7..9840f5d 100644 --- a/test/photobook.spec.ts +++ b/test/products/books/photobook.spec.ts @@ -1,6 +1,5 @@ -import { expect } from "@playwright/test"; -import { test } from "../setup"; -import { PhotoSelectorLayout } from "../layouts"; +import { test } from "../../../setup"; +import { PhotoSelectorLayout } from "../../../helpers/photosGenerator"; test.describe("Photobook", () => { test("happy path", async ({ page, getInstantEditorUrl }) => { diff --git a/yarn.lock b/yarn.lock index 852d05a..86b3707 100644 --- a/yarn.lock +++ b/yarn.lock @@ -435,6 +435,11 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@faker-js/faker@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-7.6.0.tgz#9ea331766084288634a9247fcd8b84f16ff4ba07" + integrity sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw== + "@jest/types@^27.2.5", "@jest/types@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80"