tempestweb 0.2.0__tar.gz → 0.3.0__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.
- {tempestweb-0.2.0 → tempestweb-0.3.0}/CHANGELOG.md +10 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/PKG-INFO +1 -1
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/native/index.js +3 -0
- tempestweb-0.3.0/client/native/install.js +51 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/pyproject.toml +1 -1
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/cli/commands/build.py +9 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/native/__init__.py +9 -0
- tempestweb-0.3.0/tempestweb/native/install.py +57 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/native-onnx-file.test.js +17 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/native.test.js +2 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/.gitignore +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/README.md +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/constants.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/dom.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/events.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/livereload.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/native/audio.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/native/camera.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/native/clipboard.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/native/file.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/native/geolocation.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/native/http.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/native/notifications.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/native/onnx.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/native/share.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/native/storage.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/offline/store.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/offline/sync.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/push/web-push-client.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/pwa/install-prompt.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/pwa/manifest.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/router.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/style.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/sw/register.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/sw/sw.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/tempestweb.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/transport-sse.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/transport-wasm.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/transport-ws.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/transport.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/client/virtualize.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/__init__.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/_core/__init__.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/cli/__init__.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/cli/commands/__init__.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/cli/commands/dev.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/cli/commands/new.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/cli/commands/run.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/cli/config.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/cli/loader.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/cli/main.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/cli/scaffold.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/components/__init__.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/components/fields.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/components/forms.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/core/__init__.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/core/constants.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/devserver/__init__.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/devserver/http.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/devserver/reload.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/devserver/watcher.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/native/audio.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/native/bridges.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/native/camera.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/native/clipboard.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/native/dispatch.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/native/file.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/native/geolocation.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/native/http.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/native/notifications.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/native/onnx.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/native/share.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/native/storage.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/observability/__init__.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/observability/auth.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/observability/error_boundary.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/observability/feature_flags.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/observability/logger.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/observability/telemetry.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/pwa/__init__.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/pwa/icons.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/pwa/manifest.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/pwa/pyodide_vendor.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/runtime/__init__.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/runtime/events.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/runtime/serialize.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/runtime/session.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/runtime/wasm.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/runtime/wasm_main.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/server/__init__.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/server/app.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/server/webpush.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/transports/__init__.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/transports/base.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/transports/sse.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/transports/wasm.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tempestweb/transports/websocket.py +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/dom.test.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/events.test.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/mount.test.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/offline-store.test.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/offline-sync.test.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/pwa-install-prompt.test.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/pwa-manifest.test.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/router.test.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/setup.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/smoke.test.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/style.test.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/sw-register.test.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/sw-strategies.test.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/transport-sse.test.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/transport-wasm.test.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/transport-ws.test.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/virtualize.test.js +0 -0
- {tempestweb-0.2.0 → tempestweb-0.3.0}/tests/client/web-push-client.test.js +0 -0
|
@@ -4,6 +4,16 @@ All notable changes to **tempestweb** are documented here. Format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); this project adheres to semantic
|
|
5
5
|
versioning.
|
|
6
6
|
|
|
7
|
+
## [0.3.0] — 2026-06-13
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`native.install` capability** — the PWA install flow in Python:
|
|
12
|
+
`install.state()` → `InstallState(can_install, installed)` and
|
|
13
|
+
`install.prompt()` → `"accepted" | "dismissed" | "unavailable"`. Wraps the soft
|
|
14
|
+
controller in `client/pwa/install-prompt.js` (now copied into the wasm
|
|
15
|
+
artifact) via `client/native/install.js`.
|
|
16
|
+
|
|
7
17
|
## [0.2.0] — 2026-06-13
|
|
8
18
|
|
|
9
19
|
Real-app capabilities, driven by building a full on-device vision PWA (FAMACHApp)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tempestweb
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Build web apps in typed Python — one tree, a DOM renderer, two execution modes (WASM + server).
|
|
5
5
|
Author-email: Mauricio Benjamin <mauricio.benjamin@reloverelations.com>
|
|
6
6
|
License: MIT
|
|
@@ -19,6 +19,7 @@ import { storageGet, storageList, storagePut, storageRemove } from "./storage.js
|
|
|
19
19
|
import { cameraCapture } from "./camera.js";
|
|
20
20
|
import { onnxLoad, onnxRun } from "./onnx.js";
|
|
21
21
|
import { fileSave, filePick } from "./file.js";
|
|
22
|
+
import { installState, installPrompt } from "./install.js";
|
|
22
23
|
import {
|
|
23
24
|
notificationsNotify,
|
|
24
25
|
notificationsRequestPermission,
|
|
@@ -80,6 +81,8 @@ export const HANDLERS = {
|
|
|
80
81
|
"onnx.run": onnxRun,
|
|
81
82
|
"file.save": fileSave,
|
|
82
83
|
"file.pick": filePick,
|
|
84
|
+
"install.state": installState,
|
|
85
|
+
"install.prompt": installPrompt,
|
|
83
86
|
"notifications.notify": notificationsNotify,
|
|
84
87
|
"notifications.request_permission": notificationsRequestPermission,
|
|
85
88
|
"notifications.subscribe": notificationsSubscribe,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// native/install.js — PWA install-prompt capability.
|
|
2
|
+
//
|
|
3
|
+
// Wraps the soft install controller (client/pwa/install-prompt.js) in a single
|
|
4
|
+
// process-wide instance so Python can ask whether the app is installable and
|
|
5
|
+
// fire the stashed `beforeinstallprompt` after a real gesture. The controller is
|
|
6
|
+
// created on first import so its listeners attach as early as the native bundle
|
|
7
|
+
// loads (suppressing the browser mini-infobar when it catches the event).
|
|
8
|
+
|
|
9
|
+
import { createInstallPrompt } from "../pwa/install-prompt.js";
|
|
10
|
+
|
|
11
|
+
/** @type {ReturnType<typeof createInstallPrompt> | null} */
|
|
12
|
+
let _controller = null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Lazily create (and cache) the install-prompt controller.
|
|
16
|
+
* @param {import("./index.js").NativeDeps} [deps]
|
|
17
|
+
* @returns {ReturnType<typeof createInstallPrompt>}
|
|
18
|
+
*/
|
|
19
|
+
function controller(deps) {
|
|
20
|
+
if (_controller) return _controller;
|
|
21
|
+
const win = (deps && /** @type {any} */ (deps).window) || globalThis;
|
|
22
|
+
_controller = createInstallPrompt({ window: win });
|
|
23
|
+
return _controller;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Attach listeners as early as this module loads.
|
|
27
|
+
if (typeof globalThis !== "undefined" && globalThis.addEventListener) {
|
|
28
|
+
controller();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Report the current install state.
|
|
33
|
+
* @param {Object} _args
|
|
34
|
+
* @param {import("./index.js").NativeDeps} deps
|
|
35
|
+
* @returns {Promise<{can_install:boolean, installed:boolean}>}
|
|
36
|
+
*/
|
|
37
|
+
export async function installState(_args, deps) {
|
|
38
|
+
const state = controller(deps).getState();
|
|
39
|
+
return { can_install: Boolean(state.canInstall), installed: Boolean(state.installed) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Fire the stashed native install prompt.
|
|
44
|
+
* @param {Object} _args
|
|
45
|
+
* @param {import("./index.js").NativeDeps} deps
|
|
46
|
+
* @returns {Promise<{outcome:"accepted"|"dismissed"|"unavailable"}>}
|
|
47
|
+
*/
|
|
48
|
+
export async function installPrompt(_args, deps) {
|
|
49
|
+
const outcome = await controller(deps).promptInstall();
|
|
50
|
+
return { outcome };
|
|
51
|
+
}
|
|
@@ -74,6 +74,7 @@ _NATIVE_ASSETS: tuple[str, ...] = (
|
|
|
74
74
|
"file.js",
|
|
75
75
|
"geolocation.js",
|
|
76
76
|
"http.js",
|
|
77
|
+
"install.js",
|
|
77
78
|
"notifications.js",
|
|
78
79
|
"onnx.js",
|
|
79
80
|
"share.js",
|
|
@@ -119,6 +120,7 @@ WASM_ARTIFACT_FILES: tuple[str, ...] = (
|
|
|
119
120
|
*(f"client/{asset}" for asset in (*_CLIENT_ASSETS, "transport-wasm.js")),
|
|
120
121
|
*(f"client/native/{asset}" for asset in _NATIVE_ASSETS),
|
|
121
122
|
"client/push/web-push-client.js",
|
|
123
|
+
"client/pwa/install-prompt.js",
|
|
122
124
|
)
|
|
123
125
|
|
|
124
126
|
# Files a server artifact must contain, relative to the artifact root.
|
|
@@ -773,6 +775,13 @@ def _build_wasm(
|
|
|
773
775
|
push_dest = out / "client" / "push"
|
|
774
776
|
push_dest.mkdir(parents=True, exist_ok=True)
|
|
775
777
|
shutil.copyfile(push_source, push_dest / "web-push-client.js")
|
|
778
|
+
# The install capability imports the soft install-prompt controller.
|
|
779
|
+
install_source = client / "pwa" / "install-prompt.js"
|
|
780
|
+
if not install_source.is_file():
|
|
781
|
+
raise BuildError(f"missing pwa asset: {install_source}")
|
|
782
|
+
pwa_dest = out / "client" / "pwa"
|
|
783
|
+
pwa_dest.mkdir(parents=True, exist_ok=True)
|
|
784
|
+
shutil.copyfile(install_source, pwa_dest / "install-prompt.js")
|
|
776
785
|
# Project static assets (ONNX models, vendored JS libs) copied verbatim,
|
|
777
786
|
# preserving their relative path, and precached for the offline second load.
|
|
778
787
|
assets: list[str] = []
|
|
@@ -46,6 +46,7 @@ from tempestweb.native import (
|
|
|
46
46
|
file,
|
|
47
47
|
geolocation,
|
|
48
48
|
http,
|
|
49
|
+
install,
|
|
49
50
|
notifications,
|
|
50
51
|
onnx,
|
|
51
52
|
storage,
|
|
@@ -78,6 +79,9 @@ from tempestweb.native.http import (
|
|
|
78
79
|
request,
|
|
79
80
|
upload,
|
|
80
81
|
)
|
|
82
|
+
from tempestweb.native.install import InstallState
|
|
83
|
+
from tempestweb.native.install import prompt as install_prompt
|
|
84
|
+
from tempestweb.native.install import state as install_state
|
|
81
85
|
from tempestweb.native.notifications import (
|
|
82
86
|
NotificationPermission,
|
|
83
87
|
notify,
|
|
@@ -109,6 +113,7 @@ __all__ = [
|
|
|
109
113
|
"file",
|
|
110
114
|
"geolocation",
|
|
111
115
|
"http",
|
|
116
|
+
"install",
|
|
112
117
|
"notifications",
|
|
113
118
|
"onnx",
|
|
114
119
|
"storage",
|
|
@@ -161,6 +166,10 @@ __all__ = [
|
|
|
161
166
|
"SaveResult",
|
|
162
167
|
"file_pick",
|
|
163
168
|
"file_save",
|
|
169
|
+
# install (PWA install prompt)
|
|
170
|
+
"InstallState",
|
|
171
|
+
"install_prompt",
|
|
172
|
+
"install_state",
|
|
164
173
|
# notifications
|
|
165
174
|
"NotificationPermission",
|
|
166
175
|
"notify",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Native PWA install-prompt capability (P0).
|
|
2
|
+
|
|
3
|
+
Exposes the browser's soft install flow to Python: whether the app is installable
|
|
4
|
+
(a ``beforeinstallprompt`` was captured) or already installed, and a call to fire
|
|
5
|
+
the stashed prompt after a real user gesture. ``client/native/install.js`` wraps
|
|
6
|
+
the tested ``client/pwa/install-prompt.js`` controller.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict
|
|
12
|
+
|
|
13
|
+
from tempestweb.native.dispatch import send_native_call
|
|
14
|
+
|
|
15
|
+
__all__ = ["InstallState", "prompt", "state"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InstallState(BaseModel):
|
|
19
|
+
"""The current PWA install state.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
can_install: A deferred prompt is available to fire.
|
|
23
|
+
installed: The app reports as installed (standalone / appinstalled).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
model_config = ConfigDict(frozen=True)
|
|
27
|
+
|
|
28
|
+
can_install: bool = False
|
|
29
|
+
installed: bool = False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def state() -> InstallState:
|
|
33
|
+
"""Report whether the app is installable and/or already installed.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
The current :class:`InstallState`.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
BrowserUnavailableError: If called with no native bridge installed.
|
|
40
|
+
"""
|
|
41
|
+
value = await send_native_call("install.state", {})
|
|
42
|
+
return InstallState.model_validate(value)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def prompt() -> str:
|
|
46
|
+
"""Fire the stashed native install prompt after a user gesture.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
The outcome: ``"accepted"``, ``"dismissed"``, or ``"unavailable"`` (no
|
|
50
|
+
prompt was captured, or it was already used).
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
BrowserUnavailableError: If called with no native bridge installed.
|
|
54
|
+
"""
|
|
55
|
+
value = await send_native_call("install.prompt", {})
|
|
56
|
+
outcome = value.get("outcome", "unavailable")
|
|
57
|
+
return str(outcome)
|
|
@@ -163,6 +163,23 @@ test("file.pick: reads the chosen file back as base64", async () => {
|
|
|
163
163
|
assert.equal(res.value.name, "ovino.png");
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
+
test("install.state/prompt: no captured event → not installable, unavailable", async () => {
|
|
167
|
+
const fakeWindow = {
|
|
168
|
+
addEventListener() {},
|
|
169
|
+
removeEventListener() {},
|
|
170
|
+
matchMedia: () => ({ matches: false }),
|
|
171
|
+
navigator: {},
|
|
172
|
+
};
|
|
173
|
+
const deps = { window: fakeWindow };
|
|
174
|
+
const s = await dispatch(call("install.state"), deps);
|
|
175
|
+
assert.equal(s.ok, true);
|
|
176
|
+
assert.equal(s.value.can_install, false);
|
|
177
|
+
assert.equal(s.value.installed, false);
|
|
178
|
+
const p = await dispatch(call("install.prompt"), deps);
|
|
179
|
+
assert.equal(p.ok, true);
|
|
180
|
+
assert.equal(p.value.outcome, "unavailable");
|
|
181
|
+
});
|
|
182
|
+
|
|
166
183
|
test("file.save: uses the Web Share API when it accepts files", async () => {
|
|
167
184
|
const dom = new JSDOM("<!doctype html><html><body></body></html>");
|
|
168
185
|
globalThis.Blob = dom.window.Blob;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|