diff --git a/labcontrol/gui/js_tests/addPlateModal_tests.html b/labcontrol/gui/js_tests/addPlateModal_tests.html new file mode 100644 index 00000000..e4bae49f --- /dev/null +++ b/labcontrol/gui/js_tests/addPlateModal_tests.html @@ -0,0 +1,34 @@ + + + + + + addPlateModal.js Unit Tests + + + + + + + + + + + + + + + + + +
+
+
+ + \ No newline at end of file diff --git a/labcontrol/gui/js_tests/addPlateModal_tests.js b/labcontrol/gui/js_tests/addPlateModal_tests.js new file mode 100644 index 00000000..4b743336 --- /dev/null +++ b/labcontrol/gui/js_tests/addPlateModal_tests.js @@ -0,0 +1,154 @@ +// Requires +// QUnit + +// Library under test: +// addPlateModal.js + +// Tests of function insertPlateModalDiv +QUnit.module("insertPlateModalDiv", function (hooks) { + // Verify correct behavior using default arguments + QUnit.test("default arguments", function (assert) { + //jquery selector strings + let modalIdDivSelectorStr = "body div#addPlateModal"; + let tableIdSelectorStr = "body table#searchPlateTable"; + + // before running function under test, body has no div with + // default modal id and no table with default table name + assert.equal($(modalIdDivSelectorStr).length, 0); + assert.equal($(tableIdSelectorStr).length, 0); + + insertPlateModalDiv(); + + // after running function under test, table has these elements + assert.equal($(modalIdDivSelectorStr).length, 1); + assert.equal($(tableIdSelectorStr).length, 1); + + // NOTE: unlike python unit tests, qunit does not abort the whole + // test case when an assert fails. Therefore, the cleanup can + // simply be done in-line at the end of the test. + // Also note that this clean-up can't be done automatically as the dom + // objects were added (intentionally) to body rather than to the + // qunit-fixture div. + $(modalIdDivSelectorStr).remove(); + }); + + // Verify correct behavior when arguments are explicitly specified + QUnit.test("explicit arguments", function (assert) { + //explicit names + let modalId = 'myModal'; + let tableId = 'myPlateTable'; + let parentId = 'qunit-fixture'; + + //jquery selector strings + let modalIdDivSelectorStr = "#" + parentId + " div#" + modalId; + let tableIdSelectorStr = "#" + parentId + " table#" + tableId; + + // before running function under test, qunit-fixture has no div + // with explicit modal id and no table with explicit table name + assert.equal($(modalIdDivSelectorStr).length, 0); + assert.equal($(tableIdSelectorStr).length, 0); + + insertPlateModalDiv(parentId, modalId, tableId); + + // after running function under test, table has these elements + assert.equal($(modalIdDivSelectorStr).length, 1); + assert.equal($(tableIdSelectorStr).length, 1); + }); +}); + +// Tests of functions that depend on the plate modal div having already +// been inserted +QUnit.module("modal-div-dependent tests", function (hooks) { + hooks.beforeEach(function (assert) { + insertPlateModalDiv("qunit-fixture"); + }); + + // No need for an afterEach hook because the modal div is inserted into + // the qunit-fixture div, which QUnit automatically empties after each test + + // Tests of the function populatePlateTable + QUnit.module("populatePlateTable", function (hooks) { + let qunitTbodySelectorStr = "#qunit-fixture tbody"; + + // Verify correct behavior when some realistic plates are input + QUnit.test("happy path", function (assert) { + let tbodyInnerHtml = '33Test plate 4<' + + 'td>2019-05-09 21:21:15.386036' + + 'Identification of the Microbiomes for ' + + 'Cannabis Soils' + + '30Test plate 3' + + '2019-05-09 21:21:15.386036Identification ' + + 'of the Microbiomes for Cannabis Soils' + + '' + + '' + + '27Test plate 2<' + + 'td>2019-05-09 21:21:15.386036' + + 'Identification of the Microbiomes for Cannabis ' + + 'Soils
Some other study' + + '' + + '21Test plate 1' + + '2019-05-09 21:21:15.386036' + + 'Identification of the Microbiomes for Cannabis ' + + 'Soils' + + ''; + let testPlateInfo = [ + [21, "Test plate 1", + "2019-05-09 21:21:15.386036", + ["Identification of the Microbiomes for Cannabis Soils"] + ], + [27, "Test plate 2", "2019-05-09 21:21:15.386036", + ["Identification of the Microbiomes for Cannabis Soils", + "Some other study"] + ], + [30, "Test plate 3", "2019-05-09 21:21:15.386036", + ["Identification of the Microbiomes for Cannabis Soils"] + ], + [33, "Test plate 4", "2019-05-09 21:21:15.386036", + ["Identification of the Microbiomes for Cannabis Soils"] + ] + ]; + + // before running function under test, table has no tbody elements + assert.equal($(qunitTbodySelectorStr).length, 0); + + populatePlateTable(testPlateInfo, "#searchPlateTable", + "addAplate"); + + // after running function under test, table has 1 tbody element + // containing rows for 4 plates + let tbodysList = $($(qunitTbodySelectorStr)); + assert.equal(tbodysList.length, 1); + assert.equal(tbodysList[0].innerHTML, tbodyInnerHtml) + }); + + // Verify correct behavior when no plates are input + QUnit.test("no plates", function (assert) { + let tbodyInnerHtml = 'No data available in ' + + 'table'; + + // before running function under test, table has no tbody elements + assert.equal($(qunitTbodySelectorStr).length, 0); + + populatePlateTable([], "#searchPlateTable", "addAplate"); + + // after running function under test, table has 1 tbody element + // containing a row saying there are no records + let tbodysList = $($(qunitTbodySelectorStr)); + assert.equal(tbodysList.length, 1); + assert.equal(tbodysList[0].innerHTML, tbodyInnerHtml) + }); + }); +}); + + diff --git a/labcontrol/gui/static/js/addPlateModal.js b/labcontrol/gui/static/js/addPlateModal.js index cda231dc..0a055001 100644 --- a/labcontrol/gui/static/js/addPlateModal.js +++ b/labcontrol/gui/static/js/addPlateModal.js @@ -1,38 +1,131 @@ // NB: Any page importing this script MUST define a function // addPlate that takes 1 parameter (the plate id) and does the work // of actually collecting the necessary info from the plate the -// user has chosen to add. +// user has chosen to add for use by the specific page in question. -// plateTypeList: a list of the types of plates to include in the add plate -// modal; an acceptable value for plateTypeList would be, e.g., +// Additionally, this script requires the following javascript libraries: +// jquery +// datatables + +// This method creates the plate modal div that is filled and then +// shown/hidden by other methods in this library +function insertPlateModalDiv(elementIdToAppendTo = undefined, + addPlateModalId = "addPlateModal", + plateTableId = "searchPlateTable"){ + let div = $(""); + + let selectorStr = "body"; + if (elementIdToAppendTo !== undefined){ + selectorStr = "#" + elementIdToAppendTo + } + + div.appendTo($(selectorStr)); +} + +// This method makes a get call to retrieve a list of information (specified +// by the server-side handler, not here) about each +// plate with the specified type and quantification state. +// plateTypeList: a list of the types of plates to include results; +// an acceptable value for plateTypeList would be, e.g., // ['gDNA', 'compressed gDNA']. // getOnlyQuantified: a boolean that is true if the only plates included in -// the add plate modal should be those that have already been quantified, +// the results should be those that have already been quantified, // false if all plates of the relevant types should be included. -function setUpAddPlateModal(plateTypeList, getOnlyQuantified) { +function getPlateListPromise(plateTypeList, getOnlyQuantified) { + // NB: This does NOT return a list of plate info--instead, it returns a + // "promise" that will eventually provide the user access to a list of + // plate info--AFTER the asynchronous ajax call is finished, whenever + // that turns out to be. + return $.ajax({ + url: '/plate_list', + type: 'GET', + data: { + 'plate_type': JSON.stringify(plateTypeList), + 'only_quantified': getOnlyQuantified + } + }) + .fail(function(xhr, ajaxOptions, thrownError){ + // Probably some more detailed error handling is called for, but + // a little is better than none. + alert("getPlateListPromise ajax call failed with error " + + xhr.status + ": '" + thrownError + "'."); + }) +} + +/* +* This method creates and populates a DataTables object with the input +* plateListInfo, and sets it as the contents of the dom object with the +* JQuery selector tableSelectorStr (which must already exist in the dom). +* tableSelectorStr is the string specifying the JQuery selector for the table +* to be populated, e.g. "#searchPlateTable". +* +* plateListInfo is a nested list of the format +* [ +* [21, "Test plate 1", "2019-05-09 21:21:15.386036", +* ["Identification of the Microbiomes for Cannabis Soils"] +* ], +* [27, "Test plate 2", "2019-05-09 21:21:15.386036", +* ["Identification of the Microbiomes for Cannabis Soils", +* "Some Other Qiita Study"] +* ] +* ] +* ... where the outer list contains one entry for each plate, which in turn +* contains four entries: the plate id, the plate external id (e.g., name), +* the date the plate was created, and a list of the names of all the Qiita +* studies to which ANY of the samples on the plate belong. +* specificAddPlateBtnBaseId is the base part--that is, the part that doesn't +* actually include the plate id--of the the string id for the button one +* clicks to add a particular, specific plate; e.g., "addBtnPlate". +*/ +function populatePlateTable(plateListInfo, tableSelectorStr, + specificAddPlateBtnBaseId) { // populate the table of potential plates to add - var table = $('#searchPlateTable').DataTable({ - 'ajax': {'url': '/plate_list', - 'data': { - 'plate_type': JSON.stringify(plateTypeList), - 'only_quantified': getOnlyQuantified - } - }, + return $(tableSelectorStr).DataTable({ + 'data': plateListInfo, 'columnDefs': [ { - 'targets': -1, // last column - 'data': null, - 'render': function (data, type, row, meta) { - var plateId = data[0]; - return ""; + 'targets': -1, // last column + 'data': null, + 'render': function (data, type, row, meta) { + var plateId = data[0]; + return ""; } }, { - 'targets': -2, // second to last column - 'data': null, - 'render': function (data, type, row, meta) { - // index 3 in row is the list of studies - return data[3].join('
'); + 'targets': -2, // second to last column + 'data': null, + 'render': function (data, type, row, meta) { + // index 3 in row is the list of studies + return data[3].join('
'); } } ], @@ -44,19 +137,33 @@ function setUpAddPlateModal(plateTypeList, getOnlyQuantified) { 'order': [[0, "desc"]] // order rows in desc order by plate id } ); +} - // Remove any existing event handler already attached to plate add buttons - $('#searchPlateTable tbody').off('click', 'button'); - - // Add function called by clicking on one of the buttons that adds a plate - $('#searchPlateTable tbody').on('click', 'button', function () { - var plateId = table.row($(this).parents('tr')).data()[0]; +// This method generates a function that, when called, runs addPlate() for a +// specific plateId and appropriately enables/disables the add plate modal and +// buttons. Closures are used to make that function parameterless. The +// returned function is intended to be used as the onclick action of a button +// embedded in a row in a DataTables object, and won't act correctly if it +// is used elsewhere. +// dataTable is the actual DataTables.js object produced by a call to +// $(#).DataTable(). +// addPlateModalId is the string id of the div that represents the plate modal, +// e.g. "addPlateModal". +// specificAddPlateBtnBaseId is the base part--that is, the part that doesn't +// actually include the plate id--of the the string id for the button one +// clicks to add a particular, specific plate; e.g., "addBtnPlate". +function makeAddSpecificPlateButtonOnclick(dataTable, addPlateModalId, + specificAddPlateBtnBaseId) { + return function () { + let plateId = dataTable.row($(this).parents('tr')).data()[0]; + let addPlateModalSelector = $("#" + addPlateModalId); + let addPlateBtnSelector = $("#" + specificAddPlateBtnBaseId + plateId); // Hide the modal to add plates - $('#addPlateModal').modal('hide'); + addPlateModalSelector.modal('hide'); - // Disable the button to add this particular plate while we try to add it - $('#addBtnPlate' + plateId).prop('disabled', true); + // Disable button to add this particular plate while we try to add it + addPlateBtnSelector.prop('disabled', true); try { addPlate(plateId); @@ -64,7 +171,36 @@ function setUpAddPlateModal(plateTypeList, getOnlyQuantified) { // if adding this plate *failed*, re-enable the add plate button // for this plate so the user can try again if they fix their // problem. - $('#addBtnPlate' + plateId).prop('disabled', false); + addPlateBtnSelector.prop('disabled', false); } + } +} + +function setUpAddPlateModal(plateTypeList, getOnlyQuantified, + plateTableId = "searchPlateTable", + addPlateModalId = "addPlateModal", + specificAddPlateBtnBaseId = "addBtnPlate") { + + // NOTA BENE: ajax calls are asynchronous! (synchronous are deprecated) + // This means we don't know when the result will come back and we can't do + // ANYTHING that depends on this result--i.e., everything in this function + // --until we know it is done. Failure handling is not specific to this + // function so it is specified in getPlateListPromise. + getPlateListPromise(plateTypeList, getOnlyQuantified) + .done(function (data) { + //The data come back as in a dictionary object named data, which + // contains just one entry--also with the key "data" + let plateListInfo = (data["data"]); + let tableSelectorStr = "#" + plateTableId; + let table = populatePlateTable(tableSelectorStr, plateListInfo); + + let tbodySelector = $(tableSelectorStr + ' tbody'); + // Remove any existing event handler already attached to plate add btns + tbodySelector.off('click', 'button'); + + // Add function called by clicking on one of the btns that adds a plate + let clickAddSpecificPlate = makeAddSpecificPlateButtonOnclick(table, + addPlateModalId, specificAddPlateBtnBaseId); + tbodySelector.on('click', 'button', clickAddSpecificPlate); }); } \ No newline at end of file