portacode 1.3.32__py3-none-any.whl → 1.4.15.dev3__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.
Files changed (57) hide show
  1. portacode/_version.py +2 -2
  2. portacode/cli.py +158 -14
  3. portacode/connection/client.py +127 -8
  4. portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +600 -4
  5. portacode/connection/handlers/__init__.py +30 -1
  6. portacode/connection/handlers/diff_handlers.py +603 -0
  7. portacode/connection/handlers/file_handlers.py +674 -17
  8. portacode/connection/handlers/project_aware_file_handlers.py +11 -0
  9. portacode/connection/handlers/project_state/file_system_watcher.py +31 -61
  10. portacode/connection/handlers/project_state/git_manager.py +139 -572
  11. portacode/connection/handlers/project_state/handlers.py +28 -14
  12. portacode/connection/handlers/project_state/manager.py +226 -101
  13. portacode/connection/handlers/proxmox_infra.py +2082 -0
  14. portacode/connection/handlers/session.py +465 -84
  15. portacode/connection/handlers/system_handlers.py +311 -9
  16. portacode/connection/handlers/tab_factory.py +1 -47
  17. portacode/connection/handlers/test_proxmox_infra.py +13 -0
  18. portacode/connection/handlers/update_handler.py +61 -0
  19. portacode/connection/terminal.py +64 -10
  20. portacode/keypair.py +63 -1
  21. portacode/link_capture/__init__.py +38 -0
  22. portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
  23. portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
  24. portacode/link_capture/bin/elinks +3 -0
  25. portacode/link_capture/bin/gio-open +3 -0
  26. portacode/link_capture/bin/gnome-open +3 -0
  27. portacode/link_capture/bin/gvfs-open +3 -0
  28. portacode/link_capture/bin/kde-open +3 -0
  29. portacode/link_capture/bin/kfmclient +3 -0
  30. portacode/link_capture/bin/link_capture_exec.sh +11 -0
  31. portacode/link_capture/bin/link_capture_wrapper.py +75 -0
  32. portacode/link_capture/bin/links +3 -0
  33. portacode/link_capture/bin/links2 +3 -0
  34. portacode/link_capture/bin/lynx +3 -0
  35. portacode/link_capture/bin/mate-open +3 -0
  36. portacode/link_capture/bin/netsurf +3 -0
  37. portacode/link_capture/bin/sensible-browser +3 -0
  38. portacode/link_capture/bin/w3m +3 -0
  39. portacode/link_capture/bin/x-www-browser +3 -0
  40. portacode/link_capture/bin/xdg-open +3 -0
  41. portacode/pairing.py +103 -0
  42. portacode/static/js/utils/ntp-clock.js +170 -79
  43. portacode/utils/diff_apply.py +456 -0
  44. portacode/utils/diff_renderer.py +371 -0
  45. portacode/utils/ntp_clock.py +45 -131
  46. {portacode-1.3.32.dist-info → portacode-1.4.15.dev3.dist-info}/METADATA +71 -3
  47. portacode-1.4.15.dev3.dist-info/RECORD +98 -0
  48. {portacode-1.3.32.dist-info → portacode-1.4.15.dev3.dist-info}/WHEEL +1 -1
  49. test_modules/test_device_online.py +1 -1
  50. test_modules/test_login_flow.py +8 -4
  51. test_modules/test_play_store_screenshots.py +294 -0
  52. testing_framework/.env.example +4 -1
  53. testing_framework/core/playwright_manager.py +63 -9
  54. portacode-1.3.32.dist-info/RECORD +0 -70
  55. {portacode-1.3.32.dist-info → portacode-1.4.15.dev3.dist-info}/entry_points.txt +0 -0
  56. {portacode-1.3.32.dist-info → portacode-1.4.15.dev3.dist-info}/licenses/LICENSE +0 -0
  57. {portacode-1.3.32.dist-info → portacode-1.4.15.dev3.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.3.32
3
+ Version: 1.4.15.dev3
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
7
7
  Author-email: hi@menas.pro
8
8
  Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.8
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
9
13
  Classifier: License :: OSI Approved :: MIT License
10
14
  Classifier: Operating System :: OS Independent
11
15
  Requires-Python: >=3.8
@@ -24,6 +28,7 @@ Requires-Dist: watchdog>=3.0
24
28
  Requires-Dist: diff-match-patch>=20230430
25
29
  Requires-Dist: Pygments>=2.14.0
26
30
  Requires-Dist: ntplib>=0.4.0
31
+ Requires-Dist: importlib_resources>=6.0
27
32
  Provides-Extra: dev
28
33
  Requires-Dist: black; extra == "dev"
29
34
  Requires-Dist: flake8; extra == "dev"
@@ -85,6 +90,33 @@ Once connected, you can:
85
90
  - Monitor system status
86
91
  - Access your development environment from any device
87
92
 
93
+ Want to see Portacode running inside containers or powering classrooms? Browse the [`examples/` directory](https://github.com/portacode/portacode/tree/master/examples) (also bundled in the PyPI source) for copy-paste Docker Compose setups ranging from a single-device sandbox to a ten-seat workshop fleet.
94
+
95
+ ## 🔑 Pair Devices with Zero-Touch Codes
96
+
97
+ The fastest way to bring a new machine online is with a short-lived pairing code:
98
+
99
+ 1. Log in to [https://portacode.com](https://portacode.com) and press **Pair Device** on the dashboard:
100
+ ![Pair Device button](https://raw.githubusercontent.com/portacode/portacode/master/docs/images/pair-device-button.png)
101
+ 2. A four-digit code appears (valid for 15 minutes). This code only authorizes a pairing **request**—no device can reach your account until you approve it.
102
+ 3. On the device, run Portacode with the code:
103
+ ```bash
104
+ PORTACODE_PAIRING_CODE=1234 portacode connect \
105
+ --device-name "My Laptop" \
106
+ --project-path /srv/project-one \
107
+ --project-path /srv/project-two
108
+ ```
109
+ - `--device-name` (or `PORTACODE_DEVICE_NAME`) pre-fills the friendly label shown in the dashboard.
110
+ - Repeat `--project-path /abs/path` to register up to ten Projects automatically once the request is approved.
111
+ - Automating inside Docker? Export your own `PORTACODE_PROJECT_PATHS=/srv/a:/srv/b` and convert it into repeated `--project-path` switches before invoking the CLI—see `portacode_for_school/persistent_workspace/entrypoint.sh` for a reference implementation.
112
+ 4. Because the device has no fingerprint yet, the CLI bootstraps an in-memory keypair and announces a pending request to the dashboard. You immediately see the card with the supplied metadata:
113
+ ![Pairing request card](https://raw.githubusercontent.com/portacode/portacode/master/docs/images/pairing-request.png)
114
+ 5. Click **Approve**. The CLI persists the keypair on disk and transitions into a normal authenticated connection. Future `portacode connect` runs reuse the stored RSA keys—no additional codes required unless you revoke the device.
115
+
116
+ Need to pair multiple machines at once? A single pairing code can be reused concurrently: every device that launches `portacode connect` with that code shows up as its own approval card until you accept or decline it.
117
+
118
+ This workflow works great for headless setups and containers: export the environment variables, run `portacode connect --non-interactive`, and finish the approval from the dashboard.
119
+
88
120
  ## 💡 Use Cases
89
121
 
90
122
  - **Remote Development**: Code, build, and debug from anywhere - even your phone
@@ -186,11 +218,47 @@ sudo apt-get install xclip
186
218
  ```
187
219
 
188
220
  ### Key Management
189
- Key files are stored in:
221
+ Portacode follows the OS-specific *user data* directory (via [`platformdirs`](https://pypi.org/project/platformdirs/)) and keeps its identity in `portacode/keys/`:
190
222
  - **Linux**: `~/.local/share/portacode/keys/`
191
223
  - **macOS**: `~/Library/Application Support/portacode/keys/`
192
224
  - **Windows**: `%APPDATA%\portacode\keys\`
193
225
 
226
+ When `PORTACODE_PAIRING_CODE` is set, the CLI generates an in-memory keypair, waits for dashboard approval, and only then writes the files to this directory. If that folder disappears, the CLI will create a fresh identity next time it runs.
227
+
228
+ #### Persisting Keys in Containers
229
+ Docker images (including the simple `python:3.11-slim` example that runs Portacode as `root`) store the data inside `/root/.local/share/portacode`. Bind-mount that path or override `XDG_DATA_HOME` so the keys survive container restarts:
230
+
231
+ ```yaml
232
+ services:
233
+ device-01:
234
+ build: .
235
+ environment:
236
+ PORTACODE_PAIRING_CODE: "${PORTACODE_PAIRING_CODE:-}"
237
+ volumes:
238
+ - ./data/device-01/workspace:/root/workspace
239
+ - ./data/device-01/.local/share/portacode:/root/.local/share/portacode # persists device keys
240
+ ```
241
+
242
+ Alternatively, set `XDG_DATA_HOME=/root/.portacode` before running `portacode connect` and mount that directory from the host. The rule of thumb: **persist whichever folder contains `.local/share/portacode/keys/`** so your device fingerprint sticks around.
243
+
244
+ #### Minimal Docker Example
245
+ If you want a plug-and-play container, check the `examples/simple_device/` folder that ships with this repo and the PyPI source distribution. It contains a tiny `Dockerfile` and `docker-compose.yaml` you can copy as-is. The Dockerfile installs `git` before `pip install portacode` so GitPython can interact with repositories—remember to do the same in your own images if you expect to work inside version-controlled projects.
246
+
247
+ The accompanying Compose file demonstrates how to:
248
+ - run `portacode connect --non-interactive` with a predefined `--device-name` and `--project-path`
249
+ - pass `PORTACODE_PAIRING_CODE` via environment variables
250
+ - bind-mount your workspace plus `/root/.local/share/portacode` for key persistence
251
+
252
+ Together, those 10 lines illustrate the complete flow for remotely accessing a Docker-hosted machine with Portacode.
253
+
254
+ #### Workshop Fleet Example
255
+ Training a group? `examples/workshop_fleet/` spins up ten identical containers—one per student—with their own workspace bind mounts plus a shared read-only `instructions/` folder. The Dockerfile in that folder copies everything from `initial_content/` into the image (`COPY initial_content/ /opt/initial_content/`), and the compose command seeds each student workspace on boot via `cp -an /opt/initial_content/. /root/workspace/`. That means:
256
+ - Instructors drop starter code into `initial_content/` before `docker compose up` and every container gets the same seed files without overwriting student changes after the first sync.
257
+ - The host `instructions/` directory is mounted at `/root/workspace/instructions` in **read-only** mode, so you can update agendas or hints live while students can only view them.
258
+ - Each seat persists its Portacode identity in `data/student-XX/.local/share/portacode`, so reconnecting after a restart does not need new pairing codes.
259
+
260
+ See the full walkthrough and assets in [`examples/workshop_fleet/`](https://github.com/portacode/portacode/tree/master/examples/workshop_fleet), which is also shipped inside the PyPI source tarball for offline access.
261
+
194
262
  ## 🌱 Early Stage Project
195
263
 
196
264
  **Portacode is a young project with big dreams.** We're building the future of remote development and mobile-first coding experiences. As a new project, we're actively seeking:
@@ -221,7 +289,7 @@ Check out our [GitHub repository](https://github.com/portacode/portacode) to get
221
289
 
222
290
  ## 📄 License
223
291
 
224
- MIT License - see [LICENSE](LICENSE) file for details.
292
+ MIT License - see [LICENSE](https://github.com/portacode/portacode/blob/master/LICENSE) file for details.
225
293
 
226
294
  ---
227
295
 
@@ -0,0 +1,98 @@
1
+ portacode/README.md,sha256=4dKtpvR8LNgZPVz37GmkQCMWIr_u25Ao63iW56s7Ke4,775
2
+ portacode/__init__.py,sha256=oB3sV1wXr-um-RXio73UG8E5Xx6cF2ZVJveqjNmC-vQ,1086
3
+ portacode/__main__.py,sha256=jmHTGC1hzmo9iKJLv-SSYe9BSIbPPZ2IOpecI03PlTs,296
4
+ portacode/_version.py,sha256=1sfx8JK_QXWbIlvcX2Sn3XEnIw9YLhiiBq9EMvNgnss,719
5
+ portacode/cli.py,sha256=mGLKoZ-T2FBF7IA9wUq0zyG0X9__-A1ao7gajjcVRH8,21828
6
+ portacode/data.py,sha256=5-s291bv8J354myaHm1Y7CQZTZyRzMU3TGe5U4hb-FA,1591
7
+ portacode/keypair.py,sha256=0OO4vHDcF1XMxCDqce61xFTlFwlTcmqe5HyGsXFEt7s,5838
8
+ portacode/logging_categories.py,sha256=9m-BYrjyHh1vjZYBQT4JhAh6b_oYUhIWayO-noH1cSE,5063
9
+ portacode/pairing.py,sha256=OzSuc0GhrknrDrny4aBU6IUnmKzRDTtocuDpyaVnyrs,3116
10
+ portacode/service.py,sha256=p-HHMOAl20QsdcJydcZ74Iqes-wl8G8HItdSim30pUk,16537
11
+ portacode/connection/README.md,sha256=f9rbuIEKa7cTm9C98rCiBbEtbiIXQU11esGSNhSMiJg,883
12
+ portacode/connection/__init__.py,sha256=atqcVGkViIEd7pRa6cP2do07RJOM0UWpbnz5zXjGktU,250
13
+ portacode/connection/client.py,sha256=jtLb9_YufqPkzi9t8VQH3iz_JEMisbtY6a8L9U5weiU,14181
14
+ portacode/connection/multiplex.py,sha256=L-TxqJ_ZEbfNEfu1cwxgJ5vUdyRzZjsMy2Kx1diiZys,5237
15
+ portacode/connection/terminal.py,sha256=oyLPOVLPlUuN_eRvHPGazB51yi8W8JEF3oOEYxucGTE,45069
16
+ portacode/connection/handlers/README.md,sha256=HsLZG1QK1JNm67HsgL6WoDg9nxzKXxwkc5fJPFJdX5g,12169
17
+ portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=5VZSDPvXjtOl-8YZ-2UVtH8WgPnEWe7WB33YPrS8_8Q,104190
18
+ portacode/connection/handlers/__init__.py,sha256=j69jGkf2-mYyCicvYfp2wk8-xB8yqpWktiN5xADXBno,3137
19
+ portacode/connection/handlers/base.py,sha256=oENFb-Fcfzwk99Qx8gJQriEMiwSxwygwjOiuCH36hM4,10231
20
+ portacode/connection/handlers/chunked_content.py,sha256=h6hXRmxSeOgnIxoU8CkmvEf2Odv-ajPrpHIe_W3GKcA,9251
21
+ portacode/connection/handlers/diff_handlers.py,sha256=iYTIRCcpEQ03vIPKZCsMTE5aZbQw6sF04M3dM6rUV8Q,24477
22
+ portacode/connection/handlers/file_handlers.py,sha256=nAJH8nXnX07xxD28ngLpgIUzcTuRwZBNpEGEKdRqohw,39507
23
+ portacode/connection/handlers/project_aware_file_handlers.py,sha256=AqgMnDqX2893T2NsrvUSCwjN5VKj4Pb2TN0S_SuboOE,9803
24
+ portacode/connection/handlers/project_state_handlers.py,sha256=v6ZefGW9i7n1aZLq2jOGumJIjYb6aHlPI4m1jkYewm8,1686
25
+ portacode/connection/handlers/proxmox_infra.py,sha256=mH6xxT9v9X0jwdCBjU3p48lURYisY0T0HPjuzV3eoH8,76070
26
+ portacode/connection/handlers/registry.py,sha256=qXGE60sYEWg6ZtVQzFcZ5YI2XWR6lMgw4hAL9x5qR1I,6181
27
+ portacode/connection/handlers/session.py,sha256=uNGfiO_1B9-_yjJKkpvmbiJhIl6b-UXlT86UTfd6WYE,42219
28
+ portacode/connection/handlers/system_handlers.py,sha256=fr12QpOr_Z8KYGUU-AYrTQwRPAcrLK85hvj3SEq1Kw8,14757
29
+ portacode/connection/handlers/tab_factory.py,sha256=yn93h6GASjD1VpvW1oqpax3EpoT0r7r97zFXxML1wdA,16173
30
+ portacode/connection/handlers/terminal_handlers.py,sha256=HRwHW1GiqG1NtHVEqXHKaYkFfQEzCDDH6YIlHcb4XD8,11866
31
+ portacode/connection/handlers/test_proxmox_infra.py,sha256=d6iBB4pwAqWWdEGRayLxDEexqCElbGZDJlCB4bXba24,682
32
+ portacode/connection/handlers/update_handler.py,sha256=f2K4LmG4sHJZ3LahzzoRtHBULTKkPUNwuyhwuAAg3RA,2054
33
+ portacode/connection/handlers/project_state/README.md,sha256=trdd4ig6ungmwH5SpbSLfyxbL-QgPlGNU-_XrMEiXtw,10114
34
+ portacode/connection/handlers/project_state/__init__.py,sha256=5ucIqk6Iclqg6bKkL8r_wVs5Tlt6B9J7yQH6yQUt7gc,2541
35
+ portacode/connection/handlers/project_state/file_system_watcher.py,sha256=r9_UKxWTbzum0jGqxIafe68Ced2Y3xOp3ZmkpBOfRpw,8573
36
+ portacode/connection/handlers/project_state/git_manager.py,sha256=iGQ7LYIA7uHsZHdj3HEc_LYo7S1Lqv6-AeyyMwknBPo,70027
37
+ portacode/connection/handlers/project_state/handlers.py,sha256=qgOSt26rxAGNxW07AoevTwDPBdxblX4J-dX-EjOKtg4,38232
38
+ portacode/connection/handlers/project_state/manager.py,sha256=pRMZqPOTK9YE3abNxiAbnERIJmRys673HFOEIBiKnm4,67184
39
+ portacode/connection/handlers/project_state/models.py,sha256=EZTKvxHKs8QlQUbzI0u2IqfzfRRXZixUIDBwTGCJATI,4313
40
+ portacode/connection/handlers/project_state/utils.py,sha256=LsbQr9TH9Bz30FqikmtTxco4PlB_n0kUIuPKQ6Fb_mo,1665
41
+ portacode/link_capture/__init__.py,sha256=93LjyYDqzOimsIDBhsPibTl7tr-8DiIzyDF7JWQkE2A,1231
42
+ portacode/link_capture/__pycache__/__init__.cpython-311.pyc,sha256=yKwOu63AoGpmk4l-jfGnt2G2YkI54I8MppgDvq8K4_s,2037
43
+ portacode/link_capture/bin/elinks,sha256=VWEQlNcPazqaEJdDMbXmXJ2SLaAqN__IUBQNp8sSSto,104
44
+ portacode/link_capture/bin/gio-open,sha256=jOYX5fCkqWfFAY6ebJt6C1UcXISHa4iRmLePs5jU3Ko,106
45
+ portacode/link_capture/bin/gnome-open,sha256=oZHDhCtUWURmPry5EgkZjeJWZC2f10Qk101Dc4fNTVs,108
46
+ portacode/link_capture/bin/gvfs-open,sha256=6lYC7RZJIQGwvGz7cCLPFkDTf29TNS3xuPJsSxR1kbs,107
47
+ portacode/link_capture/bin/kde-open,sha256=9tz3N3horN_SCxl1sE8jAQ2OVy004Ebtl8llKz-mz70,106
48
+ portacode/link_capture/bin/kfmclient,sha256=wkhcZdZFJ161Nua8mGZ-wtCAtzgLkqycR5Ait7Wus-g,107
49
+ portacode/link_capture/bin/link_capture_exec.sh,sha256=bFyyuR7z7sEq-laOn9PLje5eoCLXNhgQvTrBLy3jKRg,234
50
+ portacode/link_capture/bin/link_capture_wrapper.py,sha256=wvfCx2Urjzv5vNO1_58AAe4kKx2kVsmn9OoJwr6bnYI,2042
51
+ portacode/link_capture/bin/links,sha256=BD3tioDtCXD-8KRcLEOaF53T4y9CWIZ8ANasDGy0mic,103
52
+ portacode/link_capture/bin/links2,sha256=2tD7Iol-CyRJQdKs1VoNfRgaiui2mPsqcD9NolAr5_I,104
53
+ portacode/link_capture/bin/lynx,sha256=FbiePbYRM1ozi1xP779ku05W2PwgKie_oRfj1KqrT3E,102
54
+ portacode/link_capture/bin/mate-open,sha256=mL-sj3LVpzN-7Vj6gwFY8TMEG5kiRqyjwkdhMy5GRQQ,107
55
+ portacode/link_capture/bin/netsurf,sha256=Z308xsqfJOwZGdsE3atZXdeJoGl1pAEzoaX4jSMPALg,105
56
+ portacode/link_capture/bin/sensible-browser,sha256=oKVTZM1Z_mLEI2M-Lm-eaBK-Uqc00kwsa69qLhmx-kQ,114
57
+ portacode/link_capture/bin/w3m,sha256=mdM0AF4h_ElkfI2YZ-Ko1KHgWk2zLQrRwb2w1QQxXU8,101
58
+ portacode/link_capture/bin/x-www-browser,sha256=3RhqrunuuVktUf6WrbozW7qKlvfIlwXShl8PwF6Q80k,111
59
+ portacode/link_capture/bin/xdg-open,sha256=GLUv5ejdN8tym4PNtgKqTcZpF7YHIO3BnlWs3z3SQ3U,106
60
+ portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc,sha256=8NVUUJ1njsjCqZrZ2HPDmweauJQQ2W_9OWBC8Hi_o74,6049
61
+ portacode/static/js/test-ntp-clock.html,sha256=bUow9sifIuLNPqKvuPbpQozmEE6RhdCI4Plib3CqUmw,2130
62
+ portacode/static/js/utils/ntp-clock.js,sha256=t9moJyAGGU054BVtuGuSETVstlj1AoxVy44i4y9QWSs,6940
63
+ portacode/utils/NTP_ARCHITECTURE.md,sha256=WkESTbz5SNAgdmDKk3DrHMhtYOPji_Kt3_a9arWdRig,3894
64
+ portacode/utils/__init__.py,sha256=NgBlWTuNJESfIYJzP_3adI1yJQJR0XJLRpSdVNaBAN0,33
65
+ portacode/utils/diff_apply.py,sha256=4Oi7ft3VUCKmiUE4VM-OeqO7Gk6H7PF3WnN4WHXtjxI,15157
66
+ portacode/utils/diff_renderer.py,sha256=S76StnQ2DLfsz4Gg0m07UwPfRp8270PuzbNaQq-rmYk,13850
67
+ portacode/utils/ntp_clock.py,sha256=VqCnWCTehCufE43W23oB-WUdAZGeCcLxkmIOPwInYHc,2499
68
+ portacode-1.4.15.dev3.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
69
+ test_modules/README.md,sha256=Do_agkm9WhSzueXjRAkV_xEj6Emy5zB3N3VKY5Roce8,9274
70
+ test_modules/__init__.py,sha256=1LcbHodIHsB0g-g4NGjSn6AMuCoGbymvXPYLOb6Z7F0,53
71
+ test_modules/test_device_online.py,sha256=QtYq0Dq9vME8Gp2O4fGSheqVf8LUtpsSKosXXk56gGM,1654
72
+ test_modules/test_file_operations.py,sha256=KXbh9t8Fah1jZp1pEPlU4_F06iJIJr2fR-yYc4RL6m8,38372
73
+ test_modules/test_git_status_ui.py,sha256=A_qkt-0lFLwxdr7t6YQaM0HqUElDwlZi84mlngg11RA,18734
74
+ test_modules/test_login_flow.py,sha256=LyKAgd6jkhO7cvy2zgGtAuUTgkD2G4otS_1hZ6O2jZo,1882
75
+ test_modules/test_navigate_testing_folder.py,sha256=-1EXceUEwof_sYp5paMWUNT3mAv5aIpYJ65_vqFbZew,18233
76
+ test_modules/test_play_store_screenshots.py,sha256=4t9EdB-BjIhAj2EA7yHchsiI3Z5bqmIGI26Dzebru2s,11094
77
+ test_modules/test_terminal_buffer_performance.py,sha256=YQeDDZVnsQD3ug6udKUZH3NR7PHGP75uZsLZJYya7jg,12183
78
+ test_modules/test_terminal_interaction.py,sha256=AxLb63oKhNLjKrny4hBj4hhFhrmHZ5UGStYDA0KzA0w,3163
79
+ test_modules/test_terminal_loading_race_condition.py,sha256=PsGF8QzWeNNv6G7Fda6kETcBUcXyg_vRYeD-hDHAhCo,4158
80
+ test_modules/test_terminal_start.py,sha256=y3IqG54UfMk-pAQ_fn5LuoM3kki6xRm11oB5AzfC-iE,1978
81
+ testing_framework/.env.example,sha256=zGchLcB-p22YUUCU0JIyHLduLpDuFy8c5xPacctHvfY,708
82
+ testing_framework/README.md,sha256=7o04mS2siNDuHA1UBh3Uu6XCbGomKjgb8gfl8YbClhE,9662
83
+ testing_framework/__init__.py,sha256=safHXo_xBMwAwfiF_5rx0xGcPGfpBSOgkMZx04uj4No,575
84
+ testing_framework/cli.py,sha256=ZHO37QO2IqZpC9VovrAYur2Vfc2AYeDqzi9Nb4lIA-w,13434
85
+ testing_framework/requirements.txt,sha256=VeKSPyqS4MWwLhr0Upu7fImlXZQQjtL8uYeyHOjkYaE,249
86
+ testing_framework/core/__init__.py,sha256=8AJQgqSCa9WgwkQNH_wTsA3JmJ4d4FRCweI-ioDgcNI,40
87
+ testing_framework/core/base_test.py,sha256=0kKQDNCdAJyTQfJiMBzx9_2MMRrmaVfQF0cawhvian4,13149
88
+ testing_framework/core/cli_manager.py,sha256=LDH_tWn-CmO08U_rmBIPpN_O6HLaQKRjdnfKGrtqs8Y,6991
89
+ testing_framework/core/hierarchical_runner.py,sha256=tCeksh2cXbRspurSiE-mQM1M1BOPeY8mKFbjvaBTVHw,26401
90
+ testing_framework/core/playwright_manager.py,sha256=Tw46qwxIhOFkS48C2IWIQHHNpEe-iI5MSPS2P7zZAmk,22249
91
+ testing_framework/core/runner.py,sha256=j2QwNJmAxVBmJvcbVS7DgPJUKPNzqfLmt_4NNdaKmZU,19297
92
+ testing_framework/core/shared_cli_manager.py,sha256=BESSNtyQb7BOlaOvZmm04T8Uezjms4KCBs2MzTxvzYQ,8790
93
+ testing_framework/core/test_discovery.py,sha256=2FZ9fJ8Dp5dloA-fkgXoJ_gCMC_nYPBnA3Hs2xlagzM,4928
94
+ portacode-1.4.15.dev3.dist-info/METADATA,sha256=fhua1K0pTVsxZaun0VkgDDpUBh5AdovF_z9U6_LyktE,13051
95
+ portacode-1.4.15.dev3.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
96
+ portacode-1.4.15.dev3.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
+ portacode-1.4.15.dev3.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
+ portacode-1.4.15.dev3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -41,4 +41,4 @@ class DeviceOnlineTest(BaseTest):
41
41
 
42
42
  async def teardown(self):
43
43
  """Teardown for device online test."""
44
- pass
44
+ pass
@@ -1,5 +1,7 @@
1
1
  """Login flow test - simplified and fast."""
2
2
 
3
+ import os
4
+
3
5
  from testing_framework.core.base_test import BaseTest, TestResult, TestCategory
4
6
 
5
7
 
@@ -28,9 +30,11 @@ class LoginFlowTest(BaseTest):
28
30
  assert_that.status_ok(response, "Dashboard request")
29
31
  assert_that.url_contains(page, "/dashboard", "Dashboard URL")
30
32
 
31
- # Verify we have active sessions
32
- active_sessions = self.inspect().get_active_sessions()
33
- assert_that.is_true(len(active_sessions) > 0, "Should have active sessions")
33
+ # Verify we have active sessions unless explicitly allowed to skip
34
+ allow_empty_sessions = os.getenv("ALLOW_EMPTY_SESSIONS", "false").lower() in ("1", "true", "yes")
35
+ if not allow_empty_sessions:
36
+ active_sessions = self.inspect().get_active_sessions()
37
+ assert_that.is_true(len(active_sessions) > 0, "Should have active sessions")
34
38
 
35
39
  if assert_that.has_failures():
36
40
  return TestResult(self.name, False, assert_that.get_failure_message())
@@ -43,4 +47,4 @@ class LoginFlowTest(BaseTest):
43
47
 
44
48
  async def teardown(self):
45
49
  """Teardown for login test."""
46
- pass
50
+ pass
@@ -0,0 +1,294 @@
1
+ """Play Store screenshot tests for phone and tablet layouts."""
2
+
3
+ import os
4
+ from urllib.parse import urljoin
5
+
6
+ from testing_framework.core.base_test import BaseTest, TestResult, TestCategory
7
+
8
+
9
+ DEVICE_LABEL = os.getenv("PLAY_STORE_DEVICE_LABEL", "Workshop Seat 01")
10
+
11
+
12
+ class PlayStoreScreenshotLogic:
13
+ """Shared workflow for capturing dashboard and editor screenshots."""
14
+
15
+ def __init__(self):
16
+ self.device_label = DEVICE_LABEL
17
+ self.device_name = os.getenv("SCREENSHOT_DEVICE_NAME", "default")
18
+ self.dashboard_zoom = float(os.getenv("SCREENSHOT_ZOOM", "1.0"))
19
+
20
+ async def apply_zoom(self, page):
21
+ if self.dashboard_zoom != 1.0:
22
+ percent = int(self.dashboard_zoom * 100)
23
+ await page.evaluate(
24
+ f"document.body.style.zoom='{percent}%'"
25
+ )
26
+
27
+ async def capture(self, test_instance, post_editor_steps=None) -> TestResult:
28
+ page = test_instance.playwright_manager.page
29
+ base_url = test_instance.playwright_manager.base_url
30
+
31
+ # Ensure dashboard is loaded
32
+ await page.goto(urljoin(base_url, "/dashboard/"))
33
+ await page.wait_for_load_state("networkidle")
34
+
35
+ # Locate specific device card and ensure it's online
36
+ device_card = (
37
+ page.locator(".device-card.online")
38
+ .filter(has_text=self.device_label)
39
+ .first
40
+ )
41
+ try:
42
+ await device_card.wait_for(timeout=10000)
43
+ except Exception:
44
+ return TestResult(
45
+ test_instance.name,
46
+ False,
47
+ f"Device '{self.device_label}' is not online or not visible",
48
+ )
49
+
50
+ # Scroll past navbar and capture dashboard screenshot
51
+ await page.evaluate("window.scrollTo(0, 66)")
52
+ await page.wait_for_timeout(500)
53
+ await test_instance.playwright_manager.take_screenshot(
54
+ f"{self.device_name}_dashboard"
55
+ )
56
+
57
+ # Open the editor from this device card
58
+ editor_button = device_card.get_by_text("Editor")
59
+ await editor_button.wait_for(timeout=5000)
60
+ await editor_button.scroll_into_view_if_needed()
61
+ await page.wait_for_timeout(200)
62
+ try:
63
+ await editor_button.click(force=True)
64
+ except Exception as exc:
65
+ return TestResult(
66
+ test_instance.name,
67
+ False,
68
+ f"Failed to open editor for {self.device_label}: {exc}",
69
+ )
70
+
71
+ # Select the first project in the modal
72
+ await page.wait_for_selector("#projectSelectorModal.show", timeout=10000)
73
+ await page.wait_for_selector(".item.project", timeout=10000)
74
+ first_project = page.locator(".item.project").first
75
+ await first_project.click()
76
+ await page.wait_for_selector(
77
+ "#projectSelectorModal.show",
78
+ state="hidden",
79
+ timeout=10000,
80
+ )
81
+
82
+ # Handle LitElement/Shadow DOM editor readiness
83
+ try:
84
+ await page.wait_for_selector("ace-editor", timeout=15000)
85
+ await page.wait_for_function(
86
+ """
87
+ () => {
88
+ const el = document.querySelector('ace-editor');
89
+ if (!el) return false;
90
+ const shadow = el.shadowRoot;
91
+ if (shadow && shadow.querySelector('.ace_editor')) return true;
92
+ return !!el.querySelector('.ace_editor');
93
+ }
94
+ """,
95
+ timeout=20000,
96
+ )
97
+ except Exception:
98
+ test_instance.logger.warning(
99
+ "ACE editor shadow DOM not detected, proceeding with screenshot"
100
+ )
101
+
102
+ await page.wait_for_timeout(1000)
103
+ await test_instance.playwright_manager.take_screenshot(
104
+ f"{self.device_name}_editor"
105
+ )
106
+
107
+ if post_editor_steps:
108
+ result = await post_editor_steps(page, test_instance.playwright_manager)
109
+ if result:
110
+ return result
111
+
112
+ return TestResult(
113
+ test_instance.name,
114
+ True,
115
+ f"Screenshots captured for {self.device_name}",
116
+ )
117
+
118
+
119
+ class PlayStorePhoneScreenshotTest(BaseTest):
120
+ """Capture phone-friendly screenshots for Play Store listing."""
121
+
122
+ def __init__(self):
123
+ self.logic = PlayStoreScreenshotLogic()
124
+ super().__init__(
125
+ name="play_store_phone_screenshot_test",
126
+ category=TestCategory.UI,
127
+ description="Capture phone-friendly screenshots for Play Store listing",
128
+ tags=["screenshots", "store", "phone"],
129
+ depends_on=["login_flow_test"],
130
+ start_url="/dashboard/",
131
+ )
132
+
133
+ async def setup(self):
134
+ await self.logic.apply_zoom(self.playwright_manager.page)
135
+
136
+ async def run(self) -> TestResult:
137
+ return await self.logic.capture(self, self._capture_phone_views)
138
+
139
+ async def teardown(self):
140
+ pass
141
+
142
+ async def _capture_phone_views(self, page, manager) -> TestResult:
143
+ async def take(name: str):
144
+ await page.wait_for_timeout(500)
145
+ await manager.take_screenshot(f"{self.logic.device_name}_{name}")
146
+
147
+ def locator_by_text(selector: str, text: str):
148
+ return page.locator(selector).filter(has_text=text).first
149
+
150
+ # Explorer tab
151
+ explorer_tab = locator_by_text(".mobile-nav-item", "Explorer")
152
+ try:
153
+ await explorer_tab.wait_for(timeout=5000)
154
+ await explorer_tab.click()
155
+ except Exception as exc:
156
+ return TestResult(self.name, False, f"Explorer tab not accessible: {exc}")
157
+ await take("explorer")
158
+
159
+ # Git status expansion
160
+ git_info = page.locator(".git-branch-info").first
161
+ try:
162
+ await git_info.wait_for(timeout=5000)
163
+ await git_info.click()
164
+ except Exception as exc:
165
+ return TestResult(self.name, False, f"Git status section unavailable: {exc}")
166
+ await take("git_status")
167
+
168
+ # Diff button
169
+ diff_btn = page.locator(".git-action-btn.diff").first
170
+ try:
171
+ await diff_btn.wait_for(timeout=5000)
172
+ await diff_btn.click()
173
+ except Exception as exc:
174
+ return TestResult(self.name, False, f"Diff action unavailable: {exc}")
175
+ await page.wait_for_timeout(1000)
176
+ await take("git_diff")
177
+
178
+ # Terminal tab
179
+ terminal_tab = locator_by_text(".mobile-nav-item", "Terminal")
180
+ try:
181
+ await terminal_tab.wait_for(timeout=5000)
182
+ await terminal_tab.click()
183
+ except Exception as exc:
184
+ return TestResult(self.name, False, f"Terminal tab not accessible: {exc}")
185
+ await take("terminal")
186
+
187
+ # AI Chat tab
188
+ ai_chat_tab = locator_by_text(".mobile-nav-item", "AI Chat")
189
+ try:
190
+ await ai_chat_tab.wait_for(timeout=5000)
191
+ await ai_chat_tab.click()
192
+ except Exception as exc:
193
+ return TestResult(self.name, False, f"AI Chat tab not accessible: {exc}")
194
+ await take("ai_chat")
195
+
196
+ # First chat item
197
+ chat_item = page.locator(".chat-item").first
198
+ try:
199
+ await chat_item.wait_for(timeout=5000)
200
+ await chat_item.click()
201
+ except Exception as exc:
202
+ return TestResult(self.name, False, f"No AI chat history available: {exc}")
203
+ await take("ai_chat_thread")
204
+
205
+ return TestResult(
206
+ self.name,
207
+ True,
208
+ "Phone screenshots captured across Explorer, Git, Diff, Terminal, and AI Chat",
209
+ )
210
+
211
+ class PlayStoreTabletScreenshotTest(BaseTest):
212
+ """Capture tablet-friendly screenshots for Play Store listing."""
213
+
214
+ def __init__(self):
215
+ self.logic = PlayStoreScreenshotLogic()
216
+ super().__init__(
217
+ name="play_store_tablet_screenshot_test",
218
+ category=TestCategory.UI,
219
+ description="Capture tablet-friendly screenshots for Play Store listing",
220
+ tags=["screenshots", "store", "tablet"],
221
+ depends_on=["login_flow_test"],
222
+ start_url="/dashboard/",
223
+ )
224
+
225
+ async def setup(self):
226
+ await self.logic.apply_zoom(self.playwright_manager.page)
227
+
228
+ async def run(self) -> TestResult:
229
+ return await self.logic.capture(self, self._capture_tablet_views)
230
+
231
+ async def teardown(self):
232
+ pass
233
+
234
+ async def _capture_tablet_views(self, page, manager) -> TestResult:
235
+ async def click_and_wait(locator, description: str, screenshot_name: str = None):
236
+ try:
237
+ await locator.wait_for(timeout=5000)
238
+ await locator.click()
239
+ except Exception as exc:
240
+ return TestResult(self.name, False, f"Failed to interact with {description}: {exc}")
241
+ if screenshot_name:
242
+ await page.wait_for_timeout(500)
243
+ await manager.take_screenshot(f"{self.logic.device_name}_{screenshot_name}")
244
+ return None
245
+
246
+ # Helper locators
247
+ def divider_lid(title_text):
248
+ return page.locator(f'.divider-lid[title="Toggle {title_text}"]').first
249
+
250
+ def persistent_toggle(title_text):
251
+ return page.locator(
252
+ f'.persistent-toggle[title="Show {title_text}"], '
253
+ f'.persistent-toggle[title="Hide {title_text}"]'
254
+ ).first
255
+
256
+ # 1. Close terminal, expand git, open diff, capture
257
+ result = await click_and_wait(divider_lid("Terminal"), "Terminal divider")
258
+ if result:
259
+ return result
260
+ git_info = page.locator(".git-branch-info").first
261
+ result = await click_and_wait(git_info, "Git branch info", "git_status")
262
+ if result:
263
+ return result
264
+ git_diff_btn = page.locator(".git-action-btn.diff").first
265
+ result = await click_and_wait(git_diff_btn, "Git diff button", "git_version_control")
266
+ if result:
267
+ return result
268
+
269
+ # 2. Expand AI chat and open first chat
270
+ ai_chat_toggle = page.locator('.persistent-toggle.ai-chat-toggle')
271
+ result = await click_and_wait(ai_chat_toggle, "AI Chat toggle")
272
+ if result:
273
+ return result
274
+ chat_item = page.locator(".chat-item").first
275
+ result = await click_and_wait(chat_item, "AI chat conversation", "ai_chat_thread_tablet")
276
+ if result:
277
+ return result
278
+
279
+ # 3. Expand terminal, collapse file explorer, capture
280
+ terminal_tab = page.locator('.persistent-toggle.terminal-toggle-center')
281
+ result = await click_and_wait(terminal_tab, "Terminal toggle")
282
+ if result:
283
+ return result
284
+ explorer_divider = page.locator('.divider-lid.horizontal[title="Toggle File Explorer"]').first
285
+ result = await click_and_wait(explorer_divider, "Explorer divider", "terminal_focus")
286
+ if result:
287
+ return result
288
+
289
+ # 4. Collapse AI chat and expand file explorer to reset
290
+ return TestResult(
291
+ self.name,
292
+ True,
293
+ "Tablet screenshots captured across Git, diff, terminal, and AI chat views",
294
+ )
@@ -15,4 +15,7 @@ TEST_HEADLESS=false # true for headless mode, false for visible browser
15
15
  # Optional: Test Output Directories
16
16
  TEST_RESULTS_DIR=test_results
17
17
  TEST_RECORDINGS_DIR=test_recordings
18
- TEST_LOGS_DIR=test_results
18
+ TEST_LOGS_DIR=test_results
19
+
20
+ # Automation testing token (used by the testing framework to bypass captcha. Same token must be defined in ../main.env)
21
+ TEST_RUNNER_BYPASS_TOKEN=same-as-in-main-env