From 617d487eccb9564413f567112327ba9232c5e189 Mon Sep 17 00:00:00 2001 From: mcbarton <150042563+mcbarton@users.noreply.github.com> Date: Wed, 19 Nov 2025 19:39:59 +0000 Subject: [PATCH] Run xeus-cpp-lite-demo.ipynb in ci and test output for Safari, Chrome and Firefox --- .../Emscripten-Notebook-Tests/action.yml | 55 ++++ .github/actions/Jupyter-serve/action.yml | 38 +++ .github/workflows/deploy-github-page.yml | 38 ++- .github/workflows/main.yml | 29 +- CONTRIBUTING.md | 39 +++ README.md | 38 +++ docs/source/InstallationAndUsage.rst | 40 +++ docs/source/conf.py | 3 +- notebooks/xeus-cpp-lite-demo.ipynb | 28 +- overrides.json | 14 + scripts/automated-notebook-run-script.py | 308 ++++++++++++++++++ scripts/enable-downloads-safari-github-ci.py | 35 ++ 12 files changed, 640 insertions(+), 25 deletions(-) create mode 100644 .github/actions/Emscripten-Notebook-Tests/action.yml create mode 100644 .github/actions/Jupyter-serve/action.yml create mode 100644 overrides.json create mode 100644 scripts/automated-notebook-run-script.py create mode 100644 scripts/enable-downloads-safari-github-ci.py diff --git a/.github/actions/Emscripten-Notebook-Tests/action.yml b/.github/actions/Emscripten-Notebook-Tests/action.yml new file mode 100644 index 00000000..07d028b3 --- /dev/null +++ b/.github/actions/Emscripten-Notebook-Tests/action.yml @@ -0,0 +1,55 @@ +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 }} + export SAFARI_TESTS_RETURN_VALUE=0 + touch safari_diff.txt + if [[ "${{ matrix.os }}" == "macos"* ]]; then + echo "Running xeus-cpp in Jupter Lite in Safari" + python -u scripts/automated-notebook-run-script.py --driver safari --notebook ${{ inputs.notebook }} --kernel ${{ inputs.kernel }} $INPUT_TEXT + nbdiff notebooks/${{ inputs.notebook }} $HOME/Downloads/${{ inputs.notebook }} --ignore-id --ignore-metadata >> safari_diff.txt + export SAFARI_TESTS_RETURN_VALUE=$( [ -s safari_diff.txt ] && echo 1 || echo 0 ) + rm $HOME/Downloads/${{ inputs.notebook }} + fi + if [[ $SAFARI_TESTS_RETURN_VALUE -ne 0 || $FIREFOX_TESTS_RETURN_VALUE -ne 0 || $CHROME_TESTS_RETURN_VALUE -ne 0 ]]; then + if [[ "${{ matrix.os }}" == "macos"* ]]; then + echo "Diff Safari (blank means no diff)" + cat safari_diff.txt + fi + 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..8883165b --- /dev/null +++ b/.github/actions/Jupyter-serve/action.yml @@ -0,0 +1,38 @@ +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 jupyterlite-core=0.6 jupyterlite-xeus -c conda-forge -y + micromamba activate xeus-lite-host + if [[ "${{ matrix.os }}" == "macos"* ]]; then + brew install coreutils + export PATH="$HOMEBREW_PREFIX/opt/coreutils/libexec/gnubin:$PATH" + fi + timeout 900 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. This only happens for xeus-cpp-lite-demo.ipynb. + python -m pip install nbdime==3.2.0 + if [[ "${{ matrix.os }}" == "macos"* ]]; then + python -m pip install PyAutoGUI + python scripts/enable-downloads-safari-github-ci.py + fi + python -m pip install 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 cef776cb..510d318d 100644 --- a/.github/workflows/deploy-github-page.yml +++ b/.github/workflows/deploy-github-page.yml @@ -139,21 +139,29 @@ jobs: fi timeout-minutes: 4 - - name: Jupyter Lite integration - shell: bash -l {0} - run: | - micromamba create -n xeus-lite-host jupyterlite-core=0.6 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: Upload artifact uses: actions/upload-pages-artifact@v3 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2c2c8071..5fd3bd50 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -329,12 +329,29 @@ jobs: fi timeout-minutes: 4 - - name: Jupyter Lite integration - shell: bash -l {0} - run: | - micromamba create -n xeus-lite-host jupyterlite-core=0.6 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: Setup tmate session if: ${{ failure() && runner.debug }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2a2d9a23..f849303d 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 341d0203..e2628c90 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 aa242dc2..541145f1 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 8854709f..37f9738b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -37,7 +37,8 @@ micromamba create -n xeus-lite-host jupyterlite-core=0.6 jupyter_server jupyterlite-xeus -c conda-forge -y; micromamba activate xeus-lite-host; python -m pip install jupyterlite-xeus jupyter_server; -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/xeus-cpp-lite-demo.ipynb b/notebooks/xeus-cpp-lite-demo.ipynb index b32f7837..794b5403 100644 --- a/notebooks/xeus-cpp-lite-demo.ipynb +++ b/notebooks/xeus-cpp-lite-demo.ipynb @@ -585,8 +585,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 +601,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..147da365 --- /dev/null +++ b/scripts/automated-notebook-run-script.py @@ -0,0 +1,308 @@ +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 run_notebook(driver, notebook_area, args): + """This functions runs all the cells of the notebook""" + print("Running Cells") + start_time = time.time() + while True: + current_cell = driver.find_element( + By.CSS_SELECTOR, ".jp-Notebook-cell.jp-mod-selected" + ) + 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") + 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.1) + 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 + + 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.1) + if not cell_is_waiting_for_input(driver): + wait_for_idle_status(driver, current_cell, start_time, args.timeout) + + +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() diff --git a/scripts/enable-downloads-safari-github-ci.py b/scripts/enable-downloads-safari-github-ci.py new file mode 100644 index 00000000..bcbd8cf8 --- /dev/null +++ b/scripts/enable-downloads-safari-github-ci.py @@ -0,0 +1,35 @@ +import pyautogui +import time + + +def main(): + # Click Safari icon + pyautogui.moveTo(150, 720, duration=1) + pyautogui.click() + time.sleep(1) + # Click Safari Menu + pyautogui.moveTo(60, 10, duration=1) + pyautogui.click() + time.sleep(1) + # Click Settings + pyautogui.moveTo(75, 102, duration=1) + pyautogui.click() + time.sleep(2.4) + # Click websites page of settings + pyautogui.moveTo(700, 240, duration=1) + pyautogui.click() + time.sleep(1.2) + # Click Downloads section of webpages page + pyautogui.moveTo(350, 630, duration=1) + pyautogui.click() + time.sleep(1.2) + # Change ask to allow + pyautogui.moveTo(950, 690, duration=1) + pyautogui.click() + pyautogui.moveTo(950, 670, duration=1) + pyautogui.click() + time.sleep(1.2) + + +if __name__ == "__main__": + main()