diff --git a/.github/workflows/jest.yml b/.github/workflows/jest.yml index 3d1c7b780..99f2d8d8e 100644 --- a/.github/workflows/jest.yml +++ b/.github/workflows/jest.yml @@ -1,13 +1,13 @@ -# Runs jest based unit tests for the binderhub-client JS package -name: "binderhub-client unit tests" +# Runs jest based unit tests for frontend javascript and @jupyterhub/binderhub-client +name: "JS Unit tests" on: pull_request: - paths: + paths: &paths + - "binderhub/static/js/**" - "js/packages/binderhub-client/**" push: - paths: - - "js/**" + paths: *paths branches-ignore: - "dependabot/**" - "pre-commit-ci-update-config" @@ -15,15 +15,18 @@ on: workflow_dispatch: jobs: - build: + test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - run: | - cd js/packages/binderhub-client + - name: "binderhub unit tests" + run: | npm install + npm test - - run: | + - name: "@jupyterhub/binderhub-client unit tests" + run: | cd js/packages/binderhub-client + npm install npm test diff --git a/binderhub/static/js/index.js b/binderhub/static/js/index.js index 30398630c..cd1f9c871 100644 --- a/binderhub/static/js/index.js +++ b/binderhub/static/js/index.js @@ -19,6 +19,7 @@ import { BinderRepository } from '@jupyterhub/binderhub-client'; import { makeBadgeMarkup } from './src/badge'; import { getPathType, updatePathText } from './src/path'; import { nextHelpText } from './src/loading'; +import { updateFavicon } from './src/favicon'; import 'xterm/css/xterm.css'; @@ -33,14 +34,6 @@ const BASE_URL = $('#base-url').data().url; const BADGE_BASE_URL = $('#badge-base-url').data().url; let config_dict = {}; -function update_favicon(path) { - let link = document.querySelector("link[rel*='icon']") || document.createElement('link'); - link.type = 'image/x-icon'; - link.rel = 'shortcut icon'; - link.href = path; - document.getElementsByTagName('head')[0].appendChild(link); -} - function v2url(providerPrefix, repository, ref, path, pathType) { // return a v2 url from a providerPrefix, repository, ref, and (file|url)path if (repository.length === 0) { @@ -153,7 +146,7 @@ function updateUrls(formValues) { } function build(providerSpec, log, fitAddon, path, pathType) { - update_favicon(BASE_URL + "favicon_building.ico"); + updateFavicon(BASE_URL + "favicon_building.ico"); // split provider prefix off of providerSpec const spec = providerSpec.slice(providerSpec.indexOf('/') + 1); // Update the text of the loading page if it exists @@ -199,7 +192,7 @@ function build(providerSpec, log, fitAddon, path, pathType) { $("#loader").addClass("paused"); // If we fail for any reason, show an error message and logs - update_favicon(BASE_URL + "favicon_fail.ico"); + updateFavicon(BASE_URL + "favicon_fail.ico"); log.show(); if ($('div#loader-text').length > 0) { $('#loader').addClass("error"); @@ -214,7 +207,7 @@ function build(providerSpec, log, fitAddon, path, pathType) { $('#phase-launching').removeClass('hidden'); } $('#phase-launching').removeClass('hidden'); - update_favicon(BASE_URL + "favicon_success.ico"); + updateFavicon(BASE_URL + "favicon_success.ico"); }); image.onStateChange('ready', function(oldState, newState, data) { diff --git a/binderhub/static/js/src/favicon.js b/binderhub/static/js/src/favicon.js new file mode 100644 index 000000000..1353226dc --- /dev/null +++ b/binderhub/static/js/src/favicon.js @@ -0,0 +1,17 @@ +/** + * Dynamically set current page's favicon. + * + * @param {String} href Path to Favicon to use + */ +function updateFavicon(href) { + let link = document.querySelector("link[rel*='icon']"); + if(!link) { + link = document.createElement('link'); + document.getElementsByTagName('head')[0].appendChild(link); + } + link.type = 'image/x-icon'; + link.rel = 'shortcut icon'; + link.href = href; +} + +export { updateFavicon }; diff --git a/binderhub/static/js/src/favicon.test.js b/binderhub/static/js/src/favicon.test.js new file mode 100644 index 000000000..dfd515d3a --- /dev/null +++ b/binderhub/static/js/src/favicon.test.js @@ -0,0 +1,29 @@ +import { updateFavicon } from "./favicon"; + +afterEach(() => { + // Clear out HEAD after each test run, so our DOM is clean. + // Jest does *not* clear out the DOM between test runs on the same file! + document.querySelector("head").innerHTML = ''; +}); + +test("Setting favicon when there is none works", () => { + expect(document.querySelector("link[rel*='icon']")).toBeNull(); + + updateFavicon("https://example.com/somefile.png"); + + expect(document.querySelector("link[rel*='icon']").href).toBe("https://example.com/somefile.png"); +}); + +test("Setting favicon multiple times works without leaking link tags", () => { + expect(document.querySelector("link[rel*='icon']")).toBeNull(); + + updateFavicon("https://example.com/somefile.png"); + + expect(document.querySelector("link[rel*='icon']").href).toBe("https://example.com/somefile.png"); + expect(document.querySelectorAll("link[rel*='icon']").length).toBe(1); + + updateFavicon("https://example.com/some-other-file.png"); + + expect(document.querySelector("link[rel*='icon']").href).toBe("https://example.com/some-other-file.png"); + expect(document.querySelectorAll("link[rel*='icon']").length).toBe(1); +}); diff --git a/package.json b/package.json index cfdaeacc6..28c367353 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,13 @@ "@babel/cli": "^7.21.0", "@babel/core": "^7.21.4", "@babel/preset-env": "^7.21.4", + "@types/jest": "^29.5.5", + "babel-jest": "^29.7.0", "babel-loader": "^9.1.2", "css-loader": "^6.7.3", "eslint": "^8.38.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "mini-css-extract-plugin": "^2.7.5", "webpack": "^5.78.0", "webpack-cli": "^5.0.1" @@ -26,6 +30,10 @@ "scripts": { "webpack": "webpack", "webpack:watch": "webpack --watch", - "lint": "eslint binderhub/static/js" + "lint": "eslint binderhub/static/js", + "test": "jest" + }, + "jest": { + "testEnvironment": "jsdom" } }