Commit 5a58aab1 authored by Tom Jorquera's avatar Tom Jorquera

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 #5332 passed with stage
in 2 minutes and 54 seconds
FROM node:8-alpine
FROM node:8-slim
RUN apt-get update && apt-get install -y wget --no-install-recommends \
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
&& apt-get update \
&& apt-get install -y google-chrome-unstable \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get purge --auto-remove -y curl \
&& rm -rf /src/*.deb
WORKDIR /usr/src/app/hublot
......
......@@ -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;
};
return ws;
} catch (err) {
console.error(err);
return null;
}
}
};
};
......@@ -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": {
......
......@@ -8,16 +8,19 @@ services:
depends_on:
- recommender
- kaldi-gstreamer
- selenium
volumes:
- ./media:/opt/media
ports:
- "3000:3000"
recommender:
image: linagora/recommender
ports:
expose:
- "8080"
kaldi-gstreamer:
image: linagora/kaldi-gstreamer
ports:
expose:
- "80"
volumes:
- ${MODELS_PATH}:/opt/models
......@@ -26,10 +29,3 @@ services:
- NB_WORKERS
- YAML
- MODELS_PATH
selenium:
image: selenium/standalone-chrome
ports:
- "4444"
volumes:
- ./media:/opt/media
......@@ -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.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment