Home Reference Source

lib/browser/electron/electron_page.js

const fs = require('fs');
const path = require('path');
const {ipcMain} = require('electron');
const BrowserPage = require("../interface/browser_page");
const BrowserSocket = require("../interface/browser_socket");
const ElectronFinderExtention = require("./electron_finder_extention");
const {config} = require("../../utils/config");
const DateFormat = require('../../utils/date_format');
const Logger = require('../../utils/logger');
const uuidv4 = require('uuid').v4;

const TIMEOUT_MSEC = config.mocha.timeout;

class ElectronPage extends BrowserPage {
  constructor(window){
    super();
    this._window = window;
  }
  get window(){
    return this._window;
  }
  get nativeObject(){
    return this._window.browserWindow.webContents;
  }
  on(eventName, callback){
    this._addEventListener(eventName, callback, false)
  }
  once(eventName, callback){
    this._addEventListener(eventName, callback, true)
  }
  _addEventListener(eventName, callback, once = true){
    if(eventName in ElectronPage.EventMappings){
      this.nativeObject[once ? 'once' : 'on'](ElectronPage.EventMappings[eventName], callback)
    }else{
      super[once ? 'once' : 'on'](eventName, callback)
    }
  }
  removeAllListeners(eventName){
    if(eventName in ElectronPage.EventMappings){
      this.nativeObject.removeAllListeners(ElectronPage.EventMappings[eventName])
    }else{
      super.removeAllListeners(eventName);
    }
  }
  get socket(){
    this._socket = this._socket || new ElectronSocket();
    return this._socket;
  }
  get FinderClass(){
    return ElectronFinderExtention;
  }
  loadURL(url, options = {}){
    return this.nativeObject.loadURL(url)
  }
  /**
   * @return {string}
   */
  getURL(){
    return this.nativeObject.getURL();
  }
  /**
   * @return {Promise<object}
   */
  reload(){
    return this.nativeObject.reload();
  }
  executeJavaScript(code, ...args){
    return this.nativeObject.executeJavaScript(code, ...args);
  }
  openDevTools(){
    return this.nativeObject.openDevTools();
  }
  waitForDownload(action, options){
    return new Promise((resolve, reject)=>{
      let timeoutId = setTimeout(()=>{ reject(new Error('timeout')) }, TIMEOUT_MSEC);

      this.nativeObject.session.webRequest.onResponseStarted((details)=>{
        let key = Object.keys(details.responseHeaders).find(key => key.toLowerCase() == "content-disposition");
        if (!key) return;

        const disposition = details.responseHeaders[key].find(value => value.match(/[\s]*inline/) || value.match(/[\s]*attachment/));
        if (disposition.match(/[\s]*attachment/)) {

          this.nativeObject.session.once('will-download', (event, item, webContents) => {

            const result = { filename: item.getFilename() };
            // Set the save path, making Electron not to prompt a save dialog.
            const saveFilename = `${DateFormat.toTimepstampMsecString(new Date())}_${item.getFilename()}`;
            result.savedFilePath = path.resolve(path.join(config.fileDownloadDir, saveFilename));

            item.setSavePath(result.savedFilePath);
            item.on('updated', (event, state)=>{
              if(state === 'interrupted') reject(new Error('Download is interrupted'));
            })
            item.once('done', (event, state) => {
              clearTimeout(timeoutId);
              if(state == 'completed'){
                resolve(result)
              }else{
                reject(new Error(`Download failed: ${state}`))
              }
            })
          });
        }
        else if (disposition.match(/[\s]*inline/)) {

          const matched1 = disposition.match(/filename\*=(?:utf|UTF)-?8''(.*)$/);
          const matched2 = disposition.match(/filename=\"(.*)\"$/);
          if(matched1) {
            matched1[1] = decodeURIComponent(matched1[1]);
          }
          this.nativeObject.once('did-navigate', (event, url, httpResponseCode) => {
            clearTimeout(timeoutId);
            resolve({ filename: (matched1 ? matched1[1] : (matched2 ? matched2[1] : "no name")), saveedFilePaht: '' });
          });
        }
      });

      action();
    });
  }
  _waitForDialog(dialogType, action, message = undefined, isClickOK = false){
    return new Promise((resolve, reject)=>{
      const channel = 'dialog_' + uuidv4();

      let dialogCallback;

      dialogCallback = (arg, event)=>{
        if(arg == 'timeout'){
          this.socket.removeAllListeners(channel);
          return reject(new Error('timeout'));
        }

        if(message == arg || !message){
          this.socket.removeAllListeners(channel)
          event.returnValue = isClickOK;
          resolve(arg);
        }else{
          Logger.warn(`expected message is "${message}", but actual is "${arg}"`);
          event.returnValue = "keepWaiting";
        }
      };

      this.socket.on(channel, dialogCallback);

      return this.executeJavaScript(`(()=>{
        let __original_dialog_func = window.${dialogType};

        let timerId = setTimeout(()=>{
          window.browserSocket.send('${channel}', 'timeout');
          window.${dialogType} = __original_dialog_func;

        }, ${TIMEOUT_MSEC});

        let __new_dialog_func = function(message){
          window.${dialogType} = __original_dialog_func;
          var ret = browserSocket.sendSync('${channel}', message || 'hello');
          if(ret == 'keepWaiting'){
            window.${dialogType} = __new_dialog_func;
          }else{
            clearTimeout(timerId);
          }
          return ret;
        };
        window.${dialogType} = __new_dialog_func;
      })()`, true)
      .then(()=>{ return action && action() })
      .catch(reject)
    });
  }
  waitForConfirm(action, message = undefined, isClickOK = false){
    return this._waitForDialog('confirm', action, message, isClickOK);
  }
  waitForAlert(action, message = undefined){
    return this._waitForDialog('alert', action, message, true);
  }
  takeScreenshot(filename){
    try{
      if(!this.nativeObject.debugger.isAttached()){
        this.nativeObject.debugger.attach("1.1");
      }
    }catch(err){ throw new Error("Debugger attach failed : " + err) }

    return new Promise((resolve, reject)=>{
      this.nativeObject.debugger.sendCommand("Page.captureScreenshot", { format: "png" }, (err, res)=>{
        this.nativeObject.debugger.detach();
        err && err.code ? reject(err.toString()) : resolve(res.data);
      })
    })
    .then((data)=>{
      return new Promise((resolve, reject)=>{
        fs.writeFile(filename, new Buffer(data,'base64'), (err)=>{
          err ? reject(err) : resolve(filename)
        })
      })
    });
  }
}

class ElectronSocket extends BrowserSocket {
  on(channel, callback){
    return ipcMain.on(channel, (event, arg)=>{ callback(arg, event) });
  }
  once(channel, callback){
    return ipcMain.once(channel, (event, arg)=>{ callback(arg, event) });
  }
  removeAllListeners(channel){
    return ipcMain.removeAllListeners(channel);
  }
  removeListener(channel, callback){
    return ipcMain.removeListener(channel, callback);
  }
  send(channel, ...args){ throw new Error("Not Implemented") }
}

ElectronPage.EventMappings = {
  [BrowserPage.Events.Load]: 'did-finish-load',
  [BrowserPage.Events.LoadError]: 'did-fail-load',
}

module.exports = ElectronPage;