onejs-react 0.1.0 → 0.1.2

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.
@@ -148,6 +148,15 @@ export class MockVisualElement {
148
148
  }
149
149
  }
150
150
 
151
+ /**
152
+ * Mock TextElement - base text element for implicit text content
153
+ */
154
+ export class MockTextElement extends MockVisualElement {
155
+ constructor() {
156
+ super('UnityEngine.UIElements.TextElement');
157
+ }
158
+ }
159
+
151
160
  /**
152
161
  * Mock Label element
153
162
  */
@@ -225,6 +234,7 @@ export function createMockCS() {
225
234
  // UI Elements
226
235
  UIElements: {
227
236
  VisualElement: MockVisualElement,
237
+ TextElement: MockTextElement,
228
238
  Label: MockLabel,
229
239
  Button: MockButton,
230
240
  TextField: MockTextField,
@@ -279,7 +289,13 @@ export async function flushMicrotasks(): Promise<void> {
279
289
  for (let i = 0; i < 50; i++) {
280
290
  await Promise.resolve();
281
291
  // Also allow any setTimeout callbacks to run
282
- await new Promise(resolve => setImmediate ? setImmediate(resolve) : setTimeout(resolve, 0));
292
+ await new Promise<void>(resolve => {
293
+ if (typeof setImmediate !== "undefined") {
294
+ setImmediate(resolve);
295
+ } else {
296
+ setTimeout(resolve, 0);
297
+ }
298
+ });
283
299
  }
284
300
  }
285
301
 
@@ -8,23 +8,35 @@
8
8
  * - console: QuickJS built-in
9
9
  */
10
10
 
11
- import { vi } from 'vitest';
12
- import { createMockCS, resetAllMocks } from './mocks';
11
+ import { vi, beforeEach, afterEach } from "vitest";
12
+ import { createMockCS, resetAllMocks } from "./mocks";
13
+
14
+ // Extend globalThis type for our mocks
15
+ declare global {
16
+ // eslint-disable-next-line no-var
17
+ var CS: ReturnType<typeof createMockCS>;
18
+ // eslint-disable-next-line no-var
19
+ var __eventAPI: {
20
+ addEventListener: ReturnType<typeof vi.fn>;
21
+ removeEventListener: ReturnType<typeof vi.fn>;
22
+ removeAllEventListeners: ReturnType<typeof vi.fn>;
23
+ };
24
+ }
13
25
 
14
26
  // Store original globals (Node.js provides these)
15
- const originalSetTimeout = globalThis.setTimeout;
16
- const originalClearTimeout = globalThis.clearTimeout;
17
- const originalQueueMicrotask = globalThis.queueMicrotask;
27
+ const originalSetTimeout = global.setTimeout;
28
+ const originalClearTimeout = global.clearTimeout;
29
+ const originalQueueMicrotask = global.queueMicrotask;
18
30
 
19
31
  // Set up globals before each test
20
32
  beforeEach(() => {
21
33
  resetAllMocks();
22
34
 
23
35
  // Create fresh mock CS global
24
- (globalThis as any).CS = createMockCS();
36
+ global.CS = createMockCS();
25
37
 
26
38
  // Mock event API with spies
27
- (globalThis as any).__eventAPI = {
39
+ global.__eventAPI = {
28
40
  addEventListener: vi.fn(),
29
41
  removeEventListener: vi.fn(),
30
42
  removeAllEventListeners: vi.fn(),
@@ -32,18 +44,18 @@ beforeEach(() => {
32
44
 
33
45
  // Use real console but spy on it for test assertions
34
46
  // (React reconciler logs things we want to see during debugging)
35
- (globalThis as any).console = {
47
+ (global as any).console = {
36
48
  log: vi.fn(),
37
49
  error: vi.fn(),
38
50
  warn: vi.fn(),
39
51
  };
40
52
 
41
53
  // Use real queueMicrotask - React scheduler depends on it
42
- (globalThis as any).queueMicrotask = originalQueueMicrotask || ((cb: () => void) => Promise.resolve().then(cb));
54
+ (global as any).queueMicrotask = originalQueueMicrotask || ((cb: () => void) => Promise.resolve().then(cb));
43
55
 
44
56
  // Use real setTimeout/clearTimeout - React scheduler depends on them
45
- (globalThis as any).setTimeout = originalSetTimeout;
46
- (globalThis as any).clearTimeout = originalClearTimeout;
57
+ (global as any).setTimeout = originalSetTimeout;
58
+ (global as any).clearTimeout = originalClearTimeout;
47
59
  });
48
60
 
49
61
  // Clean up after each test
@@ -1,6 +1,7 @@
1
- import type { ReactElement } from 'react';
1
+ import { forwardRef, type ReactElement, type Ref } from 'react';
2
2
  import type {
3
3
  ViewProps,
4
+ TextProps,
4
5
  LabelProps,
5
6
  ButtonProps,
6
7
  TextFieldProps,
@@ -8,24 +9,37 @@ import type {
8
9
  SliderProps,
9
10
  ScrollViewProps,
10
11
  ImageProps,
11
- ListViewProps
12
+ ListViewProps,
13
+ VisualElement,
14
+ TextElement,
15
+ LabelElement,
16
+ ButtonElement,
17
+ TextFieldElement,
18
+ ToggleElement,
19
+ SliderElement,
20
+ ScrollViewElement,
21
+ ImageElement,
12
22
  } from './types';
13
23
 
24
+ // Props with ref support for intrinsic elements
25
+ type WithRef<Props, Element> = Props & { ref?: Ref<Element> };
26
+
14
27
  // Declare the intrinsic element types for JSX
15
28
  // Using 'ojs-' prefix to avoid conflicts with HTML/SVG element names in @types/react
16
29
  // For React 19 with jsx: "react-jsx", we need to augment 'react/jsx-runtime'
17
30
  declare module 'react/jsx-runtime' {
18
31
  namespace JSX {
19
32
  interface IntrinsicElements {
20
- 'ojs-view': ViewProps;
21
- 'ojs-label': LabelProps;
22
- 'ojs-button': ButtonProps;
23
- 'ojs-textfield': TextFieldProps;
24
- 'ojs-toggle': ToggleProps;
25
- 'ojs-slider': SliderProps;
26
- 'ojs-scrollview': ScrollViewProps;
27
- 'ojs-image': ImageProps;
28
- 'ojs-listview': ListViewProps;
33
+ 'ojs-view': WithRef<ViewProps, VisualElement>;
34
+ 'ojs-text': WithRef<TextProps, TextElement>;
35
+ 'ojs-label': WithRef<LabelProps, LabelElement>;
36
+ 'ojs-button': WithRef<ButtonProps, ButtonElement>;
37
+ 'ojs-textfield': WithRef<TextFieldProps, TextFieldElement>;
38
+ 'ojs-toggle': WithRef<ToggleProps, ToggleElement>;
39
+ 'ojs-slider': WithRef<SliderProps, SliderElement>;
40
+ 'ojs-scrollview': WithRef<ScrollViewProps, ScrollViewElement>;
41
+ 'ojs-image': WithRef<ImageProps, ImageElement>;
42
+ 'ojs-listview': WithRef<ListViewProps, VisualElement>;
29
43
  }
30
44
  }
31
45
  }
@@ -34,54 +48,69 @@ declare module 'react/jsx-runtime' {
34
48
  declare module 'react' {
35
49
  namespace JSX {
36
50
  interface IntrinsicElements {
37
- 'ojs-view': ViewProps;
38
- 'ojs-label': LabelProps;
39
- 'ojs-button': ButtonProps;
40
- 'ojs-textfield': TextFieldProps;
41
- 'ojs-toggle': ToggleProps;
42
- 'ojs-slider': SliderProps;
43
- 'ojs-scrollview': ScrollViewProps;
44
- 'ojs-image': ImageProps;
45
- 'ojs-listview': ListViewProps;
51
+ 'ojs-view': WithRef<ViewProps, VisualElement>;
52
+ 'ojs-text': WithRef<TextProps, TextElement>;
53
+ 'ojs-label': WithRef<LabelProps, LabelElement>;
54
+ 'ojs-button': WithRef<ButtonProps, ButtonElement>;
55
+ 'ojs-textfield': WithRef<TextFieldProps, TextFieldElement>;
56
+ 'ojs-toggle': WithRef<ToggleProps, ToggleElement>;
57
+ 'ojs-slider': WithRef<SliderProps, SliderElement>;
58
+ 'ojs-scrollview': WithRef<ScrollViewProps, ScrollViewElement>;
59
+ 'ojs-image': WithRef<ImageProps, ImageElement>;
60
+ 'ojs-listview': WithRef<ListViewProps, VisualElement>;
46
61
  }
47
62
  }
48
63
  }
49
64
 
50
65
  // Component wrappers that provide nice capitalized names
51
- // These return JSX elements with 'ojs-' prefixed type strings
66
+ // These use forwardRef to pass refs through to the intrinsic elements
52
67
 
53
- export function View(props: ViewProps): ReactElement {
54
- return <ojs-view {...props} />;
55
- }
68
+ export const View = forwardRef<VisualElement, ViewProps>((props, ref) => {
69
+ return <ojs-view ref={ref} {...props} />;
70
+ });
71
+ View.displayName = 'View';
56
72
 
57
- export function Label(props: LabelProps): ReactElement {
58
- return <ojs-label {...props} />;
59
- }
73
+ export const Text = forwardRef<TextElement, TextProps>((props, ref) => {
74
+ return <ojs-text ref={ref} {...props} />;
75
+ });
76
+ Text.displayName = 'Text';
60
77
 
61
- export function Button(props: ButtonProps): ReactElement {
62
- return <ojs-button {...props} />;
63
- }
78
+ export const Label = forwardRef<LabelElement, LabelProps>((props, ref) => {
79
+ return <ojs-label ref={ref} {...props} />;
80
+ });
81
+ Label.displayName = 'Label';
64
82
 
65
- export function TextField(props: TextFieldProps): ReactElement {
66
- return <ojs-textfield {...props} />;
67
- }
83
+ export const Button = forwardRef<ButtonElement, ButtonProps>((props, ref) => {
84
+ return <ojs-button ref={ref} {...props} />;
85
+ });
86
+ Button.displayName = 'Button';
68
87
 
69
- export function Toggle(props: ToggleProps): ReactElement {
70
- return <ojs-toggle {...props} />;
71
- }
88
+ export const TextField = forwardRef<TextFieldElement, TextFieldProps>((props, ref) => {
89
+ return <ojs-textfield ref={ref} {...props} />;
90
+ });
91
+ TextField.displayName = 'TextField';
72
92
 
73
- export function Slider(props: SliderProps): ReactElement {
74
- return <ojs-slider {...props} />;
75
- }
93
+ export const Toggle = forwardRef<ToggleElement, ToggleProps>((props, ref) => {
94
+ return <ojs-toggle ref={ref} {...props} />;
95
+ });
96
+ Toggle.displayName = 'Toggle';
76
97
 
77
- export function ScrollView(props: ScrollViewProps): ReactElement {
78
- return <ojs-scrollview {...props} />;
79
- }
98
+ export const Slider = forwardRef<SliderElement, SliderProps>((props, ref) => {
99
+ return <ojs-slider ref={ref} {...props} />;
100
+ });
101
+ Slider.displayName = 'Slider';
80
102
 
81
- export function Image(props: ImageProps): ReactElement {
82
- return <ojs-image {...props} />;
83
- }
103
+ export const ScrollView = forwardRef<ScrollViewElement, ScrollViewProps>((props, ref) => {
104
+ return <ojs-scrollview ref={ref} {...props} />;
105
+ });
106
+ ScrollView.displayName = 'ScrollView';
84
107
 
85
- export function ListView(props: ListViewProps): ReactElement {
86
- return <ojs-listview {...props} />;
87
- }
108
+ export const Image = forwardRef<ImageElement, ImageProps>((props, ref) => {
109
+ return <ojs-image ref={ref} {...props} />;
110
+ });
111
+ Image.displayName = 'Image';
112
+
113
+ export const ListView = forwardRef<VisualElement, ListViewProps>((props, ref) => {
114
+ return <ojs-listview ref={ref} {...props} />;
115
+ });
116
+ ListView.displayName = 'ListView';
@@ -0,0 +1,175 @@
1
+ import { Component, type ReactNode, type ErrorInfo } from "react"
2
+ import { View, Label } from "./components"
3
+
4
+ declare const console: { log: (...args: unknown[]) => void; error: (...args: unknown[]) => void }
5
+
6
+ export interface ErrorBoundaryProps {
7
+ children: ReactNode
8
+ /**
9
+ * Optional fallback to render when an error occurs.
10
+ * Can be a ReactNode or a function that receives error details.
11
+ */
12
+ fallback?: ReactNode | ((error: Error, errorInfo: ErrorInfo) => ReactNode)
13
+ /**
14
+ * Optional callback when an error is caught.
15
+ * Use this for logging or error reporting.
16
+ */
17
+ onError?: (error: Error, errorInfo: ErrorInfo) => void
18
+ /**
19
+ * Optional callback when the boundary resets.
20
+ */
21
+ onReset?: () => void
22
+ }
23
+
24
+ interface ErrorBoundaryState {
25
+ hasError: boolean
26
+ error: Error | null
27
+ errorInfo: ErrorInfo | null
28
+ }
29
+
30
+ /**
31
+ * Error boundary component for catching and displaying React errors.
32
+ *
33
+ * @example Basic usage with default fallback
34
+ * ```tsx
35
+ * <ErrorBoundary>
36
+ * <MyComponent />
37
+ * </ErrorBoundary>
38
+ * ```
39
+ *
40
+ * @example Custom fallback UI
41
+ * ```tsx
42
+ * <ErrorBoundary fallback={<Label>Something went wrong</Label>}>
43
+ * <MyComponent />
44
+ * </ErrorBoundary>
45
+ * ```
46
+ *
47
+ * @example Fallback function with error details
48
+ * ```tsx
49
+ * <ErrorBoundary
50
+ * fallback={(error, info) => (
51
+ * <View>
52
+ * <Label>Error: {error.message}</Label>
53
+ * <Label>Stack: {info.componentStack}</Label>
54
+ * </View>
55
+ * )}
56
+ * >
57
+ * <MyComponent />
58
+ * </ErrorBoundary>
59
+ * ```
60
+ *
61
+ * @example Error logging
62
+ * ```tsx
63
+ * <ErrorBoundary onError={(error, info) => logError(error, info)}>
64
+ * <MyComponent />
65
+ * </ErrorBoundary>
66
+ * ```
67
+ */
68
+ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
69
+ constructor(props: ErrorBoundaryProps) {
70
+ super(props)
71
+ this.state = {
72
+ hasError: false,
73
+ error: null,
74
+ errorInfo: null,
75
+ }
76
+ }
77
+
78
+ static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
79
+ return { hasError: true, error }
80
+ }
81
+
82
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
83
+ this.setState({ errorInfo })
84
+
85
+ // Log to console with helpful formatting
86
+ console.error("[OneJS React] Error caught by ErrorBoundary:")
87
+ console.error(" Error:", error.message)
88
+ if (error.stack) {
89
+ console.error(" Stack:", error.stack)
90
+ }
91
+ if (errorInfo.componentStack) {
92
+ console.error(" Component Stack:", errorInfo.componentStack)
93
+ }
94
+
95
+ // Call user-provided error handler
96
+ this.props.onError?.(error, errorInfo)
97
+ }
98
+
99
+ /**
100
+ * Reset the error boundary to try rendering children again.
101
+ * Useful after the underlying issue has been fixed.
102
+ */
103
+ reset = (): void => {
104
+ this.setState({
105
+ hasError: false,
106
+ error: null,
107
+ errorInfo: null,
108
+ })
109
+ this.props.onReset?.()
110
+ }
111
+
112
+ render(): ReactNode {
113
+ if (this.state.hasError) {
114
+ const { fallback } = this.props
115
+ const { error, errorInfo } = this.state
116
+
117
+ // Render custom fallback if provided
118
+ if (fallback !== undefined) {
119
+ if (typeof fallback === "function") {
120
+ return fallback(error!, errorInfo!)
121
+ }
122
+ return fallback
123
+ }
124
+
125
+ // Default fallback UI
126
+ return (
127
+ <View style={{
128
+ padding: 16,
129
+ backgroundColor: "#2d1b1b",
130
+ borderColor: "#ff4444",
131
+ borderWidth: 2,
132
+ }}>
133
+ <Label style={{
134
+ color: "#ff6666",
135
+ fontSize: 16,
136
+ marginBottom: 8,
137
+ }}>
138
+ Something went wrong
139
+ </Label>
140
+ <Label style={{
141
+ color: "#ffaaaa",
142
+ fontSize: 12,
143
+ }}>
144
+ {error?.message || "Unknown error"}
145
+ </Label>
146
+ </View>
147
+ )
148
+ }
149
+
150
+ return this.props.children
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Helper hook-style error info for functional components.
156
+ * Note: This is a simple utility. For actual error catching,
157
+ * you must use the ErrorBoundary class component.
158
+ */
159
+ export function formatError(error: Error, componentStack?: string): string {
160
+ const parts = [`Error: ${error.message}`]
161
+
162
+ if (error.stack) {
163
+ // Get first few lines of stack
164
+ const stackLines = error.stack.split("\n").slice(0, 5)
165
+ parts.push(`Stack:\n${stackLines.join("\n")}`)
166
+ }
167
+
168
+ if (componentStack) {
169
+ // Get first few lines of component stack
170
+ const compLines = componentStack.split("\n").slice(0, 5)
171
+ parts.push(`Component:\n${compLines.join("\n")}`)
172
+ }
173
+
174
+ return parts.join("\n\n")
175
+ }