diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 00000000..4f87a133 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,66 @@ +name: Playwright Tests CI + +on: + push: + branches: + - mariia-iakovenko + - 'release/*' + - 'feature/*' + pull_request: + branches: + - mariia-iakovenko + - 'release/*' + - 'feature/*' + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: lts/* + + - name: Install dependencies + run: npm install + + - name: Load environment variables from test.env + run: | + cp env/test.env .env + export $(grep -v '^#' .env | xargs) + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Run Playwright tests + env: + TEST_ENV: test + run: npx playwright test --reporter=html + + - name: Compress the report + if: always() + run: | + zip -r playwright-report.zip playwright-report/ + + - name: Upload Playwright Report to Artifactory + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report.zip + + - name: Upload ZIP to Uploadcare + if: always() + run: | + curl -X POST \ + -F "UPLOADCARE_STORE=1" \ + -F "UPLOADCARE_PUB_KEY=$UPLOADCARE_PUB_KEY" \ + -F "file=@playwright-report.zip" \ + https://upload.uploadcare.com/base/ + + - name: Cleanup + run: rm -rf playwright-report.zip playwright-report/ \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..815491c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Node modules +node_modules/ + +# Playwright artifacts +test-results/ +playwright-report/ +playwright/.cache/ + +# Log files +*.log + +# Build artifacts +dist/ +build/ + +# Playwright storage state +storageState.json +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..1b8aa883 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,7 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [] +} diff --git a/components/form.js b/components/form.js new file mode 100644 index 00000000..e8fde560 --- /dev/null +++ b/components/form.js @@ -0,0 +1,90 @@ +const HomePage = require("../pages/home-page"); + +/** Form class with functions to populate a form with fields. Currently is used for Signup/Login test */ + +class Form extends HomePage { + /**Locators */ + getSelectOption(optionName) { + return this.page.getByRole("option").filter({ hasText: optionName }); + } + + /** + * Populates a form with the given fields. + * + * @param {Object[]} fields An array of field objects, each with the following properties: + * @property {string} type The type of field, such as `text` or `select`. + * @property {string} label The label of the field. + * @property {string|number|boolean} value The value of the field. + * @returns {Promise} A promise that resolves when the form has been populated. + * @throws {Error} If an error occurs while populating the form. + */ + async populateForm(fields) { + let field; + try { + for (field of fields) { + await this.setField(field.type, field.label, field.value); + } + } catch (error) { + throw new Error(`Unable to set field: ${error.message}, ${error.stack}`); + } + } + + /** + * Sets the value of a field. + * + * @param {string} fieldType The type of field (text, select, dropdown, money, date, lookup, and toggle) + * @param {string} label The label of the field. + * @param {string} value The value to set. + * @throws {Error} If the field type is not supported. + */ + async setField(fieldType, label, value) { + switch (fieldType.toLowerCase()) { + case "text": + await this.setTextField(label, value); + break; + case "select": + await this.setSelectField(label, value); + break; + default: + throw new Error(`${fieldType} is not a supported field type`); + } + } + + /** + * Sets the value of a form text field. + * @param {string} label The label of the text field. + * @param {string} value The value to set the text field to. + * @throws {Error} If the text field cannot be found or cannot be set. + */ + async setTextField(label, value) { + try { + const field = this.page.getByLabel(label); + await field.clear(); + await field.fill(value); + } catch (error) { + throw new Error( + `Unable to set field ${label}: ${error.message}, ${error.stack}` + ); + } + } + + /** + * Sets the value of a form select field. + * @param {string} label The label of the select field. + * @param {string} value The value to set the select field to. + * @throws {Error} If the select field cannot be found or cannot be set. + */ + async setSelectField(label, value) { + try { + const element = this.page.getByLabel(label); + await element.click(); + await this.getSelectOption(value).click(); + } catch (error) { + throw new Error( + `Unable to set field ${label}: ${error.message}, ${error.stack}` + ); + } + } +} + +module.exports = Form; diff --git a/env/test.env b/env/test.env new file mode 100644 index 00000000..62068c30 --- /dev/null +++ b/env/test.env @@ -0,0 +1,8 @@ +# Variables set here are related to the test environment + +# CREXI +URL=https://www.crexi.com/ +PROFILE_URL=https://www.crexi.com/dashboard/profile +PROPERTY_RECORDS_URL=https://www.crexi.com/property-records/search +TEST_EMAIL=iakovenko.maria@gmail.com +TEST_PASSWORD=vagaBE?EHiG3Pr9c \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..40a6f4fb --- /dev/null +++ b/package-lock.json @@ -0,0 +1,171 @@ +{ + "name": "sdet-assignment", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "@faker-js/faker": "^9.0.0", + "dotenv": "^16.4.5", + "playwright": "^1.47.0" + }, + "devDependencies": { + "@playwright/test": "^1.47.0", + "@types/node": "^22.5.4" + } + }, + "node_modules/@faker-js/faker": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.0.tgz", + "integrity": "sha512-dTDHJSmz6c1OJ6HO7jiUiIb4sB20Dlkb3pxYsKm0qTXm2Bmj97rlXIhlvaFsW2rvCi+OLlwKLVSS6ZxFUVZvjQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.0.tgz", + "integrity": "sha512-SgAdlSwYVpToI4e/IH19IHHWvoijAYH5hu2MWSXptRypLSnzj51PcGD+rsOXFayde4P9ZLi+loXVwArg6IUkCA==", + "dev": true, + "dependencies": { + "playwright": "1.47.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.5.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.0.tgz", + "integrity": "sha512-jOWiRq2pdNAX/mwLiwFYnPHpEZ4rM+fRSQpRHwEwZlP2PUANvL3+aJOF/bvISMhFD30rqMxUB4RJx9aQbfh4Ww==", + "dependencies": { + "playwright-core": "1.47.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.0.tgz", + "integrity": "sha512-1DyHT8OqkcfCkYUD9zzUTfg7EfTd+6a8MkD/NWOvjo0u/SCNd5YmY/lJwFvUZOxJbWNds+ei7ic2+R/cRz/PDg==", + "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 + } + }, + "dependencies": { + "@faker-js/faker": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.0.tgz", + "integrity": "sha512-dTDHJSmz6c1OJ6HO7jiUiIb4sB20Dlkb3pxYsKm0qTXm2Bmj97rlXIhlvaFsW2rvCi+OLlwKLVSS6ZxFUVZvjQ==" + }, + "@playwright/test": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.0.tgz", + "integrity": "sha512-SgAdlSwYVpToI4e/IH19IHHWvoijAYH5hu2MWSXptRypLSnzj51PcGD+rsOXFayde4P9ZLi+loXVwArg6IUkCA==", + "dev": true, + "requires": { + "playwright": "1.47.0" + } + }, + "@types/node": { + "version": "22.5.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", + "dev": true, + "requires": { + "undici-types": "~6.19.2" + } + }, + "dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + }, + "playwright": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.0.tgz", + "integrity": "sha512-jOWiRq2pdNAX/mwLiwFYnPHpEZ4rM+fRSQpRHwEwZlP2PUANvL3+aJOF/bvISMhFD30rqMxUB4RJx9aQbfh4Ww==", + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.47.0" + } + }, + "playwright-core": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.0.tgz", + "integrity": "sha512-1DyHT8OqkcfCkYUD9zzUTfg7EfTd+6a8MkD/NWOvjo0u/SCNd5YmY/lJwFvUZOxJbWNds+ei7ic2+R/cRz/PDg==" + }, + "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 + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..e5a38dcf --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "dependencies": { + "@faker-js/faker": "^9.0.0", + "dotenv": "^16.4.5", + "playwright": "^1.47.0" + }, + "devDependencies": { + "@playwright/test": "^1.47.0", + "@types/node": "^22.5.4" + }, + "scripts": {} +} diff --git a/pages/account-settings-page.js b/pages/account-settings-page.js new file mode 100644 index 00000000..3c884b7d --- /dev/null +++ b/pages/account-settings-page.js @@ -0,0 +1,60 @@ +const HomePage = require("./home-page"); +const path = require("path"); + +/** Page that represents Account Settings page for update profile photo test. + * Extends HomePage class to sets the page to the correct playwright object type */ +/** + * + * @class + * @extends HomePage + */ +class AccountSettingsPage extends HomePage { + /** Locators */ + + get btnEditPersonalInfo() { + return this.page.locator( + 'div[class="card-container personal"] a[class="profile-link"]', + { hasText: " Edit " } + ); + } + + get headerEditInfo() { + return this.page.locator('[class="profile-edit-modal"] h3'); + } + + get editProfilePicture() { + return this.page.locator( + 'div[class="profile-dashboard profile-info_ava"] input[name="fileInput"]' + ); + } + + get firstName() { + return this.page.locator('input[name="firstName"]'); + } + + get lastName() { + return this.page.locator('input[name="lastName"]'); + } + + get updatePersonalData() { + return this.page.locator('button[class*="cui-button-primary"]'); + } + + get profileImageUpdated() { + return this.page.locator('[class*="thumb-image ng-star-inserted"]'); + } + + /** + * Uploads a profile picture. + * + * @param {File} file - The file to be uploaded. + * @returns {Promise} - A promise that resolves when the profile picture is uploaded. + */ + async uploadProfilePicture(file) { + await this.editProfilePicture.setInputFiles(file); + await this.updatePersonalData.waitFor({isVisible: true}, {timeout: 5000}); + await this.updatePersonalData.click(); + } +} + +module.exports = AccountSettingsPage; diff --git a/pages/home-page.js b/pages/home-page.js new file mode 100644 index 00000000..cacea7fb --- /dev/null +++ b/pages/home-page.js @@ -0,0 +1,41 @@ +const { Page } = require("@playwright/test"); + +/** Base Crexi Home page with locators for elements on main home page crexi.com and on main page after user login + * Sets the page to the correct playwright object type to enable code completion. + * Is being inherited by other pages to set pages defaullt constructors and correct playwright object type*/ + +class HomePage { + /** + * @type {Page} */ + page; + + /** + * + * @param {Page} page - the playwright page object that is tied to this page + */ + constructor(page) { + this.page = page; + } + + /**Locators */ + + get btnSignUpOrLogin() { + return this.page.locator("button.signup"); + } + + get dashboardLink() { + return this.page.locator( + 'a[class*="header-nav-link"] [class="my-crexi-text"]' + ); + } + + get accountSettingsLink() { + return this.page.locator('[data-cy="link"]:has-text("Account Settings")'); + } + + get signedInProfile() { + return this.page.locator('[class*="user-logged-in"]'); + } +} + +module.exports = HomePage; diff --git a/pages/properties-page.js b/pages/properties-page.js new file mode 100644 index 00000000..21128ea2 --- /dev/null +++ b/pages/properties-page.js @@ -0,0 +1,89 @@ +const HomePage = require("./home-page"); + +/** Page that represents Properties Page with locators and functions related to it + * ext */ + +class PropertiesPage extends HomePage { + /**Locators */ + get getFirstProperty() { + return this.page.locator('[class*="badge-container"]').first(); + } + + get propertyDetailsLink() { + return this.page + .locator("crx-smart-nav-tabs") + .getByRole("link", { name: "Property Details" }); + } + + get propertyDetailsTitle() { + return this.page.locator('[data-cy="property-details-title"]'); + } + + get propertyStatusDropdown() { + return this.page.locator('[data-cy="platformDropdown"]'); + } + + /** + * Retrieves the property status option locator based on the given option name. + * + * @param {string} optionName - The name of the option to search for. + * @returns {Locator} - The property status option element. + */ + getPropertyStatusOption(optionName) { + return this.page.locator(`[class*="options"] li:has-text("${optionName}")`); + } + + get currentPropertyStatusFilterName() { + return this.page + .locator('[class="selected ng-star-inserted"]') + .textContent(); + } + + /** + * Returns the count of search results. + * + * @returns {number} The count of search results. + */ + get searchResultsCount() { + return this.page.locator('[data-cy="resultsCount"]').textContent(); + } + + /**Methods */ + + async clickFirstPropertyDetails() { + try { + await this.getFirstProperty.waitFor({ state: "visible" }); + await this.getFirstProperty.click(); + await this.page.waitForLoadState("domcontentloaded"); + await this.propertyDetailsLink.waitFor({ state: "visible" }); + await this.propertyDetailsLink.click(); + } catch (error) { + throw new Error( + `Unable to click on first property: ${error.message}, ${error.stack}` + ); + } + } + + async setPropertyStatusFilter(propertyStatusFilterName) { + try { + if ( + (await this.currentPropertyStatusFilterName) !== + propertyStatusFilterName + ) { + await this.propertyStatusDropdown.waitFor({ state: "visible" }); + await this.propertyStatusDropdown.click(); + await this.getPropertyStatusOption(propertyStatusFilterName).waitFor({ + state: "visible", + }); + await this.getPropertyStatusOption(propertyStatusFilterName).click(); + await this.page.waitForLoadState("load"); + } + } catch (error) { + throw new Error( + `Unable to set property status: ${error.message}, ${error.stack}` + ); + } + } +} + +module.exports = PropertiesPage; diff --git a/pages/signup-login-page.js b/pages/signup-login-page.js new file mode 100644 index 00000000..4fb52b90 --- /dev/null +++ b/pages/signup-login-page.js @@ -0,0 +1,74 @@ +const HomePage = require("./home-page"); +const Form = require("../components/form"); + +/** Page that represents SignUp/Login popup with locators and functions related to it */ + +class SignUpLoginPage extends HomePage { + constructor(page) { + super(page); + this.form = new Form(page); + } + + /**Locators */ + + get btnLogin() { + return this.page.locator('button[data-cy="button-login"]'); + } + + get btnSignUp() { + return this.page.locator('button[data-cy="button-signup"]'); + } + + getSignUpModalTab(tabName) { + return this.page.locator(`#signupModal .tab:has-text("${tabName}")`); + } + + /**Methods */ + /** + * Sign up a user. + * + * @param {Object[]} fields An array of field objects, each with the following properties: + * @property {string} type The type of field, such as `text` or `select`. + * @property {string} label The label of the field. + * @property {string|number} value The value of the field. + * @throws {Error} If an error occurs during the sign-up process. + */ + async signUp(fields) { + try { + await this.btnSignUpOrLogin.click(); + await this.form.populateForm(fields); + await this.btnSignUp.click(); + await this.page.waitForLoadState("load"); + } catch (error) { + throw new Error( + `An error occurred during the sign-up process: ${error.message}, ${error.stack}` + ); + } + } + + /** + * Login a user. + * + * @param {Object[]} fields An array of field objects, each with the following properties: + * @property {string} type The type of field, such as `text` or `select`. + * @property {string} label The label of the field. + * @property {string|number} value The value of the field. + * @throws {Error} If an error occurs during the login process. + */ + + async login(fields) { + try { + await this.btnSignUpOrLogin.click(); + await this.getSignUpModalTab("Log In").click(); + await this.form.populateForm(fields); + await this.btnLogin.click(); + await this.page.waitForLoadState("domcontentloaded"); + } catch (error) { + throw new Error( + `An error occurred during the login process: ${error.message}, ${error.stack}` + ); + } + } +} + +module.exports = SignUpLoginPage; diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 00000000..6c9f5b02 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,46 @@ +const { defineConfig } = require("@playwright/test"); + +/** + * Read environment variables from .env file + * httpa://github.com/motdotla/dotenv + */ + +if (process.env.TEST_ENV === "test") { + require("dotenv").config({ path: "./env/test.env" }); +} + +/** + * @see https://playwright.dev/docs/test-configuration + */ + +module.exports = defineConfig({ + // Run tests in headless browsers + use: { + headless: process.env.CI ? true : false, + screenshot: "only-on-failure", + // Collect trace when retrying failed tests. See https://playwright.dev/docs/trace-viewer + trace: "retain-on-failure", + }, + // Test files + testDir: "./tests", + // Test timeout + timeout: 180000, + fullyParralel: true, + viewport: { width: 1920, height: 1080 }, + //Retry on CI only + retries: process.env.CI ? 1 : 0, + // Number of workers on CI, use default + workers: process.env.CI ? 4 : undefined, + // Reporter to use. See https://playwright.dev/docs/test-reporters + reporter: [ + ["html", { outputFolder: "reports/playwright" }], + ["junit", { outputFile: "results.xml" }], + ], + // Keeping only one browser in here for simplicity + projects: [ + { + name: "chrome", + use: { browserName: "chromium" }, + }, + ], +}); diff --git a/reports/playwright/index.html b/reports/playwright/index.html new file mode 100644 index 00000000..aa7fdca6 --- /dev/null +++ b/reports/playwright/index.html @@ -0,0 +1,68 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + \ No newline at end of file diff --git a/resources/login-fixture.js b/resources/login-fixture.js new file mode 100644 index 00000000..3dccd19e --- /dev/null +++ b/resources/login-fixture.js @@ -0,0 +1,48 @@ +/** This file contains the login that is usong Playwright's fixture which is used to login the test user before running the tests + * Navigation to the needed pages are done with direct url navigation as we are not testing the navigation itself but the functionality mentioned in the test + */ +const { test: base } = require("@playwright/test"); +const SignUpLoginPage = require("../pages/signup-login-page"); +const { populateLoginData } = require("./test-data/data-generator"); +const HomePage = require("../pages/home-page"); +const loadRequest = "https://t.crexi.com/track/*"; + +const test = base.extend({ + testUserLogin: async ({ page }, use) => { + await page.goto(`${process.env.URL}`); + const loginData = populateLoginData( + `${process.env.TEST_EMAIL}`, + `${process.env.TEST_PASSWORD}` + ); + const signUpLoginPage = new SignUpLoginPage(page); + const homePage = new HomePage(page); + await signUpLoginPage.login(loginData.properties); + await homePage.signedInProfile.waitFor({ + state: "visible", + timeout: 100000, + }); + await use({ page }); + }, + + testUserLoginAccountProfilePage: async ({ testUserLogin }, use) => { + const { page } = testUserLogin; + await page.goto(`${process.env.PROFILE_URL}`, { + waitUntil: "load", + timeout: 100000, + }); + await page.waitForRequest(loadRequest, { timeout: 100000 }); + await use({ page }); + }, + + testUserLoginPropertyDataPage: async ({ testUserLogin }, use) => { + const { page } = testUserLogin; + await page.goto(`${process.env.PROPERTY_RECORDS_URL}`, { + waitUntil: "load", + timeout: 100000, + }); + await page.waitForRequest(loadRequest, { timeout: 100000 }); + await use({ page }); + }, +}); + +module.exports = { test, expect: base.expect }; diff --git a/resources/photo/picture.jpg b/resources/photo/picture.jpg new file mode 100644 index 00000000..82a146fe Binary files /dev/null and b/resources/photo/picture.jpg differ diff --git a/resources/photo/picture2.jpg b/resources/photo/picture2.jpg new file mode 100644 index 00000000..d074f35b Binary files /dev/null and b/resources/photo/picture2.jpg differ diff --git a/resources/test-data/data-generator.js b/resources/test-data/data-generator.js new file mode 100644 index 00000000..9cebcb8a --- /dev/null +++ b/resources/test-data/data-generator.js @@ -0,0 +1,68 @@ +const { faker } = require("@faker-js/faker"); + +/** Function that returns generated data object for SignUp form. Uses faker library to generate random data. + *Usage of this library would depend on the project requirements. + */ + +function populateSignUpData() { + const password = faker.internet.password(); + /**Commenting some roles.For those roles we have extra logic upon sign up. + * My current logic for setting form fields is looking for label that includes text. + * Currently we have some labels that are not unique, so I commented those as well + * */ + const industryRole = [ + "Listing Broker/Agent", + "Buyer Broker/Agent", + "Selling/Buying Broker/Agent", + "Transaction Coordinator", + // "Landlord Broker/Agent", + // "Tenant Rep Broker", + "Principal", + "Lender", + "Assessor", + "Appraiser", + "Third Party Service", + // "Tenant", + "Owner/Property Manager", + "Other", + ]; + return { + properties: [ + { label: "First Name", type: "text", value: faker.person.firstName() }, + { label: "Last Name", type: "text", value: faker.person.lastName() }, + { label: "Email", type: "text", value: faker.internet.email() }, + { label: "Password", type: "text", value: password }, + { + label: "Industry Role", + type: "select", + value: industryRole[Math.floor(Math.random() * industryRole.length)], + }, + { + label: "Phone Number", + type: "text", + value: faker.phone.number({ style: "national" }), + }, + ], + }; +} +//If we would want to login with the same data as we signed up, we would take the email and password from the signUpData object and use it in the loginData object. +//In our case I am using separate data for login as I am not doing any verification of succesful sign up and login with the same user. +//In Ideal world we would want to store our sensitive data in a secure place like secret manager or encrypt it and not just store a raw data in env file. + +/** + * Generates login data for a test user. + * + * @param {string} testUserEmail - The email of the test user. + * @param {string} testUserPassword - The password of the test user. + * @returns {object} - An object containing properties for email and password. + */ +function populateLoginData(testUserEmail, testUserPassword) { + return { + properties: [ + { label: "Email", type: "text", value: testUserEmail }, + { label: "Password", type: "text", value: testUserPassword }, + ], + }; +} + +module.exports = { populateSignUpData, populateLoginData }; diff --git a/results.xml b/results.xml new file mode 100644 index 00000000..7ce44492 --- /dev/null +++ b/results.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/property-details-and-search.spec.js b/tests/property-details-and-search.spec.js new file mode 100644 index 00000000..5bfb3f56 --- /dev/null +++ b/tests/property-details-and-search.spec.js @@ -0,0 +1,33 @@ +// @ts-check +const { test, expect } = require("../resources/login-fixture"); +const PropertiesPage = require("../pages/properties-page"); +//Test uses the testUserLoginPropertyDataPage fixture to login and navigate to the Property Data page +test.describe("Property related tests: View property details and search property", () => { + test("Property Details: Users can click on a property to view its details.", async ({ + testUserLoginPropertyDataPage, + }) => { + const page = testUserLoginPropertyDataPage.page; + const propertiesPage = new PropertiesPage(page); + + await test.step("Click on property data from User profile and check property details are displayed", async () => { + await propertiesPage.clickFirstPropertyDetails(); + await expect(propertiesPage.propertyDetailsTitle).toBeVisible({ + timeout: 10000, + }); + }); + }); + + test("Users can search property by property status criteria", async ({ + testUserLoginPropertyDataPage, + }) => { + const page = testUserLoginPropertyDataPage.page; + const propertiesPage = new PropertiesPage(page); + + await test.step("Click on property data from user profile and check results are refreshed", async () => { + const initialResultCount = await propertiesPage.searchResultsCount; + await propertiesPage.setPropertyStatusFilter("For Sale"); + const actualResultCount = await propertiesPage.searchResultsCount; + await expect(initialResultCount).not.toEqual(actualResultCount); + }); + }); +}); diff --git a/tests/signup-and-login.spec.js b/tests/signup-and-login.spec.js new file mode 100644 index 00000000..51632b94 --- /dev/null +++ b/tests/signup-and-login.spec.js @@ -0,0 +1,36 @@ +// @ts-check +const { test, expect } = require("@playwright/test"); +const { + populateSignUpData, + populateLoginData, +} = require("../resources/test-data/data-generator"); +const SignUpLoginPage = require("../pages/signup-login-page"); + +test.beforeEach(async ({ page }) => { + // @ts-ignore + await page.goto(`${process.env.URL}`); +}); + +test.describe("User Login: Users can register and log in with a username and password.", () => { + /**Was not able to get succesful sign up as at the moment of developing test I was getting error (I have sent email to Rameet), + Was getting 400 error which indicates bad request but there were no errors on UI (might be a bug?)*/ + test("Sign Up to Crexi", async ({ page }) => { + const signUpData = populateSignUpData(); + const signUpLoginPage = new SignUpLoginPage(page); + await signUpLoginPage.signUp(signUpData.properties); + }); + //logging in with my test user and not with user from previous test due to error above + test("Log In to Crexi", async ({ page }) => { + // @ts-ignore + const loginData = populateLoginData( + `${process.env.TEST_EMAIL}`, + `${process.env.TEST_PASSWORD}` + ); + const signUpLoginPage = new SignUpLoginPage(page); + + await signUpLoginPage.login(loginData.properties); + await expect( + page.getByTitle("commercial real estate by crexi") + ).toBeVisible({ timeout: 10000 }); + }); +}); diff --git a/tests/update-profile-photo.spec.js b/tests/update-profile-photo.spec.js new file mode 100644 index 00000000..e2434e0b --- /dev/null +++ b/tests/update-profile-photo.spec.js @@ -0,0 +1,46 @@ +const { test, expect } = require("../resources/login-fixture"); +const AccountSettingsPage = require("../pages/account-settings-page"); + +test.describe("Upload profile photo and cover", () => { + test("Upload your profile photo ", async ({ + testUserLoginAccountProfilePage, + }) => { + const page = testUserLoginAccountProfilePage.page; + const accSettingsPage = new AccountSettingsPage(page); + const profilePhoto1 = "./resources/photo/picture.jpg"; + const profilePhoto2 = "./resources/photo/picture2.jpg"; + + test.step("Edit Personal Info, select and upload profile photo", async () => { + await accSettingsPage.btnEditPersonalInfo.click(); + await expect(accSettingsPage.headerEditInfo).toHaveText("Edit Info"); + await accSettingsPage.uploadProfilePicture(profilePhoto1); + await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes("/account") && + response.finished() && + response.status() === 200 && + response.request().method() === "PATCH" + ), + expect(accSettingsPage.profileImageUpdated).toBeVisible(), + ]); + }); + + test.step("Upload second picture to check that photo actually changed ", async () => { + await accSettingsPage.btnEditPersonalInfo.click(); + await expect(accSettingsPage.headerEditInfo).toHaveText("Edit Info"); + await accSettingsPage.uploadProfilePicture(profilePhoto2); + + await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes("/account") && + response.finished() && + response.status() === 200 && + response.request().method() === "PATCH" + ), + expect(accSettingsPage.profileImageUpdated).toBeVisible(), + ]); + }); + }); +});