From 2ccef844266796f05443760148c1de3edc868b38 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Tue, 4 Feb 2025 15:42:54 -0700 Subject: [PATCH 01/29] initial commit, added jest files --- .gitignore | 3 +++ jest.config.js | 8 ++++++++ package.json | 12 ++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 jest.config.js create mode 100644 package.json diff --git a/.gitignore b/.gitignore index da65bfd81..e1358d463 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ tethys_portal/_version.py # Required for docs build git-lfs-*/* conda.recipe/meta.yaml + +jest_coverage/ +package-lock.json \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..dda6d78c6 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: "node", + roots: ["js-tests"], + collectCoverage: true, + coverageDirectory: "jest_coverage", + coverageReporters: ["text", "lcov"], + moduleFileExtensions: ["js", "jsx", "json", "node"], + }; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 000000000..a8f2d9c2a --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "devDependencies": { + "@babel/preset-env": "^7.26.7", + "babel-jest": "^29.7.0", + "jest": "^29.7.0" + }, + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" + } +} From d0e7320e672923efc33bfec3a5f255cc3c88e502 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Wed, 5 Feb 2025 18:49:23 -0700 Subject: [PATCH 02/29] added jest test for datable_view and select_input gizmos, updated configurations for testing --- .babelrc | 3 + jest.config.js | 3 +- js-tests/gizmos/datable_view.test.js | 49 +++++++++++++ js-tests/gizmos/select_input.test.js | 68 +++++++++++++++++++ js-tests/setup.js | 16 +++++ package.json | 4 +- .../static/tethys_gizmos/js/datatable_view.js | 6 ++ .../static/tethys_gizmos/js/select_input.js | 6 ++ 8 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 .babelrc create mode 100644 js-tests/gizmos/datable_view.test.js create mode 100644 js-tests/gizmos/select_input.test.js create mode 100644 js-tests/setup.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 000000000..ff3059c3f --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@babel/preset-env"] +} \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index dda6d78c6..ebc2dc54a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,7 @@ module.exports = { testEnvironment: "node", - roots: ["js-tests"], + roots: ["/js-tests"], + setupFilesAfterEnv: ["/js-tests/setup.js"], collectCoverage: true, coverageDirectory: "jest_coverage", coverageReporters: ["text", "lcov"], diff --git a/js-tests/gizmos/datable_view.test.js b/js-tests/gizmos/datable_view.test.js new file mode 100644 index 000000000..04990f414 --- /dev/null +++ b/js-tests/gizmos/datable_view.test.js @@ -0,0 +1,49 @@ +import $ from "jquery"; + +// Mock jQuery's DataTables plugin +$.fn.DataTable = jest.fn(); + +// Function to reload the module to trigger document ready function +const reloadDatatableView = () => { + jest.resetModules(); // Clear Jest’s module cache + return require("../../tethys_gizmos/static/tethys_gizmos/js/datatable_view"); +}; + +describe("TETHYS_DATATABLE_VIEW", () => { + beforeEach(() => { + document.body.innerHTML = ` + + + + + + + + +
Column 1Column 2
Data 1Data 2
Data 3Data 4
+ `; + $.fn.DataTable = jest.fn(); + }); + + test("should initialize DataTables on document ready", (done) => { + const dataTableSpy = jest.spyOn($.fn, "DataTable"); + reloadDatatableView(); + + // Wait for jQuery's document ready function + setTimeout(() => { + // Ensure DataTables was called + expect(dataTableSpy).toHaveBeenCalled(); + dataTableSpy.mockRestore(); + done(); + }, 100); + }); + + test("should initialize DataTables when called manually", () => { + const TETHYS_DATATABLE_VIEW = reloadDatatableView(); + const tableElement = $(".data_table_gizmo_view"); + // Call the initialization function manually + TETHYS_DATATABLE_VIEW.initTableView(tableElement); + // Ensure DataTables was called + expect(tableElement.DataTable).toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/js-tests/gizmos/select_input.test.js b/js-tests/gizmos/select_input.test.js new file mode 100644 index 000000000..8e47a4894 --- /dev/null +++ b/js-tests/gizmos/select_input.test.js @@ -0,0 +1,68 @@ +import $ from "jquery"; + +$.fn.select2 = jest.fn(); + +const TETHYS_SELECT_INPUT = require("../../tethys_gizmos/static/tethys_gizmos/js/select_input"); + +const reloadSelect2 = () => { + jest.resetModules(); // Clear Jest’s module cache + return require("../../tethys_gizmos/static/tethys_gizmos/js/select_input"); +}; + +describe("TETHYS_SELECT_INPUT", () => { + beforeEach(() => { + // Set up a mock DOM element for testing + document.body.innerHTML = ` + + `; + }); + + test("should initialize Select2 on document ready", (done) => { + const select2Spy = jest.spyOn($.fn, "select2"); + reloadSelect2(); + + // Wait for jQuery's document ready function + setTimeout(() => { + // Ensure select2 was called + expect(select2Spy).toHaveBeenCalled(); + select2Spy.mockRestore(); + done(); + }, 100); + }) + + test("Module should have initSelectInput function", () => { + expect(TETHYS_SELECT_INPUT).toHaveProperty("initSelectInput"); + expect(typeof TETHYS_SELECT_INPUT.initSelectInput).toBe("function"); + }); + + test("initSelectInput should initialize Select2 on elements", () => { + const selectElement = $(".tethys-select2"); + + TETHYS_SELECT_INPUT.initSelectInput(selectElement); + + expect($.fn.select2).toHaveBeenCalledTimes(1); + }); + + test("jQuery should call select2 with correct options", () => { + const selectElement = $(".tethys-select2"); + + // Mock jQuery's .data() method to return select2 options + $.fn.data = jest.fn(() => ({ placeholder: "Select an option" })); + + TETHYS_SELECT_INPUT.initSelectInput(selectElement); + + expect($.fn.select2).toHaveBeenCalledWith({ placeholder: "Select an option" }); + }); + + test("Automatically initializes on page load", () => { + // Ensure that the select2 function was called during module initialization + expect($.fn.select2).toHaveBeenCalled(); + }); + +}); + + + \ No newline at end of file diff --git a/js-tests/setup.js b/js-tests/setup.js new file mode 100644 index 000000000..fb0fca0ae --- /dev/null +++ b/js-tests/setup.js @@ -0,0 +1,16 @@ +import { JSDOM } from "jsdom"; + + +const dom = new JSDOM("", { + url: "http://localhost", +}); + +global.window = dom.window; +global.document = dom.window.document; + +const $ = require("jquery"); + +global.$ = $; +global.jQuery = $; + +global.$.fn = global.$.fn || {}; \ No newline at end of file diff --git a/package.json b/package.json index a8f2d9c2a..a63843f44 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,9 @@ "devDependencies": { "@babel/preset-env": "^7.26.7", "babel-jest": "^29.7.0", - "jest": "^29.7.0" + "jest": "^29.7.0", + "jquery": "^3.7.1", + "jsdom": "^26.0.0" }, "scripts": { "test": "jest", diff --git a/tethys_gizmos/static/tethys_gizmos/js/datatable_view.js b/tethys_gizmos/static/tethys_gizmos/js/datatable_view.js index 07fc78d35..f3ba34e1e 100644 --- a/tethys_gizmos/static/tethys_gizmos/js/datatable_view.js +++ b/tethys_gizmos/static/tethys_gizmos/js/datatable_view.js @@ -51,3 +51,9 @@ var TETHYS_DATATABLE_VIEW = (function() { /***************************************************************************** * Public Functions *****************************************************************************/ + +/* This statement for testing coverage purposes */ +/* istanbul ignore next */ +if (typeof module !== "undefined" && module.exports) { + module.exports = { ...TETHYS_DATATABLE_VIEW} ; +} \ No newline at end of file diff --git a/tethys_gizmos/static/tethys_gizmos/js/select_input.js b/tethys_gizmos/static/tethys_gizmos/js/select_input.js index 38005e595..444882f60 100644 --- a/tethys_gizmos/static/tethys_gizmos/js/select_input.js +++ b/tethys_gizmos/static/tethys_gizmos/js/select_input.js @@ -55,3 +55,9 @@ var TETHYS_SELECT_INPUT = (function() { /***************************************************************************** * Public Functions *****************************************************************************/ + +/* This statement for testing coverage purposes */ +/* istanbul ignore next */ +if (typeof module !== "undefined" && module.exports) { + module.exports = TETHYS_SELECT_INPUT; +} \ No newline at end of file From 5536f7c730b968cd596edd98576c7e02b2f171e7 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Wed, 5 Feb 2025 19:47:25 -0700 Subject: [PATCH 03/29] added testing for slide_sheet --- js-tests/gizmos/slide_sheet.test.js | 51 +++++++++++++++++++ .../static/tethys_gizmos/js/slide_sheet.js | 15 +++++- 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 js-tests/gizmos/slide_sheet.test.js diff --git a/js-tests/gizmos/slide_sheet.test.js b/js-tests/gizmos/slide_sheet.test.js new file mode 100644 index 000000000..2ee3596dc --- /dev/null +++ b/js-tests/gizmos/slide_sheet.test.js @@ -0,0 +1,51 @@ +import $ from "jquery"; + +// Mock jQuery functions +$.fn.addClass = jest.fn(); +$.fn.removeClass = jest.fn(); + +// Import the module AFTER mocking jQuery +const SLIDE_SHEET = require("../../tethys_gizmos/static/tethys_gizmos/js/slide_sheet"); + +describe("SLIDE_SHEET", () => { + beforeEach(() => { + jest.clearAllMocks(); // Reset mocks before each test + + // Set up a mock DOM element for testing + document.body.innerHTML = ` +
+ `; + }); + + test("Module should have open and close functions", () => { + expect(SLIDE_SHEET).toHaveProperty("open"); + expect(typeof SLIDE_SHEET.open).toBe("function"); + + expect(SLIDE_SHEET).toHaveProperty("close"); + expect(typeof SLIDE_SHEET.close).toBe("function"); + }); + + test("open should add 'show' class to slide-sheet", () => { + SLIDE_SHEET.open("test-slide"); + + expect($("#test-slide.slide-sheet").addClass).toHaveBeenCalledWith("show"); + }); + + test("close should remove 'show' class from slide-sheet", () => { + SLIDE_SHEET.close("test-slide"); + + expect($("#test-slide.slide-sheet").removeClass).toHaveBeenCalledWith("show"); + }); + + test("open does nothing if id is empty", () => { + SLIDE_SHEET.open(""); + + expect($.fn.addClass).not.toHaveBeenCalled(); + }); + + test("close does nothing if id is empty", () => { + SLIDE_SHEET.close(""); + + expect($.fn.removeClass).not.toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js b/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js index 0c2a4ccc2..1ea6a5e91 100644 --- a/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js +++ b/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js @@ -26,12 +26,14 @@ var SLIDE_SHEET = (function() { *************************************************************************/ open = function(id) { // Check that id is not empty + console.log("Running open..") if (id.length) { $('#' + id + '.slide-sheet').addClass('show'); } }; close = function(id) { + console.log("Running Close...") // Check that id is not empty if (id.length) { $('#' + id + '.slide-sheet').removeClass('show'); @@ -43,9 +45,11 @@ var SLIDE_SHEET = (function() { *************************************************************************/ public_interface = { open: function(id) { + console.log("Running open public interface...") open(id); }, close: function(id) { + console.log("Running ") close(id); }, }; @@ -56,10 +60,17 @@ var SLIDE_SHEET = (function() { // Initialization: jQuery function that gets called when // the DOM tree finishes loading - $(document).ready(function(){}); + // Commented out because it is not needed for now + // $(document).ready(function(){}); return public_interface; }()); // End of package wrapper // NOTE: that the call operator (open-closed parenthesis) is used to invoke the library wrapper -// function immediately after being parsed. \ No newline at end of file +// function immediately after being parsed. + +/* This statement for testing coverage purposes */ +/* istanbul ignore next */ +if (typeof module !== "undefined" && module.exports) { + module.exports = SLIDE_SHEET; +} \ No newline at end of file From 75a7f68109e109995ba0181f2ec2d49828325fb4 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Wed, 5 Feb 2025 20:28:47 -0700 Subject: [PATCH 04/29] added frontend testing for toggle_switch gizmo --- js-tests/gizmos/datable_view.test.js | 24 +++++----- js-tests/gizmos/select_input.test.js | 6 --- js-tests/gizmos/toggle_switch.test.js | 44 +++++++++++++++++++ .../static/tethys_gizmos/js/toggle_switch.js | 6 +++ 4 files changed, 62 insertions(+), 18 deletions(-) create mode 100644 js-tests/gizmos/toggle_switch.test.js diff --git a/js-tests/gizmos/datable_view.test.js b/js-tests/gizmos/datable_view.test.js index 04990f414..9a9abdeec 100644 --- a/js-tests/gizmos/datable_view.test.js +++ b/js-tests/gizmos/datable_view.test.js @@ -25,7 +25,17 @@ describe("TETHYS_DATATABLE_VIEW", () => { $.fn.DataTable = jest.fn(); }); - test("should initialize DataTables on document ready", (done) => { + + + test("should initialize DataTables when called manually", () => { + const TETHYS_DATATABLE_VIEW = reloadDatatableView(); + const tableElement = $(".data_table_gizmo_view"); + // Call the initialization function manually + TETHYS_DATATABLE_VIEW.initTableView(tableElement); + // Ensure DataTables was called + expect(tableElement.DataTable).toHaveBeenCalled(); + }); +});test("should initialize DataTables on document ready", (done) => { const dataTableSpy = jest.spyOn($.fn, "DataTable"); reloadDatatableView(); @@ -36,14 +46,4 @@ describe("TETHYS_DATATABLE_VIEW", () => { dataTableSpy.mockRestore(); done(); }, 100); - }); - - test("should initialize DataTables when called manually", () => { - const TETHYS_DATATABLE_VIEW = reloadDatatableView(); - const tableElement = $(".data_table_gizmo_view"); - // Call the initialization function manually - TETHYS_DATATABLE_VIEW.initTableView(tableElement); - // Ensure DataTables was called - expect(tableElement.DataTable).toHaveBeenCalled(); - }); -}); \ No newline at end of file + }); \ No newline at end of file diff --git a/js-tests/gizmos/select_input.test.js b/js-tests/gizmos/select_input.test.js index 8e47a4894..75ffe99bc 100644 --- a/js-tests/gizmos/select_input.test.js +++ b/js-tests/gizmos/select_input.test.js @@ -4,11 +4,6 @@ $.fn.select2 = jest.fn(); const TETHYS_SELECT_INPUT = require("../../tethys_gizmos/static/tethys_gizmos/js/select_input"); -const reloadSelect2 = () => { - jest.resetModules(); // Clear Jest’s module cache - return require("../../tethys_gizmos/static/tethys_gizmos/js/select_input"); -}; - describe("TETHYS_SELECT_INPUT", () => { beforeEach(() => { // Set up a mock DOM element for testing @@ -22,7 +17,6 @@ describe("TETHYS_SELECT_INPUT", () => { test("should initialize Select2 on document ready", (done) => { const select2Spy = jest.spyOn($.fn, "select2"); - reloadSelect2(); // Wait for jQuery's document ready function setTimeout(() => { diff --git a/js-tests/gizmos/toggle_switch.test.js b/js-tests/gizmos/toggle_switch.test.js new file mode 100644 index 000000000..fa01514d9 --- /dev/null +++ b/js-tests/gizmos/toggle_switch.test.js @@ -0,0 +1,44 @@ +import $ from "jquery"; + +// Mock Bootstrap Switch plugin +$.fn.bootstrapSwitch = jest.fn(); + +// Import the module +const TETHYS_TOGGLE_SWITCH = require("../../tethys_gizmos/static/tethys_gizmos/js/toggle_switch"); + +describe("TETHYS_TOGGLE_SWITCH", () => { + beforeEach(() => { + jest.clearAllMocks(); // Reset mocks before each test + + // Set up a mock DOM element for testing + document.body.innerHTML = ` + + `; + }); + + test("Module should have initToggleSwitch function", () => { + expect(TETHYS_TOGGLE_SWITCH).toHaveProperty("initToggleSwitch"); + expect(typeof TETHYS_TOGGLE_SWITCH.initToggleSwitch).toBe("function"); + }); + + test("initToggleSwitch should initialize Bootstrap Switch", () => { + const switchElement = $(".bootstrap-switch"); + + TETHYS_TOGGLE_SWITCH.initToggleSwitch(switchElement); + + expect($.fn.bootstrapSwitch).toHaveBeenCalledTimes(1); + }); + + test("Automatically initializes switch elements on page load", (done) => { + // Ensure that the bootstrapSwitch function was called during module initialization + const switchSpy = jest.spyOn($.fn, "bootstrapSwitch"); + + // Wait for jQuery's document ready function + setTimeout(() => { + // Ensure bootstrapSwitch was called + expect(switchSpy).toHaveBeenCalled(); + switchSpy.mockRestore(); + done(); + }, 100); + }); +}); \ No newline at end of file diff --git a/tethys_gizmos/static/tethys_gizmos/js/toggle_switch.js b/tethys_gizmos/static/tethys_gizmos/js/toggle_switch.js index 109d9e785..e7d0dc7f6 100644 --- a/tethys_gizmos/static/tethys_gizmos/js/toggle_switch.js +++ b/tethys_gizmos/static/tethys_gizmos/js/toggle_switch.js @@ -54,3 +54,9 @@ var TETHYS_TOGGLE_SWITCH = (function() { /***************************************************************************** * Public Functions *****************************************************************************/ + +/* This statement for testing coverage purposes */ +/* istanbul ignore next */ +if (typeof module !== "undefined" && module.exports) { + module.exports = { ...TETHYS_TOGGLE_SWITCH} ; +} \ No newline at end of file From e41ae1247d9b4191da802454c851864347ad4466 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Wed, 5 Feb 2025 22:26:16 -0700 Subject: [PATCH 05/29] added testing for range_slider gizmo - may need to look at public_interface in range_slider.js --- js-tests/gizmos/range_slider.test.js | 46 +++++++++++++++++++ .../static/tethys_gizmos/js/range_slider.js | 12 ++++- 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 js-tests/gizmos/range_slider.test.js diff --git a/js-tests/gizmos/range_slider.test.js b/js-tests/gizmos/range_slider.test.js new file mode 100644 index 000000000..455c0f6e4 --- /dev/null +++ b/js-tests/gizmos/range_slider.test.js @@ -0,0 +1,46 @@ +import $ from "jquery"; + +// Import module AFTER mocking jQuery +const TETHYS_RANGE_SLIDER = require("../../tethys_gizmos/static/tethys_gizmos/js/range_slider"); + +describe("TETHYS_RANGE_SLIDER", () => { + beforeEach(() => { + jest.clearAllMocks(); + + // Set up a mock DOM element for testing + document.body.innerHTML = ` + + 50 + `; + }); + + test("Should initialize range sliders on page load", (done) => { + // Wait for jQuery's document ready function + setTimeout(() => { + const rangeInput = document.querySelector(".form-range"); + const rangeValue = document.querySelector(".range-value"); + + expect(rangeInput).not.toBeNull(); + expect(rangeValue).not.toBeNull(); + + done(); + }, 100); + }); + + test("Should update value display when slider is moved", () => { + const rangeInput = document.querySelector(".form-range"); + const rangeValue = document.querySelector(".range-value"); + + // Manually initialize range sliders + TETHYS_RANGE_SLIDER.init_range_sliders(); + rangeInput.value = "75"; + + // Trigger the input event + const event = document.createEvent("Event"); + event.initEvent("input", true, true); + rangeInput.dispatchEvent(event); + + // Check if the slider's value has updated + expect(rangeValue.innerHTML).toBe("75"); + }); +}); \ No newline at end of file diff --git a/tethys_gizmos/static/tethys_gizmos/js/range_slider.js b/tethys_gizmos/static/tethys_gizmos/js/range_slider.js index af38fd4c0..21c842220 100644 --- a/tethys_gizmos/static/tethys_gizmos/js/range_slider.js +++ b/tethys_gizmos/static/tethys_gizmos/js/range_slider.js @@ -27,6 +27,7 @@ var TETHYS_RANGE_SLIDER = (function() { init_range_sliders = function() { document.querySelectorAll('.form-range').forEach(element => { element.addEventListener('input', function(e){ + console.log("Changed: ", element.value); element.nextElementSibling.innerHTML = element.value; }); }); @@ -38,7 +39,10 @@ var TETHYS_RANGE_SLIDER = (function() { /* * Library object that contains public facing functions of the package. */ - public_interface = {}; + // TODO Make sure this is ok + public_interface = { + init_range_sliders: init_range_sliders, + }; // Initialization: jQuery function that gets called when // the DOM tree finishes loading @@ -49,3 +53,9 @@ var TETHYS_RANGE_SLIDER = (function() { return public_interface; }()); // End of package wrapper + +/* This statement for testing coverage purposes */ +/* istanbul ignore next */ +if (typeof module !== "undefined" && module.exports) { + module.exports = TETHYS_RANGE_SLIDER; +} \ No newline at end of file From c5879f1203551c75748f747574d119f430a02296 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Thu, 6 Feb 2025 17:29:11 -0700 Subject: [PATCH 06/29] added testing for gizmo_utilities file --- js-tests/gizmos/gizmo_utilities.test.js | 50 +++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 js-tests/gizmos/gizmo_utilities.test.js diff --git a/js-tests/gizmos/gizmo_utilities.test.js b/js-tests/gizmos/gizmo_utilities.test.js new file mode 100644 index 000000000..4d8585b42 --- /dev/null +++ b/js-tests/gizmos/gizmo_utilities.test.js @@ -0,0 +1,50 @@ +import $ from "jquery"; + +require("../../tethys_gizmos/static/tethys_gizmos/js/gizmo_utilities"); + +describe("gizmo_utilities.js", () => { + let testElement; + + beforeEach(() => { + // Set up a mock DOM element for testing + document.body.innerHTML = `
`; + testElement = $("#test-element"); + }); + + afterEach(() => { + clearInterval(testElement[0].sizeTO); + }); + + test("should define $.fn.changeSize", () => { + expect($.fn.changeSize).toBeDefined(); + }); + + test("should call the callback function ", (done) => { + const callback = jest.fn(); + + testElement.changeSize(callback); + + setTimeout(() => { + testElement.width(200); + testElement.height(200); + }, 50); + + setTimeout(() => { + expect(callback).toHaveBeenCalledWith(testElement); + done(); + }, 200); + }); + + test("should not trigger callback if size remains the same", (done) => { + const callback = jest.fn(); + + testElement.changeSize(callback); + + setTimeout(() => { + expect(callback).not.toHaveBeenCalled(); + done(); + }, 200); + }); + + +}); \ No newline at end of file From 7c65357736204422c9bd2de3466bdf5648511c20 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Tue, 11 Feb 2025 16:40:46 -0700 Subject: [PATCH 07/29] initial testing for cesium_map_view removed console.logs in gizmo js files --- jest.config.js | 6 +- js-tests/__mocks__/cesium.js | 46 ++++++ js-tests/gizmos/cesium_map_view.test.js | 142 ++++++++++++++++++ js-tests/gizmos/datable_view.test.js | 2 - js-tests/setup.js | 17 ++- package.json | 1 + .../tethys_gizmos/js/cesium_map_view.js | 41 +++++ .../static/tethys_gizmos/js/range_slider.js | 1 - .../static/tethys_gizmos/js/slide_sheet.js | 4 - 9 files changed, 251 insertions(+), 9 deletions(-) create mode 100644 js-tests/__mocks__/cesium.js create mode 100644 js-tests/gizmos/cesium_map_view.test.js diff --git a/jest.config.js b/jest.config.js index ebc2dc54a..7c6bef104 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,9 +1,13 @@ module.exports = { - testEnvironment: "node", + testEnvironment: "", roots: ["/js-tests"], setupFilesAfterEnv: ["/js-tests/setup.js"], collectCoverage: true, coverageDirectory: "jest_coverage", coverageReporters: ["text", "lcov"], moduleFileExtensions: ["js", "jsx", "json", "node"], + moduleNameMapper: { + "^cesium$": "/js-tests/__mocks__/cesium.js", + }, + transformIgnorePatterns: ["/node_modules/"], }; \ No newline at end of file diff --git a/js-tests/__mocks__/cesium.js b/js-tests/__mocks__/cesium.js new file mode 100644 index 000000000..2b728ba32 --- /dev/null +++ b/js-tests/__mocks__/cesium.js @@ -0,0 +1,46 @@ +module.exports = { + Viewer: jest.fn().mockImplementation(() => ({ + scene: { + globe: {}, + primitives: { add: jest.fn() }, + }, + camera: { + setView: jest.fn(), + flyTo: jest.fn(), + lookAt: jest.fn(), + lookAtTransform: jest.fn(), + viewBoundingSphere: jest.fn(), + }, + imageryLayers: { addImageryProvider: jest.fn() }, + terrainProvider: jest.fn(), + entities: { add: jest.fn() }, + clock: {}, + dataSources: { add: jest.fn() }, + trackedEntity: null, + })), + Ion: { defaultAccessToken: "mock_token" }, + ClockViewModel: jest.fn(), + WebMapServiceImageryProvider: jest.fn(), + JulianDate: { + fromIso8601: jest.fn(), + toIso8601: jest.fn(), + }, + GeoJsonDataSource: { + load: jest.fn(), + }, + CzmlDataSource: { + load: jest.fn(), + }, + TimeIntervalCollection: { + fromIso8601DateArray: jest.fn(), + }, + Cartesian3: jest.fn(), + Color: jest.fn(), + HorizontalOrigin: {}, + VerticalOrigin: {}, + PointGraphics: jest.fn(), + Material: { + fromType: jest.fn(), + }, + }; + \ No newline at end of file diff --git a/js-tests/gizmos/cesium_map_view.test.js b/js-tests/gizmos/cesium_map_view.test.js new file mode 100644 index 000000000..da1bbac73 --- /dev/null +++ b/js-tests/gizmos/cesium_map_view.test.js @@ -0,0 +1,142 @@ +import $ from "jquery"; +import * as Cesium from "cesium"; + +global.Cesium = Cesium; + +const CESIUM_MAP_VIEW = require("../../tethys_gizmos/static/tethys_gizmos/js/cesium_map_view"); +const { _testOnly } = CESIUM_MAP_VIEW; + +jest.mock('cesium'); + +jest.mock("jquery" , () => { + return jest.fn(() => ({ + data: jest.fn((key) => { + const mockData = { + "cesium-ion-token": "mock_token", + clock: { + clock: { + currentTime: "2025-02-10T12:15:00Z", + startTime: "2025-02-10T12:00:00Z", + stopTime: "2025-02-10T12:30:00Z", + }, + }, + options: { shouldInitialize: true }, + }; + return mockData[key]; + }), + })); +}); + +beforeEach(() => { + jest.clearAllMocks(); + document.body.innerHTML = ` +
+ +
+ `; +}) + +describe("CESIUM_MAP_VIEW", () => { + test("should initialize the map correctly", () => { + expect(CESIUM_MAP_VIEW).toBeDefined(); + expect(typeof CESIUM_MAP_VIEW.getMap).toBe("function"); + expect(typeof CESIUM_MAP_VIEW.reInitializeMap).toBe("function"); + }); +}); + +describe("is_empty_or_undefined", () => { + test("should return true for undefined values", () => { + expect(_testOnly.is_empty_or_undefined(undefined)).toBe(true); + }); + + test("should return true for empty values", () => { + expect(_testOnly.is_empty_or_undefined("")).toBe(true); + expect(_testOnly.is_empty_or_undefined(null)).toBe(true); + }); + + test("should return true for non-empty values", () => { + expect(_testOnly.is_empty_or_undefined("test")).toBe(true); + expect(_testOnly.is_empty_or_undefined(0)).toBe(true); + }); + + test("should return true for an empty object", () => { + expect(_testOnly.is_empty_or_undefined({})).toBe(true); + }); + + test("should return false for non-empty object", () => { + expect(_testOnly.is_empty_or_undefined({ key: "value" })).toBe(false); + }); +}) + +describe("need_to_run", () => { + test("should return true if object contains a Cesium reference", () => { + const obj = { + someProperty: { + "Cesium.Cartesian3": [0, 0, 0], // This should trigger `true` + } + }; + expect(_testOnly.need_to_run(obj)).toBe(true); + }); + + test("should return true for nested Cesium references", () => { + const obj = { + level1: { + level2: { + level3: { + "Cesium.Color": "RED" + } + } + } + }; + expect(_testOnly.need_to_run(obj)).toBe(true); + }); + + test("should return false for objects without Cesium references", () => { + const obj = { + level1: { + level2: { + level3: { + someKey: "someValue", + } + } + } + }; + expect(_testOnly.need_to_run(obj)).toBe(false); + }); + + test("should return false for an empty object", () => { + expect(_testOnly.need_to_run({})).toBe(false); + }); + + test("should return false for non-object values", () => { + expect(_testOnly.need_to_run(null)).toBe(false); + expect(_testOnly.need_to_run(undefined)).toBe(false); + expect(_testOnly.need_to_run("string")).toBe(false); + expect(_testOnly.need_to_run(123)).toBe(false); + expect(_testOnly.need_to_run([])).toBe(false); + }); + + test("should return false when object has properties but no Cesium references", () => { + const obj = { + someKey: { + anotherKey: { + deeperKey: "notCesium" + } + } + }; + expect(_testOnly.need_to_run(obj)).toBe(false); + }); + + test("should return true when Cesium reference appears deeper in the object", () => { + const obj = { + someKey: { + anotherKey: { + deeperKey: { + "Cesium.SceneMode": "MORPHING" + } + } + } + }; + expect(_testOnly.need_to_run(obj)).toBe(true); + }); +}); diff --git a/js-tests/gizmos/datable_view.test.js b/js-tests/gizmos/datable_view.test.js index 9a9abdeec..7d91e4b5f 100644 --- a/js-tests/gizmos/datable_view.test.js +++ b/js-tests/gizmos/datable_view.test.js @@ -25,8 +25,6 @@ describe("TETHYS_DATATABLE_VIEW", () => { $.fn.DataTable = jest.fn(); }); - - test("should initialize DataTables when called manually", () => { const TETHYS_DATATABLE_VIEW = reloadDatatableView(); const tableElement = $(".data_table_gizmo_view"); diff --git a/js-tests/setup.js b/js-tests/setup.js index fb0fca0ae..8d16222a3 100644 --- a/js-tests/setup.js +++ b/js-tests/setup.js @@ -1,5 +1,20 @@ -import { JSDOM } from "jsdom"; +import { TextEncoder, TextDecoder } from "util"; + +if (typeof global.TextEncoder === "undefined") { + global.TextEncoder = TextEncoder; +} + +if (typeof global.TextDecoder === "undefined") { + global.TextDecoder = TextDecoder; +} +try { + require("cesium"); + } catch (e) { + jest.mock("cesium", () => require("./__mocks__/cesium")); + } + +import { JSDOM } from "jsdom"; const dom = new JSDOM("", { url: "http://localhost", diff --git a/package.json b/package.json index a63843f44..f0dfe4a53 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "@babel/preset-env": "^7.26.7", "babel-jest": "^29.7.0", "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "jquery": "^3.7.1", "jsdom": "^26.0.0" }, diff --git a/tethys_gizmos/static/tethys_gizmos/js/cesium_map_view.js b/tethys_gizmos/static/tethys_gizmos/js/cesium_map_view.js index 6baf5a513..f4460effa 100644 --- a/tethys_gizmos/static/tethys_gizmos/js/cesium_map_view.js +++ b/tethys_gizmos/static/tethys_gizmos/js/cesium_map_view.js @@ -896,8 +896,49 @@ var CESIUM_MAP_VIEW = (function() { cesium_initialize_all(); }); + // Expose private functions for testing + public_interface._testOnly = { + cesium_base_map_init, + cesium_globe_init, + cesium_map_view_init, + cesium_initialize_all, + cesium_widgets_init, + clock_options_init, + cesium_view, + cesium_terrain, + cesium_image_layers, + cesium_load_model, + cesium_load_entities, + cesium_load_primitives, + cesium_models, + update_field, + option_checker, + cesium_shadow_options, + textarea_string_dict, + cesium_logging, + is_defined, + is_empty_or_undefined, + in_array, + string_to_object, + string_to_function, + string_w_arg_to_function, + build_options, + build_options_string, + need_to_run, + cesium_options, + json_parser, + clear_data, + cesium_time_callback + }; + return public_interface; }()); // End of package wrapper // NOTE: that the call operator (open-closed parenthesis) is used to invoke the library wrapper // function immediately after being parsed. + +/* This statement for testing coverage purposes */ +/* istanbul ignore next */ +if (typeof module !== "undefined" && module.exports) { + module.exports = CESIUM_MAP_VIEW; +} diff --git a/tethys_gizmos/static/tethys_gizmos/js/range_slider.js b/tethys_gizmos/static/tethys_gizmos/js/range_slider.js index 21c842220..516c21cea 100644 --- a/tethys_gizmos/static/tethys_gizmos/js/range_slider.js +++ b/tethys_gizmos/static/tethys_gizmos/js/range_slider.js @@ -27,7 +27,6 @@ var TETHYS_RANGE_SLIDER = (function() { init_range_sliders = function() { document.querySelectorAll('.form-range').forEach(element => { element.addEventListener('input', function(e){ - console.log("Changed: ", element.value); element.nextElementSibling.innerHTML = element.value; }); }); diff --git a/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js b/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js index 1ea6a5e91..01084a73a 100644 --- a/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js +++ b/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js @@ -26,14 +26,12 @@ var SLIDE_SHEET = (function() { *************************************************************************/ open = function(id) { // Check that id is not empty - console.log("Running open..") if (id.length) { $('#' + id + '.slide-sheet').addClass('show'); } }; close = function(id) { - console.log("Running Close...") // Check that id is not empty if (id.length) { $('#' + id + '.slide-sheet').removeClass('show'); @@ -45,11 +43,9 @@ var SLIDE_SHEET = (function() { *************************************************************************/ public_interface = { open: function(id) { - console.log("Running open public interface...") open(id); }, close: function(id) { - console.log("Running ") close(id); }, }; From 5480be54febc8dbb4eb744e7d04542f4c3a58974 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Tue, 11 Feb 2025 16:46:38 -0700 Subject: [PATCH 08/29] added jest testing to CI --- .github/workflows/tethys.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/tethys.yml b/.github/workflows/tethys.yml index 984eb7c7a..d723e6be2 100644 --- a/.github/workflows/tethys.yml +++ b/.github/workflows/tethys.yml @@ -92,6 +92,25 @@ jobs: conda activate tethys coveralls --service=github + frontend-tests: + name: Frontend Tests With Jest + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Set Up Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: "npm" + + - name: Install Dependencies + run: npm install + + - name: Run Tests + run: npm test + docker-build: name: Docker Build (${{ matrix.platform }}, ${{ matrix.django-version }}, ${{ matrix.python-version }}) runs-on: ${{ matrix.platform }} From dab91245f803c786b86de069a3ee5fb10f0a88e7 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Wed, 12 Feb 2025 10:17:37 -0700 Subject: [PATCH 09/29] removed caching in frontend testing --- .github/workflows/tethys.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tethys.yml b/.github/workflows/tethys.yml index d723e6be2..611311a30 100644 --- a/.github/workflows/tethys.yml +++ b/.github/workflows/tethys.yml @@ -103,7 +103,6 @@ jobs: uses: actions/setup-node@v3 with: node-version: 18 - cache: "npm" - name: Install Dependencies run: npm install From f882d9a1c7b9fb4706d1a83b88a3d227243263cb Mon Sep 17 00:00:00 2001 From: jakeymac Date: Tue, 18 Feb 2025 20:38:09 -0700 Subject: [PATCH 10/29] moved js_tests folder to main tests folder --- .../gizmos/cesium_map_view.test.js | 2 +- .../js_tests}/__mocks__/cesium.js | 0 tests/js_tests/gizmos/cesium_map_view.test.js | 142 ++++++++++++++++++ .../js_tests}/gizmos/datable_view.test.js | 0 .../js_tests}/gizmos/gizmo_utilities.test.js | 0 .../js_tests}/gizmos/range_slider.test.js | 0 .../js_tests}/gizmos/select_input.test.js | 0 .../js_tests}/gizmos/slide_sheet.test.js | 0 .../js_tests}/gizmos/toggle_switch.test.js | 0 {js-tests => tests/js_tests}/setup.js | 0 10 files changed, 143 insertions(+), 1 deletion(-) rename {js-tests => js_tests}/gizmos/cesium_map_view.test.js (97%) rename {js-tests => tests/js_tests}/__mocks__/cesium.js (100%) create mode 100644 tests/js_tests/gizmos/cesium_map_view.test.js rename {js-tests => tests/js_tests}/gizmos/datable_view.test.js (100%) rename {js-tests => tests/js_tests}/gizmos/gizmo_utilities.test.js (100%) rename {js-tests => tests/js_tests}/gizmos/range_slider.test.js (100%) rename {js-tests => tests/js_tests}/gizmos/select_input.test.js (100%) rename {js-tests => tests/js_tests}/gizmos/slide_sheet.test.js (100%) rename {js-tests => tests/js_tests}/gizmos/toggle_switch.test.js (100%) rename {js-tests => tests/js_tests}/setup.js (100%) diff --git a/js-tests/gizmos/cesium_map_view.test.js b/js_tests/gizmos/cesium_map_view.test.js similarity index 97% rename from js-tests/gizmos/cesium_map_view.test.js rename to js_tests/gizmos/cesium_map_view.test.js index da1bbac73..076a95214 100644 --- a/js-tests/gizmos/cesium_map_view.test.js +++ b/js_tests/gizmos/cesium_map_view.test.js @@ -3,7 +3,7 @@ import * as Cesium from "cesium"; global.Cesium = Cesium; -const CESIUM_MAP_VIEW = require("../../tethys_gizmos/static/tethys_gizmos/js/cesium_map_view"); +const CESIUM_MAP_VIEW = require("../../../tethys_gizmos/static/tethys_gizmos/js/cesium_map_view"); const { _testOnly } = CESIUM_MAP_VIEW; jest.mock('cesium'); diff --git a/js-tests/__mocks__/cesium.js b/tests/js_tests/__mocks__/cesium.js similarity index 100% rename from js-tests/__mocks__/cesium.js rename to tests/js_tests/__mocks__/cesium.js diff --git a/tests/js_tests/gizmos/cesium_map_view.test.js b/tests/js_tests/gizmos/cesium_map_view.test.js new file mode 100644 index 000000000..076a95214 --- /dev/null +++ b/tests/js_tests/gizmos/cesium_map_view.test.js @@ -0,0 +1,142 @@ +import $ from "jquery"; +import * as Cesium from "cesium"; + +global.Cesium = Cesium; + +const CESIUM_MAP_VIEW = require("../../../tethys_gizmos/static/tethys_gizmos/js/cesium_map_view"); +const { _testOnly } = CESIUM_MAP_VIEW; + +jest.mock('cesium'); + +jest.mock("jquery" , () => { + return jest.fn(() => ({ + data: jest.fn((key) => { + const mockData = { + "cesium-ion-token": "mock_token", + clock: { + clock: { + currentTime: "2025-02-10T12:15:00Z", + startTime: "2025-02-10T12:00:00Z", + stopTime: "2025-02-10T12:30:00Z", + }, + }, + options: { shouldInitialize: true }, + }; + return mockData[key]; + }), + })); +}); + +beforeEach(() => { + jest.clearAllMocks(); + document.body.innerHTML = ` +
+ +
+ `; +}) + +describe("CESIUM_MAP_VIEW", () => { + test("should initialize the map correctly", () => { + expect(CESIUM_MAP_VIEW).toBeDefined(); + expect(typeof CESIUM_MAP_VIEW.getMap).toBe("function"); + expect(typeof CESIUM_MAP_VIEW.reInitializeMap).toBe("function"); + }); +}); + +describe("is_empty_or_undefined", () => { + test("should return true for undefined values", () => { + expect(_testOnly.is_empty_or_undefined(undefined)).toBe(true); + }); + + test("should return true for empty values", () => { + expect(_testOnly.is_empty_or_undefined("")).toBe(true); + expect(_testOnly.is_empty_or_undefined(null)).toBe(true); + }); + + test("should return true for non-empty values", () => { + expect(_testOnly.is_empty_or_undefined("test")).toBe(true); + expect(_testOnly.is_empty_or_undefined(0)).toBe(true); + }); + + test("should return true for an empty object", () => { + expect(_testOnly.is_empty_or_undefined({})).toBe(true); + }); + + test("should return false for non-empty object", () => { + expect(_testOnly.is_empty_or_undefined({ key: "value" })).toBe(false); + }); +}) + +describe("need_to_run", () => { + test("should return true if object contains a Cesium reference", () => { + const obj = { + someProperty: { + "Cesium.Cartesian3": [0, 0, 0], // This should trigger `true` + } + }; + expect(_testOnly.need_to_run(obj)).toBe(true); + }); + + test("should return true for nested Cesium references", () => { + const obj = { + level1: { + level2: { + level3: { + "Cesium.Color": "RED" + } + } + } + }; + expect(_testOnly.need_to_run(obj)).toBe(true); + }); + + test("should return false for objects without Cesium references", () => { + const obj = { + level1: { + level2: { + level3: { + someKey: "someValue", + } + } + } + }; + expect(_testOnly.need_to_run(obj)).toBe(false); + }); + + test("should return false for an empty object", () => { + expect(_testOnly.need_to_run({})).toBe(false); + }); + + test("should return false for non-object values", () => { + expect(_testOnly.need_to_run(null)).toBe(false); + expect(_testOnly.need_to_run(undefined)).toBe(false); + expect(_testOnly.need_to_run("string")).toBe(false); + expect(_testOnly.need_to_run(123)).toBe(false); + expect(_testOnly.need_to_run([])).toBe(false); + }); + + test("should return false when object has properties but no Cesium references", () => { + const obj = { + someKey: { + anotherKey: { + deeperKey: "notCesium" + } + } + }; + expect(_testOnly.need_to_run(obj)).toBe(false); + }); + + test("should return true when Cesium reference appears deeper in the object", () => { + const obj = { + someKey: { + anotherKey: { + deeperKey: { + "Cesium.SceneMode": "MORPHING" + } + } + } + }; + expect(_testOnly.need_to_run(obj)).toBe(true); + }); +}); diff --git a/js-tests/gizmos/datable_view.test.js b/tests/js_tests/gizmos/datable_view.test.js similarity index 100% rename from js-tests/gizmos/datable_view.test.js rename to tests/js_tests/gizmos/datable_view.test.js diff --git a/js-tests/gizmos/gizmo_utilities.test.js b/tests/js_tests/gizmos/gizmo_utilities.test.js similarity index 100% rename from js-tests/gizmos/gizmo_utilities.test.js rename to tests/js_tests/gizmos/gizmo_utilities.test.js diff --git a/js-tests/gizmos/range_slider.test.js b/tests/js_tests/gizmos/range_slider.test.js similarity index 100% rename from js-tests/gizmos/range_slider.test.js rename to tests/js_tests/gizmos/range_slider.test.js diff --git a/js-tests/gizmos/select_input.test.js b/tests/js_tests/gizmos/select_input.test.js similarity index 100% rename from js-tests/gizmos/select_input.test.js rename to tests/js_tests/gizmos/select_input.test.js diff --git a/js-tests/gizmos/slide_sheet.test.js b/tests/js_tests/gizmos/slide_sheet.test.js similarity index 100% rename from js-tests/gizmos/slide_sheet.test.js rename to tests/js_tests/gizmos/slide_sheet.test.js diff --git a/js-tests/gizmos/toggle_switch.test.js b/tests/js_tests/gizmos/toggle_switch.test.js similarity index 100% rename from js-tests/gizmos/toggle_switch.test.js rename to tests/js_tests/gizmos/toggle_switch.test.js diff --git a/js-tests/setup.js b/tests/js_tests/setup.js similarity index 100% rename from js-tests/setup.js rename to tests/js_tests/setup.js From cd7b5cf42c52179655104ccf50df887760815a54 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Tue, 18 Feb 2025 21:20:04 -0700 Subject: [PATCH 11/29] moved test configuration and dependency files to js_tests folder --- js_tests/gizmos/cesium_map_view.test.js | 142 ------------------ .babelrc => tests/js_tests/.babelrc | 0 tests/js_tests/gizmos/datable_view.test.js | 2 +- tests/js_tests/gizmos/gizmo_utilities.test.js | 2 +- tests/js_tests/gizmos/range_slider.test.js | 2 +- tests/js_tests/gizmos/select_input.test.js | 2 +- tests/js_tests/gizmos/slide_sheet.test.js | 2 +- tests/js_tests/gizmos/toggle_switch.test.js | 2 +- .../js_tests/jest.config.js | 6 +- package.json => tests/js_tests/package.json | 0 10 files changed, 9 insertions(+), 151 deletions(-) delete mode 100644 js_tests/gizmos/cesium_map_view.test.js rename .babelrc => tests/js_tests/.babelrc (100%) rename jest.config.js => tests/js_tests/jest.config.js (66%) rename package.json => tests/js_tests/package.json (100%) diff --git a/js_tests/gizmos/cesium_map_view.test.js b/js_tests/gizmos/cesium_map_view.test.js deleted file mode 100644 index 076a95214..000000000 --- a/js_tests/gizmos/cesium_map_view.test.js +++ /dev/null @@ -1,142 +0,0 @@ -import $ from "jquery"; -import * as Cesium from "cesium"; - -global.Cesium = Cesium; - -const CESIUM_MAP_VIEW = require("../../../tethys_gizmos/static/tethys_gizmos/js/cesium_map_view"); -const { _testOnly } = CESIUM_MAP_VIEW; - -jest.mock('cesium'); - -jest.mock("jquery" , () => { - return jest.fn(() => ({ - data: jest.fn((key) => { - const mockData = { - "cesium-ion-token": "mock_token", - clock: { - clock: { - currentTime: "2025-02-10T12:15:00Z", - startTime: "2025-02-10T12:00:00Z", - stopTime: "2025-02-10T12:30:00Z", - }, - }, - options: { shouldInitialize: true }, - }; - return mockData[key]; - }), - })); -}); - -beforeEach(() => { - jest.clearAllMocks(); - document.body.innerHTML = ` -
- -
- `; -}) - -describe("CESIUM_MAP_VIEW", () => { - test("should initialize the map correctly", () => { - expect(CESIUM_MAP_VIEW).toBeDefined(); - expect(typeof CESIUM_MAP_VIEW.getMap).toBe("function"); - expect(typeof CESIUM_MAP_VIEW.reInitializeMap).toBe("function"); - }); -}); - -describe("is_empty_or_undefined", () => { - test("should return true for undefined values", () => { - expect(_testOnly.is_empty_or_undefined(undefined)).toBe(true); - }); - - test("should return true for empty values", () => { - expect(_testOnly.is_empty_or_undefined("")).toBe(true); - expect(_testOnly.is_empty_or_undefined(null)).toBe(true); - }); - - test("should return true for non-empty values", () => { - expect(_testOnly.is_empty_or_undefined("test")).toBe(true); - expect(_testOnly.is_empty_or_undefined(0)).toBe(true); - }); - - test("should return true for an empty object", () => { - expect(_testOnly.is_empty_or_undefined({})).toBe(true); - }); - - test("should return false for non-empty object", () => { - expect(_testOnly.is_empty_or_undefined({ key: "value" })).toBe(false); - }); -}) - -describe("need_to_run", () => { - test("should return true if object contains a Cesium reference", () => { - const obj = { - someProperty: { - "Cesium.Cartesian3": [0, 0, 0], // This should trigger `true` - } - }; - expect(_testOnly.need_to_run(obj)).toBe(true); - }); - - test("should return true for nested Cesium references", () => { - const obj = { - level1: { - level2: { - level3: { - "Cesium.Color": "RED" - } - } - } - }; - expect(_testOnly.need_to_run(obj)).toBe(true); - }); - - test("should return false for objects without Cesium references", () => { - const obj = { - level1: { - level2: { - level3: { - someKey: "someValue", - } - } - } - }; - expect(_testOnly.need_to_run(obj)).toBe(false); - }); - - test("should return false for an empty object", () => { - expect(_testOnly.need_to_run({})).toBe(false); - }); - - test("should return false for non-object values", () => { - expect(_testOnly.need_to_run(null)).toBe(false); - expect(_testOnly.need_to_run(undefined)).toBe(false); - expect(_testOnly.need_to_run("string")).toBe(false); - expect(_testOnly.need_to_run(123)).toBe(false); - expect(_testOnly.need_to_run([])).toBe(false); - }); - - test("should return false when object has properties but no Cesium references", () => { - const obj = { - someKey: { - anotherKey: { - deeperKey: "notCesium" - } - } - }; - expect(_testOnly.need_to_run(obj)).toBe(false); - }); - - test("should return true when Cesium reference appears deeper in the object", () => { - const obj = { - someKey: { - anotherKey: { - deeperKey: { - "Cesium.SceneMode": "MORPHING" - } - } - } - }; - expect(_testOnly.need_to_run(obj)).toBe(true); - }); -}); diff --git a/.babelrc b/tests/js_tests/.babelrc similarity index 100% rename from .babelrc rename to tests/js_tests/.babelrc diff --git a/tests/js_tests/gizmos/datable_view.test.js b/tests/js_tests/gizmos/datable_view.test.js index 7d91e4b5f..5b493550b 100644 --- a/tests/js_tests/gizmos/datable_view.test.js +++ b/tests/js_tests/gizmos/datable_view.test.js @@ -6,7 +6,7 @@ $.fn.DataTable = jest.fn(); // Function to reload the module to trigger document ready function const reloadDatatableView = () => { jest.resetModules(); // Clear Jest’s module cache - return require("../../tethys_gizmos/static/tethys_gizmos/js/datatable_view"); + return require("../../../tethys_gizmos/static/tethys_gizmos/js/datatable_view"); }; describe("TETHYS_DATATABLE_VIEW", () => { diff --git a/tests/js_tests/gizmos/gizmo_utilities.test.js b/tests/js_tests/gizmos/gizmo_utilities.test.js index 4d8585b42..3d34011eb 100644 --- a/tests/js_tests/gizmos/gizmo_utilities.test.js +++ b/tests/js_tests/gizmos/gizmo_utilities.test.js @@ -1,6 +1,6 @@ import $ from "jquery"; -require("../../tethys_gizmos/static/tethys_gizmos/js/gizmo_utilities"); +require("../../../tethys_gizmos/static/tethys_gizmos/js/gizmo_utilities"); describe("gizmo_utilities.js", () => { let testElement; diff --git a/tests/js_tests/gizmos/range_slider.test.js b/tests/js_tests/gizmos/range_slider.test.js index 455c0f6e4..34956a020 100644 --- a/tests/js_tests/gizmos/range_slider.test.js +++ b/tests/js_tests/gizmos/range_slider.test.js @@ -1,7 +1,7 @@ import $ from "jquery"; // Import module AFTER mocking jQuery -const TETHYS_RANGE_SLIDER = require("../../tethys_gizmos/static/tethys_gizmos/js/range_slider"); +const TETHYS_RANGE_SLIDER = require("../../../tethys_gizmos/static/tethys_gizmos/js/range_slider"); describe("TETHYS_RANGE_SLIDER", () => { beforeEach(() => { diff --git a/tests/js_tests/gizmos/select_input.test.js b/tests/js_tests/gizmos/select_input.test.js index 75ffe99bc..3a0e09def 100644 --- a/tests/js_tests/gizmos/select_input.test.js +++ b/tests/js_tests/gizmos/select_input.test.js @@ -2,7 +2,7 @@ import $ from "jquery"; $.fn.select2 = jest.fn(); -const TETHYS_SELECT_INPUT = require("../../tethys_gizmos/static/tethys_gizmos/js/select_input"); +const TETHYS_SELECT_INPUT = require("../../../tethys_gizmos/static/tethys_gizmos/js/select_input"); describe("TETHYS_SELECT_INPUT", () => { beforeEach(() => { diff --git a/tests/js_tests/gizmos/slide_sheet.test.js b/tests/js_tests/gizmos/slide_sheet.test.js index 2ee3596dc..a530f431c 100644 --- a/tests/js_tests/gizmos/slide_sheet.test.js +++ b/tests/js_tests/gizmos/slide_sheet.test.js @@ -5,7 +5,7 @@ $.fn.addClass = jest.fn(); $.fn.removeClass = jest.fn(); // Import the module AFTER mocking jQuery -const SLIDE_SHEET = require("../../tethys_gizmos/static/tethys_gizmos/js/slide_sheet"); +const SLIDE_SHEET = require("../../../tethys_gizmos/static/tethys_gizmos/js/slide_sheet"); describe("SLIDE_SHEET", () => { beforeEach(() => { diff --git a/tests/js_tests/gizmos/toggle_switch.test.js b/tests/js_tests/gizmos/toggle_switch.test.js index fa01514d9..868296a4a 100644 --- a/tests/js_tests/gizmos/toggle_switch.test.js +++ b/tests/js_tests/gizmos/toggle_switch.test.js @@ -4,7 +4,7 @@ import $ from "jquery"; $.fn.bootstrapSwitch = jest.fn(); // Import the module -const TETHYS_TOGGLE_SWITCH = require("../../tethys_gizmos/static/tethys_gizmos/js/toggle_switch"); +const TETHYS_TOGGLE_SWITCH = require("../../../tethys_gizmos/static/tethys_gizmos/js/toggle_switch"); describe("TETHYS_TOGGLE_SWITCH", () => { beforeEach(() => { diff --git a/jest.config.js b/tests/js_tests/jest.config.js similarity index 66% rename from jest.config.js rename to tests/js_tests/jest.config.js index 7c6bef104..524279d73 100644 --- a/jest.config.js +++ b/tests/js_tests/jest.config.js @@ -1,13 +1,13 @@ module.exports = { testEnvironment: "", - roots: ["/js-tests"], - setupFilesAfterEnv: ["/js-tests/setup.js"], + roots: [""], + setupFilesAfterEnv: ["/setup.js"], collectCoverage: true, coverageDirectory: "jest_coverage", coverageReporters: ["text", "lcov"], moduleFileExtensions: ["js", "jsx", "json", "node"], moduleNameMapper: { - "^cesium$": "/js-tests/__mocks__/cesium.js", + "^cesium$": "/__mocks__/cesium.js", }, transformIgnorePatterns: ["/node_modules/"], }; \ No newline at end of file diff --git a/package.json b/tests/js_tests/package.json similarity index 100% rename from package.json rename to tests/js_tests/package.json From 71e500047adff5f544732d11ad074c1b3f7fe7e7 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Sat, 22 Feb 2025 12:49:10 -0700 Subject: [PATCH 12/29] Updated testing to use Testing Library Tests now work with rendered templates Added python script called in tests to render django templates with contexts Redid testing for slide_sheet gizmo --- .gitignore | 3 +- tests/js_tests/gizmos/cesium_map_view.test.js | 142 ------------------ tests/js_tests/gizmos/datable_view.test.js | 47 ------ tests/js_tests/gizmos/gizmo_utilities.test.js | 50 ------ tests/js_tests/gizmos/range_slider.test.js | 46 ------ tests/js_tests/gizmos/select_input.test.js | 62 -------- tests/js_tests/gizmos/slide_sheet.test.js | 94 +++++++----- tests/js_tests/gizmos/toggle_switch.test.js | 44 ------ tests/js_tests/package.json | 5 +- tests/js_tests/render_template.py | 85 +++++++++++ tests/js_tests/setup.js | 4 +- tests/js_tests/templates/test_template.html | 1 + .../static/tethys_gizmos/js/slide_sheet.js | 2 + 13 files changed, 156 insertions(+), 429 deletions(-) delete mode 100644 tests/js_tests/gizmos/cesium_map_view.test.js delete mode 100644 tests/js_tests/gizmos/datable_view.test.js delete mode 100644 tests/js_tests/gizmos/gizmo_utilities.test.js delete mode 100644 tests/js_tests/gizmos/range_slider.test.js delete mode 100644 tests/js_tests/gizmos/select_input.test.js delete mode 100644 tests/js_tests/gizmos/toggle_switch.test.js create mode 100644 tests/js_tests/render_template.py create mode 100644 tests/js_tests/templates/test_template.html diff --git a/.gitignore b/.gitignore index e1358d463..697210fde 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,5 @@ git-lfs-*/* conda.recipe/meta.yaml jest_coverage/ -package-lock.json \ No newline at end of file +package-lock.json +tests/js_tests/rendered_templates \ No newline at end of file diff --git a/tests/js_tests/gizmos/cesium_map_view.test.js b/tests/js_tests/gizmos/cesium_map_view.test.js deleted file mode 100644 index 076a95214..000000000 --- a/tests/js_tests/gizmos/cesium_map_view.test.js +++ /dev/null @@ -1,142 +0,0 @@ -import $ from "jquery"; -import * as Cesium from "cesium"; - -global.Cesium = Cesium; - -const CESIUM_MAP_VIEW = require("../../../tethys_gizmos/static/tethys_gizmos/js/cesium_map_view"); -const { _testOnly } = CESIUM_MAP_VIEW; - -jest.mock('cesium'); - -jest.mock("jquery" , () => { - return jest.fn(() => ({ - data: jest.fn((key) => { - const mockData = { - "cesium-ion-token": "mock_token", - clock: { - clock: { - currentTime: "2025-02-10T12:15:00Z", - startTime: "2025-02-10T12:00:00Z", - stopTime: "2025-02-10T12:30:00Z", - }, - }, - options: { shouldInitialize: true }, - }; - return mockData[key]; - }), - })); -}); - -beforeEach(() => { - jest.clearAllMocks(); - document.body.innerHTML = ` -
- -
- `; -}) - -describe("CESIUM_MAP_VIEW", () => { - test("should initialize the map correctly", () => { - expect(CESIUM_MAP_VIEW).toBeDefined(); - expect(typeof CESIUM_MAP_VIEW.getMap).toBe("function"); - expect(typeof CESIUM_MAP_VIEW.reInitializeMap).toBe("function"); - }); -}); - -describe("is_empty_or_undefined", () => { - test("should return true for undefined values", () => { - expect(_testOnly.is_empty_or_undefined(undefined)).toBe(true); - }); - - test("should return true for empty values", () => { - expect(_testOnly.is_empty_or_undefined("")).toBe(true); - expect(_testOnly.is_empty_or_undefined(null)).toBe(true); - }); - - test("should return true for non-empty values", () => { - expect(_testOnly.is_empty_or_undefined("test")).toBe(true); - expect(_testOnly.is_empty_or_undefined(0)).toBe(true); - }); - - test("should return true for an empty object", () => { - expect(_testOnly.is_empty_or_undefined({})).toBe(true); - }); - - test("should return false for non-empty object", () => { - expect(_testOnly.is_empty_or_undefined({ key: "value" })).toBe(false); - }); -}) - -describe("need_to_run", () => { - test("should return true if object contains a Cesium reference", () => { - const obj = { - someProperty: { - "Cesium.Cartesian3": [0, 0, 0], // This should trigger `true` - } - }; - expect(_testOnly.need_to_run(obj)).toBe(true); - }); - - test("should return true for nested Cesium references", () => { - const obj = { - level1: { - level2: { - level3: { - "Cesium.Color": "RED" - } - } - } - }; - expect(_testOnly.need_to_run(obj)).toBe(true); - }); - - test("should return false for objects without Cesium references", () => { - const obj = { - level1: { - level2: { - level3: { - someKey: "someValue", - } - } - } - }; - expect(_testOnly.need_to_run(obj)).toBe(false); - }); - - test("should return false for an empty object", () => { - expect(_testOnly.need_to_run({})).toBe(false); - }); - - test("should return false for non-object values", () => { - expect(_testOnly.need_to_run(null)).toBe(false); - expect(_testOnly.need_to_run(undefined)).toBe(false); - expect(_testOnly.need_to_run("string")).toBe(false); - expect(_testOnly.need_to_run(123)).toBe(false); - expect(_testOnly.need_to_run([])).toBe(false); - }); - - test("should return false when object has properties but no Cesium references", () => { - const obj = { - someKey: { - anotherKey: { - deeperKey: "notCesium" - } - } - }; - expect(_testOnly.need_to_run(obj)).toBe(false); - }); - - test("should return true when Cesium reference appears deeper in the object", () => { - const obj = { - someKey: { - anotherKey: { - deeperKey: { - "Cesium.SceneMode": "MORPHING" - } - } - } - }; - expect(_testOnly.need_to_run(obj)).toBe(true); - }); -}); diff --git a/tests/js_tests/gizmos/datable_view.test.js b/tests/js_tests/gizmos/datable_view.test.js deleted file mode 100644 index 5b493550b..000000000 --- a/tests/js_tests/gizmos/datable_view.test.js +++ /dev/null @@ -1,47 +0,0 @@ -import $ from "jquery"; - -// Mock jQuery's DataTables plugin -$.fn.DataTable = jest.fn(); - -// Function to reload the module to trigger document ready function -const reloadDatatableView = () => { - jest.resetModules(); // Clear Jest’s module cache - return require("../../../tethys_gizmos/static/tethys_gizmos/js/datatable_view"); -}; - -describe("TETHYS_DATATABLE_VIEW", () => { - beforeEach(() => { - document.body.innerHTML = ` - - - - - - - - -
Column 1Column 2
Data 1Data 2
Data 3Data 4
- `; - $.fn.DataTable = jest.fn(); - }); - - test("should initialize DataTables when called manually", () => { - const TETHYS_DATATABLE_VIEW = reloadDatatableView(); - const tableElement = $(".data_table_gizmo_view"); - // Call the initialization function manually - TETHYS_DATATABLE_VIEW.initTableView(tableElement); - // Ensure DataTables was called - expect(tableElement.DataTable).toHaveBeenCalled(); - }); -});test("should initialize DataTables on document ready", (done) => { - const dataTableSpy = jest.spyOn($.fn, "DataTable"); - reloadDatatableView(); - - // Wait for jQuery's document ready function - setTimeout(() => { - // Ensure DataTables was called - expect(dataTableSpy).toHaveBeenCalled(); - dataTableSpy.mockRestore(); - done(); - }, 100); - }); \ No newline at end of file diff --git a/tests/js_tests/gizmos/gizmo_utilities.test.js b/tests/js_tests/gizmos/gizmo_utilities.test.js deleted file mode 100644 index 3d34011eb..000000000 --- a/tests/js_tests/gizmos/gizmo_utilities.test.js +++ /dev/null @@ -1,50 +0,0 @@ -import $ from "jquery"; - -require("../../../tethys_gizmos/static/tethys_gizmos/js/gizmo_utilities"); - -describe("gizmo_utilities.js", () => { - let testElement; - - beforeEach(() => { - // Set up a mock DOM element for testing - document.body.innerHTML = `
`; - testElement = $("#test-element"); - }); - - afterEach(() => { - clearInterval(testElement[0].sizeTO); - }); - - test("should define $.fn.changeSize", () => { - expect($.fn.changeSize).toBeDefined(); - }); - - test("should call the callback function ", (done) => { - const callback = jest.fn(); - - testElement.changeSize(callback); - - setTimeout(() => { - testElement.width(200); - testElement.height(200); - }, 50); - - setTimeout(() => { - expect(callback).toHaveBeenCalledWith(testElement); - done(); - }, 200); - }); - - test("should not trigger callback if size remains the same", (done) => { - const callback = jest.fn(); - - testElement.changeSize(callback); - - setTimeout(() => { - expect(callback).not.toHaveBeenCalled(); - done(); - }, 200); - }); - - -}); \ No newline at end of file diff --git a/tests/js_tests/gizmos/range_slider.test.js b/tests/js_tests/gizmos/range_slider.test.js deleted file mode 100644 index 34956a020..000000000 --- a/tests/js_tests/gizmos/range_slider.test.js +++ /dev/null @@ -1,46 +0,0 @@ -import $ from "jquery"; - -// Import module AFTER mocking jQuery -const TETHYS_RANGE_SLIDER = require("../../../tethys_gizmos/static/tethys_gizmos/js/range_slider"); - -describe("TETHYS_RANGE_SLIDER", () => { - beforeEach(() => { - jest.clearAllMocks(); - - // Set up a mock DOM element for testing - document.body.innerHTML = ` - - 50 - `; - }); - - test("Should initialize range sliders on page load", (done) => { - // Wait for jQuery's document ready function - setTimeout(() => { - const rangeInput = document.querySelector(".form-range"); - const rangeValue = document.querySelector(".range-value"); - - expect(rangeInput).not.toBeNull(); - expect(rangeValue).not.toBeNull(); - - done(); - }, 100); - }); - - test("Should update value display when slider is moved", () => { - const rangeInput = document.querySelector(".form-range"); - const rangeValue = document.querySelector(".range-value"); - - // Manually initialize range sliders - TETHYS_RANGE_SLIDER.init_range_sliders(); - rangeInput.value = "75"; - - // Trigger the input event - const event = document.createEvent("Event"); - event.initEvent("input", true, true); - rangeInput.dispatchEvent(event); - - // Check if the slider's value has updated - expect(rangeValue.innerHTML).toBe("75"); - }); -}); \ No newline at end of file diff --git a/tests/js_tests/gizmos/select_input.test.js b/tests/js_tests/gizmos/select_input.test.js deleted file mode 100644 index 3a0e09def..000000000 --- a/tests/js_tests/gizmos/select_input.test.js +++ /dev/null @@ -1,62 +0,0 @@ -import $ from "jquery"; - -$.fn.select2 = jest.fn(); - -const TETHYS_SELECT_INPUT = require("../../../tethys_gizmos/static/tethys_gizmos/js/select_input"); - -describe("TETHYS_SELECT_INPUT", () => { - beforeEach(() => { - // Set up a mock DOM element for testing - document.body.innerHTML = ` - - `; - }); - - test("should initialize Select2 on document ready", (done) => { - const select2Spy = jest.spyOn($.fn, "select2"); - - // Wait for jQuery's document ready function - setTimeout(() => { - // Ensure select2 was called - expect(select2Spy).toHaveBeenCalled(); - select2Spy.mockRestore(); - done(); - }, 100); - }) - - test("Module should have initSelectInput function", () => { - expect(TETHYS_SELECT_INPUT).toHaveProperty("initSelectInput"); - expect(typeof TETHYS_SELECT_INPUT.initSelectInput).toBe("function"); - }); - - test("initSelectInput should initialize Select2 on elements", () => { - const selectElement = $(".tethys-select2"); - - TETHYS_SELECT_INPUT.initSelectInput(selectElement); - - expect($.fn.select2).toHaveBeenCalledTimes(1); - }); - - test("jQuery should call select2 with correct options", () => { - const selectElement = $(".tethys-select2"); - - // Mock jQuery's .data() method to return select2 options - $.fn.data = jest.fn(() => ({ placeholder: "Select an option" })); - - TETHYS_SELECT_INPUT.initSelectInput(selectElement); - - expect($.fn.select2).toHaveBeenCalledWith({ placeholder: "Select an option" }); - }); - - test("Automatically initializes on page load", () => { - // Ensure that the select2 function was called during module initialization - expect($.fn.select2).toHaveBeenCalled(); - }); - -}); - - - \ No newline at end of file diff --git a/tests/js_tests/gizmos/slide_sheet.test.js b/tests/js_tests/gizmos/slide_sheet.test.js index a530f431c..099a9dcc8 100644 --- a/tests/js_tests/gizmos/slide_sheet.test.js +++ b/tests/js_tests/gizmos/slide_sheet.test.js @@ -1,51 +1,75 @@ -import $ from "jquery"; +import { execSync } from "child_process"; +import fs from "fs"; +import path from "path"; +const { screen, fireEvent } = require("@testing-library/dom"); -// Mock jQuery functions -$.fn.addClass = jest.fn(); -$.fn.removeClass = jest.fn(); +import SLIDE_SHEET from "../../../tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js" -// Import the module AFTER mocking jQuery -const SLIDE_SHEET = require("../../../tethys_gizmos/static/tethys_gizmos/js/slide_sheet"); +global.SLIDE_SHEET = SLIDE_SHEET; -describe("SLIDE_SHEET", () => { - beforeEach(() => { - jest.clearAllMocks(); // Reset mocks before each test +let renderedHtml = ''; - // Set up a mock DOM element for testing - document.body.innerHTML = ` -
- `; +const SLIDE_SHEET_ID = "test_slide_sheet_id" + +beforeAll(() => { + const templateName = "tethys_gizmos/gizmos/slide_sheet.html"; + const context = JSON.stringify({ + title: "Testing the slide sheet gizmo", + id: SLIDE_SHEET_ID, + content_template: "test_template.html" }); - test("Module should have open and close functions", () => { - expect(SLIDE_SHEET).toHaveProperty("open"); - expect(typeof SLIDE_SHEET.open).toBe("function"); + execSync(`python render_template.py ${templateName} '${context}' `, { stdio: "inherit" }); - expect(SLIDE_SHEET).toHaveProperty("close"); - expect(typeof SLIDE_SHEET.close).toBe("function"); - }); + const outputPath = path.resolve("./rendered_templates/test_slide_sheet_output.html"); + renderedHtml = fs.readFileSync(outputPath, "utf8"); +}) + +beforeEach(() => { + document.body.innerHTML = renderedHtml; + let testOpenButton = document.createElement("button"); + testOpenButton.textContent = "Open Slide Sheet"; + testOpenButton.onclick = () => SLIDE_SHEET.open(SLIDE_SHEET_ID); + document.body.appendChild(testOpenButton); - test("open should add 'show' class to slide-sheet", () => { - SLIDE_SHEET.open("test-slide"); + // Add event listeners to all elements with an onclick attribute + document.querySelectorAll("[onclick]").forEach((el) => { + const onClickAttr = el.getAttribute("onclick"); - expect($("#test-slide.slide-sheet").addClass).toHaveBeenCalledWith("show"); + // Create a new function from the onclick attribute string + if (onClickAttr) { + el.addEventListener("click", function () { + eval(onClickAttr); // 🔥 Execute the original inline script + }); + } }); +}); - test("close should remove 'show' class from slide-sheet", () => { - SLIDE_SHEET.close("test-slide"); - expect($("#test-slide.slide-sheet").removeClass).toHaveBeenCalledWith("show"); - }); - test("open does nothing if id is empty", () => { - SLIDE_SHEET.open(""); +test("Gizmo renders correctly", () => { + expect(screen.getByText("Testing the slide sheet gizmo")).toBeInTheDocument(); +}); - expect($.fn.addClass).not.toHaveBeenCalled(); - }); +test("Clicking open button opens slide sheet", () => { + const slideSheet = document.getElementById(SLIDE_SHEET_ID); + fireEvent.click(screen.getByText("Open Slide Sheet")); + expect(slideSheet.classList.contains("show")).toBe(true); + +}) - test("close does nothing if id is empty", () => { - SLIDE_SHEET.close(""); +test("Clicking close button closes slide sheet", () => { + const slideSheet = document.getElementById(SLIDE_SHEET_ID); + + SLIDE_SHEET.open(SLIDE_SHEET_ID); + expect(slideSheet.classList.contains("show")).toBe(true); + + const closeButton = screen.getByLabelText("Close"); + // closeButton.addEventListener("click", () => { + // SLIDE_SHEET.close(SLIDE_SHEET_ID); + // }); + fireEvent.click(screen.getByLabelText("Close")); + + expect(slideSheet.classList.contains("show")).toBe(false); +}); - expect($.fn.removeClass).not.toHaveBeenCalled(); - }); -}); \ No newline at end of file diff --git a/tests/js_tests/gizmos/toggle_switch.test.js b/tests/js_tests/gizmos/toggle_switch.test.js deleted file mode 100644 index 868296a4a..000000000 --- a/tests/js_tests/gizmos/toggle_switch.test.js +++ /dev/null @@ -1,44 +0,0 @@ -import $ from "jquery"; - -// Mock Bootstrap Switch plugin -$.fn.bootstrapSwitch = jest.fn(); - -// Import the module -const TETHYS_TOGGLE_SWITCH = require("../../../tethys_gizmos/static/tethys_gizmos/js/toggle_switch"); - -describe("TETHYS_TOGGLE_SWITCH", () => { - beforeEach(() => { - jest.clearAllMocks(); // Reset mocks before each test - - // Set up a mock DOM element for testing - document.body.innerHTML = ` - - `; - }); - - test("Module should have initToggleSwitch function", () => { - expect(TETHYS_TOGGLE_SWITCH).toHaveProperty("initToggleSwitch"); - expect(typeof TETHYS_TOGGLE_SWITCH.initToggleSwitch).toBe("function"); - }); - - test("initToggleSwitch should initialize Bootstrap Switch", () => { - const switchElement = $(".bootstrap-switch"); - - TETHYS_TOGGLE_SWITCH.initToggleSwitch(switchElement); - - expect($.fn.bootstrapSwitch).toHaveBeenCalledTimes(1); - }); - - test("Automatically initializes switch elements on page load", (done) => { - // Ensure that the bootstrapSwitch function was called during module initialization - const switchSpy = jest.spyOn($.fn, "bootstrapSwitch"); - - // Wait for jQuery's document ready function - setTimeout(() => { - // Ensure bootstrapSwitch was called - expect(switchSpy).toHaveBeenCalled(); - switchSpy.mockRestore(); - done(); - }, 100); - }); -}); \ No newline at end of file diff --git a/tests/js_tests/package.json b/tests/js_tests/package.json index f0dfe4a53..4324547c0 100644 --- a/tests/js_tests/package.json +++ b/tests/js_tests/package.json @@ -1,6 +1,9 @@ { "devDependencies": { - "@babel/preset-env": "^7.26.7", + "@babel/core": "^7.26.9", + "@babel/preset-env": "^7.26.9", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", "babel-jest": "^29.7.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", diff --git a/tests/js_tests/render_template.py b/tests/js_tests/render_template.py new file mode 100644 index 000000000..8b1001e38 --- /dev/null +++ b/tests/js_tests/render_template.py @@ -0,0 +1,85 @@ +import os +import sys +import json +import django +from django.conf import settings +from django.template.loader import render_to_string, select_template + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tethys_portal.settings") + +# Add tests templates directory to settings so that django finds them +TEST_TEMPLATE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "templates")) +if TEST_TEMPLATE_DIR not in settings.TEMPLATES[0]["DIRS"]: + settings.TEMPLATES[0]["DIRS"].append(TEST_TEMPLATE_DIR) + +django.setup() + +def confirm_template_path(template_name): + try: + # Second try block wrapping is required for handling + # django errors and getting it to print in js testing + try: + select_template([template_name]) + except Exception as e: + print(f"Error finding template: {e}") + traceback.print_exc(file=sys.stdout) + sys.stdout.flush() + sys.stderr.flush() + sys.exit(1) + + except Exception as e: + print(f"Error finding template: {e}") + traceback.print_exc(file=sys.stdout) + sys.stdout.flush() + sys.stderr.flush() + sys.exit(1) + +def form_output_name(template_name): + template = template_name.removesuffix(".html") + return f"{template}_output.html" + +def render_template(template_name, context): + try: + # Confirm the template path exists + confirm_template_path(template_name) + + # Render template using context + try: + try: + rendered_html = render_to_string(template_name, context) + + except Exception as e: + print(f"Error: {e}") + traceback.print_exc(file=sys.stdout) + sys.stdout.flush() + sys.stderr.flush() + sys.exit(1) + + except Exception as e: + print(f"Error: {e}") + traceback.print_exc(file=sys.stdout) + sys.stdout.flush() + sys.stderr.flush() + sys.exit(1) + + output_path = f"./rendered_templates/test_{os.path.basename(form_output_name(template_name))}" + os.makedirs(os.path.dirname(output_path), exist_ok=True) + + # Write the rendered html into the result file + with open(output_path, "w") as file: + file.write(rendered_html) + + print(f"Template rendered to {output_path}") + except Exception as e: + print(f"Error rendering template: {e}") + sys.exit(1) + +if __name__ == "__main__": + # breakpoint() + if len(sys.argv) < 3: + print("Usage: python render_template.py ''") + sys.exit(1) + + template_name = sys.argv[1] + context = json.loads(sys.argv[2]) + render_template(template_name, context) diff --git a/tests/js_tests/setup.js b/tests/js_tests/setup.js index 8d16222a3..1825ba27c 100644 --- a/tests/js_tests/setup.js +++ b/tests/js_tests/setup.js @@ -28,4 +28,6 @@ const $ = require("jquery"); global.$ = $; global.jQuery = $; -global.$.fn = global.$.fn || {}; \ No newline at end of file +global.$.fn = global.$.fn || {}; + +import "@testing-library/jest-dom"; \ No newline at end of file diff --git a/tests/js_tests/templates/test_template.html b/tests/js_tests/templates/test_template.html new file mode 100644 index 000000000..f66183eb0 --- /dev/null +++ b/tests/js_tests/templates/test_template.html @@ -0,0 +1 @@ +

This is a test template

\ No newline at end of file diff --git a/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js b/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js index 01084a73a..84cf6af27 100644 --- a/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js +++ b/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js @@ -29,6 +29,7 @@ var SLIDE_SHEET = (function() { if (id.length) { $('#' + id + '.slide-sheet').addClass('show'); } + console.log("Testing here opening..."); }; close = function(id) { @@ -36,6 +37,7 @@ var SLIDE_SHEET = (function() { if (id.length) { $('#' + id + '.slide-sheet').removeClass('show'); } + console.log("Testing here..."); }; /************************************************************************ From 6a8863be91f9551d2f808d8b35e6a5525ab001f5 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Sat, 22 Feb 2025 21:17:30 -0700 Subject: [PATCH 13/29] Cleaned up code Black fixes --- tests/js_tests/gizmos/slide_sheet.test.js | 11 ++----- tests/js_tests/render_template.py | 38 +++++++++++------------ 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/tests/js_tests/gizmos/slide_sheet.test.js b/tests/js_tests/gizmos/slide_sheet.test.js index 099a9dcc8..0f0b1ea19 100644 --- a/tests/js_tests/gizmos/slide_sheet.test.js +++ b/tests/js_tests/gizmos/slide_sheet.test.js @@ -12,7 +12,7 @@ let renderedHtml = ''; const SLIDE_SHEET_ID = "test_slide_sheet_id" beforeAll(() => { - const templateName = "tethys_gizmos/gizmos/slide_sheet.html"; + const templateName = "tethys_gizmos/gizmos/slides_sheet.html"; const context = JSON.stringify({ title: "Testing the slide sheet gizmo", id: SLIDE_SHEET_ID, @@ -32,7 +32,7 @@ beforeEach(() => { testOpenButton.onclick = () => SLIDE_SHEET.open(SLIDE_SHEET_ID); document.body.appendChild(testOpenButton); - // Add event listeners to all elements with an onclick attribute + // Add event listeners to all elements with an inline onclick attribute document.querySelectorAll("[onclick]").forEach((el) => { const onClickAttr = el.getAttribute("onclick"); @@ -60,16 +60,11 @@ test("Clicking open button opens slide sheet", () => { test("Clicking close button closes slide sheet", () => { const slideSheet = document.getElementById(SLIDE_SHEET_ID); - SLIDE_SHEET.open(SLIDE_SHEET_ID); expect(slideSheet.classList.contains("show")).toBe(true); const closeButton = screen.getByLabelText("Close"); - // closeButton.addEventListener("click", () => { - // SLIDE_SHEET.close(SLIDE_SHEET_ID); - // }); - fireEvent.click(screen.getByLabelText("Close")); - + fireEvent.click(closeButton); expect(slideSheet.classList.contains("show")).toBe(false); }); diff --git a/tests/js_tests/render_template.py b/tests/js_tests/render_template.py index 8b1001e38..399b85fd8 100644 --- a/tests/js_tests/render_template.py +++ b/tests/js_tests/render_template.py @@ -1,6 +1,7 @@ +import json import os import sys -import json + import django from django.conf import settings from django.template.loader import render_to_string, select_template @@ -8,36 +9,38 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tethys_portal.settings") # Add tests templates directory to settings so that django finds them -TEST_TEMPLATE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "templates")) +TEST_TEMPLATE_DIR = os.path.abspath( + os.path.join(os.path.dirname(__file__), "templates") +) if TEST_TEMPLATE_DIR not in settings.TEMPLATES[0]["DIRS"]: settings.TEMPLATES[0]["DIRS"].append(TEST_TEMPLATE_DIR) django.setup() + def confirm_template_path(template_name): try: - # Second try block wrapping is required for handling + # Second try block wrapping is required for handling # django errors and getting it to print in js testing try: - select_template([template_name]) + select_template([template_name]) + except Exception as e: - print(f"Error finding template: {e}") - traceback.print_exc(file=sys.stdout) - sys.stdout.flush() - sys.stderr.flush() + print(f"Error finding template: {e}") + traceback.print_exc(file=sys.stdout) sys.exit(1) except Exception as e: print(f"Error finding template: {e}") - traceback.print_exc(file=sys.stdout) - sys.stdout.flush() - sys.stderr.flush() + traceback.print_exc(file=sys.stdout) sys.exit(1) + def form_output_name(template_name): template = template_name.removesuffix(".html") return f"{template}_output.html" + def render_template(template_name, context): try: # Confirm the template path exists @@ -50,21 +53,17 @@ def render_template(template_name, context): except Exception as e: print(f"Error: {e}") - traceback.print_exc(file=sys.stdout) - sys.stdout.flush() - sys.stderr.flush() + traceback.print_exc(file=sys.stdout) sys.exit(1) except Exception as e: print(f"Error: {e}") - traceback.print_exc(file=sys.stdout) - sys.stdout.flush() - sys.stderr.flush() + traceback.print_exc(file=sys.stdout) sys.exit(1) output_path = f"./rendered_templates/test_{os.path.basename(form_output_name(template_name))}" os.makedirs(os.path.dirname(output_path), exist_ok=True) - + # Write the rendered html into the result file with open(output_path, "w") as file: file.write(rendered_html) @@ -73,7 +72,8 @@ def render_template(template_name, context): except Exception as e: print(f"Error rendering template: {e}") sys.exit(1) - + + if __name__ == "__main__": # breakpoint() if len(sys.argv) < 3: From 21343136a6f49958dc6d1bacd37cd4ed137afa1d Mon Sep 17 00:00:00 2001 From: jakeymac Date: Sat, 22 Feb 2025 21:20:38 -0700 Subject: [PATCH 14/29] Fixed template name in slide sheet tests --- tests/js_tests/gizmos/slide_sheet.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/js_tests/gizmos/slide_sheet.test.js b/tests/js_tests/gizmos/slide_sheet.test.js index 0f0b1ea19..b3093ba29 100644 --- a/tests/js_tests/gizmos/slide_sheet.test.js +++ b/tests/js_tests/gizmos/slide_sheet.test.js @@ -12,7 +12,7 @@ let renderedHtml = ''; const SLIDE_SHEET_ID = "test_slide_sheet_id" beforeAll(() => { - const templateName = "tethys_gizmos/gizmos/slides_sheet.html"; + const templateName = "tethys_gizmos/gizmos/slide_sheet.html"; const context = JSON.stringify({ title: "Testing the slide sheet gizmo", id: SLIDE_SHEET_ID, From 23ab98ec1bed14bfc01d7af8b8da4a4e750cf3e2 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Sat, 22 Feb 2025 22:17:41 -0700 Subject: [PATCH 15/29] Added testing for range_slider gizmo --- tests/js_tests/gizmos/range_slider.test.js | 57 +++++++++++++++++++ .../static/tethys_gizmos/js/slide_sheet.js | 2 - 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 tests/js_tests/gizmos/range_slider.test.js diff --git a/tests/js_tests/gizmos/range_slider.test.js b/tests/js_tests/gizmos/range_slider.test.js new file mode 100644 index 000000000..f689b01f7 --- /dev/null +++ b/tests/js_tests/gizmos/range_slider.test.js @@ -0,0 +1,57 @@ +import { execSync } from "child_process"; +import fs from "fs"; +import path from "path"; +import { screen, fireEvent } from "@testing-library/dom"; +import "@testing-library/jest-dom"; +import $ from "jquery"; + +import TETHYS_RANGE_SLIDER from "../../../tethys_gizmos/static/tethys_gizmos/js/range_slider.js"; + +global.TETHYS_RANGE_SLIDER = TETHYS_RANGE_SLIDER; + +let renderedHtml = ""; +const RANGE_SLIDER_ID = "test-range-slider"; + +beforeAll(() => { + const templateName = "tethys_gizmos/gizmos/range_slider.html"; + const context = JSON.stringify({ + name: RANGE_SLIDER_ID, + display_text: "Select Range", + min: 0, + max: 100, + step: 5, + initial: 50, + }); + + // Render the template with the context + execSync(`python render_template.py ${templateName} '${context}' `, { stdio: "inherit" }); + + const outputPath = path.resolve("./rendered_templates/test_range_slider_output.html"); + + // Read the rendered HTML + renderedHtml = fs.readFileSync(outputPath, "utf8"); +}); + +beforeEach(() => { + document.body.innerHTML = renderedHtml; + + // Initialize range slider and add event listeners + TETHYS_RANGE_SLIDER.init_range_sliders(); +}); + +test("Gizmo renders correctly", () => { + expect(screen.getByLabelText("Select Range")).toBeInTheDocument(); + expect(screen.getByRole("slider")).toBeInTheDocument(); +}); + +test("Range slider updates display text", () => { + const rangeSliderInput = document.getElementById(RANGE_SLIDER_ID); + const rangeSliderDisplay = rangeSliderInput.nextElementSibling; + + // Test for initial value + expect(rangeSliderDisplay).toHaveTextContent("50"); + + // Test for updated value with input + fireEvent.input(rangeSliderInput, { target: { value: 75 } }); + expect(rangeSliderDisplay).toHaveTextContent("75"); +}); \ No newline at end of file diff --git a/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js b/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js index 84cf6af27..01084a73a 100644 --- a/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js +++ b/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js @@ -29,7 +29,6 @@ var SLIDE_SHEET = (function() { if (id.length) { $('#' + id + '.slide-sheet').addClass('show'); } - console.log("Testing here opening..."); }; close = function(id) { @@ -37,7 +36,6 @@ var SLIDE_SHEET = (function() { if (id.length) { $('#' + id + '.slide-sheet').removeClass('show'); } - console.log("Testing here..."); }; /************************************************************************ From d51495ca9315f7b4d63e09b2a10b88f56412f0f9 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Mon, 24 Feb 2025 08:46:59 -0700 Subject: [PATCH 16/29] Added testing for datatable_view gizmo --- tests/js_tests/gizmos/datatable_view.test.js | 64 ++++++++++++++++++++ tests/js_tests/package.json | 1 + 2 files changed, 65 insertions(+) create mode 100644 tests/js_tests/gizmos/datatable_view.test.js diff --git a/tests/js_tests/gizmos/datatable_view.test.js b/tests/js_tests/gizmos/datatable_view.test.js new file mode 100644 index 000000000..cc601177f --- /dev/null +++ b/tests/js_tests/gizmos/datatable_view.test.js @@ -0,0 +1,64 @@ +import { execSync } from "child_process"; +import fs from "fs"; +import path from "path"; +const { screen, fireEvent } = require("@testing-library/dom"); + +import $ from "jquery"; + +import DataTable from "datatables.net"; +$.fn.DataTable = DataTable; + +// Import DataTable module +import TETHYS_DATATABLE_VIEW from "../../../tethys_gizmos/static/tethys_gizmos/js/datatable_view.js"; + +global.TETHYS_DATATABLE_VIEW = TETHYS_DATATABLE_VIEW; +global.Node = window.Node; +global.Option = window.Option || function Option() {}; + + +let renderedHtml = ''; + +const DATATABLE_VIEW_ID = "test_datatable_view_id"; + +beforeAll(() => { + const templateName = "tethys_gizmos/gizmos/datatable_view.html"; + const context = JSON.stringify({ + title: "Testing the datatable view gizmo", + id: DATATABLE_VIEW_ID, + + column_names: ["Column 1", "Column 2", "Column 3"], + rows: [ + ["Row 1 Column 1", "Row 1 Column 2", "Row 1 Column 3"], + ["Row 2 Column 1", "Row 2 Column 2", "Row 2 Column 3"], + ["Row 3 Column 1", "Row 3 Column 2", "Row 3 Column 3"], + ] + + }); + + execSync(`python render_template.py ${templateName} '${context}' `, { stdio: "inherit" }); + + const outputPath = path.resolve("./rendered_templates/test_datatable_view_output.html"); + renderedHtml = fs.readFileSync(outputPath, "utf8"); +}); + +beforeEach(() => { + document.body.innerHTML = renderedHtml; + + // Initialize datatable view + TETHYS_DATATABLE_VIEW.initTableView(".data_table_gizmo_view"); +}); + +test("Gizmo renders correctly", () => { + expect(screen.getByText("Column 1")).toBeInTheDocument(); + expect(screen.getByText("Column 2")).toBeInTheDocument(); + expect(screen.getByText("Column 3")).toBeInTheDocument(); + expect(screen.getByText("Row 1 Column 1")).toBeInTheDocument(); + expect(screen.getByText("Row 1 Column 2")).toBeInTheDocument(); + expect(screen.getByText("Row 1 Column 3")).toBeInTheDocument(); +}); + +test("Datatable is initialized", () => { + const dataTable = document.querySelector(".data_table_gizmo_view"); + expect(dataTable).toHaveClass("dataTable"); +}); + diff --git a/tests/js_tests/package.json b/tests/js_tests/package.json index 4324547c0..c34c64fb4 100644 --- a/tests/js_tests/package.json +++ b/tests/js_tests/package.json @@ -5,6 +5,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "babel-jest": "^29.7.0", + "datatables.net": "^2.2.2", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jquery": "^3.7.1", From 68e076a9536779459922510f6f3a103b17be5519 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Mon, 24 Feb 2025 08:47:39 -0700 Subject: [PATCH 17/29] clean up code --- tests/js_tests/gizmos/datatable_view.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/js_tests/gizmos/datatable_view.test.js b/tests/js_tests/gizmos/datatable_view.test.js index cc601177f..7a2cc320c 100644 --- a/tests/js_tests/gizmos/datatable_view.test.js +++ b/tests/js_tests/gizmos/datatable_view.test.js @@ -8,14 +8,13 @@ import $ from "jquery"; import DataTable from "datatables.net"; $.fn.DataTable = DataTable; -// Import DataTable module import TETHYS_DATATABLE_VIEW from "../../../tethys_gizmos/static/tethys_gizmos/js/datatable_view.js"; global.TETHYS_DATATABLE_VIEW = TETHYS_DATATABLE_VIEW; + global.Node = window.Node; global.Option = window.Option || function Option() {}; - let renderedHtml = ''; const DATATABLE_VIEW_ID = "test_datatable_view_id"; From 4670bc7b78410940433fb97e76565e4626776633 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Fri, 21 Mar 2025 17:17:19 -0600 Subject: [PATCH 18/29] Testing progress for select_input gizmo --- tests/js_tests/gizmos/datatable_view.test.js | 2 +- tests/js_tests/gizmos/select_input.test.js | 64 ++++++++++++++++++++ tests/js_tests/package.json | 1 + 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 tests/js_tests/gizmos/select_input.test.js diff --git a/tests/js_tests/gizmos/datatable_view.test.js b/tests/js_tests/gizmos/datatable_view.test.js index 7a2cc320c..56778b3bd 100644 --- a/tests/js_tests/gizmos/datatable_view.test.js +++ b/tests/js_tests/gizmos/datatable_view.test.js @@ -1,7 +1,7 @@ import { execSync } from "child_process"; import fs from "fs"; import path from "path"; -const { screen, fireEvent } = require("@testing-library/dom"); +const { screen } = require("@testing-library/dom"); import $ from "jquery"; diff --git a/tests/js_tests/gizmos/select_input.test.js b/tests/js_tests/gizmos/select_input.test.js new file mode 100644 index 000000000..4f85a8f8e --- /dev/null +++ b/tests/js_tests/gizmos/select_input.test.js @@ -0,0 +1,64 @@ +import { execSync } from "child_process"; +import path from "path"; +import fs from "fs"; + +const { screen } = require("@testing-library/dom"); +import userEvent from "@testing-library/user-event"; + +import TETHYS_SELECT_INPUT from "../../../tethys_gizmos/static/tethys_gizmos/js/select_input.js"; + +import $ from "jquery"; +global.$ = $; +global.jQuery = $; + +$.fn.select2 = jest.fn().mockImplementation(function () { + return this; +}); + +let renderedHtml = ""; + +beforeAll(() => { + const templateName = "tethys_gizmos/gizmos/select_input.html"; + const context = JSON.stringify({ + title: "Testing the select input gizmo", + id: "test_select_input_id", + options: [ + ["Option 1", "option1"], + ["Option 2", "option2"], + ["Option 3", "option3"], + ], + initial: "option2", + display_text: "Choose an Option", + name: "test_selector" + }); + + execSync(`python render_template.py ${templateName} '${context}'`, { stdio: "inherit" }); + const outputPath = path.resolve("./rendered_templates/test_select_input_output.html"); + renderedHtml = fs.readFileSync(outputPath, "utf8"); +}) + +beforeEach(() => { + document.body.innerHTML = renderedHtml; + TETHYS_SELECT_INPUT.initSelectInput(".tethys-select2"); +}); + +test("Gizmo renders correctly", () => { + expect(screen.getByText("Option 1")).toBeInTheDocument(); + expect(screen.getByText("Option 2")).toBeInTheDocument(); + expect(screen.getByText("Option 3")).toBeInTheDocument(); +}); + +test("The initially selected value is visible to the user", () => { + const select = screen.getByLabelText("Choose an Option"); + expect(select).toHaveDisplayValue("Option 2"); +}); + + +test("User can select a different option", async () => { + const user = userEvent.setup(); + const select = screen.getByLabelText("Choose an Option"); + + await user.selectOptions(select, "option3"); + + expect(select).toHaveDisplayValue("Option 3"); +}); \ No newline at end of file diff --git a/tests/js_tests/package.json b/tests/js_tests/package.json index c34c64fb4..706d213ec 100644 --- a/tests/js_tests/package.json +++ b/tests/js_tests/package.json @@ -4,6 +4,7 @@ "@babel/preset-env": "^7.26.9", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", + "@testing-library/user-event": "^14.6.1", "babel-jest": "^29.7.0", "datatables.net": "^2.2.2", "jest": "^29.7.0", From 8dea3b15e8d6da5cd4c21dbbf97383dd7cbb41b2 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Sat, 22 Mar 2025 12:01:15 -0600 Subject: [PATCH 19/29] Fixed render template script --- tests/js_tests/render_template.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/tests/js_tests/render_template.py b/tests/js_tests/render_template.py index 399b85fd8..23158a906 100644 --- a/tests/js_tests/render_template.py +++ b/tests/js_tests/render_template.py @@ -20,19 +20,11 @@ def confirm_template_path(template_name): try: - # Second try block wrapping is required for handling - # django errors and getting it to print in js testing - try: - select_template([template_name]) - - except Exception as e: - print(f"Error finding template: {e}") - traceback.print_exc(file=sys.stdout) - sys.exit(1) + select_template([template_name]) except Exception as e: print(f"Error finding template: {e}") - traceback.print_exc(file=sys.stdout) + sys.exit(1) @@ -48,17 +40,10 @@ def render_template(template_name, context): # Render template using context try: - try: - rendered_html = render_to_string(template_name, context) - - except Exception as e: - print(f"Error: {e}") - traceback.print_exc(file=sys.stdout) - sys.exit(1) + rendered_html = render_to_string(template_name, context) except Exception as e: print(f"Error: {e}") - traceback.print_exc(file=sys.stdout) sys.exit(1) output_path = f"./rendered_templates/test_{os.path.basename(form_output_name(template_name))}" From 74c8d791e67014b75390e110415fcaf4c4d5b19e Mon Sep 17 00:00:00 2001 From: jakeymac Date: Sat, 22 Mar 2025 12:37:45 -0600 Subject: [PATCH 20/29] fix for front end testing in CI --- .github/workflows/tethys.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tethys.yml b/.github/workflows/tethys.yml index 611311a30..4c05bfa77 100644 --- a/.github/workflows/tethys.yml +++ b/.github/workflows/tethys.yml @@ -106,9 +106,11 @@ jobs: - name: Install Dependencies run: npm install + working-directory: tethys/tests/js_tests - name: Run Tests run: npm test + working-directory: tethys/tests/js_tests docker-build: name: Docker Build (${{ matrix.platform }}, ${{ matrix.django-version }}, ${{ matrix.python-version }}) From 194786e4472f96c236e133961da58f03fb3b8e50 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Sat, 22 Mar 2025 17:35:57 -0600 Subject: [PATCH 21/29] fix for CI testing --- .github/workflows/tethys.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tethys.yml b/.github/workflows/tethys.yml index 4c05bfa77..138e6d164 100644 --- a/.github/workflows/tethys.yml +++ b/.github/workflows/tethys.yml @@ -106,11 +106,11 @@ jobs: - name: Install Dependencies run: npm install - working-directory: tethys/tests/js_tests + working-directory: tests/js_tests - name: Run Tests run: npm test - working-directory: tethys/tests/js_tests + working-directory: tests/js_tests docker-build: name: Docker Build (${{ matrix.platform }}, ${{ matrix.django-version }}, ${{ matrix.python-version }}) From 6c47881a7d3bd26e947c90f9c5b195ef5b56e319 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Sat, 22 Mar 2025 17:43:34 -0600 Subject: [PATCH 22/29] added django dependency for running front end tests --- .github/workflows/tethys.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/tethys.yml b/.github/workflows/tethys.yml index 138e6d164..c69298ae5 100644 --- a/.github/workflows/tethys.yml +++ b/.github/workflows/tethys.yml @@ -103,6 +103,16 @@ jobs: uses: actions/setup-node@v3 with: node-version: 18 + + - name: Set Up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install Python Dependencies + run: | + python -m pip install --upgrade pip + pip install django - name: Install Dependencies run: npm install From dbdba0800672d2cc44cdcc99ab83be875dfc8af4 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Sat, 22 Mar 2025 18:41:01 -0600 Subject: [PATCH 23/29] CI Update --- .github/workflows/tethys.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tethys.yml b/.github/workflows/tethys.yml index c69298ae5..3d0ba7de1 100644 --- a/.github/workflows/tethys.yml +++ b/.github/workflows/tethys.yml @@ -107,19 +107,25 @@ jobs: - name: Set Up Python uses: actions/setup-python@v4 with: - python-version: '3.11' + python-version: '3.12' - - name: Install Python Dependencies + - name: Install Tethys and Dependencies run: | + + bash ./tethys/scripts/install_tethys.sh --partial-tethys-install meds -n tethys -s $PWD/tethys -x -d 5.1 --python-version 3.12 + . ~/miniconda/etc/profile.d/conda.sh + conda activate tethys python -m pip install --upgrade pip - pip install django - + - name: Install Dependencies run: npm install working-directory: tests/js_tests - name: Run Tests - run: npm test + run: | + . ~/miniconda/etc/profile.d/conda.sh + conda activate tethys + npm test working-directory: tests/js_tests docker-build: From f82d667e35787568331b56949b6c5b1b136c7f91 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Sat, 22 Mar 2025 18:45:09 -0600 Subject: [PATCH 24/29] fix for frontend tests CI job --- .github/workflows/tethys.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tethys.yml b/.github/workflows/tethys.yml index 3d0ba7de1..fda061285 100644 --- a/.github/workflows/tethys.yml +++ b/.github/workflows/tethys.yml @@ -112,7 +112,7 @@ jobs: - name: Install Tethys and Dependencies run: | - bash ./tethys/scripts/install_tethys.sh --partial-tethys-install meds -n tethys -s $PWD/tethys -x -d 5.1 --python-version 3.12 + bash scripts/install_tethys.sh --partial-tethys-install meds -n tethys -s $PWD/tethys -x -d 5.1 --python-version 3.12 . ~/miniconda/etc/profile.d/conda.sh conda activate tethys python -m pip install --upgrade pip From ce1501ce6ef33cf6abf6d6f823bf4bbf79f17f7f Mon Sep 17 00:00:00 2001 From: jakeymac Date: Mon, 24 Mar 2025 11:27:20 -0600 Subject: [PATCH 25/29] CI fix --- .github/workflows/tethys.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tethys.yml b/.github/workflows/tethys.yml index fda061285..d64f655a6 100644 --- a/.github/workflows/tethys.yml +++ b/.github/workflows/tethys.yml @@ -112,7 +112,7 @@ jobs: - name: Install Tethys and Dependencies run: | - bash scripts/install_tethys.sh --partial-tethys-install meds -n tethys -s $PWD/tethys -x -d 5.1 --python-version 3.12 + bash scripts/install_tethys.sh --partial-tethys-install meds -n tethys -s $PWD -x -d 5.1 --python-version 3.12 . ~/miniconda/etc/profile.d/conda.sh conda activate tethys python -m pip install --upgrade pip From ed05e01438ef8acf4e1ffb44a75caa0f9c2b12d7 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Mon, 24 Mar 2025 11:34:19 -0600 Subject: [PATCH 26/29] Test fix + black fix --- tests/js_tests/package.json | 3 +++ tests/js_tests/render_template.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/js_tests/package.json b/tests/js_tests/package.json index 706d213ec..a706d2c04 100644 --- a/tests/js_tests/package.json +++ b/tests/js_tests/package.json @@ -16,5 +16,8 @@ "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage" + }, + "jest": { + "testEnvironment": "jsdom" } } diff --git a/tests/js_tests/render_template.py b/tests/js_tests/render_template.py index 23158a906..65723411b 100644 --- a/tests/js_tests/render_template.py +++ b/tests/js_tests/render_template.py @@ -24,7 +24,7 @@ def confirm_template_path(template_name): except Exception as e: print(f"Error finding template: {e}") - + sys.exit(1) From a0a871f106ad569358d78d84361322a96be412c9 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Mon, 24 Mar 2025 11:41:44 -0600 Subject: [PATCH 27/29] fix in frontend testing configuration --- tests/js_tests/jest.config.js | 2 +- tests/js_tests/package.json | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/js_tests/jest.config.js b/tests/js_tests/jest.config.js index 524279d73..a3d364bbf 100644 --- a/tests/js_tests/jest.config.js +++ b/tests/js_tests/jest.config.js @@ -1,5 +1,5 @@ module.exports = { - testEnvironment: "", + testEnvironment: "jsdom", roots: [""], setupFilesAfterEnv: ["/setup.js"], collectCoverage: true, diff --git a/tests/js_tests/package.json b/tests/js_tests/package.json index a706d2c04..706d213ec 100644 --- a/tests/js_tests/package.json +++ b/tests/js_tests/package.json @@ -16,8 +16,5 @@ "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage" - }, - "jest": { - "testEnvironment": "jsdom" } } From ee3cc29b33a1b39dfb3d129a9cf778cd37206eea Mon Sep 17 00:00:00 2001 From: jakeymac Date: Mon, 31 Mar 2025 21:04:29 -0600 Subject: [PATCH 28/29] fixes in frontend testing configuration updated slide_sheet gizmo testing --- .github/workflows/tethys.yml | 2 +- tests/js_tests/gizmos/slide_sheet.test.js | 93 +++++++++++-------- tests/js_tests/jest.config.js | 1 + tests/js_tests/jest.polyfills.js | 3 + tests/js_tests/setup.js | 10 -- .../static/tethys_gizmos/js/slide_sheet.js | 3 + 6 files changed, 63 insertions(+), 49 deletions(-) create mode 100644 tests/js_tests/jest.polyfills.js diff --git a/.github/workflows/tethys.yml b/.github/workflows/tethys.yml index d64f655a6..3b4db6e6b 100644 --- a/.github/workflows/tethys.yml +++ b/.github/workflows/tethys.yml @@ -93,7 +93,7 @@ jobs: coveralls --service=github frontend-tests: - name: Frontend Tests With Jest + name: Frontend Tests runs-on: ubuntu-latest steps: - name: Checkout Repo diff --git a/tests/js_tests/gizmos/slide_sheet.test.js b/tests/js_tests/gizmos/slide_sheet.test.js index b3093ba29..8111545fa 100644 --- a/tests/js_tests/gizmos/slide_sheet.test.js +++ b/tests/js_tests/gizmos/slide_sheet.test.js @@ -1,70 +1,87 @@ import { execSync } from "child_process"; import fs from "fs"; import path from "path"; -const { screen, fireEvent } = require("@testing-library/dom"); +const { screen, fireEvent, waitFor } = require("@testing-library/dom"); import SLIDE_SHEET from "../../../tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js" -global.SLIDE_SHEET = SLIDE_SHEET; +window.SLIDE_SHEET = SLIDE_SHEET; let renderedHtml = ''; const SLIDE_SHEET_ID = "test_slide_sheet_id" +function simulateShowClassStyling() { + // Simulate the CSS class 'show' to display or hide the slide sheet + // To be used in the tests any time a slide sheet is opened or closed + // manually or with a simulated button press + document.querySelectorAll('.slide-sheet').forEach((element) => { + if (element.classList.contains('show')) { + element.style.display = 'block'; + } + else { + element.style.display = 'none'; + } + }); +} + beforeAll(() => { + // Render the template using Python script with a provided context const templateName = "tethys_gizmos/gizmos/slide_sheet.html"; const context = JSON.stringify({ title: "Testing the slide sheet gizmo", id: SLIDE_SHEET_ID, content_template: "test_template.html" }); - execSync(`python render_template.py ${templateName} '${context}' `, { stdio: "inherit" }); - + // Read the rendered HTML file const outputPath = path.resolve("./rendered_templates/test_slide_sheet_output.html"); renderedHtml = fs.readFileSync(outputPath, "utf8"); }) beforeEach(() => { + // Set up the document body with the rendered HTML before each test runs document.body.innerHTML = renderedHtml; - let testOpenButton = document.createElement("button"); - testOpenButton.textContent = "Open Slide Sheet"; - testOpenButton.onclick = () => SLIDE_SHEET.open(SLIDE_SHEET_ID); - document.body.appendChild(testOpenButton); - - // Add event listeners to all elements with an inline onclick attribute - document.querySelectorAll("[onclick]").forEach((el) => { - const onClickAttr = el.getAttribute("onclick"); - - // Create a new function from the onclick attribute string - if (onClickAttr) { - el.addEventListener("click", function () { - eval(onClickAttr); // 🔥 Execute the original inline script - }); - } - }); }); - - test("Gizmo renders correctly", () => { - expect(screen.getByText("Testing the slide sheet gizmo")).toBeInTheDocument(); + const sheet = document.getElementById(SLIDE_SHEET_ID); + expect(sheet).toBeInTheDocument(); + expect(sheet).toHaveClass("slide-sheet"); }); -test("Clicking open button opens slide sheet", () => { - const slideSheet = document.getElementById(SLIDE_SHEET_ID); - fireEvent.click(screen.getByText("Open Slide Sheet")); - expect(slideSheet.classList.contains("show")).toBe(true); - -}) - -test("Clicking close button closes slide sheet", () => { - const slideSheet = document.getElementById(SLIDE_SHEET_ID); - SLIDE_SHEET.open(SLIDE_SHEET_ID); - expect(slideSheet.classList.contains("show")).toBe(true); - - const closeButton = screen.getByLabelText("Close"); - fireEvent.click(closeButton); - expect(slideSheet.classList.contains("show")).toBe(false); +test("Clicking the open button should show the slide sheet", async () => { + const sheet = document.getElementById(SLIDE_SHEET_ID); + // Make sure the sheet is not visible to start off + expect(sheet).not.toBeVisible(); + // Add a button to open the slide sheet + const button = document.createElement("button"); + button.className = "btn-open"; + button.textContent = "Open Slide Sheet"; + document.body.appendChild(button); + // Add an event listener to the button to open the slide sheet + const openButton = screen.getByText("Open Slide Sheet"); + openButton.addEventListener("click", () => { + SLIDE_SHEET.open(SLIDE_SHEET_ID); + }); + fireEvent.click(openButton); + simulateShowClassStyling(); + // Check that the slide sheet is now visible + expect(sheet).toBeVisible(); }); + +test("Clicking the close button should hide the slide sheet", async () => { + // Open the slide sheet first + SLIDE_SHEET.open(SLIDE_SHEET_ID); + simulateShowClassStyling(); + const sheet = document.getElementById(SLIDE_SHEET_ID); + // Make sure the sheet is visible to start off + expect(sheet).toBeVisible(); + // Now close it + const closeButton = document.querySelector(".btn-close"); + fireEvent.click(closeButton); + simulateShowClassStyling(); + // Check that the sheet is no longer visible + expect(sheet).not.toBeVisible(); +}); \ No newline at end of file diff --git a/tests/js_tests/jest.config.js b/tests/js_tests/jest.config.js index a3d364bbf..ed031f960 100644 --- a/tests/js_tests/jest.config.js +++ b/tests/js_tests/jest.config.js @@ -1,6 +1,7 @@ module.exports = { testEnvironment: "jsdom", roots: [""], + setupFiles: ["/jest.polyfills.js"], setupFilesAfterEnv: ["/setup.js"], collectCoverage: true, coverageDirectory: "jest_coverage", diff --git a/tests/js_tests/jest.polyfills.js b/tests/js_tests/jest.polyfills.js new file mode 100644 index 000000000..5e8b3c1af --- /dev/null +++ b/tests/js_tests/jest.polyfills.js @@ -0,0 +1,3 @@ +import { TextEncoder, TextDecoder } from "util"; +global.TextEncoder = TextEncoder; +global.TextDecoder = TextDecoder; \ No newline at end of file diff --git a/tests/js_tests/setup.js b/tests/js_tests/setup.js index 1825ba27c..2ad57849e 100644 --- a/tests/js_tests/setup.js +++ b/tests/js_tests/setup.js @@ -1,13 +1,3 @@ -import { TextEncoder, TextDecoder } from "util"; - -if (typeof global.TextEncoder === "undefined") { - global.TextEncoder = TextEncoder; -} - -if (typeof global.TextDecoder === "undefined") { - global.TextDecoder = TextDecoder; -} - try { require("cesium"); } catch (e) { diff --git a/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js b/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js index 01084a73a..b607884d5 100644 --- a/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js +++ b/tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js @@ -21,6 +21,9 @@ var SLIDE_SHEET = (function() { // Slide sheet var open_slide_sheet, close_slide_sheet; + // Private functions + var open, close; + /************************************************************************ * PRIVATE FUNCTION IMPLEMENTATIONS *************************************************************************/ From 257b755851826606af56ab34633050b62582efd4 Mon Sep 17 00:00:00 2001 From: jakeymac Date: Tue, 1 Apr 2025 17:22:24 -0600 Subject: [PATCH 29/29] Toggle switch testing added --- tests/js_tests/gizmos/slide_sheet.test.js | 2 +- tests/js_tests/gizmos/toggle_switch.test.js | 68 +++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 tests/js_tests/gizmos/toggle_switch.test.js diff --git a/tests/js_tests/gizmos/slide_sheet.test.js b/tests/js_tests/gizmos/slide_sheet.test.js index 8111545fa..a38edb400 100644 --- a/tests/js_tests/gizmos/slide_sheet.test.js +++ b/tests/js_tests/gizmos/slide_sheet.test.js @@ -1,7 +1,7 @@ import { execSync } from "child_process"; import fs from "fs"; import path from "path"; -const { screen, fireEvent, waitFor } = require("@testing-library/dom"); +const { screen, fireEvent } = require("@testing-library/dom"); import SLIDE_SHEET from "../../../tethys_gizmos/static/tethys_gizmos/js/slide_sheet.js" diff --git a/tests/js_tests/gizmos/toggle_switch.test.js b/tests/js_tests/gizmos/toggle_switch.test.js new file mode 100644 index 000000000..2214cfa9d --- /dev/null +++ b/tests/js_tests/gizmos/toggle_switch.test.js @@ -0,0 +1,68 @@ +import { execSync } from "child_process"; +import fs from "fs"; +import path from "path"; +const { screen, fireEvent } = require("@testing-library/dom"); + +import TOGGLE_SWITCH from "../../../tethys_gizmos/static/tethys_gizmos/js/toggle_switch.js" + +window.TOGGLE_SWITCH = TOGGLE_SWITCH; + +import $ from "jquery"; +global.$ = $; +global.jQuery = $; +$.fn.bootstrapSwitch = jest.fn(); + +let renderedHtml = ''; + +beforeAll(() => { + // Render the template using Python script with a provided context + const templateName = "tethys_gizmos/gizmos/toggle_switch.html"; + const context = JSON.stringify({ + name: "test_toggle_switch", + display_text: "Test Toggle Switch", + on_label: "On Label Text", + off_label: "Off Label Text", + initial: false + }); + execSync(`python render_template.py ${templateName} '${context}' `, { stdio: "inherit" }); + // Read the rendered HTML file + const outputPath = path.resolve("./rendered_templates/test_toggle_switch_output.html"); + renderedHtml = fs.readFileSync(outputPath, "utf8"); +}); + +beforeEach(() => { + // Set up the document body with the rendered HTML before each test runs + document.body.innerHTML = renderedHtml; + + // Initialize the toggle switch + TOGGLE_SWITCH.initToggleSwitch(".bootstrap-switch"); +}); + +test("Gizmo renders correctly", () => { + const toggleSwitch = screen.getByLabelText("Test Toggle Switch"); + expect(toggleSwitch).toBeInTheDocument(); +}); + +test("Toggle switch is unchecked by default", () => { + const toggleInput = screen.getByLabelText("Test Toggle Switch"); + expect(toggleInput).not.toBeChecked(); +}); + +test("Toggle switch initialization calls bootstrapSwitch", () => { + expect($.fn.bootstrapSwitch).toHaveBeenCalled(); +}); + +test("User can toggle the switch", () => { + const toggleInput = screen.getByLabelText("Test Toggle Switch"); + + // Check if the toggle switch is initially unchecked + expect(toggleInput).not.toBeChecked(); + + // Simulate click + fireEvent.click(toggleInput); + expect(toggleInput).toBeChecked(); + + // Simulate click again + fireEvent.click(toggleInput); + expect(toggleInput).not.toBeChecked(); +}); \ No newline at end of file