From cf46e139cd90d14368b1b86e3f472eb9bec46442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Collonval?= Date: Mon, 13 Nov 2023 15:25:05 +0100 Subject: [PATCH] Update template --- shout-button-message/.copier-answers.yml | 20 +++ shout-button-message/.gitignore | 125 ++++++++++++++++++ shout-button-message/.prettierignore | 6 + shout-button-message/.yarnrc.yml | 2 - shout-button-message/README.md | 106 ++++++++++++++- shout-button-message/install.json | 4 +- .../__init__.py | 4 +- shout-button-message/package.json | 36 +++-- shout-button-message/pyproject.toml | 21 +-- shout-button-message/setup.py | 2 +- shout-button-message/src/index.ts | 124 +++++++++++------ shout-button-message/ui-tests/README.md | 2 +- shout-button-message/ui-tests/package.json | 26 ++-- shout-button-message/ui-tests/yarn.lock | 0 14 files changed, 384 insertions(+), 94 deletions(-) create mode 100644 shout-button-message/.copier-answers.yml create mode 100644 shout-button-message/.gitignore create mode 100644 shout-button-message/.prettierignore rename shout-button-message/{shout_button_message => jupyterlab_examples_shout_button}/__init__.py (76%) create mode 100644 shout-button-message/ui-tests/yarn.lock diff --git a/shout-button-message/.copier-answers.yml b/shout-button-message/.copier-answers.yml new file mode 100644 index 00000000..2a16ba22 --- /dev/null +++ b/shout-button-message/.copier-answers.yml @@ -0,0 +1,20 @@ +# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY +_commit: v4.2.4 +_src_path: https://github.com/jupyterlab/extension-template +author_email: '' +author_name: Project Jupyter Contributors +data_format: string +file_extension: '' +has_binder: false +has_settings: false +kind: frontend +labextension_name: '@jupyterlab-examples/shout-button' +mimetype: '' +mimetype_name: '' +project_short_description: An extension that adds a button and message to the right + toolbar, with optional status bar widget in JupyterLab. +python_name: jupyterlab_examples_shout_button +repository: https://github.com/jupyterlab/extension-examples +test: true +viewer_name: '' + diff --git a/shout-button-message/.gitignore b/shout-button-message/.gitignore new file mode 100644 index 00000000..d9a8d1d8 --- /dev/null +++ b/shout-button-message/.gitignore @@ -0,0 +1,125 @@ +*.bundle.* +lib/ +node_modules/ +*.log +.eslintcache +.stylelintcache +*.egg-info/ +.ipynb_checkpoints +*.tsbuildinfo +jupyterlab_examples_shout_button/labextension +# Version file is handled by hatchling +jupyterlab_examples_shout_button/_version.py + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Created by https://www.gitignore.io/api/python +# Edit at https://www.gitignore.io/?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage/ +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# End of https://www.gitignore.io/api/python + +# OSX files +.DS_Store + +# Yarn cache +.yarn/ diff --git a/shout-button-message/.prettierignore b/shout-button-message/.prettierignore new file mode 100644 index 00000000..f45e8f0a --- /dev/null +++ b/shout-button-message/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_shout_button diff --git a/shout-button-message/.yarnrc.yml b/shout-button-message/.yarnrc.yml index fe1125f5..3186f3f0 100644 --- a/shout-button-message/.yarnrc.yml +++ b/shout-button-message/.yarnrc.yml @@ -1,3 +1 @@ -enableImmutableInstalls: false - nodeLinker: node-modules diff --git a/shout-button-message/README.md b/shout-button-message/README.md index 5a752ce5..5d04fd89 100644 --- a/shout-button-message/README.md +++ b/shout-button-message/README.md @@ -1,7 +1,101 @@ -# Dual Compatibility Shout Button (shout_button_message) +# Shout button (cross compatible extension) -This example shows dual compatibility: Make an extension that is compatible -with both JupyterLab and Jupyter Notebook by using optional features. Adds -a shout button to the right sidebar, and if running in JupyterLab, also adds -a status bar widget. This example is part of the [Extension Dual Compatibility Guide](https://jupyterlab.readthedocs.io/en/latest/extension_dual_compatibility.html). -Read more about this example on that page. +This example defines an extension that adds a button in the right sidebar that +if clicked will display an alert to the user and in JupyterLab will update +a widget in the status bar. + +![preview](./preview.jpg) + +## Jupyter Notebook / JupyterLab compatibility + +As Jupyter Notebook 7+ is built with components from JupyterLab, and since +both use the same building blocks, that means your extension can work +on both (or any other frontend built with JupyterLab components) with +little or no modification depending on its design. + +This example has a part specific to JupyterLab. This translate by having +optional dependency for your extension plugin. + +```ts +// src/index.ts#L120-L120 + +optional: [IStatusBar], +``` + +If your dependency is optional, the object pass to the `activate` method +will be `null` if no other plugin provides it. + +```ts +// src/index.ts#L124-L124 + +activate: (app: JupyterFrontEnd, statusBar: IStatusBar | null) => { +``` + +## Add the button in the sidebar + +You can add a widget to the right sidebar through the application shell: + +```ts +// src/index.ts#L128-L131 + +const shoutWidget: ShoutWidget = new ShoutWidget(); +shoutWidget.id = 'JupyterShoutWidget'; // Widgets need an id + +app.shell.add(shoutWidget, 'right'); +``` + +The `ShoutWidget` is a widget that contains a button that when clicked +emit a signal `messageShouted` that any callback can listen to to react +to it and display an alert to the user. + +```ts +// src/index.ts#L99-L103 + +shout() { + this._lastShoutTime = new Date(); + this._messageShouted.emit(this._lastShoutTime); + window.alert('Shouting at ' + this._lastShoutTime); +} +``` + +## Connect the button and the status bar + +The status bar does not exist in all Jupyter applications (e.g. in +Jupyter Notebook). So a good practice is to make that dependency +optional and test for it to be non-null to carry related action: + +```ts +// src/index.ts#L135-L135 + +if (statusBar) { +``` + +In this specific case, the action is to create a widget to add to the +status bar. You can achieve that by calling the `registerStatusItem` +method from the status bar object. + +```ts +// src/index.ts#L136-L138 + +const statusBarWidget = new ShoutStatusBarSummary(); + +statusBar.registerStatusItem('shoutStatusBarSummary', { +``` + +If you want to react to a click on the button, you can `connect` to the +widget `messageShouted` signal. In which for example, you update the +text displayed in the status bar. + +```ts +// src/index.ts#L142-L144 + +// Connect to the messageShouted to be notified when a new message +// is published and react to it by updating the status bar widget. +shoutWidget.messageShouted.connect((widget: ShoutWidget, time: Date) => { +``` + +## Where to Go Next + +You can have more information about making extension compatible with +multiple applications in the +[Extension Dual Compatibility Guide](https://jupyterlab.readthedocs.io/en/latest/extension_dual_compatibility.html). diff --git a/shout-button-message/install.json b/shout-button-message/install.json index a488195e..a27f7f3f 100644 --- a/shout-button-message/install.json +++ b/shout-button-message/install.json @@ -1,5 +1,5 @@ { "packageManager": "python", - "packageName": "shout_button_message", - "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package shout_button_message" + "packageName": "jupyterlab_examples_shout_button", + "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlab_examples_shout_button" } diff --git a/shout-button-message/shout_button_message/__init__.py b/shout-button-message/jupyterlab_examples_shout_button/__init__.py similarity index 76% rename from shout-button-message/shout_button_message/__init__.py rename to shout-button-message/jupyterlab_examples_shout_button/__init__.py index 8b56727a..2e2ab422 100644 --- a/shout-button-message/shout_button_message/__init__.py +++ b/shout-button-message/jupyterlab_examples_shout_button/__init__.py @@ -5,12 +5,12 @@ # in editable mode with pip. It is highly recommended to install # the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs import warnings - warnings.warn("Importing 'shout_button_message' outside a proper installation.") + warnings.warn("Importing 'jupyterlab_examples_shout_button' outside a proper installation.") __version__ = "dev" def _jupyter_labextension_paths(): return [{ "src": "labextension", - "dest": "shout_button_message" + "dest": "@jupyterlab-examples/shout-button" }] diff --git a/shout-button-message/package.json b/shout-button-message/package.json index 920a00cb..857e685c 100644 --- a/shout-button-message/package.json +++ b/shout-button-message/package.json @@ -1,5 +1,5 @@ { - "name": "shout_button_message", + "name": "@jupyterlab-examples/shout-button", "version": "0.1.0", "description": "An extension that adds a button and message to the right toolbar, with optional status bar widget in JupyterLab.", "keywords": [ @@ -7,15 +7,12 @@ "jupyterlab", "jupyterlab-extension" ], - "homepage": "", + "homepage": "https://github.com/jupyterlab/extension-examples", "bugs": { - "url": "/issues" + "url": "https://github.com/jupyterlab/extension-examples/issues" }, "license": "BSD-3-Clause", - "author": { - "name": "My Name", - "email": "me@test.com" - }, + "author": "Project Jupyter Contributors", "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}" @@ -25,7 +22,7 @@ "style": "style/index.css", "repository": { "type": "git", - "url": ".git" + "url": "https://github.com/jupyterlab/extension-examples.git" }, "scripts": { "build": "jlpm build:lib && jlpm build:labextension:dev", @@ -37,7 +34,7 @@ "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", "clean:lintcache": "rimraf .eslintcache .stylelintcache", - "clean:labextension": "rimraf shout_button_message/labextension shout_button_message/_version.py", + "clean:labextension": "rimraf jupyterlab_examples_shout_button/labextension jupyterlab_examples_shout_button/_version.py", "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache", "eslint": "jlpm eslint:check --fix", "eslint:check": "eslint . --cache --ext .ts,.tsx", @@ -55,27 +52,33 @@ }, "dependencies": { "@jupyterlab/application": "^4.0.0", - "@jupyterlab/statusbar": "^4.0.2", + "@jupyterlab/statusbar": "^4.0.0", + "@lumino/signaling": "^2.0.0", "@lumino/widgets": "^2.0.0" }, "devDependencies": { "@jupyterlab/builder": "^4.0.0", + "@jupyterlab/testutils": "^4.0.0", + "@types/jest": "^29.2.0", "@types/json-schema": "^7.0.11", "@types/react": "^18.0.26", - "@typescript-eslint/eslint-plugin": "^5.55.0", - "@typescript-eslint/parser": "^5.55.0", + "@types/react-addons-linked-state-mixin": "^0.14.22", + "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/parser": "^6.1.0", "css-loader": "^6.7.1", "eslint": "^8.36.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", "prettier": "^3.0.0", - "rimraf": "^4.4.1", + "rimraf": "^5.0.1", "source-map-loader": "^1.0.2", "style-loader": "^3.3.1", "stylelint": "^15.10.1", "stylelint-config-recommended": "^13.0.0", "stylelint-config-standard": "^34.0.0", + "stylelint-csstree-validator": "^3.0.0", "stylelint-prettier": "^4.0.0", "typescript": "~5.0.2", "yjs": "^13.5.0" @@ -90,7 +93,7 @@ }, "jupyterlab": { "extension": true, - "outputDir": "shout_button_message/labextension" + "outputDir": "jupyterlab_examples_shout_button/labextension" }, "eslintIgnore": [ "node_modules", @@ -175,8 +178,13 @@ "stylelint-config-standard", "stylelint-prettier/recommended" ], + "plugins": [ + "stylelint-csstree-validator" + ], "rules": { + "csstree/validator": true, "property-no-vendor-prefix": null, + "selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$", "selector-no-vendor-prefix": null, "value-no-vendor-prefix": null } diff --git a/shout-button-message/pyproject.toml b/shout-button-message/pyproject.toml index 27121c29..64c357c9 100644 --- a/shout-button-message/pyproject.toml +++ b/shout-button-message/pyproject.toml @@ -1,9 +1,9 @@ [build-system] -requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version"] +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version>=0.3.2"] build-backend = "hatchling.build" [project] -name = "shout_button_message" +name = "jupyterlab_examples_shout_button" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.8" @@ -20,6 +20,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ ] @@ -32,24 +33,24 @@ source = "nodejs" fields = ["description", "authors", "urls"] [tool.hatch.build.targets.sdist] -artifacts = ["shout_button_message/labextension"] +artifacts = ["jupyterlab_examples_shout_button/labextension"] exclude = [".github", "binder"] [tool.hatch.build.targets.wheel.shared-data] -"shout_button_message/labextension" = "share/jupyter/labextensions/shout_button_message" -"install.json" = "share/jupyter/labextensions/shout_button_message/install.json" +"jupyterlab_examples_shout_button/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/shout-button" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/shout-button/install.json" [tool.hatch.build.hooks.version] -path = "shout_button_message/_version.py" +path = "jupyterlab_examples_shout_button/_version.py" [tool.hatch.build.hooks.jupyter-builder] dependencies = ["hatch-jupyter-builder>=0.5"] build-function = "hatch_jupyter_builder.npm_builder" ensured-targets = [ - "shout_button_message/labextension/static/style.js", - "shout_button_message/labextension/package.json", + "jupyterlab_examples_shout_button/labextension/static/style.js", + "jupyterlab_examples_shout_button/labextension/package.json", ] -skip-if-exists = ["shout_button_message/labextension/static/style.js"] +skip-if-exists = ["jupyterlab_examples_shout_button/labextension/static/style.js"] [tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" @@ -59,7 +60,7 @@ npm = ["jlpm"] build_cmd = "install:extension" npm = ["jlpm"] source_dir = "src" -build_dir = "shout_button_message/labextension" +build_dir = "jupyterlab_examples_shout_button/labextension" [tool.jupyter-releaser.options] version_cmd = "hatch version" diff --git a/shout-button-message/setup.py b/shout-button-message/setup.py index bea23374..aefdf20d 100644 --- a/shout-button-message/setup.py +++ b/shout-button-message/setup.py @@ -1 +1 @@ -__import__('setuptools').setup() +__import__("setuptools").setup() diff --git a/shout-button-message/src/index.ts b/shout-button-message/src/index.ts index c42681d7..19b61003 100644 --- a/shout-button-message/src/index.ts +++ b/shout-button-message/src/index.ts @@ -5,27 +5,35 @@ import { import { IStatusBar } from '@jupyterlab/statusbar'; +import { Message } from '@lumino/messaging'; + +import { ISignal, Signal } from '@lumino/signaling'; + import { Widget } from '@lumino/widgets'; /** - * This is an optional widget that will be used - * when the JupyterLab status bar is available. + * Widget to display text it JupyterLab status bar. */ class ShoutStatusBarSummary extends Widget { - statusBarSummary: HTMLElement; + private _statusBarSummary: HTMLElement; constructor() { super(); // Display the last shout time in the status bar - this.statusBarSummary = document.createElement('p'); - this.statusBarSummary.classList.add('jp-shout-summary'); - this.statusBarSummary.innerText = 'Last Shout: (None)'; - this.node.appendChild(this.statusBarSummary); + this._statusBarSummary = document.createElement('p'); + this._statusBarSummary.classList.add('jp-shout-summary'); + this._statusBarSummary.innerText = 'Last Shout: (None)'; + this.node.appendChild(this._statusBarSummary); } + /** + * Set the widget text content + * + * @param summary The text to display + */ setSummary(summary: string) { - this.statusBarSummary.innerText = summary; + this._statusBarSummary.innerText = summary; } } @@ -35,52 +43,63 @@ class ShoutStatusBarSummary extends Widget { * status bar is available. */ class ShoutWidget extends Widget { - shoutButton: HTMLElement; - lastShoutTime: Date | null; - statusBarWidget: ShoutStatusBarSummary | null; + // The last shout time for use in the status bar + private _lastShoutTime: Date | null; + // Signal triggered when a message is shouted + private _messageShouted = new Signal(this); + // Link to the shout button + private _shoutButton: HTMLElement; - constructor(statusBar: any) { + constructor() { super(); // Create and add a button to this widget's root node const shoutButton = document.createElement('div'); shoutButton.innerText = 'Press to Shout'; - // Add a listener to "shout" when the button is clicked - shoutButton.addEventListener('click', this.shout.bind(this)); shoutButton.classList.add('jp-shout-button'); this.node.appendChild(shoutButton); - this.shoutButton = shoutButton; + this._shoutButton = shoutButton; - // Store the last shout time for use in the status bar - this.lastShoutTime = null; + this._lastShoutTime = null; + } - // Check if the status bar is available, and if so, make - // a status bar widget to hold some information - // ............................................ - // Note: In a real extension, it would be better to - // avoid holding a reference to this widget and instead - // create it in the activate function, then use Lumino - // signals to connect it to the shout function - this.statusBarWidget = null; - if (statusBar) { - this.statusBarWidget = new ShoutStatusBarSummary(); - statusBar.registerStatusItem('shoutStatusBarSummary', { - item: this.statusBarWidget - }); - } + /** + * The last shout time for use in the status bar + */ + get lastShoutTime(): Date | null { + return this._lastShoutTime; + } + + /** + * Signal emitted when a message is shouted + */ + get messageShouted(): ISignal { + return this._messageShouted; + } + + /** + * Callback when the widget is added to the DOM + */ + protected onAfterAttach(msg: Message): void { + // Add a listener to "shout" when the button is clicked + this._shoutButton.addEventListener('click', this.shout.bind(this)); + } + + /** + * Callback when the widget is removed from the DOM + */ + protected onBeforeDetach(msg: Message): void { + this._shoutButton.removeEventListener('click', this.shout.bind(this)); } - // Make an alert popup that shouts upon user click + /** + * Make an alert popup that shouts upon user click + * And signal that a message is emitted. + */ shout() { - this.lastShoutTime = new Date(); - window.alert('Shouting at ' + this.lastShoutTime); - - // Update the status bar widget if available - if (this.statusBarWidget) { - this.statusBarWidget.setSummary( - 'Last Shout: ' + this.lastShoutTime.toString() - ); - } + this._lastShoutTime = new Date(); + this._messageShouted.emit(this._lastShoutTime); + window.alert('Shouting at ' + this._lastShoutTime); } } @@ -91,7 +110,7 @@ class ShoutWidget extends Widget { * JupyterLab as optional. */ const plugin: JupyterFrontEndPlugin = { - id: 'shout_button_message:plugin', + id: '@jupyterlab-examples/shout-button:plugin', description: 'An extension that adds a button and message to the right toolbar, with optional status bar widget in JupyterLab.', autoStart: true, @@ -106,9 +125,28 @@ const plugin: JupyterFrontEndPlugin = { console.log('JupyterLab extension shout_button_message is activated!'); // Create a ShoutWidget and add it to the interface in the right sidebar - const shoutWidget: ShoutWidget = new ShoutWidget(statusBar); + const shoutWidget: ShoutWidget = new ShoutWidget(); shoutWidget.id = 'JupyterShoutWidget'; // Widgets need an id + app.shell.add(shoutWidget, 'right'); + + // Check if the status bar is available, and if so, make + // a status bar widget to hold some information + if (statusBar) { + const statusBarWidget = new ShoutStatusBarSummary(); + + statusBar.registerStatusItem('shoutStatusBarSummary', { + item: statusBarWidget + }); + + // Connect to the messageShouted to be notified when a new message + // is published and react to it by updating the status bar widget. + shoutWidget.messageShouted.connect((widget: ShoutWidget, time: Date) => { + statusBarWidget.setSummary( + 'Last Shout: ' + widget.lastShoutTime?.toString() ?? '(None)' + ); + }); + } } }; diff --git a/shout-button-message/ui-tests/README.md b/shout-button-message/ui-tests/README.md index dbe6e8aa..8d377fcc 100644 --- a/shout-button-message/ui-tests/README.md +++ b/shout-button-message/ui-tests/README.md @@ -3,7 +3,7 @@ This folder contains the integration tests of the extension. They are defined using [Playwright](https://playwright.dev/docs/intro) test runner -and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/main/galata) helper. The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). diff --git a/shout-button-message/ui-tests/package.json b/shout-button-message/ui-tests/package.json index 75b94922..66470051 100644 --- a/shout-button-message/ui-tests/package.json +++ b/shout-button-message/ui-tests/package.json @@ -1,15 +1,15 @@ { - "name": "jupyterlab_shout_button_message-ui-tests", - "version": "1.0.0", - "description": "JupyterLab jupyterlab_shout_button_message Integration Tests", - "private": true, - "scripts": { - "start": "jupyter lab --config jupyter_server_test_config.py", - "test": "jlpm playwright test", - "test:update": "jlpm playwright test --update-snapshots" - }, - "devDependencies": { - "@jupyterlab/galata": "^5.0.5", - "@playwright/test": "^1.37.0" - } + "name": "@jupyterlab-examples/shout-button-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/shout-button Integration Tests", + "private": true, + "scripts": { + "start": "jupyter lab --config jupyter_server_test_config.py", + "test": "jlpm playwright test", + "test:update": "jlpm playwright test --update-snapshots" + }, + "devDependencies": { + "@jupyterlab/galata": "^5.0.5", + "@playwright/test": "^1.37.0" + } } diff --git a/shout-button-message/ui-tests/yarn.lock b/shout-button-message/ui-tests/yarn.lock new file mode 100644 index 00000000..e69de29b