| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 38e3e01 commit c7becef
22 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -25,7 +25,7 @@ jobs: | |||
| 25 | 25 | e2e-android: | |
| 26 | 26 | needs: coverage | |
| 27 | 27 | runs-on: ubuntu-latest | |
| 28 | - timeout-minutes: 45 | ||
| 28 | + timeout-minutes: 60 | ||
| 29 | 29 | ||
| 30 | 30 | steps: | |
| 31 | 31 | - name: Checkout | |
@@ -66,7 +66,7 @@ jobs: | |||
| 66 | 66 | e2e-ios: | |
| 67 | 67 | needs: coverage | |
| 68 | 68 | runs-on: macos-latest | |
| 69 | - timeout-minutes: 45 | ||
| 69 | + timeout-minutes: 60 | ||
| 70 | 70 | ||
| 71 | 71 | steps: | |
| 72 | 72 | - name: Checkout | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -38,6 +38,7 @@ def TextInputDemo() -> pn.Element: | |||
| 38 | 38 | placeholder="Type your name here", | |
| 39 | 39 | on_change=set_name, | |
| 40 | 40 | return_key_type="done", | |
| 41 | + auto_correct=False, | ||
| 41 | 42 | style=field_style, | |
| 42 | 43 | ), | |
| 43 | 44 | result_text("Echo", name or "(empty)"), | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -15,9 +15,8 @@ | |||
| 15 | 15 | @pn.component | |
| 16 | 16 | def ParamsPassingDemo() -> pn.Element: | |
| 17 | 17 | """Render the active route's params using ``use_route``.""" | |
| 18 | - route = pn.use_route() | ||
| 18 | + params = pn.use_route() | ||
| 19 | 19 | nav = pn.use_navigation() | |
| 20 | - params = route.get("params") or {} | ||
| 21 | 20 | ||
| 22 | 21 | def push_with(value: str) -> None: | |
| 23 | 22 | nav.navigate("params_passing", {"value": value}) | |
@@ -27,7 +26,6 @@ def push_with(value: str) -> None: | |||
| 27 | 26 | "use_route reads the active route's params; navigating with new params updates the readout.", | |
| 28 | 27 | section( | |
| 29 | 28 | "Route info", | |
| 30 | - result_text("Route name", route.get("name") or "(none)"), | ||
| 31 | 29 | result_text("Param 'value'", params.get("value") or "(none)"), | |
| 32 | 30 | buttons_row( | |
| 33 | 31 | pn.Button("Push value=alpha", on_click=lambda: push_with("alpha")), | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -25,7 +25,8 @@ | |||
| 25 | 25 | # The script: | |
| 26 | 26 | # 1. Builds + installs the e2e-suite app via `pn run <platform> --no-logs`. | |
| 27 | 27 | # 2. Picks the right Maestro YAML based on platform + suite. | |
| 28 | - # 3. Runs `maestro test` and exits with Maestro's exit code. | ||
| 28 | + # 3. Runs `maestro test` up to ``MAESTRO_MAX_ATTEMPTS`` times (default | ||
| 29 | + # 2) and exits with the last attempt's exit code. | ||
| 29 | 30 | # | |
| 30 | 31 | # A successful run prints "All E2E suites passed." at the end and exits 0. | |
| 31 | 32 | # Any failed flow is reported by Maestro in its standard format; see | |
@@ -87,11 +88,37 @@ pushd examples/e2e-suite > /dev/null | |||
| 87 | 88 | pn run "$PLATFORM" --no-logs | |
| 88 | 89 | popd > /dev/null | |
| 89 | 90 | ||
| 91 | + run_maestro() { | ||
| 92 | + if [[ "$PLATFORM" == "ios" ]]; then | ||
| 93 | + maestro --platform ios test -e "APP_ID=$APP_ID" "$MAESTRO_TARGET" | ||
| 94 | + else | ||
| 95 | + maestro test -e "APP_ID=$APP_ID" "$MAESTRO_TARGET" | ||
| 96 | + fi | ||
| 97 | + } | ||
| 98 | + | ||
| 90 | 99 | printf "\n==> Running Maestro suite: %s\n" "$MAESTRO_TARGET" | |
| 91 | - if [[ "$PLATFORM" == "ios" ]]; then | ||
| 92 | - maestro --platform ios test -e "APP_ID=$APP_ID" "$MAESTRO_TARGET" | ||
| 93 | - else | ||
| 94 | - maestro test -e "APP_ID=$APP_ID" "$MAESTRO_TARGET" | ||
| 95 | - fi | ||
| 100 | + | ||
| 101 | + # Maestro's iOS XCUITest driver occasionally loses its connection to the | ||
| 102 | + # app during long suites and surfaces transient "Application is not | ||
| 103 | + # running" / "Request for viewHierarchy failed" errors that have nothing | ||
| 104 | + # to do with the test under test. Allow one automatic retry of the whole | ||
| 105 | + # suite (overridable via ``MAESTRO_MAX_ATTEMPTS``) so CI doesn't fail on | ||
| 106 | + # driver flakes. A retry can also mask a genuine race in the suite, so | ||
| 107 | + # treat the "retrying..." line as a signal to investigate, not just to | ||
| 108 | + # trust the second pass. | ||
| 109 | + MAX_ATTEMPTS="${MAESTRO_MAX_ATTEMPTS:-2}" | ||
| 110 | + attempt=1 | ||
| 111 | + while (( attempt <= MAX_ATTEMPTS )); do | ||
| 112 | + if run_maestro; then | ||
| 113 | + break | ||
| 114 | + fi | ||
| 115 | + if (( attempt == MAX_ATTEMPTS )); then | ||
| 116 | + printf "\nMaestro suite failed after %d attempt(s).\n" "$attempt" >&2 | ||
| 117 | + exit 1 | ||
| 118 | + fi | ||
| 119 | + printf "\n==> Maestro suite failed (attempt %d/%d); retrying...\n" \ | ||
| 120 | + "$attempt" "$MAX_ATTEMPTS" >&2 | ||
| 121 | + attempt=$(( attempt + 1 )) | ||
| 122 | + done | ||
| 96 | 123 | ||
| 97 | 124 | printf "\nAll E2E suites passed.\n" | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -62,6 +62,12 @@ async def fade_in(): | |||
| 62 | 62 | _TARGET_FPS = 60.0 | |
| 63 | 63 | _FRAME_DT = 1.0 / _TARGET_FPS | |
| 64 | 64 | ||
| 65 | + # Upper bound on how much wall-clock time the animation loop will try to | ||
| 66 | + # catch up on in a single iteration after thread starvation. At 60 fps | ||
| 67 | + # this is ~333 ms of simulated motion; further drift is dropped to keep | ||
| 68 | + # the loop responsive. | ||
| 69 | + _MAX_CATCHUP_FRAMES = 20 | ||
| 70 | + | ||
| 65 | 71 | _EASINGS: Dict[str, Callable[[float], float]] = { | |
| 66 | 72 | "linear": lambda t: t, | |
| 67 | 73 | "ease_in": lambda t: t * t, | |
@@ -199,6 +205,20 @@ def _ensure_thread_locked(self) -> None: | |||
| 199 | 205 | ||
| 200 | 206 | def _loop(self) -> None: | |
| 201 | 207 | last = time.monotonic() | |
| 208 | + # Clamping the per-tick dt is important for numerical stability: | ||
| 209 | + # an underdamped spring with a 0.3 s step explodes immediately, | ||
| 210 | + # and on iOS/Android the animation thread can be starved for | ||
| 211 | + # several frames during render bursts. We integrate physics on a | ||
| 212 | + # clamped dt (max 2 target frames) and sub-step when wall-clock | ||
| 213 | + # has advanced more than that, so the perceived motion still | ||
| 214 | + # tracks real time at most a couple of frames behind. After an | ||
| 215 | + # extreme starvation (e.g. the app was backgrounded for seconds) | ||
| 216 | + # we cap the catch-up at ``_MAX_CATCHUP_FRAMES`` worth of | ||
| 217 | + # physics; any further wall-clock drift is dropped on the floor, | ||
| 218 | + # which keeps the loop responsive instead of spinning forward | ||
| 219 | + # through hundreds of substeps. | ||
| 220 | + max_step = _FRAME_DT * 2.0 | ||
| 221 | + max_catchup = _FRAME_DT * _MAX_CATCHUP_FRAMES | ||
| 202 | 222 | while not self._stopped: | |
| 203 | 223 | now = time.monotonic() | |
| 204 | 224 | dt = now - last | |
@@ -209,13 +229,19 @@ def _loop(self) -> None: | |||
| 209 | 229 | time.sleep(0.05) | |
| 210 | 230 | last = time.monotonic() | |
| 211 | 231 | continue | |
| 212 | - for anim in active: | ||
| 213 | - try: | ||
| 214 | - finished = anim.advance(dt) | ||
| 215 | - except Exception: | ||
| 216 | - finished = True | ||
| 217 | - if finished: | ||
| 218 | - self.remove(anim) | ||
| 232 | + remaining = min(dt, max_catchup) | ||
| 233 | + while remaining > 0.0: | ||
| 234 | + step = remaining if remaining <= max_step else max_step | ||
| 235 | + remaining -= step | ||
| 236 | + for anim in active: | ||
| 237 | + if getattr(anim, "_completed", False): | ||
| 238 | + continue | ||
| 239 | + try: | ||
| 240 | + finished = anim.advance(step) | ||
| 241 | + except Exception: | ||
| 242 | + finished = True | ||
| 243 | + if finished: | ||
| 244 | + self.remove(anim) | ||
| 219 | 245 | time.sleep(_FRAME_DT) | |
| 220 | 246 | ||
| 221 | 247 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -401,7 +401,17 @@ class LayoutNode: | |||
| 401 | 401 | height: Computed height in points. | |
| 402 | 402 | """ | |
| 403 | 403 | ||
| 404 | - __slots__ = ("style", "children", "measure", "user_data", "x", "y", "width", "height") | ||
| 404 | + __slots__ = ( | ||
| 405 | + "style", | ||
| 406 | + "children", | ||
| 407 | + "measure", | ||
| 408 | + "user_data", | ||
| 409 | + "x", | ||
| 410 | + "y", | ||
| 411 | + "width", | ||
| 412 | + "height", | ||
| 413 | + "_pn_scroll_axis", | ||
| 414 | + ) | ||
| 405 | 415 | ||
| 406 | 416 | def __init__( | |
| 407 | 417 | self, | |
@@ -418,6 +428,14 @@ def __init__( | |||
| 418 | 428 | self.y: float = 0.0 | |
| 419 | 429 | self.width: float = 0.0 | |
| 420 | 430 | self.height: float = 0.0 | |
| 431 | + # ``"x"``/``"y"`` for scroll containers; ``None`` for everything | ||
| 432 | + # else. Consumed by ``_measure_container`` to clamp the node's | ||
| 433 | + # own main-axis size to the parent's available space while still | ||
| 434 | + # measuring children unbounded on the scroll axis (which is what | ||
| 435 | + # makes the native ``UIScrollView`` / Android ``ScrollView`` | ||
| 436 | + # actually scroll). The reconciler stamps this when building the | ||
| 437 | + # layout tree for ``ScrollView`` elements. | ||
| 438 | + self._pn_scroll_axis: Optional[str] = None | ||
| 421 | 439 | ||
| 422 | 440 | def __repr__(self) -> str: | |
| 423 | 441 | return ( | |
@@ -576,6 +594,22 @@ def _measure_container( | |||
| 576 | 594 | ||
| 577 | 595 | width = explicit_w if explicit_w is not None else (used_w + pad_x) | |
| 578 | 596 | height = explicit_h if explicit_h is not None else (used_h + pad_y) | |
| 597 | + | ||
| 598 | + # Scroll containers: clamp the container's own main-axis size to the | ||
| 599 | + # parent's available space when no explicit size was provided. The | ||
| 600 | + # children are still measured against an unbounded main-axis (handled | ||
| 601 | + # via the wrapper inserted in ``Reconciler._build_layout_tree``) so the | ||
| 602 | + # overflow becomes the scrollable region. Without this clamp, the | ||
| 603 | + # container would grow to fit its content and there would be no | ||
| 604 | + # overflow for the native ScrollView to scroll. Skipped when the | ||
| 605 | + # parent is itself unbounded, so nested scroll views still fall back | ||
| 606 | + # to natural sizing (the inner scroll is unscrollable in that case, | ||
| 607 | + # which matches the behavior in React Native). | ||
| 608 | + scroll_axis = getattr(node, "_pn_scroll_axis", None) | ||
| 609 | + if scroll_axis == "y" and explicit_h is None and math.isfinite(avail_h): | ||
| 610 | + height = avail_h | ||
| 611 | + elif scroll_axis == "x" and explicit_w is None and math.isfinite(avail_w): | ||
| 612 | + width = avail_w | ||
| 579 | 613 | return width, height | |
| 580 | 614 | ||
| 581 | 615 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -35,6 +35,7 @@ | |||
| 35 | 35 | _pn_view_visual_props: dict = {} | |
| 36 | 36 | _DRAWABLE_STYLE_KEYS = ("background_color", "border_radius", "border_width", "border_color") | |
| 37 | 37 | ||
| 38 | + | ||
| 38 | 39 | # ====================================================================== | |
| 39 | 40 | # Shared helpers | |
| 40 | 41 | # ====================================================================== | |
@@ -509,18 +510,27 @@ def onClick(self, view: Any) -> None: | |||
| 509 | 510 | class ScrollViewHandler(AndroidViewHandler): | |
| 510 | 511 | """Scroll container — wraps a single child whose height is unbounded. | |
| 511 | 512 | ||
| 513 | + Uses ``androidx.core.widget.NestedScrollView`` rather than the | ||
| 514 | + framework ``android.widget.ScrollView`` because the framework | ||
| 515 | + ScrollView always intercepts vertical gestures, even when it has | ||
| 516 | + no overflow. That breaks the common case of nesting a small | ||
| 517 | + fixed-height scroll view inside a screen-level scroll view (the | ||
| 518 | + outer steals every gesture and the inner never scrolls). | ||
| 519 | + ``NestedScrollView`` implements the standard | ||
| 520 | + ``NestedScrollingParent2`` / ``NestedScrollingChild2`` protocol so | ||
| 521 | + the outer cooperates with any nested scroll, only consuming | ||
| 522 | + leftover scroll when its child reaches its limit. | ||
| 523 | + | ||
| 512 | 524 | When a ``refresh_control`` prop is provided, wraps the scroll in | |
| 513 | 525 | a `SwipeRefreshLayout` and forwards the on-refresh callback. | |
| 514 | 526 | """ | |
| 515 | 527 | ||
| 516 | 528 | def create(self, props: Dict[str, Any]) -> Any: | |
| 517 | - sv = jclass("android.widget.ScrollView")(_ctx()) | ||
| 529 | + try: | ||
| 530 | + sv = jclass("androidx.core.widget.NestedScrollView")(_ctx()) | ||
| 531 | + except Exception: | ||
| 532 | + sv = jclass("android.widget.ScrollView")(_ctx()) | ||
| 518 | 533 | _apply_common_visual(sv, props) | |
| 519 | - # Wrap the inner ScrollView in a SwipeRefreshLayout when | ||
| 520 | - # ``refresh_control`` is asked for. Implementing this cleanly | ||
| 521 | - # would require returning a different parent; for v1, we | ||
| 522 | - # attach the listener via a wrapper that we expose to | ||
| 523 | - # add_child callers below. | ||
| 524 | 534 | return sv | |
| 525 | 535 | ||
| 526 | 536 | def update(self, native_view: Any, changed: Dict[str, Any]) -> None: | |
@@ -536,6 +546,17 @@ def remove_child(self, parent: Any, child: Any) -> None: | |||
| 536 | 546 | class TextInputHandler(AndroidViewHandler): | |
| 537 | 547 | def create(self, props: Dict[str, Any]) -> Any: | |
| 538 | 548 | et = jclass("android.widget.EditText")(_ctx()) | |
| 549 | + # Default to single-line so pressing Enter triggers IME_ACTION_DONE | ||
| 550 | + # (submit / dismiss) instead of inserting a newline. The | ||
| 551 | + # ``_apply`` path will override this if ``multiline=True`` is | ||
| 552 | + # set in props. Without this, every TextInput without an | ||
| 553 | + # explicit ``multiline`` value falls back to Android's | ||
| 554 | + # multi-line default and Enter inserts ``\n``. | ||
| 555 | + try: | ||
| 556 | + if not props.get("multiline"): | ||
| 557 | + et.setSingleLine(True) | ||
| 558 | + except Exception: | ||
| 559 | + pass | ||
| 539 | 560 | self._apply(et, props) | |
| 540 | 561 | return et | |
| 541 | 562 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments