48 KiB
Capacitor Mobile Target
Phase ID: PHASE-B
Priority: 🔴 High (Primary Target)
Estimated Duration: 4-5 weeks
Status: Planning
Dependencies: Phase A (BYOB Backend), Phase E (Target System Core)
Last Updated: 2025-12-28
Executive Summary
Enable Noodl users to build native iOS and Android applications using Capacitor as the bridge between web technologies and native device capabilities. This is the highest-priority target due to massive user demand for mobile app development without native code knowledge.
Value Proposition
| Current State | With Capacitor Target |
|---|---|
| Export static web, manually wrap in Capacitor | One-click Capacitor project export |
| No mobile preview | Hot-reload preview on device/simulator |
| No native API access | Camera, GPS, Push, Haptics nodes |
| Manual bridge setup | Automatic Capacitor bridge injection |
| 30-60s iteration cycles | 1-2s hot-reload cycles |
Technical Architecture
How Capacitor Works
Capacitor is a cross-platform native runtime that allows web apps to run natively on iOS and Android with access to native device features through JavaScript bridges.
┌─────────────────────────────────────────────────────────────────────┐
│ CAPACITOR ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ YOUR NOODL APP │ │
│ │ (HTML, CSS, JavaScript - same as web export) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ CAPACITOR BRIDGE │ │
│ │ window.Capacitor.Plugins.Camera.getPhoto() │ │
│ │ window.Capacitor.Plugins.Geolocation.getCurrentPosition() │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ iOS Native │ │ Android Native │ │ Web Fallback │ │
│ │ Swift/ObjC │ │ Kotlin/Java │ │ (PWA APIs) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Integration with Noodl
┌─────────────────────────────────────────────────────────────────────┐
│ NOODL EDITOR │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ VISUAL GRAPH │ │
│ │ [Button] ──onClick──▶ [Camera Capture] ──photo──▶ [Image] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ CAPACITOR NODE LIBRARY │ │
│ │ Camera, Geolocation, Push, Haptics, StatusBar, etc. │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────┼───────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ PREVIEW │ │ EXPORT │ │ PREVIEW │ │
│ │ (Web) │ │ (Capacitor) │ │ (Device) │ │
│ │ │ │ │ │ │ │
│ │ Mock APIs │ │ Real build │ │ Hot-reload │ │
│ │ in browser │ │ to Xcode/ │ │ on physical │ │
│ │ │ │ Android │ │ device │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Preview Modes
1. Web Preview (Default)
Standard browser preview with mocked Capacitor APIs:
// When in web preview mode, inject mock Capacitor
if (!window.Capacitor) {
window.Capacitor = {
isNativePlatform: () => false,
Plugins: {
Camera: {
async getPhoto(options: CameraOptions): Promise<Photo> {
// Use browser MediaDevices API
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
// ... capture and return photo
}
},
Geolocation: {
async getCurrentPosition(): Promise<Position> {
// Use browser Geolocation API
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(pos) => resolve({
coords: {
latitude: pos.coords.latitude,
longitude: pos.coords.longitude,
accuracy: pos.coords.accuracy,
}
}),
reject
);
});
}
},
// ... other mocked plugins
}
};
}
Limitations:
- Push notifications won't work (no registration token)
- Some device-specific features unavailable
- Camera quality may differ from native
2. Capacitor Hot-Reload Preview
Connect physical device or simulator to Noodl's dev server:
┌─────────────────────────────────────────────────────────────────────┐
│ Preview Mode: [Web ▾] │
│ ├─ Web (Browser) │
│ ├─ iOS Simulator ◀ │
│ ├─ Android Emulator │
│ └─ Physical Device │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ⚠️ Capacitor Preview Setup Required │
│ │
│ To preview on iOS Simulator: │
│ │
│ 1. Install Xcode from the Mac App Store │
│ 2. Open Terminal and run: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ npx cap run ios --livereload-url=http://localhost:8574 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ 3. Select a simulator when prompted │
│ │
│ [Generate Capacitor Project] [Copy Command] [Open Documentation] │
│ │
└─────────────────────────────────────────────────────────────────────┘
How Hot-Reload Works:
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Noodl Editor │◄───────▶│ Dev Server │◄───────▶│ iOS/Android │
│ │ WebSocket│ localhost:8574│ HTTP │ App in │
│ Make changes │─────────▶│ Serves app │─────────▶ WebView │
│ │ │ + Capacitor │ │ │
└───────────────┘ │ bridge │ │ Native APIs │
└───────────────┘ │ available │
└───────────────┘
- Noodl's dev server already serves the preview app at
localhost:8574 - Capacitor app's WebView loads from this URL instead of bundled assets
- When you make changes in Noodl, the WebView automatically refreshes
- Native Capacitor plugins are available because you're running in a native app
3. Device Preview (Physical Device)
┌─────────────────────────────────────────────────────────────────────┐
│ Preview on Device [×] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ CONNECTED DEVICES │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 📱 Richard's iPhone 15 Pro ✓ Connected │ │
│ │ iOS 17.2 • USB Connected │ │
│ │ [Open App] │ │
│ ├─────────────────────────────────────────────────────────────┤ │
│ │ 📱 Pixel 8 ✓ Connected │ │
│ │ Android 14 • WiFi (192.168.1.42) │ │
│ │ [Open App] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ───────────────────────────────────────────────────────────────── │
│ │
│ NETWORK PREVIEW (WiFi) │
│ Devices on the same network can connect to: │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ http://192.168.1.100:8574 [Copy] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄│ │
│ │ █ █ █ █│ Scan with │
│ │ █ ███ █ █ ███ █│ Noodl Preview App │
│ │ █ █ █ █│ │
│ │ ▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀│ │
│ └─────────────────┘ │
│ │
│ [Download iOS Preview App] [Download Android Preview App] │
│ │
└─────────────────────────────────────────────────────────────────────┘
Capacitor-Specific Nodes
Core Plugin Nodes
Camera Node
┌─────────────────────────────────────────────────────────────────────┐
│ 📷 Camera Capture [📱] [🖥️] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Capture ○──── (Signal - triggers capture) │
│ │
│ Source: [Camera ▾] │
│ ├─ Camera │
│ ├─ Photos Library │
│ └─ Prompt User │
│ │
│ Quality: [High ▾] │
│ ├─ Low (faster, smaller) │
│ ├─ Medium │
│ └─ High (best quality) │
│ │
│ Result Type: [Base64 ▾] │
│ ├─ Base64 (data URL) │
│ ├─ URI (file path) │
│ └─ Blob │
│ │
│ Direction: [Rear ▾] │
│ Correct Orientation: [✓] │
│ Allow Editing: [✓] │
│ Width: [1024 ] (0 = original) │
│ Height: [0 ] (0 = original) │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Photo ────○ (String - base64/URI) │
│ Format ────○ (String - "jpeg"/"png"/"gif") │
│ Captured ────○ (Signal - on success) │
│ Failed ────○ (Signal - on error) │
│ Error ────○ (String - error message) │
│ │
└─────────────────────────────────────────────────────────────────────┘
Implementation:
// packages/noodl-runtime/src/nodes/capacitor/CameraNode.ts
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
import { createNodeDefinition } from '../../nodeDefinition';
export const CameraNode = createNodeDefinition({
name: 'Camera Capture',
category: 'Device & Platform',
color: 'purple',
inputs: {
capture: { type: 'signal', displayName: 'Capture' },
source: {
type: 'enum',
displayName: 'Source',
default: 'camera',
enums: [
{ value: 'camera', label: 'Camera' },
{ value: 'photos', label: 'Photos Library' },
{ value: 'prompt', label: 'Prompt User' },
],
},
quality: {
type: 'number',
displayName: 'Quality',
default: 90,
min: 1,
max: 100,
},
resultType: {
type: 'enum',
displayName: 'Result Type',
default: 'base64',
enums: [
{ value: 'base64', label: 'Base64' },
{ value: 'uri', label: 'URI' },
],
},
direction: {
type: 'enum',
displayName: 'Direction',
default: 'rear',
enums: [
{ value: 'rear', label: 'Rear' },
{ value: 'front', label: 'Front' },
],
},
allowEditing: { type: 'boolean', displayName: 'Allow Editing', default: false },
width: { type: 'number', displayName: 'Width', default: 0 },
height: { type: 'number', displayName: 'Height', default: 0 },
},
outputs: {
photo: { type: 'string', displayName: 'Photo' },
format: { type: 'string', displayName: 'Format' },
captured: { type: 'signal', displayName: 'Captured' },
failed: { type: 'signal', displayName: 'Failed' },
error: { type: 'string', displayName: 'Error' },
},
targetCompatibility: ['capacitor', 'electron', 'web'],
async execute(inputs, outputs, context) {
try {
const sourceMap = {
camera: CameraSource.Camera,
photos: CameraSource.Photos,
prompt: CameraSource.Prompt,
};
const resultTypeMap = {
base64: CameraResultType.Base64,
uri: CameraResultType.Uri,
};
const photo = await Camera.getPhoto({
source: sourceMap[inputs.source],
resultType: resultTypeMap[inputs.resultType],
quality: inputs.quality,
allowEditing: inputs.allowEditing,
width: inputs.width || undefined,
height: inputs.height || undefined,
direction: inputs.direction === 'front' ? 'FRONT' : 'REAR',
});
if (inputs.resultType === 'base64') {
outputs.photo = `data:image/${photo.format};base64,${photo.base64String}`;
} else {
outputs.photo = photo.webPath;
}
outputs.format = photo.format;
outputs.captured.trigger();
} catch (err) {
outputs.error = err.message;
outputs.failed.trigger();
}
},
});
Geolocation Node
┌─────────────────────────────────────────────────────────────────────┐
│ 📍 Geolocation [🌐] [📱] [🖥️] [🧩] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Get Position ○──── (Signal) │
│ Watch Position ○──── (Signal - start watching) │
│ Stop Watching ○──── (Signal) │
│ │
│ High Accuracy: [✓] (uses GPS, slower, more battery) │
│ Timeout (ms): [10000] │
│ Maximum Age (ms): [0 ] (0 = always fresh) │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Latitude ────○ (Number) │
│ Longitude ────○ (Number) │
│ Accuracy ────○ (Number - meters) │
│ Altitude ────○ (Number - meters, may be null) │
│ Speed ────○ (Number - m/s, may be null) │
│ Heading ────○ (Number - degrees, may be null) │
│ Timestamp ────○ (Date) │
│ │
│ Position Updated ────○ (Signal) │
│ Error ────○ (Signal) │
│ Error Message ────○ (String) │
│ │
└─────────────────────────────────────────────────────────────────────┘
Push Notifications Node
┌─────────────────────────────────────────────────────────────────────┐
│ 🔔 Push Notifications [📱] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Register ○──── (Signal - request permission) │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Token ────○ (String - device token) │
│ Registered ────○ (Signal) │
│ Registration Failed ────○ (Signal) │
│ │
│ Notification Received ────○ (Signal) │
│ Notification Title ────○ (String) │
│ Notification Body ────○ (String) │
│ Notification Data ────○ (Object - custom payload) │
│ Notification Action ────○ (String - action ID if tapped) │
│ │
│ Notification Tapped ────○ (Signal - user tapped notif) │
│ │
└─────────────────────────────────────────────────────────────────────┘
Additional Capacitor Nodes
| Node | Category | Description |
|---|---|---|
| Haptics | Device | Trigger haptic feedback (impact, notification, selection) |
| Status Bar | UI | Control status bar appearance (style, color, visibility) |
| Keyboard | UI | Show/hide keyboard, get keyboard height |
| Share | Social | Native share sheet for content |
| App Launcher | System | Open other apps, URLs, settings |
| Device Info | System | Get device model, OS version, battery, etc. |
| Network | System | Get connection type, monitor connectivity |
| Splash Screen | UI | Control splash screen (hide, show) |
| Local Notifications | Notifications | Schedule local notifications |
| Biometric Auth | Security | Face ID, Touch ID, fingerprint auth |
| Clipboard | Utility | Read/write clipboard |
| Browser | Navigation | Open in-app browser |
| App State | Lifecycle | Monitor foreground/background state |
Project Export
Export Flow
┌─────────────────────────────────────────────────────────────────────┐
│ Export for Mobile [×] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ PLATFORM │
│ ┌───────────────────────────────┐ ┌───────────────────────────────┐ │
│ │ [Apple Logo] │ │ [Android Logo] │ │
│ │ │ │ │ │
│ │ iOS │ │ Android │ │
│ │ ✓ Selected │ │ ☐ │ │
│ └───────────────────────────────┘ └───────────────────────────────┘ │
│ │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ APP CONFIGURATION │
│ │
│ App ID: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ com.mycompany.myapp │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ ⓘ Must match your App Store / Play Store app ID │
│ │
│ App Name: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ My Awesome App │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Version: Build Number: │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ 1.0.0 │ │ 1 │ │
│ └─────────────────────────┘ └─────────────────────────┘ │
│ │
│ ▶ iOS Settings │
│ Team ID: [ABC123DEF4 ] │
│ Deployment Target: [iOS 14.0 ▾] │
│ │
│ ▶ Plugins │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ☑ @capacitor/camera (Used by: Camera Capture) │ │
│ │ ☑ @capacitor/geolocation (Used by: Geolocation) │ │
│ │ ☑ @capacitor/push-notifications (Used by: Push Notif...) │ │
│ │ ☐ @capacitor/haptics (Not used) │ │
│ │ ☐ @capacitor/share (Not used) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ▶ Permissions (auto-detected from node usage) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 📷 Camera Access │ │
│ │ "This app needs camera access to take photos" │ │
│ │ │ │
│ │ 📍 Location When In Use │ │
│ │ "This app needs your location to show nearby places" │ │
│ │ │ │
│ │ 🔔 Push Notifications │ │
│ │ (Configured in Apple Developer Portal) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ OUTPUT │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ ○ Generate Xcode/Android Studio Project │ │
│ │ Full native project ready for building and customization │ │
│ │ │ │
│ │ ● Generate Build-Ready Package RECOMMENDED │ │
│ │ Minimal project, just run `npx cap build` │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ [Cancel] [Export for iOS] │
└─────────────────────────────────────────────────────────────────────┘
Generated Project Structure
my-app-capacitor/
├── package.json # Node.js package with Capacitor deps
├── capacitor.config.ts # Capacitor configuration
├── www/ # Built Noodl app (web assets)
│ ├── index.html
│ ├── main.js
│ ├── styles.css
│ └── assets/
├── ios/ # iOS Xcode project
│ ├── App/
│ │ ├── App.xcodeproj
│ │ ├── App/
│ │ │ ├── AppDelegate.swift
│ │ │ ├── Info.plist # Permissions, app config
│ │ │ └── Assets.xcassets # App icons
│ │ └── Podfile # iOS dependencies
│ └── Pods/
├── android/ # Android Studio project
│ ├── app/
│ │ ├── build.gradle
│ │ ├── src/main/
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── java/.../MainActivity.java
│ │ │ └── res/ # Icons, splash screens
│ └── gradle/
└── README.md # Build instructions
capacitor.config.ts
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.mycompany.myapp',
appName: 'My Awesome App',
webDir: 'www',
// Development: connect to Noodl dev server
server: process.env.NODE_ENV === 'development' ? {
url: 'http://localhost:8574',
cleartext: true,
} : undefined,
plugins: {
Camera: {
// iOS requires these privacy descriptions
},
PushNotifications: {
presentationOptions: ['badge', 'sound', 'alert'],
},
},
ios: {
// iOS-specific configuration
},
android: {
// Android-specific configuration
},
};
export default config;
Implementation Phases
Phase B.1: Capacitor Bridge Integration (1 week)
Goal: Enable Noodl preview to run inside Capacitor WebView with hot-reload.
Tasks:
- Detect when running in Capacitor context (
window.Capacitor.isNativePlatform()) - Inject Capacitor bridge scripts in preview HTML
- Configure dev server for external access (CORS, network binding)
- Create "Capacitor Preview Mode" toggle in editor
- Document Xcode/Android Studio setup for live-reload
Files to Create/Modify:
packages/noodl-editor/src/frames/viewer-frame/src/views/
├── viewer.js # Modify: Add Capacitor bridge injection
└── capacitorBridge.js # New: Capacitor-specific preview logic
packages/noodl-editor/src/editor/src/views/EditorTopbar/
└── PreviewModeSelector.tsx # New: Preview mode dropdown component
Phase B.2: Core Capacitor Nodes (2 weeks)
Goal: Implement essential device capability nodes.
Week 1: High-priority nodes
- Camera Capture node
- Geolocation node
- Push Notifications node
- Haptics node
Week 2: Secondary nodes
- Share node
- Status Bar node
- Device Info node
- Network Status node
- App State node
Files to Create:
packages/noodl-runtime/src/nodes/capacitor/
├── index.ts # Export all Capacitor nodes
├── CameraNode.ts
├── GeolocationNode.ts
├── PushNotificationsNode.ts
├── HapticsNode.ts
├── ShareNode.ts
├── StatusBarNode.ts
├── DeviceInfoNode.ts
├── NetworkNode.ts
├── AppStateNode.ts
└── _mocks/ # Web fallback implementations
├── cameraMock.ts
├── geolocationMock.ts
└── ...
Phase B.3: Simulator/Device Launch (1 week)
Goal: One-click launch to iOS Simulator or Android Emulator.
Tasks:
- Detect installed simulators (
xcrun simctl list) - Detect installed Android emulators (
emulator -list-avds) - Create simulator selection UI
- Implement launch command execution
- Display QR code for physical device connection
Files to Create:
packages/noodl-editor/src/editor/src/views/panels/
└── CapacitorPreview/
├── SimulatorList.tsx
├── DeviceConnection.tsx
└── QRCodeModal.tsx
Phase B.4: Export Pipeline (1 week)
Goal: Generate production-ready Capacitor project.
Tasks:
- Build web assets to
www/folder - Generate
capacitor.config.ts - Generate
package.jsonwith correct dependencies - Auto-detect required plugins from node usage
- Generate platform-specific permission strings
- Create iOS Xcode project scaffolding
- Create Android Studio project scaffolding
- Generate README with build instructions
Files to Create:
packages/noodl-editor/src/editor/src/export/
├── capacitor/
│ ├── CapacitorExporter.ts # Main export orchestrator
│ ├── configGenerator.ts # Generate capacitor.config.ts
│ ├── packageGenerator.ts # Generate package.json
│ ├── pluginDetector.ts # Detect required plugins from graph
│ ├── permissionGenerator.ts # Generate iOS Info.plist entries
│ ├── iosProjectGenerator.ts # Scaffold iOS project
│ └── androidProjectGenerator.ts # Scaffold Android project
└── templates/
└── capacitor/
├── package.json.template
├── capacitor.config.ts.template
└── ios/
└── Info.plist.template
Web Fallback Behavior
When Capacitor nodes are used in web preview or web deployment:
| Node | Web Fallback Behavior |
|---|---|
| Camera | Uses navigator.mediaDevices.getUserMedia() |
| Geolocation | Uses navigator.geolocation |
| Push Notifications | Shows warning, suggests Web Push API setup |
| Haptics | Uses navigator.vibrate() (limited support) |
| Share | Uses Web Share API (Chrome, Safari) |
| Status Bar | No-op (not applicable to web) |
| Device Info | Returns navigator.userAgent parsed info |
| Network | Uses navigator.connection |
// Example: Camera fallback implementation
async function capturePhotoWeb(options: CameraOptions): Promise<Photo> {
// Check if we're in a native Capacitor context
if (window.Capacitor?.isNativePlatform()) {
return Camera.getPhoto(options);
}
// Web fallback using MediaDevices API
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: options.direction === 'front' ? 'user' : 'environment',
width: options.width || undefined,
height: options.height || undefined,
}
});
// Create video element to capture frame
const video = document.createElement('video');
video.srcObject = stream;
await video.play();
// Draw frame to canvas
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(video, 0, 0);
// Stop stream
stream.getTracks().forEach(track => track.stop());
// Convert to base64
const dataUrl = canvas.toDataURL('image/jpeg', options.quality / 100);
return {
base64String: dataUrl.split(',')[1],
format: 'jpeg',
webPath: dataUrl,
};
}
Testing Strategy
Unit Tests
describe('CameraNode', () => {
describe('web fallback', () => {
it('should use MediaDevices API when not in native context', async () => {
const mockGetUserMedia = jest.fn().mockResolvedValue(mockStream);
navigator.mediaDevices.getUserMedia = mockGetUserMedia;
const node = new CameraNode();
node.setInput('source', 'camera');
await node.executeCapture();
expect(mockGetUserMedia).toHaveBeenCalledWith({
video: expect.any(Object),
});
});
});
describe('native context', () => {
beforeEach(() => {
window.Capacitor = { isNativePlatform: () => true };
});
it('should use Capacitor Camera plugin', async () => {
const mockGetPhoto = jest.fn().mockResolvedValue(mockPhoto);
Camera.getPhoto = mockGetPhoto;
const node = new CameraNode();
await node.executeCapture();
expect(mockGetPhoto).toHaveBeenCalled();
});
});
});
Integration Tests
describe('Capacitor Export', () => {
it('should detect required plugins from node graph', async () => {
const project = createTestProject([
{ type: 'Camera Capture' },
{ type: 'Geolocation' },
{ type: 'Text' }, // Not a Capacitor node
]);
const detector = new PluginDetector(project);
const plugins = detector.getRequiredPlugins();
expect(plugins).toEqual([
'@capacitor/camera',
'@capacitor/geolocation',
]);
});
it('should generate valid capacitor.config.ts', async () => {
const exporter = new CapacitorExporter(mockProject);
const config = await exporter.generateConfig();
expect(config).toContain("appId: 'com.test.app'");
expect(config).toContain("appName: 'Test App'");
});
});
Manual Testing Checklist
- Create new Capacitor-target project
- Add Camera node, verify web fallback works
- Enable Capacitor preview mode
- Launch iOS Simulator with live-reload
- Take photo in simulator, verify it appears in Noodl
- Export project for iOS
- Open in Xcode, build successfully
- Run on physical iPhone, verify all features work
Success Criteria
| Criteria | Target |
|---|---|
| Hot-reload latency | < 2 seconds from save to device update |
| Export time | < 30 seconds for complete Capacitor project |
| Xcode build success | First-time build succeeds without manual fixes |
| Plugin detection accuracy | 100% of used plugins detected automatically |
| Web fallback coverage | All Capacitor nodes have functional web fallbacks |
Future Enhancements
Phase B+ Features (Post-MVP)
- Capacitor Plugins Marketplace - Browse and install community plugins
- Native UI Components - Use platform-native UI (iOS UIKit, Android Material)
- Background Tasks - Run code when app is backgrounded
- Deep Linking - Handle custom URL schemes
- In-App Purchases - Integrate with App Store / Play Store purchases
- App Store Deployment - One-click submit to stores (via Fastlane)
Advanced Native Integration
For users who need more native control:
// Custom Capacitor Plugin Node
// Allows calling any Capacitor plugin method
interface CustomPluginNodeInputs {
pluginName: string; // e.g., "@capacitor/camera"
methodName: string; // e.g., "getPhoto"
options: object; // Method parameters
}
// This enables using ANY Capacitor plugin, even community ones