Skip to content

Commit

Permalink
Add polling function management | refs #35772
Browse files Browse the repository at this point in the history
  • Loading branch information
sdiemer committed Feb 29, 2024
1 parent f52fcd0 commit 0fd93c2
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 37 deletions.
2 changes: 1 addition & 1 deletion dist/jsu.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/jsu.min.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jsu",
"version": "8",
"version": "11",
"description": "",
"main": "gulpfile.js",
"type": "module",
Expand Down
82 changes: 81 additions & 1 deletion src/jsu.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ jsu (JavaScript Utilities)

export default class JavaScriptUtilities {
constructor () {
this.version = 10; // Change this when updating this script
this.version = 11; // Change this when updating this script
this.ignoreUntilFocusChanges = false;
this.userAgent = window.navigator && window.navigator.userAgent ? window.navigator.userAgent.toLowerCase() : 'unknown';
this.userAgentData = null;
Expand Down Expand Up @@ -749,4 +749,84 @@ export default class JavaScriptUtilities {
return send.apply(this, arguments);
};
}
setupPolling (fct, interval, enabled) {
/*
This function purpose is to handle a function used for polling.
The polling will be stopped when page is hidden and restarted if visible.
The returned object can be used as an API to control the polling.
Arguments:
- `fct`:
The function to use for the polling.
It will receive a callback function as argument which must be
called once the function is done (after a request for example).
- `interval`:
The interval is the time between the end of the execution of the
function and the next execution of the function.
This means that the execution duration of the function will delay
the next run.
- `enabled`:
Boolean to indicate if the polling should be initially enabled.
Default is `true`.
*/
const polling = {
enabled: false,
timeoutId: null,
running: false,
lastRun: 0,
interval: interval,
fct: fct
};
polling.enable = function () {
if (!polling.enabled) {
polling.enabled = true;
polling.resume();
}
};
polling.disable = function () {
if (polling.enabled) {
polling.enabled = false;
polling.cancel();
}
};
polling.run = function () {
if (polling.enabled && !polling.running) {
polling.running = true;
polling.cancel();
polling.fct(function () {
polling.lastRun = (new Date()).getTime();
polling.running = false;
polling.plan(polling.interval);
});
}
};
polling.plan = function (delay) {
if (polling.enabled && !polling.timeoutId && document.visibilityState === 'visible') {
polling.timeoutId = setTimeout(function () {
polling.run();
}, delay);
}
};
polling.resume = function () {
const now = (new Date()).getTime();
const delay = Math.max(polling.lastRun + polling.interval - now, 1);
polling.plan(delay);
};
polling.cancel = function () {
if (polling.timeoutId !== null) {
clearTimeout(polling.timeoutId);
polling.timeoutId = null;
}
};
if (enabled === true || enabled === undefined) {
polling.enable();
}
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible') {
polling.resume();
} else {
polling.cancel();
}
});
return polling;
}
}
7 changes: 7 additions & 0 deletions tests/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ <h1><a href="https://github.com/UbiCastTeam/jsu">JSU test page</a></h1>
<div id="translations_report"></div>
</fieldset>

<fieldset>
<legend>Polling</legend>
<div id="polling_report">No run</div>
<button type="button" id="test_polling_disable">Disable</button>
<button type="button" id="test_polling_enable">Enable</button>
</fieldset>

<fieldset>
<legend>Requests</legend>
<button type="button" id="test_request_localhost_json">Test request on "https://localhost" as json</button>
Expand Down
22 changes: 22 additions & 0 deletions tests/manual_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,27 @@ function testTranslation () {
ele.innerHTML = '<p>' + jsu.escapeHTML(repr) + '</p>';
}

function testPolling () {
const ele = document.getElementById('polling_report');
const calls = [];
let count = 0;
const polling = jsu.setupPolling(function (callback) {
count += 1;
calls.push('Call #' + count + ' at ' + (new Date()).toTimeString());
if (calls.length > 10) {
calls.shift();
}
ele.innerHTML = calls.join('<br/>');
callback();
}, 10000);
document.getElementById('test_polling_disable').addEventListener('click', function () {
polling.disable();
});
document.getElementById('test_polling_enable').addEventListener('click', function () {
polling.enable();
});
}

function testRequest ({method, url, json, params, data, append, noText}) {
jsu.httpRequest({
url: url,
Expand Down Expand Up @@ -207,6 +228,7 @@ jsu.onDOMLoad(function () {
displayUserAgent();
testWebGL();
testTranslation();
testPolling();
document.getElementById('test_request_localhost_json').addEventListener('click', function () {
testRequest({'url': host, 'json': true});
});
Expand Down
94 changes: 69 additions & 25 deletions tests/test_jsu.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,47 @@ import JavaScriptUtilities from '../src/jsu.js';
const jsu = new JavaScriptUtilities();

describe('JSU', () => {
it('should return correct version', () => {
assert(jsu.version === 10);
it('should return the correct version', () => {
assert(jsu.version === 11);
});
it('should set/get cookies', () => {
it('should handle getCookie and setCookie', () => {
jsu.setCookie('a', '1');
const value = jsu.getCookie('a');
assert(value == '1');
});
it('should strip', () => {
it('should handle strip', () => {
const text = ' test \n test \n test \n test ';
const value = jsu.strip(text);
assert(value == 'test \n test \n test \n test');
});
it('should slugify', () => {
it('should handle slugify', () => {
const text = '>@)(#<!test?/"\'][{}=+&^`%$';
const value = jsu.slugify(text);
assert(value == 'test');
});
it('should stripHTML', () => {
it('should handle stripHTML', () => {
const text = '<div><div class="test">test</div></div>';
const value = jsu.stripHTML(text);
assert(value == 'test');
});
it('should escapeHTML and decodeHTML', () => {
it('should handle escapeHTML and decodeHTML', () => {
const html = '<div class="test">test &#34;</div>';
const encodedHTML = jsu.escapeHTML(html);
assert(encodedHTML == '&lt;div class="test"&gt;test &amp;#34;&lt;/div&gt;');
const decodedHTML = jsu.decodeHTML(encodedHTML);
assert(decodedHTML == html);
});
it('should escapeAttribute', () => {
it('should handle escapeAttribute', () => {
const html = '<div class="test">test\n\'</div>';
const encodedHTML = jsu.escapeAttribute(html);
assert(encodedHTML == '<div class=&quot;test&quot;>test&#13;&#10;&#39;</div>');
});
it('should getClickPosition', () => {
it('should handle getClickPosition', () => {
const evt = {'pageX': 10, 'pageY': 10};
const positions = jsu.getClickPosition(evt, document.body);
assert(JSON.stringify(positions) == JSON.stringify({'x': 10, 'y': 10}));
});
it('should onDOMLoad', async () => {
it('should handle onDOMLoad', async () => {
let load = false;
jsu.onDOMLoad(() => {
load = true;
Expand All @@ -55,7 +55,7 @@ describe('JSU', () => {
}, 100);
assert(load);
});
it('should do a httpRequest', async () => {
it('should handle httpRequest', async () => {
const requestStatuses = [];
const testDatas = [
{'method': 'GET', 'params': {'test': 1}},
Expand All @@ -82,15 +82,15 @@ describe('JSU', () => {
}, 500);
assert(JSON.stringify(requestStatuses) == JSON.stringify([200, 200, 200, 200, 200, 200, 200]));
}).timeout(5000);
it('should compareVersions', () => {
it('should handle compareVersions', () => {
let result = jsu.compareVersions('1.1.1', '=', '1.1.1');
assert(result == 0);
result = jsu.compareVersions('1.1.0', '=', '1.1.1');
assert(result == 1);
result = jsu.compareVersions('1.1.2', '=', '1.1.1');
assert(result == -1);
});
it('should setObjectAttributes', () => {
it('should handle setObjectAttributes', () => {
const obj = {};
const data = {'a': 1, 'b': 1, 'translations': {'en': {'a': 'a'}}};
const allowedAttributes = ['b'];
Expand All @@ -102,7 +102,7 @@ describe('JSU', () => {
assert(!obj.translations);
assert(obj.b);
});
it('should getWebglContext', () => {
it('should handle getWebglContext', () => {
console.error(process.env);
const testDatas = [
{'options': {}, 'browserName': 'chrome'},
Expand All @@ -113,17 +113,17 @@ describe('JSU', () => {
assert(jsu.getWebglContext(canvas, data.options, data.browserName));
}
});
it('should test isInIframe', () => {
it('should handle isInIframe', () => {
// karma window is in an iframe <iframe id="context" src="context.html" width="100%" height="100%"></iframe>
assert(jsu.isInIframe());
});
it('should attemptFocus', () => {
it('should handle attemptFocus', () => {
const focusableElement = document.createElement('input');
document.body.appendChild(focusableElement);
assert(jsu.isFocusable(focusableElement));
assert(jsu.attemptFocus(focusableElement));
});
it('should focusFirstDescendant and focusLastDescendant', () => {
it('should handle focusFirstDescendant and focusLastDescendant', () => {
const focusableElementOne = document.createElement('input');
focusableElementOne.id = '1';
const focusableElementTwo = document.createElement('button');
Expand All @@ -135,7 +135,7 @@ describe('JSU', () => {
assert(jsu.focusLastDescendant(document.body));
assert(document.activeElement == focusableElementTwo);
});
it('should test UA', () => {
it('should handle user agents', () => {
assert(jsu.userAgent);
assert(jsu.osName);
assert(jsu.osVersion !== undefined);
Expand All @@ -144,7 +144,7 @@ describe('JSU', () => {
assert(jsu.browserName);
assert(jsu.browserVersion);
});
it('should test isRecordingAvailable', () => {
it('should handle isRecordingAvailable', () => {
const data = [
['safari', '6', false],
['firefox', '30', false],
Expand All @@ -161,7 +161,7 @@ describe('JSU', () => {
assert(jsu.isRecordingAvailable() === result, `${browserName}@${browserVersion} isRecordingAvailable ${jsu.isRecordingAvailable()}`);
}
});
it('should manage translations', () => {
it('should handle translations', () => {
const translations = {
'fr': {
'lang': 'fr',
Expand Down Expand Up @@ -205,10 +205,10 @@ describe('JSU', () => {
assert(jsu.getDateDisplay('2021-12-30 00:12:14') == '30 December 2021 at 12:12 AM', `${jsu.getDateDisplay('2021-12-30 00:12:14')} == '30 December 2021 at 12:12 AM'`);
assert(jsu.getDateDisplay('2021-19-57 69:98:84') == '2021-19-57 69:98:84', `${jsu.getDateDisplay('2021-19-57 69:98:84')} == '2021-19-57 69:98:84'`);
assert(jsu.getDateDisplay('Invalid date') == 'Invalid date', `${jsu.getDateDisplay('Invalid date')} == 'Invalid date'`);
assert(jsu.getSizeDisplay() == '0 B', `${jsu.getSizeDisplay()} == '0 B'`);
assert(jsu.getSizeDisplay('123456789') == '123.5 MB', `${jsu.getSizeDisplay('123456789')} == '123.5 MB'`);
assert(jsu.getSizeDisplay('12345678910') == '12.3 GB', `${jsu.getSizeDisplay('12345678910')} == '123.5 MB'`);
assert(jsu.getSizeDisplay('1234567891011') == '1.2 TB', `${jsu.getSizeDisplay('1234567891011')} == '123.5 MB'`);
assert(jsu.getSizeDisplay() == '0 B', `${jsu.getSizeDisplay()} == '0 B'`);
assert(jsu.getSizeDisplay('123456789') == '123.5 MB', `${jsu.getSizeDisplay('123456789')} == '123.5 MB'`);
assert(jsu.getSizeDisplay('12345678910') == '12.3 GB', `${jsu.getSizeDisplay('12345678910')} == '123.5 MB'`);
assert(jsu.getSizeDisplay('1234567891011') == '1.2 TB', `${jsu.getSizeDisplay('1234567891011')} == '123.5 MB'`);
jsu.useLang('fr');
assert(jsu.getCurrentLang() == 'fr');
assert(JSON.stringify(jsu.getCurrentCatalog()) == JSON.stringify(translations['fr']));
Expand All @@ -217,8 +217,52 @@ describe('JSU', () => {
assert(jsu.getDateDisplay('2021-12-30 00:12:14') == '30 décembre 2021 à 00:12', `${jsu.getDateDisplay('2021-12-30 00:12:14')} == '30 décembre 2021 à 00:12'`);
assert(jsu.getDateDisplay('2021-19-57 69:98:84') == '2021-19-57 69:98:84', `${jsu.getDateDisplay('2021-19-57 69:98:84')} == '2021-19-57 69:98:84'`);
assert(jsu.getDateDisplay('Invalid date') == 'Invalid date', `${jsu.getDateDisplay('Invalid date')} == 'Invalid date'`);
assert(jsu.getSizeDisplay('123456789') == '123.5 Mo', `${jsu.getSizeDisplay('123456789')} == '123.5 Mo'`);
assert(jsu.getSizeDisplay('123456789') == '123.5 Mo', `${jsu.getSizeDisplay('123456789')} == '123.5 Mo'`);
jsu.useLang('x');
assert(jsu.translate('lang') == 'en');
});
it('should handle setupPolling', async () => {
const sleep = (time) => {
return new Promise((resolve) => {
setTimeout(resolve, time);
});
};

const start = new Date().getTime();
const calls = [];
const polling = jsu.setupPolling(function (callback) {
calls.push(new Date().getTime() - start);
callback();
}, 1000);

// Test call after init
await sleep(500);
assert(calls.length == 1);
assert(calls[0] < 100, `${calls[0]} ~= 0`);

// Test call after interval
await sleep(1000);
assert(calls.length == 2);
assert(Math.abs(calls[1] - 1000) < 100, `${calls[1]} ~= 1000`);

// Test no call when disabled
polling.disable();
await sleep(1000);
assert(calls.length == 2);

// Test immediate call after enabling because interval is already reached
polling.enable();
await sleep(100);
assert(calls.length == 3);
assert(Math.abs(calls[2] - 2500) < 100, `${calls[2]} ~= 2500`);

// Test no call after enabling because a too recent call was made
polling.disable();
polling.enable();
await sleep(100);
assert(calls.length == 3);

// Cleanup test
polling.disable();
}).timeout(5000);
});
8 changes: 4 additions & 4 deletions tests/test_upload.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require('../src/jsu.js');
import { ChunkedUpload } from '../src/lib/chunked-upload.js';

describe('Upload', () => {
it('chunkedUpload success', async () => {
it('should handle chunkedUpload success', async () => {
const blob = new Blob(['Test file content'], {
type: 'application/json',
});
Expand Down Expand Up @@ -45,7 +45,7 @@ describe('Upload', () => {
assert(progress === 100);
assert(retries === 0);
});
it('chunkedUpload retry promise', async () => {
it('should handle chunkedUpload retry promise', async () => {
const blob = new Blob(['Test file content'], {
type: 'application/json',
});
Expand Down Expand Up @@ -98,7 +98,7 @@ describe('Upload', () => {
assert(progress === 100);
assert(retries === 2);
});
it('chunkedUpload error on chunk upload', async () => {
it('should handle chunkedUpload error on chunk upload', async () => {
const blob = new Blob(['Test file content'], {
type: 'application/json',
});
Expand Down Expand Up @@ -138,7 +138,7 @@ describe('Upload', () => {
assert(progress === 95);
assert(retries === 2);
});
it('chunkedUpload error on complete', async () => {
it('should handle chunkedUpload error on complete', async () => {
const blob = new Blob(['Test file content'], {
type: 'application/json',
});
Expand Down
Loading

0 comments on commit 0fd93c2

Please sign in to comment.