├── .env.example ├── .gitattributes ├── .github └── workflows │ └── e2e.yml ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── api ├── auth.controller.ts ├── index.ts ├── models.d.ts └── requestHolder.ts ├── app ├── abstractClasses.ts ├── component │ ├── header.component.ts │ └── minicart.component.ts ├── index.ts └── page │ ├── account │ └── details.page.ts │ ├── confirmation.page.ts │ ├── contactus.page.ts │ ├── home.page.ts │ ├── product │ ├── component │ │ └── review.component.ts │ └── index.ts │ ├── shop.page.ts │ ├── signin.page.ts │ └── signup.page.ts ├── app_withLoadableComponent ├── IloadableComponent.ts ├── appComponent.ts ├── index.ts └── pages │ ├── confirmation.page.ts │ ├── dashboard.page.ts │ ├── home.page.ts │ ├── login.page.ts │ ├── product.page.ts │ └── products.page.ts ├── db ├── index.ts └── models.d.ts ├── env └── index.ts ├── fixture ├── globalBeforeEach.ts └── index.ts ├── globalSetup.ts ├── misc └── reporters │ └── step.ts ├── package-lock.json ├── package.json ├── playwright.config.ts ├── precondition └── safari.precondition.ts ├── tags.ts ├── tests ├── admin.test.ts ├── contact-us.test.ts ├── purchase.test.ts ├── review.test.ts └── shop.test.ts ├── tsconfig.json └── utils └── testSkipper.ts /.env.example: -------------------------------------------------------------------------------- 1 | # Optional 2 | CI=false 3 | # Required 4 | FRONTEND_URL=https://shopdemo-alex-hot.koyeb.app 5 | # Required 6 | API_URL=https://shopdemo-alex-hot.koyeb.app/api 7 | # Required, MONGO_DB connection 8 | DB_CONNECTION_URI= -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: e2e tests 2 | on: 3 | push: 4 | branches: [ main, master ] 5 | pull_request: 6 | branches: [ main, master ] 7 | jobs: 8 | full-e2e: 9 | env: 10 | HOME: /root 11 | timeout-minutes: 60 12 | runs-on: ubuntu-latest 13 | container: 14 | image: mcr.microsoft.com/playwright:v1.44.1-jammy 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | shardIndex: [1, 2, 3] 19 | shardTotal: [3] 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: install npm deps 23 | run: npm ci 24 | - name: running e2e tests 25 | run: npm test -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} 26 | # run: npm test 27 | - uses: actions/upload-artifact@v4 28 | if: always() 29 | with: 30 | name: playwright-report-${{ matrix.shardIndex }} 31 | # name: playwright-report-html 32 | path: playwright-report/ 33 | retention-days: 7 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # test results 2 | playwright-report 3 | test-results 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | .pnpm-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Snowpack dependency directory (https://snowpack.dev/) 50 | web_modules/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional stylelint cache 62 | .stylelintcache 63 | 64 | # Microbundle cache 65 | .rpt2_cache/ 66 | .rts2_cache_cjs/ 67 | .rts2_cache_es/ 68 | .rts2_cache_umd/ 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variable files 80 | # .env 81 | .env.development.local 82 | .env.test.local 83 | .env.production.local 84 | .env.local 85 | 86 | # parcel-bundler cache (https://parceljs.org/) 87 | .cache 88 | .parcel-cache 89 | 90 | # Next.js build output 91 | .next 92 | out 93 | 94 | # Nuxt.js build / generate output 95 | .nuxt 96 | dist 97 | 98 | # Gatsby files 99 | .cache/ 100 | # Comment in the public line in if your project uses Gatsby and not Next.js 101 | # https://nextjs.org/blog/next-9-1#public-directory-support 102 | # public 103 | 104 | # vuepress build output 105 | .vuepress/dist 106 | 107 | # vuepress v2.x temp and cache directory 108 | .temp 109 | .cache 110 | 111 | # Docusaurus cache and generated files 112 | .docusaurus 113 | 114 | # Serverless directories 115 | .serverless/ 116 | 117 | # FuseBox cache 118 | .fusebox/ 119 | 120 | # DynamoDB Local files 121 | .dynamodb/ 122 | 123 | # TernJS port file 124 | .tern-port 125 | 126 | # Stores VSCode versions used for testing VSCode extensions 127 | .vscode-test 128 | 129 | # yarn v2 130 | .yarn/cache 131 | .yarn/unplugged 132 | .yarn/build-state.yml 133 | .yarn/install-state.gz 134 | .pnp.* 135 | .env 136 | .env 137 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.14.0 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Oleksandr Khotemskyi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PWSO_JUNE_2024 2 | 3 | URLs: 4 | 1. https://shopdemo-alex-hot.koyeb.app/ (My server) 5 | 2. https://mern-store.onrender.com/ (Backup) 6 | 7 | 8 | ## Topics: 9 | 10 | ### 1 11 | - Конфігурація Playwright проекту - projects, global pre/post conditions 12 | - Використання змінних оточення для конфігурації 13 | - Playwright+Docker, приклад CI/CD на Github Actions 14 | 15 | - пофіксити копіпасту юрл 16 | - додати конфігурування юрлок через енв варіабли 17 | - налаштувати ci/cd 18 | 19 | ### 2 20 | - Композиція Page Objects та Page Components в тестах. Об’єкт Application 21 | - Фікстури, фікстури-опції, ліниві та авто фікстури 22 | 23 | ### 3 24 | - Робота з базою данних (на прикладі MongoDB) 25 | - Робота з API. Перехоплення request та response (використання проксі) 26 | - Розширення репортингу власними данними 27 | - Які ще ідеї розглянути для вашого проекту автоматизації -------------------------------------------------------------------------------- /api/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { RequestHolder } from "./requestHolder"; 2 | import type { 3 | LoginResponse, 4 | UserCreateRequest, 5 | UserCreatedResponse, 6 | } from "./models"; 7 | import { env } from "../env"; 8 | import { step } from "../misc/reporters/step"; 9 | 10 | export class AuthController extends RequestHolder { 11 | @step() 12 | async login(data: { 13 | email: string; 14 | password: string; 15 | }): Promise { 16 | const loginResponse = await this.request.post( 17 | `${env.API_URL}/auth/login`, 18 | { 19 | data, 20 | } 21 | ); 22 | 23 | return loginResponse.json() as Promise; 24 | } 25 | 26 | @step() 27 | async register(data: UserCreateRequest): Promise { 28 | const resp = await this.request.post( 29 | `${env.API_URL}/auth/register`, 30 | { 31 | data, 32 | } 33 | ); 34 | return await resp.json() as Promise; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /api/index.ts: -------------------------------------------------------------------------------- 1 | import { AuthController } from "./auth.controller"; 2 | import { RequestHolder } from "./requestHolder"; 3 | 4 | export class API extends RequestHolder { 5 | public readonly auth = new AuthController(this.request); 6 | } 7 | -------------------------------------------------------------------------------- /api/models.d.ts: -------------------------------------------------------------------------------- 1 | export interface LoginResponse { 2 | success: boolean; 3 | token: string; 4 | user: { 5 | id: string; 6 | firstName: string; 7 | lastName: string; 8 | email: string; 9 | role: string; 10 | }; 11 | } 12 | 13 | export interface UserCreatedResponse { 14 | success: boolean; 15 | subscribed: boolean; 16 | token: string; 17 | user: { 18 | id: string; 19 | firstName: string; 20 | lastName: string; 21 | email: string; 22 | role: string; 23 | }; 24 | } 25 | 26 | export interface UserCreateRequest { 27 | isSubscribed: boolean; 28 | email: string; 29 | firstName: string; 30 | lastName: string; 31 | password: string; 32 | } 33 | -------------------------------------------------------------------------------- /api/requestHolder.ts: -------------------------------------------------------------------------------- 1 | import { APIRequestContext } from "@playwright/test"; 2 | 3 | export abstract class RequestHolder { 4 | constructor(protected request: APIRequestContext) {} 5 | } 6 | -------------------------------------------------------------------------------- /app/abstractClasses.ts: -------------------------------------------------------------------------------- 1 | import type { Page } from '@playwright/test'; 2 | import { step } from '../misc/reporters/step'; 3 | 4 | export abstract class PageHolder { 5 | constructor(protected page: Page) { } 6 | } 7 | export abstract class Component extends PageHolder { 8 | abstract expectLoaded(message?: string): Promise; 9 | 10 | @step() 11 | async isLoaded(): Promise { 12 | try { 13 | await this.expectLoaded() 14 | return true 15 | } catch { 16 | return false 17 | } 18 | } 19 | } 20 | 21 | export abstract class AppPage extends Component { 22 | /** 23 | * Path to the page can be relative to the baseUrl defined in playwright.config.ts 24 | * or absolute (on your own risk) 25 | */ 26 | public abstract pagePath: string; 27 | 28 | /** 29 | * Opens the page in the browser and expectLoaded should pass 30 | */ 31 | @step() 32 | async open(path?: string) { 33 | await this.page.goto(path ?? this.pagePath); 34 | await this.expectLoaded(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/component/header.component.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@playwright/test"; 2 | import { Component } from "../abstractClasses"; 3 | import { step } from "../../misc/reporters/step"; 4 | 5 | export class Header extends Component { 6 | private shopLink = this.page.getByRole('link', { name: 'Shop' }) 7 | private cartLink = this.page.getByRole('button', { name: 'your cart' }) 8 | 9 | @step() 10 | async expectLoaded(message = 'Expected Header to be loaded'): Promise { 11 | await expect(this.shopLink, message).toBeVisible(); 12 | } 13 | 14 | @step() 15 | async openCart() { 16 | await this.cartLink.click(); 17 | } 18 | 19 | @step() 20 | async openShop() { 21 | await this.shopLink.click() 22 | } 23 | } -------------------------------------------------------------------------------- /app/component/minicart.component.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@playwright/test"; 2 | import { Component } from "../abstractClasses"; 3 | import { step } from "../../misc/reporters/step"; 4 | 5 | export class MiniCart extends Component { 6 | private proceedToCheckoutButton = this.page.getByRole('button', { name: 'Proceed To Checkout' }); 7 | private placeOrderButton = this.page.getByRole('button', { name: 'Place Order' }); 8 | 9 | @step() 10 | async expectLoaded() { 11 | await expect(this.proceedToCheckoutButton.or(this.placeOrderButton)).toBeVisible(); 12 | } 13 | 14 | @step() 15 | async proceedToCheckout() { 16 | await this.proceedToCheckoutButton.click(); 17 | } 18 | 19 | @step() 20 | async placeOrder() { 21 | await this.placeOrderButton.click(); 22 | } 23 | } -------------------------------------------------------------------------------- /app/index.ts: -------------------------------------------------------------------------------- 1 | import { API } from "../api"; 2 | import { PageHolder } from "./abstractClasses"; 3 | import { AccountDetails } from "./page/account/details.page"; 4 | import { Confirmation } from "./page/confirmation.page"; 5 | import { ContactUs } from "./page/contactus.page"; 6 | import { Home } from "./page/home.page"; 7 | import { Product } from "./page/product"; 8 | import { Shop } from "./page/shop.page"; 9 | import { SignIn } from "./page/signin.page"; 10 | import { SignUp } from "./page/signup.page"; 11 | 12 | export class Application extends PageHolder { 13 | public api = new API(this.page.request); 14 | 15 | public signUp = new SignUp(this.page); 16 | public home = new Home(this.page); 17 | public shop = new Shop(this.page); 18 | public product = new Product(this.page); 19 | public signIn = new SignIn(this.page); 20 | public accountDetails = new AccountDetails(this.page); 21 | public confirmation = new Confirmation(this.page); 22 | public contactus = new ContactUs(this.page); 23 | 24 | async headlessLogin(data: { email: string; password: string }) { 25 | const token = (await this.api.auth.login(data)).token; 26 | await this.setTokenToLocalStorage(token); 27 | } 28 | 29 | async setTokenToLocalStorage(token: string) { 30 | console.time("setTokenToLocalStorage"); 31 | await this.page.goto("/", { waitUntil: "commit" }); 32 | await this.page.evaluate( 33 | (_token) => window.localStorage.setItem("token", _token), 34 | token 35 | ); 36 | console.timeEnd("setTokenToLocalStorage"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/page/account/details.page.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@playwright/test"; 2 | import { AppPage } from "../../abstractClasses"; 3 | import { Header } from "../../component/header.component"; 4 | import { MiniCart } from "../../component/minicart.component"; 5 | import { step } from "../../../misc/reporters/step"; 6 | 7 | export class AccountDetails extends AppPage { 8 | public pagePath = '/dashboard' 9 | 10 | public header = new Header(this.page) 11 | public miniCart = new MiniCart(this.page) 12 | 13 | private heading = this.page.getByRole('heading', { name: 'Account Details' }) 14 | 15 | @step() 16 | async expectLoaded() { 17 | await expect(this.heading).toBeVisible(); 18 | } 19 | 20 | @step() 21 | async expectMenuItemVisible(menuItem: string) { 22 | await expect(this.page.getByText(menuItem)).toBeVisible(); 23 | } 24 | } -------------------------------------------------------------------------------- /app/page/confirmation.page.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@playwright/test"; 2 | import { AppPage } from "../abstractClasses"; 3 | import { step } from "../../misc/reporters/step"; 4 | 5 | export class Confirmation extends AppPage { 6 | public pagePath = 'order/success/'; 7 | 8 | private successMessage = this.page.getByRole('heading', { name: 'Thank you for your order.' }) 9 | 10 | @step() 11 | async expectLoaded(message = 'Expected confirmation page to be loaded') { 12 | await expect(this.successMessage, message).toBeVisible(); 13 | } 14 | 15 | @step() 16 | async expectOrderPlaced() { 17 | await this.expectLoaded('Expected order to be placed sucessfully'); 18 | } 19 | } -------------------------------------------------------------------------------- /app/page/contactus.page.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@playwright/test"; 2 | import { AppPage } from "../abstractClasses"; 3 | import { step } from "../../misc/reporters/step"; 4 | 5 | export class ContactUs extends AppPage { 6 | public pagePath = "/contact"; 7 | 8 | private fullNameInput = this.page.getByPlaceholder("You Full Name"); 9 | private emailInput = this.page.getByPlaceholder("Your Email Address"); 10 | private messageInput = this.page.getByPlaceholder( 11 | "Please Describe Your Message" 12 | ); 13 | private submitButton = this.page.getByRole("button", { name: "Submit" }); 14 | 15 | @step() 16 | async expectLoaded() { 17 | await expect(this.fullNameInput).toBeVisible(); 18 | await expect(this.emailInput).toBeVisible(); 19 | await expect(this.messageInput).toBeVisible(); 20 | await expect(this.submitButton).toBeVisible(); 21 | } 22 | 23 | @step() 24 | async submitContactUsForm(options: { 25 | fullName: string; 26 | email: string; 27 | message: string; 28 | }) { 29 | await this.expectLoaded(); 30 | await this.fullNameInput.fill(options.fullName); 31 | await this.emailInput.fill(options.email); 32 | await this.messageInput.fill(options.message); 33 | await this.submitButton.click(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/page/home.page.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@playwright/test"; 2 | import { AppPage } from "../abstractClasses"; 3 | import { Header } from "../component/header.component"; 4 | import { step } from "../../misc/reporters/step"; 5 | 6 | export class Home extends AppPage { 7 | public pagePath = '/'; 8 | 9 | public header = new Header(this.page); 10 | private carousel = this.page.locator('.main .homepage .home-carousel') 11 | 12 | @step() 13 | async expectLoaded(message = 'Expected Home page to be opened') { 14 | await expect(this.carousel, message).toBeVisible(); 15 | } 16 | } -------------------------------------------------------------------------------- /app/page/product/component/review.component.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@playwright/test"; 2 | import { Component } from "../../../abstractClasses"; 3 | import { step } from "../../../../misc/reporters/step"; 4 | 5 | export class Review extends Component { 6 | starRating = (star: number) => this.page.locator(`.react-stars [data-index="${star}"] .fa-star`); 7 | titleInput = this.page.getByPlaceholder('Enter Review title'); 8 | commentInput = this.page.getByPlaceholder('Write Review'); 9 | publishButton = this.page.getByRole('button', { name: 'Publish Review' }); 10 | 11 | 12 | @step() 13 | async expectLoaded(): Promise { 14 | await expect(this.starRating(0)).toBeVisible(); 15 | await expect(this.starRating(4)).toBeVisible(); 16 | await expect(this.titleInput).toBeVisible(); 17 | await expect(this.commentInput).toBeVisible(); 18 | await expect(this.publishButton).toBeVisible(); 19 | } 20 | 21 | @step() 22 | async add(options: { title: string, comment: string, stars: number }) { 23 | await this.expectLoaded(); 24 | if (options.stars < 0 || options.stars > 4) throw new Error('Stars should be between 0 and 4'); 25 | 26 | await this.titleInput.fill(options.title); 27 | await this.commentInput.fill(options.comment); 28 | await this.starRating(options.stars).click(); 29 | await this.publishButton.click(); 30 | } 31 | 32 | @step() 33 | async expectReviewAdded() { 34 | await expect(this.page.getByRole('heading', { name: 'Your review has been added' })).toBeVisible(); 35 | } 36 | } -------------------------------------------------------------------------------- /app/page/product/index.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@playwright/test"; 2 | import { AppPage } from "../../abstractClasses"; 3 | import { Header } from "../../component/header.component"; 4 | import { MiniCart } from "../../component/minicart.component"; 5 | import { Review } from "./component/review.component"; 6 | import { step } from "../../../misc/reporters/step"; 7 | 8 | export class Product extends AppPage { 9 | public pagePath = '/product'; 10 | 11 | public header = new Header(this.page); 12 | public miniCart = new MiniCart(this.page); 13 | 14 | private addToBagButton = this.page.getByRole('button', { name: 'Add To Bag' }); 15 | private removeFromBagButton = this.page.getByRole('button', { name: 'Remove From Bag' }); 16 | 17 | public reviewComponent = new Review(this.page); 18 | 19 | @step() 20 | async expectLoaded(message = 'Expected Product page to be opened') { 21 | await expect(this.addToBagButton 22 | .or(this.removeFromBagButton), 23 | message 24 | ).toBeVisible(); 25 | } 26 | 27 | // TODO: Rewrite to accept only product slug 28 | @step() 29 | override async open(productPath: string): Promise { 30 | await this.page.goto(productPath); 31 | } 32 | 33 | @step() 34 | async changeQuantity(quantity: number) { 35 | await this.page.getByPlaceholder('Product Quantity').fill(quantity.toString()); 36 | } 37 | 38 | @step() 39 | async addToBag() { 40 | await this.expectLoaded(); 41 | await this.addToBagButton.click(); 42 | } 43 | } -------------------------------------------------------------------------------- /app/page/shop.page.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@playwright/test"; 2 | import { AppPage } from "../abstractClasses"; 3 | import { Header } from "../component/header.component"; 4 | import { step } from "../../misc/reporters/step"; 5 | 6 | export class Shop extends AppPage { 7 | public pagePath = '/shop'; 8 | 9 | public header = new Header(this.page) 10 | 11 | private productList = this.page.locator('.shop .product-list') 12 | 13 | @step() 14 | async expectLoaded(message = 'Expected Shop page to be opened') { 15 | await expect(this.productList, message).toBeVisible() 16 | } 17 | 18 | @step() 19 | async openProductDetailsByName(name: string) { 20 | await this.page.getByRole('heading', { name }).click(); 21 | } 22 | } -------------------------------------------------------------------------------- /app/page/signin.page.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@playwright/test"; 2 | import { AppPage } from "../abstractClasses"; 3 | import { step } from "../../misc/reporters/step"; 4 | 5 | export class SignIn extends AppPage { 6 | public pagePath = '/login' 7 | 8 | private signInButton = this.page.getByRole('button', { name: 'Login' }) 9 | private emailInput = this.page.getByRole('main').getByPlaceholder('Please Enter Your Email') 10 | private passwordInput = this.page.getByPlaceholder('Please Enter Your Password') 11 | 12 | @step() 13 | async expectLoaded() { 14 | await expect(this.signInButton).toBeVisible(); 15 | await expect(this.emailInput).toBeVisible(); 16 | await expect(this.passwordInput).toBeVisible(); 17 | } 18 | 19 | @step() 20 | async signIn(user: { email: string, password: string }) { 21 | await this.expectLoaded(); 22 | await this.emailInput.fill(user.email) 23 | await this.passwordInput.fill(user.password) 24 | await this.signInButton.click() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/page/signup.page.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@playwright/test"; 2 | import { AppPage } from "../abstractClasses"; 3 | import { step } from "../../misc/reporters/step"; 4 | 5 | export class SignUp extends AppPage { 6 | public pagePath = '/register'; 7 | 8 | private emailInput = this.page.getByRole('main').getByPlaceholder('Please Enter Your Email'); 9 | private firstNameInput = this.page.getByPlaceholder('Please Enter Your First Name'); 10 | private lastNameInput = this.page.getByPlaceholder('Please Enter Your Last Name'); 11 | private passwordInput = this.page.getByPlaceholder('Please Enter Your Password') 12 | private signUpButton = this.page.getByRole('button', { name: 'Sign Up' }); 13 | 14 | @step() 15 | async expectLoaded() { 16 | await expect(this.emailInput, 'Expected SignUp page to be opened').toBeVisible(); 17 | } 18 | 19 | @step() 20 | async signUpNewUser() { 21 | await this.emailInput.fill(`test+${Date.now()}@test.com`); 22 | await this.firstNameInput.fill('test'); 23 | await this.lastNameInput.fill('test'); 24 | await this.passwordInput.fill('123456'); 25 | await this.signUpButton.click(); 26 | } 27 | } -------------------------------------------------------------------------------- /app_withLoadableComponent/IloadableComponent.ts: -------------------------------------------------------------------------------- 1 | export interface LoadableComponent { 2 | open(): Promise; 3 | expectLoaded(): Promise; 4 | } 5 | -------------------------------------------------------------------------------- /app_withLoadableComponent/appComponent.ts: -------------------------------------------------------------------------------- 1 | import { type Page } from "@playwright/test"; 2 | 3 | export abstract class AppComponent { 4 | constructor(protected page: Page) {} 5 | } 6 | -------------------------------------------------------------------------------- /app_withLoadableComponent/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { AppComponent } from "./appComponent"; 3 | import { Confirmation } from "./pages/confirmation.page"; 4 | import { Dashboard } from "./pages/dashboard.page"; 5 | import { Home } from "./pages/home.page"; 6 | import { Login } from "./pages/login.page"; 7 | import { Product } from "./pages/product.page"; 8 | import { Products } from "./pages/products.page"; 9 | 10 | export class Application extends AppComponent { 11 | home = new Home(this.page); 12 | login = new Login(this.page); 13 | products = new Products(this.page); 14 | product = new Product(this.page); 15 | confirmation = new Confirmation(this.page); 16 | dashboard = new Dashboard(this.page) 17 | } 18 | -------------------------------------------------------------------------------- /app_withLoadableComponent/pages/confirmation.page.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@playwright/test"; 2 | import { AppComponent } from "../appComponent"; 3 | 4 | export class Confirmation extends AppComponent { 5 | expectOrderPlaced() { 6 | return expect( 7 | this.page.locator("h2", { hasText: "Thank you for your order." }) 8 | ).toBeVisible(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app_withLoadableComponent/pages/dashboard.page.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@playwright/test"; 2 | import { AppComponent } from "../appComponent"; 3 | import { LoadableComponent } from "../IloadableComponent"; 4 | 5 | export class Dashboard extends AppComponent implements LoadableComponent { 6 | async open() { 7 | await this.page.goto("/dashboard"); 8 | } 9 | 10 | async expectLoaded() { 11 | await expect( 12 | this.page.locator(".admin .account"), 13 | "Dashboard page is not loaded" 14 | ).toBeVisible(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app_withLoadableComponent/pages/home.page.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@playwright/test"; 2 | import { AppComponent } from "../appComponent"; 3 | import { LoadableComponent } from "../IloadableComponent"; 4 | 5 | export class Home extends AppComponent implements LoadableComponent { 6 | async open() { 7 | await this.page.goto("/"); 8 | } 9 | 10 | async expectLoaded() { 11 | await expect( 12 | this.page.locator(".homepage .home-carousel"), 13 | "Home page is not loaded" 14 | ).toBeVisible(); 15 | } 16 | 17 | async openLoginPage() { 18 | await this.page.getByRole("link", { name: "Welcome! " }).click(); 19 | await this.page.getByRole("menuitem", { name: "Login" }).click(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app_withLoadableComponent/pages/login.page.ts: -------------------------------------------------------------------------------- 1 | import { AppComponent } from "../appComponent"; 2 | 3 | export class Login extends AppComponent { 4 | async doLogin(email: string, password: string) { 5 | await this.page 6 | .getByRole("main") 7 | .getByPlaceholder("Please Enter Your Email") 8 | .fill(email); 9 | await this.page 10 | .getByPlaceholder("Please Enter Your Password") 11 | .fill(password); 12 | await this.page 13 | .getByPlaceholder("Please Enter Your Password") 14 | .press("Enter"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app_withLoadableComponent/pages/product.page.ts: -------------------------------------------------------------------------------- 1 | import { AppComponent } from "../appComponent"; 2 | 3 | export class Product extends AppComponent { 4 | async placeOrder() { 5 | await this.page.getByRole("button", { name: "Place Order" }).click(); 6 | } 7 | async addToBag() { 8 | await this.page.getByRole("button", { name: "Add To Bag" }).click(); 9 | } 10 | async open(path: string) { 11 | await this.page.goto(path); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app_withLoadableComponent/pages/products.page.ts: -------------------------------------------------------------------------------- 1 | import { AppComponent } from "../appComponent"; 2 | 3 | export class Products extends AppComponent { 4 | async selectProductByName(name: string) { 5 | await this.page.getByRole("link", { name }).click(); 6 | } 7 | async open(path = "/shop/brand/Nizhyn") { 8 | await this.page.goto(path); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /db/index.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient, ServerApiVersion, ObjectId } from "mongodb"; 2 | import { randomUUID } from "node:crypto"; 3 | import { env } from "../env"; 4 | 5 | import { CreateDBUser } from "./models"; 6 | import { step } from "../misc/reporters/step"; 7 | 8 | export class DB { 9 | static async connect() { 10 | // Create a MongoClient with a MongoClientOptions object to set the Stable API version 11 | const client = new MongoClient(env.DB_CONNECTION_URI, { 12 | serverApi: { 13 | version: ServerApiVersion.v1, 14 | strict: true, 15 | deprecationErrors: true, 16 | }, 17 | }); 18 | await client.connect(); 19 | // Send a ping to confirm a successful connection 20 | await client.db("admin").command({ ping: 1 }); 21 | console.log("Successfully connected to MongoDB!"); 22 | return new DB(client); 23 | } 24 | 25 | private constructor(private client: MongoClient) {} 26 | 27 | async close() { 28 | await this.client.close(); 29 | } 30 | 31 | @step() 32 | async createAdminUser(): Promise { 33 | console.time('createAdminUser'); 34 | const doc: CreateDBUser = { 35 | _id: new ObjectId(), 36 | merchant: null, 37 | provider: "Email", 38 | role: "ROLE ADMIN", 39 | email: `testdb+${randomUUID()}@test.com`, 40 | // hashed xotabu4@gmail.com 41 | password: "$2a$10$SQtYcNaD8xJlHSIvAu/vKOt3Gr/hPzJMV2RHOXsqbhzwdKT7kqQxO", 42 | firstName: "test", 43 | lastName: "test", 44 | }; 45 | 46 | await this.client.db("test").collection("users").insertOne(doc); 47 | console.timeEnd('createAdminUser'); 48 | return doc 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /db/models.d.ts: -------------------------------------------------------------------------------- 1 | import { type ObjectId } from "mongodb"; 2 | 3 | export type CreateDBUser = { 4 | _id: ObjectId; 5 | merchant: null; 6 | provider: string; 7 | role: string; 8 | email: string; 9 | password: string; 10 | firstName: string; 11 | lastName: string; 12 | }; 13 | -------------------------------------------------------------------------------- /env/index.ts: -------------------------------------------------------------------------------- 1 | import { cleanEnv, url } from 'envalid' 2 | 3 | export const env = cleanEnv(process.env, { 4 | FRONTEND_URL: url(), 5 | API_URL: url(), 6 | DB_CONNECTION_URI: url(), 7 | }) 8 | -------------------------------------------------------------------------------- /fixture/globalBeforeEach.ts: -------------------------------------------------------------------------------- 1 | import { shopTest } from "." 2 | 3 | export const globalBeforeEach = () => { 4 | shopTest.beforeEach(()=> { 5 | console.log('global before each') 6 | }) 7 | } -------------------------------------------------------------------------------- /fixture/index.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | import { randomUUID } from "crypto"; 3 | import { 4 | UserCreateRequest, 5 | UserCreatedResponse, 6 | } from "../api/models"; 7 | import { Application } from "../app"; 8 | import { DB } from "../db"; 9 | import { CreateDBUser } from "../_prepared/db/models"; 10 | 11 | interface UserContext { 12 | userModel: UserCreateRequest; 13 | createdUser: UserCreatedResponse; 14 | } 15 | 16 | export const shopTest = test.extend< 17 | // test level 18 | { 19 | app: Application; 20 | newAdminUser: CreateDBUser; 21 | newUser: UserContext; 22 | itemAddedInCart: { 23 | itemsInCart: { slug: string }[]; 24 | }; 25 | testOptions: { 26 | itemsToAddInCart: { slug: string; quantity?: number }[]; 27 | }; 28 | }, 29 | // worker level 30 | { db: DB } 31 | >({ 32 | testOptions: [ 33 | { 34 | itemsToAddInCart: [ 35 | { 36 | slug: "cherry-tomatoes", 37 | }, 38 | ], 39 | }, 40 | { 41 | option: true, 42 | }, 43 | ], 44 | 45 | app: async ({ page }, use) => { 46 | const app = new Application(page); 47 | await use(app); 48 | }, 49 | 50 | db: [ 51 | async ({}, use) => { 52 | const db = await DB.connect(); 53 | await use(db); 54 | await db.close(); 55 | }, 56 | { scope: "worker" }, 57 | ], 58 | 59 | newAdminUser: async ({ db, app }, use) => { 60 | const admin = await db.createAdminUser(); 61 | await app.headlessLogin({ email: admin.email, password: "xotabu4@gmail.com" }); 62 | await app.home.open(); 63 | await use({...admin, password: "xotabu4@gmail.com"}); 64 | }, 65 | 66 | newUser: async ({ app }, use) => { 67 | const userModel = { 68 | isSubscribed: false, 69 | email: `test+${randomUUID()}@test.com`, 70 | firstName: "test", 71 | lastName: "test", 72 | password: "xotabu4@gmail.com", 73 | }; 74 | 75 | const createdUser = await app.api.auth.register(userModel); 76 | await app.headlessLogin(userModel); 77 | await app.home.open(); 78 | 79 | await use({ userModel, createdUser }); 80 | }, 81 | 82 | itemAddedInCart: async ({ app, testOptions }, use) => { 83 | for (const item of testOptions.itemsToAddInCart) { 84 | await app.product.open(`/product/${item.slug}`); 85 | if (item.quantity !== undefined) { 86 | await app.product.changeQuantity(item.quantity); 87 | } 88 | await app.product.addToBag(); 89 | } 90 | await use({ itemsInCart: testOptions.itemsToAddInCart }); 91 | }, 92 | }); 93 | -------------------------------------------------------------------------------- /globalSetup.ts: -------------------------------------------------------------------------------- 1 | import { type FullConfig } from "@playwright/test"; 2 | 3 | async function globalSetup(config: FullConfig) { 4 | console.log("GlobalSetup!"); 5 | } 6 | 7 | export default globalSetup; 8 | -------------------------------------------------------------------------------- /misc/reporters/step.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | 3 | /** 4 | * Decorator that wraps a function with a Playwright test step. 5 | * Used for reporting purposes. 6 | * 7 | * @example 8 | ``` 9 | import { step } from './step_decorator'; 10 | class MyTestClass { 11 | @step('optional step name') 12 | async myTestFunction() { 13 | // Test code goes here 14 | } 15 | } 16 | ``` 17 | */ 18 | export function step(message?: string) { 19 | return function actualDecorator(target: (this: This, ...args: Args) => Promise, context: ClassMethodDecoratorContext Promise>) { 20 | function replacementMethod(this: any, ...args: Args) { 21 | const name = message ?? `${this.constructor.name}.${context.name as string}`; 22 | 23 | return test.step(name, async () => target.call(this, ...args), { box: true }); 24 | } 25 | 26 | return replacementMethod; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pwso_june_2024", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "pwso_june_2024", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@playwright/test": "1.44.1", 13 | "@types/node": "^20.14.2", 14 | "dotenv": "^16.4.5", 15 | "envalid": "^8.0.0", 16 | "mongodb": "^6.7.0" 17 | }, 18 | "devDependencies": { 19 | "monocart-reporter": "^2.5.0" 20 | } 21 | }, 22 | "node_modules/@mongodb-js/saslprep": { 23 | "version": "1.1.7", 24 | "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.7.tgz", 25 | "integrity": "sha512-dCHW/oEX0KJ4NjDULBo3JiOaK5+6axtpBbS+ao2ZInoAL9/YRQLhXzSNAFz7hP4nzLkIqsfYAK/PDE3+XHny0Q==", 26 | "dependencies": { 27 | "sparse-bitfield": "^3.0.3" 28 | } 29 | }, 30 | "node_modules/@playwright/test": { 31 | "version": "1.44.1", 32 | "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz", 33 | "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==", 34 | "dependencies": { 35 | "playwright": "1.44.1" 36 | }, 37 | "bin": { 38 | "playwright": "cli.js" 39 | }, 40 | "engines": { 41 | "node": ">=16" 42 | } 43 | }, 44 | "node_modules/@types/node": { 45 | "version": "20.14.2", 46 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", 47 | "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", 48 | "dependencies": { 49 | "undici-types": "~5.26.4" 50 | } 51 | }, 52 | "node_modules/@types/webidl-conversions": { 53 | "version": "7.0.3", 54 | "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", 55 | "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" 56 | }, 57 | "node_modules/@types/whatwg-url": { 58 | "version": "11.0.5", 59 | "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", 60 | "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", 61 | "dependencies": { 62 | "@types/webidl-conversions": "*" 63 | } 64 | }, 65 | "node_modules/accepts": { 66 | "version": "1.3.8", 67 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 68 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 69 | "dev": true, 70 | "dependencies": { 71 | "mime-types": "~2.1.34", 72 | "negotiator": "0.6.3" 73 | }, 74 | "engines": { 75 | "node": ">= 0.6" 76 | } 77 | }, 78 | "node_modules/bson": { 79 | "version": "6.7.0", 80 | "resolved": "https://registry.npmjs.org/bson/-/bson-6.7.0.tgz", 81 | "integrity": "sha512-w2IquM5mYzYZv6rs3uN2DZTOBe2a0zXLj53TGDqwF4l6Sz/XsISrisXOJihArF9+BZ6Cq/GjVht7Sjfmri7ytQ==", 82 | "engines": { 83 | "node": ">=16.20.1" 84 | } 85 | }, 86 | "node_modules/cache-content-type": { 87 | "version": "1.0.1", 88 | "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", 89 | "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", 90 | "dev": true, 91 | "dependencies": { 92 | "mime-types": "^2.1.18", 93 | "ylru": "^1.2.0" 94 | }, 95 | "engines": { 96 | "node": ">= 6.0.0" 97 | } 98 | }, 99 | "node_modules/co": { 100 | "version": "4.6.0", 101 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 102 | "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", 103 | "dev": true, 104 | "engines": { 105 | "iojs": ">= 1.0.0", 106 | "node": ">= 0.12.0" 107 | } 108 | }, 109 | "node_modules/console-grid": { 110 | "version": "2.2.2", 111 | "resolved": "https://registry.npmjs.org/console-grid/-/console-grid-2.2.2.tgz", 112 | "integrity": "sha512-ohlgXexdDTKLNsZz7DSJuCAwmRc8omSS61txOk39W3NOthgKGr1a1jJpZ5BCQe4PlrwMw01OvPQ1Bl3G7Y/uFg==", 113 | "dev": true 114 | }, 115 | "node_modules/content-disposition": { 116 | "version": "0.5.4", 117 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 118 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 119 | "dev": true, 120 | "dependencies": { 121 | "safe-buffer": "5.2.1" 122 | }, 123 | "engines": { 124 | "node": ">= 0.6" 125 | } 126 | }, 127 | "node_modules/content-type": { 128 | "version": "1.0.5", 129 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 130 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 131 | "dev": true, 132 | "engines": { 133 | "node": ">= 0.6" 134 | } 135 | }, 136 | "node_modules/cookies": { 137 | "version": "0.9.1", 138 | "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", 139 | "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", 140 | "dev": true, 141 | "dependencies": { 142 | "depd": "~2.0.0", 143 | "keygrip": "~1.1.0" 144 | }, 145 | "engines": { 146 | "node": ">= 0.8" 147 | } 148 | }, 149 | "node_modules/debug": { 150 | "version": "4.3.5", 151 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", 152 | "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", 153 | "dev": true, 154 | "dependencies": { 155 | "ms": "2.1.2" 156 | }, 157 | "engines": { 158 | "node": ">=6.0" 159 | }, 160 | "peerDependenciesMeta": { 161 | "supports-color": { 162 | "optional": true 163 | } 164 | } 165 | }, 166 | "node_modules/deep-equal": { 167 | "version": "1.0.1", 168 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", 169 | "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", 170 | "dev": true 171 | }, 172 | "node_modules/delegates": { 173 | "version": "1.0.0", 174 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 175 | "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", 176 | "dev": true 177 | }, 178 | "node_modules/depd": { 179 | "version": "2.0.0", 180 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 181 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 182 | "dev": true, 183 | "engines": { 184 | "node": ">= 0.8" 185 | } 186 | }, 187 | "node_modules/destroy": { 188 | "version": "1.2.0", 189 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 190 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 191 | "dev": true, 192 | "engines": { 193 | "node": ">= 0.8", 194 | "npm": "1.2.8000 || >= 1.4.16" 195 | } 196 | }, 197 | "node_modules/dotenv": { 198 | "version": "16.4.5", 199 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", 200 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", 201 | "engines": { 202 | "node": ">=12" 203 | }, 204 | "funding": { 205 | "url": "https://dotenvx.com" 206 | } 207 | }, 208 | "node_modules/ee-first": { 209 | "version": "1.1.1", 210 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 211 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 212 | "dev": true 213 | }, 214 | "node_modules/eight-colors": { 215 | "version": "1.3.0", 216 | "resolved": "https://registry.npmjs.org/eight-colors/-/eight-colors-1.3.0.tgz", 217 | "integrity": "sha512-hVoK898cR71ADj7L1LZWaECLaSkzzPtqGXIaKv4K6Pzb72QgjLVsQaNI+ELDQQshzFvgp5xTPkaYkPGqw3YR+g==", 218 | "dev": true 219 | }, 220 | "node_modules/encodeurl": { 221 | "version": "1.0.2", 222 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 223 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 224 | "dev": true, 225 | "engines": { 226 | "node": ">= 0.8" 227 | } 228 | }, 229 | "node_modules/envalid": { 230 | "version": "8.0.0", 231 | "resolved": "https://registry.npmjs.org/envalid/-/envalid-8.0.0.tgz", 232 | "integrity": "sha512-PGeYJnJB5naN0ME6SH8nFcDj9HVbLpYIfg1p5lAyM9T4cH2lwtu2fLbozC/bq+HUUOIFxhX/LP0/GmlqPHT4tQ==", 233 | "dependencies": { 234 | "tslib": "2.6.2" 235 | }, 236 | "engines": { 237 | "node": ">=8.12" 238 | } 239 | }, 240 | "node_modules/escape-html": { 241 | "version": "1.0.3", 242 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 243 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 244 | "dev": true 245 | }, 246 | "node_modules/fresh": { 247 | "version": "0.5.2", 248 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 249 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 250 | "dev": true, 251 | "engines": { 252 | "node": ">= 0.6" 253 | } 254 | }, 255 | "node_modules/fsevents": { 256 | "version": "2.3.2", 257 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 258 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 259 | "hasInstallScript": true, 260 | "optional": true, 261 | "os": [ 262 | "darwin" 263 | ], 264 | "engines": { 265 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 266 | } 267 | }, 268 | "node_modules/has-flag": { 269 | "version": "4.0.0", 270 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 271 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 272 | "dev": true, 273 | "engines": { 274 | "node": ">=8" 275 | } 276 | }, 277 | "node_modules/has-symbols": { 278 | "version": "1.0.3", 279 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 280 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 281 | "dev": true, 282 | "engines": { 283 | "node": ">= 0.4" 284 | }, 285 | "funding": { 286 | "url": "https://github.com/sponsors/ljharb" 287 | } 288 | }, 289 | "node_modules/has-tostringtag": { 290 | "version": "1.0.2", 291 | "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", 292 | "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", 293 | "dev": true, 294 | "dependencies": { 295 | "has-symbols": "^1.0.3" 296 | }, 297 | "engines": { 298 | "node": ">= 0.4" 299 | }, 300 | "funding": { 301 | "url": "https://github.com/sponsors/ljharb" 302 | } 303 | }, 304 | "node_modules/html-escaper": { 305 | "version": "2.0.2", 306 | "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", 307 | "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", 308 | "dev": true 309 | }, 310 | "node_modules/http-assert": { 311 | "version": "1.5.0", 312 | "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", 313 | "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", 314 | "dev": true, 315 | "dependencies": { 316 | "deep-equal": "~1.0.1", 317 | "http-errors": "~1.8.0" 318 | }, 319 | "engines": { 320 | "node": ">= 0.8" 321 | } 322 | }, 323 | "node_modules/http-errors": { 324 | "version": "1.8.1", 325 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", 326 | "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", 327 | "dev": true, 328 | "dependencies": { 329 | "depd": "~1.1.2", 330 | "inherits": "2.0.4", 331 | "setprototypeof": "1.2.0", 332 | "statuses": ">= 1.5.0 < 2", 333 | "toidentifier": "1.0.1" 334 | }, 335 | "engines": { 336 | "node": ">= 0.6" 337 | } 338 | }, 339 | "node_modules/http-errors/node_modules/depd": { 340 | "version": "1.1.2", 341 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 342 | "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", 343 | "dev": true, 344 | "engines": { 345 | "node": ">= 0.6" 346 | } 347 | }, 348 | "node_modules/inherits": { 349 | "version": "2.0.4", 350 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 351 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 352 | "dev": true 353 | }, 354 | "node_modules/is-generator-function": { 355 | "version": "1.0.10", 356 | "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", 357 | "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", 358 | "dev": true, 359 | "dependencies": { 360 | "has-tostringtag": "^1.0.0" 361 | }, 362 | "engines": { 363 | "node": ">= 0.4" 364 | }, 365 | "funding": { 366 | "url": "https://github.com/sponsors/ljharb" 367 | } 368 | }, 369 | "node_modules/istanbul-lib-coverage": { 370 | "version": "3.2.2", 371 | "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", 372 | "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", 373 | "dev": true, 374 | "engines": { 375 | "node": ">=8" 376 | } 377 | }, 378 | "node_modules/istanbul-lib-report": { 379 | "version": "3.0.1", 380 | "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", 381 | "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", 382 | "dev": true, 383 | "dependencies": { 384 | "istanbul-lib-coverage": "^3.0.0", 385 | "make-dir": "^4.0.0", 386 | "supports-color": "^7.1.0" 387 | }, 388 | "engines": { 389 | "node": ">=10" 390 | } 391 | }, 392 | "node_modules/istanbul-reports": { 393 | "version": "3.1.7", 394 | "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", 395 | "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", 396 | "dev": true, 397 | "dependencies": { 398 | "html-escaper": "^2.0.0", 399 | "istanbul-lib-report": "^3.0.0" 400 | }, 401 | "engines": { 402 | "node": ">=8" 403 | } 404 | }, 405 | "node_modules/keygrip": { 406 | "version": "1.1.0", 407 | "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", 408 | "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", 409 | "dev": true, 410 | "dependencies": { 411 | "tsscmp": "1.0.6" 412 | }, 413 | "engines": { 414 | "node": ">= 0.6" 415 | } 416 | }, 417 | "node_modules/koa": { 418 | "version": "2.15.3", 419 | "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz", 420 | "integrity": "sha512-j/8tY9j5t+GVMLeioLaxweJiKUayFhlGqNTzf2ZGwL0ZCQijd2RLHK0SLW5Tsko8YyyqCZC2cojIb0/s62qTAg==", 421 | "dev": true, 422 | "dependencies": { 423 | "accepts": "^1.3.5", 424 | "cache-content-type": "^1.0.0", 425 | "content-disposition": "~0.5.2", 426 | "content-type": "^1.0.4", 427 | "cookies": "~0.9.0", 428 | "debug": "^4.3.2", 429 | "delegates": "^1.0.0", 430 | "depd": "^2.0.0", 431 | "destroy": "^1.0.4", 432 | "encodeurl": "^1.0.2", 433 | "escape-html": "^1.0.3", 434 | "fresh": "~0.5.2", 435 | "http-assert": "^1.3.0", 436 | "http-errors": "^1.6.3", 437 | "is-generator-function": "^1.0.7", 438 | "koa-compose": "^4.1.0", 439 | "koa-convert": "^2.0.0", 440 | "on-finished": "^2.3.0", 441 | "only": "~0.0.2", 442 | "parseurl": "^1.3.2", 443 | "statuses": "^1.5.0", 444 | "type-is": "^1.6.16", 445 | "vary": "^1.1.2" 446 | }, 447 | "engines": { 448 | "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" 449 | } 450 | }, 451 | "node_modules/koa-compose": { 452 | "version": "4.1.0", 453 | "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", 454 | "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", 455 | "dev": true 456 | }, 457 | "node_modules/koa-convert": { 458 | "version": "2.0.0", 459 | "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", 460 | "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", 461 | "dev": true, 462 | "dependencies": { 463 | "co": "^4.6.0", 464 | "koa-compose": "^4.1.0" 465 | }, 466 | "engines": { 467 | "node": ">= 10" 468 | } 469 | }, 470 | "node_modules/koa-static-resolver": { 471 | "version": "1.0.6", 472 | "resolved": "https://registry.npmjs.org/koa-static-resolver/-/koa-static-resolver-1.0.6.tgz", 473 | "integrity": "sha512-ZX5RshSzH8nFn05/vUNQzqw32nEigsPa67AVUr6ZuQxuGdnCcTLcdgr4C81+YbJjpgqKHfacMBd7NmJIbj7fXw==", 474 | "dev": true 475 | }, 476 | "node_modules/lz-utils": { 477 | "version": "2.0.2", 478 | "resolved": "https://registry.npmjs.org/lz-utils/-/lz-utils-2.0.2.tgz", 479 | "integrity": "sha512-i1PJN4hNEevkrvLMqNWCCac1BcB5SRaghywG7HVzWOyVkFOasLCG19ND1sY1F/ZEsM6SnGtoXyBWnmfqOM5r6g==", 480 | "dev": true 481 | }, 482 | "node_modules/make-dir": { 483 | "version": "4.0.0", 484 | "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", 485 | "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", 486 | "dev": true, 487 | "dependencies": { 488 | "semver": "^7.5.3" 489 | }, 490 | "engines": { 491 | "node": ">=10" 492 | }, 493 | "funding": { 494 | "url": "https://github.com/sponsors/sindresorhus" 495 | } 496 | }, 497 | "node_modules/media-typer": { 498 | "version": "0.3.0", 499 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 500 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 501 | "dev": true, 502 | "engines": { 503 | "node": ">= 0.6" 504 | } 505 | }, 506 | "node_modules/memory-pager": { 507 | "version": "1.5.0", 508 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", 509 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" 510 | }, 511 | "node_modules/mime-db": { 512 | "version": "1.52.0", 513 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 514 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 515 | "dev": true, 516 | "engines": { 517 | "node": ">= 0.6" 518 | } 519 | }, 520 | "node_modules/mime-types": { 521 | "version": "2.1.35", 522 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 523 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 524 | "dev": true, 525 | "dependencies": { 526 | "mime-db": "1.52.0" 527 | }, 528 | "engines": { 529 | "node": ">= 0.6" 530 | } 531 | }, 532 | "node_modules/mongodb": { 533 | "version": "6.7.0", 534 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.7.0.tgz", 535 | "integrity": "sha512-TMKyHdtMcO0fYBNORiYdmM25ijsHs+Njs963r4Tro4OQZzqYigAzYQouwWRg4OIaiLRUEGUh/1UAcH5lxdSLIA==", 536 | "dependencies": { 537 | "@mongodb-js/saslprep": "^1.1.5", 538 | "bson": "^6.7.0", 539 | "mongodb-connection-string-url": "^3.0.0" 540 | }, 541 | "engines": { 542 | "node": ">=16.20.1" 543 | }, 544 | "peerDependencies": { 545 | "@aws-sdk/credential-providers": "^3.188.0", 546 | "@mongodb-js/zstd": "^1.1.0", 547 | "gcp-metadata": "^5.2.0", 548 | "kerberos": "^2.0.1", 549 | "mongodb-client-encryption": ">=6.0.0 <7", 550 | "snappy": "^7.2.2", 551 | "socks": "^2.7.1" 552 | }, 553 | "peerDependenciesMeta": { 554 | "@aws-sdk/credential-providers": { 555 | "optional": true 556 | }, 557 | "@mongodb-js/zstd": { 558 | "optional": true 559 | }, 560 | "gcp-metadata": { 561 | "optional": true 562 | }, 563 | "kerberos": { 564 | "optional": true 565 | }, 566 | "mongodb-client-encryption": { 567 | "optional": true 568 | }, 569 | "snappy": { 570 | "optional": true 571 | }, 572 | "socks": { 573 | "optional": true 574 | } 575 | } 576 | }, 577 | "node_modules/mongodb-connection-string-url": { 578 | "version": "3.0.1", 579 | "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", 580 | "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", 581 | "dependencies": { 582 | "@types/whatwg-url": "^11.0.2", 583 | "whatwg-url": "^13.0.0" 584 | } 585 | }, 586 | "node_modules/monocart-code-viewer": { 587 | "version": "1.1.4", 588 | "resolved": "https://registry.npmjs.org/monocart-code-viewer/-/monocart-code-viewer-1.1.4.tgz", 589 | "integrity": "sha512-ehSe1lBG7D1VDVLjTkHV63J3zAgzyhlC9OaxOri7D0X4L5/EcZUOG5TEoMmYErL+YGSOQXghU9kSSAelwNnp1Q==", 590 | "dev": true 591 | }, 592 | "node_modules/monocart-coverage-reports": { 593 | "version": "2.8.5", 594 | "resolved": "https://registry.npmjs.org/monocart-coverage-reports/-/monocart-coverage-reports-2.8.5.tgz", 595 | "integrity": "sha512-IUt+0spPBKCrgDX+knp0yg1SqQBMrgV16DL3KTObvTwudpbwQRgO141tWvisfowbR849j9yCVDWAGaD8+56LeA==", 596 | "dev": true, 597 | "workspaces": [ 598 | "packages/*", 599 | "test" 600 | ], 601 | "dependencies": { 602 | "console-grid": "^2.2.2", 603 | "eight-colors": "^1.3.0", 604 | "istanbul-lib-coverage": "^3.2.2", 605 | "istanbul-lib-report": "^3.0.1", 606 | "istanbul-reports": "^3.1.7", 607 | "lz-utils": "^2.0.2", 608 | "monocart-code-viewer": "^1.1.4", 609 | "monocart-formatter": "^3.0.0", 610 | "monocart-locator": "^1.0.0", 611 | "turbogrid": "^3.2.0" 612 | }, 613 | "bin": { 614 | "mcr": "lib/cli.js" 615 | } 616 | }, 617 | "node_modules/monocart-formatter": { 618 | "version": "3.0.0", 619 | "resolved": "https://registry.npmjs.org/monocart-formatter/-/monocart-formatter-3.0.0.tgz", 620 | "integrity": "sha512-91OQpUb/9iDqvrblUv6ki11Jxi1d3Fp5u2jfVAPl3UdNp9TM+iBleLzXntUS51W0o+zoya3CJjZZ01z2XWn25g==", 621 | "dev": true, 622 | "workspaces": [ 623 | "packages/*" 624 | ] 625 | }, 626 | "node_modules/monocart-locator": { 627 | "version": "1.0.0", 628 | "resolved": "https://registry.npmjs.org/monocart-locator/-/monocart-locator-1.0.0.tgz", 629 | "integrity": "sha512-qIHJ7f99miF2HbVUWAFKR93SfgGYpFPUCQPmW9q1VXU9onxMUFJxhQDdG3HkEteogUbsKB7Gr5MRgjzcIxwTaQ==", 630 | "dev": true 631 | }, 632 | "node_modules/monocart-reporter": { 633 | "version": "2.5.0", 634 | "resolved": "https://registry.npmjs.org/monocart-reporter/-/monocart-reporter-2.5.0.tgz", 635 | "integrity": "sha512-atEEsAtBpUSYf+eahmRCAIzOZi7X03qD82qHeiuxrRWpevrODNBG38FamXB1+EWINx5Cs1YdDpzFnDk9tzXHSg==", 636 | "dev": true, 637 | "workspaces": [ 638 | "packages/*" 639 | ], 640 | "dependencies": { 641 | "console-grid": "^2.2.2", 642 | "eight-colors": "^1.3.0", 643 | "koa": "^2.15.3", 644 | "koa-static-resolver": "^1.0.6", 645 | "lz-utils": "^2.0.2", 646 | "monocart-coverage-reports": "^2.8.4", 647 | "monocart-formatter": "^3.0.0", 648 | "monocart-locator": "^1.0.0", 649 | "nodemailer": "^6.9.14", 650 | "turbogrid": "^3.2.0" 651 | }, 652 | "bin": { 653 | "monocart": "lib/cli.js" 654 | } 655 | }, 656 | "node_modules/ms": { 657 | "version": "2.1.2", 658 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 659 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 660 | "dev": true 661 | }, 662 | "node_modules/negotiator": { 663 | "version": "0.6.3", 664 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 665 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 666 | "dev": true, 667 | "engines": { 668 | "node": ">= 0.6" 669 | } 670 | }, 671 | "node_modules/nodemailer": { 672 | "version": "6.9.14", 673 | "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz", 674 | "integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==", 675 | "dev": true, 676 | "engines": { 677 | "node": ">=6.0.0" 678 | } 679 | }, 680 | "node_modules/on-finished": { 681 | "version": "2.4.1", 682 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 683 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 684 | "dev": true, 685 | "dependencies": { 686 | "ee-first": "1.1.1" 687 | }, 688 | "engines": { 689 | "node": ">= 0.8" 690 | } 691 | }, 692 | "node_modules/only": { 693 | "version": "0.0.2", 694 | "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", 695 | "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==", 696 | "dev": true 697 | }, 698 | "node_modules/parseurl": { 699 | "version": "1.3.3", 700 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 701 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 702 | "dev": true, 703 | "engines": { 704 | "node": ">= 0.8" 705 | } 706 | }, 707 | "node_modules/playwright": { 708 | "version": "1.44.1", 709 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", 710 | "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", 711 | "dependencies": { 712 | "playwright-core": "1.44.1" 713 | }, 714 | "bin": { 715 | "playwright": "cli.js" 716 | }, 717 | "engines": { 718 | "node": ">=16" 719 | }, 720 | "optionalDependencies": { 721 | "fsevents": "2.3.2" 722 | } 723 | }, 724 | "node_modules/playwright-core": { 725 | "version": "1.44.1", 726 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", 727 | "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", 728 | "bin": { 729 | "playwright-core": "cli.js" 730 | }, 731 | "engines": { 732 | "node": ">=16" 733 | } 734 | }, 735 | "node_modules/punycode": { 736 | "version": "2.3.1", 737 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 738 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 739 | "engines": { 740 | "node": ">=6" 741 | } 742 | }, 743 | "node_modules/safe-buffer": { 744 | "version": "5.2.1", 745 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 746 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 747 | "dev": true, 748 | "funding": [ 749 | { 750 | "type": "github", 751 | "url": "https://github.com/sponsors/feross" 752 | }, 753 | { 754 | "type": "patreon", 755 | "url": "https://www.patreon.com/feross" 756 | }, 757 | { 758 | "type": "consulting", 759 | "url": "https://feross.org/support" 760 | } 761 | ] 762 | }, 763 | "node_modules/semver": { 764 | "version": "7.6.2", 765 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", 766 | "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", 767 | "dev": true, 768 | "bin": { 769 | "semver": "bin/semver.js" 770 | }, 771 | "engines": { 772 | "node": ">=10" 773 | } 774 | }, 775 | "node_modules/setprototypeof": { 776 | "version": "1.2.0", 777 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 778 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 779 | "dev": true 780 | }, 781 | "node_modules/sparse-bitfield": { 782 | "version": "3.0.3", 783 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", 784 | "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", 785 | "dependencies": { 786 | "memory-pager": "^1.0.2" 787 | } 788 | }, 789 | "node_modules/statuses": { 790 | "version": "1.5.0", 791 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 792 | "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", 793 | "dev": true, 794 | "engines": { 795 | "node": ">= 0.6" 796 | } 797 | }, 798 | "node_modules/supports-color": { 799 | "version": "7.2.0", 800 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 801 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 802 | "dev": true, 803 | "dependencies": { 804 | "has-flag": "^4.0.0" 805 | }, 806 | "engines": { 807 | "node": ">=8" 808 | } 809 | }, 810 | "node_modules/toidentifier": { 811 | "version": "1.0.1", 812 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 813 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 814 | "dev": true, 815 | "engines": { 816 | "node": ">=0.6" 817 | } 818 | }, 819 | "node_modules/tr46": { 820 | "version": "4.1.1", 821 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", 822 | "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", 823 | "dependencies": { 824 | "punycode": "^2.3.0" 825 | }, 826 | "engines": { 827 | "node": ">=14" 828 | } 829 | }, 830 | "node_modules/tslib": { 831 | "version": "2.6.2", 832 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 833 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 834 | }, 835 | "node_modules/tsscmp": { 836 | "version": "1.0.6", 837 | "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", 838 | "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", 839 | "dev": true, 840 | "engines": { 841 | "node": ">=0.6.x" 842 | } 843 | }, 844 | "node_modules/turbogrid": { 845 | "version": "3.2.0", 846 | "resolved": "https://registry.npmjs.org/turbogrid/-/turbogrid-3.2.0.tgz", 847 | "integrity": "sha512-c+2qrCGWzoYpLlxtHgRJ4V5dDRE9fUT7D9maxtdBCqJ0NzCdY+x7xF3/F6cG/+n3VIzKfIS+p9Z/0YMQPf6k/Q==", 848 | "dev": true 849 | }, 850 | "node_modules/type-is": { 851 | "version": "1.6.18", 852 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 853 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 854 | "dev": true, 855 | "dependencies": { 856 | "media-typer": "0.3.0", 857 | "mime-types": "~2.1.24" 858 | }, 859 | "engines": { 860 | "node": ">= 0.6" 861 | } 862 | }, 863 | "node_modules/undici-types": { 864 | "version": "5.26.5", 865 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 866 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" 867 | }, 868 | "node_modules/vary": { 869 | "version": "1.1.2", 870 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 871 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 872 | "dev": true, 873 | "engines": { 874 | "node": ">= 0.8" 875 | } 876 | }, 877 | "node_modules/webidl-conversions": { 878 | "version": "7.0.0", 879 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", 880 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", 881 | "engines": { 882 | "node": ">=12" 883 | } 884 | }, 885 | "node_modules/whatwg-url": { 886 | "version": "13.0.0", 887 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", 888 | "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", 889 | "dependencies": { 890 | "tr46": "^4.1.1", 891 | "webidl-conversions": "^7.0.0" 892 | }, 893 | "engines": { 894 | "node": ">=16" 895 | } 896 | }, 897 | "node_modules/ylru": { 898 | "version": "1.4.0", 899 | "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz", 900 | "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==", 901 | "dev": true, 902 | "engines": { 903 | "node": ">= 4.0.0" 904 | } 905 | } 906 | } 907 | } 908 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pwso_june_2024", 3 | "version": "1.0.0", 4 | "description": "Demo project for pwso_june_2024", 5 | "scripts": { 6 | "test+env": "node --env-file=.env node_modules/@playwright/test/cli.js test", 7 | "test": "playwright test" 8 | }, 9 | "keywords": [], 10 | "author": "Oleksandr Khotemskyi ", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@playwright/test": "1.44.1", 14 | "@types/node": "^20.14.2", 15 | "dotenv": "^16.4.5", 16 | "envalid": "^8.0.0", 17 | "mongodb": "^6.7.0" 18 | }, 19 | "devDependencies": { 20 | "monocart-reporter": "^2.5.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import { env } from "./env"; 3 | import { defineConfig, devices } from "@playwright/test"; 4 | import { Tag } from "./tags"; 5 | 6 | // const projectsAll = [ 7 | // { 8 | // name: "chromium", 9 | // use: { 10 | // ...devices["Desktop Chrome"], 11 | // }, 12 | // }, 13 | // { 14 | // name: "firefox", 15 | // use: { 16 | // ...devices['Desktop Firefox'], 17 | // }, 18 | // }, 19 | // { 20 | // name: "safari", 21 | // // grepInvert: new RegExp(Tag.NotForSafari), 22 | // use: { 23 | // ...devices['Desktop Safari'], 24 | // }, 25 | // }, 26 | // ]; 27 | 28 | // const projects: any = [ 29 | // ...projectsAll.map(p => { 30 | // p.name = `smoke-${p.name}`; 31 | // // @ts-ignore 32 | // p.grep = new RegExp(Tag.Smoke); 33 | 34 | // return p; 35 | // }), 36 | // ...projectsAll, 37 | // ] 38 | 39 | // console.log(projects) 40 | 41 | /** 42 | * See https://playwright.dev/docs/test-configuration. 43 | */ 44 | export default defineConfig({ 45 | testDir: "./tests", 46 | fullyParallel: true, 47 | workers: "90%", 48 | forbidOnly: !!process.env.CI, 49 | retries: process.env.CI ? 2 : 0, 50 | reporter: [ 51 | ["list", { 52 | printSteps: true, 53 | }], 54 | // [ 55 | // "monocart-reporter", 56 | // { 57 | // name: "My Test Report", 58 | // outputFile: "./test-results/report.html", 59 | // }, 60 | // ], 61 | ["html"] 62 | ], 63 | use: { 64 | baseURL: env.FRONTEND_URL, 65 | headless: process.env.CI ? true : false, 66 | trace: 'on', 67 | screenshot: 'only-on-failure', 68 | }, 69 | globalSetup: require.resolve("./globalSetup.ts"), 70 | projects: [ 71 | { 72 | name: "chromium", 73 | use: { 74 | ...devices["Desktop Chrome"], 75 | }, 76 | }, 77 | { 78 | name: "firefox", 79 | use: { 80 | ...devices["Desktop Firefox"], 81 | }, 82 | }, 83 | { 84 | name: "precondition-safari", 85 | testDir: "precondition", 86 | testMatch: "safari.precondition.ts", 87 | use: { 88 | ...devices["Desktop Safari"], 89 | }, 90 | }, 91 | { 92 | name: "teardown-safari", 93 | testDir: "precondition", 94 | testMatch: "safari.precondition.ts", 95 | use: { 96 | ...devices["Desktop Safari"], 97 | }, 98 | }, 99 | { 100 | dependencies: ["precondition-safari"], 101 | teardown: "teardown-safari", 102 | name: "safari", 103 | grepInvert: new RegExp(Tag.NotForSafari), 104 | use: { 105 | ...devices["Desktop Safari"], 106 | }, 107 | }, 108 | ], 109 | }); 110 | -------------------------------------------------------------------------------- /precondition/safari.precondition.ts: -------------------------------------------------------------------------------- 1 | import test from "@playwright/test"; 2 | 3 | test("safari prepare", () => { 4 | console.log("something before safari is prepared"); 5 | }); 6 | -------------------------------------------------------------------------------- /tags.ts: -------------------------------------------------------------------------------- 1 | export const Tag = { 2 | NotForSafari: '@NotForSafari', 3 | Smoke: '@Smoke' 4 | } -------------------------------------------------------------------------------- /tests/admin.test.ts: -------------------------------------------------------------------------------- 1 | import { shopTest } from "../fixture"; 2 | 3 | shopTest( 4 | "Admin should see additional items in dashboard", 5 | { 6 | annotation: [{ type: "BUG", description: "http://jira.com/CS-123" }], 7 | }, 8 | async ({ app, newAdminUser }) => { 9 | await app.accountDetails.open(); 10 | await app.accountDetails.expectMenuItemVisible("Merchant"); 11 | await app.accountDetails.expectMenuItemVisible("Users"); 12 | await app.accountDetails.expectMenuItemVisible("Orders"); 13 | await app.accountDetails.expectMenuItemVisible("Reviews"); 14 | await app.accountDetails.expectMenuItemVisible("WishList"); 15 | await app.accountDetails.expectMenuItemVisible("Support"); 16 | } 17 | ); 18 | -------------------------------------------------------------------------------- /tests/contact-us.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { skipIfWebkit } from "../utils/testSkipper"; 3 | import { Tag } from "../tags"; 4 | import { randomUUID } from "node:crypto"; 5 | import { shopTest } from "../fixture"; 6 | import { globalBeforeEach } from "../fixture/globalBeforeEach"; 7 | 8 | skipIfWebkit(); 9 | 10 | globalBeforeEach(); 11 | 12 | shopTest("can submit contact us form", async ({ app: { contactus }, page }) => { 13 | await contactus.open(); 14 | await contactus.submitContactUsForm({ 15 | email: `xotabu4+${randomUUID()}@gmail.com`, 16 | fullName: "test name", 17 | message: "test message", 18 | }); 19 | await expect( 20 | page.getByPlaceholder("Please Describe Your Message") 21 | ).toBeEmpty(); 22 | }); 23 | 24 | test( 25 | "User can submit contact us form", 26 | { 27 | annotation: [{ type: "JIRA", description: "http://jira.com/CS-1232132" }], 28 | tag: [Tag.NotForSafari, Tag.Smoke], 29 | }, 30 | async ({ page }) => { 31 | await page.goto("/"); 32 | await page.getByRole("link", { name: "Contact Us" }).first().click(); 33 | 34 | await page.getByPlaceholder("You Full Name").fill("test"); 35 | await page 36 | .getByPlaceholder("Your Email Address") 37 | .fill(`test+${Date.now()}@test.com`); 38 | await page 39 | .getByPlaceholder("Please Describe Your Message") 40 | .fill( 41 | "Hello there! I eat tomatoes every day. Why your cannery is so good?" 42 | ); 43 | 44 | shopTest 45 | .info() 46 | .annotations.push({ type: "VIDEO_SESSION_ID", description: "123123" }); 47 | await page.getByRole("button", { name: "Submit" }).click(); 48 | await expect( 49 | page.getByPlaceholder("Please Describe Your Message") 50 | ).toBeEmpty(); 51 | } 52 | ); 53 | -------------------------------------------------------------------------------- /tests/purchase.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { shopTest } from "../fixture"; 3 | 4 | const testData = [ 5 | { 6 | testName: "lazy-fixt: logged in user can buy a product", 7 | }, 8 | { 9 | testName: "lazy-fixt: logged in user can purchase multiple items", 10 | testOptions: { 11 | itemsToAddInCart: [ 12 | { slug: "cherry-tomatoes" }, 13 | { slug: "MARINATED_CUCUMBERS_NEZHIN_STYLE" }, 14 | ], 15 | }, 16 | }, 17 | { 18 | testName: "lazy-fixt: logged in user can purchase item with quantity 2", 19 | testOptions: { 20 | itemsToAddInCart: [ 21 | { 22 | slug: "cherry-tomatoes", 23 | quantity: 2, 24 | }, 25 | { slug: "MARINATED_CUCUMBERS_NEZHIN_STYLE" }, 26 | ], 27 | }, 28 | }, 29 | ]; 30 | 31 | for (const td of testData) { 32 | shopTest.describe(() => { 33 | shopTest.use({ 34 | testOptions: td.testOptions, 35 | }); 36 | 37 | shopTest(td.testName, async ({ app, newUser, itemAddedInCart }) => { 38 | await app.accountDetails.miniCart.placeOrder(); 39 | await app.confirmation.expectOrderPlaced(); 40 | }); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /tests/review.test.ts: -------------------------------------------------------------------------------- 1 | import { shopTest } from "../fixture"; 2 | import { globalBeforeEach } from "../fixture/globalBeforeEach"; 3 | 4 | globalBeforeEach(); 5 | 6 | shopTest( 7 | `user can post review for product`, 8 | async ({ app: { home, shop, product }, newUser }) => { 9 | await home.header.openShop(); 10 | await shop.openProductDetailsByName("CHERRY TOMATOES"); 11 | 12 | await product.reviewComponent.add({ 13 | title: "review title", 14 | comment: "review comment", 15 | stars: 4, 16 | }); 17 | await product.reviewComponent.expectReviewAdded(); 18 | } 19 | ); 20 | -------------------------------------------------------------------------------- /tests/shop.test.ts: -------------------------------------------------------------------------------- 1 | import { shopTest } from "../fixture"; 2 | 3 | shopTest.skip( 4 | "Shop response can be mocked", 5 | async ({ app, page, newAdminUser }) => { 6 | await page.route("**/api/product/list**", async (route) => { 7 | if (route.request().method() === "GET") { 8 | route.fulfill({ 9 | json: { 10 | products: [ 11 | { 12 | _id: "64e106888e01260021ea480c", 13 | taxable: false, 14 | isActive: true, 15 | brand: { 16 | name: "Nizhyn cannery", 17 | _id: "64bbbc91e9d7a367fcb1d462", 18 | isActive: true, 19 | }, 20 | sku: "CHERRY_TOMATOES", 21 | name: "CHERRY TOMATOES", 22 | description: 23 | "cherry tomatoes, salt, sugar, greens, acetic acid, garlic, spices", 24 | quantity: 98720, 25 | price: 95, 26 | created: "2023-08-19T18:14:32.255Z", 27 | slug: "cherry-tomatoes", 28 | __v: 0, 29 | isLiked: false, 30 | totalRatings: 2045, 31 | totalReviews: 410, 32 | averageRating: 4.987804878048781, 33 | }, 34 | { 35 | _id: "64e105d58e01260021ea480b", 36 | taxable: true, 37 | isActive: true, 38 | brand: { 39 | name: "Nizhyn cannery", 40 | _id: "64bbbc91e9d7a367fcb1d462", 41 | isActive: true, 42 | }, 43 | sku: "MARINATED_SWEET_PEPPER_SLICED", 44 | name: "MARINATED SWEET PEPPER (SLICED)", 45 | description: 46 | "sweet pepper, salt, sugar, acetic acid, garlic, spices", 47 | quantity: 0, 48 | price: 75, 49 | created: "2023-08-19T18:11:33.920Z", 50 | slug: "marinated-sweet-pepper-sliced", 51 | __v: 0, 52 | totalRatings: 3, 53 | totalReviews: 1, 54 | averageRating: 3, 55 | }, 56 | { 57 | _id: "64c4fdd89bb0c600219b4280", 58 | taxable: true, 59 | isActive: true, 60 | brand: { 61 | name: "Nizhyn cannery", 62 | _id: "64bbbc91e9d7a367fcb1d462", 63 | isActive: true, 64 | }, 65 | sku: "MARINATED_CUCUMBERS_NEZHIN_STYLE", 66 | name: "MARINATED CUCUMBERS NEZHIN STYLE", 67 | description: 68 | "COMPOSITION: cucumbers, greens, salt, sugar, acetic acid, garlic, spices", 69 | quantity: 999617, 70 | price: 120, 71 | created: "2023-07-29T11:54:00.642Z", 72 | slug: "MARINATED_CUCUMBERS_NEZHIN_STYLE", 73 | __v: 0, 74 | totalRatings: 0, 75 | totalReviews: 0, 76 | averageRating: 0, 77 | }, 78 | { 79 | _id: "64bbbc98e9d7a367fcb1d463", 80 | taxable: true, 81 | isActive: true, 82 | brand: { 83 | name: "Nizhyn cannery", 84 | _id: "64bbbc91e9d7a367fcb1d462", 85 | isActive: true, 86 | }, 87 | sku: "SWEET_PEPPER_PASTE", 88 | name: "SWEET PEPPER PASTE", 89 | description: 90 | "sweet pepper, carrot, tomato paste, sunflower oil, sugar, wheat flour, greens, spices", 91 | quantity: 999940, 92 | price: 100, 93 | created: "2023-07-22T11:25:12.046Z", 94 | slug: "SWEET_PEPPER_PASTE", 95 | __v: 0, 96 | totalRatings: 0, 97 | totalReviews: 0, 98 | averageRating: 0, 99 | }, 100 | { 101 | _id: "64e106888e01260021ea480c", 102 | taxable: false, 103 | isActive: true, 104 | brand: { 105 | name: "Nizhyn cannery", 106 | _id: "64bbbc91e9d7a367fcb1d462", 107 | isActive: true, 108 | }, 109 | sku: "CHERRY_TOMATOES", 110 | name: "CHERRY TOMATOES", 111 | description: 112 | "cherry tomatoes, salt, sugar, greens, acetic acid, garlic, spices", 113 | quantity: 98720, 114 | price: 95, 115 | created: "2023-08-19T18:14:32.255Z", 116 | slug: "cherry-tomatoes", 117 | __v: 0, 118 | isLiked: false, 119 | totalRatings: 2045, 120 | totalReviews: 410, 121 | averageRating: 4.987804878048781, 122 | }, 123 | { 124 | _id: "64e105d58e01260021ea480b", 125 | taxable: true, 126 | isActive: true, 127 | brand: { 128 | name: "Nizhyn cannery", 129 | _id: "64bbbc91e9d7a367fcb1d462", 130 | isActive: true, 131 | }, 132 | sku: "MARINATED_SWEET_PEPPER_SLICED", 133 | name: "MARINATED SWEET PEPPER (SLICED)", 134 | description: 135 | "sweet pepper, salt, sugar, acetic acid, garlic, spices", 136 | quantity: 0, 137 | price: 75, 138 | created: "2023-08-19T18:11:33.920Z", 139 | slug: "marinated-sweet-pepper-sliced", 140 | __v: 0, 141 | totalRatings: 3, 142 | totalReviews: 1, 143 | averageRating: 3, 144 | }, 145 | { 146 | _id: "64c4fdd89bb0c600219b4280", 147 | taxable: true, 148 | isActive: true, 149 | brand: { 150 | name: "Nizhyn cannery", 151 | _id: "64bbbc91e9d7a367fcb1d462", 152 | isActive: true, 153 | }, 154 | sku: "MARINATED_CUCUMBERS_NEZHIN_STYLE", 155 | name: "MARINATED CUCUMBERS NEZHIN STYLE", 156 | description: 157 | "COMPOSITION: cucumbers, greens, salt, sugar, acetic acid, garlic, spices", 158 | quantity: 999617, 159 | price: 120, 160 | created: "2023-07-29T11:54:00.642Z", 161 | slug: "MARINATED_CUCUMBERS_NEZHIN_STYLE", 162 | __v: 0, 163 | totalRatings: 0, 164 | totalReviews: 0, 165 | averageRating: 0, 166 | }, 167 | { 168 | _id: "64bbbc98e9d7a367fcb1d463", 169 | taxable: true, 170 | isActive: true, 171 | brand: { 172 | name: "Nizhyn cannery", 173 | _id: "64bbbc91e9d7a367fcb1d462", 174 | isActive: true, 175 | }, 176 | sku: "SWEET_PEPPER_PASTE", 177 | name: "SWEET PEPPER PASTE", 178 | description: 179 | "sweet pepper, carrot, tomato paste, sunflower oil, sugar, wheat flour, greens, spices", 180 | quantity: 999940, 181 | price: 100, 182 | created: "2023-07-22T11:25:12.046Z", 183 | slug: "SWEET_PEPPER_PASTE", 184 | __v: 0, 185 | totalRatings: 0, 186 | totalReviews: 0, 187 | averageRating: 0, 188 | }, 189 | ], 190 | totalPages: 1, 191 | currentPage: 1, 192 | count: 8, 193 | }, 194 | }); 195 | } else { 196 | await route.continue(); 197 | } 198 | }); 199 | 200 | await app.shop.open(); 201 | console.log("Shop page is opened"); 202 | } 203 | ); 204 | 205 | shopTest("Set cookies", async ({ app, page, context }) => { 206 | await context.addCookies([ 207 | { 208 | name: "test", 209 | value: "test", 210 | domain: "httpbin.org", 211 | path: "/", 212 | }, 213 | ]); 214 | 215 | await page.goto("https://httpbin.org/cookies"); 216 | console.log("test"); 217 | }); 218 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /utils/testSkipper.ts: -------------------------------------------------------------------------------- 1 | import test from "@playwright/test"; 2 | 3 | export const skipIfWebkit = (message = "Not supported for safari") => { 4 | test.skip(() => test.info().project.name.includes("safari"), message); 5 | }; 6 | 7 | export const skipIfMobile = (message = "Not supported for safari") => { 8 | test.skip(() => test.info().project.name.includes("mobile"), message); 9 | }; 10 | --------------------------------------------------------------------------------