PhantomJS is no longer actively maintained by the original authors. Puppeteer is said to be a replacement supported and backed by the Google Chrome team, now.
You'll often end up having PhantomJS binaries connected via WebDriver to your testing framework, possibly using client/server especially if you want your test running with something else than Java. This means an overhead in terms of maintenance and performance, but still usually lighter than running a full browser (like Chrome, Firefox, IE).
It often happens when running on more then 5 (my measurement) JVM instances that the browser gets stuck and quits unexpectedly. This can be partially solved by running the instances one by one instead of parallel (this is a problem when testing Jenkins and Bamboo agents) but I don't believe this qualifies as a solution. The error is called UnreachableBrowserException.
This is an error which occurs with almost no reason, PhantomJS sometimes decides that it cannot click the element even though the element is intractable or enabled.
This happens if you have to scroll to see the element (and these are not pages that load elements with JavaScript) which is strange because PhantomJS should catch the whole page if it is not loaded explicitly with JavaScript. This problem partially goes away with re-sizing the browser, but that does not really qualify as a solution.
The error it raises is: ElementNotVisibleException.
Unlike the other options here, this is a real browser, just without the GUI parts. This means the quality of the test is much higher, and lets you do things like save to PDF or images.
Since it only tests via Chromium, it won't help you test inconsistencies between browsers like Edge or Safari. There are projects to get around this, but they aren't mature.
The integration of Node with the DOM in NW.js opens up a number of new options in how your headless testing workflow can be facilitated.
The distinct separation of JavaScript contexts, the introduction of a separate node context, and the ability to cross communicate, offers a lot of power and flexibility.
It's a lot faster than fully fletched browsers and a lot lighter. Partly because it really only focuses on headless loading of pages along with their JavaScript (not taking really care of rendering or more visual resources).
As its JavaScript and DOM engine are mostly "just good enough" and because by design it'll report all errors and stop there, many complex sites will not load properly through Zombie.js.
The SVG implementation stubs are very basic and most of the JavaScript DOM API is missing. This means that any non-trivial JavaScript code dealing with JavaScript will not break in this environment and needs to be stubbed.
Selenium supports a variety of languages including Java, Python, PHP, C#, Ruby and JavaScript ensuring that the tool is easily accessible to a wide variety of developers.
The open source community behind Selenium has a very large & engaged developer base. This ensures a stable support channel for the tool. In addition, various companies also provide support for Selenium. This active and multi-channel support provides a much more lucrative option for developers looking to implement Selenium in their workflow.
The WebDriver protocol is standardized by the World Wide Web Consortium which makes it easy for third-parties to get involved and contribute to Selenium.
WebDriver has been the foundation of many additional automation projects besides core selenium and has become the de-facto standard for UI automation.
Selenium, with its ability to interact with different testing frameworks, like NUnit or xUnit, makes it easy to get Selenium working with practically any Continuous Integration services.
If you are running multiple instances on some browser and you use xvfb to run instances on agents on Bamboo or Jenkins you will potentially have a problem with using ports.
The first instance you are starting takes on a port on the agent and then the second instance that would be run doesn't have a port to go to.
You can partially solve the problem by dynamically assigning the next available port to the new instance that comes to be executed.
This only works if we assume that all the instances start in different time intervals, but what you cannot know is which instance is going to get a port first if they start at the same time, then the instances will crash, those that started at the same time.