electron-userland / spectron

DEPRECATED: 🔎 Test Electron apps using ChromeDriver
http://electronjs.org/spectron
MIT License
1.68k stars 229 forks source link

failing to successfully launch Spectron headless in Docker on Jenkins #1034

Open DrewHagenAtInfiniteCampus opened 2 years ago

DrewHagenAtInfiniteCampus commented 2 years ago

Hello! I'm looking for some help. I've been spinning my wheels at this for a while. I am newcomer to Electron app development and am starting to get very lower level.

I'm trying to set up a development phase CI pipeline for my team using Jenkins (CI platform my company uses) with Docker. Part of this includes a quality gate with our unit tests and also our Spectron E2E tests.

So I'm trying to run our existing Spectron suite automatically headless within a Docker container on Jenkins. For the base image doing this, I've followed the Provided Docker images in the Electron Build documentation and am building my Docker container from electronuserland/builder:wine-chrome.

Right now, I am trying to start simple by just testing start of the Spectron Application and launching a window

MY SPEC:

import { appUtil } from '../utils/app-utils';

fdescribe('Application launch', () => {
    jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000;

    const app = appUtil.app;
    console.log('/---------/ SPECTRON APP AFTER INIT /---------/')
    console.log(app);

    beforeAll(async () => {
        await appUtil.startApp();
        //await app.client.waitUntilWindowLoaded();
        console.log('/---------/ SPECTRON APP AFTER START /---------/')
        console.log(app);
    });

    afterAll(async () =>
        await appUtil.stopApp()
    );

    it('Should show a window', async () => {
        console.log('/---------/ SPECTRON APP CLIENT /---------/');
        console.log(app.client);
        console.log('/---------/ APP CLIENT STATUS /---------/');
        console.log(await app.client.status());
        console.log('/---------/ APP CLIENT DESIRED CAPABILITIES CHROME OPTIONS /---------/');
        console.log(await app.client.desiredCapabilities.chromeOptions);

        let count = await app.client.getWindowCount();
        expect(count).toBe(1);
    });
});

APPUTIL (where we have Spectron Application initialized):

import * as electron from 'electron';
import { Application } from 'spectron';
import { join } from 'path';

export class AppUtil {

    app = new Application({
        path: '' + electron,
        args: [join(__dirname, '../../../..')],
        chromeDriverLogPath: '../../chromeDriver.log',
        webdriverLogPath: '../../webdriver.log'
    });  

    public async startApp(): Promise<Application> {
        if (this.appIsRunning()) {
           const result = await this.waitForAppClosure();
           if(!result){
               throw new Error("Application failed to close within the allotted timeout");
           };
        };
        await this.app.start();
        this.app.browserWindow.focus();
        this.app.browserWindow.setAlwaysOnTop(true);

        return this.app;
    }

    public async stopApp(): Promise<Application> {
        if (this.appIsRunning()) {
            this.app.stop();
            this.app.mainProcess.abort()
        };
        //allow time for terminal windows to close
        await sleep(1000);
        return Promise.resolve(this.app);
    }

    private appIsRunning(): boolean {
        return !!(this.app && this.app.isRunning());
    }

    /**
     * Wait for the app to close for a max of 5 secs
     */
    private async waitForAppClosure(): Promise<boolean> {
        let count = 0;
        while(this.appIsRunning() && (count < 5)){
            await sleep(1000);
            count++;
        };

        return !this.appIsRunning();
    }
}

export const appUtil = new AppUtil();

/**
 * Sleeps for a specified amount of time
 */
 export async function sleep(time: number): Promise<void> {
    return new Promise(r => setTimeout(r, time));
}

When I run these, I get the following result in Jenkins output:

Failures:
1) Application launch Should show a window
  Message:
    Error: A session id is required for this command but wasn't found in the response payload
  Stack:
    error properties: Object({ type: 'NoSessionIdError' })
    Error: A session id is required for this command but wasn't found in the response payload
        at windowHandles() - application.js:274:17

Suite error: Application launch
  Message:
    Error: Client initialization failed after 10 attempts: RuntimeError Client initialization failed after 10 attempts:
  Stack:
    error properties: Object({ details: undefined, type: 'RuntimeError', seleniumStack: Object({ type: 'UnknownError', message: 'An unknown server-side error occurred while processing the command.', orgStatusMessage: 'unknown error: Chrome failed to start: exited abnormally.
      (unknown error: DevToolsActivePort file doesn't exist)
      (The process started from chrome location /root/repo/apps/myapp/node_modules/spectron/lib/launcher.js is no longer running, so ChromeDriver is assuming that Chrome has crashed.)
      (Driver info: chromedriver=80.0.3987.86 (4c2763461dfdcdda46516e2a9060c22a3c1a2525-refs/branch-heads/3987@{#796}),platform=Linux 3.10.0-1127.19.1.el7.x86_64 x86_64)' }) })
    Error: unknown error: Chrome failed to start: exited abnormally.
        at <Jasmine>
        at new RuntimeError (/root/repo/apps/myapp/node_modules/spectron/node_modules/webdriverio/build/lib/utils/ErrorHandler.js:143:12)
        at Request._callback (/root/repo/apps/myapp/node_modules/spectron/node_modules/webdriverio/build/lib/utils/RequestHandler.js:318:39)
        at Request.self.callback (/root/repo/apps/myapp/node_modules/request/request.js:185:22)
        at Request.emit (events.js:376:20)
        at Request.emit (domain.js:470:12)
        at Request.<anonymous> (/root/repo/apps/myapp/node_modules/request/request.js:1154:10)
        at Request.emit (events.js:376:20)
        at Request.emit (domain.js:470:12)
        at IncomingMessage.<anonymous> (/root/repo/apps/myapp/node_modules/request/request.js:1076:12)
        at Object.onceWrapper (events.js:482:28)

Ran 1 of 219 specs
1 spec, 2 failures
Finished in 4.578 seconds

It seems like the highlight here is DevToolsActivePort file doesn't exist. When I've googled around about this, many people discuss passing arguments to Chrome Driver.

Passing in Chrome Driver Args

I noticed Spectron's README the Application API shows chromeDriverArgs. So I've been passing an array of the arguments into the Application constructor like so:

app = new Application({
        path: '' + electron,
        args: [join(__dirname, '../../../..')],
        chromeDriverLogPath: '../../chromeDriver.log',
        webdriverLogPath: '../../webdriver.log',
        chromeDriverArgs: [
            '--no-sandbox',
            '--disable-dev-shm-usage',
            '--headless',
            'start-maximized',
            'disable-infobars',
            '--disable-gpu',
            '--window-size=1920,1080'
        ]
    });

I've played around with passing various combinations of these arguments:

'--no-sandbox',
'--disable-dev-shm-usage',
'--headless',
'--remote-debugging-port=9222',
'--user-data-dir=/home/ubuntujenkins/userData',
'start-maximized',
'disable-infobars',
'--disable-gpu',
'--window-size=1920,1080'

But still, no success. Only '--remote-debugging-port=9222' and '--user-data-dir=/home/ubuntujenkins/userData' makes the `DevToolsActivePort file doesn't exist error disappear, but then it's replaced by chrome unreachable.

Xvfb Configuration?

I'm also unsure if maybe this can relate to an Xvfb misconfiguration? I've tried:

  1. running Xvfb directly... adding sleep after like I've seen in some examples:
    
    stage('test e2e') {
    steps {
        setupXvfbVariablesAndE2ETest()
    }
    }

//...

void setupXvfbVariablesAndE2ETest() { // Similar to Travis CI example here: https://www.electronjs.org/docs/tutorial/testing-on-headless-ci runCommand('Xvfb :99 -screen 0 1920x1080x24 > /dev/null 2>&1 '

  1. try running Xvfb in a Docker entrypoint:
    
    FROM electronuserland/builder:wine-chrome

ENV DISPLAY=:99

ADD docker-entrypoint.sh /docker-entrypoint.sh RUN chmod +x /docker-entrypoint.sh ENTRYPOINT [ "/docker-entrypoint.sh" ]

with

!/usr/bin/env bash

echo "Starting Xvfb" Xvfb :99 -ac & sleep 5

echo "Executing command $@" export DISPLAY=:99

exec "$@"


Lastly, I noticed the [official Electron documentation mentions a plugin for Jenkins](https://www.electronjs.org/docs/tutorial/testing-on-headless-ci#jenkins).
Is it possible on Jenkins, installing and configuring that plugin is necessary for Xvfb to behave as expected, even on a Docker container?

None of what I'm trying seems to be working. If it appears obvious to anyone that I am missing something, I'd be delighted if you let me know! I can also post some logs if requested (I have to scrub proprietary data references first).
Thanks so much!