search
HomeWeb Front-endJS TutorialPlaywright and Chrome Browser Testing in Heroku

I’ve always loved watching my unit tests run (and pass). They’re fast, and passing tests give me the assurance that my individual pieces behave like they’re supposed to. Conversely, I often struggled to prioritize end-to-end tests for the browser because writing and running them was gruelingly slow.

Fortunately, the tools for end-to-end in-browser testing have gotten much better and faster over the years. And with a headless browser setup, I can run my browser tests as part of my CI.

Recently, I came across this Heroku blog post talking about automating in-browser testing with headless Chrome within Heroku CI. Heroku has a buildpack that installs headless Chrome, which you can invoke for your tests in the CI pipeline.

The example setup from the blog post was a React app tested with Puppeteer and Jest. That’s a great start … but what if I use Playwright instead of Puppeteer? Is it possible?

I decided to investigate. As it turns out — yes, you can do this with Playwright too! So, I captured the steps you would need to get Playwright tests running on the headless Chrome browser used in Heroku CI. In this post, I’ll walk you through the steps to get set up.

A Quick Word on Browser Automation for End-to-End Testing

End-to-end testing captures how users actually interact with your app in a browser, validating complete workflows. Playwright makes this process pretty seamless with testing in Chrome, Firefox, and Safari. Of course, running a full slate of browser tests in CI is pretty heavy, which is why headless mode helps.

The Chrome for Testing buildpack from Heroku installs Chrome on a Heroku app, so you can run your Playwright tests in Heroku CI with a really lightweight setup.

Introduction to the Application for Testing

Since I was just trying this out, I forked the GitHub repo that was originally referenced in the Heroku blog post. The application was a simple React app with a link, a text input, and a submit button. There were three tests:

  1. Verify that the link works and redirects to the right location.

  2. Verify that the text input properly displays the user input.

  3. Verify that submitting the form updates the text displayed on the page.

Pretty simple. Now, I just needed to change the code to use Playwright instead of Puppeteer and Jest. Oh, and I also wanted to use pnpm instead of npm. Here’s a link to my forked GitHub repo.

Modify the Code to Use Playwright

Let’s walk through the steps I took to modify the code. I started with my forked repo, identical to the heroku-examples repo.

Use pnpm

I wanted to use pnpm instead of npm. (Personal preference.) So, here’s what I did first:

~/project$ corepack enable pnpm


~/project$ corepack use pnpm@latest

Installing pnpm@9.12.3 in the project…
…
Progress: resolved 1444, reused 1441, downloaded 2, added 1444, done
…
Done in 14.4s

~/project$ rm package-lock.json


~/project$ pnpm install # just to show everything's good


Lockfile is up to date, resolution step is skipped
Already up to date
Done in 1.3s

Add Playwright to the project

Next, I removed Puppeteer and Jest, and I added Playwright.

~/project$ pnpm remove \
           babel-jest jest jest-puppeteer @testing-library/jest-dom

~/project$ $ pnpm create playwright
Getting started with writing end-to-end tests with Playwright:
Initializing project in '.'
✔ Do you want to use TypeScript or JavaScript? · JavaScript
✔ Where to put your end-to-end tests? · tests
✔ Add a GitHub Actions workflow? (y/N) · false
✔ Install Playwright browsers (can be done manually via 'pnpm exec playwright install')? (Y/n) · false
✔ Install Playwright operating system dependencies (requires sudo / root - can be done manually via 'sudo pnpm exec playwright install-deps')? (y/N) · false

Installing Playwright Test (pnpm add --save-dev @playwright/test)…
…
Installing Types (pnpm add --save-dev @types/node)…
…
Done in 2.7s
Writing playwright.config.js.
Writing tests/example.spec.js.
Writing tests-examples/demo-todo-app.spec.js.
Writing package.json.

I also removed the Jest configuration section from package.json.

Configure Playwright to use Chromium only

You can run your Playwright tests in Chrome, Firefox, and Safari. Since I was focused on Chrome, I removed the other browsers from the projects section of the generated playwright.config.js file:

  /* Configure projects for major browsers */
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },

//    {
//      name: 'firefox',
//      use: { ...devices['Desktop Firefox'] },
//    },
//
//    {
//      name: 'webkit',
//      use: { ...devices['Desktop Safari'] },
//    },


  ],
…

Exchange the Puppeteer test code for Playwright test code

The original code had a Puppeteer test file at src/tests/puppeteer.test.js. I moved that file to tests/playwright.spec.js. Then, I updated the test to use Playwright’s conventions, which mapped over quite cleanly. The new test file looked like this:

const ROOT_URL = 'http://localhost:8080';
const { test, expect } = require('@playwright/test');

const inputSelector = 'input[name="name"]';
const submitButtonSelector = 'button[type="submit"]';
const greetingSelector = 'h5#greeting';
const name = 'John Doe';

test.beforeEach(async ({ page }) => {
  await page.goto(ROOT_URL);
});

test.describe('Playwright link', () => {
  test('should navigate to Playwright documentation page', async ({ page }) => {
    await page.click('a[href="https://playwright.dev/"]');
    await expect(page.title()).resolves.toMatch('| Playwright');
  });
});

test.describe('Text input', () => {
  test('should display the entered text in the text input', async ({ page }) => {
    await page.fill(inputSelector, name);

    // Verify the input value
    const inputValue = await page.inputValue(inputSelector);
    expect(inputValue).toBe(name);
  });
});

test.describe('Form submission', () => {
  test('should display the "Hello, X" message after form submission', async ({ page }) => {
    const expectedGreeting = `Hello, ${name}.`;
    await page.fill(inputSelector, name);
    await page.click(submitButtonSelector);

    await page.waitForSelector(greetingSelector);
    const greetingText = await page.textContent(greetingSelector);
    expect(greetingText).toBe(expectedGreeting);
  });
});

Remove start-server-and-test, using Playwright’s webServer instead

To test my React app, I needed to spin it up (at http://localhost:8080) in a separate process first, and then I could run my tests. This would be the case whether I used Puppeteer or Playwright. With Puppeteer, the Heroku example used the start-server-and-test package. However, you can configure Playwright to spin up the app before running tests. This is pretty convenient!

I removed start-server-and-test from my project.

~/project$ pnpm remove start-server-and-test

In playwright.config.js, I uncommented the webServer section at the bottom, modifying it to look like this:

  /* Run your local dev server before starting the tests */
  webServer: {
     command: 'pnpm start',
     url: 'http://127.0.0.1:8080',
     reuseExistingServer: !process.env.CI,
  },

Then, I removed the test:ci script from the original package.json file. Instead, my test script looked like this:

  "scripts": {
    …
    "test": "playwright test --project=chromium --reporter list"
  },

Install Playwright browser on my local machine

Playwright installs the latest browser binaries to use for its tests. So, on my local machine, I needed Playwright to install its version of Chromium.

~/project$ pnpm playwright install chromium


Downloading Chromium 130.0.6723.31 (playwright build v1140)
from https://playwright.azureedge.net/builds/chromium/1140/chromium-linux.zip
164.5 MiB [====================] 100%

Note: The Chrome for Testing buildpack on Heroku installs the browser we’ll use for testing. We’ll set up our CI so that Playwright uses that browser instead of spending the time and resources installing its own.

Run tests locally

With that, I was all set. It was time to try out my tests locally.

~/project$ pnpm test

> playwright test --project=chromium --reporter list

Running 3 tests using 3 workers

  ✓  1 [chromium] > playwright.spec.js:21:3 > Text input > should display the entered text in the text input (911ms)
  ✘  2 [chromium] > playwright.spec.js:14:3 > Playwright link > should navigate to Playwright documentation page (5.2s)
  ✓  3 [chromium] > playwright.spec.js:31:3 > Form submission > should display the "Hello, X" message after form submission (959ms)

...
      - waiting for locator('a[href="https://playwright.dev/"]')


      13 | test.describe('Playwright link', () => {
      14 |   test('should navigate to Playwright documentation page', async ({ page }) => {
    > 15 |     await page.click('a[href="https://playwright.dev/"]');
         |                ^
      16 |     await expect(page.title()).resolves.toMatch('| Playwright');
      17 |   });
      18 | });

Oh! That’s right. I modified my test to expect the link in the app to take me to Playwright’s documentation instead of Puppeteer’s. I needed to update src/App.js at line 19:

<link href="https://playwright.dev/" rel="noopener">
  Playwright Documentation

Now, it was time to run the tests again…

~/project$ pnpm test

> playwright test --project=chromium --reporter list

Running 3 tests using 3 workers

  ✓  1 [chromium] > playwright.spec.js:21:3 > Text input > should display the entered text in the text input (1.1s)
  ✓  2 [chromium] > playwright.spec.js:14:3 > Playwright link > should navigate to Playwright documentation page (1.1s)
  ✓  3 [chromium] > playwright.spec.js:31:3 > Form submission > should display the "Hello, X" message after form submission (1.1s)

  3 passed (5.7s)

The tests passed! Next, it was time to get us onto Heroku CI.

Deploy to Heroku to Use CI Pipeline

I followed the instructions in the Heroku blog post to get my app set up in a Heroku CI pipeline.

Create a Heroku pipeline

In Heroku, I created a new pipeline and connected it to my forked GitHub repo.

Playwright and Chrome Browser Testing in Heroku

Next, I added my app to staging.

Playwright and Chrome Browser Testing in Heroku

Then, I went to the Tests tab and clicked Enable Heroku CI.

Playwright and Chrome Browser Testing in Heroku

Finally, I modified the app.json file to remove the test script which was set to call npm test:ci. I had already removed the test:ci script from my package.json file. The test script in package.json was now the one to use, and Heroku CI would look for that one by default.

My app.json file, which made sure to use the Chrome for Testing buildpack, looked like this:

~/project$ corepack enable pnpm


~/project$ corepack use pnpm@latest

Installing pnpm@9.12.3 in the project…
…
Progress: resolved 1444, reused 1441, downloaded 2, added 1444, done
…
Done in 14.4s

~/project$ rm package-lock.json


~/project$ pnpm install # just to show everything's good


Lockfile is up to date, resolution step is skipped
Already up to date
Done in 1.3s

Initial test run

I pushed my code to GitHub, and this triggered a test run in Heroku CI.

Playwright and Chrome Browser Testing in Heroku

The test run failed, but I wasn’t worried. I knew there would be some Playwright configuration to do.

Digging around in the test log, I found this:

~/project$ pnpm remove \
           babel-jest jest jest-puppeteer @testing-library/jest-dom

~/project$ $ pnpm create playwright
Getting started with writing end-to-end tests with Playwright:
Initializing project in '.'
✔ Do you want to use TypeScript or JavaScript? · JavaScript
✔ Where to put your end-to-end tests? · tests
✔ Add a GitHub Actions workflow? (y/N) · false
✔ Install Playwright browsers (can be done manually via 'pnpm exec playwright install')? (Y/n) · false
✔ Install Playwright operating system dependencies (requires sudo / root - can be done manually via 'sudo pnpm exec playwright install-deps')? (y/N) · false

Installing Playwright Test (pnpm add --save-dev @playwright/test)…
…
Installing Types (pnpm add --save-dev @types/node)…
…
Done in 2.7s
Writing playwright.config.js.
Writing tests/example.spec.js.
Writing tests-examples/demo-todo-app.spec.js.
Writing package.json.

Playwright was looking for the Chrome browser instance. I could install it with the playwright install chromium command as part of my CI test setup. But that would defeat the whole purpose of having the Chrome for Testing buildpack. Chrome was already installed; I just needed to point to it properly.

Looking back in my test setup log for Heroku, I found these lines:

  /* Configure projects for major browsers */
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },

//    {
//      name: 'firefox',
//      use: { ...devices['Desktop Firefox'] },
//    },
//
//    {
//      name: 'webkit',
//      use: { ...devices['Desktop Safari'] },
//    },


  ],
…

So, the browser I wanted to use was at /app/.chrome-for-testing/chrome-linux64/chrome. I would just need Playwright to look there for it.

Helping Playwright find the installed Chrome browser

Note: If you’re not interested in the nitty-gritty details here, you can skip this section and simply copy the full app.json lower down. This should give you what you need to get up and running with Playwright on Heroku CI.

In Playwright’s documentation, I found that you can set an environment variable that tells Playwright if you used a custom location for all of its browser installs. That env variable is PLAYWRIGHT_BROWSERS_PATH. I decided to start there.

In app.json, I set an env variable like this:

const ROOT_URL = 'http://localhost:8080';
const { test, expect } = require('@playwright/test');

const inputSelector = 'input[name="name"]';
const submitButtonSelector = 'button[type="submit"]';
const greetingSelector = 'h5#greeting';
const name = 'John Doe';

test.beforeEach(async ({ page }) => {
  await page.goto(ROOT_URL);
});

test.describe('Playwright link', () => {
  test('should navigate to Playwright documentation page', async ({ page }) => {
    await page.click('a[href="https://playwright.dev/"]');
    await expect(page.title()).resolves.toMatch('| Playwright');
  });
});

test.describe('Text input', () => {
  test('should display the entered text in the text input', async ({ page }) => {
    await page.fill(inputSelector, name);

    // Verify the input value
    const inputValue = await page.inputValue(inputSelector);
    expect(inputValue).toBe(name);
  });
});

test.describe('Form submission', () => {
  test('should display the "Hello, X" message after form submission', async ({ page }) => {
    const expectedGreeting = `Hello, ${name}.`;
    await page.fill(inputSelector, name);
    await page.click(submitButtonSelector);

    await page.waitForSelector(greetingSelector);
    const greetingText = await page.textContent(greetingSelector);
    expect(greetingText).toBe(expectedGreeting);
  });
});

I pushed my code to GitHub to see what would happen with my tests in CI.

As expected, it failed again. However, the log error showed this:

~/project$ corepack enable pnpm


~/project$ corepack use pnpm@latest

Installing pnpm@9.12.3 in the project…
…
Progress: resolved 1444, reused 1441, downloaded 2, added 1444, done
…
Done in 14.4s

~/project$ rm package-lock.json


~/project$ pnpm install # just to show everything's good


Lockfile is up to date, resolution step is skipped
Already up to date
Done in 1.3s

That got me pretty close. I decided that I would do this:

  • Create the folders needed for where Playwright expects the Chrome browser to be. That would be a command like:
~/project$ pnpm remove \
           babel-jest jest jest-puppeteer @testing-library/jest-dom

~/project$ $ pnpm create playwright
Getting started with writing end-to-end tests with Playwright:
Initializing project in '.'
✔ Do you want to use TypeScript or JavaScript? · JavaScript
✔ Where to put your end-to-end tests? · tests
✔ Add a GitHub Actions workflow? (y/N) · false
✔ Install Playwright browsers (can be done manually via 'pnpm exec playwright install')? (Y/n) · false
✔ Install Playwright operating system dependencies (requires sudo / root - can be done manually via 'sudo pnpm exec playwright install-deps')? (y/N) · false

Installing Playwright Test (pnpm add --save-dev @playwright/test)…
…
Installing Types (pnpm add --save-dev @types/node)…
…
Done in 2.7s
Writing playwright.config.js.
Writing tests/example.spec.js.
Writing tests-examples/demo-todo-app.spec.js.
Writing package.json.
  • Create a symlink in this folder to point to the Chrome binary installed by the Heroku buildpack. That would look something like this:
  /* Configure projects for major browsers */
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },

//    {
//      name: 'firefox',
//      use: { ...devices['Desktop Firefox'] },
//    },
//
//    {
//      name: 'webkit',
//      use: { ...devices['Desktop Safari'] },
//    },


  ],
…

However, I was concerned about whether this would be future-proof. Eventually, Playwright would use a new version of Chromium, and it won’t look in a chromium-1140 folder anymore. How could I figure out where Playwright would look?

That’s when I discovered you can do a browser installation dry run.

const ROOT_URL = 'http://localhost:8080';
const { test, expect } = require('@playwright/test');

const inputSelector = 'input[name="name"]';
const submitButtonSelector = 'button[type="submit"]';
const greetingSelector = 'h5#greeting';
const name = 'John Doe';

test.beforeEach(async ({ page }) => {
  await page.goto(ROOT_URL);
});

test.describe('Playwright link', () => {
  test('should navigate to Playwright documentation page', async ({ page }) => {
    await page.click('a[href="https://playwright.dev/"]');
    await expect(page.title()).resolves.toMatch('| Playwright');
  });
});

test.describe('Text input', () => {
  test('should display the entered text in the text input', async ({ page }) => {
    await page.fill(inputSelector, name);

    // Verify the input value
    const inputValue = await page.inputValue(inputSelector);
    expect(inputValue).toBe(name);
  });
});

test.describe('Form submission', () => {
  test('should display the "Hello, X" message after form submission', async ({ page }) => {
    const expectedGreeting = `Hello, ${name}.`;
    await page.fill(inputSelector, name);
    await page.click(submitButtonSelector);

    await page.waitForSelector(greetingSelector);
    const greetingText = await page.textContent(greetingSelector);
    expect(greetingText).toBe(expectedGreeting);
  });
});

That “Install location” line was crucial. And, if we set PLAYWRIGHT_BROWSERS_PATH, here is what we would see:

~/project$ pnpm remove start-server-and-test

That’s what I want. With a little awk magic, I did this:

  /* Run your local dev server before starting the tests */
  webServer: {
     command: 'pnpm start',
     url: 'http://127.0.0.1:8080',
     reuseExistingServer: !process.env.CI,
  },

With all that figured out, I simply needed to add a test-setup script to app.json. Because PLAYWRIGHT_BROWSERS_PATH is already set in env, my script would be a little simpler. This was my final app.json file:

  "scripts": {
    …
    "test": "playwright test --project=chromium --reporter list"
  },

I’ll briefly walk through what test-setup does:

  1. Accounting for PLAYWRIGHT_BROWSERS_PATH, uses playwright install -- dry-run with awk to determine the root folder where Playwright will look for the Chrome browser. Sets this as the value for the CHROMIUM_PATH variable.

  2. Creates a new folder (and any necessary parent folders) to CHROMIUM_PATH/chrome-linux, which is the actual folder where Playwright will look for the chrome binary.

  3. Creates a symlink in that folder, for chrome to point to the Heroku buildpack installation of Chrome (/app/.chrome-for-testing/chrome-linux64/chrome).

Run tests again

With my updated app.json file, Playwright should be able to use the Chrome installation from the buildpack. It was time to run the tests once again.

Playwright and Chrome Browser Testing in Heroku

Success!

The test-setup script ran as expected.

Playwright and Chrome Browser Testing in Heroku

Playwright was able to access the chrome binary and run the tests, which passed.

Playwright and Chrome Browser Testing in Heroku

Conclusion

End-to-end testing for my web applications is becoming less cumbersome, so I’m prioritizing it more and more. In recent days, that has meant using Playwright more too. It’s flexible and fast. And now that I’ve done the work (for me and for you!) to get it up and running with the Chrome for Testing buildpack in Heroku CI, I can start building up my browser automation test suites once again.

The code for this walkthrough is available in my GitHub repository.

Happy coding!

The above is the detailed content of Playwright and Chrome Browser Testing in Heroku. For more information, please follow other related articles on the PHP Chinese website!

Statement
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
From C/C   to JavaScript: How It All WorksFrom C/C to JavaScript: How It All WorksApr 14, 2025 am 12:05 AM

The shift from C/C to JavaScript requires adapting to dynamic typing, garbage collection and asynchronous programming. 1) C/C is a statically typed language that requires manual memory management, while JavaScript is dynamically typed and garbage collection is automatically processed. 2) C/C needs to be compiled into machine code, while JavaScript is an interpreted language. 3) JavaScript introduces concepts such as closures, prototype chains and Promise, which enhances flexibility and asynchronous programming capabilities.

JavaScript Engines: Comparing ImplementationsJavaScript Engines: Comparing ImplementationsApr 13, 2025 am 12:05 AM

Different JavaScript engines have different effects when parsing and executing JavaScript code, because the implementation principles and optimization strategies of each engine differ. 1. Lexical analysis: convert source code into lexical unit. 2. Grammar analysis: Generate an abstract syntax tree. 3. Optimization and compilation: Generate machine code through the JIT compiler. 4. Execute: Run the machine code. V8 engine optimizes through instant compilation and hidden class, SpiderMonkey uses a type inference system, resulting in different performance performance on the same code.

Beyond the Browser: JavaScript in the Real WorldBeyond the Browser: JavaScript in the Real WorldApr 12, 2025 am 12:06 AM

JavaScript's applications in the real world include server-side programming, mobile application development and Internet of Things control: 1. Server-side programming is realized through Node.js, suitable for high concurrent request processing. 2. Mobile application development is carried out through ReactNative and supports cross-platform deployment. 3. Used for IoT device control through Johnny-Five library, suitable for hardware interaction.

Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)Apr 11, 2025 am 08:23 AM

I built a functional multi-tenant SaaS application (an EdTech app) with your everyday tech tool and you can do the same. First, what’s a multi-tenant SaaS application? Multi-tenant SaaS applications let you serve multiple customers from a sing

How to Build a Multi-Tenant SaaS Application with Next.js (Frontend Integration)How to Build a Multi-Tenant SaaS Application with Next.js (Frontend Integration)Apr 11, 2025 am 08:22 AM

This article demonstrates frontend integration with a backend secured by Permit, building a functional EdTech SaaS application using Next.js. The frontend fetches user permissions to control UI visibility and ensures API requests adhere to role-base

JavaScript: Exploring the Versatility of a Web LanguageJavaScript: Exploring the Versatility of a Web LanguageApr 11, 2025 am 12:01 AM

JavaScript is the core language of modern web development and is widely used for its diversity and flexibility. 1) Front-end development: build dynamic web pages and single-page applications through DOM operations and modern frameworks (such as React, Vue.js, Angular). 2) Server-side development: Node.js uses a non-blocking I/O model to handle high concurrency and real-time applications. 3) Mobile and desktop application development: cross-platform development is realized through ReactNative and Electron to improve development efficiency.

The Evolution of JavaScript: Current Trends and Future ProspectsThe Evolution of JavaScript: Current Trends and Future ProspectsApr 10, 2025 am 09:33 AM

The latest trends in JavaScript include the rise of TypeScript, the popularity of modern frameworks and libraries, and the application of WebAssembly. Future prospects cover more powerful type systems, the development of server-side JavaScript, the expansion of artificial intelligence and machine learning, and the potential of IoT and edge computing.

Demystifying JavaScript: What It Does and Why It MattersDemystifying JavaScript: What It Does and Why It MattersApr 09, 2025 am 12:07 AM

JavaScript is the cornerstone of modern web development, and its main functions include event-driven programming, dynamic content generation and asynchronous programming. 1) Event-driven programming allows web pages to change dynamically according to user operations. 2) Dynamic content generation allows page content to be adjusted according to conditions. 3) Asynchronous programming ensures that the user interface is not blocked. JavaScript is widely used in web interaction, single-page application and server-side development, greatly improving the flexibility of user experience and cross-platform development.

See all articles

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

AI Hentai Generator

AI Hentai Generator

Generate AI Hentai for free.

Hot Article

R.E.P.O. Energy Crystals Explained and What They Do (Yellow Crystal)
4 weeks agoBy尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. Best Graphic Settings
3 weeks agoBy尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. How to Fix Audio if You Can't Hear Anyone
4 weeks agoBy尊渡假赌尊渡假赌尊渡假赌
WWE 2K25: How To Unlock Everything In MyRise
1 months agoBy尊渡假赌尊渡假赌尊渡假赌

Hot Tools

SecLists

SecLists

SecLists is the ultimate security tester's companion. It is a collection of various types of lists that are frequently used during security assessments, all in one place. SecLists helps make security testing more efficient and productive by conveniently providing all the lists a security tester might need. List types include usernames, passwords, URLs, fuzzing payloads, sensitive data patterns, web shells, and more. The tester can simply pull this repository onto a new test machine and he will have access to every type of list he needs.

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

Powerful PHP integrated development environment

Atom editor mac version download

Atom editor mac version download

The most popular open source editor

PhpStorm Mac version

PhpStorm Mac version

The latest (2018.2.1) professional PHP integrated development tool

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)