pyjs-examples 0.0.1__py3-none-any.whl
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/__init__.py +1 -0
- pyjs_examples/calculator/README.md +55 -0
- pyjs_examples/calculator/__init__.py +1 -0
- pyjs_examples/calculator/__main__.py +9 -0
- pyjs_examples/calculator/calculator.py +325 -0
- pyjs_examples/calculator/tests/test_browser_calculator.py +67 -0
- pyjs_examples/calculator/tests/test_calculator.py +72 -0
- pyjs_examples/catalog.py +1433 -0
- pyjs_examples/chatroom/README.md +50 -0
- pyjs_examples/chatroom/__init__.py +1 -0
- pyjs_examples/chatroom/__main__.py +12 -0
- pyjs_examples/chatroom/chat_server.py +156 -0
- pyjs_examples/chatroom/chatroom.py +704 -0
- pyjs_examples/chatroom/tests/test_browser_chatroom.py +59 -0
- pyjs_examples/chatroom/tests/test_chat_server.py +53 -0
- pyjs_examples/chatroom/visual_test.py +156 -0
- pyjs_examples/counter.py +73 -0
- pyjs_examples/kanban/README.md +31 -0
- pyjs_examples/kanban/__init__.py +1 -0
- pyjs_examples/kanban/__main__.py +8 -0
- pyjs_examples/kanban/kanban.py +581 -0
- pyjs_examples/kanban/tests/renders/kanban-board.png +0 -0
- pyjs_examples/kanban/tests/test_browser_kanban.py +150 -0
- pyjs_examples/kanban/tests/test_kanban.py +38 -0
- pyjs_examples/kanban/visual_test.py +108 -0
- pyjs_examples/nativescript_counter/README.md +31 -0
- pyjs_examples/nativescript_counter/__init__.py +5 -0
- pyjs_examples/nativescript_counter/__main__.py +17 -0
- pyjs_examples/nativescript_counter/counter.py +54 -0
- pyjs_examples/nativescript_counter/tests/test_android_counter.py +31 -0
- pyjs_examples/nativescript_counter/tests/test_counter.py +11 -0
- pyjs_examples/samples/__init__.py +1 -0
- pyjs_examples/samples/state_counter/README.md +19 -0
- pyjs_examples/samples/state_counter/__init__.py +1 -0
- pyjs_examples/samples/state_counter/__main__.py +3 -0
- pyjs_examples/samples/state_counter/state_counter.py +60 -0
- pyjs_examples/samples/state_counter/tests/test_browser_state_counter.py +18 -0
- pyjs_examples/samples/state_counter/tests/test_state_counter.py +17 -0
- pyjs_examples/samples/ui_dialog/README.md +19 -0
- pyjs_examples/samples/ui_dialog/__init__.py +1 -0
- pyjs_examples/samples/ui_dialog/__main__.py +3 -0
- pyjs_examples/samples/ui_dialog/tests/test_browser_ui_dialog.py +21 -0
- pyjs_examples/samples/ui_dialog/tests/test_ui_dialog.py +10 -0
- pyjs_examples/samples/ui_dialog/ui_dialog.py +65 -0
- pyjs_examples/space_invaders/README.md +45 -0
- pyjs_examples/space_invaders/__init__.py +1 -0
- pyjs_examples/space_invaders/__main__.py +3 -0
- pyjs_examples/space_invaders/space_invaders.py +1025 -0
- pyjs_examples/space_invaders/tests/renders/action_scene.png +0 -0
- pyjs_examples/space_invaders/tests/renders/initial_wave.png +0 -0
- pyjs_examples/space_invaders/tests/test_browser_game.py +86 -0
- pyjs_examples/space_invaders/tests/test_game.py +83 -0
- pyjs_examples/todomvc/README.md +50 -0
- pyjs_examples/todomvc/__init__.py +1 -0
- pyjs_examples/todomvc/__main__.py +12 -0
- pyjs_examples/todomvc/base.css +141 -0
- pyjs_examples/todomvc/index.css +393 -0
- pyjs_examples/todomvc/tests/test_browser_todomvc.py +145 -0
- pyjs_examples/todomvc/tests/test_todomvc.py +84 -0
- pyjs_examples/todomvc/todomvc.py +426 -0
- pyjs_examples/workspace/README.md +25 -0
- pyjs_examples/workspace/__init__.py +1 -0
- pyjs_examples/workspace/__main__.py +3 -0
- pyjs_examples/workspace/tests/test_browser_workspace.py +109 -0
- pyjs_examples/workspace/tests/test_workspace.py +27 -0
- pyjs_examples/workspace/workspace.py +749 -0
- pyjs_examples-0.0.1.dist-info/METADATA +71 -0
- pyjs_examples-0.0.1.dist-info/RECORD +71 -0
- pyjs_examples-0.0.1.dist-info/WHEEL +5 -0
- pyjs_examples-0.0.1.dist-info/entry_points.txt +11 -0
- pyjs_examples-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pyjs import js
|
|
3
|
+
from pyjs.testing.web import assert_render, mount
|
|
4
|
+
from pyjs.web.dom import Element
|
|
5
|
+
from pyjs.web.dragdrop import DataTransfer, DragEvent
|
|
6
|
+
from pyjs.web.events import Event
|
|
7
|
+
from pyjs_examples.kanban.kanban import KanbanBoard, KanbanCard, KanbanColumn
|
|
8
|
+
|
|
9
|
+
pytestmark = pytest.mark.pyjs_web
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@js
|
|
13
|
+
def mounted_board() -> KanbanBoard:
|
|
14
|
+
board = KanbanBoard()
|
|
15
|
+
mount(board)
|
|
16
|
+
return board
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@js
|
|
20
|
+
def card_order(column: KanbanColumn) -> str:
|
|
21
|
+
card_ids: list[str] = []
|
|
22
|
+
for card in column.cards.children:
|
|
23
|
+
card_id = card.getAttribute("data-card-id")
|
|
24
|
+
if card_id is not None:
|
|
25
|
+
card_ids.append(card_id)
|
|
26
|
+
return ",".join(card_ids)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@js
|
|
30
|
+
def drag_to(source: KanbanCard, target: Element, client_y: int = 0):
|
|
31
|
+
transfer = DataTransfer()
|
|
32
|
+
source.dispatchEvent(
|
|
33
|
+
DragEvent(
|
|
34
|
+
"dragstart",
|
|
35
|
+
bubbles=True,
|
|
36
|
+
cancelable=True,
|
|
37
|
+
dataTransfer=transfer,
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
target.dispatchEvent(
|
|
41
|
+
DragEvent(
|
|
42
|
+
"dragover",
|
|
43
|
+
bubbles=True,
|
|
44
|
+
cancelable=True,
|
|
45
|
+
clientY=client_y,
|
|
46
|
+
dataTransfer=transfer,
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
target.dispatchEvent(
|
|
50
|
+
DragEvent(
|
|
51
|
+
"drop",
|
|
52
|
+
bubbles=True,
|
|
53
|
+
cancelable=True,
|
|
54
|
+
clientY=client_y,
|
|
55
|
+
dataTransfer=transfer,
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
source.dispatchEvent(DragEvent("dragend", bubbles=True, dataTransfer=transfer))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@js
|
|
62
|
+
def drop_before(source: KanbanCard, target: KanbanCard):
|
|
63
|
+
bounds = target.getBoundingClientRect()
|
|
64
|
+
drag_to(source, target, round(bounds.top + 1))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@js
|
|
68
|
+
def drop_after(source: KanbanCard, target: KanbanCard):
|
|
69
|
+
bounds = target.getBoundingClientRect()
|
|
70
|
+
drag_to(source, target, round(bounds.bottom - 1))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@js
|
|
74
|
+
def test_drag_moves_a_card_between_columns_and_updates_counts():
|
|
75
|
+
board = mounted_board()
|
|
76
|
+
|
|
77
|
+
drag_to(board.onboarding_card, board.in_progress.cards)
|
|
78
|
+
|
|
79
|
+
assert card_order(board.todo) == "launch-checklist,customer-interview"
|
|
80
|
+
assert card_order(board.in_progress) == "drag-drop,empty-states,onboarding-audit"
|
|
81
|
+
assert board.todo.count.textContent == "2"
|
|
82
|
+
assert board.in_progress.count.textContent == "3"
|
|
83
|
+
assert board.onboarding_card.getAttribute("data-dragging") is None
|
|
84
|
+
assert not board.in_progress.cards.hasAttribute("data-drag-over")
|
|
85
|
+
assert board.status.textContent == "Audit onboarding flow moved to In progress"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@js
|
|
89
|
+
def test_drop_position_reorders_before_and_after_cards():
|
|
90
|
+
board = mounted_board()
|
|
91
|
+
|
|
92
|
+
drop_before(board.interview_card, board.onboarding_card)
|
|
93
|
+
assert card_order(board.todo) == (
|
|
94
|
+
"customer-interview,onboarding-audit,launch-checklist"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
drop_after(board.interview_card, board.checklist_card)
|
|
98
|
+
assert card_order(board.todo) == (
|
|
99
|
+
"onboarding-audit,launch-checklist,customer-interview"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@js
|
|
104
|
+
def test_invalid_and_self_drops_leave_the_board_unchanged():
|
|
105
|
+
board = mounted_board()
|
|
106
|
+
initial_todo = card_order(board.todo)
|
|
107
|
+
initial_done = card_order(board.done)
|
|
108
|
+
|
|
109
|
+
drag_to(board.onboarding_card, board.onboarding_card)
|
|
110
|
+
|
|
111
|
+
invalid = DataTransfer()
|
|
112
|
+
invalid.setData("application/x-pyjs-kanban-card", "missing-card")
|
|
113
|
+
board.done.cards.dispatchEvent(
|
|
114
|
+
DragEvent("drop", bubbles=True, cancelable=True, dataTransfer=invalid)
|
|
115
|
+
)
|
|
116
|
+
board.done.cards.dispatchEvent(DragEvent("drop", bubbles=True, cancelable=True))
|
|
117
|
+
|
|
118
|
+
assert card_order(board.todo) == initial_todo
|
|
119
|
+
assert card_order(board.done) == initial_done
|
|
120
|
+
assert board.todo.count.textContent == "3"
|
|
121
|
+
assert board.done.count.textContent == "2"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@js
|
|
125
|
+
def test_arrow_controls_move_and_reorder_cards_without_dragging():
|
|
126
|
+
board = mounted_board()
|
|
127
|
+
|
|
128
|
+
board.drag_card.move_down_button.dispatchEvent(Event("click"))
|
|
129
|
+
assert card_order(board.in_progress) == "empty-states,drag-drop"
|
|
130
|
+
assert board.drag_card.move_down_button.hasAttribute("disabled")
|
|
131
|
+
assert not board.drag_card.move_up_button.hasAttribute("disabled")
|
|
132
|
+
|
|
133
|
+
board.drag_card.move_left_button.dispatchEvent(Event("click"))
|
|
134
|
+
assert card_order(board.todo) == (
|
|
135
|
+
"onboarding-audit,launch-checklist,customer-interview,drag-drop"
|
|
136
|
+
)
|
|
137
|
+
assert card_order(board.in_progress) == "empty-states"
|
|
138
|
+
assert board.todo.count.textContent == "4"
|
|
139
|
+
assert board.in_progress.count.textContent == "1"
|
|
140
|
+
assert board.drag_card.move_left_button.hasAttribute("disabled")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@js
|
|
144
|
+
def test_kanban_visual_showcase():
|
|
145
|
+
board = mounted_board()
|
|
146
|
+
drag_to(board.onboarding_card, board.in_progress.cards)
|
|
147
|
+
|
|
148
|
+
assert card_order(board.in_progress) == "drag-drop,empty-states,onboarding-audit"
|
|
149
|
+
mount(board.columns)
|
|
150
|
+
assert_render(board.columns, "kanban-board", pixel_threshold=10)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pyjs.web.ssr import render_html
|
|
3
|
+
from pyjs_examples.kanban.kanban import KanbanBoard, KanbanCard, main
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.mark.pyjs_python
|
|
7
|
+
def test_board_renders_dom_owned_columns_and_cards():
|
|
8
|
+
board = main()
|
|
9
|
+
|
|
10
|
+
assert isinstance(board, KanbanBoard)
|
|
11
|
+
assert [len(column.cards.children) for column in (board.todo, board.in_progress, board.done)] == [
|
|
12
|
+
3,
|
|
13
|
+
2,
|
|
14
|
+
2,
|
|
15
|
+
]
|
|
16
|
+
count_html = render_html(board.todo.count)
|
|
17
|
+
assert 'data-column-count=""' in count_html
|
|
18
|
+
assert 'aria-label="Todo card count"' in count_html
|
|
19
|
+
assert count_html.endswith(">3</span>")
|
|
20
|
+
|
|
21
|
+
first = board.todo.cards.children[0]
|
|
22
|
+
assert isinstance(first, KanbanCard)
|
|
23
|
+
assert first.getAttribute("data-card-id") == "onboarding-audit"
|
|
24
|
+
assert first.getAttribute("draggable") == "true"
|
|
25
|
+
assert first.move_left_button.hasAttribute("disabled")
|
|
26
|
+
assert first.move_up_button.hasAttribute("disabled")
|
|
27
|
+
assert not first.move_right_button.hasAttribute("disabled")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.mark.pyjs_python
|
|
31
|
+
def test_main_board_contains_accessible_drag_alternatives():
|
|
32
|
+
board = KanbanBoard()
|
|
33
|
+
|
|
34
|
+
assert board.drag_card.move_left_button.getAttribute("aria-label") == (
|
|
35
|
+
"Move Build drag-and-drop left"
|
|
36
|
+
)
|
|
37
|
+
assert board.drag_card.move_up_button.getAttribute("title") == "Move up"
|
|
38
|
+
assert board.status.getAttribute("aria-live") == "polite"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from pyjs.tooling import stop_process
|
|
12
|
+
from pyjs.tooling.browser import Browser, BrowserPage, free_port, wait_for_http
|
|
13
|
+
|
|
14
|
+
EXAMPLE_ROOT = Path(__file__).resolve().parent
|
|
15
|
+
OUTPUT = Path.cwd() / "screenshots"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def board_state(page: BrowserPage) -> dict:
|
|
19
|
+
return {
|
|
20
|
+
"todo": await page.attributes('[data-testid="todo-cards"] > [data-card-id]', "data-card-id"),
|
|
21
|
+
"inProgress": await page.attributes(
|
|
22
|
+
'[data-testid="in-progress-cards"] > [data-card-id]', "data-card-id"
|
|
23
|
+
),
|
|
24
|
+
"done": await page.attributes('[data-testid="done-cards"] > [data-card-id]', "data-card-id"),
|
|
25
|
+
"counts": await page.texts("[data-column-count]"),
|
|
26
|
+
"status": await page.text("[data-board-status]"),
|
|
27
|
+
"activeDropZones": await page.attributes("[data-drag-over]", "data-testid"),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def exercise(page: BrowserPage) -> dict:
|
|
32
|
+
await page.wait_for_selector("kanban-board", "the hydrated Kanban board")
|
|
33
|
+
await page.drag(
|
|
34
|
+
'[data-card-id="onboarding-audit"]',
|
|
35
|
+
'[data-card-id="drag-drop"]',
|
|
36
|
+
target_y_ratio=0.2,
|
|
37
|
+
)
|
|
38
|
+
await page.wait_for(
|
|
39
|
+
"document.querySelector('[data-testid=\"in-progress-cards\"]')"
|
|
40
|
+
"?.firstElementChild?.getAttribute('data-card-id') === 'onboarding-audit'",
|
|
41
|
+
"the card to move through Chrome's native drag lifecycle",
|
|
42
|
+
)
|
|
43
|
+
await page.drag(
|
|
44
|
+
'[data-card-id="launch-checklist"]',
|
|
45
|
+
'[data-testid="done-cards"]',
|
|
46
|
+
target_y_ratio=0.95,
|
|
47
|
+
)
|
|
48
|
+
await page.wait_for(
|
|
49
|
+
"document.querySelector('[data-testid=\"done-cards\"]')"
|
|
50
|
+
"?.lastElementChild?.getAttribute('data-card-id') === 'launch-checklist'",
|
|
51
|
+
"a card dropped in the column's empty space",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
result = await board_state(page)
|
|
55
|
+
expected = {
|
|
56
|
+
"todo": ["customer-interview"],
|
|
57
|
+
"inProgress": ["onboarding-audit", "drag-drop", "empty-states"],
|
|
58
|
+
"done": ["scope-release", "browser-runner", "launch-checklist"],
|
|
59
|
+
"counts": ["1", "3", "3"],
|
|
60
|
+
"status": "Write launch checklist moved to Done",
|
|
61
|
+
"activeDropZones": [],
|
|
62
|
+
}
|
|
63
|
+
if result != expected:
|
|
64
|
+
raise RuntimeError(f"Unexpected board state: {json.dumps(result)}")
|
|
65
|
+
if page.exceptions:
|
|
66
|
+
raise RuntimeError(f"Browser errors: {'; '.join(page.exceptions)}")
|
|
67
|
+
|
|
68
|
+
await page.settle()
|
|
69
|
+
await page.screenshot(OUTPUT / "kanban.png", selector="[data-kanban-board]")
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def main() -> None:
|
|
74
|
+
port = free_port()
|
|
75
|
+
url = f"http://127.0.0.1:{port}/"
|
|
76
|
+
environment = os.environ.copy()
|
|
77
|
+
environment["PYJS_PORT"] = str(port)
|
|
78
|
+
with tempfile.TemporaryFile(mode="w+") as server_log:
|
|
79
|
+
server = subprocess.Popen(
|
|
80
|
+
[sys.executable, "kanban.py"],
|
|
81
|
+
cwd=EXAMPLE_ROOT,
|
|
82
|
+
env=environment,
|
|
83
|
+
stdout=server_log,
|
|
84
|
+
stderr=subprocess.STDOUT,
|
|
85
|
+
)
|
|
86
|
+
try:
|
|
87
|
+
await wait_for_http(url, timeout=20)
|
|
88
|
+
async with Browser() as browser:
|
|
89
|
+
page = await browser.new_page(url)
|
|
90
|
+
await page.set_viewport(1440, 1000)
|
|
91
|
+
result = await exercise(page)
|
|
92
|
+
OUTPUT.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
(OUTPUT / "kanban.json").write_text(
|
|
94
|
+
json.dumps(result, indent=2) + "\n",
|
|
95
|
+
encoding="utf-8",
|
|
96
|
+
)
|
|
97
|
+
print(json.dumps(result, indent=2))
|
|
98
|
+
except BaseException:
|
|
99
|
+
stop_process(server)
|
|
100
|
+
server_log.seek(0)
|
|
101
|
+
sys.stderr.write(server_log.read())
|
|
102
|
+
raise
|
|
103
|
+
finally:
|
|
104
|
+
stop_process(server)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# NativeScript Counter
|
|
2
|
+
|
|
3
|
+
A minimal native Android application written in Python and styled with Tailwind CSS. PyJS
|
|
4
|
+
transpiles the application and manages its private Node and NativeScript toolchain outside the
|
|
5
|
+
source tree.
|
|
6
|
+
|
|
7
|
+
With an Android SDK and at least one virtual device installed:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
python -m pyjs_examples.nativescript_counter
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The first run downloads the managed Node and NativeScript tools and creates a generated native
|
|
14
|
+
build workspace under `~/.cache/pyjs/`.
|
|
15
|
+
|
|
16
|
+
Run the host-side unit test normally:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
python -m pytest -q examples/nativescript_counter/tests
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Run the Android UI test on an existing emulator, or name an AVD for PyJS to start:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python -m pytest -q examples/nativescript_counter/tests \
|
|
26
|
+
--pyjs-target=android --pyjs-android-avd=Routine_Test
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The UI test launches the generated app, finds controls by NativeScript `testID`, taps the real
|
|
30
|
+
button, and waits for the native label to update. It retains a screenshot, Android UI hierarchy,
|
|
31
|
+
app-scoped logcat, and JSON artifact manifest under `.pytest_cache/pyjs/artifacts/android/`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from pyjs.tooling.nativescript import NativeScriptProject
|
|
2
|
+
|
|
3
|
+
from .counter import main
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run() -> None:
|
|
7
|
+
project = NativeScriptProject(
|
|
8
|
+
main,
|
|
9
|
+
name="PyJS Counter",
|
|
10
|
+
app_id="org.pyjs.counter",
|
|
11
|
+
)
|
|
12
|
+
device = project.run_android()
|
|
13
|
+
print(f"PyJS Counter is running on {device.identifier}.")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if __name__ == "__main__":
|
|
17
|
+
run()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from pyjs import js
|
|
2
|
+
from pyjs.nativescript import Application, Button, EventData, Label, Page, StackLayout
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@js
|
|
6
|
+
class Counter:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.count = 0
|
|
9
|
+
|
|
10
|
+
self.value = Label()
|
|
11
|
+
self.value.id = "counter-value"
|
|
12
|
+
self.value.testID = "counter-value"
|
|
13
|
+
self.value.text = "0"
|
|
14
|
+
self.value.className = "mb-6 text-center text-6xl font-bold text-slate-50"
|
|
15
|
+
|
|
16
|
+
self.button = Button()
|
|
17
|
+
self.button.id = "increment-button"
|
|
18
|
+
self.button.testID = "increment-button"
|
|
19
|
+
self.button.text = "Increment"
|
|
20
|
+
self.button.className = "mx-8 h-14 rounded-lg bg-sky-400 text-xl font-semibold text-sky-950"
|
|
21
|
+
self.button.on(Button.tapEvent, self.increment)
|
|
22
|
+
|
|
23
|
+
def increment(self, event: EventData):
|
|
24
|
+
self.count += 1
|
|
25
|
+
self.value.text = str(self.count)
|
|
26
|
+
|
|
27
|
+
def page(self) -> Page:
|
|
28
|
+
content = StackLayout()
|
|
29
|
+
content.className = "bg-slate-950 p-8"
|
|
30
|
+
content.verticalAlignment = "middle"
|
|
31
|
+
|
|
32
|
+
title = Label()
|
|
33
|
+
title.text = "PyJS + NativeScript"
|
|
34
|
+
title.className = "mb-4 text-center text-2xl font-bold text-sky-300"
|
|
35
|
+
|
|
36
|
+
content.addChild(title)
|
|
37
|
+
content.addChild(self.value)
|
|
38
|
+
content.addChild(self.button)
|
|
39
|
+
|
|
40
|
+
page = Page()
|
|
41
|
+
page.actionBarHidden = True
|
|
42
|
+
page.className = "bg-slate-950"
|
|
43
|
+
page.content = content
|
|
44
|
+
return page
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@js
|
|
48
|
+
def create_page() -> Page:
|
|
49
|
+
return Counter().page()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@js
|
|
53
|
+
def main():
|
|
54
|
+
Application.run(create_page)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pyjs.testing.android import AndroidTestSession
|
|
5
|
+
from pyjs.tooling.android import AndroidSDK
|
|
6
|
+
from pyjs.tooling.nativescript import NativeScriptProject
|
|
7
|
+
from pyjs_examples.nativescript_counter.counter import main
|
|
8
|
+
|
|
9
|
+
pytestmark = pytest.mark.pyjs_android
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_counter_runs_on_android(request, pyjs_artifacts_dir: Path):
|
|
13
|
+
android = AndroidSDK()
|
|
14
|
+
project = NativeScriptProject(
|
|
15
|
+
main,
|
|
16
|
+
name="PyJS Counter",
|
|
17
|
+
app_id="org.pyjs.counter",
|
|
18
|
+
android=android,
|
|
19
|
+
)
|
|
20
|
+
device = project.run_android(request.config.getoption("--pyjs-android-avd"))
|
|
21
|
+
session = AndroidTestSession(android, device, project.app_id)
|
|
22
|
+
artifacts = pyjs_artifacts_dir / "android"
|
|
23
|
+
|
|
24
|
+
with session.artifacts_on_failure(artifacts, "nativescript-counter"):
|
|
25
|
+
session.launch()
|
|
26
|
+
session.wait_for_element(test_id="counter-value", text="0")
|
|
27
|
+
session.tap_element(test_id="increment-button")
|
|
28
|
+
session.wait_for_element(test_id="counter-value", text="1")
|
|
29
|
+
assert session.log_errors() == ""
|
|
30
|
+
|
|
31
|
+
session.capture_artifacts(artifacts, "nativescript-counter")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Small, focused PyJS samples."""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Reactive counter
|
|
2
|
+
|
|
3
|
+
This focused PyJS sample demonstrates a state field, an effect, browser
|
|
4
|
+
events, server rendering, and tests in a complete runnable project.
|
|
5
|
+
|
|
6
|
+
## Run
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
python -m pyjs_examples.samples.state_counter
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Open <http://127.0.0.1:8000/>.
|
|
13
|
+
|
|
14
|
+
## Test
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
python -m pytest -q
|
|
18
|
+
python -m pytest -q --pyjs-target=web
|
|
19
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""A focused reactive-state sample."""
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from pyjs import effect, js, state
|
|
2
|
+
from pyjs.web.domx import CustomElement, button, ref, tag
|
|
3
|
+
from pyjs.web.events import Event
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@js
|
|
7
|
+
class Counter(CustomElement):
|
|
8
|
+
count = state(0)
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
super().__init__()
|
|
12
|
+
self.value = ref(tag("strong", {"style": "font-size:3rem"}, "0"))
|
|
13
|
+
self.decrement_button = ref(button({"type": "button"}, "−"))
|
|
14
|
+
self.increment_button = ref(button({"type": "button"}, "+"))
|
|
15
|
+
self.decrement_button.addEventListener("click", self.decrement)
|
|
16
|
+
self.increment_button.addEventListener("click", self.increment)
|
|
17
|
+
tag(
|
|
18
|
+
self,
|
|
19
|
+
tag(
|
|
20
|
+
"main",
|
|
21
|
+
{
|
|
22
|
+
"style": (
|
|
23
|
+
"min-height:100vh;display:grid;place-items:center;background:#f8fafc;"
|
|
24
|
+
"font:16px system-ui,sans-serif"
|
|
25
|
+
)
|
|
26
|
+
},
|
|
27
|
+
tag(
|
|
28
|
+
"section",
|
|
29
|
+
{
|
|
30
|
+
"style": (
|
|
31
|
+
"display:grid;grid-template-columns:3rem 8rem 3rem;align-items:center;"
|
|
32
|
+
"gap:1rem;text-align:center"
|
|
33
|
+
)
|
|
34
|
+
},
|
|
35
|
+
self.decrement_button,
|
|
36
|
+
self.value,
|
|
37
|
+
self.increment_button,
|
|
38
|
+
),
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@effect(count)
|
|
43
|
+
def render_count(self):
|
|
44
|
+
self.value.textContent = str(self.count)
|
|
45
|
+
|
|
46
|
+
def decrement(self, event: Event):
|
|
47
|
+
self.count -= 1
|
|
48
|
+
|
|
49
|
+
def increment(self, event: Event):
|
|
50
|
+
self.count += 1
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main():
|
|
54
|
+
return Counter()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
from pyjs.server import serve
|
|
59
|
+
|
|
60
|
+
serve("state_counter")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pyjs import js
|
|
3
|
+
from pyjs.web.dom import document
|
|
4
|
+
from pyjs.web.events import Event
|
|
5
|
+
from pyjs_examples.samples.state_counter.state_counter import Counter
|
|
6
|
+
|
|
7
|
+
pytestmark = pytest.mark.pyjs_web
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@js
|
|
11
|
+
def test_counter_buttons_update_the_mounted_element():
|
|
12
|
+
document.body.replaceChildren()
|
|
13
|
+
counter = Counter()
|
|
14
|
+
document.body.append(counter)
|
|
15
|
+
|
|
16
|
+
counter.increment_button.dispatchEvent(Event("click"))
|
|
17
|
+
|
|
18
|
+
assert counter.value.textContent == "1"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pyjs import js
|
|
3
|
+
from pyjs.web.events import Event
|
|
4
|
+
from pyjs_examples.samples.state_counter.state_counter import Counter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.pyjs_python
|
|
8
|
+
@js
|
|
9
|
+
def test_counter_changes_in_both_directions():
|
|
10
|
+
counter = Counter()
|
|
11
|
+
|
|
12
|
+
counter.increment(Event("click"))
|
|
13
|
+
counter.increment(Event("click"))
|
|
14
|
+
counter.decrement(Event("click"))
|
|
15
|
+
|
|
16
|
+
assert counter.count == 1
|
|
17
|
+
assert counter.value.textContent == "1"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Dialog
|
|
2
|
+
|
|
3
|
+
This focused PyJS sample places the `pyjs.web` dialog component in a
|
|
4
|
+
complete runnable and tested project.
|
|
5
|
+
|
|
6
|
+
## Run
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
python -m pyjs_examples.samples.ui_dialog
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Open <http://127.0.0.1:8000/>.
|
|
13
|
+
|
|
14
|
+
## Test
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
python -m pytest -q
|
|
18
|
+
python -m pytest -q --pyjs-target=web
|
|
19
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""A focused accessible-dialog sample."""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pyjs import js
|
|
3
|
+
from pyjs.web.dom import document
|
|
4
|
+
from pyjs.web.events import Event
|
|
5
|
+
from pyjs_examples.samples.ui_dialog.ui_dialog import DialogSample
|
|
6
|
+
|
|
7
|
+
pytestmark = pytest.mark.pyjs_web
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@js
|
|
11
|
+
def test_dialog_opens_and_closes():
|
|
12
|
+
document.body.replaceChildren()
|
|
13
|
+
app = DialogSample()
|
|
14
|
+
document.body.append(app)
|
|
15
|
+
|
|
16
|
+
app.dialog.trigger.element.dispatchEvent(Event("click"))
|
|
17
|
+
assert app.dialog.is_open
|
|
18
|
+
assert app.dialog.content.dialog_element.open
|
|
19
|
+
|
|
20
|
+
app.dialog.close()
|
|
21
|
+
assert not app.dialog.is_open
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pyjs_examples.samples.ui_dialog.ui_dialog import DialogSample
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@pytest.mark.pyjs_python
|
|
6
|
+
def test_dialog_sample_has_an_accessible_label():
|
|
7
|
+
app = DialogSample()
|
|
8
|
+
|
|
9
|
+
assert app.dialog.content.dialog_element.getAttribute("aria-label") == "Welcome to PyJS"
|
|
10
|
+
assert app.dialog.trigger.element.getAttribute("aria-haspopup") == "dialog"
|