mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
Update test suite (#2181)
* pyscript.web tests pass with upytest. * Refactor of old integration tests to new Python tests. * Added comprehensive test suite for Python based `pyscript` module. * Add integration tests to Makefile (and CI) * Remove un-needed upload action. * Ensure fails are properly logged as an array. Remove the explicit test step, since this is already built into the build step. * Bump polyscript. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
This commit is contained in:
committed by
GitHub
parent
ae66d13d57
commit
06138bbb48
14
.github/workflows/test.yml
vendored
14
.github/workflows/test.yml
vendored
@@ -69,12 +69,7 @@ jobs:
|
||||
make setup
|
||||
|
||||
- name: Build
|
||||
run: make build
|
||||
|
||||
- name: Integration Tests
|
||||
#run: make test-integration-parallel
|
||||
run: |
|
||||
make test-integration
|
||||
run: make build # Integration tests run in the build step.
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -83,10 +78,3 @@ jobs:
|
||||
pyscript.core/dist/
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: success() || failure()
|
||||
with:
|
||||
name: test_results
|
||||
path: test_results/
|
||||
if-no-files-found: error
|
||||
|
||||
14
Makefile
14
Makefile
@@ -12,7 +12,7 @@ all:
|
||||
@echo "make clean - clean up auto-generated assets."
|
||||
@echo "make build - build PyScript."
|
||||
@echo "make precommit-check - run the precommit checks (run eslint)."
|
||||
@echo "make test-integration - run all integration tests sequentially."
|
||||
@echo "make test - run all automated tests in playwright."
|
||||
@echo "make fmt - format the code."
|
||||
@echo "make fmt-check - check the code formatting.\n"
|
||||
|
||||
@@ -62,15 +62,9 @@ build:
|
||||
precommit-check:
|
||||
pre-commit run --all-files
|
||||
|
||||
# Run all integration tests sequentially.
|
||||
test-integration:
|
||||
mkdir -p test_results
|
||||
pytest -vv $(ARGS) pyscript.core/tests/integration/ --log-cli-level=warning --junitxml=test_results/integration.xml
|
||||
|
||||
# Run all integration tests in parallel.
|
||||
test-integration-parallel:
|
||||
mkdir -p test_results
|
||||
pytest --numprocesses auto -vv $(ARGS) pyscript.core/tests/integration/ --log-cli-level=warning --junitxml=test_results/integration.xml
|
||||
# Run all automated tests in playwright.
|
||||
test:
|
||||
cd pyscript.core && npm run test:integration
|
||||
|
||||
# Format the code.
|
||||
fmt: fmt-py
|
||||
|
||||
@@ -71,6 +71,8 @@ Read the [contributing guide](https://docs.pyscript.net/latest/contributing/) to
|
||||
|
||||
Check out the [developing process](https://docs.pyscript.net/latest/developers/) documentation for more information on how to setup your development environment.
|
||||
|
||||
For technical details of the code, please see the [README](pyscript.core/README) in `pyscript.core`.
|
||||
|
||||
## Governance
|
||||
|
||||
The [PyScript organization governance](https://github.com/pyscript/governance) is documented in a separate repository.
|
||||
|
||||
@@ -37,13 +37,25 @@ make setup
|
||||
|
||||
This will create a tests environment [in the root of the project, named `./env`]and install all the dependencies needed to run the tests.
|
||||
|
||||
After the command has completed and the tests environment has been created, you can run the **integration tests** with
|
||||
After the command has completed and the tests environment has been created, you can run the **automated tests** with
|
||||
the following command:
|
||||
|
||||
```
|
||||
make test-integration
|
||||
make test
|
||||
```
|
||||
|
||||
(This essentially runs the `npm run test:integration` command in the right place. This is defined in PyScript's `package.json` file.)
|
||||
|
||||
Tests are found in the `tests` directory. These are organised into three locations:
|
||||
|
||||
1. `python` - the Python based test suite to exercise Python code **within** PyScript.
|
||||
2. `javascript` - JavaScript tests to exercise PyScript itself, in the browser.
|
||||
3. `manual` - containing tests to run manually in a browser, due to the complex nature of the tests.
|
||||
|
||||
We use [Playwright](https://playwright.dev/) to automate the running of the Python and JavaScript test suites. We use [uPyTest](https://github.com/ntoll/upytest) as a test framework for the Python test suite. uPyTest is a "PyTest inspired" framework for running tests in the browser on both MicroPython and Pyodide.
|
||||
|
||||
The automated (Playwright) tests are specified in the `tests/integration.spec.js` file.
|
||||
|
||||
## `pyscript` python package
|
||||
|
||||
The `pyscript` package available in _Python_ lives in the folder `src/stdlib/pyscript/`.
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"@ungap/with-resolvers": "^0.1.0",
|
||||
"@webreflection/idb-map": "^0.3.1",
|
||||
"basic-devtools": "^0.1.6",
|
||||
"polyscript": "^0.15.7",
|
||||
"polyscript": "^0.15.8",
|
||||
"sabayon": "^0.5.2",
|
||||
"sticky-module": "^0.1.1",
|
||||
"to-json-callback": "^0.1.1",
|
||||
|
||||
49
pyscript.core/tests/README.md
Normal file
49
pyscript.core/tests/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# PyScript Test Suite
|
||||
|
||||
There are three aspects to our test suite. These are reflected in the layout of
|
||||
the test directory:
|
||||
|
||||
1. `python` - contains the Python based test suite to exercise Python code
|
||||
**within** PyScript. These tests are run four differeng ways to ensure all
|
||||
combination of MicroPython/Pyodide and main thread/worker contexts are
|
||||
checked.
|
||||
2. `javascript` - contains JavaScript tests to exercise PyScript _itself_, in
|
||||
the browser.
|
||||
3. `manual` - contains tests to run manually in a browser, due to the complex
|
||||
nature of the tests.
|
||||
|
||||
We use [Playwright](https://playwright.dev/) to automate the running of the
|
||||
Python and JavaScript test suites. We use
|
||||
[uPyTest](https://github.com/ntoll/upytest) as a test framework for the Python
|
||||
test suite. uPyTest is a "PyTest inspired" framework for running tests in the
|
||||
browser on both MicroPython and Pyodide.
|
||||
|
||||
The automated (Playwright) tests are specified in the `integration.spec.js`
|
||||
file in this directory.
|
||||
|
||||
All automatic tests live in either the `python` or `javascript` folders. All
|
||||
the tests in these folder are run by CI or locally run by `make test` in the
|
||||
root of this project. Alternatively, run `npm run test:integration` in the
|
||||
PyScript source directory.
|
||||
|
||||
Similarly, some tests can only be run manually (due to their nature or
|
||||
underlying complexity). These are in the `manual` directory and are in the form
|
||||
of separate directories (each containing an `index.html`) or individual `*.html`
|
||||
files to which you point your browser. Each separate test may exercise
|
||||
JavaScript or Python code (or both), and the context for each separate test is
|
||||
kept carefully isolated.
|
||||
|
||||
Some rules of thumb:
|
||||
|
||||
* We don't test upstream projects: we assume they have their own test suites,
|
||||
and if we find bugs, we file an issue upstream with an example of how to
|
||||
recreate the problem.
|
||||
* We don't test browser functionality, we just have to trust that browsers work
|
||||
as advertised. Once again, if we find an issue, we report upstream.
|
||||
* All test cases should include commentary describing the **intent** and
|
||||
context of the test.
|
||||
* Tests in Python use [uPyTest](https://github.com/ntoll/upytest) (see the
|
||||
README for documentation), an "inspired by PyTest" test framework that works
|
||||
with both MicroPython and Pyodide in the browser. This means that all
|
||||
Python tests should work with both interpreters.
|
||||
* Tests in JavaScript... (Andrea to explain). ;-)
|
||||
File diff suppressed because one or more lines are too long
@@ -1,7 +1,43 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('Python unit tests - MicroPython on MAIN thread', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/python/index.html');
|
||||
test.setTimeout(120*1000); // Increase timeout for this test.
|
||||
const result = page.locator("#result"); // Payload for results will be here.
|
||||
await result.waitFor(); // wait for the result.
|
||||
const data = JSON.parse(await result.textContent()); // get the result data.
|
||||
await expect(data.fails).toMatchObject([]); // ensure no test failed.
|
||||
});
|
||||
|
||||
test('Python unit tests - Pyodide on MAIN thread', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/python/index.html?type=py');
|
||||
test.setTimeout(120*1000); // Increase timeout for this test.
|
||||
const result = page.locator("#result"); // Payload for results will be here.
|
||||
await result.waitFor(); // wait for the result.
|
||||
const data = JSON.parse(await result.textContent()); // get the result data.
|
||||
await expect(data.fails).toMatchObject([]); // ensure no test failed.
|
||||
});
|
||||
|
||||
test('Python unit tests - MicroPython on WORKER', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/python/index.html?worker');
|
||||
test.setTimeout(120*1000); // Increase timeout for this test.
|
||||
const result = page.locator("#result"); // Payload for results will be here.
|
||||
await result.waitFor(); // wait for the result.
|
||||
const data = JSON.parse(await result.textContent()); // get the result data.
|
||||
await expect(data.fails).toMatchObject([]); // ensure no test failed.
|
||||
});
|
||||
|
||||
test('Python unit tests - Pyodide on WORKER', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/python/index.html?type=py&worker');
|
||||
test.setTimeout(120*1000); // Increase timeout for this test.
|
||||
const result = page.locator("#result"); // Payload for results will be here.
|
||||
await result.waitFor(); // wait for the result.
|
||||
const data = JSON.parse(await result.textContent()); // get the result data.
|
||||
await expect(data.fails).toMatchObject([]); // ensure no test failed.
|
||||
});
|
||||
|
||||
test('MicroPython display', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/mpy.html');
|
||||
await page.goto('http://localhost:8080/tests/javascript/mpy.html');
|
||||
await page.waitForSelector('html.done.worker');
|
||||
const body = await page.evaluate(() => document.body.innerText);
|
||||
await expect(body.trim()).toBe([
|
||||
@@ -18,7 +54,7 @@ test('MicroPython hooks', async ({ page }) => {
|
||||
if (!text.startsWith('['))
|
||||
logs.push(text);
|
||||
});
|
||||
await page.goto('http://localhost:8080/tests/js-integration/hooks.html');
|
||||
await page.goto('http://localhost:8080/tests/javascript/hooks.html');
|
||||
await page.waitForSelector('html.done.worker');
|
||||
await expect(logs.join('\n')).toBe([
|
||||
'main onReady',
|
||||
@@ -43,7 +79,7 @@ test('MicroPython + Pyodide js_modules', async ({ page }) => {
|
||||
if (!text.startsWith('['))
|
||||
logs.push(text);
|
||||
});
|
||||
await page.goto('http://localhost:8080/tests/js-integration/js_modules.html');
|
||||
await page.goto('http://localhost:8080/tests/javascript/js_modules.html');
|
||||
await page.waitForSelector('html.done');
|
||||
await expect(logs.length).toBe(6);
|
||||
await expect(logs[0]).toBe(logs[1]);
|
||||
@@ -53,69 +89,70 @@ test('MicroPython + Pyodide js_modules', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('MicroPython + configURL', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/config-url.html');
|
||||
await page.goto('http://localhost:8080/tests/javascript/config-url.html');
|
||||
await page.waitForSelector('html.main.worker');
|
||||
});
|
||||
|
||||
test('Pyodide + terminal on Main', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/py-terminal-main.html');
|
||||
await page.goto('http://localhost:8080/tests/javascript/py-terminal-main.html');
|
||||
await page.waitForSelector('html.ok');
|
||||
});
|
||||
|
||||
|
||||
test('Pyodide + terminal on Worker', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/py-terminal-worker.html');
|
||||
await page.goto('http://localhost:8080/tests/javascript/py-terminal-worker.html');
|
||||
await page.waitForSelector('html.ok');
|
||||
});
|
||||
|
||||
test('Pyodide + multiple terminals via Worker', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/py-terminals.html');
|
||||
await page.goto('http://localhost:8080/tests/javascript/py-terminals.html');
|
||||
await page.waitForSelector('html.first.second');
|
||||
});
|
||||
|
||||
test('MicroPython + Pyodide fetch', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/fetch/index.html');
|
||||
await page.goto('http://localhost:8080/tests/javascript/fetch/index.html');
|
||||
await page.waitForSelector('html.mpy.py');
|
||||
});
|
||||
|
||||
test('MicroPython + Pyodide ffi', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/ffi.html');
|
||||
await page.goto('http://localhost:8080/tests/javascript/ffi.html');
|
||||
await page.waitForSelector('html.mpy.py');
|
||||
});
|
||||
|
||||
test('MicroPython + Storage', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/storage.html');
|
||||
await page.goto('http://localhost:8080/tests/javascript/storage.html');
|
||||
await page.waitForSelector('html.ok');
|
||||
});
|
||||
|
||||
test('MicroPython + JS Storage', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/js-storage.html');
|
||||
await page.goto('http://localhost:8080/tests/javascript/js-storage.html');
|
||||
await page.waitForSelector('html.ok');
|
||||
});
|
||||
|
||||
test('MicroPython + workers', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/workers/index.html');
|
||||
test.setTimeout(120*1000); // Increase timeout for this test.
|
||||
await page.goto('http://localhost:8080/tests/javascript/workers/index.html');
|
||||
await page.waitForSelector('html.mpy.py');
|
||||
});
|
||||
|
||||
test('MicroPython Editor setup error', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/issue-2093/index.html');
|
||||
await page.goto('http://localhost:8080/tests/javascript/issue-2093/index.html');
|
||||
await page.waitForSelector('html.errored');
|
||||
});
|
||||
|
||||
test('MicroPython async @when listener', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/async-listener.html');
|
||||
await page.goto('http://localhost:8080/tests/javascript/async-listener.html');
|
||||
await page.waitForSelector('html.ok');
|
||||
});
|
||||
|
||||
test('Pyodide loader', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/loader/index.html');
|
||||
await page.goto('http://localhost:8080/tests/javascript/loader/index.html');
|
||||
await page.waitForSelector('html.ok');
|
||||
const body = await page.evaluate(() => document.body.textContent);
|
||||
await expect(body.includes('Loaded Pyodide')).toBe(true);
|
||||
});
|
||||
|
||||
test('Py and Mpy config["type"]', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/config_type.html');
|
||||
await page.goto('http://localhost:8080/tests/javascript/config_type.html');
|
||||
await page.waitForSelector('html.mpy.py');
|
||||
});
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
import shutil
|
||||
import threading
|
||||
from http.server import HTTPServer as SuperHTTPServer
|
||||
from http.server import SimpleHTTPRequestHandler
|
||||
|
||||
import pytest
|
||||
|
||||
from .support import Logger
|
||||
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
"""
|
||||
If we pass --clear-http-cache, we don't enter the main pytest logic, but
|
||||
use our custom main instead
|
||||
"""
|
||||
|
||||
def mymain(config, session):
|
||||
print()
|
||||
print("-" * 20, "SmartRouter HTTP cache", "-" * 20)
|
||||
# unfortunately pytest-cache doesn't offer a public API to selectively
|
||||
# clear the cache, so we need to peek its internal. The good news is
|
||||
# that pytest-cache is very old, stable and robust, so it's likely
|
||||
# that this won't break anytime soon.
|
||||
cache = config.cache
|
||||
base = cache._cachedir.joinpath(cache._CACHE_PREFIX_VALUES, "pyscript")
|
||||
if not base.exists():
|
||||
print("No cache found, nothing to do")
|
||||
return 0
|
||||
#
|
||||
print("Requests found in the cache:")
|
||||
for f in base.rglob("*"):
|
||||
if f.is_file():
|
||||
# requests are saved in dirs named pyscript/http:/foo/bar, let's turn
|
||||
# them into a proper url
|
||||
url = str(f.relative_to(base))
|
||||
url = url.replace(":/", "://")
|
||||
print(" ", url)
|
||||
shutil.rmtree(base)
|
||||
print("Cache cleared")
|
||||
return 0
|
||||
|
||||
if config.option.clear_http_cache:
|
||||
from _pytest.main import wrap_session
|
||||
|
||||
return wrap_session(config, mymain)
|
||||
return None
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""
|
||||
THIS IS A WORKAROUND FOR A pytest QUIRK!
|
||||
|
||||
At the moment of writing this conftest defines two new options, --dev and
|
||||
--no-fake-server, but because of how pytest works, they are available only
|
||||
if this is the "root conftest" for the test session.
|
||||
|
||||
This means that if you are in the pyscript.core directory:
|
||||
|
||||
$ py.test # does NOT work
|
||||
$ py.test tests/integration/ # works
|
||||
|
||||
This happens because there is also test py-unit directory, so in the first
|
||||
case the "root conftest" would be tests/conftest.py (which doesn't exist)
|
||||
instead of this.
|
||||
|
||||
There are various workarounds, but for now we can just detect it and
|
||||
inform the user.
|
||||
|
||||
Related StackOverflow answer: https://stackoverflow.com/a/51733980
|
||||
"""
|
||||
if not hasattr(config.option, "dev"):
|
||||
msg = """
|
||||
Running a bare "pytest" command from the pyscript.core directory
|
||||
is not supported. Please use one of the following commands:
|
||||
- pytest tests/integration
|
||||
- pytest tests/*
|
||||
- cd tests/integration; pytest
|
||||
"""
|
||||
pytest.fail(msg)
|
||||
else:
|
||||
if config.option.dev:
|
||||
config.option.headed = True
|
||||
config.option.no_fake_server = True
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def logger():
|
||||
return Logger()
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--no-fake-server",
|
||||
action="store_true",
|
||||
help="Use a real HTTP server instead of http://fakeserver",
|
||||
)
|
||||
parser.addoption(
|
||||
"--dev",
|
||||
action="store_true",
|
||||
help="Automatically open a devtools panel. Implies --headed and --no-fake-server",
|
||||
)
|
||||
parser.addoption(
|
||||
"--clear-http-cache",
|
||||
action="store_true",
|
||||
help="Clear the cache of HTTP requests for SmartRouter",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def browser_type_launch_args(request):
|
||||
"""
|
||||
Override the browser_type_launch_args defined by pytest-playwright to
|
||||
support --devtools.
|
||||
|
||||
NOTE: this has been tested with pytest-playwright==0.3.0. It might break
|
||||
with newer versions of it.
|
||||
"""
|
||||
# this calls the "original" fixture defined by pytest_playwright.py
|
||||
launch_options = request.getfixturevalue("browser_type_launch_args")
|
||||
if request.config.option.dev:
|
||||
launch_options["devtools"] = True
|
||||
return launch_options
|
||||
|
||||
|
||||
class DevServer(SuperHTTPServer):
|
||||
"""
|
||||
Class for wrapper to run SimpleHTTPServer on Thread.
|
||||
Ctrl +Only Thread remains dead when terminated with C.
|
||||
Keyboard Interrupt passes.
|
||||
"""
|
||||
|
||||
def __init__(self, base_url, *args, **kwargs):
|
||||
self.base_url = base_url
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
self.server_close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def dev_server(logger):
|
||||
class MyHTTPRequestHandler(SimpleHTTPRequestHandler):
|
||||
enable_cors_headers = True
|
||||
|
||||
@classmethod
|
||||
def my_headers(cls):
|
||||
if cls.enable_cors_headers:
|
||||
return {
|
||||
"Cross-Origin-Embedder-Policy": "require-corp",
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
}
|
||||
return {}
|
||||
|
||||
def end_headers(self):
|
||||
self.send_my_headers()
|
||||
SimpleHTTPRequestHandler.end_headers(self)
|
||||
|
||||
def send_my_headers(self):
|
||||
for k, v in self.my_headers().items():
|
||||
self.send_header(k, v)
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
logger.log("http_server", fmt % args, color="blue")
|
||||
|
||||
host, port = "localhost", 8080
|
||||
base_url = f"http://{host}:{port}"
|
||||
|
||||
# serve_Run forever under thread
|
||||
server = DevServer(base_url, (host, port), MyHTTPRequestHandler)
|
||||
|
||||
thread = threading.Thread(None, server.run)
|
||||
thread.start()
|
||||
|
||||
yield server # Transition to test here
|
||||
|
||||
# End thread
|
||||
server.shutdown()
|
||||
thread.join()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,478 +0,0 @@
|
||||
import re
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
|
||||
from .support import (
|
||||
PageErrors,
|
||||
PageErrorsDidNotRaise,
|
||||
PyScriptTest,
|
||||
with_execution_thread,
|
||||
)
|
||||
|
||||
|
||||
@with_execution_thread(None)
|
||||
class TestSupport(PyScriptTest):
|
||||
"""
|
||||
These are NOT tests about PyScript.
|
||||
|
||||
They test the PyScriptTest class, i.e. we want to ensure that all the
|
||||
testing machinery that we have works correctly.
|
||||
"""
|
||||
|
||||
def test_basic(self):
|
||||
"""
|
||||
Very basic test, just to check that we can write, serve and read a simple
|
||||
HTML (no pyscript yet)
|
||||
"""
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<h1>Hello world</h1>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
content = self.page.content()
|
||||
assert "<h1>Hello world</h1>" in content
|
||||
|
||||
def test_await_with_run_js(self):
|
||||
self.run_js(
|
||||
"""
|
||||
function resolveAfter200MilliSeconds(x) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(x);
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
const x = await resolveAfter200MilliSeconds(10);
|
||||
console.log(x);
|
||||
"""
|
||||
)
|
||||
|
||||
assert self.console.log.lines[-1] == "10"
|
||||
|
||||
def test_console(self):
|
||||
"""
|
||||
Test that we capture console.log messages correctly.
|
||||
"""
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
console.log("my log 1");
|
||||
console.debug("my debug");
|
||||
console.info("my info");
|
||||
console.error("my error");
|
||||
console.warn("my warning");
|
||||
console.log("my log 2");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
assert len(self.console.all.messages) == 6
|
||||
assert self.console.all.lines == [
|
||||
"my log 1",
|
||||
"my debug",
|
||||
"my info",
|
||||
"my error",
|
||||
"my warning",
|
||||
"my log 2",
|
||||
]
|
||||
|
||||
# fmt: off
|
||||
assert self.console.all.text == textwrap.dedent("""
|
||||
my log 1
|
||||
my debug
|
||||
my info
|
||||
my error
|
||||
my warning
|
||||
my log 2
|
||||
""").strip()
|
||||
# fmt: on
|
||||
|
||||
assert self.console.log.lines == ["my log 1", "my log 2"]
|
||||
assert self.console.debug.lines == ["my debug"]
|
||||
|
||||
def test_check_js_errors_simple(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>throw new Error('this is an error');</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(PageErrors) as exc:
|
||||
self.check_js_errors()
|
||||
# check that the exception message contains the error message and the
|
||||
# stack trace
|
||||
msg = str(exc.value)
|
||||
expected = textwrap.dedent(
|
||||
f"""
|
||||
JS errors found: 1
|
||||
Error: this is an error
|
||||
at {self.http_server_addr}/mytest.html:.*
|
||||
"""
|
||||
).strip()
|
||||
assert re.search(expected, msg)
|
||||
#
|
||||
# after a call to check_js_errors, the errors are cleared
|
||||
self.check_js_errors()
|
||||
#
|
||||
# JS exceptions are also available in self.console.js_error
|
||||
assert self.console.js_error.lines[0].startswith("Error: this is an error")
|
||||
|
||||
def test_check_js_errors_expected(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>throw new Error('this is an error');</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
self.check_js_errors("this is an error")
|
||||
|
||||
def test_check_js_errors_expected_but_didnt_raise(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>throw new Error('this is an error 2');</script>
|
||||
<script>throw new Error('this is an error 4');</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(PageErrorsDidNotRaise) as exc:
|
||||
self.check_js_errors(
|
||||
"this is an error 1",
|
||||
"this is an error 2",
|
||||
"this is an error 3",
|
||||
"this is an error 4",
|
||||
)
|
||||
#
|
||||
msg = str(exc.value)
|
||||
expected = textwrap.dedent(
|
||||
"""
|
||||
The following JS errors were expected but could not be found:
|
||||
- this is an error 1
|
||||
- this is an error 3
|
||||
"""
|
||||
).strip()
|
||||
assert re.search(expected, msg)
|
||||
|
||||
def test_check_js_errors_multiple(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>throw new Error('error 1');</script>
|
||||
<script>throw new Error('error 2');</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(PageErrors) as exc:
|
||||
self.check_js_errors()
|
||||
#
|
||||
msg = str(exc.value)
|
||||
expected = textwrap.dedent(
|
||||
f"""
|
||||
JS errors found: 2
|
||||
Error: error 1
|
||||
at {self.http_server_addr}/mytest.html:.*
|
||||
Error: error 2
|
||||
at {self.http_server_addr}/mytest.html:.*
|
||||
"""
|
||||
).strip()
|
||||
assert re.search(expected, msg)
|
||||
#
|
||||
# check that errors are cleared
|
||||
self.check_js_errors()
|
||||
|
||||
def test_check_js_errors_some_expected_but_others_not(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>throw new Error('expected 1');</script>
|
||||
<script>throw new Error('NOT expected 2');</script>
|
||||
<script>throw new Error('expected 3');</script>
|
||||
<script>throw new Error('NOT expected 4');</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(PageErrors) as exc:
|
||||
self.check_js_errors("expected 1", "expected 3")
|
||||
#
|
||||
msg = str(exc.value)
|
||||
expected = textwrap.dedent(
|
||||
f"""
|
||||
JS errors found: 2
|
||||
Error: NOT expected 2
|
||||
at {self.http_server_addr}/mytest.html:.*
|
||||
Error: NOT expected 4
|
||||
at {self.http_server_addr}/mytest.html:.*
|
||||
"""
|
||||
).strip()
|
||||
assert re.search(expected, msg)
|
||||
|
||||
def test_check_js_errors_expected_not_found_but_other_errors(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>throw new Error('error 1');</script>
|
||||
<script>throw new Error('error 2');</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(PageErrorsDidNotRaise) as exc:
|
||||
self.check_js_errors("this is not going to be found")
|
||||
#
|
||||
msg = str(exc.value)
|
||||
expected = textwrap.dedent(
|
||||
f"""
|
||||
The following JS errors were expected but could not be found:
|
||||
- this is not going to be found
|
||||
---
|
||||
The following JS errors were raised but not expected:
|
||||
Error: error 1
|
||||
at {self.http_server_addr}/mytest.html:.*
|
||||
Error: error 2
|
||||
at {self.http_server_addr}/mytest.html:.*
|
||||
"""
|
||||
).strip()
|
||||
assert re.search(expected, msg)
|
||||
|
||||
def test_clear_js_errors(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>throw new Error('this is an error');</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
self.clear_js_errors()
|
||||
# self.check_js_errors does not raise, because the errors have been
|
||||
# cleared
|
||||
self.check_js_errors()
|
||||
|
||||
def test_wait_for_console_simple(self):
|
||||
"""
|
||||
Test that self.wait_for_console actually waits.
|
||||
If it's buggy, the test will try to read self.console.log BEFORE the
|
||||
log has been written and it will fail.
|
||||
"""
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
console.log('Page loaded!');
|
||||
}, 100);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
# we use a timeout of 200ms to give plenty of time to the page to
|
||||
# actually run the setTimeout callback
|
||||
self.wait_for_console("Page loaded!", timeout=200)
|
||||
assert self.console.log.lines[-1] == "Page loaded!"
|
||||
|
||||
def test_wait_for_console_timeout(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(TimeoutError):
|
||||
self.wait_for_console("This text will never be printed", timeout=200)
|
||||
|
||||
def test_wait_for_console_dont_wait_if_already_emitted(self):
|
||||
"""
|
||||
If the text is already on the console, wait_for_console() should return
|
||||
immediately without waiting.
|
||||
"""
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
console.log('Hello world')
|
||||
console.log('Page loaded!');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
self.wait_for_console("Page loaded!", timeout=200)
|
||||
assert self.console.log.lines[-2] == "Hello world"
|
||||
assert self.console.log.lines[-1] == "Page loaded!"
|
||||
# the following call should return immediately without waiting
|
||||
self.wait_for_console("Hello world", timeout=1)
|
||||
|
||||
def test_wait_for_console_exception_1(self):
|
||||
"""
|
||||
Test that if a JS exception is raised while waiting for the console, we
|
||||
report the exception and not the timeout.
|
||||
|
||||
There are two main cases:
|
||||
1. there is an exception and the console message does not appear
|
||||
2. there is an exception but the console message appears anyway
|
||||
|
||||
This test checks for case 1. Case 2 is tested by
|
||||
test_wait_for_console_exception_2
|
||||
"""
|
||||
# case 1: there is an exception and the console message does not appear
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>throw new Error('this is an error');</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
# "Page loaded!" will never appear, of course.
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(PageErrors) as exc:
|
||||
self.wait_for_console("Page loaded!", timeout=200)
|
||||
assert "this is an error" in str(exc.value)
|
||||
assert isinstance(exc.value.__context__, TimeoutError)
|
||||
#
|
||||
# if we use check_js_errors=False, the error are ignored, but we get the
|
||||
# Timeout anyway
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(TimeoutError):
|
||||
self.wait_for_console("Page loaded!", timeout=200, check_js_errors=False)
|
||||
# we still got a PageErrors, so we need to manually clear it, else the
|
||||
# test fails at teardown
|
||||
self.clear_js_errors()
|
||||
|
||||
def test_wait_for_console_exception_2(self):
|
||||
"""
|
||||
See the description in test_wait_for_console_exception_1.
|
||||
"""
|
||||
# case 2: there is an exception, but the console message appears
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
console.log('Page loaded!');
|
||||
}, 100);
|
||||
throw new Error('this is an error');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(PageErrors) as exc:
|
||||
self.wait_for_console("Page loaded!", timeout=200)
|
||||
assert "this is an error" in str(exc.value)
|
||||
#
|
||||
# with check_js_errors=False, the Error is ignored and the
|
||||
# wait_for_console succeeds
|
||||
self.goto("mytest.html")
|
||||
self.wait_for_console("Page loaded!", timeout=200, check_js_errors=False)
|
||||
# clear the errors, else the test fails at teardown
|
||||
self.clear_js_errors()
|
||||
|
||||
def test_wait_for_console_match_substring(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
console.log('Foo Bar Baz');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(TimeoutError):
|
||||
self.wait_for_console("Bar", timeout=200)
|
||||
#
|
||||
self.wait_for_console("Bar", timeout=200, match_substring=True)
|
||||
assert self.console.log.lines[-1] == "Foo Bar Baz"
|
||||
|
||||
def test_iter_locator(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<div>foo</div>
|
||||
<div>bar</div>
|
||||
<div>baz</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
divs = self.page.locator("div")
|
||||
assert divs.count() == 3
|
||||
texts = [el.inner_text() for el in self.iter_locator(divs)]
|
||||
assert texts == ["foo", "bar", "baz"]
|
||||
|
||||
def test_smartrouter_cache(self):
|
||||
if self.router is None:
|
||||
pytest.skip("Cannot test SmartRouter with --dev")
|
||||
|
||||
# this is not an image but who cares, I just want the browser to make
|
||||
# an HTTP request
|
||||
URL = "https://raw.githubusercontent.com/pyscript/pyscript/main/README.md"
|
||||
doc = f"""
|
||||
<html>
|
||||
<body>
|
||||
<img src="{URL}">
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
#
|
||||
self.router.clear_cache(URL)
|
||||
self.goto("mytest.html")
|
||||
assert self.router.requests == [
|
||||
(200, "fake_server", "https://fake_server/mytest.html"),
|
||||
(200, "NETWORK", URL),
|
||||
]
|
||||
#
|
||||
# let's visit the page again, now it should be cached
|
||||
self.goto("mytest.html")
|
||||
assert self.router.requests == [
|
||||
# 1st visit
|
||||
(200, "fake_server", "https://fake_server/mytest.html"),
|
||||
(200, "NETWORK", URL),
|
||||
# 2nd visit
|
||||
(200, "fake_server", "https://fake_server/mytest.html"),
|
||||
(200, "CACHED", URL),
|
||||
]
|
||||
|
||||
def test_404(self):
|
||||
"""
|
||||
Test that we capture a 404 in loading a page that does not exist.
|
||||
"""
|
||||
self.goto("this_url_does_not_exist.html")
|
||||
if self.dev_server:
|
||||
error = "Failed to load resource: the server responded with a status of 404 (File not found)"
|
||||
else:
|
||||
error = "Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
assert [error] == self.console.all.lines
|
||||
@@ -1,404 +0,0 @@
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest, only_main, skip_worker
|
||||
|
||||
|
||||
class TestBasic(PyScriptTest):
|
||||
def test_pyscript_exports(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import RUNNING_IN_WORKER, PyWorker, window, document, sync, current_target
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
assert self.console.error.lines == []
|
||||
|
||||
def test_script_py_hello(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
import js
|
||||
js.console.log('hello from script py')
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines == ["hello from script py"]
|
||||
|
||||
def test_py_script_hello(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
import js
|
||||
js.console.log('hello from py-script')
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines == ["hello from py-script"]
|
||||
|
||||
def test_execution_thread(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
import pyscript
|
||||
import js
|
||||
js.console.log("worker?", pyscript.RUNNING_IN_WORKER)
|
||||
</script>
|
||||
""",
|
||||
)
|
||||
assert self.execution_thread in ("main", "worker")
|
||||
in_worker = self.execution_thread == "worker"
|
||||
in_worker = str(in_worker).lower()
|
||||
assert self.console.log.lines[-1] == f"worker? {in_worker}"
|
||||
|
||||
@skip_worker("NEXT: it should show a nice error on the page")
|
||||
def test_no_cors_headers(self):
|
||||
self.disable_cors_headers()
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
import js
|
||||
js.console.log("hello")
|
||||
</script>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
assert self.headers == {}
|
||||
if self.execution_thread == "main":
|
||||
self.wait_for_pyscript()
|
||||
assert self.console.log.lines == ["hello"]
|
||||
self.assert_no_banners()
|
||||
else:
|
||||
# XXX adapt and fix the test
|
||||
expected_alert_banner_msg = (
|
||||
'(PY1000): When execution_thread is "worker", the site must be cross origin '
|
||||
"isolated, but crossOriginIsolated is false. To be cross origin isolated, "
|
||||
"the server must use https and also serve with the following headers: "
|
||||
'{"Cross-Origin-Embedder-Policy":"require-corp",'
|
||||
'"Cross-Origin-Opener-Policy":"same-origin"}. '
|
||||
"The problem may be that one or both of these are missing."
|
||||
)
|
||||
alert_banner = self.page.wait_for_selector(".py-error")
|
||||
assert expected_alert_banner_msg in alert_banner.inner_text()
|
||||
|
||||
def test_print(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
print('hello pyscript')
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "hello pyscript"
|
||||
|
||||
@only_main
|
||||
def test_input_exception(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" async="false">
|
||||
input("what's your name?")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
self.check_py_errors(
|
||||
"Exception: input() doesn't work when PyScript runs in the main thread."
|
||||
)
|
||||
|
||||
@skip_worker("NEXT: exceptions should be displayed in the DOM")
|
||||
def test_python_exception(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
print('hello pyscript')
|
||||
raise Exception('this is an error')
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
assert "hello pyscript" in self.console.log.lines
|
||||
self.check_py_errors("Exception: this is an error")
|
||||
#
|
||||
# check that we show the traceback in the page. Note that here we
|
||||
# display the "raw" python traceback, without the "[pyexec] Python
|
||||
# exception:" line (which is useful in the console, but not for the
|
||||
# user)
|
||||
banner = self.page.locator(".py-error")
|
||||
tb_lines = banner.inner_text().splitlines()
|
||||
assert tb_lines[0] == "Traceback (most recent call last):"
|
||||
assert tb_lines[-1] == "Exception: this is an error"
|
||||
|
||||
@skip_worker("NEXT: py-click doesn't work inside workers")
|
||||
def test_python_exception_in_event_handler(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button py-click="onclick">Click me</button>
|
||||
<script type="py">
|
||||
def onclick(event):
|
||||
raise Exception("this is an error inside handler")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
self.page.locator("button").click()
|
||||
self.wait_for_console(
|
||||
"Exception: this is an error inside handler", match_substring=True
|
||||
)
|
||||
|
||||
self.check_py_errors("Exception: this is an error inside handler")
|
||||
|
||||
## error in DOM
|
||||
tb_lines = self.page.locator(".py-error").inner_text().splitlines()
|
||||
assert tb_lines[0] == "Traceback (most recent call last):"
|
||||
assert tb_lines[-1] == "Exception: this is an error inside handler"
|
||||
|
||||
@only_main
|
||||
def test_execution_in_order(self):
|
||||
"""
|
||||
Check that they script py tags are executed in the same order they are
|
||||
defined
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">import js; js.console.log('one')</script>
|
||||
<script type="py">js.console.log('two')</script>
|
||||
<script type="py">js.console.log('three')</script>
|
||||
<script type="py">js.console.log('four')</script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-4:] == [
|
||||
"one",
|
||||
"two",
|
||||
"three",
|
||||
"four",
|
||||
]
|
||||
|
||||
def test_escaping_of_angle_brackets(self):
|
||||
"""
|
||||
Check that script tags escape angle brackets
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
import js
|
||||
js.console.log("A", 1<2, 1>2)
|
||||
js.console.log("B <div></div>")
|
||||
</script>
|
||||
<py-script>
|
||||
import js
|
||||
js.console.log("C", 1<2, 1>2)
|
||||
js.console.log("D <div></div>")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
# in workers the order of execution is not guaranteed, better to play
|
||||
# safe
|
||||
lines = sorted(self.console.log.lines[-4:])
|
||||
assert lines == [
|
||||
"A true false",
|
||||
"B <div></div>",
|
||||
"C true false",
|
||||
"D <div></div>",
|
||||
]
|
||||
|
||||
def test_packages(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
packages = ["asciitree"]
|
||||
</py-config>
|
||||
<script type="py">
|
||||
import js
|
||||
import asciitree
|
||||
js.console.log('hello', asciitree.__name__)
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
assert self.console.log.lines[-3:] == [
|
||||
"Loading asciitree", # printed by pyodide
|
||||
"Loaded asciitree", # printed by pyodide
|
||||
"hello asciitree", # printed by us
|
||||
]
|
||||
|
||||
@pytest.mark.skip("NEXT: No banner")
|
||||
def test_non_existent_package(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
packages = ["i-dont-exist"]
|
||||
</py-config>
|
||||
<script type="py">
|
||||
print('hello')
|
||||
</script>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
|
||||
expected_alert_banner_msg = (
|
||||
"(PY1001): Unable to install package(s) 'i-dont-exist'. "
|
||||
"Unable to find package in PyPI. Please make sure you have "
|
||||
"entered a correct package name."
|
||||
)
|
||||
|
||||
alert_banner = self.page.wait_for_selector(".alert-banner")
|
||||
assert expected_alert_banner_msg in alert_banner.inner_text()
|
||||
self.check_py_errors("Can't fetch metadata for 'i-dont-exist'")
|
||||
|
||||
@pytest.mark.skip("NEXT: No banner")
|
||||
def test_no_python_wheel(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
packages = ["opsdroid"]
|
||||
</py-config>
|
||||
<script type="py">
|
||||
print('hello')
|
||||
</script>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
|
||||
expected_alert_banner_msg = (
|
||||
"(PY1001): Unable to install package(s) 'opsdroid'. "
|
||||
"Reason: Can't find a pure Python 3 Wheel for package(s) 'opsdroid'"
|
||||
)
|
||||
|
||||
alert_banner = self.page.wait_for_selector(".alert-banner")
|
||||
assert expected_alert_banner_msg in alert_banner.inner_text()
|
||||
self.check_py_errors("Can't find a pure Python 3 wheel for 'opsdroid'")
|
||||
|
||||
@only_main
|
||||
def test_dynamically_add_py_script_tag(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script>
|
||||
function addPyScriptTag(event) {
|
||||
let tag = document.createElement('py-script');
|
||||
tag.innerHTML = "print('hello world')";
|
||||
document.body.appendChild(tag);
|
||||
}
|
||||
addPyScriptTag()
|
||||
</script>
|
||||
""",
|
||||
timeout=20000,
|
||||
)
|
||||
self.page.locator("py-script")
|
||||
|
||||
assert self.console.log.lines[-1] == "hello world"
|
||||
|
||||
def test_py_script_src_attribute(self):
|
||||
self.writefile("foo.py", "print('hello from foo')")
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" src="foo.py"></script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "hello from foo"
|
||||
|
||||
@skip_worker("NEXT: banner not shown")
|
||||
def test_py_script_src_not_found(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" src="foo.py"></script>
|
||||
""",
|
||||
check_js_errors=False,
|
||||
)
|
||||
assert "Failed to load resource" in self.console.error.lines[0]
|
||||
|
||||
# TODO: we need to be sure errors make sense from both main and worker worlds
|
||||
expected_msg = "(PY0404): Fetching from URL foo.py failed with error 404"
|
||||
assert any((expected_msg in line) for line in self.console.error.lines)
|
||||
assert self.assert_banner_message(expected_msg)
|
||||
|
||||
# TODO: ... and we shouldn't: it's a module and we better don't leak in global
|
||||
@pytest.mark.skip("NEXT: we don't expose pyscript on window")
|
||||
def test_js_version(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
self.page.add_script_tag(content="console.log(pyscript.version)")
|
||||
|
||||
assert (
|
||||
re.match(r"\d{4}\.\d{2}\.\d+(\.[a-zA-Z0-9]+)?", self.console.log.lines[-1])
|
||||
is not None
|
||||
)
|
||||
|
||||
# TODO: ... and we shouldn't: it's a module and we better don't leak in global
|
||||
@pytest.mark.skip("NEXT: we don't expose pyscript on window")
|
||||
def test_python_version(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
import js
|
||||
js.console.log(pyscript.__version__)
|
||||
js.console.log(str(pyscript.version_info))
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
assert (
|
||||
re.match(r"\d{4}\.\d{2}\.\d+(\.[a-zA-Z0-9]+)?", self.console.log.lines[-2])
|
||||
is not None
|
||||
)
|
||||
assert (
|
||||
re.match(
|
||||
r"version_info\(year=\d{4}, month=\d{2}, "
|
||||
r"minor=\d+, releaselevel='([a-zA-Z0-9]+)?'\)",
|
||||
self.console.log.lines[-1],
|
||||
)
|
||||
is not None
|
||||
)
|
||||
|
||||
@pytest.mark.skip("NEXT: works with <py-script> not with <script>")
|
||||
def test_getPySrc_returns_source_code(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>print("hello from py-script")</py-script>
|
||||
<script type="py">print("hello from script py")</script>
|
||||
"""
|
||||
)
|
||||
pyscript_tag = self.page.locator("py-script")
|
||||
assert pyscript_tag.inner_html() == ""
|
||||
assert (
|
||||
pyscript_tag.evaluate("node => node.srcCode")
|
||||
== 'print("hello from py-script")'
|
||||
)
|
||||
script_py_tag = self.page.locator('script[type="py"]')
|
||||
assert (
|
||||
script_py_tag.evaluate("node => node.srcCode")
|
||||
== 'print("hello from script py")'
|
||||
)
|
||||
|
||||
@skip_worker("NEXT: py-click doesn't work inside workers")
|
||||
def test_py_attribute_without_id(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button py-click="myfunc">Click me</button>
|
||||
<script type="py">
|
||||
def myfunc(event):
|
||||
print("hello world!")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
btn = self.page.wait_for_selector("button")
|
||||
btn.click()
|
||||
self.wait_for_console("hello world!")
|
||||
assert self.console.log.lines[-1] == "hello world!"
|
||||
assert self.console.error.lines == []
|
||||
|
||||
def test_py_all_done_event(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script>
|
||||
addEventListener("py:all-done", () => console.log("2"))
|
||||
</script>
|
||||
<script type="py">
|
||||
print("1")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines == ["1", "2"]
|
||||
assert self.console.error.lines == []
|
||||
@@ -1,526 +0,0 @@
|
||||
################################################################################
|
||||
|
||||
import base64
|
||||
import html
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from .support import (
|
||||
PageErrors,
|
||||
PyScriptTest,
|
||||
filter_inner_text,
|
||||
filter_page_content,
|
||||
only_main,
|
||||
skip_worker,
|
||||
wait_for_render,
|
||||
)
|
||||
|
||||
DISPLAY_OUTPUT_ID_PATTERN = r'script-py[id^="py-"]'
|
||||
|
||||
|
||||
class TestDisplay(PyScriptTest):
|
||||
def test_simple_display(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
print('ciao')
|
||||
from pyscript import display
|
||||
display("hello world")
|
||||
</script>
|
||||
""",
|
||||
timeout=20000,
|
||||
)
|
||||
node_list = self.page.query_selector_all(DISPLAY_OUTPUT_ID_PATTERN)
|
||||
pattern = r"<div>hello world</div>"
|
||||
assert node_list[0].inner_html() == pattern
|
||||
assert len(node_list) == 1
|
||||
|
||||
def test_consecutive_display(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" async="false">
|
||||
from pyscript import display
|
||||
display('hello 1')
|
||||
</script>
|
||||
<p>hello 2</p>
|
||||
<script type="py" async="false">
|
||||
from pyscript import display
|
||||
display('hello 3')
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
inner_text = self.page.inner_text("body")
|
||||
lines = inner_text.splitlines()
|
||||
|
||||
lines = [line for line in filter_page_content(lines)] # remove empty lines
|
||||
assert lines == ["hello 1", "hello 2", "hello 3"]
|
||||
|
||||
def test_target_parameter(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display('hello world', target="mydiv")
|
||||
</script>
|
||||
<div id="mydiv"></div>
|
||||
"""
|
||||
)
|
||||
mydiv = self.page.locator("#mydiv")
|
||||
assert mydiv.inner_text() == "hello world"
|
||||
|
||||
def test_target_parameter_with_sharp(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display('hello world', target="#mydiv")
|
||||
</script>
|
||||
<div id="mydiv"></div>
|
||||
"""
|
||||
)
|
||||
mydiv = self.page.locator("#mydiv")
|
||||
assert mydiv.inner_text() == "hello world"
|
||||
|
||||
def test_non_existing_id_target_raises_value_error(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display('hello world', target="non-existing")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
error_msg = (
|
||||
f"Invalid selector with id=non-existing. Cannot be found in the page."
|
||||
)
|
||||
self.check_py_errors(f"ValueError: {error_msg}")
|
||||
|
||||
def test_empty_string_target_raises_value_error(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display('hello world', target="")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
self.check_py_errors(f"ValueError: Cannot have an empty target")
|
||||
|
||||
def test_non_string_target_values_raise_typerror(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display("hello False", target=False)
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
error_msg = f"target must be str or None, not bool"
|
||||
self.check_py_errors(f"TypeError: {error_msg}")
|
||||
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display("hello False", target=123)
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
error_msg = f"target must be str or None, not int"
|
||||
self.check_py_errors(f"TypeError: {error_msg}")
|
||||
|
||||
@skip_worker("NEXT: display(target=...) does not work")
|
||||
def test_tag_target_attribute(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" target="hello">
|
||||
from pyscript import display
|
||||
display('hello')
|
||||
display("goodbye world", target="goodbye")
|
||||
display('world')
|
||||
</script>
|
||||
<div id="hello"></div>
|
||||
<div id="goodbye"></div>
|
||||
"""
|
||||
)
|
||||
hello = self.page.locator("#hello")
|
||||
assert hello.inner_text() == "hello\nworld"
|
||||
|
||||
goodbye = self.page.locator("#goodbye")
|
||||
assert goodbye.inner_text() == "goodbye world"
|
||||
|
||||
@skip_worker("NEXT: display target does not work properly")
|
||||
def test_target_script_py(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div>ONE</div>
|
||||
<script type="py" id="two">
|
||||
# just a placeholder
|
||||
</script>
|
||||
<div>THREE</div>
|
||||
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display('TWO', target="two")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
text = self.page.inner_text("body")
|
||||
assert text == "ONE\nTWO\nTHREE"
|
||||
|
||||
@skip_worker("NEXT: display target does not work properly")
|
||||
def test_consecutive_display_target(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" id="first" async="false">
|
||||
from pyscript import display
|
||||
display('hello 1')
|
||||
</script>
|
||||
<p>hello in between 1 and 2</p>
|
||||
<script type="py" id="second" async="false">
|
||||
from pyscript import display
|
||||
display('hello 2', target="second")
|
||||
</script>
|
||||
<script type="py" id="third" async="false">
|
||||
from pyscript import display
|
||||
display('hello 3')
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
inner_text = self.page.inner_text("body")
|
||||
lines = inner_text.splitlines()
|
||||
lines = [line for line in filter_page_content(lines)] # remove empty lines
|
||||
assert lines == ["hello 1", "hello in between 1 and 2", "hello 2", "hello 3"]
|
||||
|
||||
def test_multiple_display_calls_same_tag(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display('hello')
|
||||
display('world')
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
tag = self.page.locator("script-py")
|
||||
lines = tag.inner_text().splitlines()
|
||||
assert lines == ["hello", "world"]
|
||||
|
||||
@only_main # with workers, two tags are two separate interpreters
|
||||
def test_implicit_target_from_a_different_tag(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
def say_hello():
|
||||
display('hello')
|
||||
</script>
|
||||
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
say_hello()
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
elems = self.page.locator("script-py")
|
||||
py0 = elems.nth(0)
|
||||
py1 = elems.nth(1)
|
||||
assert py0.inner_text() == ""
|
||||
assert py1.inner_text() == "hello"
|
||||
|
||||
@skip_worker("NEXT: py-click doesn't work")
|
||||
def test_no_explicit_target(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
def display_hello(error):
|
||||
display('hello world')
|
||||
</script>
|
||||
<button id="my-button" py-click="display_hello">Click me</button>
|
||||
"""
|
||||
)
|
||||
self.page.locator("button").click()
|
||||
|
||||
text = self.page.locator("script-py").text_content()
|
||||
assert "hello world" in text
|
||||
|
||||
@skip_worker("NEXT: display target does not work properly")
|
||||
def test_explicit_target_pyscript_tag(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
def display_hello():
|
||||
display('hello', target='second-pyscript-tag')
|
||||
</script>
|
||||
<script type="py" id="second-pyscript-tag">
|
||||
display_hello()
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
text = self.page.locator("script-py").nth(1).inner_text()
|
||||
assert text == "hello"
|
||||
|
||||
@skip_worker("NEXT: display target does not work properly")
|
||||
def test_explicit_target_on_button_tag(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
def display_hello(error):
|
||||
display('hello', target='my-button')
|
||||
</script>
|
||||
<button id="my-button" py-click="display_hello">Click me</button>
|
||||
"""
|
||||
)
|
||||
self.page.locator("text=Click me").click()
|
||||
text = self.page.locator("id=my-button").inner_text()
|
||||
assert "hello" in text
|
||||
|
||||
def test_append_true(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display('AAA', append=True)
|
||||
display('BBB', append=True)
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
output = self.page.locator("script-py")
|
||||
assert output.inner_text() == "AAA\nBBB"
|
||||
|
||||
def test_append_false(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display('AAA', append=False)
|
||||
display('BBB', append=False)
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
output = self.page.locator("script-py")
|
||||
assert output.inner_text() == "BBB"
|
||||
|
||||
def test_display_multiple_values(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
hello = 'hello'
|
||||
world = 'world'
|
||||
display(hello, world)
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
output = self.page.locator("script-py")
|
||||
assert output.inner_text() == "hello\nworld"
|
||||
|
||||
def test_display_multiple_append_false(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display('hello', append=False)
|
||||
display('world', append=False)
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
output = self.page.locator("script-py")
|
||||
assert output.inner_text() == "world"
|
||||
|
||||
# TODO: this is a display.py issue to fix when append=False is used
|
||||
# do not use the first element, just clean up and then append
|
||||
# remove the # display comment once that's done
|
||||
def test_display_multiple_append_false_with_target(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="circle-div"></div>
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
class Circle:
|
||||
r = 0
|
||||
def _repr_svg_(self):
|
||||
return (
|
||||
f'<svg height="{self.r*2}" width="{self.r*2}">'
|
||||
f'<circle cx="{self.r}" cy="{self.r}" r="{self.r}" fill="red" /></svg>'
|
||||
)
|
||||
|
||||
circle = Circle()
|
||||
|
||||
circle.r += 5
|
||||
# display(circle, target="circle-div", append=False)
|
||||
circle.r += 5
|
||||
display(circle, target="circle-div", append=False)
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
innerhtml = self.page.locator("id=circle-div").inner_html()
|
||||
assert (
|
||||
innerhtml
|
||||
== '<svg height="20" width="20"><circle cx="10" cy="10" r="10" fill="red"></circle></svg>' # noqa: E501
|
||||
)
|
||||
assert self.console.error.lines == []
|
||||
|
||||
def test_display_list_dict_tuple(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
l = ['A', 1, '!']
|
||||
d = {'B': 2, 'List': l}
|
||||
t = ('C', 3, '!')
|
||||
display(l, d, t)
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
inner_text = self.page.inner_text("html")
|
||||
filtered_inner_text = filter_inner_text(inner_text)
|
||||
print(filtered_inner_text)
|
||||
assert (
|
||||
filtered_inner_text
|
||||
== "['A', 1, '!']\n{'B': 2, 'List': ['A', 1, '!']}\n('C', 3, '!')"
|
||||
)
|
||||
|
||||
def test_display_should_escape(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display("<p>hello world</p>")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
out = self.page.locator("script-py > div")
|
||||
assert out.inner_html() == html.escape("<p>hello world</p>")
|
||||
assert out.inner_text() == "<p>hello world</p>"
|
||||
|
||||
def test_display_HTML(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display, HTML
|
||||
display(HTML("<p>hello world</p>"))
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
out = self.page.locator("script-py > div")
|
||||
assert out.inner_html() == "<p>hello world</p>"
|
||||
assert out.inner_text() == "hello world"
|
||||
|
||||
@skip_worker("NEXT: matplotlib-pyodide backend does not work")
|
||||
def test_image_display(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config> packages = ["matplotlib"] </py-config>
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
import matplotlib.pyplot as plt
|
||||
xpoints = [3, 6, 9]
|
||||
ypoints = [1, 2, 3]
|
||||
plt.plot(xpoints, ypoints)
|
||||
display(plt)
|
||||
</script>
|
||||
""",
|
||||
timeout=30 * 1000,
|
||||
)
|
||||
wait_for_render(self.page, "*", "<img src=['\"]data:image")
|
||||
test = self.page.wait_for_selector("img")
|
||||
img_src = test.get_attribute("src").replace(
|
||||
"data:image/png;charset=utf-8;base64,", ""
|
||||
)
|
||||
img_data = np.asarray(Image.open(io.BytesIO(base64.b64decode(img_src))))
|
||||
with Image.open(
|
||||
os.path.join(os.path.dirname(__file__), "test_assets", "line_plot.png"),
|
||||
) as image:
|
||||
ref_data = np.asarray(image)
|
||||
|
||||
deviation = np.mean(np.abs(img_data - ref_data))
|
||||
assert deviation == 0.0
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_empty_HTML_and_console_output(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
import js
|
||||
print('print from python')
|
||||
js.console.log('print from js')
|
||||
js.console.error('error from js');
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
inner_html = self.page.content()
|
||||
assert re.search("", inner_html)
|
||||
console_text = self.console.all.lines
|
||||
assert "print from python" in console_text
|
||||
assert "print from js" in console_text
|
||||
assert "error from js" in console_text
|
||||
|
||||
def test_text_HTML_and_console_output(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
import js
|
||||
display('this goes to the DOM')
|
||||
print('print from python')
|
||||
js.console.log('print from js')
|
||||
js.console.error('error from js');
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
inner_text = self.page.inner_text("script-py")
|
||||
assert inner_text == "this goes to the DOM"
|
||||
assert self.console.log.lines[-2:] == [
|
||||
"print from python",
|
||||
"print from js",
|
||||
]
|
||||
print(self.console.error.lines)
|
||||
assert self.console.error.lines[-1] == "error from js"
|
||||
|
||||
def test_console_line_break(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
print('1print\\n2print')
|
||||
print('1console\\n2console')
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
console_text = self.console.all.lines
|
||||
assert console_text.index("1print") == (console_text.index("2print") - 1)
|
||||
assert console_text.index("1console") == (console_text.index("2console") - 1)
|
||||
|
||||
@skip_worker("NEXT: display target does not work properly")
|
||||
def test_image_renders_correctly(self):
|
||||
"""
|
||||
This is just a sanity check to make sure that images are rendered
|
||||
in a reasonable way.
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
packages = ["pillow"]
|
||||
</py-config>
|
||||
|
||||
<div id="img-target" />
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
from PIL import Image
|
||||
img = Image.new("RGB", (4, 4), color=(0, 0, 0))
|
||||
display(img, target='img-target', append=False)
|
||||
</script>
|
||||
""",
|
||||
)
|
||||
|
||||
img_src = self.page.locator("img").get_attribute("src")
|
||||
assert img_src.startswith("data:image/png;charset=utf-8;base64")
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
@@ -1,205 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest, filter_inner_text, only_main
|
||||
|
||||
|
||||
class TestAsync(PyScriptTest):
|
||||
# ensure_future() and create_task() should behave similarly;
|
||||
# we'll use the same source code to test both
|
||||
coroutine_script = """
|
||||
<script type="py">
|
||||
import js
|
||||
import asyncio
|
||||
js.console.log("first")
|
||||
async def main():
|
||||
await asyncio.sleep(1)
|
||||
js.console.log("third")
|
||||
asyncio.{func}(main())
|
||||
js.console.log("second")
|
||||
</script>
|
||||
"""
|
||||
|
||||
def test_asyncio_ensure_future(self):
|
||||
self.pyscript_run(self.coroutine_script.format(func="ensure_future"))
|
||||
self.wait_for_console("third")
|
||||
assert self.console.log.lines[-3:] == ["first", "second", "third"]
|
||||
|
||||
def test_asyncio_create_task(self):
|
||||
self.pyscript_run(self.coroutine_script.format(func="create_task"))
|
||||
self.wait_for_console("third")
|
||||
assert self.console.log.lines[-3:] == ["first", "second", "third"]
|
||||
|
||||
def test_asyncio_gather(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" id="pys">
|
||||
import asyncio
|
||||
import js
|
||||
from pyodide.ffi import to_js
|
||||
|
||||
async def coro(delay):
|
||||
await asyncio.sleep(delay)
|
||||
return(delay)
|
||||
|
||||
async def get_results():
|
||||
results = await asyncio.gather(*[coro(d) for d in range(3,0,-1)])
|
||||
js.console.log(str(results)) #Compare to string representation, not Proxy
|
||||
js.console.log("DONE")
|
||||
|
||||
asyncio.ensure_future(get_results())
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
self.wait_for_console("DONE")
|
||||
assert self.console.log.lines[-2:] == ["[3, 2, 1]", "DONE"]
|
||||
|
||||
@only_main
|
||||
def test_multiple_async(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
import js
|
||||
import asyncio
|
||||
async def a_func():
|
||||
for i in range(3):
|
||||
js.console.log('A', i)
|
||||
await asyncio.sleep(0.1)
|
||||
asyncio.ensure_future(a_func())
|
||||
</script>
|
||||
|
||||
<script type="py">
|
||||
import js
|
||||
import asyncio
|
||||
async def b_func():
|
||||
for i in range(3):
|
||||
js.console.log('B', i)
|
||||
await asyncio.sleep(0.1)
|
||||
js.console.log('b func done')
|
||||
asyncio.ensure_future(b_func())
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
self.wait_for_console("b func done")
|
||||
assert self.console.log.lines == [
|
||||
"A 0",
|
||||
"B 0",
|
||||
"A 1",
|
||||
"B 1",
|
||||
"A 2",
|
||||
"B 2",
|
||||
"b func done",
|
||||
]
|
||||
|
||||
@only_main
|
||||
def test_multiple_async_multiple_display_targeted(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" id="pyA">
|
||||
from pyscript import display
|
||||
import js
|
||||
import asyncio
|
||||
|
||||
async def a_func():
|
||||
for i in range(2):
|
||||
display(f'A{i}', target='pyA', append=True)
|
||||
js.console.log("A", i)
|
||||
await asyncio.sleep(0.1)
|
||||
asyncio.ensure_future(a_func())
|
||||
|
||||
</script>
|
||||
|
||||
<script type="py" id="pyB">
|
||||
from pyscript import display
|
||||
import js
|
||||
import asyncio
|
||||
|
||||
async def a_func():
|
||||
for i in range(2):
|
||||
display(f'B{i}', target='pyB', append=True)
|
||||
js.console.log("B", i)
|
||||
await asyncio.sleep(0.1)
|
||||
js.console.log("B DONE")
|
||||
|
||||
asyncio.ensure_future(a_func())
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
self.wait_for_console("B DONE")
|
||||
inner_text = self.page.inner_text("html")
|
||||
assert "A0\nA1\nB0\nB1" in filter_inner_text(inner_text)
|
||||
|
||||
def test_async_display_untargeted(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
import asyncio
|
||||
import js
|
||||
|
||||
async def a_func():
|
||||
display('A')
|
||||
await asyncio.sleep(1)
|
||||
js.console.log("DONE")
|
||||
|
||||
asyncio.ensure_future(a_func())
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
self.wait_for_console("DONE")
|
||||
assert self.page.locator("script-py").inner_text() == "A"
|
||||
|
||||
@only_main
|
||||
def test_sync_and_async_order(self):
|
||||
"""
|
||||
The order of execution is defined as follows:
|
||||
1. first, we execute all the script tags in order
|
||||
2. then, we start all the tasks which were scheduled with create_task
|
||||
|
||||
Note that tasks are started *AFTER* all py-script tags have been
|
||||
executed. That's why the console.log() inside mytask1 and mytask2 are
|
||||
executed after e.g. js.console.log("6").
|
||||
"""
|
||||
src = """
|
||||
<script type="py">
|
||||
import js
|
||||
js.console.log("1")
|
||||
</script>
|
||||
|
||||
<script type="py">
|
||||
import asyncio
|
||||
import js
|
||||
|
||||
async def mytask1():
|
||||
js.console.log("7")
|
||||
await asyncio.sleep(0)
|
||||
js.console.log("9")
|
||||
|
||||
js.console.log("2")
|
||||
asyncio.create_task(mytask1())
|
||||
js.console.log("3")
|
||||
</script>
|
||||
|
||||
<script type="py">
|
||||
import js
|
||||
js.console.log("4")
|
||||
</script>
|
||||
|
||||
<script type="py">
|
||||
import asyncio
|
||||
import js
|
||||
|
||||
async def mytask2():
|
||||
js.console.log("8")
|
||||
await asyncio.sleep(0)
|
||||
js.console.log("10")
|
||||
js.console.log("DONE")
|
||||
|
||||
js.console.log("5")
|
||||
asyncio.create_task(mytask2())
|
||||
js.console.log("6")
|
||||
</script>
|
||||
"""
|
||||
self.pyscript_run(src, wait_for_pyscript=False)
|
||||
self.wait_for_console("DONE")
|
||||
lines = self.console.log.lines[-11:]
|
||||
assert lines == ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "DONE"]
|
||||
@@ -1,66 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="See PR #938")
|
||||
class TestImportmap(PyScriptTest):
|
||||
def test_importmap(self):
|
||||
src = """
|
||||
export function say_hello(who) {
|
||||
console.log("hello from", who);
|
||||
}
|
||||
"""
|
||||
self.writefile("mymod.js", src)
|
||||
#
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"mymod": "/mymod.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
import { say_hello } from "mymod";
|
||||
say_hello("JS");
|
||||
</script>
|
||||
|
||||
<script type="py">
|
||||
import mymod
|
||||
mymod.say_hello("Python")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines == [
|
||||
"hello from JS",
|
||||
"hello from Python",
|
||||
]
|
||||
|
||||
def test_invalid_json(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="importmap">
|
||||
this is not valid JSON
|
||||
</script>
|
||||
|
||||
<script type="py">
|
||||
print("hello world")
|
||||
</script>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
# this error is raised by the browser itself, when *it* tries to parse
|
||||
# the import map
|
||||
self.check_js_errors("Failed to parse import map")
|
||||
|
||||
self.wait_for_pyscript()
|
||||
assert self.console.log.lines == [
|
||||
"hello world",
|
||||
]
|
||||
# this warning is shown by pyscript, when *we* try to parse the import
|
||||
# map
|
||||
banner = self.page.locator(".py-warning")
|
||||
assert "Failed to parse import map" in banner.inner_text()
|
||||
@@ -1,29 +0,0 @@
|
||||
from .support import PyScriptTest, with_execution_thread
|
||||
|
||||
|
||||
@with_execution_thread(None)
|
||||
class TestSmokeTests(PyScriptTest):
|
||||
"""
|
||||
Each example requires the same three tests:
|
||||
- Test that the initial markup loads properly (currently done by
|
||||
testing the <title> tag's content)
|
||||
- Testing that pyscript is loading properly
|
||||
- Testing that the page contains appropriate content after rendering
|
||||
"""
|
||||
|
||||
def test_pydom(self):
|
||||
# Test the full pydom test suite by running it in the browser
|
||||
self.goto("tests/pyscript_dom/index.html?-v&-s")
|
||||
assert self.page.title() == "PyDom Test Suite"
|
||||
|
||||
# wait for the test suite to finish
|
||||
self.wait_for_console(
|
||||
"============================= test session starts =============================="
|
||||
)
|
||||
|
||||
self.assert_no_banners()
|
||||
|
||||
results = self.page.inner_html("#tests-terminal")
|
||||
assert results
|
||||
assert "PASSED" in results
|
||||
assert "FAILED" not in results
|
||||
@@ -1,98 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest
|
||||
|
||||
pytest.skip(
|
||||
reason="NEXT: pyscript API changed doesn't expose pyscript to window anymore",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
|
||||
class TestInterpreterAccess(PyScriptTest):
|
||||
"""Test accessing Python objects from JS via pyscript.interpreter"""
|
||||
|
||||
def test_interpreter_python_access(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
x = 1
|
||||
def py_func():
|
||||
return 2
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
self.run_js(
|
||||
"""
|
||||
const x = await pyscript.interpreter.globals.get('x');
|
||||
const py_func = await pyscript.interpreter.globals.get('py_func');
|
||||
const py_func_res = await py_func();
|
||||
console.log(`x is ${x}`);
|
||||
console.log(`py_func() returns ${py_func_res}`);
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-2:] == [
|
||||
"x is 1",
|
||||
"py_func() returns 2",
|
||||
]
|
||||
|
||||
def test_interpreter_script_execution(self):
|
||||
"""Test running Python code from js via pyscript.interpreter"""
|
||||
self.pyscript_run("")
|
||||
|
||||
self.run_js(
|
||||
"""
|
||||
const interface = pyscript.interpreter._remote.interface;
|
||||
await interface.runPython('print("Interpreter Ran This")');
|
||||
"""
|
||||
)
|
||||
|
||||
expected_message = "Interpreter Ran This"
|
||||
assert self.console.log.lines[-1] == expected_message
|
||||
|
||||
py_terminal = self.page.wait_for_selector("py-terminal")
|
||||
assert py_terminal.text_content() == expected_message
|
||||
|
||||
def test_backward_compatibility_runtime_script_execution(self):
|
||||
"""Test running Python code from js via pyscript.runtime"""
|
||||
self.pyscript_run("")
|
||||
|
||||
self.run_js(
|
||||
"""
|
||||
const interface = pyscript.runtime._remote.interpreter;
|
||||
await interface.runPython('print("Interpreter Ran This")');
|
||||
"""
|
||||
)
|
||||
|
||||
expected_message = "Interpreter Ran This"
|
||||
assert self.console.log.lines[-1] == expected_message
|
||||
|
||||
py_terminal = self.page.wait_for_selector("py-terminal")
|
||||
assert py_terminal.text_content() == expected_message
|
||||
|
||||
def test_backward_compatibility_runtime_python_access(self):
|
||||
"""Test accessing Python objects from JS via pyscript.runtime"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
x = 1
|
||||
def py_func():
|
||||
return 2
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
self.run_js(
|
||||
"""
|
||||
const x = await pyscript.interpreter.globals.get('x');
|
||||
const py_func = await pyscript.interpreter.globals.get('py_func');
|
||||
const py_func_res = await py_func();
|
||||
console.log(`x is ${x}`);
|
||||
console.log(`py_func() returns ${py_func_res}`);
|
||||
"""
|
||||
)
|
||||
|
||||
assert self.console.log.lines[-2:] == [
|
||||
"x is 1",
|
||||
"py_func() returns 2",
|
||||
]
|
||||
@@ -1,419 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest, skip_worker
|
||||
|
||||
pytest.skip(
|
||||
reason="NEXT: plugins not supported",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
# Source code of a simple plugin that creates a Custom Element for testing purposes
|
||||
CE_PLUGIN_CODE = """
|
||||
from pyscript import Plugin
|
||||
from js import console
|
||||
|
||||
plugin = Plugin('py-upper')
|
||||
|
||||
console.log("py_upper Plugin loaded")
|
||||
|
||||
@plugin.register_custom_element('py-up')
|
||||
class Upper:
|
||||
def __init__(self, element):
|
||||
self.element = element
|
||||
|
||||
def connect(self):
|
||||
console.log("Upper plugin connected")
|
||||
return self.element.originalInnerHTML.upper()
|
||||
"""
|
||||
|
||||
# Source of a plugin hooks into the PyScript App lifecycle events
|
||||
HOOKS_PLUGIN_CODE = """
|
||||
from pyscript import Plugin
|
||||
from js import console
|
||||
|
||||
class TestLogger(Plugin):
|
||||
def configure(self, config):
|
||||
console.log('configure called')
|
||||
|
||||
def beforeLaunch(self, config):
|
||||
console.log('beforeLaunch called')
|
||||
|
||||
def afterSetup(self, config):
|
||||
console.log('afterSetup called')
|
||||
|
||||
def afterStartup(self, config):
|
||||
console.log('afterStartup called')
|
||||
|
||||
def beforePyScriptExec(self, interpreter, src, pyScriptTag):
|
||||
console.log(f'beforePyScriptExec called')
|
||||
console.log(f'before_src:{src}')
|
||||
|
||||
def afterPyScriptExec(self, interpreter, src, pyScriptTag, result):
|
||||
console.log(f'afterPyScriptExec called')
|
||||
console.log(f'after_src:{src}')
|
||||
|
||||
def onUserError(self, config):
|
||||
console.log('onUserError called')
|
||||
|
||||
|
||||
plugin = TestLogger()
|
||||
"""
|
||||
|
||||
# Source of script that defines a plugin with only beforePyScriptExec and
|
||||
# afterPyScriptExec methods
|
||||
PYSCRIPT_HOOKS_PLUGIN_CODE = """
|
||||
from pyscript import Plugin
|
||||
from js import console
|
||||
|
||||
class ExecTestLogger(Plugin):
|
||||
|
||||
async def beforePyScriptExec(self, interpreter, src, pyScriptTag):
|
||||
console.log(f'beforePyScriptExec called')
|
||||
console.log(f'before_src:{src}')
|
||||
|
||||
async def afterPyScriptExec(self, interpreter, src, pyScriptTag, result):
|
||||
console.log(f'afterPyScriptExec called')
|
||||
console.log(f'after_src:{src}')
|
||||
console.log(f'result:{result}')
|
||||
|
||||
|
||||
plugin = ExecTestLogger()
|
||||
"""
|
||||
|
||||
# Source of script that defines a plugin with only beforePyScriptExec and
|
||||
# afterPyScriptExec methods
|
||||
PYREPL_HOOKS_PLUGIN_CODE = """
|
||||
from pyscript import Plugin
|
||||
from js import console
|
||||
|
||||
console.warn("This is in pyrepl hooks file")
|
||||
|
||||
class PyReplTestLogger(Plugin):
|
||||
|
||||
def beforePyReplExec(self, interpreter, src, outEl, pyReplTag):
|
||||
console.log(f'beforePyReplExec called')
|
||||
console.log(f'before_src:{src}')
|
||||
|
||||
def afterPyReplExec(self, interpreter, src, outEl, pyReplTag, result):
|
||||
console.log(f'afterPyReplExec called')
|
||||
console.log(f'after_src:{src}')
|
||||
console.log(f'result:{result}')
|
||||
|
||||
|
||||
plugin = PyReplTestLogger()
|
||||
"""
|
||||
|
||||
# Source of a script that doesn't call define a `plugin` attribute
|
||||
NO_PLUGIN_CODE = """
|
||||
from pyscript import Plugin
|
||||
from js import console
|
||||
|
||||
class TestLogger(Plugin):
|
||||
pass
|
||||
"""
|
||||
|
||||
# Source code of a simple plugin that creates a Custom Element for testing purposes
|
||||
CODE_CE_PLUGIN_BAD_RETURNS = """
|
||||
from pyscript import Plugin
|
||||
from js import console
|
||||
|
||||
plugin = Plugin('py-broken')
|
||||
|
||||
@plugin.register_custom_element('py-up')
|
||||
class Upper:
|
||||
def __init__(self, element):
|
||||
self.element = element
|
||||
|
||||
def connect(self):
|
||||
# Just returning something... anything other than a string should be ignore
|
||||
return Plugin
|
||||
"""
|
||||
HTML_TEMPLATE_WITH_TAG = """
|
||||
<py-config>
|
||||
plugins = [
|
||||
"./{plugin_name}.py"
|
||||
]
|
||||
</py-config>
|
||||
|
||||
<{tagname}>
|
||||
{html}
|
||||
</{tagname}>
|
||||
"""
|
||||
HTML_TEMPLATE_NO_TAG = """
|
||||
<py-config>
|
||||
plugins = [
|
||||
"./{plugin_name}.py"
|
||||
]
|
||||
</py-config>
|
||||
"""
|
||||
|
||||
|
||||
def prepare_test(
|
||||
plugin_name, code, tagname="", html="", template=HTML_TEMPLATE_WITH_TAG
|
||||
):
|
||||
"""
|
||||
Prepares the test by writing a new plugin file named `plugin_name`.py, with `code` as its
|
||||
content and run `pyscript_run` on `template` formatted with the above inputs to create the
|
||||
page HTML code.
|
||||
|
||||
For example:
|
||||
|
||||
>> @prepare_test('py-upper', CE_PLUGIN_CODE, tagname='py-up', html="Hello World")
|
||||
>> def my_foo(...):
|
||||
>> ...
|
||||
|
||||
will:
|
||||
|
||||
* write a new `py-upper.py` file to the FS
|
||||
* the contents of `py-upper.py` is equal to CE_PLUGIN_CODE
|
||||
* call self.pyscript_run with the following string:
|
||||
'''
|
||||
<py-config>
|
||||
plugins = [
|
||||
"./py-upper.py"
|
||||
]
|
||||
</py-config>
|
||||
|
||||
<py-up>
|
||||
{html}
|
||||
</py-up>
|
||||
'''
|
||||
* call `my_foo` just like a normal decorator would
|
||||
|
||||
"""
|
||||
|
||||
def dec(f):
|
||||
def _inner(self, *args, **kws):
|
||||
self.writefile(f"{plugin_name}.py", code)
|
||||
page_html = template.format(
|
||||
plugin_name=plugin_name, tagname=tagname, html=html
|
||||
)
|
||||
self.pyscript_run(page_html)
|
||||
return f(self, *args, **kws)
|
||||
|
||||
return _inner
|
||||
|
||||
return dec
|
||||
|
||||
|
||||
class TestPlugin(PyScriptTest):
|
||||
@skip_worker("FIXME: relative paths")
|
||||
@prepare_test("py-upper", CE_PLUGIN_CODE, tagname="py-up", html="Hello World")
|
||||
def test_py_plugin_inline(self):
|
||||
"""Test that a regular plugin that returns new HTML content from connected works"""
|
||||
# GIVEN a plugin that returns the all caps version of the tag innerHTML and logs text
|
||||
# during it's execution/hooks
|
||||
|
||||
# EXPECT the plugin logs to be present in the console logs
|
||||
log_lines = self.console.log.lines
|
||||
for log_line in ["py_upper Plugin loaded", "Upper plugin connected"]:
|
||||
assert log_line in log_lines
|
||||
|
||||
# EXPECT the inner text of the Plugin CustomElement to be all caps
|
||||
rendered_text = self.page.locator("py-up").inner_text()
|
||||
assert rendered_text == "HELLO WORLD"
|
||||
|
||||
@skip_worker("FIXME: relative paths")
|
||||
@prepare_test("hooks_logger", HOOKS_PLUGIN_CODE, template=HTML_TEMPLATE_NO_TAG)
|
||||
def test_execution_hooks(self):
|
||||
"""Test that a Plugin that hooks into the PyScript App events, gets called
|
||||
for each one of them"""
|
||||
# GIVEN a plugin that logs specific strings for each app execution event
|
||||
hooks_available = ["afterSetup", "afterStartup"]
|
||||
hooks_unavailable = [
|
||||
"configure",
|
||||
"beforeLaunch",
|
||||
"beforePyScriptExec",
|
||||
"afterPyScriptExec",
|
||||
"beforePyReplExec",
|
||||
"afterPyReplExec",
|
||||
]
|
||||
|
||||
# EXPECT it to log the correct logs for the events it intercepts
|
||||
log_lines = self.console.log.lines
|
||||
num_calls = {
|
||||
method: log_lines.count(f"{method} called") for method in hooks_available
|
||||
}
|
||||
expected_calls = {method: 1 for method in hooks_available}
|
||||
assert num_calls == expected_calls
|
||||
|
||||
# EXPECT it to NOT be called (hence not log anything) the events that happen
|
||||
# before it's ready, hence is not called
|
||||
unavailable_called = {
|
||||
method: f"{method} called" in log_lines for method in hooks_unavailable
|
||||
}
|
||||
assert unavailable_called == {method: False for method in hooks_unavailable}
|
||||
|
||||
# TODO: It'd be actually better to check that the events get called in order
|
||||
|
||||
@skip_worker("FIXME: relative paths")
|
||||
@prepare_test(
|
||||
"exec_test_logger",
|
||||
PYSCRIPT_HOOKS_PLUGIN_CODE,
|
||||
template=HTML_TEMPLATE_NO_TAG + "\n<script type='py' id='pyid'>x=2; x</script>",
|
||||
)
|
||||
def test_pyscript_exec_hooks(self):
|
||||
"""Test that the beforePyScriptExec and afterPyScriptExec hooks work as intended"""
|
||||
assert self.page.locator("script") is not None
|
||||
|
||||
log_lines: list[str] = self.console.log.lines
|
||||
|
||||
assert "beforePyScriptExec called" in log_lines
|
||||
assert "afterPyScriptExec called" in log_lines
|
||||
|
||||
# These could be made better with a utility function that found log lines
|
||||
# that match a filter function, or start with something
|
||||
assert "before_src:x=2; x" in log_lines
|
||||
assert "after_src:x=2; x" in log_lines
|
||||
assert "result:2" in log_lines
|
||||
|
||||
@skip_worker("FIXME: relative paths")
|
||||
@prepare_test(
|
||||
"pyrepl_test_logger",
|
||||
PYREPL_HOOKS_PLUGIN_CODE,
|
||||
template=HTML_TEMPLATE_NO_TAG + "\n<py-repl id='pyid'>x=2; x</py-repl>",
|
||||
)
|
||||
def test_pyrepl_exec_hooks(self):
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
# allow afterPyReplExec to also finish before the test finishes
|
||||
self.wait_for_console("result:2")
|
||||
|
||||
log_lines: list[str] = self.console.log.lines
|
||||
|
||||
assert "beforePyReplExec called" in log_lines
|
||||
assert "afterPyReplExec called" in log_lines
|
||||
|
||||
# These could be made better with a utility function that found log lines
|
||||
# that match a filter function, or start with something
|
||||
assert "before_src:x=2; x" in log_lines
|
||||
assert "after_src:x=2; x" in log_lines
|
||||
assert "result:2" in log_lines
|
||||
|
||||
@skip_worker("FIXME: relative paths")
|
||||
@prepare_test("no_plugin", NO_PLUGIN_CODE)
|
||||
def test_no_plugin_attribute_error(self):
|
||||
"""
|
||||
Test a plugin that do not add the `plugin` attribute to its module
|
||||
"""
|
||||
# GIVEN a Plugin NO `plugin` attribute in it's module
|
||||
error_msg = (
|
||||
"[pyscript/main] Cannot find plugin on Python module no_plugin! Python plugins "
|
||||
'modules must contain a "plugin" attribute. For more information check the '
|
||||
"plugins documentation."
|
||||
)
|
||||
# EXPECT an error for the missing attribute
|
||||
assert error_msg in self.console.error.lines
|
||||
|
||||
@skip_worker("FIXME: relative paths")
|
||||
def test_fetch_python_plugin(self):
|
||||
"""
|
||||
Test that we can fetch a plugin from a remote URL. Note we need to use
|
||||
the 'raw' URL for the plugin, otherwise the request will be rejected
|
||||
by cors policy.
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
plugins = [
|
||||
"https://raw.githubusercontent.com/FabioRosado/pyscript-plugins/main/python/hello-world.py"
|
||||
]
|
||||
|
||||
</py-config>
|
||||
<py-hello-world></py-hello-world>
|
||||
"""
|
||||
)
|
||||
|
||||
hello_element = self.page.locator("py-hello-world")
|
||||
assert hello_element.inner_html() == '<div id="hello">Hello World!</div>'
|
||||
|
||||
def test_fetch_js_plugin(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
plugins = [
|
||||
"https://raw.githubusercontent.com/FabioRosado/pyscript-plugins/main/js/hello-world.js"
|
||||
]
|
||||
</py-config>
|
||||
"""
|
||||
)
|
||||
|
||||
hello_element = self.page.locator("py-hello-world")
|
||||
assert hello_element.inner_html() == "<h1>Hello, world!</h1>"
|
||||
|
||||
def test_fetch_js_plugin_bare(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
plugins = [
|
||||
"https://raw.githubusercontent.com/FabioRosado/pyscript-plugins/main/js/hello-world-base.js"
|
||||
]
|
||||
</py-config>
|
||||
"""
|
||||
)
|
||||
|
||||
hello_element = self.page.locator("py-hello-world")
|
||||
assert hello_element.inner_html() == "<h1>Hello, world!</h1>"
|
||||
|
||||
def test_fetch_plugin_no_file_extension(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
plugins = [
|
||||
"https://non-existent.blah/hello-world"
|
||||
]
|
||||
</py-config>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
|
||||
expected_msg = (
|
||||
"(PY2000): Unable to load plugin from "
|
||||
"'https://non-existent.blah/hello-world'. Plugins "
|
||||
"need to contain a file extension and be either a "
|
||||
"python or javascript file."
|
||||
)
|
||||
|
||||
assert self.assert_banner_message(expected_msg)
|
||||
|
||||
def test_fetch_js_plugin_non_existent(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
plugins = [
|
||||
"http://non-existent.example.com/hello-world.js"
|
||||
]
|
||||
</py-config>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
|
||||
expected_msg = (
|
||||
"(PY0001): Fetching from URL "
|
||||
"http://non-existent.example.com/hello-world.js failed "
|
||||
"with error 'Failed to fetch'. Are your filename and "
|
||||
"path correct?"
|
||||
)
|
||||
|
||||
assert self.assert_banner_message(expected_msg)
|
||||
|
||||
def test_fetch_js_no_export(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
plugins = [
|
||||
"https://raw.githubusercontent.com/FabioRosado/pyscript-plugins/main/js/hello-world-no-export.js"
|
||||
]
|
||||
</py-config>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
|
||||
expected_message = (
|
||||
"(PY2001): Unable to load plugin from "
|
||||
"'https://raw.githubusercontent.com/FabioRosado/pyscript-plugins"
|
||||
"/main/js/hello-world-no-export.js'. "
|
||||
"Plugins need to contain a default export."
|
||||
)
|
||||
|
||||
assert self.assert_banner_message(expected_message)
|
||||
@@ -1,215 +0,0 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest, with_execution_thread
|
||||
|
||||
|
||||
# Disable the main/worker dual testing, for two reasons:
|
||||
#
|
||||
# 1. the <py-config> logic happens before we start the worker, so there is
|
||||
# no point in running these tests twice
|
||||
#
|
||||
# 2. the logic to inject execution_thread into <py-config> works only with
|
||||
# plain <py-config> tags, but here we want to test all weird combinations
|
||||
# of config
|
||||
@with_execution_thread(None)
|
||||
class TestConfig(PyScriptTest):
|
||||
def test_py_config_inline_pyscript(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
name = "foobar"
|
||||
</py-config>
|
||||
|
||||
<py-script async>
|
||||
from pyscript import window
|
||||
window.console.log("config name:", window.pyConfig.name)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "config name: foobar"
|
||||
|
||||
@pytest.mark.skip("NEXT: works with <py-script> not with <script>")
|
||||
def test_py_config_inline_scriptpy(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
name = "foobar"
|
||||
</py-config>
|
||||
|
||||
<script type="py" async>
|
||||
from pyscript import window
|
||||
window.console.log("config name:", window.pyConfig.name)
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "config name: foobar"
|
||||
|
||||
@pytest.mark.skip("NEXT: works with <py-script> not with <script>")
|
||||
def test_py_config_external(self):
|
||||
pyconfig_toml = """
|
||||
name = "app with external config"
|
||||
"""
|
||||
self.writefile("pyconfig.toml", pyconfig_toml)
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config src="pyconfig.toml"></py-config>
|
||||
|
||||
<script type="py" async>
|
||||
from pyscript import window
|
||||
window.console.log("config name:", window.pyConfig.name)
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "config name: app with external config"
|
||||
|
||||
def test_invalid_json_config(self):
|
||||
# we need wait_for_pyscript=False because we bail out very soon,
|
||||
# before being able to write 'PyScript page fully initialized'
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config type="json">
|
||||
[[
|
||||
</py-config>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
banner = self.page.wait_for_selector(".py-error")
|
||||
# assert "Unexpected end of JSON input" in self.console.error.text
|
||||
expected = "(PY1000): Invalid JSON\n" "Unexpected end of JSON input"
|
||||
assert banner.inner_text() == expected
|
||||
|
||||
def test_invalid_toml_config(self):
|
||||
# we need wait_for_pyscript=False because we bail out very soon,
|
||||
# before being able to write 'PyScript page fully initialized'
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
[[
|
||||
</py-config>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
banner = self.page.wait_for_selector(".py-error")
|
||||
# assert "Expected DoubleQuote" in self.console.error.text
|
||||
expected = (
|
||||
"(PY1000): Invalid TOML\n"
|
||||
"Expected DoubleQuote, Whitespace, or [a-z], [A-Z], "
|
||||
'[0-9], "-", "_" but end of input found.'
|
||||
)
|
||||
assert banner.inner_text() == expected
|
||||
|
||||
def test_ambiguous_py_config(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>name = "first"</py-config>
|
||||
|
||||
<script type="py" config="second.toml"></script>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
banner = self.page.wait_for_selector(".py-error")
|
||||
expected = "(PY0409): Ambiguous py-config VS config attribute"
|
||||
assert banner.text_content() == expected
|
||||
|
||||
def test_multiple_attributes_py_config(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" config="first.toml"></script>
|
||||
<script type="py" config="second.toml"></script>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
banner = self.page.wait_for_selector(".py-error")
|
||||
expected = "(PY0409): Unable to use different configs on main"
|
||||
assert banner.text_content() == expected
|
||||
|
||||
def test_multiple_py_config(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
name = "foobar"
|
||||
</py-config>
|
||||
|
||||
<py-config>
|
||||
name = "this is ignored"
|
||||
</py-config>
|
||||
|
||||
<script type="py">
|
||||
import js
|
||||
#config = js.pyscript_get_config()
|
||||
#js.console.log("config name:", config.name)
|
||||
</script>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
banner = self.page.wait_for_selector(".py-error")
|
||||
expected = "(PY0409): Too many py-config"
|
||||
assert banner.text_content() == expected
|
||||
|
||||
def test_paths(self):
|
||||
self.writefile("a.py", "x = 'hello from A'")
|
||||
self.writefile("b.py", "x = 'hello from B'")
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
[[fetch]]
|
||||
files = ["./a.py", "./b.py"]
|
||||
</py-config>
|
||||
|
||||
<script type="py">
|
||||
import js
|
||||
import a, b
|
||||
js.console.log(a.x)
|
||||
js.console.log(b.x)
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-2:] == [
|
||||
"hello from A",
|
||||
"hello from B",
|
||||
]
|
||||
|
||||
@pytest.mark.skip("NEXT: emit an error if fetch fails")
|
||||
def test_paths_that_do_not_exist(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
[[fetch]]
|
||||
files = ["./f.py"]
|
||||
</py-config>
|
||||
|
||||
<script type="py">
|
||||
print("this should not be printed")
|
||||
</script>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
|
||||
expected = "(PY0404): Fetching from URL ./f.py failed with " "error 404"
|
||||
inner_html = self.page.locator(".py-error").inner_html()
|
||||
assert expected in inner_html
|
||||
assert expected in self.console.error.lines[-1]
|
||||
assert self.console.log.lines == []
|
||||
|
||||
def test_paths_from_packages(self):
|
||||
self.writefile("utils/__init__.py", "")
|
||||
self.writefile("utils/a.py", "x = 'hello from A'")
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
[[fetch]]
|
||||
from = "utils"
|
||||
to_folder = "pkg"
|
||||
files = ["__init__.py", "a.py"]
|
||||
</py-config>
|
||||
|
||||
<script type="py">
|
||||
import js
|
||||
from pkg.a import x
|
||||
js.console.log(x)
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "hello from A"
|
||||
@@ -1,663 +0,0 @@
|
||||
import platform
|
||||
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest, skip_worker
|
||||
|
||||
pytest.skip(
|
||||
reason="NEXT: pyscript NEXT doesn't support the REPL yet",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
|
||||
class TestPyRepl(PyScriptTest):
|
||||
def _replace(self, py_repl, newcode):
|
||||
"""
|
||||
Clear the editor and write new code in it.
|
||||
WARNING: this assumes that the textbox has already the focus
|
||||
"""
|
||||
# clear the editor, write new code
|
||||
if "macOS" in platform.platform():
|
||||
self.page.keyboard.press("Meta+A")
|
||||
else:
|
||||
self.page.keyboard.press("Control+A")
|
||||
|
||||
self.page.keyboard.press("Backspace")
|
||||
self.page.keyboard.type(newcode)
|
||||
|
||||
def test_repl_loads(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl></py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.query_selector("py-repl .py-repl-box")
|
||||
assert py_repl
|
||||
|
||||
def test_execute_preloaded_source(self):
|
||||
"""
|
||||
Unfortunately it tests two things at once, but it's impossible to write a
|
||||
smaller test. I think this is the most basic test that we can write.
|
||||
|
||||
We test that:
|
||||
1. the source code that we put in the tag is loaded inside the editor
|
||||
2. clicking the button executes it
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl>
|
||||
print('hello from py-repl')
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
src = py_repl.locator("div.cm-content").inner_text()
|
||||
assert "print('hello from py-repl')" in src
|
||||
py_repl.locator("button").click()
|
||||
self.page.wait_for_selector("py-terminal")
|
||||
assert self.console.log.lines[-1] == "hello from py-repl"
|
||||
|
||||
def test_execute_code_typed_by_the_user(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl></py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.type('print("hello")')
|
||||
py_repl.locator("button").click()
|
||||
self.page.wait_for_selector("py-terminal")
|
||||
assert self.console.log.lines[-1] == "hello"
|
||||
|
||||
def test_execute_on_shift_enter(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl>
|
||||
print("hello world")
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
self.page.wait_for_selector("py-repl .py-repl-run-button")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
self.page.wait_for_selector("py-terminal")
|
||||
|
||||
assert self.console.log.lines[-1] == "hello world"
|
||||
|
||||
# Shift-enter should not add a newline to the editor
|
||||
assert self.page.locator(".cm-line").count() == 1
|
||||
|
||||
@skip_worker("FIXME: display()")
|
||||
def test_display(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl>
|
||||
display('hello world')
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert out_div.inner_text() == "hello world"
|
||||
|
||||
@skip_worker("TIMEOUT")
|
||||
def test_show_last_expression(self):
|
||||
"""
|
||||
Test that we display() the value of the last expression, as you would
|
||||
expect by a REPL
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl>
|
||||
42
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert out_div.inner_text() == "42"
|
||||
|
||||
@skip_worker("TIMEOUT")
|
||||
def test_show_last_expression_with_output(self):
|
||||
"""
|
||||
Test that we display() the value of the last expression, as you would
|
||||
expect by a REPL
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="repl-target"></div>
|
||||
<py-repl output="repl-target">
|
||||
42
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
out_div = py_repl.locator("div.py-repl-output")
|
||||
assert out_div.all_inner_texts()[0] == ""
|
||||
|
||||
out_div = self.page.wait_for_selector("#repl-target")
|
||||
assert out_div.inner_text() == "42"
|
||||
|
||||
@skip_worker("FIXME: display()")
|
||||
def test_run_clears_previous_output(self):
|
||||
"""
|
||||
Check that we clear the previous output of the cell before executing it
|
||||
again
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl>
|
||||
display('hello world')
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert out_div.inner_text() == "hello world"
|
||||
# clear the editor, write new code, execute
|
||||
self._replace(py_repl, "display('another output')")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
# test runner can be too fast, the line below should wait for output to change
|
||||
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert out_div.inner_text() == "another output"
|
||||
|
||||
def test_python_exception(self):
|
||||
"""
|
||||
See also test01_basic::test_python_exception, since it's very similar
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl>
|
||||
raise Exception('this is an error')
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
self.page.wait_for_selector(".py-error")
|
||||
#
|
||||
# check that we sent the traceback to the console
|
||||
tb_lines = self.console.error.lines[-1].splitlines()
|
||||
assert tb_lines[0] == "[pyexec] Python exception:"
|
||||
assert tb_lines[1] == "Traceback (most recent call last):"
|
||||
assert tb_lines[-1] == "Exception: this is an error"
|
||||
#
|
||||
# check that we show the traceback in the page
|
||||
err_pre = py_repl.locator("div.py-repl-output > pre.py-error")
|
||||
tb_lines = err_pre.inner_text().splitlines()
|
||||
assert tb_lines[0] == "Traceback (most recent call last):"
|
||||
assert tb_lines[-1] == "Exception: this is an error"
|
||||
#
|
||||
self.check_py_errors("this is an error")
|
||||
|
||||
@skip_worker("FIXME: display()")
|
||||
def test_multiple_repls(self):
|
||||
"""
|
||||
Multiple repls showing in the correct order in the page
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl data-testid=="first"> display("first") </py-repl>
|
||||
<py-repl data-testid=="second"> display("second") </py-repl>
|
||||
"""
|
||||
)
|
||||
first_py_repl = self.page.get_by_text("first")
|
||||
first_py_repl.click()
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert self.page.inner_text("#py-internal-0-repl-output") == "first"
|
||||
|
||||
second_py_repl = self.page.get_by_text("second")
|
||||
second_py_repl.click()
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
self.page.wait_for_selector("#py-internal-1-repl-output")
|
||||
assert self.page.inner_text("#py-internal-1-repl-output") == "second"
|
||||
|
||||
@skip_worker("FIXME: display()")
|
||||
def test_python_exception_after_previous_output(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl>
|
||||
display('hello world')
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert out_div.inner_text() == "hello world"
|
||||
#
|
||||
# clear the editor, write new code, execute
|
||||
self._replace(py_repl, "0/0")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
# test runner can be too fast, the line below should wait for output to change
|
||||
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert "hello world" not in out_div.inner_text()
|
||||
assert "ZeroDivisionError" in out_div.inner_text()
|
||||
#
|
||||
self.check_py_errors("ZeroDivisionError")
|
||||
|
||||
@skip_worker("FIXME: js.document")
|
||||
def test_hide_previous_error_after_successful_run(self):
|
||||
"""
|
||||
this tests the fact that a new error div should be created once there's an
|
||||
error but also that it should disappear automatically once the error
|
||||
is fixed
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl>
|
||||
raise Exception('this is an error')
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert "this is an error" in out_div.inner_text()
|
||||
#
|
||||
self._replace(py_repl, "display('hello')")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
# test runner can be too fast, the line below should wait for output to change
|
||||
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert out_div.inner_text() == "hello"
|
||||
#
|
||||
self.check_py_errors("this is an error")
|
||||
|
||||
def test_output_attribute_does_not_exist(self):
|
||||
"""
|
||||
If we try to use an attribute which doesn't exist, we display an error
|
||||
instead
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl output="I-dont-exist">
|
||||
print('I will not be executed')
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
|
||||
banner = self.page.wait_for_selector(".py-warning")
|
||||
|
||||
banner_content = banner.inner_text()
|
||||
expected = (
|
||||
'output = "I-dont-exist" does not match the id of any element on the page.'
|
||||
)
|
||||
assert banner_content == expected
|
||||
|
||||
@skip_worker("TIMEOUT")
|
||||
def test_auto_generate(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl auto-generate="true">
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repls = self.page.locator("py-repl")
|
||||
outputs = py_repls.locator("div.py-repl-output")
|
||||
assert py_repls.count() == 1
|
||||
assert outputs.count() == 1
|
||||
#
|
||||
# evaluate the py-repl, and wait for the newly generated one
|
||||
self.page.keyboard.type("'hello'")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
self.page.locator('py-repl[exec-id="1"]').wait_for()
|
||||
assert py_repls.count() == 2
|
||||
assert outputs.count() == 2
|
||||
#
|
||||
# now we type something else: the new py-repl should have the focus
|
||||
self.page.keyboard.type("'world'")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
self.page.locator('py-repl[exec-id="2"]').wait_for()
|
||||
assert py_repls.count() == 3
|
||||
assert outputs.count() == 3
|
||||
#
|
||||
# check that the code and the outputs are in order
|
||||
out_texts = [el.inner_text() for el in self.iter_locator(outputs)]
|
||||
assert out_texts == ["hello", "world", ""]
|
||||
|
||||
@skip_worker("FIXME: display()")
|
||||
def test_multiple_repls_mixed_display_order(self):
|
||||
"""
|
||||
Displaying several outputs that don't obey the order in which the original
|
||||
repl displays were created using the auto_generate attr
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl auto-generate="true" data-testid=="first"> display("root first") </py-repl>
|
||||
<py-repl auto-generate="true" data-testid=="second"> display("root second") </py-repl>
|
||||
"""
|
||||
)
|
||||
|
||||
second_py_repl = self.page.get_by_text("root second")
|
||||
second_py_repl.click()
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
self.page.wait_for_selector("#py-internal-1-repl-output")
|
||||
self.page.keyboard.type("display('second children')")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
self.page.wait_for_selector("#py-internal-1-1-repl-output")
|
||||
|
||||
first_py_repl = self.page.get_by_text("root first")
|
||||
first_py_repl.click()
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
self.page.keyboard.type("display('first children')")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
self.page.wait_for_selector("#py-internal-0-1-repl-output")
|
||||
|
||||
assert self.page.inner_text("#py-internal-1-1-repl-output") == "second children"
|
||||
assert self.page.inner_text("#py-internal-0-1-repl-output") == "first children"
|
||||
|
||||
@skip_worker("FIXME: display()")
|
||||
def test_repl_output_attribute(self):
|
||||
# Test that output attribute sends stdout to the element
|
||||
# with the given ID, but not display()
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="repl-target"></div>
|
||||
<py-repl output="repl-target">
|
||||
print('print from py-repl')
|
||||
display('display from py-repl')
|
||||
</py-repl>
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
|
||||
target = self.page.wait_for_selector("#repl-target")
|
||||
assert "print from py-repl" in target.inner_text()
|
||||
|
||||
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert out_div.inner_text() == "display from py-repl"
|
||||
|
||||
self.assert_no_banners()
|
||||
|
||||
@skip_worker("FIXME: js.document")
|
||||
def test_repl_output_display_async(self):
|
||||
# py-repls running async code are not expected to
|
||||
# send display to element element
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="repl-target"></div>
|
||||
<script type="py">
|
||||
import asyncio
|
||||
import js
|
||||
|
||||
async def print_it():
|
||||
await asyncio.sleep(1)
|
||||
print('print from py-repl')
|
||||
|
||||
|
||||
async def display_it():
|
||||
display('display from py-repl')
|
||||
await asyncio.sleep(2)
|
||||
|
||||
async def done():
|
||||
await asyncio.sleep(3)
|
||||
js.console.log("DONE")
|
||||
</script>
|
||||
|
||||
<py-repl output="repl-target">
|
||||
asyncio.ensure_future(print_it());
|
||||
asyncio.ensure_future(display_it());
|
||||
asyncio.ensure_future(done());
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
|
||||
self.wait_for_console("DONE")
|
||||
|
||||
assert self.page.locator("#repl-target").text_content() == ""
|
||||
self.assert_no_banners()
|
||||
|
||||
@skip_worker("FIXME: js.document")
|
||||
def test_repl_stdio_dynamic_tags(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="first"></div>
|
||||
<div id="second"></div>
|
||||
<py-repl output="first">
|
||||
import js
|
||||
|
||||
print("first.")
|
||||
|
||||
# Using string, since no clean way to write to the
|
||||
# code contents of the CodeMirror in a PyRepl
|
||||
newTag = '<py-repl id="second-repl" output="second">print("second.")</py-repl>'
|
||||
js.document.body.innerHTML += newTag
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
|
||||
assert self.page.wait_for_selector("#first").inner_text() == "first.\n"
|
||||
|
||||
second_repl = self.page.locator("py-repl#second-repl")
|
||||
second_repl.locator("button").click()
|
||||
assert self.page.wait_for_selector("#second").inner_text() == "second.\n"
|
||||
|
||||
def test_repl_output_id_errors(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl output="not-on-page">
|
||||
print("bad.")
|
||||
print("bad.")
|
||||
</py-repl>
|
||||
|
||||
<py-repl output="not-on-page">
|
||||
print("bad.")
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repls = self.page.query_selector_all("py-repl")
|
||||
for repl in py_repls:
|
||||
repl.query_selector_all("button")[0].click()
|
||||
|
||||
banner = self.page.wait_for_selector(".py-warning")
|
||||
|
||||
banner_content = banner.inner_text()
|
||||
expected = (
|
||||
'output = "not-on-page" does not match the id of any element on the page.'
|
||||
)
|
||||
|
||||
assert banner_content == expected
|
||||
|
||||
def test_repl_stderr_id_errors(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl stderr="not-on-page">
|
||||
import sys
|
||||
print("bad.", file=sys.stderr)
|
||||
print("bad.", file=sys.stderr)
|
||||
</py-repl>
|
||||
|
||||
<py-repl stderr="not-on-page">
|
||||
print("bad.", file=sys.stderr)
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repls = self.page.query_selector_all("py-repl")
|
||||
for repl in py_repls:
|
||||
repl.query_selector_all("button")[0].click()
|
||||
|
||||
banner = self.page.wait_for_selector(".py-warning")
|
||||
|
||||
banner_content = banner.inner_text()
|
||||
expected = (
|
||||
'stderr = "not-on-page" does not match the id of any element on the page.'
|
||||
)
|
||||
|
||||
assert banner_content == expected
|
||||
|
||||
def test_repl_output_stderr(self):
|
||||
# Test that stderr works, and routes to the same location as stdout
|
||||
# Also, repls with the stderr attribute route to an additional location
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="stdout-div"></div>
|
||||
<div id="stderr-div"></div>
|
||||
<py-repl output="stdout-div" stderr="stderr-div">
|
||||
import sys
|
||||
print("one.", file=sys.stderr)
|
||||
print("two.")
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
|
||||
assert self.page.wait_for_selector("#stdout-div").inner_text() == "one.\ntwo.\n"
|
||||
assert self.page.wait_for_selector("#stderr-div").inner_text() == "one.\n"
|
||||
self.assert_no_banners()
|
||||
|
||||
@skip_worker("TIMEOUT")
|
||||
def test_repl_output_attribute_change(self):
|
||||
# If the user changes the 'output' attribute of a <py-repl> tag mid-execution,
|
||||
# Output should no longer go to the selected div and a warning should appear
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="first"></div>
|
||||
<div id="second"></div>
|
||||
<!-- There is no tag with id "third" -->
|
||||
<py-repl id="repl-tag" output="first">
|
||||
print("one.")
|
||||
|
||||
# Change the 'output' attribute of this tag
|
||||
import js
|
||||
this_tag = js.document.getElementById("repl-tag")
|
||||
|
||||
this_tag.setAttribute("output", "second")
|
||||
print("two.")
|
||||
|
||||
this_tag.setAttribute("output", "third")
|
||||
print("three.")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
|
||||
assert self.page.wait_for_selector("#first").inner_text() == "one.\n"
|
||||
assert self.page.wait_for_selector("#second").inner_text() == "two.\n"
|
||||
|
||||
expected_alert_banner_msg = (
|
||||
'output = "third" does not match the id of any element on the page.'
|
||||
)
|
||||
|
||||
alert_banner = self.page.wait_for_selector(".alert-banner")
|
||||
assert expected_alert_banner_msg in alert_banner.inner_text()
|
||||
|
||||
@skip_worker("TIMEOUT")
|
||||
def test_repl_output_element_id_change(self):
|
||||
# If the user changes the ID of the targeted DOM element mid-execution,
|
||||
# Output should no longer go to the selected element and a warning should appear
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="first"></div>
|
||||
<div id="second"></div>
|
||||
<!-- There is no tag with id "third" -->
|
||||
<py-repl id="pyscript-tag" output="first">
|
||||
print("one.")
|
||||
|
||||
# Change the ID of the targeted DIV to something else
|
||||
import js
|
||||
target_tag = js.document.getElementById("first")
|
||||
|
||||
# should fail and show banner
|
||||
target_tag.setAttribute("id", "second")
|
||||
print("two.")
|
||||
|
||||
# But changing both the 'output' attribute and the id of the target
|
||||
# should work
|
||||
target_tag.setAttribute("id", "third")
|
||||
js.document.getElementById("pyscript-tag").setAttribute("output", "third")
|
||||
print("three.")
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
|
||||
# Note the ID of the div has changed by the time of this assert
|
||||
assert self.page.wait_for_selector("#third").inner_text() == "one.\nthree.\n"
|
||||
|
||||
expected_alert_banner_msg = (
|
||||
'output = "first" does not match the id of any element on the page.'
|
||||
)
|
||||
alert_banner = self.page.wait_for_selector(".alert-banner")
|
||||
assert expected_alert_banner_msg in alert_banner.inner_text()
|
||||
|
||||
def test_repl_load_content_from_src(self):
|
||||
self.writefile("loadReplSrc1.py", "print('1')")
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl id="py-repl1" output="replOutput1" src="./loadReplSrc1.py"></py-repl>
|
||||
<div id="replOutput1"></div>
|
||||
"""
|
||||
)
|
||||
successMsg = "[py-repl] loading code from ./loadReplSrc1.py to repl...success"
|
||||
assert self.console.info.lines[-1] == successMsg
|
||||
|
||||
py_repl = self.page.locator("py-repl")
|
||||
code = py_repl.locator("div.cm-content").inner_text()
|
||||
assert "print('1')" in code
|
||||
|
||||
@skip_worker("TIMEOUT")
|
||||
def test_repl_src_change(self):
|
||||
self.writefile("loadReplSrc2.py", "2")
|
||||
self.writefile("loadReplSrc3.py", "print('3')")
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl id="py-repl2" output="replOutput2" src="./loadReplSrc2.py"></py-repl>
|
||||
<div id="replOutput2"></div>
|
||||
|
||||
<py-repl id="py-repl3" output="replOutput3">
|
||||
import js
|
||||
target_tag = js.document.getElementById("py-repl2")
|
||||
target_tag.setAttribute("src", "./loadReplSrc3.py")
|
||||
</py-repl>
|
||||
<div id="replOutput3"></div>
|
||||
"""
|
||||
)
|
||||
|
||||
successMsg1 = "[py-repl] loading code from ./loadReplSrc2.py to repl...success"
|
||||
assert self.console.info.lines[-1] == successMsg1
|
||||
|
||||
py_repl3 = self.page.locator("py-repl#py-repl3")
|
||||
py_repl3.locator("button").click()
|
||||
py_repl2 = self.page.locator("py-repl#py-repl2")
|
||||
py_repl2.locator("button").click()
|
||||
self.page.wait_for_selector("py-terminal")
|
||||
assert self.console.log.lines[-1] == "3"
|
||||
|
||||
successMsg2 = "[py-repl] loading code from ./loadReplSrc3.py to repl...success"
|
||||
assert self.console.info.lines[-1] == successMsg2
|
||||
|
||||
def test_repl_src_path_that_do_not_exist(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl id="py-repl4" output="replOutput4" src="./loadReplSrc4.py"></py-repl>
|
||||
<div id="replOutput4"></div>
|
||||
"""
|
||||
)
|
||||
errorMsg = (
|
||||
"(PY0404): Fetching from URL ./loadReplSrc4.py "
|
||||
"failed with error 404 (Not Found). "
|
||||
"Are your filename and path correct?"
|
||||
)
|
||||
assert self.console.error.lines[-1] == errorMsg
|
||||
@@ -1,188 +0,0 @@
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from .support import PageErrors, PyScriptTest, only_worker, skip_worker
|
||||
|
||||
|
||||
class TestPyTerminal(PyScriptTest):
|
||||
@skip_worker("We do support multiple worker terminal now")
|
||||
def test_multiple_terminals(self):
|
||||
"""
|
||||
Multiple terminals are not currently supported
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" terminal></script>
|
||||
<script type="py" terminal></script>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
check_js_errors=False,
|
||||
)
|
||||
assert self.assert_banner_message("You can use at most 1 main terminal")
|
||||
|
||||
with pytest.raises(PageErrors, match="You can use at most 1 main terminal"):
|
||||
self.check_js_errors()
|
||||
|
||||
# TODO: interactive shell still unclear
|
||||
# @only_worker
|
||||
# def test_py_terminal_input(self):
|
||||
# """
|
||||
# Only worker py-terminal accepts an input
|
||||
# """
|
||||
# self.pyscript_run(
|
||||
# """
|
||||
# <script type="py" terminal></script>
|
||||
# """,
|
||||
# wait_for_pyscript=False,
|
||||
# )
|
||||
# self.page.get_by_text(">>> ", exact=True).wait_for()
|
||||
# self.page.keyboard.type("'the answer is ' + str(6 * 7)")
|
||||
# self.page.keyboard.press("Enter")
|
||||
# self.page.get_by_text("the answer is 42").wait_for()
|
||||
|
||||
@only_worker
|
||||
def test_py_terminal_os_write(self):
|
||||
"""
|
||||
An `os.write("text")` should land in the terminal
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" terminal>
|
||||
import os
|
||||
os.write(1, str.encode("hello\\n"))
|
||||
os.write(2, str.encode("world\\n"))
|
||||
</script>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
self.page.get_by_text("hello\n").wait_for()
|
||||
self.page.get_by_text("world\n").wait_for()
|
||||
|
||||
def test_py_terminal(self):
|
||||
"""
|
||||
1. <py-terminal> should redirect stdout and stderr to the DOM
|
||||
|
||||
2. they also go to the console as usual
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" terminal>
|
||||
import sys
|
||||
print('hello world')
|
||||
print('this goes to stderr', file=sys.stderr)
|
||||
print('this goes to stdout')
|
||||
</script>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
self.page.get_by_text("hello world").wait_for()
|
||||
term = self.page.locator("py-terminal")
|
||||
term_lines = term.inner_text().splitlines()
|
||||
assert term_lines[0:3] == [
|
||||
"hello world",
|
||||
"this goes to stderr",
|
||||
"this goes to stdout",
|
||||
]
|
||||
|
||||
@skip_worker(
|
||||
"Workers don't have events + two different workers don't share the same I/O"
|
||||
)
|
||||
def test_button_action(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
def greetings(event):
|
||||
print('hello world')
|
||||
</script>
|
||||
<script type="py" terminal></script>
|
||||
|
||||
<button id="my-button" py-click="greetings">Click me</button>
|
||||
"""
|
||||
)
|
||||
term = self.page.locator("py-terminal")
|
||||
self.page.locator("button").click()
|
||||
last_line = self.page.get_by_text("hello world")
|
||||
last_line.wait_for()
|
||||
assert term.inner_text().rstrip() == "hello world"
|
||||
|
||||
def test_xterm_function(self):
|
||||
"""Test a few basic behaviors of the xtermjs terminal.
|
||||
|
||||
This test isn't meant to capture all of the behaviors of an xtermjs terminal;
|
||||
rather, it confirms with a few basic formatting sequences that (1) the xtermjs
|
||||
terminal is functioning/loaded correctly and (2) that output toward that terminal
|
||||
isn't being escaped in a way that prevents it reacting to escape sequences. The
|
||||
main goal is preventing regressions.
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" terminal>
|
||||
print("\x1b[33mYellow\x1b[0m")
|
||||
print("\x1b[4mUnderline\x1b[24m")
|
||||
print("\x1b[1mBold\x1b[22m")
|
||||
print("\x1b[3mItalic\x1b[23m")
|
||||
print("done")
|
||||
</script>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
|
||||
# Wait for "done" to actually appear in the xterm; may be delayed,
|
||||
# since xtermjs processes its input buffer in chunks
|
||||
last_line = self.page.get_by_text("done")
|
||||
last_line.wait_for()
|
||||
|
||||
# Yes, this is not ideal. However, per http://xtermjs.org/docs/guides/hooks/
|
||||
# "It is not possible to conclude, whether or when a certain chunk of data
|
||||
# will finally appear on the screen," which is what we'd really like to know.
|
||||
# By waiting for the "done" test to appear above, we get close, however it is
|
||||
# possible for the text to appear and not be 'processed' (i.e.) formatted. This
|
||||
# small delay should avoid that.
|
||||
time.sleep(1)
|
||||
|
||||
rows = self.page.locator(".xterm-rows")
|
||||
|
||||
# The following use locator.evaluate() and getComputedStyle to get
|
||||
# the computed CSS values; this tests that the lines are rendering
|
||||
# properly in a better way than just testing whether they
|
||||
# get the right css classes from xtermjs
|
||||
|
||||
# First line should be yellow
|
||||
first_line = rows.locator("div").nth(0)
|
||||
first_char = first_line.locator("span").nth(0)
|
||||
color = first_char.evaluate(
|
||||
"(element) => getComputedStyle(element).getPropertyValue('color')"
|
||||
)
|
||||
assert color == "rgb(196, 160, 0)"
|
||||
|
||||
# Second line should be underlined
|
||||
second_line = rows.locator("div").nth(1)
|
||||
first_char = second_line.locator("span").nth(0)
|
||||
text_decoration = first_char.evaluate(
|
||||
"(element) => getComputedStyle(element).getPropertyValue('text-decoration')"
|
||||
)
|
||||
assert "underline" in text_decoration
|
||||
|
||||
# We'll make sure the 'bold' font weight is more than the
|
||||
# default font weight without specifying a specific value
|
||||
baseline_font_weight = first_char.evaluate(
|
||||
"(element) => getComputedStyle(element).getPropertyValue('font-weight')"
|
||||
)
|
||||
|
||||
# Third line should be bold
|
||||
third_line = rows.locator("div").nth(2)
|
||||
first_char = third_line.locator("span").nth(0)
|
||||
font_weight = first_char.evaluate(
|
||||
"(element) => getComputedStyle(element).getPropertyValue('font-weight')"
|
||||
)
|
||||
assert int(font_weight) > int(baseline_font_weight)
|
||||
|
||||
# Fourth line should be italic
|
||||
fourth_line = rows.locator("div").nth(3)
|
||||
first_char = fourth_line.locator("span").nth(0)
|
||||
font_style = first_char.evaluate(
|
||||
"(element) => getComputedStyle(element).getPropertyValue('font-style')"
|
||||
)
|
||||
assert font_style == "italic"
|
||||
@@ -1,804 +0,0 @@
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest, only_main, skip_worker
|
||||
|
||||
DEFAULT_ELEMENT_ATTRIBUTES = {
|
||||
"accesskey": "s",
|
||||
"autocapitalize": "off",
|
||||
"autofocus": True,
|
||||
"contenteditable": True,
|
||||
"draggable": True,
|
||||
"enterkeyhint": "go",
|
||||
"hidden": False,
|
||||
"id": "whateverid",
|
||||
"lang": "br",
|
||||
"nonce": "123",
|
||||
"part": "part1:exposed1",
|
||||
"popover": True,
|
||||
"slot": "slot1",
|
||||
"spellcheck": False,
|
||||
"tabindex": 3,
|
||||
"title": "whatevertitle",
|
||||
"translate": "no",
|
||||
"virtualkeyboardpolicy": "manual",
|
||||
}
|
||||
|
||||
INTERPRETERS = ["py", "mpy"]
|
||||
|
||||
|
||||
@pytest.fixture(params=INTERPRETERS)
|
||||
def interpreter(request):
|
||||
return request.param
|
||||
|
||||
|
||||
class TestElements(PyScriptTest):
|
||||
"""Test all elements in the pyweb.ui.elements module.
|
||||
|
||||
This class tests all elements in the pyweb.ui.elements module. It creates
|
||||
an element of each type, both executing in the main thread and in a worker.
|
||||
It runs each test for each interpreter defined in `INTERPRETERS`
|
||||
|
||||
Each individual element test looks for the element properties, sets a value
|
||||
on each the supported properties and checks if the element was created correctly
|
||||
and all it's properties were set correctly.
|
||||
"""
|
||||
|
||||
@property
|
||||
def expected_missing_file_errors(self):
|
||||
# In fake server conditions this test will not throw an error due to missing files.
|
||||
# If we want to skip the test, use:
|
||||
# pytest.skip("Skipping: fake server doesn't throw 404 errors on missing local files.")
|
||||
return (
|
||||
[
|
||||
"Failed to load resource: the server responded with a status of 404 (File not found)"
|
||||
]
|
||||
if self.dev_server
|
||||
else []
|
||||
)
|
||||
|
||||
def _create_el_and_basic_asserts(
|
||||
self,
|
||||
el_type,
|
||||
el_text=None,
|
||||
interpreter="py",
|
||||
properties=None,
|
||||
expected_errors=None,
|
||||
additional_selector_rules=None,
|
||||
):
|
||||
"""Create an element with all its properties set, by running <script type=<interpreter> ... >
|
||||
, and check if the element was created correctly and all its properties were set correctly.
|
||||
"""
|
||||
expected_errors = expected_errors or []
|
||||
if not properties:
|
||||
properties = {}
|
||||
|
||||
def parse_value(v):
|
||||
if isinstance(v, bool):
|
||||
return str(v)
|
||||
|
||||
return f"'{v}'"
|
||||
|
||||
attributes = ""
|
||||
if el_text:
|
||||
attributes += f'"{el_text}",'
|
||||
|
||||
if properties:
|
||||
attributes += ", ".join(
|
||||
[f"{k}={parse_value(v)}" for k, v in properties.items()]
|
||||
)
|
||||
|
||||
# Let's make sure the body of the page is clean first
|
||||
body = self.page.locator("body")
|
||||
assert body.inner_html() == ""
|
||||
|
||||
# Let's make sure the element is not in the page
|
||||
element = self.page.locator(el_type)
|
||||
assert not element.count()
|
||||
|
||||
# Let's create the element
|
||||
code_ = f"""
|
||||
from pyscript import when
|
||||
<script type="{interpreter}">
|
||||
from pyscript.web import page, {el_type}
|
||||
el = {el_type}({attributes})
|
||||
page.body.append(el)
|
||||
</script>
|
||||
"""
|
||||
self.pyscript_run(code_)
|
||||
|
||||
# Let's keep the tag in 2 variables, one for the selector and another to
|
||||
# check the return tag from the selector
|
||||
locator_type = el_tag = el_type[:-1] if el_type.endswith("_") else el_type
|
||||
if additional_selector_rules:
|
||||
locator_type += f"{additional_selector_rules}"
|
||||
|
||||
el = self.page.locator(locator_type)
|
||||
tag = el.evaluate("node => node.tagName")
|
||||
assert tag == el_tag.upper()
|
||||
if el_text:
|
||||
assert el.inner_html() == el_text
|
||||
assert el.text_content() == el_text
|
||||
|
||||
# if we expect specific errors, check that they are in the console
|
||||
if expected_errors:
|
||||
for error in expected_errors:
|
||||
assert error in self.console.error.lines
|
||||
else:
|
||||
# if we don't expect errors, check that there are no errors
|
||||
assert self.console.error.lines == []
|
||||
|
||||
if properties:
|
||||
for k, v in properties.items():
|
||||
actual_val = el.evaluate(f"node => node.{k}")
|
||||
assert actual_val == v
|
||||
return el
|
||||
|
||||
def test_a(self, interpreter):
|
||||
a = self._create_el_and_basic_asserts("a", "click me", interpreter)
|
||||
assert a.text_content() == "click me"
|
||||
|
||||
def test_abbr(self, interpreter):
|
||||
abbr = self._create_el_and_basic_asserts(
|
||||
"abbr", "some text", interpreter=interpreter
|
||||
)
|
||||
assert abbr.text_content() == "some text"
|
||||
|
||||
def test_address(self, interpreter):
|
||||
address = self._create_el_and_basic_asserts("address", "some text", interpreter)
|
||||
assert address.text_content() == "some text"
|
||||
|
||||
def test_area(self, interpreter):
|
||||
properties = {
|
||||
"shape": "poly",
|
||||
"coords": "129,0,260,95,129,138",
|
||||
"href": "https://developer.mozilla.org/docs/Web/HTTP",
|
||||
"target": "_blank",
|
||||
"alt": "HTTP",
|
||||
}
|
||||
# TODO: Check why click times out
|
||||
self._create_el_and_basic_asserts(
|
||||
"area", interpreter=interpreter, properties=properties
|
||||
)
|
||||
|
||||
def test_article(self, interpreter):
|
||||
self._create_el_and_basic_asserts("article", "some text", interpreter)
|
||||
|
||||
def test_aside(self, interpreter):
|
||||
self._create_el_and_basic_asserts("aside", "some text", interpreter)
|
||||
|
||||
def test_audio(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"audio",
|
||||
interpreter=interpreter,
|
||||
properties={"src": "http://localhost:8080/somefile.ogg", "controls": True},
|
||||
expected_errors=self.expected_missing_file_errors,
|
||||
)
|
||||
|
||||
def test_b(self, interpreter):
|
||||
self._create_el_and_basic_asserts("b", "some text", interpreter)
|
||||
|
||||
def test_blockquote(self, interpreter):
|
||||
self._create_el_and_basic_asserts("blockquote", "some text", interpreter)
|
||||
|
||||
def test_br(self, interpreter):
|
||||
self._create_el_and_basic_asserts("br", interpreter=interpreter)
|
||||
|
||||
def test_element_button(self, interpreter):
|
||||
button = self._create_el_and_basic_asserts("button", "click me", interpreter)
|
||||
assert button.inner_html() == "click me"
|
||||
|
||||
def test_element_button_attributes(self, interpreter):
|
||||
button = self._create_el_and_basic_asserts(
|
||||
"button", "click me", interpreter, None
|
||||
)
|
||||
assert button.inner_html() == "click me"
|
||||
|
||||
def test_canvas(self, interpreter):
|
||||
properties = {
|
||||
"height": 100,
|
||||
"width": 120,
|
||||
}
|
||||
# TODO: Check why click times out
|
||||
self._create_el_and_basic_asserts(
|
||||
"canvas", "alt text for canvas", interpreter, properties=properties
|
||||
)
|
||||
|
||||
def test_caption(self, interpreter):
|
||||
self._create_el_and_basic_asserts("caption", "some text", interpreter)
|
||||
|
||||
def test_cite(self, interpreter):
|
||||
self._create_el_and_basic_asserts("cite", "some text", interpreter)
|
||||
|
||||
def test_code(self, interpreter):
|
||||
self._create_el_and_basic_asserts("code", "import pyweb", interpreter)
|
||||
|
||||
def test_data(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"data", "some text", interpreter, properties={"value": "123"}
|
||||
)
|
||||
|
||||
def test_datalist(self, interpreter):
|
||||
self._create_el_and_basic_asserts("datalist", "some items", interpreter)
|
||||
|
||||
def test_dd(self, interpreter):
|
||||
self._create_el_and_basic_asserts("dd", "some text", interpreter)
|
||||
|
||||
def test_del_(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"del_", "some text", interpreter, properties={"cite": "http://example.com/"}
|
||||
)
|
||||
|
||||
def test_details(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"details", "some text", interpreter, properties={"open": True}
|
||||
)
|
||||
|
||||
def test_dialog(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"dialog", "some text", interpreter, properties={"open": True}
|
||||
)
|
||||
|
||||
def test_div(self, interpreter):
|
||||
div = self._create_el_and_basic_asserts("div", "click me", interpreter)
|
||||
assert div.inner_html() == "click me"
|
||||
|
||||
def test_dl(self, interpreter):
|
||||
self._create_el_and_basic_asserts("dl", "some text", interpreter)
|
||||
|
||||
def test_dt(self, interpreter):
|
||||
self._create_el_and_basic_asserts("dt", "some text", interpreter)
|
||||
|
||||
def test_em(self, interpreter):
|
||||
self._create_el_and_basic_asserts("em", "some text", interpreter)
|
||||
|
||||
def test_embed(self, interpreter):
|
||||
# NOTE: Types actually matter and embed expects a string for height and width
|
||||
# while other elements expect an int
|
||||
|
||||
# TODO: It's important that we add typing soon to help with the user experience
|
||||
properties = {
|
||||
"src": "http://localhost:8080/somefile.ogg",
|
||||
"type": "video/ogg",
|
||||
"width": "250",
|
||||
"height": "200",
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"embed",
|
||||
interpreter=interpreter,
|
||||
properties=properties,
|
||||
expected_errors=self.expected_missing_file_errors,
|
||||
)
|
||||
|
||||
def test_fieldset(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"fieldset", "some text", interpreter, properties={"name": "some name"}
|
||||
)
|
||||
|
||||
def test_figcaption(self, interpreter):
|
||||
self._create_el_and_basic_asserts("figcaption", "some text", interpreter)
|
||||
|
||||
def test_figure(self, interpreter):
|
||||
self._create_el_and_basic_asserts("figure", "some text", interpreter)
|
||||
|
||||
def test_footer(self, interpreter):
|
||||
self._create_el_and_basic_asserts("footer", "some text", interpreter)
|
||||
|
||||
def test_form(self, interpreter):
|
||||
properties = {
|
||||
"action": "https://example.com/submit",
|
||||
"method": "post",
|
||||
"name": "some name",
|
||||
"autocomplete": "on",
|
||||
"rel": "external",
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"form", "some text", interpreter, properties=properties
|
||||
)
|
||||
|
||||
def test_h1(self, interpreter):
|
||||
self._create_el_and_basic_asserts("h1", "some text", interpreter)
|
||||
|
||||
def test_h2(self, interpreter):
|
||||
self._create_el_and_basic_asserts("h2", "some text", interpreter)
|
||||
|
||||
def test_h3(self, interpreter):
|
||||
self._create_el_and_basic_asserts("h3", "some text", interpreter)
|
||||
|
||||
def test_h4(self, interpreter):
|
||||
self._create_el_and_basic_asserts("h4", "some text", interpreter)
|
||||
|
||||
def test_h5(self, interpreter):
|
||||
self._create_el_and_basic_asserts("h5", "some text", interpreter)
|
||||
|
||||
def test_h6(self, interpreter):
|
||||
self._create_el_and_basic_asserts("h6", "some text", interpreter)
|
||||
|
||||
def test_header(self, interpreter):
|
||||
self._create_el_and_basic_asserts("header", "some text", interpreter)
|
||||
|
||||
def test_hgroup(self, interpreter):
|
||||
self._create_el_and_basic_asserts("hgroup", "some text", interpreter)
|
||||
|
||||
def test_hr(self, interpreter):
|
||||
self._create_el_and_basic_asserts("hr", interpreter=interpreter)
|
||||
|
||||
def test_i(self, interpreter):
|
||||
self._create_el_and_basic_asserts("i", "some text", interpreter)
|
||||
|
||||
def test_iframe(self, interpreter):
|
||||
# TODO: same comment about defining the right types
|
||||
properties = {
|
||||
"src": "http://localhost:8080/somefile.html",
|
||||
"width": "250",
|
||||
"height": "200",
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"iframe",
|
||||
interpreter,
|
||||
properties=properties,
|
||||
expected_errors=self.expected_missing_file_errors,
|
||||
)
|
||||
|
||||
def test_img(self, interpreter):
|
||||
properties = {
|
||||
"src": "http://localhost:8080/somefile.png",
|
||||
"alt": "some image",
|
||||
"width": 250,
|
||||
"height": 200,
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"img",
|
||||
interpreter=interpreter,
|
||||
properties=properties,
|
||||
expected_errors=self.expected_missing_file_errors,
|
||||
)
|
||||
|
||||
def test_input(self, interpreter):
|
||||
# TODO: we need multiple input tests
|
||||
properties = {
|
||||
"type": "text",
|
||||
"value": "some value",
|
||||
"name": "some name",
|
||||
"autofocus": True,
|
||||
"pattern": "[A-Za-z]{3}",
|
||||
"placeholder": "some placeholder",
|
||||
"required": True,
|
||||
"size": 20,
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"input_", interpreter=interpreter, properties=properties
|
||||
)
|
||||
|
||||
def test_ins(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"ins", "some text", interpreter, properties={"cite": "http://example.com/"}
|
||||
)
|
||||
|
||||
def test_kbd(self, interpreter):
|
||||
self._create_el_and_basic_asserts("kbd", "some text", interpreter)
|
||||
|
||||
def test_label(self, interpreter):
|
||||
self._create_el_and_basic_asserts("label", "some text", interpreter)
|
||||
|
||||
def test_legend(self, interpreter):
|
||||
self._create_el_and_basic_asserts("legend", "some text", interpreter)
|
||||
|
||||
def test_li(self, interpreter):
|
||||
self._create_el_and_basic_asserts("li", "some text", interpreter)
|
||||
|
||||
def test_link(self, interpreter):
|
||||
properties = {
|
||||
"href": "http://localhost:8080/somefile.css",
|
||||
"rel": "stylesheet",
|
||||
"type": "text/css",
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"link",
|
||||
interpreter=interpreter,
|
||||
properties=properties,
|
||||
expected_errors=self.expected_missing_file_errors,
|
||||
additional_selector_rules="[href='http://localhost:8080/somefile.css']",
|
||||
)
|
||||
|
||||
def test_main(self, interpreter):
|
||||
self._create_el_and_basic_asserts("main", "some text", interpreter)
|
||||
|
||||
def test_map(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"map_", "some text", interpreter, properties={"name": "somemap"}
|
||||
)
|
||||
|
||||
def test_mark(self, interpreter):
|
||||
self._create_el_and_basic_asserts("mark", "some text", interpreter)
|
||||
|
||||
def test_menu(self, interpreter):
|
||||
self._create_el_and_basic_asserts("menu", "some text", interpreter)
|
||||
|
||||
def test_meter(self, interpreter):
|
||||
properties = {
|
||||
"value": 50,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"low": 30,
|
||||
"high": 80,
|
||||
"optimum": 50,
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"meter", "some text", interpreter, properties=properties
|
||||
)
|
||||
|
||||
def test_nav(self, interpreter):
|
||||
self._create_el_and_basic_asserts("nav", "some text", interpreter)
|
||||
|
||||
def test_object(self, interpreter):
|
||||
properties = {
|
||||
"data": "http://localhost:8080/somefile.swf",
|
||||
"type": "application/x-shockwave-flash",
|
||||
"width": "250",
|
||||
"height": "200",
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"object_",
|
||||
interpreter=interpreter,
|
||||
properties=properties,
|
||||
)
|
||||
|
||||
def test_ol(self, interpreter):
|
||||
self._create_el_and_basic_asserts("ol", "some text", interpreter)
|
||||
|
||||
def test_optgroup(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"optgroup", "some text", interpreter, properties={"label": "some label"}
|
||||
)
|
||||
|
||||
def test_option(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"option", "some text", interpreter, properties={"value": "some value"}
|
||||
)
|
||||
|
||||
def test_output(self, interpreter):
|
||||
self._create_el_and_basic_asserts("output", "some text", interpreter)
|
||||
|
||||
def test_p(self, interpreter):
|
||||
self._create_el_and_basic_asserts("p", "some text", interpreter)
|
||||
|
||||
def test_picture(self, interpreter):
|
||||
self._create_el_and_basic_asserts("picture", "some text", interpreter)
|
||||
|
||||
def test_pre(self, interpreter):
|
||||
self._create_el_and_basic_asserts("pre", "some text", interpreter)
|
||||
|
||||
def test_progress(self, interpreter):
|
||||
properties = {
|
||||
"value": 50,
|
||||
"max": 100,
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"progress", "some text", interpreter, properties=properties
|
||||
)
|
||||
|
||||
def test_q(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"q", "some text", interpreter, properties={"cite": "http://example.com/"}
|
||||
)
|
||||
|
||||
def test_s(self, interpreter):
|
||||
self._create_el_and_basic_asserts("s", "some text", interpreter)
|
||||
|
||||
# def test_script(self):
|
||||
# self._create_el_and_basic_asserts("script", "some text")
|
||||
|
||||
def test_section(self, interpreter):
|
||||
self._create_el_and_basic_asserts("section", "some text", interpreter)
|
||||
|
||||
def test_select(self, interpreter):
|
||||
self._create_el_and_basic_asserts("select", "some text", interpreter)
|
||||
|
||||
def test_small(self, interpreter):
|
||||
self._create_el_and_basic_asserts("small", "some text", interpreter)
|
||||
|
||||
def test_source(self, interpreter):
|
||||
properties = {
|
||||
"src": "http://localhost:8080/somefile.ogg",
|
||||
"type": "audio/ogg",
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"source",
|
||||
interpreter=interpreter,
|
||||
properties=properties,
|
||||
)
|
||||
|
||||
def test_span(self, interpreter):
|
||||
self._create_el_and_basic_asserts("span", "some text", interpreter)
|
||||
|
||||
def test_strong(self, interpreter):
|
||||
self._create_el_and_basic_asserts("strong", "some text", interpreter)
|
||||
|
||||
def test_style(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"style",
|
||||
"body {background-color: red;}",
|
||||
interpreter,
|
||||
)
|
||||
|
||||
def test_sub(self, interpreter):
|
||||
self._create_el_and_basic_asserts("sub", "some text", interpreter)
|
||||
|
||||
def test_summary(self, interpreter):
|
||||
self._create_el_and_basic_asserts("summary", "some text", interpreter)
|
||||
|
||||
def test_sup(self, interpreter):
|
||||
self._create_el_and_basic_asserts("sup", "some text", interpreter)
|
||||
|
||||
def test_table(self, interpreter):
|
||||
self._create_el_and_basic_asserts("table", "some text", interpreter)
|
||||
|
||||
def test_tbody(self, interpreter):
|
||||
self._create_el_and_basic_asserts("tbody", "some text", interpreter)
|
||||
|
||||
def test_td(self, interpreter):
|
||||
self._create_el_and_basic_asserts("td", "some text", interpreter)
|
||||
|
||||
def test_template(self, interpreter):
|
||||
# We are not checking the content of template since it's sort of
|
||||
# special element
|
||||
self._create_el_and_basic_asserts("template", interpreter=interpreter)
|
||||
|
||||
def test_textarea(self, interpreter):
|
||||
self._create_el_and_basic_asserts("textarea", "some text", interpreter)
|
||||
|
||||
def test_tfoot(self, interpreter):
|
||||
self._create_el_and_basic_asserts("tfoot", "some text", interpreter)
|
||||
|
||||
def test_th(self, interpreter):
|
||||
self._create_el_and_basic_asserts("th", "some text", interpreter)
|
||||
|
||||
def test_thead(self, interpreter):
|
||||
self._create_el_and_basic_asserts("thead", "some text", interpreter)
|
||||
|
||||
def test_time(self, interpreter):
|
||||
self._create_el_and_basic_asserts("time", "some text", interpreter)
|
||||
|
||||
def test_title(self, interpreter):
|
||||
self._create_el_and_basic_asserts("title", "some text", interpreter)
|
||||
|
||||
def test_tr(self, interpreter):
|
||||
self._create_el_and_basic_asserts("tr", "some text", interpreter)
|
||||
|
||||
def test_track(self, interpreter):
|
||||
properties = {
|
||||
"src": "http://localhost:8080/somefile.vtt",
|
||||
"kind": "subtitles",
|
||||
"srclang": "en",
|
||||
"label": "English",
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"track",
|
||||
interpreter=interpreter,
|
||||
properties=properties,
|
||||
)
|
||||
|
||||
def test_u(self, interpreter):
|
||||
self._create_el_and_basic_asserts("u", "some text", interpreter)
|
||||
|
||||
def test_ul(self, interpreter):
|
||||
self._create_el_and_basic_asserts("ul", "some text", interpreter)
|
||||
|
||||
def test_var(self, interpreter):
|
||||
self._create_el_and_basic_asserts("var", "some text", interpreter)
|
||||
|
||||
def test_video(self, interpreter):
|
||||
properties = {
|
||||
"src": "http://localhost:8080/somefile.ogg",
|
||||
"controls": True,
|
||||
"width": 250,
|
||||
"height": 200,
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"video",
|
||||
interpreter=interpreter,
|
||||
properties=properties,
|
||||
expected_errors=self.expected_missing_file_errors,
|
||||
)
|
||||
|
||||
def test_append_py_element(self, interpreter):
|
||||
# Let's make sure the body of the page is clean first
|
||||
body = self.page.locator("body")
|
||||
assert body.inner_html() == ""
|
||||
|
||||
# Let's make sure the element is not in the page
|
||||
element = self.page.locator("div")
|
||||
assert not element.count()
|
||||
|
||||
div_text_content = "Luke, I am your father"
|
||||
p_text_content = "noooooooooo!"
|
||||
# Let's create the element
|
||||
code_ = f"""
|
||||
from pyscript import when
|
||||
<script type="{interpreter}">
|
||||
from pyscript.web import page, div, p
|
||||
|
||||
el = div("{div_text_content}")
|
||||
child = p('{p_text_content}')
|
||||
el.append(child)
|
||||
page.body.append(el)
|
||||
</script>
|
||||
"""
|
||||
self.pyscript_run(code_)
|
||||
|
||||
# Let's keep the tag in 2 variables, one for the selector and another to
|
||||
# check the return tag from the selector
|
||||
el = self.page.locator("div")
|
||||
tag = el.evaluate("node => node.tagName")
|
||||
assert tag == "DIV"
|
||||
assert el.text_content() == f"{div_text_content}{p_text_content}"
|
||||
|
||||
assert (
|
||||
el.evaluate("node => node.children.length") == 1
|
||||
), "There should be only 1 child"
|
||||
assert el.evaluate("node => node.children[0].tagName") == "P"
|
||||
assert (
|
||||
el.evaluate("node => node.children[0].parentNode.textContent")
|
||||
== f"{div_text_content}{p_text_content}"
|
||||
)
|
||||
assert el.evaluate("node => node.children[0].textContent") == p_text_content
|
||||
|
||||
def test_append_proxy_element(self, interpreter):
|
||||
# Let's make sure the body of the page is clean first
|
||||
body = self.page.locator("body")
|
||||
assert body.inner_html() == ""
|
||||
|
||||
# Let's make sure the element is not in the page
|
||||
element = self.page.locator("div")
|
||||
assert not element.count()
|
||||
|
||||
div_text_content = "Luke, I am your father"
|
||||
p_text_content = "noooooooooo!"
|
||||
# Let's create the element
|
||||
code_ = f"""
|
||||
from pyscript import when
|
||||
<script type="{interpreter}">
|
||||
from pyscript import document
|
||||
from pyscript.web import page, div, p
|
||||
|
||||
el = div("{div_text_content}")
|
||||
child = document.createElement('P')
|
||||
child.textContent = '{p_text_content}'
|
||||
el.append(child)
|
||||
page.body.append(el)
|
||||
</script>
|
||||
"""
|
||||
self.pyscript_run(code_)
|
||||
|
||||
# Let's keep the tag in 2 variables, one for the selector and another to
|
||||
# check the return tag from the selector
|
||||
el = self.page.locator("div")
|
||||
tag = el.evaluate("node => node.tagName")
|
||||
assert tag == "DIV"
|
||||
assert el.text_content() == f"{div_text_content}{p_text_content}"
|
||||
|
||||
assert (
|
||||
el.evaluate("node => node.children.length") == 1
|
||||
), "There should be only 1 child"
|
||||
assert el.evaluate("node => node.children[0].tagName") == "P"
|
||||
assert (
|
||||
el.evaluate("node => node.children[0].parentNode.textContent")
|
||||
== f"{div_text_content}{p_text_content}"
|
||||
)
|
||||
assert el.evaluate("node => node.children[0].textContent") == p_text_content
|
||||
|
||||
def test_append_py_elementcollection(self, interpreter):
|
||||
# Let's make sure the body of the page is clean first
|
||||
body = self.page.locator("body")
|
||||
assert body.inner_html() == ""
|
||||
|
||||
# Let's make sure the element is not in the page
|
||||
element = self.page.locator("div")
|
||||
assert not element.count()
|
||||
|
||||
div_text_content = "Luke, I am your father"
|
||||
p_text_content = "noooooooooo!"
|
||||
p2_text_content = "not me!"
|
||||
# Let's create the element
|
||||
code_ = f"""
|
||||
from pyscript import when
|
||||
<script type="{interpreter}">
|
||||
from pyscript.web import page, div, p, ElementCollection
|
||||
|
||||
el = div("{div_text_content}")
|
||||
child1 = p('{p_text_content}')
|
||||
child2 = p('{p2_text_content}', id='child2')
|
||||
collection = ElementCollection([child1, child2])
|
||||
el.append(collection)
|
||||
page.body.append(el)
|
||||
</script>
|
||||
"""
|
||||
self.pyscript_run(code_)
|
||||
|
||||
# Let's keep the tag in 2 variables, one for the selector and another to
|
||||
# check the return tag from the selector
|
||||
el = self.page.locator("div")
|
||||
tag = el.evaluate("node => node.tagName")
|
||||
assert tag == "DIV"
|
||||
parent_full_content = f"{div_text_content}{p_text_content}{p2_text_content}"
|
||||
assert el.text_content() == parent_full_content
|
||||
|
||||
assert (
|
||||
el.evaluate("node => node.children.length") == 2
|
||||
), "There should be only 1 child"
|
||||
assert el.evaluate("node => node.children[0].tagName") == "P"
|
||||
assert (
|
||||
el.evaluate("node => node.children[0].parentNode.textContent")
|
||||
== parent_full_content
|
||||
)
|
||||
assert el.evaluate("node => node.children[0].textContent") == p_text_content
|
||||
|
||||
assert el.evaluate("node => node.children[1].tagName") == "P"
|
||||
assert el.evaluate("node => node.children[1].id") == "child2"
|
||||
assert (
|
||||
el.evaluate("node => node.children[1].parentNode.textContent")
|
||||
== parent_full_content
|
||||
)
|
||||
assert el.evaluate("node => node.children[1].textContent") == p2_text_content
|
||||
|
||||
def test_append_js_element_nodelist(self, interpreter):
|
||||
# Let's make sure the body of the page is clean first
|
||||
body = self.page.locator("body")
|
||||
assert body.inner_html() == ""
|
||||
|
||||
# Let's make sure the element is not in the page
|
||||
element = self.page.locator("div")
|
||||
assert not element.count()
|
||||
|
||||
div_text_content = "Luke, I am your father"
|
||||
p_text_content = "noooooooooo!"
|
||||
p2_text_content = "not me!"
|
||||
# Let's create the element
|
||||
code_ = f"""
|
||||
from pyscript import when
|
||||
<script type="{interpreter}">
|
||||
from pyscript import document
|
||||
from pyscript.web import page, div, p, ElementCollection
|
||||
|
||||
el = div("{div_text_content}")
|
||||
child1 = p('{p_text_content}')
|
||||
child2 = p('{p2_text_content}', id='child2')
|
||||
|
||||
page.body.append(child1)
|
||||
page.body.append(child2)
|
||||
|
||||
nodes = document.querySelectorAll('p')
|
||||
el.append(nodes)
|
||||
|
||||
page.body.append(el)
|
||||
</script>
|
||||
"""
|
||||
self.pyscript_run(code_)
|
||||
|
||||
# Let's keep the tag in 2 variables, one for the selector and another to
|
||||
# check the return tag from the selector
|
||||
el = self.page.locator("div")
|
||||
tag = el.evaluate("node => node.tagName")
|
||||
assert tag == "DIV"
|
||||
parent_full_content = f"{div_text_content}{p_text_content}{p2_text_content}"
|
||||
assert el.text_content() == parent_full_content
|
||||
|
||||
assert (
|
||||
el.evaluate("node => node.children.length") == 2
|
||||
), "There should be only 1 child"
|
||||
assert el.evaluate("node => node.children[0].tagName") == "P"
|
||||
assert (
|
||||
el.evaluate("node => node.children[0].parentNode.textContent")
|
||||
== parent_full_content
|
||||
)
|
||||
assert el.evaluate("node => node.children[0].textContent") == p_text_content
|
||||
|
||||
assert el.evaluate("node => node.children[1].tagName") == "P"
|
||||
assert el.evaluate("node => node.children[1].id") == "child2"
|
||||
assert (
|
||||
el.evaluate("node => node.children[1].parentNode.textContent")
|
||||
== parent_full_content
|
||||
)
|
||||
assert el.evaluate("node => node.children[1].textContent") == p2_text_content
|
||||
@@ -1,124 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest, with_execution_thread
|
||||
|
||||
|
||||
# these tests don't need to run in 'main' and 'worker' modes: the workers are
|
||||
# already tested explicitly by some of them (see e.g.
|
||||
# test_script_type_py_worker_attribute)
|
||||
@with_execution_thread(None)
|
||||
class TestScriptTypePyScript(PyScriptTest):
|
||||
def test_display_line_break(self):
|
||||
self.pyscript_run(
|
||||
r"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display('hello\nworld')
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
text_content = self.page.locator("script-py").text_content()
|
||||
assert "hello\nworld" == text_content
|
||||
|
||||
def test_amp(self):
|
||||
self.pyscript_run(
|
||||
r"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display('a & b')
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
text_content = self.page.locator("script-py").text_content()
|
||||
assert "a & b" == text_content
|
||||
|
||||
def test_quot(self):
|
||||
self.pyscript_run(
|
||||
r"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display('a " b')
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
text_content = self.page.locator("script-py").text_content()
|
||||
assert "a " b" == text_content
|
||||
|
||||
def test_lt_gt(self):
|
||||
self.pyscript_run(
|
||||
r"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display('< < > >')
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
text_content = self.page.locator("script-py").text_content()
|
||||
assert "< < > >" == text_content
|
||||
|
||||
def test_dynamically_add_script_type_py_tag(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script>
|
||||
function addPyScriptTag() {
|
||||
let tag = document.createElement('script');
|
||||
tag.type = 'py';
|
||||
tag.textContent = "print('hello world')";
|
||||
document.body.appendChild(tag);
|
||||
}
|
||||
addPyScriptTag();
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
# please note the test here was on timeout
|
||||
# incapable of finding a <button> after the script
|
||||
self.page.locator("script-py")
|
||||
|
||||
assert self.console.log.lines[-1] == "hello world"
|
||||
|
||||
def test_script_type_py_src_attribute(self):
|
||||
self.writefile("foo.py", "print('hello from foo')")
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" src="foo.py"></script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "hello from foo"
|
||||
|
||||
def test_script_type_py_worker_attribute(self):
|
||||
self.writefile("foo.py", "print('hello from foo')")
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" src="foo.py" worker></script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "hello from foo"
|
||||
|
||||
@pytest.mark.skip("FIXME: output attribute is not implemented")
|
||||
def test_script_type_py_output_attribute(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="first"></div>
|
||||
<script type="py" output="first">
|
||||
print("<p>Hello</p>")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
text = self.page.locator("#first").text_content()
|
||||
assert "<p>Hello</p>" in text
|
||||
|
||||
@pytest.mark.skip("FIXME: stderr attribute is not implemented")
|
||||
def test_script_type_py_stderr_attribute(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="stdout-div"></div>
|
||||
<div id="stderr-div"></div>
|
||||
<script type="py" output="stdout-div" stderr="stderr-div">
|
||||
import sys
|
||||
print("one.", file=sys.stderr)
|
||||
print("two.")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
assert self.page.locator("#stdout-div").text_content() == "one.two."
|
||||
assert self.page.locator("#stderr-div").text_content() == "one."
|
||||
@@ -1,32 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest
|
||||
|
||||
|
||||
class TestShadowRoot(PyScriptTest):
|
||||
@pytest.mark.skip("NEXT: Element interface is gone. Replace with PyDom")
|
||||
def test_reachable_shadow_root(self):
|
||||
self.pyscript_run(
|
||||
r"""
|
||||
<script>
|
||||
// reason to wait for py-script is that it's the entry point for
|
||||
// all patches and the MutationObserver, otherwise being this a synchronous
|
||||
// script the constructor gets instantly invoked at the node before
|
||||
// py-script gets a chance to initialize itself.
|
||||
customElements.whenDefined('py-script').then(() => {
|
||||
customElements.define('s-r', class extends HTMLElement {
|
||||
constructor() {
|
||||
super().attachShadow({mode: 'closed'}).innerHTML =
|
||||
'<div id="shadowed">OK</div>';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<s-r></s-r>
|
||||
<script type="py">
|
||||
import js
|
||||
js.console.log(Element("shadowed").innerHtml)
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "OK"
|
||||
@@ -1,122 +0,0 @@
|
||||
import pytest
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from .support import PyScriptTest, skip_worker
|
||||
|
||||
pytest.skip(reason="NEXT: Should we remove the splashscreen?", allow_module_level=True)
|
||||
|
||||
|
||||
class TestSplashscreen(PyScriptTest):
|
||||
def test_autoshow_and_autoclose(self):
|
||||
"""
|
||||
By default, we show the splashscreen and we close it when the loading is
|
||||
complete.
|
||||
|
||||
XXX: this test is a bit fragile: now it works reliably because the
|
||||
startup is so slow that when we do expect(div).to_be_visible(), the
|
||||
splashscreen is still there. But in theory, if the startup become very
|
||||
fast, it could happen that by the time we arrive in python lang, it
|
||||
has already been removed.
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
print('hello pyscript')
|
||||
</script>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
div = self.page.locator("py-splashscreen > div")
|
||||
expect(div).to_be_visible()
|
||||
expect(div).to_contain_text("Python startup...")
|
||||
assert "Python startup..." in self.console.info.text
|
||||
#
|
||||
# now we wait for the startup to complete
|
||||
self.wait_for_pyscript()
|
||||
#
|
||||
# and now the splashscreen should have been removed
|
||||
expect(div).to_be_hidden()
|
||||
assert self.page.locator("py-locator").count() == 0
|
||||
|
||||
assert "hello pyscript" in self.console.log.lines
|
||||
|
||||
def test_autoclose_false(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
[splashscreen]
|
||||
autoclose = false
|
||||
</py-config>
|
||||
<script type="py">
|
||||
print('hello pyscript')
|
||||
</script>
|
||||
""",
|
||||
)
|
||||
div = self.page.locator("py-splashscreen > div")
|
||||
expect(div).to_be_visible()
|
||||
expect(div).to_contain_text("Python startup...")
|
||||
expect(div).to_contain_text("Startup complete")
|
||||
assert "hello pyscript" in self.console.log.lines
|
||||
|
||||
def test_autoclose_loader_deprecated(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
autoclose_loader = false
|
||||
</py-config>
|
||||
<script type="py">
|
||||
print('hello pyscript')
|
||||
</script>
|
||||
""",
|
||||
)
|
||||
warning = self.page.locator(".py-warning")
|
||||
inner_text = warning.inner_html()
|
||||
assert "The setting autoclose_loader is deprecated" in inner_text
|
||||
|
||||
div = self.page.locator("py-splashscreen > div")
|
||||
expect(div).to_be_visible()
|
||||
expect(div).to_contain_text("Python startup...")
|
||||
expect(div).to_contain_text("Startup complete")
|
||||
assert "hello pyscript" in self.console.log.lines
|
||||
|
||||
def test_splashscreen_disabled_option(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
[splashscreen]
|
||||
enabled = false
|
||||
</py-config>
|
||||
|
||||
<script type="py">
|
||||
def test():
|
||||
print("Hello pyscript!")
|
||||
test()
|
||||
</script>
|
||||
""",
|
||||
)
|
||||
assert self.page.locator("py-splashscreen").count() == 0
|
||||
assert self.console.log.lines[-1] == "Hello pyscript!"
|
||||
py_terminal = self.page.wait_for_selector("py-terminal")
|
||||
assert py_terminal.inner_text() == "Hello pyscript!\n"
|
||||
|
||||
@skip_worker("FIXME: js.document")
|
||||
def test_splashscreen_custom_message(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
[splashscreen]
|
||||
autoclose = false
|
||||
</py-config>
|
||||
|
||||
<script type="py">
|
||||
from js import document
|
||||
|
||||
splashscreen = document.querySelector("py-splashscreen")
|
||||
splashscreen.log("Hello, world!")
|
||||
</script>
|
||||
""",
|
||||
)
|
||||
|
||||
splashscreen = self.page.locator("py-splashscreen")
|
||||
assert splashscreen.count() == 1
|
||||
assert "Hello, world!" in splashscreen.inner_text()
|
||||
@@ -1,370 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest, skip_worker
|
||||
|
||||
pytest.skip(reason="NEXT: entire stdio should be reviewed", allow_module_level=True)
|
||||
|
||||
|
||||
class TestOutputHandling(PyScriptTest):
|
||||
# Source of a script to test the TargetedStdio functionality
|
||||
|
||||
def test_targeted_stdio_solo(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
terminal = true
|
||||
</py-config>
|
||||
<py-terminal></py-terminal>
|
||||
<div id="container">
|
||||
<div id="first"></div>
|
||||
<div id="second"></div>
|
||||
<div id="third"></div>
|
||||
</div>
|
||||
<script type="py" output="first">print("first 1.")</script>
|
||||
<script type="py" output="second">print("second.")</script>
|
||||
<script type="py" output="third">print("third.")</script>
|
||||
<script type="py" output="first">print("first 2.")</script>
|
||||
<script type="py">print("no output.")</script>
|
||||
"""
|
||||
)
|
||||
|
||||
# Check that page has desired parent/child structure, and that
|
||||
# Output divs are correctly located
|
||||
assert (container := self.page.locator("#container")).count() > 0
|
||||
assert (first_div := container.locator("#first")).count() > 0
|
||||
assert (second_div := container.locator("#second")).count() > 0
|
||||
assert (third_div := container.locator("#third")).count() > 0
|
||||
|
||||
# Check that output ends up in proper div
|
||||
assert first_div.text_content() == "first 1.first 2."
|
||||
assert second_div.text_content() == "second."
|
||||
assert third_div.text_content() == "third."
|
||||
|
||||
# Check that tag with no otuput attribute doesn't end up in container at all
|
||||
assert container.get_by_text("no output.").count() == 0
|
||||
|
||||
# Check that all output ends up in py-terminal
|
||||
assert (
|
||||
self.page.locator("py-terminal").text_content()
|
||||
== "first 1.second.third.first 2.no output."
|
||||
)
|
||||
|
||||
# Check that all output ends up in the dev console, in order
|
||||
last_index = -1
|
||||
for line in ["first 1.", "second.", "third.", "first 2.", "no output."]:
|
||||
assert (line_index := self.console.log.lines.index(line)) > -1
|
||||
assert line_index > last_index
|
||||
last_index = line_index
|
||||
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_stdio_escape(self):
|
||||
# Test that text that looks like HTML tags is properly escaped in stdio
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="first"></div>
|
||||
<script type="py" output="first">
|
||||
print("<p>Hello</p>")
|
||||
print('<img src="https://example.net">')
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
text = self.page.locator("#first").text_content()
|
||||
|
||||
assert "<p>Hello</p>" in text
|
||||
assert '<img src="https://example.net">' in text
|
||||
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_targeted_stdio_linebreaks(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="first"></div>
|
||||
<script type="py" output="first">
|
||||
print("one.")
|
||||
print("two.")
|
||||
print("three.")
|
||||
</script>
|
||||
|
||||
<div id="second"></div>
|
||||
<script type="py" output="second">
|
||||
print("one.\\ntwo.\\nthree.")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
# check line breaks at end of each input
|
||||
assert self.page.locator("#first").inner_html() == "one.<br>two.<br>three.<br>"
|
||||
|
||||
# new lines are converted to line breaks
|
||||
assert self.page.locator("#second").inner_html() == "one.<br>two.<br>three.<br>"
|
||||
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_targeted_stdio_async(self):
|
||||
# Test the behavior of stdio capture in async contexts
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
import asyncio
|
||||
import js
|
||||
|
||||
async def coro(value, delay):
|
||||
print(value)
|
||||
await asyncio.sleep(delay)
|
||||
js.console.log(f"DONE {value}")
|
||||
</script>
|
||||
|
||||
<div id="first"></div>
|
||||
<script type="py">
|
||||
asyncio.ensure_future(coro("first", 1))
|
||||
</script>
|
||||
|
||||
<div id="second"></div>
|
||||
<script type="py" output="second">
|
||||
asyncio.ensure_future(coro("second", 1))
|
||||
</script>
|
||||
|
||||
<div id="third"></div>
|
||||
<script type="py" output="third">
|
||||
asyncio.ensure_future(coro("third", 0))
|
||||
</script>
|
||||
|
||||
<script type="py" output="third">
|
||||
asyncio.ensure_future(coro("DONE", 3))
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
self.wait_for_console("DONE DONE")
|
||||
|
||||
# script tags without output parameter should not send
|
||||
# stdout to element
|
||||
assert self.page.locator("#first").text_content() == ""
|
||||
|
||||
# script tags with output parameter not expected to send
|
||||
# std to element in coroutine
|
||||
assert self.page.locator("#second").text_content() == ""
|
||||
assert self.page.locator("#third").text_content() == ""
|
||||
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_targeted_stdio_interleaved(self):
|
||||
# Test that synchronous writes to stdout are placed correctly, even
|
||||
# While interleaved with scheduling coroutines in the same tag
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="good"></div>
|
||||
<div id="bad"></div>
|
||||
<script type="py" output="good">
|
||||
import asyncio
|
||||
import js
|
||||
|
||||
async def coro_bad(value, delay):
|
||||
print(value)
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
print("one.")
|
||||
asyncio.ensure_future(coro_bad("badone.", 0.1))
|
||||
print("two.")
|
||||
asyncio.ensure_future(coro_bad("badtwo.", 0.2))
|
||||
print("three.")
|
||||
asyncio.ensure_future(coro_bad("badthree.", 0))
|
||||
asyncio.ensure_future(coro_bad("DONE", 1))
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
# Three prints should appear from synchronous writes
|
||||
assert self.page.locator("#good").text_content() == "one.two.three."
|
||||
|
||||
# Check that all output ends up in the dev console, in order
|
||||
last_index = -1
|
||||
for line in ["one.", "two.", "three.", "badthree.", "badone.", "badtwo."]:
|
||||
assert (line_index := self.console.log.lines.index(line)) > -1
|
||||
assert line_index > last_index
|
||||
|
||||
self.assert_no_banners()
|
||||
|
||||
@skip_worker("FIXME: js.document")
|
||||
def test_targeted_stdio_dynamic_tags(self):
|
||||
# Test that creating py-script tags via Python still leaves
|
||||
# stdio targets working
|
||||
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="first"></div>
|
||||
<div id="second"></div>
|
||||
<script type="py" output="first">
|
||||
print("first.")
|
||||
|
||||
import js
|
||||
tag = js.document.createElement("py-script")
|
||||
tag.innerText = "print('second.')"
|
||||
tag.setAttribute("output", "second")
|
||||
js.document.body.appendChild(tag)
|
||||
|
||||
print("first.")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
# Ensure second tag was added to page
|
||||
assert (second_div := self.page.locator("#second")).count() > 0
|
||||
|
||||
# Ensure output when to correct locations
|
||||
assert self.page.locator("#first").text_content() == "first.first."
|
||||
assert second_div.text_content() == "second."
|
||||
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_stdio_stdout_id_errors(self):
|
||||
# Test that using an ID not present on the page as the Output
|
||||
# Attribute creates exactly 1 warning banner per missing id
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" output="not-on-page">
|
||||
print("bad.")
|
||||
</script>
|
||||
|
||||
<div id="on-page"></div>
|
||||
<script type="py">
|
||||
print("good.")
|
||||
</script>
|
||||
|
||||
<script type="py" output="not-on-page">
|
||||
print("bad.")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
banner = self.page.query_selector_all(".py-warning")
|
||||
assert len(banner) == 1
|
||||
banner_content = banner[0].inner_text()
|
||||
expected = (
|
||||
'output = "not-on-page" does not match the id of any element on the page.'
|
||||
)
|
||||
|
||||
assert banner_content == expected
|
||||
|
||||
def test_stdio_stderr_id_errors(self):
|
||||
# Test that using an ID not present on the page as the stderr
|
||||
# attribute creates exactly 1 warning banner per missing id
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" stderr="not-on-page">
|
||||
import sys
|
||||
print("bad.", file=sys.stderr)
|
||||
</script>
|
||||
|
||||
<div id="on-page"></div>
|
||||
<script type="py">
|
||||
print("good.", file=sys.stderr)
|
||||
</script>
|
||||
|
||||
<script type="py" stderr="not-on-page">
|
||||
print("bad.", file=sys.stderr)
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
banner = self.page.query_selector_all(".py-warning")
|
||||
assert len(banner) == 1
|
||||
banner_content = banner[0].inner_text()
|
||||
expected = (
|
||||
'stderr = "not-on-page" does not match the id of any element on the page.'
|
||||
)
|
||||
|
||||
assert banner_content == expected
|
||||
|
||||
def test_stdio_stderr(self):
|
||||
# Test that stderr works, and routes to the same location as stdout
|
||||
# Also, script tags with the stderr attribute route to an additional location
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="stdout-div"></div>
|
||||
<div id="stderr-div"></div>
|
||||
<script type="py" output="stdout-div" stderr="stderr-div">
|
||||
import sys
|
||||
print("one.", file=sys.stderr)
|
||||
print("two.")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
assert self.page.locator("#stdout-div").text_content() == "one.two."
|
||||
assert self.page.locator("#stderr-div").text_content() == "one."
|
||||
self.assert_no_banners()
|
||||
|
||||
@skip_worker("FIXME: js.document")
|
||||
def test_stdio_output_attribute_change(self):
|
||||
# If the user changes the 'output' attribute of a <script type="py"> tag mid-execution,
|
||||
# Output should no longer go to the selected div and a warning should appear
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="first"></div>
|
||||
<div id="second"></div>
|
||||
<!-- There is no tag with id "third" -->
|
||||
<script type="py" id="pyscript-tag" output="first">
|
||||
print("one.")
|
||||
|
||||
# Change the 'output' attribute of this tag
|
||||
import js
|
||||
this_tag = js.document.getElementById("pyscript-tag")
|
||||
|
||||
this_tag.setAttribute("output", "second")
|
||||
print("two.")
|
||||
|
||||
this_tag.setAttribute("output", "third")
|
||||
print("three.")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
assert self.page.locator("#first").text_content() == "one."
|
||||
assert self.page.locator("#second").text_content() == "two."
|
||||
expected_alert_banner_msg = (
|
||||
'output = "third" does not match the id of any element on the page.'
|
||||
)
|
||||
|
||||
alert_banner = self.page.locator(".alert-banner")
|
||||
assert expected_alert_banner_msg in alert_banner.inner_text()
|
||||
|
||||
@skip_worker("FIXME: js.document")
|
||||
def test_stdio_target_element_id_change(self):
|
||||
# If the user changes the ID of the targeted DOM element mid-execution,
|
||||
# Output should no longer go to the selected element and a warning should appear
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="first"></div>
|
||||
<div id="second"></div>
|
||||
<!-- There is no tag with id "third" -->
|
||||
<script type="py" id="pyscript-tag" output="first">
|
||||
print("one.")
|
||||
|
||||
# Change the ID of the targeted DIV to something else
|
||||
import js
|
||||
target_tag = js.document.getElementById("first")
|
||||
|
||||
# should fail and show banner
|
||||
target_tag.setAttribute("id", "second")
|
||||
print("two.")
|
||||
|
||||
# But changing both the 'output' attribute and the id of the target
|
||||
# should work
|
||||
target_tag.setAttribute("id", "third")
|
||||
js.document.getElementById("pyscript-tag").setAttribute("output", "third")
|
||||
print("three.")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
# Note the ID of the div has changed by the time of this assert
|
||||
assert self.page.locator("#third").text_content() == "one.three."
|
||||
|
||||
expected_alert_banner_msg = (
|
||||
'output = "first" does not match the id of any element on the page.'
|
||||
)
|
||||
alert_banner = self.page.locator(".alert-banner")
|
||||
assert expected_alert_banner_msg in alert_banner.inner_text()
|
||||
@@ -1,25 +0,0 @@
|
||||
import pytest
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from .support import PyScriptTest, with_execution_thread
|
||||
|
||||
|
||||
@with_execution_thread(None)
|
||||
class TestStyle(PyScriptTest):
|
||||
def test_pyscript_not_defined(self):
|
||||
"""Test raw elements that are not defined for display:none"""
|
||||
doc = """
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="build/core.css" />
|
||||
</head>
|
||||
<body>
|
||||
<py-config>hello</py-config>
|
||||
<py-script>hello</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("test-not-defined-css.html", doc)
|
||||
self.goto("test-not-defined-css.html")
|
||||
expect(self.page.locator("py-config")).to_be_hidden()
|
||||
expect(self.page.locator("py-script")).to_be_hidden()
|
||||
@@ -1,54 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest, skip_worker
|
||||
|
||||
|
||||
class TestWarningsAndBanners(PyScriptTest):
|
||||
# Test the behavior of generated warning banners
|
||||
|
||||
def test_deprecate_loading_scripts_from_latest(self):
|
||||
# Use a script tag with an invalid output attribute to generate a warning, but only one
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
print("whatever..")
|
||||
</script>
|
||||
""",
|
||||
extra_head='<script type="ignore-me" src="https://pyscript.net/latest/any-path-triggers-the-warning-anyway.js"></script>',
|
||||
)
|
||||
|
||||
# wait for the banner to appear (we could have a page.locater call but for some reason
|
||||
# the worker takes to long to render on CI, since it's a test we can afford 2 calls)
|
||||
loc = self.page.wait_for_selector(".py-error")
|
||||
assert (
|
||||
loc.inner_text()
|
||||
== "Loading scripts from latest is deprecated and will be removed soon. Please use a specific version instead."
|
||||
)
|
||||
|
||||
# Only one banner should appear
|
||||
loc = self.page.locator(".py-error")
|
||||
assert loc.count() == 1
|
||||
|
||||
@pytest.mark.skip("NEXT: To check if behaviour is consistent with classic")
|
||||
def test_create_singular_warning(self):
|
||||
# Use a script tag with an invalid output attribute to generate a warning, but only one
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" output="foo">
|
||||
print("one.")
|
||||
print("two.")
|
||||
</script>
|
||||
<script type="py" output="foo">
|
||||
print("three.")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
loc = self.page.locator(".alert-banner")
|
||||
|
||||
# Only one banner should appear
|
||||
assert loc.count() == 1
|
||||
assert (
|
||||
loc.text_content()
|
||||
== 'output = "foo" does not match the id of any element on the page.'
|
||||
)
|
||||
@@ -1,178 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest, skip_worker
|
||||
|
||||
|
||||
class TestEventHandler(PyScriptTest):
|
||||
def test_when_decorator_with_event(self):
|
||||
"""When the decorated function takes a single parameter,
|
||||
it should be passed the event object
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button id="foo_id">foo_button</button>
|
||||
<script type="py">
|
||||
from pyscript import when
|
||||
@when("click", selector="#foo_id")
|
||||
def foo(evt):
|
||||
print(f"clicked {evt.target.id}")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
self.page.locator("text=foo_button").click()
|
||||
self.wait_for_console("clicked foo_id")
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_when_decorator_without_event(self):
|
||||
"""When the decorated function takes no parameters (not including 'self'),
|
||||
it should be called without the event object
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button id="foo_id">foo_button</button>
|
||||
<script type="py">
|
||||
from pyscript import when
|
||||
@when("click", selector="#foo_id")
|
||||
def foo():
|
||||
print("The button was clicked")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
self.page.locator("text=foo_button").click()
|
||||
self.wait_for_console("The button was clicked")
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_multiple_when_decorators_with_event(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button id="foo_id">foo_button</button>
|
||||
<button id="bar_id">bar_button</button>
|
||||
<script type="py">
|
||||
from pyscript import when
|
||||
@when("click", selector="#foo_id")
|
||||
def foo_click(evt):
|
||||
print(f"foo_click! id={evt.target.id}")
|
||||
@when("click", selector="#bar_id")
|
||||
def bar_click(evt):
|
||||
print(f"bar_click! id={evt.target.id}")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
self.page.locator("text=foo_button").click()
|
||||
self.wait_for_console("foo_click! id=foo_id")
|
||||
self.page.locator("text=bar_button").click()
|
||||
self.wait_for_console("bar_click! id=bar_id")
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_two_when_decorators(self):
|
||||
"""When decorating a function twice, both should function"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button id="foo_id">foo_button</button>
|
||||
<button class="bar_class">bar_button</button>
|
||||
<script type="py">
|
||||
from pyscript import when
|
||||
@when("click", selector="#foo_id")
|
||||
@when("mouseover", selector=".bar_class")
|
||||
def foo(evt):
|
||||
print(f"got event: {evt.type}")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
self.page.locator("text=bar_button").hover()
|
||||
self.wait_for_console("got event: mouseover")
|
||||
self.page.locator("text=foo_button").click()
|
||||
self.wait_for_console("got event: click")
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_two_when_decorators_same_element(self):
|
||||
"""When decorating a function twice *on the same DOM element*, both should function"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button id="foo_id">foo_button</button>
|
||||
<script type="py">
|
||||
from pyscript import when
|
||||
@when("click", selector="#foo_id")
|
||||
@when("mouseover", selector="#foo_id")
|
||||
def foo(evt):
|
||||
print(f"got event: {evt.type}")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
self.page.locator("text=foo_button").hover()
|
||||
self.wait_for_console("got event: mouseover")
|
||||
self.page.locator("text=foo_button").click()
|
||||
self.wait_for_console("got event: click")
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_when_decorator_multiple_elements(self):
|
||||
"""The @when decorator's selector should successfully select multiple
|
||||
DOM elements
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button class="bar_class">button1</button>
|
||||
<button class="bar_class">button2</button>
|
||||
<script type="py">
|
||||
from pyscript import when
|
||||
@when("click", selector=".bar_class")
|
||||
def foo(evt):
|
||||
print(f"{evt.target.innerText} was clicked")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
self.page.locator("text=button1").click()
|
||||
self.page.locator("text=button2").click()
|
||||
self.wait_for_console("button2 was clicked")
|
||||
assert "button1 was clicked" in self.console.log.lines
|
||||
assert "button2 was clicked" in self.console.log.lines
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_when_decorator_duplicate_selectors(self):
|
||||
""" """
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button id="foo_id">foo_button</button>
|
||||
<script type="py">
|
||||
from pyscript import when
|
||||
@when("click", selector="#foo_id")
|
||||
@when("click", selector="#foo_id")
|
||||
def foo(evt):
|
||||
foo.n += 1
|
||||
print(f"click {foo.n} on {evt.target.id}")
|
||||
foo.n = 0
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
self.page.locator("text=foo_button").click()
|
||||
self.wait_for_console("click 1 on foo_id")
|
||||
self.wait_for_console("click 2 on foo_id")
|
||||
self.assert_no_banners()
|
||||
|
||||
@skip_worker("NEXT: error banner not shown")
|
||||
def test_when_decorator_invalid_selector(self):
|
||||
"""When the selector parameter of @when is invalid, it should show an error"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button id="foo_id">foo_button</button>
|
||||
<script type="py">
|
||||
from pyscript import when
|
||||
@when("click", selector="#.bad")
|
||||
def foo(evt):
|
||||
...
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
self.page.locator("text=foo_button").click()
|
||||
msg = "Failed to execute 'querySelectorAll' on 'Document': '#.bad' is not a valid selector."
|
||||
error = self.page.wait_for_selector(".py-error")
|
||||
banner_text = error.inner_text()
|
||||
|
||||
if msg not in banner_text:
|
||||
raise AssertionError(
|
||||
f"Expected message '{msg}' does not "
|
||||
f"match banner text '{banner_text}'"
|
||||
)
|
||||
|
||||
assert msg in self.console.error.lines[-1]
|
||||
self.check_py_errors(msg)
|
||||
@@ -1,130 +0,0 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>PyDom Test Suite</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
|
||||
<style>
|
||||
@import url("https://fonts.googleapis.com/css?family=Roboto:100,400");
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
*:before, *:after {
|
||||
box-sizing: inherit;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Courier New", Courier, "Lucida Sans Typewriter", "Lucida Typewriter", monospace;
|
||||
font-size: 14px; font-style: normal; font-variant: normal; font-weight: 400; line-height: 20px;
|
||||
}
|
||||
|
||||
h1 { font-size: 24px; font-weight: 700; line-height: 26.4px; }
|
||||
h2 { font-size: 14px; font-weight: 700; line-height: 15.4px; }
|
||||
|
||||
#tests-terminal{
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py" src="run_tests.py" config="tests.toml"></script>
|
||||
|
||||
<h1>pyscript.dom Tests</h1>
|
||||
<p>You can pass test parameters to this test suite by passing them as query params on the url.
|
||||
For instance, to pass "-v -s --pdb" to pytest, you would use the following url:
|
||||
<label style="color: blue">?-v&-s&--pdb</label>
|
||||
</p>
|
||||
<div id="tests-terminal"></div>
|
||||
|
||||
<template id="test_card_with_element_template">
|
||||
<p>This is a test. {foo}</p>
|
||||
</template>
|
||||
|
||||
<div id="test_id_selector" style="visibility: hidden;">You found test_id_selector</div>
|
||||
<div id="test_class_selector" class="a-test-class" style="visibility: hidden;">You found test_class_selector</div>
|
||||
<div id="test_selector_w_children" class="a-test-class" style="visibility: hidden;">
|
||||
<div id="test_selector_w_children_child_1" class="a-test-class" style="visibility: hidden;">Child 1</div>
|
||||
<div id="test_selector_w_children_child_2" style="visibility: hidden;">Child 2</div>
|
||||
</div>
|
||||
|
||||
<div id="div-no-classes"></div>
|
||||
|
||||
<div style="visibility: hidden;">
|
||||
<h2>Test Read and Write</h2>
|
||||
<div id="test_rr_div">Content test_rr_div</div>
|
||||
<h3 id="test_rr_h3">Content test_rr_h3</h3>
|
||||
|
||||
<div id="multi-elem-div" class="multi-elems">Content multi-elem-div</div>
|
||||
<p id="multi-elem-p" class="multi-elems">Content multi-elem-p</p>
|
||||
<h2 id="multi-elem-h2" class="multi-elems">Content multi-elem-h2</h2>
|
||||
|
||||
<form>
|
||||
<input id="test_rr_input_text" type="text" value="Content test_rr_input_text">
|
||||
<input id="test_rr_input_button" type="button" value="Content test_rr_input_button">
|
||||
<input id="test_rr_input_email" type="email" value="Content test_rr_input_email">
|
||||
<input id="test_rr_input_password" type="password" value="Content test_rr_input_password">
|
||||
</form>
|
||||
|
||||
<select id="test_select_element"></select>
|
||||
<select id="test_select_element_w_options">
|
||||
<option value="1">Option 1</option>
|
||||
<option value="2" selected="selected">Option 2</option>
|
||||
</select>
|
||||
<select id="test_select_element_to_clear">
|
||||
<option value="1">Option 1</option>
|
||||
<option value="2">Option 2</option>
|
||||
<option value="4">Option 4</option>
|
||||
</select>
|
||||
|
||||
<select id="test_select_element_to_remove">
|
||||
<option value="1">Option 1</option>
|
||||
<option value="2">Option 2</option>
|
||||
<option value="3">Option 3</option>
|
||||
<option value="4">Option 4</option>
|
||||
</select>
|
||||
|
||||
<div id="element-creation-test"></div>
|
||||
|
||||
<button id="a-test-button">I'm a button to be clicked</button>
|
||||
<button>I'm another button you can click</button>
|
||||
<button id="a-third-button">2 is better than 3 :)</button>
|
||||
|
||||
<div id="element-append-tests"></div>
|
||||
<p class="collection"></p>
|
||||
<div class="collection"></div>
|
||||
<h3 class="collection"></h3>
|
||||
|
||||
<div id="element_attribute_tests"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<script defer>
|
||||
console.log("remapping console.log")
|
||||
const terminalDiv = document.getElementById("tests-terminal");
|
||||
const log = console.log.bind(console)
|
||||
let testsStarted = false;
|
||||
console.log = (...args) => {
|
||||
let txt = args.join(" ");
|
||||
let token = "<br>";
|
||||
if (txt.endsWith("FAILED"))
|
||||
token = " ❌<br>";
|
||||
else if (txt.endsWith("PASSED"))
|
||||
token = " ✅<br>";
|
||||
if (testsStarted)
|
||||
terminalDiv.innerHTML += args.join(" ") + token;
|
||||
|
||||
log(...args)
|
||||
|
||||
// if we got the flag that tests are starting, then we can start logging
|
||||
if (args.join(" ") == "tests starting")
|
||||
testsStarted = true;
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,7 +0,0 @@
|
||||
print("tests starting")
|
||||
import pytest
|
||||
from pyscript import window
|
||||
|
||||
args = window.location.search.replace("?", "").split("&")
|
||||
|
||||
pytest.main(args)
|
||||
@@ -1,8 +0,0 @@
|
||||
packages = [
|
||||
"pytest"
|
||||
]
|
||||
|
||||
[[fetch]]
|
||||
from = "tests/"
|
||||
files = ["__init__.py", "conftest.py", "test_dom.py"]
|
||||
to_folder = "tests"
|
||||
@@ -1,15 +0,0 @@
|
||||
import pytest
|
||||
from js import document, localStorage
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def before_tests():
|
||||
"""
|
||||
Ensure browser storage is always reset to empty. Remove the app
|
||||
placeholder. Reset the page title.
|
||||
"""
|
||||
localStorage.clear()
|
||||
# app_placeholder = document.querySelector("pyper-app")
|
||||
# if app_placeholder:
|
||||
# app_placeholder.remove()
|
||||
document.querySelector("title").innerText = "Web API PyTest Suite"
|
||||
@@ -1,460 +0,0 @@
|
||||
from pyscript import document, when
|
||||
from pyscript.web import Element, ElementCollection, div, p, page
|
||||
|
||||
|
||||
class TestDocument:
|
||||
def test__element(self):
|
||||
assert page.body._dom_element == document.body
|
||||
assert page.head._dom_element == document.head
|
||||
|
||||
|
||||
def test_getitem_by_id():
|
||||
# GIVEN an existing element on the page with a known text content
|
||||
id_ = "test_id_selector"
|
||||
txt = "You found test_id_selector"
|
||||
selector = f"#{id_}"
|
||||
# EXPECT the element to be found by id
|
||||
result = page.find(selector)
|
||||
div = result[0]
|
||||
# EXPECT the element text value to match what we expect and what
|
||||
# the JS document.querySelector API would return
|
||||
assert document.querySelector(selector).innerHTML == div.innerHTML == txt
|
||||
# EXPECT the results to be of the right types
|
||||
assert isinstance(div, Element)
|
||||
assert isinstance(result, ElementCollection)
|
||||
|
||||
|
||||
def test_getitem_by_class():
|
||||
ids = [
|
||||
"test_class_selector",
|
||||
"test_selector_w_children",
|
||||
"test_selector_w_children_child_1",
|
||||
]
|
||||
expected_class = "a-test-class"
|
||||
result = page.find(f".{expected_class}")
|
||||
|
||||
# EXPECT to find exact number of elements with the class in the page (== 3)
|
||||
assert len(result) == 3
|
||||
|
||||
# EXPECT that all element ids are in the expected list
|
||||
assert [el.id for el in result] == ids
|
||||
|
||||
|
||||
def test_read_n_write_collection_elements():
|
||||
elements = page.find(".multi-elems")
|
||||
|
||||
for element in elements:
|
||||
assert element.innerHTML == f"Content {element.id.replace('#', '')}"
|
||||
|
||||
new_content = "New Content"
|
||||
elements.innerHTML = new_content
|
||||
for element in elements:
|
||||
assert element.innerHTML == new_content
|
||||
|
||||
|
||||
class TestElement:
|
||||
def test_query(self):
|
||||
# GIVEN an existing element on the page, with at least 1 child element
|
||||
id_ = "test_selector_w_children"
|
||||
parent_div = page.find(f"#{id_}")[0]
|
||||
|
||||
# EXPECT it to be able to query for the first child element
|
||||
div = parent_div.find("div")[0]
|
||||
|
||||
# EXPECT the new element to be associated with the parent
|
||||
assert div.parent == parent_div
|
||||
# EXPECT the new element to be an Element
|
||||
assert isinstance(div, Element)
|
||||
# EXPECT the div attributes to be == to how they are configured in the page
|
||||
assert div.innerHTML == "Child 1"
|
||||
assert div.id == "test_selector_w_children_child_1"
|
||||
|
||||
def test_equality(self):
|
||||
# GIVEN 2 different Elements pointing to the same underlying element
|
||||
id_ = "test_id_selector"
|
||||
selector = f"#{id_}"
|
||||
div = page.find(selector)[0]
|
||||
div2 = page.find(selector)[0]
|
||||
|
||||
# EXPECT them to be equal
|
||||
assert div == div2
|
||||
# EXPECT them to be different objects
|
||||
assert div is not div2
|
||||
|
||||
# EXPECT their value to always be equal
|
||||
assert div.innerHTML == div2.innerHTML
|
||||
div.innerHTML = "some value"
|
||||
|
||||
assert div.innerHTML == div2.innerHTML == "some value"
|
||||
|
||||
def test_append_element(self):
|
||||
id_ = "element-append-tests"
|
||||
div = page.find(f"#{id_}")[0]
|
||||
len_children_before = len(div.children)
|
||||
new_el = p("new element")
|
||||
div.append(new_el)
|
||||
assert len(div.children) == len_children_before + 1
|
||||
assert div.children[-1] == new_el
|
||||
|
||||
def test_append_dom_element_element(self):
|
||||
id_ = "element-append-tests"
|
||||
div = page.find(f"#{id_}")[0]
|
||||
len_children_before = len(div.children)
|
||||
new_el = p("new element")
|
||||
div.append(new_el._dom_element)
|
||||
assert len(div.children) == len_children_before + 1
|
||||
assert div.children[-1] == new_el
|
||||
|
||||
def test_append_collection(self):
|
||||
id_ = "element-append-tests"
|
||||
div = page.find(f"#{id_}")[0]
|
||||
len_children_before = len(div.children)
|
||||
collection = page.find(".collection")
|
||||
div.append(collection)
|
||||
assert len(div.children) == len_children_before + len(collection)
|
||||
|
||||
for i in range(len(collection)):
|
||||
assert div.children[-1 - i] == collection[-1 - i]
|
||||
|
||||
def test_read_classes(self):
|
||||
id_ = "test_class_selector"
|
||||
expected_class = "a-test-class"
|
||||
div = page.find(f"#{id_}")[0]
|
||||
assert div.classes == [expected_class]
|
||||
|
||||
def test_add_remove_class(self):
|
||||
id_ = "div-no-classes"
|
||||
classname = "tester-class"
|
||||
div = page.find(f"#{id_}")[0]
|
||||
assert not div.classes
|
||||
div.classes.add(classname)
|
||||
same_div = page.find(f"#{id_}")[0]
|
||||
assert div.classes == [classname] == same_div.classes
|
||||
div.classes.remove(classname)
|
||||
assert div.classes == [] == same_div.classes
|
||||
|
||||
def test_when_decorator(self):
|
||||
called = False
|
||||
|
||||
just_a_button = page.find("#a-test-button")[0]
|
||||
|
||||
@when("click", just_a_button)
|
||||
def on_click(event):
|
||||
nonlocal called
|
||||
called = True
|
||||
|
||||
# Now let's simulate a click on the button (using the low level JS API)
|
||||
# so we don't risk dom getting in the way
|
||||
assert not called
|
||||
just_a_button._dom_element.click()
|
||||
|
||||
assert called
|
||||
|
||||
def test_inner_html_attribute(self):
|
||||
# GIVEN an existing element on the page with a known empty text content
|
||||
div = page.find("#element_attribute_tests")[0]
|
||||
|
||||
# WHEN we set the html attribute
|
||||
div.innerHTML = "<b>New Content</b>"
|
||||
|
||||
# EXPECT the element html and underlying JS Element innerHTML property
|
||||
# to match what we expect and what
|
||||
assert div.innerHTML == div._dom_element.innerHTML == "<b>New Content</b>"
|
||||
assert div.textContent == div._dom_element.textContent == "New Content"
|
||||
|
||||
def test_text_attribute(self):
|
||||
# GIVEN an existing element on the page with a known empty text content
|
||||
div = page.find("#element_attribute_tests")[0]
|
||||
|
||||
# WHEN we set the html attribute
|
||||
div.textContent = "<b>New Content</b>"
|
||||
|
||||
# EXPECT the element html and underlying JS Element innerHTML property
|
||||
# to match what we expect and what
|
||||
assert (
|
||||
div.innerHTML
|
||||
== div._dom_element.innerHTML
|
||||
== "<b>New Content</b>"
|
||||
)
|
||||
assert div.textContent == div._dom_element.textContent == "<b>New Content</b>"
|
||||
|
||||
|
||||
class TestCollection:
|
||||
def test_iter_eq_children(self):
|
||||
elements = page.find(".multi-elems")
|
||||
assert [el for el in elements] == [el for el in elements.elements]
|
||||
assert len(elements) == 3
|
||||
|
||||
def test_slices(self):
|
||||
elements = page.find(".multi-elems")
|
||||
assert elements[0]
|
||||
_slice = elements[:2]
|
||||
assert len(_slice) == 2
|
||||
for i, el in enumerate(_slice):
|
||||
assert el == elements[i]
|
||||
assert elements[:] == elements
|
||||
|
||||
def test_style_rule(self):
|
||||
selector = ".multi-elems"
|
||||
elements = page.find(selector)
|
||||
for el in elements:
|
||||
assert el.style["background-color"] != "red"
|
||||
|
||||
elements.style["background-color"] = "red"
|
||||
|
||||
for i, el in enumerate(page.find(selector)):
|
||||
assert elements[i].style["background-color"] == "red"
|
||||
assert el.style["background-color"] == "red"
|
||||
|
||||
elements.style.remove("background-color")
|
||||
|
||||
for i, el in enumerate(page.find(selector)):
|
||||
assert el.style["background-color"] != "red"
|
||||
assert elements[i].style["background-color"] != "red"
|
||||
|
||||
def test_when_decorator(self):
|
||||
called = False
|
||||
|
||||
buttons_collection = page.find("button")
|
||||
|
||||
@when("click", buttons_collection)
|
||||
def on_click(event):
|
||||
nonlocal called
|
||||
called = True
|
||||
|
||||
# Now let's simulate a click on the button (using the low level JS API)
|
||||
# so we don't risk dom getting in the way
|
||||
assert not called
|
||||
for button in buttons_collection:
|
||||
button._dom_element.click()
|
||||
assert called
|
||||
called = False
|
||||
|
||||
|
||||
class TestCreation:
|
||||
def test_create_document_element(self):
|
||||
# TODO: This test should probably be removed since it's testing the elements
|
||||
# module.
|
||||
new_el = div("new element")
|
||||
new_el.id = "new_el_id"
|
||||
assert isinstance(new_el, Element)
|
||||
assert new_el._dom_element.tagName == "DIV"
|
||||
# EXPECT the new element to be associated with the document
|
||||
assert new_el.parent is None
|
||||
page.body.append(new_el)
|
||||
|
||||
assert page.find("#new_el_id")[0].parent == page.body
|
||||
|
||||
def test_create_element_child(self):
|
||||
selector = "#element-creation-test"
|
||||
parent_div = page.find(selector)[0]
|
||||
|
||||
# Creating an element from another element automatically creates that element
|
||||
# as a child of the original element
|
||||
new_el = p("a div", classes=["code-description"], innerHTML="Ciao PyScripters!")
|
||||
parent_div.append(new_el)
|
||||
|
||||
assert isinstance(new_el, Element)
|
||||
assert new_el._dom_element.tagName == "P"
|
||||
|
||||
# EXPECT the new element to be associated with the document
|
||||
assert new_el.parent == parent_div
|
||||
assert page.find(selector)[0].children[0] == new_el
|
||||
|
||||
|
||||
class TestInput:
|
||||
input_ids = [
|
||||
"test_rr_input_text",
|
||||
"test_rr_input_button",
|
||||
"test_rr_input_email",
|
||||
"test_rr_input_password",
|
||||
]
|
||||
|
||||
def test_value(self):
|
||||
for id_ in self.input_ids:
|
||||
expected_type = id_.split("_")[-1]
|
||||
result = page.find(f"#{id_}")
|
||||
input_el = result[0]
|
||||
assert input_el._dom_element.type == expected_type
|
||||
assert input_el.value == f"Content {id_}" == input_el._dom_element.value
|
||||
|
||||
# Check that we can set the value
|
||||
new_value = f"New Value {expected_type}"
|
||||
input_el.value = new_value
|
||||
assert input_el.value == new_value
|
||||
|
||||
# Check that we can set the value back to the original using
|
||||
# the collection
|
||||
new_value = f"Content {id_}"
|
||||
result.value = new_value
|
||||
assert input_el.value == new_value
|
||||
|
||||
def test_set_value_collection(self):
|
||||
for id_ in self.input_ids:
|
||||
input_el = page.find(f"#{id_}")
|
||||
|
||||
assert input_el.value[0] == f"Content {id_}" == input_el[0].value
|
||||
|
||||
new_value = f"New Value {id_}"
|
||||
input_el.value = new_value
|
||||
assert input_el.value[0] == new_value == input_el[0].value
|
||||
|
||||
# TODO: We only attach attributes to the classes that have them now which means we
|
||||
# would have to have some other way to help users if using attributes that aren't
|
||||
# actually on the class. Maybe a job for __setattr__?
|
||||
#
|
||||
# def test_element_without_value(self):
|
||||
# result = page.find(f"#tests-terminal"][0]
|
||||
# with pytest.raises(AttributeError):
|
||||
# result.value = "some value"
|
||||
#
|
||||
# def test_element_without_value_via_collection(self):
|
||||
# result = page.find(f"#tests-terminal"]
|
||||
# with pytest.raises(AttributeError):
|
||||
# result.value = "some value"
|
||||
|
||||
|
||||
class TestSelect:
|
||||
def test_select_options_iter(self):
|
||||
select = page.find(f"#test_select_element_w_options")[0]
|
||||
|
||||
for i, option in enumerate(select.options, 1):
|
||||
assert option.value == f"{i}"
|
||||
assert option.innerHTML == f"Option {i}"
|
||||
|
||||
def test_select_options_len(self):
|
||||
select = page.find(f"#test_select_element_w_options")[0]
|
||||
assert len(select.options) == 2
|
||||
|
||||
def test_select_options_clear(self):
|
||||
select = page.find(f"#test_select_element_to_clear")[0]
|
||||
assert len(select.options) == 3
|
||||
|
||||
select.options.clear()
|
||||
|
||||
assert len(select.options) == 0
|
||||
|
||||
def test_select_element_add(self):
|
||||
# GIVEN the existing select element with no options
|
||||
select = page.find(f"#test_select_element")[0]
|
||||
|
||||
# EXPECT the select element to have no options
|
||||
assert len(select.options) == 0
|
||||
|
||||
# WHEN we add an option
|
||||
select.options.add(value="1", html="Option 1")
|
||||
|
||||
# EXPECT the select element to have 1 option matching the attributes
|
||||
# we passed in
|
||||
assert len(select.options) == 1
|
||||
assert select.options[0].value == "1"
|
||||
assert select.options[0].innerHTML == "Option 1"
|
||||
|
||||
# WHEN we add another option (blank this time)
|
||||
select.options.add("")
|
||||
|
||||
# EXPECT the select element to have 2 options
|
||||
assert len(select.options) == 2
|
||||
|
||||
# EXPECT the last option to have an empty value and html
|
||||
assert select.options[1].value == ""
|
||||
assert select.options[1].innerHTML == ""
|
||||
|
||||
# WHEN we add another option (this time adding it in between the other 2
|
||||
# options by using an integer index)
|
||||
select.options.add(value="2", html="Option 2", before=1)
|
||||
|
||||
# EXPECT the select element to have 3 options
|
||||
assert len(select.options) == 3
|
||||
|
||||
# EXPECT the middle option to have the value and html we passed in
|
||||
assert select.options[0].value == "1"
|
||||
assert select.options[0].innerHTML == "Option 1"
|
||||
assert select.options[1].value == "2"
|
||||
assert select.options[1].innerHTML == "Option 2"
|
||||
assert select.options[2].value == ""
|
||||
assert select.options[2].innerHTML == ""
|
||||
|
||||
# WHEN we add another option (this time adding it in between the other 2
|
||||
# options but using the option itself)
|
||||
select.options.add(
|
||||
value="3", html="Option 3", before=select.options[2], selected=True
|
||||
)
|
||||
|
||||
# EXPECT the select element to have 3 options
|
||||
assert len(select.options) == 4
|
||||
|
||||
# EXPECT the middle option to have the value and html we passed in
|
||||
assert select.options[0].value == "1"
|
||||
assert select.options[0].innerHTML == "Option 1"
|
||||
assert (
|
||||
select.options[0].selected
|
||||
== select.options[0]._dom_element.selected
|
||||
== False
|
||||
)
|
||||
assert select.options[1].value == "2"
|
||||
assert select.options[1].innerHTML == "Option 2"
|
||||
assert select.options[2].value == "3"
|
||||
assert select.options[2].innerHTML == "Option 3"
|
||||
assert (
|
||||
select.options[2].selected
|
||||
== select.options[2]._dom_element.selected
|
||||
== True
|
||||
)
|
||||
assert select.options[3].value == ""
|
||||
assert select.options[3].innerHTML == ""
|
||||
|
||||
# WHEN we add another option (this time adding it in between the other 2
|
||||
# options but using the JS element of the option itself)
|
||||
select.options.add(
|
||||
value="2a", html="Option 2a", before=select.options[2]._dom_element
|
||||
)
|
||||
|
||||
# EXPECT the select element to have 3 options
|
||||
assert len(select.options) == 5
|
||||
|
||||
# EXPECT the middle option to have the value and html we passed in
|
||||
assert select.options[0].value == "1"
|
||||
assert select.options[0].innerHTML == "Option 1"
|
||||
assert select.options[1].value == "2"
|
||||
assert select.options[1].innerHTML == "Option 2"
|
||||
assert select.options[2].value == "2a"
|
||||
assert select.options[2].innerHTML == "Option 2a"
|
||||
assert select.options[3].value == "3"
|
||||
assert select.options[3].innerHTML == "Option 3"
|
||||
assert select.options[4].value == ""
|
||||
assert select.options[4].innerHTML == ""
|
||||
|
||||
def test_select_options_remove(self):
|
||||
# GIVEN the existing select element with 3 options
|
||||
select = page.find(f"#test_select_element_to_remove")[0]
|
||||
|
||||
# EXPECT the select element to have 3 options
|
||||
assert len(select.options) == 4
|
||||
# EXPECT the options to have the values originally set
|
||||
assert select.options[0].value == "1"
|
||||
assert select.options[1].value == "2"
|
||||
assert select.options[2].value == "3"
|
||||
assert select.options[3].value == "4"
|
||||
|
||||
# WHEN we remove the second option (index starts at 0)
|
||||
select.options.remove(1)
|
||||
|
||||
# EXPECT the select element to have 2 options
|
||||
assert len(select.options) == 3
|
||||
# EXPECT the options to have the values originally set but the second
|
||||
assert select.options[0].value == "1"
|
||||
assert select.options[1].value == "3"
|
||||
assert select.options[2].value == "4"
|
||||
|
||||
def test_select_get_selected_option(self):
|
||||
# GIVEN the existing select element with one selected option
|
||||
select = page.find(f"#test_select_element_w_options")[0]
|
||||
|
||||
# WHEN we get the selected option
|
||||
selected_option = select.options.selected
|
||||
|
||||
# EXPECT the selected option to be correct
|
||||
assert selected_option.value == "2"
|
||||
assert selected_option.innerHTML == "Option 2"
|
||||
assert selected_option.selected == selected_option._dom_element.selected == True
|
||||
7
pyscript.core/tests/python/example_js_module.js
Normal file
7
pyscript.core/tests/python/example_js_module.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/*
|
||||
A simple JavaScript module to test the integration with Python.
|
||||
*/
|
||||
|
||||
export function hello() {
|
||||
return "Hello from JavaScript!";
|
||||
}
|
||||
7
pyscript.core/tests/python/example_js_worker_module.js
Normal file
7
pyscript.core/tests/python/example_js_worker_module.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/*
|
||||
A simple JavaScript module to test the integration with Python on a worker.
|
||||
*/
|
||||
|
||||
export function hello() {
|
||||
return "Hello from JavaScript in a web worker!";
|
||||
}
|
||||
22
pyscript.core/tests/python/helper.js
Normal file
22
pyscript.core/tests/python/helper.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const qs = new URLSearchParams(location.search);
|
||||
|
||||
// src= to NOT have a config
|
||||
const src = qs.has('src') ? qs.get('src') : './main.py';
|
||||
|
||||
// config= to NOT have a src
|
||||
const config = qs.has('config') ? qs.get('config') : './settings.json';
|
||||
|
||||
// terminal=0 to NOT have a terminal
|
||||
const terminal = qs.has('terminal');
|
||||
|
||||
// worker=1 to have a worker
|
||||
const worker = qs.has('worker');
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.type = qs.get('type') || 'mpy';
|
||||
if (src) script.src = src;
|
||||
if (config) script.setAttribute('config', config);
|
||||
script.toggleAttribute('terminal', terminal);
|
||||
script.toggleAttribute('worker', worker);
|
||||
|
||||
document.write(script.outerHTML);
|
||||
75
pyscript.core/tests/python/index.html
Normal file
75
pyscript.core/tests/python/index.html
Normal file
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Pure Python PyScript tests</title>
|
||||
<link rel="stylesheet" href="../../../dist/core.css">
|
||||
<script type="module" src="../../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script src="./helper.js"></script>
|
||||
<!-- script type="mpy" src="./main.py" config="./settings.json" terminal></script-->
|
||||
<template id="test_card_with_element_template">
|
||||
<p>This is a test. {foo}</p>
|
||||
</template>
|
||||
|
||||
<div id="test_id_selector" style="visibility: hidden;">You found test_id_selector</div>
|
||||
<div id="test_class_selector" class="a-test-class" style="visibility: hidden;">You found test_class_selector</div>
|
||||
<div id="test_selector_w_children" class="a-test-class" style="visibility: hidden;">
|
||||
<div id="test_selector_w_children_child_1" class="a-test-class" style="visibility: hidden;">Child 1</div>
|
||||
<div id="test_selector_w_children_child_2" style="visibility: hidden;">Child 2</div>
|
||||
</div>
|
||||
|
||||
<div id="div-no-classes"></div>
|
||||
|
||||
<div style="visibility: hidden;">
|
||||
<h2>Test Read and Write</h2>
|
||||
<div id="test_rr_div">Content test_rr_div</div>
|
||||
<h3 id="test_rr_h3">Content test_rr_h3</h3>
|
||||
|
||||
<div id="multi-elem-div" class="multi-elems">Content multi-elem-div</div>
|
||||
<p id="multi-elem-p" class="multi-elems">Content multi-elem-p</p>
|
||||
<h2 id="multi-elem-h2" class="multi-elems">Content multi-elem-h2</h2>
|
||||
|
||||
<form>
|
||||
<input id="test_rr_input_text" type="text" value="Content test_rr_input_text">
|
||||
<input id="test_rr_input_button" type="button" value="Content test_rr_input_button">
|
||||
<input id="test_rr_input_email" type="email" value="Content test_rr_input_email">
|
||||
<input id="test_rr_input_password" type="password" value="Content test_rr_input_password">
|
||||
</form>
|
||||
|
||||
<select id="test_select_element"></select>
|
||||
<select id="test_select_element_w_options">
|
||||
<option value="1">Option 1</option>
|
||||
<option value="2" selected="selected">Option 2</option>
|
||||
</select>
|
||||
<select id="test_select_element_to_clear">
|
||||
<option value="1">Option 1</option>
|
||||
<option value="2">Option 2</option>
|
||||
<option value="4">Option 4</option>
|
||||
</select>
|
||||
|
||||
<select id="test_select_element_to_remove">
|
||||
<option value="1">Option 1</option>
|
||||
<option value="2">Option 2</option>
|
||||
<option value="3">Option 3</option>
|
||||
<option value="4">Option 4</option>
|
||||
</select>
|
||||
|
||||
<div id="element-creation-test"></div>
|
||||
|
||||
<button id="a-test-button">I'm a button to be clicked</button>
|
||||
<button>I'm another button you can click</button>
|
||||
<button id="a-third-button">2 is better than 3 :)</button>
|
||||
|
||||
<div id="element-append-tests"></div>
|
||||
<p class="collection"></p>
|
||||
<div class="collection"></div>
|
||||
<h3 class="collection"></h3>
|
||||
|
||||
<div id="element_attribute_tests"></div>
|
||||
</div>
|
||||
<div id="test-element-container"></div>
|
||||
</body>
|
||||
</html>
|
||||
8
pyscript.core/tests/python/main.py
Normal file
8
pyscript.core/tests/python/main.py
Normal file
@@ -0,0 +1,8 @@
|
||||
import json
|
||||
|
||||
import upytest
|
||||
from pyscript import web
|
||||
|
||||
result = await upytest.run("./tests")
|
||||
output = web.div(json.dumps(result), id="result")
|
||||
web.page.append(output)
|
||||
26
pyscript.core/tests/python/settings.json
Normal file
26
pyscript.core/tests/python/settings.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"files": {
|
||||
"https://raw.githubusercontent.com/ntoll/upytest/1.0.7/upytest.py": "",
|
||||
"./tests/test_config.py": "tests/test_config.py",
|
||||
"./tests/test_current_target.py": "tests/test_current_target.py",
|
||||
"./tests/test_display.py": "tests/test_display.py",
|
||||
"./tests/test_document.py": "tests/test_document.py",
|
||||
"./tests/test_fetch.py": "tests/test_fetch.py",
|
||||
"./tests/test_ffi.py": "tests/test_ffi.py",
|
||||
"./tests/test_js_modules.py": "tests/test_js_modules.py",
|
||||
"./tests/test_storage.py": "tests/test_storage.py",
|
||||
"./tests/test_running_in_worker.py": "tests/test_running_in_worker.py",
|
||||
"./tests/test_web.py": "tests/test_web.py",
|
||||
"./tests/test_websocket.py": "tests/test_websocket.py",
|
||||
"./tests/test_when.py": "tests/test_when.py",
|
||||
"./tests/test_window.py": "tests/test_window.py"
|
||||
},
|
||||
"js_modules": {
|
||||
"main": {
|
||||
"./example_js_module.js": "greeting"
|
||||
},
|
||||
"worker": {
|
||||
"./example_js_worker_module.js": "greeting_worker"
|
||||
}
|
||||
}
|
||||
}
|
||||
18
pyscript.core/tests/python/tests/test_config.py
Normal file
18
pyscript.core/tests/python/tests/test_config.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Tests for the pyscript.config dictionary.
|
||||
"""
|
||||
|
||||
from pyscript import config, document, fetch
|
||||
|
||||
|
||||
async def test_config_reads_expected_settings_correctly():
|
||||
"""
|
||||
The config dictionary should read expected settings for this test suite.
|
||||
|
||||
Just grab the raw JSON for the settings and compare it to the config
|
||||
dictionary.
|
||||
"""
|
||||
url = document.location.href.rsplit("/", 1)[0] + "/settings.json"
|
||||
raw_config = await fetch(url).json()
|
||||
for key, value in raw_config.items():
|
||||
assert config[key] == value, f"Expected {key} to be {value}, got {config[key]}"
|
||||
22
pyscript.core/tests/python/tests/test_current_target.py
Normal file
22
pyscript.core/tests/python/tests/test_current_target.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
Ensure the pyscript.current_target function returns the expected target
|
||||
element's id.
|
||||
"""
|
||||
|
||||
from pyscript import RUNNING_IN_WORKER, current_target
|
||||
from upytest import is_micropython
|
||||
|
||||
|
||||
def test_current_target():
|
||||
"""
|
||||
The current_target function should return the expected target element's id.
|
||||
"""
|
||||
expected = "py-0"
|
||||
if is_micropython:
|
||||
if RUNNING_IN_WORKER:
|
||||
expected = "mpy-w0-target"
|
||||
else:
|
||||
expected = "mpy-0"
|
||||
elif RUNNING_IN_WORKER:
|
||||
expected = "py-w0-target"
|
||||
assert current_target() == expected, f"Expected {expected} got {current_target()}"
|
||||
289
pyscript.core/tests/python/tests/test_display.py
Normal file
289
pyscript.core/tests/python/tests/test_display.py
Normal file
@@ -0,0 +1,289 @@
|
||||
"""
|
||||
Tests for the display function in PyScript.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
import upytest
|
||||
from pyscript import HTML, RUNNING_IN_WORKER, display, py_import, web
|
||||
|
||||
|
||||
async def get_display_container():
|
||||
"""
|
||||
Get the element that contains the output of the display function.
|
||||
"""
|
||||
if RUNNING_IN_WORKER:
|
||||
# Needed to ensure the DOM has time to catch up with the display calls
|
||||
# made in the worker thread.
|
||||
await asyncio.sleep(0.01)
|
||||
py_display = web.page.find("script-py")
|
||||
if len(py_display) == 1:
|
||||
return py_display[0]
|
||||
mpy_display = web.page.find("script-mpy")
|
||||
if len(mpy_display) == 1:
|
||||
return mpy_display[0]
|
||||
return None
|
||||
|
||||
|
||||
async def setup():
|
||||
"""
|
||||
Setup function for the test_display.py module. Remove all references to the
|
||||
display output in the DOM so we always start from a clean state.
|
||||
"""
|
||||
container = await get_display_container()
|
||||
if container:
|
||||
container.replaceChildren()
|
||||
target_container = web.page.find("#test-element-container")[0]
|
||||
target_container.innerHTML = ""
|
||||
|
||||
|
||||
async def teardown():
|
||||
"""
|
||||
Like setup.
|
||||
"""
|
||||
container = await get_display_container()
|
||||
if container:
|
||||
container.replaceChildren()
|
||||
target_container = web.page.find("#test-element-container")[0]
|
||||
target_container.innerHTML = ""
|
||||
|
||||
|
||||
async def test_simple_display():
|
||||
"""
|
||||
Test the display function with a simple string.
|
||||
"""
|
||||
display("Hello, world")
|
||||
container = await get_display_container()
|
||||
assert len(container.children) == 1, "Expected one child in the display container."
|
||||
assert (
|
||||
container.children[0].tagName == "DIV"
|
||||
), "Expected a div element in the display container."
|
||||
assert container.children[0].innerHTML == "Hello, world"
|
||||
|
||||
|
||||
async def test_consecutive_display():
|
||||
"""
|
||||
Display order should be preserved.
|
||||
"""
|
||||
display("hello 1")
|
||||
display("hello 2")
|
||||
container = await get_display_container()
|
||||
assert (
|
||||
len(container.children) == 2
|
||||
), "Expected two children in the display container."
|
||||
assert container.children[0].innerHTML == "hello 1"
|
||||
assert container.children[1].innerHTML == "hello 2"
|
||||
|
||||
|
||||
def test_target_parameter():
|
||||
"""
|
||||
The output from display is placed in the target element.
|
||||
"""
|
||||
display("hello world", target="test-element-container")
|
||||
target = web.page.find("#test-element-container")[0]
|
||||
assert target.innerText == "hello world"
|
||||
|
||||
|
||||
def test_target_parameter_with_hash():
|
||||
"""
|
||||
The target parameter can have a hash in front of it.
|
||||
"""
|
||||
display("hello world", target="#test-element-container")
|
||||
target = web.page.find("#test-element-container")[0]
|
||||
assert target.innerText == "hello world"
|
||||
|
||||
|
||||
def test_non_existing_id_target_raises_value_error():
|
||||
"""
|
||||
If the target parameter is set to a non-existing element, a ValueError should be raised.
|
||||
"""
|
||||
with upytest.raises(ValueError):
|
||||
display("hello world", target="non-existing")
|
||||
|
||||
|
||||
def test_empty_string_target_raises_value_error():
|
||||
"""
|
||||
If the target parameter is an empty string, a ValueError should be raised.
|
||||
"""
|
||||
with upytest.raises(ValueError) as exc:
|
||||
display("hello world", target="")
|
||||
assert str(exc.exception) == "Cannot have an empty target"
|
||||
|
||||
|
||||
def test_non_string_target_values_raise_typerror():
|
||||
"""
|
||||
The target parameter must be a string.
|
||||
"""
|
||||
with upytest.raises(TypeError) as exc:
|
||||
display("hello world", target=True)
|
||||
assert str(exc.exception) == "target must be str or None, not bool"
|
||||
|
||||
with upytest.raises(TypeError) as exc:
|
||||
display("hello world", target=123)
|
||||
assert str(exc.exception) == "target must be str or None, not int"
|
||||
|
||||
|
||||
async def test_tag_target_attribute():
|
||||
"""
|
||||
The order and arrangement of the display calls (including targets) should be preserved.
|
||||
"""
|
||||
display("item 1")
|
||||
display("item 2", target="test-element-container")
|
||||
display("item 3")
|
||||
container = await get_display_container()
|
||||
assert (
|
||||
len(container.children) == 2
|
||||
), "Expected two children in the display container."
|
||||
assert container.children[0].innerHTML == "item 1"
|
||||
assert container.children[1].innerHTML == "item 3"
|
||||
target = web.page.find("#test-element-container")[0]
|
||||
assert target.innerText == "item 2"
|
||||
|
||||
|
||||
async def test_multiple_display_calls_same_tag():
|
||||
"""
|
||||
Multiple display calls in the same script tag should be displayed in order.
|
||||
"""
|
||||
display("item 1")
|
||||
display("item 2")
|
||||
container = await get_display_container()
|
||||
assert (
|
||||
len(container.children) == 2
|
||||
), "Expected two children in the display container."
|
||||
assert container.children[0].innerHTML == "item 1"
|
||||
assert container.children[1].innerHTML == "item 2"
|
||||
|
||||
|
||||
async def test_append_true():
|
||||
"""
|
||||
Explicit append flag as true should append to the expected container element.
|
||||
"""
|
||||
display("item 1", append=True)
|
||||
display("item 2", append=True)
|
||||
container = await get_display_container()
|
||||
assert (
|
||||
len(container.children) == 2
|
||||
), "Expected two children in the display container."
|
||||
assert container.children[0].innerHTML == "item 1"
|
||||
assert container.children[1].innerHTML == "item 2"
|
||||
|
||||
|
||||
async def test_append_false():
|
||||
"""
|
||||
Explicit append flag as false should replace the expected container element.
|
||||
"""
|
||||
display("item 1", append=False)
|
||||
display("item 2", append=False)
|
||||
container = await get_display_container()
|
||||
assert container.innerText == "item 2"
|
||||
|
||||
|
||||
async def test_display_multiple_values():
|
||||
"""
|
||||
Display multiple values in the same call.
|
||||
"""
|
||||
display("hello", "world")
|
||||
container = await get_display_container()
|
||||
assert container.innerText == "hello\nworld", container.innerText
|
||||
|
||||
|
||||
async def test_display_multiple_append_false():
|
||||
display("hello", "world", append=False)
|
||||
container = await get_display_container()
|
||||
assert container.innerText == "world"
|
||||
|
||||
|
||||
def test_display_multiple_append_false_with_target():
|
||||
"""
|
||||
TODO: this is a display.py issue to fix when append=False is used
|
||||
do not use the first element, just clean up and then append
|
||||
remove the # display comment once that's done
|
||||
"""
|
||||
|
||||
class Circle:
|
||||
r = 0
|
||||
|
||||
def _repr_svg_(self):
|
||||
return (
|
||||
f'<svg height="{self.r*2}" width="{self.r*2}">'
|
||||
f'<circle cx="{self.r}" cy="{self.r}" r="{self.r}" fill="red"></circle></svg>'
|
||||
)
|
||||
|
||||
circle = Circle()
|
||||
circle.r += 5
|
||||
display(circle, circle, target="test-element-container", append=False)
|
||||
target = web.page.find("#test-element-container")[0]
|
||||
assert target.innerHTML == circle._repr_svg_()
|
||||
|
||||
|
||||
async def test_display_list_dict_tuple():
|
||||
"""
|
||||
Display a list, dictionary, and tuple with the expected __repr__.
|
||||
|
||||
NOTE: MicroPython doesn't (yet) have ordered dicts. Hence the rather odd
|
||||
check that the dictionary is displayed as a string.
|
||||
"""
|
||||
l = ["A", 1, "!"]
|
||||
d = {"B": 2, "List": l}
|
||||
t = ("C", 3, "!")
|
||||
display(l, d, t)
|
||||
container = await get_display_container()
|
||||
l2, d2, t2 = container.innerText.split("\n")
|
||||
assert l == eval(l2)
|
||||
assert d == eval(d2)
|
||||
assert t == eval(t2)
|
||||
|
||||
|
||||
async def test_display_should_escape():
|
||||
display("<p>hello world</p>")
|
||||
container = await get_display_container()
|
||||
assert container[0].innerHTML == "<p>hello world</p>"
|
||||
assert container.innerText == "<p>hello world</p>"
|
||||
|
||||
|
||||
async def test_display_HTML():
|
||||
display(HTML("<p>hello world</p>"))
|
||||
container = await get_display_container()
|
||||
assert container[0].innerHTML == "<p>hello world</p>"
|
||||
assert container.innerText == "hello world"
|
||||
|
||||
|
||||
@upytest.skip(
|
||||
"Pyodide main thread only",
|
||||
skip_when=upytest.is_micropython or RUNNING_IN_WORKER,
|
||||
)
|
||||
async def test_image_display():
|
||||
"""
|
||||
Check an image is displayed correctly.
|
||||
"""
|
||||
mpl = await py_import("matplotlib")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
xpoints = [3, 6, 9]
|
||||
ypoints = [1, 2, 3]
|
||||
plt.plot(xpoints, ypoints)
|
||||
display(plt)
|
||||
container = await get_display_container()
|
||||
img = container.find("img")[0]
|
||||
img_src = img.getAttribute("src").replace(
|
||||
"data:image/png;charset=utf-8;base64,", ""
|
||||
)
|
||||
assert len(img_src) > 0
|
||||
|
||||
|
||||
@upytest.skip(
|
||||
"Pyodide main thread only",
|
||||
skip_when=upytest.is_micropython or RUNNING_IN_WORKER,
|
||||
)
|
||||
async def test_image_renders_correctly():
|
||||
"""
|
||||
This is just a sanity check to make sure that images are rendered
|
||||
in a reasonable way.
|
||||
"""
|
||||
from PIL import Image
|
||||
|
||||
img = Image.new("RGB", (4, 4), color=(0, 0, 0))
|
||||
display(img, target="test-element-container", append=False)
|
||||
target = web.page.find("#test-element-container")[0]
|
||||
img = target.find("img")[0]
|
||||
assert img.src.startswith("data:image/png;charset=utf-8;base64")
|
||||
17
pyscript.core/tests/python/tests/test_document.py
Normal file
17
pyscript.core/tests/python/tests/test_document.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
Sanity check for the pyscript.document object.
|
||||
"""
|
||||
|
||||
from pyscript import document
|
||||
|
||||
|
||||
def test_document():
|
||||
"""
|
||||
The document object should be available and we can change its attributes
|
||||
(in this case, the title).
|
||||
"""
|
||||
title = document.title
|
||||
assert title
|
||||
document.title = "A new title"
|
||||
assert document.title == "A new title"
|
||||
document.title = title
|
||||
83
pyscript.core/tests/python/tests/test_fetch.py
Normal file
83
pyscript.core/tests/python/tests/test_fetch.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
Ensure the pyscript.test function behaves as expected.
|
||||
"""
|
||||
|
||||
from pyscript import fetch
|
||||
|
||||
|
||||
async def test_fetch_json():
|
||||
"""
|
||||
The fetch function should return the expected JSON response.
|
||||
"""
|
||||
response = await fetch("https://jsonplaceholder.typicode.com/todos/1")
|
||||
assert response.ok
|
||||
data = await response.json()
|
||||
assert data["userId"] == 1
|
||||
assert data["id"] == 1
|
||||
assert data["title"] == "delectus aut autem"
|
||||
assert data["completed"] is False
|
||||
|
||||
|
||||
async def test_fetch_text():
|
||||
"""
|
||||
The fetch function should return the expected text response.
|
||||
"""
|
||||
response = await fetch("https://jsonplaceholder.typicode.com/todos/1")
|
||||
assert response.ok
|
||||
text = await response.text()
|
||||
assert "delectus aut autem" in text
|
||||
assert "completed" in text
|
||||
assert "false" in text
|
||||
assert "1" in text
|
||||
|
||||
|
||||
async def test_fetch_bytearray():
|
||||
"""
|
||||
The fetch function should return the expected bytearray response.
|
||||
"""
|
||||
response = await fetch("https://jsonplaceholder.typicode.com/todos/1")
|
||||
assert response.ok
|
||||
data = await response.bytearray()
|
||||
assert b"delectus aut autem" in data
|
||||
assert b"completed" in data
|
||||
assert b"false" in data
|
||||
assert b"1" in data
|
||||
|
||||
|
||||
async def test_fetch_array_buffer():
|
||||
"""
|
||||
The fetch function should return the expected array buffer response.
|
||||
"""
|
||||
response = await fetch("https://jsonplaceholder.typicode.com/todos/1")
|
||||
assert response.ok
|
||||
data = await response.arrayBuffer()
|
||||
bytes_ = bytes(data)
|
||||
assert b"delectus aut autem" in bytes_
|
||||
assert b"completed" in bytes_
|
||||
assert b"false" in bytes_
|
||||
assert b"1" in bytes_
|
||||
|
||||
|
||||
async def test_fetch_ok():
|
||||
"""
|
||||
The fetch function should return a response with ok set to True for an
|
||||
existing URL.
|
||||
"""
|
||||
response = await fetch("https://jsonplaceholder.typicode.com/todos/1")
|
||||
assert response.ok
|
||||
assert response.status == 200
|
||||
data = await response.json()
|
||||
assert data["userId"] == 1
|
||||
assert data["id"] == 1
|
||||
assert data["title"] == "delectus aut autem"
|
||||
assert data["completed"] is False
|
||||
|
||||
|
||||
async def test_fetch_not_ok():
|
||||
"""
|
||||
The fetch function should return a response with ok set to False for a
|
||||
non-existent URL.
|
||||
"""
|
||||
response = await fetch("https://jsonplaceholder.typicode.com/todos/1000")
|
||||
assert not response.ok
|
||||
assert response.status == 404
|
||||
40
pyscript.core/tests/python/tests/test_ffi.py
Normal file
40
pyscript.core/tests/python/tests/test_ffi.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
Exercise (as much as is possible) the pyscript.ffi namespace.
|
||||
"""
|
||||
|
||||
import upytest
|
||||
from pyscript import ffi
|
||||
|
||||
|
||||
def test_create_proxy():
|
||||
"""
|
||||
The create_proxy function should return a proxy object that is callable.
|
||||
"""
|
||||
|
||||
def func():
|
||||
return 42
|
||||
|
||||
proxy = ffi.create_proxy(func)
|
||||
assert proxy() == 42
|
||||
if upytest.is_micropython:
|
||||
from jsffi import JsProxy
|
||||
else:
|
||||
from pyodide.ffi import JsProxy
|
||||
assert isinstance(proxy, JsProxy)
|
||||
|
||||
|
||||
def test_to_js():
|
||||
"""
|
||||
The to_js function should convert a Python object to a JavaScript object.
|
||||
In this instance, a Python dict should be converted to a JavaScript object
|
||||
represented by a JsProxy object.
|
||||
"""
|
||||
obj = {"a": 1, "b": 2}
|
||||
js_obj = ffi.to_js(obj)
|
||||
assert js_obj.a == 1
|
||||
assert js_obj.b == 2
|
||||
if upytest.is_micropython:
|
||||
from jsffi import JsProxy
|
||||
else:
|
||||
from pyodide.ffi import JsProxy
|
||||
assert isinstance(js_obj, JsProxy)
|
||||
52
pyscript.core/tests/python/tests/test_js_modules.py
Normal file
52
pyscript.core/tests/python/tests/test_js_modules.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Ensure referenced JavaScript modules are available via the pyscript.js_modules
|
||||
object.
|
||||
"""
|
||||
|
||||
import upytest
|
||||
from pyscript import RUNNING_IN_WORKER
|
||||
|
||||
|
||||
@upytest.skip("Main thread only.", skip_when=RUNNING_IN_WORKER)
|
||||
def test_js_module_is_available_on_main():
|
||||
"""
|
||||
The "hello" function in the example_js_module.js file is available via the
|
||||
js_modules object while running in the main thread. See the settings.json
|
||||
file for the configuration that makes this possible.
|
||||
"""
|
||||
from pyscript.js_modules import greeting
|
||||
|
||||
assert greeting.hello() == "Hello from JavaScript!"
|
||||
|
||||
|
||||
@upytest.skip("Worker only.", skip_when=not RUNNING_IN_WORKER)
|
||||
def test_js_module_is_available_on_worker():
|
||||
"""
|
||||
The "hello" function in the example_js_module.js file is available via the
|
||||
js_modules object while running in a worker. See the settings.json file for
|
||||
the configuration that makes this possible.
|
||||
"""
|
||||
from pyscript.js_modules import greeting
|
||||
|
||||
assert greeting.hello() == "Hello from JavaScript!"
|
||||
|
||||
|
||||
@upytest.skip("Worker only.", skip_when=not RUNNING_IN_WORKER)
|
||||
def test_js_module_is_available_on_worker():
|
||||
"""
|
||||
The "hello" function in the example_js_worker_module.js file is available
|
||||
via the js_modules object while running in a worker.
|
||||
"""
|
||||
from pyscript.js_modules import greeting_worker
|
||||
|
||||
assert greeting_worker.hello() == "Hello from JavaScript in a web worker!"
|
||||
|
||||
|
||||
@upytest.skip("Main thread only.", skip_when=RUNNING_IN_WORKER)
|
||||
def test_js_worker_module_is_not_available_on_main():
|
||||
"""
|
||||
The "hello" function in the example_js_worker_module.js file is not
|
||||
available via the js_modules object while running in the main thread.
|
||||
"""
|
||||
with upytest.raises(ImportError):
|
||||
from pyscript.js_modules import greeting_worker
|
||||
27
pyscript.core/tests/python/tests/test_running_in_worker.py
Normal file
27
pyscript.core/tests/python/tests/test_running_in_worker.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
Ensure the pyscript.RUNNING_IN_WORKER flag is set correctly (a sanity check).
|
||||
"""
|
||||
|
||||
import upytest
|
||||
from pyscript import RUNNING_IN_WORKER, document
|
||||
|
||||
# In the test suite, running in a worker is flagged by the presence of the
|
||||
# "worker" query string. We do this to avoid using RUNNING_IN_WORKER to skip
|
||||
# tests that check RUNNING_IN_WORKER.
|
||||
in_worker = "worker" in document.location.search.lower()
|
||||
|
||||
|
||||
@upytest.skip("Main thread only.", skip_when=in_worker)
|
||||
def test_running_in_main():
|
||||
"""
|
||||
The flag should be False.
|
||||
"""
|
||||
assert RUNNING_IN_WORKER is False
|
||||
|
||||
|
||||
@upytest.skip("Worker only.", skip_when=not in_worker)
|
||||
def test_running_in_worker():
|
||||
"""
|
||||
The flag should be True.
|
||||
"""
|
||||
assert RUNNING_IN_WORKER is True
|
||||
89
pyscript.core/tests/python/tests/test_storage.py
Normal file
89
pyscript.core/tests/python/tests/test_storage.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
Ensure the pyscript.storage object behaves as a Python dict.
|
||||
"""
|
||||
|
||||
from pyscript import Storage, storage
|
||||
|
||||
test_store = None
|
||||
|
||||
|
||||
async def setup():
|
||||
global test_store
|
||||
if test_store is None:
|
||||
test_store = await storage("test_store")
|
||||
test_store.clear()
|
||||
await test_store.sync()
|
||||
|
||||
|
||||
async def teardown():
|
||||
if test_store:
|
||||
test_store.clear()
|
||||
await test_store.sync()
|
||||
|
||||
|
||||
async def test_storage_as_dict():
|
||||
"""
|
||||
The storage object should behave as a Python dict.
|
||||
"""
|
||||
# Assign
|
||||
test_store["a"] = 1
|
||||
# Retrieve
|
||||
assert test_store["a"] == 1
|
||||
assert "a" in test_store
|
||||
assert len(test_store) == 1
|
||||
# Iterate
|
||||
for k, v in test_store.items():
|
||||
assert k == "a"
|
||||
assert v == 1
|
||||
# Remove
|
||||
del test_store["a"]
|
||||
assert "a" not in test_store
|
||||
assert len(test_store) == 0
|
||||
|
||||
|
||||
async def test_storage_types():
|
||||
"""
|
||||
The storage object should support different types of values.
|
||||
"""
|
||||
test_store["boolean"] = False
|
||||
test_store["integer"] = 42
|
||||
test_store["float"] = 3.14
|
||||
test_store["string"] = "hello"
|
||||
test_store["none"] = None
|
||||
test_store["list"] = [1, 2, 3]
|
||||
test_store["dict"] = {"a": 1, "b": 2}
|
||||
test_store["tuple"] = (1, 2, 3)
|
||||
test_store["bytearray"] = bytearray(b"hello")
|
||||
test_store["memoryview"] = memoryview(b"hello")
|
||||
await test_store.sync()
|
||||
assert test_store["boolean"] is False
|
||||
assert isinstance(test_store["boolean"], bool)
|
||||
assert test_store["integer"] == 42
|
||||
assert isinstance(test_store["integer"], int)
|
||||
assert test_store["float"] == 3.14
|
||||
assert isinstance(test_store["float"], float)
|
||||
assert test_store["string"] == "hello"
|
||||
assert isinstance(test_store["string"], str)
|
||||
assert test_store["none"] is None
|
||||
assert isinstance(test_store["none"], type(None))
|
||||
assert test_store["list"] == [1, 2, 3]
|
||||
assert isinstance(test_store["list"], list)
|
||||
assert test_store["dict"] == {"a": 1, "b": 2}
|
||||
assert isinstance(test_store["dict"], dict)
|
||||
assert test_store["tuple"] == (1, 2, 3)
|
||||
assert isinstance(test_store["tuple"], tuple)
|
||||
assert test_store["bytearray"] == bytearray(b"hello")
|
||||
assert isinstance(test_store["bytearray"], bytearray)
|
||||
assert test_store["memoryview"] == memoryview(b"hello")
|
||||
assert isinstance(test_store["memoryview"], memoryview)
|
||||
|
||||
|
||||
async def test_storage_clear():
|
||||
"""
|
||||
The clear method should remove all items from the storage object.
|
||||
"""
|
||||
test_store["a"] = 1
|
||||
test_store["b"] = 2
|
||||
assert len(test_store) == 2
|
||||
test_store.clear()
|
||||
assert len(test_store) == 0
|
||||
1143
pyscript.core/tests/python/tests/test_web.py
Normal file
1143
pyscript.core/tests/python/tests/test_web.py
Normal file
File diff suppressed because it is too large
Load Diff
99
pyscript.core/tests/python/tests/test_websocket.py
Normal file
99
pyscript.core/tests/python/tests/test_websocket.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Exercise the pyscript.Websocket class.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from pyscript import WebSocket
|
||||
|
||||
|
||||
async def test_websocket_with_attributes():
|
||||
"""
|
||||
Event handlers assigned via object attributes.
|
||||
|
||||
The Websocket class should be able to connect to a websocket server and
|
||||
send and receive messages.
|
||||
|
||||
Use of echo.websocket.org means:
|
||||
|
||||
1) When connecting it responds with a "Request served by" message.
|
||||
2) When sending a message it echos it back.
|
||||
"""
|
||||
connected_flag = False
|
||||
closed_flag = False
|
||||
messages = []
|
||||
ready_to_test = asyncio.Event()
|
||||
|
||||
def on_open(event):
|
||||
nonlocal connected_flag
|
||||
connected_flag = True
|
||||
ws.send("Hello, world!") # A message to echo.
|
||||
|
||||
def on_message(event):
|
||||
messages.append(event.data)
|
||||
if len(messages) == 2: # We're done.
|
||||
ws.close()
|
||||
|
||||
def on_close(event):
|
||||
nonlocal closed_flag
|
||||
closed_flag = True
|
||||
ready_to_test.set() # Finished!
|
||||
|
||||
ws = WebSocket(url="wss://echo.websocket.org")
|
||||
ws.onopen = on_open
|
||||
ws.onmessage = on_message
|
||||
ws.onclose = on_close
|
||||
# Wait for everything to be finished.
|
||||
await ready_to_test.wait()
|
||||
assert connected_flag is True
|
||||
assert len(messages) == 2
|
||||
assert "request served by" in messages[0].lower()
|
||||
assert messages[1] == "Hello, world!"
|
||||
assert closed_flag is True
|
||||
|
||||
|
||||
async def test_websocket_with_init():
|
||||
"""
|
||||
Event handlers assigned via __init__ arguments.
|
||||
|
||||
The Websocket class should be able to connect to a websocket server and
|
||||
send and receive messages.
|
||||
|
||||
Use of echo.websocket.org means:
|
||||
|
||||
1) When connecting it responds with a "Request served by" message.
|
||||
2) When sending a message it echos it back.
|
||||
"""
|
||||
connected_flag = False
|
||||
closed_flag = False
|
||||
messages = []
|
||||
ready_to_test = asyncio.Event()
|
||||
|
||||
def on_open(event):
|
||||
nonlocal connected_flag
|
||||
connected_flag = True
|
||||
ws.send("Hello, world!") # A message to echo.
|
||||
|
||||
def on_message(event):
|
||||
messages.append(event.data)
|
||||
if len(messages) == 2: # We're done.
|
||||
ws.close()
|
||||
|
||||
def on_close(event):
|
||||
nonlocal closed_flag
|
||||
closed_flag = True
|
||||
ready_to_test.set() # Finished!
|
||||
|
||||
ws = WebSocket(
|
||||
url="wss://echo.websocket.org",
|
||||
onopen=on_open,
|
||||
onmessage=on_message,
|
||||
onclose=on_close,
|
||||
)
|
||||
# Wait for everything to be finished.
|
||||
await ready_to_test.wait()
|
||||
assert connected_flag is True
|
||||
assert len(messages) == 2
|
||||
assert "request served by" in messages[0].lower()
|
||||
assert messages[1] == "Hello, world!"
|
||||
assert closed_flag is True
|
||||
216
pyscript.core/tests/python/tests/test_when.py
Normal file
216
pyscript.core/tests/python/tests/test_when.py
Normal file
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
Tests for the pyscript.when decorator.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
import upytest
|
||||
from pyscript import RUNNING_IN_WORKER, web
|
||||
|
||||
|
||||
def get_container():
|
||||
return web.page.find("#test-element-container")[0]
|
||||
|
||||
|
||||
def setup():
|
||||
container = get_container()
|
||||
container.innerHTML = ""
|
||||
|
||||
|
||||
def teardown():
|
||||
container = get_container()
|
||||
container.innerHTML = ""
|
||||
|
||||
|
||||
async def test_when_decorator_with_event():
|
||||
"""
|
||||
When the decorated function takes a single parameter,
|
||||
it should be passed the event object
|
||||
"""
|
||||
btn = web.button("foo_button", id="foo_id")
|
||||
container = get_container()
|
||||
container.append(btn)
|
||||
|
||||
called = False
|
||||
call_flag = asyncio.Event()
|
||||
|
||||
@web.when("click", selector="#foo_id")
|
||||
def foo(evt):
|
||||
nonlocal called
|
||||
called = evt
|
||||
call_flag.set()
|
||||
|
||||
btn.click()
|
||||
await call_flag.wait()
|
||||
assert called.target.id == "foo_id"
|
||||
|
||||
|
||||
async def test_when_decorator_without_event():
|
||||
"""
|
||||
When the decorated function takes no parameters (not including 'self'),
|
||||
it should be called without the event object
|
||||
"""
|
||||
btn = web.button("foo_button", id="foo_id")
|
||||
container = get_container()
|
||||
container.append(btn)
|
||||
|
||||
called = False
|
||||
call_flag = asyncio.Event()
|
||||
|
||||
@web.when("click", selector="#foo_id")
|
||||
def foo():
|
||||
nonlocal called
|
||||
called = True
|
||||
call_flag.set()
|
||||
|
||||
btn.click()
|
||||
await call_flag.wait()
|
||||
assert called
|
||||
|
||||
|
||||
async def test_two_when_decorators():
|
||||
"""
|
||||
When decorating a function twice, both should function
|
||||
"""
|
||||
btn = web.button("foo_button", id="foo_id")
|
||||
container = get_container()
|
||||
container.append(btn)
|
||||
|
||||
called1 = False
|
||||
called2 = False
|
||||
call_flag1 = asyncio.Event()
|
||||
call_flag2 = asyncio.Event()
|
||||
|
||||
@web.when("click", selector="#foo_id")
|
||||
def foo1(evt):
|
||||
nonlocal called1
|
||||
called1 = True
|
||||
call_flag1.set()
|
||||
|
||||
@web.when("click", selector="#foo_id")
|
||||
def foo2(evt):
|
||||
nonlocal called2
|
||||
called2 = True
|
||||
call_flag2.set()
|
||||
|
||||
btn.click()
|
||||
await call_flag1.wait()
|
||||
await call_flag2.wait()
|
||||
assert called1
|
||||
assert called2
|
||||
|
||||
|
||||
async def test_two_when_decorators_same_element():
|
||||
"""
|
||||
When decorating a function twice *on the same DOM element*, both should
|
||||
function
|
||||
"""
|
||||
btn = web.button("foo_button", id="foo_id")
|
||||
container = get_container()
|
||||
container.append(btn)
|
||||
|
||||
counter = 0
|
||||
call_flag = asyncio.Event()
|
||||
|
||||
@web.when("click", selector="#foo_id")
|
||||
@web.when("click", selector="#foo_id")
|
||||
def foo(evt):
|
||||
nonlocal counter
|
||||
counter += 1
|
||||
call_flag.set()
|
||||
|
||||
assert counter == 0, counter
|
||||
btn.click()
|
||||
await call_flag.wait()
|
||||
assert counter == 2, counter
|
||||
|
||||
|
||||
async def test_when_decorator_multiple_elements():
|
||||
"""
|
||||
The @when decorator's selector should successfully select multiple
|
||||
DOM elements
|
||||
"""
|
||||
btn1 = web.button(
|
||||
"foo_button1",
|
||||
id="foo_id1",
|
||||
classes=[
|
||||
"foo_class",
|
||||
],
|
||||
)
|
||||
btn2 = web.button(
|
||||
"foo_button2",
|
||||
id="foo_id2",
|
||||
classes=[
|
||||
"foo_class",
|
||||
],
|
||||
)
|
||||
container = get_container()
|
||||
container.append(btn1)
|
||||
container.append(btn2)
|
||||
|
||||
counter = 0
|
||||
call_flag1 = asyncio.Event()
|
||||
call_flag2 = asyncio.Event()
|
||||
|
||||
@web.when("click", selector=".foo_class")
|
||||
def foo(evt):
|
||||
nonlocal counter
|
||||
counter += 1
|
||||
if evt.target.id == "foo_id1":
|
||||
call_flag1.set()
|
||||
else:
|
||||
call_flag2.set()
|
||||
|
||||
assert counter == 0, counter
|
||||
btn1.click()
|
||||
await call_flag1.wait()
|
||||
assert counter == 1, counter
|
||||
btn2.click()
|
||||
await call_flag2.wait()
|
||||
assert counter == 2, counter
|
||||
|
||||
|
||||
async def test_when_decorator_duplicate_selectors():
|
||||
"""
|
||||
When is not idempotent, so it should be possible to add multiple
|
||||
@when decorators with the same selector.
|
||||
"""
|
||||
btn = web.button("foo_button", id="foo_id")
|
||||
container = get_container()
|
||||
container.append(btn)
|
||||
|
||||
counter = 0
|
||||
call_flag = asyncio.Event()
|
||||
|
||||
@web.when("click", selector="#foo_id")
|
||||
@web.when("click", selector="#foo_id") # duplicate
|
||||
def foo1(evt):
|
||||
nonlocal counter
|
||||
counter += 1
|
||||
call_flag.set()
|
||||
|
||||
assert counter == 0, counter
|
||||
btn.click()
|
||||
await call_flag.wait()
|
||||
assert counter == 2, counter
|
||||
|
||||
|
||||
@upytest.skip(
|
||||
"Only works in Pyodide on main thread",
|
||||
skip_when=upytest.is_micropython or RUNNING_IN_WORKER,
|
||||
)
|
||||
def test_when_decorator_invalid_selector():
|
||||
"""
|
||||
When the selector parameter of @when is invalid, it should raise an error.
|
||||
"""
|
||||
if upytest.is_micropython:
|
||||
from jsffi import JsException
|
||||
else:
|
||||
from pyodide.ffi import JsException
|
||||
|
||||
with upytest.raises(JsException) as e:
|
||||
|
||||
@web.when("click", selector="#.bad")
|
||||
def foo(evt): ...
|
||||
|
||||
assert "'#.bad' is not a valid selector" in str(e.exception), str(e.exception)
|
||||
25
pyscript.core/tests/python/tests/test_window.py
Normal file
25
pyscript.core/tests/python/tests/test_window.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
Ensure the pyscript.window object refers to the main thread's window object.
|
||||
"""
|
||||
|
||||
import upytest
|
||||
from pyscript import RUNNING_IN_WORKER, window
|
||||
|
||||
|
||||
@upytest.skip("Main thread only.", skip_when=RUNNING_IN_WORKER)
|
||||
def test_window_in_main_thread():
|
||||
"""
|
||||
The window object should refer to the main thread's window object.
|
||||
"""
|
||||
# The window will have a document.
|
||||
assert window.document
|
||||
|
||||
|
||||
@upytest.skip("Worker only.", skip_when=not RUNNING_IN_WORKER)
|
||||
def test_window_in_worker():
|
||||
"""
|
||||
The window object should refer to the worker's self object, even though
|
||||
this code is running in a web worker.
|
||||
"""
|
||||
# The window will have a document.
|
||||
assert window.document
|
||||
@@ -1,251 +0,0 @@
|
||||
try:
|
||||
from textwrap import dedent
|
||||
except ImportError:
|
||||
dedent = lambda x: x
|
||||
|
||||
import examples
|
||||
import shoelace
|
||||
import styles
|
||||
from markdown import markdown
|
||||
from pyscript import when, window
|
||||
from pyweb import pydom
|
||||
from pyweb.ui import elements as el
|
||||
from pyweb.ui.elements import a, button, div, grid, h1, h2, h3
|
||||
|
||||
MAIN_PAGE_MARKDOWN = dedent(
|
||||
"""
|
||||
## What is pyweb.ui?
|
||||
Pyweb UI is a totally immagnary exercise atm but..... imagine it is a Python library that allows you to create
|
||||
web applications using Python only.
|
||||
|
||||
It is based on base HTML/JS components but is extensible, for instance, it can have a [Shoelace](https://shoelace.style/) backend...
|
||||
|
||||
PyWeb is a Python library that allows you to create web applications using Python only.
|
||||
|
||||
## What can I do with Pyweb.ui?
|
||||
|
||||
You can create web applications using Python only.
|
||||
"""
|
||||
)
|
||||
|
||||
# First thing we do is to load all the external resources we need
|
||||
shoelace.load_resources()
|
||||
|
||||
|
||||
# Let's define some convenience functions first
|
||||
def create_component_details(component_label, component):
|
||||
"""Create a component details card.
|
||||
|
||||
Args:
|
||||
component (str): The name of the component to create.
|
||||
|
||||
Returns:
|
||||
the component created
|
||||
|
||||
"""
|
||||
# Get the example from the examples catalog
|
||||
example = component["instance"]
|
||||
details = (
|
||||
getattr(example, "__doc__", "")
|
||||
or f"Details missing for component {component_label}"
|
||||
)
|
||||
|
||||
return div(
|
||||
[
|
||||
# Title and description (description is picked from the class docstring)
|
||||
h1(component_label),
|
||||
markdown(details),
|
||||
# Example section
|
||||
h2("Example:"),
|
||||
create_component_example(component["instance"], component["code"]),
|
||||
],
|
||||
style={"margin": "20px"},
|
||||
)
|
||||
|
||||
|
||||
def add_component_section(component_label, component, parent_div):
|
||||
"""Create a link to a component and add it to the left panel.
|
||||
|
||||
Args:
|
||||
component (str): The name of the component to add.
|
||||
|
||||
Returns:
|
||||
the component created
|
||||
|
||||
"""
|
||||
# Create the component link element
|
||||
div_ = div(
|
||||
a(component_label, href="#"),
|
||||
style={"display": "block", "text-align": "center", "margin": "auto"},
|
||||
)
|
||||
|
||||
# Create a handler that opens the component details when the link is clicked
|
||||
@when("click", div_)
|
||||
def _change():
|
||||
new_main = create_component_details(component_label, component)
|
||||
main_area.html = ""
|
||||
main_area.append(new_main)
|
||||
|
||||
# Add the new link element to the parent div (left panel)
|
||||
parent_div.append(div_)
|
||||
return div_
|
||||
|
||||
|
||||
def create_component_example(widget, code):
|
||||
"""Create a grid div with the widget on the left side and the relate code
|
||||
on the right side.
|
||||
|
||||
Args:
|
||||
widget (ElementBase): The widget to add to the grid.
|
||||
code (str): The code to add to the grid.
|
||||
|
||||
Returns:
|
||||
the grid created
|
||||
|
||||
"""
|
||||
# Create the grid that splits the window in two columns (25% and 75%)
|
||||
grid_ = grid("29% 2% 74%")
|
||||
|
||||
# Add the widget
|
||||
grid_.append(div(widget, style=styles.STYLE_EXAMPLE_INSTANCE))
|
||||
|
||||
# Add the code div
|
||||
widget_code = markdown(dedent(f"""```python\n{code}\n```"""))
|
||||
grid_.append(shoelace.Divider(vertical=True))
|
||||
grid_.append(div(widget_code, style=styles.STYLE_CODE_BLOCK))
|
||||
|
||||
return grid_
|
||||
|
||||
|
||||
def create_main_area():
|
||||
"""Create the main area of the right side of page, with the description of the
|
||||
demo itself and how to use it.
|
||||
|
||||
Returns:
|
||||
the main area
|
||||
|
||||
"""
|
||||
div_ = div(
|
||||
[
|
||||
h1("Welcome to PyWeb UI!", style={"text-align": "center"}),
|
||||
markdown(MAIN_PAGE_MARKDOWN),
|
||||
]
|
||||
)
|
||||
|
||||
main = el.main(
|
||||
style={
|
||||
"padding-top": "4rem",
|
||||
"padding-bottom": "7rem",
|
||||
"max-width": "52rem",
|
||||
"margin-left": "auto",
|
||||
"margin-right": "auto",
|
||||
"padding-left": "1.5rem",
|
||||
"padding-right": "1.5rem",
|
||||
"width": "100%",
|
||||
}
|
||||
)
|
||||
main.append(div_)
|
||||
|
||||
return main
|
||||
|
||||
|
||||
def create_basic_components_page(label, kit_name):
|
||||
"""Create the basic components page.
|
||||
|
||||
Returns:
|
||||
the main area
|
||||
|
||||
"""
|
||||
div_ = div(h2(label))
|
||||
|
||||
for component_label, component in examples.kits[kit_name].items():
|
||||
div_.append(h3(component_label))
|
||||
div_.append(create_component_example(component["instance"], component["code"]))
|
||||
|
||||
return div_
|
||||
|
||||
|
||||
# ********** CREATE ALL THE LAYOUT **********
|
||||
|
||||
main_grid = grid("140px 20px auto", style={"min-height": "100%"})
|
||||
|
||||
# ********** MAIN PANEL **********
|
||||
main_area = create_main_area()
|
||||
|
||||
|
||||
def write_to_main(content):
|
||||
main_area.html = ""
|
||||
main_area.append(content)
|
||||
|
||||
|
||||
def restore_home():
|
||||
write_to_main(create_main_area())
|
||||
|
||||
|
||||
def basic_components():
|
||||
write_to_main(
|
||||
create_basic_components_page(label="Basic Components", kit_name="elements")
|
||||
)
|
||||
# Make sure we highlight the code
|
||||
window.hljs.highlightAll()
|
||||
|
||||
|
||||
def markdown_components():
|
||||
write_to_main(create_basic_components_page(label="", kit_name="markdown"))
|
||||
|
||||
|
||||
def create_new_section(title, parent_div):
|
||||
basic_components_text = h3(
|
||||
title, style={"text-align": "left", "margin": "20px auto 0"}
|
||||
)
|
||||
parent_div.append(basic_components_text)
|
||||
parent_div.append(
|
||||
shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"})
|
||||
)
|
||||
return basic_components_text
|
||||
|
||||
|
||||
# ********** LEFT PANEL **********
|
||||
left_div = div()
|
||||
left_panel_title = h1(
|
||||
"PyWeb.UI", style={"text-align": "center", "margin": "20px auto 30px"}
|
||||
)
|
||||
left_div.append(left_panel_title)
|
||||
left_div.append(shoelace.Divider(style={"margin-bottom": "30px"}))
|
||||
# Let's map the creation of the main area to when the user clocks on "Components"
|
||||
when("click", left_panel_title)(restore_home)
|
||||
|
||||
# BASIC COMPONENTS
|
||||
basic_components_text = h3(
|
||||
"Basic Components",
|
||||
style={"text-align": "left", "margin": "20px auto 0", "cursor": "pointer"},
|
||||
)
|
||||
left_div.append(basic_components_text)
|
||||
left_div.append(shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"}))
|
||||
# Let's map the creation of the main area to when the user clocks on "Components"
|
||||
when("click", basic_components_text)(basic_components)
|
||||
|
||||
# MARKDOWN COMPONENTS
|
||||
markdown_title = create_new_section("Markdown", left_div)
|
||||
when("click", markdown_title)(markdown_components)
|
||||
|
||||
|
||||
# SHOELACE COMPONENTS
|
||||
shoe_components_text = h3(
|
||||
"Shoe Components", style={"text-align": "left", "margin": "20px auto 0"}
|
||||
)
|
||||
left_div.append(shoe_components_text)
|
||||
left_div.append(shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"}))
|
||||
|
||||
# Create the links to the components on th left panel
|
||||
print("SHOELACE EXAMPLES", examples.kits["shoelace"])
|
||||
for component_label, component in examples.kits["shoelace"].items():
|
||||
add_component_section(component_label, component, left_div)
|
||||
|
||||
left_div.append(shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"}))
|
||||
left_div.append(a("Gallery", href="gallery.html", style={"text-align": "left"}))
|
||||
# ********** ADD LEFT AND MAIN PANEL TO MAIN **********
|
||||
main_grid.append(left_div)
|
||||
main_grid.append(shoelace.Divider(vertical=True))
|
||||
main_grid.append(main_area)
|
||||
pydom.body.append(main_grid)
|
||||
@@ -1,300 +0,0 @@
|
||||
from markdown import markdown
|
||||
from pyscript import when, window
|
||||
from pyweb import pydom
|
||||
from pyweb.ui.elements import (
|
||||
a,
|
||||
br,
|
||||
button,
|
||||
code,
|
||||
div,
|
||||
grid,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
img,
|
||||
input_,
|
||||
p,
|
||||
small,
|
||||
strong,
|
||||
)
|
||||
from shoelace import (
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
CopyButton,
|
||||
Details,
|
||||
Dialog,
|
||||
Divider,
|
||||
Icon,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Range,
|
||||
Rating,
|
||||
RelativeTime,
|
||||
Skeleton,
|
||||
Spinner,
|
||||
Switch,
|
||||
Tag,
|
||||
Textarea,
|
||||
)
|
||||
|
||||
LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||
details_code = """
|
||||
LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||
Details(LOREM_IPSUM, summary="Try me")
|
||||
"""
|
||||
example_dialog_close_btn = Button("Close")
|
||||
example_dialog = Dialog(div([p(LOREM_IPSUM), example_dialog_close_btn]), label="Try me")
|
||||
example_dialog_btn = Button("Open Dialog")
|
||||
|
||||
|
||||
def toggle_dialog():
|
||||
example_dialog.open = not (example_dialog.open)
|
||||
|
||||
|
||||
when("click", example_dialog_btn)(toggle_dialog)
|
||||
when("click", example_dialog_close_btn)(toggle_dialog)
|
||||
|
||||
pydom.body.append(example_dialog)
|
||||
|
||||
|
||||
# ELEMENTS
|
||||
|
||||
# Button
|
||||
btn = button("Click me!")
|
||||
when("click", btn)(lambda: window.alert("Clicked!"))
|
||||
|
||||
# Inputs
|
||||
inputs_div = div()
|
||||
inputs_code = []
|
||||
for input_type in [
|
||||
"text",
|
||||
"password",
|
||||
"email",
|
||||
"number",
|
||||
"date",
|
||||
"time",
|
||||
"color",
|
||||
"range",
|
||||
]:
|
||||
inputs_div.append(input_(type=input_type, style={"display": "block"}))
|
||||
inputs_code.append(f"input_(type='{input_type}')")
|
||||
|
||||
|
||||
headers_div = div()
|
||||
headers_code = []
|
||||
for header in [h1, h2, h3, h4, h5, h6]:
|
||||
headers_div.append(header(f"{header.tag.upper()} header"))
|
||||
headers_code.append(f'{header.tag}("{header.tag.upper()} header")')
|
||||
headers_code = "\n".join(headers_code)
|
||||
|
||||
rich_input = input_(
|
||||
type="text",
|
||||
name="some name",
|
||||
autofocus=True,
|
||||
pattern="\w{3,16}",
|
||||
placeholder="add text with > 3 chars",
|
||||
required=True,
|
||||
size="20",
|
||||
)
|
||||
inputs_div.append(rich_input)
|
||||
inputs_code.append("# You can create inputs with more options like")
|
||||
inputs_code.append("# this by passing properties as kwargs")
|
||||
inputs_code.append(
|
||||
"input_(type='text', name='some name', autofocus=True, pattern='\\w{3,16}', placeholder='add text with > 3 chars', required=True, size='20')"
|
||||
)
|
||||
inputs_code = "\n".join(inputs_code)
|
||||
|
||||
MARKDOWN_EXAMPLE = """# This is a header
|
||||
|
||||
This is a ~~paragraph~~ text with **bold** and *italic* text in it!
|
||||
"""
|
||||
|
||||
kits = {
|
||||
"shoelace": {
|
||||
"Alert": {
|
||||
"instance": Alert(
|
||||
"This is a standard alert. You can customize its content and even the icon."
|
||||
),
|
||||
"code": "Alert('This is a standard alert. You can customize its content and even the icon.'",
|
||||
},
|
||||
"Icon": {
|
||||
"instance": Icon(name="heart"),
|
||||
"code": 'Icon(name="heart")',
|
||||
},
|
||||
"Button": {
|
||||
"instance": Button("Try me"),
|
||||
"code": 'Button("Try me")',
|
||||
},
|
||||
"Card": {
|
||||
"instance": Card(
|
||||
p("This is a cool card!"),
|
||||
image="https://pyscript.net/assets/images/pyscript-sticker-black.svg",
|
||||
footer=div([Button("More Info"), Rating()]),
|
||||
),
|
||||
"code": """
|
||||
Card(p("This is a cool card!"), image="https://pyscript.net/assets/images/pyscript-sticker-black.svg", footer=div([Button("More Info"), Rating()]))
|
||||
""",
|
||||
},
|
||||
"Details": {
|
||||
"instance": Details(LOREM_IPSUM, summary="Try me"),
|
||||
"code": 'Details(LOREM_IPSUM, summary="Try me")',
|
||||
},
|
||||
"Dialog": {
|
||||
"instance": example_dialog_btn,
|
||||
"code": 'Dialog(div([p(LOREM_IPSUM), Button("Close")]), summary="Try me")',
|
||||
},
|
||||
"Divider": {
|
||||
"instance": Divider(),
|
||||
"code": "Divider()",
|
||||
},
|
||||
"Rating": {
|
||||
"instance": Rating(),
|
||||
"code": "Rating()",
|
||||
},
|
||||
"Radio": {
|
||||
"instance": Radio("Option 42"),
|
||||
"code": code('Radio("Option 42")'),
|
||||
},
|
||||
"Radio Group": {
|
||||
"instance": RadioGroup(
|
||||
[
|
||||
Radio("radio 1", name="radio 1", value=1, style={"margin": "20px"}),
|
||||
Radio("radio 2", name="radio 2", value=2, style={"margin": "20px"}),
|
||||
Radio("radio 3", name="radio 3", value=3, style={"margin": "20px"}),
|
||||
],
|
||||
label="Select an option",
|
||||
),
|
||||
"code": code(
|
||||
"""
|
||||
RadioGroup([Radio("radio 1", name="radio 1", value=1, style={"margin": "20px"}),
|
||||
Radio("radio 2", name="radio 2", value=2, style={"margin": "20px"}),
|
||||
Radio("radio 3", name="radio 3", value=3, style={"margin": "20px"})],
|
||||
label="Select an option"),"""
|
||||
),
|
||||
},
|
||||
"CopyButton": {
|
||||
"instance": CopyButton(
|
||||
value="PyShoes!",
|
||||
copy_label="Copy me!",
|
||||
sucess_label="Copied, check your clipboard!",
|
||||
error_label="Oops, something went wrong!",
|
||||
feedback_timeout=2000,
|
||||
tooltip_placement="top",
|
||||
),
|
||||
"code": 'CopyButton(value="PyShoes!", copy_label="Copy me!", sucess_label="Copied, check your clipboard!", error_label="Oops, something went wrong!", feedback_timeout=2000, tooltip_placement="top")',
|
||||
},
|
||||
"Skeleton": {
|
||||
"instance": Skeleton(effect="pulse"),
|
||||
"code": "Skeleton(effect='pulse')",
|
||||
},
|
||||
"Spinner": {
|
||||
"instance": Spinner(),
|
||||
"code": "Spinner()",
|
||||
},
|
||||
"Switch": {
|
||||
"instance": Switch(name="switch", size="large"),
|
||||
"code": 'Switch(name="switch", size="large")',
|
||||
},
|
||||
"Textarea": {
|
||||
"instance": Textarea(
|
||||
name="textarea",
|
||||
label="Textarea",
|
||||
size="medium",
|
||||
help_text="This is a textarea",
|
||||
resize="auto",
|
||||
),
|
||||
"code": 'Textarea(name="textarea", label="Textarea", size="medium", help_text="This is a textarea", resize="auto")',
|
||||
},
|
||||
"Tag": {
|
||||
"instance": Tag("Tag", variant="primary", size="medium"),
|
||||
"code": 'Tag("Tag", variant="primary", size="medium")',
|
||||
},
|
||||
"Range": {
|
||||
"instance": Range(min=0, max=100, value=50),
|
||||
"code": "Range(min=0, max=100, value=50)",
|
||||
},
|
||||
"RelativeTime": {
|
||||
"instance": RelativeTime(date="2021-01-01T00:00:00Z"),
|
||||
"code": 'RelativeTime(date="2021-01-01T00:00:00Z")',
|
||||
},
|
||||
# "SplitPanel": {
|
||||
# "instance": SplitPanel(
|
||||
# div("First panel"), div("Second panel"), orientation="vertical"
|
||||
# ),
|
||||
# "code": code(
|
||||
# 'SplitPanel(div("First panel"), div("Second panel"), orientation="vertical")'
|
||||
# ),
|
||||
# },
|
||||
},
|
||||
"elements": {
|
||||
"button": {
|
||||
"instance": btn,
|
||||
"code": """btn = button("Click me!")
|
||||
when('click', btn)(lambda: window.alert("Clicked!"))
|
||||
parentdiv.append(btn)
|
||||
""",
|
||||
},
|
||||
"div": {
|
||||
"instance": div(
|
||||
"This is a div",
|
||||
style={
|
||||
"text-align": "center",
|
||||
"margin": "0 auto",
|
||||
"background-color": "cornsilk",
|
||||
},
|
||||
),
|
||||
"code": 'div("This is a div", style={"text-align": "center", "margin": "0 auto", "background-color": "cornsilk"})',
|
||||
},
|
||||
"input": {"instance": inputs_div, "code": inputs_code},
|
||||
"grid": {
|
||||
"instance": grid(
|
||||
"30% 70%",
|
||||
[
|
||||
div("This is a grid", style={"background-color": "lightblue"}),
|
||||
p("with 2 elements", style={"background-color": "lightyellow"}),
|
||||
],
|
||||
),
|
||||
"code": 'grid([div("This is a grid")])',
|
||||
},
|
||||
"headers": {"instance": headers_div, "code": headers_code},
|
||||
"a": {
|
||||
"instance": a(
|
||||
"Click here for something awesome",
|
||||
href="https://pyscript.net",
|
||||
target="_blank",
|
||||
),
|
||||
"code": 'a("Click here for something awesome", href="https://pyscript.net", target="_blank")',
|
||||
},
|
||||
"br": {
|
||||
"instance": div([p("This is a paragraph"), br(), p("with a line break")]),
|
||||
"code": 'div([p("This is a paragraph"), br(), p("with a line break")])',
|
||||
},
|
||||
"img": {
|
||||
"instance": img(src="./giphy_winner.gif", style={"max-width": "200px"}),
|
||||
"code": 'img(src="./giphy_winner.gif", style={"max-width": "200px"})',
|
||||
},
|
||||
"code": {
|
||||
"instance": code("print('Hello, World!')"),
|
||||
"code": "code(\"print('Hello, World!')\")",
|
||||
},
|
||||
"p": {"instance": p("This is a paragraph"), "code": 'p("This is a paragraph")'},
|
||||
"small": {
|
||||
"instance": small("This is a small text"),
|
||||
"code": 'small("This is a small text")',
|
||||
},
|
||||
"strong": {
|
||||
"instance": strong("This is a strong text"),
|
||||
"code": 'strong("This is a strong text")',
|
||||
},
|
||||
},
|
||||
"markdown": {
|
||||
"markdown": {
|
||||
"instance": markdown(MARKDOWN_EXAMPLE),
|
||||
"code": f'markdown("""{MARKDOWN_EXAMPLE}""")',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>PyDom UI</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/lib/marked.umd.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
|
||||
<!-- and it's easy to individually load additional languages -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/go.min.js"></script>
|
||||
|
||||
<script>hljs.highlightAll();</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, "system-ui", "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="mpy" src="./gallery.py" config="./pyscript.toml"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,180 +0,0 @@
|
||||
try:
|
||||
from textwrap import dedent
|
||||
except ImportError:
|
||||
dedent = lambda x: x
|
||||
|
||||
import inspect
|
||||
|
||||
import shoelace
|
||||
import styles
|
||||
import tictactoe
|
||||
from markdown import markdown
|
||||
from pyscript import when, window
|
||||
from pyweb import pydom
|
||||
from pyweb.ui import elements as el
|
||||
|
||||
MAIN_PAGE_MARKDOWN = dedent(
|
||||
"""
|
||||
This gallery is a collection of demos using the PyWeb.UI library. There are meant
|
||||
to be examples of how to use the library to create GUI applications using Python
|
||||
only.
|
||||
|
||||
## How to use the gallery
|
||||
|
||||
Simply click on the demo you want to see and the details will appear on the right
|
||||
"""
|
||||
)
|
||||
|
||||
# First thing we do is to load all the external resources we need
|
||||
shoelace.load_resources()
|
||||
|
||||
|
||||
def add_demo(demo_name, demo_creator_cb, parent_div, source=None):
|
||||
"""Create a link to a component and add it to the left panel.
|
||||
|
||||
Args:
|
||||
component (str): The name of the component to add.
|
||||
|
||||
Returns:
|
||||
the component created
|
||||
|
||||
"""
|
||||
# Create the component link element
|
||||
div = el.div(el.a(demo_name, href="#"), style=styles.STYLE_LEFT_PANEL_LINKS)
|
||||
|
||||
# Create a handler that opens the component details when the link is clicked
|
||||
@when("click", div)
|
||||
def _change():
|
||||
if source:
|
||||
demo_div = el.grid("50% 50%")
|
||||
demo_div.append(demo_creator_cb())
|
||||
widget_code = markdown(dedent(f"""```python\n{source}\n```"""))
|
||||
demo_div.append(el.div(widget_code, style=styles.STYLE_CODE_BLOCK))
|
||||
else:
|
||||
demo_div = demo_creator_cb()
|
||||
demo_div.style["margin"] = "20px"
|
||||
write_to_main(demo_div)
|
||||
window.hljs.highlightAll()
|
||||
|
||||
# Add the new link element to the parent div (left panel)
|
||||
parent_div.append(div)
|
||||
return div
|
||||
|
||||
|
||||
def create_main_area():
|
||||
"""Create the main area of the right side of page, with the description of the
|
||||
demo itself and how to use it.
|
||||
|
||||
Returns:
|
||||
the main area
|
||||
|
||||
"""
|
||||
return el.div(
|
||||
[
|
||||
el.h1("PyWeb UI Gallery", style={"text-align": "center"}),
|
||||
markdown(MAIN_PAGE_MARKDOWN),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def create_markdown_app():
|
||||
"""Create the basic components page.
|
||||
|
||||
Returns:
|
||||
the main area
|
||||
|
||||
"""
|
||||
translate_button = shoelace.Button("Convert", variant="primary")
|
||||
markdown_txt_area = shoelace.TextArea(label="Use this to write your Markdown")
|
||||
result_div = el.div(style=styles.STYLE_MARKDOWN_RESULT)
|
||||
|
||||
@when("click", translate_button)
|
||||
def translate_markdown():
|
||||
result_div.html = markdown(markdown_txt_area.value).html
|
||||
|
||||
return el.div(
|
||||
[
|
||||
el.h2("Markdown"),
|
||||
markdown_txt_area,
|
||||
translate_button,
|
||||
result_div,
|
||||
],
|
||||
style={"margin": "20px"},
|
||||
)
|
||||
|
||||
|
||||
# ********** MAIN PANEL **********
|
||||
main_area = create_main_area()
|
||||
|
||||
|
||||
def write_to_main(content):
|
||||
main_area.html = ""
|
||||
main_area.append(content)
|
||||
|
||||
|
||||
def restore_home():
|
||||
write_to_main(create_main_area())
|
||||
|
||||
|
||||
def create_new_section(title, parent_div):
|
||||
basic_components_text = el.h3(
|
||||
title, style={"text-align": "left", "margin": "20px auto 0"}
|
||||
)
|
||||
parent_div.append(basic_components_text)
|
||||
parent_div.append(
|
||||
shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"})
|
||||
)
|
||||
return basic_components_text
|
||||
|
||||
|
||||
# ********** LEFT PANEL **********
|
||||
left_panel_title = el.h1("PyWeb.UI", style=styles.STYLE_LEFT_PANEL_TITLE)
|
||||
left_div = el.div(
|
||||
[
|
||||
left_panel_title,
|
||||
shoelace.Divider(style={"margin-bottom": "30px"}),
|
||||
el.h3("Demos", style=styles.STYLE_LEFT_PANEL_TITLE),
|
||||
]
|
||||
)
|
||||
|
||||
# Let's map the creation of the main area to when the user clocks on "Components"
|
||||
when("click", left_panel_title)(restore_home)
|
||||
|
||||
# ------ ADD DEMOS ------
|
||||
markdown_source = """
|
||||
translate_button = shoelace.Button("Convert", variant="primary")
|
||||
markdown_txt_area = shoelace.TextArea(label="Markdown",
|
||||
help_text="Write your Mardown here and press convert to see the result",
|
||||
)
|
||||
result_div = el.div(style=styles.STYLE_MARKDOWN_RESULT)
|
||||
@when("click", translate_button)
|
||||
def translate_markdown():
|
||||
result_div.html = markdown(markdown_txt_area.value).html
|
||||
|
||||
el.div([
|
||||
el.h2("Markdown"),
|
||||
markdown_txt_area,
|
||||
translate_button,
|
||||
result_div,
|
||||
])
|
||||
"""
|
||||
add_demo("Markdown", create_markdown_app, left_div, source=markdown_source)
|
||||
add_demo(
|
||||
"Tic Tac Toe",
|
||||
tictactoe.create_tic_tac_toe,
|
||||
left_div,
|
||||
source=inspect.getsource(tictactoe),
|
||||
)
|
||||
|
||||
left_div.append(shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"}))
|
||||
left_div.append(el.a("Examples", href="/tests/ui/", style={"text-align": "left"}))
|
||||
|
||||
# ********** CREATE ALL THE LAYOUT **********
|
||||
grid = el.grid("minmax(100px, 200px) 20px auto", style={"min-height": "100%"})
|
||||
grid.append(left_div)
|
||||
grid.append(shoelace.Divider(vertical=True))
|
||||
grid.append(main_area)
|
||||
|
||||
pydom.body.append(grid)
|
||||
pydom.body.append(el.a("Back to the main page", href="/tests/ui/", target="_blank"))
|
||||
pydom.body.append(el.a("Hidden!!!", href="/tests/ui/", target="_blank", hidden=True))
|
||||
@@ -1,39 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>PyDom UI</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/lib/marked.umd.min.js"></script>
|
||||
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
|
||||
<!-- and it's easy to individually load additional languages -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/go.min.js"></script>
|
||||
<!-- SHOWLACE CUSTOM CSS -->
|
||||
<style>
|
||||
</style>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, "system-ui", "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"
|
||||
}
|
||||
|
||||
input:invalid {
|
||||
background-color: lightpink;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="mpy" src="./demo.py" config="./pyscript.toml"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,8 +0,0 @@
|
||||
packages = []
|
||||
|
||||
[files]
|
||||
"./examples.py" = "./examples.py"
|
||||
"./tictactoe.py" = "./tictactoe.py"
|
||||
"./styles.py" = "./styles.py"
|
||||
"./shoelace.py" = "./shoelace.py"
|
||||
"./markdown.py" = "./markdown.py"
|
||||
Reference in New Issue
Block a user