diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml deleted file mode 100644 index 3cb0c05..0000000 --- a/.github/workflows/playwright.yml +++ /dev/null @@ -1,27 +0,0 @@ -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 ci - - name: Install Playwright Browsers - run: npx playwright install --with-deps - - name: Run Playwright tests - run: QASE_MODE=testops npx playwright test - - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 diff --git a/.history/helpers/fileUploadHelper_20241014104838.ts b/.history/helpers/fileUploadHelper_20241014104838.ts new file mode 100644 index 0000000..206ff6a --- /dev/null +++ b/.history/helpers/fileUploadHelper_20241014104838.ts @@ -0,0 +1,41 @@ +import { Page } from '@playwright/test'; +import * as fs from 'fs'; +import * as path from 'path'; + +// Helper provides utility methods for uploading files during tests + +export class FileUploadHelper { + private uploadFolderPath: string; + + constructor(private page: Page) { + // Define the path for the UploadFiles folder inside the 'data' directory dynamically + this.uploadFolderPath = path.resolve(process.cwd(), 'data/UploadFiles'); + + // Ensure the UploadFiles directory exists + this.ensureUploadFolderExists(); + } + + // Ensure the UploadFiles directory exists + private ensureUploadFolderExists() { + if (!fs.existsSync(this.uploadFolderPath)) { + fs.mkdirSync(this.uploadFolderPath, { recursive: true }); // Create the folder if it doesn't exist + } + } + + // Upload a random file from the UploadFiles folder + async uploadRandomFile() { + const files = fs.readdirSync(this.uploadFolderPath); // Read files in the folder + + if (files.length === 0) { + throw new Error('No files available in the UploadFiles directory.'); + } + + // Select a random file and upload + const randomFile = files[Math.floor(Math.random() * files.length)]; + const filePath = path.join(this.uploadFolderPath, randomFile); + await this.page.setInputFiles('input[type="file"]', filePath); // Upload the selected file + + // Log file upload for debugging purposes + console.log(`Uploading file: ${filePath}`); + } +} \ No newline at end of file diff --git a/.history/helpers/fileUploadHelper_20241023222951.ts b/.history/helpers/fileUploadHelper_20241023222951.ts new file mode 100644 index 0000000..b31f162 --- /dev/null +++ b/.history/helpers/fileUploadHelper_20241023222951.ts @@ -0,0 +1,41 @@ +import { Page } from '@playwright/test'; +import * as fs from 'fs'; // A Node.js module for working with files and directories (reading, writing, deleting files, etc.). +import * as path from 'path'; // A Node.js module for working with file and directory paths, allowing dynamic and absolute path creation. + +// Helper provides utility methods for uploading files during tests + +export class FileUploadHelper { + private uploadFolderPath: string; + + constructor(private page: Page) { + // Define the path for the UploadFiles folder inside the 'data' directory dynamically + this.uploadFolderPath = path.resolve(process.cwd(), 'data/UploadFiles'); + + // Ensure the UploadFiles directory exists + this.ensureUploadFolderExists(); + } + + // Ensure the UploadFiles directory exists + private ensureUploadFolderExists() { + if (!fs.existsSync(this.uploadFolderPath)) { + fs.mkdirSync(this.uploadFolderPath, { recursive: true }); // Create the folder if it doesn't exist + } + } + + // Upload a random file from the UploadFiles folder + async uploadRandomFile() { + const files = fs.readdirSync(this.uploadFolderPath); // Read files in the folder + + if (files.length === 0) { + throw new Error('No files available in the UploadFiles directory.'); + } + + // Select a random file and upload + const randomFile = files[Math.floor(Math.random() * files.length)]; + const filePath = path.join(this.uploadFolderPath, randomFile); + await this.page.setInputFiles('input[type="file"]', filePath); // Upload the selected file + + // Log file upload for debugging purposes + console.log(`Uploading file: ${filePath}`); + } +} \ No newline at end of file diff --git a/.history/helpers/uiHelper_20241024091741.ts b/.history/helpers/uiHelper_20241024091741.ts new file mode 100644 index 0000000..907ff10 --- /dev/null +++ b/.history/helpers/uiHelper_20241024091741.ts @@ -0,0 +1,184 @@ +import { Page, expect } from '@playwright/test'; +import { urls } from '../data/urls'; + +/** + * Helpers: + * Helpers are utility functions or classes designed to simplify and organize common + * tasks within your test suite, such as interacting with UI elements, handling file uploads, + * or generating test data. By abstracting repetitive code into helpers, you can maintain + * cleaner test files and promote reusability. Helpers reduce duplication and make your + * tests easier to maintain, improving overall test readability and structure. + */ + +export class UIHelper { + constructor(private page: Page) {} + + // ------------------------------------------------------------------------- + // NAVIGATION HELPERS + // ------------------------------------------------------------------------- + + // Navigate to the base URL (homepage) + async navigateToHome() { + await this.page.goto(urls.baseURL); + await this.page.waitForURL(urls.baseURL); + } + + // Navigate to the communities page + async navigateToCommunities() { + await this.page.goto(urls.communitiesURL); + await this.page.waitForURL(urls.communitiesURL); + } + + // Navigate to the My Dashboard page + async navigateToMyDashboard() { + await this.page.getByRole('link', { name: 'My dashboard' }).click(); + } + + // Navigate to the new community page + async navigateToNewCommunity() { + await this.page.goto(urls.newCommunityURL); + await this.page.waitForURL(urls.newCommunityURL); + } + + // Navigate to the detail of the first record + async firstRecordDetail() { + await this.page.waitForSelector('//a[contains(@href, "/records/")][1]', { state: 'visible' }); + await this.page.click('//a[contains(@href, "/records/")][1]'); + } + + // Navigate to a specific URL + async goto(url: string) { + await this.page.goto(url); + } + + // ------------------------------------------------------------------------- + // OTHER UI HELPERS + // ------------------------------------------------------------------------- + + // Define valid roles using a union of string literals (can add more as needed) + async clickByRole(role: 'button' | 'link' | 'checkbox' | 'heading' | 'menuitem' | 'textbox', name: string) { + await this.page.getByRole(role, { name }).click(); + } + + // Implement clickByLabel to handle clicking elements by their label + async clickByLabel(label: string) { + await this.page.getByLabel(label).click(); + } + + // Fill an input field by its placeholder text + async fillPlaceholder(placeholder: string, value: string) { + await this.page.getByPlaceholder(placeholder).fill(value); + } + + // Click an element by its text content with error handling + async clickByText(text: string, retries: number = 3) { + for (let attempt = 0; attempt < retries; attempt++) { + try { + await this.page.click(`text=${text}`); + console.log(`Successfully clicked on text: ${text}`); + break; + } catch (error) { + console.error(`Attempt ${attempt + 1} failed to click on text: ${text}`, error); + if (attempt === retries - 1) { + await this.captureScreenshotOnError(`clickByText_${text}`); + throw error; // Re-throw after final attempt + } + } + } + } + + // Wait for a specific text to be visible on the page with optional timeout + async waitForText(text: string, timeout: number = 30000): Promise { + try { + await this.page.waitForSelector(`text=${text}`, { state: 'visible', timeout }); + return true; + } catch (error) { + console.error(`Text "${text}" not found within timeout`); + return false; + } + } + + // Click a button based on its displayed text with error handling + async clickButtonByText(buttonText: string) { + try { + await this.page.click(`text=${buttonText}`); + } catch (error) { + console.error(`Failed to click on button with text: ${buttonText}`, error); + await this.captureScreenshotOnError(`clickButtonByText_${buttonText}`); + throw error; + } + } + + // Wait for a specific element to become visible with optional timeout + async waitForElement(selector: string, timeout: number = 30000) { + try { + await this.page.waitForSelector(selector, { state: 'visible', timeout }); + } catch (error) { + console.error(`Element ${selector} not visible within timeout`, error); + await this.captureScreenshotOnError(`waitForElement_${selector}`); + throw error; + } + } + + // Check if an element is enabled on the page + async isElementEnabled(selector: string): Promise { + try { + return await this.page.isEnabled(selector); + } catch (error) { + console.error(`Failed to check if element ${selector} is enabled`, error); + await this.captureScreenshotOnError(`isElementEnabled_${selector}`); + throw error; + } + } + + // ------------------------------------------------------------------------- + // FORM HELPERS + // ------------------------------------------------------------------------- + + // Select an option in a dropdown by value + async selectOptionByValue(selector: string, value: string) { + try { + await this.page.selectOption(selector, value); + } catch (error) { + console.error(`Failed to select option "${value}" from ${selector}`, error); + await this.captureScreenshotOnError(`selectOptionByValue_${value}`); + throw error; + } + } + + // Toggle a checkbox + async toggleCheckbox(selector: string, check: boolean = true) { + const isChecked = await this.page.isChecked(selector); + if (isChecked !== check) { + await this.page.check(selector); + } + } + + // ------------------------------------------------------------------------- + // SCREENSHOT AND ERROR HANDLING HELPERS + // ------------------------------------------------------------------------- + + // Capture a screenshot when an error occurs + async captureScreenshotOnError(fileName: string) { + try { + const screenshotPath = `screenshots/${fileName}.png`; + await this.page.screenshot({ path: screenshotPath }); + console.log(`Screenshot captured: ${screenshotPath}`); + } catch (screenshotError) { + console.error(`Failed to capture screenshot: ${screenshotError}`); + } + } + + // ------------------------------------------------------------------------- + // ASSERTION HELPERS + // ------------------------------------------------------------------------- + + // Assert that text is visible on the page + async assertTextVisible(text: string) { + const isVisible = await this.page.isVisible(`text=${text}`); + if (!isVisible) { + await this.captureScreenshotOnError(`assertTextVisible_${text}`); + } + expect(isVisible).toBe(true, `Expected text "${text}" to be visible.`); + } +} diff --git a/.history/helpers/uiHelper_20241024091932.ts b/.history/helpers/uiHelper_20241024091932.ts new file mode 100644 index 0000000..b151ae5 --- /dev/null +++ b/.history/helpers/uiHelper_20241024091932.ts @@ -0,0 +1,184 @@ +import { Page, expect } from '@playwright/test'; +import { urls } from '../data/urls'; + +/** + * Helpers: + * Helpers are utility functions or classes designed to simplify and organize common + * tasks within your test suite, such as interacting with UI elements, handling file uploads, + * or generating test data. By abstracting repetitive code into helpers, you can maintain + * cleaner test files and promote reusability. Helpers reduce duplication and make your + * tests easier to maintain, improving overall test readability and structure. + */ + +export class UIHelper { + constructor(private page: Page) {} + + // ------------------------------------------------------------------------- + // NAVIGATION HELPERS + // ------------------------------------------------------------------------- + + // Navigate to the base URL (homepage) + async navigateToHome() { + await this.page.goto(urls.baseURL); + await this.page.waitForURL(urls.baseURL); + } + + // Navigate to the communities page + async navigateToCommunities() { + await this.page.goto(urls.communitiesURL); + await this.page.waitForURL(urls.communitiesURL); + } + + // Navigate to the My Dashboard page + async navigateToMyDashboard() { + await this.page.getByRole('link', { name: 'My dashboard' }).click(); + } + + // Navigate to the new community page + async navigateToNewCommunity() { + await this.page.goto(urls.newCommunityURL); + await this.page.waitForURL(urls.newCommunityURL); + } + + // Navigate to the detail of the first record + async firstRecordDetail() { + await this.page.waitForSelector('//a[contains(@href, "/records/")][1]', { state: 'visible' }); + await this.page.click('//a[contains(@href, "/records/")][1]'); + } + + // Navigate to a specific URL + async goto(url: string) { + await this.page.goto(url); + } + + // ------------------------------------------------------------------------- + // OTHER UI HELPERS + // ------------------------------------------------------------------------- + + // Define valid roles using a union of string literals (can add more as needed) + async clickByRole(role: 'button' | 'link' | 'checkbox' | 'heading' | 'menuitem' | 'textbox', name: string) { + await this.page.getByRole(role, { name }).click(); + } + + // Implement clickByLabel to handle clicking elements by their label + async clickByLabel(label: string) { + await this.page.getByLabel(label).click(); + } + + // Fill an input field by its placeholder text + async fillPlaceholder(placeholder: string, value: string) { + await this.page.getByPlaceholder(placeholder).fill(value); + } + + // Click an element by its text content with error handling + async clickByText(text: string, retries: number = 3) { + for (let attempt = 0; attempt < retries; attempt++) { + try { + await this.page.click(`text=${text}`); + console.log(`Successfully clicked on text: ${text}`); + break; + } catch (error) { + console.error(`Attempt ${attempt + 1} failed to click on text: ${text}`, error); + if (attempt === retries - 1) { + await this.captureScreenshotOnError(`clickByText_${text}`); + throw error; // Re-throw after final attempt + } + } + } + } + + // Wait for a specific text to be visible on the page with optional timeout + async waitForText(text: string, timeout: number = 30000): Promise { + try { + await this.page.waitForSelector(`text=${text}`, { state: 'visible', timeout }); + return true; + } catch (error) { + console.error(`Text "${text}" not found within timeout`); + return false; + } + } + + // Click a button based on its displayed text with error handling + async clickButtonByText(buttonText: string) { + try { + await this.page.click(`text=${buttonText}`); + } catch (error) { + console.error(`Failed to click on button with text: ${buttonText}`, error); + await this.captureScreenshotOnError(`clickButtonByText_${buttonText}`); + throw error; + } + } + + // Wait for a specific element to become visible with optional timeout + async waitForElement(selector: string, timeout: number = 30000) { + try { + await this.page.waitForSelector(selector, { state: 'visible', timeout }); + } catch (error) { + console.error(`Element ${selector} not visible within timeout`, error); + await this.captureScreenshotOnError(`waitForElement_${selector}`); + throw error; + } + } + + // Check if an element is enabled on the page + async isElementEnabled(selector: string): Promise { + try { + return await this.page.isEnabled(selector); + } catch (error) { + console.error(`Failed to check if element ${selector} is enabled`, error); + await this.captureScreenshotOnError(`isElementEnabled_${selector}`); + throw error; + } + } + + // ------------------------------------------------------------------------- + // FORM HELPERS + // ------------------------------------------------------------------------- + + // Select an option in a dropdown by value + async selectOptionByValue(selector: string, value: string) { + try { + await this.page.selectOption(selector, value); + } catch (error) { + console.error(`Failed to select option "${value}" from ${selector}`, error); + await this.captureScreenshotOnError(`selectOptionByValue_${value}`); + throw error; + } + } + + // Toggle a checkbox + async toggleCheckbox(selector: string, check: boolean = true) { + const isChecked = await this.page.isChecked(selector); + if (isChecked !== check) { + await this.page.check(selector); + } + } + + // ------------------------------------------------------------------------- + // SCREENSHOT AND ERROR HANDLING HELPERS + // ------------------------------------------------------------------------- + + // Capture a screenshot when an error occurs + async captureScreenshotOnError(fileName: string) { + try { + const screenshotPath = `screenshots/${fileName}.png`; + await this.page.screenshot({ path: screenshotPath }); + console.log(`Screenshot captured: ${screenshotPath}`); + } catch (screenshotError) { + console.error(`Failed to capture screenshot: ${screenshotError}`); + } + } + + // ------------------------------------------------------------------------- + // ASSERTION HELPERS + // ------------------------------------------------------------------------- + + // Assert that text is visible on the page + async assertTextVisible(text: string) { + const isVisible = await this.page.isVisible(`text=${text}`); + if (!isVisible) { + await this.captureScreenshotOnError(`assertTextVisible_${text}`); + } + expect(isVisible).toBe(true); + } +} diff --git a/.history/helpers/uiHelper_20241024092108.ts b/.history/helpers/uiHelper_20241024092108.ts new file mode 100644 index 0000000..54cc5c1 --- /dev/null +++ b/.history/helpers/uiHelper_20241024092108.ts @@ -0,0 +1,184 @@ +import { Page, expect } from '@playwright/test'; +import { urls } from '../data/urls'; + +/** + * Helpers: + * Helpers are utility functions or classes designed to simplify and organize common + * tasks within your test suite, such as interacting with UI elements, handling file uploads, + * or generating test data. By abstracting repetitive code into helpers, you can maintain + * cleaner test files and promote reusability. Helpers reduce duplication and make your + * tests easier to maintain, improving overall test readability and structure. + */ + +export class UIHelper { + constructor(private page: Page) {} + + // ------------------------------------------------------------------------- + // NAVIGATION HELPERS + // ------------------------------------------------------------------------- + + // Navigate to the base URL (homepage) + async navigateToHome() { + await this.page.goto(urls.baseURL); + await this.page.waitForURL(urls.baseURL); + } + + // Navigate to the communities page + async navigateToCommunities() { + await this.page.goto(urls.communitiesURL); + await this.page.waitForURL(urls.communitiesURL); + } + + // Navigate to the My Dashboard page + async navigateToMyDashboard() { + await this.page.getByRole('link', { name: 'My dashboard' }).click(); + } + + // Navigate to the new community page + async navigateToNewCommunity() { + await this.page.goto(urls.newCommunityURL); + await this.page.waitForURL(urls.newCommunityURL); + } + + // Navigate to the detail of the first record + async firstRecordDetail() { + await this.page.waitForSelector('//a[contains(@href, "/records/")][1]', { state: 'visible' }); + await this.page.click('//a[contains(@href, "/records/")][1]'); + } + + // Navigate to a specific URL + async goto(url: string) { + await this.page.goto(url); + } + + // ------------------------------------------------------------------------- + // OTHER UI HELPERS + // ------------------------------------------------------------------------- + + // Define valid roles using a union of string literals (can add more as needed) + async clickByRole(role: 'button' | 'link' | 'checkbox' | 'heading' | 'menuitem' | 'textbox', name: string) { + await this.page.getByRole(role, { name }).click(); + } + + // Implement clickByLabel to handle clicking elements by their label + async clickByLabel(label: string) { + await this.page.getByLabel(label).click(); + } + + // Fill an input field by its placeholder text + async fillPlaceholder(placeholder: string, value: string) { + await this.page.getByPlaceholder(placeholder).fill(value); + } + + // Click an element by its text content with error handling + async clickByText(text: string, retries: number = 3) { // set number of retries + for (let attempt = 0; attempt < retries; attempt++) { + try { + await this.page.click(`text=${text}`); + console.log(`Successfully clicked on text: ${text}`); + break; + } catch (error) { + console.error(`Attempt ${attempt + 1} failed to click on text: ${text}`, error); + if (attempt === retries - 1) { + await this.captureScreenshotOnError(`clickByText_${text}`); + throw error; // Re-throw after final attempt + } + } + } + } + + // Wait for a specific text to be visible on the page with optional timeout + async waitForText(text: string, timeout: number = 30000): Promise { + try { + await this.page.waitForSelector(`text=${text}`, { state: 'visible', timeout }); + return true; + } catch (error) { + console.error(`Text "${text}" not found within timeout`); + return false; + } + } + + // Click a button based on its displayed text with error handling + async clickButtonByText(buttonText: string) { + try { + await this.page.click(`text=${buttonText}`); + } catch (error) { + console.error(`Failed to click on button with text: ${buttonText}`, error); + await this.captureScreenshotOnError(`clickButtonByText_${buttonText}`); + throw error; + } + } + + // Wait for a specific element to become visible with optional timeout + async waitForElement(selector: string, timeout: number = 30000) { + try { + await this.page.waitForSelector(selector, { state: 'visible', timeout }); + } catch (error) { + console.error(`Element ${selector} not visible within timeout`, error); + await this.captureScreenshotOnError(`waitForElement_${selector}`); + throw error; + } + } + + // Check if an element is enabled on the page + async isElementEnabled(selector: string): Promise { + try { + return await this.page.isEnabled(selector); + } catch (error) { + console.error(`Failed to check if element ${selector} is enabled`, error); + await this.captureScreenshotOnError(`isElementEnabled_${selector}`); + throw error; + } + } + + // ------------------------------------------------------------------------- + // FORM HELPERS + // ------------------------------------------------------------------------- + + // Select an option in a dropdown by value + async selectOptionByValue(selector: string, value: string) { + try { + await this.page.selectOption(selector, value); + } catch (error) { + console.error(`Failed to select option "${value}" from ${selector}`, error); + await this.captureScreenshotOnError(`selectOptionByValue_${value}`); + throw error; + } + } + + // Toggle a checkbox + async toggleCheckbox(selector: string, check: boolean = true) { + const isChecked = await this.page.isChecked(selector); + if (isChecked !== check) { + await this.page.check(selector); + } + } + + // ------------------------------------------------------------------------- + // SCREENSHOT AND ERROR HANDLING HELPERS + // ------------------------------------------------------------------------- + + // Capture a screenshot when an error occurs + async captureScreenshotOnError(fileName: string) { + try { + const screenshotPath = `screenshots/${fileName}.png`; + await this.page.screenshot({ path: screenshotPath }); + console.log(`Screenshot captured: ${screenshotPath}`); + } catch (screenshotError) { + console.error(`Failed to capture screenshot: ${screenshotError}`); + } + } + + // ------------------------------------------------------------------------- + // ASSERTION HELPERS + // ------------------------------------------------------------------------- + + // Assert that text is visible on the page + async assertTextVisible(text: string) { + const isVisible = await this.page.isVisible(`text=${text}`); + if (!isVisible) { + await this.captureScreenshotOnError(`assertTextVisible_${text}`); + } + expect(isVisible).toBe(true); + } +} diff --git a/.history/playwright.config_20241023215330.ts b/.history/playwright.config_20241023215330.ts new file mode 100644 index 0000000..964ec0d --- /dev/null +++ b/.history/playwright.config_20241023215330.ts @@ -0,0 +1,59 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + timeout: 30000, // 30-second timeout per test + + reporter: [ + ['list'], + ['playwright-qase-reporter', { + debug: false, // Optional, add debug mode + testops: { + api: { + token: process.env.QASE_TESTOPS_API_TOKEN, + }, + project: process.env.QASE_TESTOPS_PROJECT, + environment: process.env.QASE_ENVIRONMENT || 'production', + uploadAttachments: true, // Enables uploading screenshots/videos on failure + run: { + complete: true, + }, + }, + logging: true, // Keep logging enabled for extra visibility + }], + ['html'], + ], + + use: { + headless: process.env.CI ? true : process.env.HEADLESS !== 'false', // Always headless in CI, else respect HEADLESS + trace: process.env.CI ? 'on-first-retry' : 'off', // Enable trace in CI + video: process.env.RECORD_VIDEO === 'true' ? 'on' : 'retain-on-failure', // Video recording if enabled, or retain on failure + screenshot: 'only-on-failure', // Capture screenshot on test failure + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + /* Uncomment to test cross-browser */ + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + ], + + /* Uncomment for local dev server */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // }, +}); diff --git a/.history/playwright.config_20241023215923.ts b/.history/playwright.config_20241023215923.ts new file mode 100644 index 0000000..f1e3220 --- /dev/null +++ b/.history/playwright.config_20241023215923.ts @@ -0,0 +1,59 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + // timeout: 30000, // 30-second timeout per test + + reporter: [ + ['list'], + ['playwright-qase-reporter', { + debug: false, // Optional, add debug mode + testops: { + api: { + token: process.env.QASE_TESTOPS_API_TOKEN, + }, + project: process.env.QASE_TESTOPS_PROJECT, + environment: process.env.QASE_ENVIRONMENT || 'production', + uploadAttachments: true, // Enables uploading screenshots/videos on failure + run: { + complete: true, + }, + }, + logging: true, // Keep logging enabled for extra visibility + }], + ['html'], + ], + + use: { + headless: process.env.CI ? true : process.env.HEADLESS !== 'false', // Always headless in CI, else respect HEADLESS + trace: process.env.CI ? 'on-first-retry' : 'off', // Enable trace in CI + video: process.env.RECORD_VIDEO === 'true' ? 'on' : 'retain-on-failure', // Video recording if enabled, or retain on failure + screenshot: 'only-on-failure', // Capture screenshot on test failure + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + /* Uncomment to test cross-browser */ + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + ], + + /* Uncomment for local dev server */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // }, +}); diff --git a/helpers/fileUploadHelper.ts b/helpers/fileUploadHelper.ts index 206ff6a..b31f162 100644 --- a/helpers/fileUploadHelper.ts +++ b/helpers/fileUploadHelper.ts @@ -1,6 +1,6 @@ import { Page } from '@playwright/test'; -import * as fs from 'fs'; -import * as path from 'path'; +import * as fs from 'fs'; // A Node.js module for working with files and directories (reading, writing, deleting files, etc.). +import * as path from 'path'; // A Node.js module for working with file and directory paths, allowing dynamic and absolute path creation. // Helper provides utility methods for uploading files during tests diff --git a/helpers/uiHelper.ts b/helpers/uiHelper.ts index 637a6bd..54cc5c1 100644 --- a/helpers/uiHelper.ts +++ b/helpers/uiHelper.ts @@ -1,5 +1,5 @@ -import { Page } from '@playwright/test'; -import { urls } from '../data/urls'; // assuming URLs are stored here +import { Page, expect } from '@playwright/test'; +import { urls } from '../data/urls'; /** * Helpers: @@ -10,8 +10,6 @@ import { urls } from '../data/urls'; // assuming URLs are stored here * tests easier to maintain, improving overall test readability and structure. */ -// Helper provides utility methods for common UI interactions - export class UIHelper { constructor(private page: Page) {} @@ -58,7 +56,7 @@ export class UIHelper { // ------------------------------------------------------------------------- // Define valid roles using a union of string literals (can add more as needed) - async clickByRole(role: 'button' | 'link' | 'checkbox' | 'heading' | 'menuitem' | 'textbox', name: string) { + async clickByRole(role: 'button' | 'link' | 'checkbox' | 'heading' | 'menuitem' | 'textbox', name: string) { await this.page.getByRole(role, { name }).click(); } @@ -72,28 +70,115 @@ export class UIHelper { await this.page.getByPlaceholder(placeholder).fill(value); } - // Click an element by its text content - async clickByText(text: string) { - await this.page.click(`text=${text}`); + // Click an element by its text content with error handling + async clickByText(text: string, retries: number = 3) { // set number of retries + for (let attempt = 0; attempt < retries; attempt++) { + try { + await this.page.click(`text=${text}`); + console.log(`Successfully clicked on text: ${text}`); + break; + } catch (error) { + console.error(`Attempt ${attempt + 1} failed to click on text: ${text}`, error); + if (attempt === retries - 1) { + await this.captureScreenshotOnError(`clickByText_${text}`); + throw error; // Re-throw after final attempt + } + } + } } - // Wait for a specific text to be visible on the page and return its visibility status - async waitForText(text: string): Promise { - return await this.page.getByText(text).isVisible(); + // Wait for a specific text to be visible on the page with optional timeout + async waitForText(text: string, timeout: number = 30000): Promise { + try { + await this.page.waitForSelector(`text=${text}`, { state: 'visible', timeout }); + return true; + } catch (error) { + console.error(`Text "${text}" not found within timeout`); + return false; + } } - // Click a button based on its displayed text + // Click a button based on its displayed text with error handling async clickButtonByText(buttonText: string) { - await this.page.click(`text=${buttonText}`); + try { + await this.page.click(`text=${buttonText}`); + } catch (error) { + console.error(`Failed to click on button with text: ${buttonText}`, error); + await this.captureScreenshotOnError(`clickButtonByText_${buttonText}`); + throw error; + } } - // Wait for a specific element to become visible on the page - async waitForElement(selector: string) { - await this.page.waitForSelector(selector, { state: 'visible' }); + // Wait for a specific element to become visible with optional timeout + async waitForElement(selector: string, timeout: number = 30000) { + try { + await this.page.waitForSelector(selector, { state: 'visible', timeout }); + } catch (error) { + console.error(`Element ${selector} not visible within timeout`, error); + await this.captureScreenshotOnError(`waitForElement_${selector}`); + throw error; + } } // Check if an element is enabled on the page async isElementEnabled(selector: string): Promise { - return await this.page.isEnabled(selector); + try { + return await this.page.isEnabled(selector); + } catch (error) { + console.error(`Failed to check if element ${selector} is enabled`, error); + await this.captureScreenshotOnError(`isElementEnabled_${selector}`); + throw error; + } + } + + // ------------------------------------------------------------------------- + // FORM HELPERS + // ------------------------------------------------------------------------- + + // Select an option in a dropdown by value + async selectOptionByValue(selector: string, value: string) { + try { + await this.page.selectOption(selector, value); + } catch (error) { + console.error(`Failed to select option "${value}" from ${selector}`, error); + await this.captureScreenshotOnError(`selectOptionByValue_${value}`); + throw error; + } + } + + // Toggle a checkbox + async toggleCheckbox(selector: string, check: boolean = true) { + const isChecked = await this.page.isChecked(selector); + if (isChecked !== check) { + await this.page.check(selector); + } + } + + // ------------------------------------------------------------------------- + // SCREENSHOT AND ERROR HANDLING HELPERS + // ------------------------------------------------------------------------- + + // Capture a screenshot when an error occurs + async captureScreenshotOnError(fileName: string) { + try { + const screenshotPath = `screenshots/${fileName}.png`; + await this.page.screenshot({ path: screenshotPath }); + console.log(`Screenshot captured: ${screenshotPath}`); + } catch (screenshotError) { + console.error(`Failed to capture screenshot: ${screenshotError}`); + } + } + + // ------------------------------------------------------------------------- + // ASSERTION HELPERS + // ------------------------------------------------------------------------- + + // Assert that text is visible on the page + async assertTextVisible(text: string) { + const isVisible = await this.page.isVisible(`text=${text}`); + if (!isVisible) { + await this.captureScreenshotOnError(`assertTextVisible_${text}`); + } + expect(isVisible).toBe(true); } } diff --git a/screenshots/Upload a file successfully (Qase ID: 7).png b/screenshots/Upload a file successfully (Qase ID: 7).png new file mode 100644 index 0000000..d70754d Binary files /dev/null and b/screenshots/Upload a file successfully (Qase ID: 7).png differ diff --git a/screenshots/Upload_a_file_successfully__Qase_ID__7_.png b/screenshots/Upload_a_file_successfully__Qase_ID__7_.png new file mode 100644 index 0000000..21d3f53 Binary files /dev/null and b/screenshots/Upload_a_file_successfully__Qase_ID__7_.png differ