@coventia/softphone 0.1.0 → 0.2.1

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 (54) hide show
  1. package/dist/{call-BTYlT2SU.d.cts → call-VKq1I8yP.d.cts} +158 -5
  2. package/dist/{call-BTYlT2SU.d.ts → call-VKq1I8yP.d.ts} +158 -5
  3. package/dist/{chunk-N2N2EQQJ.js → chunk-7R6LOOAI.js} +4 -2
  4. package/dist/chunk-7R6LOOAI.js.map +1 -0
  5. package/dist/{chunk-5BGEHMPP.js → chunk-EYEDPD7Y.js} +146 -20
  6. package/dist/chunk-EYEDPD7Y.js.map +1 -0
  7. package/dist/{chunk-3V2L6UTK.js → chunk-LMJXX4YM.js} +17 -3
  8. package/dist/chunk-LMJXX4YM.js.map +1 -0
  9. package/dist/element/index.d.ts +162 -7
  10. package/dist/element/index.js +866 -421
  11. package/dist/element/index.js.map +1 -1
  12. package/dist/{engine-D9nbeY08.d.cts → engine-BlQBo2Ys.d.cts} +1 -1
  13. package/dist/{engine-Doa6Brkm.d.ts → engine-C0BOkIcW.d.ts} +1 -1
  14. package/dist/index.cjs +586 -28
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +5 -5
  17. package/dist/index.d.ts +5 -5
  18. package/dist/index.js +335 -8
  19. package/dist/index.js.map +1 -1
  20. package/dist/react/index.cjs +15 -0
  21. package/dist/react/index.cjs.map +1 -1
  22. package/dist/react/index.d.cts +14 -3
  23. package/dist/react/index.d.ts +14 -3
  24. package/dist/react/index.js +2 -2
  25. package/dist/{softphone-DhuhcKCy.d.cts → softphone-Cyq3hiIM.d.ts} +17 -2
  26. package/dist/{softphone-DHmhotbf.d.ts → softphone-DlGV12Xp.d.cts} +17 -2
  27. package/dist/softphone.css +477 -35
  28. package/dist/{sources-N4BOEDRL.js → sources-IRKNQPWW.js} +108 -5
  29. package/dist/sources-IRKNQPWW.js.map +1 -0
  30. package/dist/testing/index.cjs +41 -5
  31. package/dist/testing/index.cjs.map +1 -1
  32. package/dist/testing/index.d.cts +8 -2
  33. package/dist/testing/index.d.ts +8 -2
  34. package/dist/testing/index.js +41 -5
  35. package/dist/testing/index.js.map +1 -1
  36. package/dist/ui/index.cjs +846 -410
  37. package/dist/ui/index.cjs.map +1 -1
  38. package/dist/ui/index.d.cts +17 -6
  39. package/dist/ui/index.d.ts +17 -6
  40. package/dist/ui/index.js +878 -456
  41. package/dist/ui/index.js.map +1 -1
  42. package/dist/zadarma/index.cjs +144 -18
  43. package/dist/zadarma/index.cjs.map +1 -1
  44. package/dist/zadarma/index.d.cts +2 -2
  45. package/dist/zadarma/index.d.ts +2 -2
  46. package/dist/zadarma/index.js +1 -1
  47. package/dist/zadarma-engine-L5DEFXUS.js +5 -0
  48. package/dist/{zadarma-engine-DN2SZSQ3.js.map → zadarma-engine-L5DEFXUS.js.map} +1 -1
  49. package/package.json +1 -1
  50. package/dist/chunk-3V2L6UTK.js.map +0 -1
  51. package/dist/chunk-5BGEHMPP.js.map +0 -1
  52. package/dist/chunk-N2N2EQQJ.js.map +0 -1
  53. package/dist/sources-N4BOEDRL.js.map +0 -1
  54. package/dist/zadarma-engine-DN2SZSQ3.js +0 -5
@@ -7,14 +7,16 @@ function nextId(prefix) {
7
7
  return `${prefix}-${seq}`;
8
8
  }
9
9
  var MockEngineCall = class _MockEngineCall {
10
- constructor(id, direction, remoteNumber) {
10
+ constructor(id, direction, remoteNumber, onConsultCreated) {
11
11
  this.id = id;
12
12
  this.direction = direction;
13
13
  this.remoteNumber = remoteNumber;
14
+ this.onConsultCreated = onConsultCreated;
14
15
  }
15
16
  id;
16
17
  direction;
17
18
  remoteNumber;
19
+ onConsultCreated;
18
20
  #listeners = /* @__PURE__ */ new Set();
19
21
  #ended = false;
20
22
  onEvent(listener) {
@@ -56,15 +58,20 @@ var MockEngineCall = class _MockEngineCall {
56
58
  this.fire({ type: "ended", reason: "transferred" });
57
59
  }
58
60
  async startConsult(target) {
59
- return new _MockEngineCall(nextId("consult"), "outbound", target);
61
+ const consult = new _MockEngineCall(nextId("consult"), "outbound", target);
62
+ this.onConsultCreated?.(consult);
63
+ return consult;
60
64
  }
61
- async completeTransfer(_consult) {
65
+ async completeTransfer(consult) {
66
+ consult.fire({ type: "ended", reason: "transferred" });
62
67
  this.fire({ type: "ended", reason: "transferred" });
63
68
  }
64
69
  };
65
70
  var MockProvider = class {
66
71
  #state = "stopped";
67
72
  #current = null;
73
+ /** The consult leg from the current call's own `startConsult`, if any — see `simulateConsult*`. */
74
+ #currentConsult = null;
68
75
  #incoming = /* @__PURE__ */ new Set();
69
76
  #stateListeners = /* @__PURE__ */ new Set();
70
77
  #errorListeners = /* @__PURE__ */ new Set();
@@ -91,15 +98,21 @@ var MockProvider = class {
91
98
  return () => this.#errorListeners.delete(listener);
92
99
  }
93
100
  async placeCall(destination, _options) {
94
- const call = new MockEngineCall(nextId("out"), "outbound", destination);
101
+ const call = new MockEngineCall(nextId("out"), "outbound", destination, (c) => {
102
+ this.#currentConsult = c;
103
+ });
95
104
  this.#current = call;
105
+ this.#currentConsult = null;
96
106
  return call;
97
107
  }
98
108
  // ── The simulation surface (SPEC §5.7) ────────────────────────────────────
99
109
  /** An inbound INVITE lands. Fires the engine's `onIncoming` — the Softphone raises IncomingCall. */
100
110
  simulateIncoming(options) {
101
- const call = new MockEngineCall(nextId("in"), "inbound", options.from);
111
+ const call = new MockEngineCall(nextId("in"), "inbound", options.from, (c) => {
112
+ this.#currentConsult = c;
113
+ });
102
114
  this.#current = call;
115
+ this.#currentConsult = null;
103
116
  for (const listener of [...this.#incoming]) listener(call);
104
117
  }
105
118
  /** The remote answered our outbound call (or a provisional 180/183 first, if `progress` is set). */
@@ -152,6 +165,23 @@ var MockProvider = class {
152
165
  simulateNetworkRecover() {
153
166
  this.#requireCurrent("simulateNetworkRecover").fire({ type: "reconnected" });
154
167
  }
168
+ // ── The consult leg (attended transfer, SPEC §5.6) ─────────────────────────
169
+ //
170
+ // A `startConsult()` call creates a SECOND `MockEngineCall`, tracked separately from `#current` —
171
+ // the methods above all target the main call, so scripting the consult leg's own outcome (it rings,
172
+ // it's answered, or it fails on its own) needs its own surface rather than overloading `#current`.
173
+ /** The target of the current call's `startConsult` is ringing. Optional early media. */
174
+ simulateConsultProgress(kind = "ringing") {
175
+ this.#requireCurrentConsult("simulateConsultProgress").fire({ type: "progress", response: kind });
176
+ }
177
+ /** The consult target answered — the agent can now `completeTransfer()` or `cancelConsult()`. */
178
+ simulateConsultAnswer() {
179
+ this.#requireCurrentConsult("simulateConsultAnswer").fire({ type: "accepted" });
180
+ }
181
+ /** The consult leg ended on its own — busy, no answer, rejected, or a generic failure. */
182
+ simulateConsultHangup(reason = "no-answer") {
183
+ this.#requireCurrentConsult("simulateConsultHangup").fire({ type: "ended", reason });
184
+ }
155
185
  // ── internals ─────────────────────────────────────────────────────────────
156
186
  #setState(state) {
157
187
  if (state === this.#state) return;
@@ -164,6 +194,12 @@ var MockProvider = class {
164
194
  }
165
195
  return this.#current;
166
196
  }
197
+ #requireCurrentConsult(op) {
198
+ if (!this.#currentConsult || this.#currentConsult.ended) {
199
+ throw new Error(`MockProvider.${op}() called with no active consult leg.`);
200
+ }
201
+ return this.#currentConsult;
202
+ }
167
203
  };
168
204
 
169
205
  // src/testing/index.ts
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/testing/mock.ts","../../src/testing/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { CallEngine, EngineCall, EngineCallEvent } from '../core/engine.js';\r\nimport type { CallDirection, CallOptions, SoftphoneState } from '../core/types.js';\r\nimport type { HangupReason } from '../core/call.js';\r\nimport { createError, type SoftphoneError, type SoftphoneErrorCode } from '../core/errors.js';\r\n\r\n/**\r\n * MockProvider — a `CallEngine` that scripts a full call lifecycle with no Zadarma, no microphone, no\r\n * network and no real phone call (SPEC §5.7).\r\n *\r\n * It is DOUBLE-DUTY, and that is the point:\r\n * - the engine behind the playgrounds, so a full call (ring → answer → hold → transfer → hang up)\r\n * is drivable in a browser instantly;\r\n * - the test suite's fake.\r\n * The SAME object both ways, kept honest by the CallEngine contract suite — so what the playground\r\n * exercises is what the real engine must also satisfy.\r\n *\r\n * Everything is explicit and deterministic: nothing auto-advances on a timer. The host (a test, or a\r\n * playground button) decides when the remote answers, holds, drops the network, or hangs up. That is\r\n * what makes it usable as a test fake and as a demo without divergence.\r\n */\r\n\r\nlet seq = 0;\r\nfunction nextId(prefix: string): string {\r\n seq += 1;\r\n return `${prefix}-${seq}`;\r\n}\r\n\r\nclass MockEngineCall implements EngineCall {\r\n readonly #listeners = new Set<(event: EngineCallEvent) => void>();\r\n #ended = false;\r\n\r\n constructor(\r\n readonly id: string,\r\n readonly direction: CallDirection,\r\n readonly remoteNumber: string,\r\n ) {}\r\n\r\n onEvent(listener: (event: EngineCallEvent) => void): () => void {\r\n this.#listeners.add(listener);\r\n return () => this.#listeners.delete(listener);\r\n }\r\n\r\n /** @internal — fire a raw engine signal, asynchronously, as a real transport would. */\r\n fire(event: EngineCallEvent): void {\r\n if (this.#ended) return;\r\n if (event.type === 'ended') this.#ended = true;\r\n queueMicrotask(() => {\r\n for (const listener of [...this.#listeners]) listener(event);\r\n });\r\n }\r\n\r\n get ended(): boolean {\r\n return this.#ended;\r\n }\r\n\r\n // ── Commands (local side) ─────────────────────────────────────────────────\r\n async answer(): Promise<void> {\r\n // Inbound: the local user accepted. Media comes up.\r\n this.fire({ type: 'accepted' });\r\n }\r\n async reject(): Promise<void> {\r\n this.fire({ type: 'ended', reason: 'rejected' });\r\n }\r\n async hangup(): Promise<void> {\r\n this.fire({ type: 'ended', reason: 'local' });\r\n }\r\n mute(_muted: boolean): void {\r\n // The engine only carries the effect; Call owns the mute state + event.\r\n }\r\n async hold(): Promise<void> {\r\n this.fire({ type: 'held', originator: 'local' });\r\n }\r\n async unhold(): Promise<void> {\r\n this.fire({ type: 'unheld', originator: 'local' });\r\n }\r\n sendDigits(_digits: string): void {\r\n // A real engine sends RFC 2833 / SIP INFO; the mock has nothing to send to.\r\n }\r\n async transfer(_target: string): Promise<void> {\r\n this.fire({ type: 'ended', reason: 'transferred' });\r\n }\r\n async startConsult(target: string): Promise<EngineCall> {\r\n return new MockEngineCall(nextId('consult'), 'outbound', target);\r\n }\r\n async completeTransfer(_consult: EngineCall): Promise<void> {\r\n this.fire({ type: 'ended', reason: 'transferred' });\r\n }\r\n}\r\n\r\nexport interface SimulateIncomingOptions {\r\n readonly from: string;\r\n /**\r\n * No `displayName` here, deliberately. `EngineCall` carries only `remoteNumber` — the seam has no\r\n * display-name channel, because caller identity is resolved from CONTACTS, not from the SIP `From`\r\n * header (`CallImpl.setRemoteContact`). Until then `remote.displayName` IS the number.\r\n *\r\n * Accepting a name here would let consumers write passing tests for behaviour the real engine can\r\n * never produce. The mock must not outrun the seam it stands in for.\r\n */\r\n}\r\n\r\nexport class MockProvider implements CallEngine {\r\n #state: SoftphoneState = 'stopped';\r\n #current: MockEngineCall | null = null;\r\n readonly #incoming = new Set<(call: EngineCall) => void>();\r\n readonly #stateListeners = new Set<(state: SoftphoneState) => void>();\r\n readonly #errorListeners = new Set<(error: SoftphoneError) => void>();\r\n\r\n get state(): SoftphoneState {\r\n return this.#state;\r\n }\r\n\r\n async start(): Promise<void> {\r\n this.#setState('online');\r\n }\r\n async stop(): Promise<void> {\r\n this.#current = null;\r\n this.#setState('stopped');\r\n }\r\n\r\n onIncoming(listener: (call: EngineCall) => void): () => void {\r\n this.#incoming.add(listener);\r\n return () => this.#incoming.delete(listener);\r\n }\r\n onStateChange(listener: (state: SoftphoneState) => void): () => void {\r\n this.#stateListeners.add(listener);\r\n return () => this.#stateListeners.delete(listener);\r\n }\r\n onError(listener: (error: SoftphoneError) => void): () => void {\r\n this.#errorListeners.add(listener);\r\n return () => this.#errorListeners.delete(listener);\r\n }\r\n\r\n async placeCall(destination: string, _options?: CallOptions): Promise<EngineCall> {\r\n const call = new MockEngineCall(nextId('out'), 'outbound', destination);\r\n this.#current = call;\r\n return call;\r\n }\r\n\r\n // ── The simulation surface (SPEC §5.7) ────────────────────────────────────\r\n\r\n /** An inbound INVITE lands. Fires the engine's `onIncoming` — the Softphone raises IncomingCall. */\r\n simulateIncoming(options: SimulateIncomingOptions): void {\r\n const call = new MockEngineCall(nextId('in'), 'inbound', options.from);\r\n this.#current = call;\r\n for (const listener of [...this.#incoming]) listener(call);\r\n }\r\n\r\n /** The remote answered our outbound call (or a provisional 180/183 first, if `progress` is set). */\r\n simulateRemoteAnswer(): void {\r\n this.#requireCurrent('simulateRemoteAnswer').fire({ type: 'accepted' });\r\n }\r\n\r\n /** Outbound provisional response — remote is ringing. Optional early media. */\r\n simulateProgress(kind: 'ringing' | 'early' = 'ringing'): void {\r\n this.#requireCurrent('simulateProgress').fire({ type: 'progress', response: kind });\r\n }\r\n\r\n /**\r\n * Remote media arrives — what `ontrack` produces on a real call.\r\n *\r\n * Added after the SDK shipped with NO audio at all: `core` hands the stream out and the UI attaches\r\n * it, but nothing did, and no test could see that because the mock had no way to produce a stream.\r\n * The seam always carried one (`EngineCallEvent` has `{ type: 'stream' }`); only the driver was\r\n * missing — unlike `displayName`, which the seam genuinely cannot carry (see SimulateIncomingOptions).\r\n *\r\n * The default is a stand-in object: jsdom has no `MediaStream`, and every consumer only ever hands\r\n * this to `srcObject`.\r\n */\r\n simulateRemoteStream(stream: MediaStream = {} as MediaStream): void {\r\n this.#requireCurrent('simulateRemoteStream').fire({ type: 'stream', stream });\r\n }\r\n\r\n simulateRemoteHold(): void {\r\n this.#requireCurrent('simulateRemoteHold').fire({ type: 'held', originator: 'remote' });\r\n }\r\n simulateRemoteUnhold(): void {\r\n this.#requireCurrent('simulateRemoteUnhold').fire({ type: 'unheld', originator: 'remote' });\r\n }\r\n\r\n simulateRemoteHangup(reason: HangupReason = 'remote'): void {\r\n this.#requireCurrent('simulateRemoteHangup').fire({ type: 'ended', reason });\r\n }\r\n\r\n /**\r\n * Raise any catalogued failure, exactly as a real engine would.\r\n *\r\n * The point is the two TREATMENTS: pass a fatal code (`net/registration-failed`) and the widget\r\n * replaces the dialpad with a gate that cannot be dismissed; pass a non-fatal one (`call/missed`)\r\n * and it shows a muted line under the number that the next keypress clears. A playground wants\r\n * both on a button, and neither can be reached by driving a mock call to a happy ending.\r\n */\r\n simulateError(code: SoftphoneErrorCode, cause?: unknown): void {\r\n const error = createError(code, cause);\r\n for (const listener of [...this.#errorListeners]) listener(error);\r\n }\r\n\r\n /** Media lost — the engine begins an ICE restart. The call is NOT over. */\r\n simulateNetworkDrop(): void {\r\n this.#requireCurrent('simulateNetworkDrop').fire({ type: 'reconnecting' });\r\n }\r\n simulateNetworkRecover(): void {\r\n this.#requireCurrent('simulateNetworkRecover').fire({ type: 'reconnected' });\r\n }\r\n\r\n // ── internals ─────────────────────────────────────────────────────────────\r\n #setState(state: SoftphoneState): void {\r\n if (state === this.#state) return;\r\n this.#state = state;\r\n for (const listener of [...this.#stateListeners]) listener(state);\r\n }\r\n\r\n #requireCurrent(op: string): MockEngineCall {\r\n if (!this.#current || this.#current.ended) {\r\n throw new Error(`MockProvider.${op}() called with no active call.`);\r\n }\r\n return this.#current;\r\n }\r\n}\r\n","/**\r\n * @coventia/softphone/testing — MockProvider and test helpers.\r\n *\r\n * Its own entry point so it never lands in a consumer's production bundle. See SPEC §5.7.\r\n */\r\nimport type { TokenSource, VoiceToken } from '../core/types.js';\r\n\r\nexport { MockProvider } from './mock.js';\r\nexport type { SimulateIncomingOptions } from './mock.js';\r\n\r\n/**\r\n * A TokenSource that returns a fixed, far-future fake token.\r\n *\r\n * `SoftphoneConfig.tokenSource` is required (SPEC §6.1) because the real Zadarma path needs it — but\r\n * the mock engine never mints credentials, so tests and playgrounds need something to satisfy the\r\n * type without a backend. This is that something. Never ship it.\r\n */\r\nexport function mockTokenSource(): TokenSource {\r\n return {\r\n async getToken(): Promise<VoiceToken> {\r\n return {\r\n token: 'mock-voice-token',\r\n identity: 'mock-user',\r\n // Far future; the mock does not refresh.\r\n expiresAt: new Date(4102444800000), // 2100-01-01\r\n };\r\n },\r\n };\r\n}\r\n"]}
1
+ {"version":3,"sources":["../../src/testing/mock.ts","../../src/testing/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { CallEngine, EngineCall, EngineCallEvent } from '../core/engine.js';\r\nimport type { CallDirection, CallOptions, SoftphoneState } from '../core/types.js';\r\nimport type { HangupReason } from '../core/call.js';\r\nimport { createError, type SoftphoneError, type SoftphoneErrorCode } from '../core/errors.js';\r\n\r\n/**\r\n * MockProvider — a `CallEngine` that scripts a full call lifecycle with no Zadarma, no microphone, no\r\n * network and no real phone call (SPEC §5.7).\r\n *\r\n * It is DOUBLE-DUTY, and that is the point:\r\n * - the engine behind the playgrounds, so a full call (ring → answer → hold → transfer → hang up)\r\n * is drivable in a browser instantly;\r\n * - the test suite's fake.\r\n * The SAME object both ways, kept honest by the CallEngine contract suite — so what the playground\r\n * exercises is what the real engine must also satisfy.\r\n *\r\n * Everything is explicit and deterministic: nothing auto-advances on a timer. The host (a test, or a\r\n * playground button) decides when the remote answers, holds, drops the network, or hangs up. That is\r\n * what makes it usable as a test fake and as a demo without divergence.\r\n */\r\n\r\nlet seq = 0;\r\nfunction nextId(prefix: string): string {\r\n seq += 1;\r\n return `${prefix}-${seq}`;\r\n}\r\n\r\nclass MockEngineCall implements EngineCall {\r\n readonly #listeners = new Set<(event: EngineCallEvent) => void>();\r\n #ended = false;\r\n\r\n constructor(\r\n readonly id: string,\r\n readonly direction: CallDirection,\r\n readonly remoteNumber: string,\r\n /** Notified with the new leg when `startConsult` creates one — lets `MockProvider` script it. */\r\n private readonly onConsultCreated?: (consult: MockEngineCall) => void,\r\n ) {}\r\n\r\n onEvent(listener: (event: EngineCallEvent) => void): () => void {\r\n this.#listeners.add(listener);\r\n return () => this.#listeners.delete(listener);\r\n }\r\n\r\n /** @internal — fire a raw engine signal, asynchronously, as a real transport would. */\r\n fire(event: EngineCallEvent): void {\r\n if (this.#ended) return;\r\n if (event.type === 'ended') this.#ended = true;\r\n queueMicrotask(() => {\r\n for (const listener of [...this.#listeners]) listener(event);\r\n });\r\n }\r\n\r\n get ended(): boolean {\r\n return this.#ended;\r\n }\r\n\r\n // ── Commands (local side) ─────────────────────────────────────────────────\r\n async answer(): Promise<void> {\r\n // Inbound: the local user accepted. Media comes up.\r\n this.fire({ type: 'accepted' });\r\n }\r\n async reject(): Promise<void> {\r\n this.fire({ type: 'ended', reason: 'rejected' });\r\n }\r\n async hangup(): Promise<void> {\r\n this.fire({ type: 'ended', reason: 'local' });\r\n }\r\n mute(_muted: boolean): void {\r\n // The engine only carries the effect; Call owns the mute state + event.\r\n }\r\n async hold(): Promise<void> {\r\n this.fire({ type: 'held', originator: 'local' });\r\n }\r\n async unhold(): Promise<void> {\r\n this.fire({ type: 'unheld', originator: 'local' });\r\n }\r\n sendDigits(_digits: string): void {\r\n // A real engine sends RFC 2833 / SIP INFO; the mock has nothing to send to.\r\n }\r\n async transfer(_target: string): Promise<void> {\r\n this.fire({ type: 'ended', reason: 'transferred' });\r\n }\r\n async startConsult(target: string): Promise<EngineCall> {\r\n const consult = new MockEngineCall(nextId('consult'), 'outbound', target);\r\n this.onConsultCreated?.(consult);\r\n return consult;\r\n }\r\n async completeTransfer(consult: EngineCall): Promise<void> {\r\n // Both legs end — the real engine's REFER…Replaces bridges caller↔consult-target and drops both\r\n // the original session and the consult session (`call-impl.ts`'s own comment on `completeTransfer`).\r\n (consult as MockEngineCall).fire({ type: 'ended', reason: 'transferred' });\r\n this.fire({ type: 'ended', reason: 'transferred' });\r\n }\r\n}\r\n\r\nexport interface SimulateIncomingOptions {\r\n readonly from: string;\r\n /**\r\n * No `displayName` here, deliberately. `EngineCall` carries only `remoteNumber` — the seam has no\r\n * display-name channel, because caller identity is resolved from CONTACTS, not from the SIP `From`\r\n * header (`CallImpl.setRemoteContact`). Until then `remote.displayName` IS the number.\r\n *\r\n * Accepting a name here would let consumers write passing tests for behaviour the real engine can\r\n * never produce. The mock must not outrun the seam it stands in for.\r\n */\r\n}\r\n\r\nexport class MockProvider implements CallEngine {\r\n #state: SoftphoneState = 'stopped';\r\n #current: MockEngineCall | null = null;\r\n /** The consult leg from the current call's own `startConsult`, if any — see `simulateConsult*`. */\r\n #currentConsult: MockEngineCall | null = null;\r\n readonly #incoming = new Set<(call: EngineCall) => void>();\r\n readonly #stateListeners = new Set<(state: SoftphoneState) => void>();\r\n readonly #errorListeners = new Set<(error: SoftphoneError) => void>();\r\n\r\n get state(): SoftphoneState {\r\n return this.#state;\r\n }\r\n\r\n async start(): Promise<void> {\r\n this.#setState('online');\r\n }\r\n async stop(): Promise<void> {\r\n this.#current = null;\r\n this.#setState('stopped');\r\n }\r\n\r\n onIncoming(listener: (call: EngineCall) => void): () => void {\r\n this.#incoming.add(listener);\r\n return () => this.#incoming.delete(listener);\r\n }\r\n onStateChange(listener: (state: SoftphoneState) => void): () => void {\r\n this.#stateListeners.add(listener);\r\n return () => this.#stateListeners.delete(listener);\r\n }\r\n onError(listener: (error: SoftphoneError) => void): () => void {\r\n this.#errorListeners.add(listener);\r\n return () => this.#errorListeners.delete(listener);\r\n }\r\n\r\n async placeCall(destination: string, _options?: CallOptions): Promise<EngineCall> {\r\n const call = new MockEngineCall(nextId('out'), 'outbound', destination, (c) => {\r\n this.#currentConsult = c;\r\n });\r\n this.#current = call;\r\n this.#currentConsult = null;\r\n return call;\r\n }\r\n\r\n // ── The simulation surface (SPEC §5.7) ────────────────────────────────────\r\n\r\n /** An inbound INVITE lands. Fires the engine's `onIncoming` — the Softphone raises IncomingCall. */\r\n simulateIncoming(options: SimulateIncomingOptions): void {\r\n const call = new MockEngineCall(nextId('in'), 'inbound', options.from, (c) => {\r\n this.#currentConsult = c;\r\n });\r\n this.#current = call;\r\n this.#currentConsult = null;\r\n for (const listener of [...this.#incoming]) listener(call);\r\n }\r\n\r\n /** The remote answered our outbound call (or a provisional 180/183 first, if `progress` is set). */\r\n simulateRemoteAnswer(): void {\r\n this.#requireCurrent('simulateRemoteAnswer').fire({ type: 'accepted' });\r\n }\r\n\r\n /** Outbound provisional response — remote is ringing. Optional early media. */\r\n simulateProgress(kind: 'ringing' | 'early' = 'ringing'): void {\r\n this.#requireCurrent('simulateProgress').fire({ type: 'progress', response: kind });\r\n }\r\n\r\n /**\r\n * Remote media arrives — what `ontrack` produces on a real call.\r\n *\r\n * Added after the SDK shipped with NO audio at all: `core` hands the stream out and the UI attaches\r\n * it, but nothing did, and no test could see that because the mock had no way to produce a stream.\r\n * The seam always carried one (`EngineCallEvent` has `{ type: 'stream' }`); only the driver was\r\n * missing — unlike `displayName`, which the seam genuinely cannot carry (see SimulateIncomingOptions).\r\n *\r\n * The default is a stand-in object: jsdom has no `MediaStream`, and every consumer only ever hands\r\n * this to `srcObject`.\r\n */\r\n simulateRemoteStream(stream: MediaStream = {} as MediaStream): void {\r\n this.#requireCurrent('simulateRemoteStream').fire({ type: 'stream', stream });\r\n }\r\n\r\n simulateRemoteHold(): void {\r\n this.#requireCurrent('simulateRemoteHold').fire({ type: 'held', originator: 'remote' });\r\n }\r\n simulateRemoteUnhold(): void {\r\n this.#requireCurrent('simulateRemoteUnhold').fire({ type: 'unheld', originator: 'remote' });\r\n }\r\n\r\n simulateRemoteHangup(reason: HangupReason = 'remote'): void {\r\n this.#requireCurrent('simulateRemoteHangup').fire({ type: 'ended', reason });\r\n }\r\n\r\n /**\r\n * Raise any catalogued failure, exactly as a real engine would.\r\n *\r\n * The point is the two TREATMENTS: pass a fatal code (`net/registration-failed`) and the widget\r\n * replaces the dialpad with a gate that cannot be dismissed; pass a non-fatal one (`call/missed`)\r\n * and it shows a muted line under the number that the next keypress clears. A playground wants\r\n * both on a button, and neither can be reached by driving a mock call to a happy ending.\r\n */\r\n simulateError(code: SoftphoneErrorCode, cause?: unknown): void {\r\n const error = createError(code, cause);\r\n for (const listener of [...this.#errorListeners]) listener(error);\r\n }\r\n\r\n /** Media lost — the engine begins an ICE restart. The call is NOT over. */\r\n simulateNetworkDrop(): void {\r\n this.#requireCurrent('simulateNetworkDrop').fire({ type: 'reconnecting' });\r\n }\r\n simulateNetworkRecover(): void {\r\n this.#requireCurrent('simulateNetworkRecover').fire({ type: 'reconnected' });\r\n }\r\n\r\n // ── The consult leg (attended transfer, SPEC §5.6) ─────────────────────────\r\n //\r\n // A `startConsult()` call creates a SECOND `MockEngineCall`, tracked separately from `#current` —\r\n // the methods above all target the main call, so scripting the consult leg's own outcome (it rings,\r\n // it's answered, or it fails on its own) needs its own surface rather than overloading `#current`.\r\n\r\n /** The target of the current call's `startConsult` is ringing. Optional early media. */\r\n simulateConsultProgress(kind: 'ringing' | 'early' = 'ringing'): void {\r\n this.#requireCurrentConsult('simulateConsultProgress').fire({ type: 'progress', response: kind });\r\n }\r\n /** The consult target answered — the agent can now `completeTransfer()` or `cancelConsult()`. */\r\n simulateConsultAnswer(): void {\r\n this.#requireCurrentConsult('simulateConsultAnswer').fire({ type: 'accepted' });\r\n }\r\n /** The consult leg ended on its own — busy, no answer, rejected, or a generic failure. */\r\n simulateConsultHangup(reason: HangupReason = 'no-answer'): void {\r\n this.#requireCurrentConsult('simulateConsultHangup').fire({ type: 'ended', reason });\r\n }\r\n\r\n // ── internals ─────────────────────────────────────────────────────────────\r\n #setState(state: SoftphoneState): void {\r\n if (state === this.#state) return;\r\n this.#state = state;\r\n for (const listener of [...this.#stateListeners]) listener(state);\r\n }\r\n\r\n #requireCurrent(op: string): MockEngineCall {\r\n if (!this.#current || this.#current.ended) {\r\n throw new Error(`MockProvider.${op}() called with no active call.`);\r\n }\r\n return this.#current;\r\n }\r\n\r\n #requireCurrentConsult(op: string): MockEngineCall {\r\n if (!this.#currentConsult || this.#currentConsult.ended) {\r\n throw new Error(`MockProvider.${op}() called with no active consult leg.`);\r\n }\r\n return this.#currentConsult;\r\n }\r\n}\r\n","/**\r\n * @coventia/softphone/testing — MockProvider and test helpers.\r\n *\r\n * Its own entry point so it never lands in a consumer's production bundle. See SPEC §5.7.\r\n */\r\nimport type { TokenSource, VoiceToken } from '../core/types.js';\r\n\r\nexport { MockProvider } from './mock.js';\r\nexport type { SimulateIncomingOptions } from './mock.js';\r\n\r\n/**\r\n * A TokenSource that returns a fixed, far-future fake token.\r\n *\r\n * `SoftphoneConfig.tokenSource` is required (SPEC §6.1) because the real Zadarma path needs it — but\r\n * the mock engine never mints credentials, so tests and playgrounds need something to satisfy the\r\n * type without a backend. This is that something. Never ship it.\r\n */\r\nexport function mockTokenSource(): TokenSource {\r\n return {\r\n async getToken(): Promise<VoiceToken> {\r\n return {\r\n token: 'mock-voice-token',\r\n identity: 'mock-user',\r\n // Far future; the mock does not refresh.\r\n expiresAt: new Date(4102444800000), // 2100-01-01\r\n };\r\n },\r\n };\r\n}\r\n"]}