diff --git a/.github/actions/Emscripten-Notebook-Tests/action.yml b/.github/actions/Emscripten-Notebook-Tests/action.yml
new file mode 100644
index 00000000..f4a0b1d8
--- /dev/null
+++ b/.github/actions/Emscripten-Notebook-Tests/action.yml
@@ -0,0 +1,42 @@
+name: 'Run kernel in notebook within Jupyter Lite'
+description: 'This action runs the chosen kernel in notebook within Jupyter Lite'
+
+inputs:
+ notebook:
+ description: "The notebook to run the kernel in"
+ required: true
+ type: string
+ kernel:
+ description: "The kernel to use"
+ required: true
+ type: string
+
+runs:
+ using: composite
+ steps:
+ - name: Jupyter Lite integration test
+ shell: bash -l {0}
+ run: |
+ set -e
+ micromamba activate xeus-lite-host
+ export INPUT_TEXT=""
+ if [[ "${{ inputs.notebook }}" == "xeus-cpp-lite-demo.ipynb"* ]]; then
+ export INPUT_TEXT="--stdin Smudge"
+ fi
+ echo "Running xeus-cpp in Jupter Lite in Chrome"
+ python -u scripts/automated-notebook-run-script.py --driver chrome --notebook ${{ inputs.notebook }} --kernel ${{ inputs.kernel }} $INPUT_TEXT
+ nbdiff notebooks/${{ inputs.notebook }} $HOME/Downloads/${{ inputs.notebook }} --ignore-id --ignore-metadata >> chrome_diff.txt
+ export CHROME_TESTS_RETURN_VALUE=$( [ -s chrome_diff.txt ] && echo 1 || echo 0 )
+ rm $HOME/Downloads/${{ inputs.notebook }}
+ echo "Running xeus-cpp in Jupter Lite in Firefox"
+ python -u scripts/automated-notebook-run-script.py --driver firefox --notebook ${{ inputs.notebook }} --kernel ${{ inputs.kernel }} $INPUT_TEXT
+ nbdiff notebooks/${{ inputs.notebook }} $HOME/Downloads/${{ inputs.notebook }} --ignore-id --ignore-metadata >> firefox_diff.txt
+ export FIREFOX_TESTS_RETURN_VALUE=$( [ -s firefox_diff.txt ] && echo 1 || echo 0 )
+ rm $HOME/Downloads/${{ inputs.notebook }}
+ if [[ $FIREFOX_TESTS_RETURN_VALUE -ne 0 || $CHROME_TESTS_RETURN_VALUE -ne 0 ]]; then
+ echo "Diff Firefox (blank means no diff)"
+ cat firefox_diff.txt
+ echo "Diff Chrome (blank means no diff)"
+ cat chrome_diff.txt
+ exit 1
+ fi
diff --git a/.github/actions/Jupyter-serve/action.yml b/.github/actions/Jupyter-serve/action.yml
new file mode 100644
index 00000000..846dae17
--- /dev/null
+++ b/.github/actions/Jupyter-serve/action.yml
@@ -0,0 +1,34 @@
+name: 'This sets up our Jupyter Lite website, so we can run xeus-cpp in it'
+description: 'This action sets up our Jupyter Lite website, so we can run xeus-cpp in it'
+
+runs:
+ using: composite
+ steps:
+ - name: Jupyter Lite integration test
+ shell: bash -l {0}
+ run: |
+ set -e
+ micromamba create -n xeus-lite-host jupyter_server jupyterlite-xeus -c conda-forge
+ micromamba activate xeus-lite-host
+ if [[ "${{ matrix.os }}" == "macos"* ]]; then
+ brew install coreutils
+ export PATH="$HOMEBREW_PREFIX/opt/coreutils/libexec/gnubin:$PATH"
+ fi
+ timeout 1800 jupyter lite serve --settings-overrides=overrides.json \
+ --XeusAddon.prefix=${{ env.PREFIX }} \
+ --XeusAddon.mounts="${{ env.PREFIX }}/share/xeus-cpp/tagfiles:/share/xeus-cpp/tagfiles" \
+ --XeusAddon.mounts="${{ env.PREFIX }}/etc/xeus-cpp/tags.d:/etc/xeus-cpp/tags.d" \
+ --contents README.md \
+ --contents notebooks/xeus-cpp-lite-demo.ipynb \
+ --contents notebooks/tinyraytracer.ipynb \
+ --contents notebooks/images/marie.png \
+ --contents notebooks/audio/audio.wav \
+ --output-dir dist &
+ # There is a bug in nbdime after 3.2.0 where it will show the filenames as if there was a diff
+ # but there is no diff with the options chosen below (the latest doesn't show a diff, just the filenames with +++
+ # and --- as if it was planning to show a diff.
+ python -m pip install nbdime==3.2.0 selenium
+ # This sleep is to force enough time for the jupyter site to build before trying
+ # to run notebooks in it. If you try to run the notebooks before the website is
+ # ready the ci python script will crash saying ti cannot access the url
+ sleep 10
diff --git a/.github/workflows/deploy-github-page.yml b/.github/workflows/deploy-github-page.yml
index 82c27c95..d63b8d3d 100644
--- a/.github/workflows/deploy-github-page.yml
+++ b/.github/workflows/deploy-github-page.yml
@@ -139,21 +139,50 @@ jobs:
fi
timeout-minutes: 4
- - name: Jupyter Lite integration
- shell: bash -l {0}
- run: |
- micromamba create -n xeus-lite-host jupyter_server jupyterlite-xeus -c conda-forge
- micromamba activate xeus-lite-host
- jupyter lite build \
- --XeusAddon.prefix=${{ env.PREFIX }} \
- --XeusAddon.mounts="${{ env.PREFIX }}/share/xeus-cpp/tagfiles:/share/xeus-cpp/tagfiles" \
- --XeusAddon.mounts="${{ env.PREFIX }}/etc/xeus-cpp/tags.d:/etc/xeus-cpp/tags.d" \
- --contents README.md \
- --contents notebooks/xeus-cpp-lite-demo.ipynb \
- --contents notebooks/tinyraytracer.ipynb \
- --contents notebooks/images/marie.png \
- --contents notebooks/audio/audio.wav \
- --output-dir dist
+ - name: Serve Jupyter Lite website
+ uses: ./.github/actions/Jupyter-serve
+
+ - name: Test C++17 kernel in xeus-cpp-lite-demo.ipynb
+ uses: ./.github/actions/Emscripten-Notebook-Tests
+ with:
+ notebook: "xeus-cpp-lite-demo.ipynb"
+ kernel: "C++17"
+ timeout-minutes: 5
+
+ - name: Test C++20 kernel in xeus-cpp-lite-demo.ipynb
+ uses: ./.github/actions/Emscripten-Notebook-Tests
+ with:
+ notebook: "xeus-cpp-lite-demo.ipynb"
+ kernel: "C++20"
+ timeout-minutes: 5
+
+ - name: Test C++23 kernel in xeus-cpp-lite-demo.ipynb
+ uses: ./.github/actions/Emscripten-Notebook-Tests
+ with:
+ notebook: "xeus-cpp-lite-demo.ipynb"
+ kernel: "C++23"
+ timeout-minutes: 5
+
+ - name: Test C++17 kernel in tinyraytracer.ipynb
+ uses: ./.github/actions/Emscripten-Notebook-Tests
+ with:
+ notebook: "tinyraytracer.ipynb"
+ kernel: "C++17"
+ timeout-minutes: 5
+
+ - name: Test C++20 kernel in tinyraytracer.ipynb
+ uses: ./.github/actions/Emscripten-Notebook-Tests
+ with:
+ notebook: "tinyraytracer.ipynb"
+ kernel: "C++20"
+ timeout-minutes: 5
+
+ - name: Test C++23 kernel in tinyraytracer.ipynb
+ uses: ./.github/actions/Emscripten-Notebook-Tests
+ with:
+ notebook: "tinyraytracer.ipynb"
+ kernel: "C++23"
+ timeout-minutes: 5
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index d6e227b7..361d8a4c 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -335,12 +335,50 @@ jobs:
fi
timeout-minutes: 4
- - name: Jupyter Lite integration
- shell: bash -l {0}
- run: |
- micromamba create -n xeus-lite-host jupyterlite-xeus -c conda-forge
- micromamba activate xeus-lite-host
- jupyter lite build --XeusAddon.prefix=${{ env.PREFIX }}
+ - name: Serve Jupyter Lite website
+ uses: ./.github/actions/Jupyter-serve
+
+ - name: Test C++17 kernel in xeus-cpp-lite-demo.ipynb
+ uses: ./.github/actions/Emscripten-Notebook-Tests
+ with:
+ notebook: "xeus-cpp-lite-demo.ipynb"
+ kernel: "C++17"
+ timeout-minutes: 5
+
+ - name: Test C++20 kernel in xeus-cpp-lite-demo.ipynb
+ uses: ./.github/actions/Emscripten-Notebook-Tests
+ with:
+ notebook: "xeus-cpp-lite-demo.ipynb"
+ kernel: "C++20"
+ timeout-minutes: 5
+
+ - name: Test C++23 kernel in xeus-cpp-lite-demo.ipynb
+ uses: ./.github/actions/Emscripten-Notebook-Tests
+ with:
+ notebook: "xeus-cpp-lite-demo.ipynb"
+ kernel: "C++23"
+ timeout-minutes: 5
+
+ - name: Test C++17 kernel in tinyraytracer.ipynb
+ uses: ./.github/actions/Emscripten-Notebook-Tests
+ with:
+ notebook: "tinyraytracer.ipynb"
+ kernel: "C++17"
+ timeout-minutes: 5
+
+ - name: Test C++20 kernel in tinyraytracer.ipynb
+ uses: ./.github/actions/Emscripten-Notebook-Tests
+ with:
+ notebook: "tinyraytracer.ipynb"
+ kernel: "C++20"
+ timeout-minutes: 5
+
+ - name: Test C++23 kernel in tinyraytracer.ipynb
+ uses: ./.github/actions/Emscripten-Notebook-Tests
+ with:
+ notebook: "tinyraytracer.ipynb"
+ kernel: "C++23"
+ timeout-minutes: 5
- name: Setup tmate session
if: ${{ failure() && runner.debug }}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6b3fb9d7..7035131b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -164,4 +164,43 @@ jupyter lite serve --XeusAddon.prefix=$PREFIX \
--contents notebooks/tinyraytracer.ipynb \
--contents notebooks/images/marie.png \
--contents notebooks/audio/audio.wav
+
+### xeus-cpp Jupyter Lite tests
+
+It is possible to run test the xeus-cpp in Jupyter Lite deployment, using a Selenium based script and the nbdiff Jupyter tool.
+In order to install these dependencies execute the following (you will need the Jupyter Lite website running in the background when
+executing the python script)
+
+```bash
+python -m pip install nbdime==3.2.0 selenium
+```
+
+Then to run the by executing (--run-browser-gui option exists if you want Chrome or Firefox to show a gui browser as they run)
+
+```bash
+python -u scripts/automated-notebook-run-script.py --driver browser --notebook notebook --kernel kernel --stdin Test_Input --timeout timeout
+```
+
+For example
+
+```bash
+python -u scripts/automated-notebook-run-script.py --driver chrome --notebook xeus-cpp-lite-demo.ipynb --kernel C++20 --stdin Smudge --timeout 200
+```
+
+will run the xeus-cpp-lite-demo.ipynb notebook in chrome, with the C++20 kernel, enter Test_Name in the standard input box in this notebook,
+and stop the script, if the notebook has finished running after 200 seconds. This works for Safari, Chrome and Firefox. It should be noted
+that in the case of Safari, safari driver must be enabled, and downloads must be enabled from 127.0.0.1 . Once the script has finished
+executing you can compare the notebook download from the one in the repo, to check the deployment works as expected, by executing
+(replace notebook_run with the notebook chosen for the Python script)
+
+```bash
+nbdiff notebook_run $HOME/Downlaods/notebook_run --ignore-id --ignore-metadata
+```
+
+For example after running the above test command, to test no changes have occurred, execute
+
+```bash
+nbdiff xeus-cpp-lite-demo.ipynb $HOME/Downlaods/xeus-cpp-lite-demo.ipynb --ignore-id --ignore-metadata
+```
+
```
diff --git a/README.md b/README.md
index 4847d051..8fa2ca8a 100644
--- a/README.md
+++ b/README.md
@@ -181,6 +181,44 @@ jupyter lite serve --XeusAddon.prefix=$PREFIX \
--contents notebooks/audio/audio.wav
```
+### xeus-cpp Jupyter Lite tests
+
+It is possible to run test the xeus-cpp in Jupyter Lite deployment, using a Selenium based script and the nbdiff Jupyter tool.
+In order to install these dependencies execute the following (you will need the Jupyter Lite website running in the background when
+executing the python script)
+
+```bash
+python -m pip install nbdime==3.2.0 selenium
+```
+
+Then to run the by executing (--run-browser-gui option exists if you want Chrome or Firefox to show a gui browser as they run)
+
+```bash
+python -u scripts/automated-notebook-run-script.py --driver browser --notebook notebook --kernel kernel --stdin Test_Input --timeout timeout
+```
+
+For example
+
+```bash
+python -u scripts/automated-notebook-run-script.py --driver chrome --notebook xeus-cpp-lite-demo.ipynb --kernel C++20 --stdin Smudge --timeout 200
+```
+
+will run the xeus-cpp-lite-demo.ipynb notebook in chrome, with the C++20 kernel, enter Test_Name in the standard input box in this notebook,
+and stop the script, if the notebook has finished running after 200 seconds. This works for Safari, Chrome and Firefox. It should be noted
+that in the case of Safari, safari driver must be enabled, and downloads must be enabled from 127.0.0.1 . Once the script has finished
+executing you can compare the notebook download from the one in the repo, to check the deployment works as expected, by executing
+(replace notebook_run with the notebook chosen for the Python script)
+
+```bash
+nbdiff notebook_run $HOME/Downlaods/notebook_run --ignore-id --ignore-metadata
+```
+
+For example after running the above test command, to test no changes have occurred, execute
+
+```bash
+nbdiff xeus-cpp-lite-demo.ipynb $HOME/Downlaods/xeus-cpp-lite-demo.ipynb --ignore-id --ignore-metadata
+```
+
## Trying it online
To try out xeus-cpp interactively in your web browser, just click on the binder link:
diff --git a/docs/source/InstallationAndUsage.rst b/docs/source/InstallationAndUsage.rst
index 13dd7d94..ead33d70 100644
--- a/docs/source/InstallationAndUsage.rst
+++ b/docs/source/InstallationAndUsage.rst
@@ -162,6 +162,46 @@ To build and test Jupyter Lite with this kernel locally you can execute the foll
--contents notebooks/images/marie.png \
--contents notebooks/audio/audio.wav
+xeus-cpp Jupyter Lite tests
+========================
+
+It is possible to run test the xeus-cpp in Jupyter Lite deployment, using a Selenium based script and the nbdiff Jupyter tool.
+In order to install these dependencies execute the following (you will need the Jupyter Lite website running in the background when
+executing the python script)
+
+.. code-block:: bash
+
+ python -m pip install nbdime==3.2.0 selenium
+
+Then to run the by executing (--run-browser-gui option exists if you want Chrome or Firefox to show a gui browser as they run)
+
+.. code-block:: bash
+
+ python -u scripts/automated-notebook-run-script.py --driver browser --notebook notebook --kernel kernel --stdin Test_Input --timeout timeout
+
+For example
+
+.. code-block:: bash
+
+ python -u scripts/automated-notebook-run-script.py --driver chrome --notebook xeus-cpp-lite-demo.ipynb --kernel C++20 --stdin Smudge --timeout 200
+
+will run the xeus-cpp-lite-demo.ipynb notebook in chrome, with the C++20 kernel, enter Test_Name in the standard input box in this notebook,
+and stop the script, if the notebook has finished running after 200 seconds. This works for Safari, Chrome and Firefox. It should be noted
+that in the case of Safari, safari driver must be enabled, and downloads must be enabled from 127.0.0.1 . Once the script has finished
+executing you can compare the notebook download from the one in the repo, to check the deployment works as expected, by executing
+(replace notebook_run with the notebook chosen for the Python script)
+
+.. code-block:: bash
+
+ nbdiff notebook_run $HOME/Downlaods/notebook_run --ignore-id --ignore-metadata
+
+
+For example after running the above test command, to test no changes have occurred, execute
+
+.. code-block:: bash
+
+ nbdiff xeus-cpp-lite-demo.ipynb $HOME/Downlaods/xeus-cpp-lite-demo.ipynb --ignore-id --ignore-metadata
+
Installing from conda-forge
===========================
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 462202b1..e952f347 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -38,7 +38,8 @@
cd {XEUS_CPP_ROOT};
micromamba create -n xeus-lite-host jupyter_server jupyterlite-xeus -c conda-forge -y;
micromamba activate xeus-lite-host;
-jupyter lite build --XeusAddon.prefix=$PREFIX \\
+jupyter lite build --settings-overrides=overrides.json \\
+ --XeusAddon.prefix=$PREFIX \\
--XeusAddon.mounts="$PREFIX/share/xeus-cpp/tagfiles:/share/xeus-cpp/tagfiles" \
--XeusAddon.mounts="$PREFIX/etc/xeus-cpp/tags.d:/etc/xeus-cpp/tags.d" \
--contents notebooks/xeus-cpp-lite-demo.ipynb \\
diff --git a/notebooks/tinyraytracer.ipynb b/notebooks/tinyraytracer.ipynb
index 543eff08..3a3bd96f 100644
--- a/notebooks/tinyraytracer.ipynb
+++ b/notebooks/tinyraytracer.ipynb
@@ -10,6 +10,8 @@
"file_extension": ".cpp",
"mimetype": "text/x-c++src",
"name": "C++",
+ "nbconvert_exporter": "",
+ "pygments_lexer": "",
"version": "23"
}
},
diff --git a/notebooks/xeus-cpp-lite-demo.ipynb b/notebooks/xeus-cpp-lite-demo.ipynb
index b32f7837..defc07cc 100644
--- a/notebooks/xeus-cpp-lite-demo.ipynb
+++ b/notebooks/xeus-cpp-lite-demo.ipynb
@@ -10,6 +10,8 @@
"file_extension": ".cpp",
"mimetype": "text/x-c++src",
"name": "C++",
+ "nbconvert_exporter": "",
+ "pygments_lexer": "",
"version": "23"
}
},
@@ -287,7 +289,7 @@
{
"output_type": "display_data",
"data": {
- "text/html": "\n ",
+ "text/html": "\n ",
"text/plain": "https://en.cppreference.com/w/cpp/container/vector"
},
"metadata": {}
@@ -585,8 +587,14 @@
"metadata": {
"trusted": true
},
- "outputs": [],
- "execution_count": null
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdin",
+ "text": " Smudge\n"
+ }
+ ],
+ "execution_count": 29
},
{
"id": "8ec65830-4cb5-4d01-a860-f6c46ac4f60f",
@@ -595,8 +603,24 @@
"metadata": {
"trusted": true
},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": "Your name is Smudge"
+ }
+ ],
+ "execution_count": 30
+ },
+ {
+ "id": "e4ad61d7-092c-49f6-98a7-6449fd74d731",
+ "cell_type": "code",
+ "source": "",
+ "metadata": {
+ "trusted": true
+ },
"outputs": [],
"execution_count": null
}
]
-}
+}
\ No newline at end of file
diff --git a/overrides.json b/overrides.json
new file mode 100644
index 00000000..6c67db51
--- /dev/null
+++ b/overrides.json
@@ -0,0 +1,14 @@
+{
+ "@jupyterlab/notebook-extension:panel": {
+ "toolbar": [
+ {
+ "name": "download",
+ "label": "Download",
+ "args": {},
+ "command": "docmanager:download",
+ "icon": "ui-components:download",
+ "rank": 50
+ }
+ ]
+ }
+}
diff --git a/scripts/automated-notebook-run-script.py b/scripts/automated-notebook-run-script.py
new file mode 100644
index 00000000..fa7c17dd
--- /dev/null
+++ b/scripts/automated-notebook-run-script.py
@@ -0,0 +1,330 @@
+import argparse
+from selenium import webdriver
+from selenium.webdriver.chrome.options import Options as ChromeOptions
+from selenium.webdriver.firefox.options import Options as FirefoxOptions
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.webdriver.common.action_chains import ActionChains
+from selenium.webdriver.common.by import By
+from selenium.webdriver.common.keys import Keys
+import time
+import sys
+
+
+def cell_is_waiting_for_input(driver):
+ """
+ This function returns true if Jupyter is currently waiting the user to enter
+ text in a box.
+ """
+ try:
+ return driver.find_element(By.CSS_SELECTOR, ".jp-Stdin-input").is_displayed()
+ except Exception:
+ pass
+
+ return False
+
+
+def wait_for_idle_status(driver, current_cell, start_time, timeout):
+ """
+ This function checks whether the kernel is Idle. Used to decide when to move
+ onto executing the next cell. The 0.01 seconds sleep between checks, is to limit
+ the number of checks per second. After the kernel has gone Idle, we print the contents
+ of the cell (source and output) to the terminal.
+ """
+ while (
+ "Idle"
+ not in driver.find_elements(By.CSS_SELECTOR, "span.jp-StatusBar-TextItem")[
+ 2
+ ].text
+ ):
+ elapsed = time.time() - start_time
+ # This timeout is provided in case the notebppks stalls during
+ # its execution.
+ if elapsed > timeout:
+ print(
+ f"Timeout reached ({elapsed:.1f} seconds). Stopping Notebook execution."
+ )
+ sys.exit(1)
+ time.sleep(0.01)
+
+ print(current_cell.text)
+
+def rewind_to_first_cell(cell):
+ while True:
+ prev = cell.find_elements(
+ By.XPATH,
+ "preceding-sibling::div[contains(@class,'jp-Notebook-cell')][1]"
+ )
+ if not prev:
+ return cell
+ cell = prev[0]
+
+
+
+def run_notebook(driver, notebook_area, args):
+ """This functions runs all the cells of the notebook"""
+ print("Running Cells")
+ start_time = time.time()
+ current_cell = driver.find_element(
+ By.CSS_SELECTOR, ".jp-Notebook-cell.jp-mod-selected"
+ )
+
+ current_cell = rewind_to_first_cell(current_cell)
+
+ while True:
+ editor_divs = current_cell.find_elements(
+ By.CSS_SELECTOR, ".jp-InputArea-editor div"
+ )
+
+ cell_content = "".join(
+ div.get_attribute("textContent") for div in editor_divs
+ ).strip()
+
+ if not cell_content:
+ # An empty cell is used to determine the end of the notebook
+ # that is being executed.
+ print("Empty cell reached")
+ break
+
+ if cell_is_waiting_for_input(driver):
+ print("Cell requesting input")
+ current_cell = driver.find_element(
+ By.CSS_SELECTOR, ".jp-Notebook-cell.jp-mod-selected"
+ )
+ input_box = WebDriverWait(driver, 5).until(
+ EC.visibility_of_element_located((By.CSS_SELECTOR, ".jp-Stdin-input"))
+ )
+ input_box.click()
+ input_box.send_keys(f"{args.stdin}")
+ time.sleep(0.25)
+ input_box.send_keys(Keys.CONTROL, Keys.ENTER)
+ next_cell = current_cell.find_element(
+ By.XPATH,
+ "following-sibling::div[contains(@class,'jp-Notebook-cell')][1]",
+ )
+ driver.execute_script(
+ "arguments[0].scrollIntoView({block:'center'});", next_cell
+ )
+ next_cell.click()
+ if args.driver == "safari":
+ driver.execute_script(
+ """
+ const evt = new KeyboardEvent('keydown', {
+ key: 'Enter',
+ code: 'Enter',
+ keyCode: 13,
+ which: 13,
+ shiftKey: true,
+ bubbles: true
+ });
+ document.activeElement.dispatchEvent(evt);
+ """
+ )
+ wait_for_idle_status(driver, current_cell, start_time, args.timeout)
+ current_cell = next_cell
+ time.sleep(0.25)
+
+ notebook_area.send_keys(Keys.SHIFT, Keys.ENTER)
+ # This sleep is there is allow time for the box for standard input
+ # to appear, if it needs to after executing the cell
+ time.sleep(0.25)
+ if not cell_is_waiting_for_input(driver):
+ wait_for_idle_status(driver, current_cell, start_time, args.timeout)
+ next_cell = current_cell.find_element(
+ By.XPATH,
+ "following-sibling::div[contains(@class,'jp-Notebook-cell')][1]",
+ )
+ current_cell = next_cell
+
+def download_notebook(driver):
+ """This function is used to download the notebook currently open."""
+ print("Downloading notebook by clicking download button")
+ search_script = """
+ function deepQuerySelector(root, selector) {
+ const walker = document.createTreeWalker(
+ root,
+ NodeFilter.SHOW_ELEMENT,
+ {
+ acceptNode: node => NodeFilter.FILTER_ACCEPT
+ },
+ false
+ );
+
+ while (walker.nextNode()) {
+ let node = walker.currentNode;
+
+ // Check if this node matches
+ if (node.matches && node.matches(selector)) {
+ return node;
+ }
+
+ // If this element has a shadow root, search inside it
+ if (node.shadowRoot) {
+ const found = deepQuerySelector(node.shadowRoot, selector);
+ if (found) return found;
+ }
+ }
+ return null;
+ }
+
+ return deepQuerySelector(document, "jp-button[data-command='docmanager:download']");
+ """
+
+ download_button = driver.execute_script(search_script)
+
+ time.sleep(1)
+ driver.execute_script(
+ """
+ const el = arguments[0];
+
+ // Force element to be visible and focused
+ el.scrollIntoView({block: 'center', inline: 'center'});
+
+ // Dispatch real mouse events since Safari WebDriver ignores .click() on Web Components
+ ['pointerdown', 'mousedown', 'mouseup', 'click'].forEach(type => {
+ el.dispatchEvent(new MouseEvent(type, {
+ bubbles: true,
+ cancelable: true,
+ composed: true, // IMPORTANT for shadow DOM
+ view: window
+ }));
+ });
+ """,
+ download_button,
+ )
+
+ time.sleep(1)
+
+
+def choose_kernel(driver, args):
+ """This function sets the kernel based on the user input."""
+ kernel_button = driver.find_element(
+ By.CSS_SELECTOR, "jp-button.jp-Toolbar-kernelName.jp-ToolbarButtonComponent"
+ )
+ driver.execute_script("arguments[0].click();", kernel_button)
+ driver.switch_to.active_element.send_keys(Keys.TAB)
+ time.sleep(0.01)
+ ActionChains(driver).send_keys(f"{args.kernel}").perform()
+ time.sleep(0.01)
+ ActionChains(driver).send_keys(Keys.TAB).perform()
+ time.sleep(0.01)
+ ActionChains(driver).send_keys(Keys.ENTER).perform()
+ time.sleep(0.01)
+
+
+def save_notebook(notebook_area):
+ """This function saves the notebook."""
+ print("Saving the notebook")
+ notebook_area.send_keys(Keys.COMMAND, "s")
+ time.sleep(0.5)
+
+
+def clear_notebook_output(driver, notebook_area):
+ """
+ This function clears the output of all cells in the notebook, before it is
+ executed by run_notebook.
+ """
+ ActionChains(driver).context_click(notebook_area).pause(0.01).send_keys(
+ Keys.DOWN * 9
+ ).pause(0.01).send_keys(Keys.ENTER).pause(0.01).perform()
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Run Selenium with a chosen driver")
+ parser.add_argument(
+ "--driver",
+ type=str,
+ default="chrome",
+ choices=["chrome", "firefox", "safari"],
+ help="Choose which WebDriver to use",
+ )
+ parser.add_argument(
+ "--notebook",
+ type=str,
+ required=True,
+ help="Notebook to execute",
+ )
+ parser.add_argument(
+ "--kernel",
+ type=str,
+ required=True,
+ help="Kernel to run notebook in",
+ )
+ parser.add_argument(
+ "--stdin",
+ type=str,
+ help="Text to pass to standard input",
+ )
+ parser.add_argument(
+ "--timeout",
+ type=int,
+ default=120,
+ help="Maximum time (in seconds) allowed for notebook execution before timeout.",
+ )
+ parser.add_argument(
+ "--run-browser-gui",
+ action="store_true",
+ help="Run browser with a visible GUI (disables headless mode).",
+ )
+
+ args = parser.parse_args()
+ URL = f"http://127.0.0.1:8000/lab/index.html?path={args.notebook}"
+
+ # This will start the right driver depending on what
+ # driver option is chosen
+ if args.driver == "chrome":
+ options = ChromeOptions()
+ if not args.run_browser_gui:
+ options.add_argument("--headless")
+ options.add_argument("--no-sandbox")
+ driver = webdriver.Chrome(options=options)
+
+ elif args.driver == "firefox":
+ options = FirefoxOptions()
+ if not args.run_browser_gui:
+ options.add_argument("--headless")
+ driver = webdriver.Firefox(options=options)
+
+ elif args.driver == "safari":
+ driver = webdriver.Safari()
+
+ wait = WebDriverWait(driver, 30)
+
+ # Open Jupyter Lite with the notebook requested
+ driver.get(URL)
+
+ # Waiting for Jupyter Lite URL to finish loading
+ notebook_area = wait.until(
+ EC.presence_of_element_located((By.CSS_SELECTOR, ".jp-Notebook"))
+ )
+
+ # Without this sleep, the ci will fail for Safari will fail
+ # Unable to currently determine root cause. This is not needed
+ # locally.
+ time.sleep(1)
+
+ # This clears the output of the reference notebook
+ # before executing it, so that when we download it,
+ # we know the output is purely from the ci running
+ # the notebook.
+ clear_notebook_output(driver, notebook_area)
+
+ # Select Kernel based on input
+ choose_kernel(driver, args)
+
+ # This will run all the cells of the chosen notebook
+ run_notebook(driver, notebook_area, args)
+
+ # This section saves the notebook,
+ save_notebook(notebook_area)
+
+ # This section downloads the notebook, so it can be compared
+ # to a reference notebook
+ download_notebook(driver)
+
+ # Close browser
+ driver.quit()
+
+
+if __name__ == "__main__":
+ main()