diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..9662b54 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm install -g pnpm && pnpm install + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps + - name: Run Playwright tests + run: pnpm exec playwright test + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4892497 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +.env \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/README.md b/README.md index 88be3ea..e160cec 100644 --- a/README.md +++ b/README.md @@ -1 +1,25 @@ -# shay-automation-task \ No newline at end of file +# shay-automation-task + +This project is the automation home assignment - https://docs.google.com/document/d/1ixrNCz_rgYYVfeGnUi9ZnBL-OUsqvsP4IaxIAuguRWk/edit + +## Prerequisites + +Before you begin, ensure you have met the following requirements: + +- Node.js (version 18.x or later) +- pnpm (version 8.x or later) +- Access to Cloudinary account credentials + +## Setup + +1. **Clone this Repository** +2. Install the project dependencies using pnpm: +pnpm install +3. Create a .env file in the root directory of the project and add your Cloudinary credentials: +EMAIL=your-email +PASSWORD=your-password + **Make sure to replace your-email and your-password with your actual Cloudinary credentials** +4. Install Playwright: +pnpm exec playwright install +5. To run the tests, use the following command: +pnpm exec playwright test diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0f873a7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,69 @@ +{ + "name": "shay-automation-task", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "shay-automation-task", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@playwright/test": "^1.45.3", + "playwright": "^1.45.3" + } + }, + "node_modules/@playwright/test": { + "version": "1.45.3", + "resolved": "https://cloudinary-232482882421.d.codeartifact.us-east-1.amazonaws.com/npm/cld-npm-store/@playwright/test/-/test-1.45.3.tgz", + "integrity": "sha512-UKF4XsBfy+u3MFWEH44hva1Q8Da28G6RFtR2+5saw+jgAFQV5yYnB1fu68Mz7fO+5GJF3wgwAIs0UelU8TxFrA==", + "dependencies": { + "playwright": "1.45.3" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://cloudinary-232482882421.d.codeartifact.us-east-1.amazonaws.com/npm/cld-npm-store/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.45.3", + "resolved": "https://cloudinary-232482882421.d.codeartifact.us-east-1.amazonaws.com/npm/cld-npm-store/playwright/-/playwright-1.45.3.tgz", + "integrity": "sha512-QhVaS+lpluxCaioejDZ95l4Y4jSFCsBvl2UZkpeXlzxmqS+aABr5c82YmfMHrL6x27nvrvykJAFpkzT2eWdJww==", + "dependencies": { + "playwright-core": "1.45.3" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.45.3", + "resolved": "https://cloudinary-232482882421.d.codeartifact.us-east-1.amazonaws.com/npm/cld-npm-store/playwright-core/-/playwright-core-1.45.3.tgz", + "integrity": "sha512-+ym0jNbcjikaOwwSZycFbwkWgfruWvYlJfThKYAlImbxUgdWFO2oW70ojPm4OpE4t6TAo2FY/smM+hpVTtkhDA==", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ad937e8 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "shay-automation-task", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "dotenv": "^16.4.5", + "playwright": "^1.45.3", + "uuid": "^10.0.0" + }, + "devDependencies": { + "@playwright/test": "^1.45.3", + "@types/node": "^22.0.0" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..31eedbd --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,82 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Set timeout to 50 sec */ + timeout: 60000, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + headless: false, + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* 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/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..20e81d5 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,74 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + playwright: + specifier: ^1.45.3 + version: 1.45.3 + uuid: + specifier: ^10.0.0 + version: 10.0.0 + +devDependencies: + '@playwright/test': + specifier: ^1.45.3 + version: 1.45.3 + '@types/node': + specifier: ^22.0.0 + version: 22.0.0 + +packages: + + /@playwright/test@1.45.3: + resolution: {integrity: sha512-UKF4XsBfy+u3MFWEH44hva1Q8Da28G6RFtR2+5saw+jgAFQV5yYnB1fu68Mz7fO+5GJF3wgwAIs0UelU8TxFrA==} + engines: {node: '>=18'} + hasBin: true + dependencies: + playwright: 1.45.3 + dev: true + + /@types/node@22.0.0: + resolution: {integrity: sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==} + dependencies: + undici-types: 6.11.1 + dev: true + + /dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + dev: false + + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + requiresBuild: true + optional: true + + /playwright-core@1.45.3: + resolution: {integrity: sha512-+ym0jNbcjikaOwwSZycFbwkWgfruWvYlJfThKYAlImbxUgdWFO2oW70ojPm4OpE4t6TAo2FY/smM+hpVTtkhDA==} + engines: {node: '>=18'} + hasBin: true + + /playwright@1.45.3: + resolution: {integrity: sha512-QhVaS+lpluxCaioejDZ95l4Y4jSFCsBvl2UZkpeXlzxmqS+aABr5c82YmfMHrL6x27nvrvykJAFpkzT2eWdJww==} + engines: {node: '>=18'} + hasBin: true + dependencies: + playwright-core: 1.45.3 + optionalDependencies: + fsevents: 2.3.2 + + /undici-types@6.11.1: + resolution: {integrity: sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==} + dev: true + + /uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + dev: false diff --git a/tests/pom/assetManagePage.ts b/tests/pom/assetManagePage.ts new file mode 100644 index 0000000..c796dc6 --- /dev/null +++ b/tests/pom/assetManagePage.ts @@ -0,0 +1,17 @@ +import {Locator, Page} from '@playwright/test'; + +const ASSET_TITLE_SELECTOR = "//*[@data-test='manage-top-bar']//*[@data-test='asset-title']";; + + +/** + * Asset manage page object + */ +export class AssetManagePage { + public page: Page; + public assetTitle: Locator + + constructor(page: Page) { + this.page = page; + this.assetTitle = page.locator(ASSET_TITLE_SELECTOR); + } +} \ No newline at end of file diff --git a/tests/pom/loginPage.ts b/tests/pom/loginPage.ts new file mode 100644 index 0000000..f8a2908 --- /dev/null +++ b/tests/pom/loginPage.ts @@ -0,0 +1,37 @@ +import {Locator, Page} from '@playwright/test'; + +const EMAIL_FIELD_SELECTOR = "//*[@id='user_session_email']"; +const PASSWORD_FIELD_SELECTOR = "//*[@id='user_session_password']"; +const LOGIN_BUTTON_SELECTOR = "//*[@id='sign-in']"; + + +/** + * Login page object + */ +export class LoginPage { + public page: Page; + public emailInput: Locator; + public passwordInput: Locator; + public loginButton: Locator; + + + constructor(page: Page) { + this.page = page; + this.emailInput = page.locator(EMAIL_FIELD_SELECTOR); + this.passwordInput = page.locator(PASSWORD_FIELD_SELECTOR); + this.loginButton = page.locator(LOGIN_BUTTON_SELECTOR); + } + + /** + * Make login + */ + public async login(email: string, password: string) { + await this.emailInput.fill(email); + await this.passwordInput.fill(password); + await this.loginButton.click(); + } + + public async goto(): Promise { + await this.page.goto('https://console-staging.cloudinary.com/users/login'); + } +} \ No newline at end of file diff --git a/tests/pom/mediaLibraryPage.ts b/tests/pom/mediaLibraryPage.ts new file mode 100644 index 0000000..3ca5f06 --- /dev/null +++ b/tests/pom/mediaLibraryPage.ts @@ -0,0 +1,32 @@ +import {Locator, Page} from '@playwright/test'; + +const ASSETS_TAB_SELECTOR = "//*[@data-test='tab-mediaLibraryAssets']"; +const UPLOAD_BUTTON_SELECTOR = "//*[@data-test='upload-btn']"; +const ASSET_ACTION_MANAGE_BUTTON_SELECTOR = "//*[@data-test='action-manage-btn']"; + + + + +/** + * Media Library object + */ +export class MediaLibraryPage { + public page: Page; + public assetsTab: Locator; + public uploadButton: Locator; + public assetActionManageButton: Locator; + + + + constructor(page: Page) { + this.page = page; + this.assetsTab = page.locator(ASSETS_TAB_SELECTOR); + this.uploadButton = page.locator(UPLOAD_BUTTON_SELECTOR); + this.assetActionManageButton = page.locator(ASSET_ACTION_MANAGE_BUTTON_SELECTOR); + } + + public async goToMLHomePage() : Promise { + await this.page.goto('https://console-staging.cloudinary.com/console/media_library/homepage'); + } + +} \ No newline at end of file diff --git a/tests/pom/uploadWidgetFrame.ts b/tests/pom/uploadWidgetFrame.ts new file mode 100644 index 0000000..9c288bb --- /dev/null +++ b/tests/pom/uploadWidgetFrame.ts @@ -0,0 +1,35 @@ +import {FrameLocator, Locator, Page} from '@playwright/test'; + +const UPLOAD_WIDGET_IFRAME_LOCATOR = "//iframe[@data-test='uw-iframe']"; +const ADVANCED_BUTTON_SELECTOR = "//*[@data-test='btn-advanced' and @type='button']"; +const PUBLIC_ID_INPUT_SELECTOR = "//*[@data-test='public-id']"; +const UPLOAD_INPUT_SELECTOR = "//input[@name='file']"; +const UPLOAD_STATUS_COMPLETED_SELECTOR = "//*[@data-test='show-completed-button']"; + + +/** + * Upload widget iFrame object + */ +export class UploadWidgetFrame { + public page: Page; + public uploadWidgetIframe: FrameLocator; + public advanceButton: Locator; + public publicId: Locator; + public fileInput: Locator; + public uploadStatusCompleted: Locator; + + + constructor(page: Page) { + this.page = page; + this.uploadWidgetIframe = page.frameLocator(UPLOAD_WIDGET_IFRAME_LOCATOR); + this.advanceButton = page.frameLocator(UPLOAD_WIDGET_IFRAME_LOCATOR).locator(ADVANCED_BUTTON_SELECTOR); + this.publicId = page.frameLocator(UPLOAD_WIDGET_IFRAME_LOCATOR).locator(PUBLIC_ID_INPUT_SELECTOR); + this.fileInput = page.frameLocator(UPLOAD_WIDGET_IFRAME_LOCATOR).locator(UPLOAD_INPUT_SELECTOR); + this.uploadStatusCompleted = page.frameLocator(UPLOAD_WIDGET_IFRAME_LOCATOR).locator(UPLOAD_STATUS_COMPLETED_SELECTOR); + } + public async uploadLocalFile(filePath: string) { + // Wait for the file input to be visible before setting the file + await this.fileInput.waitFor({ state: 'visible' }); + await this.fileInput.setInputFiles(filePath); + } +} \ No newline at end of file diff --git a/tests/specs/homeAssignment.spec.ts b/tests/specs/homeAssignment.spec.ts new file mode 100644 index 0000000..db93c72 --- /dev/null +++ b/tests/specs/homeAssignment.spec.ts @@ -0,0 +1,62 @@ +import { test, expect } from '@playwright/test'; +import { LoginPage } from '../pom/LoginPage'; +import {MediaLibraryPage} from "../pom/mediaLibraryPage"; +import {UploadWidgetFrame} from "../pom/uploadWidgetFrame"; +import * as path from "path"; +import { v4 as uuidv4 } from "uuid"; +import {AssetManagePage} from "../pom/assetManagePage"; +// @ts-ignore +import dotenv from 'dotenv'; + +// Load environment variables from .env file +dotenv.config(); + +//Generating unique public ID +const uniquePublicId = `ha-${uuidv4()}`; +test('Shay Automation home assignment', async ({ page }) => { + await test.step('Login to Cloudinary', async () => { + const loginPage = new LoginPage(page); + await loginPage.goto(); + await loginPage.login(process.env.EMAIL, process.env.PASSWORD); + const cloudinaryLogo = page.locator("//*[@data-testid='cloudlogo']"); + // Wait for Cloudinary logo to be displayed, so we know the login succeeded + await cloudinaryLogo.waitFor(); + }); + await test.step('Open Media Library assets tab', async () => { + const mlPage = new MediaLibraryPage(page); + await mlPage.goToMLHomePage(); + await mlPage.assetsTab.click(); + await expect(page).toHaveURL(/.*media_library/); + }); + await test.step('Click on Upload button to open upload widget', async () => { + const mlPage = new MediaLibraryPage(page); + await mlPage.uploadButton.click(); + const uploadWidgetIframe = page.locator("//iframe[@data-test='uw-iframe']"); + // Wait for upload widget to open + await uploadWidgetIframe.waitFor(); + }); + await test.step('Click on Advance and insert public ID', async () => { + const uwFrame = new UploadWidgetFrame(page); + await uwFrame.advanceButton.click(); + await uwFrame.publicId.fill(uniquePublicId); + await uwFrame.advanceButton.click(); // click again on Advance to close it. + }); + await test.step('Upload any image from my PC ', async () => { + const uwFrame = new UploadWidgetFrame(page); + const filePath = path.resolve('/Users/shaylevi/Downloads/image_upload.jpg'); + await uwFrame.uploadLocalFile(filePath); + await uwFrame.uploadStatusCompleted.waitFor(); + }); + await test.step('Right click on the uploaded image and open manage page', async () => { + const mlAssetPage = new MediaLibraryPage(page); + const uploadedAsset = page.locator(`//*[@data-test-specifier='${uniquePublicId}']`); + await uploadedAsset.waitFor(); + await uploadedAsset.click({button: "right", force: true}); + await mlAssetPage.assetActionManageButton.click(); + }); + await test.step('Verify, that Public ID that was filled previously appears correctly', async () => { + const assetManagePage = new AssetManagePage(page); + const assetManagePublicId = await assetManagePage.assetTitle.innerText(); + expect(assetManagePublicId).toBe(uniquePublicId); + }); +}); \ No newline at end of file