- Notifications
You must be signed in to change notification settings - Fork27
Add e2e tests#331
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
base:staging
Are you sure you want to change the base?
Uh oh!
There was an error while loading.Please reload this page.
Add e2e tests#331
Changes from1 commit
4906b4c1238be6d96ac6b7b40625783d9f4862a87b692efed4d34598a966b031b880a4f9e369cc124c2ef6c751f2ddeb392e65792d7233b7bfa04c1610c3f5File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
Initialize Playwright e2e test architecture
- Loading branch information
Uh oh!
There was an error while loading.Please reload this page.
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| name: Playwright Tests | ||
| on: | ||
| push: | ||
| branches: [ main, staging ] | ||
| pull_request: | ||
| branches: [ main, staging ] | ||
| 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. [blocker]: Playwright CI never launches backend. The Playwright workflow installs dependencies and immediately runs There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. [blocker] This job installs deps and immediately executes | ||
| run: npx playwright test | ||
| - uses: actions/upload-artifact@v4 | ||
| if: ${{ !cancelled() }} | ||
| with: | ||
| name: playwright-report | ||
| path: playwright-report/ | ||
| retention-days: 30 | ||
| ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| import { APIRequestContext, request } from "@playwright/test" | ||
| const getRequest = async (url: string, headers?: Record<string, string>, body?: any, availableRequest?: APIRequestContext) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. [major]: API request contexts leak. Each helper creates a | ||
| const requestOptions = { | ||
| data: body, | ||
| headers: headers || undefined, | ||
| }; | ||
| const requestContext = availableRequest || (await request.newContext()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. [major] Each helper here spins up a fresh | ||
| const response = await requestContext.get(url, requestOptions); | ||
| return response; | ||
| }; | ||
| const postRequest = async (url: string, body?: any, availableRequest?: APIRequestContext, headers?: Record<string, string>) => { | ||
| const requestOptions = { | ||
| data: body, | ||
| headers: headers || undefined, | ||
| }; | ||
| const requestContext = availableRequest || (await request.newContext()); | ||
| const response = await requestContext.post(url, requestOptions); | ||
| return response; | ||
| }; | ||
| const deleteRequest = async (url: string, headers?: Record<string, string>, body?: any) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. [major]: DELETE helpers drop authentication. | ||
| const requestOptions = { | ||
| data: body, | ||
| headers: headers || undefined, | ||
| }; | ||
| const requestContext = await request.newContext(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. [major] Unlike the GET/POST/PATCH helpers, | ||
| const response = await requestContext.delete(url, requestOptions); | ||
| return response; | ||
| }; | ||
| const patchRequest = async (url: string, body?: any, availableRequest?: APIRequestContext, headers?: Record<string, string>) => { | ||
| const requestOptions = { | ||
| data: body, | ||
| headers: headers || undefined, | ||
| }; | ||
| const requestContext = availableRequest || (await request.newContext()); | ||
| const response = await requestContext.patch(url, requestOptions); | ||
| return response; | ||
| }; | ||
| export { getRequest, deleteRequest, postRequest, patchRequest } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { Page } from "playwright"; | ||
| export default class BasePage { | ||
| protected page: Page; | ||
| constructor(page: Page) { | ||
| this.page = page; | ||
| } | ||
| async initPage() { | ||
| await this.page.waitForLoadState(); | ||
| } | ||
| getCurrentURL(): string { | ||
| return this.page.url(); | ||
| } | ||
| async refreshPage() { | ||
| await this.page.reload({ waitUntil: "networkidle" }); | ||
| } | ||
| async waitForResponse(url: string) { | ||
| const response = await this.page.waitForResponse(url); | ||
| return response; | ||
| } | ||
| async waitForUrl(url: string) { | ||
| return this.page.waitForURL(url); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import { chromium, Browser, BrowserContext, Page, firefox } from 'playwright'; | ||
| import { test } from '@playwright/test'; | ||
| import BasePage from './basePage'; | ||
| async function launchBrowser(projectName: string): Promise<Browser> { | ||
| if (projectName.toLowerCase().includes('firefox')) { | ||
| return firefox.launch(); | ||
| } | ||
| return chromium.launch(); | ||
| } | ||
| export default class BrowserWrapper { | ||
| private browser: Browser | null = null; | ||
| private context: BrowserContext | null = null; | ||
| private page: Page | null = null; | ||
| async createNewPage<T extends BasePage>(PageClass: new (page: Page) => T, url?: string) { | ||
| if (!this.browser) { | ||
| const projectName = test.info().project.name; | ||
| this.browser = await launchBrowser(projectName); | ||
| } | ||
| if (!this.context) { | ||
| this.context = await this.browser.newContext(); | ||
| } | ||
| if (!this.page) { | ||
| this.page = await this.context.newPage(); | ||
| } | ||
| if (url) { | ||
| await this.navigateTo(url) | ||
| } | ||
| const pageInstance = new PageClass(this.page); | ||
| return pageInstance; | ||
| } | ||
| getContext(): BrowserContext | null { | ||
| return this.context; | ||
| } | ||
| async getPage() { | ||
| if (!this.page) { | ||
| throw new Error('Browser is not launched yet!'); | ||
| } | ||
| return this.page; | ||
| } | ||
| async setPageToFullScreen() { | ||
| if (!this.page) { | ||
| throw new Error('Browser is not launched yet!'); | ||
| } | ||
| await this.page.setViewportSize({ width: 1920, height: 1080 }); | ||
| } | ||
| async navigateTo(url: string) { | ||
| if (!this.page) { | ||
| throw new Error('Browser is not launched yet!'); | ||
| } | ||
| await this.page.goto(url); | ||
| await this.page.waitForLoadState('networkidle'); | ||
| } | ||
| async closePage() { | ||
| if (this.page) { | ||
| await this.page.close(); | ||
| this.page = null; | ||
| } | ||
| } | ||
| async closeBrowser() { | ||
| ||
| await this.closePage(); | ||
| if (this.browser) { | ||
| await this.browser.close(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import { Locator, Page } from "playwright"; | ||
| export function delay(ms: number) { | ||
| return new Promise((resolve) => { | ||
| setTimeout(resolve, ms); | ||
| }); | ||
| } | ||
| export const waitForElementToBeVisible = async ( | ||
| locator: Locator, | ||
| time = 500, | ||
| retry = 10 | ||
| ): Promise<boolean> => { | ||
| for (let i = 0; i < retry; i += 1) { | ||
| try { | ||
| if (await locator.isVisible()) { | ||
| return true; | ||
| } | ||
| } catch (error) { | ||
| console.error(`Error checking element visibility: ${error}`); | ||
| } | ||
| await delay(time); | ||
| } | ||
| return false; | ||
| }; | ||
| export const waitForElementToNotBeVisible = async ( | ||
| locator: Locator, | ||
| time = 500, | ||
| retry = 10 | ||
| ): Promise<boolean> => { | ||
| for (let i = 0; i < retry; i += 1) { | ||
| try { | ||
| if (!(await locator.isVisible())) { | ||
| return true; | ||
| } | ||
| } catch (error) { | ||
| console.error(`Error checking element visibility: ${error}`); | ||
| } | ||
| await delay(time); | ||
| } | ||
| return false; | ||
| }; | ||
| export const waitForElementToBeEnabled = async ( | ||
| locator: Locator, | ||
| time = 500, | ||
| retry = 10 | ||
| ): Promise<boolean> => { | ||
| for (let i = 0; i < retry; i += 1) { | ||
| try { | ||
| if (await locator.isEnabled()) { | ||
| return true; | ||
| } | ||
| } catch (error) { | ||
| console.error(`Error checking element enabled: ${error}`); | ||
| } | ||
| await delay(time); | ||
| } | ||
| return false; | ||
| }; | ||
| export function getRandomString(prefix = "", delimiter = "_"): string { | ||
| const uuid = crypto.randomUUID(); | ||
| const timestamp = Date.now(); | ||
| return `${prefix}${prefix ? delimiter : ""}${uuid}-${timestamp}`; | ||
| } |
Uh oh!
There was an error while loading.Please reload this page.