@juzi/wechaty 1.0.146 → 1.0.148
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/dist/cjs/src/mods/impls.d.ts +2 -2
- package/dist/cjs/src/mods/impls.d.ts.map +1 -1
- package/dist/cjs/src/mods/impls.js +2 -1
- package/dist/cjs/src/mods/impls.js.map +1 -1
- package/dist/cjs/src/mods/users.d.ts +2 -1
- package/dist/cjs/src/mods/users.d.ts.map +1 -1
- package/dist/cjs/src/package-json.js +4 -4
- package/dist/cjs/src/package-json.js.map +1 -1
- package/dist/cjs/src/schemas/call-events.d.ts +20 -0
- package/dist/cjs/src/schemas/call-events.d.ts.map +1 -0
- package/dist/cjs/src/schemas/call-events.js +7 -0
- package/dist/cjs/src/schemas/call-events.js.map +1 -0
- package/dist/cjs/src/schemas/mod.d.ts +3 -2
- package/dist/cjs/src/schemas/mod.d.ts.map +1 -1
- package/dist/cjs/src/schemas/mod.js +3 -1
- package/dist/cjs/src/schemas/mod.js.map +1 -1
- package/dist/cjs/src/schemas/wechaty-events.d.ts +23 -2
- package/dist/cjs/src/schemas/wechaty-events.d.ts.map +1 -1
- package/dist/cjs/src/schemas/wechaty-events.js +6 -0
- package/dist/cjs/src/schemas/wechaty-events.js.map +1 -1
- package/dist/cjs/src/user-modules/call.d.ts +158 -0
- package/dist/cjs/src/user-modules/call.d.ts.map +1 -1
- package/dist/cjs/src/user-modules/call.js +289 -1
- package/dist/cjs/src/user-modules/call.js.map +1 -1
- package/dist/cjs/src/user-modules/call.spec.d.ts +7 -0
- package/dist/cjs/src/user-modules/call.spec.d.ts.map +1 -0
- package/dist/cjs/src/user-modules/call.spec.js +759 -0
- package/dist/cjs/src/user-modules/call.spec.js.map +1 -0
- package/dist/cjs/src/user-modules/contact.d.ts +16 -0
- package/dist/cjs/src/user-modules/contact.d.ts.map +1 -1
- package/dist/cjs/src/user-modules/contact.js +16 -0
- package/dist/cjs/src/user-modules/contact.js.map +1 -1
- package/dist/cjs/src/user-modules/mod.d.ts +5 -4
- package/dist/cjs/src/user-modules/mod.d.ts.map +1 -1
- package/dist/cjs/src/user-modules/mod.js +2 -1
- package/dist/cjs/src/user-modules/mod.js.map +1 -1
- package/dist/cjs/src/wechaty/wechaty-base.d.ts +66 -7
- package/dist/cjs/src/wechaty/wechaty-base.d.ts.map +1 -1
- package/dist/cjs/src/wechaty/wechaty-base.js +28 -0
- package/dist/cjs/src/wechaty/wechaty-base.js.map +1 -1
- package/dist/cjs/src/wechaty/wechaty-impl.spec.js +10 -8
- package/dist/cjs/src/wechaty/wechaty-impl.spec.js.map +1 -1
- package/dist/cjs/src/wechaty-mixins/gerror-mixin.d.ts +1 -1
- package/dist/cjs/src/wechaty-mixins/io-mixin.d.ts +2 -2
- package/dist/cjs/src/wechaty-mixins/login-mixin.d.ts +15 -5
- package/dist/cjs/src/wechaty-mixins/login-mixin.d.ts.map +1 -1
- package/dist/cjs/src/wechaty-mixins/misc-mixin.d.ts +15 -5
- package/dist/cjs/src/wechaty-mixins/misc-mixin.d.ts.map +1 -1
- package/dist/cjs/src/wechaty-mixins/plugin-mixin.d.ts +32 -6
- package/dist/cjs/src/wechaty-mixins/plugin-mixin.d.ts.map +1 -1
- package/dist/cjs/src/wechaty-mixins/puppet-mixin.d.ts +39 -5
- package/dist/cjs/src/wechaty-mixins/puppet-mixin.d.ts.map +1 -1
- package/dist/cjs/src/wechaty-mixins/puppet-mixin.js +149 -0
- package/dist/cjs/src/wechaty-mixins/puppet-mixin.js.map +1 -1
- package/dist/cjs/src/wechaty-mixins/wechatify-user-module-mixin.d.ts +5 -3
- package/dist/cjs/src/wechaty-mixins/wechatify-user-module-mixin.d.ts.map +1 -1
- package/dist/cjs/src/wechaty-mixins/wechatify-user-module-mixin.js +3 -0
- package/dist/cjs/src/wechaty-mixins/wechatify-user-module-mixin.js.map +1 -1
- package/dist/esm/src/mods/impls.d.ts +2 -2
- package/dist/esm/src/mods/impls.d.ts.map +1 -1
- package/dist/esm/src/mods/impls.js +1 -1
- package/dist/esm/src/mods/impls.js.map +1 -1
- package/dist/esm/src/mods/users.d.ts +2 -1
- package/dist/esm/src/mods/users.d.ts.map +1 -1
- package/dist/esm/src/package-json.js +4 -4
- package/dist/esm/src/package-json.js.map +1 -1
- package/dist/esm/src/schemas/call-events.d.ts +20 -0
- package/dist/esm/src/schemas/call-events.d.ts.map +1 -0
- package/dist/esm/src/schemas/call-events.js +4 -0
- package/dist/esm/src/schemas/call-events.js.map +1 -0
- package/dist/esm/src/schemas/mod.d.ts +3 -2
- package/dist/esm/src/schemas/mod.d.ts.map +1 -1
- package/dist/esm/src/schemas/mod.js +2 -1
- package/dist/esm/src/schemas/mod.js.map +1 -1
- package/dist/esm/src/schemas/wechaty-events.d.ts +23 -2
- package/dist/esm/src/schemas/wechaty-events.d.ts.map +1 -1
- package/dist/esm/src/schemas/wechaty-events.js +6 -0
- package/dist/esm/src/schemas/wechaty-events.js.map +1 -1
- package/dist/esm/src/user-modules/call.d.ts +158 -0
- package/dist/esm/src/user-modules/call.d.ts.map +1 -1
- package/dist/esm/src/user-modules/call.js +289 -1
- package/dist/esm/src/user-modules/call.js.map +1 -1
- package/dist/esm/src/user-modules/call.spec.d.ts +7 -0
- package/dist/esm/src/user-modules/call.spec.d.ts.map +1 -0
- package/dist/esm/src/user-modules/call.spec.js +734 -0
- package/dist/esm/src/user-modules/call.spec.js.map +1 -0
- package/dist/esm/src/user-modules/contact.d.ts +16 -0
- package/dist/esm/src/user-modules/contact.d.ts.map +1 -1
- package/dist/esm/src/user-modules/contact.js +16 -0
- package/dist/esm/src/user-modules/contact.js.map +1 -1
- package/dist/esm/src/user-modules/mod.d.ts +5 -4
- package/dist/esm/src/user-modules/mod.d.ts.map +1 -1
- package/dist/esm/src/user-modules/mod.js +2 -2
- package/dist/esm/src/user-modules/mod.js.map +1 -1
- package/dist/esm/src/wechaty/wechaty-base.d.ts +66 -7
- package/dist/esm/src/wechaty/wechaty-base.d.ts.map +1 -1
- package/dist/esm/src/wechaty/wechaty-base.js +28 -0
- package/dist/esm/src/wechaty/wechaty-base.js.map +1 -1
- package/dist/esm/src/wechaty/wechaty-impl.spec.js +10 -8
- package/dist/esm/src/wechaty/wechaty-impl.spec.js.map +1 -1
- package/dist/esm/src/wechaty-mixins/gerror-mixin.d.ts +1 -1
- package/dist/esm/src/wechaty-mixins/io-mixin.d.ts +2 -2
- package/dist/esm/src/wechaty-mixins/login-mixin.d.ts +15 -5
- package/dist/esm/src/wechaty-mixins/login-mixin.d.ts.map +1 -1
- package/dist/esm/src/wechaty-mixins/misc-mixin.d.ts +15 -5
- package/dist/esm/src/wechaty-mixins/misc-mixin.d.ts.map +1 -1
- package/dist/esm/src/wechaty-mixins/plugin-mixin.d.ts +32 -6
- package/dist/esm/src/wechaty-mixins/plugin-mixin.d.ts.map +1 -1
- package/dist/esm/src/wechaty-mixins/puppet-mixin.d.ts +39 -5
- package/dist/esm/src/wechaty-mixins/puppet-mixin.d.ts.map +1 -1
- package/dist/esm/src/wechaty-mixins/puppet-mixin.js +149 -0
- package/dist/esm/src/wechaty-mixins/puppet-mixin.js.map +1 -1
- package/dist/esm/src/wechaty-mixins/wechatify-user-module-mixin.d.ts +5 -3
- package/dist/esm/src/wechaty-mixins/wechatify-user-module-mixin.d.ts.map +1 -1
- package/dist/esm/src/wechaty-mixins/wechatify-user-module-mixin.js +4 -1
- package/dist/esm/src/wechaty-mixins/wechatify-user-module-mixin.js.map +1 -1
- package/package.json +3 -3
- package/src/mods/impls.ts +2 -0
- package/src/mods/users.ts +6 -0
- package/src/package-json.ts +4 -4
- package/src/schemas/call-events.ts +35 -0
- package/src/schemas/mod.ts +6 -0
- package/src/schemas/wechaty-events.ts +28 -0
- package/src/user-modules/call.spec.ts +929 -0
- package/src/user-modules/call.ts +373 -0
- package/src/user-modules/contact.ts +18 -0
- package/src/user-modules/mod.ts +11 -0
- package/src/wechaty/wechaty-base.ts +40 -1
- package/src/wechaty/wechaty-impl.spec.ts +4 -0
- package/src/wechaty-mixins/puppet-mixin.ts +184 -0
- package/src/wechaty-mixins/wechatify-user-module-mixin.ts +6 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --no-warnings --loader ts-node/esm
|
|
2
|
+
/**
|
|
3
|
+
* Tests for the Call first-class object and its lifecycle state machine
|
|
4
|
+
* against the @juzi/wechaty-puppet@^1.0.138 contract.
|
|
5
|
+
*/
|
|
6
|
+
import { test, sinon, } from 'tstest';
|
|
7
|
+
import * as PUPPET from '@juzi/wechaty-puppet';
|
|
8
|
+
import { PuppetMock } from '@juzi/wechaty-puppet-mock';
|
|
9
|
+
import { WechatyBuilder } from '../wechaty-builder.js';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
const sandbox = sinon.createSandbox();
|
|
14
|
+
function buildWechaty() {
|
|
15
|
+
const puppet = new PuppetMock();
|
|
16
|
+
const wechaty = WechatyBuilder.build({ puppet });
|
|
17
|
+
return { puppet, wechaty };
|
|
18
|
+
}
|
|
19
|
+
function stubCallPayload(puppet, factory) {
|
|
20
|
+
const stub = sandbox.stub().callsFake(async (callId) => {
|
|
21
|
+
await new Promise(setImmediate);
|
|
22
|
+
return factory(callId);
|
|
23
|
+
});
|
|
24
|
+
puppet.callPayload = stub;
|
|
25
|
+
// Used by Call.sync()
|
|
26
|
+
puppet.callPayloadDirty = sandbox.stub().resolves(undefined);
|
|
27
|
+
return stub;
|
|
28
|
+
}
|
|
29
|
+
async function startAndLogin(puppet, wechaty, userId = 'bot-self') {
|
|
30
|
+
sandbox.stub(puppet, 'contactPayload').callsFake(async (id) => {
|
|
31
|
+
await new Promise(setImmediate);
|
|
32
|
+
return { id, name: id };
|
|
33
|
+
});
|
|
34
|
+
sandbox.stub(puppet, 'contactSearch').callsFake(async (...args) => {
|
|
35
|
+
await new Promise(setImmediate);
|
|
36
|
+
return [args[0]?.id ?? userId];
|
|
37
|
+
});
|
|
38
|
+
await wechaty.start();
|
|
39
|
+
await puppet.login(userId);
|
|
40
|
+
}
|
|
41
|
+
function flush(ticks = 8) {
|
|
42
|
+
let p = Promise.resolve();
|
|
43
|
+
for (let i = 0; i < ticks; i++) {
|
|
44
|
+
p = p.then(() => new Promise(resolve => setImmediate(resolve)));
|
|
45
|
+
}
|
|
46
|
+
return p;
|
|
47
|
+
}
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// 1. bot.call() — outgoing, mints callId via puppet.callInvite, hydrates payload
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
test('bot.call() returns an outgoing Call with status=calling, hydrated payload', async (t) => {
|
|
52
|
+
const { puppet, wechaty } = buildWechaty();
|
|
53
|
+
await startAndLogin(puppet, wechaty);
|
|
54
|
+
const CALL_ID = 'call-id-minted-by-protocol';
|
|
55
|
+
const PEER_A_ID = 'peer-a';
|
|
56
|
+
const PEER_B_ID = 'peer-b';
|
|
57
|
+
const START_TS = 1_700_000_000_000;
|
|
58
|
+
const callInviteStub = sandbox.stub().resolves(CALL_ID);
|
|
59
|
+
puppet.callInvite = callInviteStub;
|
|
60
|
+
stubCallPayload(puppet, (id) => ({
|
|
61
|
+
id,
|
|
62
|
+
starter: 'bot-self',
|
|
63
|
+
participants: [PEER_A_ID, PEER_B_ID],
|
|
64
|
+
media: PUPPET.types.CallMediaType.Video,
|
|
65
|
+
startTime: START_TS,
|
|
66
|
+
}));
|
|
67
|
+
const contactA = wechaty.Contact.load(PEER_A_ID);
|
|
68
|
+
const contactB = wechaty.Contact.load(PEER_B_ID);
|
|
69
|
+
const call = await wechaty.call([contactA, contactB], { media: PUPPET.types.CallMediaType.Video });
|
|
70
|
+
t.equal(call.id, CALL_ID, 'call.id should match callInvite return');
|
|
71
|
+
t.equal(call.direction(), 'outgoing', 'direction should be outgoing');
|
|
72
|
+
t.equal(call.status(), 'calling', 'status should be calling');
|
|
73
|
+
t.equal(call.media(), PUPPET.types.CallMediaType.Video, 'media should reflect payload');
|
|
74
|
+
t.same(call.startTime(), new Date(START_TS), 'startTime should match payload');
|
|
75
|
+
t.equal(call.endTime(), undefined, 'endTime should be undefined for a live call');
|
|
76
|
+
const participants = await call.participants();
|
|
77
|
+
t.same(participants.map(c => c.id).sort(), [PEER_A_ID, PEER_B_ID].sort(), 'participants should match payload');
|
|
78
|
+
t.ok(callInviteStub.calledOnce, 'puppet.callInvite should be called once');
|
|
79
|
+
t.same(callInviteStub.firstCall.args, [[PEER_A_ID, PEER_B_ID], PUPPET.types.CallMediaType.Video], 'callInvite args should be (contactIds, media)');
|
|
80
|
+
await wechaty.stop();
|
|
81
|
+
sandbox.restore();
|
|
82
|
+
});
|
|
83
|
+
test('bot.call() rejects when contacts list is empty', async (t) => {
|
|
84
|
+
const { puppet, wechaty } = buildWechaty();
|
|
85
|
+
await startAndLogin(puppet, wechaty);
|
|
86
|
+
await t.rejects(wechaty.call([]), /at least one contact/, 'empty contacts should reject');
|
|
87
|
+
await wechaty.stop();
|
|
88
|
+
sandbox.restore();
|
|
89
|
+
});
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// 2. contact.call() — 1v1 syntactic sugar over bot.call()
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
test('contact.call() delegates to bot.call([this])', async (t) => {
|
|
94
|
+
const { puppet, wechaty } = buildWechaty();
|
|
95
|
+
await startAndLogin(puppet, wechaty);
|
|
96
|
+
const CALL_ID = 'call-id-sugar';
|
|
97
|
+
const PEER_ID = 'peer-sugar';
|
|
98
|
+
const callInviteStub = sandbox.stub().resolves(CALL_ID);
|
|
99
|
+
puppet.callInvite = callInviteStub;
|
|
100
|
+
stubCallPayload(puppet, (id) => ({
|
|
101
|
+
id,
|
|
102
|
+
starter: 'bot-self',
|
|
103
|
+
participants: [PEER_ID],
|
|
104
|
+
media: PUPPET.types.CallMediaType.Audio,
|
|
105
|
+
startTime: 1,
|
|
106
|
+
}));
|
|
107
|
+
const contact = wechaty.Contact.load(PEER_ID);
|
|
108
|
+
const call = await contact.call();
|
|
109
|
+
t.equal(call.id, CALL_ID, 'call.id should match callInvite return');
|
|
110
|
+
t.equal(call.media(), PUPPET.types.CallMediaType.Audio, 'default media should be audio');
|
|
111
|
+
t.same(callInviteStub.firstCall.args, [[PEER_ID], PUPPET.types.CallMediaType.Audio], 'callInvite args should be ([peerId], audio)');
|
|
112
|
+
await wechaty.stop();
|
|
113
|
+
sandbox.restore();
|
|
114
|
+
});
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// 3. Incoming Invite — bot emits 'call' with a hydrated Call
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
test('incoming call: Invite hydrates payload then emits bot.on("call")', async (t) => {
|
|
119
|
+
const { puppet, wechaty } = buildWechaty();
|
|
120
|
+
await startAndLogin(puppet, wechaty);
|
|
121
|
+
const CALL_ID = 'call-id-incoming';
|
|
122
|
+
const CALLER_ID = 'caller-incoming';
|
|
123
|
+
const START_TS = 1_700_000_000_999;
|
|
124
|
+
stubCallPayload(puppet, (id) => ({
|
|
125
|
+
id,
|
|
126
|
+
starter: CALLER_ID,
|
|
127
|
+
participants: [CALLER_ID, 'bot-self'],
|
|
128
|
+
media: PUPPET.types.CallMediaType.Video,
|
|
129
|
+
startTime: START_TS,
|
|
130
|
+
}));
|
|
131
|
+
let received;
|
|
132
|
+
wechaty.on('call', (c) => { received = c; });
|
|
133
|
+
puppet.emit('call', {
|
|
134
|
+
callId: CALL_ID,
|
|
135
|
+
signal: PUPPET.types.CallSignal.Invite,
|
|
136
|
+
contactId: CALLER_ID,
|
|
137
|
+
timestamp: START_TS,
|
|
138
|
+
});
|
|
139
|
+
await flush();
|
|
140
|
+
t.ok(received, 'bot should emit call event for Invite');
|
|
141
|
+
t.equal(received.id, CALL_ID, 'call.id should match');
|
|
142
|
+
t.equal(received.direction(), 'incoming', 'direction should be incoming');
|
|
143
|
+
t.equal(received.status(), 'ringing', 'status should be ringing');
|
|
144
|
+
t.equal(received.media(), PUPPET.types.CallMediaType.Video, 'media should reflect payload');
|
|
145
|
+
const starter = await received.starter();
|
|
146
|
+
t.ok(starter, 'starter should be resolvable');
|
|
147
|
+
t.equal(starter.id, CALLER_ID, 'starter should be the caller');
|
|
148
|
+
await wechaty.stop();
|
|
149
|
+
sandbox.restore();
|
|
150
|
+
});
|
|
151
|
+
test('duplicate Invite for the same callId is ignored', async (t) => {
|
|
152
|
+
const { puppet, wechaty } = buildWechaty();
|
|
153
|
+
await startAndLogin(puppet, wechaty);
|
|
154
|
+
const CALL_ID = 'call-id-dup';
|
|
155
|
+
const CALLER_ID = 'caller-dup';
|
|
156
|
+
stubCallPayload(puppet, (id) => ({
|
|
157
|
+
id,
|
|
158
|
+
starter: CALLER_ID,
|
|
159
|
+
participants: [CALLER_ID, 'bot-self'],
|
|
160
|
+
media: PUPPET.types.CallMediaType.Audio,
|
|
161
|
+
startTime: 1,
|
|
162
|
+
}));
|
|
163
|
+
let count = 0;
|
|
164
|
+
wechaty.on('call', () => { count++; });
|
|
165
|
+
puppet.emit('call', {
|
|
166
|
+
callId: CALL_ID,
|
|
167
|
+
signal: PUPPET.types.CallSignal.Invite,
|
|
168
|
+
contactId: CALLER_ID,
|
|
169
|
+
timestamp: 1,
|
|
170
|
+
});
|
|
171
|
+
await flush();
|
|
172
|
+
puppet.emit('call', {
|
|
173
|
+
callId: CALL_ID,
|
|
174
|
+
signal: PUPPET.types.CallSignal.Invite,
|
|
175
|
+
contactId: CALLER_ID,
|
|
176
|
+
timestamp: 2,
|
|
177
|
+
});
|
|
178
|
+
await flush();
|
|
179
|
+
t.equal(count, 1, "'call' should fire once even on duplicate Invite");
|
|
180
|
+
await wechaty.stop();
|
|
181
|
+
sandbox.restore();
|
|
182
|
+
});
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// 4. Ringing / Accept double-emit on outgoing
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
test('outgoing Ringing → object + bot emit; Accept → object + bot emit, status=connected', async (t) => {
|
|
187
|
+
const { puppet, wechaty } = buildWechaty();
|
|
188
|
+
await startAndLogin(puppet, wechaty);
|
|
189
|
+
const CALL_ID = 'call-id-outgoing-ringing-accept';
|
|
190
|
+
const PEER_ID = 'peer-ringing-accept';
|
|
191
|
+
puppet.callInvite = sandbox.stub().resolves(CALL_ID);
|
|
192
|
+
stubCallPayload(puppet, (id) => ({
|
|
193
|
+
id,
|
|
194
|
+
starter: 'bot-self',
|
|
195
|
+
participants: ['bot-self', PEER_ID],
|
|
196
|
+
media: PUPPET.types.CallMediaType.Audio,
|
|
197
|
+
startTime: 1,
|
|
198
|
+
}));
|
|
199
|
+
const contact = wechaty.Contact.load(PEER_ID);
|
|
200
|
+
const call = await contact.call();
|
|
201
|
+
let objectRinging = false;
|
|
202
|
+
let botRinging = false;
|
|
203
|
+
call.on('ringing', () => { objectRinging = true; });
|
|
204
|
+
wechaty.on('call-ringing', (c) => { if (c.id === CALL_ID)
|
|
205
|
+
botRinging = true; });
|
|
206
|
+
puppet.emit('call', {
|
|
207
|
+
callId: CALL_ID,
|
|
208
|
+
signal: PUPPET.types.CallSignal.Ringing,
|
|
209
|
+
contactId: PEER_ID,
|
|
210
|
+
timestamp: 2,
|
|
211
|
+
});
|
|
212
|
+
await flush();
|
|
213
|
+
t.ok(objectRinging, "object should emit 'ringing'");
|
|
214
|
+
t.ok(botRinging, "bot should emit 'call-ringing'");
|
|
215
|
+
t.equal(call.status(), 'ringing', 'status should be ringing');
|
|
216
|
+
let objectActor;
|
|
217
|
+
let botActor;
|
|
218
|
+
call.on('accept', actor => { objectActor = actor; });
|
|
219
|
+
wechaty.on('call-accept', (c, actor) => {
|
|
220
|
+
if (c.id === CALL_ID) {
|
|
221
|
+
botActor = actor;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
puppet.emit('call', {
|
|
225
|
+
callId: CALL_ID,
|
|
226
|
+
signal: PUPPET.types.CallSignal.Accept,
|
|
227
|
+
contactId: PEER_ID,
|
|
228
|
+
timestamp: 3,
|
|
229
|
+
});
|
|
230
|
+
await flush();
|
|
231
|
+
t.ok(objectActor, 'object accept actor should be set');
|
|
232
|
+
t.equal(objectActor.id, PEER_ID, 'object accept actor should be the peer');
|
|
233
|
+
t.ok(botActor, 'bot accept actor should be set');
|
|
234
|
+
t.equal(botActor.id, PEER_ID, 'bot accept actor should be the peer');
|
|
235
|
+
t.equal(call.status(), 'connected', 'status should be connected after Accept');
|
|
236
|
+
await wechaty.stop();
|
|
237
|
+
sandbox.restore();
|
|
238
|
+
});
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// 5. accept() control on incoming
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
test('incoming call.accept() invokes puppet.callAccept(callId) and connects', async (t) => {
|
|
243
|
+
const { puppet, wechaty } = buildWechaty();
|
|
244
|
+
await startAndLogin(puppet, wechaty);
|
|
245
|
+
const CALL_ID = 'call-id-accept';
|
|
246
|
+
const CALLER_ID = 'caller-accept';
|
|
247
|
+
stubCallPayload(puppet, (id) => ({
|
|
248
|
+
id,
|
|
249
|
+
starter: CALLER_ID,
|
|
250
|
+
participants: [CALLER_ID, 'bot-self'],
|
|
251
|
+
media: PUPPET.types.CallMediaType.Audio,
|
|
252
|
+
startTime: 1,
|
|
253
|
+
}));
|
|
254
|
+
const acceptStub = sandbox.stub().resolves(undefined);
|
|
255
|
+
puppet.callAccept = acceptStub;
|
|
256
|
+
let incoming;
|
|
257
|
+
wechaty.on('call', (c) => { incoming = c; });
|
|
258
|
+
puppet.emit('call', {
|
|
259
|
+
callId: CALL_ID,
|
|
260
|
+
signal: PUPPET.types.CallSignal.Invite,
|
|
261
|
+
contactId: CALLER_ID,
|
|
262
|
+
timestamp: 1,
|
|
263
|
+
});
|
|
264
|
+
await flush();
|
|
265
|
+
t.ok(incoming, 'should receive incoming call');
|
|
266
|
+
await incoming.accept();
|
|
267
|
+
t.equal(incoming.status(), 'connected', 'status should be connected after accept()');
|
|
268
|
+
t.ok(acceptStub.calledOnce, 'puppet.callAccept should be called once');
|
|
269
|
+
t.same(acceptStub.firstCall.args, [CALL_ID], 'callAccept args should be [callId]');
|
|
270
|
+
await wechaty.stop();
|
|
271
|
+
sandbox.restore();
|
|
272
|
+
});
|
|
273
|
+
test('outgoing call.accept() throws (invalid direction)', async (t) => {
|
|
274
|
+
const { puppet, wechaty } = buildWechaty();
|
|
275
|
+
await startAndLogin(puppet, wechaty);
|
|
276
|
+
puppet.callInvite = sandbox.stub().resolves('call-id-outgoing-accept-bad');
|
|
277
|
+
stubCallPayload(puppet, (id) => ({
|
|
278
|
+
id,
|
|
279
|
+
starter: 'bot-self',
|
|
280
|
+
participants: ['peer'],
|
|
281
|
+
media: PUPPET.types.CallMediaType.Audio,
|
|
282
|
+
startTime: 1,
|
|
283
|
+
}));
|
|
284
|
+
const contact = wechaty.Contact.load('peer');
|
|
285
|
+
const call = await contact.call();
|
|
286
|
+
await t.rejects(call.accept(), /invalid/i, 'accept() on outgoing should throw');
|
|
287
|
+
await wechaty.stop();
|
|
288
|
+
sandbox.restore();
|
|
289
|
+
});
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// 6. reject() control on incoming + Reject signal terminal handling (outgoing 1v1)
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
test('incoming call.reject() invokes puppet.callReject(callId, reason) and ends', async (t) => {
|
|
294
|
+
const { puppet, wechaty } = buildWechaty();
|
|
295
|
+
await startAndLogin(puppet, wechaty);
|
|
296
|
+
const CALL_ID = 'call-id-reject';
|
|
297
|
+
stubCallPayload(puppet, (id) => ({
|
|
298
|
+
id,
|
|
299
|
+
starter: 'caller-x',
|
|
300
|
+
participants: ['caller-x', 'bot-self'],
|
|
301
|
+
media: PUPPET.types.CallMediaType.Audio,
|
|
302
|
+
startTime: 1,
|
|
303
|
+
}));
|
|
304
|
+
const rejectStub = sandbox.stub().resolves(undefined);
|
|
305
|
+
puppet.callReject = rejectStub;
|
|
306
|
+
let incoming;
|
|
307
|
+
wechaty.on('call', (c) => { incoming = c; });
|
|
308
|
+
puppet.emit('call', {
|
|
309
|
+
callId: CALL_ID,
|
|
310
|
+
signal: PUPPET.types.CallSignal.Invite,
|
|
311
|
+
contactId: 'caller-x',
|
|
312
|
+
timestamp: 1,
|
|
313
|
+
});
|
|
314
|
+
await flush();
|
|
315
|
+
await incoming.reject('busy');
|
|
316
|
+
t.equal(incoming.status(), 'ended', 'status should be ended after reject()');
|
|
317
|
+
t.same(rejectStub.firstCall.args, [CALL_ID, 'busy'], 'callReject args should be [callId, reason]');
|
|
318
|
+
await wechaty.stop();
|
|
319
|
+
sandbox.restore();
|
|
320
|
+
});
|
|
321
|
+
test('outgoing 1v1 Reject from peer → bot emits call-reject + call-ended, pool cleaned', async (t) => {
|
|
322
|
+
const { puppet, wechaty } = buildWechaty();
|
|
323
|
+
await startAndLogin(puppet, wechaty);
|
|
324
|
+
const CALL_ID = 'call-id-outgoing-rejected';
|
|
325
|
+
const PEER_ID = 'peer-rejecter';
|
|
326
|
+
puppet.callInvite = sandbox.stub().resolves(CALL_ID);
|
|
327
|
+
// Initial payload: live; on second pull (sync after Reject) the call has endTime set.
|
|
328
|
+
let pulls = 0;
|
|
329
|
+
puppet.callPayload = sandbox.stub().callsFake(async (id) => {
|
|
330
|
+
pulls++;
|
|
331
|
+
await new Promise(setImmediate);
|
|
332
|
+
return {
|
|
333
|
+
id,
|
|
334
|
+
starter: 'bot-self',
|
|
335
|
+
participants: ['bot-self', PEER_ID],
|
|
336
|
+
media: PUPPET.types.CallMediaType.Audio,
|
|
337
|
+
startTime: 1,
|
|
338
|
+
endTime: pulls === 1 ? undefined : 100,
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
puppet.callPayloadDirty = sandbox.stub().resolves(undefined);
|
|
342
|
+
const contact = wechaty.Contact.load(PEER_ID);
|
|
343
|
+
const call = await contact.call();
|
|
344
|
+
let rejectActor;
|
|
345
|
+
let rejectReason;
|
|
346
|
+
let endedEmitCount = 0;
|
|
347
|
+
wechaty.on('call-reject', (c, actor, reason) => {
|
|
348
|
+
if (c.id === CALL_ID) {
|
|
349
|
+
rejectActor = actor;
|
|
350
|
+
rejectReason = reason;
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
wechaty.on('call-ended', (c) => { if (c.id === CALL_ID)
|
|
354
|
+
endedEmitCount++; });
|
|
355
|
+
puppet.emit('call', {
|
|
356
|
+
callId: CALL_ID,
|
|
357
|
+
signal: PUPPET.types.CallSignal.Reject,
|
|
358
|
+
contactId: PEER_ID,
|
|
359
|
+
reason: 'busy',
|
|
360
|
+
timestamp: 50,
|
|
361
|
+
});
|
|
362
|
+
await flush();
|
|
363
|
+
t.ok(rejectActor, 'call-reject actor should be set');
|
|
364
|
+
t.equal(rejectActor.id, PEER_ID, 'call-reject actor should be the peer');
|
|
365
|
+
t.equal(rejectReason, 'busy', 'call-reject reason should be passed through');
|
|
366
|
+
t.equal(endedEmitCount, 1, 'call-ended should fire exactly once after Reject terminates the 1v1 call');
|
|
367
|
+
t.equal(call.status(), 'ended', 'status should be ended after terminal Reject');
|
|
368
|
+
t.notOk(wechaty.__callPool.has(CALL_ID), 'call should be evicted from pool');
|
|
369
|
+
await wechaty.stop();
|
|
370
|
+
sandbox.restore();
|
|
371
|
+
});
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
// 7. Cancel signal received by callee → cancel + ended emitted
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
test('Cancel signal terminates incoming call; emits cancel + ended; pool cleaned; status flips before cancel handler runs', async (t) => {
|
|
376
|
+
const { puppet, wechaty } = buildWechaty();
|
|
377
|
+
await startAndLogin(puppet, wechaty);
|
|
378
|
+
const CALL_ID = 'call-id-cancel';
|
|
379
|
+
const CALLER_ID = 'caller-cancel';
|
|
380
|
+
stubCallPayload(puppet, (id) => ({
|
|
381
|
+
id,
|
|
382
|
+
starter: CALLER_ID,
|
|
383
|
+
participants: [CALLER_ID, 'bot-self'],
|
|
384
|
+
media: PUPPET.types.CallMediaType.Audio,
|
|
385
|
+
startTime: 1,
|
|
386
|
+
}));
|
|
387
|
+
let incoming;
|
|
388
|
+
wechaty.on('call', (c) => { incoming = c; });
|
|
389
|
+
puppet.emit('call', {
|
|
390
|
+
callId: CALL_ID,
|
|
391
|
+
signal: PUPPET.types.CallSignal.Invite,
|
|
392
|
+
contactId: CALLER_ID,
|
|
393
|
+
timestamp: 1,
|
|
394
|
+
});
|
|
395
|
+
await flush();
|
|
396
|
+
let objectCancelReason;
|
|
397
|
+
let botCancelReason;
|
|
398
|
+
let statusInCancelHandler;
|
|
399
|
+
let endedFiredCount = 0;
|
|
400
|
+
incoming.on('cancel', reason => {
|
|
401
|
+
objectCancelReason = reason;
|
|
402
|
+
statusInCancelHandler = incoming.status();
|
|
403
|
+
});
|
|
404
|
+
wechaty.on('call-cancel', (c, reason) => {
|
|
405
|
+
if (c.id === CALL_ID) {
|
|
406
|
+
botCancelReason = reason;
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
wechaty.on('call-ended', (c) => { if (c.id === CALL_ID)
|
|
410
|
+
endedFiredCount++; });
|
|
411
|
+
puppet.emit('call', {
|
|
412
|
+
callId: CALL_ID,
|
|
413
|
+
signal: PUPPET.types.CallSignal.Cancel,
|
|
414
|
+
contactId: CALLER_ID,
|
|
415
|
+
reason: 'caller-aborted',
|
|
416
|
+
timestamp: 2,
|
|
417
|
+
});
|
|
418
|
+
await flush();
|
|
419
|
+
t.equal(objectCancelReason, 'caller-aborted', "object 'cancel' reason should match");
|
|
420
|
+
t.equal(botCancelReason, 'caller-aborted', "bot 'call-cancel' reason should match");
|
|
421
|
+
t.equal(endedFiredCount, 1, "'call-ended' should fire exactly once after Cancel (idempotent via __endedEmitted)");
|
|
422
|
+
t.equal(statusInCancelHandler, 'ended', "status should be 'ended' inside the cancel handler (no race window)");
|
|
423
|
+
t.equal(incoming.status(), 'ended', 'status should be ended');
|
|
424
|
+
t.notOk(wechaty.__callPool.has(CALL_ID), 'call should be evicted from pool');
|
|
425
|
+
await wechaty.stop();
|
|
426
|
+
sandbox.restore();
|
|
427
|
+
});
|
|
428
|
+
// ---------------------------------------------------------------------------
|
|
429
|
+
// 8. hangup() control on connected
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
test('call.hangup() on connected call invokes puppet.callHangup and ends', async (t) => {
|
|
432
|
+
const { puppet, wechaty } = buildWechaty();
|
|
433
|
+
await startAndLogin(puppet, wechaty);
|
|
434
|
+
const CALL_ID = 'call-id-hangup';
|
|
435
|
+
const PEER_ID = 'peer-hangup';
|
|
436
|
+
puppet.callInvite = sandbox.stub().resolves(CALL_ID);
|
|
437
|
+
stubCallPayload(puppet, (id) => ({
|
|
438
|
+
id,
|
|
439
|
+
starter: 'bot-self',
|
|
440
|
+
participants: ['bot-self', PEER_ID],
|
|
441
|
+
media: PUPPET.types.CallMediaType.Audio,
|
|
442
|
+
startTime: 1,
|
|
443
|
+
}));
|
|
444
|
+
const hangupStub = sandbox.stub().resolves(undefined);
|
|
445
|
+
puppet.callHangup = hangupStub;
|
|
446
|
+
const contact = wechaty.Contact.load(PEER_ID);
|
|
447
|
+
const call = await contact.call();
|
|
448
|
+
puppet.emit('call', {
|
|
449
|
+
callId: CALL_ID,
|
|
450
|
+
signal: PUPPET.types.CallSignal.Accept,
|
|
451
|
+
contactId: PEER_ID,
|
|
452
|
+
timestamp: 2,
|
|
453
|
+
});
|
|
454
|
+
await flush();
|
|
455
|
+
t.equal(call.status(), 'connected', 'prerequisite: call should be connected');
|
|
456
|
+
await call.hangup('done');
|
|
457
|
+
t.equal(call.status(), 'ended', 'status should be ended after hangup()');
|
|
458
|
+
t.same(hangupStub.firstCall.args, [CALL_ID, 'done'], 'callHangup args should be [callId, reason]');
|
|
459
|
+
await wechaty.stop();
|
|
460
|
+
sandbox.restore();
|
|
461
|
+
});
|
|
462
|
+
test('call.hangup() rejects when puppet.callHangup fails but local status still ends', async (t) => {
|
|
463
|
+
const { puppet, wechaty } = buildWechaty();
|
|
464
|
+
await startAndLogin(puppet, wechaty);
|
|
465
|
+
puppet.callInvite = sandbox.stub().resolves('call-id-hangup-fail');
|
|
466
|
+
stubCallPayload(puppet, (id) => ({
|
|
467
|
+
id,
|
|
468
|
+
starter: 'bot-self',
|
|
469
|
+
participants: ['bot-self', 'peer'],
|
|
470
|
+
media: PUPPET.types.CallMediaType.Audio,
|
|
471
|
+
startTime: 1,
|
|
472
|
+
}));
|
|
473
|
+
const hangupStub = sandbox.stub().rejects(new Error('network error'));
|
|
474
|
+
puppet.callHangup = hangupStub;
|
|
475
|
+
const contact = wechaty.Contact.load('peer');
|
|
476
|
+
const call = await contact.call();
|
|
477
|
+
puppet.emit('call', {
|
|
478
|
+
callId: call.id,
|
|
479
|
+
signal: PUPPET.types.CallSignal.Accept,
|
|
480
|
+
contactId: 'peer',
|
|
481
|
+
timestamp: 2,
|
|
482
|
+
});
|
|
483
|
+
await flush();
|
|
484
|
+
t.equal(call.status(), 'connected', 'prerequisite: call should be connected');
|
|
485
|
+
await t.rejects(call.hangup(), /network error/, 'hangup() should re-throw');
|
|
486
|
+
t.equal(call.status(), 'ended', 'status should still be ended despite failure');
|
|
487
|
+
await wechaty.stop();
|
|
488
|
+
sandbox.restore();
|
|
489
|
+
});
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
// 9. add() invokes puppet.callAdd(callId, contactIds)
|
|
492
|
+
// ---------------------------------------------------------------------------
|
|
493
|
+
test('call.add() invokes puppet.callAdd with the contact ids', async (t) => {
|
|
494
|
+
const { puppet, wechaty } = buildWechaty();
|
|
495
|
+
await startAndLogin(puppet, wechaty);
|
|
496
|
+
const CALL_ID = 'call-id-add';
|
|
497
|
+
puppet.callInvite = sandbox.stub().resolves(CALL_ID);
|
|
498
|
+
stubCallPayload(puppet, (id) => ({
|
|
499
|
+
id,
|
|
500
|
+
starter: 'bot-self',
|
|
501
|
+
participants: ['bot-self', 'peer'],
|
|
502
|
+
media: PUPPET.types.CallMediaType.Audio,
|
|
503
|
+
startTime: 1,
|
|
504
|
+
}));
|
|
505
|
+
const addStub = sandbox.stub().resolves(undefined);
|
|
506
|
+
puppet.callAdd = addStub;
|
|
507
|
+
const contact = wechaty.Contact.load('peer');
|
|
508
|
+
const call = await contact.call();
|
|
509
|
+
const newContactA = wechaty.Contact.load('newA');
|
|
510
|
+
const newContactB = wechaty.Contact.load('newB');
|
|
511
|
+
await call.add([newContactA, newContactB]);
|
|
512
|
+
t.same(addStub.firstCall.args, [CALL_ID, ['newA', 'newB']], 'callAdd args should be [callId, contactIds]');
|
|
513
|
+
await wechaty.stop();
|
|
514
|
+
sandbox.restore();
|
|
515
|
+
});
|
|
516
|
+
// ---------------------------------------------------------------------------
|
|
517
|
+
// 10. mediaEndpoint() pass-through
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
test('call.mediaEndpoint() forwards to puppet.callMediaEndpoint', async (t) => {
|
|
520
|
+
const { puppet, wechaty } = buildWechaty();
|
|
521
|
+
await startAndLogin(puppet, wechaty);
|
|
522
|
+
const CALL_ID = 'call-id-media';
|
|
523
|
+
puppet.callInvite = sandbox.stub().resolves(CALL_ID);
|
|
524
|
+
stubCallPayload(puppet, (id) => ({
|
|
525
|
+
id,
|
|
526
|
+
starter: 'bot-self',
|
|
527
|
+
participants: ['bot-self', 'peer'],
|
|
528
|
+
media: PUPPET.types.CallMediaType.Audio,
|
|
529
|
+
startTime: 1,
|
|
530
|
+
}));
|
|
531
|
+
const endpoint = {
|
|
532
|
+
url: 'wss://media.example/sfu',
|
|
533
|
+
token: 'token-xyz',
|
|
534
|
+
};
|
|
535
|
+
const endpointStub = sandbox.stub().resolves(endpoint);
|
|
536
|
+
puppet.callMediaEndpoint = endpointStub;
|
|
537
|
+
const contact = wechaty.Contact.load('peer');
|
|
538
|
+
const call = await contact.call();
|
|
539
|
+
const got = await call.mediaEndpoint();
|
|
540
|
+
t.same(got, endpoint, 'mediaEndpoint should pass through');
|
|
541
|
+
t.same(endpointStub.firstCall.args, [CALL_ID], 'callMediaEndpoint args should be [callId]');
|
|
542
|
+
await wechaty.stop();
|
|
543
|
+
sandbox.restore();
|
|
544
|
+
});
|
|
545
|
+
// ---------------------------------------------------------------------------
|
|
546
|
+
// 11. wechaty.stop() drains the call pool
|
|
547
|
+
// ---------------------------------------------------------------------------
|
|
548
|
+
test('wechaty.stop() clears the call pool', async (t) => {
|
|
549
|
+
const { puppet, wechaty } = buildWechaty();
|
|
550
|
+
await startAndLogin(puppet, wechaty);
|
|
551
|
+
puppet.callInvite = sandbox.stub().resolves('call-id-stop');
|
|
552
|
+
stubCallPayload(puppet, (id) => ({
|
|
553
|
+
id,
|
|
554
|
+
starter: 'bot-self',
|
|
555
|
+
participants: ['bot-self', 'peer'],
|
|
556
|
+
media: PUPPET.types.CallMediaType.Audio,
|
|
557
|
+
startTime: 1,
|
|
558
|
+
}));
|
|
559
|
+
const contact = wechaty.Contact.load('peer');
|
|
560
|
+
await contact.call();
|
|
561
|
+
t.ok(wechaty.__callPool.size > 0, 'pool should be non-empty before stop()');
|
|
562
|
+
await wechaty.stop();
|
|
563
|
+
t.equal(wechaty.__callPool.size, 0, 'pool should be empty after stop()');
|
|
564
|
+
sandbox.restore();
|
|
565
|
+
});
|
|
566
|
+
// ---------------------------------------------------------------------------
|
|
567
|
+
// 12. Unknown callId signal is silently dropped (no error event)
|
|
568
|
+
// ---------------------------------------------------------------------------
|
|
569
|
+
test('signal for unknown callId is dropped, not surfaced as error', async (t) => {
|
|
570
|
+
const { puppet, wechaty } = buildWechaty();
|
|
571
|
+
await startAndLogin(puppet, wechaty);
|
|
572
|
+
let errorEmitted = false;
|
|
573
|
+
wechaty.on('error', () => { errorEmitted = true; });
|
|
574
|
+
puppet.emit('call', {
|
|
575
|
+
callId: 'never-seen',
|
|
576
|
+
signal: PUPPET.types.CallSignal.Hangup,
|
|
577
|
+
contactId: 'someone',
|
|
578
|
+
timestamp: 1,
|
|
579
|
+
});
|
|
580
|
+
await flush();
|
|
581
|
+
t.notOk(errorEmitted, 'unknown callId should not emit error');
|
|
582
|
+
await wechaty.stop();
|
|
583
|
+
sandbox.restore();
|
|
584
|
+
});
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
// 13. Local control methods evict from pool + emit object 'ended'
|
|
587
|
+
// ---------------------------------------------------------------------------
|
|
588
|
+
test("local reject() evicts from pool and emits object 'ended' + bot 'call-ended'", async (t) => {
|
|
589
|
+
const { puppet, wechaty } = buildWechaty();
|
|
590
|
+
await startAndLogin(puppet, wechaty);
|
|
591
|
+
const CALL_ID = 'call-id-local-reject';
|
|
592
|
+
const CALLER_ID = 'caller-local-reject';
|
|
593
|
+
stubCallPayload(puppet, (id) => ({
|
|
594
|
+
id,
|
|
595
|
+
starter: CALLER_ID,
|
|
596
|
+
participants: [CALLER_ID, 'bot-self'],
|
|
597
|
+
media: PUPPET.types.CallMediaType.Audio,
|
|
598
|
+
startTime: 1,
|
|
599
|
+
}));
|
|
600
|
+
puppet.callReject = sandbox.stub().resolves(undefined);
|
|
601
|
+
let incoming;
|
|
602
|
+
wechaty.on('call', (c) => { incoming = c; });
|
|
603
|
+
puppet.emit('call', {
|
|
604
|
+
callId: CALL_ID,
|
|
605
|
+
signal: PUPPET.types.CallSignal.Invite,
|
|
606
|
+
contactId: CALLER_ID,
|
|
607
|
+
timestamp: 1,
|
|
608
|
+
});
|
|
609
|
+
await flush();
|
|
610
|
+
t.ok(incoming, 'should receive incoming call');
|
|
611
|
+
t.ok(wechaty.__callPool.has(CALL_ID), 'pool should contain call before reject');
|
|
612
|
+
let endedEmitted = false;
|
|
613
|
+
let botEndedCount = 0;
|
|
614
|
+
incoming.on('ended', () => { endedEmitted = true; });
|
|
615
|
+
wechaty.on('call-ended', (c) => { if (c.id === CALL_ID)
|
|
616
|
+
botEndedCount++; });
|
|
617
|
+
await incoming.reject('busy');
|
|
618
|
+
t.ok(endedEmitted, "object 'ended' should fire after local reject()");
|
|
619
|
+
t.equal(botEndedCount, 1, "bot 'call-ended' should fire exactly once after local reject()");
|
|
620
|
+
t.notOk(wechaty.__callPool.has(CALL_ID), 'call should be evicted from pool after reject()');
|
|
621
|
+
await wechaty.stop();
|
|
622
|
+
sandbox.restore();
|
|
623
|
+
});
|
|
624
|
+
test("local cancel() evicts from pool and emits object 'ended' + bot 'call-ended'", async (t) => {
|
|
625
|
+
const { puppet, wechaty } = buildWechaty();
|
|
626
|
+
await startAndLogin(puppet, wechaty);
|
|
627
|
+
const CALL_ID = 'call-id-local-cancel';
|
|
628
|
+
const PEER_ID = 'peer-local-cancel';
|
|
629
|
+
puppet.callInvite = sandbox.stub().resolves(CALL_ID);
|
|
630
|
+
stubCallPayload(puppet, (id) => ({
|
|
631
|
+
id,
|
|
632
|
+
starter: 'bot-self',
|
|
633
|
+
participants: ['bot-self', PEER_ID],
|
|
634
|
+
media: PUPPET.types.CallMediaType.Audio,
|
|
635
|
+
startTime: 1,
|
|
636
|
+
}));
|
|
637
|
+
puppet.callCancel = sandbox.stub().resolves(undefined);
|
|
638
|
+
const contact = wechaty.Contact.load(PEER_ID);
|
|
639
|
+
const call = await contact.call();
|
|
640
|
+
t.ok(wechaty.__callPool.has(CALL_ID), 'pool should contain call before cancel');
|
|
641
|
+
let endedEmitted = false;
|
|
642
|
+
let botEndedCount = 0;
|
|
643
|
+
call.on('ended', () => { endedEmitted = true; });
|
|
644
|
+
wechaty.on('call-ended', (c) => { if (c.id === CALL_ID)
|
|
645
|
+
botEndedCount++; });
|
|
646
|
+
await call.cancel();
|
|
647
|
+
t.ok(endedEmitted, "object 'ended' should fire after local cancel()");
|
|
648
|
+
t.equal(botEndedCount, 1, "bot 'call-ended' should fire exactly once after local cancel()");
|
|
649
|
+
t.notOk(wechaty.__callPool.has(CALL_ID), 'call should be evicted from pool after cancel()');
|
|
650
|
+
await wechaty.stop();
|
|
651
|
+
sandbox.restore();
|
|
652
|
+
});
|
|
653
|
+
test("local hangup() evicts from pool and emits object 'ended' + bot 'call-ended'", async (t) => {
|
|
654
|
+
const { puppet, wechaty } = buildWechaty();
|
|
655
|
+
await startAndLogin(puppet, wechaty);
|
|
656
|
+
const CALL_ID = 'call-id-local-hangup';
|
|
657
|
+
const PEER_ID = 'peer-local-hangup';
|
|
658
|
+
puppet.callInvite = sandbox.stub().resolves(CALL_ID);
|
|
659
|
+
stubCallPayload(puppet, (id) => ({
|
|
660
|
+
id,
|
|
661
|
+
starter: 'bot-self',
|
|
662
|
+
participants: ['bot-self', PEER_ID],
|
|
663
|
+
media: PUPPET.types.CallMediaType.Audio,
|
|
664
|
+
startTime: 1,
|
|
665
|
+
}));
|
|
666
|
+
puppet.callHangup = sandbox.stub().resolves(undefined);
|
|
667
|
+
const contact = wechaty.Contact.load(PEER_ID);
|
|
668
|
+
const call = await contact.call();
|
|
669
|
+
puppet.emit('call', {
|
|
670
|
+
callId: CALL_ID,
|
|
671
|
+
signal: PUPPET.types.CallSignal.Accept,
|
|
672
|
+
contactId: PEER_ID,
|
|
673
|
+
timestamp: 2,
|
|
674
|
+
});
|
|
675
|
+
await flush();
|
|
676
|
+
t.equal(call.status(), 'connected', 'prerequisite: call should be connected');
|
|
677
|
+
t.ok(wechaty.__callPool.has(CALL_ID), 'pool should contain call before hangup');
|
|
678
|
+
let endedEmitted = false;
|
|
679
|
+
let botEndedCount = 0;
|
|
680
|
+
call.on('ended', () => { endedEmitted = true; });
|
|
681
|
+
wechaty.on('call-ended', (c) => { if (c.id === CALL_ID)
|
|
682
|
+
botEndedCount++; });
|
|
683
|
+
await call.hangup('done');
|
|
684
|
+
t.ok(endedEmitted, "object 'ended' should fire after local hangup()");
|
|
685
|
+
t.equal(botEndedCount, 1, "bot 'call-ended' should fire exactly once after local hangup()");
|
|
686
|
+
t.notOk(wechaty.__callPool.has(CALL_ID), 'call should be evicted from pool after hangup()');
|
|
687
|
+
await wechaty.stop();
|
|
688
|
+
sandbox.restore();
|
|
689
|
+
});
|
|
690
|
+
// ---------------------------------------------------------------------------
|
|
691
|
+
// 14. dirty(Call) refreshes user-layer __payload
|
|
692
|
+
// ---------------------------------------------------------------------------
|
|
693
|
+
test('dirty(Call) refreshes user-layer payload so getters see new value', async (t) => {
|
|
694
|
+
const { puppet, wechaty } = buildWechaty();
|
|
695
|
+
await startAndLogin(puppet, wechaty);
|
|
696
|
+
const CALL_ID = 'call-id-dirty';
|
|
697
|
+
const CALLER_ID = 'caller-dirty';
|
|
698
|
+
// First pull returns Audio; subsequent pulls return Video, simulating a
|
|
699
|
+
// server-side media switch (e.g. add() upgraded the call).
|
|
700
|
+
let pulls = 0;
|
|
701
|
+
puppet.callPayload = sandbox.stub().callsFake(async (id) => {
|
|
702
|
+
pulls++;
|
|
703
|
+
await new Promise(setImmediate);
|
|
704
|
+
return {
|
|
705
|
+
id,
|
|
706
|
+
starter: CALLER_ID,
|
|
707
|
+
participants: [CALLER_ID, 'bot-self'],
|
|
708
|
+
media: pulls === 1 ? PUPPET.types.CallMediaType.Audio : PUPPET.types.CallMediaType.Video,
|
|
709
|
+
startTime: 1,
|
|
710
|
+
};
|
|
711
|
+
});
|
|
712
|
+
puppet.callPayloadDirty = sandbox.stub().resolves(undefined);
|
|
713
|
+
let incoming;
|
|
714
|
+
wechaty.on('call', (c) => { incoming = c; });
|
|
715
|
+
puppet.emit('call', {
|
|
716
|
+
callId: CALL_ID,
|
|
717
|
+
signal: PUPPET.types.CallSignal.Invite,
|
|
718
|
+
contactId: CALLER_ID,
|
|
719
|
+
timestamp: 1,
|
|
720
|
+
});
|
|
721
|
+
await flush();
|
|
722
|
+
t.ok(incoming, 'should receive incoming call');
|
|
723
|
+
t.equal(incoming.media(), PUPPET.types.CallMediaType.Audio, 'initial media should be Audio');
|
|
724
|
+
puppet.emit('dirty', {
|
|
725
|
+
payloadType: PUPPET.types.Payload.Call,
|
|
726
|
+
payloadId: CALL_ID,
|
|
727
|
+
});
|
|
728
|
+
await flush();
|
|
729
|
+
t.equal(incoming.media(), PUPPET.types.CallMediaType.Video, 'media should reflect refreshed payload after dirty');
|
|
730
|
+
t.ok(puppet.callPayloadDirty.called, 'puppet.callPayloadDirty should be invoked');
|
|
731
|
+
await wechaty.stop();
|
|
732
|
+
sandbox.restore();
|
|
733
|
+
});
|
|
734
|
+
//# sourceMappingURL=call.spec.js.map
|