Commit da35b2e8 authored by Tom Jorquera's avatar Tom Jorquera
Browse files

Replace selenium by puppeteer

Big overhaul replacing selenium by puppeteer. This does not work for now due to
problems with SSL being a requirement for external endpoints.
parent fed80bf1
Pipeline #5293 failed with stage
in 10 seconds
......@@ -23,11 +23,9 @@
//
// It is the first file of the `controller` folder to be loaded in the client.
/* global robotController:true document angular easyrtc */
/* global robotController:true room document angular easyrtc */
/* exported robotController */
const room = arguments[0];
robotController = {
$scope: angular.element(document.body).scope().$root,
$injector: angular.element(document.body).injector(),
......@@ -45,7 +43,7 @@ robotController = {
},
getRemoteParticipants: () => {
const participants = easyrtc.getRoomOccupantsAsArray(room);
const participants = easyrtc.getRoomOccupantsAsArray(room); // Note that room is exposed globally
const res = [];
if (participants) {
for (let i = 0; i < participants.length; i++) {
......
......@@ -69,6 +69,9 @@ describe('client/controller', () => {
global.angular = angular;
global.easyrtc = easyRTCMock(['p1', 'p2', 'p3']);
// Room is supposed to be exposed globally
global.room = 'test';
/* eslint-disable import/no-unassigned-import */
require('./controller.js');
/* eslint-enable */
......
......@@ -25,41 +25,46 @@
robotLib.stt = function (config) {
return {
getTranscriptSocket: onSegment => {
const ws = new WebSocket(config.gstreamerURL + '/client/ws/speech?content-type=audio/x-matroska,,+rate=(int)48000,+channels=(int)1');
ws.onopen = function () {
console.info('ws to stt module open');
};
ws.onclose = function () {
console.info('ws to stt module closed');
};
ws.onerror = function (event) {
console.info('ws to stt module error: ' + event);
};
ws.onmessage = function (event) {
const hyp = JSON.parse(event.data);
if (hyp.status === 0) {
if (hyp.result !== undefined && hyp.result.final) {
const trans = ((hyp.result.hypotheses)[0]).transcript;
let start;
let end;
if (hyp['segment-start'] && hyp['segment-length']) {
start = JSON.parse(hyp['segment-start']);
end = parseFloat(hyp['segment-start']) + parseFloat(hyp['segment-length']);
} else {
const time = new Date().getTime() / 1000;
start = time;
end = time + 1;
}
try{
const ws = new WebSocket(config.gstreamerURL + '/client/ws/speech?content-type=audio/x-matroska,,+rate=(int)48000,+channels=(int)1');
ws.onopen = function () {
console.info('ws to stt module open');
};
ws.onclose = function () {
console.info('ws to stt module closed');
};
ws.onerror = function (event) {
console.info('ws to stt module error: ' + event);
};
ws.onmessage = function (event) {
const hyp = JSON.parse(event.data);
if (hyp.status === 0) {
if (hyp.result !== undefined && hyp.result.final) {
const trans = ((hyp.result.hypotheses)[0]).transcript;
let start;
let end;
if (hyp['segment-start'] && hyp['segment-length']) {
start = JSON.parse(hyp['segment-start']);
end = parseFloat(hyp['segment-start']) + parseFloat(hyp['segment-length']);
} else {
const time = new Date().getTime() / 1000;
start = time;
end = time + 1;
}
onSegment({
from: start,
until: end,
text: trans
});
}
onSegment({
from: start,
until: end,
text: trans
});
}
}
};
return ws;
} catch (e) {
console.error(e);
return null;
}
};
return ws;
}
};
};
......@@ -119,8 +119,12 @@ robot = {
},
stopRecordParticipant(easyrtcid) {
robot.participantsMediaRecorders[easyrtcid].stop();
robot.recordedParticipantsWS[easyrtcid].close();
if (robot.participantsMediaRecorders[easyrtcid]) {
robot.participantsMediaRecorders[easyrtcid].stop();
}
if (robot.recordedParticipantsWS[easyrtcid]) {
robot.recordedParticipantsWS[easyrtcid].close();
}
},
checkDisconnect() {
......@@ -156,9 +160,7 @@ robot = {
robotLib.archive = robotLib.archive(robot.clientConfig);
robotController.onAttendeePush = (e, data) => {
if (data.easyrtcid === robotController.getMyId()) {
robot.onConnection();
} else {
if (data.easyrtcid !== robotController.getMyId()) {
robot.recordParticipant(data.easyrtcid);
}
};
......@@ -171,9 +173,7 @@ robot = {
robotController.getWebRTCAdapter().addDisconnectCallback(() => {
robot.clearConnection();
});
},
onConnection: () => {
const recoStartRetry = () => {
if (robotLib.reco.start(robot.room)) {
clearInterval(robot.recoInterval);
......@@ -204,14 +204,13 @@ robot = {
robot.isDisconnected = true;
robotController.disconnect();
robot.clearConnection();
robot.notifyEndToServer();
return true;
},
// This function is set by the server
/* eslint-disable no-undef */
notifyEndToServer();
/* eslint-enable */
// This function will be overridden by a server callback
notifyEndToServer: () => {
console.error('`notifyEndToServer`: Server has not registered a callback!');
return true;
}
};
......
......@@ -82,6 +82,9 @@ describe('client/robot without init', () => {
})
};
// This is supposed to be set by the server
global.notifyEndToServer = () => {};
/* eslint-disable import/no-unassigned-import */
require('./robot.js');
/* eslint-enable */
......@@ -184,7 +187,6 @@ describe('client/robot', () => {
/* eslint-disable import/no-unassigned-import */
require('./robot.js');
global.robot.start();
global.robot.onConnection();
/* eslint-enable */
});
......
......@@ -3,23 +3,18 @@
"url": "http://hubl.in"
},
"runner": {
"driver" : {
"host": "selenium",
"port": 4444,
"desiredCapabilities": {
"browserName": "chrome",
"chromeOptions": {
"args" : [
"--disable-web-security",
"--user-data-dir=./tmp/chromium",
"--allow-running-insecure-content",
"--use-fake-device-for-media-stream",
"--use-file-for-fake-audio-capture=/opt/media/silence.wav",
"--use-file-for-fake-video-capture=/opt/media/logo.y4m",
"--use-fake-ui-for-media-stream"
]
}
}
"displayClientConsole": false,
"puppeteer": {
"headless": true,
"args" : [
"--disable-web-security",
"--user-data-dir=./tmp/chromium",
"--allow-running-insecure-content",
"--use-fake-device-for-media-stream",
"--use-file-for-fake-audio-capture=/opt/media/silence.wav",
"--use-file-for-fake-video-capture=/opt/media/logo.y4m",
"--use-fake-ui-for-media-stream"
]
}
},
"client": {
......
......@@ -26,50 +26,47 @@
const create = (runner, modules, config) => {
const controller = {
registry: {},
client: room => {
client: async room => {
if (room in controller.registry) {
return null;
}
controller.registry[room] = runner.run(modules,
config.visio.url,
room,
config.client);
controller.registry[room] = await runner.run(modules,
config.visio.url,
room,
config.client);
// Add callback for client to inform when it leaves the room
controller.registry[room]
.timeouts('script', 2147483000) // We need big timeouts here (here ~600h)
.executeAsync(done => {
// Note that this code is executed in the selenium-driven browser
/* eslint-disable no-undef */
robot.notifyEndToServer = () => {
done('finished');
console.log('notified server I finished');
};
/* eslint-enable */
}).then(() => {
delete controller.registry[room];
console.log('client for room', room, 'finished');
}).catch(err => {
console.log('got error', err);
// Careful, there is a bug here with chomedriver; if we open the
// console of the selenium-driven browser, an exception is thrown.
// This means that this part may break in dev (but not in prod).
// See issue #65 for more details.
});
registerClientEndCallback(room);
return controller.registry[room];
},
forceDisconnect: room => {
controller.registry[room].execute(() => {
if (!controller.registry[room]) {
return false;
}
controller.registry[room].evaluate(() => {
/* eslint-disable no-undef */
robot.stop();
/* eslint-enable */
}).end();
delete controller.registry[room];
});
// Client-side `robot.stop` will call `registerClientEndCallback`
// callback to clean registry
return true;
}
};
function registerClientEndCallback(room) {
controller.registry[room].exposeFunction(
'notifyEndToServer',
() => {
controller.registry[room].close();
delete controller.registry[room];
console.log('client for room', room, 'finished');
});
}
return controller;
};
......
......@@ -24,34 +24,31 @@ const {create} = require('./controller.js');
// Here are some needed mocks
const functionRunnerMock = {
callbacks: [],
catch() {
return functionRunnerMock;
},
end() {
return functionRunnerMock;
},
execute() {
return functionRunnerMock;
},
executeAsync() {
return functionRunnerMock;
},
timeouts() {
return functionRunnerMock;
},
then(callback) {
functionRunnerMock.callbacks.push(callback);
return functionRunnerMock;
global.robot = {
stopCalled: false,
stop: () => {
global.robot.stopCalled = true;
}
};
function mockClient() {
const res = {
exposedFunctions: {},
exposeFunction: (name, f) => {
res.exposedFunctions[name] = f;
},
evaluate: f => {
f();
},
close: () => {}
};
return res;
}
const runnerMock = {
run: () => {
return functionRunnerMock;
exposedFunction: {},
run: async () => {
return mockClient();
}
};
......@@ -64,53 +61,50 @@ let controller;
describe('controller', () => {
beforeEach(() => {
global.robot.stopCalled = false;
controller = create(runnerMock, [], configMock);
functionRunnerMock.callbacks = [];
});
test('should allow to create a client to a new room', () => {
expect(controller.client).toBeDefined();
});
test('should register a newly created client to its room', () => {
const client = controller.client('test');
test('should register a newly created client to its room', async done => {
const client = await controller.client('test');
expect(controller.registry).toHaveProperty('test', client);
done();
});
test('should return null when trying to create a client for an existing room', () => {
controller.client('test');
const client2 = controller.client('test');
test('should return null when trying to create a client for an existing room', async done => {
await controller.client('test');
const client2 = await controller.client('test');
expect(client2).toBeNull();
done();
});
test('should not replace the client for an existing room', () => {
const client1 = controller.client('test');
controller.client('test');
test('should not replace the client for an existing room', async () => {
const client1 = await controller.client('test');
await controller.client('test');
expect(controller.registry).toHaveProperty('test', client1);
});
test('should not have any room in registry when the client is disconnect', () => {
controller.client('test');
test('should execute robot.stop() when the client is disconnect', async () => {
await controller.client('test');
expect(global.robot.stopCalled).toBe(false);
controller.forceDisconnect('test');
expect(controller.registry).toEqual({});
});
test('should have a room in registry when one client disconnect', () => {
controller.client('test1');
const client2 = controller.client('test2');
controller.forceDisconnect('test1');
expect(controller.registry).toHaveProperty('test2', client2);
expect(global.robot.stopCalled).toBe(true);
});
test('should have registered a finish callback to the client', () => {
controller.client('test');
expect(functionRunnerMock.callbacks.length).toBe(1);
test('should have registered a finish callback to the client', async () => {
const client = await controller.client('test');
expect(client.exposedFunctions).toHaveProperty('notifyEndToServer');
expect(client.exposedFunctions.notifyEndToServer).not.toBeNull();
});
test('should clean registry on client finish callback', () => {
controller.client('test');
test('should clean registry on client finish callback', async () => {
const client = await controller.client('test');
expect(controller.registry).toHaveProperty('test');
functionRunnerMock.callbacks[0]();
client.exposedFunctions.notifyEndToServer();
expect(controller.registry).not.toHaveProperty('test');
});
});
......@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const webdriverio = require('webdriverio');
const puppeteer = require('puppeteer');
// Utility function to resolve a list of promise-based function calls
// on a given element list sequentially
......@@ -28,39 +28,116 @@ function resolveSequentially(f, elements) {
new Promise(resolve => resolve()));
}
module.exports = config => ({
run: (controllerFilesList, server, room, clientConfig) => {
console.log('runner is up');
const client = webdriverio.remote(config.driver);
console.log('runner created client');
return client.init()
.then(() => console.log('runner: client started'))
.url(server + '/' + room)
.then(() => console.log('runner: connecting to url'))
.waitForVisible('#displayname', 30000)
.then(() => console.log('runner: displayname visible'))
.then(() => resolveSequentially(f => client.execute(f, room, clientConfig),
controllerFilesList))
.then(() => console.log('runner: modules resolved'))
.then(() => client.execute(clientConfig =>
/* eslint-disable no-undef */
robotController.external.load(clientConfig), clientConfig)
/* eslint-enable */
)
.then(() => console.log('runner: external loaded'))
.then(() => client.execute((room, clientConfig) => {
setTimeout(() => {
/* eslint-disable no-undef */
robot.start(room, clientConfig);
/* eslint-enable */
}, 500);
}, room, clientConfig))
.then(() => console.log('runner: robot started'))
.setValue('#displayname', clientConfig.name)
.click('.btn')
.then(() => console.log('runner: button clicked'))
.waitForExist('//div[@video-id="video-thumb8"]', 30000)
.then(() => console.log('runner: video exists'))
.catch(err => console.log('runner: error %j', err));
module.exports = config => {
const b = puppeteer.launch(config.puppeteer);
function displayClientConsole(page, room) {
page.on('console', msg => {
if (msg.type === 'error') {
console.log('room', room, ':', '[' + msg.type + ']', msg.args[0].jsonValue());
} else {
console.log('room', room, ':', '[' + msg.type + ']', msg.text);
}
});
page.on('warning', warn => {
console.error('room', room, ':', '[WARN ' + warn.code + ']', warn.message);
console.error('room', room, ':', warn.stack);
});
page.on('error', err => {
console.error('room', room, ':', '[ERROR ' + err.code + ']', err.message);
console.error('room', room, ':', err.stack);
});
page.on('pageerror', err => {
console.error('room', room, ':', '[PAGEERROR]', err);
});
}
});
return {
run: async (controllerFilesList, server, room, clientConfig) => {
try {
const browser = await b;
console.log('runner is up');
const page = await browser.newPage();
console.log('runner: client started');
if (config.displayClientConsole) {
displayClientConsole(page, room);
}
// Connect to the room
await page.goto(server + '/' + room);
console.log('runner: connecting to url');
await page.waitForSelector('#displayname', {
visible: true
});
console.log('runner: displayname visible');
const nameField = await page.$('#displayname');
// Clean field of possible old input
await nameField.focus();
await page.keyboard.down('Control');
await page.keyboard.press('a');
await page.keyboard.up('Control');
await page.keyboard.press('Delete');
await nameField.type(clientConfig.name);
const submitButton = await page.$('.btn');
await submitButton.click();
console.log('runner: name submitted');
await page.waitForSelector('[video-id=video-thumb8]');
console.log('runner: video exists');
// Expose the room globally
await page.evaluate(
r => {
room = r;
},
room);
console.log('runner: room set');
// Load the robot
await resolveSequentially(
source => page.evaluate(source),
controllerFilesList);
console.log('runner: modules resolved');
await page.evaluate(
clientConfig => {
/* eslint-disable no-undef */
robotController.external.load(clientConfig);
/* eslint-enable */
},
clientConfig);
console.log('runner: external loaded');
// Run the robot
await page.evaluate((room, clientConfig) => {
setTimeout(
() => {
/* eslint-disable no-undef */
robot.start(room, clientConfig);
/* eslint-enable */
},
500);
}, room, clientConfig);
console.log('runner: robot started');
return page;
} catch (err) {
console.error('runner: error ', err);
return null;
}
}
};
};
......@@ -16,7 +16,7 @@
"body-parser": "^1.18.2",
"express": "^4.16.2",
"mz": "2.6.0",
"webdriverio": "4.7.1"
"puppeteer": "0.12.0",
},
"devDependencies": {
"jest": "20.0.0",
......
This diff is collapsed.