{"id":15102665,"url":"https://github.com/andrelkj/zombieplus","last_synced_at":"2026-02-27T15:39:17.795Z","repository":{"id":252599057,"uuid":"840867229","full_name":"andrelkj/zombieplus","owner":"andrelkj","description":"Project to automate Zombie+ (a copy of Disney+ platform) using playwright framework to write tests, postgres SQL and Docker to store and mantain data. REST API for backend validation, and github actions as pipeline for continuos integration.","archived":false,"fork":false,"pushed_at":"2024-08-24T21:13:55.000Z","size":9816,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-03T13:59:02.511Z","etag":null,"topics":["api-rest","css","github-actions","html","javascript","playwright","postgresql","test-automation","testing"],"latest_commit_sha":null,"homepage":"","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/andrelkj.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-08-10T23:49:42.000Z","updated_at":"2024-08-24T21:13:58.000Z","dependencies_parsed_at":"2024-08-24T22:25:06.633Z","dependency_job_id":"8d3dfac1-c544-4f29-88ab-403f552317be","html_url":"https://github.com/andrelkj/zombieplus","commit_stats":null,"previous_names":["andrelkj/zombieplus"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/andrelkj/zombieplus","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrelkj%2Fzombieplus","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrelkj%2Fzombieplus/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrelkj%2Fzombieplus/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrelkj%2Fzombieplus/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andrelkj","download_url":"https://codeload.github.com/andrelkj/zombieplus/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrelkj%2Fzombieplus/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29902001,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-27T14:46:13.553Z","status":"ssl_error","status_checked_at":"2026-02-27T14:46:10.522Z","response_time":57,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-rest","css","github-actions","html","javascript","playwright","postgresql","test-automation","testing"],"created_at":"2024-09-25T19:04:11.976Z","updated_at":"2026-02-27T15:39:17.779Z","avatar_url":"https://github.com/andrelkj.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Playwright\n\n![poster](https://raw.githubusercontent.com/qaxperience/thumbnails/main/playwright-zombie.png)\n\n## 🤘 About\n\nRepository for the Zombie Plus system's automated test project, built during the Playwright Zombie Edition course! Playwright is an open-source tool developed by Microsoft that revolutionizes test automation in web systems, offering an effective and highly reliable approach.\n\n## 💻 Technologies\n\n- Node.js\n- Playwright\n- Javascript\n- Faker\n- PostgreSQL\n\n## 🤖 How to run\n\n1. Clone the repository, install dependencies with `npm install`\n2. Run tests in Headless mode with `npx playwright test`\n3. View the test report with `npx playwright show-report`\n\n## 📑 Documents\n\nFor this project two main documents were created:\n\n- [Zombie+ Roadmap](https://whimsical.com/zombie-roadmap-2G4AWdgEXavAxBQidogS4o) - that includes test cases for each application functionality\n- [Zombie+ Regression Tests](https://qaxperience.notion.site/Zombie-Regression-Tests-5d726cfee1484a2e9ee177b9467cb00c) - that contains a brief test plan for regression test execution\n\n## 📊 Database\n\nWe are using Postgres SQL as the main database that is running locally through Docker containers. In order to stablish connection to the postgres database we installed the pg library `npm i pg --save-dev`.\n\n## 📨 API\n\nTo manage requests from Zombie+ application we are using Insomnia.\n\n## ⚙️ Commands\n\n### Running tests\n\nTo run playwright tests we need to run `npx playwright test`.\n\n**Note:** playwright standard is to run tests in headless mode, you can run the assisted execution by adding --headed statement.\n\nSome common statements are:\n\n- `--headed` to run the test in assisted mode\n- `debug` to run the test execution step by step in debbuger mode\n- `ui` to open playwright gui page\n\n## 💡 Tricks\n\n### Temporary elements\n\nTo work with temporary components like toasts you can set a checkpoint or timeout to wait for the toast,\n\n```js\nawait page.getByText('seus dados conosco').click();\n```\n\nuse playwright ui `npx playwright test --ui` to look for it, and then use the following code to get page html content and look for the complete element you need.\n\n```js\n// handling toast and temporary elements\nconst content = await page.content();\nconsole.log(content);\n```\n\n**Note:** it might be required when working with temporary elements that are hard to interact with.\n\n### Validation of elements with same locator\n\nPlaywright handles validations against the same element locator or multielement locators as a forEach loop. It means that you can use arrays to declare what is expected and playwright will run it as a loop for each locator found to the given selector:\n\n```js\n// working with multielement locators\nawait expect(page.locator('.alert')).toHaveText([\n  'Campo obrigatório',\n  'Campo obrigatório',\n]);\n```\n\n### Regex\n\nIt is possible to use regex with locators and arguments or expected messages to create a more dynamic and reusable test structure.\n\nUsing regex to get only a piece of the url:\n\n```js\n  async isLoggedIn() {\n    await this.page.waitForLoadState('networkidle'); // wait all network traffic is finished\n    await expect(this.page).toHaveURL(/.*admin/);\n  }\n```\n\nUsing CSS selectors with regex `span[class$=alert]` to simplify element search:\n\n```js\n  async alertHaveText(text) {\n    const alert = this.page.locator('span[class$=alert]');\n    await expect(alert).toHaveText(text);\n  }\n```\n\n### Snake case\n\nJavascript standard convension is to use camelCase model, although we are using snake_case notation in the [MoviesPage](./tests/pages/MoviesPage.js) just to match this project database and REST API notation which can provide a better data handling and management.\n\n### Elements relation by label\n\nin HTML when there is a label for using the same value as the id of a child element it stablish a connection in between these elements:\n\n```html\n\u003clabel for=\"title\" class=\"sc-kAyceB cmzCot\"\n  \u003e\u003cspan class=\"field-name\"\u003eTitulo do filme\u003c/span\n  \u003e\u003cinput name=\"title\" id=\"title\" placeholder=\"Digite aqui\" value=\"\"\n/\u003e\u003c/label\u003e\n```\n\n**Note:** the for value of the label (title) is the same as the input id value (title) which imply that they are related to each other.\n\nand playwright allow you to use `getByLabel` function to look for the child element based into this connection:\n\n```js\n  async create(title, overview, company, release_year) {\n    await this.page.locator('a[href$=\"register\"]').click()\n    await this.page.getByLabel('Titulo do filme').fill(title)\n  }\n```\n\n### Database connection for reliable and reusable test data\n\nWhen working with tests data require data injection, a good practice is to stablish connections with the available database to manage the data available easly.\n\nBy using this approach you can generate independent, reusable, fresh data on every test execution, avoiding:\n\n- storage space consumption\n- flaky tests caused by mutable shared data\n- large loads of data to handle\n\nTo connect with the database we first need to provide the credentials:\n\n```js\nconst DbConfig = {\n  user: 'username', // username to access your\n  host: 'localhost', // since where running locally\n  database: 'dbname', // db name where you want to run the query against\n  password: 'password', // password to access your database\n  port: 5432,\n};\n```\n\n**Note:** you can get this information into your database properties.\n\nthen you stablesh the connection, handling errors with:\n\n```js\nexport async function executeSQL(sqlScript) {\n  const pool = new Pool(DbConfig);\n\n  try {\n    const client = await pool.connect();\n\n    const result = await client.query(sqlScript);\n    console.log(result.rows);\n  } catch (error) {\n    console.log('Erro ao executar o SQL ' + error);\n  }\n}\n```\n\n### Centralize Page Object Model (POM) imports\n\nWhen using POM it is required to import each page context so you can actually use their own functions, as the application grows this imports can get out of hand though and represent a lot of your code lines.\n\nOne way to handle it is by defining a [index.js](./tests/support/index.js) file in the support folder that will empower the existent page context from playwright with all of your imports:\n\n```js\nconst { test: base } = require('@playwright/test');\n\nconst { LandingPage } = require('../pages/LandingPage');\nconst { LoginPage } = require('../pages/LoginPage');\nconst { MoviesPage } = require('../pages/MoviesPage');\nconst { Toast } = require('../pages/Components');\n\n// create a updated page context that extends the actual page plus all POM imports injected\nconst test = base.extend({\n  page: async ({ page }, use) =\u003e {\n    await use({\n      ...page,\n      landing: new LandingPage(page),\n      login: new LoginPage(page),\n      movies: new MoviesPage(page),\n      toast: new Toast(page),\n    });\n  },\n});\n\nexport { test };\n```\n\nafter that you can replace the test import from playwright to your new context inside your test file:\n\n```js\n// standard test import from playwright\nconst { test: base } = require('@playwright/test');\n```\n\n```js\n// new import from index.js\nconst { test } = require('../support');\n```\n\n**Note:** Javascript understands index.js files as the main ones, it means that even though you don't expecify it whitin the import this file will be use.\n\nat the end you just need to update your test case to use the new format and remove all the old imports:\n\n```js\nconst { test } = require('../support');\n\nconst data = require('../support/fixtures/movies.json');\nconst { executeSQL } = require('../support/database');\n\ntest('deve poder cadastrar um novo filme', async ({ page }) =\u003e {\n  // é importante estar logado\n  const movie = data.create;\n  await executeSQL(`DELETE FROM public.movies WHERE title = '${movie.title}';`);\n\n  await page.login.visit();\n  await page.login.submit('admin@zombieplus.com', 'pwd123');\n  await page.movies.isLoggedIn();\n\n  await page.movies.create(\n    movie.title,\n    movie.overview,\n    movie.company,\n    movie.release_year\n  );\n  await page.toast.containText('Cadastro realizado com sucesso!');\n});\n```\n\n**Note:** although it helps to centralize all import into a single file you need to consider it's impact into the execution performance once you'll load the context of all pages everytime instead of only the page specific contexts as before.\n\n#### Update to import standard functions from page context\n\nThe approach used above to inject can be applied to centralize POM imports, but it causes standard playwright functions that comes with the page context to be lost.\n\nIn order to fix that we updated the [index.js](./tests/support/index.js) file to store the original page content into a variable and then injecting each individual page initialization to it:\n\n```js\nconst test = base.extend({\n  page: async ({ page }, use) =\u003e {\n    // store the original page context into a variable\n    const context = page;\n\n    // inject each individual page into the page context\n    context['landing'] = new LandingPage(page);\n    context['login'] = new LoginPage(page);\n    context['movies'] = new MoviesPage(page);\n    context['toast'] = new Toast(page);\n\n    await use(context);\n  },\n});\n```\n\n**Note:** at this point you should be able to use both native playwright and page specific functions.\n\n### Parallel execution issues\n\nPlaywright executes test cases and test suites in parallel by default:\n\n```js\n...\nmodule.exports = defineConfig({\n  ...\n  fullyParallel: true,\n  ...\n})\n```\n\nalthough when working with requests to the API of beforeAll hooks, this parallelization of execution can cause issues because this beforeAll will be called more than 1 time (it turns into a beforeEach basically), e.g. public.movie delete query that deletes movies in the middle of the test execution.\n\nto fix that we can define [playwright config file](./playwright.config.js) `fullyParallel: false,` which will still run suites in multitread, but will execute each individual test case at a time.\n\n#### Define specific deletions\n\nAnother alternative to keep test cases running in parallel would be to set one deletion query for each test case that would delete test case specific movies by title:\n\n```js\ntest('deve poder cadastrar um novo filme', async ({ page }) =\u003e {\n  const movie = data.create;\n  await executeSQL(`DELETE FROM public.movies WHERE title='${movie.title}'`);\n\n  await page.login.do('admin@zombieplus.com', 'pwd123', 'Admin');\n  await page.movies.create(movie);\n  await page.popup.haveText(\n    `O filme '${movie.title}' foi adicionado ao catálogo.`\n  );\n});\n```\n\n## ✅ Best practices\n\n### Test independence\n\nPlaywright is build in a way that all test cases are executed simultaneasly so dependent test cases often fail once there is no assurance that they'll be executed in the same order every time.\n\nWith that said dependent test cases as those two here:\n\n```js\n// register the movie\ntest('deve poder cadastrar um novo filme', async ({ page }) =\u003e {\n  const movie = data.create;\n  await executeSQL(`DELETE FROM public.movies WHERE title = '${movie.title}';`);\n  await page.login.do('admin@zombieplus.com', 'pwd123', 'Admin');\n  await page.movies.create(movie);\n  await page.toast.containText('Cadastro realizado com sucesso!');\n});\n\n// try to register the same movie again\ntest('não deve cadastrar quando o título é duplicado', async ({ page }) =\u003e {\n  const movie = data.create;\n\n  await page.login.do('admin@zombieplus.com', 'pwd123', 'Admin');\n  await page.movies.create(movie);\n  await page.toast.containText(\n    'Este conteúdo já encontra-se cadastrado no catálogo'\n  );\n});\n```\n\nAre mostly likely going to fail once the duplicated scenario can run first and cause to movie to be successfully registered and then the movie registration scenarion runs secondly receiving the duplicated error message.\n\n**Note:** other frameworks may allow you to run tests sequentially making it work, but it still a bad practice in automation as both tests might fail in case something happens in the process.\n\n### Page Object Model (POM)\n\nWhen using POM it is import to keep the rules and folder structure to the letter so you avoid management issues in the future.\n\nAs an example we have the `isLoggedIn` function that is part of the user login validation and was previously defined inside the LoginPage,\n\n```js\n  async isLoggedIn() {\n    await this.page.waitForLoadState('networkidle'); // wait all network traffic is finished\n    await expect(this.page).toHaveURL(/.*admin/);\n  }\n```\n\nalthough once you log in into the application you're no longer in the login page but in the movies page instead, so we created the [MoviesPage](./tests/pages/MoviesPage.js) file and moved the `isLoggedIn` function to it, and updated the [login spec](./tests/e2e/login.spec.js) to keep up with the new page:\n\n```js\ntest('deve logar como administrador', async ({ page }) =\u003e {\n  await loginPage.visit();\n  await loginPage.submit('admin@zombieplus.com', 'pwd123');\n  await moviesPage.isLoggedIn();\n});\n```\n\n### Custom Actions\n\nDifferently than the POM, custom actions model focus on specific and possible actions breaking free from the pages concept to allow a more flexible approach (e.g. [Movies.js](./tests/actions/Movies.js) file will contain all actions related to movies - create, delete, and so on).\n\nTo make this change we updated the pages folder name to actions, removed Page from the file names and classes, and then made the required changes within the code:\n\nChanges in [index.js](./tests/support/index.js) file to import the new classes and files:\n\n```js\nconst { test: base, expect } = require('@playwright/test');\n\nconst { Leads } = require('../actions/Leads');\nconst { Login } = require('../actions/Login');\nconst { Movies } = require('../actions/Movies');\nconst { Toast } = require('../actions/Components');\n\n// create a updated page context that extends the actual page plus all POM imports\nconst test = base.extend({\n  page: async ({ page }, use) =\u003e {\n    // store the original page context into a variable\n    const context = page;\n\n    // inject each individual page into the page context\n    context['leads'] = new Leads(page);\n    context['login'] = new Login(page);\n    context['movies'] = new Movies(page);\n    context['toast'] = new Toast(page);\n\n    await use(context);\n  },\n});\n\nexport { test, expect };\n```\n\nChanges in the [leads.spec.js](./tests/e2e/leads.spec.js) file to call **leads** instead of **landingPage**:\n\n```js\nconst { test, expect } = require('../support');\nconst { faker } = require('@faker-js/faker');\n\ntest('deve cadastrar um lead na fila de espera', async ({ page }) =\u003e {\n  const leadName = faker.person.fullName();\n  const leadEmail = faker.internet.email();\n\n  await page.leads.visit();\n  await page.leads.openLeadModal();\n  await page.leads.submitLeadForm(leadName, leadEmail);\n\n  const message =\n    'Agradecemos por compartilhar seus dados conosco. Em breve, nossa equipe entrará em contato!';\n  await page.toast.containText(message);\n});\n\ntest('não deve cadastrar quando o email já existe', async ({\n  page,\n  request,\n}) =\u003e {\n  const leadName = faker.person.fullName();\n  const leadEmail = faker.internet.email();\n\n  // send a new lead through API\n  const newLead = await request.post('http://localhost:3333/leads', {\n    data: {\n      name: leadName,\n      email: leadEmail,\n    },\n  });\n\n  // confirm status OK is returned (200-299)\n  expect(newLead.ok()).toBeTruthy();\n\n  await page.leads.visit();\n  await page.leads.openLeadModal();\n  await page.leads.submitLeadForm(leadName, leadEmail);\n\n  const message =\n    'O endereço de e-mail fornecido já está registrado em nossa fila de espera.';\n  await page.toast.containText(message);\n});\n\ntest('não deve cadastrar com email incorreto', async ({ page }) =\u003e {\n  await page.leads.visit();\n  await page.leads.openLeadModal();\n  await page.leads.submitLeadForm('Customer User', 'customer.test.com');\n  await page.leads.alertHaveText('Email incorreto');\n});\n\ntest('não deve cadastrar quando o nome não é preenchido', async ({ page }) =\u003e {\n  await page.leads.visit();\n  await page.leads.openLeadModal();\n  await page.leads.submitLeadForm('', 'customer@test.com');\n  await page.leads.alertHaveText('Campo obrigatório');\n});\n\ntest('não deve cadastrar quando o email não é preenchido', async ({ page }) =\u003e {\n  await page.leads.visit();\n  await page.leads.openLeadModal();\n  await page.leads.submitLeadForm('Customer User', '');\n  await page.leads.alertHaveText('Campo obrigatório');\n});\n\ntest('não deve cadastrar quando nenhum campo é preenchido', async ({\n  page,\n}) =\u003e {\n  await page.leads.visit();\n  await page.leads.openLeadModal();\n  await page.leads.submitLeadForm('', '');\n  await page.leads.alertHaveText(['Campo obrigatório', 'Campo obrigatório']);\n});\n```\n\n### Data handling\n\nIt is common to integrate your framework with the database to manage and handle the test data during it's execution, one good practice though is to refresh your data before your test execution:\n\n```js\ntest.beforeAll(async () =\u003e {\n  await executeSQL(`DELETE FROM public.movies WHERE`);\n});\n```\n\nGiven that by deleting it after the test execution there will be no data to work with or troubleshoot in case you need to.\n\n### Using API requests\n\nOnce your project starts to become more complex you'll face test cases that will require duplicated steps to setup the actual test scenario (e.g. duplicated movie title scenario):\n\n```js\ntest('não deve cadastrar quando o título é duplicado', async ({ page }) =\u003e {\n  const movie = data.duplicate;\n  await page.movies.create(movie); // setup step to create a movie\n  await page.movies.create(movie); // step to duplicate the movie\n  await page.toast.containText(\n    'Este conteúdo já encontra-se cadastrado no catálogo'\n  );\n});\n```\n\nIn this case we can use API requests to setup data in the backend:\n\n```js\nconst { expect } = require('@playwright/test');\n\nexport class Api {\n  constructor(request) {\n    this.request = request;\n    this.token = undefined;\n  }\n\n  // define setToken function to send a post request with the given JSON payload\n  async setToken() {\n    const response = await this.request.post('http://localhost:3333/sessions', {\n      data: {\n        email: 'admin@zombieplus.com',\n        password: 'pwd123',\n      },\n    });\n\n    // expect for any 2.. response status code\n    expect(response.ok()).toBeTruthy();\n\n    // transform response into JSON and store token value into a variable\n    const body = JSON.parse(await response.text());\n    this.token = 'Bearer ' + body.token;\n    console.log(this.token);\n  }\n\n  async postMovie(movie) {\n    // set the token to authenticate the user\n    await this.setToken();\n\n    // setup headers information\n    const response = await this.request.post('http://localhost:3333/movies', {\n      headers: {\n        Authorization: this.token,\n        ContentType: 'multipart/form-data',\n        Accept: 'application/json, text/plain, */*',\n      },\n      // fill the body payload\n      multipart: {\n        title: movie.title,\n        overview: movie.overview,\n        company_id: 'b7289a60-19a3-4d65-9ec4-8a852fe07695',\n        release_year: movie.release_year,\n        featured: movie.featured,\n      },\n    });\n\n    expect(response.ok()).toBeTruthy();\n  }\n}\n```\n\nand then we just need to import the [request file](./tests/support/api/index.js) to your [main import file](./tests/support/index.js) adding the new api context:\n\n```js\nconst { Api } = require('./api');\n\nconst test = base.extend({\n  ...\n  request: async ({ request }, use) =\u003e {\n    const context = request;\n\n    context['api'] = new Api(request);\n\n    await use(context);\n  },\n});\n```\n\nand finally call the function from within the test case:\n\n```js\ntest('não deve cadastrar quando o título é duplicado', async ({\n  page,\n  request,\n}) =\u003e {\n  const movie = data.duplicate;\n\n  // setup the movie in the backend\n  await request.api.postMovie(movie);\n\n  // try to register the movie again\n  await page.login.do('admin@zombieplus.com', 'pwd123', 'Admin');\n  await page.movies.create(movie);\n  await page.toast.containText(\n    'Este conteúdo já encontra-se cadastrado no catálogo'\n  );\n});\n```\n\n**Note:** it is a good practice to create a separate api folder to keep all your api requests.\n\n### Create selector based and key/parent values\n\nIt is pretty common to have duplicated classes for elements like lists, tables and others, and in this case using the class is not ideal once you might enconter duplication issues even if you delete the previous data.\n\nA better aproach is to use key values or parent elements to ensure a more assertive selector, and xpath is a good options for that so you could use `//td[text()=\"Guerra Mundial Z\"]/..//button`. But Plawright have it's own way to handle this locators with **getByRole** function:\n\n```js\nawait page.getByRole('row', { name: movie.title }).getByRole('button').click();\n```\n\n### Dotenv configuration file\n\nWhen you start working with different environments it might be intersting to create a `.env` file that will store all your environment dependencies.\n\nIn order to use it you:\n\n1. First create and store the data into the .env file:\n\n```conf\nBASE_API=http://apiendpoint:NNNN\nBASE_URL=http://webendpoint:NNNN\n\n# Database\nDB_HOST=dbhost\nDB_NAME=dbname\nDB_USER=dbuser\nDB_PASSWORD=dbpass\nDB_PORT=5432\n```\n\n2. Then, to manage your environments you can install dotenv library with `npm install dotenv`\n3. Import the dotenv configuration `require('dotenv').config()` to all the files you want to use them in\n4. Replace the data for the environment variables defined\n\n```js\n// database.js\nrequire('dotenv').config();\n...\n\nconst DbConfig = {\n  user: process.env.DB_USER,\n  host: process.env.DB_HOST,\n  database: process.env.DB_NAME,\n  password: process.env.DB_PASSWORD,\n  port: process.env.DB_PORT,\n};\n...\n```\n\n```js\n// api/index.js\nrequire('dotenv').config()\nconst { expect } = require('@playwright/test');\n\nexport class Api {\n  constructor(request) {\n    this.baseApi = process.env.BASE_API\n    ...\n  }\n\n  // define setToken function to send a post request with the given JSON payload\n  async setToken() {\n    const response = await this.request.post(this.baseApi + '/sessions', {\n      data: {\n        email: 'admin@zombieplus.com',\n        password: 'pwd123',\n      },\n    });\n    ...\n  }\n  ...\n}\n```\n\n### Reporting\n\nWe're using [Tesults](https://www.tesults.com/docs/playwright) as reporter for that:\n\n1. Install Tesults with `npm install playwright-tesults-reporter`\n2. Add the reporter to [playwright config file](./playwright.config.js)\n\n```js\n...\n  reporter: [\n    ['dot'],\n    ['playwright-tesults-reporter', { 'tesults-target': 'token' }],\n  ],\n...\n```\n\n3. Go to your account on [tesults](https://www.tesults.com/)\n4. Create a new project under configuration\n5. Link the project with the generated token\n\n```js\n  reporter: [\n    ['dot'],\n    ['playwright-tesults-reporter', { 'tesults-target': process.env.TOKEN }],\n  ],\n```\n\n**Note:** you'll need an account but there's a free version available and ideally you should store your token in a hidden file as a `.env` config file.\n\n---\n\nCurso disponível em https://qaxperience.com\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandrelkj%2Fzombieplus","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandrelkj%2Fzombieplus","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandrelkj%2Fzombieplus/lists"}