From b0ce514e37c8a19dc591af6a22c31a8c3bf57c06 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 12:30:37 +0000 Subject: [PATCH] Documented `tests/pages/basePage.ts` with JSDoc comments for the class and all public methods. Documented `tests/login.spec.ts`, `tests/seed.spec.ts`, and `tests/userSendsMessage.spec.ts`. --- README.md | 170 ++++++++++++++----------- e2e/example.spec.ts | 9 ++ package-lock.json | 16 ++- tests/login.spec.ts | 29 ++++- tests/pages/basePage.ts | 78 +++++++++++- tests/pages/composeMessage.ts | 59 +++++++-- tests/pages/globalTearDown.ts | 10 +- tests/pages/hooks.ts | 10 +- tests/pages/linkedInSearchPage.ts | 44 ++++--- tests/pages/logAndReport.ts | 15 ++- tests/pages/loginPage.ts | 36 +++++- tests/pages/logoutPage.ts | 20 ++- tests/pages/readRecruiterNames.ts | 24 +++- tests/pages/userLoginandLogout.spec.ts | 15 ++- tests/seed.spec.ts | 8 ++ tests/userSendsMessage.spec.ts | 19 ++- 16 files changed, 446 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index b991d30..f6e70b3 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,124 @@ # LinkedIn Automation Testing Project -## πŸ“Œ Overview - -## E2E Test Automation Framework -Simulates a real user journey on LinkedIn β€” from login, navigating to search, selecting recruiters, composing messages, and logging out. -It doesn’t just test isolated units; instead, it validates the entire workflow across multiple pages and components. - -The framework also integrates with Zephyr for Jira for test case management, and is designed to run locally as well as in CI/CD pipelines (GitHub Actions, Jenkins, Docker). - -## πŸ› οΈ Tech Stack - -Playwright (E2E browser automation) - -TypeScript (strong typing and maintainability) - -ExcelJS / XLSX (data-driven tests using recruiter names) - -Node.js & npm (dependency management) - -Dotenv (environment variables for credentials & config) - -Zephyr Reporter (test management integration with Jira) - -Allure Reports (visual test execution reports) -Docker & Jenkins (CI/CD pipeline support) - -## Acknowledgements -Swaroop Landge https://www.linkedin.com/in/swaroop-landge-9a5b9111/ for continuous support, direction, and encouragement in shaping my journey into QA Automation. - -## 🧩 Page Object Model (POM) - -This project uses the Page Object Model (POM) design pattern for clean, reusable, and maintainable test automation. Each page has its own class with dedicated methods. - -basePage.ts β†’ Core utilities (navigation, waits, common actions) - -loginPage.ts β†’ Handles login (username, password, submit) - -linkedInSearchPage.ts β†’ Manages recruiter search - -composeMessage.ts β†’ Automates recruiter message composition +## πŸ“Œ Overview -logoutPage.ts β†’ Handles logout flow +This project is an **End-to-End (E2E) Test Automation Framework** designed to simulate a real user journey on LinkedIn. It automates critical workflows such as logging in, searching for recruiters, composing personalized messages, and logging out. -readRecruiterNames.ts β†’ Reads recruiter data from Excel (data-driven tests) +The framework goes beyond isolated unit tests by validating the entire workflow across multiple pages and components. It integrates with **Zephyr for Jira** for test case management and is built to run both locally and in CI/CD pipelines (GitHub Actions, Jenkins, Docker). -## flowchart TD - A[Login Page] --> B[LinkedIn Search Page] - B --> C[Compose Message Page] - C --> D[Logout Page] - E[Excel Data] --> B +## πŸš€ Features +- **E2E Browser Automation**: Uses **Playwright** for reliable and fast browser interactions. +- **Page Object Model (POM)**: Implements POM design patterns for maintainable and scalable code. +- **Data-Driven Testing**: Reads recruiter names from Excel files (`.xlsx`) to dynamically generate test cases. +- **TypeScript**: Written in TypeScript for strong typing and better developer tooling. +- **Environment Management**: Uses `.env` files for secure handling of credentials and configuration. +- **Robust Reporting**: Integrates **Allure Reports** for visual execution results and **Zephyr** for Jira test management. +- **Email Notifications**: Automatically emails execution logs and reports upon test completion. +- **CI/CD Ready**: Configured for Docker and Jenkins integration. -βš™οΈ Setup & Installation -1. Clone Repository -git clone https://github.com/sameermanzur/Linkedin-Automation-Testing-Project/tree/End-to-End-Flow -cd LinkedIn-Automation-Project +## πŸ› οΈ Tech Stack -2. Install Dependencies +- **[Playwright](https://playwright.dev/)**: E2E testing framework. +- **[TypeScript](https://www.typescriptlang.org/)**: Programming language. +- **[Node.js](https://nodejs.org/)**: Runtime environment. +- **[ExcelJS](https://github.com/exceljs/exceljs) / [XLSX](https://docs.sheetjs.com/)**: For reading Excel data. +- **[Dotenv](https://github.com/motdotla/dotenv)**: Environment variable management. +- **[Nodemailer](https://nodemailer.com/)**: For sending email reports. +- **[Allure](https://allurereport.org/)**: Reporting tool. + +## πŸ“‚ Project Structure + +``` +β”œβ”€β”€ data/ +β”‚ └── recruiterList.xlsx # Data file containing recruiter names +β”œβ”€β”€ e2e/ +β”‚ └── example.spec.ts # Sample Playwright test +β”œβ”€β”€ tests/ +β”‚ β”œβ”€β”€ pages/ # Page Object Models and Utilities +β”‚ β”‚ β”œβ”€β”€ basePage.ts # Base class for all pages +β”‚ β”‚ β”œβ”€β”€ loginPage.ts # Login page interactions +β”‚ β”‚ β”œβ”€β”€ linkedInSearchPage.ts # Search functionality +β”‚ β”‚ β”œβ”€β”€ composeMessage.ts # Message composition logic +β”‚ β”‚ β”œβ”€β”€ logoutPage.ts # Logout interactions +β”‚ β”‚ β”œβ”€β”€ readRecruiterNames.ts # Excel reading utility +β”‚ β”‚ β”œβ”€β”€ logAndReport.ts # Email reporting utility +β”‚ β”‚ β”œβ”€β”€ hooks.ts # Test hooks (setup/teardown) +β”‚ β”‚ └── globalTearDown.ts # Global cleanup scripts +β”‚ β”œβ”€β”€ login.spec.ts # Login test suite +β”‚ β”œβ”€β”€ userSendsMessage.spec.ts # Main E2E workflow test +β”‚ └── seed.spec.ts # Data seeding placeholder +β”œβ”€β”€ playwright.config.ts # Playwright configuration +β”œβ”€β”€ package.json # Project dependencies and scripts +└── README.md # Project documentation +``` + +## βš™οΈ Setup & Installation + +### 1. Clone the Repository +```bash +git clone https://github.com/sameermanzur/Linkedin-Automation-Testing-Project.git +cd Linkedin-Automation-Testing-Project +``` + +### 2. Install Dependencies +```bash npm install +``` -3. Configure Environment - -Create a .env file: +### 3. Configure Environment Variables +Create a `.env` file in the root directory and add the following variables: +```env BASE_URL=https://www.linkedin.com -USERNAME=your_email@example.com -PASSWORD=your_password +LINKEDIN_USERNAME=your_email@example.com +LINKEDIN_PASSWORD=your_password +GOOGLE_CLIENT_ID=your_google_client_id +GOOGLE_CLIENT_SECRET=your_google_client_secret +GOOGLE_REFRESH_TOKEN=your_google_refresh_token +GOOGLE_USER_EMAIL=your_email_for_sending_reports +REPORT_RECIPIENT=recipient_email@example.com ZEPHYR_TOKEN=your_zephyr_api_key +``` -4. Prepare Test Data +### 4. Prepare Test Data +Ensure `data/recruiterList.xlsx` exists and contains a list of names. The file should have headers like `First Name`, `Last Name`, or `Name`. -Update ./data/recruiterNames.xlsx with recruiter names in FirstName / LastName / Name format. +## ▢️ Running Tests -▢️ Running Tests -Local Execution +### Run All Tests +```bash npx playwright test +``` -Run Specific Test -npx playwright test tests/verifyE2EuserFlow.spec.ts --headed - -Clear Cache & Cookies (best practice hooks) +### Run Specific Test File +```bash +npx playwright test tests/userSendsMessage.spec.ts +``` -Hooks automatically clear session storage, cookies, and cache before each test run. +### Run in Headed Mode (Visible Browser) +```bash +npx playwright test --headed +``` -🧩 Key Features +### Generate Allure Report +```bash +npx allure generate ./allure-results --clean +npx allure open +``` -βœ… Data-driven testing using Excel recruiter list +## 🧩 Page Object Model (POM) Details -βœ… Page Object Model (POM) for maintainability +- **BasePage**: Contains common methods like `b_navigateTo`, `b_clickElement`, and `b_fillField`. +- **LoginPage**: Encapsulates login logic (`enterUserName`, `enterPassword`, `login`). +- **LinkedInSearchPage**: Handles navigating to the feed and searching for recruiters. +- **ComposeMessagePage**: Manages opening message dialogs, generating personalized text, and sending messages. +- **LogoutPage**: Handles the logout process to ensure a clean state. -βœ… Environment-driven setup with .env +## 🀝 Acknowledgements -βœ… Clear cache/cookies hooks for clean sessions +Special thanks to [Swaroop Landge](https://www.linkedin.com/in/swaroop-landge-9a5b9111/) for his continuous support and mentorship in QA Automation. -βœ… Integration with Jira Zephyr for reporting +## πŸ“„ License -βœ… CI/CD ready (Docker + Jenkins + GitHub Actions) +This project is licensed under the ISC License. diff --git a/e2e/example.spec.ts b/e2e/example.spec.ts index 54a906a..ca157fe 100644 --- a/e2e/example.spec.ts +++ b/e2e/example.spec.ts @@ -1,5 +1,9 @@ import { test, expect } from '@playwright/test'; +/** + * Test Case: has title + * Verifies that the Playwright homepage has the correct title. + */ test('has title', async ({ page }) => { await page.goto('https://playwright.dev/'); @@ -7,6 +11,11 @@ test('has title', async ({ page }) => { await expect(page).toHaveTitle(/Playwright/); }); +/** + * Test Case: get started link + * Verifies that the 'Get started' link on the Playwright homepage works correctly + * and navigates to the Installation page. + */ test('get started link', async ({ page }) => { await page.goto('https://playwright.dev/'); diff --git a/package-lock.json b/package-lock.json index 4f9bc0b..818b2dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -787,6 +787,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2332,6 +2333,7 @@ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright": "1.56.1" }, @@ -2439,6 +2441,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3214,6 +3217,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.12.0" } @@ -3526,6 +3530,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3589,6 +3594,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3624,6 +3630,7 @@ "integrity": "sha512-vSc+7iBmilejRbSiv0dakl+/EONHFUs3yDmEOydKmF0+aOMczRyMYOBvU42Ob51PrZozi11ExiQj9SCMH0c4bQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "allure-js-commons": "3.4.1" }, @@ -4056,6 +4063,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001735", "electron-to-chromium": "^1.5.204", @@ -5281,6 +5289,7 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -8605,6 +8614,7 @@ "integrity": "sha512-m1r6vPRnNbPVfhXWiuFuK3JlneI0717iMHqsj9MaCF/lCQ7nAdX2sklqgQmKnnG8Jg6INHgP3oaHcHSuBfZooQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@readme/json-schema-ref-parser": "^1.1.0", "@types/json-schema": "^7.0.11", @@ -8794,7 +8804,8 @@ "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/optionator": { "version": "0.9.4", @@ -9108,6 +9119,7 @@ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright-core": "1.56.1" }, @@ -10477,6 +10489,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -10646,6 +10659,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/tests/login.spec.ts b/tests/login.spec.ts index 156e3d3..944cec7 100644 --- a/tests/login.spec.ts +++ b/tests/login.spec.ts @@ -2,12 +2,28 @@ import { test, expect } from '@playwright/test'; import { LoginPage } from './pages/loginPage' import { sendReportEmail } from './pages/logAndReport'; +/** + * Test Suite: Login Tests + * Contains tests related to the user login functionality. + */ test.describe('Login Tests', () => { + /** + * Before each test, navigate to the base URL. + */ test.beforeEach(async ({ page }) => { const login = new LoginPage(page); await login.b_navigateTo(process.env.BASE_URL!); }); + /** + * Test Case [T2]: Login with Valid credentials. + * Tagged as @smoke. + * + * Steps: + * 1. Perform login with valid username and password. + * 2. Verify that the URL contains "feed", indicating a successful login. + * 3. Trigger the email report (Note: This might be better placed in a global teardown). + */ test('T2] Login with Valid credentials', {tag:'@smoke'}, async ({page }) => { const login = new LoginPage(page); await login.login(process.env.LINKEDIN_USERNAME!, process.env.LINKEDIN_PASSWORD!); @@ -16,6 +32,16 @@ test.describe('Login Tests', () => { await sendReportEmail(); }); + /** + * Test Case [T3]: Session persistence after login. + * + * Steps: + * 1. Login with valid credentials. + * 2. Open a new tab (page) in the same context. + * 3. Navigate to the base URL in the new tab. + * 4. Verify that the user is still logged in (URL pattern check). + * 5. Reload the original page and verify persistence. + */ test('[T3] Session persistence after login', async ({ page, context }) => { const login = new LoginPage(page); await login.login(process.env.LINKEDIN_USERNAME!, process.env.LINKEDIN_PASSWORD!); @@ -28,6 +54,3 @@ test.describe('Login Tests', () => { await expect(newPage).toHaveURL(/.*homepage/); }); }); - - - diff --git a/tests/pages/basePage.ts b/tests/pages/basePage.ts index acacac8..4722857 100644 --- a/tests/pages/basePage.ts +++ b/tests/pages/basePage.ts @@ -1,56 +1,130 @@ import { Page, Locator, expect } from '@playwright/test'; +/** + * Default maximum timeout for operations in milliseconds. + */ export const maxTimeout = 30_000; +/** + * BasePage class providing common utility methods for page interactions. + * This class serves as the parent class for all other page objects, offering + * shared functionality for element interaction, navigation, and validation. + */ export default class BasePage { + /** + * The Playwright Page instance used for interactions. + */ protected readonly page: Page; + /** + * Initializes a new instance of the BasePage class. + * @param page - The Playwright Page object. + */ constructor(page: Page) { this.page = page; } - + /** + * Navigates to the specified URL. + * @param url - The URL to navigate to. + * @param timeout - The maximum time to wait for the navigation in milliseconds. Defaults to maxTimeout. + * @returns A promise that resolves when the navigation is complete. + */ async b_navigateTo(url: string, timeout: number = maxTimeout) { await this.page.goto(url, { timeout, waitUntil: 'networkidle' }); } + /** + * Waits for an element to be visible on the page. + * @param locator - The locator of the element to wait for. + * @param timeout - The maximum time to wait in milliseconds. Defaults to maxTimeout. + * @returns A promise that resolves when the element is visible. + */ async b_waitForElementVisible(locator: Locator, timeout: number = maxTimeout) { await locator.waitFor({ state: 'visible', timeout }); } + /** + * Fills a text field with the specified text. + * @param element - The locator of the input field. + * @param text - The text to enter into the field. + * @param isForceFill - Whether to force fill the input (unused in current implementation but kept for compatibility). Defaults to false. + * @param timeout - The maximum time to wait for the element to be visible in milliseconds. Defaults to maxTimeout. + * @returns A promise that resolves when the field is filled. + */ async b_fillField(element: Locator, text: string, isForceFill: boolean = false, timeout: number = maxTimeout) { await this.b_waitForElementVisible(element, timeout); await element.pressSequentially(text, { timeout }); } + /** + * Clicks on an element. + * @param element - The locator of the element to click. + * @param timeout - The maximum time to wait for the element to be visible in milliseconds. Defaults to maxTimeout. + * @returns A promise that resolves when the element is clicked. + */ async b_clickElement(element: Locator, timeout: number = maxTimeout) { await this.b_waitForElementVisible(element, timeout); await element.click({ timeout }); } + /** + * Clears the text content of an input field. + * @param locator - The locator of the input field to clear. + * @returns A promise that resolves when the field is cleared. + */ async b_clearField(locator: Locator) { await this.b_waitForElementVisible(locator); await locator.fill(''); } + /** + * Verifies that an element contains specific text. + * @param locator - The locator of the element to check. + * @param expected - The expected text content. + * @returns A promise that resolves if the text is present, otherwise throws an error. + */ async b_textvisible(locator: Locator, expected: string) { await expect(locator).toHaveText(expected, { timeout: maxTimeout }); } - async b_getElementCount(element: Locator, maxTimeout?: number): Promise { + /** + * Gets the count of elements matching the locator. + * @param element - The locator of the elements to count. + * @param maxTimeout - The maximum time to wait for the element to be visible in milliseconds. + * @returns A promise that resolves to the number of matching elements. + */ + async b_getElementCount(element: Locator, maxTimeout?: number): Promise { await this.b_waitForElementVisible(element, maxTimeout); return await element.count(); } + /** + * Waits for the page to reach the 'networkidle' state and adds a small delay. + * @returns A promise that resolves when the page has loaded. + */ async b_waitForPageToLoad(): Promise { await this.page.waitForLoadState("networkidle"); await this.page.waitForTimeout(1000); } + /** + * Selects an option from a static dropdown by its label. + * @param element - The locator of the dropdown element. + * @param dropDownText - The text of the option to select. + * @returns A promise that resolves when the option is selected. + */ async b_selectStaticDropDown(element: Locator, dropDownText: string): Promise { await element.selectOption({ label: dropDownText }); } + /** + * Selects an option from a dynamic dropdown. + * @param dropdownLocator - The locator of the dropdown trigger. + * @param dropdownValuesLocator - The locator for the list of options. + * @param dropDownText - The text of the option to select. + * @returns A promise that resolves when the option is selected. + */ async b_selectDynamicDropDown(dropdownLocator: Locator, dropdownValuesLocator: Locator, dropDownText: string): Promise { await dropdownLocator.click(); const optionLocator = dropdownValuesLocator.locator(`text=${dropDownText}`); diff --git a/tests/pages/composeMessage.ts b/tests/pages/composeMessage.ts index d52a863..38eb8da 100644 --- a/tests/pages/composeMessage.ts +++ b/tests/pages/composeMessage.ts @@ -2,6 +2,11 @@ import { Page, Locator } from '@playwright/test'; import BasePage from './basePage.js' import type { Row } from './readRecruiterNames.ts'; +/** + * Returns the weekday name in Sydney, Australia for the current date or a future date. + * @param dayOffset - The number of days to add to the current date. Defaults to 0. + * @returns The name of the weekday (e.g., "Monday", "Tuesday"). + */ function sydneyWeekday(dayOffset = 0): string { const now = new Date(); const d = new Date(now); @@ -9,7 +14,13 @@ function sydneyWeekday(dayOffset = 0): string { return d.toLocaleDateString('en-AU', { weekday: 'long', timeZone: 'Australia/Sydney' }); } -function displayName(r: Row): string { +/** + * Extracts and returns a display name from a Row object. + * It attempts to find "First Name", "Last Name", or "Name" keys in the row data. + * @param r - The row object containing recruiter data. + * @returns The composed full name, or the value of the first key, or 'there' as a fallback. + */ +export function displayName(r: Row): string { const keys = Object.keys(r); const firstKey = keys.find(k => /first/i.test(k)); const lastKey = keys.find(k => /last/i.test(k)); @@ -26,8 +37,12 @@ function displayName(r: Row): string { return 'there'; } -// Dynamically Generate message using function -// +/** + * Dynamically generates a message for a recruiter based on row data. + * @param r - The row object containing recruiter data. + * @param dayOffset - Optional day offset for the weekday greeting. Defaults to 0. + * @returns The formatted message string. + */ export function buildMessage(r: Row, dayOffset = 0): string { const weekday = sydneyWeekday(dayOffset); const smile = 'πŸ™‚'; @@ -46,11 +61,23 @@ export function buildMessage(r: Row, dayOffset = 0): string { ].join('\n'); } +/** + * ComposeMessagePage class handles the interactions for composing and sending messages on LinkedIn. + */ export class ComposeMessagePage extends BasePage { + /** Locator for the 'Message' button on a profile. */ private readonly clickMessageButton: Locator; + /** Locator for the message text box. */ private readonly messageBox: Locator; + /** Locator for the 'Send' button. */ private readonly sendButton: Locator; + /** Locator for the button to close the message box. */ private readonly closeMessageBox: Locator; + + /** + * Initializes a new instance of the ComposeMessagePage class. + * @param page - The Playwright Page object. + */ constructor(page: Page) { super(page); this.clickMessageButton= page.locator('button[aria-label^="Message"]'); @@ -59,10 +86,20 @@ export class ComposeMessagePage extends BasePage { this.closeMessageBox = page.getByRole('button', { name: /^Close your conversation with/i}) // Used Aria label to avoid Dynamic locators } + /** + * Clicks the 'Message' button to open the conversation window. + * @returns A promise that resolves when the button is clicked. + */ async clickMessage(){ await this.b_clickElement(this.clickMessageButton); } + /** + * Generates and fills the message box with a personalized message. + * @param row - The row data containing recruiter details. + * @param dayOffset - Optional day offset for the weekday. + * @returns A promise that resolves to the generated message text. + */ async fillMessageFromRow(row: Row, dayOffset = 0): Promise { const text = buildMessage(row, dayOffset); try { @@ -75,15 +112,21 @@ export class ComposeMessagePage extends BasePage { return text; } + /** + * Clicks the 'Send' button to send the composed message. + * @returns A promise that resolves when the send button is clicked. + */ async sendMessage() { await this.b_clickElement(this.sendButton); } -async closeMessage(){ - await this.b_clickElement(this.closeMessageBox) -} + /** + * Closes the message conversation window. + * @returns A promise that resolves when the close button is clicked. + */ + async closeMessage(){ + await this.b_clickElement(this.closeMessageBox) + } } export default ComposeMessagePage; -export { displayName }; - diff --git a/tests/pages/globalTearDown.ts b/tests/pages/globalTearDown.ts index 20985d7..6019087 100644 --- a/tests/pages/globalTearDown.ts +++ b/tests/pages/globalTearDown.ts @@ -1,8 +1,16 @@ import { sendReportEmail } from "./logAndReport"; +/** + * Global teardown function executed after all tests have finished. + * + * It is responsible for triggering the email report generation and sending process. + * This ensures that stakeholders receive the test results automatically upon completion. + * + * @returns A promise that resolves when the email report process is initiated. + */ async function globalTearDown() { console.log('sending email report...') await sendReportEmail(); } -export default globalTearDown; \ No newline at end of file +export default globalTearDown; diff --git a/tests/pages/hooks.ts b/tests/pages/hooks.ts index 3a8c058..c3507ab 100644 --- a/tests/pages/hooks.ts +++ b/tests/pages/hooks.ts @@ -1,6 +1,14 @@ import { test as base } from '@playwright/test'; -// Extend the base test without replacing Playwright's built-in context management +/** + * Extends the base Playwright test to include custom context initialization. + * + * This extension ensures a clean state for each test by: + * 1. Clearing all cookies in the browser context. + * 2. Adding an initialization script to clear `localStorage` and `sessionStorage` before the page loads. + * + * It preserves Playwright's built-in context management while adding these cleanup steps. + */ const test = base.extend({ context: async ({ context }, use) => { await context.clearCookies(); diff --git a/tests/pages/linkedInSearchPage.ts b/tests/pages/linkedInSearchPage.ts index 0b40b6f..036d238 100644 --- a/tests/pages/linkedInSearchPage.ts +++ b/tests/pages/linkedInSearchPage.ts @@ -1,14 +1,22 @@ import { Page, Locator, expect } from '@playwright/test'; import BasePage from './basePage'; -// Calling Recruiter name list from the recruiter data file - - +/** + * LinkedInSearchPage class handles searching for recruiters on LinkedIn. + * It extends BasePage to provide search functionality and navigation. + */ export class LinkedInSearchPage extends BasePage { + /** Locator for the global search box. */ private readonly searchBox: Locator; + /** Locator for the list of search results. */ private readonly resultsListItems: Locator; + /** Locator for the first link in the search results. */ private readonly firstResultLink: Locator; + /** + * Initializes a new instance of the LinkedInSearchPage class. + * @param page - The Playwright Page object. + */ constructor(page: Page) { super(page); this.searchBox = page.locator('input[aria-label="Search"], input[placeholder="Search"]'); @@ -16,20 +24,24 @@ export class LinkedInSearchPage extends BasePage { this.firstResultLink = this.resultsListItems.first().locator('a[href*="/in/"]'); } -// Naviagte to LinkedIn feed where global search is visible - -async gotoFeed() { - await this.page.goto('https://www.linkedin.com/feed/', {waitUntil: 'domcontentloaded'}); - await expect(this.searchBox).toBeVisible({timeout:15000}); -} + /** + * Navigates to the LinkedIn feed page where the global search is visible. + * Waits for the search box to become visible. + * @returns A promise that resolves when the feed page is loaded and the search box is visible. + */ + async gotoFeed() { + await this.page.goto('https://www.linkedin.com/feed/', { waitUntil: 'domcontentloaded' }); + await expect(this.searchBox).toBeVisible({ timeout: 15000 }); + } -// Search for recruiter names -async searchForRecruiterNames(recruiterName: string) { + /** + * Searches for a recruiter by name using the global search box. + * @param recruiterName - The name of the recruiter to search for. + * @returns A promise that resolves when the search query is submitted. + */ + async searchForRecruiterNames(recruiterName: string) { await this.searchBox.click(); await this.searchBox.fill(recruiterName); - await this.searchBox.press('Enter'); + await this.searchBox.press('Enter'); } - -}; - -// +} diff --git a/tests/pages/logAndReport.ts b/tests/pages/logAndReport.ts index 231d89d..b153508 100644 --- a/tests/pages/logAndReport.ts +++ b/tests/pages/logAndReport.ts @@ -20,6 +20,19 @@ const { REPORT_RECIPIENT } = process.env; +/** + * Sends an email report with attached test results and execution logs using Gmail OAuth2 authentication. + * + * It performs the following steps: + * 1. Authenticates using Google OAuth2. + * 2. Locates the report zip and execution log files. + * 3. Creates a nodemailer transporter with OAuth2 configuration. + * 4. Constructs the email with attachments. + * 5. Sends the email. + * + * @returns A promise that resolves when the email is successfully sent. + * @throws Will throw an error if the OAuth2 token cannot be retrieved or if sending the email fails. + */ export async function sendReportEmail() { // 1. OAuth2 client const oAuth2Client = new google.auth.OAuth2( @@ -80,4 +93,4 @@ export async function sendReportEmail() { } catch (err) { console.error('email sending failed', err); throw err; -}}; \ No newline at end of file +}}; diff --git a/tests/pages/loginPage.ts b/tests/pages/loginPage.ts index 066ff39..710c638 100644 --- a/tests/pages/loginPage.ts +++ b/tests/pages/loginPage.ts @@ -1,11 +1,22 @@ import { Page, Locator, expect } from '@playwright/test'; import BasePage from './basePage'; +/** + * LoginPage class handles the interaction with the login page of the application. + * It extends the BasePage to leverage common page utilities. + */ export class LoginPage extends BasePage { + /** Locator for the username input field. */ private readonly usernameInput: Locator; + /** Locator for the password input field. */ private readonly passwordInput: Locator; + /** Locator for the login button. */ private readonly loginButton: Locator; + /** + * Initializes a new instance of the LoginPage class. + * @param page - The Playwright Page object. + */ constructor(page: Page) { super(page); this.usernameInput = page.locator("//*[@aria-label='Email or phone']"); @@ -13,24 +24,41 @@ export class LoginPage extends BasePage { this.loginButton = page.locator('[type="submit"]'); } + /** + * Enters the username into the username input field. + * @param username - The username/email to enter. + * @returns A promise that resolves when the username is entered. + */ async enterUserName(username: string) { await this.b_fillField(this.usernameInput, username); } + /** + * Enters the password into the password input field. + * @param password - The password to enter. + * @returns A promise that resolves when the password is entered. + */ async enterPassword(password: string) { await this.b_fillField(this.passwordInput, password); } + /** + * Clicks the login button to submit the credentials. + * @returns A promise that resolves when the button is clicked. + */ async clickLoginButton() { await this.b_clickElement(this.loginButton); } - - // Combined login method + /** + * Performs the full login workflow: entering username, password, and clicking login. + * @param username - The username/email to use for login. + * @param password - The password to use for login. + * @returns A promise that resolves when the login process is initiated. + */ async login(username: string, password: string) { await this.enterUserName(username); await this.enterPassword(password); await this.clickLoginButton(); } - }; - +} diff --git a/tests/pages/logoutPage.ts b/tests/pages/logoutPage.ts index eccdced..4e25ba4 100644 --- a/tests/pages/logoutPage.ts +++ b/tests/pages/logoutPage.ts @@ -1,23 +1,39 @@ import BasePage from "./basePage"; import { Page, Locator,expect } from "playwright/test"; +/** + * LogoutPage class handles the user logout process. + * It extends BasePage to utilize common page interaction methods. + */ export class LogoutPage extends BasePage { + /** Locator for the profile navigation button (Me). */ private readonly navigationButton: Locator; + /** Locator for the sign-out button. */ private readonly signOutButton: Locator; + /** + * Initializes a new instance of the LogoutPage class. + * @param page - The Playwright Page object. + */ constructor(page: Page) { super(page); this.navigationButton = page.locator("//span[normalize-space(text())='Me']"); this.signOutButton = page.locator("//p[normalize-space(text())='Sign out']"); } + /** + * Clicks the profile/navigation button to reveal the logout option. + * @returns A promise that resolves when the navigation button is clicked. + */ async navigateButton() { await this.b_clickElement(this.navigationButton); } + /** + * Clicks the sign-out button to log the user out. + * @returns A promise that resolves when the sign-out button is clicked. + */ async clickSignOut() { await this.b_clickElement(this.signOutButton); } } - - diff --git a/tests/pages/readRecruiterNames.ts b/tests/pages/readRecruiterNames.ts index 6be5310..4681d53 100644 --- a/tests/pages/readRecruiterNames.ts +++ b/tests/pages/readRecruiterNames.ts @@ -2,8 +2,19 @@ import XLSX from 'xlsx'; import * as path from 'path'; import * as fs from 'fs'; +/** + * Type definition for a generic row object from an Excel sheet. + */ export type Row = Record; +/** + * Reads an Excel file and converts a specific sheet to a JSON array of Row objects. + * @param file - The path to the Excel file. + * @param sheet - The name or index of the sheet to read. Defaults to the first sheet. + * @returns A promise that resolves to an array of Row objects. + * @throws Will throw an error if the file does not exist or is not readable. + * @throws Will throw an error if the specified sheet is not found. + */ export async function readExcel( file: string, sheet?: string | number @@ -28,7 +39,12 @@ export async function readExcel( return XLSX.utils.sheet_to_json(ws, { defval: '' }); } -// Build a display name from whatever headers the sheet has +/** + * Derives a full display name from a row object based on common header patterns. + * Looks for keys resembling "First", "Last", or "Name". + * @param row - The row object containing potential name data. + * @returns The derived name string, or an empty string if no name could be found. + */ export function deriveName(row: Row): string { const keys = Object.keys(row); const firstKey = keys.find(k => /(^|[^a-z])first([^a-z]|$)/i.test(k)); @@ -44,6 +60,12 @@ export function deriveName(row: Row): string { return ''; } +/** + * Extracts a list of formatted recruiter names from an Excel file. + * @param filePath - The path to the Excel file. + * @param sheet - The name or index of the sheet to read. + * @returns A promise that resolves to an array of recruiter name strings. + */ export async function getRecruiterNames( filePath: string, sheet?: string | number diff --git a/tests/pages/userLoginandLogout.spec.ts b/tests/pages/userLoginandLogout.spec.ts index a027275..d9fd027 100644 --- a/tests/pages/userLoginandLogout.spec.ts +++ b/tests/pages/userLoginandLogout.spec.ts @@ -2,7 +2,18 @@ import { test } from "./hooks"; import { LoginPage } from "./loginPage"; import { LogoutPage } from "./logoutPage"; - +/** + * Test case: User is able to login and logout successfully. + * + * This test verifies the core authentication flow: + * 1. Navigate to the base URL. + * 2. Log in using credentials from environment variables. + * 3. Log out using the logout page object. + * 4. Close the page. + * + * Pre-requisites: + * - Environment variables BASE_URL, LINKEDIN_USERNAME, and LINKEDIN_PASSWORD must be set. + */ test('user is able to login and logout successfully', async ({ page }) => { if (!process.env.BASE_URL || !process.env.LINKEDIN_USERNAME || !process.env.LINKEDIN_PASSWORD) { throw new Error(" Missing required environment variables (BASE_URL, LINKEDIN_USERNAME, LINKEDIN_PASSWORD)"); @@ -16,5 +27,3 @@ test('user is able to login and logout successfully', async ({ page }) => { await logOut.clickSignOut(); await page.close(); }) - - diff --git a/tests/seed.spec.ts b/tests/seed.spec.ts index ef5ce4c..a0981bb 100644 --- a/tests/seed.spec.ts +++ b/tests/seed.spec.ts @@ -1,6 +1,14 @@ import { test, expect } from '@playwright/test'; +/** + * Test Suite: Test group + * Placeholder for seed or data generation tests. + */ test.describe('Test group', () => { + /** + * Test Case: seed + * Currently empty, intended for code generation or data seeding logic. + */ test('seed', async ({ page }) => { // generate code here. }); diff --git a/tests/userSendsMessage.spec.ts b/tests/userSendsMessage.spec.ts index 6261d72..8d746c7 100644 --- a/tests/userSendsMessage.spec.ts +++ b/tests/userSendsMessage.spec.ts @@ -7,6 +7,24 @@ import { LogoutPage } from './pages/logoutPage'; import { sendReportEmail } from './pages/logAndReport'; +/** + * Test Case [T2]: Verify user sends message (end-to-end) flow. + * This test uses the `.only` annotation, meaning it will be the only test run in this file. + * + * Flow: + * 1. Check for required environment variables. + * 2. Login to LinkedIn. + * 3. Read a list of recruiter names from an Excel file. + * 4. Iterate through each name: + * a. Search for the recruiter. + * b. Open the message dialog. + * c. Compose a personalized message. + * d. Send the message. + * e. Close the message dialog. + * f. Return to the feed to reset for the next search. + * 5. Logout after all messages are sent. + * 6. Send an email report with execution logs. + */ test.only('[T2] Verify user sends message (end-to-end) flow', async ({ page, browser }) => { // Verify env Variables are getting loaded @@ -42,4 +60,3 @@ test.only('[T2] Verify user sends message (end-to-end) flow', async ({ page, bro // email logs and report await sendReportEmail(); }); -