pyjs-examples 0.0.1__tar.gz → 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/PKG-INFO +5 -4
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/README.md +3 -2
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/calculator/README.md +4 -1
- pyjs_examples-0.0.2/calculator/__main__.py +15 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/calculator/calculator.py +122 -7
- pyjs_examples-0.0.2/calculator/tests/test_pwa.py +67 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/chatroom/chatroom.py +35 -32
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/chatroom/tests/test_browser_chatroom.py +17 -13
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/kanban/tests/test_browser_kanban.py +6 -1
- pyjs_examples-0.0.2/navigation/README.md +20 -0
- pyjs_examples-0.0.2/navigation/__init__.py +1 -0
- pyjs_examples-0.0.2/navigation/__main__.py +49 -0
- pyjs_examples-0.0.2/navigation/navigation.py +800 -0
- pyjs_examples-0.0.2/navigation/tests/test_browser_navigation.py +83 -0
- pyjs_examples-0.0.2/navigation/tests/test_navigation.py +59 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/pyjs_examples.egg-info/PKG-INFO +5 -4
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/pyjs_examples.egg-info/SOURCES.txt +14 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/pyjs_examples.egg-info/entry_points.txt +1 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/pyjs_examples.egg-info/requires.txt +1 -1
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/pyproject.toml +10 -2
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/space_invaders/space_invaders.py +8 -5
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/todomvc/tests/test_browser_todomvc.py +18 -11
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/todomvc/todomvc.py +12 -10
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/workspace/tests/test_browser_workspace.py +2 -1
- pyjs_examples-0.0.1/calculator/__main__.py +0 -9
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/__init__.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/calculator/__init__.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/calculator/tests/test_browser_calculator.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/calculator/tests/test_calculator.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/catalog.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/chatroom/README.md +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/chatroom/__init__.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/chatroom/__main__.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/chatroom/chat_server.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/chatroom/tests/test_chat_server.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/chatroom/visual_test.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/counter.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/kanban/README.md +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/kanban/__init__.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/kanban/__main__.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/kanban/kanban.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/kanban/tests/renders/kanban-board.png +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/kanban/tests/test_kanban.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/kanban/visual_test.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/nativescript_counter/README.md +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/nativescript_counter/__init__.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/nativescript_counter/__main__.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/nativescript_counter/counter.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/nativescript_counter/tests/test_android_counter.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/nativescript_counter/tests/test_counter.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/pyjs_examples.egg-info/dependency_links.txt +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/pyjs_examples.egg-info/top_level.txt +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/samples/__init__.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/samples/state_counter/README.md +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/samples/state_counter/__init__.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/samples/state_counter/__main__.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/samples/state_counter/state_counter.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/samples/state_counter/tests/test_browser_state_counter.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/samples/state_counter/tests/test_state_counter.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/samples/ui_dialog/README.md +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/samples/ui_dialog/__init__.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/samples/ui_dialog/__main__.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/samples/ui_dialog/tests/test_browser_ui_dialog.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/samples/ui_dialog/tests/test_ui_dialog.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/samples/ui_dialog/ui_dialog.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/setup.cfg +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/space_invaders/README.md +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/space_invaders/__init__.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/space_invaders/__main__.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/space_invaders/tests/renders/action_scene.png +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/space_invaders/tests/renders/initial_wave.png +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/space_invaders/tests/test_browser_game.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/space_invaders/tests/test_game.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/todomvc/README.md +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/todomvc/__init__.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/todomvc/__main__.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/todomvc/base.css +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/todomvc/index.css +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/todomvc/tests/test_todomvc.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/workspace/README.md +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/workspace/__init__.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/workspace/__main__.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/workspace/tests/test_workspace.py +0 -0
- {pyjs_examples-0.0.1 → pyjs_examples-0.0.2}/workspace/workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyjs-examples
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: Official example applications for PyJS.
|
|
5
5
|
Author-email: Lex Berezhny <lex@damoti.com>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -18,7 +18,7 @@ Classifier: Topic :: Internet :: WWW/HTTP
|
|
|
18
18
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
19
|
Requires-Python: >=3.14
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
|
-
Requires-Dist: pyjs==0.0.
|
|
21
|
+
Requires-Dist: pyjs==0.0.2
|
|
22
22
|
Provides-Extra: test
|
|
23
23
|
Requires-Dist: pytest>=9.0; extra == "test"
|
|
24
24
|
|
|
@@ -31,7 +31,7 @@ browse in the repository while installing as an ordinary top-level package.
|
|
|
31
31
|
From the repository root, install the core and examples from a source checkout:
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
|
|
34
|
+
uv sync
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
An installed PyJS release provides the same package through the `examples` extra:
|
|
@@ -40,13 +40,14 @@ An installed PyJS release provides the same package through the `examples` extra
|
|
|
40
40
|
python -m pip install "pyjs[examples]"
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
Run
|
|
43
|
+
Run each complete application with the command shown:
|
|
44
44
|
|
|
45
45
|
| Example | Command |
|
|
46
46
|
| --- | --- |
|
|
47
47
|
| [Calculator](calculator/) | `python -m pyjs_examples.calculator` |
|
|
48
48
|
| [Chatroom](chatroom/) | `python -m pyjs_examples.chatroom` |
|
|
49
49
|
| [Kanban board](kanban/) | `python -m pyjs_examples.kanban` |
|
|
50
|
+
| [Navigation workspace](navigation/) | `python -m pyjs_examples.navigation` |
|
|
50
51
|
| [NativeScript Counter](nativescript_counter/) | `python -m pyjs_examples.nativescript_counter` |
|
|
51
52
|
| [Space Invaders](space_invaders/) | `python -m pyjs_examples.space_invaders` |
|
|
52
53
|
| [TodoMVC](todomvc/) | `python -m pyjs_examples.todomvc` |
|
|
@@ -7,7 +7,7 @@ browse in the repository while installing as an ordinary top-level package.
|
|
|
7
7
|
From the repository root, install the core and examples from a source checkout:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
10
|
+
uv sync
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
An installed PyJS release provides the same package through the `examples` extra:
|
|
@@ -16,13 +16,14 @@ An installed PyJS release provides the same package through the `examples` extra
|
|
|
16
16
|
python -m pip install "pyjs[examples]"
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Run
|
|
19
|
+
Run each complete application with the command shown:
|
|
20
20
|
|
|
21
21
|
| Example | Command |
|
|
22
22
|
| --- | --- |
|
|
23
23
|
| [Calculator](calculator/) | `python -m pyjs_examples.calculator` |
|
|
24
24
|
| [Chatroom](chatroom/) | `python -m pyjs_examples.chatroom` |
|
|
25
25
|
| [Kanban board](kanban/) | `python -m pyjs_examples.kanban` |
|
|
26
|
+
| [Navigation workspace](navigation/) | `python -m pyjs_examples.navigation` |
|
|
26
27
|
| [NativeScript Counter](nativescript_counter/) | `python -m pyjs_examples.nativescript_counter` |
|
|
27
28
|
| [Space Invaders](space_invaders/) | `python -m pyjs_examples.space_invaders` |
|
|
28
29
|
| [TodoMVC](todomvc/) | `python -m pyjs_examples.todomvc` |
|
|
@@ -13,6 +13,7 @@ It demonstrates:
|
|
|
13
13
|
- sign, percent, clear, and divide-by-zero behavior
|
|
14
14
|
- asynchronous result persistence through an HTTP POST route
|
|
15
15
|
- per-browser in-memory sessions that restore the last result after refresh
|
|
16
|
+
- a Python-authored service worker with cache-first offline application loading
|
|
16
17
|
- browser tests that interact with the real DOM through button events
|
|
17
18
|
|
|
18
19
|
The example intentionally stays focused on calculator behavior. Reusable widget
|
|
@@ -30,7 +31,9 @@ From the repository root:
|
|
|
30
31
|
Open `http://127.0.0.1:8000/`. Completing a calculation with `=` saves its
|
|
31
32
|
result to the server. Refreshing the page restores that result through the
|
|
32
33
|
browser's session cookie. Sessions are intentionally in memory, so restarting
|
|
33
|
-
the server clears them.
|
|
34
|
+
the server clears them. The app also registers `/calculator-serviceworker.js`.
|
|
35
|
+
After its first controlled reload, the calculator can load and calculate while
|
|
36
|
+
the browser is offline.
|
|
34
37
|
|
|
35
38
|
## Run the tests
|
|
36
39
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from pyjs.server import serve
|
|
4
|
+
|
|
5
|
+
from .calculator import render, save_result, service_worker_javascript
|
|
6
|
+
|
|
7
|
+
serve(
|
|
8
|
+
"pyjs_examples.calculator.calculator",
|
|
9
|
+
render=render,
|
|
10
|
+
routes={
|
|
11
|
+
("POST", "/api/calculator/result"): save_result,
|
|
12
|
+
("GET", "/calculator-serviceworker.js"): service_worker_javascript,
|
|
13
|
+
},
|
|
14
|
+
port=int(os.environ.get("PYJS_PORT", "8000")),
|
|
15
|
+
)
|
|
@@ -1,10 +1,94 @@
|
|
|
1
1
|
from pyjs import effect, js, nojs, state
|
|
2
2
|
from pyjs.web.domx import ProxyElement, button, ref, tag, tw
|
|
3
3
|
from pyjs.web.events import Event
|
|
4
|
-
from pyjs.web.network import fetch
|
|
4
|
+
from pyjs.web.network import Request, Response, fetch
|
|
5
|
+
from pyjs.web.serviceworker import (
|
|
6
|
+
ExtendableEvent,
|
|
7
|
+
FetchEvent,
|
|
8
|
+
caches,
|
|
9
|
+
navigator,
|
|
10
|
+
service_worker_scope,
|
|
11
|
+
)
|
|
5
12
|
from pyjs.web.ui import Widget
|
|
6
13
|
|
|
7
14
|
|
|
15
|
+
@js
|
|
16
|
+
async def install_app_shell(cache_name: str, assets: list[str]):
|
|
17
|
+
cache = await caches.open(cache_name)
|
|
18
|
+
await cache.addAll(assets)
|
|
19
|
+
await service_worker_scope().skipWaiting()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@js
|
|
23
|
+
async def activate_app_shell():
|
|
24
|
+
await service_worker_scope().clients.claim()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@js
|
|
28
|
+
async def cache_first(request: Request, cache_name: str) -> Response:
|
|
29
|
+
cached = await caches.match(request)
|
|
30
|
+
if cached is not None:
|
|
31
|
+
return cached
|
|
32
|
+
response = await fetch(request.url)
|
|
33
|
+
cache = await caches.open(cache_name)
|
|
34
|
+
await cache.put(request, response.clone())
|
|
35
|
+
return response
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@js
|
|
39
|
+
class InstallHandler:
|
|
40
|
+
def __init__(self, cache_name: str, assets: list[str]):
|
|
41
|
+
self.cache_name = cache_name
|
|
42
|
+
self.assets = assets
|
|
43
|
+
|
|
44
|
+
@js(include=True)
|
|
45
|
+
def handleEvent(self, event: ExtendableEvent):
|
|
46
|
+
event.waitUntil(install_app_shell(self.cache_name, self.assets))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@js
|
|
50
|
+
class ActivateHandler:
|
|
51
|
+
@js(include=True)
|
|
52
|
+
def handleEvent(self, event: ExtendableEvent):
|
|
53
|
+
event.waitUntil(activate_app_shell())
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@js
|
|
57
|
+
class FetchHandler:
|
|
58
|
+
def __init__(self, cache_name: str):
|
|
59
|
+
self.cache_name = cache_name
|
|
60
|
+
|
|
61
|
+
@js(include=True)
|
|
62
|
+
def handleEvent(self, event: FetchEvent):
|
|
63
|
+
if event.request.method == "GET":
|
|
64
|
+
event.respondWith(cache_first(event.request, self.cache_name))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@js
|
|
68
|
+
def service_worker_main():
|
|
69
|
+
cache_name = "pyjs-calculator-v1"
|
|
70
|
+
scope = service_worker_scope()
|
|
71
|
+
scope.addEventListener("install", InstallHandler(cache_name, ["/", "/index.css"]))
|
|
72
|
+
scope.addEventListener("activate", ActivateHandler())
|
|
73
|
+
scope.addEventListener("fetch", FetchHandler(cache_name))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@nojs
|
|
77
|
+
def service_worker_javascript(request):
|
|
78
|
+
from pyjs.server import ServerResponse
|
|
79
|
+
from pyjs.transpiler import bundle
|
|
80
|
+
|
|
81
|
+
source, _ = bundle(service_worker_main, include_main=True, include_css=False)
|
|
82
|
+
return ServerResponse(
|
|
83
|
+
source,
|
|
84
|
+
content_type="application/javascript",
|
|
85
|
+
headers={
|
|
86
|
+
"Cache-Control": "no-cache",
|
|
87
|
+
"Service-Worker-Allowed": "/",
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
8
92
|
@js
|
|
9
93
|
class CalculatorKeyListener:
|
|
10
94
|
def __init__(self, app: CalculatorApp, kind: str, value: str):
|
|
@@ -78,8 +162,14 @@ class CalculatorApp(Widget):
|
|
|
78
162
|
replace_display = state(True)
|
|
79
163
|
error = state(False)
|
|
80
164
|
save_url = state("")
|
|
81
|
-
|
|
82
|
-
|
|
165
|
+
service_worker_url = state("")
|
|
166
|
+
|
|
167
|
+
def __init__(
|
|
168
|
+
self,
|
|
169
|
+
initial_value: str = "0",
|
|
170
|
+
save_url: str = "",
|
|
171
|
+
service_worker_url: str = "",
|
|
172
|
+
):
|
|
83
173
|
super().__init__()
|
|
84
174
|
self.display = ref(
|
|
85
175
|
tag(
|
|
@@ -98,6 +188,7 @@ class CalculatorApp(Widget):
|
|
|
98
188
|
)
|
|
99
189
|
self.display_value = initial_value
|
|
100
190
|
self.save_url = save_url
|
|
191
|
+
self.service_worker_url = service_worker_url
|
|
101
192
|
|
|
102
193
|
self.clear_button = ref(CalculatorKey("AC", "clear", "action", False))
|
|
103
194
|
self.clear_button.bind(self, "action", "clear")
|
|
@@ -180,6 +271,12 @@ class CalculatorApp(Widget):
|
|
|
180
271
|
card,
|
|
181
272
|
)
|
|
182
273
|
|
|
274
|
+
async def initialize(self):
|
|
275
|
+
if self.service_worker_url == "":
|
|
276
|
+
return
|
|
277
|
+
registration = await navigator.serviceWorker.register(self.service_worker_url)
|
|
278
|
+
self.set_data("service-worker", registration.scope)
|
|
279
|
+
|
|
183
280
|
@effect(display_value)
|
|
184
281
|
def render_display(self):
|
|
185
282
|
self.display.textContent = self.display_value
|
|
@@ -294,13 +391,25 @@ class CalculatorApp(Widget):
|
|
|
294
391
|
self.percent()
|
|
295
392
|
|
|
296
393
|
|
|
297
|
-
def main(
|
|
298
|
-
|
|
394
|
+
def main(
|
|
395
|
+
initial_value: str = "0",
|
|
396
|
+
save_url: str = "",
|
|
397
|
+
service_worker_url: str = "",
|
|
398
|
+
):
|
|
399
|
+
return CalculatorApp(
|
|
400
|
+
initial_value=initial_value,
|
|
401
|
+
save_url=save_url,
|
|
402
|
+
service_worker_url=service_worker_url,
|
|
403
|
+
)
|
|
299
404
|
|
|
300
405
|
|
|
301
406
|
@nojs
|
|
302
407
|
def render(request):
|
|
303
|
-
return main(
|
|
408
|
+
return main(
|
|
409
|
+
request.session.get("last_result", "0"),
|
|
410
|
+
"/api/calculator/result",
|
|
411
|
+
"/calculator-serviceworker.js",
|
|
412
|
+
)
|
|
304
413
|
|
|
305
414
|
|
|
306
415
|
@nojs
|
|
@@ -316,10 +425,16 @@ def save_result(request):
|
|
|
316
425
|
|
|
317
426
|
|
|
318
427
|
if __name__ == "__main__":
|
|
428
|
+
import os
|
|
429
|
+
|
|
319
430
|
from pyjs.server import serve
|
|
320
431
|
|
|
321
432
|
serve(
|
|
322
433
|
"calculator",
|
|
323
434
|
render=render,
|
|
324
|
-
routes={
|
|
435
|
+
routes={
|
|
436
|
+
("POST", "/api/calculator/result"): save_result,
|
|
437
|
+
("GET", "/calculator-serviceworker.js"): service_worker_javascript,
|
|
438
|
+
},
|
|
439
|
+
port=int(os.environ.get("PYJS_PORT", "8000")),
|
|
325
440
|
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from pyjs.tooling import stop_process
|
|
9
|
+
from pyjs.tooling.browser import Browser, free_port, wait_for_http
|
|
10
|
+
|
|
11
|
+
pytestmark = pytest.mark.pyjs_python
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_calculator_registers_python_service_worker_and_runs_offline():
|
|
15
|
+
async def run():
|
|
16
|
+
port = free_port()
|
|
17
|
+
url = f"http://127.0.0.1:{port}/"
|
|
18
|
+
environment = os.environ.copy()
|
|
19
|
+
environment["PYJS_PORT"] = str(port)
|
|
20
|
+
environment["PYTHONUNBUFFERED"] = "1"
|
|
21
|
+
application = Path(__file__).parents[1] / "calculator.py"
|
|
22
|
+
server = subprocess.Popen(
|
|
23
|
+
[sys.executable, str(application)],
|
|
24
|
+
env=environment,
|
|
25
|
+
stdout=subprocess.PIPE,
|
|
26
|
+
stderr=subprocess.STDOUT,
|
|
27
|
+
text=True,
|
|
28
|
+
)
|
|
29
|
+
try:
|
|
30
|
+
try:
|
|
31
|
+
await wait_for_http(url, timeout=120)
|
|
32
|
+
except TimeoutError as error:
|
|
33
|
+
stop_process(server)
|
|
34
|
+
output = server.stdout.read() if server.stdout is not None else ""
|
|
35
|
+
raise TimeoutError(f"{error}\nServer output:\n{output}") from error
|
|
36
|
+
async with Browser() as browser:
|
|
37
|
+
page = await browser.new_page(url)
|
|
38
|
+
await page.wait_for_selector("calculator-app")
|
|
39
|
+
await page.wait_for(
|
|
40
|
+
"navigator.serviceWorker.controller !== null",
|
|
41
|
+
"the calculator service worker to control the page",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
scope = await page.attribute("calculator-app", "data-service-worker")
|
|
45
|
+
assert scope == url
|
|
46
|
+
|
|
47
|
+
await page.navigate(url)
|
|
48
|
+
await page.wait_for_selector('[data-testid="calculator-display"]')
|
|
49
|
+
cached_urls = await page.evaluate(
|
|
50
|
+
"(async () => {"
|
|
51
|
+
" const cache = await caches.open('pyjs-calculator-v1');"
|
|
52
|
+
" return (await cache.keys()).map((request) => request.url);"
|
|
53
|
+
"})()"
|
|
54
|
+
)
|
|
55
|
+
assert url in cached_urls
|
|
56
|
+
assert any(cached_url.endswith(".js") for cached_url in cached_urls)
|
|
57
|
+
|
|
58
|
+
await page.set_offline()
|
|
59
|
+
await page.navigate(url)
|
|
60
|
+
await page.wait_for_selector('[data-testid="calculator-display"]')
|
|
61
|
+
await page.click('[data-key="7"]')
|
|
62
|
+
assert await page.text('[data-testid="calculator-display"]') == "7"
|
|
63
|
+
assert page.exceptions == []
|
|
64
|
+
finally:
|
|
65
|
+
stop_process(server)
|
|
66
|
+
|
|
67
|
+
asyncio.run(run())
|
|
@@ -1,31 +1,29 @@
|
|
|
1
|
+
from typing import TypedDict
|
|
2
|
+
|
|
1
3
|
from pyjs import effect, js, nojs, state
|
|
4
|
+
from pyjs.javascript import JSON
|
|
2
5
|
from pyjs.web.dom import location, sessionStorage
|
|
3
6
|
from pyjs.web.domx import button, form, input, ref, tag, tw
|
|
4
7
|
from pyjs.web.events import Event, MessageEvent
|
|
5
|
-
from pyjs.web.javascript import JSON
|
|
6
8
|
from pyjs.web.network import WebSocket
|
|
7
9
|
from pyjs.web.ui import Widget
|
|
8
10
|
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
self.text = ""
|
|
17
|
-
self.time = ""
|
|
12
|
+
class ChatMessage(TypedDict):
|
|
13
|
+
id: int
|
|
14
|
+
room: str
|
|
15
|
+
sender: str
|
|
16
|
+
text: str
|
|
17
|
+
time: str
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
self.users: list[str] = []
|
|
28
|
-
self.message = ""
|
|
20
|
+
class ChatSnapshot(TypedDict):
|
|
21
|
+
type: str
|
|
22
|
+
room: str
|
|
23
|
+
rooms: list[str]
|
|
24
|
+
messages: list[ChatMessage]
|
|
25
|
+
users: list[str]
|
|
26
|
+
message: str
|
|
29
27
|
|
|
30
28
|
|
|
31
29
|
@js
|
|
@@ -441,10 +439,7 @@ class ChatRoomApp(Widget):
|
|
|
441
439
|
def initialize(self):
|
|
442
440
|
self.socket: WebSocket | None = None
|
|
443
441
|
name = sessionStorage.getItem("pyjs-chatroom-display-name")
|
|
444
|
-
if name is None:
|
|
445
|
-
self.show_sign_in()
|
|
446
|
-
return
|
|
447
|
-
if name == "":
|
|
442
|
+
if name is None or name == "":
|
|
448
443
|
self.show_sign_in()
|
|
449
444
|
return
|
|
450
445
|
self.name = name
|
|
@@ -535,14 +530,14 @@ class ChatRoomApp(Widget):
|
|
|
535
530
|
socket.send(JSON.stringify(message))
|
|
536
531
|
|
|
537
532
|
def receive(self, message: ChatSnapshot):
|
|
538
|
-
if message
|
|
539
|
-
self.show_error(message
|
|
533
|
+
if message["type"] == "error":
|
|
534
|
+
self.show_error(message["message"])
|
|
540
535
|
return
|
|
541
536
|
self.hide_error()
|
|
542
|
-
self.room = message
|
|
543
|
-
self.render_rooms(message
|
|
544
|
-
self.render_messages(message
|
|
545
|
-
self.render_members(message
|
|
537
|
+
self.room = message["room"]
|
|
538
|
+
self.render_rooms(message["rooms"])
|
|
539
|
+
self.render_messages(message["messages"])
|
|
540
|
+
self.render_members(message["users"])
|
|
546
541
|
|
|
547
542
|
def render_rooms(self, rooms: list[str]):
|
|
548
543
|
self.room_list.replaceChildren()
|
|
@@ -604,7 +599,7 @@ class ChatRoomApp(Widget):
|
|
|
604
599
|
tag(
|
|
605
600
|
"article",
|
|
606
601
|
tw("group flex gap-3 rounded-lg px-2 py-2 hover:bg-slate-50"),
|
|
607
|
-
{"data-message-id": str(message
|
|
602
|
+
{"data-message-id": str(message["id"])},
|
|
608
603
|
tag(
|
|
609
604
|
"div",
|
|
610
605
|
tw(
|
|
@@ -619,8 +614,16 @@ class ChatRoomApp(Widget):
|
|
|
619
614
|
tag(
|
|
620
615
|
"div",
|
|
621
616
|
tw("flex items-baseline gap-2"),
|
|
622
|
-
tag(
|
|
623
|
-
|
|
617
|
+
tag(
|
|
618
|
+
"strong",
|
|
619
|
+
tw("text-sm font-bold text-slate-900"),
|
|
620
|
+
message["sender"],
|
|
621
|
+
),
|
|
622
|
+
tag(
|
|
623
|
+
"time",
|
|
624
|
+
tw("text-[0.7rem] text-slate-400"),
|
|
625
|
+
message["time"],
|
|
626
|
+
),
|
|
624
627
|
),
|
|
625
628
|
tag(
|
|
626
629
|
"p",
|
|
@@ -628,7 +631,7 @@ class ChatRoomApp(Widget):
|
|
|
628
631
|
"mt-0.5 whitespace-pre-wrap break-words text-sm leading-6 "
|
|
629
632
|
"text-slate-700"
|
|
630
633
|
),
|
|
631
|
-
message
|
|
634
|
+
message["text"],
|
|
632
635
|
),
|
|
633
636
|
),
|
|
634
637
|
)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
from pyjs import js
|
|
3
|
-
from pyjs.testing.web import mount
|
|
3
|
+
from pyjs.testing.web import mount
|
|
4
4
|
from pyjs.web.dom import sessionStorage
|
|
5
|
+
from pyjs.web.domx import require_element
|
|
5
6
|
from pyjs.web.events import Event
|
|
6
7
|
from pyjs_examples.chatroom.chatroom import ChatMessage, ChatRoomApp, ChatSnapshot
|
|
7
8
|
|
|
@@ -36,18 +37,21 @@ def test_chatroom_prompts_for_a_per_tab_display_name():
|
|
|
36
37
|
def test_chatroom_renders_rooms_messages_and_presence_from_a_snapshot():
|
|
37
38
|
sessionStorage.setItem("pyjs-chatroom-display-name", "Alice")
|
|
38
39
|
app = mounted_chatroom()
|
|
39
|
-
message = ChatMessage(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
snapshot
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
40
|
+
message = ChatMessage(
|
|
41
|
+
id=1,
|
|
42
|
+
room="design",
|
|
43
|
+
sender="Bob",
|
|
44
|
+
text="The new navigation is ready.",
|
|
45
|
+
time="09:42",
|
|
46
|
+
)
|
|
47
|
+
snapshot = ChatSnapshot(
|
|
48
|
+
type="snapshot",
|
|
49
|
+
room="design",
|
|
50
|
+
rooms=["general", "design"],
|
|
51
|
+
messages=[message],
|
|
52
|
+
users=["Alice", "Bob"],
|
|
53
|
+
message="",
|
|
54
|
+
)
|
|
51
55
|
|
|
52
56
|
app.receive(snapshot)
|
|
53
57
|
|
|
@@ -147,4 +147,9 @@ def test_kanban_visual_showcase():
|
|
|
147
147
|
|
|
148
148
|
assert card_order(board.in_progress) == "drag-drop,empty-states,onboarding-audit"
|
|
149
149
|
mount(board.columns)
|
|
150
|
-
assert_render(
|
|
150
|
+
assert_render(
|
|
151
|
+
board.columns,
|
|
152
|
+
"kanban-board",
|
|
153
|
+
pixel_threshold=10,
|
|
154
|
+
max_different_pixel_ratio=0.06,
|
|
155
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Navigation Workspace Example
|
|
2
|
+
|
|
3
|
+
This example demonstrates server-rendered pages inside a persistent application layout. Normal
|
|
4
|
+
requests return complete HTML documents. Navigation initiated inside the application requests only
|
|
5
|
+
the selected page, updates the `PageOutlet`, and preserves the sidebar and header without requiring
|
|
6
|
+
a client-side routing table.
|
|
7
|
+
|
|
8
|
+
Run it from the repository root:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
.venv/bin/python -m pyjs_examples.navigation
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Open `http://127.0.0.1:8000/`, then move between the eight pages using the sidebar.
|
|
15
|
+
|
|
16
|
+
Run its server and browser coverage with:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
.venv/bin/python -m pytest -q examples/navigation/tests
|
|
20
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Persistent layout and partial page navigation example."""
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from pyjs.server import serve
|
|
4
|
+
from pyjs_examples.navigation.navigation import (
|
|
5
|
+
analytics_page,
|
|
6
|
+
analytics_view,
|
|
7
|
+
calendar_page,
|
|
8
|
+
calendar_view,
|
|
9
|
+
layout,
|
|
10
|
+
messages_page,
|
|
11
|
+
messages_view,
|
|
12
|
+
overview_page,
|
|
13
|
+
overview_view,
|
|
14
|
+
projects_page,
|
|
15
|
+
projects_view,
|
|
16
|
+
reports_page,
|
|
17
|
+
reports_view,
|
|
18
|
+
settings_page,
|
|
19
|
+
settings_view,
|
|
20
|
+
team_page,
|
|
21
|
+
team_view,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
serve(
|
|
25
|
+
"pyjs_examples.navigation.navigation",
|
|
26
|
+
routes={
|
|
27
|
+
("GET", "/"): overview_page,
|
|
28
|
+
("GET", "/analytics"): analytics_page,
|
|
29
|
+
("GET", "/calendar"): calendar_page,
|
|
30
|
+
("GET", "/messages"): messages_page,
|
|
31
|
+
("GET", "/projects"): projects_page,
|
|
32
|
+
("GET", "/reports"): reports_page,
|
|
33
|
+
("GET", "/settings"): settings_page,
|
|
34
|
+
("GET", "/team"): team_page,
|
|
35
|
+
},
|
|
36
|
+
layout=layout,
|
|
37
|
+
layout_id="atlas",
|
|
38
|
+
page_entry_points=(
|
|
39
|
+
overview_view,
|
|
40
|
+
analytics_view,
|
|
41
|
+
calendar_view,
|
|
42
|
+
messages_view,
|
|
43
|
+
projects_view,
|
|
44
|
+
reports_view,
|
|
45
|
+
settings_view,
|
|
46
|
+
team_view,
|
|
47
|
+
),
|
|
48
|
+
port=int(os.environ.get("PYJS_PORT", "8000")),
|
|
49
|
+
)
|