diff --git a/.github/workflows/jest.yml b/.github/workflows/jest.yml index 0223df31c..ea29486da 100644 --- a/.github/workflows/jest.yml +++ b/.github/workflows/jest.yml @@ -5,12 +5,12 @@ on: pull_request: paths: - "binderhub/static/js/**" - - "js/packages/binderhub-client/**" + - "js/**" - ".github/workflows/jest.yml" push: paths: - "binderhub/static/js/**" - - "js/packages/binderhub-client/**" + - "js/**" - ".github/workflows/jest.yml" branches-ignore: - "dependabot/**" diff --git a/binderhub/repoproviders.py b/binderhub/repoproviders.py index 538a88c3d..799901545 100644 --- a/binderhub/repoproviders.py +++ b/binderhub/repoproviders.py @@ -226,8 +226,13 @@ class FakeProvider(RepoProvider): "displayName": "Fake", "id": "fake", "enabled": False, - "spec": {"validateRegex": ".*"}, - "repo": {"label": "Fake Repo", "placeholder": "", "urlEncode": False}, + "spec": {"validateRegex": ".+"}, + "detect": {"regex": "(?.+)"}, + "repo": { + "label": "Fake Repo", + "placeholder": "example: fake", + "urlEncode": False, + }, "ref": { "enabled": False, }, diff --git a/js/packages/binderhub-react-components/babel.config.js b/js/packages/binderhub-react-components/babel.config.js new file mode 100644 index 000000000..898ec947a --- /dev/null +++ b/js/packages/binderhub-react-components/babel.config.js @@ -0,0 +1,6 @@ +module.exports = { + presets: [ + ["@babel/preset-env", { targets: { node: "current" } }], + ["@babel/preset-react", { runtime: "automatic" }], + ], +}; diff --git a/js/packages/binderhub-react-components/package.json b/js/packages/binderhub-react-components/package.json index ee17021b2..f242f327c 100644 --- a/js/packages/binderhub-react-components/package.json +++ b/js/packages/binderhub-react-components/package.json @@ -7,8 +7,13 @@ }, "scripts": { "build": "esbuild src/*.jsx --loader:.ico=dataurl --bundle --external:react --outdir=dist", - "lint": "eslint binderhub/static/js js", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "jest" + }, + "jest": { + "testEnvironment": "jsdom", + "setupFilesAfterEnv": [ + "/../../../setupTests.js" + ] }, "repository": { "type": "git", @@ -24,6 +29,8 @@ }, "homepage": "https://github.com/jupyterhub/binderhub#readme", "devDependencies": { + "@babel/preset-env": "^7.28.3", + "@babel/preset-react": "^7.27.1", "esbuild": "^0.25.6", "eslint": "^9.31.0" }, diff --git a/js/packages/binderhub-react-components/src/LinkGenerator.jsx b/js/packages/binderhub-react-components/src/LinkGenerator.jsx index 6288b45c3..40b310bc7 100644 --- a/js/packages/binderhub-react-components/src/LinkGenerator.jsx +++ b/js/packages/binderhub-react-components/src/LinkGenerator.jsx @@ -201,6 +201,8 @@ export function LinkGenerator({ const results = re.exec(repo); if (results !== null && results.groups && results.groups.repo) { setRepo(results.groups.repo); + } else { + setRepo(""); } } else { setRepo(e.target.value); diff --git a/js/packages/binderhub-react-components/src/LinkGenerator.test.jsx b/js/packages/binderhub-react-components/src/LinkGenerator.test.jsx new file mode 100644 index 000000000..677b400a5 --- /dev/null +++ b/js/packages/binderhub-react-components/src/LinkGenerator.test.jsx @@ -0,0 +1,77 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { LinkGenerator } from "./LinkGenerator"; +import { useState } from "react"; + +const mockProviders = window.pageConfig.repoProviders; + +const publicBaseUrl = new URL("https://example.org/"); + +function TestLinkGeneratorWrapper() { + const [selectedProvider, setSelectedProvider] = useState(mockProviders[0]); + const [repo, setRepo] = useState(""); + const [reference, setReference] = useState(""); + const [urlPath, setUrlPath] = useState(""); + const [isLaunching, setIsLaunching] = useState(false); + + return ( + + ); +} + +describe("LinkGenerator", () => { + it("updates launch-url from repo, ref and file", async () => { + const user = userEvent.setup(); + render(); + + // This lookup uses the aria label + const repoInput = screen.getByRole("textbox", { + name: "Enter repository URL", + }); + await user.type(repoInput, "my-org/my-repo"); + + const refInput = screen.getByLabelText("Git ref (branch, tag, or commit)"); + await user.type(refInput, "my-branch"); + + const pathInput = screen.getByLabelText("File to open (in JupyterLab)"); + await user.type(pathInput, "notebooks/test.ipynb"); + + const expectedUrl = + "https://example.org/v2/gh/my-org/my-repo/my-branch?urlpath=%2Fdoc%2Ftree%2Fnotebooks%2Ftest.ipynb"; + expect(screen.getByTestId("launch-url").textContent).toBe(expectedUrl); + }); + + it("renders initial placeholder and restores it if repo is deleted", async () => { + const user = userEvent.setup(); + render(); + + const defaultLaunchUrl = + "Fill in the fields to see a URL for sharing your Binder."; + expect(screen.getByTestId("launch-url").textContent).toBe(defaultLaunchUrl); + + const repoInput = screen.getByRole("textbox", { + name: "Enter repository URL", + }); + await user.type(repoInput, "x"); + + expect(screen.getByTestId("launch-url").textContent).toBe( + "https://example.org/v2/gh/x/HEAD", + ); + + await user.type(repoInput, "{backspace}"); + expect(screen.getByTestId("launch-url").textContent).toBe(defaultLaunchUrl); + }); +}); diff --git a/testing/local-binder-mocked-hub/binderhub_config.py b/testing/local-binder-mocked-hub/binderhub_config.py index 142b36c34..e8ead12e7 100644 --- a/testing/local-binder-mocked-hub/binderhub_config.py +++ b/testing/local-binder-mocked-hub/binderhub_config.py @@ -14,7 +14,7 @@ c.BinderHub.use_registry = True c.BinderHub.registry_class = FakeRegistry c.BinderHub.builder_required = False -c.BinderHub.repo_providers = {"gh": FakeProvider} +c.BinderHub.repo_providers = {"fake": FakeProvider} c.BinderHub.build_class = FakeBuild # Uncomment the following line to enable BinderHub's API only mode