yaver-feedback-react-native 0.8.13 → 0.9.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.
- package/app.plugin.js +126 -9
- package/dist/BlackBox.js +9 -1
- package/dist/DeployPanel.js +65 -0
- package/dist/Discovery.js +13 -2
- package/dist/FeedbackModal.js +477 -66
- package/dist/MachinePickerScreen.js +14 -5
- package/dist/P2PClient.d.ts +94 -1
- package/dist/P2PClient.js +281 -2
- package/dist/ShakeDetector.js +2 -0
- package/dist/VibeChatScreen.d.ts +6 -1
- package/dist/VibeChatScreen.js +204 -1
- package/dist/YaverFeedback.d.ts +98 -0
- package/dist/YaverFeedback.js +509 -46
- package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
- package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
- package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
- package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
- package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
- package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
- package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
- package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
- package/dist/__tests__/P2PClient.test.js +2 -2
- package/dist/__tests__/ReportIdentity.test.d.ts +1 -0
- package/dist/__tests__/ReportIdentity.test.js +168 -0
- package/dist/__tests__/SDKToken.test.js +1 -1
- package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
- package/dist/__tests__/ShakeToggle.test.js +121 -0
- package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
- package/dist/__tests__/pickTargetDevice.test.js +89 -0
- package/dist/__tests__/reloadActions.test.d.ts +1 -0
- package/dist/__tests__/reloadActions.test.js +129 -0
- package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
- package/dist/__tests__/reloadActionsParity.test.js +42 -0
- package/dist/__tests__/types.test.js +5 -5
- package/dist/_core/device.d.ts +19 -9
- package/dist/_core/device.js +20 -14
- package/dist/capture.d.ts +6 -0
- package/dist/capture.js +53 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11 -1
- package/dist/reloadActions.d.ts +88 -0
- package/dist/reloadActions.js +200 -0
- package/dist/storeShots.d.ts +67 -0
- package/dist/storeShots.js +137 -0
- package/dist/types.d.ts +215 -3
- package/dist/voice.d.ts +61 -0
- package/dist/voice.js +246 -0
- package/package.json +14 -3
- package/src/BlackBox.ts +10 -1
- package/src/DeployPanel.tsx +74 -0
- package/src/Discovery.ts +13 -2
- package/src/FeedbackModal.tsx +563 -87
- package/src/MachinePickerScreen.tsx +12 -3
- package/src/P2PClient.ts +314 -2
- package/src/ShakeDetector.ts +1 -0
- package/src/VibeChatScreen.tsx +219 -0
- package/src/YaverFeedback.ts +498 -46
- package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
- package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
- package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
- package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
- package/src/__tests__/P2PClient.test.ts +2 -2
- package/src/__tests__/ReportIdentity.test.ts +203 -0
- package/src/__tests__/SDKToken.test.ts +1 -1
- package/src/__tests__/ShakeToggle.test.ts +153 -0
- package/src/__tests__/pickTargetDevice.test.ts +101 -0
- package/src/__tests__/reloadActions.test.ts +171 -0
- package/src/__tests__/reloadActionsParity.test.ts +49 -0
- package/src/__tests__/types.test.ts +5 -5
- package/src/_core/device.ts +20 -14
- package/src/capture.ts +51 -0
- package/src/index.ts +21 -0
- package/src/reloadActions.ts +273 -0
- package/src/storeShots.ts +189 -0
- package/src/types.ts +217 -3
- package/src/voice.ts +270 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// BlackBox relay-password sourcing.
|
|
2
|
+
//
|
|
3
|
+
// This exists because BlackBox.start() used to read the relay password off
|
|
4
|
+
// the feedback config:
|
|
5
|
+
//
|
|
6
|
+
// BlackBox.relayPassword =
|
|
7
|
+
// (feedbackConfig as { relayPassword?: string }).relayPassword ?? '';
|
|
8
|
+
//
|
|
9
|
+
// `relayPassword` is not a key on FeedbackConfig. The cast is what stopped
|
|
10
|
+
// the compiler from saying so, and the field was therefore permanently ''.
|
|
11
|
+
// Since every X-Relay-Password header is written behind an
|
|
12
|
+
// `if (BlackBox.relayPassword)` guard, the header was never sent, and the
|
|
13
|
+
// relay 401s an empty password (relay/server.go). Net effect: on cellular
|
|
14
|
+
// (the only time the relay is used at all) the SSE command channel died,
|
|
15
|
+
// so the reload that lands after an agent fix never fired. On-LAN the
|
|
16
|
+
// password is unused, so this was invisible in local dev — it only ever
|
|
17
|
+
// broke remote users.
|
|
18
|
+
//
|
|
19
|
+
// The contract pinned here: BlackBox takes its relay password from
|
|
20
|
+
// YaverFeedback.getRelayPassword(), the same source P2PClient uses.
|
|
21
|
+
|
|
22
|
+
import { BlackBox } from '../BlackBox';
|
|
23
|
+
import { YaverFeedback } from '../YaverFeedback';
|
|
24
|
+
|
|
25
|
+
jest.mock('react-native', () => ({
|
|
26
|
+
Platform: { OS: 'ios' },
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
jest.useFakeTimers();
|
|
30
|
+
|
|
31
|
+
const mockFetch = jest.fn();
|
|
32
|
+
global.fetch = mockFetch as any;
|
|
33
|
+
|
|
34
|
+
class MockAbortController {
|
|
35
|
+
signal = { aborted: false };
|
|
36
|
+
abort = jest.fn(() => {
|
|
37
|
+
this.signal.aborted = true;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
global.AbortController = MockAbortController as any;
|
|
41
|
+
|
|
42
|
+
function emptySse(): ReadableStream<Uint8Array> {
|
|
43
|
+
return new ReadableStream<Uint8Array>({
|
|
44
|
+
start(controller) {
|
|
45
|
+
controller.close();
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
jest.clearAllMocks();
|
|
52
|
+
mockFetch.mockImplementation((url: string) => {
|
|
53
|
+
if (String(url).includes('/blackbox/command-stream')) {
|
|
54
|
+
return Promise.resolve({ ok: true, body: emptySse() });
|
|
55
|
+
}
|
|
56
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
57
|
+
});
|
|
58
|
+
// A relay-routed agent URL — the shape discovery produces off-LAN.
|
|
59
|
+
YaverFeedback.init({
|
|
60
|
+
agentUrl: 'https://public.yaver.io/d/macmini-1',
|
|
61
|
+
authToken: 'tok',
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
BlackBox.stop();
|
|
67
|
+
jest.restoreAllMocks();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('BlackBox relay password', () => {
|
|
71
|
+
it('sends X-Relay-Password on the SSE command stream, sourced from getRelayPassword()', async () => {
|
|
72
|
+
jest.spyOn(YaverFeedback, 'getRelayPassword').mockReturnValue('s3cret-relay-pw');
|
|
73
|
+
|
|
74
|
+
BlackBox.start({ deviceId: 'dev-abc', appName: 'talos-mobile' });
|
|
75
|
+
await Promise.resolve();
|
|
76
|
+
await Promise.resolve();
|
|
77
|
+
|
|
78
|
+
const sseCall = mockFetch.mock.calls.find(([url]) =>
|
|
79
|
+
String(url).includes('/blackbox/command-stream'),
|
|
80
|
+
);
|
|
81
|
+
expect(sseCall).toBeDefined();
|
|
82
|
+
const [, init] = sseCall!;
|
|
83
|
+
expect(init.headers).toEqual(
|
|
84
|
+
expect.objectContaining({ 'X-Relay-Password': 's3cret-relay-pw' }),
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('sends X-Relay-Password on the event flush', async () => {
|
|
89
|
+
jest.spyOn(YaverFeedback, 'getRelayPassword').mockReturnValue('s3cret-relay-pw');
|
|
90
|
+
|
|
91
|
+
// maxBufferSize:1 makes push() flush synchronously on the first event,
|
|
92
|
+
// so this doesn't hang on interval semantics.
|
|
93
|
+
BlackBox.start({ deviceId: 'dev-abc', maxBufferSize: 1 });
|
|
94
|
+
BlackBox.log('hello');
|
|
95
|
+
await Promise.resolve();
|
|
96
|
+
|
|
97
|
+
const flush = mockFetch.mock.calls.find(([url]) =>
|
|
98
|
+
String(url).includes('/blackbox/events'),
|
|
99
|
+
);
|
|
100
|
+
expect(flush).toBeDefined();
|
|
101
|
+
const [, init] = flush!;
|
|
102
|
+
expect(init.headers).toEqual(
|
|
103
|
+
expect.objectContaining({ 'X-Relay-Password': 's3cret-relay-pw' }),
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('omits the header entirely when there is no relay password (direct LAN)', async () => {
|
|
108
|
+
// An empty header value is itself a 401 at the relay, so "no password"
|
|
109
|
+
// must mean "no header", not "empty header".
|
|
110
|
+
jest.spyOn(YaverFeedback, 'getRelayPassword').mockReturnValue('');
|
|
111
|
+
|
|
112
|
+
BlackBox.start({ deviceId: 'dev-abc' });
|
|
113
|
+
await Promise.resolve();
|
|
114
|
+
await Promise.resolve();
|
|
115
|
+
|
|
116
|
+
const sseCall = mockFetch.mock.calls.find(([url]) =>
|
|
117
|
+
String(url).includes('/blackbox/command-stream'),
|
|
118
|
+
);
|
|
119
|
+
expect(sseCall).toBeDefined();
|
|
120
|
+
const [, init] = sseCall!;
|
|
121
|
+
expect(init.headers).not.toHaveProperty('X-Relay-Password');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('does not read a relayPassword key off the feedback config', () => {
|
|
125
|
+
// The original bug in one assertion: FeedbackConfig has no such key, so
|
|
126
|
+
// anyone reintroducing that read gets '' and silently breaks the relay.
|
|
127
|
+
expect(YaverFeedback.getConfig()).not.toHaveProperty('relayPassword');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// BlackBox auto-start config passthrough.
|
|
2
|
+
//
|
|
3
|
+
// The natural way to configure the flight recorder — and the one the README's
|
|
4
|
+
// own snippet shows — is:
|
|
5
|
+
//
|
|
6
|
+
// YaverFeedback.init({ trigger: 'shake' });
|
|
7
|
+
// BlackBox.start({ appName: 'my-app' });
|
|
8
|
+
//
|
|
9
|
+
// That start() early-returns: a zero-config init has no agentUrl yet, because
|
|
10
|
+
// discovery resolves it asynchronously. The SDK's autoStartBlackBox then fires
|
|
11
|
+
// ~500ms later once the agent and token exist — and used to call start() with
|
|
12
|
+
// no arguments, quietly replacing the host's config with defaults and leaving
|
|
13
|
+
// appName as ''. Hosts had no way to tell: the recorder streamed, just
|
|
14
|
+
// anonymously.
|
|
15
|
+
//
|
|
16
|
+
// config.blackBox is now the supported channel, and auto-start honours it.
|
|
17
|
+
|
|
18
|
+
import { BlackBox } from '../BlackBox';
|
|
19
|
+
import { YaverFeedback } from '../YaverFeedback';
|
|
20
|
+
|
|
21
|
+
jest.mock('react-native', () => ({
|
|
22
|
+
Platform: { OS: 'ios' },
|
|
23
|
+
DeviceEventEmitter: { emit: jest.fn(), addListener: jest.fn() },
|
|
24
|
+
NativeModules: {},
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
jest.mock('../ShakeDetector', () => ({
|
|
28
|
+
ShakeDetector: jest.fn().mockImplementation(() => ({
|
|
29
|
+
start: jest.fn(),
|
|
30
|
+
stop: jest.fn(),
|
|
31
|
+
})),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
jest.useFakeTimers();
|
|
35
|
+
|
|
36
|
+
const mockFetch = jest.fn(() =>
|
|
37
|
+
Promise.resolve({ ok: true, json: () => Promise.resolve({}), body: null }),
|
|
38
|
+
);
|
|
39
|
+
global.fetch = mockFetch as any;
|
|
40
|
+
|
|
41
|
+
class MockAbortController {
|
|
42
|
+
signal = { aborted: false };
|
|
43
|
+
abort = jest.fn();
|
|
44
|
+
}
|
|
45
|
+
global.AbortController = MockAbortController as any;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
jest.clearAllMocks();
|
|
49
|
+
YaverFeedback.destroy();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
BlackBox.stop();
|
|
54
|
+
YaverFeedback.destroy();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('autoStartBlackBox config passthrough', () => {
|
|
58
|
+
it('starts BlackBox with the config passed on init', async () => {
|
|
59
|
+
const startSpy = jest.spyOn(BlackBox, 'start').mockImplementation(() => {});
|
|
60
|
+
|
|
61
|
+
YaverFeedback.init({
|
|
62
|
+
trigger: 'shake',
|
|
63
|
+
agentUrl: 'http://localhost:18080',
|
|
64
|
+
authToken: 'tok',
|
|
65
|
+
blackBox: { appName: 'talos-mobile', flushInterval: 3000, maxBufferSize: 25 },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await jest.advanceTimersByTimeAsync(600);
|
|
69
|
+
|
|
70
|
+
expect(startSpy).toHaveBeenCalledWith(
|
|
71
|
+
expect.objectContaining({
|
|
72
|
+
appName: 'talos-mobile',
|
|
73
|
+
flushInterval: 3000,
|
|
74
|
+
maxBufferSize: 25,
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
startSpy.mockRestore();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('still auto-starts when no blackBox config is given', async () => {
|
|
81
|
+
const startSpy = jest.spyOn(BlackBox, 'start').mockImplementation(() => {});
|
|
82
|
+
|
|
83
|
+
YaverFeedback.init({
|
|
84
|
+
trigger: 'shake',
|
|
85
|
+
agentUrl: 'http://localhost:18080',
|
|
86
|
+
authToken: 'tok',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await jest.advanceTimersByTimeAsync(600);
|
|
90
|
+
|
|
91
|
+
expect(startSpy).toHaveBeenCalled();
|
|
92
|
+
startSpy.mockRestore();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('does not auto-start when autoStartBlackBox is false', async () => {
|
|
96
|
+
const startSpy = jest.spyOn(BlackBox, 'start').mockImplementation(() => {});
|
|
97
|
+
|
|
98
|
+
YaverFeedback.init({
|
|
99
|
+
trigger: 'shake',
|
|
100
|
+
agentUrl: 'http://localhost:18080',
|
|
101
|
+
authToken: 'tok',
|
|
102
|
+
autoStartBlackBox: false,
|
|
103
|
+
blackBox: { appName: 'talos-mobile' },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await jest.advanceTimersByTimeAsync(600);
|
|
107
|
+
|
|
108
|
+
expect(startSpy).not.toHaveBeenCalled();
|
|
109
|
+
startSpy.mockRestore();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// BlackBox auto-start on a COLD start — the case that decides whether Hermes
|
|
2
|
+
// hot reload works at all.
|
|
3
|
+
//
|
|
4
|
+
// The command channel that carries the agent's `reload_bundle` is BlackBox's
|
|
5
|
+
// SSE stream. If BlackBox never starts, a fix pushed from the dev machine has
|
|
6
|
+
// nowhere to land and the phone just sits there.
|
|
7
|
+
//
|
|
8
|
+
// Auto-start used to be a single 500ms timeout gated on `agentUrl && token`.
|
|
9
|
+
// A real app passes NEITHER to init() — they're restored from storage — so on
|
|
10
|
+
// a cold start the sequence is:
|
|
11
|
+
//
|
|
12
|
+
// init() -> void hydrateSession()
|
|
13
|
+
// -> AsyncStorage read (token)
|
|
14
|
+
// -> AsyncStorage read (deviceId)
|
|
15
|
+
// -> await discoverAgent() <- Convex round trip + LAN probe
|
|
16
|
+
//
|
|
17
|
+
// which does not complete in 500ms. The one-shot check found no agentUrl, gave
|
|
18
|
+
// up, and hot reload was silently dead until the user shook the device and
|
|
19
|
+
// drove discovery by hand. These tests pin the retry that fixes it, and the
|
|
20
|
+
// bounds that keep it from becoming a forever-poll or a 401 storm.
|
|
21
|
+
|
|
22
|
+
import { BlackBox } from '../BlackBox';
|
|
23
|
+
import { YaverFeedback } from '../YaverFeedback';
|
|
24
|
+
|
|
25
|
+
jest.mock('react-native', () => ({
|
|
26
|
+
Platform: { OS: 'ios' },
|
|
27
|
+
DeviceEventEmitter: { emit: jest.fn(), addListener: jest.fn() },
|
|
28
|
+
NativeModules: {},
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.mock('../ShakeDetector', () => ({
|
|
32
|
+
ShakeDetector: jest.fn().mockImplementation(() => ({
|
|
33
|
+
start: jest.fn(),
|
|
34
|
+
stop: jest.fn(),
|
|
35
|
+
})),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
// Discovery is the slow step we're modelling. Resolves only after the test
|
|
39
|
+
// flips `discovered`, standing in for "the Convex round trip finally landed".
|
|
40
|
+
let discovered: string | null = null;
|
|
41
|
+
jest.mock('../Discovery', () => ({
|
|
42
|
+
YaverDiscovery: {
|
|
43
|
+
discover: jest.fn(() =>
|
|
44
|
+
Promise.resolve(discovered ? { url: discovered } : null),
|
|
45
|
+
),
|
|
46
|
+
},
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
// The token cache hydrateSession() reads.
|
|
50
|
+
let storedToken: string | null = null;
|
|
51
|
+
jest.mock('../auth', () => ({
|
|
52
|
+
...jest.requireActual('../auth'),
|
|
53
|
+
getToken: jest.fn(() => Promise.resolve(storedToken)),
|
|
54
|
+
getSelectedDeviceId: jest.fn(() => Promise.resolve(null)),
|
|
55
|
+
configureAuthEndpoints: jest.fn(),
|
|
56
|
+
setStrictNativeAuth: jest.fn(),
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
jest.useFakeTimers();
|
|
60
|
+
|
|
61
|
+
const mockFetch = jest.fn(() =>
|
|
62
|
+
Promise.resolve({ ok: true, json: () => Promise.resolve({}), body: null }),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
class MockAbortController {
|
|
66
|
+
signal = { aborted: false };
|
|
67
|
+
abort = jest.fn();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// fetch and AbortController are process-globals, not per-file module state:
|
|
71
|
+
// jest gives each test FILE its own module registry but they share one worker
|
|
72
|
+
// process. Assigning them at module scope (as the sibling auto-start suite
|
|
73
|
+
// does) leaves them mocked for every file that runs afterwards in the same
|
|
74
|
+
// worker. Save and restore instead, so this suite can't be the reason some
|
|
75
|
+
// unrelated file sees a stubbed fetch.
|
|
76
|
+
const realFetch = global.fetch;
|
|
77
|
+
const realAbortController = global.AbortController;
|
|
78
|
+
|
|
79
|
+
beforeAll(() => {
|
|
80
|
+
global.fetch = mockFetch as any;
|
|
81
|
+
global.AbortController = MockAbortController as any;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterAll(() => {
|
|
85
|
+
global.fetch = realFetch;
|
|
86
|
+
global.AbortController = realAbortController;
|
|
87
|
+
// Leave no timer chain behind for the next file in this worker.
|
|
88
|
+
YaverFeedback.destroy();
|
|
89
|
+
jest.useRealTimers();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
beforeEach(() => {
|
|
93
|
+
jest.clearAllMocks();
|
|
94
|
+
discovered = null;
|
|
95
|
+
storedToken = null;
|
|
96
|
+
YaverFeedback.destroy();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
afterEach(() => {
|
|
100
|
+
BlackBox.stop();
|
|
101
|
+
YaverFeedback.destroy();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('BlackBox auto-start on a cold start', () => {
|
|
105
|
+
it('starts once discovery resolves well after the old 500ms window', async () => {
|
|
106
|
+
const startSpy = jest.spyOn(BlackBox, 'start').mockImplementation(() => {});
|
|
107
|
+
storedToken = 'tok-from-storage';
|
|
108
|
+
|
|
109
|
+
// Nothing passed in — the real cold-start shape.
|
|
110
|
+
YaverFeedback.init({ trigger: 'shake', blackBox: { appName: 'talos-mobile' } });
|
|
111
|
+
|
|
112
|
+
// The window the old implementation checked, and only that window.
|
|
113
|
+
await jest.advanceTimersByTimeAsync(600);
|
|
114
|
+
expect(startSpy).not.toHaveBeenCalled();
|
|
115
|
+
|
|
116
|
+
// Discovery lands late, as it does on a real cold, off-LAN start.
|
|
117
|
+
discovered = 'http://10.0.0.5:18080';
|
|
118
|
+
await jest.advanceTimersByTimeAsync(4000);
|
|
119
|
+
|
|
120
|
+
expect(startSpy).toHaveBeenCalledWith(
|
|
121
|
+
expect.objectContaining({ appName: 'talos-mobile' }),
|
|
122
|
+
);
|
|
123
|
+
startSpy.mockRestore();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('never starts without a token, however long it waits', async () => {
|
|
127
|
+
// The guard that matters: starting tokenless makes the SSE channel 401 and
|
|
128
|
+
// retry with backoff — the string-concat + JSON-parse loop that SIGSEGV'd
|
|
129
|
+
// Hermes on iOS 18.3.1. Retrying must not erode it.
|
|
130
|
+
const startSpy = jest.spyOn(BlackBox, 'start').mockImplementation(() => {});
|
|
131
|
+
storedToken = null;
|
|
132
|
+
discovered = 'http://10.0.0.5:18080';
|
|
133
|
+
|
|
134
|
+
YaverFeedback.init({ trigger: 'shake' });
|
|
135
|
+
await jest.advanceTimersByTimeAsync(90_000);
|
|
136
|
+
|
|
137
|
+
expect(startSpy).not.toHaveBeenCalled();
|
|
138
|
+
startSpy.mockRestore();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('gives up rather than polling forever', async () => {
|
|
142
|
+
const startSpy = jest.spyOn(BlackBox, 'start').mockImplementation(() => {});
|
|
143
|
+
storedToken = null;
|
|
144
|
+
|
|
145
|
+
YaverFeedback.init({ trigger: 'shake' });
|
|
146
|
+
// Past the ~60s bound.
|
|
147
|
+
await jest.advanceTimersByTimeAsync(90_000);
|
|
148
|
+
|
|
149
|
+
// Credentials arrive after the window closed — the poll is done.
|
|
150
|
+
storedToken = 'late';
|
|
151
|
+
discovered = 'http://10.0.0.5:18080';
|
|
152
|
+
await jest.advanceTimersByTimeAsync(10_000);
|
|
153
|
+
|
|
154
|
+
expect(startSpy).not.toHaveBeenCalled();
|
|
155
|
+
startSpy.mockRestore();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('signing in re-arms after the bound has passed', async () => {
|
|
159
|
+
// Giving up is only acceptable because this path exists: a user who signs
|
|
160
|
+
// in later gets the channel without restarting the app.
|
|
161
|
+
const startSpy = jest.spyOn(BlackBox, 'start').mockImplementation(() => {});
|
|
162
|
+
|
|
163
|
+
YaverFeedback.init({ trigger: 'shake' });
|
|
164
|
+
await jest.advanceTimersByTimeAsync(90_000);
|
|
165
|
+
expect(startSpy).not.toHaveBeenCalled();
|
|
166
|
+
|
|
167
|
+
discovered = 'http://10.0.0.5:18080';
|
|
168
|
+
await YaverFeedback.setAuthToken('tok-from-login');
|
|
169
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
170
|
+
|
|
171
|
+
expect(startSpy).toHaveBeenCalled();
|
|
172
|
+
startSpy.mockRestore();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('destroy() cancels a pending retry', async () => {
|
|
176
|
+
// A retry chain firing against a torn-down config would start the recorder
|
|
177
|
+
// for an SDK the host believes is off.
|
|
178
|
+
const startSpy = jest.spyOn(BlackBox, 'start').mockImplementation(() => {});
|
|
179
|
+
storedToken = 'tok';
|
|
180
|
+
|
|
181
|
+
YaverFeedback.init({ trigger: 'shake' });
|
|
182
|
+
await jest.advanceTimersByTimeAsync(600);
|
|
183
|
+
|
|
184
|
+
YaverFeedback.destroy();
|
|
185
|
+
discovered = 'http://10.0.0.5:18080';
|
|
186
|
+
await jest.advanceTimersByTimeAsync(10_000);
|
|
187
|
+
|
|
188
|
+
expect(startSpy).not.toHaveBeenCalled();
|
|
189
|
+
startSpy.mockRestore();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-lane DOM icon (feedback-sdk-lanes audit 2026-07-28). RN-web inside
|
|
3
|
+
* Yaver's fullScreen preview WebView: native shake can't fire and the container
|
|
4
|
+
* overlay is occluded, so the SDK mounts an occlusion-proof DOM icon. Node env,
|
|
5
|
+
* so we stub Platform.OS='web' + a minimal DOM.
|
|
6
|
+
*/
|
|
7
|
+
jest.mock('react-native', () => ({
|
|
8
|
+
Platform: { OS: 'web' },
|
|
9
|
+
NativeModules: {},
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
// Minimal DOM stub — enough for mountBrowserLaneIcon's createElement/appendChild.
|
|
13
|
+
function installFakeDom() {
|
|
14
|
+
const els: Record<string, any> = {};
|
|
15
|
+
const makeEl = () => ({
|
|
16
|
+
id: '',
|
|
17
|
+
textContent: '',
|
|
18
|
+
title: '',
|
|
19
|
+
style: {} as Record<string, string>,
|
|
20
|
+
_handlers: {} as Record<string, unknown>,
|
|
21
|
+
setAttribute() {},
|
|
22
|
+
setPointerCapture() {},
|
|
23
|
+
releasePointerCapture() {},
|
|
24
|
+
getBoundingClientRect: () => ({ left: 0, top: 0, width: 44, height: 44 }),
|
|
25
|
+
addEventListener(k: string, fn: unknown) { this._handlers[k] = fn; },
|
|
26
|
+
});
|
|
27
|
+
(globalThis as any).window = {
|
|
28
|
+
innerWidth: 390, innerHeight: 844,
|
|
29
|
+
addEventListener() {},
|
|
30
|
+
__yaverLane: 'browser',
|
|
31
|
+
};
|
|
32
|
+
(globalThis as any).localStorage = { getItem: () => null, setItem() {} };
|
|
33
|
+
(globalThis as any).document = {
|
|
34
|
+
body: { appendChild(el: any) { els[el.id] = el; } },
|
|
35
|
+
getElementById: (id: string) => els[id] || null,
|
|
36
|
+
createElement: () => makeEl(),
|
|
37
|
+
};
|
|
38
|
+
return { els };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('RN SDK browser-lane icon', () => {
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
delete (globalThis as any).window;
|
|
44
|
+
delete (globalThis as any).document;
|
|
45
|
+
delete (globalThis as any).localStorage;
|
|
46
|
+
jest.resetModules();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('detectWebLane reads window.__yaverLane on web', () => {
|
|
50
|
+
installFakeDom();
|
|
51
|
+
const { YaverFeedback } = require('../YaverFeedback');
|
|
52
|
+
expect(YaverFeedback.detectWebLane()).toBe('browser');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('mounts a fixed, max-z-index, draggable DOM icon in the browser lane', () => {
|
|
56
|
+
const { els } = installFakeDom();
|
|
57
|
+
const { YaverFeedback } = require('../YaverFeedback');
|
|
58
|
+
YaverFeedback.mountBrowserLaneIcon();
|
|
59
|
+
const btn = els['yaver-feedback-btn'];
|
|
60
|
+
expect(btn).toBeTruthy();
|
|
61
|
+
expect(btn.textContent).toBe('Y');
|
|
62
|
+
expect(btn.style.cssText).toContain('position:fixed');
|
|
63
|
+
expect(btn.style.cssText).toContain('z-index:99999');
|
|
64
|
+
// Draggable: pointer handlers wired.
|
|
65
|
+
expect(typeof btn._handlers['pointerdown']).toBe('function');
|
|
66
|
+
expect(typeof btn._handlers['pointerup']).toBe('function');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('is idempotent — a second mount does not stack a second icon', () => {
|
|
70
|
+
const { els } = installFakeDom();
|
|
71
|
+
const { YaverFeedback } = require('../YaverFeedback');
|
|
72
|
+
YaverFeedback.mountBrowserLaneIcon();
|
|
73
|
+
const first = els['yaver-feedback-btn'];
|
|
74
|
+
YaverFeedback.mountBrowserLaneIcon();
|
|
75
|
+
expect(els['yaver-feedback-btn']).toBe(first);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('no-ops when not in a browser lane', () => {
|
|
79
|
+
const { els } = installFakeDom();
|
|
80
|
+
(globalThis as any).window.__yaverLane = undefined;
|
|
81
|
+
const { YaverFeedback } = require('../YaverFeedback');
|
|
82
|
+
YaverFeedback.mountBrowserLaneIcon();
|
|
83
|
+
expect(els['yaver-feedback-btn']).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -159,7 +159,7 @@ describe('P2PClient', () => {
|
|
|
159
159
|
const bundle = {
|
|
160
160
|
metadata: {
|
|
161
161
|
timestamp: '2026-03-24T00:00:00Z',
|
|
162
|
-
|
|
162
|
+
deviceInfo: {
|
|
163
163
|
platform: 'ios',
|
|
164
164
|
osVersion: '18.0',
|
|
165
165
|
model: 'iPhone 16',
|
|
@@ -191,7 +191,7 @@ describe('P2PClient', () => {
|
|
|
191
191
|
const bundle = {
|
|
192
192
|
metadata: {
|
|
193
193
|
timestamp: '2026-03-24T00:00:00Z',
|
|
194
|
-
|
|
194
|
+
deviceInfo: { platform: 'ios', osVersion: '18', model: 'iPhone', screenWidth: 393, screenHeight: 852 },
|
|
195
195
|
app: {},
|
|
196
196
|
},
|
|
197
197
|
screenshots: [],
|