react-pebble 0.1.1 → 0.2.0

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.
@@ -0,0 +1,371 @@
1
+ #!/bin/bash
2
+ # Test all examples on the Pebble Alloy emulator with button press verification.
3
+ #
4
+ # For each example:
5
+ # 1. Compile + build + install to emulator
6
+ # 2. Screenshot initial state
7
+ # 3. Send button presses where applicable
8
+ # 4. Screenshot after each button interaction
9
+ #
10
+ # Usage:
11
+ # ./scripts/test-emulator.sh # test all examples
12
+ # ./scripts/test-emulator.sh counter # test one example
13
+ # SETTLE_MS=200 ./scripts/test-emulator.sh async-list # with settle delay
14
+ #
15
+ # Screenshots saved to /tmp/react-pebble-emu-test/
16
+
17
+ set -e
18
+
19
+ FILTER="${1:-}"
20
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
21
+ PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
22
+ BUILD_DIR="$PROJECT_DIR/.pebble-build"
23
+ SCREENSHOT_DIR="/tmp/react-pebble-emu-test"
24
+
25
+ mkdir -p "$SCREENSHOT_DIR"
26
+
27
+ PASSED=0
28
+ FAILED=0
29
+ TOTAL=0
30
+
31
+ # Deploy an example and take initial screenshot
32
+ deploy_example() {
33
+ local name="$1"
34
+ echo ""
35
+ echo "========================================"
36
+ echo " Testing: $name"
37
+ echo "========================================"
38
+
39
+ cd "$PROJECT_DIR"
40
+
41
+ # Compile
42
+ ENTRY="examples/${name}.tsx" npx vite build --config vite.config.plugin-test.js 2>&1 | grep "\[react-pebble\]" || true
43
+
44
+ # Build
45
+ cd "$BUILD_DIR"
46
+ pebble build 2>&1 | tail -2
47
+
48
+ # Kill any existing emulator session
49
+ pebble kill >/dev/null 2>&1 || true
50
+ sleep 1
51
+
52
+ # Install and wait for app to start
53
+ pebble install --emulator emery --logs > /tmp/react-pebble-emu.log 2>&1 &
54
+ EMU_PID=$!
55
+ sleep 8
56
+
57
+ # Initial screenshot
58
+ pebble screenshot "$SCREENSHOT_DIR/${name}-initial.png" 2>/dev/null || true
59
+ echo " [screenshot] ${name}-initial.png"
60
+ }
61
+
62
+ # Take a named screenshot
63
+ screenshot() {
64
+ local name="$1"
65
+ sleep 1
66
+ pebble screenshot "$SCREENSHOT_DIR/${name}.png" 2>/dev/null || true
67
+ echo " [screenshot] ${name}.png"
68
+ }
69
+
70
+ # Send a button click and wait
71
+ click() {
72
+ local button="$1"
73
+ pebble emu-button click "$button" 2>/dev/null || true
74
+ sleep 0.8
75
+ }
76
+
77
+ # Clean up emulator after test
78
+ cleanup() {
79
+ pebble kill >/dev/null 2>&1 || true
80
+ kill $EMU_PID 2>/dev/null || true
81
+ wait $EMU_PID 2>/dev/null || true
82
+ }
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # Test functions for each example
86
+ # ---------------------------------------------------------------------------
87
+
88
+ test_watchface() {
89
+ deploy_example watchface
90
+ # Watchface has no buttons - just verify it renders
91
+ cleanup
92
+ }
93
+
94
+ test_counter() {
95
+ deploy_example counter
96
+ # UP increments, DOWN decrements, SELECT resets
97
+ click up
98
+ screenshot counter-after-up
99
+ click up
100
+ click up
101
+ screenshot counter-after-3up
102
+ click down
103
+ screenshot counter-after-down
104
+ click select
105
+ screenshot counter-after-reset
106
+ cleanup
107
+ }
108
+
109
+ test_toggle() {
110
+ deploy_example toggle
111
+ # SELECT toggles on/off
112
+ click select
113
+ screenshot toggle-on
114
+ click select
115
+ screenshot toggle-off
116
+ cleanup
117
+ }
118
+
119
+ test_views() {
120
+ deploy_example views
121
+ # SELECT toggles between main and detail view
122
+ click select
123
+ screenshot views-detail
124
+ click select
125
+ screenshot views-main
126
+ cleanup
127
+ }
128
+
129
+ test_multiview() {
130
+ deploy_example multiview
131
+ # UP=settings, DOWN=about, SELECT=home
132
+ click up
133
+ screenshot multiview-settings
134
+ click down
135
+ screenshot multiview-about
136
+ click select
137
+ screenshot multiview-home
138
+ cleanup
139
+ }
140
+
141
+ test_simple_list() {
142
+ deploy_example simple-list
143
+ # DOWN scrolls list down, UP scrolls up
144
+ click down
145
+ screenshot simple-list-scroll1
146
+ click down
147
+ screenshot simple-list-scroll2
148
+ click up
149
+ screenshot simple-list-scrollback
150
+ cleanup
151
+ }
152
+
153
+ test_selected_list() {
154
+ deploy_example selected-list
155
+ # DOWN/UP move selection highlight
156
+ click down
157
+ screenshot selected-list-sel1
158
+ click down
159
+ screenshot selected-list-sel2
160
+ click down
161
+ screenshot selected-list-sel3
162
+ click up
163
+ screenshot selected-list-selback
164
+ cleanup
165
+ }
166
+
167
+ test_rich_list() {
168
+ deploy_example rich-list
169
+ click down
170
+ screenshot rich-list-sel1
171
+ click down
172
+ screenshot rich-list-sel2
173
+ click up
174
+ screenshot rich-list-selback
175
+ cleanup
176
+ }
177
+
178
+ test_tasks() {
179
+ deploy_example tasks
180
+ # DOWN/UP navigate, SELECT goes to detail, BACK returns
181
+ click down
182
+ screenshot tasks-sel1
183
+ click select
184
+ screenshot tasks-detail
185
+ click back
186
+ screenshot tasks-back
187
+ cleanup
188
+ }
189
+
190
+ test_jira_lite() {
191
+ deploy_example jira-lite
192
+ # DOWN/UP navigate issues, SELECT opens detail, BACK returns
193
+ click down
194
+ screenshot jira-lite-sel1
195
+ click down
196
+ screenshot jira-lite-sel2
197
+ click select
198
+ screenshot jira-lite-detail
199
+ click back
200
+ screenshot jira-lite-back
201
+ cleanup
202
+ }
203
+
204
+ test_stopwatch() {
205
+ deploy_example stopwatch
206
+ # SELECT starts/stops, DOWN resets
207
+ click select
208
+ sleep 2
209
+ screenshot stopwatch-running
210
+ click select
211
+ screenshot stopwatch-stopped
212
+ click down
213
+ screenshot stopwatch-reset
214
+ cleanup
215
+ }
216
+
217
+ test_nested_cond() {
218
+ deploy_example nested-cond
219
+ # UP toggles header, DOWN toggles footer
220
+ click up
221
+ screenshot nested-cond-no-header
222
+ click down
223
+ screenshot nested-cond-no-footer
224
+ click up
225
+ screenshot nested-cond-both-back
226
+ cleanup
227
+ }
228
+
229
+ test_circles() {
230
+ deploy_example circles
231
+ # No buttons - just verify circles render
232
+ cleanup
233
+ }
234
+
235
+ test_analog_clock() {
236
+ deploy_example analog-clock
237
+ # No buttons - just verify it renders
238
+ cleanup
239
+ }
240
+
241
+ test_dashboard() {
242
+ deploy_example dashboard
243
+ # No buttons - watchface dashboard with battery/connection
244
+ cleanup
245
+ }
246
+
247
+ test_layout_demo() {
248
+ deploy_example layout-demo
249
+ # No buttons - static layout demo
250
+ cleanup
251
+ }
252
+
253
+ test_settings() {
254
+ deploy_example settings
255
+ # UP increases font, DOWN decreases, SELECT cycles theme
256
+ click up
257
+ screenshot settings-font-up
258
+ click up
259
+ screenshot settings-font-up2
260
+ click down
261
+ screenshot settings-font-down
262
+ click select
263
+ screenshot settings-theme1
264
+ click select
265
+ screenshot settings-theme2
266
+ cleanup
267
+ }
268
+
269
+ test_animation() {
270
+ deploy_example animation
271
+ # No buttons - animation runs automatically
272
+ sleep 2
273
+ screenshot animation-mid
274
+ sleep 2
275
+ screenshot animation-late
276
+ cleanup
277
+ }
278
+
279
+ test_compass() {
280
+ deploy_example compass
281
+ # No buttons - compass reads sensors
282
+ # Test accelerometer tap event
283
+ pebble emu-tap --direction x+ 2>/dev/null || true
284
+ sleep 1
285
+ screenshot compass-after-tap
286
+ cleanup
287
+ }
288
+
289
+ test_weather() {
290
+ deploy_example weather
291
+ cleanup
292
+ }
293
+
294
+ test_async_list() {
295
+ SETTLE_MS=200 deploy_example async-list
296
+ click down
297
+ screenshot async-list-scroll
298
+ cleanup
299
+ }
300
+
301
+ test_jira_list() {
302
+ SETTLE_MS=200 deploy_example jira-list
303
+ click down
304
+ screenshot jira-list-sel1
305
+ click select
306
+ screenshot jira-list-detail
307
+ click back
308
+ screenshot jira-list-back
309
+ cleanup
310
+ }
311
+
312
+ # ---------------------------------------------------------------------------
313
+ # Run tests
314
+ # ---------------------------------------------------------------------------
315
+
316
+ ALL_EXAMPLES=(
317
+ watchface counter toggle views multiview
318
+ simple-list selected-list rich-list tasks jira-lite
319
+ stopwatch nested-cond circles analog-clock
320
+ dashboard layout-demo settings animation compass
321
+ weather
322
+ )
323
+
324
+ # Skip async examples by default (need SETTLE_MS)
325
+ ASYNC_EXAMPLES=(async-list jira-list)
326
+
327
+ run_test() {
328
+ local name="$1"
329
+ TOTAL=$((TOTAL + 1))
330
+
331
+ # Convert dashes to underscores for function name
332
+ local fn_name="test_${name//-/_}"
333
+
334
+ if declare -f "$fn_name" > /dev/null 2>&1; then
335
+ if $fn_name 2>&1; then
336
+ echo " [PASS] $name"
337
+ PASSED=$((PASSED + 1))
338
+ else
339
+ echo " [FAIL] $name"
340
+ FAILED=$((FAILED + 1))
341
+ fi
342
+ else
343
+ echo " [SKIP] $name (no test function)"
344
+ fi
345
+ }
346
+
347
+ echo "=== react-pebble emulator test suite ==="
348
+ echo "Screenshots: $SCREENSHOT_DIR"
349
+ echo ""
350
+
351
+ if [ -n "$FILTER" ]; then
352
+ run_test "$FILTER"
353
+ else
354
+ for ex in "${ALL_EXAMPLES[@]}"; do
355
+ run_test "$ex"
356
+ done
357
+ # Also run async examples
358
+ for ex in "${ASYNC_EXAMPLES[@]}"; do
359
+ run_test "$ex"
360
+ done
361
+ fi
362
+
363
+ echo ""
364
+ echo "========================================"
365
+ echo " Results: $PASSED passed, $FAILED failed out of $TOTAL"
366
+ echo " Screenshots: $SCREENSHOT_DIR"
367
+ echo "========================================"
368
+
369
+ if [ $FAILED -gt 0 ]; then
370
+ exit 1
371
+ fi
@@ -38,6 +38,8 @@ export interface CompileResult {
38
38
  hasButtons: boolean;
39
39
  /** Message keys used by useMessage hooks */
40
40
  messageKeys: string[];
41
+ /** Mock data source from useMessage (for generating phone-side JS) */
42
+ mockDataSource: string | null;
41
43
  /** Diagnostic messages from the compiler */
42
44
  diagnostics: string;
43
45
  }
@@ -109,7 +111,12 @@ export async function compileToPiu(options: CompileOptions): Promise<CompileResu
109
111
  const msgMatch = diagnostics.match(/useMessage detected: key="([^"]+)"/);
110
112
  if (msgMatch?.[1]) messageKeys.push(msgMatch[1]);
111
113
 
114
+ // Extract mock data source for phone-side JS generation
115
+ let mockDataSource: string | null = null;
116
+ const mockMatch = diagnostics.match(/mockDataValue=([\s\S]*?)(?:\n[A-Z]|\n$)/);
117
+ if (mockMatch?.[1]) mockDataSource = mockMatch[1].trim();
118
+
112
119
  log(`Compiled ${exampleName}: ${code.split('\n').length} lines, buttons=${hasButtons}, messageKeys=[${messageKeys.join(',')}]`);
113
120
 
114
- return { code, hasButtons, messageKeys, diagnostics };
121
+ return { code, hasButtons, messageKeys, mockDataSource, diagnostics };
115
122
  }
@@ -107,6 +107,7 @@ export interface RectProps extends PositionProps, SizeProps {
107
107
  fill?: ColorName;
108
108
  stroke?: ColorName;
109
109
  strokeWidth?: number;
110
+ borderRadius?: number;
110
111
  children?: ReactNode;
111
112
  }
112
113
 
@@ -151,13 +152,17 @@ export function Line(props: LineProps) {
151
152
 
152
153
  export interface ImageProps extends PositionProps {
153
154
  bitmap: unknown;
155
+ /** Rotation in radians. */
156
+ rotation?: number;
157
+ /** Scale factor (1 = original size). */
158
+ scale?: number;
154
159
  }
155
160
 
156
161
  export function Image(props: ImageProps) {
157
162
  return React.createElement('pbl-image', props);
158
163
  }
159
164
 
160
- export interface GroupProps extends PositionProps {
165
+ export interface GroupProps extends PositionProps, SizeProps {
161
166
  children?: ReactNode;
162
167
  }
163
168
 
@@ -165,6 +170,75 @@ export function Group({ children, ...props }: GroupProps) {
165
170
  return React.createElement('pbl-group', props, children);
166
171
  }
167
172
 
173
+ // ---------------------------------------------------------------------------
174
+ // Flow layout containers
175
+ // ---------------------------------------------------------------------------
176
+
177
+ export interface ColumnProps extends PositionProps, SizeProps {
178
+ /** Gap between children in pixels (default 0). */
179
+ gap?: number;
180
+ children?: ReactNode;
181
+ }
182
+
183
+ /**
184
+ * Stacks children vertically, auto-computing each child's `y` offset.
185
+ * Children should specify `h` (or `height`) for correct stacking;
186
+ * children without a height are given a default of 20px.
187
+ */
188
+ export function Column({ x = 0, y = 0, gap = 0, children, ...props }: ColumnProps) {
189
+ let offsetY = 0;
190
+ const mapped = toArray(children).map((child, i) => {
191
+ if (!child || typeof child !== 'object') return child;
192
+ const vnode = child as { type: unknown; props: Record<string, unknown> };
193
+ if (!vnode.type || !vnode.props) return child;
194
+ const childH = (vnode.props.h ?? vnode.props.height ?? 20) as number;
195
+ const el = React.createElement(
196
+ vnode.type as string,
197
+ { ...(vnode.props as object), y: offsetY, key: i },
198
+ );
199
+ offsetY += childH + gap;
200
+ return el;
201
+ });
202
+
203
+ return React.createElement('pbl-group', { x, y, ...props }, ...(mapped as ReactNode[]));
204
+ }
205
+
206
+ export interface RowProps extends PositionProps, SizeProps {
207
+ /** Gap between children in pixels (default 0). */
208
+ gap?: number;
209
+ children?: ReactNode;
210
+ }
211
+
212
+ /**
213
+ * Stacks children horizontally, auto-computing each child's `x` offset.
214
+ * Children should specify `w` (or `width`) for correct stacking;
215
+ * children without a width are given a default of 40px.
216
+ */
217
+ export function Row({ x = 0, y = 0, gap = 0, children, ...props }: RowProps) {
218
+ let offsetX = 0;
219
+ const mapped = toArray(children).map((child, i) => {
220
+ if (!child || typeof child !== 'object') return child;
221
+ const vnode = child as { type: unknown; props: Record<string, unknown> };
222
+ if (!vnode.type || !vnode.props) return child;
223
+ const childW = (vnode.props.w ?? vnode.props.width ?? 40) as number;
224
+ const el = React.createElement(
225
+ vnode.type as string,
226
+ { ...(vnode.props as object), x: offsetX, key: i },
227
+ );
228
+ offsetX += childW + gap;
229
+ return el;
230
+ });
231
+
232
+ return React.createElement('pbl-group', { x, y, ...props }, ...(mapped as ReactNode[]));
233
+ }
234
+
235
+ /** Flatten ComponentChildren into an array, filtering nulls. */
236
+ function toArray(children: ReactNode): unknown[] {
237
+ if (children == null) return [];
238
+ if (Array.isArray(children)) return children.filter(Boolean);
239
+ return [children];
240
+ }
241
+
168
242
  export interface StatusBarProps {
169
243
  color?: ColorName;
170
244
  backgroundColor?: ColorName;