Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"permissions": {
"allow": [
"WebFetch(domain:docs.expo.dev)",
"mcp__ide__getDiagnostics",
"Bash(yarn test:*)",
"Bash(yarn why:*)",
"Bash(npm ls:*)",
"Bash(npm view:*)",
"WebSearch",
"WebFetch(domain:github.com)",
"Bash(npx tsc:*)"
]
}
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ coverage/

# Expo asset cache
.expo-assets/
CLAUDE.md
54 changes: 46 additions & 8 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,17 @@ yarn workspace @react-three/native run test:coverage
```
packages/native/
├── tests/
│ ├── setup.ts # Test configuration
│ └── canvas.test.tsx # Canvas tests
├── __mocks__/ # Module mocks
│ ├── expo-gl.ts
│ ├── expo-asset.ts
│ ├── expo-file-system.ts
│ └── react-native.ts
│ ├── setup.ts # Test configuration & globals
│ ├── canvas.test.tsx # Canvas component tests
│ ├── polyfills.test.ts # Polyfill tests
│ ├── renderer.test.tsx # Renderer tests (placeholder)
│ ├── pointerEventPollyfill.ts # JSDOM PointerEvent fix
│ └── mocks/ # Vitest-compatible mocks
│ ├── expo-gl.ts
│ ├── expo-asset.ts
│ ├── expo-file-system.ts
│ └── react-native.ts
├── __mocks__/ # Legacy mocks (reference)
└── vitest.config.ts # Vitest configuration
```

Expand Down Expand Up @@ -145,16 +149,46 @@ yarn test # Run tests
yarn build:lib # Build library
```

## 📋 Test Categories

### Unit Tests (packages/native/tests/)

Fast, mocked tests that run in isolation:
- **Canvas tests**: Module exports, component structure
- **Polyfills tests**: Three.js loader patches
- **Renderer tests**: (placeholder for future tests)

### Integration Tests (apps/example/ - Future)

Real React Native environment tests:
- Full Canvas rendering with @react-three/fiber
- Touch event handling
- Asset loading with real expo modules

Note: Some unit tests are marked as `skipped` because they require full @react-three/fiber reconciler integration, which is better suited for integration tests.

## 🐛 Troubleshooting

### Pre-Alpha Test Status

⚠️ This library is in **pre-alpha** development. Tests may fail due to ongoing architectural changes. The goal is for tests to **run** without import/setup errors - some test failures are expected.

### "Cannot find module" errors

Make sure you're in the right directory:
Make sure you're in the right directory and dependencies are installed:
```bash
cd packages/native
yarn install
yarn test
```

### Missing `@react-three/test-renderer` or `react-nil`

These are required devDependencies. If missing, add them:
```bash
yarn workspace @react-three/native add -D @react-three/test-renderer react-nil
```

### Mocks not working

Check that `tests/setup.ts` imports all needed mocks:
Expand All @@ -164,6 +198,10 @@ import '../__mocks__/expo-asset';
// etc...
```

### PointerEvent not defined

JSDOM doesn't include PointerEvent. The test setup includes a polyfill in `tests/pointerEventPollyfill.ts` that's automatically applied via `tests/setup.ts`.

### Coverage not generating

Install the coverage provider:
Expand Down
98 changes: 95 additions & 3 deletions packages/native/__mocks__/expo-gl.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,105 @@
import * as React from 'react'
import type { GLViewProps } from 'expo-gl'
import { WebGL2RenderingContext } from '@react-three/test-renderer/src/WebGL2RenderingContext'

//* WebGL2 Mock Context ==============================

// Minimal WebGL2RenderingContext mock for testing
// Based on @react-three/test-renderer's implementation
class MockWebGL2RenderingContext {
canvas: HTMLCanvasElement
drawingBufferWidth: number
drawingBufferHeight: number

constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas
this.drawingBufferWidth = canvas.width || 1280
this.drawingBufferHeight = canvas.height || 800
}

// WebGL methods (no-ops for testing)
getParameter = () => null
getExtension = () => null
createShader = () => ({})
createProgram = () => ({})
createBuffer = () => ({})
createTexture = () => ({})
createFramebuffer = () => ({})
createRenderbuffer = () => ({})
bindBuffer = () => {}
bindTexture = () => {}
bindFramebuffer = () => {}
bindRenderbuffer = () => {}
bufferData = () => {}
shaderSource = () => {}
compileShader = () => {}
attachShader = () => {}
linkProgram = () => {}
useProgram = () => {}
enable = () => {}
disable = () => {}
clear = () => {}
clearColor = () => {}
clearDepth = () => {}
viewport = () => {}
scissor = () => {}
drawArrays = () => {}
drawElements = () => {}
getShaderParameter = () => true
getProgramParameter = () => true
getShaderInfoLog = () => ''
getProgramInfoLog = () => ''
getUniformLocation = () => ({})
getAttribLocation = () => 0
enableVertexAttribArray = () => {}
vertexAttribPointer = () => {}
uniform1i = () => {}
uniform1f = () => {}
uniform2f = () => {}
uniform3f = () => {}
uniform4f = () => {}
uniformMatrix4fv = () => {}
texImage2D = () => {}
texParameteri = () => {}
pixelStorei = () => {}
generateMipmap = () => {}
deleteShader = () => {}
deleteProgram = () => {}
deleteBuffer = () => {}
deleteTexture = () => {}
deleteFramebuffer = () => {}
deleteRenderbuffer = () => {}
blendFunc = () => {}
blendEquation = () => {}
depthFunc = () => {}
depthMask = () => {}
cullFace = () => {}
frontFace = () => {}
activeTexture = () => {}
flush = () => {}
finish = () => {}
getContextAttributes = () => ({})
isContextLost = () => false
getSupportedExtensions = () => []
readPixels = () => {}
renderbufferStorage = () => {}
framebufferTexture2D = () => {}
framebufferRenderbuffer = () => {}
checkFramebufferStatus = () => 36053 // GL_FRAMEBUFFER_COMPLETE

// Expo-specific extension
endFrameEXP = () => {}
}

//* GLView Mock Component ==============================

export function GLView({ onContextCreate, ref, ...props }: GLViewProps & any) {
React.useLayoutEffect(() => {
const gl = new WebGL2RenderingContext({ width: 1280, height: 800 } as HTMLCanvasElement)
gl.endFrameEXP = () => {}
const canvas = { width: 1280, height: 800 } as HTMLCanvasElement
const gl = new MockWebGL2RenderingContext(canvas)
onContextCreate(gl as any)
}, [onContextCreate])

return React.createElement('glview', props)
}

export { MockWebGL2RenderingContext as ExpoWebGLRenderingContext }
51 changes: 47 additions & 4 deletions packages/native/__mocks__/react-native.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
import * as React from 'react'
import { ViewProps, LayoutChangeEvent } from 'react-native'

export class View extends React.Component<Omit<ViewProps, 'children'> & { children: React.ReactNode }> {
//* Type Definitions ==============================

// Inline types to avoid importing from react-native (would cause circular dependency)
interface LayoutRectangle {
x: number
y: number
width: number
height: number
}

interface LayoutChangeEvent {
nativeEvent: {
layout: LayoutRectangle
}
}

interface ViewProps {
onLayout?: (event: LayoutChangeEvent) => void
style?: any
children?: React.ReactNode
[key: string]: any
}

//* Component Mocks ==============================

export class View extends React.Component<ViewProps> {
componentDidMount() {
this.props.onLayout?.({
nativeEvent: {
Expand All @@ -12,7 +36,7 @@ export class View extends React.Component<Omit<ViewProps, 'children'> & { childr
height: 800,
},
},
} as LayoutChangeEvent)
})
}

render() {
Expand All @@ -21,6 +45,8 @@ export class View extends React.Component<Omit<ViewProps, 'children'> & { childr
}
}

//* API Mocks ==============================

export const StyleSheet = {
absoluteFill: {
position: 'absolute',
Expand All @@ -29,6 +55,7 @@ export const StyleSheet = {
top: 0,
bottom: 0,
},
create: (styles: any) => styles,
}

export const PanResponder = {
Expand All @@ -43,12 +70,28 @@ export const Image = {

export const Platform = {
OS: 'web',
select: (options: any) => options.web ?? options.default,
}

export const NativeModules = {}
export const NativeModules = {
BlobModule: {
BLOB_URI_SCHEME: 'blob:',
},
}

export const PixelRatio = {
get() {
return 1
},
}

export const Dimensions = {
get() {
return { width: 1280, height: 800, scale: 1 }
},
}

export const AppState = {
currentState: 'active',
addEventListener: () => ({ remove: () => {} }),
}
4 changes: 3 additions & 1 deletion packages/native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
},
"devDependencies": {
"@react-three/fiber": "^9.4.0",
"@react-three/test-renderer": "^9.0.0",
"@testing-library/react": "^16.1.0",
"@types/node": "^22.10.2",
"@types/react": "^19.0.6",
Expand All @@ -102,6 +103,7 @@
"jsdom": "^25.0.1",
"react": "^19.0.0",
"react-native": "0.81.4",
"react-nil": "^2.0.0",
"rimraf": "^6.0.1",
"three": "^0.172.0",
"typescript": "^5.7.2",
Expand All @@ -111,4 +113,4 @@
"publishConfig": {
"access": "public"
}
}
}
14 changes: 13 additions & 1 deletion packages/native/src/polyfills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,19 @@ export function polyfills() {
.then(async (uri) => {
const base64 = await fs.readAsStringAsync(uri, { encoding: fs.EncodingType.Base64 })
const data = Buffer.from(base64, 'base64')
onLoad?.(data.buffer)

switch (this.responseType) {
case 'arrayBuffer':
return onLoad?.(data.buffer)
case 'blob':
// @ts-ignore
return onLoad?.(new Blob([data.buffer]))
// case 'document':
case 'json':
return onLoad?.(JSON.parse(THREE.LoaderUtils.decodeText(data)))
default:
return onLoad?.(THREE.LoaderUtils.decodeText(data))
}
})
.catch((error) => {
onError?.(error)
Expand Down
Loading
Loading