diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d636e86c..b2f101ae 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,7 +9,7 @@ on: jobs: build_extensions: runs-on: ${{ matrix.os }} - + strategy: fail-fast: false matrix: @@ -67,8 +67,7 @@ jobs: diff ../hello-world/.eslintignore .eslintignore diff ../hello-world/.eslintrc.js .eslintrc.js diff ../hello-world/.gitignore .gitignore - diff ../hello-world/ui-tests/playwright.config.ts ./ui-tests/playwright.config.ts - diff ../hello-world/ui-tests/README.md ./ui-tests/README.md + diff ../hello-world/ui-tests/playwright.config.js ./ui-tests/playwright.config.js shell: bash - name: Install node if: steps.filter.outputs.extension == 'true' @@ -98,7 +97,7 @@ jobs: - name: Install the Python dependencies if: steps.filter.outputs.extension == 'true' run: | - python -m pip install --upgrade pip jupyter_packaging~=0.10 jupyterlab~=3.1 + python -m pip install --upgrade pip "jupyterlab>=4.0.0.b0" - name: Install the NPM dependencies if: steps.filter.outputs.extension == 'true' run: jlpm @@ -112,16 +111,21 @@ jobs: jupyter labextension list 2>&1 | tee labextension.list cat labextension.list | grep -ie "@jupyterlab-examples/*.*OK" python -m jupyterlab.browser_check - pip uninstall -y $(python setup.py --name) shell: bash + - name: Install galata + if: steps.filter.outputs.extension == 'true' && startsWith(runner.os, 'Linux') + working-directory: ${{ matrix.example }}/ui-tests + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + run: jlpm install + - name: Install kernel-output dependencies + if: steps.filter.outputs.extension == 'true' && ${{ matrix.example }} == 'kernel-output' + run: pip install numpy pandas - name: Integration tests if: steps.filter.outputs.extension == 'true' && startsWith(runner.os, 'Linux') - run: | - docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down || true - docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env pull -q || true - docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build - docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e + working-directory: ${{ matrix.example }}/ui-tests + run: jlpm playwright test - name: Upload UI Test artifacts if: steps.filter.outputs.extension == 'true' && startsWith(runner.os, 'Linux') && always() uses: actions/upload-artifact@v3 @@ -129,12 +133,11 @@ jobs: name: ui-test-output path: | ${{ matrix.example }}/ui-tests/test-results - - name: Stop containers - if: steps.filter.outputs.extension == 'true' && startsWith(runner.os, 'Linux') && always() + - name: Uninstall extension + if: steps.filter.outputs.extension == 'true' && ( startsWith(runner.os, 'Linux') || startsWith(runner.os, 'macOS') ) run: | - # Print jupyterlab logs before removing the containers using the container name set in docker-compose file - docker logs jupyterlab - docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + export NAME=`python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['name'])"` + pip uninstall -y ${NAME} build_serverextension: runs-on: ${{ matrix.os }} @@ -173,8 +176,7 @@ jobs: diff hello-world/tsconfig.json server-extension/tsconfig.json diff hello-world/.eslintignore server-extension/.eslintignore diff hello-world/.eslintrc.js server-extension/.eslintrc.js - diff hello-world/ui-tests/playwright.config.ts server-extension/ui-tests/playwright.config.ts - diff hello-world/ui-tests/README.md server-extension/ui-tests/README.md + diff hello-world/ui-tests/playwright.config.js server-extension/ui-tests/playwright.config.js shell: bash - name: Install Python if: steps.filter.outputs.extension == 'true' @@ -199,7 +201,7 @@ jobs: - name: Install the Python dependencies if: steps.filter.outputs.extension == 'true' run: | - python -m pip install --upgrade pip jupyter_packaging~=0.10 jupyterlab~=3.1 + python -m pip install --upgrade pip jupyterlab~=4.0.0b0 build twine hatch - name: Install the NPM dependencies if: steps.filter.outputs.extension == 'true' run: | @@ -215,20 +217,20 @@ jobs: # Force the usage of the source distribution (good practice) run: | cd server-extension - python setup.py sdist - pip install ./dist/jlab_ext_example* --pre --find-links=dist --no-cache-dir + python -m build --sdist + pip install ./dist/jupyterlab_examples_server* --pre --find-links=dist --no-cache-dir python -m jupyterlab.browser_check - name: Check extension as dev if: steps.filter.outputs.extension == 'true' && ( startsWith(runner.os, 'Linux') || startsWith(runner.os, 'macOS') ) run: | jupyter server extension list 2>&1 | tee serverextension.list - cat serverextension.list | grep -ie "jlab_ext_example.*OK" + cat serverextension.list | grep -ie "jupyterlab_examples_server.*OK" jupyter labextension list 2>&1 | tee labextension.list cat labextension.list | grep -ie "@jupyterlab-examples/server-extension.*OK" - name: Clean extension installation if: steps.filter.outputs.extension == 'true' run: | - pip uninstall -y jlab_ext_example + pip uninstall -y jupyterlab_examples_server jupyter lab clean jupyter server extension list jupyter labextension list @@ -241,19 +243,21 @@ jobs: if: steps.filter.outputs.extension == 'true' && ( startsWith(runner.os, 'Linux') || startsWith(runner.os, 'macOS') ) run: | jupyter server extension list 2>&1 | tee serverextension.list - cat serverextension.list | grep -ie "jlab_ext_example.*OK" + cat serverextension.list | grep -ie "jupyterlab_examples_server.*OK" jupyter labextension list 2>&1 | tee labextension.list cat labextension.list | grep -ie "@jupyterlab-examples/server-extension.*OK" python -m jupyterlab.browser_check + - name: Install galata + if: steps.filter.outputs.extension == 'true' && startsWith(runner.os, 'Linux') + working-directory: server-extension/ui-tests + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + run: jlpm install - name: Integration tests if: steps.filter.outputs.extension == 'true' && startsWith(runner.os, 'Linux') - run: | - cd server-extension - docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down || true - docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env pull -q || true - docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build - docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e + working-directory: server-extension/ui-tests + run: jlpm playwright test - name: Upload UI Test artifacts if: steps.filter.outputs.extension == 'true' && startsWith(runner.os, 'Linux') && always() uses: actions/upload-artifact@v3 @@ -261,13 +265,11 @@ jobs: name: ui-test-output path: | server-extension/ui-tests/test-results - - name: Stop containers - if: steps.filter.outputs.extension == 'true' && startsWith(runner.os, 'Linux') && always() + - name: Uninstall extension + if: steps.filter.outputs.extension == 'true' && ( startsWith(runner.os, 'Linux') || startsWith(runner.os, 'macOS') ) run: | - cd server-extension - # Print jupyterlab logs before removing the containers using the container name set in docker-compose file - docker logs jupyterlab - docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + export NAME=`python -c "import tomllib; print(tomllib.load(open('server-extension/pyproject.toml', 'rb'))['project']['name'])"` + pip uninstall -y ${NAME} build_all: runs-on: ${{ matrix.os }} @@ -309,7 +311,7 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - name: Install the Python dependencies - run: python -m pip install jupyter_packaging~=0.10 jupyterlab~=3.1 pytest pytest-check-links + run: python -m pip install jupyterlab~=4.0.0b0 pytest pytest-check-links - name: Bootstrap the jlpm deps run: jlpm - name: Build all the extensions @@ -318,4 +320,4 @@ jobs: jlpm lint:check jlpm install-ext # Check links as last step as new tutorial may set links not yet valid (like file not yet in master) - pytest --check-links + jlpm run lerna exec --concurrency 4 -- "pytest --check-links" diff --git a/.gitignore b/.gitignore index 8b43cc12..ab6f10cb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ node_modules/ *.tsbuildinfo *.lock __pycache__ +.yarn/ # Ensure embedme does not run ont node_modules README.md files. **/node_modules/**/README.md diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/LICENSE b/LICENSE index 6f69e2cd..080e074a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2020, Project Jupyter Contributors. +Copyright (c) 2020-2023, Project Jupyter Contributors. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/command-palette/.eslintrc.js b/command-palette/.eslintrc.js index c2375786..665374bf 100644 --- a/command-palette/.eslintrc.js +++ b/command-palette/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/command-palette/.gitignore b/command-palette/.gitignore index 18cace13..ef3ace98 100644 --- a/command-palette/.gitignore +++ b/command-palette/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/command-palette/.prettierignore b/command-palette/.prettierignore new file mode 100644 index 00000000..f79cab93 --- /dev/null +++ b/command-palette/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_command_palette diff --git a/command-palette/.prettierrc b/command-palette/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/command-palette/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/command-palette/.stylelintrc b/command-palette/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/command-palette/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/command-palette/.yarnrc.yml b/command-palette/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/command-palette/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/command-palette/CHANGELOG.md b/command-palette/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/command-palette/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/command-palette/LICENSE b/command-palette/LICENSE new file mode 100644 index 00000000..54d885af --- /dev/null +++ b/command-palette/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/command-palette/MANIFEST.in b/command-palette/MANIFEST.in deleted file mode 100644 index 31fb6283..00000000 --- a/command-palette/MANIFEST.in +++ /dev/null @@ -1,22 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml -include jupyter-config/jupyterlab_examples_command_palette.json - -include package.json -include ts*.json - -graft jupyterlab_examples_command_palette/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/command-palette/README.md b/command-palette/README.md index a1459cd4..ae091337 100644 --- a/command-palette/README.md +++ b/command-palette/README.md @@ -42,13 +42,13 @@ const extension: JupyterFrontEndPlugin = { console.log( `jlab-examples:command-palette has been called ${args['origin']}.` ); - }, + } }); // Add the command to the command palette const category = 'Extension Examples'; palette.addItem({ command, category, args: { origin: 'from palette' } }); - }, + } ``` The `ICommandPalette` diff --git a/command-palette/RELEASE.md b/command-palette/RELEASE.md index 26bb779b..c353aefe 100644 --- a/command-palette/RELEASE.md +++ b/command-palette/RELEASE.md @@ -1,22 +1,62 @@ # Making a new release of jupyterlab_examples_command_palette -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser The extension repository should already be compatible with the Jupyter Releaser. -Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. Here is a summary of the steps to cut a new release: -- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) -- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository - Go to the Actions panel -- Run the "Draft Changelog" workflow -- Merge the Changelog PR -- Run the "Draft Release" workflow -- Run the "Publish Release" workflow +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow ## Publishing to `conda-forge` diff --git a/command-palette/babel.config.js b/command-palette/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/command-palette/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/command-palette/jest.config.js b/command-palette/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/command-palette/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/command-palette/jupyterlab_examples_command_palette/__init__.py b/command-palette/jupyterlab_examples_command_palette/__init__.py index 353ab522..f56e7c06 100644 --- a/command-palette/jupyterlab_examples_command_palette/__init__.py +++ b/command-palette/jupyterlab_examples_command_palette/__init__.py @@ -1,19 +1,9 @@ - -import json -import os.path as osp - from ._version import __version__ -HERE = osp.abspath(osp.dirname(__file__)) - -with open(osp.join(HERE, 'labextension', 'package.json')) as fid: - data = json.load(fid) def _jupyter_labextension_paths(): return [{ - 'src': 'labextension', - 'dest': data['name'] + "src": "labextension", + "dest": "@jupyterlab-examples/command-palette" }] - - diff --git a/command-palette/jupyterlab_examples_command_palette/_version.py b/command-palette/jupyterlab_examples_command_palette/_version.py index ee864fc9..133868ac 100644 --- a/command-palette/jupyterlab_examples_command_palette/_version.py +++ b/command-palette/jupyterlab_examples_command_palette/_version.py @@ -1,2 +1,4 @@ -version_info = (0, 1, 0) -__version__ = ".".join(map(str, version_info)) +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/command-palette/package.json b/command-palette/package.json index de8eafa6..e53b251a 100644 --- a/command-palette/package.json +++ b/command-palette/package.json @@ -14,11 +14,11 @@ "license": "BSD-3-Clause", "author": { "name": "Project Jupyter Contributors", - "email": "" + "email": "me@test.com" }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "style/**/*.{css,eot,js,gif,html,jpg,json,png,svg,woff2,ttf}" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -27,49 +27,75 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jupyterlab_examples_command_palette/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_command_palette/labextension jupyterlab_examples_command_palette/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0" + "@jupyterlab/application": "^4.0.0-beta.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "extension": true, "outputDir": "jupyterlab_examples_command_palette/labextension" - }, - "styleModule": "style/index.js" + } } diff --git a/command-palette/preview.png b/command-palette/preview.png index 140f69dd..b3c1169d 100644 Binary files a/command-palette/preview.png and b/command-palette/preview.png differ diff --git a/command-palette/pyproject.toml b/command-palette/pyproject.toml index 46cd2dab..de6bbee1 100644 --- a/command-palette/pyproject.toml +++ b/command-palette/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_command_palette/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_command_palette/labextension/static/style.js", "jupyterlab_examples_command_palette/labextension/package.json"] +[project] +name = "jupyterlab_examples_command_palette" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_command_palette/labextension"] +exclude = [".github", "binder"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_command_palette/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/command-palette" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/command-palette/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_command_palette/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_command_palette/labextension/static/style.js", + "jupyterlab_examples_command_palette/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_command_palette/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_command_palette/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_command_palette/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/command-palette/setup.py b/command-palette/setup.py index ce603aa0..bea23374 100644 --- a/command-palette/setup.py +++ b/command-palette/setup.py @@ -1,83 +1 @@ -""" -jupyterlab_examples_command_palette setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_command_palette" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/command-palette" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/command-palette/src/index.ts b/command-palette/src/index.ts index a8f8eb88..2dfdd692 100644 --- a/command-palette/src/index.ts +++ b/command-palette/src/index.ts @@ -1,6 +1,6 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; import { ICommandPalette } from '@jupyterlab/apputils'; @@ -25,13 +25,13 @@ const extension: JupyterFrontEndPlugin = { console.log( `jlab-examples:command-palette has been called ${args['origin']}.` ); - }, + } }); // Add the command to the command palette const category = 'Extension Examples'; palette.addItem({ command, category, args: { origin: 'from palette' } }); - }, + } }; export default extension; diff --git a/command-palette/style/base.css b/command-palette/style/base.css index e69de29b..e11f4577 100644 --- a/command-palette/style/base.css +++ b/command-palette/style/base.css @@ -0,0 +1,5 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ diff --git a/command-palette/style/index.css b/command-palette/style/index.css index 8a7ea29e..e98119b5 100644 --- a/command-palette/style/index.css +++ b/command-palette/style/index.css @@ -1 +1 @@ -@import url('base.css'); +@import 'base.css'; diff --git a/command-palette/tsconfig.json b/command-palette/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/command-palette/tsconfig.json +++ b/command-palette/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/command-palette/tsconfig.test.json b/command-palette/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/command-palette/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/command-palette/ui-tests/.env b/command-palette/ui-tests/.env deleted file mode 100644 index b2c68144..00000000 --- a/command-palette/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=command-palette/jupyterlab_examples_command_palette -EXT_NAME=command-palette \ No newline at end of file diff --git a/command-palette/ui-tests/README.md b/command-palette/ui-tests/README.md index d2be7291..5e839c6b 100644 --- a/command-palette/ui-tests/README.md +++ b/command-palette/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _command-palette_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _command-palette_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _command-palette_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _command-palette_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/command-palette/ui-tests/jupyter_server_test_config.py b/command-palette/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/command-palette/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/command-palette/ui-tests/package.json b/command-palette/ui-tests/package.json index 13ef0857..1be2f0ea 100644 --- a/command-palette/ui-tests/package.json +++ b/command-palette/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/command-palette-tests", - "version": "0.1.0", - "description": "Integration test for command-palette example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/command-palette-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/command-palette 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/command-palette/ui-tests/playwright.config.js b/command-palette/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/command-palette/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/command-palette/ui-tests/playwright.config.ts b/command-palette/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/command-palette/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/command-palette/ui-tests/tests/command-palette.spec.ts b/command-palette/ui-tests/tests/command-palette.spec.ts index b7e3065b..5b7200fd 100644 --- a/command-palette/ui-tests/tests/command-palette.spec.ts +++ b/command-palette/ui-tests/tests/command-palette.spec.ts @@ -3,11 +3,11 @@ import { test, expect } from '@jupyterlab/galata'; test.use({ autoGoto: false }); test('should emit console message when called from palette', async ({ - page, + page }) => { const logs: string[] = []; - page.on('console', (message) => { + page.on('console', message => { logs.push(message.text()); }); @@ -26,7 +26,7 @@ test('should emit console message when called from palette', async ({ await page.click('text=Execute jlab-examples:command-palette Command'); expect( - logs.filter((s) => + logs.filter(s => s.startsWith('jlab-examples:command-palette has been called from palette') ) ).toHaveLength(1); diff --git a/commands/.eslintrc.js b/commands/.eslintrc.js index c2375786..665374bf 100644 --- a/commands/.eslintrc.js +++ b/commands/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/commands/.gitignore b/commands/.gitignore index 18cace13..ef3ace98 100644 --- a/commands/.gitignore +++ b/commands/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/commands/.prettierignore b/commands/.prettierignore new file mode 100644 index 00000000..95c1746b --- /dev/null +++ b/commands/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_commands diff --git a/commands/.prettierrc b/commands/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/commands/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/commands/.stylelintrc b/commands/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/commands/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/commands/.yarnrc.yml b/commands/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/commands/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/commands/CHANGELOG.md b/commands/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/commands/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/commands/LICENSE b/commands/LICENSE new file mode 100644 index 00000000..54d885af --- /dev/null +++ b/commands/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/commands/MANIFEST.in b/commands/MANIFEST.in deleted file mode 100644 index 88b2f920..00000000 --- a/commands/MANIFEST.in +++ /dev/null @@ -1,22 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml -include jupyter-config/jupyterlab_examples_commands.json - -include package.json -include ts*.json - -graft jupyterlab_examples_commands/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/commands/README.md b/commands/README.md index acc89d28..daec692a 100644 --- a/commands/README.md +++ b/commands/README.md @@ -40,16 +40,16 @@ const extension: JupyterFrontEndPlugin = { if (orig !== 'init') { window.alert(`jlab-examples:command has been called from ${orig}.`); } - }, + } }); // Call the command execution - commands.execute(command, { origin: 'init' }).catch((reason) => { + commands.execute(command, { origin: 'init' }).catch(reason => { console.error( `An error occurred during the execution of jlab-examples:command.\n${reason}` ); }); - }, + } }; ``` @@ -71,12 +71,12 @@ with the unique command id and optionally the arguments. ```ts // src/index.ts#L31-L36 - commands.execute(command, { origin: 'init' }).catch((reason) => { + commands.execute(command, { origin: 'init' }).catch(reason => { console.error( `An error occurred during the execution of jlab-examples:command.\n${reason}` ); }); -}, +} ``` When running JupyterLab with this extension, the following message should diff --git a/commands/RELEASE.md b/commands/RELEASE.md index b8469fd9..584f331d 100644 --- a/commands/RELEASE.md +++ b/commands/RELEASE.md @@ -1,22 +1,62 @@ # Making a new release of jupyterlab_examples_commands -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser The extension repository should already be compatible with the Jupyter Releaser. -Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. Here is a summary of the steps to cut a new release: -- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) -- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository - Go to the Actions panel -- Run the "Draft Changelog" workflow -- Merge the Changelog PR -- Run the "Draft Release" workflow -- Run the "Publish Release" workflow +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow ## Publishing to `conda-forge` diff --git a/commands/babel.config.js b/commands/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/commands/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/commands/jest.config.js b/commands/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/commands/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/commands/jupyterlab_examples_commands/__init__.py b/commands/jupyterlab_examples_commands/__init__.py index 353ab522..3db50ae5 100644 --- a/commands/jupyterlab_examples_commands/__init__.py +++ b/commands/jupyterlab_examples_commands/__init__.py @@ -1,19 +1,9 @@ - -import json -import os.path as osp - from ._version import __version__ -HERE = osp.abspath(osp.dirname(__file__)) - -with open(osp.join(HERE, 'labextension', 'package.json')) as fid: - data = json.load(fid) def _jupyter_labextension_paths(): return [{ - 'src': 'labextension', - 'dest': data['name'] + "src": "labextension", + "dest": "@jupyterlab-examples/commands" }] - - diff --git a/commands/jupyterlab_examples_commands/_version.py b/commands/jupyterlab_examples_commands/_version.py index ee864fc9..133868ac 100644 --- a/commands/jupyterlab_examples_commands/_version.py +++ b/commands/jupyterlab_examples_commands/_version.py @@ -1,2 +1,4 @@ -version_info = (0, 1, 0) -__version__ = ".".join(map(str, version_info)) +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/commands/package.json b/commands/package.json index a05d47bd..3d07ad15 100644 --- a/commands/package.json +++ b/commands/package.json @@ -18,7 +18,7 @@ }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "style/**/*.{css,eot,js,gif,html,jpg,json,png,svg,woff2,ttf}" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -27,49 +27,75 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jupyterlab_examples_commands/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_commands/labextension jupyterlab_examples_commands/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0" + "@jupyterlab/application": "^4.0.0-beta.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "extension": true, "outputDir": "jupyterlab_examples_commands/labextension" - }, - "styleModule": "style/index.js" + } } diff --git a/commands/preview.png b/commands/preview.png index fa94d40c..c0974543 100644 Binary files a/commands/preview.png and b/commands/preview.png differ diff --git a/commands/pyproject.toml b/commands/pyproject.toml index 291cc0d2..8c5bc0ac 100644 --- a/commands/pyproject.toml +++ b/commands/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_commands/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_commands/labextension/static/style.js", "jupyterlab_examples_commands/labextension/package.json"] +[project] +name = "jupyterlab_examples_commands" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_commands/labextension"] +exclude = [".github", "binder"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_commands/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/commands" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/commands/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_commands/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_commands/labextension/static/style.js", + "jupyterlab_examples_commands/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_commands/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_commands/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_commands/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/commands/setup.py b/commands/setup.py index b1272fc9..bea23374 100644 --- a/commands/setup.py +++ b/commands/setup.py @@ -1,83 +1 @@ -""" -jupyterlab_examples_commands setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_commands" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/commands" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/commands/src/index.ts b/commands/src/index.ts index df8b5b1e..610e9a0b 100644 --- a/commands/src/index.ts +++ b/commands/src/index.ts @@ -1,6 +1,6 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; /** @@ -24,16 +24,16 @@ const extension: JupyterFrontEndPlugin = { if (orig !== 'init') { window.alert(`jlab-examples:command has been called from ${orig}.`); } - }, + } }); // Call the command execution - commands.execute(command, { origin: 'init' }).catch((reason) => { + commands.execute(command, { origin: 'init' }).catch(reason => { console.error( `An error occurred during the execution of jlab-examples:command.\n${reason}` ); }); - }, + } }; export default extension; diff --git a/commands/style/base.css b/commands/style/base.css index e69de29b..e11f4577 100644 --- a/commands/style/base.css +++ b/commands/style/base.css @@ -0,0 +1,5 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ diff --git a/commands/style/index.css b/commands/style/index.css index 8a7ea29e..e98119b5 100644 --- a/commands/style/index.css +++ b/commands/style/index.css @@ -1 +1 @@ -@import url('base.css'); +@import 'base.css'; diff --git a/commands/tsconfig.json b/commands/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/commands/tsconfig.json +++ b/commands/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/commands/tsconfig.test.json b/commands/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/commands/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/commands/ui-tests/.env b/commands/ui-tests/.env deleted file mode 100644 index 04e2151c..00000000 --- a/commands/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=commands/jupyterlab_examples_commands -EXT_NAME=commands \ No newline at end of file diff --git a/commands/ui-tests/README.md b/commands/ui-tests/README.md index d2be7291..d8bbbcd2 100644 --- a/commands/ui-tests/README.md +++ b/commands/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _commands_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _commands_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _commands_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _commands_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/commands/ui-tests/jupyter_server_test_config.py b/commands/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/commands/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/commands/ui-tests/package.json b/commands/ui-tests/package.json index d11e83d6..ad8fb27f 100644 --- a/commands/ui-tests/package.json +++ b/commands/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/commands-tests", - "version": "0.1.0", - "description": "Integration test for commands example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/commands-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/commands 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/commands/ui-tests/playwright.config.js b/commands/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/commands/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/commands/ui-tests/playwright.config.ts b/commands/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/commands/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/commands/ui-tests/tests/commands.spec.ts b/commands/ui-tests/tests/commands.spec.ts index 6c5fd3e8..f67c2ed9 100644 --- a/commands/ui-tests/tests/commands.spec.ts +++ b/commands/ui-tests/tests/commands.spec.ts @@ -5,13 +5,13 @@ test.use({ autoGoto: false }); test('should emit console message', async ({ page }) => { const logs: string[] = []; - page.on('console', (message) => { + page.on('console', message => { logs.push(message.text()); }); await page.goto(); expect( - logs.filter((s) => s === 'jlab-examples:command has been called from init.') + logs.filter(s => s === 'jlab-examples:command has been called from init.') ).toHaveLength(1); }); diff --git a/completer/.eslintrc.js b/completer/.eslintrc.js index c2375786..665374bf 100644 --- a/completer/.eslintrc.js +++ b/completer/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/completer/.gitignore b/completer/.gitignore index 18cace13..ef3ace98 100644 --- a/completer/.gitignore +++ b/completer/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/completer/.prettierignore b/completer/.prettierignore new file mode 100644 index 00000000..ecc5c40e --- /dev/null +++ b/completer/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_completer diff --git a/completer/.prettierrc b/completer/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/completer/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/completer/.stylelintrc b/completer/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/completer/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/completer/.yarnrc.yml b/completer/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/completer/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/completer/CHANGELOG.md b/completer/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/completer/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/completer/LICENSE b/completer/LICENSE new file mode 100644 index 00000000..54d885af --- /dev/null +++ b/completer/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/completer/MANIFEST.in b/completer/MANIFEST.in deleted file mode 100644 index 5133280b..00000000 --- a/completer/MANIFEST.in +++ /dev/null @@ -1,22 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml -include jupyter-config/jupyterlab_examples_completer.json - -include package.json -include ts*.json - -graft jupyterlab_examples_completer/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/completer/README.md b/completer/README.md index f187c87c..a290c868 100644 --- a/completer/README.md +++ b/completer/README.md @@ -15,24 +15,22 @@ In this example, you will learn how to customize the behavior of JupyterLab note ## Code structure -The code is split into three parts: +The code is split into two parts: 1. the JupyterLab plugin that activates all the extension components and connects them to the main _JupyterLab_ application via commands, -2. a custom `CompletionConnector`, adapted from [jupyterlab/packages/completer/src/connector.ts](https://github.com/jupyterlab/jupyterlab/blob/v3.0.0/packages/completer/src/connector.ts), - that aggregates completion results from three sources: _JupyterLab_'s existing `KernelConnector` and `ContextConnector`, plus... -3. `CustomConnector`, a lightweight source of mocked completion results. +2. `CustomConnector`, a lightweight source of mocked completion results. -The first part is contained in the `index.ts` file, the second is in `connector.ts`, and the third is in `customconnector.ts`. +The first part is contained in the `index.ts` file, the second is in `customconnector.ts`. ## Creating a custom DataConnector -`src/customconnector.ts` defines a `CustomConnector` to generate mock autocomplete suggestions. Like the `ContextConnector` it is based on, `CustomConnector` extends _JupyterLab_'s abstract [`DataConnector`](https://jupyterlab.readthedocs.io/en/latest/api/classes/statedb.DataConnector.html) class. +`src/customconnector.ts` defines a `CustomConnector` to generate mock autocomplete suggestions. It implements _JupyterLab_'s interface [`ICompletionProvider`](https://github.com/jupyterlab/jupyterlab/blob/b279092d88de650ea36460689257e1b8e8a418bf/packages/completer/src/tokens.ts#L44) class. -The only abstract method in `DataConnector` is `fetch`, which must be implemented in your `CustomConnector`. +The two methods which must be implemented in your `CustomConnector` from `ICompletionProvider` are `fetch` and `isApplicable`, which must be implemented in your `CustomConnector`. ```ts -// src/customconnector.ts#L28-L43 +// src/customconnector.ts#L25-L43 /** * Fetch completion requests. @@ -41,179 +39,72 @@ The only abstract method in `DataConnector` is `fetch`, which must be implemente * @returns Completion reply */ fetch( - request: CompletionHandler.IRequest -): Promise { - if (!this._editor) { + request: CompletionHandler.IRequest, + context: ICompletionContext +): Promise { + const editor = context.editor; + + if (!editor) { return Promise.reject('No editor'); } - return new Promise((resolve) => { - resolve(Private.completionHint(this._editor)); + return new Promise(resolve => { + resolve(Private.completionHint(editor!)); }); } ``` -This calls a private `completionHint` function, which, like `ContextConnector`'s `contextHint` function, uses the `CodeEditor.IEditor` widget to determine the token to suggest matches for. +This calls a private `completionHint` function, which uses the `CodeEditor.IEditor` widget to determine the token to suggest matches for. ```ts -// src/customconnector.ts#L73-L78 +// src/customconnector.ts#L74-L78 export function completionHint( editor: CodeEditor.IEditor -): CompletionHandler.IReply { +): CompletionHandler.ICompletionItemsReply { // Find the token at the cursor - const cursor = editor.getCursorPosition(); - const token = editor.getTokenForPosition(cursor); + const token = editor.getTokenAtCursor(); ``` -A list of mock completion tokens is then created to return as `matches` in the `CompletionHandler.IReply` response. +A list of mock completion tokens is then created to return as `ICompletionItemsReply` response. ```ts -// src/customconnector.ts#L80-L97 +// src/customconnector.ts#L80-L99 // Create a list of matching tokens. const tokenList = [ { value: token.value + 'Magic', offset: token.offset, type: 'magic' }, { value: token.value + 'Science', offset: token.offset, type: 'science' }, - { value: token.value + 'Neither', offset: token.offset }, + { value: token.value + 'Neither', offset: token.offset } ]; // Only choose the ones that have a non-empty type field, which are likely to be of interest. -const completionList = tokenList.filter((t) => t.type).map((t) => t.value); +const completionList = tokenList.filter(t => t.type).map(t => t.value); // Remove duplicate completions from the list const matches = Array.from(new Set(completionList)); +const items = new Array(); +matches.forEach(label => items.push({ label })); + return { start: token.offset, end: token.offset + token.value.length, - matches, - metadata: {}, + items }; ``` ## Aggregating connector responses -[_JupyterLab_'s `CompletionConnector`](https://github.com/jupyterlab/jupyterlab/blob/v3.0.0/packages/completer/src/connector.ts) fetches and merges completion responses from `KernelConnector` and `ContextConnector`. The modified `CompletionConnector` in `src/connector.ts` is more general; given an array of `DataConnectors`, it can fetch and merge completion matches from every connector provided. - -```ts -// src/connector.ts#L33-L50 - -/** - * Fetch completion requests. - * - * @param request - The completion request text and details. - * @returns Completion reply - */ -fetch( - request: CompletionHandler.IRequest -): Promise { - return Promise.all( - this._connectors.map((connector) => connector.fetch(request)) - ).then((replies) => { - const definedReplies = replies.filter( - (reply): reply is CompletionHandler.IReply => !!reply - ); - return Private.mergeReplies(definedReplies); - }); -} -``` - -## Disabling a JupyterLab plugin - -[_JupyterLab_'s completer-extension](https://github.com/jupyterlab/jupyterlab/tree/v3.0.0/packages/completer-extension) includes a notebooks plugin that registers notebooks for code completion. Your extension will override the notebooks plugin's behavior, so you can [disable notebooks](https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html#disabling-other-extensions) in your `.package.json`: - -```json5 -// package.json#L81-L88 - -"jupyterlab": { - "extension": true, - "schemaDir": "schema", - "outputDir": "jupyterlab_examples_completer/labextension", - "disabledExtensions": [ - "@jupyterlab/completer-extension:notebooks" - ] -}, -``` - -## Asynchronous extension initialization - -`index.ts` contains the code to initialize this extension. Nearly all of the code in `index.ts` is copied directly from the notebooks plugin. - -Note that the extension commands you're overriding are unified into one namespace at the top of the file: - -```ts -// src/index.ts#L21-L29 - -namespace CommandIDs { - export const invoke = 'completer:invoke'; - - export const invokeNotebook = 'completer:invoke-notebook'; - - export const select = 'completer:select'; - - export const selectNotebook = 'completer:select-notebook'; -} -``` - -`index.ts` imports four connector classes, two from `JupyterLab`: - - -```ts -// src/index.ts#L6-L10 - -import { - ContextConnector, - ICompletionManager, - KernelConnector, -} from '@jupyterlab/completer'; -``` - - -and two from this extension: +[_JupyterLab_'s `CompletionManager`](https://github.com/jupyterlab/jupyterlab/blob/master/packages/completer/src/manager.ts) fetches and merges completion responses from `KernelConnector` and `ContextConnector` (https://github.com/jupyterlab/jupyterlab/blob/b279092d88de650ea36460689257e1b8e8a418bf/packages/completer-extension/src/index.ts#L29). +We add our new completer provider to it: ```ts -// src/index.ts#L14-L16 +// src/index.ts#L22-L22 -import { CompletionConnector } from './connector'; - -import { CustomConnector } from './customconnector'; +completionManager.registerProvider(new CustomCompleterProvider()); ``` -Just like the notebooks plugin, when you update the handler for a notebook call `updateConnector`: - -```ts -// src/index.ts#L74-L76 - -// Update the handler whenever the prompt or session changes -panel.content.activeCellChanged.connect(updateConnector); -panel.sessionContext.sessionChanged.connect(updateConnector); -``` - -which, unlike the notebooks plugin, instantiates `KernelConnector`, `ContextConnector`, and `CustomConnector`, then passes them to your modified `CompletionConnector`: - - -```ts -// src/index.ts#L58-L72 - -const updateConnector = () => { - editor = panel.content.activeCell?.editor ?? null; - options.session = panel.sessionContext.session; - options.editor = editor; - handler.editor = editor; - - const kernel = new KernelConnector(options); - const context = new ContextConnector(options); - const custom = new CustomConnector(options); - handler.connector = new CompletionConnector([ - kernel, - context, - custom, - ]); -}; -``` - - ## Where to go next Create a [server extension](../server-extension) to serve up custom completion matches. diff --git a/completer/RELEASE.md b/completer/RELEASE.md index e1b03300..914c495b 100644 --- a/completer/RELEASE.md +++ b/completer/RELEASE.md @@ -1,22 +1,62 @@ # Making a new release of jupyterlab_examples_completer -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser The extension repository should already be compatible with the Jupyter Releaser. -Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. Here is a summary of the steps to cut a new release: -- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) -- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository - Go to the Actions panel -- Run the "Draft Changelog" workflow -- Merge the Changelog PR -- Run the "Draft Release" workflow -- Run the "Publish Release" workflow +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow ## Publishing to `conda-forge` diff --git a/completer/babel.config.js b/completer/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/completer/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/completer/jest.config.js b/completer/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/completer/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/completer/jupyterlab_examples_completer/__init__.py b/completer/jupyterlab_examples_completer/__init__.py index 353ab522..0178445b 100644 --- a/completer/jupyterlab_examples_completer/__init__.py +++ b/completer/jupyterlab_examples_completer/__init__.py @@ -1,19 +1,9 @@ - -import json -import os.path as osp - from ._version import __version__ -HERE = osp.abspath(osp.dirname(__file__)) - -with open(osp.join(HERE, 'labextension', 'package.json')) as fid: - data = json.load(fid) def _jupyter_labextension_paths(): return [{ - 'src': 'labextension', - 'dest': data['name'] + "src": "labextension", + "dest": "@jupyterlab-examples/completer" }] - - diff --git a/completer/jupyterlab_examples_completer/_version.py b/completer/jupyterlab_examples_completer/_version.py index ee864fc9..133868ac 100644 --- a/completer/jupyterlab_examples_completer/_version.py +++ b/completer/jupyterlab_examples_completer/_version.py @@ -1,2 +1,4 @@ -version_info = (0, 1, 0) -__version__ = ".".join(map(str, version_info)) +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/completer/package.json b/completer/package.json index 50f67626..56e488fc 100644 --- a/completer/package.json +++ b/completer/package.json @@ -1,7 +1,7 @@ { "name": "@jupyterlab-examples/completer", "version": "0.1.0", - "description": "minimal lab example", + "description": "Minimal lab extension setting up the completion", "keywords": [ "jupyter", "jupyterlab", @@ -18,7 +18,8 @@ }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "style/**/*.{css,eot,js,gif,html,jpg,json,png,svg,woff2,ttf}" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", + "schema/*.json" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -27,64 +28,80 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jupyterlab_examples_completer/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_completer/labextension jupyterlab_examples_completer/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0", - "@jupyterlab/codeeditor": "^3.1.0", - "@jupyterlab/completer": "^3.1.0", - "@jupyterlab/launcher": "^3.1.0", - "@jupyterlab/notebook": "^3.1.0", - "@jupyterlab/outputarea": "^3.1.0", - "@jupyterlab/statedb": "^3.1.0", - "@jupyterlab/translation": "^3.1.0", - "@lumino/algorithm": "^1.6.0", - "@lumino/coreutils": "^1.8.0", - "@lumino/datagrid": "^0.27.0", - "@lumino/disposable": "^1.7.0" + "@jupyterlab/application": "^4.0.0-beta.0", + "@jupyterlab/codeeditor": "^4.0.0-beta.0", + "@jupyterlab/completer": "^4.0.0-beta.0", + "@jupyterlab/notebook": "^4.0.0-beta.0", + "@jupyterlab/settingregistry": "^4.0.0-beta.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "extension": true, - "schemaDir": "schema", "outputDir": "jupyterlab_examples_completer/labextension", - "disabledExtensions": [ - "@jupyterlab/completer-extension:notebooks" - ] - }, - "styleModule": "style/index.js" + "schemaDir": "schema" + } } diff --git a/completer/preview.png b/completer/preview.png index da772eac..85db5b0d 100644 Binary files a/completer/preview.png and b/completer/preview.png differ diff --git a/completer/pyproject.toml b/completer/pyproject.toml index 194f1337..86918d65 100644 --- a/completer/pyproject.toml +++ b/completer/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_completer/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_completer/labextension/static/style.js", "jupyterlab_examples_completer/labextension/package.json"] +[project] +name = "jupyterlab_examples_completer" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_completer/labextension"] +exclude = [".github", "binder"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_completer/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/completer" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/completer/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_completer/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_completer/labextension/static/style.js", + "jupyterlab_examples_completer/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_completer/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_completer/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_completer/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/completer/setup.py b/completer/setup.py index f647ecbc..bea23374 100644 --- a/completer/setup.py +++ b/completer/setup.py @@ -1,83 +1 @@ -""" -jupyterlab_examples_completer setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_completer" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/completer" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/completer/src/connector.ts b/completer/src/connector.ts deleted file mode 100644 index 1c0b32a0..00000000 --- a/completer/src/connector.ts +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -// Modified from jupyterlab/packages/completer/src/connector.ts - -import { DataConnector } from '@jupyterlab/statedb'; -import { CompletionHandler } from '@jupyterlab/completer'; - -/** - * A multi-connector connector for completion handlers. - */ -export class CompletionConnector extends DataConnector< - CompletionHandler.IReply, - void, - CompletionHandler.IRequest -> { - /** - * Create a new connector for completion requests. - * - * @param connectors - Connectors to request matches from, ordered by metadata preference (descending). - */ - constructor( - connectors: DataConnector< - CompletionHandler.IReply, - void, - CompletionHandler.IRequest - >[] - ) { - super(); - this._connectors = connectors; - } - - /** - * Fetch completion requests. - * - * @param request - The completion request text and details. - * @returns Completion reply - */ - fetch( - request: CompletionHandler.IRequest - ): Promise { - return Promise.all( - this._connectors.map((connector) => connector.fetch(request)) - ).then((replies) => { - const definedReplies = replies.filter( - (reply): reply is CompletionHandler.IReply => !!reply - ); - return Private.mergeReplies(definedReplies); - }); - } - - private _connectors: DataConnector< - CompletionHandler.IReply, - void, - CompletionHandler.IRequest - >[]; -} - -/** - * A namespace for private functionality. - */ -namespace Private { - /** - * Merge results from multiple connectors. - * - * @param replies - Array of completion results. - * @returns IReply with a superset of all matches. - */ - export function mergeReplies( - replies: Array - ): CompletionHandler.IReply { - // Filter replies with matches. - const repliesWithMatches = replies.filter((rep) => rep.matches.length > 0); - // If no replies contain matches, return an empty IReply. - if (repliesWithMatches.length === 0) { - return replies[0]; - } - // If only one reply contains matches, return it. - if (repliesWithMatches.length === 1) { - return repliesWithMatches[0]; - } - - // Collect unique matches from all replies. - const matches: Set = new Set(); - repliesWithMatches.forEach((reply) => { - reply.matches.forEach((match) => matches.add(match)); - }); - - // Note that the returned metadata field only contains items in the first member of repliesWithMatches. - return { ...repliesWithMatches[0], matches: [...matches] }; - } -} diff --git a/completer/src/customconnector.ts b/completer/src/customconnector.ts index adf4d394..1269616b 100644 --- a/completer/src/customconnector.ts +++ b/completer/src/customconnector.ts @@ -4,25 +4,22 @@ // Modified from jupyterlab/packages/completer/src/contextconnector.ts import { CodeEditor } from '@jupyterlab/codeeditor'; -import { DataConnector } from '@jupyterlab/statedb'; -import { CompletionHandler } from '@jupyterlab/completer'; +import { + CompletionHandler, + ICompletionContext, + ICompletionProvider +} from '@jupyterlab/completer'; /** * A custom connector for completion handlers. */ -export class CustomConnector extends DataConnector< - CompletionHandler.IReply, - void, - CompletionHandler.IRequest -> { +export class CustomCompleterProvider implements ICompletionProvider { /** - * Create a new custom connector for completion requests. - * - * @param options - The instatiation options for the custom connector. + * The context completion provider is applicable on all cases. + * @param context - additional information about context of completion request */ - constructor(options: CustomConnector.IOptions) { - super(); - this._editor = options.editor; + async isApplicable(context: ICompletionContext): Promise { + return true; } /** @@ -32,17 +29,21 @@ export class CustomConnector extends DataConnector< * @returns Completion reply */ fetch( - request: CompletionHandler.IRequest - ): Promise { - if (!this._editor) { + request: CompletionHandler.IRequest, + context: ICompletionContext + ): Promise { + const editor = context.editor; + + if (!editor) { return Promise.reject('No editor'); } - return new Promise((resolve) => { - resolve(Private.completionHint(this._editor)); + return new Promise(resolve => { + resolve(Private.completionHint(editor!)); }); } - private _editor: CodeEditor.IEditor | null; + readonly identifier = 'CompletionProvider:custom'; + readonly renderer = null; } /** @@ -72,28 +73,29 @@ namespace Private { */ export function completionHint( editor: CodeEditor.IEditor - ): CompletionHandler.IReply { + ): CompletionHandler.ICompletionItemsReply { // Find the token at the cursor - const cursor = editor.getCursorPosition(); - const token = editor.getTokenForPosition(cursor); + const token = editor.getTokenAtCursor(); // Create a list of matching tokens. const tokenList = [ { value: token.value + 'Magic', offset: token.offset, type: 'magic' }, { value: token.value + 'Science', offset: token.offset, type: 'science' }, - { value: token.value + 'Neither', offset: token.offset }, + { value: token.value + 'Neither', offset: token.offset } ]; // Only choose the ones that have a non-empty type field, which are likely to be of interest. - const completionList = tokenList.filter((t) => t.type).map((t) => t.value); + const completionList = tokenList.filter(t => t.type).map(t => t.value); // Remove duplicate completions from the list const matches = Array.from(new Set(completionList)); + const items = new Array(); + matches.forEach(label => items.push({ label })); + return { start: token.offset, end: token.offset + token.value.length, - matches, - metadata: {}, + items }; } } diff --git a/completer/src/index.ts b/completer/src/index.ts index b53c0a63..bb399d19 100644 --- a/completer/src/index.ts +++ b/completer/src/index.ts @@ -1,32 +1,11 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; +import { ICompletionProviderManager } from '@jupyterlab/completer'; +import { INotebookTracker } from '@jupyterlab/notebook'; -import { - ContextConnector, - ICompletionManager, - KernelConnector, -} from '@jupyterlab/completer'; - -import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook'; - -import { CompletionConnector } from './connector'; - -import { CustomConnector } from './customconnector'; - -/** - * The command IDs used by the console plugin. - */ -namespace CommandIDs { - export const invoke = 'completer:invoke'; - - export const invokeNotebook = 'completer:invoke-notebook'; - - export const select = 'completer:select'; - - export const selectNotebook = 'completer:select-notebook'; -} +import { CustomCompleterProvider } from './customconnector'; /** * Initialization data for the extension. @@ -34,77 +13,16 @@ namespace CommandIDs { const extension: JupyterFrontEndPlugin = { id: 'completer', autoStart: true, - requires: [ICompletionManager, INotebookTracker], + requires: [ICompletionProviderManager, INotebookTracker], activate: async ( app: JupyterFrontEnd, - completionManager: ICompletionManager, + completionManager: ICompletionProviderManager, notebooks: INotebookTracker ) => { - console.log('JupyterLab custom completer extension is activated!'); - - // Modelled after completer-extension's notebooks plugin - notebooks.widgetAdded.connect( - (sender: INotebookTracker, panel: NotebookPanel) => { - let editor = panel.content.activeCell?.editor ?? null; - const session = panel.sessionContext.session; - const options = { session, editor }; - const connector = new CompletionConnector([]); - const handler = completionManager.register({ - connector, - editor, - parent: panel, - }); - - const updateConnector = () => { - editor = panel.content.activeCell?.editor ?? null; - options.session = panel.sessionContext.session; - options.editor = editor; - handler.editor = editor; - - const kernel = new KernelConnector(options); - const context = new ContextConnector(options); - const custom = new CustomConnector(options); - handler.connector = new CompletionConnector([ - kernel, - context, - custom, - ]); - }; - - // Update the handler whenever the prompt or session changes - panel.content.activeCellChanged.connect(updateConnector); - panel.sessionContext.sessionChanged.connect(updateConnector); - } - ); + completionManager.registerProvider(new CustomCompleterProvider()); - // Add notebook completer command. - app.commands.addCommand(CommandIDs.invokeNotebook, { - execute: () => { - const panel = notebooks.currentWidget; - if (panel && panel.content.activeCell?.model.type === 'code') { - return app.commands.execute(CommandIDs.invoke, { id: panel.id }); - } - }, - }); - - // Add notebook completer select command. - app.commands.addCommand(CommandIDs.selectNotebook, { - execute: () => { - const id = notebooks.currentWidget && notebooks.currentWidget.id; - - if (id) { - return app.commands.execute(CommandIDs.select, { id }); - } - }, - }); - - // Set enter key for notebook completer select command. - app.commands.addKeyBinding({ - command: CommandIDs.selectNotebook, - keys: ['Enter'], - selector: '.jp-Notebook .jp-mod-completer-active', - }); - }, + console.log('JupyterLab custom completer extension is activated!'); + } }; export default extension; diff --git a/completer/style/base.css b/completer/style/base.css index e69de29b..e11f4577 100644 --- a/completer/style/base.css +++ b/completer/style/base.css @@ -0,0 +1,5 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ diff --git a/completer/style/index.css b/completer/style/index.css index d703e460..e98119b5 100644 --- a/completer/style/index.css +++ b/completer/style/index.css @@ -1,16 +1 @@ -.jp-example-view { - background-color: AliceBlue; -} - -.jp-example-button { - background-color: red; - border-radius: 12px; - border: none; - color: white; - padding: 15px 32px; - margin: 30px; - text-align: center; - text-decoration: none; - display: inline-block; - font-size: 16px; -} +@import 'base.css'; diff --git a/completer/tsconfig.json b/completer/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/completer/tsconfig.json +++ b/completer/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/completer/tsconfig.test.json b/completer/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/completer/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/completer/ui-tests/.env b/completer/ui-tests/.env deleted file mode 100644 index 342f6569..00000000 --- a/completer/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=completer/jupyterlab_examples_completer -EXT_NAME=completer \ No newline at end of file diff --git a/completer/ui-tests/README.md b/completer/ui-tests/README.md index d2be7291..a3bb6026 100644 --- a/completer/ui-tests/README.md +++ b/completer/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _completer_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _completer_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _completer_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _completer_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/completer/ui-tests/jupyter_server_test_config.py b/completer/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/completer/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/completer/ui-tests/package.json b/completer/ui-tests/package.json index 52055658..691dfd90 100644 --- a/completer/ui-tests/package.json +++ b/completer/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/completer-tests", - "version": "0.1.0", - "description": "Integration test for completer example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/completer-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/completer 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/completer/ui-tests/playwright.config.js b/completer/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/completer/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/completer/ui-tests/playwright.config.ts b/completer/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/completer/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/completer/ui-tests/tests/completer.spec.ts b/completer/ui-tests/tests/completer.spec.ts index 14b0ed70..90b1c285 100644 --- a/completer/ui-tests/tests/completer.spec.ts +++ b/completer/ui-tests/tests/completer.spec.ts @@ -1,21 +1,9 @@ import { test, expect } from '@jupyterlab/galata'; +import { ElementHandle } from '@playwright/test'; test('should open a notebook and use the completer', async ({ page }) => { - // Go to http://localhost:8888/lab? - - // Click text=File - await page.click('text=File'); - - // Click ul[role="menu"] >> text=New - await page.click('ul[role="menu"] >> text=New'); - - // Click #jp-mainmenu-file-new >> text=Notebook - await Promise.all([ - page.waitForNavigation(/*{ url: 'http://localhost:8888/lab/tree/Untitled.ipynb' }*/), - page.click('#jp-mainmenu-file-new >> text=Notebook'), - ]); - - // Click button:has-text("Select") + // Create a new Notebook + await page.menu.clickMenuItem('File>New>Notebook'); await page.click('button:has-text("Select")'); // Wait until kernel is ready @@ -23,13 +11,11 @@ test('should open a notebook and use the completer', async ({ page }) => { '#jp-main-statusbar >> text=Python 3 (ipykernel) | Idle' ); - // Click div[role="presentation"]:has-text("​") - await page.click('div[role="presentation"]:has-text("​")'); - - // Fill textarea - await page.fill('textarea', 'y'); + // Type 'y' in first cell + await page.notebook.enterCellEditingMode(0); + await page.keyboard.press('y'); - let suggestions = null; + let suggestions: ElementHandle | null = null; let counter = 20; while (suggestions === null && counter > 0) { // Press Tab @@ -38,9 +24,10 @@ test('should open a notebook and use the completer', async ({ page }) => { // Wait for completion pop-up try { suggestions = await page.waitForSelector('code:has-text("yMagic")', { - timeout: 1000, + timeout: 1000 }); } catch { + await page.keyboard.press('Backspace'); } finally { counter -= 1; } @@ -49,7 +36,7 @@ test('should open a notebook and use the completer', async ({ page }) => { // Click on suggestions await Promise.all([ page.waitForSelector('code:has-text("yMagic")', { state: 'hidden' }), - suggestions.click(), + suggestions!.click() ]); expect( diff --git a/contentheader/.eslintrc.js b/contentheader/.eslintrc.js index c2375786..665374bf 100644 --- a/contentheader/.eslintrc.js +++ b/contentheader/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/contentheader/.gitignore b/contentheader/.gitignore index 18cace13..ef3ace98 100644 --- a/contentheader/.gitignore +++ b/contentheader/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/contentheader/.prettierignore b/contentheader/.prettierignore new file mode 100644 index 00000000..8747eb69 --- /dev/null +++ b/contentheader/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_contentheader diff --git a/contentheader/.prettierrc b/contentheader/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/contentheader/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/contentheader/.stylelintrc b/contentheader/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/contentheader/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/contentheader/.yarnrc.yml b/contentheader/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/contentheader/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/contentheader/CHANGELOG.md b/contentheader/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/contentheader/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/contentheader/LICENSE b/contentheader/LICENSE new file mode 100644 index 00000000..54d885af --- /dev/null +++ b/contentheader/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/contentheader/MANIFEST.in b/contentheader/MANIFEST.in deleted file mode 100644 index fb4f2e0c..00000000 --- a/contentheader/MANIFEST.in +++ /dev/null @@ -1,24 +0,0 @@ -include LICENSE -include *.md -include pyproject.toml - -include package.json -include install.json -include ts*.json -include yarn.lock - -graft jupyterlab_examples_contentheader/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib -prune binder - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/contentheader/README.md b/contentheader/README.md index 7cfb64d9..facde7d3 100644 --- a/contentheader/README.md +++ b/contentheader/README.md @@ -8,28 +8,29 @@ This JupyterLab example extension is intended to demo one specific feature of `M > As background, `MainAreaWidget` is a high-level JupyterLab widget that conventionally is used to enclose the Launcher (shown above) or a notebook editor. The `contentHeader`, in turn, is a Lumino `BoxPanel` widget positioned at the very top of this main area. This makes the `contentHeader` potentially useful to extensions needing some place to put content that the user will _always see_. -In code: after you get a `MainAreaWidget`, for example via +In code: the command `jlab-examples:contentheader` creates a widget and check if it is an instance of `MainAreaWidget`. A specific text content is then added to this widget (here the current GMT time). ```ts -// src/index.ts#L37-L37 - -const main = app.shell.currentWidget; -``` - -you can then create a widget of interest, for example as - -```ts -// src/index.ts#L40-L40 - -const widget = new Widget(); -``` - -before finally adding it to the JupyterLab main area's `contentHeader` real estate at its very top: - -```ts -// src/index.ts#L45-L45 - -main.contentHeader.addWidget(widget); +// src/index.ts#L34-L51 + +commands.addCommand(command, { + label: 'Populate content header (time example)', + caption: 'Populate content header (time example)', + execute: (args: any) => { + // Check to ensure this is a MainAreaWidget + const widget = app.shell.currentWidget; + + if (widget instanceof MainAreaWidget) { + widget.addClass('example-extension-contentheader-widget'); + widget.node.textContent = generateContent(); + + // Every so often, update the widget's contents + setInterval(() => { + widget.node.textContent = generateContent(); + }, 1000); + } + } +}); ``` ## Install diff --git a/contentheader/RELEASE.md b/contentheader/RELEASE.md new file mode 100644 index 00000000..74ffa551 --- /dev/null +++ b/contentheader/RELEASE.md @@ -0,0 +1,65 @@ +# Making a new release of jupyterlab_examples_contentheader + +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` + +## Automated releases with the Jupyter Releaser + +The extension repository should already be compatible with the Jupyter Releaser. + +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. + +Here is a summary of the steps to cut a new release: + +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository +- Go to the Actions panel +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow + +## Publishing to `conda-forge` + +If the package is not on conda forge yet, check the documentation to learn how to add it: https://conda-forge.org/docs/maintainer/adding_pkgs.html + +Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically. diff --git a/contentheader/babel.config.js b/contentheader/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/contentheader/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/contentheader/jest.config.js b/contentheader/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/contentheader/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/contentheader/jupyterlab_examples_contentheader/__init__.py b/contentheader/jupyterlab_examples_contentheader/__init__.py index 63040033..31b59620 100644 --- a/contentheader/jupyterlab_examples_contentheader/__init__.py +++ b/contentheader/jupyterlab_examples_contentheader/__init__.py @@ -1,17 +1,9 @@ - -import json -from pathlib import Path - from ._version import __version__ -HERE = Path(__file__).parent.resolve() - -with (HERE / "labextension" / "package.json").open() as fid: - data = json.load(fid) def _jupyter_labextension_paths(): return [{ "src": "labextension", - "dest": data["name"] + "dest": "@jupyterlab-examples/contentheader" }] diff --git a/contentheader/jupyterlab_examples_contentheader/_version.py b/contentheader/jupyterlab_examples_contentheader/_version.py index 89190b47..133868ac 100644 --- a/contentheader/jupyterlab_examples_contentheader/_version.py +++ b/contentheader/jupyterlab_examples_contentheader/_version.py @@ -1,23 +1,4 @@ -import json -from pathlib import Path - -__all__ = ["__version__"] - -def _fetchVersion(): - HERE = Path(__file__).parent.resolve() - - for settings in HERE.rglob("package.json"): - try: - with settings.open() as f: - version = json.load(f)["version"] - return ( - version.replace("-alpha.", "a") - .replace("-beta.", "b") - .replace("-rc.", "rc") - ) - except FileNotFoundError: - pass - - raise FileNotFoundError(f"Could not find package.json under dir {HERE!s}") - -__version__ = _fetchVersion() +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/contentheader/package.json b/contentheader/package.json index 196a2d18..e09261a2 100644 --- a/contentheader/package.json +++ b/contentheader/package.json @@ -14,17 +14,12 @@ "license": "BSD-3-Clause", "author": { "name": "Project Jupyter Contributors", - "email": "" + "email": "me@test.com" }, - "contributors": [ - { - "name": "Ahmed Fasih", - "email": "afasih@bloomberg.net" - } - ], "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}" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", + "schema/*.json" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -33,39 +28,65 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "clean": "jlpm run clean:lib", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "clean:labextension": "rimraf jupyterlab_examples_contentheader/labextension", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_contentheader/labextension jupyterlab_examples_contentheader/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", "watch:src": "tsc -w", "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0" + "@jupyterlab/application": "^4.0.0-beta.0", + "@jupyterlab/settingregistry": "^4.0.0-beta.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", @@ -78,13 +99,5 @@ "jupyterlab": { "extension": true, "outputDir": "jupyterlab_examples_contentheader/labextension" - }, - "jupyter-releaser": { - "hooks": { - "before-build-npm": [ - "python -m pip install jupyterlab~=3.1", - "jlpm" - ] - } } } diff --git a/contentheader/preview.gif b/contentheader/preview.gif index 922f97eb..25abf0b1 100644 Binary files a/contentheader/preview.gif and b/contentheader/preview.gif differ diff --git a/contentheader/pyproject.toml b/contentheader/pyproject.toml index 686e39d9..2e358340 100644 --- a/contentheader/pyproject.toml +++ b/contentheader/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_contentheader/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_contentheader/labextension/static/style.js", "jupyterlab_examples_contentheader/labextension/package.json"] +[project] +name = "jupyterlab_examples_contentheader" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_contentheader/labextension"] +exclude = [".github", "binder"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_contentheader/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/contentheader" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/contentheader/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_contentheader/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_contentheader/labextension/static/style.js", + "jupyterlab_examples_contentheader/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_contentheader/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_contentheader/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_contentheader/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/contentheader/setup.py b/contentheader/setup.py index af64a3f0..bea23374 100644 --- a/contentheader/setup.py +++ b/contentheader/setup.py @@ -1,95 +1 @@ -""" -jupyterlab_examples_contentheader setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_contentheader" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/contentheader" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) -version = ( - pkg_json["version"] - .replace("-alpha.", "a") - .replace("-beta.", "b") - .replace("-rc.", "rc") -) - -setup_args = dict( - name=name, - version=version, - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - license_file="LICENSE", - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Framework :: Jupyter", - "Framework :: Jupyter :: JupyterLab", - "Framework :: Jupyter :: JupyterLab :: 3", - "Framework :: Jupyter :: JupyterLab :: Extensions", - "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/contentheader/src/index.ts b/contentheader/src/index.ts index 8b575003..f225e915 100644 --- a/contentheader/src/index.ts +++ b/contentheader/src/index.ts @@ -1,9 +1,9 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; import { ICommandPalette } from '@jupyterlab/apputils'; -import { Widget } from '@lumino/widgets'; +/*import { Widget } from '@lumino/widgets';*/ import { MainAreaWidget } from '@jupyterlab/apputils'; /** @@ -12,6 +12,7 @@ import { MainAreaWidget } from '@jupyterlab/apputils'; * @returns string */ function generateContent(): string { + /*console.log('Time in GMT is: ' + new Date().toUTCString())*/ return 'Time in GMT is: ' + new Date().toUTCString(); } @@ -24,39 +25,36 @@ const plugin: JupyterFrontEndPlugin = { optional: [ICommandPalette], activate: (app: JupyterFrontEnd, pal?: ICommandPalette) => { console.log('JupyterLab extension contentheader is activated!'); - const { commands } = app; const command = 'jlab-examples:contentheader'; + const { commands } = app; // Create the command, which can be easily invoked by // 1- opening the Command Palette, and // 2- running "Populate content header...". + commands.addCommand(command, { label: 'Populate content header (time example)', caption: 'Populate content header (time example)', execute: (args: any) => { // Check to ensure this is a MainAreaWidget - const main = app.shell.currentWidget; - if (main instanceof MainAreaWidget) { - // Create a widget - const widget = new Widget(); + const widget = app.shell.currentWidget; + + if (widget instanceof MainAreaWidget) { widget.addClass('example-extension-contentheader-widget'); widget.node.textContent = generateContent(); - // and insert it into the header - main.contentHeader.addWidget(widget); - // Every so often, update the widget's contents setInterval(() => { widget.node.textContent = generateContent(); }, 1000); } - }, + } }); // Create a command palette entry for easy access const category = 'Extension Examples'; if (pal) { pal.addItem({ command, category, args: { origin: 'from palette' } }); } - }, + } }; export default plugin; diff --git a/contentheader/style/base.css b/contentheader/style/base.css index 9b9443b7..e11f4577 100644 --- a/contentheader/style/base.css +++ b/contentheader/style/base.css @@ -1,6 +1,5 @@ -.example-extension-contentheader-widget { - min-height: 1rem; - background-color: white; - color: black; - padding-left: 0.5rem; -} +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ diff --git a/contentheader/style/index.css b/contentheader/style/index.css index 8a7ea29e..e98119b5 100644 --- a/contentheader/style/index.css +++ b/contentheader/style/index.css @@ -1 +1 @@ -@import url('base.css'); +@import 'base.css'; diff --git a/contentheader/tsconfig.json b/contentheader/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/contentheader/tsconfig.json +++ b/contentheader/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/contentheader/tsconfig.test.json b/contentheader/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/contentheader/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/contentheader/ui-tests/.env b/contentheader/ui-tests/.env deleted file mode 100644 index bb0ac972..00000000 --- a/contentheader/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=contentheader/jupyterlab_examples_contentheader -EXT_NAME=contentheader \ No newline at end of file diff --git a/contentheader/ui-tests/README.md b/contentheader/ui-tests/README.md index d2be7291..f93fe129 100644 --- a/contentheader/ui-tests/README.md +++ b/contentheader/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _contentheader_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _contentheader_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _contentheader_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _contentheader_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/contentheader/ui-tests/jupyter_server_test_config.py b/contentheader/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/contentheader/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/contentheader/ui-tests/package.json b/contentheader/ui-tests/package.json index 49479139..2847fe74 100644 --- a/contentheader/ui-tests/package.json +++ b/contentheader/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/contentheader-tests", - "version": "0.1.0", - "description": "Integration test for widgets example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/contentheader-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/contentheader 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } -} \ No newline at end of file +} diff --git a/contentheader/ui-tests/playwright.config.js b/contentheader/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/contentheader/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/contentheader/ui-tests/playwright.config.ts b/contentheader/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/contentheader/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/contentheader/ui-tests/tests/playwright.config.js b/contentheader/ui-tests/tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/contentheader/ui-tests/tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/context-menu/.eslintrc.js b/context-menu/.eslintrc.js index c2375786..665374bf 100644 --- a/context-menu/.eslintrc.js +++ b/context-menu/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/context-menu/.gitignore b/context-menu/.gitignore index 18cace13..ef3ace98 100644 --- a/context-menu/.gitignore +++ b/context-menu/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/context-menu/.prettierignore b/context-menu/.prettierignore new file mode 100644 index 00000000..c99b4b20 --- /dev/null +++ b/context-menu/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyter_examples_context_menu diff --git a/context-menu/.prettierrc b/context-menu/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/context-menu/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/context-menu/.stylelintrc b/context-menu/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/context-menu/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/context-menu/.yarnrc.yml b/context-menu/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/context-menu/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/context-menu/CHANGELOG.md b/context-menu/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/context-menu/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/context-menu/LICENSE b/context-menu/LICENSE new file mode 100644 index 00000000..54d885af --- /dev/null +++ b/context-menu/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/context-menu/MANIFEST.in b/context-menu/MANIFEST.in deleted file mode 100644 index 84b5924d..00000000 --- a/context-menu/MANIFEST.in +++ /dev/null @@ -1,22 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml -include jupyter-config/jupyterlab_examples_context_menu.json - -include package.json -include ts*.json - -graft jupyterlab_examples_context_menu/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/context-menu/README.md b/context-menu/README.md index a45beb14..bdb42c77 100644 --- a/context-menu/README.md +++ b/context-menu/README.md @@ -21,7 +21,7 @@ First of all, you will start looking into the declaration of the extension: // src/index.ts#L9-L13 const extension: JupyterFrontEndPlugin = { - id: 'context-menu', + id: '@jupyterlab-examples/context-menu:plugin', autoStart: true, requires: [IFileBrowserFactory], activate: (app: JupyterFrontEnd, factory: IFileBrowserFactory) => { @@ -34,21 +34,25 @@ The first step is to define the command that will be executed when clicking on t ```ts -// src/index.ts#L14-L27 +// src/index.ts#L14-L31 app.commands.addCommand('jlab-examples/context-menu:open', { label: 'Example', caption: "Example context menu button for file browser's items.", icon: buildIcon, execute: () => { - const file = factory.tracker.currentWidget.selectedItems().next(); - - showDialog({ - title: file.name, - body: 'Path: ' + file.path, - buttons: [Dialog.okButton()], - }).catch((e) => console.log(e)); - }, + const file = factory.tracker.currentWidget + ?.selectedItems() + .next().value; + + if (file) { + showDialog({ + title: file.name, + body: 'Path: ' + file.path, + buttons: [Dialog.okButton()] + }).catch(e => console.log(e)); + } + } }); ``` diff --git a/context-menu/RELEASE.md b/context-menu/RELEASE.md index 06f37a4d..89de9e81 100644 --- a/context-menu/RELEASE.md +++ b/context-menu/RELEASE.md @@ -1,22 +1,62 @@ -# Making a new release of jupyterlab_examples_context_menu +# Making a new release of jupyter_examples_context_menu -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser The extension repository should already be compatible with the Jupyter Releaser. -Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. Here is a summary of the steps to cut a new release: -- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) -- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository - Go to the Actions panel -- Run the "Draft Changelog" workflow -- Merge the Changelog PR -- Run the "Draft Release" workflow -- Run the "Publish Release" workflow +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow ## Publishing to `conda-forge` diff --git a/context-menu/babel.config.js b/context-menu/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/context-menu/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/context-menu/install.json b/context-menu/install.json index db8ba8e6..12769a50 100644 --- a/context-menu/install.json +++ b/context-menu/install.json @@ -1,5 +1,5 @@ { "packageManager": "python", - "packageName": "jupyterlab_examples_context_menu", - "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlab_examples_context_menu" + "packageName": "jupyter_examples_context_menu", + "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyter_examples_context_menu" } diff --git a/context-menu/jest.config.js b/context-menu/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/context-menu/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/context-menu/jupyterlab_examples_context_menu/__init__.py b/context-menu/jupyterlab_examples_context_menu/__init__.py index 353ab522..a3099b5a 100644 --- a/context-menu/jupyterlab_examples_context_menu/__init__.py +++ b/context-menu/jupyterlab_examples_context_menu/__init__.py @@ -1,19 +1,9 @@ - -import json -import os.path as osp - from ._version import __version__ -HERE = osp.abspath(osp.dirname(__file__)) - -with open(osp.join(HERE, 'labextension', 'package.json')) as fid: - data = json.load(fid) def _jupyter_labextension_paths(): return [{ - 'src': 'labextension', - 'dest': data['name'] + "src": "labextension", + "dest": "@jupyterlab-examples/context-menu" }] - - diff --git a/context-menu/jupyterlab_examples_context_menu/_version.py b/context-menu/jupyterlab_examples_context_menu/_version.py index ee864fc9..133868ac 100644 --- a/context-menu/jupyterlab_examples_context_menu/_version.py +++ b/context-menu/jupyterlab_examples_context_menu/_version.py @@ -1,2 +1,4 @@ -version_info = (0, 1, 0) -__version__ = ".".join(map(str, version_info)) +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/context-menu/package.json b/context-menu/package.json index f2c84d4b..ff6622e9 100644 --- a/context-menu/package.json +++ b/context-menu/package.json @@ -18,8 +18,8 @@ }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "style/**/*.{css,eot,js,gif,html,jpg,json,png,svg,woff2,ttf}", - "schema/**/*.json" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", + "schema/*.json" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -28,53 +28,77 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jupyterlab_examples_context_menu/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyter_examples_context_menu/labextension jupyter_examples_context_menu/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0", - "@jupyterlab/apputils": "^3.1.0", - "@jupyterlab/filebrowser": "^3.1.0", - "@jupyterlab/ui-components": "^3.1.0" + "@jupyterlab/application": "^4.0.0-beta.0", + "@jupyterlab/settingregistry": "^4.0.0-beta.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "extension": true, - "outputDir": "jupyterlab_examples_context_menu/labextension", + "outputDir": "jupyter_examples_context_menu/labextension", "schemaDir": "schema" - }, - "styleModule": "style/index.js" + } } diff --git a/context-menu/preview.gif b/context-menu/preview.gif deleted file mode 100644 index 03c353f9..00000000 Binary files a/context-menu/preview.gif and /dev/null differ diff --git a/context-menu/pyproject.toml b/context-menu/pyproject.toml index 57220902..d2889690 100644 --- a/context-menu/pyproject.toml +++ b/context-menu/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_context_menu/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_context_menu/labextension/static/style.js", "jupyterlab_examples_context_menu/labextension/package.json"] +[project] +name = "jupyter_examples_context_menu" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.version] +source = "nodejs" -[tool.jupyter-packaging.build-args] +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] + +[tool.hatch.build.targets.sdist] +artifacts = ["jupyter_examples_context_menu/labextension"] +exclude = [".github", "binder"] + +[tool.hatch.build.targets.wheel.shared-data] +"jupyter_examples_context_menu/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/context-menu" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/context-menu/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyter_examples_context_menu/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyter_examples_context_menu/labextension/static/style.js", + "jupyter_examples_context_menu/labextension/package.json", +] +skip-if-exists = ["jupyter_examples_context_menu/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_context_menu/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyter_examples_context_menu/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/context-menu/setup.py b/context-menu/setup.py index c1f5e879..bea23374 100644 --- a/context-menu/setup.py +++ b/context-menu/setup.py @@ -1,83 +1 @@ -""" -jupyterlab_examples_context_menu setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_context_menu" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/context-menu" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/context-menu/src/index.ts b/context-menu/src/index.ts index 730ff295..0c6b4918 100644 --- a/context-menu/src/index.ts +++ b/context-menu/src/index.ts @@ -1,13 +1,13 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; import { IFileBrowserFactory } from '@jupyterlab/filebrowser'; import { showDialog, Dialog } from '@jupyterlab/apputils'; import { buildIcon } from '@jupyterlab/ui-components'; const extension: JupyterFrontEndPlugin = { - id: 'context-menu', + id: '@jupyterlab-examples/context-menu:plugin', autoStart: true, requires: [IFileBrowserFactory], activate: (app: JupyterFrontEnd, factory: IFileBrowserFactory) => { @@ -16,16 +16,20 @@ const extension: JupyterFrontEndPlugin = { caption: "Example context menu button for file browser's items.", icon: buildIcon, execute: () => { - const file = factory.tracker.currentWidget.selectedItems().next(); + const file = factory.tracker.currentWidget + ?.selectedItems() + .next().value; - showDialog({ - title: file.name, - body: 'Path: ' + file.path, - buttons: [Dialog.okButton()], - }).catch((e) => console.log(e)); - }, + if (file) { + showDialog({ + title: file.name, + body: 'Path: ' + file.path, + buttons: [Dialog.okButton()] + }).catch(e => console.log(e)); + } + } }); - }, + } }; export default extension; diff --git a/context-menu/style/base.css b/context-menu/style/base.css index e69de29b..e11f4577 100644 --- a/context-menu/style/base.css +++ b/context-menu/style/base.css @@ -0,0 +1,5 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ diff --git a/context-menu/style/index.css b/context-menu/style/index.css index e69de29b..e98119b5 100644 --- a/context-menu/style/index.css +++ b/context-menu/style/index.css @@ -0,0 +1 @@ +@import 'base.css'; diff --git a/context-menu/tsconfig.json b/context-menu/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/context-menu/tsconfig.json +++ b/context-menu/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/context-menu/tsconfig.test.json b/context-menu/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/context-menu/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/context-menu/ui-tests/.env b/context-menu/ui-tests/.env deleted file mode 100644 index 3277c390..00000000 --- a/context-menu/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=context-menu/jupyterlab_examples_context_menu -EXT_NAME=context-menu \ No newline at end of file diff --git a/context-menu/ui-tests/README.md b/context-menu/ui-tests/README.md index d2be7291..4eba0eb6 100644 --- a/context-menu/ui-tests/README.md +++ b/context-menu/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _context-menu_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _context-menu_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _context-menu_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _context-menu_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/context-menu/ui-tests/jupyter_server_test_config.py b/context-menu/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/context-menu/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/context-menu/ui-tests/package.json b/context-menu/ui-tests/package.json index 81342c2d..5adb1e74 100644 --- a/context-menu/ui-tests/package.json +++ b/context-menu/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/context-menu-tests", - "version": "0.1.0", - "description": "Integration test for context-menu example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/context-menu-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/context-menu 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/context-menu/ui-tests/playwright.config.js b/context-menu/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/context-menu/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/context-menu/ui-tests/playwright.config.ts b/context-menu/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/context-menu/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/context-menu/ui-tests/tests/context-menu.spec.ts b/context-menu/ui-tests/tests/context-menu.spec.ts index 266d23b1..3a29e122 100644 --- a/context-menu/ui-tests/tests/context-menu.spec.ts +++ b/context-menu/ui-tests/tests/context-menu.spec.ts @@ -9,7 +9,7 @@ test('should have new context menu for example files', async ({ page }) => { .locator('[aria-label="File Browser Section"]') .getByText('untitled.txt') .click({ - button: 'right', + button: 'right' }); await page.getByRole('menuitem', { name: 'Rename' }).click(); @@ -27,14 +27,14 @@ test('should have new context menu for example files', async ({ page }) => { .locator('[aria-label="File Browser Section"]') .getByText('test.example') .click({ - button: 'right', + button: 'right' }); await page.getByRole('menuitem', { name: 'Example' }).click(); - await expect( - page.getByText(/^Path: ([\w-]+\/)?test\.example$/) - ).toHaveCount(1); + await expect(page.getByText(/^Path: ([\w-]+\/)?test\.example$/)).toHaveCount( + 1 + ); await page.getByRole('button', { name: /ok/i }).click(); }); diff --git a/custom-log-console/.eslintrc.js b/custom-log-console/.eslintrc.js index c2375786..665374bf 100644 --- a/custom-log-console/.eslintrc.js +++ b/custom-log-console/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/custom-log-console/.gitignore b/custom-log-console/.gitignore index 18cace13..ef3ace98 100644 --- a/custom-log-console/.gitignore +++ b/custom-log-console/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/custom-log-console/.prettierignore b/custom-log-console/.prettierignore new file mode 100644 index 00000000..eafa4f91 --- /dev/null +++ b/custom-log-console/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_custom_log_console diff --git a/custom-log-console/.prettierrc b/custom-log-console/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/custom-log-console/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/custom-log-console/.stylelintrc b/custom-log-console/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/custom-log-console/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/custom-log-console/.yarnrc.yml b/custom-log-console/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/custom-log-console/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/custom-log-console/CHANGELOG.md b/custom-log-console/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/custom-log-console/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/custom-log-console/LICENSE b/custom-log-console/LICENSE new file mode 100644 index 00000000..54d885af --- /dev/null +++ b/custom-log-console/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/custom-log-console/MANIFEST.in b/custom-log-console/MANIFEST.in deleted file mode 100644 index 2133e4ee..00000000 --- a/custom-log-console/MANIFEST.in +++ /dev/null @@ -1,22 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml -include jupyter-config/jupyterlab_examples_custom_log_console.json - -include package.json -include ts*.json - -graft jupyterlab_examples_custom_log_console/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/custom-log-console/README.md b/custom-log-console/README.md index 88f0a866..ad0e3c28 100644 --- a/custom-log-console/README.md +++ b/custom-log-console/README.md @@ -28,7 +28,7 @@ First of all, you will start by looking into the declaration of the extension: // src/index.ts#L26-L35 const extension: JupyterFrontEndPlugin = { - id: 'custom-log-console', + id: '@jupyterlab-examples/custom-log-console:plugin', autoStart: true, requires: [ICommandPalette, IRenderMimeRegistry, ILayoutRestorer], activate: ( @@ -48,8 +48,8 @@ In the `activate` function, the first step is to declare `logConsolePanel` and ` ```ts // src/index.ts#L38-L39 -let logConsolePanel: LogConsolePanel = null; -let logConsoleWidget: MainAreaWidget = null; +let logConsolePanel: LogConsolePanel | null = null; +let logConsoleWidget: MainAreaWidget | null = null; ``` @@ -72,7 +72,7 @@ To initialize a new `LogConsoleWidget` you have to create a `LogConsolePanel` to logConsolePanel = new LogConsolePanel( new LoggerRegistry({ defaultRendermime: rendermime, - maxLength: 1000, + maxLength: 1000 }) ); ``` @@ -95,7 +95,7 @@ Now you are ready to initialize a new `MainAreaWidget` passing the `logConsolePa // src/index.ts#L82-L84 logConsoleWidget = new MainAreaWidget({ - content: logConsolePanel, + content: logConsolePanel }); ``` @@ -131,7 +131,7 @@ commands.addCommand('jlab-examples/custom-log-console:open', { } else { createLogConsoleWidget(); } - }, + } }); ``` @@ -145,7 +145,7 @@ Finally, you can send log messages calling `log` method present on the `logger` const msg: IHtmlLog = { type: 'html', level: 'debug', - data: '
Hello world HTML!!
', + data: '
Hello world HTML!!
' }; logConsolePanel?.logger?.log(msg); @@ -161,7 +161,7 @@ logConsolePanel?.logger?.log(msg); const msg: ITextLog = { type: 'text', level: 'info', - data: 'Hello world text!!', + data: 'Hello world text!!' }; logConsolePanel?.logger?.log(msg); @@ -178,14 +178,14 @@ logConsolePanel?.logger?.log(msg); const data: nbformat.IOutput = { output_type: 'display_data', data: { - 'text/plain': 'Hello world nbformat!!', - }, + 'text/plain': 'Hello world nbformat!!' + } }; const msg: IOutputLog = { type: 'output', level: 'warning', - data, + data }; logConsolePanel?.logger?.log(msg); diff --git a/custom-log-console/RELEASE.md b/custom-log-console/RELEASE.md index 150aa1f9..d4d40410 100644 --- a/custom-log-console/RELEASE.md +++ b/custom-log-console/RELEASE.md @@ -1,22 +1,62 @@ # Making a new release of jupyterlab_examples_custom_log_console -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser The extension repository should already be compatible with the Jupyter Releaser. -Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. Here is a summary of the steps to cut a new release: -- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) -- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository - Go to the Actions panel -- Run the "Draft Changelog" workflow -- Merge the Changelog PR -- Run the "Draft Release" workflow -- Run the "Publish Release" workflow +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow ## Publishing to `conda-forge` diff --git a/custom-log-console/babel.config.js b/custom-log-console/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/custom-log-console/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/custom-log-console/jest.config.js b/custom-log-console/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/custom-log-console/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/custom-log-console/jupyterlab_examples_custom_log_console/__init__.py b/custom-log-console/jupyterlab_examples_custom_log_console/__init__.py index 353ab522..c9e3c7b7 100644 --- a/custom-log-console/jupyterlab_examples_custom_log_console/__init__.py +++ b/custom-log-console/jupyterlab_examples_custom_log_console/__init__.py @@ -1,19 +1,9 @@ - -import json -import os.path as osp - from ._version import __version__ -HERE = osp.abspath(osp.dirname(__file__)) - -with open(osp.join(HERE, 'labextension', 'package.json')) as fid: - data = json.load(fid) def _jupyter_labextension_paths(): return [{ - 'src': 'labextension', - 'dest': data['name'] + "src": "labextension", + "dest": "@jupyterlab-examples/custom-log-console" }] - - diff --git a/custom-log-console/jupyterlab_examples_custom_log_console/_version.py b/custom-log-console/jupyterlab_examples_custom_log_console/_version.py index ee864fc9..133868ac 100644 --- a/custom-log-console/jupyterlab_examples_custom_log_console/_version.py +++ b/custom-log-console/jupyterlab_examples_custom_log_console/_version.py @@ -1,2 +1,4 @@ -version_info = (0, 1, 0) -__version__ = ".".join(map(str, version_info)) +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/custom-log-console/package.json b/custom-log-console/package.json index 47261ebf..8530cd42 100644 --- a/custom-log-console/package.json +++ b/custom-log-console/package.json @@ -14,12 +14,12 @@ "license": "BSD-3-Clause", "author": { "name": "Project Jupyter Contributors", - "email": "" + "email": "me@test.com" }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "style/**/*.{css,eot,js,gif,html,jpg,json,png,svg,woff2,ttf}", - "schema/**/*.json" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", + "schema/*.json" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -28,58 +28,85 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jupyterlab_examples_custom_log_console/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_custom_log_console/labextension jupyterlab_examples_custom_log_console/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0", - "@jupyterlab/apputils": "^3.1.0", - "@jupyterlab/coreutils": "^5.1.0", - "@jupyterlab/logconsole": "^3.1.0", - "@jupyterlab/nbformat": "^3.1.0", - "@jupyterlab/rendermime": "^3.1.0", - "@jupyterlab/ui-components": "^3.1.0", - "@lumino/coreutils": "^1.5.3", - "@lumino/widgets": "^1.19.0" + "@jupyterlab/application": "^4.0.0-beta.0", + "@jupyterlab/apputils": "^4.0.0-beta.0", + "@jupyterlab/coreutils": "^6.0.0-beta.0", + "@jupyterlab/logconsole": "^4.0.0-beta.0", + "@jupyterlab/nbformat": "^4.0.0-beta.0", + "@jupyterlab/rendermime": "^4.0.0-beta.0", + "@jupyterlab/settingregistry": "^4.0.0-beta.0", + "@jupyterlab/ui-components": "^4.0.0-beta.0", + "@lumino/coreutils": "^2.0.0", + "@lumino/widgets": "^2.0.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "extension": true, "outputDir": "jupyterlab_examples_custom_log_console/labextension", "schemaDir": "schema" - }, - "styleModule": "style/index.js" + } } diff --git a/custom-log-console/preview.gif b/custom-log-console/preview.gif index b542c0c8..ea1acdf1 100644 Binary files a/custom-log-console/preview.gif and b/custom-log-console/preview.gif differ diff --git a/custom-log-console/pyproject.toml b/custom-log-console/pyproject.toml index bcf6ba4d..39125c32 100644 --- a/custom-log-console/pyproject.toml +++ b/custom-log-console/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_custom_log_console/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_custom_log_console/labextension/static/style.js", "jupyterlab_examples_custom_log_console/labextension/package.json"] +[project] +name = "jupyterlab_examples_custom_log_console" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_custom_log_console/labextension"] +exclude = [".github", "binder"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_custom_log_console/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/custom-log-console" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/custom-log-console/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_custom_log_console/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_custom_log_console/labextension/static/style.js", + "jupyterlab_examples_custom_log_console/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_custom_log_console/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_custom_log_console/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_custom_log_console/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/custom-log-console/schema/plugin.json b/custom-log-console/schema/plugin.json index edad10e2..e18837a6 100644 --- a/custom-log-console/schema/plugin.json +++ b/custom-log-console/schema/plugin.json @@ -5,7 +5,7 @@ "main": [ { "id": "jp-mainmenu-example-log-console", - "label": "Log Console Example", + "label": "Examples", "rank": 80, "items": [ { diff --git a/custom-log-console/setup.py b/custom-log-console/setup.py index 75b3cb58..bea23374 100644 --- a/custom-log-console/setup.py +++ b/custom-log-console/setup.py @@ -1,83 +1 @@ -""" -jupyterlab_examples_custom_log_console setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_custom_log_console" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/custom-log-console" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/custom-log-console/src/index.ts b/custom-log-console/src/index.ts index 133b9d56..8700ed22 100644 --- a/custom-log-console/src/index.ts +++ b/custom-log-console/src/index.ts @@ -1,20 +1,20 @@ import { JupyterFrontEnd, JupyterFrontEndPlugin, - ILayoutRestorer, + ILayoutRestorer } from '@jupyterlab/application'; import { ICommandPalette, MainAreaWidget, WidgetTracker, - CommandToolbarButton, + CommandToolbarButton } from '@jupyterlab/apputils'; import { LoggerRegistry, LogConsolePanel, IHtmlLog, ITextLog, - IOutputLog, + IOutputLog } from '@jupyterlab/logconsole'; import { addIcon, clearIcon, listIcon } from '@jupyterlab/ui-components'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; @@ -24,7 +24,7 @@ import * as nbformat from '@jupyterlab/nbformat'; import LogLevelSwitcher from './logLevelSwitcher'; const extension: JupyterFrontEndPlugin = { - id: 'custom-log-console', + id: '@jupyterlab-examples/custom-log-console:plugin', autoStart: true, requires: [ICommandPalette, IRenderMimeRegistry, ILayoutRestorer], activate: ( @@ -35,29 +35,29 @@ const extension: JupyterFrontEndPlugin = { ) => { const { commands } = app; - let logConsolePanel: LogConsolePanel = null; - let logConsoleWidget: MainAreaWidget = null; + let logConsolePanel: LogConsolePanel | null = null; + let logConsoleWidget: MainAreaWidget | null = null; const tracker = new WidgetTracker>({ - namespace: 'example-custom-log-console', + namespace: 'example-custom-log-console' }); restorer.restore(tracker, { command: 'jlab-examples/custom-log-console:open', - name: () => 'example-custom-log-console', + name: () => 'example-custom-log-console' }); commands.addCommand('jlab-examples/custom-log-console:checkpoint', { execute: () => logConsolePanel?.logger?.checkpoint(), icon: addIcon, isEnabled: () => !!logConsolePanel && logConsolePanel.source !== null, - label: 'Add Checkpoint', + label: 'Add Checkpoint' }); commands.addCommand('jlab-examples/custom-log-console:clear', { execute: () => logConsolePanel?.logger?.clear(), icon: clearIcon, isEnabled: () => !!logConsolePanel && logConsolePanel.source !== null, - label: 'Clear Log', + label: 'Clear Log' }); commands.addCommand('jlab-examples/custom-log-console:level', { execute: (args: any) => { @@ -66,21 +66,21 @@ const extension: JupyterFrontEndPlugin = { } }, isEnabled: () => !!logConsolePanel && logConsolePanel.source !== null, - label: (args) => `Set Log Level to ${args.level as string}`, + label: args => `Set Log Level to ${args.level as string}` }); const createLogConsoleWidget = (): void => { logConsolePanel = new LogConsolePanel( new LoggerRegistry({ defaultRendermime: rendermime, - maxLength: 1000, + maxLength: 1000 }) ); logConsolePanel.source = 'custom-log-console'; logConsoleWidget = new MainAreaWidget({ - content: logConsolePanel, + content: logConsolePanel }); logConsoleWidget.addClass('jp-LogConsole'); logConsoleWidget.title.label = 'Custom Log console'; @@ -90,14 +90,14 @@ const extension: JupyterFrontEndPlugin = { 'checkpoint', new CommandToolbarButton({ commands: app.commands, - id: 'jlab-examples/custom-log-console:checkpoint', + id: 'jlab-examples/custom-log-console:checkpoint' }) ); logConsoleWidget.toolbar.addItem( 'clear', new CommandToolbarButton({ commands: app.commands, - id: 'jlab-examples/custom-log-console:clear', + id: 'jlab-examples/custom-log-console:clear' }) ); logConsoleWidget.toolbar.addItem( @@ -128,63 +128,63 @@ const extension: JupyterFrontEndPlugin = { } else { createLogConsoleWidget(); } - }, + } }); palette.addItem({ command: 'jlab-examples/custom-log-console:open', - category: 'Examples', + category: 'Examples' }); commands.addCommand('jlab-examples/custom-log-console:logHTMLMessage', { - label: 'HTML log message', + label: 'HTML Log Message', caption: 'Custom HTML log message example.', execute: () => { const msg: IHtmlLog = { type: 'html', level: 'debug', - data: '
Hello world HTML!!
', + data: '
Hello world HTML!!
' }; logConsolePanel?.logger?.log(msg); - }, + } }); commands.addCommand('jlab-examples/custom-log-console:logTextMessage', { - label: 'Text log message', + label: 'Text Log Message', caption: 'Custom text log message example.', execute: () => { const msg: ITextLog = { type: 'text', level: 'info', - data: 'Hello world text!!', + data: 'Hello world text!!' }; logConsolePanel?.logger?.log(msg); - }, + } }); commands.addCommand('jlab-examples/custom-log-console:logOutputMessage', { - label: 'Output log message', + label: 'Output Log Message', caption: 'Custom notebook output log message example.', execute: () => { const data: nbformat.IOutput = { output_type: 'display_data', data: { - 'text/plain': 'Hello world nbformat!!', - }, + 'text/plain': 'Hello world nbformat!!' + } }; const msg: IOutputLog = { type: 'output', level: 'warning', - data, + data }; logConsolePanel?.logger?.log(msg); - }, + } }); - }, + } }; export default extension; diff --git a/custom-log-console/src/logLevelSwitcher.tsx b/custom-log-console/src/logLevelSwitcher.tsx index 7549e9c9..b2fa9213 100644 --- a/custom-log-console/src/logLevelSwitcher.tsx +++ b/custom-log-console/src/logLevelSwitcher.tsx @@ -19,7 +19,7 @@ export default class LogLevelSwitcher extends ReactWidget { super(); this.addClass('jp-LogConsole-toolbarLogLevel'); this._logConsole = widget; - this._logConsole.logger.level = 'debug'; + this._logConsole.logger!.level = 'debug'; if (widget.source) { this.update(); } @@ -91,7 +91,7 @@ export default class LogLevelSwitcher extends ReactWidget { logger === null ? [] : ['Critical', 'Error', 'Warning', 'Info', 'Debug'].map( - (label) => ({ label, value: label.toLowerCase() }) + label => ({ label, value: label.toLowerCase() }) ) } /> diff --git a/custom-log-console/style/base.css b/custom-log-console/style/base.css index e69de29b..e11f4577 100644 --- a/custom-log-console/style/base.css +++ b/custom-log-console/style/base.css @@ -0,0 +1,5 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ diff --git a/custom-log-console/style/index.css b/custom-log-console/style/index.css index 8a7ea29e..e98119b5 100644 --- a/custom-log-console/style/index.css +++ b/custom-log-console/style/index.css @@ -1 +1 @@ -@import url('base.css'); +@import 'base.css'; diff --git a/custom-log-console/tsconfig.json b/custom-log-console/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/custom-log-console/tsconfig.json +++ b/custom-log-console/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/custom-log-console/tsconfig.test.json b/custom-log-console/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/custom-log-console/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/custom-log-console/ui-tests/.env b/custom-log-console/ui-tests/.env deleted file mode 100644 index 4b2e358f..00000000 --- a/custom-log-console/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=custom-log-console/jupyterlab_examples_custom_log_console -EXT_NAME=custom-log-console \ No newline at end of file diff --git a/custom-log-console/ui-tests/README.md b/custom-log-console/ui-tests/README.md index d2be7291..feb6a556 100644 --- a/custom-log-console/ui-tests/README.md +++ b/custom-log-console/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _custom-log-console_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _custom-log-console_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _custom-log-console_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _custom-log-console_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/custom-log-console/ui-tests/jupyter_server_test_config.py b/custom-log-console/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/custom-log-console/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/custom-log-console/ui-tests/package.json b/custom-log-console/ui-tests/package.json index 7effc463..d97d41a4 100644 --- a/custom-log-console/ui-tests/package.json +++ b/custom-log-console/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/custom-log-console-tests", - "version": "0.1.0", - "description": "Integration test for custom-log-console example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/custom-log-console-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/custom-log-console 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/custom-log-console/ui-tests/playwright.config.js b/custom-log-console/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/custom-log-console/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/custom-log-console/ui-tests/playwright.config.ts b/custom-log-console/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/custom-log-console/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/custom-log-console/ui-tests/tests/custom-log-console.spec.ts b/custom-log-console/ui-tests/tests/custom-log-console.spec.ts index 9af95ffa..974732db 100644 --- a/custom-log-console/ui-tests/tests/custom-log-console.spec.ts +++ b/custom-log-console/ui-tests/tests/custom-log-console.spec.ts @@ -1,13 +1,13 @@ import { test, expect } from '@jupyterlab/galata'; test('should open a log panel and filter message depending on the log level.', async ({ - page, + page }) => { - // Click text=Log Console Example - await page.click('text=Log Console Example'); + // Click text=Examples + await page.getByRole('menuitem', { name: 'Examples' }).click(); // Click ul[role="menu"] >> text=Custom Log Console - await page.click('ul[role="menu"] >> text=Custom Log Console'); + await page.getByRole('menuitem', { name: 'Custom Log Console' }).click(); // Click div[role="main"] >> text=Log: custom-log-console expect( @@ -16,24 +16,24 @@ test('should open a log panel and filter message depending on the log level.', a ) ).toBeTruthy(); - // Click text=Log Console Example menu - await page.click('text=Log Console Example'); + // Click text=Examples menu + await page.getByRole('menuitem', { name: 'Examples' }).click(); // Click text=HTML log message - await page.click('text=HTML log message'); + await page.getByText('HTML Log Message').click(); expect(await page.waitForSelector('text=Hello world HTML!!')).toBeTruthy(); - // Click text=Log Console Example - await page.click('text=Log Console Example'); + // Click text=Examples + await page.getByRole('menuitem', { name: 'Examples' }).click(); // Click text=Text log message - await page.click('text=Text log message'); + await page.getByText('Text Log Message').click(); expect(await page.waitForSelector('text=Hello world text!!')).toBeTruthy(); - // Click text=Log Console Example - await page.click('text=Log Console Example'); + // Click text=Examples + await page.getByRole('menuitem', { name: 'Examples' }).click(); // Click text=Output log message - await page.click('text=Output log message'); + await page.getByText('Output Log Message').click(); expect( await page.waitForSelector('text=Hello world nbformat!!') @@ -50,25 +50,25 @@ test('should open a log panel and filter message depending on the log level.', a // Select warning await page.selectOption('[aria-label="Log level"]', 'warning'); - // Click text=Log Console Example - await page.click('text=Log Console Example'); + // Click text=Examples + await page.getByRole('menuitem', { name: 'Examples' }).click(); // Click text=Output log message - await page.click('text=Output log message'); + await page.getByText('Output Log Message').click(); expect( await page.waitForSelector('text=Hello world nbformat!!') ).toBeTruthy(); - // Click text=Log Console Example - await page.click('text=Log Console Example'); + // Click text=Examples + await page.getByRole('menuitem', { name: 'Examples' }).click(); // Click text=HTML log message - await page.click('text=HTML log message'); + await page.getByText('HTML Log Message').click(); let failed = true; try { await page.waitForSelector('text=Hello world HTML!!', { state: 'attached', - timeout: 200, + timeout: 200 }); } catch (e) { failed = false; @@ -79,10 +79,10 @@ test('should open a log panel and filter message depending on the log level.', a // Select debug await page.selectOption('[aria-label="Log level"]', 'debug'); - // Click text=Log Console Example - await page.click('text=Log Console Example'); + // Click text=Examples + await page.getByRole('menuitem', { name: 'Examples' }).click(); // Click text=HTML log message - await page.click('text=HTML log message'); + await page.getByText('HTML Log Message').click(); expect(await page.waitForSelector('text=Hello world HTML!!')).toBeTruthy(); }); diff --git a/datagrid/.eslintrc.js b/datagrid/.eslintrc.js index c2375786..665374bf 100644 --- a/datagrid/.eslintrc.js +++ b/datagrid/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/datagrid/.gitignore b/datagrid/.gitignore index 18cace13..ef3ace98 100644 --- a/datagrid/.gitignore +++ b/datagrid/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/datagrid/.prettierignore b/datagrid/.prettierignore new file mode 100644 index 00000000..804ac358 --- /dev/null +++ b/datagrid/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_datagrid diff --git a/datagrid/.prettierrc b/datagrid/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/datagrid/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/datagrid/.stylelintrc b/datagrid/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/datagrid/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/datagrid/.yarnrc.yml b/datagrid/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/datagrid/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/datagrid/CHANGELOG.md b/datagrid/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/datagrid/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/datagrid/LICENSE b/datagrid/LICENSE new file mode 100644 index 00000000..54d885af --- /dev/null +++ b/datagrid/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/datagrid/MANIFEST.in b/datagrid/MANIFEST.in deleted file mode 100644 index a2f60e5c..00000000 --- a/datagrid/MANIFEST.in +++ /dev/null @@ -1,22 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml -include jupyter-config/jupyterlab_examples_datagrid.json - -include package.json -include ts*.json - -graft jupyterlab_examples_datagrid/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/datagrid/README.md b/datagrid/README.md index 61699ff7..9a6e2019 100644 --- a/datagrid/README.md +++ b/datagrid/README.md @@ -2,7 +2,7 @@ > Display a Datagrid as a Lumino Widget. -![Datagrid](preview.png) +![Datagrid](preview.gif) JupyterLab is built on top of [Lumino](https://github.com/jupyterlab/lumino). That library defines `Widget` as the primary interface brick. diff --git a/datagrid/RELEASE.md b/datagrid/RELEASE.md index 72b38089..6491d9c1 100644 --- a/datagrid/RELEASE.md +++ b/datagrid/RELEASE.md @@ -1,22 +1,62 @@ # Making a new release of jupyterlab_examples_datagrid -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser The extension repository should already be compatible with the Jupyter Releaser. -Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. Here is a summary of the steps to cut a new release: -- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) -- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository - Go to the Actions panel -- Run the "Draft Changelog" workflow -- Merge the Changelog PR -- Run the "Draft Release" workflow -- Run the "Publish Release" workflow +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow ## Publishing to `conda-forge` diff --git a/datagrid/babel.config.js b/datagrid/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/datagrid/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/datagrid/jest.config.js b/datagrid/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/datagrid/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/datagrid/jupyterlab_examples_datagrid/__init__.py b/datagrid/jupyterlab_examples_datagrid/__init__.py index 353ab522..364f5684 100644 --- a/datagrid/jupyterlab_examples_datagrid/__init__.py +++ b/datagrid/jupyterlab_examples_datagrid/__init__.py @@ -1,19 +1,9 @@ - -import json -import os.path as osp - from ._version import __version__ -HERE = osp.abspath(osp.dirname(__file__)) - -with open(osp.join(HERE, 'labextension', 'package.json')) as fid: - data = json.load(fid) def _jupyter_labextension_paths(): return [{ - 'src': 'labextension', - 'dest': data['name'] + "src": "labextension", + "dest": "@jupyterlab-examples/datagrid" }] - - diff --git a/datagrid/jupyterlab_examples_datagrid/_version.py b/datagrid/jupyterlab_examples_datagrid/_version.py index ee864fc9..133868ac 100644 --- a/datagrid/jupyterlab_examples_datagrid/_version.py +++ b/datagrid/jupyterlab_examples_datagrid/_version.py @@ -1,2 +1,4 @@ -version_info = (0, 1, 0) -__version__ = ".".join(map(str, version_info)) +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/datagrid/package.json b/datagrid/package.json index af998d93..cfc27f02 100644 --- a/datagrid/package.json +++ b/datagrid/package.json @@ -1,7 +1,7 @@ { "name": "@jupyterlab-examples/datagrid", "version": "0.1.0", - "description": "minimal lab example", + "description": "Minimal JupyterLab extension to display a datagrid as a Lumino widget.", "keywords": [ "jupyter", "jupyterlab", @@ -18,8 +18,8 @@ }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "style/**/*.{css,eot,js,gif,html,jpg,json,png,svg,woff2,ttf}", - "schema/**/*.json" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", + "schema/*.json" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -28,55 +28,81 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jupyterlab_examples_datagrid/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_datagrid/labextension jupyterlab_examples_datagrid/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0", - "@jupyterlab/translation": "^3.1.0", - "@lumino/algorithm": "^1.3.3", - "@lumino/coreutils": "^1.5.3", - "@lumino/datagrid": "^0.14.1", - "@lumino/disposable": "^1.4.3" + "@jupyterlab/application": "^4.0.0-beta.0", + "@jupyterlab/translation": "^4.0.0-beta.0", + "@lumino/algorithm": "^2.0.0", + "@lumino/coreutils": "^2.0.0", + "@lumino/datagrid": "^2.0.0", + "@lumino/disposable": "^2.0.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "extension": true, "outputDir": "jupyterlab_examples_datagrid/labextension", "schemaDir": "schema" - }, - "styleModule": "style/index.js" + } } diff --git a/datagrid/preview.gif b/datagrid/preview.gif new file mode 100644 index 00000000..be4aaefc Binary files /dev/null and b/datagrid/preview.gif differ diff --git a/datagrid/preview.png b/datagrid/preview.png deleted file mode 100644 index d24a6b46..00000000 Binary files a/datagrid/preview.png and /dev/null differ diff --git a/datagrid/pyproject.toml b/datagrid/pyproject.toml index e9d7d664..7ee92c36 100644 --- a/datagrid/pyproject.toml +++ b/datagrid/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_datagrid/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_datagrid/labextension/static/style.js", "jupyterlab_examples_datagrid/labextension/package.json"] +[project] +name = "jupyterlab_examples_datagrid" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_datagrid/labextension"] +exclude = [".github", "binder"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_datagrid/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/datagrid" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/datagrid/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_datagrid/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_datagrid/labextension/static/style.js", + "jupyterlab_examples_datagrid/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_datagrid/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_datagrid/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_datagrid/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/datagrid/setup.py b/datagrid/setup.py index 52707cf8..bea23374 100644 --- a/datagrid/setup.py +++ b/datagrid/setup.py @@ -1,83 +1 @@ -""" -jupyterlab_examples_datagrid setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_datagrid" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/datagrid" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/datagrid/src/index.ts b/datagrid/src/index.ts index 4f705d09..4ef5e176 100644 --- a/datagrid/src/index.ts +++ b/datagrid/src/index.ts @@ -1,6 +1,6 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; import { ICommandPalette } from '@jupyterlab/apputils'; @@ -8,7 +8,7 @@ import { ICommandPalette } from '@jupyterlab/apputils'; import { ITranslator, nullTranslator, - TranslationBundle, + TranslationBundle } from '@jupyterlab/translation'; import { DataGrid, DataModel } from '@lumino/datagrid'; @@ -19,7 +19,7 @@ import { StackedPanel } from '@lumino/widgets'; * Initialization data for the extension1 extension. */ const extension: JupyterFrontEndPlugin = { - id: 'datagrid', + id: '@jupyterlab-examples/datagrid:plugin', autoStart: true, requires: [ICommandPalette, ITranslator], activate: ( @@ -37,10 +37,10 @@ const extension: JupyterFrontEndPlugin = { execute: () => { const widget = new DataGridPanel(); shell.add(widget, 'main'); - }, + } }); palette.addItem({ command, category: 'Extension Examples' }); - }, + } }; export default extension; diff --git a/datagrid/tsconfig.json b/datagrid/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/datagrid/tsconfig.json +++ b/datagrid/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/datagrid/tsconfig.test.json b/datagrid/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/datagrid/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/datagrid/ui-tests/.env b/datagrid/ui-tests/.env deleted file mode 100644 index 6acc6cbf..00000000 --- a/datagrid/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=datagrid/jupyterlab_examples_datagrid -EXT_NAME=datagrid \ No newline at end of file diff --git a/datagrid/ui-tests/README.md b/datagrid/ui-tests/README.md index d2be7291..c387d5a1 100644 --- a/datagrid/ui-tests/README.md +++ b/datagrid/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _datagrid_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _datagrid_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _datagrid_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _datagrid_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/datagrid/ui-tests/jupyter_server_test_config.py b/datagrid/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/datagrid/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/datagrid/ui-tests/package.json b/datagrid/ui-tests/package.json index e2a6e83e..72a2d158 100644 --- a/datagrid/ui-tests/package.json +++ b/datagrid/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/datagrid-tests", - "version": "0.1.0", - "description": "Integration test for datagrid example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/datagrid-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/datagrid 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/datagrid/ui-tests/playwright.config.js b/datagrid/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/datagrid/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/datagrid/ui-tests/playwright.config.ts b/datagrid/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/datagrid/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/datagrid/ui-tests/tests/datagrid.spec.ts b/datagrid/ui-tests/tests/datagrid.spec.ts index c8ab0eae..f6dde509 100644 --- a/datagrid/ui-tests/tests/datagrid.spec.ts +++ b/datagrid/ui-tests/tests/datagrid.spec.ts @@ -2,16 +2,11 @@ import { test, expect } from '@jupyterlab/galata'; test('should open a datagrid panel', async ({ page }) => { // Close filebrowser - await page.click('text=View'); - await Promise.all([ - page.waitForSelector('#filebrowser', { state: 'hidden' }), - page.click('ul[role="menu"] >> text=Show Left Sidebar'), - ]); - // Click text=DataGrid Example - await page.click('text=DataGrid Example'); - // Click ul[role="menu"] >> text=Open a Datagrid - await page.click('ul[role="menu"] >> text=Open a Datagrid'); + // Click on DataGrid Example + await page.getByText('DataGrid Example').click(); + // Click on Open a Datagrid + await page.getByRole('menuitem', { name: 'Open a Datagrid' }).click(); expect( await page.waitForSelector('div[role="main"] >> text=Datagrid Example View') diff --git a/datagrid/ui-tests/tests/datagrid.spec.ts-snapshots/datagrid-example-linux.png b/datagrid/ui-tests/tests/datagrid.spec.ts-snapshots/datagrid-example-linux.png index 8df0b54e..93a88431 100644 Binary files a/datagrid/ui-tests/tests/datagrid.spec.ts-snapshots/datagrid-example-linux.png and b/datagrid/ui-tests/tests/datagrid.spec.ts-snapshots/datagrid-example-linux.png differ diff --git a/documents/.eslintrc.js b/documents/.eslintrc.js index c2375786..665374bf 100644 --- a/documents/.eslintrc.js +++ b/documents/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/documents/.gitignore b/documents/.gitignore index 18cace13..ef3ace98 100644 --- a/documents/.gitignore +++ b/documents/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/documents/.yarnrc.yml b/documents/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/documents/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/documents/CHANGELOG.md b/documents/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/documents/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/documents/LICENSE b/documents/LICENSE new file mode 100644 index 00000000..54d885af --- /dev/null +++ b/documents/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/documents/MANIFEST.in b/documents/MANIFEST.in deleted file mode 100644 index 041b2fd7..00000000 --- a/documents/MANIFEST.in +++ /dev/null @@ -1,22 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml -include jupyter-config/jupyterlab_examples_hello_world.json - -include package.json -include ts*.json - -graft jupyterlab_examples_hello_world/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/documents/README.md b/documents/README.md index b4004814..a32715d3 100644 --- a/documents/README.md +++ b/documents/README.md @@ -28,7 +28,7 @@ The easiest way of creating a new widget factory is extending from the `ABCWidge ```ts -// src/factory.ts#L33-L40 +// src/factory.ts#L31-L38 protected createNewWidget( context: DocumentRegistry.IContext @@ -45,7 +45,7 @@ On the other hand, to create a `ModelFactory`, you need to implement the interfa ```ts -// src/factory.ts#L46-L47 +// src/factory.ts#L44-L45 export class ExampleDocModelFactory implements DocumentRegistry.IModelFactory @@ -56,11 +56,13 @@ At the same time, you need to implement the method `createNew`. The `DocumentMan ```ts -// src/factory.ts#L109-L111 +// src/factory.ts#L109-L117 -createNew(languagePreference?: string, modelDB?: IModelDB): ExampleDocModel { - return new ExampleDocModel(languagePreference, modelDB); -} +createNew( + options: DocumentRegistry.IModelOptions +): ExampleDocModel { + return new ExampleDocModel( + options.languagePreference, ``` @@ -151,13 +153,13 @@ To sync content between clients, Yjs uses providers. Providers abstract Yjs from Another critical component of Yjs is Awareness. Every Yjs document has an `awareness` attribute that enables you to share user's information like its name, cursor, mouse pointer position, etc. The `awareness` attribute doesn't persist across sessions. Instead, Yjs uses a tiny state-based Awareness CRDT that propagates JSON objects to all users. When you go offline, your awareness state is automatically deleted and notifies all users that you went offline. -After a short explanation of Yjs' features, now it's time to start with the implementation. You can create a new shared model by extending from `YDocument`. [YDocument](https://jupyter-ydoc.readthedocs.io/en/latest/api/classes/YDocument.html) is a generic implementation of a shared model that handles the initialization of the `YDoc` and already implements some functionalities like the changes history. +After a short explanation of Yjs' features, now it's time to start with the implementation. You can create a new shared model by extending from `YDocument`. [YDocument](https://jupyter-ydoc.readthedocs.io/en/latest/api/classes/YDocument-1.html) is a generic implementation of a shared model that handles the initialization of the `YDoc` and already implements some functionalities like the changes history. To create a new shared object, you have to use the `ydoc`. The new attribute will be linked to the `ydoc` and sync between the different clients automatically. You can also listen to changes on the shared attributes to propagate them to the `DocumentWidget`. ```ts -// src/model.ts#L340-L341 +// src/model.ts#L347-L348 this._content = this.ydoc.getMap('content'); this._content.observe(this._contentObserver); @@ -168,7 +170,7 @@ To access the information about the different users connected, you can use the ` ```ts -// src/model.ts#L279-L279 +// src/model.ts#L285-L285 this.sharedModel.awareness.setLocalStateField('mouse', pos); ``` @@ -176,7 +178,7 @@ this.sharedModel.awareness.setLocalStateField('mouse', pos); ```ts -// src/model.ts#L302-L302 +// src/model.ts#L308-L308 const clients = this.sharedModel.awareness.getStates(); ``` @@ -184,7 +186,7 @@ const clients = this.sharedModel.awareness.getStates(); ```ts -// src/model.ts#L41-L41 +// src/model.ts#L48-L48 this.sharedModel.awareness.on('change', this._onClientChanged); ``` @@ -194,7 +196,7 @@ Every time you modify a shared property, this property triggers an event in all ```ts -// src/model.ts#L183-L186 +// src/model.ts#L189-L192 this.sharedModel.transact(() => { this.sharedModel.setContent('position', { x: obj.x, y: obj.y }); diff --git a/documents/RELEASE.md b/documents/RELEASE.md index 09d31f43..0d1087af 100644 --- a/documents/RELEASE.md +++ b/documents/RELEASE.md @@ -1,22 +1,62 @@ # Making a new release of jupyterlab_examples_documents -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser The extension repository should already be compatible with the Jupyter Releaser. -Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. Here is a summary of the steps to cut a new release: -- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) -- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository - Go to the Actions panel -- Run the "Draft Changelog" workflow -- Merge the Changelog PR -- Run the "Draft Release" workflow -- Run the "Publish Release" workflow +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow ## Publishing to `conda-forge` diff --git a/documents/babel.config.js b/documents/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/documents/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/documents/jest.config.js b/documents/jest.config.js new file mode 100644 index 00000000..15cdce7f --- /dev/null +++ b/documents/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*', + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+`, + ], +}; diff --git a/documents/jupyterlab_examples_documents/__init__.py b/documents/jupyterlab_examples_documents/__init__.py index 353ab522..a842440c 100644 --- a/documents/jupyterlab_examples_documents/__init__.py +++ b/documents/jupyterlab_examples_documents/__init__.py @@ -1,19 +1,9 @@ - -import json -import os.path as osp - from ._version import __version__ -HERE = osp.abspath(osp.dirname(__file__)) - -with open(osp.join(HERE, 'labextension', 'package.json')) as fid: - data = json.load(fid) def _jupyter_labextension_paths(): return [{ - 'src': 'labextension', - 'dest': data['name'] + "src": "labextension", + "dest": "@jupyterlab-examples/documents" }] - - diff --git a/documents/jupyterlab_examples_documents/_version.py b/documents/jupyterlab_examples_documents/_version.py index 351fbde5..133868ac 100644 --- a/documents/jupyterlab_examples_documents/_version.py +++ b/documents/jupyterlab_examples_documents/_version.py @@ -1,18 +1,4 @@ -import json -from pathlib import Path - -__all__ = ["__version__"] - -def _fetchVersion(): - HERE = Path(__file__).parent.resolve() - - for settings in HERE.rglob("package.json"): - try: - with settings.open() as f: - return json.load(f)["version"] - except FileNotFoundError: - pass - - raise FileNotFoundError(f"Could not find package.json under dir {HERE!s}") - -__version__ = _fetchVersion() +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/documents/package.json b/documents/package.json index 7073bfd1..d808c438 100644 --- a/documents/package.json +++ b/documents/package.json @@ -1,7 +1,7 @@ { "name": "@jupyterlab-examples/documents", "version": "0.1.0", - "description": "minimal lab example for a document widget", + "description": "Minimal JupyterLab extension for a document widget.", "keywords": [ "jupyter", "jupyterlab", @@ -14,11 +14,11 @@ "license": "BSD-3-Clause", "author": { "name": "Project Jupyter Contributors", - "email": "" + "email": "me@test.com" }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "style/**/*.{css,eot,js,gif,html,jpg,json,png,svg,woff2,ttf}" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -27,62 +27,88 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jupyterlab_examples_documents/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_documents/labextension jupyterlab_examples_documents/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0", - "@jupyterlab/apputils": "^3.1.0", - "@jupyterlab/coreutils": "^5.1.0", - "@jupyterlab/docregistry": "^3.1.0", - "@jupyterlab/observables": "^4.1.0", - "@jupyterlab/services": "^6.1.0", - "@jupyterlab/shared-models": "^3.1.0", - "@lumino/coreutils": "^1.5.3", - "@lumino/signaling": "^1.4.3", + "@jupyter/ydoc": "^0.3.4", + "@jupyterlab/application": "^4.0.0-beta.0", + "@jupyterlab/apputils": "^4.0.0-beta.0", + "@jupyterlab/coreutils": "^6.0.0-beta.0", + "@jupyterlab/docregistry": "^4.0.0-beta.0", + "@jupyterlab/observables": "^4.0.0-beta.0", + "@jupyterlab/services": "^7.0.0-beta.0", + "@lumino/coreutils": "^2.0.0", + "@lumino/signaling": "^2.0.0", "fast-xml-parser": "^3.19.0", - "react": "^17.0.1", - "react-dom": "^17.0.1", + "react": "^18.0.0", + "react-dom": "^18.0.0", "react-json-tree": "^0.15.0", "yjs": "^13.5.6" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "extension": true, "outputDir": "jupyterlab_examples_documents/labextension" - }, - "styleModule": "style/index.js" + } } diff --git a/documents/pyproject.toml b/documents/pyproject.toml index 82b60604..b06f5edf 100644 --- a/documents/pyproject.toml +++ b/documents/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_documents/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_documents/labextension/static/style.js", "jupyterlab_examples_documents/labextension/package.json"] +[project] +name = "jupyterlab_examples_documents" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_documents/labextension"] +exclude = [".github", "binder"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_documents/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/documents" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/documents/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_documents/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_documents/labextension/static/style.js", + "jupyterlab_examples_documents/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_documents/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_documents/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_documents/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/documents/setup.py b/documents/setup.py index e56af651..bea23374 100644 --- a/documents/setup.py +++ b/documents/setup.py @@ -1,83 +1 @@ -""" -jupyterlab_examples_documents setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_documents" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/documents" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/documents/src/factory.ts b/documents/src/factory.ts index 2905cd4c..406499cb 100644 --- a/documents/src/factory.ts +++ b/documents/src/factory.ts @@ -1,12 +1,10 @@ import { ABCWidgetFactory, DocumentRegistry } from '@jupyterlab/docregistry'; -import { IModelDB } from '@jupyterlab/observables'; - import { Contents } from '@jupyterlab/services'; import { ExampleDocWidget, ExamplePanel } from './widget'; -import { ExampleDocModel } from './model'; +import { ExampleDoc, ExampleDocModel } from './model'; /** * A widget factory to create new instances of ExampleDocWidget. @@ -73,6 +71,8 @@ export class ExampleDocModelFactory return 'text'; } + readonly collaborative: boolean = true; + /** * Get whether the model factory has been disposed. * @@ -106,8 +106,14 @@ export class ExampleDocModelFactory * @param modelDB Model database * @returns The model */ - createNew(languagePreference?: string, modelDB?: IModelDB): ExampleDocModel { - return new ExampleDocModel(languagePreference, modelDB); + createNew( + options: DocumentRegistry.IModelOptions + ): ExampleDocModel { + return new ExampleDocModel( + options.languagePreference, + options.sharedModel, + options.collaborationEnabled + ); } private _disposed = false; diff --git a/documents/src/model.ts b/documents/src/model.ts index 9d877d4d..06ae87c8 100644 --- a/documents/src/model.ts +++ b/documents/src/model.ts @@ -1,8 +1,6 @@ import { DocumentRegistry } from '@jupyterlab/docregistry'; -import { YDocument, MapChange } from '@jupyterlab/shared-models'; - -import { IModelDB, ModelDB } from '@jupyterlab/observables'; +import { YDocument, MapChange, DocumentChange } from '@jupyter/ydoc'; import { IChangedArgs } from '@jupyterlab/coreutils'; @@ -33,8 +31,17 @@ export class ExampleDocModel implements DocumentRegistry.IModel { * @param languagePreference Language * @param modelDB Document model database */ - constructor(languagePreference?: string, modelDB?: IModelDB) { - this.modelDB = modelDB || new ModelDB(); + constructor( + languagePreference?: string, + sharedModel?: ExampleDoc, + collaborationEnabled?: boolean + ) { + this._collaborationEnabled = collaborationEnabled ?? true; + if (sharedModel) { + this.sharedModel = sharedModel; + } else { + this.sharedModel = ExampleDoc.create(); + } // Listening for changes on the shared model to propagate them this.sharedModel.changed.connect(this._onSharedModelChanged); @@ -54,6 +61,12 @@ export class ExampleDocModel implements DocumentRegistry.IModel { this._dirty = value; } + /*Whether the model is collaborative or not. + */ + get collaborative(): boolean { + return this._collaborationEnabled; + } + /** * get/set the readOnly attribute to know whether this model * is read only or not @@ -127,21 +140,14 @@ export class ExampleDocModel implements DocumentRegistry.IModel { * defaultKernelName and defaultKernelLanguage are only used by the Notebook widget * or documents that use kernels, and they store the name and the language of the kernel. */ - readonly defaultKernelName: string; - readonly defaultKernelLanguage: string; - - /** - * modelBD is the datastore for the content of the document. - * modelDB is not a shared datastore so we don't use it on this example since - * this example is a shared document. - */ - readonly modelDB: IModelDB; + readonly defaultKernelName: string = ''; + readonly defaultKernelLanguage: string = ''; /** * New datastore introduced in JupyterLab v3.1 to store shared data and make notebooks * collaborative */ - readonly sharedModel: ExampleDoc = ExampleDoc.create(); + readonly sharedModel: ExampleDoc; /** * Dispose of the resources held by the model. @@ -274,7 +280,7 @@ export class ExampleDocModel implements DocumentRegistry.IModel { * * @param pos Mouse position */ - setClient(pos: Position): void { + setClient(pos: Position | undefined): void { // Adds the position of the mouse from the client to the shared state. this.sharedModel.awareness.setLocalStateField('mouse', pos); } @@ -310,6 +316,7 @@ export class ExampleDocModel implements DocumentRegistry.IModel { private _stateChanged = new Signal>(this); private _clientChanged = new Signal>(this); private _sharedModelChanged = new Signal(this); + private _collaborationEnabled: boolean; } /** @@ -324,7 +331,7 @@ export class ExampleDocModel implements DocumentRegistry.IModel { * This type represents the different changes that may happen and ready to use * for the widget. */ -export type ExampleDocChange = { +export type ExampleDocChange = DocumentChange & { contextChange?: MapChange; contentChange?: string; positionChange?: Position; @@ -341,6 +348,8 @@ export class ExampleDoc extends YDocument { this._content.observe(this._contentObserver); } + readonly version: string = '1.0.0'; + /** * Dispose of the resources. */ diff --git a/documents/src/widget.tsx b/documents/src/widget.tsx index 8d9433b3..5cfcf5b4 100644 --- a/documents/src/widget.tsx +++ b/documents/src/widget.tsx @@ -208,8 +208,8 @@ export class ExamplePanel extends Widget { const id = key.toString(); if (client.mouse && this._clients[id]) { - this._clients[id].style.left = client.mouse.x + 'px'; - this._clients[id].style.top = client.mouse.y + 'px'; + this._clients[id]!.style.left = client.mouse.x + 'px'; + this._clients[id]!.style.top = client.mouse.y + 'px'; } else if (client.mouse && !this._clients[id]) { const el = document.createElement('div'); el.className = 'jp-example-client'; @@ -220,7 +220,7 @@ export class ExamplePanel extends Widget { this._clients[id] = el; this.node.appendChild(el); } else if (!client.mouse && this._clients[id]) { - this.node.removeChild(this._clients[id]); + this.node.removeChild(this._clients[id]!); this._clients[id] = undefined; } } @@ -233,6 +233,6 @@ export class ExamplePanel extends Widget { private _isDown: boolean; private _offset: Position; private _cube: HTMLElement; - private _clients: { [id: string]: HTMLElement }; + private _clients: { [id: string]: HTMLElement | undefined }; private _context: DocumentRegistry.IContext; } diff --git a/documents/style/base.css b/documents/style/base.css index 6d1b93fd..e11f4577 100644 --- a/documents/style/base.css +++ b/documents/style/base.css @@ -1,27 +1,5 @@ -.jp-example-canvas { - width: 100%; - height: 100%; - background-color: var(--jp-cell-editor-background); -} +/* + See the JupyterLab Developer Guide for useful CSS Patterns: -.jp-example-cube { - width: 100px; - height: 100px; - position: absolute; - background-color: var(--jp-private-notebook-selected-color); - display: flex; - justify-content: center; - align-items: center; -} - -.jp-example-client { - width: 25px; - height: 25px; - position: absolute; - z-index: 10; - background-color: var(--jp-private-notebook-active-color); - border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; -} + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ diff --git a/documents/tsconfig.json b/documents/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/documents/tsconfig.json +++ b/documents/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/documents/tsconfig.test.json b/documents/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/documents/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/documents/ui-tests/.env b/documents/ui-tests/.env deleted file mode 100644 index 87276153..00000000 --- a/documents/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=documents/jupyterlab_examples_documents -EXT_NAME=documents \ No newline at end of file diff --git a/documents/ui-tests/README.md b/documents/ui-tests/README.md index d2be7291..c387d5a1 100644 --- a/documents/ui-tests/README.md +++ b/documents/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _datagrid_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _datagrid_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _datagrid_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _datagrid_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/documents/ui-tests/jupyter_server_test_config.py b/documents/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/documents/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/documents/ui-tests/package.json b/documents/ui-tests/package.json index b51c7482..21baaa6c 100644 --- a/documents/ui-tests/package.json +++ b/documents/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/documents-tests", - "version": "0.1.0", - "description": "Integration test for documents example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/documents-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/documents 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/documents/ui-tests/playwright.config.js b/documents/ui-tests/playwright.config.js new file mode 100644 index 00000000..e1153773 --- /dev/null +++ b/documents/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI, + }, +}; diff --git a/documents/ui-tests/playwright.config.ts b/documents/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/documents/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/end-to-end-tests/Dockerfile b/end-to-end-tests/Dockerfile index 5991a2e0..a68e98c9 100644 --- a/end-to-end-tests/Dockerfile +++ b/end-to-end-tests/Dockerfile @@ -4,6 +4,6 @@ FROM jupyter/base-notebook USER root # Upgrade JupyterLab -RUN python -m pip install --upgrade jupyterlab~=3.6 +RUN python -m pip install --upgrade jupyterlab~=4.0.0b0 USER 1000 diff --git a/environment.yml b/environment.yml index 9cf0f1ce..01663817 100644 --- a/environment.yml +++ b/environment.yml @@ -1,9 +1,9 @@ name: jupyterlab-extension-examples channels: - conda-forge + - conda-forge/label/jupyterlab_beta dependencies: - - jupyter-packaging>=0.10.0,<2 - - jupyterlab=3 + - jupyterlab>=4.0.0b0 - nodejs=18 - pytest - pytest-check-links diff --git a/hello-world/.eslintrc.js b/hello-world/.eslintrc.js index c2375786..665374bf 100644 --- a/hello-world/.eslintrc.js +++ b/hello-world/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/hello-world/.gitignore b/hello-world/.gitignore index 18cace13..ef3ace98 100644 --- a/hello-world/.gitignore +++ b/hello-world/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/hello-world/.prettierignore b/hello-world/.prettierignore new file mode 100644 index 00000000..e4341a44 --- /dev/null +++ b/hello-world/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_hello_world diff --git a/hello-world/.prettierrc b/hello-world/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/hello-world/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/hello-world/.stylelintrc b/hello-world/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/hello-world/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/hello-world/.yarnrc.yml b/hello-world/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/hello-world/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/hello-world/CHANGELOG.md b/hello-world/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/hello-world/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/hello-world/LICENSE b/hello-world/LICENSE new file mode 100644 index 00000000..54d885af --- /dev/null +++ b/hello-world/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/hello-world/MANIFEST.in b/hello-world/MANIFEST.in deleted file mode 100644 index 041b2fd7..00000000 --- a/hello-world/MANIFEST.in +++ /dev/null @@ -1,22 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml -include jupyter-config/jupyterlab_examples_hello_world.json - -include package.json -include ts*.json - -graft jupyterlab_examples_hello_world/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/hello-world/README.md b/hello-world/README.md index a8a0ea80..926d2310 100644 --- a/hello-world/README.md +++ b/hello-world/README.md @@ -25,7 +25,7 @@ author_name []: tuto author_email []: tuto@help.you labextension_name [myextension]: hello-world python_name [hello_world]: -project_short_description [A JupyterLab extension.]: Minimal JupyterLab example +project_short_description [A JupyterLab extension.]: Minimal JupyterLab example. has_settings [n]: has_server_extension [n]: has_binder [n]: y @@ -39,39 +39,49 @@ that looks like this: ```bash hello_world/ -│ .eslintignore -│ .eslintrc.js -│ .gitignore -│ .prettierignore -│ .prettierrc -│ install.json -│ LICENSE -│ MANIFEST.in -│ package.json -│ pyproject.toml -│ README.md -│ setup.py -│ tsconfig.json -│ -├───.github -│ └───workflows -│ build.yml -│ -├───binder -│ environment.yml -│ postBuild -│ -├───hello_world -│ __init__.py -│ _version.py -│ -├───src -│ index.ts -│ -└───style - base.css - index.css - index.js +├── babel.config.js +├── CHANGELOG.md +├── .eslintignore +├── .eslintrc.js +├── .github +│   └── workflows +│   ├── build.yml +│   ├── check-release.yml +│   └── update-integration-tests.yml +├── .gitignore +├── hello_world +│   └── __init__.py +├── install.json +├── jest.config.js +├── LICENSE +├── package.json +├── .prettierignore +├── .prettierrc +├── pyproject.toml +├── README.md +├── RELEASE.md +├── setup.py +├── src +│   ├── index.ts +│   └── __tests__ +│   └── hello_world.spec.ts +├── style +│   ├── base.css +│   ├── index.css +│   └── index.js +├── .stylelintrc +├── tsconfig.json +├── tsconfig.test.json +├── ui-tests +│   ├── jupyter_server_test_config.py +│   ├── package.json +│   ├── playwright.config.js +│   ├── README.md +│   ├── tests +│   │   └── hello_world.spec.ts +│   └── yarn.lock +└── .yarnrc.yml + ``` Those files can be separated in 4 groups: @@ -91,7 +101,6 @@ Those files can be separated in 4 groups: - Packaging as a Python package: - `setup.py` contains information about the Python package such as what to package - `pyproject.toml` contains the dependencies to create the Python package - - `MANIFEST.in` contains list of non-Python files to include in the Python package - `install.json` contains information retrieved by JupyterLab to help users know how to manage the package - `hello_world/` folder contains the final code to be distributed @@ -108,7 +117,7 @@ logic of the extension. It begins with the following import section: import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; ``` @@ -120,10 +129,10 @@ called `@jupyterlab/application`. The dependency of your extension on this package is declared in the file `package.json`: ```json5 -// package.json#L49-L51 +// package.json#L60-L62 "dependencies": { - "@jupyterlab/application": "^3.1.0" + "@jupyterlab/application": "^4.0.0-beta.0" }, ``` @@ -135,7 +144,7 @@ of the `JupyterFrontEndPlugin` class: // src/index.ts#L9-L12 const plugin: JupyterFrontEndPlugin = { - id: 'hello-world:plugin', + id: '@jupyterlab-examples/hello-world:plugin', autoStart: true, activate: (app: JupyterFrontEnd) => { ``` @@ -147,7 +156,7 @@ const plugin: JupyterFrontEndPlugin = { ```ts // src/index.ts#L14-L17 - }, + } }; export default plugin; @@ -234,7 +243,7 @@ a bit. Simply replace the `activate` function with the following lines: activate: (app: JupyterFrontEnd) => { console.log('the JupyterLab main application:', app); -}, +} ``` diff --git a/hello-world/RELEASE.md b/hello-world/RELEASE.md index 9c11f641..75b0c529 100644 --- a/hello-world/RELEASE.md +++ b/hello-world/RELEASE.md @@ -1,6 +1,49 @@ # Making a new release of jupyterlab_examples_hello_world -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python +packages. All of the Python +packaging instructions in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, we first need to install `build`. + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser diff --git a/hello-world/babel.config.js b/hello-world/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/hello-world/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/hello-world/jest.config.js b/hello-world/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/hello-world/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/hello-world/jupyterlab_examples_hello_world/__init__.py b/hello-world/jupyterlab_examples_hello_world/__init__.py index 353ab522..a73c941a 100644 --- a/hello-world/jupyterlab_examples_hello_world/__init__.py +++ b/hello-world/jupyterlab_examples_hello_world/__init__.py @@ -1,19 +1,7 @@ - -import json -import os.path as osp - from ._version import __version__ -HERE = osp.abspath(osp.dirname(__file__)) - -with open(osp.join(HERE, 'labextension', 'package.json')) as fid: - data = json.load(fid) - def _jupyter_labextension_paths(): return [{ - 'src': 'labextension', - 'dest': data['name'] + "src": "labextension", + "dest": "@jupyterlab-examples/hello-world" }] - - - diff --git a/hello-world/jupyterlab_examples_hello_world/_version.py b/hello-world/jupyterlab_examples_hello_world/_version.py index 351fbde5..133868ac 100644 --- a/hello-world/jupyterlab_examples_hello_world/_version.py +++ b/hello-world/jupyterlab_examples_hello_world/_version.py @@ -1,18 +1,4 @@ -import json -from pathlib import Path - -__all__ = ["__version__"] - -def _fetchVersion(): - HERE = Path(__file__).parent.resolve() - - for settings in HERE.rglob("package.json"): - try: - with settings.open() as f: - return json.load(f)["version"] - except FileNotFoundError: - pass - - raise FileNotFoundError(f"Could not find package.json under dir {HERE!s}") - -__version__ = _fetchVersion() +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/hello-world/package.json b/hello-world/package.json index d3ec37ac..bca70524 100644 --- a/hello-world/package.json +++ b/hello-world/package.json @@ -1,7 +1,7 @@ { "name": "@jupyterlab-examples/hello-world", "version": "0.1.0", - "description": "minimal lab example", + "description": "Minimal lab example.", "keywords": [ "jupyter", "jupyterlab", @@ -18,7 +18,7 @@ }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "style/**/*.{css,eot,js,gif,html,jpg,json,png,svg,woff2,ttf}" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -27,49 +27,75 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jupyterlab_examples_hello_world/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_hello_world/labextension jupyterlab_examples_hello_world/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0" + "@jupyterlab/application": "^4.0.0-beta.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "extension": true, "outputDir": "jupyterlab_examples_hello_world/labextension" - }, - "styleModule": "style/index.js" + } } diff --git a/hello-world/preview.png b/hello-world/preview.png index bc552a24..08958b5e 100644 Binary files a/hello-world/preview.png and b/hello-world/preview.png differ diff --git a/hello-world/pyproject.toml b/hello-world/pyproject.toml index be75e07d..e433979e 100644 --- a/hello-world/pyproject.toml +++ b/hello-world/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_hello_world/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_hello_world/labextension/static/style.js", "jupyterlab_examples_hello_world/labextension/package.json"] +[project] +name = "jupyterlab_examples_hello_world" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_hello_world/labextension"] +exclude = [".github", "binder"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_hello_world/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/hello-world" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/hello-world/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_hello_world/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_hello_world/labextension/static/style.js", + "jupyterlab_examples_hello_world/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_hello_world/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_hello_world/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_hello_world/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/hello-world/setup.py b/hello-world/setup.py index eba74404..bea23374 100644 --- a/hello-world/setup.py +++ b/hello-world/setup.py @@ -1,83 +1 @@ -""" -jupyterlab_examples_hello_world setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_hello_world" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/hello-world" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/hello-world/src/index.ts b/hello-world/src/index.ts index b4fd166c..c9635bdc 100644 --- a/hello-world/src/index.ts +++ b/hello-world/src/index.ts @@ -1,17 +1,17 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; /** * Initialization data for the hello-world extension. */ const plugin: JupyterFrontEndPlugin = { - id: 'hello-world:plugin', + id: '@jupyterlab-examples/hello-world:plugin', autoStart: true, activate: (app: JupyterFrontEnd) => { console.log('the JupyterLab main application:', app); - }, + } }; export default plugin; diff --git a/hello-world/style/base.css b/hello-world/style/base.css index e69de29b..e11f4577 100644 --- a/hello-world/style/base.css +++ b/hello-world/style/base.css @@ -0,0 +1,5 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ diff --git a/hello-world/style/index.css b/hello-world/style/index.css index 8a7ea29e..e98119b5 100644 --- a/hello-world/style/index.css +++ b/hello-world/style/index.css @@ -1 +1 @@ -@import url('base.css'); +@import 'base.css'; diff --git a/hello-world/tsconfig.json b/hello-world/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/hello-world/tsconfig.json +++ b/hello-world/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/hello-world/tsconfig.test.json b/hello-world/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/hello-world/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/hello-world/ui-tests/.env b/hello-world/ui-tests/.env deleted file mode 100644 index dad7414f..00000000 --- a/hello-world/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=hello-world/jupyterlab_examples_hello_world -EXT_NAME=hello-world \ No newline at end of file diff --git a/hello-world/ui-tests/README.md b/hello-world/ui-tests/README.md index d2be7291..b42b1960 100644 --- a/hello-world/ui-tests/README.md +++ b/hello-world/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _hello-world_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _hello-world_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _hello-world_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _hello-world_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/hello-world/ui-tests/jupyter_server_test_config.py b/hello-world/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/hello-world/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/hello-world/ui-tests/package.json b/hello-world/ui-tests/package.json index 2353caa7..f70305e4 100644 --- a/hello-world/ui-tests/package.json +++ b/hello-world/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/hello-world-tests", - "version": "0.1.0", - "description": "Integration test for hello-world example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/hello-world-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/hello-world 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/hello-world/ui-tests/playwright.config.js b/hello-world/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/hello-world/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/hello-world/ui-tests/playwright.config.ts b/hello-world/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/hello-world/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/hello-world/ui-tests/tests/hello-world.spec.ts b/hello-world/ui-tests/tests/hello-world.spec.ts index 693975f6..120af15e 100644 --- a/hello-world/ui-tests/tests/hello-world.spec.ts +++ b/hello-world/ui-tests/tests/hello-world.spec.ts @@ -5,13 +5,13 @@ test.use({ autoGoto: false }); test('should emit console message', async ({ page }) => { const logs: string[] = []; - page.on('console', (message) => { + page.on('console', message => { logs.push(message.text()); }); await page.goto(); expect( - logs.filter((s) => s.startsWith('the JupyterLab main application')) + logs.filter(s => s.startsWith('the JupyterLab main application')) ).toHaveLength(1); }); diff --git a/kernel-messaging/.eslintrc.js b/kernel-messaging/.eslintrc.js index c2375786..665374bf 100644 --- a/kernel-messaging/.eslintrc.js +++ b/kernel-messaging/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/kernel-messaging/.gitignore b/kernel-messaging/.gitignore index 18cace13..ef3ace98 100644 --- a/kernel-messaging/.gitignore +++ b/kernel-messaging/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/kernel-messaging/.prettierignore b/kernel-messaging/.prettierignore new file mode 100644 index 00000000..5c0d7644 --- /dev/null +++ b/kernel-messaging/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_kernel_messaging diff --git a/kernel-messaging/.prettierrc b/kernel-messaging/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/kernel-messaging/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/kernel-messaging/.stylelintrc b/kernel-messaging/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/kernel-messaging/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/kernel-messaging/.yarnrc.yml b/kernel-messaging/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/kernel-messaging/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/kernel-messaging/CHANGELOG.md b/kernel-messaging/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/kernel-messaging/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/kernel-messaging/LICENSE b/kernel-messaging/LICENSE new file mode 100644 index 00000000..54d885af --- /dev/null +++ b/kernel-messaging/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/kernel-messaging/MANIFEST.in b/kernel-messaging/MANIFEST.in deleted file mode 100644 index 1ffdbd62..00000000 --- a/kernel-messaging/MANIFEST.in +++ /dev/null @@ -1,22 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml -include jupyter-config/jupyterlab_examples_kernel_messaging.json - -include package.json -include ts*.json - -graft jupyterlab_examples_kernel_messaging/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/kernel-messaging/README.md b/kernel-messaging/README.md index e3788851..1e500107 100644 --- a/kernel-messaging/README.md +++ b/kernel-messaging/README.md @@ -41,22 +41,22 @@ that manages a single kernel session. Here is the code to initialize such sessio this._sessionContext = new SessionContext({ sessionManager: manager.sessions, specsManager: manager.kernelspecs, - name: 'Extension Examples', + name: 'Extension Examples' }); ``` - + ```ts void this._sessionContext .initialize() - .then(async (value) => { + .then(async value => { if (value) { - await sessionContextDialogs.selectKernel(this._sessionContext); + await this._sessionContextDialogs.selectKernel(this._sessionContext); } }) - .catch((reason) => { + .catch(reason => { console.error( `Failed to initialize the session in ExamplePanel.\n${reason}` ); @@ -81,7 +81,7 @@ to free the kernel session resources if the panel is closed. The whole adapted panel class looks like this: ```ts -// src/panel.ts#L31-L85 +// src/panel.ts#L31-L91 export class ExamplePanel extends StackedPanel { constructor(manager: ServiceManager.IManager, translator?: ITranslator) { @@ -96,21 +96,26 @@ export class ExamplePanel extends StackedPanel { this._sessionContext = new SessionContext({ sessionManager: manager.sessions, specsManager: manager.kernelspecs, - name: 'Extension Examples', + name: 'Extension Examples' }); this._model = new KernelModel(this._sessionContext); this._example = new KernelView(this._model); this.addWidget(this._example); + + this._sessionContextDialogs = new SessionContextDialogs({ + translator: translator + }); + void this._sessionContext .initialize() - .then(async (value) => { + .then(async value => { if (value) { - await sessionContextDialogs.selectKernel(this._sessionContext); + await this._sessionContextDialogs.selectKernel(this._sessionContext); } }) - .catch((reason) => { + .catch(reason => { console.error( `Failed to initialize the session in ExamplePanel.\n${reason}` ); @@ -134,6 +139,7 @@ export class ExamplePanel extends StackedPanel { private _model: KernelModel; private _sessionContext: SessionContext; private _example: KernelView; + private _sessionContextDialogs: SessionContextDialogs; private _translator: ITranslator; private _trans: TranslationBundle; @@ -148,7 +154,7 @@ Once a kernel is initialized and ready, code can be executed with the following // src/model.ts#L46-L48 this.future = this._sessionContext.session?.kernel?.requestExecute({ - code, + code }); ``` @@ -205,7 +211,7 @@ export class KernelModel { return; } this.future = this._sessionContext.session?.kernel?.requestExecute({ - code, + code }); } diff --git a/kernel-messaging/RELEASE.md b/kernel-messaging/RELEASE.md index a9ff827d..24d32f6d 100644 --- a/kernel-messaging/RELEASE.md +++ b/kernel-messaging/RELEASE.md @@ -1,22 +1,62 @@ # Making a new release of jupyterlab_examples_kernel_messaging -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser The extension repository should already be compatible with the Jupyter Releaser. -Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. Here is a summary of the steps to cut a new release: -- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) -- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository - Go to the Actions panel -- Run the "Draft Changelog" workflow -- Merge the Changelog PR -- Run the "Draft Release" workflow -- Run the "Publish Release" workflow +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow ## Publishing to `conda-forge` diff --git a/kernel-messaging/babel.config.js b/kernel-messaging/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/kernel-messaging/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/kernel-messaging/jest.config.js b/kernel-messaging/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/kernel-messaging/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/kernel-messaging/jupyterlab_examples_kernel_messaging/__init__.py b/kernel-messaging/jupyterlab_examples_kernel_messaging/__init__.py index 353ab522..e90f3bec 100644 --- a/kernel-messaging/jupyterlab_examples_kernel_messaging/__init__.py +++ b/kernel-messaging/jupyterlab_examples_kernel_messaging/__init__.py @@ -1,19 +1,9 @@ - -import json -import os.path as osp - from ._version import __version__ -HERE = osp.abspath(osp.dirname(__file__)) - -with open(osp.join(HERE, 'labextension', 'package.json')) as fid: - data = json.load(fid) def _jupyter_labextension_paths(): return [{ - 'src': 'labextension', - 'dest': data['name'] + "src": "labextension", + "dest": "@jupyterlab-examples/kernel-messaging" }] - - diff --git a/kernel-messaging/jupyterlab_examples_kernel_messaging/_version.py b/kernel-messaging/jupyterlab_examples_kernel_messaging/_version.py index ee864fc9..133868ac 100644 --- a/kernel-messaging/jupyterlab_examples_kernel_messaging/_version.py +++ b/kernel-messaging/jupyterlab_examples_kernel_messaging/_version.py @@ -1,2 +1,4 @@ -version_info = (0, 1, 0) -__version__ = ".".join(map(str, version_info)) +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/kernel-messaging/package.json b/kernel-messaging/package.json index 7082dbcb..a3f05df4 100644 --- a/kernel-messaging/package.json +++ b/kernel-messaging/package.json @@ -1,7 +1,7 @@ { "name": "@jupyterlab-examples/kernel-messaging", "version": "0.1.0", - "description": "minimal lab example", + "description": "Extension exafor kernel messaging", "keywords": [ "jupyter", "jupyterlab", @@ -14,12 +14,12 @@ "license": "BSD-3-Clause", "author": { "name": "Project Jupyter Contributors", - "email": "" + "email": "me@test.com" }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "style/**/*.{css,eot,js,gif,html,jpg,json,png,svg,woff2,ttf}", - "schema/**/*.json" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", + "schema/*.json" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -28,58 +28,86 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jupyterlab_examples_kernel_messaging/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_kernel_messaging/labextension jupyterlab_examples_kernel_messaging/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0", - "@jupyterlab/launcher": "^3.1.0", - "@jupyterlab/nbformat": "^3.1.0", - "@jupyterlab/translation": "^3.1.0", - "@lumino/algorithm": "^1.3.3", - "@lumino/coreutils": "^1.5.3", - "@lumino/datagrid": "^0.5.2", - "@lumino/disposable": "^1.4.3", - "@lumino/widgets": "^1.19.0" + "@jupyterlab/application": "^4.0.0-beta.0", + "@jupyterlab/apputils": "^4.0.0-beta.0", + "@jupyterlab/launcher": "^4.0.0-beta.0", + "@jupyterlab/nbformat": "^4.0.0-beta.0", + "@jupyterlab/services": "^7.0.0-beta.0", + "@jupyterlab/settingregistry": "^4.0.0-beta.0", + "@jupyterlab/translation": "^4.0.0-beta.0", + "@lumino/messaging": "^2.0.0", + "@lumino/signaling": "^2.0.0", + "@lumino/widgets": "^2.0.0", + "react": "^18.2.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "extension": true, "outputDir": "jupyterlab_examples_kernel_messaging/labextension", "schemaDir": "schema" - }, - "styleModule": "style/index.js" + } } diff --git a/kernel-messaging/preview.gif b/kernel-messaging/preview.gif index 19acc01d..e82bc148 100644 Binary files a/kernel-messaging/preview.gif and b/kernel-messaging/preview.gif differ diff --git a/kernel-messaging/pyproject.toml b/kernel-messaging/pyproject.toml index 480a969d..4b335978 100644 --- a/kernel-messaging/pyproject.toml +++ b/kernel-messaging/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_kernel_messaging/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_kernel_messaging/labextension/static/style.js", "jupyterlab_examples_kernel_messaging/labextension/package.json"] +[project] +name = "jupyterlab_examples_kernel_messaging" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_kernel_messaging/labextension"] +exclude = [".github", "binder"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_kernel_messaging/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/kernel-messaging" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/kernel-messaging/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_kernel_messaging/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_kernel_messaging/labextension/static/style.js", + "jupyterlab_examples_kernel_messaging/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_kernel_messaging/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_kernel_messaging/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_kernel_messaging/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/kernel-messaging/setup.py b/kernel-messaging/setup.py index 79ee1da4..bea23374 100644 --- a/kernel-messaging/setup.py +++ b/kernel-messaging/setup.py @@ -1,83 +1 @@ -""" -jupyterlab_examples_kernel_messaging setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_kernel_messaging" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/kernel-messaging" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/kernel-messaging/src/index.ts b/kernel-messaging/src/index.ts index 08f6da2a..a891839c 100644 --- a/kernel-messaging/src/index.ts +++ b/kernel-messaging/src/index.ts @@ -1,6 +1,6 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; import { ICommandPalette } from '@jupyterlab/apputils'; @@ -22,11 +22,11 @@ namespace CommandIDs { * Initialization data for the extension. */ const extension: JupyterFrontEndPlugin = { - id: 'kernel-messaging', + id: '@jupyterlab-examples/kernel-messaging:plugin', autoStart: true, optional: [ILauncher], requires: [ICommandPalette, ITranslator], - activate: activate, + activate: activate }; /** @@ -52,7 +52,7 @@ function activate( if (launcher) { launcher.add({ command: CommandIDs.create, - category: category, + category: category }); } @@ -71,7 +71,7 @@ function activate( commands.addCommand(CommandIDs.create, { label: trans.__('Open the Kernel Messaging Panel'), caption: trans.__('Open the Kernel Messaging Panel'), - execute: createPanel, + execute: createPanel }); // add items in command palette and menu diff --git a/kernel-messaging/src/model.ts b/kernel-messaging/src/model.ts index 928e9cbf..d9a233f7 100644 --- a/kernel-messaging/src/model.ts +++ b/kernel-messaging/src/model.ts @@ -44,7 +44,7 @@ export class KernelModel { return; } this.future = this._sessionContext.session?.kernel?.requestExecute({ - code, + code }); } diff --git a/kernel-messaging/src/panel.ts b/kernel-messaging/src/panel.ts index 466ddab9..bea11823 100644 --- a/kernel-messaging/src/panel.ts +++ b/kernel-messaging/src/panel.ts @@ -1,13 +1,13 @@ import { SessionContext, ISessionContext, - sessionContextDialogs, + SessionContextDialogs } from '@jupyterlab/apputils'; import { ITranslator, nullTranslator, - TranslationBundle, + TranslationBundle } from '@jupyterlab/translation'; import { ServiceManager } from '@jupyterlab/services'; @@ -41,21 +41,26 @@ export class ExamplePanel extends StackedPanel { this._sessionContext = new SessionContext({ sessionManager: manager.sessions, specsManager: manager.kernelspecs, - name: 'Extension Examples', + name: 'Extension Examples' }); this._model = new KernelModel(this._sessionContext); this._example = new KernelView(this._model); this.addWidget(this._example); + + this._sessionContextDialogs = new SessionContextDialogs({ + translator: translator + }); + void this._sessionContext .initialize() - .then(async (value) => { + .then(async value => { if (value) { - await sessionContextDialogs.selectKernel(this._sessionContext); + await this._sessionContextDialogs.selectKernel(this._sessionContext); } }) - .catch((reason) => { + .catch(reason => { console.error( `Failed to initialize the session in ExamplePanel.\n${reason}` ); @@ -79,6 +84,7 @@ export class ExamplePanel extends StackedPanel { private _model: KernelModel; private _sessionContext: SessionContext; private _example: KernelView; + private _sessionContextDialogs: SessionContextDialogs; private _translator: ITranslator; private _trans: TranslationBundle; diff --git a/kernel-messaging/style/base.css b/kernel-messaging/style/base.css index d703e460..499b8bb0 100644 --- a/kernel-messaging/style/base.css +++ b/kernel-messaging/style/base.css @@ -1,3 +1,9 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ + .jp-example-view { background-color: AliceBlue; } diff --git a/kernel-messaging/style/index.css b/kernel-messaging/style/index.css index 8a7ea29e..e98119b5 100644 --- a/kernel-messaging/style/index.css +++ b/kernel-messaging/style/index.css @@ -1 +1 @@ -@import url('base.css'); +@import 'base.css'; diff --git a/kernel-messaging/tsconfig.json b/kernel-messaging/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/kernel-messaging/tsconfig.json +++ b/kernel-messaging/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/kernel-messaging/tsconfig.test.json b/kernel-messaging/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/kernel-messaging/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/kernel-messaging/ui-tests/.env b/kernel-messaging/ui-tests/.env deleted file mode 100644 index f9b5c34e..00000000 --- a/kernel-messaging/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=kernel-messaging/jupyterlab_examples_kernel_messaging -EXT_NAME=kernel-messaging \ No newline at end of file diff --git a/kernel-messaging/ui-tests/README.md b/kernel-messaging/ui-tests/README.md index d2be7291..0da319cf 100644 --- a/kernel-messaging/ui-tests/README.md +++ b/kernel-messaging/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _kernel-messaging_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _kernel-messaging_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _kernel-messaging_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _kernel-messaging_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/kernel-messaging/ui-tests/jupyter_server_test_config.py b/kernel-messaging/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/kernel-messaging/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/kernel-messaging/ui-tests/package.json b/kernel-messaging/ui-tests/package.json index bcfc278e..2ef61698 100644 --- a/kernel-messaging/ui-tests/package.json +++ b/kernel-messaging/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/kernel-messaging-tests", - "version": "0.1.0", - "description": "Integration test for kernel-messaging example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/kernel-messaging-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/kernel-messaging 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/kernel-messaging/ui-tests/playwright.config.js b/kernel-messaging/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/kernel-messaging/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/kernel-messaging/ui-tests/playwright.config.ts b/kernel-messaging/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/kernel-messaging/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/kernel-messaging/ui-tests/tests/kernel-messaging.spec.ts b/kernel-messaging/ui-tests/tests/kernel-messaging.spec.ts index 8d181e0b..fbeb71ae 100644 --- a/kernel-messaging/ui-tests/tests/kernel-messaging.spec.ts +++ b/kernel-messaging/ui-tests/tests/kernel-messaging.spec.ts @@ -1,11 +1,9 @@ import { test, expect } from '@jupyterlab/galata'; test('should open a panel connected to a kernel', async ({ page }) => { - await page.getByText('Kernel Messaging', { exact: true }).click(); - - await page - .getByRole('menuitem', { name: 'Open the Kernel Messaging Panel' }) - .click(); + await page.menu.clickMenuItem( + 'Kernel Messaging>Open the Kernel Messaging Panel' + ); await page.getByRole('button', { name: 'Select' }).click(); @@ -14,17 +12,18 @@ test('should open a panel connected to a kernel', async ({ page }) => { await page.getByRole('button', { name: 'Compute 3+5' }).click(); - await expect.soft( - page.getByText( - '{"data":{"text/plain":"8"},"metadata":{},"execution_count":1}' + await expect + .soft( + page.getByText( + '{"data":{"text/plain":"8"},"metadata":{},"execution_count":1}' + ) ) - ).toHaveCount(1); + .toHaveCount(1); // Close filebrowser - await page.getByText('View', { exact: true }).click(); await Promise.all([ page.waitForSelector('#filebrowser', { state: 'hidden' }), - page.getByRole('menuitem', { name: 'Show Left Sidebar Ctrl+B' }).click(), + page.menu.clickMenuItem('View>File Browser') ]); // Compare screenshot with a stored reference. diff --git a/kernel-messaging/ui-tests/tests/kernel-messaging.spec.ts-snapshots/kernel-messaging-example-linux.png b/kernel-messaging/ui-tests/tests/kernel-messaging.spec.ts-snapshots/kernel-messaging-example-linux.png index c7f72df3..f6144a63 100644 Binary files a/kernel-messaging/ui-tests/tests/kernel-messaging.spec.ts-snapshots/kernel-messaging-example-linux.png and b/kernel-messaging/ui-tests/tests/kernel-messaging.spec.ts-snapshots/kernel-messaging-example-linux.png differ diff --git a/kernel-output/.eslintrc.js b/kernel-output/.eslintrc.js index c2375786..665374bf 100644 --- a/kernel-output/.eslintrc.js +++ b/kernel-output/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/kernel-output/.gitignore b/kernel-output/.gitignore index 18cace13..ef3ace98 100644 --- a/kernel-output/.gitignore +++ b/kernel-output/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/kernel-output/.prettierignore b/kernel-output/.prettierignore new file mode 100644 index 00000000..6fb0aed3 --- /dev/null +++ b/kernel-output/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_kernel_output diff --git a/kernel-output/.prettierrc b/kernel-output/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/kernel-output/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/kernel-output/.stylelintrc b/kernel-output/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/kernel-output/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/kernel-output/.yarnrc.yml b/kernel-output/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/kernel-output/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/kernel-output/CHANGELOG.md b/kernel-output/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/kernel-output/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/kernel-output/LICENSE b/kernel-output/LICENSE new file mode 100644 index 00000000..54d885af --- /dev/null +++ b/kernel-output/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/kernel-output/MANIFEST.in b/kernel-output/MANIFEST.in deleted file mode 100644 index d67f9240..00000000 --- a/kernel-output/MANIFEST.in +++ /dev/null @@ -1,22 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml -include jupyter-config/jupyterlab_examples_kernel_output.json - -include package.json -include ts*.json - -graft jupyterlab_examples_kernel_output/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/kernel-output/README.md b/kernel-output/README.md index 88c634c2..932fe6eb 100644 --- a/kernel-output/README.md +++ b/kernel-output/README.md @@ -41,7 +41,7 @@ object ([see the documentation](https://jupyterlab.github.io/jupyterlab/classes/ Here it is stored in the private `_sessionContext` variable: ```ts -// src/panel.ts#L95-L95 +// src/panel.ts#L99-L99 private _sessionContext: SessionContext; ``` @@ -55,7 +55,7 @@ the kernel) is started with these lines: this._sessionContext = new SessionContext({ sessionManager: manager.sessions, specsManager: manager.kernelspecs, - name: 'Kernel Output', + name: 'Kernel Output' }); ``` @@ -63,28 +63,38 @@ The private session variable is exposed as read-only for other users through a getter method: ```ts -// src/panel.ts#L73-L75 +// src/panel.ts#L77-L79 get session(): ISessionContext { return this._sessionContext; } ``` -Once you have created a session, the associated kernel can be initialized -with this line: +A session dialog is created: + +```ts +// src/panel.ts#L57-L59 + +this._sessionContextDialogs = new SessionContextDialogs({ + translator: translator +}); +``` + +Once you have created a session and a session dialog, the associated kernel can +be initialized with this lines: ```ts -// src/panel.ts#L59-L70 +// src/panel.ts#L63-L74 void this._sessionContext .initialize() - .then(async (value) => { + .then(async value => { if (value) { - await sessionContextDialogs.selectKernel(this._sessionContext); + await this._sessionContextDialogs.selectKernel(this._sessionContext); } }) - .catch((reason) => { + .catch(reason => { console.error( `Failed to initialize the session in ExamplePanel.\n${reason}` ); @@ -99,7 +109,7 @@ The following two methods ensure the clean disposal of the session when you close the panel. ```ts -// src/panel.ts#L77-L80 +// src/panel.ts#L81-L84 dispose(): void { this._sessionContext.dispose(); @@ -108,7 +118,7 @@ dispose(): void { ``` ```ts -// src/panel.ts#L90-L93 +// src/panel.ts#L94-L97 protected onCloseRequest(msg: Message): void { super.onCloseRequest(msg); @@ -129,7 +139,7 @@ the data to show: this._outputareamodel = new OutputAreaModel(); this._outputarea = new SimplifiedOutputArea({ model: this._outputareamodel, - rendermime: rendermime, + rendermime: rendermime }); ``` @@ -138,14 +148,14 @@ some code to a kernel through a `ISessionContext` ([see documentation](https://j in the specific `SimplifiedOutputArea` object you created: ```ts -// src/panel.ts#L82-L88 +// src/panel.ts#L86-L92 execute(code: string): void { SimplifiedOutputArea.execute(code, this._outputarea, this._sessionContext) - .then((msg: KernelMessage.IExecuteReplyMsg) => { + .then((msg: KernelMessage.IExecuteReplyMsg | undefined) => { console.log(msg); }) - .catch((reason) => console.error(reason)); + .catch(reason => console.error(reason)); } ``` @@ -158,7 +168,7 @@ To display the `SimplifiedOutputArea` Widget you need to add it to your panel with: ```ts -// src/panel.ts#L57-L57 +// src/panel.ts#L61-L61 this.addWidget(this._outputarea); ``` @@ -188,7 +198,7 @@ on a list: // src/index.ts#L99-L102 // add items in command palette and menu -[CommandIDs.create, CommandIDs.execute].forEach((command) => { +[CommandIDs.create, CommandIDs.execute].forEach(command => { palette.addItem({ command, category }); }); ``` @@ -244,14 +254,14 @@ commands.addCommand(CommandIDs.execute, { const input = await InputDialog.getText({ title: trans.__('Code to execute'), okLabel: trans.__('Execute'), - placeholder: trans.__('Statement to execute'), + placeholder: trans.__('Statement to execute') }); // Execute the statement if (input.button.accept) { - const code = input.value; + const code = input.value || ''; panel.execute(code); } - }, + } }); ``` diff --git a/kernel-output/RELEASE.md b/kernel-output/RELEASE.md index f1fa3914..032c42ef 100644 --- a/kernel-output/RELEASE.md +++ b/kernel-output/RELEASE.md @@ -1,22 +1,62 @@ # Making a new release of jupyterlab_examples_kernel_output -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser The extension repository should already be compatible with the Jupyter Releaser. -Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. Here is a summary of the steps to cut a new release: -- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) -- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository - Go to the Actions panel -- Run the "Draft Changelog" workflow -- Merge the Changelog PR -- Run the "Draft Release" workflow -- Run the "Publish Release" workflow +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow ## Publishing to `conda-forge` diff --git a/kernel-output/babel.config.js b/kernel-output/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/kernel-output/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/kernel-output/jest.config.js b/kernel-output/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/kernel-output/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/kernel-output/jupyterlab_examples_kernel_output/__init__.py b/kernel-output/jupyterlab_examples_kernel_output/__init__.py index 353ab522..1942d76b 100644 --- a/kernel-output/jupyterlab_examples_kernel_output/__init__.py +++ b/kernel-output/jupyterlab_examples_kernel_output/__init__.py @@ -1,19 +1,9 @@ - -import json -import os.path as osp - from ._version import __version__ -HERE = osp.abspath(osp.dirname(__file__)) - -with open(osp.join(HERE, 'labextension', 'package.json')) as fid: - data = json.load(fid) def _jupyter_labextension_paths(): return [{ - 'src': 'labextension', - 'dest': data['name'] + "src": "labextension", + "dest": "@jupyterlab-examples/kernel-output" }] - - diff --git a/kernel-output/jupyterlab_examples_kernel_output/_version.py b/kernel-output/jupyterlab_examples_kernel_output/_version.py index ee864fc9..133868ac 100644 --- a/kernel-output/jupyterlab_examples_kernel_output/_version.py +++ b/kernel-output/jupyterlab_examples_kernel_output/_version.py @@ -1,2 +1,4 @@ -version_info = (0, 1, 0) -__version__ = ".".join(map(str, version_info)) +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/kernel-output/package.json b/kernel-output/package.json index c5430cff..9002b9df 100644 --- a/kernel-output/package.json +++ b/kernel-output/package.json @@ -1,7 +1,7 @@ { "name": "@jupyterlab-examples/kernel-output", "version": "0.1.0", - "description": "minimal lab example", + "description": "Extension to interact with kernel and display output", "keywords": [ "jupyter", "jupyterlab", @@ -18,8 +18,8 @@ }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "style/**/*.{css,eot,js,gif,html,jpg,json,png,svg,woff2,ttf}", - "schema/**/*.json" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", + "schema/*.json" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -28,57 +28,83 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jupyterlab_examples_kernel_output/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_kernel_output/labextension jupyterlab_examples_kernel_output/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0", - "@jupyterlab/launcher": "^3.1.0", - "@jupyterlab/outputarea": "^3.1.0", - "@jupyterlab/translation": "^3.1.0", - "@lumino/algorithm": "^1.3.3", - "@lumino/coreutils": "^1.5.3", - "@lumino/datagrid": "^0.3.1", - "@lumino/disposable": "^1.4.3" + "@jupyterlab/application": "^4.0.0-beta.0", + "@jupyterlab/launcher": "^4.0.0-beta.0", + "@jupyterlab/outputarea": "^4.0.0-beta.0", + "@jupyterlab/rendermime": "^4.0.0-beta.0", + "@jupyterlab/settingregistry": "^4.0.0-beta.0", + "@jupyterlab/translation": "^4.0.0-beta.0", + "@lumino/messaging": "^2.0.0", + "@lumino/widgets": "^2.0.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "extension": true, "outputDir": "jupyterlab_examples_kernel_output/labextension", "schemaDir": "schema" - }, - "styleModule": "style/index.js" + } } diff --git a/kernel-output/preview.gif b/kernel-output/preview.gif index d8a013a9..838380c0 100644 Binary files a/kernel-output/preview.gif and b/kernel-output/preview.gif differ diff --git a/kernel-output/pyproject.toml b/kernel-output/pyproject.toml index e01c5416..36edad71 100644 --- a/kernel-output/pyproject.toml +++ b/kernel-output/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_kernel_output/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_kernel_output/labextension/static/style.js", "jupyterlab_examples_kernel_output/labextension/package.json"] +[project] +name = "jupyterlab_examples_kernel_output" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_kernel_output/labextension"] +exclude = [".github", "binder"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_kernel_output/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/kernel-output" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/kernel-output/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_kernel_output/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_kernel_output/labextension/static/style.js", + "jupyterlab_examples_kernel_output/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_kernel_output/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_kernel_output/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_kernel_output/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/kernel-output/setup.py b/kernel-output/setup.py index abdca81f..bea23374 100644 --- a/kernel-output/setup.py +++ b/kernel-output/setup.py @@ -1,83 +1 @@ -""" -jupyterlab_examples_kernel_output setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_kernel_output" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/kernel-output" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/kernel-output/src/index.ts b/kernel-output/src/index.ts index da5714d7..f1cbc7ef 100644 --- a/kernel-output/src/index.ts +++ b/kernel-output/src/index.ts @@ -1,6 +1,6 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; import { ICommandPalette, InputDialog } from '@jupyterlab/apputils'; @@ -26,11 +26,11 @@ namespace CommandIDs { * Initialization data for the extension. */ const extension: JupyterFrontEndPlugin = { - id: 'kernel-output', + id: '@jupyterlab-examples/kernel-output:plugin', autoStart: true, optional: [ILauncher], requires: [ICommandPalette, IRenderMimeRegistry, ITranslator], - activate: activate, + activate: activate }; /** @@ -71,7 +71,7 @@ function activate( commands.addCommand(CommandIDs.create, { label: trans.__('Open the Kernel Output Panel'), caption: trans.__('Open the Kernel Output Panel'), - execute: createPanel, + execute: createPanel }); commands.addCommand(CommandIDs.execute, { @@ -86,18 +86,18 @@ function activate( const input = await InputDialog.getText({ title: trans.__('Code to execute'), okLabel: trans.__('Execute'), - placeholder: trans.__('Statement to execute'), + placeholder: trans.__('Statement to execute') }); // Execute the statement if (input.button.accept) { - const code = input.value; + const code = input.value || ''; panel.execute(code); } - }, + } }); // add items in command palette and menu - [CommandIDs.create, CommandIDs.execute].forEach((command) => { + [CommandIDs.create, CommandIDs.execute].forEach(command => { palette.addItem({ command, category }); }); @@ -105,7 +105,7 @@ function activate( if (launcher) { launcher.add({ command: CommandIDs.create, - category: category, + category: category }); } } diff --git a/kernel-output/src/panel.ts b/kernel-output/src/panel.ts index 2c0d6d84..d0c1ef76 100644 --- a/kernel-output/src/panel.ts +++ b/kernel-output/src/panel.ts @@ -1,7 +1,7 @@ import { ISessionContext, SessionContext, - sessionContextDialogs, + SessionContextDialogs } from '@jupyterlab/apputils'; import { OutputAreaModel, SimplifiedOutputArea } from '@jupyterlab/outputarea'; @@ -13,7 +13,7 @@ import { KernelMessage, ServiceManager } from '@jupyterlab/services'; import { ITranslator, nullTranslator, - TranslationBundle, + TranslationBundle } from '@jupyterlab/translation'; import { Message } from '@lumino/messaging'; @@ -45,25 +45,29 @@ export class ExamplePanel extends StackedPanel { this._sessionContext = new SessionContext({ sessionManager: manager.sessions, specsManager: manager.kernelspecs, - name: 'Kernel Output', + name: 'Kernel Output' }); this._outputareamodel = new OutputAreaModel(); this._outputarea = new SimplifiedOutputArea({ model: this._outputareamodel, - rendermime: rendermime, + rendermime: rendermime + }); + + this._sessionContextDialogs = new SessionContextDialogs({ + translator: translator }); this.addWidget(this._outputarea); void this._sessionContext .initialize() - .then(async (value) => { + .then(async value => { if (value) { - await sessionContextDialogs.selectKernel(this._sessionContext); + await this._sessionContextDialogs.selectKernel(this._sessionContext); } }) - .catch((reason) => { + .catch(reason => { console.error( `Failed to initialize the session in ExamplePanel.\n${reason}` ); @@ -81,10 +85,10 @@ export class ExamplePanel extends StackedPanel { execute(code: string): void { SimplifiedOutputArea.execute(code, this._outputarea, this._sessionContext) - .then((msg: KernelMessage.IExecuteReplyMsg) => { + .then((msg: KernelMessage.IExecuteReplyMsg | undefined) => { console.log(msg); }) - .catch((reason) => console.error(reason)); + .catch(reason => console.error(reason)); } protected onCloseRequest(msg: Message): void { @@ -95,6 +99,7 @@ export class ExamplePanel extends StackedPanel { private _sessionContext: SessionContext; private _outputarea: SimplifiedOutputArea; private _outputareamodel: OutputAreaModel; + private _sessionContextDialogs: SessionContextDialogs; private _translator: ITranslator; private _trans: TranslationBundle; diff --git a/kernel-output/style/base.css b/kernel-output/style/base.css index d703e460..2784237c 100644 --- a/kernel-output/style/base.css +++ b/kernel-output/style/base.css @@ -1,3 +1,8 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ .jp-example-view { background-color: AliceBlue; } diff --git a/kernel-output/style/index.css b/kernel-output/style/index.css index 8a7ea29e..e98119b5 100644 --- a/kernel-output/style/index.css +++ b/kernel-output/style/index.css @@ -1 +1 @@ -@import url('base.css'); +@import 'base.css'; diff --git a/kernel-output/tsconfig.json b/kernel-output/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/kernel-output/tsconfig.json +++ b/kernel-output/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/kernel-output/tsconfig.test.json b/kernel-output/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/kernel-output/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/kernel-output/ui-tests/.env b/kernel-output/ui-tests/.env deleted file mode 100644 index 54ff1afa..00000000 --- a/kernel-output/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=kernel-output/jupyterlab_examples_kernel_output -EXT_NAME=kernel-output \ No newline at end of file diff --git a/kernel-output/ui-tests/README.md b/kernel-output/ui-tests/README.md index d2be7291..e091f401 100644 --- a/kernel-output/ui-tests/README.md +++ b/kernel-output/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _kernel-output_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _kernel-output_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _kernel-output_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _kernel-output_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/kernel-output/ui-tests/jupyter_server_test_config.py b/kernel-output/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/kernel-output/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/kernel-output/ui-tests/package.json b/kernel-output/ui-tests/package.json index 4b0e6978..9bb619de 100644 --- a/kernel-output/ui-tests/package.json +++ b/kernel-output/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/kernel-output-tests", - "version": "0.1.0", - "description": "Integration test for kernel-output example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/kernel-output-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/kernel-output 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/kernel-output/ui-tests/playwright.config.js b/kernel-output/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/kernel-output/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/kernel-output/ui-tests/playwright.config.ts b/kernel-output/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/kernel-output/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/kernel-output/ui-tests/tests/kernel-output.spec.ts b/kernel-output/ui-tests/tests/kernel-output.spec.ts index 21fc73c5..1aaea01e 100644 --- a/kernel-output/ui-tests/tests/kernel-output.spec.ts +++ b/kernel-output/ui-tests/tests/kernel-output.spec.ts @@ -4,45 +4,31 @@ test('should open a panel connected to a notebook kernel', async ({ page }) => { test.setTimeout(120000); // Install pandas through console - await page.getByRole('menuitem', { name: 'File' }).click(); - await page.getByRole('listitem').filter({ hasText: 'New' }).click(); - await page.getByRole('menuitem', { name: 'Console', exact: true }).click(); + await page.menu.clickMenuItem('File>New>Console'); await page.getByRole('button', { name: 'Select' }).click(); await page.getByText('| Idle').waitFor(); - await page.fill( - '.jp-CodeConsole-input >> textarea', - '!mamba install -qy pandas' - ); - await page.getByRole('menuitem', { name: 'Run' }).click(); - await page.getByRole('menuitem', { name: 'Run Cell Shift+Enter' }).click(); - await page.waitForSelector('text=Executing transaction: ...working... done'); + await page.click('.jp-CodeConsole-promptCell .jp-InputArea-editor'); + await page.keyboard.type('!mamba install -qy pandas', { delay: 100 }); + + await page.menu.clickMenuItem('Run>Run Cell (forced)'); // Create a notebook - await page.getByRole('menuitem', { name: 'File' }).click(); - await page.getByRole('listitem').filter({ hasText: 'New' }).click(); - await page.getByRole('menuitem', { name: 'Notebook', exact: true }).click(); - await page.getByRole('button', { name: 'Select', exact: true }).click(); + await page.notebook.createNew(); await page.getByText('| Idle').waitFor(); - await page - .getByRole('region', { name: 'notebook content' }) - .locator('.jp-Editor >> textarea') - .fill('import numpy\nimport pandas\ndf = pandas.DataFrame(numpy.eye(5))'); - - await page.getByRole('menuitem', { name: 'Run' }).click(); - await page - .getByRole('menuitem', { name: 'Run Selected Cells Shift+Enter' }) - .click(); + await page.notebook.setCell( + 0, + 'code', + 'import numpy\nimport pandas\ndf = pandas.DataFrame(numpy.eye(5))' + ); + await page.notebook.runCell(0); - await page.getByRole('menuitem', { name: 'Kernel Output' }).click(); - await page - .getByRole('menuitem', { name: 'Open the Kernel Output Panel' }) - .click(); + await page.menu.clickMenuItem('Kernel Output>Open the Kernel Output Panel'); // Select Notebook kernel await page.locator('.jp-Dialog-body').locator('select').selectOption({ - label: 'Untitled.ipynb', + label: 'Untitled.ipynb' }); await page.getByRole('button', { name: 'Select', exact: true }).click(); @@ -71,10 +57,9 @@ test('should open a panel connected to a notebook kernel', async ({ page }) => { .locator('div[role="main"] >> text=Kernel Output Example View') .click(); - await page.getByRole('menuitem', { name: 'Kernel Output' }).click(); - await page - .getByRole('menuitem', { name: 'Contact Kernel and Execute Code' }) - .click(); + await page.menu.clickMenuItem( + 'Kernel Output>Contact Kernel and Execute Code' + ); // Fill [placeholder="Statement to execute"] await page.locator('[placeholder="Statement to execute"]').fill('df'); @@ -84,10 +69,9 @@ test('should open a panel connected to a notebook kernel', async ({ page }) => { await expect.soft(page.locator('th')).toHaveCount(11); // Close filebrowser - await page.getByText('View', { exact: true }).click(); await Promise.all([ page.locator('#filebrowser').waitFor({ state: 'hidden' }), - page.getByRole('menuitem', { name: 'Show Left Sidebar Ctrl+B' }).click(), + page.menu.clickMenuItem('View>File Browser') ]); // Compare screenshot with a stored reference. diff --git a/kernel-output/ui-tests/tests/kernel-output.spec.ts-snapshots/kernel-output-example-linux.png b/kernel-output/ui-tests/tests/kernel-output.spec.ts-snapshots/kernel-output-example-linux.png index 5c61da91..e215998e 100644 Binary files a/kernel-output/ui-tests/tests/kernel-output.spec.ts-snapshots/kernel-output-example-linux.png and b/kernel-output/ui-tests/tests/kernel-output.spec.ts-snapshots/kernel-output-example-linux.png differ diff --git a/launcher/.eslintrc.js b/launcher/.eslintrc.js index c2375786..665374bf 100644 --- a/launcher/.eslintrc.js +++ b/launcher/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/launcher/.gitignore b/launcher/.gitignore index 18cace13..ef3ace98 100644 --- a/launcher/.gitignore +++ b/launcher/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/launcher/.prettierignore b/launcher/.prettierignore new file mode 100644 index 00000000..07c8e553 --- /dev/null +++ b/launcher/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_launcher diff --git a/launcher/.prettierrc b/launcher/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/launcher/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/launcher/.stylelintrc b/launcher/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/launcher/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/launcher/.yarnrc.yml b/launcher/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/launcher/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/launcher/CHANGELOG.md b/launcher/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/launcher/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/launcher/LICENSE b/launcher/LICENSE index 5df00610..54d885af 100644 --- a/launcher/LICENSE +++ b/launcher/LICENSE @@ -1,7 +1,6 @@ BSD 3-Clause License -Copyright (c) 2019, Jeremy Tuloup -Copyright (c) 2019, Jupyter Development Team +Copyright (c) 2023, Project Jupyter Contributors All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/launcher/MANIFEST.in b/launcher/MANIFEST.in deleted file mode 100644 index 0551ee1f..00000000 --- a/launcher/MANIFEST.in +++ /dev/null @@ -1,22 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml -include jupyter-config/jupyterlab_examples_launcher.json - -include package.json -include ts*.json - -graft jupyterlab_examples_launcher/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/launcher/README.md b/launcher/README.md index 1b2f3066..03961c45 100644 --- a/launcher/README.md +++ b/launcher/README.md @@ -19,31 +19,34 @@ The command will create a new Python file and then open it: ```ts -// src/index.ts#L41-L64 +// src/index.ts#L38-L64 commands.addCommand(command, { - label: (args) => (args['isPalette'] ? 'New Python File' : 'Python File'), + label: args => + args['isPalette'] + ? 'New Python File From Extension' + : 'Python File From Extension', caption: 'Create a new Python file', - icon: (args) => (args['isPalette'] ? null : icon), - execute: async (args) => { + icon: args => (args['isPalette'] ? undefined : icon), + execute: async args => { // Get the directory in which the Python file must be created; // otherwise take the current filebrowser directory const cwd = - args['cwd'] || browserFactory.tracker.currentWidget.model.path; + args['cwd'] || browserFactory.tracker.currentWidget?.model.path; // Create a new untitled python file const model = await commands.execute('docmanager:new-untitled', { path: cwd, type: 'file', - ext: 'py', + ext: 'py' }); // Open the newly created file with the 'Editor' return commands.execute('docmanager:open', { path: model.path, - factory: FACTORY, + factory: FACTORY }); - }, + } }); ``` @@ -58,7 +61,7 @@ jlpm add @jupyterlab/launcher Then you can use it in the extension by importing it: ```ts -// src/index.ts#L10-L10 +// src/index.ts#L7-L7 import { ILauncher } from '@jupyterlab/launcher'; ``` @@ -66,13 +69,13 @@ import { ILauncher } from '@jupyterlab/launcher'; And finally you can add it to the list of dependencies: ```ts -// src/index.ts#L23-L33 +// src/index.ts#L20-L30 const extension: JupyterFrontEndPlugin = { - id: 'launcher', + id: '@jupyterlab-examples/launcher:plugin', autoStart: true, requires: [IFileBrowserFactory], - optional: [ILauncher, ICommandPalette], + optional: [ILauncher, ICommandPalette, ISettingRegistry], activate: ( app: JupyterFrontEnd, browserFactory: IFileBrowserFactory, @@ -99,7 +102,7 @@ if (launcher) { launcher.add({ command, category: 'Extension Examples', - rank: 1, + rank: 1 }); } ``` diff --git a/launcher/RELEASE.md b/launcher/RELEASE.md index dcb88139..f7f55df2 100644 --- a/launcher/RELEASE.md +++ b/launcher/RELEASE.md @@ -1,6 +1,49 @@ # Making a new release of jupyterlab_examples_launcher -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python +packages. All of the Python +packaging instructions in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, we first need to install `build`. + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser diff --git a/launcher/babel.config.js b/launcher/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/launcher/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/launcher/jest.config.js b/launcher/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/launcher/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/launcher/jupyterlab_examples_launcher/__init__.py b/launcher/jupyterlab_examples_launcher/__init__.py index 353ab522..b854a59e 100644 --- a/launcher/jupyterlab_examples_launcher/__init__.py +++ b/launcher/jupyterlab_examples_launcher/__init__.py @@ -1,19 +1,7 @@ - -import json -import os.path as osp - from ._version import __version__ -HERE = osp.abspath(osp.dirname(__file__)) - -with open(osp.join(HERE, 'labextension', 'package.json')) as fid: - data = json.load(fid) - def _jupyter_labextension_paths(): return [{ - 'src': 'labextension', - 'dest': data['name'] + "src": "labextension", + "dest": "@jupyterlab-examples/launcher" }] - - - diff --git a/launcher/jupyterlab_examples_launcher/_version.py b/launcher/jupyterlab_examples_launcher/_version.py index ee864fc9..133868ac 100644 --- a/launcher/jupyterlab_examples_launcher/_version.py +++ b/launcher/jupyterlab_examples_launcher/_version.py @@ -1,2 +1,4 @@ -version_info = (0, 1, 0) -__version__ = ".".join(map(str, version_info)) +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/launcher/package.json b/launcher/package.json index 82a5a1cc..9f67d29b 100644 --- a/launcher/package.json +++ b/launcher/package.json @@ -28,53 +28,80 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jupyterlab_examples_launcher/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_launcher/labextension jupyterlab_examples_launcher/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0", - "@jupyterlab/filebrowser": "^3.1.0", - "@jupyterlab/launcher": "^3.1.0" + "@jupyterlab/application": "^4.0.0-beta.0", + "@jupyterlab/filebrowser": "^4.0.0-beta.0", + "@jupyterlab/launcher": "^4.0.0-beta.0", + "@jupyterlab/settingregistry": "^4.0.0-beta.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/*.svg", "style/index.js" ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "extension": true, "outputDir": "jupyterlab_examples_launcher/labextension", "schemaDir": "schema" - }, - "styleModule": "style/index.js" + } } diff --git a/launcher/preview.gif b/launcher/preview.gif index 0f9558a4..189d31be 100644 Binary files a/launcher/preview.gif and b/launcher/preview.gif differ diff --git a/launcher/pyproject.toml b/launcher/pyproject.toml index 8445c8fe..0afa3b17 100644 --- a/launcher/pyproject.toml +++ b/launcher/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_launcher/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_launcher/labextension/static/style.js", "jupyterlab_examples_launcher/labextension/package.json"] +[project] +name = "jupyterlab_examples_launcher" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_launcher/labextension"] +exclude = [".github", "binder"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_launcher/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/launcher" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/launcher/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_launcher/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_launcher/labextension/static/style.js", + "jupyterlab_examples_launcher/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_launcher/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_launcher/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_launcher/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/launcher/schema/plugin.json b/launcher/schema/plugin.json index dba59271..143ed597 100644 --- a/launcher/schema/plugin.json +++ b/launcher/schema/plugin.json @@ -8,13 +8,15 @@ "items": [ { "type": "submenu", - "id": "jp-mainmenu-file-new", - "items": [ - { - "command": "jlab-examples:create-new-python-file", - "rank": 30 - } - ] + "submenu": { + "id": "jp-mainmenu-file-new", + "items": [ + { + "command": "jlab-examples:create-new-python-file", + "rank": 80 + } + ] + } } ] } diff --git a/launcher/setup.py b/launcher/setup.py index 416e968f..bea23374 100644 --- a/launcher/setup.py +++ b/launcher/setup.py @@ -1,83 +1 @@ -""" -jupyterlab_examples_launcher setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_launcher" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/launcher" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/launcher/src/index.ts b/launcher/src/index.ts index 0b931c22..de598479 100644 --- a/launcher/src/index.ts +++ b/launcher/src/index.ts @@ -1,14 +1,11 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; - import { ICommandPalette } from '@jupyterlab/apputils'; - import { IFileBrowserFactory } from '@jupyterlab/filebrowser'; - import { ILauncher } from '@jupyterlab/launcher'; - +import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { LabIcon } from '@jupyterlab/ui-components'; import pythonIconStr from '../style/Python-logo-notext.svg'; @@ -21,10 +18,10 @@ namespace CommandIDs { } const extension: JupyterFrontEndPlugin = { - id: 'launcher', + id: '@jupyterlab-examples/launcher:plugin', autoStart: true, requires: [IFileBrowserFactory], - optional: [ILauncher, ICommandPalette], + optional: [ILauncher, ICommandPalette, ISettingRegistry], activate: ( app: JupyterFrontEnd, browserFactory: IFileBrowserFactory, @@ -35,32 +32,35 @@ const extension: JupyterFrontEndPlugin = { const command = CommandIDs.createNew; const icon = new LabIcon({ name: 'launcher:python-icon', - svgstr: pythonIconStr, + svgstr: pythonIconStr }); commands.addCommand(command, { - label: (args) => (args['isPalette'] ? 'New Python File' : 'Python File'), + label: args => + args['isPalette'] + ? 'New Python File From Extension' + : 'Python File From Extension', caption: 'Create a new Python file', - icon: (args) => (args['isPalette'] ? null : icon), - execute: async (args) => { + icon: args => (args['isPalette'] ? undefined : icon), + execute: async args => { // Get the directory in which the Python file must be created; // otherwise take the current filebrowser directory const cwd = - args['cwd'] || browserFactory.tracker.currentWidget.model.path; + args['cwd'] || browserFactory.tracker.currentWidget?.model.path; // Create a new untitled python file const model = await commands.execute('docmanager:new-untitled', { path: cwd, type: 'file', - ext: 'py', + ext: 'py' }); // Open the newly created file with the 'Editor' return commands.execute('docmanager:open', { path: model.path, - factory: FACTORY, + factory: FACTORY }); - }, + } }); // Add the command to the launcher @@ -68,7 +68,7 @@ const extension: JupyterFrontEndPlugin = { launcher.add({ command, category: 'Extension Examples', - rank: 1, + rank: 1 }); } @@ -77,10 +77,10 @@ const extension: JupyterFrontEndPlugin = { palette.addItem({ command, args: { isPalette: true }, - category: PALETTE_CATEGORY, + category: PALETTE_CATEGORY }); } - }, + } }; export default extension; diff --git a/launcher/style/base.css b/launcher/style/base.css index e69de29b..e11f4577 100644 --- a/launcher/style/base.css +++ b/launcher/style/base.css @@ -0,0 +1,5 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ diff --git a/launcher/style/index.css b/launcher/style/index.css index 8a7ea29e..e98119b5 100644 --- a/launcher/style/index.css +++ b/launcher/style/index.css @@ -1 +1 @@ -@import url('base.css'); +@import 'base.css'; diff --git a/launcher/tsconfig.json b/launcher/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/launcher/tsconfig.json +++ b/launcher/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/launcher/tsconfig.test.json b/launcher/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/launcher/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/launcher/ui-tests/.env b/launcher/ui-tests/.env deleted file mode 100644 index f224d04c..00000000 --- a/launcher/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=launcher/jupyterlab_examples_launcher -EXT_NAME=launcher \ No newline at end of file diff --git a/launcher/ui-tests/README.md b/launcher/ui-tests/README.md index d2be7291..f8210484 100644 --- a/launcher/ui-tests/README.md +++ b/launcher/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _launcher_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _launcher_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _launcher_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _launcher_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/launcher/ui-tests/jupyter_server_test_config.py b/launcher/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/launcher/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/launcher/ui-tests/package.json b/launcher/ui-tests/package.json index dbdcd530..a4e2740a 100644 --- a/launcher/ui-tests/package.json +++ b/launcher/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/launcher-tests", - "version": "0.1.0", - "description": "Integration test for launcher example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/launcher-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/launcher 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/launcher/ui-tests/playwright.config.js b/launcher/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/launcher/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/launcher/ui-tests/playwright.config.ts b/launcher/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/launcher/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/launcher/ui-tests/tests/launcher.spec.ts b/launcher/ui-tests/tests/launcher.spec.ts index aacee174..1bd4d53b 100644 --- a/launcher/ui-tests/tests/launcher.spec.ts +++ b/launcher/ui-tests/tests/launcher.spec.ts @@ -9,3 +9,11 @@ test('should add a card to create Python file', async ({ page }) => { // Click div[role="main"] >> text=untitled.py await page.click('div[role="main"] >> text=untitled.py'); }); + +test('should add a submenu in jp-mainmenu-file-new', async ({ page }) => { + // Click on the menu item + await page.menu.clickMenuItem('File>New>Python File From Extension'); + + // Click div[role="main"] >> text=untitled.py + await page.click('div[role="main"] >> text=untitled.py'); +}); diff --git a/log-messages/.eslintrc.js b/log-messages/.eslintrc.js index c2375786..665374bf 100644 --- a/log-messages/.eslintrc.js +++ b/log-messages/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/log-messages/.gitignore b/log-messages/.gitignore index 18cace13..ef3ace98 100644 --- a/log-messages/.gitignore +++ b/log-messages/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/log-messages/.prettierignore b/log-messages/.prettierignore new file mode 100644 index 00000000..05a34af8 --- /dev/null +++ b/log-messages/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_log_messages diff --git a/log-messages/.prettierrc b/log-messages/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/log-messages/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/log-messages/.stylelintrc b/log-messages/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/log-messages/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/log-messages/.yarnrc.yml b/log-messages/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/log-messages/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/log-messages/CHANGELOG.md b/log-messages/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/log-messages/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/log-messages/LICENSE b/log-messages/LICENSE new file mode 100644 index 00000000..54d885af --- /dev/null +++ b/log-messages/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/log-messages/MANIFEST.in b/log-messages/MANIFEST.in deleted file mode 100644 index 37ebc43a..00000000 --- a/log-messages/MANIFEST.in +++ /dev/null @@ -1,22 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml -include jupyter-config/jupyterlab_examples_log_messages.json - -include package.json -include ts*.json - -graft jupyterlab_examples_log_messages/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/log-messages/README.md b/log-messages/README.md index 0234383c..17845c76 100644 --- a/log-messages/README.md +++ b/log-messages/README.md @@ -23,12 +23,13 @@ First of all, you will start looking into the declaration of the extension: ```ts -// src/index.ts#L8-L16 +// src/index.ts#L8-L17 + const extension: JupyterFrontEndPlugin = { - id: 'log-messages', + id: '@jupyterlab-examples/log-messages:plugin', autoStart: true, - requires: [ILoggerRegistry, INotebookTracker], + requires: [ILoggerRegistry, INotebookTracker, ISettingRegistry], activate: ( app: JupyterFrontEnd, loggerRegistry: ILoggerRegistry, @@ -46,7 +47,7 @@ The first step is to obtain the logger of the active notebook. You can use `logg // src/index.ts#L23-L25 const logger = loggerRegistry.getLogger( - nbtracker.currentWidget?.context.path + nbtracker.currentWidget?.context.path || '' ); ``` @@ -60,7 +61,7 @@ Finally, you can send log messages by calling the `log` method of the `logger` o const msg: ITextLog = { type: 'text', level: 'info', - data: 'Hello world text!!', + data: 'Hello world text!!' }; logger?.log(msg); diff --git a/log-messages/RELEASE.md b/log-messages/RELEASE.md index 7ef7bd84..14f61018 100644 --- a/log-messages/RELEASE.md +++ b/log-messages/RELEASE.md @@ -1,22 +1,62 @@ # Making a new release of jupyterlab_examples_log_messages -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser The extension repository should already be compatible with the Jupyter Releaser. -Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. Here is a summary of the steps to cut a new release: -- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) -- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository - Go to the Actions panel -- Run the "Draft Changelog" workflow -- Merge the Changelog PR -- Run the "Draft Release" workflow -- Run the "Publish Release" workflow +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow ## Publishing to `conda-forge` diff --git a/log-messages/babel.config.js b/log-messages/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/log-messages/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/log-messages/jest.config.js b/log-messages/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/log-messages/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/log-messages/jupyterlab_examples_log_messages/__init__.py b/log-messages/jupyterlab_examples_log_messages/__init__.py index 353ab522..5f4782d9 100644 --- a/log-messages/jupyterlab_examples_log_messages/__init__.py +++ b/log-messages/jupyterlab_examples_log_messages/__init__.py @@ -1,19 +1,7 @@ - -import json -import os.path as osp - from ._version import __version__ -HERE = osp.abspath(osp.dirname(__file__)) - -with open(osp.join(HERE, 'labextension', 'package.json')) as fid: - data = json.load(fid) - def _jupyter_labextension_paths(): return [{ - 'src': 'labextension', - 'dest': data['name'] + "src": "labextension", + "dest": "@jupyterlab-examples/log-messages" }] - - - diff --git a/log-messages/jupyterlab_examples_log_messages/_version.py b/log-messages/jupyterlab_examples_log_messages/_version.py index ee864fc9..133868ac 100644 --- a/log-messages/jupyterlab_examples_log_messages/_version.py +++ b/log-messages/jupyterlab_examples_log_messages/_version.py @@ -1,2 +1,4 @@ -version_info = (0, 1, 0) -__version__ = ".".join(map(str, version_info)) +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/log-messages/package.json b/log-messages/package.json index 124584f5..5cd43d87 100644 --- a/log-messages/package.json +++ b/log-messages/package.json @@ -18,7 +18,7 @@ }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "style/**/*.{css,eot,js,gif,html,jpg,json,png,svg,woff2,ttf}", + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", "schema/**/*.json" ], "main": "lib/index.js", @@ -28,59 +28,88 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jupyterlab_examples_log_messages/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_log_messages/labextension jupyterlab_examples_log_messages/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0", - "@jupyterlab/apputils": "^3.1.0", - "@jupyterlab/coreutils": "^5.1.0", - "@jupyterlab/logconsole": "^3.1.0", - "@jupyterlab/nbformat": "^3.1.0", - "@jupyterlab/notebook": "^3.1.0", - "@jupyterlab/rendermime": "^3.1.0", - "@jupyterlab/ui-components": "^3.1.0", - "@lumino/coreutils": "^1.5.3", - "@lumino/widgets": "^1.19.0" + "@jupyterlab/application": "^4.0.0-beta.0", + "@jupyterlab/apputils": "^4.0.0-beta.0", + "@jupyterlab/coreutils": "^6.0.0-beta.0", + "@jupyterlab/filebrowser": "^4.0.0-beta.0", + "@jupyterlab/logconsole": "^4.0.0-beta.0", + "@jupyterlab/mainmenu": "^4.0.0-beta.0", + "@jupyterlab/nbformat": "^4.0.0-beta.0", + "@jupyterlab/notebook": "^4.0.0-beta.0", + "@jupyterlab/rendermime": "^4.0.0-beta.0", + "@jupyterlab/settingregistry": "^4.0.0-beta.0", + "@jupyterlab/ui-components": "^4.0.0-beta.0", + "@lumino/coreutils": "^2.0.0", + "@lumino/widgets": "^2.0.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "extension": true, "outputDir": "jupyterlab_examples_log_messages/labextension", "schemaDir": "schema" - }, - "styleModule": "style/index.js" + } } diff --git a/log-messages/preview.gif b/log-messages/preview.gif index 1d721fa4..d38f3b4f 100644 Binary files a/log-messages/preview.gif and b/log-messages/preview.gif differ diff --git a/log-messages/pyproject.toml b/log-messages/pyproject.toml index 5627f269..72fa6237 100644 --- a/log-messages/pyproject.toml +++ b/log-messages/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_log_messages/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_log_messages/labextension/static/style.js", "jupyterlab_examples_log_messages/labextension/package.json"] +[project] +name = "jupyterlab_examples_log_messages" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_log_messages/labextension"] +exclude = [".github", "binder"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_log_messages/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/log-messages" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/log-messages/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_log_messages/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_log_messages/labextension/static/style.js", + "jupyterlab_examples_log_messages/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_log_messages/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_log_messages/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_log_messages/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/log-messages/setup.py b/log-messages/setup.py index a82d4dc3..bea23374 100644 --- a/log-messages/setup.py +++ b/log-messages/setup.py @@ -1,83 +1 @@ -""" -jupyterlab_examples_log_messages setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_log_messages" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/log-messages" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/log-messages/src/index.ts b/log-messages/src/index.ts index 3163cc65..6f104873 100644 --- a/log-messages/src/index.ts +++ b/log-messages/src/index.ts @@ -1,40 +1,40 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; import { ILoggerRegistry, ITextLog } from '@jupyterlab/logconsole'; import { INotebookTracker } from '@jupyterlab/notebook'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; const extension: JupyterFrontEndPlugin = { - id: 'log-messages', + id: '@jupyterlab-examples/log-messages:plugin', autoStart: true, - requires: [ILoggerRegistry, INotebookTracker], + requires: [ILoggerRegistry, INotebookTracker, ISettingRegistry], activate: ( app: JupyterFrontEnd, loggerRegistry: ILoggerRegistry, nbtracker: INotebookTracker ) => { const { commands } = app; - commands.addCommand('jlab-examples/log-messages:logTextMessage', { label: 'Text log message', caption: 'Custom text log message example.', execute: () => { const logger = loggerRegistry.getLogger( - nbtracker.currentWidget?.context.path + nbtracker.currentWidget?.context.path || '' ); console.log(logger); const msg: ITextLog = { type: 'text', level: 'info', - data: 'Hello world text!!', + data: 'Hello world text!!' }; logger?.log(msg); - }, + } }); - }, + } }; export default extension; diff --git a/log-messages/style/base.css b/log-messages/style/base.css index e69de29b..e11f4577 100644 --- a/log-messages/style/base.css +++ b/log-messages/style/base.css @@ -0,0 +1,5 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ diff --git a/log-messages/style/index.css b/log-messages/style/index.css index 8a7ea29e..e98119b5 100644 --- a/log-messages/style/index.css +++ b/log-messages/style/index.css @@ -1 +1 @@ -@import url('base.css'); +@import 'base.css'; diff --git a/log-messages/tsconfig.json b/log-messages/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/log-messages/tsconfig.json +++ b/log-messages/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/log-messages/tsconfig.test.json b/log-messages/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/log-messages/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/log-messages/ui-tests/.env b/log-messages/ui-tests/.env deleted file mode 100644 index eced602c..00000000 --- a/log-messages/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=log-messages/jupyterlab_examples_log_messages -EXT_NAME=log-messages \ No newline at end of file diff --git a/log-messages/ui-tests/README.md b/log-messages/ui-tests/README.md index d2be7291..6411354f 100644 --- a/log-messages/ui-tests/README.md +++ b/log-messages/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _log-messages_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _log-messages_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _log-messages_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _log-messages_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/log-messages/ui-tests/jupyter_server_test_config.py b/log-messages/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/log-messages/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/log-messages/ui-tests/package.json b/log-messages/ui-tests/package.json index 0cf45497..d2d6a09c 100644 --- a/log-messages/ui-tests/package.json +++ b/log-messages/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/log-messages-tests", - "version": "0.1.0", - "description": "Integration test for log-messages example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/log-messages-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/log-messages 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/log-messages/ui-tests/playwright.config.js b/log-messages/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/log-messages/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/log-messages/ui-tests/playwright.config.ts b/log-messages/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/log-messages/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/log-messages/ui-tests/tests/log-messages.spec.ts b/log-messages/ui-tests/tests/log-messages.spec.ts index 0bb1fd32..d38eec00 100644 --- a/log-messages/ui-tests/tests/log-messages.spec.ts +++ b/log-messages/ui-tests/tests/log-messages.spec.ts @@ -1,38 +1,35 @@ import { test, expect } from '@jupyterlab/galata'; test('should capture log messages in dedicated panel', async ({ page }) => { - // Click File menu - await page.click('text=File'); - // Click ul[role="menu"] >> text=New - await page.click('ul[role="menu"] >> text=New'); - // Click #jp-mainmenu-file-new >> text=Notebook - await page.click('#jp-mainmenu-file-new >> text=Notebook'); + // Open a new Notebook + await page.menu.clickMenuItem('File>New>Notebook'); // Click button:has-text("Select") await page.click('button:has-text("Select")'); - // Click text=View - await page.click('text=View'); - // Click ul[role="menu"] >> text=Show Log Console - await page.click('ul[role="menu"] >> text=Show Log Console'); + // Open the console + await page.menu.clickMenuItem('View>Show Log Console'); // Drag and drop the split to display a bigger log panel. - const splitHandle = await page.$('div.lm-SplitPanel-handle'); - const splitHandleBBox = await splitHandle.boundingBox(); + // const splitHandle = await page.$('div.lm-SplitPanel-handle'); + const splitHandle = await page.$( + '#jp-main-split-panel > div.lm-SplitPanel-handle:not(.lm-mod-hidden)' + ); + const splitHandleBBox = await splitHandle!.boundingBox(); await page.mouse.move( - splitHandleBBox.x + 0.5 * splitHandleBBox.width, - splitHandleBBox.y + 0.5 + splitHandleBBox.height + splitHandleBBox!.x + 0.5 * splitHandleBBox!.width, + splitHandleBBox!.y + 0.5 + splitHandleBBox!.height ); await page.mouse.down(); await page.mouse.move( - splitHandleBBox.x + 0.5 * splitHandleBBox.width, - splitHandleBBox.y + 0.5 + splitHandleBBox.height - 200 + splitHandleBBox!.x + 0.5 * splitHandleBBox!.width, + splitHandleBBox!.y + 0.5 + splitHandleBBox!.height - 200 ); await page.mouse.up(); - // Click text=Log Messages Example - await page.click('text=Log Messages Example'); - // Click text=Text log message - await page.click('text=Text log message'); + await page.pause(); + + // Click the log message menu entry + await page.menu.clickMenuItem('Log Messages Example>Text log message'); let failed = true; try { @@ -45,10 +42,8 @@ test('should capture log messages in dedicated panel', async ({ page }) => { // Select debug await page.selectOption('[aria-label="Log level"]', 'debug'); - // Click text=Log Messages Example - await page.click('text=Log Messages Example'); - // Click text=Text log message - await page.click('text=Text log message'); + // Click the log message menu entry + await page.menu.clickMenuItem('Log Messages Example>Text log message'); expect(await page.waitForSelector('text=Hello world text!!')).toBeTruthy(); diff --git a/main-menu/.eslintrc.js b/main-menu/.eslintrc.js index c2375786..665374bf 100644 --- a/main-menu/.eslintrc.js +++ b/main-menu/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/main-menu/.gitignore b/main-menu/.gitignore index 18cace13..ef3ace98 100644 --- a/main-menu/.gitignore +++ b/main-menu/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/main-menu/.prettierignore b/main-menu/.prettierignore new file mode 100644 index 00000000..ab2a14bc --- /dev/null +++ b/main-menu/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_main_menu diff --git a/main-menu/.prettierrc b/main-menu/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/main-menu/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/main-menu/.stylelintrc b/main-menu/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/main-menu/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/main-menu/.yarnrc.yml b/main-menu/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/main-menu/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/main-menu/CHANGELOG.md b/main-menu/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/main-menu/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/main-menu/LICENSE b/main-menu/LICENSE new file mode 100644 index 00000000..54d885af --- /dev/null +++ b/main-menu/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/main-menu/MANIFEST.in b/main-menu/MANIFEST.in deleted file mode 100644 index 1a2c9e9a..00000000 --- a/main-menu/MANIFEST.in +++ /dev/null @@ -1,22 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml -include jupyter-config/jupyterlab_examples_main_menu.json - -include package.json -include ts*.json - -graft jupyterlab_examples_main_menu/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/main-menu/README.md b/main-menu/README.md index 74ae832a..b99d9a25 100644 --- a/main-menu/README.md +++ b/main-menu/README.md @@ -32,7 +32,7 @@ commands.addCommand(command, { window.alert( `jlab-examples:main-menu has been called ${args['origin']}.` ); - }, + } }); ``` @@ -71,7 +71,7 @@ The creation of a settings file is described in the [settings example](../settin -Main menu can be added and edited through the `main` property of the special +Main menu can be added and edited through the `main` property of the special key `jupyter.lab.menus`. That property accepts a list of menus; each item will have an entry in the main menu bar. @@ -111,6 +111,19 @@ The list of default menu `id`s is available in the [documentation](https://jupyt > See also the [documentation](https://jupyterlab.readthedocs.io/en/stable/extension/extension_points.html#settings-defined-menu). +**WARNING** The extension id must contain the package name and the schema file name: + + +```ts +// src/index.ts#L12-L12 + +id: '@jupyterlab-examples/main-menu:plugin', +``` + + +- `@jupyterlab-examples/main-menu` is the package name in `package.json` file +- `:plugin` come from the schema file `schema/plugin.json` + With this extension installed, a new menu _Main Menu Example_ should be present. And when clicking on the menu item _jlab-examples:main-menu_, the following text should appear in the web browser console. diff --git a/main-menu/RELEASE.md b/main-menu/RELEASE.md index df664801..346c986f 100644 --- a/main-menu/RELEASE.md +++ b/main-menu/RELEASE.md @@ -1,22 +1,62 @@ # Making a new release of jupyterlab_examples_main_menu -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser The extension repository should already be compatible with the Jupyter Releaser. -Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. Here is a summary of the steps to cut a new release: -- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) -- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository - Go to the Actions panel -- Run the "Draft Changelog" workflow -- Merge the Changelog PR -- Run the "Draft Release" workflow -- Run the "Publish Release" workflow +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow ## Publishing to `conda-forge` diff --git a/main-menu/babel.config.js b/main-menu/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/main-menu/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/main-menu/jest.config.js b/main-menu/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/main-menu/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/main-menu/jupyterlab_examples_main_menu/__init__.py b/main-menu/jupyterlab_examples_main_menu/__init__.py index 353ab522..1c80509f 100644 --- a/main-menu/jupyterlab_examples_main_menu/__init__.py +++ b/main-menu/jupyterlab_examples_main_menu/__init__.py @@ -1,19 +1,7 @@ - -import json -import os.path as osp - from ._version import __version__ -HERE = osp.abspath(osp.dirname(__file__)) - -with open(osp.join(HERE, 'labextension', 'package.json')) as fid: - data = json.load(fid) - def _jupyter_labextension_paths(): return [{ - 'src': 'labextension', - 'dest': data['name'] + "src": "labextension", + "dest": "@jupyterlab-examples/main-menu" }] - - - diff --git a/main-menu/jupyterlab_examples_main_menu/_version.py b/main-menu/jupyterlab_examples_main_menu/_version.py index ee864fc9..133868ac 100644 --- a/main-menu/jupyterlab_examples_main_menu/_version.py +++ b/main-menu/jupyterlab_examples_main_menu/_version.py @@ -1,2 +1,4 @@ -version_info = (0, 1, 0) -__version__ = ".".join(map(str, version_info)) +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/main-menu/package.json b/main-menu/package.json index 7118055c..e5d64950 100644 --- a/main-menu/package.json +++ b/main-menu/package.json @@ -18,8 +18,8 @@ }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "style/**/*.{css,eot,js,gif,html,jpg,json,png,svg,woff2,ttf}", - "schema/**/*.json" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", + "schema/*.json" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -28,51 +28,77 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jupyterlab_examples_main_menu/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_main_menu/labextension jupyterlab_examples_main_menu/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0", - "@jupyterlab/apputils": "^3.1.0" + "@jupyterlab/application": "^4.0.0-beta.0", + "@jupyterlab/settingregistry": "^4.0.0-beta.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "extension": true, "outputDir": "jupyterlab_examples_main_menu/labextension", "schemaDir": "schema" - }, - "styleModule": "style/index.js" + } } diff --git a/main-menu/preview.png b/main-menu/preview.png index 5b652eeb..d8c8948f 100644 Binary files a/main-menu/preview.png and b/main-menu/preview.png differ diff --git a/main-menu/pyproject.toml b/main-menu/pyproject.toml index e77152ac..a874cf99 100644 --- a/main-menu/pyproject.toml +++ b/main-menu/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_main_menu/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_main_menu/labextension/static/style.js", "jupyterlab_examples_main_menu/labextension/package.json"] +[project] +name = "jupyterlab_examples_main_menu" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_main_menu/labextension"] +exclude = [".github", "binder"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_main_menu/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/main-menu" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/main-menu/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_main_menu/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_main_menu/labextension/static/style.js", + "jupyterlab_examples_main_menu/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_main_menu/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_main_menu/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_main_menu/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/main-menu/setup.py b/main-menu/setup.py index 0aad1baa..bea23374 100644 --- a/main-menu/setup.py +++ b/main-menu/setup.py @@ -1,83 +1 @@ -""" -jupyterlab_examples_main_menu setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_main_menu" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/main-menu" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/main-menu/src/index.ts b/main-menu/src/index.ts index 3646c8f8..3b592b1d 100644 --- a/main-menu/src/index.ts +++ b/main-menu/src/index.ts @@ -1,6 +1,6 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; import { ICommandPalette } from '@jupyterlab/apputils'; @@ -9,7 +9,7 @@ import { ICommandPalette } from '@jupyterlab/apputils'; * Initialization data for the main menu example. */ const extension: JupyterFrontEndPlugin = { - id: 'main-menu', + id: '@jupyterlab-examples/main-menu:plugin', autoStart: true, requires: [ICommandPalette], activate: (app: JupyterFrontEnd, palette: ICommandPalette) => { @@ -27,7 +27,7 @@ const extension: JupyterFrontEndPlugin = { window.alert( `jlab-examples:main-menu has been called ${args['origin']}.` ); - }, + } }); // Add the command to the command palette @@ -35,9 +35,9 @@ const extension: JupyterFrontEndPlugin = { palette.addItem({ command, category, - args: { origin: 'from the palette' }, + args: { origin: 'from the palette' } }); - }, + } }; export default extension; diff --git a/main-menu/style/base.css b/main-menu/style/base.css index e69de29b..e11f4577 100644 --- a/main-menu/style/base.css +++ b/main-menu/style/base.css @@ -0,0 +1,5 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ diff --git a/main-menu/style/index.css b/main-menu/style/index.css index 8a7ea29e..e98119b5 100644 --- a/main-menu/style/index.css +++ b/main-menu/style/index.css @@ -1 +1 @@ -@import url('base.css'); +@import 'base.css'; diff --git a/main-menu/tsconfig.json b/main-menu/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/main-menu/tsconfig.json +++ b/main-menu/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/main-menu/tsconfig.test.json b/main-menu/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/main-menu/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/main-menu/ui-tests/.env b/main-menu/ui-tests/.env deleted file mode 100644 index 93354cc9..00000000 --- a/main-menu/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=main-menu/jupyterlab_examples_main_menu -EXT_NAME=main-menu \ No newline at end of file diff --git a/main-menu/ui-tests/README.md b/main-menu/ui-tests/README.md index d2be7291..1af3d85d 100644 --- a/main-menu/ui-tests/README.md +++ b/main-menu/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _main-menu_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _main-menu_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _main-menu_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _main-menu_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/main-menu/ui-tests/jupyter_server_test_config.py b/main-menu/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/main-menu/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/main-menu/ui-tests/package.json b/main-menu/ui-tests/package.json index cbc66272..28ab7ade 100644 --- a/main-menu/ui-tests/package.json +++ b/main-menu/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/main-menu-tests", - "version": "0.1.0", - "description": "Integration test for main-menu example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/main-menu-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/main-menu 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/main-menu/ui-tests/playwright.config.js b/main-menu/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/main-menu/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/main-menu/ui-tests/playwright.config.ts b/main-menu/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/main-menu/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/main-menu/ui-tests/tests/main-menu.spec.ts b/main-menu/ui-tests/tests/main-menu.spec.ts index 2563f104..15a3d9cc 100644 --- a/main-menu/ui-tests/tests/main-menu.spec.ts +++ b/main-menu/ui-tests/tests/main-menu.spec.ts @@ -1,20 +1,17 @@ import { test, expect } from '@jupyterlab/galata'; test('should emit a message in a dialog when menu is triggered', async ({ - page, + page }) => { const logs: string[] = []; - page.on('console', (message) => { + page.on('console', message => { logs.push(message.text()); }); - // Click text=Main Menu Example - await page.click('text=Main Menu Example'); - // Add listener to check alert message // > Alert are not capture by the recording - page.once('dialog', async (dialog) => { + page.once('dialog', async dialog => { expect(dialog.message()).toEqual( 'jlab-examples:main-menu has been called from the menu.' ); @@ -22,14 +19,14 @@ test('should emit a message in a dialog when menu is triggered', async ({ dialog.dismiss().catch(() => {}); }); - // Click ul[role="menu"] >> text=Execute jlab-examples:main-menu Command - await page.click( - 'ul[role="menu"] >> text=Execute jlab-examples:main-menu Command' + // Click the menu entry + await page.menu.clickMenuItem( + 'Main Menu Example>Execute jlab-examples:main-menu Command' ); expect( logs.filter( - (s) => s === 'jlab-examples:main-menu has been called from the menu.' + s => s === 'jlab-examples:main-menu has been called from the menu.' ) ).toHaveLength(1); }); diff --git a/package.json b/package.json index 7cf71309..cb2908da 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "install": "lerna bootstrap", "build-ext": "lerna run build:all", "clean-ext": "lerna run clean", - "embedme": "embedme \"[!n]*/**/README.md\"", + "embedme": "embedme \"[!n]*/README.md\"", "install-py": "lerna exec --concurrency 4 -- python -m pip install .", "install-ext": "lerna run install:extension", "lint": "jlpm && jlpm run prettier", @@ -24,6 +24,7 @@ "command-palette", "commands", "completer", + "contentheader", "context-menu", "custom-log-console", "datagrid", diff --git a/react-widget/.eslintrc.js b/react-widget/.eslintrc.js index c2375786..665374bf 100644 --- a/react-widget/.eslintrc.js +++ b/react-widget/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/react-widget/.gitignore b/react-widget/.gitignore index 18cace13..ef3ace98 100644 --- a/react-widget/.gitignore +++ b/react-widget/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/react-widget/.prettierignore b/react-widget/.prettierignore new file mode 100644 index 00000000..25e1617a --- /dev/null +++ b/react-widget/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_react_widget diff --git a/react-widget/.prettierrc b/react-widget/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/react-widget/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/react-widget/.stylelintrc b/react-widget/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/react-widget/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/react-widget/.yarnrc.yml b/react-widget/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/react-widget/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/react-widget/CHANGELOG.md b/react-widget/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/react-widget/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/react-widget/LICENSE b/react-widget/LICENSE new file mode 100644 index 00000000..54d885af --- /dev/null +++ b/react-widget/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/react-widget/MANIFEST.in b/react-widget/MANIFEST.in deleted file mode 100644 index 6444b9ab..00000000 --- a/react-widget/MANIFEST.in +++ /dev/null @@ -1,22 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml -include jupyter-config/jupyterlab_examples_react_widget.json - -include package.json -include ts*.json - -graft jupyterlab_examples_react_widget/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/react-widget/RELEASE.md b/react-widget/RELEASE.md index 6be4d2e8..516d52eb 100644 --- a/react-widget/RELEASE.md +++ b/react-widget/RELEASE.md @@ -1,22 +1,62 @@ # Making a new release of jupyterlab_examples_react_widget -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser The extension repository should already be compatible with the Jupyter Releaser. -Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. Here is a summary of the steps to cut a new release: -- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) -- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository - Go to the Actions panel -- Run the "Draft Changelog" workflow -- Merge the Changelog PR -- Run the "Draft Release" workflow -- Run the "Publish Release" workflow +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow ## Publishing to `conda-forge` diff --git a/react-widget/babel.config.js b/react-widget/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/react-widget/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/react-widget/jest.config.js b/react-widget/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/react-widget/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/react-widget/jupyterlab_examples_react_widget/__init__.py b/react-widget/jupyterlab_examples_react_widget/__init__.py index 353ab522..aad79e98 100644 --- a/react-widget/jupyterlab_examples_react_widget/__init__.py +++ b/react-widget/jupyterlab_examples_react_widget/__init__.py @@ -1,19 +1,7 @@ - -import json -import os.path as osp - from ._version import __version__ -HERE = osp.abspath(osp.dirname(__file__)) - -with open(osp.join(HERE, 'labextension', 'package.json')) as fid: - data = json.load(fid) - def _jupyter_labextension_paths(): return [{ - 'src': 'labextension', - 'dest': data['name'] + "src": "labextension", + "dest": "@jupyterlab-examples/react-widget" }] - - - diff --git a/react-widget/jupyterlab_examples_react_widget/_version.py b/react-widget/jupyterlab_examples_react_widget/_version.py index ee864fc9..133868ac 100644 --- a/react-widget/jupyterlab_examples_react_widget/_version.py +++ b/react-widget/jupyterlab_examples_react_widget/_version.py @@ -1,2 +1,4 @@ -version_info = (0, 1, 0) -__version__ = ".".join(map(str, version_info)) +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/react-widget/package.json b/react-widget/package.json index 2ec99f47..59095351 100644 --- a/react-widget/package.json +++ b/react-widget/package.json @@ -1,7 +1,7 @@ { "name": "@jupyterlab-examples/react-widget", "version": "0.1.0", - "description": "Example of using a React Widget in a Jupyterlab extension", + "description": "Example of using a React Widget in a Jupyterlab extension.", "keywords": [ "jupyter", "jupyterlab", @@ -18,7 +18,7 @@ }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "style/**/*.{css,eot,js,gif,html,jpg,json,png,svg,woff2,ttf}" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -27,51 +27,77 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jupyterlab_examples_react_widget/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_react_widget/labextension jupyterlab_examples_react_widget/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0", - "@jupyterlab/launcher": "^3.1.0", - "@jupyterlab/ui-components": "^3.1.0" + "@jupyterlab/application": "^4.0.0-beta.0", + "@jupyterlab/launcher": "^4.0.0-beta.0", + "@jupyterlab/ui-components": "^4.0.0-beta.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "extension": true, "outputDir": "jupyterlab_examples_react_widget/labextension" - }, - "styleModule": "style/index.js" + } } diff --git a/react-widget/preview.gif b/react-widget/preview.gif index 50d5f4d0..c2ce7e23 100644 Binary files a/react-widget/preview.gif and b/react-widget/preview.gif differ diff --git a/react-widget/preview2.gif b/react-widget/preview2.gif index 4cae47d8..f541337a 100644 Binary files a/react-widget/preview2.gif and b/react-widget/preview2.gif differ diff --git a/react-widget/pyproject.toml b/react-widget/pyproject.toml index 14e2b947..391bd874 100644 --- a/react-widget/pyproject.toml +++ b/react-widget/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_react_widget/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_react_widget/labextension/static/style.js", "jupyterlab_examples_react_widget/labextension/package.json"] +[project] +name = "jupyterlab_examples_react_widget" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_react_widget/labextension"] +exclude = [".github", "binder"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_react_widget/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/react-widget" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/react-widget/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_react_widget/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_react_widget/labextension/static/style.js", + "jupyterlab_examples_react_widget/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_react_widget/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_react_widget/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_react_widget/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/react-widget/setup.py b/react-widget/setup.py index 67366b17..bea23374 100644 --- a/react-widget/setup.py +++ b/react-widget/setup.py @@ -1,83 +1 @@ -""" -jupyterlab_examples_react_widget setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_react_widget" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/react-widget" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/react-widget/src/index.ts b/react-widget/src/index.ts index 606fdfe2..0b7ca284 100644 --- a/react-widget/src/index.ts +++ b/react-widget/src/index.ts @@ -1,14 +1,10 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; - import { MainAreaWidget } from '@jupyterlab/apputils'; - import { ILauncher } from '@jupyterlab/launcher'; - import { reactIcon } from '@jupyterlab/ui-components'; - import { CounterWidget } from './widget'; /** @@ -32,22 +28,22 @@ const extension: JupyterFrontEndPlugin = { commands.addCommand(command, { caption: 'Create a new React Widget', label: 'React Widget', - icon: (args) => (args['isPalette'] ? null : reactIcon), + icon: args => (args['isPalette'] ? undefined : reactIcon), execute: () => { const content = new CounterWidget(); const widget = new MainAreaWidget({ content }); widget.title.label = 'React Widget'; widget.title.icon = reactIcon; app.shell.add(widget, 'main'); - }, + } }); if (launcher) { launcher.add({ - command, + command }); } - }, + } }; export default extension; diff --git a/react-widget/style/base.css b/react-widget/style/base.css index d8657f4c..84efdfe4 100644 --- a/react-widget/style/base.css +++ b/react-widget/style/base.css @@ -1,3 +1,9 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ + .jp-ReactWidget { color: var(--jp-ui-font-color1); background: var(--jp-layout-color1); diff --git a/react-widget/style/index.css b/react-widget/style/index.css index 8a7ea29e..e98119b5 100644 --- a/react-widget/style/index.css +++ b/react-widget/style/index.css @@ -1 +1 @@ -@import url('base.css'); +@import 'base.css'; diff --git a/react-widget/tsconfig.json b/react-widget/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/react-widget/tsconfig.json +++ b/react-widget/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/react-widget/tsconfig.test.json b/react-widget/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/react-widget/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/react-widget/ui-tests/.env b/react-widget/ui-tests/.env deleted file mode 100644 index 0e0d33e4..00000000 --- a/react-widget/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=react-widget/jupyterlab_examples_react_widget -EXT_NAME=react-widget \ No newline at end of file diff --git a/react-widget/ui-tests/README.md b/react-widget/ui-tests/README.md index d2be7291..d05d2fa9 100644 --- a/react-widget/ui-tests/README.md +++ b/react-widget/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _react-widget_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _react-widget_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _react-widget_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _react-widget_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/react-widget/ui-tests/jupyter_server_test_config.py b/react-widget/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/react-widget/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/react-widget/ui-tests/package.json b/react-widget/ui-tests/package.json index 71215096..ffe1670b 100644 --- a/react-widget/ui-tests/package.json +++ b/react-widget/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/react-widget-tests", - "version": "0.1.0", - "description": "Integration test for react-widget example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/react-widget-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/react-widget 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/react-widget/ui-tests/playwright.config.js b/react-widget/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/react-widget/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/react-widget/ui-tests/playwright.config.ts b/react-widget/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/react-widget/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/react-widget/ui-tests/tests/react-widget.spec.ts b/react-widget/ui-tests/tests/react-widget.spec.ts index 1d087c89..e0ef50ff 100644 --- a/react-widget/ui-tests/tests/react-widget.spec.ts +++ b/react-widget/ui-tests/tests/react-widget.spec.ts @@ -20,10 +20,9 @@ test('should open a new panel with a react component', async ({ page }) => { expect(await page.waitForSelector('text=You clicked 4 times!')).toBeTruthy(); // Close filebrowser - await page.click('text=View'); await Promise.all([ page.waitForSelector('#filebrowser', { state: 'hidden' }), - page.click('ul[role="menu"] >> text=Show Left Sidebar'), + page.menu.clickMenuItem('View>File Browser') ]); expect(await page.screenshot()).toMatchSnapshot('react-widget-example.png'); diff --git a/react-widget/ui-tests/tests/react-widget.spec.ts-snapshots/react-widget-example-linux.png b/react-widget/ui-tests/tests/react-widget.spec.ts-snapshots/react-widget-example-linux.png index 86394246..78ac656d 100644 Binary files a/react-widget/ui-tests/tests/react-widget.spec.ts-snapshots/react-widget-example-linux.png and b/react-widget/ui-tests/tests/react-widget.spec.ts-snapshots/react-widget-example-linux.png differ diff --git a/server-extension/.eslintrc.js b/server-extension/.eslintrc.js index c2375786..665374bf 100644 --- a/server-extension/.eslintrc.js +++ b/server-extension/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/server-extension/.gitignore b/server-extension/.gitignore index f074e38e..37789971 100644 --- a/server-extension/.gitignore +++ b/server-extension/.gitignore @@ -1,9 +1,18 @@ *.bundle.* lib/ node_modules/ +*.log +.eslintcache +.stylelintcache +*.egg-info/ .ipynb_checkpoints *.tsbuildinfo -*/labextension/*.tgz +jupyterlab_examples_server/labextension + + +# 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 @@ -33,7 +42,6 @@ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ -*.egg-info/ .installed.cfg *.egg MANIFEST @@ -56,6 +64,7 @@ htmlcov/ .coverage.* .cache nosetests.xml +coverage/ coverage.xml *.cover .hypothesis/ @@ -106,4 +115,10 @@ dmypy.json # Pyre type checker .pyre/ -# End of https://www.gitignore.io/api/python \ No newline at end of file +# End of https://www.gitignore.io/api/python + +# OSX files +.DS_Store + +# Yarn cache +.yarn/ diff --git a/server-extension/.prettierignore b/server-extension/.prettierignore new file mode 100644 index 00000000..8add04f1 --- /dev/null +++ b/server-extension/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_server diff --git a/server-extension/.prettierrc b/server-extension/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/server-extension/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/server-extension/.stylelintrc b/server-extension/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/server-extension/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/server-extension/.yarnrc.yml b/server-extension/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/server-extension/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/server-extension/CHANGELOG.md b/server-extension/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/server-extension/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/server-extension/LICENSE b/server-extension/LICENSE index f5fc0e44..54d885af 100644 --- a/server-extension/LICENSE +++ b/server-extension/LICENSE @@ -1,21 +1,21 @@ BSD 3-Clause License -Copyright (c) 2019, JupyterLab +Copyright (c) 2023, Project Jupyter Contributors All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE @@ -26,4 +26,4 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/server-extension/MANIFEST.in b/server-extension/MANIFEST.in deleted file mode 100644 index 7ff9b4a7..00000000 --- a/server-extension/MANIFEST.in +++ /dev/null @@ -1,23 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml - -recursive-include jupyter-config *.json - -include package.json -include ts*.json -graft jlab_ext_example/labextension -graft jlab_ext_example/public - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/server-extension/README.md b/server-extension/README.md index 86be73ce..196c6c69 100644 --- a/server-extension/README.md +++ b/server-extension/README.md @@ -32,7 +32,7 @@ like this (be careful to set _has_server_extension_ to _y_): author_name []: tuto author_email []: tuto@help.you labextension_name [myextension]: jlab-ext-example -python_name [jlab_ext_example]: +python_name [jupyterlab_examples_server]: project_short_description [A JupyterLab extension.]: A minimal JupyterLab extension with backend and frontend parts. has_settings [n]: has_server_extension [n]: y @@ -42,11 +42,11 @@ repository [https://github.com/github_username/jlab-ext-example]: > The python name should not contain `-`. It is nice for user to test your extension online, so the `has_binder` was set to _yes_. -The cookiecutter creates the directory `jlab_ext_example` [or your extension name] +The cookiecutter creates the directory `jupyterlab_examples_server` [or your extension name] that looks like this: ```bash -jlab_ext_example/ +jupyterlab_examples_server/ │  # Generic Files │ .gitignore │ install.json # Information retrieved by JupyterLab to help users know how to manage the extension @@ -68,9 +68,9 @@ jlab_ext_example/ │ │ # Backend (server) Files ├───jupyter-config -│ jlab_ext_example.json # Server extension enabler +│ jupyterlab_examples_server.json # Server extension enabler │ -├───jlab_ext_example +├───jupyterlab_examples_server │ handlers.py # API handler (where things happen) │ _version.py # Server extension version │ __init__.py # Hook the extension in the server @@ -121,15 +121,18 @@ is printed in the web browser console: ```ts -// src/index.ts#L36-L42 +// src/index.ts#L33-L42 -// GET request -try { - const data = await requestAPI('hello'); - console.log(data); -} catch (reason) { - console.error(`Error on GET /jlab-ext-example/hello.\n${reason}`); -} +) => { + // GET request + try { + const data = await requestAPI('hello'); + console.log(data); + } catch (reason) { + console.error( + `Error on GET /jupyterlab_examples_server/hello.\n${reason}` + ); + } ``` @@ -138,7 +141,7 @@ using the `await` keyword: ```ts -// src/index.ts#L38-L38 +// src/index.ts#L36-L36 const data = await requestAPI('hello'); ``` @@ -171,12 +174,12 @@ const dataToSend = { name: 'George' }; try { const reply = await requestAPI('hello', { body: JSON.stringify(dataToSend), - method: 'POST', + method: 'POST' }); console.log(reply); } catch (reason) { console.error( - `Error on POST /jlab-ext-example/hello ${dataToSend}.\n${reason}` + `Error on POST /jupyterlab_examples_server/hello ${dataToSend}.\n${reason}` ); } ``` @@ -194,7 +197,7 @@ Its definition is : ```ts -// src/handler.ts#L12-L37 +// src/handler.ts#L12-L45 export async function requestAPI( endPoint = '', @@ -204,7 +207,7 @@ export async function requestAPI( const settings = ServerConnection.makeSettings(); const requestUrl = URLExt.join( settings.baseUrl, - 'jlab-ext-example', + 'jupyterlab_examples_server', // API Namespace endPoint ); @@ -212,13 +215,21 @@ export async function requestAPI( try { response = await ServerConnection.makeRequest(requestUrl, init, settings); } catch (error) { - throw new ServerConnection.NetworkError(error); + throw new ServerConnection.NetworkError(error as any); } - const data = await response.json(); + let data: any = await response.text(); + + if (data.length > 0) { + try { + data = JSON.parse(data); + } catch (error) { + console.log('Not a JSON response body.', response); + } + } if (!response.ok) { - throw new ServerConnection.ResponseError(response, data.message); + throw new ServerConnection.ResponseError(response, data.message || data); } return data; @@ -259,7 +270,7 @@ The next step is to build the full request URL: const requestUrl = URLExt.join( settings.baseUrl, - 'jlab-ext-example', + 'jupyterlab_examples_server', // API Namespace endPoint ``` @@ -295,12 +306,20 @@ JSON. And the resulting data is returned. ```ts -// src/handler.ts#L31-L37 +// src/handler.ts#L31-L45 + +let data: any = await response.text(); -const data = await response.json(); +if (data.length > 0) { + try { + data = JSON.parse(data); + } catch (error) { + console.log('Not a JSON response body.', response); + } +} if (!response.ok) { - throw new ServerConnection.ResponseError(response, data.message); + throw new ServerConnection.ResponseError(response, data.message || data); } return data; @@ -323,7 +342,7 @@ commands.addCommand(command, { execute: () => { const widget = new IFrameWidget(); shell.add(widget, 'main'); - }, + } }); palette.addItem({ command, category: category }); @@ -332,7 +351,7 @@ if (launcher) { // Add launcher launcher.add({ command: command, - category: category, + category: category }); } ``` @@ -358,42 +377,36 @@ JupyterLab server is built on top of the [Tornado](https://www.tornadoweb.org/en your extension needs to be defined as a proper Python package with some hook functions: ```py -# jlab_ext_example/__init__.py - -import json -from pathlib import Path +# jupyterlab_examples_server/__init__.py from .handlers import setup_handlers from ._version import __version__ -HERE = Path(__file__).parent.resolve() - -with (HERE / "labextension" / "package.json").open() as fid: - data = json.load(fid) - def _jupyter_labextension_paths(): - return [{"src": "labextension", "dest": data["name"]}] + return [{"src": "labextension", "dest": "@jupyterlab-examples/server-extension"}] def _jupyter_server_extension_points(): - return [{"module": "jlab_ext_example"}] + return [{ + "module": "jupyterlab_examples_server" + }] def _load_jupyter_server_extension(server_app): """Registers the API handler to receive HTTP requests from the frontend extension. + Parameters ---------- server_app: jupyterlab.labapp.LabApp JupyterLab application instance """ - url_path = "jlab-ext-example" - setup_handlers(server_app.web_app, url_path) - server_app.log.info( - f"Registered jlab_ext_example extension at URL path /{url_path}" - ) + setup_handlers(server_app.web_app) + name = "jupyterlab_examples_server" + server_app.log.info(f"Registered {name} server extension") -# For backward compatibility with the classical notebook + +# For backward compatibility with notebook server - useful for Binder/JupyterHub load_jupyter_server_extension = _load_jupyter_server_extension ``` @@ -403,22 +416,22 @@ to the server. But the most important one is `_load_jupyter_server_extension` that register new handlers. ```py -# jlab_ext_example/__init__.py#L29-L29 +# jupyterlab_examples_server/__init__.py#L23-L23 -setup_handlers(server_app.web_app, url_path) +setup_handlers(server_app.web_app) ``` A handler is registered in the web application by linking an url to a class. In this example the url is _base_server_url_`/jlab-ext-example/hello` and the class handler is `RouteHandler`: ```py -# jlab_ext_example/handlers.py#L28-L34 +# jupyterlab_examples_server/handlers.py#L28-L34 host_pattern = ".*$" base_url = web_app.settings["base_url"] # Prepend the base_url so that it works in a JupyterHub setting -route_pattern = url_path_join(base_url, url_path, "hello") +route_pattern = url_path_join(base_url, "jupyterlab_examples_server", "hello") handlers = [(route_pattern, RouteHandler)] web_app.add_handlers(host_pattern, handlers) ``` @@ -428,7 +441,7 @@ implement the wanted HTTP verbs. For example, here, `/jlab-ext-example/hello` ca by a _GET_ or a _POST_ request. They will call the `get` or `post` method respectively. ```py -# jlab_ext_example/handlers.py#L11-L24 +# jupyterlab_examples_server/handlers.py#L11-L24 class RouteHandler(APIHandler): # The following decorator should be present on all verb methods (head, get, post, @@ -436,7 +449,7 @@ class RouteHandler(APIHandler): # Jupyter server @tornado.web.authenticated def get(self): - self.finish(json.dumps({"data": "This is /jlab-ext-example/hello endpoint!"})) + self.finish(json.dumps({"data": "This is /jupyterlab_examples_server/hello endpoint!"})) @tornado.web.authenticated def post(self): @@ -457,10 +470,10 @@ by calling the `finish` method. That method can optionally take an argument that become the response body of the request in the frontend. ```py -# jlab_ext_example/handlers.py#L16-L17 +# jupyterlab_examples_server/handlers.py#L16-L17 def get(self): - self.finish(json.dumps({"data": "This is /jlab-ext-example/hello endpoint!"})) + self.finish(json.dumps({"data": "This is /jupyterlab_examples_server/hello endpoint!"})) ``` In Jupyter, it is common to use JSON as format between the frontend and the backend. @@ -472,7 +485,7 @@ sent by the frontend. When using JSON as communication format, you can directly `get_json_body` helper method to convert the request body into a Python dictionary. ```py -# jlab_ext_example/handlers.py#L22-L23 +# jupyterlab_examples_server/handlers.py#L22-L23 input_data = self.get_json_body() data = {"greetings": "Hello {}, enjoy JupyterLab!".format(input_data["name"])} @@ -482,15 +495,15 @@ The part responsible to serve static content with a `StaticFileHandler` handler is the following: ```py -# jlab_ext_example/handlers.py#L37-L43 +# jupyterlab_examples_server/handlers.py#L37-L43 -doc_url = url_path_join(base_url, url_path, "public") +doc_url = url_path_join(base_url, "jupyterlab_examples_server", "public") doc_dir = os.getenv( "JLAB_SERVER_EXAMPLE_STATIC_DIR", os.path.join(os.path.dirname(__file__), "public"), ) handlers = [("{}/(.*)".format(doc_url), StaticFileHandler, {"path": doc_dir})] -web_app.add_handlers(".*$", handlers) +web_app.add_handlers(host_pattern, handlers) ``` **Security Note** @@ -517,160 +530,132 @@ through package managers like `pip`. > JupyterLab,...). > As this package is a setup requirement, it needs to be specified in the `pyproject.toml` to be installed by `pip`. -The `setup.py` file is the entry point to describe package metadata: +To deploy simultaneously the frontend and the backend, +the frontend NPM package needs to be built and inserted in the Python package. This is +done using a special a dedicated builder following [PEP 517](https://www.python.org/dev/peps/pep-0517) from package [jupyter-packaging](https://github.com/jupyter/jupyter-packaging). Its configuration is done in `pyproject.toml`: ```py -# setup.py - -""" -jlab_ext_example setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jlab_ext_example" - -lab_path = (HERE / name.replace("-", "_") / "labextension") +# pyproject.toml -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") +[build-system] +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" + +[project] +name = "jupyterlab_examples_server" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] - -labext_name = "@jupyterlab-examples/server-extension" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), - ("etc/jupyter/jupyter_server_config.d", - "jupyter-config/server-config", "jlab_ext_example.json"), - # For backward compatibility with notebook server - ("etc/jupyter/jupyter_notebook_config.d", - "jupyter-config/nb-config", "jlab_ext_example.json"), +dependencies = [ + "jupyter_server>=2.0.1,<3" +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[project.optional-dependencies] +test = [ + "coverage", + "pytest", + "pytest-asyncio", + "pytest-cov", + "pytest-jupyter[server]>=0.6.0" ] -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[ - "jupyter_server>=1.6,<2" - ], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) +[tool.hatch.version] +source = "nodejs" -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) - -``` - -But in this case, it is a bit more complicated to build the frontend extension and ship it -directly with the Python package. To deploy simultaneously the frontend and the backend, -the frontend NPM package needs to be built and inserted in the Python package. This is -done using a special a dedicated builder following [PEP 517](https://www.python.org/dev/peps/pep-0517) from package [jupyter-packaging](https://github.com/jupyter/jupyter-packaging). Its configuration is done in `pyproject.toml`: +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -```py -# pyproject.toml +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_server/labextension"] +exclude = [".github", "binder"] -[build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_server/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/server-extension" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/server-extension/install.json" +"jupyter-config/server-config" = "etc/jupyter/jupyter_server_config.d" +"jupyter-config/nb-config" = "etc/jupyter/jupyter_notebook_config.d" -[tool.jupyter-packaging.options] -skip-if-exists = ["jlab_ext_example/labextension/static/style.js"] -ensured-targets = ["jlab_ext_example/labextension/static/style.js", "jlab_ext_example/labextension/package.json"] +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_server/_version.py" -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_server/labextension/static/style.js", + "jupyterlab_examples_server/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_server/labextension/static/style.js"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jlab_ext_example/labextension/**", "yarn.lock", ".*", "package-lock.json"] - -``` +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_server/labextension" -It will build the frontend NPM package through its _factory_: +[tool.jupyter-releaser.options] +version_cmd = "hatch version" -```py -# pyproject.toml#L9-L14 +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.check-wheel-contents] +ignore = ["W002"] -[tool.jupyter-packaging.build-args] -build_cmd = "build:prod" -npm = ["jlpm"] ``` -It will ensure one of the generated files is `jlab_ext_example/labextension/package.json`: +It will build the frontend NPM package through its _factory_, and will ensure one of the +generated files is `jupyterlab_examples_server/labextension/package.json`: ```py -# pyproject.toml#L7-L7 +# pyproject.toml#L57-L68 + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_server/labextension/static/style.js", + "jupyterlab_examples_server/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_server/labextension/static/style.js"] -ensured-targets = ["jlab_ext_example/labextension/static/style.js", "jlab_ext_example/labextension/package.json"] +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] +build_cmd = "build:prod" +npm = ["jlpm"] ``` It will copy the NPM package in the Python package and force it to be copied in a place JupyterLab is looking for frontend extensions when the Python package is installed: ```py -# setup.py#L26-L26 +# pyproject.toml#L48-L49 -("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_server/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/server-extension" ``` The last piece of configuration needed is the enabling of the server extension. This is @@ -678,12 +663,12 @@ done by copying the following JSON file: ```json5 -// jupyter-config/server-config/jlab_ext_example.json +// jupyter-config/server-config/jupyterlab_examples_server.json { "ServerApp": { "jpserver_extensions": { - "jlab_ext_example": true + "jupyterlab_examples_server": true } } } @@ -694,20 +679,18 @@ done by copying the following JSON file: in the appropriate jupyter folder (`etc/jupyter/jupyter_server_config.d`): ```py -# setup.py#L28-L29 +# pyproject.toml#L51-L51 -("etc/jupyter/jupyter_server_config.d", - "jupyter-config/server-config", "jlab_ext_example.json"), +"jupyter-config/server-config" = "etc/jupyter/jupyter_server_config.d" ``` For backward compatibility with the classical notebook, the old version of that file is copied in (`etc/jupyter/jupyter_notebook_config.d`): ```py -# setup.py#L31-L32 +# pyproject.toml#L52-L52 -("etc/jupyter/jupyter_notebook_config.d", - "jupyter-config/nb-config", "jlab_ext_example.json"), +"jupyter-config/nb-config" = "etc/jupyter/jupyter_notebook_config.d" ``` ### JupyterLab Extension Manager @@ -719,7 +702,7 @@ user about that dependency by adding the `discovery` metadata to your `package.j file: ```json5 -// package.json#L74-L84 +// package.json#L101-L111 "jupyterlab": { "discovery": { @@ -728,7 +711,7 @@ file: "pip" ], "base": { - "name": "jlab_ext_example" + "name": "jupyterlab_examples_server" } } }, @@ -737,7 +720,7 @@ file: In this example, the extension requires a `server` extension: ```json5 -// package.json#L75-L75 +// package.json#L102-L102 "discovery": { ``` @@ -745,7 +728,7 @@ In this example, the extension requires a `server` extension: And that server extension is available through `pip`: ```json5 -// package.json#L76-L78 +// package.json#L103-L105 "server": { "managers": [ @@ -761,7 +744,7 @@ With the packaging described above, installing the extension is done in one comm ```bash # Install the server extension and # copy the frontend extension where JupyterLab can find it -pip install jlab_ext_example +pip install jupyterlab_examples_server ``` As developer, you might want to install the package in local editable mode. @@ -774,7 +757,7 @@ pip install -e . # Link your development version of the extension with JupyterLab jupyter labextension develop . --overwrite # Enable the server extension -jupyter server extension enable jlab_ext_example +jupyter server extension enable jupyterlab_examples_server # Rebuild extension Typescript source after making changes jlpm run build ``` diff --git a/server-extension/RELEASE.md b/server-extension/RELEASE.md index f1f5434e..7dd87f82 100644 --- a/server-extension/RELEASE.md +++ b/server-extension/RELEASE.md @@ -1,22 +1,62 @@ -# Making a new release of jlab_ext_example +# Making a new release of jupyterlab_examples_server -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser The extension repository should already be compatible with the Jupyter Releaser. -Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. Here is a summary of the steps to cut a new release: -- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) -- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository - Go to the Actions panel -- Run the "Draft Changelog" workflow -- Merge the Changelog PR -- Run the "Draft Release" workflow -- Run the "Publish Release" workflow +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow ## Publishing to `conda-forge` diff --git a/server-extension/babel.config.js b/server-extension/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/server-extension/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/server-extension/conftest.py b/server-extension/conftest.py new file mode 100644 index 00000000..6543bb3d --- /dev/null +++ b/server-extension/conftest.py @@ -0,0 +1,8 @@ +import pytest + +pytest_plugins = ("pytest_jupyter.jupyter_server", ) + + +@pytest.fixture +def jp_server_config(jp_server_config): + return {"ServerApp": {"jpserver_extensions": {"jupyterlab_examples_server": True}}} diff --git a/server-extension/install.json b/server-extension/install.json index 772fb6ad..adb48c82 100644 --- a/server-extension/install.json +++ b/server-extension/install.json @@ -1,5 +1,5 @@ { "packageManager": "python", - "packageName": "jlab_ext_example", - "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jlab_ext_example" + "packageName": "jupyterlab_examples_server", + "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlab_examples_server" } diff --git a/server-extension/jest.config.js b/server-extension/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/server-extension/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/server-extension/jlab_ext_example/__init__.py b/server-extension/jlab_ext_example/__init__.py deleted file mode 100644 index f63305e4..00000000 --- a/server-extension/jlab_ext_example/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -import json -from pathlib import Path - -from .handlers import setup_handlers -from ._version import __version__ - -HERE = Path(__file__).parent.resolve() - -with (HERE / "labextension" / "package.json").open() as fid: - data = json.load(fid) - - -def _jupyter_labextension_paths(): - return [{"src": "labextension", "dest": data["name"]}] - - -def _jupyter_server_extension_points(): - return [{"module": "jlab_ext_example"}] - - -def _load_jupyter_server_extension(server_app): - """Registers the API handler to receive HTTP requests from the frontend extension. - Parameters - ---------- - server_app: jupyterlab.labapp.LabApp - JupyterLab application instance - """ - url_path = "jlab-ext-example" - setup_handlers(server_app.web_app, url_path) - server_app.log.info( - f"Registered jlab_ext_example extension at URL path /{url_path}" - ) - -# For backward compatibility with the classical notebook -load_jupyter_server_extension = _load_jupyter_server_extension diff --git a/server-extension/jlab_ext_example/_version.py b/server-extension/jlab_ext_example/_version.py deleted file mode 100644 index ee864fc9..00000000 --- a/server-extension/jlab_ext_example/_version.py +++ /dev/null @@ -1,2 +0,0 @@ -version_info = (0, 1, 0) -__version__ = ".".join(map(str, version_info)) diff --git a/server-extension/jupyter-config/nb-config/jlab_ext_example.json b/server-extension/jupyter-config/nb-config/jupyterlab_examples_server.json similarity index 60% rename from server-extension/jupyter-config/nb-config/jlab_ext_example.json rename to server-extension/jupyter-config/nb-config/jupyterlab_examples_server.json index 26b21bd8..b6d24a19 100644 --- a/server-extension/jupyter-config/nb-config/jlab_ext_example.json +++ b/server-extension/jupyter-config/nb-config/jupyterlab_examples_server.json @@ -1,7 +1,7 @@ { "NotebookApp": { "nbserver_extensions": { - "jlab_ext_example": true + "jupyterlab_examples_server": true } } } diff --git a/server-extension/jupyter-config/server-config/jlab_ext_example.json b/server-extension/jupyter-config/server-config/jupyterlab_examples_server.json similarity index 59% rename from server-extension/jupyter-config/server-config/jlab_ext_example.json rename to server-extension/jupyter-config/server-config/jupyterlab_examples_server.json index 03f6eef1..e718dda4 100644 --- a/server-extension/jupyter-config/server-config/jlab_ext_example.json +++ b/server-extension/jupyter-config/server-config/jupyterlab_examples_server.json @@ -1,7 +1,7 @@ { "ServerApp": { "jpserver_extensions": { - "jlab_ext_example": true + "jupyterlab_examples_server": true } } } diff --git a/server-extension/jupyterlab_examples_server/__init__.py b/server-extension/jupyterlab_examples_server/__init__.py new file mode 100644 index 00000000..217035b2 --- /dev/null +++ b/server-extension/jupyterlab_examples_server/__init__.py @@ -0,0 +1,29 @@ +from .handlers import setup_handlers +from ._version import __version__ + + +def _jupyter_labextension_paths(): + return [{"src": "labextension", "dest": "@jupyterlab-examples/server-extension"}] + + +def _jupyter_server_extension_points(): + return [{ + "module": "jupyterlab_examples_server" + }] + + +def _load_jupyter_server_extension(server_app): + """Registers the API handler to receive HTTP requests from the frontend extension. + + Parameters + ---------- + server_app: jupyterlab.labapp.LabApp + JupyterLab application instance + """ + setup_handlers(server_app.web_app) + name = "jupyterlab_examples_server" + server_app.log.info(f"Registered {name} server extension") + + +# For backward compatibility with notebook server - useful for Binder/JupyterHub +load_jupyter_server_extension = _load_jupyter_server_extension diff --git a/server-extension/jupyterlab_examples_server/_version.py b/server-extension/jupyterlab_examples_server/_version.py new file mode 100644 index 00000000..133868ac --- /dev/null +++ b/server-extension/jupyterlab_examples_server/_version.py @@ -0,0 +1,4 @@ +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/server-extension/jlab_ext_example/handlers.py b/server-extension/jupyterlab_examples_server/handlers.py similarity index 78% rename from server-extension/jlab_ext_example/handlers.py rename to server-extension/jupyterlab_examples_server/handlers.py index 723b3b51..2c6505f3 100644 --- a/server-extension/jlab_ext_example/handlers.py +++ b/server-extension/jupyterlab_examples_server/handlers.py @@ -14,7 +14,7 @@ class RouteHandler(APIHandler): # Jupyter server @tornado.web.authenticated def get(self): - self.finish(json.dumps({"data": "This is /jlab-ext-example/hello endpoint!"})) + self.finish(json.dumps({"data": "This is /jupyterlab_examples_server/hello endpoint!"})) @tornado.web.authenticated def post(self): @@ -24,20 +24,20 @@ def post(self): self.finish(json.dumps(data)) -def setup_handlers(web_app, url_path): +def setup_handlers(web_app): host_pattern = ".*$" base_url = web_app.settings["base_url"] # Prepend the base_url so that it works in a JupyterHub setting - route_pattern = url_path_join(base_url, url_path, "hello") + route_pattern = url_path_join(base_url, "jupyterlab_examples_server", "hello") handlers = [(route_pattern, RouteHandler)] web_app.add_handlers(host_pattern, handlers) # Prepend the base_url so that it works in a JupyterHub setting - doc_url = url_path_join(base_url, url_path, "public") + doc_url = url_path_join(base_url, "jupyterlab_examples_server", "public") doc_dir = os.getenv( "JLAB_SERVER_EXAMPLE_STATIC_DIR", os.path.join(os.path.dirname(__file__), "public"), ) handlers = [("{}/(.*)".format(doc_url), StaticFileHandler, {"path": doc_dir})] - web_app.add_handlers(".*$", handlers) + web_app.add_handlers(host_pattern, handlers) diff --git a/server-extension/jlab_ext_example/public/index.html b/server-extension/jupyterlab_examples_server/public/index.html similarity index 63% rename from server-extension/jlab_ext_example/public/index.html rename to server-extension/jupyterlab_examples_server/public/index.html index b048fb48..1cbfbaad 100644 --- a/server-extension/jlab_ext_example/public/index.html +++ b/server-extension/jupyterlab_examples_server/public/index.html @@ -5,6 +5,6 @@ Server Content - This content is served from the jlab_ext_example server extension. + This content is served from the jupyterlab_examples_server extension. diff --git a/server-extension/package.json b/server-extension/package.json index 45e1594a..ca49e42d 100644 --- a/server-extension/package.json +++ b/server-extension/package.json @@ -18,7 +18,7 @@ }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "style/**/*.{css,eot,js,gif,html,jpg,json,png,svg,woff2,ttf}" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -27,50 +27,77 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jlab_ext_example/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_server/labextension jupyterlab_examples_server/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0", - "@jupyterlab/coreutils": "^5.1.0", - "@jupyterlab/launcher": "^3.1.0", - "@jupyterlab/services": "^6.1.0" + "@jupyterlab/application": "^4.0.0-beta.0", + "@jupyterlab/coreutils": "^6.0.0-beta.0", + "@jupyterlab/launcher": "^4.0.0-beta.0", + "@jupyterlab/services": "^7.0.0-beta.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "mkdirp": "^1.0.3", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "discovery": { "server": { @@ -78,12 +105,11 @@ "pip" ], "base": { - "name": "jlab_ext_example" + "name": "jupyterlab_examples_server" } } }, "extension": true, - "outputDir": "jlab_ext_example/labextension" - }, - "styleModule": "style/index.js" + "outputDir": "jupyterlab_examples_server/labextension" + } } diff --git a/server-extension/preview.png b/server-extension/preview.png old mode 100755 new mode 100644 index f3c9e650..d6b4d9a0 Binary files a/server-extension/preview.png and b/server-extension/preview.png differ diff --git a/server-extension/pyproject.toml b/server-extension/pyproject.toml index 51025724..2d6f5bd1 100644 --- a/server-extension/pyproject.toml +++ b/server-extension/pyproject.toml @@ -1,17 +1,88 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jlab_ext_example/labextension/static/style.js"] -ensured-targets = ["jlab_ext_example/labextension/static/style.js", "jlab_ext_example/labextension/package.json"] +[project] +name = "jupyterlab_examples_server" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ + "jupyter_server>=2.0.1,<3" +] +dynamic = ["version", "description", "authors", "urls", "keywords"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[project.optional-dependencies] +test = [ + "coverage", + "pytest", + "pytest-asyncio", + "pytest-cov", + "pytest-jupyter[server]>=0.6.0" +] -[tool.jupyter-packaging.build-args] +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] + +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_server/labextension"] +exclude = [".github", "binder"] + +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_server/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/server-extension" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/server-extension/install.json" +"jupyter-config/server-config" = "etc/jupyter/jupyter_server_config.d" +"jupyter-config/nb-config" = "etc/jupyter/jupyter_notebook_config.d" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_server/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_server/labextension/static/style.js", + "jupyterlab_examples_server/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_server/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jlab_ext_example/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_server/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/server-extension/setup.py b/server-extension/setup.py index 2cfb79f1..bea23374 100644 --- a/server-extension/setup.py +++ b/server-extension/setup.py @@ -1,90 +1 @@ -""" -jlab_ext_example setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jlab_ext_example" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/server-extension" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), - ("etc/jupyter/jupyter_server_config.d", - "jupyter-config/server-config", "jlab_ext_example.json"), - # For backward compatibility with notebook server - ("etc/jupyter/jupyter_notebook_config.d", - "jupyter-config/nb-config", "jlab_ext_example.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[ - "jupyter_server>=1.6,<2" - ], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/server-extension/src/handler.ts b/server-extension/src/handler.ts index edd15ec4..25b8148a 100644 --- a/server-extension/src/handler.ts +++ b/server-extension/src/handler.ts @@ -17,7 +17,7 @@ export async function requestAPI( const settings = ServerConnection.makeSettings(); const requestUrl = URLExt.join( settings.baseUrl, - 'jlab-ext-example', + 'jupyterlab_examples_server', // API Namespace endPoint ); @@ -25,13 +25,21 @@ export async function requestAPI( try { response = await ServerConnection.makeRequest(requestUrl, init, settings); } catch (error) { - throw new ServerConnection.NetworkError(error); + throw new ServerConnection.NetworkError(error as any); } - const data = await response.json(); + let data: any = await response.text(); + + if (data.length > 0) { + try { + data = JSON.parse(data); + } catch (error) { + console.log('Not a JSON response body.', response); + } + } if (!response.ok) { - throw new ServerConnection.ResponseError(response, data.message); + throw new ServerConnection.ResponseError(response, data.message || data); } return data; diff --git a/server-extension/src/index.ts b/server-extension/src/index.ts index 6552ee9a..b734aa20 100644 --- a/server-extension/src/index.ts +++ b/server-extension/src/index.ts @@ -1,6 +1,6 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; import { ICommandPalette, IFrame } from '@jupyterlab/apputils'; @@ -22,7 +22,7 @@ namespace CommandIDs { * Initialization data for the server-extension-example extension. */ const extension: JupyterFrontEndPlugin = { - id: 'server-extension-example', + id: '@jupyterlab-examples/server-extension:plugin', autoStart: true, optional: [ILauncher], requires: [ICommandPalette], @@ -31,14 +31,14 @@ const extension: JupyterFrontEndPlugin = { palette: ICommandPalette, launcher: ILauncher | null ) => { - console.log('JupyterLab extension server-extension-example is activated!'); - // GET request try { const data = await requestAPI('hello'); console.log(data); } catch (reason) { - console.error(`Error on GET /jlab-ext-example/hello.\n${reason}`); + console.error( + `Error on GET /jupyterlab_examples_server/hello.\n${reason}` + ); } // POST request @@ -46,12 +46,12 @@ const extension: JupyterFrontEndPlugin = { try { const reply = await requestAPI('hello', { body: JSON.stringify(dataToSend), - method: 'POST', + method: 'POST' }); console.log(reply); } catch (reason) { console.error( - `Error on POST /jlab-ext-example/hello ${dataToSend}.\n${reason}` + `Error on POST /jupyterlab_examples_server/hello ${dataToSend}.\n${reason}` ); } @@ -65,7 +65,7 @@ const extension: JupyterFrontEndPlugin = { execute: () => { const widget = new IFrameWidget(); shell.add(widget, 'main'); - }, + } }); palette.addItem({ command, category: category }); @@ -74,10 +74,14 @@ const extension: JupyterFrontEndPlugin = { // Add launcher launcher.add({ command: command, - category: category, + category: category }); } - }, + + console.log( + 'JupyterLab extension @jupyterlab-examples/server-extension:plugin is activated!' + ); + } }; export default extension; @@ -86,7 +90,7 @@ class IFrameWidget extends IFrame { constructor() { super(); const baseUrl = PageConfig.getBaseUrl(); - this.url = baseUrl + 'jlab-ext-example/public/index.html'; + this.url = baseUrl + 'jupyterlab_examples_server/public/index.html'; this.id = 'doc-example'; this.title.label = 'Server Doc'; this.title.closable = true; diff --git a/server-extension/style/base.css b/server-extension/style/base.css index e69de29b..e11f4577 100644 --- a/server-extension/style/base.css +++ b/server-extension/style/base.css @@ -0,0 +1,5 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ diff --git a/server-extension/style/index.css b/server-extension/style/index.css index 8a7ea29e..e98119b5 100644 --- a/server-extension/style/index.css +++ b/server-extension/style/index.css @@ -1 +1 @@ -@import url('base.css'); +@import 'base.css'; diff --git a/server-extension/tsconfig.json b/server-extension/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/server-extension/tsconfig.json +++ b/server-extension/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/server-extension/tsconfig.test.json b/server-extension/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/server-extension/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/server-extension/ui-tests/.env b/server-extension/ui-tests/.env deleted file mode 100644 index 5a6b3bf8..00000000 --- a/server-extension/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=server-extension/jlab_ext_example -EXT_NAME=server-extension \ No newline at end of file diff --git a/server-extension/ui-tests/README.md b/server-extension/ui-tests/README.md index d2be7291..f849ac84 100644 --- a/server-extension/ui-tests/README.md +++ b/server-extension/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _server-extension_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _server-extension_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _server-extension_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _server-extension_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/server-extension/ui-tests/jupyter_server_test_config.py b/server-extension/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/server-extension/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/server-extension/ui-tests/package.json b/server-extension/ui-tests/package.json index 2298094f..99e22593 100644 --- a/server-extension/ui-tests/package.json +++ b/server-extension/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/server-extension-tests", - "version": "0.1.0", - "description": "Integration test for server-extension example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/server-extension-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/server-extension 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/server-extension/ui-tests/playwright.config.js b/server-extension/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/server-extension/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/server-extension/ui-tests/playwright.config.ts b/server-extension/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/server-extension/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/server-extension/ui-tests/tests/server-extension.spec.ts b/server-extension/ui-tests/tests/server-extension.spec.ts index 3b0ad890..4f4d9e07 100644 --- a/server-extension/ui-tests/tests/server-extension.spec.ts +++ b/server-extension/ui-tests/tests/server-extension.spec.ts @@ -5,26 +5,26 @@ test.use({ autoGoto: false }); test('should store state between reloads', async ({ page }) => { await Promise.all([ page.waitForRequest( - (request) => - request.url().search(/jlab-ext-example\/hello/) >= 0 && + request => + request.url().search('jupyterlab_examples_server/hello') >= 0 && request.method() === 'GET' ), page.waitForRequest( - (request) => - request.url().search(/jlab-ext-example\/hello/) >= 0 && + request => + request.url().search('/jupyterlab_examples_server/hello') >= 0 && request.method() === 'POST' && request.postDataJSON()?.name === 'George' ), - page.goto(), + page.goto() ]); await page.waitForSelector('div[role="main"] >> text=Launcher'); await page .waitForSelector('text=Get Server Content in a IFrame Widget') - .then((h) => h.scrollIntoViewIfNeeded()); + .then(h => h.scrollIntoViewIfNeeded()); - // Click text=Get Server Content in a IFrame Widget + // Click the launcher widget to open an IFrame Widget await page.click('text=Get Server Content in a IFrame Widget'); // Wait for div[role="main"] >> text=Server Doc @@ -32,9 +32,9 @@ test('should store state between reloads', async ({ page }) => { expect( await page - .frame({ url: /\/jlab-ext-example\/public\/index.html/ }) - .waitForSelector( - 'text=This content is served from the jlab_ext_example server extension.' + .frame({ url: '/jupyterlab_examples_server/public/index.html' }) + ?.waitForSelector( + 'text=This content is served from the jupyterlab_examples_server extension.' ) ).toBeTruthy(); }); diff --git a/settings/.eslintrc.js b/settings/.eslintrc.js index c2375786..665374bf 100644 --- a/settings/.eslintrc.js +++ b/settings/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/settings/.gitignore b/settings/.gitignore index 18cace13..ef3ace98 100644 --- a/settings/.gitignore +++ b/settings/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/settings/.prettierignore b/settings/.prettierignore new file mode 100644 index 00000000..57517b29 --- /dev/null +++ b/settings/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_settings diff --git a/settings/.prettierrc b/settings/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/settings/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/settings/.stylelintrc b/settings/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/settings/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/settings/.yarnrc.yml b/settings/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/settings/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/settings/CHANGELOG.md b/settings/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/settings/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/settings/LICENSE b/settings/LICENSE new file mode 100644 index 00000000..54d885af --- /dev/null +++ b/settings/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/settings/MANIFEST.in b/settings/MANIFEST.in deleted file mode 100644 index be45817b..00000000 --- a/settings/MANIFEST.in +++ /dev/null @@ -1,22 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml -include jupyter-config/jupyterlab_examples_settings.json - -include package.json -include ts*.json - -graft jupyterlab_examples_settings/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/settings/README.md b/settings/README.md index bc8c4d0f..048f8fbc 100644 --- a/settings/README.md +++ b/settings/README.md @@ -130,13 +130,13 @@ the `package.json` file in the `jupyterlab` section (here `schema`): ```json5 -// package.json#L73-L77 +// package.json#L99-L103 "jupyterlab": { "extension": true, - "schemaDir": "schema", - "outputDir": "jupyterlab_examples_settings/labextension" -}, + "outputDir": "jupyterlab_examples_settings/labextension", + "schemaDir": "schema" +} ``` @@ -147,8 +147,8 @@ And you should not forget to add it to the files of the package: "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "schema/**/*.json", - "style/**/*.{css,eot,js,gif,html,jpg,json,png,svg,woff2,ttf}" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", + "schema/*.json" ], ``` @@ -200,7 +200,7 @@ const extension: JupyterFrontEndPlugin = { // Programmatically change a setting Promise.all([ setting.set('flag', !flag), - setting.set('limit', limit + 1), + setting.set('limit', limit + 1) ]) .then(() => { const newLimit = setting.get('limit').composite as number; @@ -209,20 +209,20 @@ const extension: JupyterFrontEndPlugin = { `Settings Example extension: Limit is set to '${newLimit}' and flag to '${newFlag}'` ); }) - .catch((reason) => { + .catch(reason => { console.error( `Something went wrong when changing the settings.\n${reason}` ); }); - }, + } }); }) - .catch((reason) => { + .catch(reason => { console.error( `Something went wrong when reading the settings.\n${reason}` ); }); - }, + } }; ``` @@ -287,7 +287,7 @@ execute: () => { // Programmatically change a setting Promise.all([ setting.set('flag', !flag), - setting.set('limit', limit + 1), + setting.set('limit', limit + 1) ]) .then(() => { const newLimit = setting.get('limit').composite as number; @@ -296,12 +296,12 @@ execute: () => { `Settings Example extension: Limit is set to '${newLimit}' and flag to '${newFlag}'` ); }) - .catch((reason) => { + .catch(reason => { console.error( `Something went wrong when changing the settings.\n${reason}` ); }); -}, +} ``` @@ -313,7 +313,7 @@ new value. // src/index.ts#L55-L56 setting.set('flag', !flag), -setting.set('limit', limit + 1), +setting.set('limit', limit + 1) ``` diff --git a/settings/RELEASE.md b/settings/RELEASE.md index 07824c0b..b88d75ad 100644 --- a/settings/RELEASE.md +++ b/settings/RELEASE.md @@ -1,22 +1,62 @@ # Making a new release of jupyterlab_examples_settings -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser The extension repository should already be compatible with the Jupyter Releaser. -Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. Here is a summary of the steps to cut a new release: -- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) -- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository - Go to the Actions panel -- Run the "Draft Changelog" workflow -- Merge the Changelog PR -- Run the "Draft Release" workflow -- Run the "Publish Release" workflow +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow ## Publishing to `conda-forge` diff --git a/settings/babel.config.js b/settings/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/settings/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/settings/jest.config.js b/settings/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/settings/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/settings/jupyterlab_examples_settings/__init__.py b/settings/jupyterlab_examples_settings/__init__.py index 353ab522..0244f539 100644 --- a/settings/jupyterlab_examples_settings/__init__.py +++ b/settings/jupyterlab_examples_settings/__init__.py @@ -1,19 +1,9 @@ - -import json -import os.path as osp - from ._version import __version__ -HERE = osp.abspath(osp.dirname(__file__)) - -with open(osp.join(HERE, 'labextension', 'package.json')) as fid: - data = json.load(fid) def _jupyter_labextension_paths(): return [{ - 'src': 'labextension', - 'dest': data['name'] + "src": "labextension", + "dest": "@jupyterlab-examples/settings" }] - - diff --git a/settings/jupyterlab_examples_settings/_version.py b/settings/jupyterlab_examples_settings/_version.py index ee864fc9..133868ac 100644 --- a/settings/jupyterlab_examples_settings/_version.py +++ b/settings/jupyterlab_examples_settings/_version.py @@ -1,2 +1,4 @@ -version_info = (0, 1, 0) -__version__ = ".".join(map(str, version_info)) +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/settings/package.json b/settings/package.json index 9cf65abc..b34a668f 100644 --- a/settings/package.json +++ b/settings/package.json @@ -18,8 +18,8 @@ }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "schema/**/*.json", - "style/**/*.{css,eot,js,gif,html,jpg,json,png,svg,woff2,ttf}" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", + "schema/*.json" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -28,52 +28,77 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jupyterlab_examples_settings/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_settings/labextension jupyterlab_examples_settings/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0", - "@jupyterlab/settingregistry": "^3.1.0", - "@lumino/widgets": "^1.19.0" + "@jupyterlab/application": "^4.0.0-beta.0", + "@jupyterlab/settingregistry": "^4.0.0-beta.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "extension": true, - "schemaDir": "schema", - "outputDir": "jupyterlab_examples_settings/labextension" - }, - "styleModule": "style/index.js" + "outputDir": "jupyterlab_examples_settings/labextension", + "schemaDir": "schema" + } } diff --git a/settings/preview.gif b/settings/preview.gif index 072c26eb..241b9734 100644 Binary files a/settings/preview.gif and b/settings/preview.gif differ diff --git a/settings/pyproject.toml b/settings/pyproject.toml index 2524a0b5..753cc6f7 100644 --- a/settings/pyproject.toml +++ b/settings/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_settings/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_settings/labextension/static/style.js", "jupyterlab_examples_settings/labextension/package.json"] +[project] +name = "jupyterlab_examples_settings" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_settings/labextension"] +exclude = [".github", "binder"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_settings/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/settings" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/settings/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_settings/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_settings/labextension/static/style.js", + "jupyterlab_examples_settings/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_settings/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_settings/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_settings/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/settings/setup.py b/settings/setup.py index d3a86d05..bea23374 100644 --- a/settings/setup.py +++ b/settings/setup.py @@ -1,83 +1 @@ -""" -jupyterlab_examples_settings setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_settings" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/settings" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/settings/src/index.ts b/settings/src/index.ts index 1efa0387..59fa3541 100644 --- a/settings/src/index.ts +++ b/settings/src/index.ts @@ -1,6 +1,6 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; @@ -53,7 +53,7 @@ const extension: JupyterFrontEndPlugin = { // Programmatically change a setting Promise.all([ setting.set('flag', !flag), - setting.set('limit', limit + 1), + setting.set('limit', limit + 1) ]) .then(() => { const newLimit = setting.get('limit').composite as number; @@ -62,20 +62,20 @@ const extension: JupyterFrontEndPlugin = { `Settings Example extension: Limit is set to '${newLimit}' and flag to '${newFlag}'` ); }) - .catch((reason) => { + .catch(reason => { console.error( `Something went wrong when changing the settings.\n${reason}` ); }); - }, + } }); }) - .catch((reason) => { + .catch(reason => { console.error( `Something went wrong when reading the settings.\n${reason}` ); }); - }, + } }; export default extension; diff --git a/settings/style/base.css b/settings/style/base.css index e69de29b..e11f4577 100644 --- a/settings/style/base.css +++ b/settings/style/base.css @@ -0,0 +1,5 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ diff --git a/settings/style/index.css b/settings/style/index.css index 8a7ea29e..e98119b5 100644 --- a/settings/style/index.css +++ b/settings/style/index.css @@ -1 +1 @@ -@import url('base.css'); +@import 'base.css'; diff --git a/settings/tsconfig.json b/settings/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/settings/tsconfig.json +++ b/settings/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/settings/tsconfig.test.json b/settings/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/settings/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/settings/ui-tests/.env b/settings/ui-tests/.env deleted file mode 100644 index 2e71caf1..00000000 --- a/settings/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=settings/jupyterlab_examples_settings -EXT_NAME=settings \ No newline at end of file diff --git a/settings/ui-tests/README.md b/settings/ui-tests/README.md index d2be7291..1fd5276b 100644 --- a/settings/ui-tests/README.md +++ b/settings/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _settings_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _settings_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _settings_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _settings_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/settings/ui-tests/jupyter_server_test_config.py b/settings/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/settings/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/settings/ui-tests/package.json b/settings/ui-tests/package.json index 9c9c6116..cf536019 100644 --- a/settings/ui-tests/package.json +++ b/settings/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/settings-tests", - "version": "0.1.0", - "description": "Integration test for settings example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/settings-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/settings 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/settings/ui-tests/playwright.config.js b/settings/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/settings/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/settings/ui-tests/playwright.config.ts b/settings/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/settings/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/settings/ui-tests/tests/settings.spec.ts b/settings/ui-tests/tests/settings.spec.ts index 9e909d83..8f56981c 100644 --- a/settings/ui-tests/tests/settings.spec.ts +++ b/settings/ui-tests/tests/settings.spec.ts @@ -5,7 +5,7 @@ test.use({ autoGoto: false }); test('should emit console message', async ({ page }) => { const logs: string[] = []; - page.on('console', (message) => { + page.on('console', message => { logs.push(message.text()); }); @@ -14,7 +14,7 @@ test('should emit console message', async ({ page }) => { expect .soft( logs.filter( - (s) => + s => s === "Settings Example extension: Limit is set to '25' and flag to 'false'" ) @@ -24,7 +24,7 @@ test('should emit console message', async ({ page }) => { // Click text=Toggle Flag and Increment Limit await page.getByRole('menuitem', { name: 'Settings Example' }).click(); - page.once('console', (message) => { + page.once('console', message => { expect .soft(message.text()) .toEqual( @@ -32,7 +32,7 @@ test('should emit console message', async ({ page }) => { ); }); - page.once('dialog', (dialog) => { + page.once('dialog', dialog => { expect .soft(dialog.message()) .toEqual( @@ -43,29 +43,28 @@ test('should emit console message', async ({ page }) => { await Promise.all([ page.waitForEvent('console'), page.waitForEvent('dialog'), - page - .getByRole('menuitem', { name: 'Toggle Flag and Increment Limit' }) - .click(), + page.menu.clickMenuItem('Settings Example>Toggle Flag and Increment Limit') ]); - await page.getByRole('menuitem', { name: 'Settings Example' }).click(); + await page.menu.open('Settings Example'); await expect .soft( - page.getByRole('menuitem', { name: 'Toggle Flag and Increment Limit' }) + page.locator( + 'li[data-command="@jupyterlab-examples/settings:toggle-flag"]' + ) ) .toHaveClass(/lm-mod-toggled/); await page.keyboard.press('Escape'); - await page.getByRole('menuitem', { name: 'Settings', exact: true }).click(); - await page - .getByRole('menuitem', { name: 'Advanced Settings Editor' }) - .click(); + await page.menu.clickMenuItem('Settings>Settings Editor'); - await page.getByRole('tab', { name: 'Settings Example' }).click(); + await page.click( + '.jp-PluginList-entry[data-id="@jupyterlab-examples/settings:settings-example"]' + ); let msg = ''; - page.once('console', (message) => { + page.once('console', message => { msg = message.text(); }); diff --git a/signals/.eslintrc.js b/signals/.eslintrc.js index c2375786..665374bf 100644 --- a/signals/.eslintrc.js +++ b/signals/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/signals/.gitignore b/signals/.gitignore index 18cace13..ef3ace98 100644 --- a/signals/.gitignore +++ b/signals/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/signals/.prettierignore b/signals/.prettierignore new file mode 100644 index 00000000..e4a1549b --- /dev/null +++ b/signals/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_signals diff --git a/signals/.prettierrc b/signals/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/signals/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/signals/.stylelintrc b/signals/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/signals/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/signals/.yarnrc.yml b/signals/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/signals/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/signals/CHANGELOG.md b/signals/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/signals/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/signals/LICENSE b/signals/LICENSE new file mode 100644 index 00000000..54d885af --- /dev/null +++ b/signals/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/signals/MANIFEST.in b/signals/MANIFEST.in deleted file mode 100644 index d65867cc..00000000 --- a/signals/MANIFEST.in +++ /dev/null @@ -1,22 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml -include jupyter-config/jupyterlab_examples_signals.json - -include package.json -include ts*.json - -graft jupyterlab_examples_signals/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/signals/README.md b/signals/README.md index 3463d6eb..04701af7 100644 --- a/signals/README.md +++ b/signals/README.md @@ -81,7 +81,7 @@ constructor(options = { node: document.createElement('button') }) { // src/button.ts#L28-L30 private _count: ICount = { - clickCount: 0, + clickCount: 0 }; ``` diff --git a/signals/RELEASE.md b/signals/RELEASE.md index 5ea5ae3c..cdc77803 100644 --- a/signals/RELEASE.md +++ b/signals/RELEASE.md @@ -1,22 +1,62 @@ # Making a new release of jupyterlab_examples_signals -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser The extension repository should already be compatible with the Jupyter Releaser. -Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. Here is a summary of the steps to cut a new release: -- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) -- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository - Go to the Actions panel -- Run the "Draft Changelog" workflow -- Merge the Changelog PR -- Run the "Draft Release" workflow -- Run the "Publish Release" workflow +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow ## Publishing to `conda-forge` diff --git a/signals/babel.config.js b/signals/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/signals/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/signals/jest.config.js b/signals/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/signals/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/signals/jupyterlab_examples_signals/__init__.py b/signals/jupyterlab_examples_signals/__init__.py index 353ab522..4a955c72 100644 --- a/signals/jupyterlab_examples_signals/__init__.py +++ b/signals/jupyterlab_examples_signals/__init__.py @@ -1,19 +1,9 @@ - -import json -import os.path as osp - from ._version import __version__ -HERE = osp.abspath(osp.dirname(__file__)) - -with open(osp.join(HERE, 'labextension', 'package.json')) as fid: - data = json.load(fid) def _jupyter_labextension_paths(): return [{ - 'src': 'labextension', - 'dest': data['name'] + "src": "labextension", + "dest": "@jupyterlab-examples/signals" }] - - diff --git a/signals/jupyterlab_examples_signals/_version.py b/signals/jupyterlab_examples_signals/_version.py index ee864fc9..133868ac 100644 --- a/signals/jupyterlab_examples_signals/_version.py +++ b/signals/jupyterlab_examples_signals/_version.py @@ -1,2 +1,4 @@ -version_info = (0, 1, 0) -__version__ = ".".join(map(str, version_info)) +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/signals/package.json b/signals/package.json index 357ea382..aff19345 100644 --- a/signals/package.json +++ b/signals/package.json @@ -1,7 +1,7 @@ { "name": "@jupyterlab-examples/signals", "version": "0.1.0", - "description": "minimal lab example", + "description": "Minimal example using signals.", "keywords": [ "jupyter", "jupyterlab", @@ -18,8 +18,8 @@ }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "style/**/*.{css,eot,js,gif,html,jpg,json,png,svg,woff2,ttf}", - "schema/**/*.json" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", + "schema/*.json" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -28,59 +28,86 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jupyterlab_examples_signals/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_signals/labextension jupyterlab_examples_signals/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0", - "@jupyterlab/apputils": "^3.1.0", - "@jupyterlab/launcher": "^3.1.0", - "@jupyterlab/translation": "^3.1.0", - "@lumino/algorithm": "^1.3.3", - "@lumino/coreutils": "^1.5.3", - "@lumino/datagrid": "^0.3.1", - "@lumino/disposable": "^1.4.3", - "@lumino/signaling": "^1.4.3", - "@lumino/widgets": "^1.19.0" + "@jupyterlab/application": "^4.0.0-beta.0", + "@jupyterlab/apputils": "^4.0.0-beta.0", + "@jupyterlab/launcher": "^4.0.0-beta.0", + "@jupyterlab/settingregistry": "^4.0.0-beta.0", + "@jupyterlab/translation": "^4.0.0-beta.0", + "@lumino/algorithm": "^2.0.0", + "@lumino/coreutils": "^2.0.0", + "@lumino/datagrid": "^2.0.0", + "@lumino/disposable": "^2.0.0", + "@lumino/signaling": "^2.0.0", + "@lumino/widgets": "^2.0.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "extension": true, "outputDir": "jupyterlab_examples_signals/labextension", "schemaDir": "schema" - }, - "styleModule": "style/index.js" + } } diff --git a/signals/preview.png b/signals/preview.png index 4ca8238d..fc1a0be7 100644 Binary files a/signals/preview.png and b/signals/preview.png differ diff --git a/signals/pyproject.toml b/signals/pyproject.toml index 0cff54c3..02dc3821 100644 --- a/signals/pyproject.toml +++ b/signals/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_signals/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_signals/labextension/static/style.js", "jupyterlab_examples_signals/labextension/package.json"] +[project] +name = "jupyterlab_examples_signals" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_signals/labextension"] +exclude = [".github", "binder"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_signals/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/signals" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/signals/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_signals/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_signals/labextension/static/style.js", + "jupyterlab_examples_signals/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_signals/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_signals/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_signals/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/signals/setup.py b/signals/setup.py index 9a9b3c49..bea23374 100644 --- a/signals/setup.py +++ b/signals/setup.py @@ -1,83 +1 @@ -""" -jupyterlab_examples_signals setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_signals" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/signals" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/signals/src/button.ts b/signals/src/button.ts index f19ab1ab..addb47cf 100644 --- a/signals/src/button.ts +++ b/signals/src/button.ts @@ -26,7 +26,7 @@ export class ButtonWidget extends Widget { } private _count: ICount = { - clickCount: 0, + clickCount: 0 }; private _stateChanged = new Signal(this); diff --git a/signals/src/index.ts b/signals/src/index.ts index 7d7e5053..50cfd9c5 100644 --- a/signals/src/index.ts +++ b/signals/src/index.ts @@ -1,12 +1,9 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; - import { ICommandPalette } from '@jupyterlab/apputils'; - import { ILauncher } from '@jupyterlab/launcher'; - import { ITranslator } from '@jupyterlab/translation'; import { SignalExamplePanel } from './panel'; @@ -22,11 +19,11 @@ namespace CommandIDs { * Initialization data for the extension. */ const extension: JupyterFrontEndPlugin = { - id: 'signals', + id: '@jupyterlab-examples/signals:plugin', autoStart: true, optional: [ILauncher], requires: [ICommandPalette, ITranslator], - activate, + activate }; /** @@ -52,7 +49,7 @@ function activate( if (launcher) { launcher.add({ command: CommandIDs.create, - category: category, + category: category }); } @@ -74,7 +71,7 @@ function activate( commands.addCommand(CommandIDs.create, { label: trans.__('Open the Signal Example Panel'), caption: trans.__('Open the Signal Example Panel'), - execute: createPanel, + execute: createPanel }); // Add items in command palette and menu diff --git a/signals/src/panel.ts b/signals/src/panel.ts index b9fd669a..c38d361f 100644 --- a/signals/src/panel.ts +++ b/signals/src/panel.ts @@ -1,7 +1,7 @@ import { ITranslator, nullTranslator, - TranslationBundle, + TranslationBundle } from '@jupyterlab/translation'; import { Panel } from '@lumino/widgets'; diff --git a/signals/style/base.css b/signals/style/base.css index 52582a1d..878df4a9 100644 --- a/signals/style/base.css +++ b/signals/style/base.css @@ -1,3 +1,9 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ + .jp-SignalExamplePanel { background-color: AliceBlue; } diff --git a/signals/style/index.css b/signals/style/index.css index 8a7ea29e..e98119b5 100644 --- a/signals/style/index.css +++ b/signals/style/index.css @@ -1 +1 @@ -@import url('base.css'); +@import 'base.css'; diff --git a/signals/tsconfig.json b/signals/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/signals/tsconfig.json +++ b/signals/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/signals/tsconfig.test.json b/signals/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/signals/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/signals/ui-tests/.env b/signals/ui-tests/.env deleted file mode 100644 index 82239508..00000000 --- a/signals/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=signals/jupyterlab_examples_signals -EXT_NAME=signals \ No newline at end of file diff --git a/signals/ui-tests/README.md b/signals/ui-tests/README.md index d2be7291..1e6aaa27 100644 --- a/signals/ui-tests/README.md +++ b/signals/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _signals_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _signals_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _signals_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _signals_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/signals/ui-tests/jupyter_server_test_config.py b/signals/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/signals/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/signals/ui-tests/package.json b/signals/ui-tests/package.json index 46244755..42fcbed7 100644 --- a/signals/ui-tests/package.json +++ b/signals/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/signals-tests", - "version": "0.1.0", - "description": "Integration test for signals example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/signals-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/signals 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/signals/ui-tests/playwright.config.js b/signals/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/signals/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/signals/ui-tests/playwright.config.ts b/signals/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/signals/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/signals/ui-tests/tests/signals.spec.ts b/signals/ui-tests/tests/signals.spec.ts index d4d22ebc..b0bd8437 100644 --- a/signals/ui-tests/tests/signals.spec.ts +++ b/signals/ui-tests/tests/signals.spec.ts @@ -1,21 +1,18 @@ import { test, expect } from '@jupyterlab/galata'; test('should emit console message and alert when button is pressed', async ({ - page, + page }) => { - // Click text=Signal Example - await page.click('text=Signal Example'); - - // Click ul[role="menu"] >> text=Open the Signal Example Panel - await page.click('ul[role="menu"] >> text=Open the Signal Example Panel'); + // Open the Signal Example Panel + await page.menu.clickMenuItem('Signal Example>Open the Signal Example Panel'); // Click text=Click me - page.once('console', (message) => { + page.once('console', message => { expect( message.text().startsWith('Hey, a Signal has been received from') ).toEqual(true); }); - page.once('dialog', (dialog) => { + page.once('dialog', dialog => { expect(dialog.message()).toEqual( 'The big red button has been clicked 1 times.' ); @@ -24,10 +21,9 @@ test('should emit console message and alert when button is pressed', async ({ await page.click('text=Click me'); // Close filebrowser - await page.click('text=View'); await Promise.all([ page.waitForSelector('#filebrowser', { state: 'hidden' }), - page.click('ul[role="menu"] >> text=Show Left Sidebar'), + page.menu.clickMenuItem('View>File Browser') ]); expect(await page.screenshot()).toMatchSnapshot('signals-example.png'); diff --git a/signals/ui-tests/tests/signals.spec.ts-snapshots/signals-example-linux.png b/signals/ui-tests/tests/signals.spec.ts-snapshots/signals-example-linux.png index 3c4b8d10..60cd3a49 100644 Binary files a/signals/ui-tests/tests/signals.spec.ts-snapshots/signals-example-linux.png and b/signals/ui-tests/tests/signals.spec.ts-snapshots/signals-example-linux.png differ diff --git a/state/.eslintcache b/state/.eslintcache new file mode 100644 index 00000000..b3948069 --- /dev/null +++ b/state/.eslintcache @@ -0,0 +1 @@ +[{"/home/brichet/projects/extension-examples/state/src/index.ts":"1"},{"size":1714,"mtime":1680616229076}] \ No newline at end of file diff --git a/state/.eslintrc.js b/state/.eslintrc.js index c2375786..665374bf 100644 --- a/state/.eslintrc.js +++ b/state/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/state/.gitignore b/state/.gitignore index 18cace13..ef3ace98 100644 --- a/state/.gitignore +++ b/state/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/state/.prettierignore b/state/.prettierignore new file mode 100644 index 00000000..0d7ae076 --- /dev/null +++ b/state/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_state diff --git a/state/.prettierrc b/state/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/state/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/state/.stylelintrc b/state/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/state/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/state/.yarnrc.yml b/state/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/state/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/state/CHANGELOG.md b/state/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/state/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/state/LICENSE b/state/LICENSE new file mode 100644 index 00000000..54d885af --- /dev/null +++ b/state/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/state/MANIFEST.in b/state/MANIFEST.in deleted file mode 100644 index 2faf7778..00000000 --- a/state/MANIFEST.in +++ /dev/null @@ -1,22 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml -include jupyter-config/jupyterlab_examples_state.json - -include package.json -include ts*.json - -graft jupyterlab_examples_state/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/state/README.md b/state/README.md index def70247..f4a88342 100644 --- a/state/README.md +++ b/state/README.md @@ -19,7 +19,7 @@ jlpm add @jupyterlab/statedb Once this is done. You can import the interface in your code. ```ts -// src/index.ts#L8-L8 +// src/index.ts#L6-L6 import { IStateDB } from '@jupyterlab/statedb'; ``` @@ -27,7 +27,7 @@ import { IStateDB } from '@jupyterlab/statedb'; To see how you can access the state, let's have a look at `src/index.ts`. ```ts -// src/index.ts#L17-L57 +// src/index.ts#L14-L55 const extension: JupyterFrontEndPlugin = { id: PLUGIN_ID, @@ -40,7 +40,7 @@ const extension: JupyterFrontEndPlugin = { app.restored // Get the state object .then(() => state.fetch(PLUGIN_ID)) - .then((value) => { + .then(value => { // Get the option attribute if (value) { option = (value as ReadonlyJSONObject)['option'] as string; @@ -51,24 +51,25 @@ const extension: JupyterFrontEndPlugin = { return InputDialog.getItem({ title: 'Pick an option to persist by the State Example extension', items: options, - current: Math.max(0, options.indexOf(option)), + current: Math.max(0, options.indexOf(option)) }); }) - .then((result) => { + .then(result => { // If the user click on the accept button of the dialog if (result.button.accept) { // Get the user option - option = result.value; + option = result.value || ''; + console.log(`Option "${option}" selected.`); // Save the option in the state database return state.save(PLUGIN_ID, { option }); } }) - .catch((reason) => { + .catch(reason => { console.error( `Something went wrong when reading the state for ${PLUGIN_ID}.\n${reason}` ); }); - }, + } }; ``` @@ -85,7 +86,7 @@ loading the state data for your plugin: ```ts -// src/index.ts#L25-L27 +// src/index.ts#L22-L24 app.restored // Get the state object @@ -100,7 +101,7 @@ should be specifically set when accessing the value. For instance, in this example the variable `option` is of type `string`: ```ts -// src/index.ts#L30-L33 +// src/index.ts#L27-L30 if (value) { option = (value as ReadonlyJSONObject)['option'] as string; @@ -116,12 +117,12 @@ In the example, once the state is read, the user is prompted to choose an option an item list with the default option being stored as a state variable. ```ts -// src/index.ts#L36-L40 +// src/index.ts#L33-L37 return InputDialog.getItem({ title: 'Pick an option to persist by the State Example extension', items: options, - current: Math.max(0, options.indexOf(option)), + current: Math.max(0, options.indexOf(option)) }); ``` @@ -129,7 +130,7 @@ This implies to store the new option done by the user in the state. This is done using the `save` method of `IStateDB`: ```ts -// src/index.ts#L48-L48 +// src/index.ts#L46-L46 return state.save(PLUGIN_ID, { option }); ``` diff --git a/state/RELEASE.md b/state/RELEASE.md index fb007cc4..dfb8ef60 100644 --- a/state/RELEASE.md +++ b/state/RELEASE.md @@ -1,22 +1,62 @@ # Making a new release of jupyterlab_examples_state -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser The extension repository should already be compatible with the Jupyter Releaser. -Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. Here is a summary of the steps to cut a new release: -- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) -- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository - Go to the Actions panel -- Run the "Draft Changelog" workflow -- Merge the Changelog PR -- Run the "Draft Release" workflow -- Run the "Publish Release" workflow +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow ## Publishing to `conda-forge` diff --git a/state/babel.config.js b/state/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/state/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/state/jest.config.js b/state/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/state/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/state/jupyterlab_examples_state/__init__.py b/state/jupyterlab_examples_state/__init__.py index 353ab522..43d4d445 100644 --- a/state/jupyterlab_examples_state/__init__.py +++ b/state/jupyterlab_examples_state/__init__.py @@ -1,19 +1,9 @@ - -import json -import os.path as osp - from ._version import __version__ -HERE = osp.abspath(osp.dirname(__file__)) - -with open(osp.join(HERE, 'labextension', 'package.json')) as fid: - data = json.load(fid) def _jupyter_labextension_paths(): return [{ - 'src': 'labextension', - 'dest': data['name'] + "src": "labextension", + "dest": "@jupyterlab-examples/state" }] - - diff --git a/state/jupyterlab_examples_state/_version.py b/state/jupyterlab_examples_state/_version.py index ee864fc9..133868ac 100644 --- a/state/jupyterlab_examples_state/_version.py +++ b/state/jupyterlab_examples_state/_version.py @@ -1,2 +1,4 @@ -version_info = (0, 1, 0) -__version__ = ".".join(map(str, version_info)) +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/state/package.json b/state/package.json index 16d254e4..3b28d567 100644 --- a/state/package.json +++ b/state/package.json @@ -18,7 +18,7 @@ }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "style/**/*.{css,eot,js,gif,html,jpg,json,png,svg,woff2,ttf}" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -27,52 +27,78 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jupyterlab_examples_state/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_state/labextension jupyterlab_examples_state/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0", - "@jupyterlab/apputils": "^3.1.0", - "@jupyterlab/statedb": "^3.1.0", - "@lumino/coreutils": "^1.5.3" + "@jupyterlab/application": "^4.0.0-beta.0", + "@jupyterlab/apputils": "^4.0.0-beta.0", + "@jupyterlab/statedb": "^4.0.0-beta.0", + "@lumino/coreutils": "^2.0.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "extension": true, "outputDir": "jupyterlab_examples_state/labextension" - }, - "styleModule": "style/index.js" + } } diff --git a/state/preview.gif b/state/preview.gif index 091d83d6..d2299c96 100644 Binary files a/state/preview.gif and b/state/preview.gif differ diff --git a/state/pyproject.toml b/state/pyproject.toml index 3f29c5ca..9a5fd492 100644 --- a/state/pyproject.toml +++ b/state/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_state/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_state/labextension/static/style.js", "jupyterlab_examples_state/labextension/package.json"] +[project] +name = "jupyterlab_examples_state" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_state/labextension"] +exclude = [".github", "binder"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_state/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/state" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/state/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_state/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_state/labextension/static/style.js", + "jupyterlab_examples_state/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_state/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_state/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_state/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/state/setup.py b/state/setup.py index 8630ac09..bea23374 100644 --- a/state/setup.py +++ b/state/setup.py @@ -1,83 +1 @@ -""" -jupyterlab_examples_state setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_state" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/state" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/state/src/index.ts b/state/src/index.ts index de5ab8e1..d674af5a 100644 --- a/state/src/index.ts +++ b/state/src/index.ts @@ -1,12 +1,9 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; - import { InputDialog } from '@jupyterlab/apputils'; - import { IStateDB } from '@jupyterlab/statedb'; - import { ReadonlyJSONObject } from '@lumino/coreutils'; const PLUGIN_ID = '@jupyterlab-examples/state:state-example'; @@ -25,7 +22,7 @@ const extension: JupyterFrontEndPlugin = { app.restored // Get the state object .then(() => state.fetch(PLUGIN_ID)) - .then((value) => { + .then(value => { // Get the option attribute if (value) { option = (value as ReadonlyJSONObject)['option'] as string; @@ -36,24 +33,25 @@ const extension: JupyterFrontEndPlugin = { return InputDialog.getItem({ title: 'Pick an option to persist by the State Example extension', items: options, - current: Math.max(0, options.indexOf(option)), + current: Math.max(0, options.indexOf(option)) }); }) - .then((result) => { + .then(result => { // If the user click on the accept button of the dialog if (result.button.accept) { // Get the user option - option = result.value; + option = result.value || ''; + console.log(`Option "${option}" selected.`); // Save the option in the state database return state.save(PLUGIN_ID, { option }); } }) - .catch((reason) => { + .catch(reason => { console.error( `Something went wrong when reading the state for ${PLUGIN_ID}.\n${reason}` ); }); - }, + } }; export default extension; diff --git a/state/style/base.css b/state/style/base.css index e69de29b..e11f4577 100644 --- a/state/style/base.css +++ b/state/style/base.css @@ -0,0 +1,5 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ diff --git a/state/style/index.css b/state/style/index.css index 8a7ea29e..e98119b5 100644 --- a/state/style/index.css +++ b/state/style/index.css @@ -1 +1 @@ -@import url('base.css'); +@import 'base.css'; diff --git a/state/tsconfig.json b/state/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/state/tsconfig.json +++ b/state/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/state/tsconfig.test.json b/state/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/state/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/state/ui-tests/.env b/state/ui-tests/.env deleted file mode 100644 index ef742515..00000000 --- a/state/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=state/jupyterlab_examples_state -EXT_NAME=state \ No newline at end of file diff --git a/state/ui-tests/README.md b/state/ui-tests/README.md index d2be7291..53d17607 100644 --- a/state/ui-tests/README.md +++ b/state/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _state_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _state_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _state_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _state_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/state/ui-tests/jupyter_server_test_config.py b/state/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/state/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/state/ui-tests/package.json b/state/ui-tests/package.json index 0ab5cb69..95f17524 100644 --- a/state/ui-tests/package.json +++ b/state/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/state-tests", - "version": "0.1.0", - "description": "Integration test for state example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/state-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/state 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/state/ui-tests/playwright.config.js b/state/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/state/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/state/ui-tests/playwright.config.ts b/state/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/state/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/state/ui-tests/tests/state.spec.ts b/state/ui-tests/tests/state.spec.ts index 496812a8..535e1847 100644 --- a/state/ui-tests/tests/state.spec.ts +++ b/state/ui-tests/tests/state.spec.ts @@ -19,13 +19,13 @@ test('should store state between reloads', async ({ page }) => { await Promise.all([ page.waitForRequest( - (request) => + request => request.url().search(/\/api\/workspaces\/default/) >= 0 && request.method() === 'PUT' && '@jupyterlab-examples/state:state-example' in request.postDataJSON()?.data ), - page.getByRole('button', { name: /ok/i }).click(), + page.getByRole('button', { name: /ok/i }).click() ]); // Reload page diff --git a/toolbar-button/.eslintrc.js b/toolbar-button/.eslintrc.js index c2375786..665374bf 100644 --- a/toolbar-button/.eslintrc.js +++ b/toolbar-button/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/toolbar-button/.gitignore b/toolbar-button/.gitignore index 18cace13..ef3ace98 100644 --- a/toolbar-button/.gitignore +++ b/toolbar-button/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/toolbar-button/.prettierignore b/toolbar-button/.prettierignore new file mode 100644 index 00000000..4fd1b91e --- /dev/null +++ b/toolbar-button/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_toolbar_button diff --git a/toolbar-button/.prettierrc b/toolbar-button/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/toolbar-button/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/toolbar-button/.stylelintrc b/toolbar-button/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/toolbar-button/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/toolbar-button/.yarnrc.yml b/toolbar-button/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/toolbar-button/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/toolbar-button/CHANGELOG.md b/toolbar-button/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/toolbar-button/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/toolbar-button/LICENSE b/toolbar-button/LICENSE new file mode 100644 index 00000000..54d885af --- /dev/null +++ b/toolbar-button/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/toolbar-button/MANIFEST.in b/toolbar-button/MANIFEST.in deleted file mode 100644 index b454bc4b..00000000 --- a/toolbar-button/MANIFEST.in +++ /dev/null @@ -1,25 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml -recursive-include jupyter-config *.json - -include package.json -include install.json -include ts*.json -include yarn.lock - -graft jupyterlab_examples_toolbar_button/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib -prune binder - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/toolbar-button/Preview.gif b/toolbar-button/Preview.gif deleted file mode 100644 index ad274221..00000000 Binary files a/toolbar-button/Preview.gif and /dev/null differ diff --git a/toolbar-button/README.md b/toolbar-button/README.md index c26c3b21..8a2567b0 100644 --- a/toolbar-button/README.md +++ b/toolbar-button/README.md @@ -2,7 +2,7 @@ This example shows how to add a button to the notebook toolbar. -![Toolbar button](Preview.gif) +![Toolbar button](preview.gif) In this particular example, the button will clear all cell outputs @@ -15,7 +15,7 @@ import { IDisposable, DisposableDelegate } from '@lumino/disposable'; import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; import { ToolbarButton } from '@jupyterlab/apputils'; @@ -25,7 +25,7 @@ import { DocumentRegistry } from '@jupyterlab/docregistry'; import { NotebookActions, NotebookPanel, - INotebookModel, + INotebookModel } from '@jupyterlab/notebook'; ``` @@ -37,7 +37,7 @@ Firstly you have to register the plugin information. For that you have to pass a const plugin: JupyterFrontEndPlugin = { activate, id: 'toolbar-button', - autoStart: true, + autoStart: true }; ``` @@ -68,7 +68,7 @@ export class ButtonExtension className: 'clear-output-button', label: 'Clear All Outputs', onClick: clearOutput, - tooltip: 'Clear All Outputs', + tooltip: 'Clear All Outputs' }); panel.toolbar.insertItem(10, 'clearOutputs', button); diff --git a/toolbar-button/RELEASE.md b/toolbar-button/RELEASE.md index c320aca4..13c6e251 100644 --- a/toolbar-button/RELEASE.md +++ b/toolbar-button/RELEASE.md @@ -1,22 +1,62 @@ # Making a new release of jupyterlab_examples_toolbar_button -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser The extension repository should already be compatible with the Jupyter Releaser. -Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. Here is a summary of the steps to cut a new release: -- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) -- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository - Go to the Actions panel -- Run the "Draft Changelog" workflow -- Merge the Changelog PR -- Run the "Draft Release" workflow -- Run the "Publish Release" workflow +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow ## Publishing to `conda-forge` diff --git a/toolbar-button/babel.config.js b/toolbar-button/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/toolbar-button/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/toolbar-button/install.json b/toolbar-button/install.json index 49a36d35..d974957f 100644 --- a/toolbar-button/install.json +++ b/toolbar-button/install.json @@ -1,5 +1,5 @@ { "packageManager": "python", "packageName": "jupyterlab_examples_toolbar_button", - "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package clear_cell_outputs" + "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlab_examples_toolbar_button" } diff --git a/toolbar-button/jest.config.js b/toolbar-button/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/toolbar-button/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/toolbar-button/jupyterlab_examples_toolbar_button/__init__.py b/toolbar-button/jupyterlab_examples_toolbar_button/__init__.py index 63040033..8fa2f522 100644 --- a/toolbar-button/jupyterlab_examples_toolbar_button/__init__.py +++ b/toolbar-button/jupyterlab_examples_toolbar_button/__init__.py @@ -1,17 +1,9 @@ - -import json -from pathlib import Path - from ._version import __version__ -HERE = Path(__file__).parent.resolve() - -with (HERE / "labextension" / "package.json").open() as fid: - data = json.load(fid) def _jupyter_labextension_paths(): return [{ "src": "labextension", - "dest": data["name"] + "dest": "@jupyterlab-examples/toolbar-button" }] diff --git a/toolbar-button/jupyterlab_examples_toolbar_button/_version.py b/toolbar-button/jupyterlab_examples_toolbar_button/_version.py index 351fbde5..133868ac 100644 --- a/toolbar-button/jupyterlab_examples_toolbar_button/_version.py +++ b/toolbar-button/jupyterlab_examples_toolbar_button/_version.py @@ -1,18 +1,4 @@ -import json -from pathlib import Path - -__all__ = ["__version__"] - -def _fetchVersion(): - HERE = Path(__file__).parent.resolve() - - for settings in HERE.rglob("package.json"): - try: - with settings.open() as f: - return json.load(f)["version"] - except FileNotFoundError: - pass - - raise FileNotFoundError(f"Could not find package.json under dir {HERE!s}") - -__version__ = _fetchVersion() +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/toolbar-button/package.json b/toolbar-button/package.json index 4dfac61c..d24ab759 100644 --- a/toolbar-button/package.json +++ b/toolbar-button/package.json @@ -1,7 +1,7 @@ { "name": "@jupyterlab-examples/toolbar-button", "version": "0.1.0", - "description": "A JupyterLab extension for clearing all cells output at once.", + "description": "A JupyterLab extension adding a button to the Notebook toolbar.", "keywords": [ "jupyter", "jupyterlab", @@ -13,12 +13,12 @@ }, "license": "BSD-3-Clause", "author": { - "name": "Yash Singhal", - "email": "ramusinghal112@gmail.com" + "name": "Project Jupyter Contributors", + "email": "" }, "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}" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -27,51 +27,77 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jupyterlab_examples_toolbar_button/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_toolbar_button/labextension jupyterlab_examples_toolbar_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0", - "@jupyterlab/apputils": "^3.1.0", - "@jupyterlab/docregistry": "^3.1.0", - "@jupyterlab/notebook": "^3.1.0", - "@lumino/disposable": "^1.4.3" + "@jupyterlab/application": "^4.0.0-beta.0", + "@jupyterlab/apputils": "^4.0.0-beta.0", + "@jupyterlab/docregistry": "^4.0.0-beta.0", + "@jupyterlab/notebook": "^4.0.0-beta.0", + "@lumino/disposable": "^2.0.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "extension": true, "outputDir": "jupyterlab_examples_toolbar_button/labextension" diff --git a/toolbar-button/preview.gif b/toolbar-button/preview.gif new file mode 100644 index 00000000..93783f3b Binary files /dev/null and b/toolbar-button/preview.gif differ diff --git a/toolbar-button/pyproject.toml b/toolbar-button/pyproject.toml index 6b5302ed..0ab006a8 100644 --- a/toolbar-button/pyproject.toml +++ b/toolbar-button/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_toolbar_button/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_toolbar_button/labextension/static/style.js", "jupyterlab_examples_toolbar_button/labextension/package.json"] +[project] +name = "jupyterlab_examples_toolbar_button" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_toolbar_button/labextension"] +exclude = [".github", "binder"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_toolbar_button/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/toolbar-button" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/toolbar-button/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_toolbar_button/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_toolbar_button/labextension/static/style.js", + "jupyterlab_examples_toolbar_button/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_toolbar_button/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_toolbar_button/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_toolbar_button/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/toolbar-button/setup.py b/toolbar-button/setup.py index bcaf517a..bea23374 100644 --- a/toolbar-button/setup.py +++ b/toolbar-button/setup.py @@ -1,83 +1 @@ -""" -jupyterlab_examples_toolbar_button setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_toolbar_button" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/toolbar-button" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/toolbar-button/src/index.ts b/toolbar-button/src/index.ts index 8c66487c..c3178dd3 100644 --- a/toolbar-button/src/index.ts +++ b/toolbar-button/src/index.ts @@ -2,7 +2,7 @@ import { IDisposable, DisposableDelegate } from '@lumino/disposable'; import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; import { ToolbarButton } from '@jupyterlab/apputils'; @@ -12,7 +12,7 @@ import { DocumentRegistry } from '@jupyterlab/docregistry'; import { NotebookActions, NotebookPanel, - INotebookModel, + INotebookModel } from '@jupyterlab/notebook'; /** @@ -21,7 +21,7 @@ import { const plugin: JupyterFrontEndPlugin = { activate, id: 'toolbar-button', - autoStart: true, + autoStart: true }; /** @@ -48,7 +48,7 @@ export class ButtonExtension className: 'clear-output-button', label: 'Clear All Outputs', onClick: clearOutput, - tooltip: 'Clear All Outputs', + tooltip: 'Clear All Outputs' }); panel.toolbar.insertItem(10, 'clearOutputs', button); diff --git a/toolbar-button/style/base.css b/toolbar-button/style/base.css index e69de29b..e11f4577 100644 --- a/toolbar-button/style/base.css +++ b/toolbar-button/style/base.css @@ -0,0 +1,5 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ diff --git a/toolbar-button/style/index.css b/toolbar-button/style/index.css index 8a7ea29e..e98119b5 100644 --- a/toolbar-button/style/index.css +++ b/toolbar-button/style/index.css @@ -1 +1 @@ -@import url('base.css'); +@import 'base.css'; diff --git a/toolbar-button/tsconfig.json b/toolbar-button/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/toolbar-button/tsconfig.json +++ b/toolbar-button/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/toolbar-button/tsconfig.test.json b/toolbar-button/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/toolbar-button/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/toolbar-button/ui-tests/.env b/toolbar-button/ui-tests/.env deleted file mode 100644 index c721e3c0..00000000 --- a/toolbar-button/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=toolbar-button/jupyterlab_examples_toolbar_button -EXT_NAME=toolbar-button \ No newline at end of file diff --git a/toolbar-button/ui-tests/README.md b/toolbar-button/ui-tests/README.md index d2be7291..a76656a7 100644 --- a/toolbar-button/ui-tests/README.md +++ b/toolbar-button/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _toolbar-button_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _toolbar-button_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _toolbar-button_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _toolbar-button_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/toolbar-button/ui-tests/jupyter_server_test_config.py b/toolbar-button/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/toolbar-button/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/toolbar-button/ui-tests/package.json b/toolbar-button/ui-tests/package.json index 712b701d..2bb0802e 100644 --- a/toolbar-button/ui-tests/package.json +++ b/toolbar-button/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/toolbar-button-tests", - "version": "0.1.0", - "description": "Integration test for toolbar-button example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/toolbar-button-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/toolbar-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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/toolbar-button/ui-tests/playwright.config.js b/toolbar-button/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/toolbar-button/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/toolbar-button/ui-tests/playwright.config.ts b/toolbar-button/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/toolbar-button/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/toolbar-button/ui-tests/tests/toolbar-button.spec.ts b/toolbar-button/ui-tests/tests/toolbar-button.spec.ts index 2d0365dd..4e97a57c 100644 --- a/toolbar-button/ui-tests/tests/toolbar-button.spec.ts +++ b/toolbar-button/ui-tests/tests/toolbar-button.spec.ts @@ -1,13 +1,8 @@ import { test, expect } from '@jupyterlab/galata'; test('should clear all outputs when clicked', async ({ page }) => { - // Click text=File - await page.click('text=File'); - // Click ul[role="menu"] >> text=New - await page.click('ul[role="menu"] >> text=New'); - // Click #jp-mainmenu-file-new >> text=Notebook - await page.click('#jp-mainmenu-file-new >> text=Notebook'); - // Click button:has-text("Select") + // Create a new Notebook + await page.menu.clickMenuItem('File>New>Notebook'); await page.click('button:has-text("Select")'); await page.waitForSelector('text=| Idle'); @@ -23,8 +18,7 @@ test('should clear all outputs when clicked', async ({ page }) => { await page.keyboard.press('Shift+Enter'); // Click .lm-Widget.p-Widget.jp-RenderedText - const OUTPUT = - '.lm-Widget.p-Widget.jp-RenderedText >> text=Hello, JupyterLab'; + const OUTPUT = '.lm-Widget.jp-RenderedText >> text=Hello, JupyterLab'; expect(await page.waitForSelector(OUTPUT)).toBeTruthy(); // Click button:has-text("Clear All Outputs") diff --git a/widgets/.eslintrc.js b/widgets/.eslintrc.js index c2375786..665374bf 100644 --- a/widgets/.eslintrc.js +++ b/widgets/.eslintrc.js @@ -3,16 +3,14 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:jsdoc/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', + 'plugin:prettier/recommended' ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', - sourceType: 'module', + sourceType: 'module' }, - plugins: ['@typescript-eslint', 'jsdoc'], + plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/naming-convention': [ 'error', @@ -21,9 +19,9 @@ module.exports = { format: ['PascalCase'], custom: { regex: '^I[A-Z]', - match: true, - }, - }, + match: true + } + } ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', @@ -32,22 +30,10 @@ module.exports = { '@typescript-eslint/quotes': [ 'error', 'single', - { avoidEscape: true, allowTemplateLiterals: false }, + { avoidEscape: true, allowTemplateLiterals: false } ], curly: ['error', 'all'], eqeqeq: 'error', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/no-types': 'warn', - 'prefer-arrow-callback': 'error', - }, - settings: { - jsdoc: { - mode: 'typescript', - }, - react: { - version: 'detect', - }, - }, + 'prefer-arrow-callback': 'error' + } }; diff --git a/widgets/.gitignore b/widgets/.gitignore index 18cace13..ef3ace98 100644 --- a/widgets/.gitignore +++ b/widgets/.gitignore @@ -4,3 +4,10 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Yarn cache +.yarn/ diff --git a/widgets/.prettierignore b/widgets/.prettierignore new file mode 100644 index 00000000..e968270f --- /dev/null +++ b/widgets/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +jupyterlab_examples_widgets diff --git a/widgets/.prettierrc b/widgets/.prettierrc new file mode 100644 index 00000000..d0824a69 --- /dev/null +++ b/widgets/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/widgets/.stylelintrc b/widgets/.stylelintrc new file mode 100644 index 00000000..0e1ff303 --- /dev/null +++ b/widgets/.stylelintrc @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } +} diff --git a/widgets/.yarnrc.yml b/widgets/.yarnrc.yml new file mode 100644 index 00000000..fe1125f5 --- /dev/null +++ b/widgets/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/widgets/CHANGELOG.md b/widgets/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/widgets/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/widgets/LICENSE b/widgets/LICENSE new file mode 100644 index 00000000..54d885af --- /dev/null +++ b/widgets/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/widgets/MANIFEST.in b/widgets/MANIFEST.in deleted file mode 100644 index 04b6c5db..00000000 --- a/widgets/MANIFEST.in +++ /dev/null @@ -1,22 +0,0 @@ -include LICENSE -include README.md -include pyproject.toml -include jupyter-config/jupyterlab_examples_widgets.json - -include package.json -include ts*.json - -graft jupyterlab_examples_widgets/labextension - -# Javascript files -graft src -graft style -prune **/node_modules -prune lib - -# Patterns to exclude from any directory -global-exclude *~ -global-exclude *.pyc -global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints diff --git a/widgets/README.md b/widgets/README.md index be754189..2144094e 100644 --- a/widgets/README.md +++ b/widgets/README.md @@ -49,7 +49,7 @@ in this example: execute: () => { const widget = new ExampleWidget(); shell.add(widget, 'main'); -}, +} ``` @@ -80,13 +80,12 @@ You can associate style properties to the custom CSS class in the file `style/base.css`: - + ```css .jp-example-view { background-color: AliceBlue; } - ``` diff --git a/widgets/RELEASE.md b/widgets/RELEASE.md index 21df6b41..8e94a348 100644 --- a/widgets/RELEASE.md +++ b/widgets/RELEASE.md @@ -1,22 +1,62 @@ # Making a new release of jupyterlab_examples_widgets -The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using `hatch`. By default this will create a tag. +See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. + +```bash +hatch version +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` ## Automated releases with the Jupyter Releaser The extension repository should already be compatible with the Jupyter Releaser. -Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. Here is a summary of the steps to cut a new release: -- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) -- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository - Go to the Actions panel -- Run the "Draft Changelog" workflow -- Merge the Changelog PR -- Run the "Draft Release" workflow -- Run the "Publish Release" workflow +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow ## Publishing to `conda-forge` diff --git a/widgets/babel.config.js b/widgets/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/widgets/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/widgets/jest.config.js b/widgets/jest.config.js new file mode 100644 index 00000000..38993844 --- /dev/null +++ b/widgets/jest.config.js @@ -0,0 +1,21 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = ['@jupyterlab/'].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [ + ...baseConfig.transformIgnorePatterns, + `/node_modules/(?!${esModules}).+` + ] +}; diff --git a/widgets/jupyterlab_examples_widgets/__init__.py b/widgets/jupyterlab_examples_widgets/__init__.py index 353ab522..f9afb9a5 100644 --- a/widgets/jupyterlab_examples_widgets/__init__.py +++ b/widgets/jupyterlab_examples_widgets/__init__.py @@ -1,19 +1,9 @@ - -import json -import os.path as osp - from ._version import __version__ -HERE = osp.abspath(osp.dirname(__file__)) - -with open(osp.join(HERE, 'labextension', 'package.json')) as fid: - data = json.load(fid) def _jupyter_labextension_paths(): return [{ - 'src': 'labextension', - 'dest': data['name'] + "src": "labextension", + "dest": "@jupyterlab-examples/widgets" }] - - diff --git a/widgets/jupyterlab_examples_widgets/_version.py b/widgets/jupyterlab_examples_widgets/_version.py index ee864fc9..133868ac 100644 --- a/widgets/jupyterlab_examples_widgets/_version.py +++ b/widgets/jupyterlab_examples_widgets/_version.py @@ -1,2 +1,4 @@ -version_info = (0, 1, 0) -__version__ = ".".join(map(str, version_info)) +# This file is auto-generated by Hatchling. As such, do not: +# - modify +# - track in version control e.g. be sure to add to .gitignore +__version__ = VERSION = '0.1.0' diff --git a/widgets/package.json b/widgets/package.json index e8a4eadc..83748b9b 100644 --- a/widgets/package.json +++ b/widgets/package.json @@ -1,7 +1,7 @@ { "name": "@jupyterlab-examples/widgets", "version": "0.1.0", - "description": "minimal lab example", + "description": "Minimal lab extension opening a main area widget.", "keywords": [ "jupyter", "jupyterlab", @@ -14,12 +14,12 @@ "license": "BSD-3-Clause", "author": { "name": "Project Jupyter Contributors", - "email": "" + "email": "me@test.com" }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "style/**/*.{css,eot,js,gif,html,jpg,json,png,svg,woff2,ttf}", - "schema/**/*.json" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", + "schema/*.json" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -28,53 +28,78 @@ "type": "git", "url": "https://github.com/jupyterlab/extension-examples.git" }, + "workspaces": [ + "ui-tests" + ], "scripts": { - "build": "jlpm run build:lib && jlpm run build:labextension:dev", - "build:all": "jlpm run build:lib && jlpm run build:labextension", + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", - "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension", - "clean": "jlpm run clean:lib", - "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", - "clean:labextension": "rimraf jupyterlab_examples_widgets/labextension", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "eslint": "eslint . --ext .ts,.tsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx", - "install:extension": "jlpm run build", - "prepare": "jlpm run clean && jlpm run build:prod", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf jupyterlab_examples_widgets/labextension jupyterlab_examples_widgets/_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", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", - "watch:labextension": "jupyter labextension watch .", - "watch:src": "tsc -w" + "watch:src": "tsc -w", + "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^3.1.0", - "@lumino/algorithm": "^1.3.3", - "@lumino/coreutils": "^1.5.3", - "@lumino/disposable": "^1.4.3" + "@jupyterlab/application": "^4.0.0-beta.0", + "@jupyterlab/settingregistry": "^4.0.0-beta.0", + "@lumino/widgets": "^2.0.0" }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-jsdoc": "^40.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.18.3", + "@jupyterlab/builder": "^4.0.0-beta.0", + "@jupyterlab/testutils": "^4.0.0-beta.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", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "rimraf": "^3.0.2", - "typescript": "~4.1.3" + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, "jupyterlab": { "extension": true, "outputDir": "jupyterlab_examples_widgets/labextension", "schemaDir": "schema" - }, - "styleModule": "style/index.js" + } } diff --git a/widgets/preview.png b/widgets/preview.png index 21cbcb7f..21cde00b 100644 Binary files a/widgets/preview.png and b/widgets/preview.png differ diff --git a/widgets/pyproject.toml b/widgets/pyproject.toml index d4003b5a..dba10c80 100644 --- a/widgets/pyproject.toml +++ b/widgets/pyproject.toml @@ -1,17 +1,76 @@ [build-system] -requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] -build-backend = "jupyter_packaging.build_api" +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0b0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" -[tool.jupyter-packaging.options] -skip-if-exists = ["jupyterlab_examples_widgets/labextension/static/style.js"] -ensured-targets = ["jupyterlab_examples_widgets/labextension/static/style.js", "jupyterlab_examples_widgets/labextension/package.json"] +[project] +name = "jupyterlab_examples_widgets" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] -[tool.jupyter-packaging.builder] -factory = "jupyter_packaging.npm_builder" +[tool.hatch.build.targets.sdist] +artifacts = ["jupyterlab_examples_widgets/labextension"] +exclude = [".github", "binder"] -[tool.jupyter-packaging.build-args] +[tool.hatch.build.targets.wheel.shared-data] +"jupyterlab_examples_widgets/labextension" = "share/jupyter/labextensions/@jupyterlab-examples/widgets" +"install.json" = "share/jupyter/labextensions/@jupyterlab-examples/widgets/install.json" + +[tool.hatch.build.hooks.version] +path = "jupyterlab_examples_widgets/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "jupyterlab_examples_widgets/labextension/static/style.js", + "jupyterlab_examples_widgets/labextension/package.json", +] +skip-if-exists = ["jupyterlab_examples_widgets/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] -[tool.check-manifest] -ignore = ["jupyterlab_examples_widgets/labextension/**", "yarn.lock", ".*", "package-lock.json"] +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "jupyterlab_examples_widgets/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0b0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/widgets/setup.py b/widgets/setup.py index 9a0f0d1d..bea23374 100644 --- a/widgets/setup.py +++ b/widgets/setup.py @@ -1,83 +1 @@ -""" -jupyterlab_examples_widgets setup -""" -import json -import sys -from pathlib import Path - -import setuptools - -HERE = Path(__file__).parent.resolve() - -# The name of the project -name = "jupyterlab_examples_widgets" - -lab_path = (HERE / name.replace("-", "_") / "labextension") - -# Representative files that should exist after a successful build -ensured_targets = [ - str(lab_path / "package.json"), - str(lab_path / "static/style.js") -] - -labext_name = "@jupyterlab-examples/widgets" - -data_files_spec = [ - ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), - ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), -] - -long_description = (HERE / "README.md").read_text() - -# Get the package info from package.json -pkg_json = json.loads((HERE / "package.json").read_bytes()) - -setup_args = dict( - name=name, - version=pkg_json["version"], - url=pkg_json["homepage"], - author=pkg_json["author"]["name"], - author_email=pkg_json["author"]["email"], - description=pkg_json["description"], - license=pkg_json["license"], - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[], - zip_safe=False, - include_package_data=True, - python_requires=">=3.6", - platforms="Linux, Mac OS X, Windows", - keywords=["Jupyter", "JupyterLab", "JupyterLab3"], - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Jupyter", - ], -) - -try: - from jupyter_packaging import ( - wrap_installers, - npm_builder, - get_data_files - ) - post_develop = npm_builder( - build_cmd="install:extension", source_dir="src", build_dir=lab_path - ) - setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) - setup_args["data_files"] = get_data_files(data_files_spec) -except ImportError as e: - import logging - logging.basicConfig(format="%(levelname)s: %(message)s") - logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") - if not ("--name" in sys.argv or "--version" in sys.argv): - raise e - -if __name__ == "__main__": - setuptools.setup(**setup_args) +__import__('setuptools').setup() diff --git a/widgets/src/index.ts b/widgets/src/index.ts index a42858dc..a804a705 100644 --- a/widgets/src/index.ts +++ b/widgets/src/index.ts @@ -1,6 +1,6 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; import { ICommandPalette } from '@jupyterlab/apputils'; @@ -11,7 +11,7 @@ import { Widget } from '@lumino/widgets'; * Activate the widgets example extension. */ const extension: JupyterFrontEndPlugin = { - id: 'widgets-example', + id: '@jupyterlab-examples/widgets:plugin', autoStart: true, requires: [ICommandPalette], activate: (app: JupyterFrontEnd, palette: ICommandPalette) => { @@ -24,10 +24,10 @@ const extension: JupyterFrontEndPlugin = { execute: () => { const widget = new ExampleWidget(); shell.add(widget, 'main'); - }, + } }); palette.addItem({ command, category: 'Extension Examples' }); - }, + } }; export default extension; diff --git a/widgets/style/base.css b/widgets/style/base.css index 3166234c..0ce1ce7c 100644 --- a/widgets/style/base.css +++ b/widgets/style/base.css @@ -1,3 +1,9 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ + .jp-example-view { background-color: AliceBlue; } diff --git a/widgets/style/index.css b/widgets/style/index.css index 8a7ea29e..e98119b5 100644 --- a/widgets/style/index.css +++ b/widgets/style/index.css @@ -1 +1 @@ -@import url('base.css'); +@import 'base.css'; diff --git a/widgets/tsconfig.json b/widgets/tsconfig.json index 7df12ea9..4f3547da 100644 --- a/widgets/tsconfig.json +++ b/widgets/tsconfig.json @@ -16,9 +16,9 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "strictNullChecks": false, - "target": "es2018", - "types": [] + "strictNullChecks": true, + "target": "ES2018", + "types": ["jest"] }, "include": ["src/*"] } diff --git a/widgets/tsconfig.test.json b/widgets/tsconfig.test.json new file mode 100644 index 00000000..1c66acf6 --- /dev/null +++ b/widgets/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/widgets/ui-tests/.env b/widgets/ui-tests/.env deleted file mode 100644 index c60bd571..00000000 --- a/widgets/ui-tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -EXT_FOLDER=widgets/jupyterlab_examples_widgets -EXT_NAME=widgets \ No newline at end of file diff --git a/widgets/ui-tests/README.md b/widgets/ui-tests/README.md index d2be7291..b68db28d 100644 --- a/widgets/ui-tests/README.md +++ b/widgets/ui-tests/README.md @@ -1,117 +1,148 @@ -# Test +# Integration Testing -The test will produce a video to help debugging and check what happened. +This folder contains the integration tests of the extension. -To execute integration tests, you have two options: +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. -- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution. -- run tests locally (cons: will interact with your JupyterLab user settings) +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). -## Test on docker +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +## Run the tests + +> All commands are assumed to be executed from the _widgets_ directory + +To run the tests, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Execute the docker stack in the example folder: +> Check the extension is installed in JupyterLab. +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test ``` -## Test locally +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the _widgets_ directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password +> Check the extension is installed in JupyterLab. -``` -jupyter lab --ServerApp.token= --ServerApp.password= +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests: +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: +```sh +cd ./ui-tests +jlpm playwright test -u ``` -cd ui-tests -jlpm install -npx playwright install -npx playwright test -``` -# Create tests +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the _widgets_ directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: +> Check the extension is installed in JupyterLab. -**Using docker** +2. Install test dependencies (needed only once): -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -**Using local installation** +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 ``` -3. Launch the code generator tool: - -``` -cd ui-tests -jlpm install -npx playwright install -npx playwright codegen localhost:8888 -``` +## Debug tests -# Debug tests +> All commands are assumed to be executed from the _widgets_ directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: -``` +```sh jlpm install -jlpm run build:prod +jlpm build:prod ``` -2. Start JupyterLab _with the extension installed_ without any token or password: - -**Using docker** - -``` -docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab -``` +> Check the extension is installed in JupyterLab. -**Using local installation** +2. Install test dependencies (needed only once): -``` -jupyter lab --ServerApp.token= --ServerApp.password= +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. ``` -3. Launch the debug tool: +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): -``` -cd ui-tests -jlpm install -npx playwright install -PWDEBUG=1 npx playwright test +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test ``` diff --git a/widgets/ui-tests/jupyter_server_test_config.py b/widgets/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..46e31889 --- /dev/null +++ b/widgets/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,14 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) +# FIXME upstream +c.LabApp.dev_mode = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/widgets/ui-tests/package.json b/widgets/ui-tests/package.json index 43227648..74fc4436 100644 --- a/widgets/ui-tests/package.json +++ b/widgets/ui-tests/package.json @@ -1,14 +1,15 @@ { - "name": "@jupyterlab-examples/widgets-tests", - "version": "0.1.0", - "description": "Integration test for widgets example", - "repository": "https://github.com/jupyterlab/extension-examples", - "author": "Project Jupyter Contributors", - "license": "BSD-3-Clause", + "name": "@jupyterlab-examples/widgets-ui-tests", + "version": "1.0.0", + "description": "JupyterLab @jupyterlab-examples/widgets 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": "^4.5.1", - "@playwright/test": "1.31.2", - "typescript": "~4.1.3" + "@jupyterlab/galata": "^5.0.0-beta.0", + "@playwright/test": "^1.31.0" } } diff --git a/widgets/ui-tests/playwright.config.js b/widgets/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/widgets/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/widgets/ui-tests/playwright.config.ts b/widgets/ui-tests/playwright.config.ts deleted file mode 100644 index 223d8850..00000000 --- a/widgets/ui-tests/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - timeout: 60000, - use: { - // Browser options - // headless: false, - // slowMo: 500, - - // Context options - viewport: { width: 1280, height: 720 }, - - // Artifacts - video: 'on', - }, -}; - -export default config; diff --git a/widgets/ui-tests/tests/widgets.spec.ts b/widgets/ui-tests/tests/widgets.spec.ts index be3840bd..fd6388db 100644 --- a/widgets/ui-tests/tests/widgets.spec.ts +++ b/widgets/ui-tests/tests/widgets.spec.ts @@ -2,17 +2,13 @@ import { test, expect } from '@jupyterlab/galata'; test('should open a widget panel', async ({ page }) => { // Close filebrowser - await page.click('text=View'); await Promise.all([ page.waitForSelector('#filebrowser', { state: 'hidden' }), - page.click('ul[role="menu"] >> text=Show Left Sidebar'), + page.menu.clickMenuItem('View>File Browser') ]); - // Click text=Widget Example - await page.click('text=Widget Example'); - - // Click ul[role="menu"] >> text=Open a Tab Widget - await page.click('ul[role="menu"] >> text=Open a Tab Widget'); + // Open a new tab from menu + await page.menu.clickMenuItem('Widget Example>Open a Tab Widget'); await page.click('div[role="main"] >> text=Widget Example View'); diff --git a/widgets/ui-tests/tests/widgets.spec.ts-snapshots/widgets-example-linux.png b/widgets/ui-tests/tests/widgets.spec.ts-snapshots/widgets-example-linux.png index ae917a82..6fc3995a 100644 Binary files a/widgets/ui-tests/tests/widgets.spec.ts-snapshots/widgets-example-linux.png and b/widgets/ui-tests/tests/widgets.spec.ts-snapshots/widgets-example-linux.png differ