From 56430f6bdbf6b52d13c437da3a5781435fa5f6e0 Mon Sep 17 00:00:00 2001 From: Dipjyoti Metia Date: Sun, 17 Sep 2023 17:45:07 +1000 Subject: [PATCH] update --- Dockerfile | 1 + package.json | 13 ++-- playwright.config.ts | 20 +++--- reportConfig.ts | 15 ++-- src/config/config.json | 3 - src/config/config.ts | 25 ++++++- src/config/config.yaml | 2 + src/core/apiActions.ts | 71 +++++++++++++++++++ src/core/webActions.ts | 135 ++++++++++++++++++++++++++++++++++++ src/pages/login.ts | 34 ++++++--- src/pages/posts.ts | 2 +- src/tests/home.test.spec.ts | 10 +-- src/types.d.ts | 6 +- yarn.lock | 10 +++ 14 files changed, 304 insertions(+), 43 deletions(-) delete mode 100644 src/config/config.json create mode 100644 src/config/config.yaml create mode 100644 src/core/apiActions.ts create mode 100644 src/core/webActions.ts diff --git a/Dockerfile b/Dockerfile index b2de6ada..09bca20e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ WORKDIR /app COPY package.json . COPY yarn.lock . COPY playwright.config.ts . +COPY reportConfig.ts . COPY tsconfig.json . ENV CI=1 diff --git a/package.json b/package.json index a5a9baf7..00f92337 100644 --- a/package.json +++ b/package.json @@ -20,25 +20,28 @@ "homepage": "https://github.com/dipjyotimetia/PlaywrightTestFramework#readme", "dependencies": { "@faker-js/faker": "^8.0.2", + "allure-playwright": "^2.8.1", "axios": "^1.5.0", "config": "^3.3.9", + "csv-parse": "5.5.0", "date-fns": "^2.30.0", "fs-extra": "^11.1.1", "fse": "^4.0.1", + "js-yaml": "^4.1.0", "lodash": "^4.17.21", "mkdirp": "^3.0.1", + "pg": "^8.11.3", "recursive-readdir": "^2.2.3", "rimraf": "^5.0.1", "winston": "^3.10.0", - "yaml": "^2.3.2", - "pg": "^8.11.3", - "allure-playwright": "^2.8.1", - "csv-parse": "5.5.0" + "yaml": "^2.3.2" }, "devDependencies": { + "@playwright/test": "^1.38.0", + "@types/js-yaml": "4.0.6", + "@types/node": "^20.6.2", "eslint": "^8.49.0", "eslint-plugin-playwright": "0.16.0", - "@playwright/test": "^1.38.0", "typescript": "^5.2.2" } } diff --git a/playwright.config.ts b/playwright.config.ts index 9df07bef..c79b55a2 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -16,12 +16,16 @@ export default defineConfig({ // Opt out of parallel tests on CI. workers: process.env.CI ? 1 : undefined, - // Reporter to use - reporter: [['html', 'line'], ['./reportConfig.ts'], ["allure-playwright", { - detail: true, - outputFolder: "allure-results", - suiteTitle: false, - },]], + // Reporter configuration + reporter: [ + ['html', 'line'], // HTML and Line reporters + ['./reportConfig.ts'], // Custom reporter configuration file + ['allure-playwright', { + detail: true, + outputFolder: "allure-results", + suiteTitle: false, + }], + ], // use: { // // Base URL to use in actions like `await page.goto('/')`. @@ -34,7 +38,7 @@ export default defineConfig({ projects: [ { name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + use: { ...devices['Desktop Chrome'] }, // Use Desktop Chrome device configuration }, ], -}); \ No newline at end of file +}); diff --git a/reportConfig.ts b/reportConfig.ts index 6278fe00..62d89f51 100644 --- a/reportConfig.ts +++ b/reportConfig.ts @@ -9,19 +9,20 @@ if (!existsSync(logDir)) { mkdirSync(logDir); } -const console = new transports.Console(); +const consoleTransport = new transports.Console(); +const fileTransport = new transports.File({ filename: 'logs/info.log', level: 'info' }); + const logger = createLogger({ level: 'info', format: format.json(), transports: [ - // - Write all logs with importance level of `info` or less than it - new transports.File({ filename: 'logs/info.log', level: 'info' }), + // Write all logs with importance level of `info` or less than it to console + consoleTransport, + // Write all logs with importance level of `info` to a log file + fileTransport, ], }); -// Writes logs to console -logger.add(console); - export default class MyReporter implements Reporter { onBegin(config: FullConfig, suite: Suite) { @@ -45,4 +46,4 @@ export default class MyReporter implements Reporter { onError(error: TestError): void { logger.error(error.message); } -} \ No newline at end of file +} diff --git a/src/config/config.json b/src/config/config.json deleted file mode 100644 index 544b7b4d..00000000 --- a/src/config/config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} \ No newline at end of file diff --git a/src/config/config.ts b/src/config/config.ts index 48007676..84d8d82b 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1,2 +1,23 @@ -export const BASE_URL = ''; -export const BASE_API = ''; \ No newline at end of file +import * as fs from 'fs'; +import * as yaml from 'js-yaml'; + +export interface AppConfig { + BASE_URL: string; + BASE_API: string; +} + +export function readConfigFile(filePath: string): AppConfig | null { + if (!fs.existsSync(filePath)) { + console.error('Config file does not exist:', filePath); + return null; + } + + try { + const fileContents = fs.readFileSync(filePath, 'utf-8'); + const config = yaml.load(fileContents) as AppConfig; + return config; + } catch (error) { + console.error('Error reading or parsing config file:', error); + return null; + } +} \ No newline at end of file diff --git a/src/config/config.yaml b/src/config/config.yaml new file mode 100644 index 00000000..cddc2404 --- /dev/null +++ b/src/config/config.yaml @@ -0,0 +1,2 @@ +BASE_URL: "https://react-redux.realworld.io/" +BASE_API: "" \ No newline at end of file diff --git a/src/core/apiActions.ts b/src/core/apiActions.ts new file mode 100644 index 00000000..9d624697 --- /dev/null +++ b/src/core/apiActions.ts @@ -0,0 +1,71 @@ +import { BrowserContext, Page } from '@playwright/test'; + +// Function to mock an API call +export async function mockApiCall(context: BrowserContext | Page, route: string, response: any): Promise { + await context.route(route, (route) => { + // Set the response status and body + route.fulfill({ + status: 200, + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(response) + }); + }); +} + +// Function to mock an API response with a delay +export async function mockApiCallWithDelay( + context: BrowserContext | Page, + route: string, + response: any, + delay: number +): Promise { + await context.route(route, (route) => { + // Set a delay before fulfilling the request + setTimeout(() => { + route.fulfill({ + status: 200, + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(response) + }); + }, delay); + }); +} + +// Function to mock an API response not found +export async function mockApiCallNotFound( + context: BrowserContext | Page, + route: string +): Promise { + await context.route(route, (route) => { + route.fulfill({ + status: 404, + headers: { + 'Content-Type': 'application/json' + }, + body: '{}' + }); + }); +} + + +// Function to mock an API response with a header +export async function mockApiCallWithHeader( + context: BrowserContext | Page, + route: string, + headerName: string, + headerValue: string +): Promise { + await context.route(route, (route) => { + route.fulfill({ + status: 200, + headers: { + [headerName]: headerValue + }, + body: '{}' + }); + }); +} \ No newline at end of file diff --git a/src/core/webActions.ts b/src/core/webActions.ts new file mode 100644 index 00000000..082c74a5 --- /dev/null +++ b/src/core/webActions.ts @@ -0,0 +1,135 @@ +import { Page, ElementHandle } from '@playwright/test'; + +// Function to navigate to a URL +export async function navigateToUrl(page: Page, url: string): Promise { + await page.goto(url); +} + +// Function to wait for an element to appear on the page +export async function waitForElement(page: Page, selector: string, timeout = 5000): Promise { + await page.waitForSelector(selector, { timeout }); +} + +// Function to click an element by its selector +export async function clickElement(page: Page, selector: string): Promise { + await page.click(selector); +} + +// Function to fill an input field with text +export async function fillInputField(page: Page, selector: string, text: string): Promise { + await page.fill(selector, text); +} + +// Function to capture a screenshot +export async function captureScreenshot(page: Page, path: string): Promise { + await page.screenshot({ path }); +} + +// Function to get the text content of an element +export async function getElementText(page: Page, selector: string): Promise { + const element = await page.$(selector); + if (!element) { + throw new Error(`Element with selector ${selector} not found.`); + } + return element.textContent(); +} + +// Function to wait for navigation to complete +export async function waitForNavigation(page: Page, url: string): Promise { + await page.waitForURL(url); +} + +// Function to check if an element exists on the page +export async function doesElementExist(page: Page, selector: string): Promise { + const element = await page.$(selector); + return !!element; +} + +// Function to clear the content of an input field +export async function clearInputField(page: Page, selector: string): Promise { + await page.fill(selector, ''); // Fill with an empty string to clear the field +} + +// Function to select an option from a dropdown by value +export async function selectDropdownOptionByValue(page: Page, selector: string, value: string): Promise { + await page.selectOption(selector, { value }); +} + +// Function to hover over an element +export async function hoverOverElement(page: Page, selector: string): Promise { + await page.hover(selector); +} + +// Function to right-click an element +export async function rightClickElement(page: Page, selector: string): Promise { + await page.click(selector, { button: 'right' }); +} + +// Function to get an attribute's value of an element +export async function getElementAttributeValue( + page: Page, + selector: string, + attributeName: string +): Promise { + const element = await page.$(selector); + if (!element) { + throw new Error(`Element with selector ${selector} not found.`); + } + return element.getAttribute(attributeName); +} + +// Function to retrieve an array of elements by selector +export async function getElements(page: Page, selector: string): Promise { + return await page.locator(selector).elementHandles(); +} + +// Function to switch to a new tab or window by index +export async function switchToTabOrWindowByIndex(page: Page, index: number): Promise { + const pages = await page.context().pages(); + if (index >= 0 && index < pages.length) { + await pages[index].bringToFront(); + } else { + throw new Error(`Tab or window with index ${index} not found.`); + } +} + +// Function to click an element and wait for a navigation event +export async function clickAndWaitForNavigation(page: Page, selector: string): Promise { + await Promise.all([ + page.click(selector), + page.waitForNavigation({ waitUntil: 'domcontentloaded' }), + ]); +} + +// Function to scroll an element into view +export async function scrollElementIntoView(page: Page, selector: string): Promise { + const element = await page.$(selector); + if (element) { + await element.scrollIntoViewIfNeeded(); + } +} + +// Function to take a screenshot of the current page +export async function takeScreenshot(page: Page, fileName: string): Promise { + await page.screenshot({ path: fileName }); +} + +// Function to wait for an element to be visible and clickable +export async function waitForElementToBeClickable(page: Page, selector: string): Promise { + return page.waitForSelector(selector, { state: 'visible' }); +} + +// Function to type text into an input field +export async function typeText(page: Page, selector: string, text: string): Promise { + await page.type(selector, text); +} + +// Function to press the Enter key +export async function pressEnter(page: Page, selector: string): Promise { + await page.press(selector, 'Enter'); +} + +// Function to press the Tab key +export async function pressTab(page: Page, selector: string): Promise { + await page.press(selector, 'Tab'); +} diff --git a/src/pages/login.ts b/src/pages/login.ts index d2474f07..dfff09cb 100644 --- a/src/pages/login.ts +++ b/src/pages/login.ts @@ -1,22 +1,36 @@ import { Page } from "@playwright/test"; -import { TestInfo } from "@playwright/test/types/test"; +import { TestInfo } from "@playwright/test"; +import { AppConfig } from "../config/config"; -export const LoginPge = async (page: Page, testInfo: TestInfo) => { +export const LoginPage = async (page: Page, testInfo: TestInfo) => { try { + // Navigate to the website await page.goto("https://react-redux.realworld.io/"); - console.log("page title", await page.title()); + + // Log page title + console.log("Page title:", await page.title()); + + // Click the login link await page.click('[href="#login"]'); + + // Fill in email and password fields await page.fill('input[type="email"]', 'testauto@gmail.com'); await page.fill('input[type="password"]', 'Password1'); - await page.click('button[type="submit"]') - await testInfo.attach("basic-page-screen", { + + // Click the submit button + await page.click('button[type="submit"]'); + + // Attach a screenshot as a test artifact + await testInfo.attach("LoginPageScreenshot", { body: await page.screenshot(), contentType: "image/png", }); - await page.waitForTimeout(8000) + + // Wait for a moment (you can adjust the duration) + await page.waitForTimeout(8000); } catch (error) { - await page.screenshot({ path: 'screenshot.png', fullPage: true }); - throw error + // Capture a screenshot on error + await page.screenshot({ path: 'error-screenshot.png', fullPage: true }); + throw error; // Rethrow the error to fail the test } - -} \ No newline at end of file +}; diff --git a/src/pages/posts.ts b/src/pages/posts.ts index 186e4cdf..4dbb39eb 100644 --- a/src/pages/posts.ts +++ b/src/pages/posts.ts @@ -4,7 +4,7 @@ import * as faker from "@faker-js/faker"; export const AddPost = async (page: Page) => { await page.click('text=/.*New Post.*/'); await page.click('input[placeholder="Article Title"]'); - await page.fill('input[placeholder="Article Title"]', faker.faker.random.alphaNumeric(10)); + await page.fill('input[placeholder="Article Title"]', faker.faker.lorem.word()); await page.click('input[placeholder="What\'s this article about?"]'); await page.fill('input[placeholder="What\'s this article about?"]', 'Is it working'); await page.click('textarea[placeholder="Write your article (in markdown)"]'); diff --git a/src/tests/home.test.spec.ts b/src/tests/home.test.spec.ts index e062e946..d2a0c03d 100644 --- a/src/tests/home.test.spec.ts +++ b/src/tests/home.test.spec.ts @@ -1,17 +1,17 @@ import { test } from "@playwright/test"; import { allure } from "allure-playwright"; -import { LoginPge } from "../pages/login"; +import { LoginPage } from "../pages/login"; import { AddPost } from "../pages/posts"; import { AddComments } from "../pages/comments"; -test.describe(() => { +test.describe("Test Suite", () => { // All tests in this describe group will get 2 retry attempts. test.describe.configure({ retries: 2, mode: 'serial' }); test("Home page should have the correct title", async ({ page }, testInfo) => { - allure.link("https://playwright.dev", "playwright-site"); // link with name - allure.issue("Issue Name", "https://github.com/allure-framework/allure-js/issues/352"); - await LoginPge(page, testInfo); + await allure.link("https://playwright.dev", "playwright-site"); // link with name + await allure.issue("Issue Name", "https://github.com/allure-framework/allure-js/issues/352"); + await LoginPage(page, testInfo); await AddPost(page); await AddComments(page); }); diff --git a/src/types.d.ts b/src/types.d.ts index 480f6fb4..c440db93 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,7 +1,9 @@ -import { Browser, Page } from "playwright"; +// global.d.ts + +import { Browser, Page } from "@playwright/test"; declare global { const page: Page; const browser: Browser; const browserName: string; -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index d0a2c368..f0abfb45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -129,6 +129,16 @@ dependencies: playwright "1.38.0" +"@types/js-yaml@4.0.6": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.6.tgz#4b3afd5158b8749095b1f096967b6d0f838d862f" + integrity sha512-ACTuifTSIIbyksx2HTon3aFtCKWcID7/h3XEmRpDYdMCXxPbl+m9GteOJeaAkiAta/NJaSFuA7ahZ0NkwajDSw== + +"@types/node@^20.6.2": + version "20.6.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.6.2.tgz#a065925409f59657022e9063275cd0b9bd7e1b12" + integrity sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw== + "@types/triple-beam@^1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.2.tgz#38ecb64f01aa0d02b7c8f4222d7c38af6316fef8"