diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..e01b3e62 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [rmorshea] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..037e3c2f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Documentation + url: https://idom-docs.herokuapp.com/ + about: Refer to the documentation before starting a discussion + - name: Community Support + url: https://github.com/idom-team/idom/discussions + about: Report issues, request features, and ask questions diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..d3097b33 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,35 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: "0 0 * * *" + +jobs: + test-python-versions: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + steps: + - uses: actions/checkout@v2 + - uses: nanasess/setup-chromedriver@master + - uses: actions/setup-node@v2-beta + with: + node-version: "14" + - name: Use Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install Python Dependencies + run: pip install -r requirements/test-run.txt + - name: Run Tests + run: | + npm install -g npm@latest + npm --version + nox -s test -- --headless diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..434278f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,128 @@ +# Django # +logs +*.log +*.pot +*.pyc +.dccachea +__pycache__ +db.sqlite3 +media +cache +static-deploy +data +settings.json + +# Backup files # +*.bak + +# If you are using PyCharm # +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/gradle.xml +.idea/**/libraries +*.iws /out/ + +# Python # +*.py[cod] +*$py.class + +# Distribution / packaging +.Python build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +.pytest_cache/ +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery +celerybeat-schedule.* + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# Sublime Text # +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache +*.sublime-workspace +*.sublime-project + +# sftp configuration file +sftp-config.json + +# Package control specific files Package +Control.last-run +Control.ca-list +Control.ca-bundle +Control.system-ca-bundle +GitHub.sublime-settings + +# Visual Studio Code # +.vscode +.vscode/* +.vscode/settings.json +.vscode/tasks.json +.vscode/launch.json +.vscode/extensions.json +.history +%SystemDrive% + +# Mac file system +.DS_Store diff --git a/README.md b/README.md index 2444be03..943a2aed 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,52 @@ # Django IDOM -Support for IDOM in Django + + Tests + + + Version Info + + + License: MIT + + +A package for building highly interactive user interfaces in pure Python inspired by +[ReactJS](https://reactjs.org/). + +**Be sure to [read the IDOM Documentation](https://idom-docs.herokuapp.com)!** + +If you have ideas or find a bug, be sure to post an +[issue](https://github.com/idom-team/django-idom/issues) +or create a +[pull request](https://github.com/idom-team/django-idom/pulls). Thanks in advance! + +

+ + Try it Now + Binder + +

+ +Click the badge above to get started! It will take you to a [Jupyter Notebooks](https://jupyter.org/) +hosted by [Binder](https://mybinder.org/) with some great examples. + +### Or Install it Now + +```bash +pip install django-idom +``` + +# Django Integration + +This version of IDOM can be directly integrated into Django. For example + +```python +# Example code goes here +``` + +For examples on how to use IDOM, [read the IDOM Documentation](https://idom-docs.herokuapp.com). diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 00000000..4a943345 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +import os +import re +import subprocess +from pathlib import Path +from typing import List, Tuple + +import nox +from nox.sessions import Session + + +HERE = Path(__file__).parent +POSARGS_PATTERN = re.compile(r"^(\w+)\[(.+)\]$") + + +@nox.session(reuse_venv=True) +def manage(session: Session) -> None: + session.install("-r", "requirements.txt") + session.install("idom[stable]") + session.install("-e", ".") + session.chdir("tests") + + build_js_on_commands = ["runserver"] + if set(session.posargs).intersection(build_js_on_commands): + session.run("python", "manage.py", "build_js") + + session.run("python", "manage.py", *session.posargs) + + +@nox.session(reuse_venv=True) +def format(session: Session) -> None: + install_requirements_file(session, "check-style") + session.run("black", ".") + session.run("isort", ".") + + +@nox.session +def test(session: Session) -> None: + """Run the complete test suite""" + session.install("--upgrade", "pip", "setuptools", "wheel") + session.notify("test_suite", posargs=session.posargs) + session.notify("test_style") + + +@nox.session +def test_suite(session: Session) -> None: + """Run the Python-based test suite""" + install_requirements_file(session, "test-env") + session.install(".[all]") + + session.chdir(HERE / "tests") + session.env["IDOM_DEBUG_MODE"] = "1" + session.env["SELENIUM_HEADLESS"] = str(int("--headless" in session.posargs)) + session.run("python", "manage.py", "build_js") + session.run("python", "manage.py", "test") + + +@nox.session +def test_style(session: Session) -> None: + """Check that style guidelines are being followed""" + install_requirements_file(session, "check-style") + session.run("flake8", "src/django_idom", "tests") + black_default_exclude = r"\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist" + session.run( + "black", + ".", + "--check", + "--exclude", + rf"/({black_default_exclude}|venv|node_modules)/", + ) + session.run("isort", ".", "--check-only") + + +def install_requirements_file(session: Session, name: str) -> None: + file_path = HERE / "requirements" / (name + ".txt") + assert file_path.exists(), f"requirements file {file_path} does not exist" + session.install("-r", str(file_path)) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..18a77f5a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,12 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.isort] +multi_line_output = 3 +force_grid_wrap = 0 +use_parentheses = "True" +ensure_newline_before_comments = "True" +include_trailing_comma = "True" +line_length = 88 +lines_after_imports = 2 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..22728910 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +-r requirements/pkg-deps.txt +-r requirements/check-style.txt +-r requirements/test-env.txt +-r requirements/test-run.txt diff --git a/requirements/check-style.txt b/requirements/check-style.txt new file mode 100644 index 00000000..5e5647ac --- /dev/null +++ b/requirements/check-style.txt @@ -0,0 +1,3 @@ +black +flake8 +isort diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt new file mode 100644 index 00000000..b823d656 --- /dev/null +++ b/requirements/pkg-deps.txt @@ -0,0 +1,2 @@ +channels<4.0.0 # Django websocket features +idom<1.0.0 # Python React diff --git a/requirements/test-env.txt b/requirements/test-env.txt new file mode 100644 index 00000000..c100c316 --- /dev/null +++ b/requirements/test-env.txt @@ -0,0 +1,6 @@ +django +selenium + +# required due issue with channels: +# https://github.com/django/channels/issues/1639#issuecomment-817994671 +twisted<21 diff --git a/requirements/test-run.txt b/requirements/test-run.txt new file mode 100644 index 00000000..816817c6 --- /dev/null +++ b/requirements/test-run.txt @@ -0,0 +1 @@ +nox diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..998619dd --- /dev/null +++ b/setup.cfg @@ -0,0 +1,11 @@ +[bdist_wheel] +universal=1 + +[flake8] +ignore = E203, E266, E501, W503, F811, N802 +max-line-length = 88 +max-complexity = 18 +select = B,C,E,F,W,T4,B9,N,ROH +exclude = + .eggs/* + .nox/* diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..dbdbbf67 --- /dev/null +++ b/setup.py @@ -0,0 +1,96 @@ +import os +import sys +from pathlib import Path + +from setuptools import find_packages, setup + + +# the name of the project +name = "django_idom" + +# basic paths used to gather files +root_dir = Path(__file__).parent +src_dir = root_dir / "src" +package_dir = src_dir / name + + +# ----------------------------------------------------------------------------- +# Package Definition +# ----------------------------------------------------------------------------- + + +package = { + "name": name, + "python_requires": ">=3.7", + "packages": find_packages(str(src_dir)), + "package_dir": {"": "src"}, + "description": "Control the web with Python", + "author": "Ryan Morshead", + "author_email": "ryan.morshead@gmail.com", + "url": "https://github.com/idom-team/django-idom", + "license": "MIT", + "platforms": "Linux, Mac OS X, Windows", + "keywords": ["interactive", "widgets", "DOM", "React"], + "zip_safe": False, + "classifiers": [ + "Framework :: Django", + "Framework :: Django :: 3.1", + "Framework :: Django :: 3.2", + "Operating System :: OS Independent", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Topic :: Multimedia :: Graphics", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Environment :: Web Environment", + ], +} + + +# ----------------------------------------------------------------------------- +# Library Version +# ----------------------------------------------------------------------------- + +with open(os.path.join(package_dir, "__init__.py")) as f: + for line in f.read().split("\n"): + if line.startswith("__version__ = "): + package["version"] = eval(line.split("=", 1)[1]) + break + else: + print("No version found in %s/__init__.py" % package_dir) + sys.exit(1) + + +# ----------------------------------------------------------------------------- +# Requirements +# ----------------------------------------------------------------------------- + + +requirements = [] +with (root_dir / "requirements" / "pkg-deps.txt").open() as f: + for line in map(str.strip, f): + if not line.startswith("#"): + requirements.append(line) +package["install_requires"] = requirements + + +# ----------------------------------------------------------------------------- +# Library Description +# ----------------------------------------------------------------------------- + + +with (root_dir / "README.md").open() as f: + long_description = f.read() + +package["long_description"] = long_description +package["long_description_content_type"] = "text/markdown" + + +# ----------------------------------------------------------------------------- +# Install It +# ----------------------------------------------------------------------------- + + +if __name__ == "__main__": + setup(**package) diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py new file mode 100644 index 00000000..60932c7a --- /dev/null +++ b/src/django_idom/__init__.py @@ -0,0 +1,5 @@ +from .websocket_consumer import IdomAsyncWebSocketConsumer + + +__version__ = "0.0.1" +__all__ = ["IdomAsyncWebSocketConsumer"] diff --git a/src/django_idom/websocket_consumer.py b/src/django_idom/websocket_consumer.py new file mode 100644 index 00000000..2fae2869 --- /dev/null +++ b/src/django_idom/websocket_consumer.py @@ -0,0 +1,44 @@ +"""Anything used to construct a websocket endpoint""" +import asyncio +from typing import Any + +from channels.generic.websocket import AsyncJsonWebsocketConsumer +from idom.core.dispatcher import dispatch_single_view +from idom.core.layout import Layout, LayoutEvent +from idom.core.proto import ComponentConstructor + + +class IdomAsyncWebSocketConsumer(AsyncJsonWebsocketConsumer): + """Communicates with the browser to perform actions on-demand.""" + + def __init__( + self, component: ComponentConstructor, *args: Any, **kwargs: Any + ) -> None: + self._idom_component_constructor = component + super().__init__(*args, **kwargs) + + async def connect(self) -> None: + await super().connect() + self._idom_dispatcher_future = asyncio.ensure_future(self._run_dispatch_loop()) + + async def disconnect(self, code: int) -> None: + if self._idom_dispatcher_future.done(): + await self._idom_dispatcher_future + else: + self._idom_dispatcher_future.cancel() + await super().disconnect(code) + + async def receive_json(self, content: Any, **kwargs: Any) -> None: + await self._idom_recv_queue.put(LayoutEvent(**content)) + + async def _run_dispatch_loop(self): + self._idom_recv_queue = recv_queue = asyncio.Queue() + try: + await dispatch_single_view( + Layout(self._idom_component_constructor()), + self.send_json, + recv_queue.get, + ) + except Exception: + await self.close() + raise diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 00000000..bf4a6d58 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +test_app/static/build.js diff --git a/tests/js/.gitignore b/tests/js/.gitignore new file mode 100644 index 00000000..5980a333 --- /dev/null +++ b/tests/js/.gitignore @@ -0,0 +1,2 @@ +# Javascript +node_modules diff --git a/tests/js/package-lock.json b/tests/js/package-lock.json new file mode 100644 index 00000000..6639bf96 --- /dev/null +++ b/tests/js/package-lock.json @@ -0,0 +1,603 @@ +{ + "name": "tests", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "tests", + "version": "1.0.0", + "dependencies": { + "idom-client-react": "^0.8.2" + }, + "devDependencies": { + "prettier": "^2.2.1", + "rollup": "^2.35.1", + "rollup-plugin-commonjs": "^10.1.0", + "rollup-plugin-node-resolve": "^5.2.0", + "rollup-plugin-replace": "^2.2.0" + } + }, + "node_modules/@types/estree": { + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz", + "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==", + "dev": true + }, + "node_modules/@types/node": { + "version": "15.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz", + "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==", + "dev": true + }, + "node_modules/@types/resolve": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", + "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/builtin-modules": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", + "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + }, + "node_modules/fast-json-patch": { + "version": "3.0.0-1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.0.0-1.tgz", + "integrity": "sha512-6pdFb07cknxvPzCeLsFHStEy+MysPJPgZQ9LbQ/2O67unQF93SNqfdSqnPPl71YMHX+AD8gbl7iuoGFzHEdDuw==" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/htm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.0.4.tgz", + "integrity": "sha512-VRdvxX3tmrXuT/Ovt59NMp/ORMFi4bceFMDjos1PV4E0mV+5votuID8R60egR9A4U8nLt238R/snlJGz3UYiTQ==" + }, + "node_modules/idom-client-react": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.8.2.tgz", + "integrity": "sha512-pK4FjyfVIaOVA/R0sj6Ulvpo3FATFU11TbnoqgzGbXjjY7kYPuR3x2pa/M6MY/Ot9yUb2Nsz9Gr1Vj8QPJ6GwA==", + "dependencies": { + "fast-json-patch": "^3.0.0-1", + "htm": "^3.0.3" + }, + "peerDependencies": { + "react": "^16.13.1", + "react-dom": "^16.13.1" + } + }, + "node_modules/is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.4" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/prettier": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz", + "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "node_modules/react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + }, + "peerDependencies": { + "react": "^16.14.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "peer": true + }, + "node_modules/resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.51.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.51.2.tgz", + "integrity": "sha512-ReV2eGEadA7hmXSzjxdDKs10neqH2QURf2RxJ6ayAlq93ugy6qIvXMmbc5cWMGCDh1h5T4thuWO1e2VNbMq8FA==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.1" + } + }, + "node_modules/rollup-plugin-commonjs": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz", + "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-commonjs.", + "dev": true, + "dependencies": { + "estree-walker": "^0.6.1", + "is-reference": "^1.1.2", + "magic-string": "^0.25.2", + "resolve": "^1.11.0", + "rollup-pluginutils": "^2.8.1" + }, + "peerDependencies": { + "rollup": ">=1.12.0" + } + }, + "node_modules/rollup-plugin-node-resolve": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz", + "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-node-resolve.", + "dev": true, + "dependencies": { + "@types/resolve": "0.0.8", + "builtin-modules": "^3.1.0", + "is-module": "^1.0.0", + "resolve": "^1.11.1", + "rollup-pluginutils": "^2.8.1" + }, + "peerDependencies": { + "rollup": ">=1.11.0" + } + }, + "node_modules/rollup-plugin-replace": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz", + "integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==", + "deprecated": "This module has moved and is now available at @rollup/plugin-replace. Please update your dependencies. This version is no longer maintained.", + "dev": true, + "dependencies": { + "magic-string": "^0.25.2", + "rollup-pluginutils": "^2.6.0" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + } + }, + "dependencies": { + "@types/estree": { + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz", + "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==", + "dev": true + }, + "@types/node": { + "version": "15.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz", + "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==", + "dev": true + }, + "@types/resolve": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", + "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "builtin-modules": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", + "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", + "dev": true + }, + "estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + }, + "fast-json-patch": { + "version": "3.0.0-1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.0.0-1.tgz", + "integrity": "sha512-6pdFb07cknxvPzCeLsFHStEy+MysPJPgZQ9LbQ/2O67unQF93SNqfdSqnPPl71YMHX+AD8gbl7iuoGFzHEdDuw==" + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "htm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.0.4.tgz", + "integrity": "sha512-VRdvxX3tmrXuT/Ovt59NMp/ORMFi4bceFMDjos1PV4E0mV+5votuID8R60egR9A4U8nLt238R/snlJGz3UYiTQ==" + }, + "idom-client-react": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.8.2.tgz", + "integrity": "sha512-pK4FjyfVIaOVA/R0sj6Ulvpo3FATFU11TbnoqgzGbXjjY7kYPuR3x2pa/M6MY/Ot9yUb2Nsz9Gr1Vj8QPJ6GwA==", + "requires": { + "fast-json-patch": "^3.0.0-1", + "htm": "^3.0.3" + } + }, + "is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "requires": { + "@types/estree": "*" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "peer": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "prettier": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz", + "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==", + "dev": true + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "peer": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + } + }, + "react-dom": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "peer": true + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "rollup": { + "version": "2.51.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.51.2.tgz", + "integrity": "sha512-ReV2eGEadA7hmXSzjxdDKs10neqH2QURf2RxJ6ayAlq93ugy6qIvXMmbc5cWMGCDh1h5T4thuWO1e2VNbMq8FA==", + "dev": true, + "requires": { + "fsevents": "~2.3.1" + } + }, + "rollup-plugin-commonjs": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz", + "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1", + "is-reference": "^1.1.2", + "magic-string": "^0.25.2", + "resolve": "^1.11.0", + "rollup-pluginutils": "^2.8.1" + } + }, + "rollup-plugin-node-resolve": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz", + "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==", + "dev": true, + "requires": { + "@types/resolve": "0.0.8", + "builtin-modules": "^3.1.0", + "is-module": "^1.0.0", + "resolve": "^1.11.1", + "rollup-pluginutils": "^2.8.1" + } + }, + "rollup-plugin-replace": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz", + "integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==", + "dev": true, + "requires": { + "magic-string": "^0.25.2", + "rollup-pluginutils": "^2.6.0" + } + }, + "rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1" + } + }, + "scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + } + } +} diff --git a/tests/js/package.json b/tests/js/package.json new file mode 100644 index 00000000..3844cf32 --- /dev/null +++ b/tests/js/package.json @@ -0,0 +1,23 @@ +{ + "name": "tests", + "version": "1.0.0", + "description": "test app for idom_django websocket server", + "main": "src/index.js", + "files": [ + "src/**/*.js" + ], + "scripts": { + "build": "rollup --config", + "format": "prettier --ignore-path .gitignore --write ." + }, + "dependencies": { + "idom-client-react": "^0.8.2" + }, + "devDependencies": { + "prettier": "^2.2.1", + "rollup": "^2.35.1", + "rollup-plugin-commonjs": "^10.1.0", + "rollup-plugin-node-resolve": "^5.2.0", + "rollup-plugin-replace": "^2.2.0" + } +} diff --git a/tests/js/rollup.config.js b/tests/js/rollup.config.js new file mode 100644 index 00000000..ad597a61 --- /dev/null +++ b/tests/js/rollup.config.js @@ -0,0 +1,33 @@ +import resolve from "rollup-plugin-node-resolve"; +import commonjs from "rollup-plugin-commonjs"; +import replace from "rollup-plugin-replace"; + +const { PRODUCTION } = process.env; + +export default { + input: "src/index.js", + output: { + file: "../test_app/static/build.js", + format: "esm", + }, + plugins: [ + resolve(), + commonjs(), + replace({ + "process.env.NODE_ENV": JSON.stringify( + PRODUCTION ? "production" : "development" + ), + }), + ], + onwarn: function (warning) { + // Skip certain warnings + + // should intercept ... but doesn't in some rollup versions + if (warning.code === "THIS_IS_UNDEFINED") { + return; + } + + // console.warn everything else + console.warn(warning.message); + }, +}; diff --git a/tests/js/src/index.js b/tests/js/src/index.js new file mode 100644 index 00000000..613515dc --- /dev/null +++ b/tests/js/src/index.js @@ -0,0 +1,13 @@ +import { mountLayoutWithWebSocket } from "idom-client-react"; + +// Set up a websocket at the base endpoint +let LOCATION = window.location; +let WS_PROTOCOL = ""; +if (LOCATION.protocol == "https:") { + WS_PROTOCOL = "wss://"; +} else { + WS_PROTOCOL = "ws://"; +} +let WS_ENDPOINT_URL = WS_PROTOCOL + LOCATION.host; + +mountLayoutWithWebSocket(document.getElementById("mount"), WS_ENDPOINT_URL); diff --git a/tests/manage.py b/tests/manage.py new file mode 100644 index 00000000..234cb618 --- /dev/null +++ b/tests/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/tests/test_app/__init__.py b/tests/test_app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_app/asgi.py b/tests/test_app/asgi.py new file mode 100644 index 00000000..113b147f --- /dev/null +++ b/tests/test_app/asgi.py @@ -0,0 +1,35 @@ +""" +ASGI config for test_app project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +""" + +import os + +from django.conf.urls import url +from django.core.asgi import get_asgi_application + +from django_idom import IdomAsyncWebSocketConsumer # noqa: E402 + +from .views import Root + + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings") + +# Fetch ASGI application before importing dependencies that require ORM models. +http_asgi_app = get_asgi_application() + +from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402 + + +application = ProtocolTypeRouter( + { + "http": http_asgi_app, + "websocket": URLRouter( + [url("", IdomAsyncWebSocketConsumer.as_asgi(component=Root))] + ), + } +) diff --git a/tests/test_app/management/__init__.py b/tests/test_app/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_app/management/commands/__init__.py b/tests/test_app/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_app/management/commands/build_js.py b/tests/test_app/management/commands/build_js.py new file mode 100644 index 00000000..61af5ed9 --- /dev/null +++ b/tests/test_app/management/commands/build_js.py @@ -0,0 +1,16 @@ +import subprocess +from pathlib import Path + +from django.core.management.base import BaseCommand + + +HERE = Path(__file__).parent +JS_DIR = HERE.parent.parent.parent / "js" + + +class Command(BaseCommand): + help = "Build javascript source for test app" + + def handle(self, *args, **options): + subprocess.run(["npm", "install"], cwd=JS_DIR, check=True) + subprocess.run(["npm", "run", "build"], cwd=JS_DIR, check=True) diff --git a/tests/test_app/migrations/__init__.py b/tests/test_app/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py new file mode 100644 index 00000000..73eda0fa --- /dev/null +++ b/tests/test_app/settings.py @@ -0,0 +1,122 @@ +""" +Django settings for test_app project. + +Generated by 'django-admin startproject' using Django 3.2.3. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" +import os +import sys +from pathlib import Path + + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent +SRC_DIR = BASE_DIR.parent / "src" + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-n!bd1#+7ufw5#9ipayu9k(lyu@za$c2ajbro7es(v8_7w1$=&c" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True +ALLOWED_HOSTS = [] + +# Application definition +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "channels", # Websocket library + "test_app", # This test application +] +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] +ROOT_URLCONF = "test_app.urls" +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(BASE_DIR, "test_app", "templates")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] +ASGI_APPLICATION = "test_app.asgi.application" +sys.path.append(str(SRC_DIR)) + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), + "TEST": { + "NAME": os.path.join(BASE_DIR, "db_test.sqlite3"), + }, + }, +} + +# Password validation +# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" +USE_I18N = True +USE_L10N = True +USE_TZ = True + + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +STATIC_ROOT = os.path.join(BASE_DIR, "static-deploy") + +# Static Files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ +STATIC_URL = "/static/" +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, "test_app", "static"), +] +STATICFILES_FINDERS = [ + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", +] diff --git a/tests/test_app/static/favicon.ico b/tests/test_app/static/favicon.ico new file mode 100644 index 00000000..7005d293 Binary files /dev/null and b/tests/test_app/static/favicon.ico differ diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html new file mode 100644 index 00000000..d2a88607 --- /dev/null +++ b/tests/test_app/templates/base.html @@ -0,0 +1,21 @@ +{% load static %} + + + + + + + + IDOM + + + +

IDOM Test Page

+
+ + + diff --git a/tests/test_app/tests.py b/tests/test_app/tests.py new file mode 100644 index 00000000..1a80ee5a --- /dev/null +++ b/tests/test_app/tests.py @@ -0,0 +1,37 @@ +import os + +from channels.testing import ChannelsLiveServerTestCase +from selenium import webdriver +from selenium.webdriver.support.ui import WebDriverWait + + +class TestIdomCapabilities(ChannelsLiveServerTestCase): + def setUp(self): + self.driver = make_driver(5, 5) + self.driver.get(self.live_server_url) + + def tearDown(self) -> None: + self.driver.quit() + + def wait_until(self, condition, timeout=5): + WebDriverWait(self.driver, timeout).until(lambda driver: condition()) + + def test_hello_world(self): + self.driver.find_element_by_id("hello-world") + + def test_counter(self): + button = self.driver.find_element_by_id("counter-inc") + count = self.driver.find_element_by_id("counter-num") + + for i in range(5): + self.wait_until(lambda: count.get_attribute("data-count") == str(i)) + button.click() + + +def make_driver(page_load_timeout, implicit_wait_timeout): + options = webdriver.ChromeOptions() + options.headless = bool(int(os.environ.get("SELENIUM_HEADLESS", 0))) + driver = webdriver.Chrome(options=options) + driver.set_page_load_timeout(page_load_timeout) + driver.implicitly_wait(implicit_wait_timeout) + return driver diff --git a/tests/test_app/urls.py b/tests/test_app/urls.py new file mode 100644 index 00000000..4449e447 --- /dev/null +++ b/tests/test_app/urls.py @@ -0,0 +1,25 @@ +"""test_app URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.2/topics/http/urls/ + +Examples: + +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') + +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') + +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.urls import path + +from .views import base_template + + +urlpatterns = [path("", base_template)] diff --git a/tests/test_app/views.py b/tests/test_app/views.py new file mode 100644 index 00000000..a996eb1e --- /dev/null +++ b/tests/test_app/views.py @@ -0,0 +1,34 @@ +import idom +from django.http import HttpResponse +from django.template import loader + + +def base_template(request): + template = loader.get_template("base.html") + context = {} + return HttpResponse(template.render(context, request)) + + +@idom.component +def Root(): + return idom.html.div(HelloWorld(), Counter()) + + +@idom.component +def HelloWorld(): + return idom.html.h1({"id": "hello-world"}, "Hello World!") + + +@idom.component +def Counter(): + count, set_count = idom.hooks.use_state(0) + return idom.html.div( + idom.html.button( + {"id": "counter-inc", "onClick": lambda event: set_count(count + 1)}, + "Click me!", + ), + idom.html.p( + {"id": "counter-num", "data-count": count}, + f"Current count is: {count}", + ), + )