Commit 92a09327 authored by Romain Courteaud's avatar Romain Courteaud

Add declareJob function.

Jobs are executed on demand only when the gadget is in the DOM.

Calling the same job a second time cancel the previous execution.
parent f55764ba
...@@ -401,6 +401,9 @@ ...@@ -401,6 +401,9 @@
g.__monitor.cancel(); g.__monitor.cancel();
} }
g.__monitor = new Monitor(); g.__monitor = new Monitor();
g.__job_dict = {};
g.__job_list = [];
g.__job_triggered = false;
g.__monitor.fail(function (error) { g.__monitor.fail(function (error) {
if (!(error instanceof RSVP.CancellationError)) { if (!(error instanceof RSVP.CancellationError)) {
return g.aq_reportServiceError(error); return g.aq_reportServiceError(error);
...@@ -463,18 +466,64 @@ ...@@ -463,18 +466,64 @@
return this; return this;
}; };
function runJob(gadget, name, callback, argument_list) {
var job_promise = new RSVP.Queue()
.push(function () {
return callback.apply(gadget, argument_list);
});
if (gadget.__job_dict.hasOwnProperty(name)) {
gadget.__job_dict[name].cancel();
}
gadget.__job_dict[name] = job_promise;
gadget.__monitor.monitor(new RSVP.Queue()
.push(function () {
return job_promise;
})
.push(undefined, function (error) {
if (!(error instanceof RSVP.CancellationError)) {
throw error;
}
}));
}
function startService(gadget) { function startService(gadget) {
gadget.__monitor.monitor(new RSVP.Queue() gadget.__monitor.monitor(new RSVP.Queue()
.push(function () { .push(function () {
var i, var i,
service_list = gadget.constructor.__service_list; service_list = gadget.constructor.__service_list,
job_list = gadget.__job_list;
for (i = 0; i < service_list.length; i += 1) { for (i = 0; i < service_list.length; i += 1) {
gadget.__monitor.monitor(service_list[i].apply(gadget)); gadget.__monitor.monitor(service_list[i].apply(gadget));
} }
for (i = 0; i < job_list.length; i += 1) {
runJob(gadget, job_list[i][0], job_list[i][1], job_list[i][2]);
}
gadget.__job_list = [];
gadget.__job_triggered = true;
}) })
); );
} }
/////////////////////////////////////////////////////////////////
// RenderJSGadget.declareJob
// gadget internal method, which trigger execution
// of a function inside a service
/////////////////////////////////////////////////////////////////
RenderJSGadget.declareJob = function (name, callback) {
this.prototype[name] = function () {
var context = this,
argument_list = arguments;
if (context.__job_triggered) {
runJob(context, name, callback, argument_list);
} else {
context.__job_list.push([name, callback, argument_list]);
}
};
// Allow chain
return this;
};
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
// RenderJSGadget.declareMethod // RenderJSGadget.declareMethod
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
...@@ -1072,6 +1121,8 @@ ...@@ -1072,6 +1121,8 @@
tmp_constructor.__service_list = RenderJSGadget.__service_list.slice(); tmp_constructor.__service_list = RenderJSGadget.__service_list.slice();
tmp_constructor.declareMethod = tmp_constructor.declareMethod =
RenderJSGadget.declareMethod; RenderJSGadget.declareMethod;
tmp_constructor.declareJob =
RenderJSGadget.declareJob;
tmp_constructor.declareAcquiredMethod = tmp_constructor.declareAcquiredMethod =
RenderJSGadget.declareAcquiredMethod; RenderJSGadget.declareAcquiredMethod;
tmp_constructor.allowPublicAcquisition = tmp_constructor.allowPublicAcquisition =
...@@ -1247,6 +1298,7 @@ ...@@ -1247,6 +1298,7 @@
RenderJSGadget.call(this); RenderJSGadget.call(this);
}; };
tmp_constructor.declareMethod = RenderJSGadget.declareMethod; tmp_constructor.declareMethod = RenderJSGadget.declareMethod;
tmp_constructor.declareJob = RenderJSGadget.declareJob;
tmp_constructor.declareAcquiredMethod = tmp_constructor.declareAcquiredMethod =
RenderJSGadget.declareAcquiredMethod; RenderJSGadget.declareAcquiredMethod;
tmp_constructor.allowPublicAcquisition = tmp_constructor.allowPublicAcquisition =
...@@ -1380,6 +1432,8 @@ ...@@ -1380,6 +1432,8 @@
tmp_constructor.declareService = tmp_constructor.declareService =
RenderJSGadget.declareService; RenderJSGadget.declareService;
tmp_constructor.declareJob =
RenderJSGadget.declareJob;
tmp_constructor.onEvent = tmp_constructor.onEvent =
RenderJSGadget.onEvent; RenderJSGadget.onEvent;
tmp_constructor.declareAcquiredMethod = tmp_constructor.declareAcquiredMethod =
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
var gk = rJS(window), var gk = rJS(window),
ready_called = false, ready_called = false,
service_started = false, service_started = false,
job_started = false,
event_started = false; event_started = false;
gk.ready(function (g) { gk.ready(function (g) {
...@@ -24,9 +25,18 @@ ...@@ -24,9 +25,18 @@
.declareMethod('wasServiceStarted', function () { .declareMethod('wasServiceStarted', function () {
return service_started; return service_started;
}) })
.declareMethod('triggerJob', function () {
return this.runJob();
})
.declareMethod('wasEventStarted', function () { .declareMethod('wasEventStarted', function () {
return event_started; return event_started;
}) })
.declareMethod('wasJobStarted', function () {
return job_started;
})
.declareJob('runJob', function () {
job_started = true;
})
.declareMethod('canReportServiceError', function () { .declareMethod('canReportServiceError', function () {
return (this.aq_reportServiceError !== undefined); return (this.aq_reportServiceError !== undefined);
}) })
......
...@@ -2279,6 +2279,625 @@ ...@@ -2279,6 +2279,625 @@
}); });
}); });
/////////////////////////////////////////////////////////////////
// RenderJSGadgetKlass.declareJob
/////////////////////////////////////////////////////////////////
module("RenderJSGadgetKlass.declareJob", {
setup: function () {
renderJS.clearGadgetKlassList();
this.server = sinon.fakeServer.create();
this.server.autoRespond = true;
this.server.autoRespondAfter = 5;
},
teardown: function () {
this.server.restore();
delete this.server;
}
});
test('is chainable', function () {
// Check that declareJob is chainable
// Subclass RenderJSGadget to not pollute its namespace
var Klass = function () {
RenderJSGadget.call(this);
}, result;
Klass.prototype = new RenderJSGadget();
Klass.prototype.constructor = Klass;
Klass.__service_list = [];
Klass.declareJob = RenderJSGadget.declareJob;
result = Klass.declareJob('runServiceMethod', function () {
return;
});
// onEvent is chainable
equal(result, Klass);
});
test('creates methods on the prototype', function () {
// Check that declareMethod create a callable on the prototype
// Subclass RenderJSGadget to not pollute its namespace
var Klass = function () {
RenderJSGadget.call(this);
}, gadget;
Klass.prototype = new RenderJSGadget();
Klass.prototype.constructor = Klass;
Klass.declareJob = RenderJSGadget.declareJob;
gadget = new Klass();
equal(gadget.testFoo, undefined);
Klass.declareJob('testFoo', function () {
return;
});
// Method is added on the instance class prototype
equal(RenderJSGadget.prototype.testFoo, undefined);
ok(gadget.testFoo !== undefined);
ok(Klass.prototype.testFoo !== undefined);
equal(Klass.prototype.testFoo, gadget.testFoo);
});
/////////////////////////////////////////////////////////////////
// Service status
/////////////////////////////////////////////////////////////////
function declareJobToCheck(klass, name, service_status) {
service_status.start_count = 0;
service_status.stop_count = 0;
service_status.status = undefined;
klass.declareJob(name, function (parameter) {
service_status.start_count += 1;
service_status.parameter = parameter;
return new RSVP.Queue()
.push(function () {
service_status.status = "started";
return RSVP.defer().promise;
})
.push(undefined, function (error) {
service_status.stop_count += 1;
if (error instanceof RSVP.CancellationError) {
service_status.status = "stopped";
} else {
service_status.status = "error";
}
throw error;
});
});
}
test('job untouched when gadget not in DOM', function () {
// Subclass RenderJSGadget to not pollute its namespace
var service1 = {},
service2 = {},
g,
gadget = new RenderJSGadget(),
html_url = 'https://example.org/files/qunittest/test500.html';
gadget.__sub_gadget_dict = {};
this.server.respondWith("GET", html_url, [200, {
"Content-Type": "text/html"
}, "<html><body></body></html>"]);
stop();
renderJS.declareGadgetKlass(html_url)
.then(function (Klass) {
declareJobToCheck(Klass, 'runJob1', service1);
declareJobToCheck(Klass, 'runJob2', service2);
return gadget.declareGadget(
html_url
);
})
.then(function (result) {
g = result;
return RSVP.all([
g.runJob1('foo'),
g.runJob2('bar')
]);
})
.then(function () {
return RSVP.delay(50);
})
.then(function () {
equal(service1.start_count, 0);
equal(service1.stop_count, 0);
equal(service1.status, undefined);
equal(service1.parameter, undefined);
equal(service2.start_count, 0);
equal(service2.stop_count, 0);
equal(service2.status, undefined);
equal(service2.parameter, undefined);
})
.fail(function (e) {
ok(false, e);
})
.always(function () {
start();
});
});
test('job triggered when gadget in DOM', function () {
// Subclass RenderJSGadget to not pollute its namespace
var service1 = {},
service2 = {},
g,
gadget = new RenderJSGadget(),
html_url = 'https://example.org/files/qunittest/test501.html';
gadget.__sub_gadget_dict = {};
this.server.respondWith("GET", html_url, [200, {
"Content-Type": "text/html"
}, "<html><body></body></html>"]);
document.getElementById('qunit-fixture').innerHTML = "<div></div>";
stop();
renderJS.declareGadgetKlass(html_url)
.then(function (Klass) {
declareJobToCheck(Klass, 'runJob1', service1);
declareJobToCheck(Klass, 'runJob2', service2);
return gadget.declareGadget(
html_url,
{element: document.getElementById('qunit-fixture')
.querySelector("div")}
);
})
.then(function (result) {
g = result;
return RSVP.all([
g.runJob1('foo'),
g.runJob2('bar')
]);
})
.then(function (g) {
return RSVP.delay(50);
})
.then(function () {
equal(service1.start_count, 1);
equal(service1.stop_count, 0);
equal(service1.status, "started");
equal(service1.parameter, 'foo');
equal(service2.start_count, 1);
equal(service2.stop_count, 0);
equal(service2.status, "started");
equal(service2.parameter, 'bar');
})
.fail(function (e) {
ok(false, e);
})
.always(function () {
start();
});
});
test('job triggered when gadget element added in DOM', function () {
// Subclass RenderJSGadget to not pollute its namespace
var service1 = {},
service2 = {},
g,
gadget = new RenderJSGadget(),
html_url = 'https://example.org/files/qunittest/test502.html';
gadget.__sub_gadget_dict = {};
this.server.respondWith("GET", html_url, [200, {
"Content-Type": "text/html"
}, "<html><body></body></html>"]);
document.getElementById('qunit-fixture').innerHTML = "<div></div>";
stop();
renderJS.declareGadgetKlass(html_url)
.then(function (Klass) {
declareJobToCheck(Klass, 'runJob1', service1);
declareJobToCheck(Klass, 'runJob2', service2);
return gadget.declareGadget(
html_url
);
})
.then(function (result) {
g = result;
return RSVP.all([
g.runJob1('foo'),
g.runJob2('bar')
]);
})
.then(function () {
document
.getElementById('qunit-fixture')
.querySelector("div")
.appendChild(g.__element);
return RSVP.delay(50);
})
.then(function () {
equal(service1.start_count, 1);
equal(service1.stop_count, 0);
equal(service1.status, "started");
equal(service1.parameter, 'foo');
equal(service2.start_count, 1);
equal(service2.stop_count, 0);
equal(service2.status, "started");
equal(service2.parameter, 'bar');
})
.fail(function (e) {
ok(false, e);
})
.always(function () {
start();
});
});
test('job stopped when gadget element removed from DOM', function () {
// Subclass RenderJSGadget to not pollute its namespace
var service1 = {},
service2 = {},
g,
gadget = new RenderJSGadget(),
html_url = 'https://example.org/files/qunittest/test504.html';
gadget.__sub_gadget_dict = {};
this.server.respondWith("GET", html_url, [200, {
"Content-Type": "text/html"
}, "<html><body></body></html>"]);
document.getElementById('qunit-fixture').innerHTML = "<div></div>";
stop();
renderJS.declareGadgetKlass(html_url)
.then(function (Klass) {
declareJobToCheck(Klass, 'runJob1', service1);
declareJobToCheck(Klass, 'runJob2', service2);
return gadget.declareGadget(
html_url,
{element: document.getElementById('qunit-fixture')
.querySelector("div")}
);
})
.then(function (result) {
g = result;
return RSVP.all([
g.runJob1('foo'),
g.runJob2('bar')
]);
})
.then(function () {
return RSVP.delay(50);
})
.then(function () {
document
.getElementById('qunit-fixture')
.innerHTML = "";
return RSVP.delay(50);
})
.then(function () {
equal(service1.start_count, 1);
equal(service1.stop_count, 1);
equal(service1.status, "stopped");
equal(service1.parameter, 'foo');
equal(service2.start_count, 1);
equal(service2.stop_count, 1);
equal(service2.status, "stopped");
equal(service2.parameter, 'bar');
})
.fail(function (e) {
ok(false, e);
})
.always(function () {
start();
});
});
test('job can not be restarted', function () {
// Subclass RenderJSGadget to not pollute its namespace
var service1 = {},
service2 = {},
gadget = new RenderJSGadget(),
created_gadget,
html_url = 'https://example.org/files/qunittest/test506.html';
gadget.__sub_gadget_dict = {};
this.server.respondWith("GET", html_url, [200, {
"Content-Type": "text/html"
}, "<html><body></body></html>"]);
document.getElementById('qunit-fixture').innerHTML = "<div></div>";
stop();
renderJS.declareGadgetKlass(html_url)
.then(function (Klass) {
declareJobToCheck(Klass, 'runJob1', service1);
declareJobToCheck(Klass, 'runJob2', service2);
return gadget.declareGadget(
html_url,
{element: document.getElementById('qunit-fixture')
.querySelector("div")}
);
})
.then(function (result) {
created_gadget = result;
return RSVP.all([
created_gadget.runJob1('foo'),
created_gadget.runJob2('bar')
]);
})
.then(function () {
return RSVP.delay(50);
})
.then(function () {
document.getElementById('qunit-fixture').innerHTML = "";
return RSVP.delay(50);
})
.then(function () {
document
.getElementById('qunit-fixture')
.appendChild(created_gadget.__element);
return RSVP.delay(50);
})
.then(function () {
equal(service1.start_count, 1);
equal(service1.stop_count, 1);
equal(service1.status, "stopped");
equal(service1.parameter, 'foo');
equal(service2.start_count, 1);
equal(service2.stop_count, 1);
equal(service2.status, "stopped");
equal(service2.parameter, 'bar');
})
.fail(function (e) {
ok(false, e);
})
.always(function () {
start();
});
});
test('job can be triggered multiple times', function () {
// Subclass RenderJSGadget to not pollute its namespace
var service1 = {},
service2 = {},
g,
gadget = new RenderJSGadget(),
html_url = 'https://example.org/files/qunittest/test501.html';
gadget.__sub_gadget_dict = {};
this.server.respondWith("GET", html_url, [200, {
"Content-Type": "text/html"
}, "<html><body></body></html>"]);
document.getElementById('qunit-fixture').innerHTML = "<div></div>";
stop();
renderJS.declareGadgetKlass(html_url)
.then(function (Klass) {
declareJobToCheck(Klass, 'runJob1', service1);
declareJobToCheck(Klass, 'runJob2', service2);
return gadget.declareGadget(
html_url,
{element: document.getElementById('qunit-fixture')
.querySelector("div")}
);
})
.then(function (result) {
g = result;
return RSVP.all([
g.runJob1('foo'),
g.runJob2('bar')
]);
})
.then(function () {
return RSVP.delay(50);
})
.then(function () {
g.runJob1('foo2');
return RSVP.delay(50);
})
.then(function () {
// First job should be cancelled
equal(service1.start_count, 2);
equal(service1.stop_count, 1);
equal(service1.status, "started");
equal(service1.parameter, 'foo2');
equal(service2.start_count, 1);
equal(service2.stop_count, 0);
equal(service2.status, "started");
equal(service2.parameter, 'bar');
})
.fail(function (e) {
ok(false, e);
})
.always(function () {
start();
});
});
test('job is local to a gadget instance', function () {
// Subclass RenderJSGadget to not pollute its namespace
var service1 = {},
g,
gadget = new RenderJSGadget(),
html_url = 'https://example.org/files/qunittest/test501.html';
gadget.__sub_gadget_dict = {};
this.server.respondWith("GET", html_url, [200, {
"Content-Type": "text/html"
}, "<html><body></body></html>"]);
document.getElementById('qunit-fixture').innerHTML =
"<div></div><span></span>";
stop();
renderJS.declareGadgetKlass(html_url)
.then(function (Klass) {
declareJobToCheck(Klass, 'runJob1', service1);
return RSVP.all([
gadget.declareGadget(
html_url,
{element: document.getElementById('qunit-fixture')
.querySelector("span")}
),
gadget.declareGadget(
html_url,
{element: document.getElementById('qunit-fixture')
.querySelector("div")}
)
]);
})
.then(function (result_list) {
g = result_list[1];
return g.runJob1('foo');
})
.then(function (g) {
return RSVP.delay(50);
})
.then(function () {
equal(service1.start_count, 1);
equal(service1.stop_count, 0);
equal(service1.status, "started");
equal(service1.parameter, 'foo');
})
.fail(function (e) {
ok(false, e);
})
.always(function () {
start();
});
});
/////////////////////////////////////////////////////////////////
// Job error handling
/////////////////////////////////////////////////////////////////
test('Job error are reported to parent gadget', function () {
// Subclass RenderJSGadget to not pollute its namespace
var ParentKlass = function () {
RenderJSGadget.call(this);
},
gadget,
catched_error,
html_url = 'https://example.org/files/qunittest/test508.html';
ParentKlass.prototype = new RenderJSGadget();
ParentKlass.prototype.constructor = ParentKlass;
ParentKlass.prototype.__acquired_method_dict = {};
ParentKlass.allowPublicAcquisition = RenderJSGadget.allowPublicAcquisition;
ParentKlass.allowPublicAcquisition('reportServiceError',
function (argument_list) {
catched_error = argument_list[0];
return;
});
gadget = new ParentKlass();
gadget.__sub_gadget_dict = {};
// Subclass RenderJSGadget to not pollute its namespace
this.server.respondWith("GET", html_url, [200, {
"Content-Type": "text/html"
}, "<html><body></body></html>"]);
document.getElementById('qunit-fixture').innerHTML = "<div></div>";
stop();
renderJS.declareGadgetKlass(html_url)
.then(function (Klass) {
Klass.declareJob('crashJob', function () {
throw new Error("My service crashed!");
});
return gadget.declareGadget(
html_url,
{element: document.getElementById('qunit-fixture')
.querySelector("div")}
);
})
.then(function (g) {
return g.crashJob('foo');
})
.then(function () {
return RSVP.delay(50);
})
.then(function () {
ok(catched_error instanceof Error);
equal(
catched_error.message,
"My service crashed!"
);
})
.fail(function (e) {
ok(false, e);
})
.always(function () {
start();
});
});
test('Job error are reported after added to the DOM', function () {
// Subclass RenderJSGadget to not pollute its namespace
var ParentKlass = function () {
RenderJSGadget.call(this);
},
gadget,
g,
catched_error,
html_url = 'https://example.org/files/qunittest/test508.html';
ParentKlass.prototype = new RenderJSGadget();
ParentKlass.prototype.constructor = ParentKlass;
ParentKlass.prototype.__acquired_method_dict = {};
ParentKlass.allowPublicAcquisition = RenderJSGadget.allowPublicAcquisition;
ParentKlass.allowPublicAcquisition('reportServiceError',
function (argument_list) {
catched_error = argument_list[0];
return;
});
gadget = new ParentKlass();
gadget.__sub_gadget_dict = {};
// Subclass RenderJSGadget to not pollute its namespace
this.server.respondWith("GET", html_url, [200, {
"Content-Type": "text/html"
}, "<html><body></body></html>"]);
document.getElementById('qunit-fixture').innerHTML = "<div></div>";
stop();
renderJS.declareGadgetKlass(html_url)
.then(function (Klass) {
Klass.declareJob('crashJob', function () {
throw new Error("My service crashed!");
});
return gadget.declareGadget(
html_url
);
})
.then(function (result) {
g = result;
return g.crashJob('foo');
})
.then(function () {
return RSVP.delay(50);
})
.then(function () {
equal(catched_error, undefined);
})
.then(function () {
document
.getElementById('qunit-fixture')
.querySelector("div")
.appendChild(g.__element);
return RSVP.delay(50);
})
.then(function () {
ok(catched_error instanceof Error);
equal(
catched_error.message,
"My service crashed!"
);
})
.fail(function (e) {
ok(false, e);
})
.always(function () {
start();
});
});
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
// RenderJSIframeGadget // RenderJSIframeGadget
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
...@@ -3580,6 +4199,25 @@ ...@@ -3580,6 +4199,25 @@
equal(result, true); equal(result, true);
}) })
// Check that job was not started
.push(function () {
return new_gadget.wasJobStarted();
})
.push(function (result) {
equal(result, false);
})
// Check that job can be triggered
.push(function () {
return new_gadget.triggerJob();
})
.push(function () {
return new_gadget.wasJobStarted();
})
.push(function (result) {
equal(result, true);
})
// Check that service are started // Check that service are started
.push(function () { .push(function () {
return new_gadget.wasServiceStarted(); return new_gadget.wasServiceStarted();
...@@ -4255,6 +4893,7 @@ ...@@ -4255,6 +4893,7 @@
ok(root_gadget.hasOwnProperty("__sub_gadget_dict")); ok(root_gadget.hasOwnProperty("__sub_gadget_dict"));
deepEqual(root_gadget.__sub_gadget_dict, {}); deepEqual(root_gadget.__sub_gadget_dict, {});
deepEqual(root_gadget_klass.__service_list, []); deepEqual(root_gadget_klass.__service_list, []);
deepEqual(root_gadget.__job_list, []);
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
return root_gadget.declareGadget("./embedded.html", { return root_gadget.declareGadget("./embedded.html", {
......
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