yaver-feedback-react-native 0.5.3 → 0.5.4

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.
Files changed (50) hide show
  1. package/README.md +8 -0
  2. package/dist/AuthOverlay.d.ts +16 -0
  3. package/dist/AuthOverlay.js +104 -0
  4. package/dist/BlackBox.d.ts +154 -0
  5. package/dist/BlackBox.js +395 -0
  6. package/dist/ConnectionScreen.d.ts +13 -0
  7. package/dist/ConnectionScreen.js +373 -0
  8. package/dist/Discovery.d.ts +59 -0
  9. package/dist/Discovery.js +293 -0
  10. package/dist/FeedbackModal.d.ts +11 -0
  11. package/dist/FeedbackModal.js +623 -0
  12. package/dist/FixReport.d.ts +23 -0
  13. package/dist/FixReport.js +282 -0
  14. package/dist/FloatingButton.d.ts +71 -0
  15. package/dist/FloatingButton.js +778 -0
  16. package/dist/LoginScreen.d.ts +14 -0
  17. package/dist/LoginScreen.js +317 -0
  18. package/dist/MachinePickerScreen.d.ts +19 -0
  19. package/dist/MachinePickerScreen.js +175 -0
  20. package/dist/P2PClient.d.ts +136 -0
  21. package/dist/P2PClient.js +357 -0
  22. package/dist/ShakeDetector.d.ts +39 -0
  23. package/dist/ShakeDetector.js +111 -0
  24. package/dist/YaverFeedback.d.ts +198 -0
  25. package/dist/YaverFeedback.js +679 -0
  26. package/dist/YaverUpdates.d.ts +78 -0
  27. package/dist/YaverUpdates.js +272 -0
  28. package/dist/__tests__/Discovery.test.d.ts +1 -0
  29. package/dist/__tests__/Discovery.test.js +164 -0
  30. package/dist/__tests__/P2PClient.test.d.ts +1 -0
  31. package/dist/__tests__/P2PClient.test.js +169 -0
  32. package/dist/__tests__/SDKToken.test.d.ts +1 -0
  33. package/dist/__tests__/SDKToken.test.js +215 -0
  34. package/dist/__tests__/YaverFeedback.test.d.ts +1 -0
  35. package/dist/__tests__/YaverFeedback.test.js +161 -0
  36. package/dist/__tests__/types.test.d.ts +1 -0
  37. package/dist/__tests__/types.test.js +219 -0
  38. package/dist/auth.d.ts +105 -0
  39. package/dist/auth.js +282 -0
  40. package/dist/capture.d.ts +27 -0
  41. package/dist/capture.js +74 -0
  42. package/dist/expo.d.ts +15 -0
  43. package/dist/expo.js +62 -0
  44. package/dist/index.d.ts +48 -0
  45. package/dist/index.js +80 -0
  46. package/dist/types.d.ts +282 -0
  47. package/dist/types.js +2 -0
  48. package/dist/upload.d.ts +13 -0
  49. package/dist/upload.js +59 -0
  50. package/package.json +6 -3
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,215 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const P2PClient_1 = require("../P2PClient");
4
+ // Mock react-native Platform
5
+ jest.mock('react-native', () => ({
6
+ Platform: { OS: 'ios' },
7
+ }));
8
+ // Mock fetch globally
9
+ const mockFetch = jest.fn();
10
+ global.fetch = mockFetch;
11
+ // Mock AbortController
12
+ class MockAbortController {
13
+ constructor() {
14
+ this.signal = {};
15
+ this.abort = jest.fn();
16
+ }
17
+ }
18
+ global.AbortController = MockAbortController;
19
+ beforeEach(() => {
20
+ jest.clearAllMocks();
21
+ mockFetch.mockReset();
22
+ });
23
+ describe('SDK Token Auth', () => {
24
+ describe('Bearer token in requests', () => {
25
+ it('sends SDK token in Authorization header for feedback', async () => {
26
+ mockFetch.mockResolvedValue({
27
+ ok: true,
28
+ json: () => Promise.resolve({ id: 'report-1' }),
29
+ text: () => Promise.resolve(''),
30
+ });
31
+ const client = new P2PClient_1.P2PClient('http://192.168.1.10:18080', 'sdk-token-abc123');
32
+ const bundle = {
33
+ metadata: {
34
+ timestamp: '2026-03-25T00:00:00Z',
35
+ device: { platform: 'ios', osVersion: '18', model: 'iPhone', screenWidth: 393, screenHeight: 852 },
36
+ app: {},
37
+ },
38
+ screenshots: [],
39
+ };
40
+ await client.uploadFeedback(bundle);
41
+ expect(mockFetch).toHaveBeenCalledWith('http://192.168.1.10:18080/feedback', expect.objectContaining({
42
+ headers: expect.objectContaining({
43
+ Authorization: 'Bearer sdk-token-abc123',
44
+ }),
45
+ }));
46
+ });
47
+ it('sends SDK token in Authorization header for voice', async () => {
48
+ mockFetch.mockResolvedValue({
49
+ ok: true,
50
+ json: () => Promise.resolve({ voiceInputEnabled: true }),
51
+ text: () => Promise.resolve(''),
52
+ });
53
+ const client = new P2PClient_1.P2PClient('http://localhost:18080', 'my-sdk-token');
54
+ await client.voiceStatus();
55
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:18080/voice/status', expect.objectContaining({
56
+ headers: expect.objectContaining({
57
+ Authorization: 'Bearer my-sdk-token',
58
+ }),
59
+ }));
60
+ });
61
+ it('sends SDK token for test session', async () => {
62
+ mockFetch.mockResolvedValue({
63
+ ok: true,
64
+ json: () => Promise.resolve({ sessionId: 'sess-1' }),
65
+ text: () => Promise.resolve(''),
66
+ });
67
+ const client = new P2PClient_1.P2PClient('http://localhost:18080', 'test-sdk-token');
68
+ await client.startTestSession();
69
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:18080/test-app/start', expect.objectContaining({
70
+ headers: expect.objectContaining({
71
+ Authorization: 'Bearer test-sdk-token',
72
+ }),
73
+ }));
74
+ });
75
+ });
76
+ describe('setAuthToken()', () => {
77
+ it('updates token for subsequent requests', async () => {
78
+ mockFetch.mockResolvedValue({
79
+ ok: true,
80
+ json: () => Promise.resolve({ hostname: 'test' }),
81
+ text: () => Promise.resolve(''),
82
+ });
83
+ const client = new P2PClient_1.P2PClient('http://localhost:18080', 'old-token');
84
+ client.setAuthToken('new-sdk-token');
85
+ await client.info();
86
+ expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
87
+ headers: expect.objectContaining({
88
+ Authorization: 'Bearer new-sdk-token',
89
+ }),
90
+ }));
91
+ });
92
+ });
93
+ describe('Error handling for SDK token auth failures', () => {
94
+ it('throws on 403 Forbidden (scope violation)', async () => {
95
+ mockFetch.mockResolvedValue({
96
+ ok: false,
97
+ status: 403,
98
+ text: () => Promise.resolve('SDK token scope does not allow this endpoint'),
99
+ });
100
+ const client = new P2PClient_1.P2PClient('http://localhost:18080', 'limited-sdk');
101
+ await expect(client.info()).rejects.toThrow('403');
102
+ });
103
+ it('throws on 403 Forbidden (IP binding)', async () => {
104
+ mockFetch.mockResolvedValue({
105
+ ok: false,
106
+ status: 403,
107
+ text: () => Promise.resolve('SDK token not allowed from this IP'),
108
+ });
109
+ const client = new P2PClient_1.P2PClient('http://localhost:18080', 'ip-bound-sdk');
110
+ await expect(client.info()).rejects.toThrow('403');
111
+ });
112
+ it('throws on 401 Unauthorized (expired token)', async () => {
113
+ mockFetch.mockResolvedValue({
114
+ ok: false,
115
+ status: 401,
116
+ text: () => Promise.resolve('Invalid or expired SDK token'),
117
+ });
118
+ const client = new P2PClient_1.P2PClient('http://localhost:18080', 'expired-sdk');
119
+ await expect(client.info()).rejects.toThrow('401');
120
+ });
121
+ });
122
+ describe('Token rotation', () => {
123
+ it('rotateToken() calls POST /sdk/token/rotate', async () => {
124
+ mockFetch.mockResolvedValue({
125
+ ok: true,
126
+ json: () => Promise.resolve({
127
+ token: 'new-rotated-token',
128
+ expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000,
129
+ }),
130
+ text: () => Promise.resolve(''),
131
+ });
132
+ const client = new P2PClient_1.P2PClient('http://localhost:18080', 'current-sdk-token');
133
+ const result = await client.rotateToken();
134
+ expect(result.token).toBe('new-rotated-token');
135
+ expect(result.expiresAt).toBeGreaterThan(Date.now());
136
+ // Verify the request was made correctly
137
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:18080/sdk/token/rotate', expect.objectContaining({
138
+ method: 'POST',
139
+ headers: expect.objectContaining({
140
+ Authorization: 'Bearer current-sdk-token',
141
+ }),
142
+ }));
143
+ });
144
+ it('rotateToken() auto-updates internal token', async () => {
145
+ mockFetch.mockResolvedValueOnce({
146
+ ok: true,
147
+ json: () => Promise.resolve({
148
+ token: 'rotated-token-xyz',
149
+ expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000,
150
+ }),
151
+ text: () => Promise.resolve(''),
152
+ });
153
+ const client = new P2PClient_1.P2PClient('http://localhost:18080', 'old-token');
154
+ await client.rotateToken();
155
+ // Now make another request — should use the new token
156
+ mockFetch.mockResolvedValueOnce({
157
+ ok: true,
158
+ json: () => Promise.resolve({ hostname: 'test' }),
159
+ text: () => Promise.resolve(''),
160
+ });
161
+ await client.info();
162
+ // Second call should use the rotated token
163
+ expect(mockFetch).toHaveBeenLastCalledWith(expect.any(String), expect.objectContaining({
164
+ headers: expect.objectContaining({
165
+ Authorization: 'Bearer rotated-token-xyz',
166
+ }),
167
+ }));
168
+ });
169
+ it('rotateToken() throws on failure', async () => {
170
+ mockFetch.mockResolvedValue({
171
+ ok: false,
172
+ status: 401,
173
+ text: () => Promise.resolve('Token already rotated'),
174
+ });
175
+ const client = new P2PClient_1.P2PClient('http://localhost:18080', 'already-rotated');
176
+ await expect(client.rotateToken()).rejects.toThrow('Token rotation failed');
177
+ });
178
+ });
179
+ describe('Health endpoint with TLS info', () => {
180
+ it('health response includes TLS fingerprint', async () => {
181
+ mockFetch.mockResolvedValue({
182
+ ok: true,
183
+ json: () => Promise.resolve({
184
+ ok: true,
185
+ hostname: 'MacBook',
186
+ version: '1.45.0',
187
+ tlsFingerprint: 'abc123def456789',
188
+ tlsPort: 18443,
189
+ }),
190
+ text: () => Promise.resolve(''),
191
+ });
192
+ const client = new P2PClient_1.P2PClient('http://localhost:18080', 'tok');
193
+ const info = await client.info();
194
+ expect(info.hostname).toBe('MacBook');
195
+ expect(info.version).toBe('1.45.0');
196
+ });
197
+ });
198
+ describe('Multiple SDK token scenarios', () => {
199
+ it('works with long token strings', async () => {
200
+ const longToken = 'a'.repeat(64); // 64-char hex token
201
+ mockFetch.mockResolvedValue({ ok: true });
202
+ const client = new P2PClient_1.P2PClient('http://localhost:18080', longToken);
203
+ const healthy = await client.health();
204
+ expect(healthy).toBe(true);
205
+ });
206
+ it('health check does not send auth header', async () => {
207
+ mockFetch.mockResolvedValue({ ok: true });
208
+ const client = new P2PClient_1.P2PClient('http://localhost:18080', 'secret-sdk');
209
+ await client.health();
210
+ // health() should NOT send Authorization header
211
+ const call = mockFetch.mock.calls[0];
212
+ expect(call[1].headers).toBeUndefined();
213
+ });
214
+ });
215
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const YaverFeedback_1 = require("../YaverFeedback");
4
+ // Mock react-native: DeviceEventEmitter for event dispatch + Platform so
5
+ // ShakeDetector.start() can branch on iOS without hitting a real RN runtime.
6
+ jest.mock('react-native', () => ({
7
+ DeviceEventEmitter: {
8
+ emit: jest.fn(),
9
+ addListener: jest.fn(() => ({ remove: jest.fn() })),
10
+ },
11
+ Platform: { OS: 'ios' },
12
+ }));
13
+ // Mock Discovery
14
+ jest.mock('../Discovery', () => ({
15
+ YaverDiscovery: {
16
+ discover: jest.fn(),
17
+ },
18
+ }));
19
+ // Reset module-level state between tests by re-requiring
20
+ beforeEach(() => {
21
+ // YaverFeedback uses module-level variables (config, enabled, p2pClient).
22
+ // We reset them by calling init with a known state or relying on isInitialized checks.
23
+ // For a clean slate, we re-init with enabled=false then verify.
24
+ jest.clearAllMocks();
25
+ });
26
+ describe('YaverFeedback', () => {
27
+ describe('init()', () => {
28
+ it('sets config correctly with defaults', () => {
29
+ YaverFeedback_1.YaverFeedback.init({
30
+ authToken: 'test-token',
31
+ agentUrl: 'http://localhost:18080',
32
+ });
33
+ const cfg = YaverFeedback_1.YaverFeedback.getConfig();
34
+ expect(cfg).not.toBeNull();
35
+ expect(cfg.authToken).toBe('test-token');
36
+ expect(cfg.agentUrl).toBe('http://localhost:18080');
37
+ expect(cfg.trigger).toBe('shake');
38
+ expect(cfg.maxRecordingDuration).toBe(120);
39
+ expect(cfg.feedbackMode).toBe('batch');
40
+ expect(cfg.agentCommentaryLevel).toBe(0);
41
+ });
42
+ it('respects user-provided values over defaults', () => {
43
+ YaverFeedback_1.YaverFeedback.init({
44
+ authToken: 'tok',
45
+ trigger: 'floating-button',
46
+ maxRecordingDuration: 60,
47
+ feedbackMode: 'live',
48
+ agentCommentaryLevel: 7,
49
+ });
50
+ const cfg = YaverFeedback_1.YaverFeedback.getConfig();
51
+ expect(cfg.trigger).toBe('floating-button');
52
+ expect(cfg.maxRecordingDuration).toBe(60);
53
+ expect(cfg.feedbackMode).toBe('live');
54
+ expect(cfg.agentCommentaryLevel).toBe(7);
55
+ });
56
+ it('with enabled=false sets enabled to false', () => {
57
+ YaverFeedback_1.YaverFeedback.init({
58
+ authToken: 'tok',
59
+ enabled: false,
60
+ });
61
+ expect(YaverFeedback_1.YaverFeedback.isInitialized()).toBe(true);
62
+ expect(YaverFeedback_1.YaverFeedback.isEnabled()).toBe(false);
63
+ });
64
+ it('with enabled=true sets enabled to true', () => {
65
+ YaverFeedback_1.YaverFeedback.init({
66
+ authToken: 'tok',
67
+ enabled: true,
68
+ });
69
+ expect(YaverFeedback_1.YaverFeedback.isEnabled()).toBe(true);
70
+ });
71
+ it('creates P2PClient when agentUrl is provided', () => {
72
+ YaverFeedback_1.YaverFeedback.init({
73
+ authToken: 'tok',
74
+ agentUrl: 'http://192.168.1.10:18080',
75
+ });
76
+ expect(YaverFeedback_1.YaverFeedback.getP2PClient()).not.toBeNull();
77
+ });
78
+ it('does not create P2PClient when agentUrl is omitted', () => {
79
+ YaverFeedback_1.YaverFeedback.init({
80
+ authToken: 'tok',
81
+ });
82
+ expect(YaverFeedback_1.YaverFeedback.getP2PClient()).toBeNull();
83
+ });
84
+ });
85
+ describe('isInitialized()', () => {
86
+ it('returns true after init()', () => {
87
+ YaverFeedback_1.YaverFeedback.init({ authToken: 'tok' });
88
+ expect(YaverFeedback_1.YaverFeedback.isInitialized()).toBe(true);
89
+ });
90
+ });
91
+ describe('setEnabled()', () => {
92
+ it('toggles enabled state', () => {
93
+ YaverFeedback_1.YaverFeedback.init({ authToken: 'tok', enabled: true });
94
+ expect(YaverFeedback_1.YaverFeedback.isEnabled()).toBe(true);
95
+ YaverFeedback_1.YaverFeedback.setEnabled(false);
96
+ expect(YaverFeedback_1.YaverFeedback.isEnabled()).toBe(false);
97
+ YaverFeedback_1.YaverFeedback.setEnabled(true);
98
+ expect(YaverFeedback_1.YaverFeedback.isEnabled()).toBe(true);
99
+ });
100
+ });
101
+ describe('getConfig()', () => {
102
+ it('returns the config after init', () => {
103
+ YaverFeedback_1.YaverFeedback.init({
104
+ authToken: 'my-token',
105
+ agentUrl: 'http://10.0.0.1:18080',
106
+ });
107
+ const cfg = YaverFeedback_1.YaverFeedback.getConfig();
108
+ expect(cfg).toBeDefined();
109
+ expect(cfg.authToken).toBe('my-token');
110
+ expect(cfg.agentUrl).toBe('http://10.0.0.1:18080');
111
+ });
112
+ });
113
+ describe('getFeedbackMode()', () => {
114
+ it('defaults to batch when no config', () => {
115
+ // After any init, feedbackMode defaults to 'batch'
116
+ YaverFeedback_1.YaverFeedback.init({ authToken: 'tok' });
117
+ expect(YaverFeedback_1.YaverFeedback.getFeedbackMode()).toBe('batch');
118
+ });
119
+ it('returns configured mode', () => {
120
+ YaverFeedback_1.YaverFeedback.init({ authToken: 'tok', feedbackMode: 'narrated' });
121
+ expect(YaverFeedback_1.YaverFeedback.getFeedbackMode()).toBe('narrated');
122
+ });
123
+ it('returns live when configured', () => {
124
+ YaverFeedback_1.YaverFeedback.init({ authToken: 'tok', feedbackMode: 'live' });
125
+ expect(YaverFeedback_1.YaverFeedback.getFeedbackMode()).toBe('live');
126
+ });
127
+ });
128
+ describe('getCommentaryLevel()', () => {
129
+ it('defaults to 0', () => {
130
+ YaverFeedback_1.YaverFeedback.init({ authToken: 'tok' });
131
+ expect(YaverFeedback_1.YaverFeedback.getCommentaryLevel()).toBe(0);
132
+ });
133
+ it('returns configured level', () => {
134
+ YaverFeedback_1.YaverFeedback.init({ authToken: 'tok', agentCommentaryLevel: 5 });
135
+ expect(YaverFeedback_1.YaverFeedback.getCommentaryLevel()).toBe(5);
136
+ });
137
+ it('returns max level when set to 10', () => {
138
+ YaverFeedback_1.YaverFeedback.init({ authToken: 'tok', agentCommentaryLevel: 10 });
139
+ expect(YaverFeedback_1.YaverFeedback.getCommentaryLevel()).toBe(10);
140
+ });
141
+ });
142
+ describe('startReport()', () => {
143
+ it('does nothing when not enabled', async () => {
144
+ YaverFeedback_1.YaverFeedback.init({ authToken: 'tok', enabled: false });
145
+ // Should not throw
146
+ await YaverFeedback_1.YaverFeedback.startReport();
147
+ const { DeviceEventEmitter } = require('react-native');
148
+ expect(DeviceEventEmitter.emit).not.toHaveBeenCalled();
149
+ });
150
+ it('emits event when enabled and agentUrl is set', async () => {
151
+ YaverFeedback_1.YaverFeedback.init({
152
+ authToken: 'tok',
153
+ enabled: true,
154
+ agentUrl: 'http://localhost:18080',
155
+ });
156
+ await YaverFeedback_1.YaverFeedback.startReport();
157
+ const { DeviceEventEmitter } = require('react-native');
158
+ expect(DeviceEventEmitter.emit).toHaveBeenCalledWith('yaverFeedback:startReport');
159
+ });
160
+ });
161
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ describe('React Native SDK types', () => {
4
+ describe('FeedbackConfig', () => {
5
+ it('can be constructed with only required fields', () => {
6
+ const config = {
7
+ authToken: 'test-token',
8
+ };
9
+ expect(config.authToken).toBe('test-token');
10
+ expect(config.agentUrl).toBeUndefined();
11
+ expect(config.trigger).toBeUndefined();
12
+ expect(config.enabled).toBeUndefined();
13
+ expect(config.maxRecordingDuration).toBeUndefined();
14
+ expect(config.feedbackMode).toBeUndefined();
15
+ expect(config.agentCommentaryLevel).toBeUndefined();
16
+ });
17
+ it('can be constructed with all optional fields', () => {
18
+ const config = {
19
+ authToken: 'tok',
20
+ agentUrl: 'http://192.168.1.10:18080',
21
+ trigger: 'shake',
22
+ enabled: true,
23
+ maxRecordingDuration: 60,
24
+ feedbackMode: 'live',
25
+ agentCommentaryLevel: 7,
26
+ };
27
+ expect(config.trigger).toBe('shake');
28
+ expect(config.feedbackMode).toBe('live');
29
+ expect(config.agentCommentaryLevel).toBe(7);
30
+ });
31
+ it('accepts all trigger types', () => {
32
+ const triggers = ['shake', 'floating-button', 'manual'];
33
+ triggers.forEach((trigger) => {
34
+ const config = { authToken: 'tok', trigger };
35
+ expect(config.trigger).toBe(trigger);
36
+ });
37
+ });
38
+ it('accepts all feedback modes', () => {
39
+ const modes = ['live', 'narrated', 'batch'];
40
+ modes.forEach((mode) => {
41
+ const config = { authToken: 'tok', feedbackMode: mode };
42
+ expect(config.feedbackMode).toBe(mode);
43
+ });
44
+ });
45
+ });
46
+ describe('FeedbackBundle', () => {
47
+ it('can be constructed with required fields', () => {
48
+ const bundle = {
49
+ metadata: {
50
+ timestamp: '2026-03-24T12:00:00Z',
51
+ device: {
52
+ platform: 'ios',
53
+ osVersion: '18.0',
54
+ model: 'iPhone 16 Pro',
55
+ screenWidth: 393,
56
+ screenHeight: 852,
57
+ },
58
+ app: {
59
+ bundleId: 'com.example.app',
60
+ version: '1.0.0',
61
+ buildNumber: '42',
62
+ },
63
+ },
64
+ screenshots: [],
65
+ };
66
+ expect(bundle.metadata.timestamp).toBe('2026-03-24T12:00:00Z');
67
+ expect(bundle.metadata.device.platform).toBe('ios');
68
+ expect(bundle.screenshots).toEqual([]);
69
+ expect(bundle.video).toBeUndefined();
70
+ expect(bundle.audio).toBeUndefined();
71
+ });
72
+ it('can include optional video, audio, and screenshots', () => {
73
+ const bundle = {
74
+ metadata: {
75
+ timestamp: '2026-03-24T12:00:00Z',
76
+ device: {
77
+ platform: 'android',
78
+ osVersion: '15',
79
+ model: 'Pixel 9',
80
+ screenWidth: 412,
81
+ screenHeight: 915,
82
+ },
83
+ app: {},
84
+ userNote: 'This button does not work',
85
+ },
86
+ video: '/tmp/recording.mp4',
87
+ audio: '/tmp/voice.m4a',
88
+ screenshots: ['/tmp/ss1.png', '/tmp/ss2.png'],
89
+ };
90
+ expect(bundle.video).toBe('/tmp/recording.mp4');
91
+ expect(bundle.audio).toBe('/tmp/voice.m4a');
92
+ expect(bundle.screenshots).toHaveLength(2);
93
+ expect(bundle.metadata.userNote).toBe('This button does not work');
94
+ });
95
+ });
96
+ describe('TimelineEvent', () => {
97
+ it('has correct structure for screenshot event', () => {
98
+ const event = {
99
+ type: 'screenshot',
100
+ path: '/tmp/screenshot.png',
101
+ timestamp: '2026-03-24T12:00:05Z',
102
+ };
103
+ expect(event.type).toBe('screenshot');
104
+ expect(event.path).toBe('/tmp/screenshot.png');
105
+ expect(event.duration).toBeUndefined();
106
+ });
107
+ it('supports optional duration for audio/video events', () => {
108
+ const event = {
109
+ type: 'audio',
110
+ path: '/tmp/voice.m4a',
111
+ timestamp: '2026-03-24T12:00:10Z',
112
+ duration: 15.5,
113
+ };
114
+ expect(event.duration).toBe(15.5);
115
+ });
116
+ it('accepts all event types', () => {
117
+ const types = ['screenshot', 'audio', 'video'];
118
+ types.forEach((type) => {
119
+ const event = { type, path: '/tmp/file', timestamp: 'now' };
120
+ expect(event.type).toBe(type);
121
+ });
122
+ });
123
+ });
124
+ describe('DeviceInfo', () => {
125
+ it('has all required fields', () => {
126
+ const device = {
127
+ platform: 'ios',
128
+ osVersion: '18.0',
129
+ model: 'iPhone 16 Pro Max',
130
+ screenWidth: 430,
131
+ screenHeight: 932,
132
+ };
133
+ expect(device.platform).toBe('ios');
134
+ expect(device.screenWidth).toBe(430);
135
+ expect(device.screenHeight).toBe(932);
136
+ });
137
+ });
138
+ describe('AppInfo', () => {
139
+ it('can be empty (all fields optional)', () => {
140
+ const app = {};
141
+ expect(app.bundleId).toBeUndefined();
142
+ expect(app.version).toBeUndefined();
143
+ expect(app.buildNumber).toBeUndefined();
144
+ });
145
+ it('can have all fields populated', () => {
146
+ const app = {
147
+ bundleId: 'com.yaver.test',
148
+ version: '2.0.0',
149
+ buildNumber: '100',
150
+ };
151
+ expect(app.bundleId).toBe('com.yaver.test');
152
+ });
153
+ });
154
+ describe('FeedbackReport', () => {
155
+ it('has correct structure', () => {
156
+ const report = {
157
+ id: 'report-123',
158
+ bundle: {
159
+ metadata: {
160
+ timestamp: 'now',
161
+ device: { platform: 'ios', osVersion: '18', model: 'iPhone', screenWidth: 393, screenHeight: 852 },
162
+ app: {},
163
+ },
164
+ screenshots: [],
165
+ },
166
+ status: 'pending',
167
+ };
168
+ expect(report.status).toBe('pending');
169
+ expect(report.error).toBeUndefined();
170
+ });
171
+ it('accepts all status values', () => {
172
+ const statuses = ['pending', 'uploading', 'uploaded', 'failed'];
173
+ statuses.forEach((status) => {
174
+ const report = {
175
+ id: '1',
176
+ bundle: {
177
+ metadata: {
178
+ timestamp: 'now',
179
+ device: { platform: 'ios', osVersion: '18', model: 'iPhone', screenWidth: 393, screenHeight: 852 },
180
+ app: {},
181
+ },
182
+ screenshots: [],
183
+ },
184
+ status,
185
+ };
186
+ expect(report.status).toBe(status);
187
+ });
188
+ });
189
+ });
190
+ describe('AgentCommentary', () => {
191
+ it('has correct structure', () => {
192
+ const commentary = {
193
+ id: 'cmt-1',
194
+ timestamp: '2026-03-24T12:00:00Z',
195
+ message: 'I see a layout issue on the login screen',
196
+ type: 'observation',
197
+ };
198
+ expect(commentary.type).toBe('observation');
199
+ });
200
+ it('accepts all commentary types', () => {
201
+ const types = ['observation', 'suggestion', 'question', 'action'];
202
+ types.forEach((type) => {
203
+ const c = { id: '1', timestamp: 'now', message: 'test', type };
204
+ expect(c.type).toBe(type);
205
+ });
206
+ });
207
+ });
208
+ describe('FeedbackStreamEvent', () => {
209
+ it('has correct structure', () => {
210
+ const event = {
211
+ type: 'tap',
212
+ timestamp: '2026-03-24T12:00:00Z',
213
+ data: { x: 100, y: 200 },
214
+ };
215
+ expect(event.type).toBe('tap');
216
+ expect(event.data.x).toBe(100);
217
+ });
218
+ });
219
+ });