From 83f45b2e718c13139cf8e631fe88a97b04060ae4 Mon Sep 17 00:00:00 2001 From: ste Date: Tue, 17 Apr 2018 15:13:38 +0100 Subject: [PATCH 1/6] GPII-2946: "momentary", "maintained", and "hybrid" key token gestures. --- .../userListeners/src/listeners.js | 70 +++++- .../userListeners/test/listenersTests.js | 199 ++++++++++++++++++ 2 files changed, 260 insertions(+), 9 deletions(-) create mode 100644 gpii/node_modules/userListeners/test/listenersTests.js diff --git a/gpii/node_modules/userListeners/src/listeners.js b/gpii/node_modules/userListeners/src/listeners.js index 133d84f9f..90905fee5 100644 --- a/gpii/node_modules/userListeners/src/listeners.js +++ b/gpii/node_modules/userListeners/src/listeners.js @@ -57,6 +57,10 @@ fluid.defaults("gpii.userListeners", { }); // A user listener. +// Listeners can have either "momentary", "maintained", or "hybrid" gesture modes: +// - Momentary: token is presented to key-in, then presented again to key-out, like a caps-lock key. (momentary = true) +// - Maintained: token is presented to key-in, then removed to key-out, like the shift key. (momentary = false) +// - Hybrid mode detects what the user is trying to do, based on how soon the token is removed. (detectMode = true) fluid.defaults("gpii.userListener", { gradeNames: ["fluid.component"], events: { @@ -86,11 +90,22 @@ fluid.defaults("gpii.userListener", { "{that}", "{arguments}.0" // The error. ] + }, + callFlowManager: { + funcName: "gpii.userListeners.callFlowManager", + args: [ + "{that}", + "{arguments}.0", // Token. + "{arguments}.1" // Action ("login", "logout", or "proximityTriggered"). + ] } }, members: { - // Set to true to call proximityTriggered, otherwise login/logout. - proximity: false, + // True to auto-detect momentary or maintained key token usage, based on how soon it was removed. Otherwise, + // use {that}.momentary. + detectMode: false, + // Set to true to call proximityTriggered, otherwise login/logout (ignored if that.detectMode is true). + momentary: false, // Override to provide the name of the listener. listenerName: "no-name", // Number of failures @@ -118,13 +133,16 @@ fluid.defaults("gpii.userListener", { }, // Seconds (multiplied by failureCount) to wait before restarting. - failDelay: 10 + failDelay: 10, + // If the token is removed before the timeout (seconds), then "momentary mode" is used. Otherwise, "maintained mode" + // is used. + detectModeTimeout: 4 }); /** * Handles the onTokenArrive event. * - * It calls the "login" action for non-proximity devices, otherwise "proximityTriggered". + * It calls the "login" action for maintained key tokens, "proximityTriggered" for momentary. * * @param that {Component} An instance of gpii.userListener. * @param token {string} The token from the user listener. @@ -132,14 +150,25 @@ fluid.defaults("gpii.userListener", { gpii.userListeners.tokenArrived = function (that, token) { fluid.log(that.listenerName + " token arrived: " + token); - var action = that.proximity ? "proximityTriggered" : "login"; - request("http://localhost:8081/user/" + token + "/" + action); + var momentary = that.momentary; + + if (that.detectMode) { + if (that.momentaryDetected) { + // A token has previously been presented (and quickly removed). + momentary = true; + } + + that.arrivalTime = process.hrtime(); + delete that.momentaryDetected; + } + + that.callFlowManager(token, momentary ? "proximityTriggered" : "login"); }; /** * Handles the onTokenRemove event. * - * It calls the "logout" action for non-proximity devices, otherwise it does nothing. + * It calls the "logout" action for maintained key tokens, "proximityTriggered" for momentary. * * @param that {Component} An instance of gpii.userListener. * @param token {string} The token from the user listener. @@ -147,9 +176,32 @@ gpii.userListeners.tokenArrived = function (that, token) { gpii.userListeners.tokenRemoved = function (that, token) { fluid.log(that.listenerName + " token removed: " + token); - if (!that.proximity) { - request("http://localhost:8081/user/" + token + "/logout"); + var momentary = that.momentary; + + if (that.detectMode) { + // If the token was removed before the timeout, then ignore the removal (key-out will be performed when the next + // token is presented). + var time = process.hrtime(that.arrivalTime)[0]; + that.momentaryDetected = time < that.options.detectModeTimeout; + momentary = that.momentaryDetected; + delete that.arrivalTime; } + + // Don't logout for momentary + if (!momentary) { + that.callFlowManager(token, "logout"); + } +}; + +/** + * Calls the flow manager with the user token and action. + * + * @param that {Component} An instance of gpii.userListener. + * @param token {string} The token from the user listener. + * @param action {String} Action to invoke ("login", "logout", or "proximityTriggered"). + */ +gpii.userListeners.callFlowManager = function (that, token, action) { + request("http://localhost:8081/user/" + token + "/" + action); }; /** diff --git a/gpii/node_modules/userListeners/test/listenersTests.js b/gpii/node_modules/userListeners/test/listenersTests.js new file mode 100644 index 000000000..172b834b9 --- /dev/null +++ b/gpii/node_modules/userListeners/test/listenersTests.js @@ -0,0 +1,199 @@ +/* + * User listener tests + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; +var fluid = require("infusion"); + +var jqUnit = fluid.require("node-jqunit"); +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.tests.userListener"); + +require("../index.js"); + + +fluid.defaults("gpii.tests.userListener.testListener", { + gradeNames: ["fluid.component", "fluid.contextAware", "gpii.userListener"], + invokers: { + startListener: "fluid.identity", + stopListener: "fluid.identity" + }, + members: { + listenerName: "testListener" + } +}); + + +var teardowns = []; +jqUnit.module("gpii.tests.userListener", { + teardown: function () { + while (teardowns.length) { + teardowns.pop()(); + } + } +}); + +gpii.tests.userListener.toggleModeTests = fluid.freezeRecursive({ + "maintained-short": { + momentary: false, + detectMode: false, + time: 1, + expect: ["login", "logout"] + }, + "maintained-long": { + momentary: false, + detectMode: false, + time: 100, + expect: ["login", "logout"] + }, + "momentary-short": { + momentary: true, + detectMode: false, + time: 1, + expect: ["proximityTriggered", null, "proximityTriggered", null] + }, + "momentary-long": { + momentary: true, + detectMode: false, + time: 100, + expect: ["proximityTriggered", null, "proximityTriggered", null] + }, + "hybrid-short": { + momentary: false, + detectMode: true, + time: 1, + expect: ["login", null, "proximityTriggered", null] + }, + "hybrid-short-again": { + momentary: false, + detectMode: true, + time: 1, + expect: ["proximityTriggered", null, "proximityTriggered", null] + }, + "hybrid-long": { + momentary: false, + detectMode: true, + time: 100, + expect: ["proximityTriggered", "logout"] + }, + "hybrid-long-again": { + momentary: false, + detectMode: true, + time: 100, + expect: ["login", "logout"] + }, + "hybrid-short-momentary": { + momentary: true, + detectMode: true, + time: 1, + expect: ["proximityTriggered", null, "proximityTriggered", null] + }, + "hybrid-long-momentary": { + momentary: true, + detectMode: true, + time: 100, + expect: ["proximityTriggered", "logout" ] + }, + "hybrid-long-momentary-again": { + momentary: true, + detectMode: true, + time: 100, + expect: ["proximityTriggered", "logout" ] + } +}); + + +// Tests USB device removal. +jqUnit.test("User listener - toggle mode timings", function () { + + var tests = gpii.tests.userListener.toggleModeTests; + var currentTest; + var currentTestKey; + var testEvent; + var expectIndex; + + fluid.each(tests, function (test) { + jqUnit.expect(test.expect.filter(fluid.identity).length * 2); + }); + + // Mock hrtime to return the desired time, instead of waiting. + var hrtimeOrig = process.hrtime; + teardowns.push(function () { + process.hrtime = hrtimeOrig; + }); + process.hrtime = function (t) { + if (t) { + // Return the duration + return [currentTest.time, 1]; + } else { + // The current time isn't used. + return [1, 1]; + } + }; + + // Check the correct calls for flow manager. + var callFlowManager = function (token, action) { + var expectAction = currentTest.expect[expectIndex]; + fluid.log(testEvent, ": ", action, "+", token); + + if (expectAction === null || (testEvent !== "arrive") && (testEvent !== "remove")) { + jqUnit.fail("Unexpected invocation of callFlowManager - " + currentTestKey); + } + + jqUnit.assertEquals(testEvent + " action as expected - " + currentTestKey, expectAction, action); + jqUnit.assertEquals(testEvent + " token as expected for " + currentTestKey, "token-" + currentTestKey, token); + }; + + // Create the test listener component. + var listeners = gpii.userListeners({ + components: { + testListener: { + type: "gpii.tests.userListener.testListener" + } + }, + distributeOptions: { + record: { + callFlowManager: callFlowManager + }, + target: "{that testListener}.options.invokers" + } + }); + + var listener = listeners.testListener; + + + fluid.each(tests, function (test, key) { + currentTest = test; + currentTestKey = key; + + listener.momentary = test.momentary; + listener.detectMode = test.detectMode; + listener.detectModeTimeout = 5; + + for (expectIndex = 0; expectIndex < currentTest.expect.length; expectIndex++) { + testEvent = "arrive"; + listener.events.onTokenArrive.fire(listener, "token-" + currentTestKey); + expectIndex++; + testEvent = "remove"; + listener.events.onTokenRemove.fire(listener, "token-" + currentTestKey); + } + + testEvent = null; + }); + +}); + From 71331ce6537061d45dec462cf25a94be1ceb793c Mon Sep 17 00:00:00 2001 From: ste Date: Tue, 17 Apr 2018 15:41:50 +0100 Subject: [PATCH 2/6] GPII-2946: Enabled hybrid listener mode --- gpii/node_modules/userListeners/src/listeners.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpii/node_modules/userListeners/src/listeners.js b/gpii/node_modules/userListeners/src/listeners.js index 90905fee5..a9ce0da97 100644 --- a/gpii/node_modules/userListeners/src/listeners.js +++ b/gpii/node_modules/userListeners/src/listeners.js @@ -103,7 +103,7 @@ fluid.defaults("gpii.userListener", { members: { // True to auto-detect momentary or maintained key token usage, based on how soon it was removed. Otherwise, // use {that}.momentary. - detectMode: false, + detectMode: true, // Set to true to call proximityTriggered, otherwise login/logout (ignored if that.detectMode is true). momentary: false, // Override to provide the name of the listener. From 6aaec3fa7fd19803c3bd458b2df2ed491129a00c Mon Sep 17 00:00:00 2001 From: ste Date: Wed, 9 May 2018 14:00:08 +0100 Subject: [PATCH 3/6] GPII-2946: Temporary fix for using different devices with hybrid mode --- gpii/node_modules/userListeners/src/listeners.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/gpii/node_modules/userListeners/src/listeners.js b/gpii/node_modules/userListeners/src/listeners.js index a9ce0da97..c8177c291 100644 --- a/gpii/node_modules/userListeners/src/listeners.js +++ b/gpii/node_modules/userListeners/src/listeners.js @@ -139,6 +139,7 @@ fluid.defaults("gpii.userListener", { detectModeTimeout: 4 }); +gpii.userListeners.state = {}; /** * Handles the onTokenArrive event. * @@ -153,13 +154,13 @@ gpii.userListeners.tokenArrived = function (that, token) { var momentary = that.momentary; if (that.detectMode) { - if (that.momentaryDetected) { + if (gpii.userListeners.state.momentaryDetected) { // A token has previously been presented (and quickly removed). momentary = true; } - that.arrivalTime = process.hrtime(); - delete that.momentaryDetected; + gpii.userListeners.state.arrivalTime = process.hrtime(); + delete gpii.userListeners.state.momentaryDetected; } that.callFlowManager(token, momentary ? "proximityTriggered" : "login"); @@ -181,10 +182,10 @@ gpii.userListeners.tokenRemoved = function (that, token) { if (that.detectMode) { // If the token was removed before the timeout, then ignore the removal (key-out will be performed when the next // token is presented). - var time = process.hrtime(that.arrivalTime)[0]; - that.momentaryDetected = time < that.options.detectModeTimeout; - momentary = that.momentaryDetected; - delete that.arrivalTime; + var time = process.hrtime(gpii.userListeners.state.arrivalTime)[0]; + gpii.userListeners.state.momentaryDetected = time < that.options.detectModeTimeout; + momentary = gpii.userListeners.state.momentaryDetected; + delete gpii.userListeners.state.arrivalTime; } // Don't logout for momentary From 873a99cfed1adb4925a07c1a94fc229762953398 Mon Sep 17 00:00:00 2001 From: ste Date: Fri, 11 May 2018 21:49:40 +0100 Subject: [PATCH 4/6] GPII-2946: If login fails due to already being logged in, send proximityChange. --- gpii/node_modules/userListeners/src/listeners.js | 14 +++++++++++++- gpii/node_modules/userListeners/test/all-tests.js | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/gpii/node_modules/userListeners/src/listeners.js b/gpii/node_modules/userListeners/src/listeners.js index c8177c291..55f083c85 100644 --- a/gpii/node_modules/userListeners/src/listeners.js +++ b/gpii/node_modules/userListeners/src/listeners.js @@ -202,7 +202,19 @@ gpii.userListeners.tokenRemoved = function (that, token) { * @param action {String} Action to invoke ("login", "logout", or "proximityTriggered"). */ gpii.userListeners.callFlowManager = function (that, token, action) { - request("http://localhost:8081/user/" + token + "/" + action); + fluid.log("userListener: sending " + token + "/" + action); + request("http://localhost:8081/user/" + token + "/" + action, function (error, response, body) { + fluid.log("userListener: received " + body); + if (!error && action === "login") { + var result = JSON.parse(body); + // If a login failed due to already being logged in, send proximityTriggered to cause a log out+in. + // This occurs when trying to login while already logged in via another listener device (and user listeners + // aren't to keep the "logged in" state). + if (result.isError && result.message && result.message.indexOf("already logged in") > 0) { + gpii.userListeners.callFlowManager(that, token, "proximityTriggered"); + } + } + }); }; /** diff --git a/gpii/node_modules/userListeners/test/all-tests.js b/gpii/node_modules/userListeners/test/all-tests.js index 1b2f7faba..45f11feb6 100644 --- a/gpii/node_modules/userListeners/test/all-tests.js +++ b/gpii/node_modules/userListeners/test/all-tests.js @@ -20,3 +20,4 @@ require("./pcscTests.js"); require("./usbTests.js"); +require("./listenersTests.js"); From 3f842b9f595289477f9d765b7ae4d190ed1a9058 Mon Sep 17 00:00:00 2001 From: ste Date: Mon, 9 Jul 2018 14:54:17 +0100 Subject: [PATCH 5/6] GPII-2946: Made JSDoc linter happy --- gpii/node_modules/userListeners/src/listeners.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gpii/node_modules/userListeners/src/listeners.js b/gpii/node_modules/userListeners/src/listeners.js index 795b0d72a..c49378a10 100644 --- a/gpii/node_modules/userListeners/src/listeners.js +++ b/gpii/node_modules/userListeners/src/listeners.js @@ -197,9 +197,9 @@ gpii.userListeners.tokenRemoved = function (that, token) { /** * Calls the flow manager with the user token and action. * - * @param that {Component} An instance of gpii.userListener. - * @param token {string} The token from the user listener. - * @param action {String} Action to invoke ("login", "logout", or "proximityTriggered"). + * @param {Component} that - An instance of gpii.userListener. + * @param {String} token - The token from the user listener. + * @param {String} action - Action to invoke ("login", "logout", or "proximityTriggered"). */ gpii.userListeners.callFlowManager = function (that, token, action) { fluid.log("userListener: sending " + token + "/" + action); From 3a5764f924d3b77df9d0a6c516b7cb81e5fa0cfb Mon Sep 17 00:00:00 2001 From: ste Date: Mon, 9 Jul 2018 17:25:18 +0100 Subject: [PATCH 6/6] GPII-2946: Made JSDoc linter happier --- gpii/node_modules/userListeners/test/listenersTests.js | 1 - 1 file changed, 1 deletion(-) diff --git a/gpii/node_modules/userListeners/test/listenersTests.js b/gpii/node_modules/userListeners/test/listenersTests.js index 172b834b9..affce3ce2 100644 --- a/gpii/node_modules/userListeners/test/listenersTests.js +++ b/gpii/node_modules/userListeners/test/listenersTests.js @@ -196,4 +196,3 @@ jqUnit.test("User listener - toggle mode timings", function () { }); }); -