Giter Site home page Giter Site logo

minkelectrondriver's Introduction

Mink Electron Driver

Build Status (Linux / TravisCI) Build Status (Windows / AppVeyor) Minimum PHP Version License Coverage Packagist Dependency Status

Mink Electron Driver (replaces JsonWireProtocol and PhantomJS with Electron)

Electron is between 2 to 3 times faster than PhantomJS (source).

Table Of Contents

Features and Advantages

  • Fully-featured web browser
  • Quite fast, compared to alternatives
  • Built with modern components
  • Well supported (Electron receives frequent updates)
  • Easily understandable codebase (it's either PHP or JS)
  • Well tested (in addition to Mink driver tests, there are others)

Requirements

There a few things which are not taken care of automatically depending on your system.

  • Basics
    • PHP (5.5+) and Composer
    • Node.js (4+) and npm
  • Linux
    • If run headless (ie, without a desktop) you need to install xvfb
    • Some libraries are required, more details here
  • Windows

Installation

First make sure that the requirements above are met.

Next, simply install the driver in your Behat project via Composer:

composer require uuf6429/mink-electron-driver

How Does It Work?

       PHP + Mink Driver Interface                    Node.js + Electron API
 ________________  |  ______________________     _____________  |  _______________ 
|  Behat + Mink  |_v_|       Client         |___|    Server   |_V_|   Electron    |
| (Your Project) |---| (ElectronDriver.php) |---| (Server.js) |---| (Web Browser) |
'----------------'   '----------------------' ^ '-------------'   '---------------'
                                              |
                      DNode comm. over UDS (with inet sockets fallback)

Since one cannot easily control Node.js from PHP, a client-server approach was taken, with a fast and lightweight transport (unix domain sockets) protocol (dnode).

The driver on the PHP side simply tells the server what to do and it controls the Electron web browser.

The main reason why a client-server approach was taken is that Mink is synchronous by design.

minkelectrondriver's People

Contributors

uuf6429 avatar

Stargazers

 avatar

Watchers

 avatar  avatar  avatar

minkelectrondriver's Issues

Improve custom tests

  • Split big test file into smaller, more specific, test files
  • Separate test files from support files (Config, Logger, Listener etc)
  • Stop using external services and make use of an internal server
  • Maybe cover a few new test cases on the way

ChangeEventTest::testIssue178

This test is failing due to the fact that the value of text fields (input / textarea) is set directly, without triggering key events.

Sort out appveyor breakages

There seem to be various issues on appveyor, most notably:

  • ERR_CONNECTION_REFUSED error seems to come up very often
  • "No data found for resource with given identifier" race condition seem to happen frequently

Manage window names properly

Right now windowIdNameMap has a few issues:

  • to get the window name, we currently read window.name from js - unfortunately this does not contain the generated name (from #16) - the best solution would be to force the generated name into the browser side
  • it is an object hash and getWindowNames() is not reliable since hash object order is undefined (see #20)
  • ideally there should be a separate object to manage windows (see #20)

On a sidenote:

  • also as per this discussion, generated names should be electron_[window|frame]<id> for some consistency
  • closing a popup window causes an infinite loop waiting for the page to stop reloading (because we assume that onbeforeunload was triggered for navigation, not closing)

Find a way to return all cookies

Some people need to get all cookies for the current frame.

There are two ways of doing this:

  • Implement a non-standard method (eg; getCookies())
  • Similar to selenium API, return all cookies when getCookie() method is called without args
    Method signature would be too incompatible with Mink's, so decided to go with the other approach.

Screenshot only works for visible area

This seems to be a known limitation of Electron.

Potential solution:

  • create browser windows with some special settings:
    options.x = 0;
    options.y = 0;
    options.enableLargerThanScreen = true;
  • find content size (document.body.scroll<S>)
  • resize the window (content size + window.outer<S> - window.inner<S>)
  • take screenshot
  • resize back to original size

Some tentative code:

                Logger.debug('getScreenshot()');

                screenshotResponse = null;

                currWindow.webContents
                    .executeJavaScript('({' +
                        'cw: document.body.clientWidth,' +
                        'ch: document.body.clientHeight,' +
                        'pw: window.outerWidth - window.innerWidth,' +
                        'ph: window.outerHeight - window.innerHeight' +
                    '})', false, function (s) {
                        const scaleFactor = Electron.screen.getPrimaryDisplay().scaleFactor,
                            origSize = currWindow.getSize();

                        Logger.alert('Setting window size to %dx%d...', s.cw + s.pw, s.ch + s.ph);
                        currWindow.setSize(s.cw + s.pw, s.ch + s.ph, false);
                        Logger.alert('Window size is now: %j', currWindow.getSize());

                        const tryTakingScreenshot = function (tries) {
                            currWindow.capturePage(
                                /*{
                                    x: 0,
                                    y: 0,
                                    width: +s.cw * scaleFactor,
                                    height: +s.ch * scaleFactor
                                },*/
                                function (image) {
                                    const data = image.toPNG().toString('base64');

                                    if (data) {
                                        screenshotResponse = {'base64data': data};
                                        currWindow.setSize(origSize[0], origSize[1], false);
                                    } else if (tries > 0) {
                                        Logger.warn('Failed to take screen shot, trying again (try %d).', tries);
                                        setTimeout(function () {
                                            tryTakingScreenshot(tries - 1);
                                        }, 200);
                                    } else {
                                        screenshotResponse = {'error': 'Gave up trying to take screen shot after several tries.'};
                                        currWindow.setSize(origSize[0], origSize[1], false);
                                    }
                                }
                            );
                        };

                        tryTakingScreenshot(5);
                    });

                cb();

Also see: https://github.com/FWeinb/electron-screenshot-app/blob/6c165089797d30ad66a0b19398ca52446e0d3e88/index.js

WindowTest::testWindowMaximize

Not entirely clear why this test fails, but it seems to be related to headless runs.

Some possible solutions (if there's no obvious reason for failure):

  • either disable functionality when running in headless mode
  • or try faking it (eg, resize the window to fill the screen or something)

setValue might be used with non-string values

setValue() could be called with a non-string (and non-array) value. It needs to handle all types, especially stuff like numbers. A possible fix is to stringify unsupported values:

element.value = '';
if (value !== null) {                               // <------ in case we received a` null`
    // try inserting values via (synthetic) key events
    const keys = value.toString().split();          // <------ stringify everything else
    for (let i = 0; i < keys.length; i++) {
        this.syn.key(element, keys[i]);
    }
    // if key events failed setting value, set it directly
    if (element.value !== value) {
        element.value = value;
    }
}
// trigger change event
this.syn.trigger(element, 'change', {});
break;

Unrelated: fix spelling mistake in Window is corrently waiting for a response

Screenshot page resize race condition

In cases where the page content is too big, resizing the page is not instantaneous and causes a contorted, resized sceenshot instead. Maybe hook into resized event to know when window resizing is done?

Random failure in WindowTest::testWindow

1494611636.538327 INFO       PHPUnit Start Test Behat\Mink\Tests\Driver\Js\WindowTest::testWindow
1494611636.550000 INFO         Page is unloading.
1494611636.553000 INFO         Unlinked window named null from id "1" for "http://localhost:8002/issue225.html".
1494611636.783000 INFO         Linked window named "electron_window_1" with id "1" for "http://localhost:8002/window.html".
1494611636.810000 INFO         Page finished loading.
1494611637.040000 INFO         Creating window "popup_1" for url "http://localhost:8002/popup1.html".
1494611637.040000 INFO         Page is unloading.
1494611637.061000 INFO         Browser window created with id 2.
1494611637.300000 INFO         Linked window named "popup_1" with id "2" for "http://localhost:8002/popup1.html".
1494611637.344000 INFO         Page finished loading.
1494611637.538000 INFO         Creating window "popup_2" for url "http://localhost:8002/popup2.html".
1494611637.538000 INFO         Page is unloading.
1494611637.556000 INFO         Browser window created with id 3.
1494611637.762000 ERRR         Uncaught exception: Error: Window named "popup_2" not found (possibly name cache not in sync).
                                   at findWindowByName (/home/travis/build/uuf6429/MinkElectronDriver/src/Server/Server.js:224:23)
                                   at switchToWindow (/home/travis/build/uuf6429/MinkElectronDriver/src/Server/Server.js:552:59)
                                   at Proto.apply (/home/travis/build/uuf6429/MinkElectronDriver/node_modules/dnode-protocol/index.js:123:13)
                                   at Proto.handle (/home/travis/build/uuf6429/MinkElectronDriver/node_modules/dnode-protocol/index.js:86:18)
                                   at dnode.handle (/home/travis/build/uuf6429/MinkElectronDriver/node_modules/dnode/lib/dnode.js:140:21)
                                   at dnode.write (/home/travis/build/uuf6429/MinkElectronDriver/node_modules/dnode/lib/dnode.js:106:22)
                                   at Socket.ondata (_stream_readable.js:555:20)
                                   at emitOne (events.js:96:13)
                                   at Socket.emit (events.js:188:7)
                                   at readableAddChunk (_stream_readable.js:176:18)
1494611637.766000 INFO         Window "popup_1" (id 2) has been closed.
1494611637.770000 INFO         Window "" (id 3) has been closed.
1494611637.778000 INFO         Window "electron_window_1" (id 1) has been closed.
1494611637.830043 ERRR         PHPUnit Test Error uuf6429\DnodeSyncClient\Exception\IOException: Can't read response from remote
                               
                               /home/travis/build/uuf6429/MinkElectronDriver/vendor/uuf6429/dnode-php-sync-client/src/uuf6429/DnodeSyncClient/Connection.php:104
                               /home/travis/build/uuf6429/MinkElectronDriver/src/ElectronDriver.php:827
                               /home/travis/build/uuf6429/MinkElectronDriver/src/ElectronDriver.php:244
                               /home/travis/build/uuf6429/MinkElectronDriver/vendor/behat/mink/src/Session.php:304
                               /home/travis/build/uuf6429/MinkElectronDriver/vendor/mink/driver-testsuite/tests/Js/WindowTest.php:29

Preload.js should bind permanently to events

Unfortunately, using event properties is not reliable since the TA environment might decide to overwrite the property for some reason. Therefore, use the safer addEventListener alternative.

    window.addEventListener('error', function (error) {
        setExecutionError(error);
        return true;
    });

    window.addEventListener('beforeunload', function () {
        setWindowUnloading(true);
        setWindowIdName(electronWindow.id, null, location.href);
    });

Decouple server from driver

The main objective is to decrease installation requirements and load of the current package by making using of a compiled executable server.

Definite action points

  • Move electron server stuff to MinkElectronServer repository
  • Move npm bridge to require-dev in composer
  • Add MinkElectronServer to require-dev in composer
  • MinkElectronServer should be built by something like electron-packager (how to) and binaries stored in GitHub release

Unclear/risky action points

  • MinkElectronDriver should know where to find the server executable:
    • for dev, from vendor/MinkElectronServer/... (the current electron+js etc path)
    • for prod, from vendor/bin (the path where MinkElectronServerPrebuilt puts the exe) we may have to do some fun to find the OS-dependent exe name
  • CI tool runs for MinkElectronServer should publish executables to repository releases
  • A composer installer package that installs executables from MinkElectronServer releases depending on OS

Repository Contents Purpose
MinkElectronDriver Composer package with driver and extension used by Mink & Behat Used in Mink / Behat BDD projects
MinkElectronServer Composer package with server source code - Not used directly except for development
- CI/D stores builds in GitHub releases
- Requires Node.js+Electron+Electron-Packager
MinkElectronServerPrebuilt Composer binary installer Installs prebuilt MinkElectronServer binary depending on OS

Support switching to an iframe

This is currently difficult (if not impossible) because one cannot retrieve a WebView instance for an iframe.

More details: electron/electron#5115

In addition to the issue above, I'm not sufficiently convinced that \Behat\Mink\Tests\Driver\Basic\IFrameTest sufficiently tests this functionality - for instance, what if TA switches to an iframe and attempt to read the response body?

Create a FrameManager helper

This object should be managing all windows/iframes for Electron.

Purposes:

  • keep track of frame names (including generating them whenever required)
  • keep an up-to-date list of existing iframes/windows where each record has:
    • unique electron id
    • name (autogenerated whenever required)
    • debugger frameId maybe?
    • reference to the electron object
  • exposes helpers to:
    • get window name from id
    • set window name by id
    • get object reference from id
    • get list of window names (important: order by creation time)
  • maybe keep track of main browser window and current window/iframe

Disallow arbitrary file upload from js

In some cases we need to trigger a file upload from the page JS.

The JS running inside the browser is not usually allowed to perform this action (for obvious security reasons).

In our case, we need this functionality without opening a huge security hole. The solution is to have a whitelist of files that can be uploaded:

-> setValue(element, value)
-- allowedFiles = [value]
-- <- setFileValue(value)
-- if value in allowedFiles ...

Issue happens here: https://github.com/uuf6429/MinkElectronDriver/blob/master/src/Server/Server.js#L267

When output is disabled, flushServerOutput fails with an exception

When a null logger (or null) is passed to the driver constructor, output is disabled.
However, when we later attempt to flush the process output, a LogicException is thrown.
The solution is to flush output only if it's not disabled:

    protected function flushServerOutput()
    {
        if ($this->electronProcess && !$this->electronProcess->isOutputDisabled()) {
            $this->electronProcess->getOutput();
        }
    }

Autogenerate window name when none set

Whenever a window is created without a name, instead of assign an empty string as a name, generate one automatically (to avoid duplication issues).
The TA user will still be able to work with this behaviour by looking at the last item in getWindowNames() after creating a new window.

Correctly handle mouse event submission

Right now there is a general issue with dispatching mouse events; it's done asynchronously via RemoteDebug and the callback handler is called when the event is sent, not received and processed.

Hence in some cases, for example, we fail to properly detect page state (like navigation).

A possible solution is to bind a mouse handler to the affected element that sets the execution response, instead of relying on the callback handler.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.