create-octane-xplat 0.11.1 → 0.12.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-octane-xplat",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Scaffold a minimal @octane-xplat/ui app — one Octane codebase for web + iOS + Android",
5
5
  "keywords": [
6
6
  "create",
package/scaffold.mjs CHANGED
@@ -34,7 +34,7 @@ export const targets = {
34
34
  label: 'iOS (NativeScript)',
35
35
  requires: ['native-shared'],
36
36
  files: ['App_Resources/iOS'],
37
- scripts: ['dev:ios', 'build:ios'],
37
+ scripts: ['dev:ios', 'test:ios', 'build:ios'],
38
38
  devDeps: ['@nativescript/ios'],
39
39
  note: 'needs macOS + Xcode — `pnpm xplat doctor` checks the toolchain',
40
40
  },
@@ -42,7 +42,7 @@ export const targets = {
42
42
  label: 'Android (NativeScript)',
43
43
  requires: ['native-shared'],
44
44
  files: ['App_Resources/Android'],
45
- scripts: ['dev:android', 'build:android'],
45
+ scripts: ['dev:android', 'test:android', 'build:android'],
46
46
  devDeps: ['@nativescript/android'],
47
47
  note: 'needs Android SDK + a compatible JDK — `pnpm xplat doctor` checks the toolchain',
48
48
  },
@@ -67,6 +67,12 @@ export const targets = {
67
67
  'tsconfig.native.json',
68
68
  'references.d.ts',
69
69
  'src/main.ts',
70
+ // On-device Vitest lane — `ns test` builds src/test.ts under
71
+ // --env.unitTesting and runs every src/**/*.spec file on a
72
+ // simulator/emulator through the upstream runner.
73
+ 'src/test.ts',
74
+ 'src/app.spec.tsrx',
75
+ 'vitest.config.mts',
70
76
  ],
71
77
  deps: [
72
78
  '@nativescript-community/gesturehandler',
@@ -80,8 +86,10 @@ export const targets = {
80
86
  devDeps: [
81
87
  '@nativescript-community/vite-octane',
82
88
  '@nativescript/types',
89
+ '@nativescript/unit-test-runner',
83
90
  '@nativescript/vite',
84
91
  'nativescript',
92
+ 'vitest',
85
93
  ],
86
94
  typecheck: 'tsrx-tsc --noEmit -p tsconfig.native.json',
87
95
  },
@@ -7,3 +7,32 @@ Octane Xplat app — one TypeScript codebase for web + iOS + Android (Octane
7
7
  Before writing code, read `.agents/skills/xplat/SKILL.md` (or invoke the
8
8
  `xplat` skill). Its invariants are enforced by `pnpm lint` — violations
9
9
  usually still compile but silently break on one platform.
10
+
11
+ ## Stable automation targets
12
+
13
+ ```tsx
14
+ import { Button } from '@octane-xplat/ui'
15
+
16
+ export function AddItem() {
17
+ return <Button testID="packing.add-item" label="Add item" accessibilityLabel="Add item" />
18
+ }
19
+ ```
20
+
21
+ Assign optional semantic `testID` names to meaningful controls and asserted
22
+ state as you build: `packing.add-item`, `profile.name`,
23
+ `packing.item.${item.key}.open`. Keep names unique among mounted targets,
24
+ stable across localization, and based on durable repeated-item keys rather
25
+ than array positions. Preserve `id` values, useful accessibility labels and
26
+ roles. Do not put IDs on every decorative child or expose personal data.
27
+ Inputs target their editable host; overlays target mounted content. Verify
28
+ forwarding when composing another component; inherited types alone do not
29
+ promise platform support. See [qualified hosts and mappings](https://octane-xplat.goddardai.org/verify/test-identifiers).
30
+
31
+ Argent is optional AI-agent device/browser control through MCP, with no app
32
+ runtime dependency. Use [official setup](https://docs.swmansion.com/argent/docs/fundamentals/installation/)
33
+ and the [qualified local recipe](https://octane-xplat.goddardai.org/verify/argent)
34
+ (version 0.27.0). Prefer stable IDs for reliable targeting while keeping
35
+ spoken accessibility semantics. Discovery and replay have different native
36
+ prerequisites. Chromium prefers DOM `id` over `data-testid` when both exist.
37
+ The recipe states the actual iOS/Android/Chromium scope and limits; do not
38
+ infer AppKit, physical-device, or arbitrary typing support.
@@ -48,5 +48,13 @@
48
48
  <string>UIInterfaceOrientationLandscapeLeft</string>
49
49
  <string>UIInterfaceOrientationLandscapeRight</string>
50
50
  </array>
51
+ <key>NSAppTransportSecurity</key>
52
+ <dict>
53
+ <!-- Loopback WebSocket for `ns test` (@nativescript/unit-test-runner):
54
+ the on-device coordinator reaches the host Vitest session at
55
+ ws://127.0.0.1:<port>. Scoped to local networking only. -->
56
+ <key>NSAllowsLocalNetworking</key>
57
+ <true/>
58
+ </dict>
51
59
  </dict>
52
60
  </plist>
@@ -8,6 +8,8 @@
8
8
  "dev": "vite",
9
9
  "dev:ios": "ns run ios",
10
10
  "dev:android": "ns run android",
11
+ "test:ios": "ns test ios",
12
+ "test:android": "ns test android",
11
13
  "build": "vite build",
12
14
  "build:ios": "ns build ios",
13
15
  "build:android": "ns build android",
@@ -19,28 +21,31 @@
19
21
  "@nativescript-community/gesturehandler": "2.0.45",
20
22
  "@nativescript-community/octane": "0.2.4",
21
23
  "@nativescript-community/ui-drawer": "0.1.32",
22
- "@nativescript/core": "9.1.2",
23
- "@octane-xplat/effects": "^0.11.1",
24
- "@octane-xplat/image": "^0.11.1",
25
- "@octane-xplat/platform": "^0.11.1",
26
- "@octane-xplat/ui": "^0.11.1",
24
+ "@nativescript/core": "9.1.3",
25
+ "@octane-xplat/effects": "^0.12.0",
26
+ "@octane-xplat/image": "^0.12.0",
27
+ "@octane-xplat/platform": "^0.12.0",
28
+ "@octane-xplat/ui": "^0.12.0",
27
29
  "@valor/nativescript-websockets": "2.0.3",
28
- "octane": "0.6.3"
30
+ "octane": "0.10.2"
29
31
  },
30
32
  "devDependencies": {
31
33
  "@nativescript-community/vite-octane": "0.2.1",
32
34
  "@nativescript/android": "9.1.1",
33
35
  "@nativescript/ios": "9.1.0",
34
36
  "@nativescript/types": "9.1.4",
37
+ "@nativescript/unit-test-runner": "5.0.0",
35
38
  "@nativescript/vite": "8.0.17",
36
- "@octane-xplat/cli": "^0.11.1",
37
- "@octane-xplat/lint": "^0.11.1",
39
+ "@octane-xplat/cli": "^0.12.0",
40
+ "@octane-xplat/lint": "^0.12.0",
38
41
  "@octanejs/vite-plugin": "0.1.61",
42
+ "@tsrx/oxc": "0.16.0",
39
43
  "@tsrx/typescript-plugin": "0.4.11",
40
44
  "nativescript": "9.1.1",
41
45
  "oxlint": "^1.74.0",
42
46
  "typescript": "5.9.3",
43
- "vite": "8.3.0"
47
+ "vite": "8.3.0",
48
+ "vitest": "4.1.11"
44
49
  },
45
50
  "packageManager": "pnpm@11.24.0"
46
51
  }
@@ -21,35 +21,24 @@ allowUnusedPatches: true
21
21
  # applied via pnpm patchedDependencies. Managed upstream by @octane-xplat/cli (`xplat
22
22
  # patches apply` refreshes, `xplat patches check` verifies); do not edit by hand.
23
23
  patchedDependencies:
24
- # Published octane 0.6.3 lacks live signal reads under universal renderers. Adds signal
25
- # ownership, read tracking, and local signal hooks; JSX prop lowering and __methodDep
26
- # already ship upstream. Routes uncaught universal event-handler errors to onUncaughtError
27
- # when configured. Pins instance signal/query declarations to their declaring owner so
28
- # child reads and event handlers share one request. Makes universal useImperativeHandle
29
- # honor optional refs, ref replacement, nested ref arrays, and callback cleanup. Also
30
- # allows async and generator helpers that are not components in universal compiler
31
- # modules, while preserving errors when those functions are used as components
32
- # (octanejs/octane#1469). Adds renderer-owned Children.toArray/map for transparent
33
- # deferred JSX groups, plan ranges/slots and arrays, preserving keyed wrapper identity
34
- # while leaving scoped owners intact.
35
- octane@0.6.3: node_modules/.pnpm-config/@octane-xplat/patches/patches/octane@0.6.3.patch
36
- # FlexboxLayout getChildMeasureSpec subtracts the child's own padding where AOSP subtracts
37
- # parent padding + child margins — a column-stretched row re-measurement clamps padded
38
- # children (37→17 dip, squished controls). Adds squircle (continuous) corner clipping for
39
- # iOS backgrounds and CSS @layer cascade support to the native css-tree adapter (layer
40
- # nodes, first-seen layerPath ordinals, layer rank ahead of specificity with unlayered
41
- # last — sans !important). Ports the runtime half of NativeScript PR #11446 (fixes
42
- # #11444): TabView onItemsChanged keeps retained items attached instead of
43
- # unloading/re-adding them (the churn that left TabViewItem-hosted Frames isLoaded=false
44
- # on iOS and detached from their tab fragment on Android), iOS items writes load the
45
- # selected item's view once controllers exist, and Frame._pushInFrameStack only promotes
46
- # frames whose ancestors present them (Frame.topmost stops returning hidden preloaded tab
47
- # frames). Also maps CSS font-weight to the OpenType wght axis for custom Android fonts on
48
- # API 26+, preserves explicit variation settings, and falls back to the prior file load
49
- # when the axis is unsupported. Carries NativeScript PR #11481: preserves percentage
50
- # translate values through parsing and resolves each axis against the view's own
51
- # dimensions, including native views and animations.
52
- "@nativescript/core@9.1.2": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript__core@9.1.2.patch
24
+ # Published 0.10.2 ships async/generator non-component helper compilation and Descriptor
25
+ # lexical/ancestor ownership, so the helper hunks are removed and the old four-hunk
26
+ # instance ownership fix becomes one declaring-owner reader fallback per distribution.
27
+ # Retains renderer-owned Children.toArray/map with explicit universal-native re-exports
28
+ # for older runtimes, universal signal-read tracking and local hooks, root event-error
29
+ # forwarding, and the complete universal imperative-ref lifecycle. Connects universal
30
+ # signal-owner identities to the upstream enclosingOwner chain so child reads reuse
31
+ # instance declarations. Verified against the registry 0.10.2 tarball and the patched
32
+ # 0.6.3 output.
33
+ octane@0.10.2: node_modules/.pnpm-config/@octane-xplat/patches/patches/octane@0.10.2.patch
34
+ # Published 9.1.3 ships the #11446 nested-tab-frame lifecycle fix and iOS explicit-size
35
+ # flex alignment, so those hunks are removed. Retains the unshipped parent-padding
36
+ # measure-spec fix and Android explicit-size flex alignment, iOS continuous corner
37
+ # clipping, CSS @layer cascade support, custom Android variable-font weight mapping, and
38
+ # percentage translate parsing, native view, and animation support. Verified against the
39
+ # registry 9.1.3 tarball; it has no Windows implementation, so the separate Windows
40
+ # preview patch remains.
41
+ "@nativescript/core@9.1.3": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript__core@9.1.3.patch
53
42
  # @nativescript/vite serializes bundled CSS and dev app.css through reworkcss `css`@3.0.0,
54
43
  # which mis-parses @layer into garbage selectors — rules die before reaching
55
44
  # _populateRules. Adds atlayer() covering block and statement forms. Applies to a
@@ -118,4 +107,4 @@ patchedDependencies:
118
107
  # bytes to disk.
119
108
  "@nativescript-community/ui-image@5.0.17": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript-community__ui-image@5.0.17.patch
120
109
  configDependencies:
121
- "@octane-xplat/patches": 0.11.1
110
+ "@octane-xplat/patches": 0.12.0
@@ -1 +1,2 @@
1
1
  /// <reference types="@nativescript/types" />
2
+ /// <reference types="vite/client" />
@@ -28,16 +28,16 @@ export function App() @{
28
28
  <Text className="muted">one codebase — web · iOS · android</Text>
29
29
 
30
30
  <HStack className="counter">
31
- <Pressable className="btn" onPress={() => setCount(count - 1)}><Text>−</Text></Pressable>
32
- <Text className="count">{count}</Text>
33
- <Pressable className="btn" onPress={() => setCount(count + 1)}><Text>+</Text></Pressable>
31
+ <Pressable testID="counter.decrement" accessibilityLabel="Decrease count" className="btn" onPress={() => setCount(count - 1)}><Text>−</Text></Pressable>
32
+ <Text testID="counter.value" className="count">{count}</Text>
33
+ <Pressable testID="counter.increment" accessibilityLabel="Increase count" className="btn" onPress={() => setCount(count + 1)}><Text>+</Text></Pressable>
34
34
  </HStack>
35
35
 
36
36
  <ScrollableArea>
37
37
  {FEATURES.map((f) => <Text className="chip">{f}</Text>)}
38
38
  </ScrollableArea>
39
39
 
40
- <Pressable className="link" onPress={() => {
40
+ <Pressable testID="settings.toggle-theme" className="link" onPress={() => {
41
41
  const next = !dark;
42
42
  storage.setString('theme-preference', next ? 'dark' : 'light');
43
43
  setOverride(next);
@@ -0,0 +1,132 @@
1
+ // Starter spec — `pnpm test:ios` runs this inside the real iOS runtime
2
+ // (same renderer, native views, and services your app uses).
3
+ //
4
+ // mountXplat() renders a component onto the test host page and unmounts it
5
+ // when the test finishes; tap()/enterText() dispatch through the same
6
+ // gesture observers as real touches — that proves your wiring, not OS
7
+ // hit-testing (device journeys like that belong to Maestro).
8
+ import { describe, expect, it } from 'vitest'
9
+ import { useEffect, useState } from 'octane'
10
+ import { Pressable, Text, TextInput, View } from '@octane-xplat/ui'
11
+ import {
12
+ type TextInputView,
13
+ enterText,
14
+ findByTestId,
15
+ mountXplat,
16
+ nextRenderPass,
17
+ tap,
18
+ viewText,
19
+ waitUntil,
20
+ } from '@octane-xplat/platform/testing'
21
+
22
+ import { App } from './App.tsrx'
23
+
24
+ // Controlled input — typing flows up through onChange and back down as the
25
+ // rendered value, and parent writes reach the native field.
26
+ function NameForm() @{
27
+ const [name, setName] = useState('')
28
+
29
+ <View>
30
+ <TextInput
31
+ testID="greeting.name"
32
+ value={name}
33
+ onChange={setName}
34
+ placeholder="Your name"
35
+ />
36
+ <Pressable testID="greeting.reset" onPress={() => setName('Bob')}>
37
+ <Text>set bob</Text>
38
+ </Pressable>
39
+ <Text>{'greeting: ' + (name || 'stranger')}</Text>
40
+ </View>
41
+ }
42
+
43
+ // Conditional mount — a subtree exists only while its state says so.
44
+ function Details() @{
45
+ const [open, setOpen] = useState(false)
46
+
47
+ <View>
48
+ <Pressable testID="details.toggle" onPress={() => setOpen(!open)}>
49
+ <Text>toggle</Text>
50
+ </Pressable>
51
+ @if (open) {
52
+ <Text>detail: open</Text>
53
+ }
54
+ </View>
55
+ }
56
+
57
+ // Async states — loading first, then a settled value or an error. The
58
+ // cleanup flag keeps a late timer from writing into an unmounted tree.
59
+ function FetchLabel(props: { fail?: boolean }) @{
60
+ const [state, setState] = useState('loading')
61
+ useEffect(() => {
62
+ let live = true
63
+ const timer = setTimeout(() => {
64
+ if (live) setState(props.fail ? 'error' : 'ready')
65
+ }, 30)
66
+
67
+ return () => {
68
+ live = false
69
+ clearTimeout(timer)
70
+ }
71
+ }, [])
72
+
73
+ <Text>{'fetch: ' + state}</Text>
74
+ }
75
+
76
+ describe('starter app on device', () => {
77
+ it('mounts, reacts to a tap, and unmounts cleanly', async () => {
78
+ const { view, host, unmount } = await mountXplat(App)
79
+
80
+ const increment = findByTestId(view, 'counter.increment')
81
+ const count = findByTestId<TextInputView>(view, 'counter.value')
82
+ expect(increment?.isLoaded, 'counter.increment resolves to a loaded native view').toBe(true)
83
+
84
+ await tap(increment!)
85
+ await waitUntil(() => count?.text === '1', {
86
+ message: 'tap did not reach the increment Pressable',
87
+ })
88
+
89
+ await unmount()
90
+ await nextRenderPass()
91
+ expect(host.content).toBe(null)
92
+ })
93
+
94
+ it('round-trips a controlled TextInput', async () => {
95
+ const { view } = await mountXplat(NameForm)
96
+ const field = findByTestId<TextInputView>(view, 'greeting.name')
97
+ expect(field?.isLoaded).toBe(true)
98
+
99
+ await enterText(field!, 'Ada')
100
+ await waitUntil(() => viewText(view).includes('greeting: Ada'), {
101
+ message: 'onChange did not deliver the typed text',
102
+ })
103
+
104
+ await tap(findByTestId(view, 'greeting.reset')!)
105
+ await waitUntil(() => field?.text === 'Bob', {
106
+ message: 'parent write did not reach the native field',
107
+ })
108
+ })
109
+
110
+ it('mounts and unmounts a conditional subtree', async () => {
111
+ const { view } = await mountXplat(Details)
112
+ expect(viewText(view).includes('detail: open')).toBe(false)
113
+
114
+ await tap(findByTestId(view, 'details.toggle')!)
115
+ await waitUntil(() => viewText(view).includes('detail: open'))
116
+
117
+ await tap(findByTestId(view, 'details.toggle')!)
118
+ await waitUntil(() => !viewText(view).includes('detail: open'))
119
+ })
120
+
121
+ it('settles async loading and error states', async () => {
122
+ const ready = await mountXplat(FetchLabel)
123
+ await waitUntil(() => viewText(ready.view).includes('fetch: ready'), {
124
+ message: 'loading state never settled',
125
+ })
126
+
127
+ const failed = await mountXplat(FetchLabel, { fail: true })
128
+ await waitUntil(() => viewText(failed.view).includes('fetch: error'), {
129
+ message: 'error state never surfaced',
130
+ })
131
+ })
132
+ })
@@ -0,0 +1,22 @@
1
+ // Test bundle entry — `ns test` builds with --env.unitTesting, and the
2
+ // xplatNative preset swaps the app entry (src/main.ts) for this file, so the
3
+ // device boots the Vitest coordinator instead of the app.
4
+ import '@valor/nativescript-websockets'
5
+ import { Application } from '@nativescript/core'
6
+ import {
7
+ NativeScriptVitestCoordinator,
8
+ createNativeScriptTestRegistry,
9
+ } from '@nativescript/unit-test-runner/runtime'
10
+
11
+ import { createVitestHostPage } from '@nativescript/unit-test-runner/testing'
12
+
13
+ const coordinator = new NativeScriptVitestCoordinator({
14
+ // Every spec matched by vitest.config.mts `include` must also match this
15
+ // glob — vite statically bundles each as a lazily imported chunk that the
16
+ // registry loads on demand.
17
+ registry: createNativeScriptTestRegistry(import.meta.glob('./**/*.spec.{ts,tsx,tsrx}')),
18
+ })
19
+
20
+ void coordinator.start()
21
+ // The host page keeps the screen free as a mountXplat() surface for UI specs.
22
+ Application.run({ create: () => createVitestHostPage(coordinator) })
@@ -0,0 +1,30 @@
1
+ import { defineConfig } from 'vitest/config'
2
+ import { nativeScript } from '@nativescript/unit-test-runner'
3
+
4
+ // `pnpm test:ios` / `pnpm test:android` (ns test) drive this config: the
5
+ // plugin launches `ns run <platform> --no-hmr --env.unitTesting` and
6
+ // orchestrates spec execution on-device over a loopback WebSocket — Vitest
7
+ // stays the orchestrator on the host.
8
+ //
9
+ // On hosts without Simulator.app (Xcode 27+), `ns run` can only target an
10
+ // already-booted simulator: `xcrun simctl boot <udid>` first, then pass
11
+ // `--device <udid>` (or NS_DEVICE).
12
+ export default defineConfig({
13
+ plugins: [
14
+ nativeScript({
15
+ platform: process.env.NS_PLATFORM || 'ios', // 'android' | 'ios' | 'visionos'
16
+ device: process.env.NS_DEVICE || undefined,
17
+ // Keep this in sync with the import.meta.glob in src/test.ts —
18
+ // the device can only run files that made it into the bundle.
19
+ include: ['src/**/*.spec.{ts,tsx,tsrx}'],
20
+ // The connect clock starts when the host session opens and covers
21
+ // `ns run`'s native build + install + boot — a cold build easily
22
+ // exceeds the 120s default.
23
+ connectTimeout: 600_000,
24
+ }),
25
+ ],
26
+ test: {
27
+ // Device runs include app startup and real layout passes.
28
+ testTimeout: 30_000,
29
+ },
30
+ })