diff --git a/.github/workflows/screenshot-comparison.yml b/.github/workflows/screenshot-comparison.yml index 2517515..043071e 100644 --- a/.github/workflows/screenshot-comparison.yml +++ b/.github/workflows/screenshot-comparison.yml @@ -12,6 +12,9 @@ jobs: check-screenshot: name: Check Screenshot runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write # needed to post the comment on screenshot mismatch steps: - name: Checkout uses: actions/checkout@v4 @@ -41,10 +44,41 @@ jobs: pip install --upgrade Pillow - uses: nanasess/setup-chromedriver@master - - run: | + - name: Run screenshot test + run: | export DISPLAY=:99 chromedriver --url-base=/wd/hub & sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & # optional python3 $GITHUB_WORKSPACE/test/test.py python3 $GITHUB_WORKSPACE/test/test_figures.py + - name: Upload screenshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: screenshot-comparison + path: | + widget-01.png + comparison.png + test/widget-sample.png + if-no-files-found: ignore + + - name: Comment on PR with refresh instructions + if: failure() && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const url = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: [ + `📷 **Screenshot comparison failed.**`, + ``, + `Download the **screenshot-comparison** artifact from [this run](${url}) and open \`comparison.png\` — it shows **reference | new | diff** side by side.`, + ``, + `If the change is acceptable, replace \`test/widget-sample.png\` with the new \`widget-01.png\` from the artifact and push the update.`, + ].join('\n'), + }); + diff --git a/js/widget.js b/js/widget.js index 0c666c1..b9e7e85 100644 --- a/js/widget.js +++ b/js/widget.js @@ -22,6 +22,21 @@ function generateRandomString(length) { return result.substring(0, length); } +// Stop keydown events from bubbling out of the editor's DOM. Without this, +// when the widget is hosted inside a JupyterLab notebook output area, +// global keybindings like Shift+L are caught by JupyterLab's command-mode +// keybindings on the surrounding cell — Lumino calls preventDefault(), which +// in turn suppresses the subsequent `input` event that CodeMirror 6 relies on +// to insert characters, so the keystroke silently disappears. +// stopPropagation() only blocks ancestors; CodeMirror's own handlers on the +// same element still run, so editor behavior is unchanged. +const stopKeydownPropagation = EditorView.domEventHandlers({ + keydown: (event) => { + event.stopPropagation(); + return false; + } +}); + export default{ initialize({ model }) { @@ -133,7 +148,8 @@ export default{ var myDocstringCodeMirror; const mySignatureCodeMirror = editorFromTextArea(document.getElementById(theTextareaId + '-signature'), [EditorState.readOnly.of(true),lineNumbers(),indentUnit.of(" ") -, +, + stopKeydownPropagation, history(), drawSelection(), dropCursor(), @@ -165,7 +181,7 @@ export default{ if (model.get('docstring') !== '') { myDocstringCodeMirror = editorFromTextArea(document.getElementById(theTextareaId + '-docstring'), [lineNumbers(), EditorState.readOnly.of(true), EditorView.editorAttributes.of({class:"widget-code-input-docstring"}), gutter({class:"forced-indent"}), guttercomp.of(lineNumbers()), - + stopKeydownPropagation, history(), drawSelection(), dropCursor(), @@ -204,7 +220,8 @@ export default{ - var myBodyCodeMirror = editorFromTextArea(document.getElementById(theTextareaId + '-body'), [guttercomp.of(lineNumbers()),gutter({class:"forced-indent"}), bodyUpdateListenerCompartment.of([]), + var myBodyCodeMirror = editorFromTextArea(document.getElementById(theTextareaId + '-body'), [guttercomp.of(lineNumbers()),gutter({class:"forced-indent"}), bodyUpdateListenerCompartment.of([]), + stopKeydownPropagation, history(), drawSelection(), dropCursor(), @@ -363,6 +380,7 @@ function signatureValueChanged() { EditorState.readOnly.of(true), EditorView.editorAttributes.of({ class: 'widget-code-input-docstring' }), gutter({ class: 'forced-indent' }),guttercomp.of(lineNumbers()), + stopKeydownPropagation, syntaxHighlighting(defaultHighlightStyle, { fallback: true }), python(), indentUnit.of(' '), diff --git a/test/test.py b/test/test.py index e4e2496..253c967 100644 --- a/test/test.py +++ b/test/test.py @@ -1,16 +1,8 @@ # Generated by Selenium IDE -import pytest import time -import json import os from selenium import webdriver from selenium.webdriver.chrome.options import Options -from selenium.webdriver.common.by import By -from selenium.webdriver.common.action_chains import ActionChains -from selenium.webdriver.support import expected_conditions -from selenium.webdriver.support.wait import WebDriverWait -from selenium.webdriver.common.keys import Keys -from selenium.webdriver.common.desired_capabilities import DesiredCapabilities class test_widget(): def setup_method(self, method): @@ -30,11 +22,6 @@ def download_widget_image(self): time.sleep(3) self.driver.save_screenshot("widget-01.png") - self.driver.find_element(By.CLASS_NAME, 'widget-code-input-signature').click() - self.driver.find_element(By.TAG_NAME, 'body').send_keys(Keys.END) - time.sleep(3) - self.driver.save_screenshot("widget-02.png") - test = test_widget() test.setup_method('Chrome') test.download_widget_image() diff --git a/test/test_figures.py b/test/test_figures.py index b1e208d..2811ec7 100644 --- a/test/test_figures.py +++ b/test/test_figures.py @@ -1,12 +1,22 @@ from PIL import Image, ImageChops, ImageStat -image1 = Image.open('widget-01.png') -image2 = Image.open('test/widget-sample.png') +new = Image.open('widget-01.png') +reference = Image.open('test/widget-sample.png') -diff = ImageChops.difference(image1, image2) +diff = ImageChops.difference(new, reference) stat = ImageStat.Stat(diff) if sum(stat.mean) == 0: - print('images are the same') + print('images are the same') else: - raise Exception("The result is NOT the same as expected. Please check matplotlib version.") + w = max(new.width, reference.width, diff.width) + h = max(new.height, reference.height, diff.height) + composite = Image.new('RGB', (w * 3, h), 'white') + composite.paste(reference, (0, 0)) # left: reference + composite.paste(new, (w, 0)) # middle: new from this run + composite.paste(diff, (w * 2, 0)) # right: pixel-wise diff + composite.save('comparison.png') + print(f"Screenshots differ. Mean per-channel diff: {stat.mean}") + print("Inspect the 'screenshot-comparison' workflow artifact.") + print("If the change is acceptable, replace test/widget-sample.png with widget-01.png and commit.") + raise SystemExit(1) diff --git a/test/widget-sample.png b/test/widget-sample.png index c3a1f90..d4d1ca9 100644 Binary files a/test/widget-sample.png and b/test/widget-sample.png differ