diff --git a/.github/workflows/check-tests.yml b/.github/workflows/check-tests.yml new file mode 100644 index 00000000..36f0f91f --- /dev/null +++ b/.github/workflows/check-tests.yml @@ -0,0 +1,71 @@ +name: Tests validation + +on: + push: + branches: ["*.0"] + pull_request: + branches: ["*"] + schedule: + - cron: "0 0 * * *" + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + + - name: Install dependencies + run: python -m pip install -U cookiecutter check-manifest jupyterlab~=3.1 + + - name: Create the extension + run: | + set -eux + python -m cookiecutter --no-input . + + - name: Test the extension + working-directory: myextension + run: | + set -eux + jlpm + jlpm test + + - name: Install the extension + working-directory: myextension + run: | + set -eux + python -m pip install . + + - name: Install dependencies + working-directory: myextension/ui-tests + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + run: jlpm install + + - name: Set up browser cache + uses: actions/cache@v2 + with: + path: | + ${{ github.workspace }}/pw-browsers + key: ${{ runner.os }}-${{ hashFiles('myextension/ui-tests/yarn.lock') }} + + - name: Install browser + run: jlpm playwright install chromium + working-directory: myextension/ui-tests + + - name: Execute integration tests + working-directory: myextension/ui-tests + run: | + jlpm playwright test + + - name: Upload Playwright Test report + if: always() + uses: actions/upload-artifact@v2 + with: + name: myextension-playwright-tests + path: | + myextension/ui-tests/test-results + myextension/ui-tests/playwright-report diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 478c0b0e..532040ac 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,7 +30,7 @@ jobs: - name: Install dependencies run: | - python -m pip install cookiecutter check-manifest build + python -m pip install cookiecutter check-manifest - name: Create pure frontend extension env: @@ -39,20 +39,20 @@ jobs: run: | set -eux # Trick to use custom parameters - python -c "from cookiecutter.main import cookiecutter; import json, os; f=open('cookiecutter.json'); d=json.load(f); f.close(); d['labextension_name']=os.getenv('NAME'); cookiecutter('.', extra_context=d, no_input=True)" + python -c "from cookiecutter.main import cookiecutter; import json, os; f=open('cookiecutter.json'); d=json.load(f); f.close(); d['kind']=d['kind'][0]; d['labextension_name']=os.getenv('NAME'); cookiecutter('.', extra_context=d, no_input=True)" pushd ${PYNAME} - pip install jupyterlab + python -m pip install jupyterlab jlpm jlpm stylelint-config-prettier-check jlpm lint:check - pip install -e . + python -m pip install -e . jupyter labextension develop . --overwrite jupyter labextension list jupyter labextension list 2>&1 | grep -ie "${NAME}.*OK" python -m jupyterlab.browser_check jupyter labextension uninstall ${NAME} - pip uninstall -y ${NAME} jupyterlab + python -m pip uninstall -y ${NAME} jupyterlab git init && git add . check-manifest -v @@ -60,6 +60,48 @@ jobs: popd rm -rf ${NAME} + no-tests: + runs-on: ubuntu-latest + strategy: + matrix: + # This will be used by the base setup action + python-version: ["3.10"] + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + + - name: Install dependencies + run: | + python -m pip install cookiecutter check-manifest + + - name: Create pure frontend extension + run: | + set -eux + # Trick to use custom parameters + python -c "from cookiecutter.main import cookiecutter; import json; f=open('cookiecutter.json'); d=json.load(f); f.close(); d['kind']=d['kind'][0]; d['test']='n'; cookiecutter('.', extra_context=d, no_input=True)" + pushd myextension + pip install jupyterlab + jlpm + jlpm lint:check + pip install -e . + jupyter labextension develop . --overwrite + jupyter labextension list + jupyter labextension list 2>&1 | grep -ie "myextension.*OK" + python -m jupyterlab.browser_check + + jupyter labextension uninstall myextension + pip uninstall -y myextension jupyterlab + + git init && git add . + check-manifest -v + + popd + rm -rf myextension + settings: runs-on: ubuntu-latest strategy: @@ -76,13 +118,13 @@ jobs: - name: Install dependencies run: | - python -m pip install cookiecutter check-manifest build + python -m pip install cookiecutter check-manifest - name: Create pure frontend extension run: | set -eux # Trick to use custom parameters - python -c "from cookiecutter.main import cookiecutter; import json; f=open('cookiecutter.json'); d=json.load(f); f.close(); d['has_settings']='y'; cookiecutter('.', extra_context=d, no_input=True)" + python -c "from cookiecutter.main import cookiecutter; import json; f=open('cookiecutter.json'); d=json.load(f); f.close(); d['kind']=d['kind'][0]; d['has_settings']='y'; cookiecutter('.', extra_context=d, no_input=True)" pushd myextension pip install jupyterlab jlpm @@ -154,11 +196,14 @@ jobs: # Trick to use custom parameters python -c "from cookiecutter.main import cookiecutter; import json; f=open('cookiecutter.json'); d=json.load(f); f.close(); d['kind']='server'; cookiecutter('.', extra_context=d, no_input=True)" cd myextension - pip install -e . - pip install jupyterlab + python -m pip install -e .[test] + python -m pip install jupyterlab jupyter labextension develop . --overwrite jupyter server extension enable myextension + # Check unit tests are passing + python -m pytest -vv -r ap --cov myextension + - name: Check pip develop method run: | set -eux @@ -177,7 +222,7 @@ jobs: jupyter labextension build ./myextension jupyter labextension uninstall myextension - pip uninstall -y myextension jupyterlab + python -m pip uninstall -y myextension jupyterlab python -c "import shutil; shutil.rmtree('myextension')" @@ -186,11 +231,11 @@ jobs: # Trick to use custom parameters python -c "from cookiecutter.main import cookiecutter; import json; f=open('cookiecutter.json'); d=json.load(f); f.close(); d['kind']='server'; cookiecutter('.', extra_context=d, no_input=True)" cd myextension - pip install jupyterlab + python -m pip install jupyterlab jupyter lab clean --all python -m build --sdist cd dist - pip install myextension-0.1.0.tar.gz + python -m pip install myextension-0.1.0.tar.gz - name: Check install tarball method run: | @@ -205,7 +250,7 @@ jobs: python -m jupyterlab.browser_check cp myextension/dist/*.tar.gz myextension.tar.gz - pip uninstall -y myextension jupyterlab + python -m pip uninstall -y myextension jupyterlab rm -rf myextension shell: bash @@ -248,8 +293,8 @@ jobs: sudo rm -rf $(which node) sudo rm -rf $(which node) - pip install myextension.tar.gz - pip install jupyterlab + python -m pip install myextension.tar.gz + python -m pip install jupyterlab jupyter labextension list 2>&1 | grep -ie "myextension.*OK" jupyter server extension list jupyter server extension list 2>&1 | grep -ie "myextension.*OK" @@ -279,17 +324,17 @@ jobs: # Trick to use custom parameters python -c "from cookiecutter.main import cookiecutter; import json; f=open('cookiecutter.json'); d=json.load(f); f.close(); d['kind']='theme'; cookiecutter('.', extra_context=d, no_input=True)" pushd mytheme - pip install jupyterlab + python -m pip install jupyterlab jlpm jlpm lint:check - pip install -e . + python -m pip install -e . jupyter labextension develop . --overwrite jupyter labextension list jupyter labextension list 2>&1 | grep -ie "mytheme.*OK" python -m jupyterlab.browser_check jupyter labextension uninstall mytheme - pip uninstall -y mytheme jupyterlab + python -m pip uninstall -y mytheme jupyterlab git init && git add . check-manifest -v diff --git a/cookiecutter.json b/cookiecutter.json index 2c9a9368..16c4c9ba 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -7,5 +7,6 @@ "project_short_description": "A JupyterLab extension.", "has_settings": "n", "has_binder": "n", + "test": "y", "repository": "https://github.com/github_username/{{ cookiecutter.labextension_name }}" } diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 5600c3f5..58de3d4d 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -4,11 +4,14 @@ PROJECT_DIRECTORY = Path.cwd() -def remove_path(path: str) -> None: +def remove_path(path: Path) -> None: """Remove the provided path. If the target path is a directory, remove it recursively. """ + if not path.exists(): + return + if path.is_file(): path.unlink() elif path.is_dir(): @@ -35,10 +38,21 @@ def remove_path(path: str) -> None: for f in ( "{{ cookiecutter.python_name }}/handlers.py", "src/handler.ts", - "jupyter-config" + "jupyter-config", + "conftest.py", + "{{ cookiecutter.python_name }}/tests" ): remove_path(PROJECT_DIRECTORY / f) if not "{{ cookiecutter.has_binder }}".lower().startswith("y"): remove_path(PROJECT_DIRECTORY / "binder") remove_path(PROJECT_DIRECTORY / ".github/workflows/binder-on-pr.yml") + + if not "{{ cookiecutter.test }}".lower().startswith("y"): + remove_path(PROJECT_DIRECTORY / ".github" / "workflows" / "update-integration-tests.yml") + remove_path(PROJECT_DIRECTORY / "src" / "__tests__") + remove_path(PROJECT_DIRECTORY / "ui-tests") + remove_path(PROJECT_DIRECTORY / "{{ cookiecutter.python_name }}" / "tests") + remove_path(PROJECT_DIRECTORY / "babel.config.js") + remove_path(PROJECT_DIRECTORY / "conftest.py") + remove_path(PROJECT_DIRECTORY / "jest.config.js") diff --git a/{{cookiecutter.python_name}}/.eslintignore b/{{cookiecutter.python_name}}/.eslintignore index 5c99ba78..fffa32fd 100644 --- a/{{cookiecutter.python_name}}/.eslintignore +++ b/{{cookiecutter.python_name}}/.eslintignore @@ -3,3 +3,6 @@ dist coverage **/*.d.ts tests + +**/__tests__ +ui-tests diff --git a/{{cookiecutter.python_name}}/.github/workflows/build.yml b/{{cookiecutter.python_name}}/.github/workflows/build.yml index 04557a93..cc8ca757 100644 --- a/{{cookiecutter.python_name}}/.github/workflows/build.yml +++ b/{{cookiecutter.python_name}}/.github/workflows/build.yml @@ -9,6 +9,7 @@ on: jobs: build: runs-on: ubuntu-latest + steps: - name: Checkout uses: actions/checkout@v2 @@ -19,30 +20,45 @@ jobs: - name: Install dependencies run: python -m pip install -U jupyterlab~=3.1 check-manifest - - name: Build the extension + - name: Lint the extension run: | set -eux jlpm - jlpm lint:check - python -m pip install . + jlpm run lint:check +{% if cookiecutter.test.lower().startswith('y') %} + - name: Test the extension + run: | + set -eux + jlpm run test +{% endif %} + - name: Build the extension + run: | + set -eux + python -m pip install .[test] {% if cookiecutter.kind.lower() == 'server' %} + pytest -vv -r ap --cov {{ cookiecutter.python_name }} + jupyter server extension list jupyter server extension list 2>&1 | grep -ie "{{ cookiecutter.python_name }}.*OK" {% endif %} + jupyter labextension list jupyter labextension list 2>&1 | grep -ie "{{ cookiecutter.labextension_name }}.*OK" python -m jupyterlab.browser_check + - name: Package the extension + run: | + set -eux check-manifest -v pip install build - python -m build --sdist - cp dist/*.tar.gz myextension.tar.gz + python -m build pip uninstall -y "{{ cookiecutter.python_name }}" jupyterlab - rm -rf myextension - - uses: actions/upload-artifact@v2 + - name: Upload extension packages + uses: actions/upload-artifact@v2 with: - name: myextension-sdist - path: myextension.tar.gz + name: extension-artifacts + path: dist/{{ cookiecutter.python_name }}* + if-no-files-found: error test_isolated: needs: build @@ -54,18 +70,80 @@ jobs: - name: Install Python uses: actions/setup-python@v2 with: - python-version: '3.8' + python-version: '3.9' architecture: 'x64' - uses: actions/download-artifact@v2 with: - name: myextension-sdist + name: extension-artifacts - name: Install and Test run: | set -eux # Remove NodeJS, twice to take care of system and locally installed node versions. sudo rm -rf $(which node) sudo rm -rf $(which node) - pip install myextension.tar.gz - pip install jupyterlab + + pip install "jupyterlab~=3.1" {{ cookiecutter.python_name }}*.whl + +{% if cookiecutter.kind.lower() == 'server' %} + jupyter server extension list + jupyter server extension list 2>&1 | grep -ie "{{ cookiecutter.python_name }}.*OK" +{% endif %} + jupyter labextension list jupyter labextension list 2>&1 | grep -ie "{{ cookiecutter.labextension_name }}.*OK" python -m jupyterlab.browser_check --no-chrome-test +{% if cookiecutter.test.lower().startswith('y') %} + integration-tests: + name: Integration tests + needs: build + runs-on: ubuntu-latest + + env: + PLAYWRIGHT_BROWSERS_PATH: ${{ "{{ github.workspace }}" }}/pw-browsers + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + + - name: Download extension package + uses: actions/download-artifact@v2 + with: + name: extension-artifacts + + - name: Install the extension + run: | + set -eux + python -m pip install "jupyterlab~=3.1" {{ cookiecutter.python_name }}*.whl + + - name: Install dependencies + working-directory: ui-tests + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + run: jlpm install +{% raw %} + - name: Set up browser cache + uses: actions/cache@v2 + with: + path: | + ${{ github.workspace }}/pw-browsers + key: ${{ runner.os }}-${{ hashFiles('ui-tests/yarn.lock') }} +{% endraw %} + - name: Install browser + run: jlpm playwright install chromium + working-directory: ui-tests + + - name: Execute integration tests + working-directory: ui-tests + run: | + jlpm playwright test + + - name: Upload Playwright Test report + if: always() + uses: actions/upload-artifact@v2 + with: + name: {{ cookiecutter.python_name }}-playwright-tests + path: | + ui-tests/test-results + ui-tests/playwright-report{% endif %} diff --git a/{{cookiecutter.python_name}}/.github/workflows/update-integration-tests.yml b/{{cookiecutter.python_name}}/.github/workflows/update-integration-tests.yml new file mode 100644 index 00000000..71a4c511 --- /dev/null +++ b/{{cookiecutter.python_name}}/.github/workflows/update-integration-tests.yml @@ -0,0 +1,44 @@ +name: Update Playwright Snapshots + +on: + issue_comment: + types: [created, edited] + +permissions: + contents: write + pull-requests: write + +jobs: + {# Escape double curly brace #} + {% raw %} + update-snapshots: + if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, 'please update playwright snapshots') }} + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure git to use https + run: git config --global hub.protocol https + + - name: Checkout the branch from the PR that triggered the job + run: hub pr checkout ${{ github.event.issue.number }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies + run: | + set -eux + jlpm + python -m pip install . + + - uses: jupyterlab/maintainer-tools/.github/actions/update-snapshots@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + # Playwright knows how to start JupyterLab server + start_server_script: 'null' + test_folder: ui-tests + {% endraw %} \ No newline at end of file diff --git a/{{cookiecutter.python_name}}/.gitignore b/{{cookiecutter.python_name}}/.gitignore index f65e39ac..dd448c5c 100644 --- a/{{cookiecutter.python_name}}/.gitignore +++ b/{{cookiecutter.python_name}}/.gitignore @@ -8,6 +8,10 @@ node_modules/ *.tsbuildinfo {{cookiecutter.python_name}}/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 diff --git a/{{cookiecutter.python_name}}/MANIFEST.in b/{{cookiecutter.python_name}}/MANIFEST.in index 9fa017c7..f7776de0 100644 --- a/{{cookiecutter.python_name}}/MANIFEST.in +++ b/{{cookiecutter.python_name}}/MANIFEST.in @@ -1,11 +1,13 @@ include LICENSE include *.md include pyproject.toml{% if cookiecutter.kind == "server" %} -recursive-include jupyter-config *.json{% endif %} +recursive-include jupyter-config *.json{% endif %}{% if cookiecutter.test.lower().startswith('y') %} +include conftest.py{% endif %} include package.json include install.json -include ts*.json +include ts*.json{% if cookiecutter.test.lower().startswith('y') %} +include *.config.js{% endif %} include yarn.lock graft {{ cookiecutter.python_name }}/labextension @@ -13,7 +15,8 @@ graft {{ cookiecutter.python_name }}/labextension # Javascript files graft src graft style{% if cookiecutter.has_settings.lower().startswith('y') %} -graft schema{% endif %} +graft schema{% endif %}{% if cookiecutter.test.lower().startswith('y') %} +graft ui-tests{% endif %} prune **/node_modules prune lib prune binder diff --git a/{{cookiecutter.python_name}}/README.md b/{{cookiecutter.python_name}}/README.md index 8947613c..6d775be1 100644 --- a/{{cookiecutter.python_name}}/README.md +++ b/{{cookiecutter.python_name}}/README.md @@ -97,7 +97,43 @@ pip uninstall {{ cookiecutter.python_name }} In development mode, you will also need to remove the symlink created by `jupyter labextension develop` command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions` folder is located. Then you can remove the symlink named `{{ cookiecutter.labextension_name }}` within that folder. +{% if cookiecutter.test.lower().startswith('y') %} +### Testing the extension{% if cookiecutter.kind.lower() == 'server' %} +#### Server tests + +This extension is using [Pytest](https://docs.pytest.org/) for Python code testing. + +Install test dependencies (needed only once): + +```sh +pip install -e ".[test]" +``` + +To execute them, run: + +```sh +pytest -vv -r ap --cov {{ cookiecutter.python_name }} +```{% endif %} + +#### Frontend tests + +This extension is using [Jest](https://jestjs.io/) for JavaScript code testing. + +To execute them, execute: + +```sh +jlpm +jlpm test +``` + +#### Integration tests + +This extension uses [Playwright](https://playwright.dev/docs/intro/) for the integration tests (aka user level tests). +More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab. + +More information are provided within the [ui-tests](./ui-tests/README.md) README. +{% endif %} ### Packaging the extension See [RELEASE](RELEASE.md) diff --git a/{{cookiecutter.python_name}}/babel.config.js b/{{cookiecutter.python_name}}/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/{{cookiecutter.python_name}}/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/{{cookiecutter.python_name}}/conftest.py b/{{cookiecutter.python_name}}/conftest.py new file mode 100644 index 00000000..26d7ca5e --- /dev/null +++ b/{{cookiecutter.python_name}}/conftest.py @@ -0,0 +1,8 @@ +import pytest + +pytest_plugins = ("jupyter_server.pytest_plugin", ) + + +@pytest.fixture +def jp_server_config(jp_server_config): + return {"ServerApp": {"jpserver_extensions": {"{{ cookiecutter.python_name }}": True}}} diff --git a/{{cookiecutter.python_name}}/jest.config.js b/{{cookiecutter.python_name}}/jest.config.js new file mode 100644 index 00000000..514c3809 --- /dev/null +++ b/{{cookiecutter.python_name}}/jest.config.js @@ -0,0 +1,42 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = [ + '@jupyterlab/', + 'lib0', + 'y\\-protocols', + 'y\\-websocket', + 'yjs' +].join('|'); + +const jlabConfig = jestJupyterLab(__dirname); + +const { + moduleFileExtensions, + moduleNameMapper, + preset, + setupFilesAfterEnv, + setupFiles, + testPathIgnorePatterns, + transform +} = jlabConfig; + +module.exports = { + moduleFileExtensions, + moduleNameMapper, + preset, + setupFilesAfterEnv, + setupFiles, + testPathIgnorePatterns, + transform, + automock: false, + collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'], + coverageDirectory: 'coverage', + coverageReporters: ['lcov', 'text'], + globals: { + 'ts-jest': { + tsconfig: 'tsconfig.json' + } + }, + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [`/node_modules/(?!${esModules}).+`] +}; diff --git a/{{cookiecutter.python_name}}/package.json b/{{cookiecutter.python_name}}/package.json index 3fdc1ab9..b59aa9fd 100644 --- a/{{cookiecutter.python_name}}/package.json +++ b/{{cookiecutter.python_name}}/package.json @@ -48,7 +48,8 @@ "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\"", + "stylelint:check": "stylelint --cache \"style/**/*.css\"",{% if cookiecutter.test.lower().startswith('y') %} + "test": "jest --coverage",{% endif %} "watch": "run-p watch:src watch:labextension", "watch:src": "tsc -w", "watch:labextension": "jupyter labextension watch ." @@ -62,12 +63,17 @@ {% endif %} }, "devDependencies": { - "@jupyterlab/builder": "^3.1.0", + {% if cookiecutter.test.lower().startswith('y') %}"@babel/core": "^7.0.0", + "@babel/preset-env": "^7.0.0", + {% endif %}"@jupyterlab/builder": "^3.1.0",{% if cookiecutter.test.lower().startswith('y') %} + "@jupyterlab/testutils": "^3.0.0", + "@types/jest": "^26.0.0",{% endif %} "@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-prettier": "^3.1.4",{% if cookiecutter.kind.lower() == 'server' %} + "eslint-plugin-prettier": "^3.1.4",{% if cookiecutter.test.lower().startswith('y') %} + "jest": "^26.0.0",{% endif %}{% if cookiecutter.kind.lower() == 'server' %} "mkdirp": "^1.0.3",{% endif %} "npm-run-all": "^4.1.5", "prettier": "^2.1.1", @@ -77,7 +83,8 @@ "stylelint-config-recommended": "^6.0.0", "stylelint-config-standard": "~24.0.0", "stylelint-prettier": "^2.0.0", - "typescript": "~4.1.3" + "typescript": "~4.1.3"{% if cookiecutter.test.lower().startswith('y') %}, + "ts-jest": "^26.0.0"{% endif %} }, "sideEffects": [ "style/*.css"{% if cookiecutter.kind.lower() != 'theme' %}, diff --git a/{{cookiecutter.python_name}}/setup.py b/{{cookiecutter.python_name}}/setup.py index 49dc72e5..72649139 100644 --- a/{{cookiecutter.python_name}}/setup.py +++ b/{{cookiecutter.python_name}}/setup.py @@ -55,10 +55,19 @@ license_file="LICENSE", long_description=long_description, long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[{% if cookiecutter.kind.lower() == "server" %} + packages=setuptools.find_packages(),{% if cookiecutter.kind.lower() == "server" %} + install_requires=[ "jupyter_server>=1.6,<2" - {% endif %}], + ], + extras_require={ + "test": [{% if cookiecutter.test.lower().startswith('y') %} + "coverage", + "pytest", + "pytest-asyncio", + "pytest-cov", + "pytest-tornasync" + {% endif %}] + },{% endif %} zip_safe=False, include_package_data=True, python_requires=">=3.7", diff --git a/{{cookiecutter.python_name}}/src/__tests__/{{cookiecutter.python_name}}.spec.ts b/{{cookiecutter.python_name}}/src/__tests__/{{cookiecutter.python_name}}.spec.ts new file mode 100644 index 00000000..244d0548 --- /dev/null +++ b/{{cookiecutter.python_name}}/src/__tests__/{{cookiecutter.python_name}}.spec.ts @@ -0,0 +1,9 @@ +/** + * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests + */ + +describe('{{ cookiecutter.labextension_name }}', () => { + it('should be tested', () => { + expect(1 + 1).toEqual(2); + }); +}); diff --git a/{{cookiecutter.python_name}}/tsconfig.json b/{{cookiecutter.python_name}}/tsconfig.json index b9c465a8..c8eb70e6 100644 --- a/{{cookiecutter.python_name}}/tsconfig.json +++ b/{{cookiecutter.python_name}}/tsconfig.json @@ -18,7 +18,7 @@ "strict": true, "strictNullChecks": true, "target": "es2017", - "types": [] + "types": [{% if cookiecutter.test.lower().startswith('y') %}"jest"{% endif %}] }, "include": ["src/*"] } diff --git a/{{cookiecutter.python_name}}/ui-tests/README.md b/{{cookiecutter.python_name}}/ui-tests/README.md new file mode 100644 index 00000000..faef1bd3 --- /dev/null +++ b/{{cookiecutter.python_name}}/ui-tests/README.md @@ -0,0 +1,149 @@ +# Integration Testing + +This folder contains the integration tests of the extension. + +They are defined using [Playwright](https://playwright.dev/docs/intro/) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. + +The Playwright configuration is defined in [playwright.config.js](../playwright.config.js) +in the root directory. + +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](../jupyter_server_test_config.py) in the root directory. + +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 root directory + +To run the tests, you need to: + +1. Compile the extension: + +```sh +jlpm install +jlpm build:prod +``` + +> Check the extension is installed in JupyterLab. + +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. +``` + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test +``` + +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 root 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 build:prod +``` + +> Check the extension is installed in JupyterLab. + +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. +``` + +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: + +```sh +cd ./ui-tests +jlpm playwright test -u +``` + +> 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 root directory + +To create tests, the easiest way is to use the code generator tool of playwright: + +1. Compile the extension: + +```sh +jlpm install +jlpm build:prod +``` + +> Check the extension is installed in JupyterLab. + +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. +``` + +3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): + +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 +``` + +## Debug tests + +> All commands are assumed to be executed from the root directory + +To debug tests, a good way is to use the inspector tool of playwright: + +1. Compile the extension: + +```sh +jlpm install +jlpm build:prod +``` + +> Check the extension is installed in JupyterLab. + +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. +``` + +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): + +```sh +cd ./ui-tests +PWDEBUG=1 jlpm playwright test +``` diff --git a/{{cookiecutter.python_name}}/ui-tests/jupyter_server_test_config.py b/{{cookiecutter.python_name}}/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..5ba7a914 --- /dev/null +++ b/{{cookiecutter.python_name}}/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,20 @@ +"""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 tempfile import mkdtemp + +c.ServerApp.port = 8888 +c.ServerApp.port_retries = 0 +c.ServerApp.open_browser = False + +c.ServerApp.root_dir = mkdtemp(prefix='galata-test-') +c.ServerApp.token = "" +c.ServerApp.password = "" +c.ServerApp.disable_check_xsrf = True +c.LabApp.expose_app_in_browser = True + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/{{cookiecutter.python_name}}/ui-tests/package.json b/{{cookiecutter.python_name}}/ui-tests/package.json new file mode 100644 index 00000000..0b57eebb --- /dev/null +++ b/{{cookiecutter.python_name}}/ui-tests/package.json @@ -0,0 +1,13 @@ +{ + "name": "{{ cookiecutter.labextension_name }}-ui-tests", + "version": "1.0.0", + "description": "JupyterLab {{ cookiecutter.labextension_name }} Integration Tests", + "private": true, + "scripts": { + "start": "jupyter lab --config jupyter_server_test_config.py", + "test": "jlpm playwright test" + }, + "devDependencies": { + "@jupyterlab/galata": "^4.3.0" + } +} diff --git a/{{cookiecutter.python_name}}/ui-tests/playwright.config.js b/{{cookiecutter.python_name}}/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/{{cookiecutter.python_name}}/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/{{cookiecutter.python_name}}/ui-tests/tests/{{cookiecutter.python_name}}.spec.ts b/{{cookiecutter.python_name}}/ui-tests/tests/{{cookiecutter.python_name}}.spec.ts new file mode 100644 index 00000000..a0c95499 --- /dev/null +++ b/{{cookiecutter.python_name}}/ui-tests/tests/{{cookiecutter.python_name}}.spec.ts @@ -0,0 +1,21 @@ +import { expect, test } from '@jupyterlab/galata'; + +/** + * Don't load JupyterLab webpage before running the tests. + * This is required to ensure we capture all log messages. + */ +test.use({ autoGoto: false }); + +test('should emit an activation console message', async ({ page }) => { + const logs: string[] = []; + + page.on('console', message => { + logs.push(message.text()); + }); + + await page.goto(); + + expect( + logs.filter(s => s === 'JupyterLab extension {{ cookiecutter.labextension_name }} is activated!') + ).toHaveLength(1); +}); diff --git a/{{cookiecutter.python_name}}/{{cookiecutter.python_name}}/tests/__init__.py b/{{cookiecutter.python_name}}/{{cookiecutter.python_name}}/tests/__init__.py new file mode 100644 index 00000000..edc5557d --- /dev/null +++ b/{{cookiecutter.python_name}}/{{cookiecutter.python_name}}/tests/__init__.py @@ -0,0 +1 @@ +"""Python unit tests for {{ cookiecutter.python_name }}.""" diff --git a/{{cookiecutter.python_name}}/{{cookiecutter.python_name}}/tests/test_handlers.py b/{{cookiecutter.python_name}}/{{cookiecutter.python_name}}/tests/test_handlers.py new file mode 100644 index 00000000..f8df064b --- /dev/null +++ b/{{cookiecutter.python_name}}/{{cookiecutter.python_name}}/tests/test_handlers.py @@ -0,0 +1,13 @@ +import json + + +async def test_get_example(jp_fetch): + # When + response = await jp_fetch("{{ cookiecutter.python_name | replace('_', '-') }}", "get_example") + + # Then + assert response.code == 200 + payload = json.loads(response.body) + assert payload == { + "data": "This is /{{ cookiecutter.python_name | replace('_', '-') }}/get_example endpoint!" + } \ No newline at end of file