@leg3ndy/otto-bridge 0.7.4 → 0.7.5

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.
@@ -1190,10 +1190,14 @@ export class NativeMacOSJobExecutor {
1190
1190
  if (!selected) {
1191
1191
  throw new Error(`Nao consegui localizar a conversa do WhatsApp com ${action.contact}.`);
1192
1192
  }
1193
+ const beforeSend = await this.readWhatsAppVisibleConversation(action.contact, 8).catch(() => ({
1194
+ messages: [],
1195
+ summary: "",
1196
+ }));
1193
1197
  await reporter.progress(progressPercent, `Enviando a mensagem para ${action.contact} no WhatsApp`);
1194
1198
  await this.sendWhatsAppMessage(action.text);
1195
1199
  await delay(900);
1196
- const verification = await this.verifyWhatsAppLastMessage(action.text);
1200
+ const verification = await this.verifyWhatsAppLastMessageAgainstBaseline(action.text, beforeSend.messages);
1197
1201
  if (!verification.ok) {
1198
1202
  throw new Error(verification.reason || `Nao consegui confirmar o envio da mensagem para ${action.contact} no WhatsApp.`);
1199
1203
  }
@@ -2619,26 +2623,51 @@ return { messages: messages.slice(-maxMessages) };
2619
2623
  };
2620
2624
  }
2621
2625
  async verifyWhatsAppLastMessage(expectedText) {
2626
+ return this.verifyWhatsAppLastMessageAgainstBaseline(expectedText);
2627
+ }
2628
+ async verifyWhatsAppLastMessageAgainstBaseline(expectedText, previousMessages) {
2622
2629
  const backgroundBrowser = await this.getWhatsAppBackgroundBrowser().catch(() => null);
2623
2630
  if (backgroundBrowser) {
2624
- return backgroundBrowser.verifyLastMessage(expectedText);
2631
+ return backgroundBrowser.verifyLastMessage(expectedText, previousMessages);
2625
2632
  }
2626
- const chat = await this.readWhatsAppVisibleConversation("Contato", 6);
2633
+ const baseline = Array.isArray(previousMessages) ? previousMessages : [];
2634
+ const chat = await this.readWhatsAppVisibleConversation("Contato", Math.max(8, baseline.length + 2));
2627
2635
  if (!chat.messages.length) {
2628
2636
  return {
2629
2637
  ok: false,
2630
2638
  reason: "Nao consegui ler as mensagens visiveis apos o envio no WhatsApp.",
2631
2639
  };
2632
2640
  }
2633
- const normalizedExpected = normalizeText(expectedText).slice(0, 60);
2634
- const matched = chat.messages.some((item) => normalizeText(item.text).includes(normalizedExpected));
2635
- if (!matched) {
2641
+ const normalizedExpected = normalizeText(expectedText).slice(0, 120);
2642
+ const normalizeMessage = (item) => `${normalizeText(item.author)}|${normalizeText(item.text)}`;
2643
+ const beforeSignature = baseline.map(normalizeMessage).join("\n");
2644
+ const afterSignature = chat.messages.map(normalizeMessage).join("\n");
2645
+ const changed = beforeSignature !== afterSignature;
2646
+ const beforeMatches = baseline.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
2647
+ const afterMatches = chat.messages.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
2648
+ const latest = chat.messages[chat.messages.length - 1] || null;
2649
+ const latestAuthor = normalizeText(latest?.author || "");
2650
+ const latestText = normalizeText(latest?.text || "");
2651
+ const latestMatches = latestText.includes(normalizedExpected) && (latestAuthor === "voce" || latestAuthor === "você");
2652
+ if ((changed && latestMatches) || (changed && afterMatches > beforeMatches)) {
2653
+ return { ok: true, reason: "" };
2654
+ }
2655
+ if (!changed) {
2656
+ return {
2657
+ ok: false,
2658
+ reason: "O WhatsApp nao mostrou mudanca visivel na conversa depois da tentativa de envio.",
2659
+ };
2660
+ }
2661
+ if (afterMatches <= beforeMatches) {
2636
2662
  return {
2637
2663
  ok: false,
2638
- reason: "Nao consegui confirmar visualmente a mensagem enviada no WhatsApp.",
2664
+ reason: "A conversa mudou, mas nao apareceu uma nova mensagem com o texto esperado no WhatsApp.",
2639
2665
  };
2640
2666
  }
2641
- return { ok: true, reason: "" };
2667
+ return {
2668
+ ok: false,
2669
+ reason: "Nao consegui confirmar visualmente a nova mensagem enviada no WhatsApp.",
2670
+ };
2642
2671
  }
2643
2672
  async takeScreenshot(targetPath) {
2644
2673
  const artifactsDir = path.join(os.homedir(), ".otto-bridge", "artifacts");
@@ -125,6 +125,21 @@ export class MacOSWhatsAppHelperRuntime {
125
125
  await this.start();
126
126
  await this.call("hide_background");
127
127
  }
128
+ async nativeInsertText(text) {
129
+ await this.start();
130
+ const result = await this.call("native_insert_text", { text });
131
+ return result?.ok === true;
132
+ }
133
+ async nativeClearText() {
134
+ await this.start();
135
+ const result = await this.call("native_clear_text");
136
+ return result?.ok === true;
137
+ }
138
+ async nativePressEnter() {
139
+ await this.start();
140
+ const result = await this.call("native_press_enter");
141
+ return result?.ok === true;
142
+ }
128
143
  async evaluate(script) {
129
144
  await this.start();
130
145
  return await this.call("evaluate_js", { script });
@@ -287,12 +302,16 @@ export class MacOSWhatsAppHelperRuntime {
287
302
  if (!(prepared.ok === true)) {
288
303
  return false;
289
304
  }
305
+ let enterTriggered = false;
306
+ let clickFallbackTriggered = false;
290
307
  const deadline = Date.now() + 6_000;
291
308
  while (Date.now() < deadline) {
292
309
  await delay(500);
293
310
  const result = await this.evaluate(`
294
311
  (() => {
295
312
  const query = ${JSON.stringify(contact)};
313
+ const enterTriggered = ${JSON.stringify(enterTriggered)};
314
+ const clickFallbackTriggered = ${JSON.stringify(clickFallbackTriggered)};
296
315
  const normalize = (value) => String(value || "").normalize("NFD").replace(/[\\u0300-\\u036f]/g, "").toLowerCase().trim();
297
316
  const normalizedQuery = normalize(query);
298
317
 
@@ -305,6 +324,37 @@ export class MacOSWhatsAppHelperRuntime {
305
324
  return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
306
325
  }
307
326
 
327
+ function isConversationOpen() {
328
+ const footerVisible = Array.from(document.querySelectorAll('footer, main footer'))
329
+ .some((node) => isVisible(node));
330
+ const composerVisible = Array.from(document.querySelectorAll('[data-testid="conversation-compose-box-input"], footer div[contenteditable="true"], main footer [contenteditable="true"], footer textarea'))
331
+ .some((node) => isVisible(node));
332
+ const headerMatch = Array.from(document.querySelectorAll('header span[title], header div[title], main span[title], main div[title]'))
333
+ .filter((node) => node instanceof HTMLElement)
334
+ .filter((node) => isVisible(node))
335
+ .some((node) => {
336
+ const text = normalize(node.getAttribute("title") || node.textContent || "");
337
+ return text === normalizedQuery || text.includes(normalizedQuery);
338
+ });
339
+ return footerVisible || composerVisible || headerMatch;
340
+ }
341
+
342
+ if (isConversationOpen()) {
343
+ return { clicked: true, activatedBy: "already-open" };
344
+ }
345
+
346
+ const searchCandidates = Array.from(document.querySelectorAll('div[contenteditable="true"][role="textbox"], input[role="textbox"], textarea, div[contenteditable="true"][data-tab], [data-testid="chat-list-search"] [contenteditable="true"]'))
347
+ .filter((node) => node instanceof HTMLElement)
348
+ .filter((node) => isVisible(node));
349
+ const searchBox = searchCandidates[0];
350
+ if (!enterTriggered && searchBox instanceof HTMLElement) {
351
+ searchBox.focus();
352
+ searchBox.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
353
+ searchBox.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
354
+ searchBox.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
355
+ return { clicked: false, enterTriggered: true };
356
+ }
357
+
308
358
  const titleNodes = Array.from(document.querySelectorAll('span[title], div[title]'))
309
359
  .filter((node) => node instanceof HTMLElement)
310
360
  .filter((node) => isVisible(node))
@@ -325,17 +375,27 @@ export class MacOSWhatsAppHelperRuntime {
325
375
  return { clicked: false };
326
376
  }
327
377
 
378
+ if (clickFallbackTriggered) {
379
+ return { clicked: false };
380
+ }
381
+
328
382
  const winner = titleNodes[0];
329
383
  const target = winner.container instanceof HTMLElement ? winner.container : winner.node;
330
384
  target.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
331
385
  if (typeof target.click === "function") {
332
386
  target.click();
333
- return { clicked: true };
387
+ return { clicked: false, clickFallbackTriggered: true };
334
388
  }
335
389
  target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
336
- return { clicked: true };
390
+ return { clicked: false, clickFallbackTriggered: true };
337
391
  })()
338
392
  `);
393
+ if (result.enterTriggered === true) {
394
+ enterTriggered = true;
395
+ }
396
+ if (result.clickFallbackTriggered === true) {
397
+ clickFallbackTriggered = true;
398
+ }
339
399
  if (result.clicked === true) {
340
400
  return true;
341
401
  }
@@ -398,50 +458,91 @@ export class MacOSWhatsAppHelperRuntime {
398
458
  }
399
459
 
400
460
  const composer = candidates[0].node;
401
- composer.focus();
402
461
  if (composer instanceof HTMLInputElement || composer instanceof HTMLTextAreaElement) {
462
+ composer.focus();
463
+ composer.click();
403
464
  composer.value = "";
404
465
  composer.dispatchEvent(new Event("input", { bubbles: true }));
405
466
  composer.value = value;
406
467
  composer.dispatchEvent(new Event("input", { bubbles: true }));
407
- } else {
408
- const selection = window.getSelection();
409
- const range = document.createRange();
410
- range.selectNodeContents(composer);
411
- selection?.removeAllRanges();
412
- selection?.addRange(range);
413
- document.execCommand("selectAll", false);
414
- document.execCommand("delete", false);
415
- document.execCommand("insertText", false, value);
416
- if ((composer.innerText || "").trim() !== value.trim()) {
417
- composer.textContent = value;
468
+ composer.click();
469
+
470
+ const sendCandidates = Array.from(document.querySelectorAll('[data-testid="compose-btn-send"], button[aria-label*="Send"], button[aria-label*="Enviar"], span[data-icon="send"], div[role="button"][aria-label*="Send"], div[role="button"][aria-label*="Enviar"]'))
471
+ .map((node) => node instanceof HTMLElement ? (node.closest('button, div[role="button"]') || node) : null)
472
+ .filter((node) => node instanceof HTMLElement)
473
+ .filter((node) => isVisible(node));
474
+
475
+ const sendButton = sendCandidates[0];
476
+ if (sendButton instanceof HTMLElement) {
477
+ sendButton.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
478
+ if (typeof sendButton.click === "function") {
479
+ sendButton.click();
480
+ return { sent: true };
481
+ }
482
+ sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
483
+ return { sent: true };
418
484
  }
419
- composer.dispatchEvent(new Event("input", { bubbles: true }));
485
+
486
+ composer.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
487
+ composer.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
488
+ composer.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
489
+ return { sent: true };
490
+ } else {
491
+ composer.focus();
492
+ composer.click();
493
+ return { nativeInputRequired: true };
494
+ }
495
+ })()
496
+ `);
497
+ if (result.nativeInputRequired === true) {
498
+ const cleared = await this.nativeClearText();
499
+ if (!cleared) {
500
+ throw new Error("O helper nativo do WhatsApp nao conseguiu limpar o rascunho da mensagem.");
501
+ }
502
+ const inserted = await this.nativeInsertText(text);
503
+ if (!inserted) {
504
+ throw new Error("O helper nativo do WhatsApp nao conseguiu digitar a mensagem.");
505
+ }
506
+ await delay(250);
507
+ const clickResult = await this.evaluate(`
508
+ (() => {
509
+ function isVisible(element) {
510
+ if (!(element instanceof HTMLElement)) return false;
511
+ const rect = element.getBoundingClientRect();
512
+ if (rect.width < 6 || rect.height < 6) return false;
513
+ const style = window.getComputedStyle(element);
514
+ if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0) return false;
515
+ return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
420
516
  }
421
- composer.click();
422
517
 
423
- const sendCandidates = Array.from(document.querySelectorAll('[data-testid="compose-btn-send"], button[aria-label*="Send"], button[aria-label*="Enviar"], span[data-icon="send"], div[role="button"][aria-label*="Send"], div[role="button"][aria-label*="Enviar"]'))
518
+ const sendCandidates = Array.from(document.querySelectorAll('[data-testid="compose-btn-send"], button[aria-label*="Send"], button[aria-label*="Enviar"], span[data-icon="send"], div[role="button"][aria-label*="Send"], div[role="button"][aria-label*="Enviar"], footer [data-icon="send-filled"], footer [data-icon="wds-ic-send-filled"]'))
424
519
  .map((node) => node instanceof HTMLElement ? (node.closest('button, div[role="button"]') || node) : null)
425
520
  .filter((node) => node instanceof HTMLElement)
426
521
  .filter((node) => isVisible(node));
427
522
 
428
523
  const sendButton = sendCandidates[0];
429
- if (sendButton instanceof HTMLElement) {
430
- sendButton.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
431
- if (typeof sendButton.click === "function") {
432
- sendButton.click();
433
- return { sent: true };
434
- }
435
- sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
436
- return { sent: true };
524
+ if (!(sendButton instanceof HTMLElement)) {
525
+ return { clicked: false };
437
526
  }
438
527
 
439
- composer.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
440
- composer.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
441
- composer.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
442
- return { sent: true };
528
+ sendButton.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
529
+ if (typeof sendButton.click === "function") {
530
+ sendButton.click();
531
+ return { clicked: true };
532
+ }
533
+ sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
534
+ return { clicked: true };
443
535
  })()
444
- `);
536
+ `);
537
+ if (clickResult.clicked === true) {
538
+ return;
539
+ }
540
+ const submitted = await this.nativePressEnter();
541
+ if (!submitted) {
542
+ throw new Error("O helper nativo do WhatsApp nao conseguiu enviar a mensagem.");
543
+ }
544
+ return;
545
+ }
445
546
  if (result.sent === true) {
446
547
  return;
447
548
  }
@@ -495,23 +596,45 @@ export class MacOSWhatsAppHelperRuntime {
495
596
  : "(sem mensagens visiveis na conversa)",
496
597
  };
497
598
  }
498
- async verifyLastMessage(expectedText) {
499
- const chat = await this.readVisibleConversation(6);
599
+ async verifyLastMessage(expectedText, previousMessages) {
600
+ const baseline = Array.isArray(previousMessages) ? previousMessages : [];
601
+ const chat = await this.readVisibleConversation(Math.max(8, baseline.length + 2));
500
602
  if (!chat.messages.length) {
501
603
  return {
502
604
  ok: false,
503
605
  reason: "Nao consegui ler as mensagens visiveis apos o envio no WhatsApp.",
504
606
  };
505
607
  }
506
- const normalizedExpected = normalizeText(expectedText).slice(0, 60);
507
- const matched = chat.messages.some((item) => normalizeText(item.text).includes(normalizedExpected));
508
- if (!matched) {
608
+ const normalizedExpected = normalizeText(expectedText).slice(0, 120);
609
+ const normalizeMessage = (item) => `${normalizeText(item.author)}|${normalizeText(item.text)}`;
610
+ const beforeSignature = baseline.map(normalizeMessage).join("\n");
611
+ const afterSignature = chat.messages.map(normalizeMessage).join("\n");
612
+ const changed = beforeSignature !== afterSignature;
613
+ const beforeMatches = baseline.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
614
+ const afterMatches = chat.messages.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
615
+ const latest = chat.messages[chat.messages.length - 1] || null;
616
+ const latestAuthor = normalizeText(latest?.author || "");
617
+ const latestText = normalizeText(latest?.text || "");
618
+ const latestMatches = latestText.includes(normalizedExpected) && (latestAuthor === "voce" || latestAuthor === "você");
619
+ if ((changed && latestMatches) || (changed && afterMatches > beforeMatches)) {
620
+ return { ok: true, reason: "" };
621
+ }
622
+ if (!changed) {
623
+ return {
624
+ ok: false,
625
+ reason: "O WhatsApp nao mostrou mudanca visivel na conversa depois da tentativa de envio.",
626
+ };
627
+ }
628
+ if (afterMatches <= beforeMatches) {
509
629
  return {
510
630
  ok: false,
511
- reason: "Nao consegui confirmar na conversa do WhatsApp se a mensagem foi enviada.",
631
+ reason: "A conversa mudou, mas nao apareceu uma nova mensagem com o texto esperado no WhatsApp.",
512
632
  };
513
633
  }
514
- return { ok: true, reason: "" };
634
+ return {
635
+ ok: false,
636
+ reason: "Nao consegui confirmar na conversa do WhatsApp se a nova mensagem foi enviada.",
637
+ };
515
638
  }
516
639
  handleStdout(chunk) {
517
640
  this.stdoutBuffer += chunk;
@@ -90,6 +90,13 @@ final class OttoWhatsAppHelper: NSObject, WKNavigationDelegate {
90
90
  case "hide_background":
91
91
  hideBackground()
92
92
  sendResponse(id: id, result: ["background": true])
93
+ case "native_insert_text":
94
+ let text = String(describing: params["text"] ?? "")
95
+ sendResponse(id: id, result: ["ok": nativeInsertText(text)])
96
+ case "native_clear_text":
97
+ sendResponse(id: id, result: ["ok": nativeClearText()])
98
+ case "native_press_enter":
99
+ sendResponse(id: id, result: ["ok": nativePressEnter()])
93
100
  case "load_whatsapp":
94
101
  ensureWhatsAppLoaded()
95
102
  sendResponse(id: id, result: ["url": webView.url?.absoluteString ?? ""])
@@ -134,6 +141,14 @@ final class OttoWhatsAppHelper: NSObject, WKNavigationDelegate {
134
141
  app.activate(ignoringOtherApps: true)
135
142
  }
136
143
 
144
+ private func prepareInteractionWindow() {
145
+ window.ignoresMouseEvents = true
146
+ window.alphaValue = 0
147
+ window.setFrame(NSRect(x: -2200, y: 80, width: 1320, height: 920), display: false)
148
+ window.makeKeyAndOrderFront(nil)
149
+ app.activate(ignoringOtherApps: true)
150
+ }
151
+
137
152
  private func hideBackground() {
138
153
  window.ignoresMouseEvents = true
139
154
  window.alphaValue = 0
@@ -141,6 +156,44 @@ final class OttoWhatsAppHelper: NSObject, WKNavigationDelegate {
141
156
  window.orderFrontRegardless()
142
157
  }
143
158
 
159
+ private func currentTextInputClient() -> NSTextInputClient? {
160
+ if let client = window.firstResponder as? NSTextInputClient {
161
+ return client
162
+ }
163
+ if let client = webView as? NSTextInputClient {
164
+ return client
165
+ }
166
+ return nil
167
+ }
168
+
169
+ private func nativeInsertText(_ text: String) -> Bool {
170
+ prepareInteractionWindow()
171
+ guard let client = currentTextInputClient() else {
172
+ return false
173
+ }
174
+ client.insertText(text, replacementRange: NSRange(location: NSNotFound, length: 0))
175
+ return true
176
+ }
177
+
178
+ private func nativeClearText() -> Bool {
179
+ prepareInteractionWindow()
180
+ guard let responder = window.firstResponder else {
181
+ return false
182
+ }
183
+ responder.selectAll(nil)
184
+ responder.doCommand(by: #selector(NSResponder.deleteBackward(_:)))
185
+ return true
186
+ }
187
+
188
+ private func nativePressEnter() -> Bool {
189
+ prepareInteractionWindow()
190
+ guard let responder = window.firstResponder else {
191
+ return false
192
+ }
193
+ responder.doCommand(by: #selector(NSResponder.insertNewline(_:)))
194
+ return true
195
+ }
196
+
144
197
  private func evaluateJavaScript(_ script: String, completion: @escaping (Any?, Error?) -> Void) {
145
198
  webView.evaluateJavaScript(script) { result, error in
146
199
  completion(result, error)
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const BRIDGE_CONFIG_VERSION = 1;
2
- export const BRIDGE_VERSION = "0.7.4";
2
+ export const BRIDGE_VERSION = "0.7.5";
3
3
  export const BRIDGE_PACKAGE_NAME = "@leg3ndy/otto-bridge";
4
4
  export const DEFAULT_API_BASE_URL = "http://localhost:8000";
5
5
  export const DEFAULT_POLL_INTERVAL_MS = 3000;
@@ -693,26 +693,48 @@ export class WhatsAppBackgroundBrowser {
693
693
  : "(sem mensagens visiveis na conversa)",
694
694
  };
695
695
  }
696
- async verifyLastMessage(expectedText) {
696
+ async verifyLastMessage(expectedText, previousMessages) {
697
697
  if (this.helperRuntime) {
698
- return await this.helperRuntime.verifyLastMessage(expectedText);
698
+ return await this.helperRuntime.verifyLastMessage(expectedText, previousMessages);
699
699
  }
700
- const chat = await this.readVisibleConversation(6);
700
+ const baseline = Array.isArray(previousMessages) ? previousMessages : [];
701
+ const chat = await this.readVisibleConversation(Math.max(8, baseline.length + 2));
701
702
  if (!chat.messages.length) {
702
703
  return {
703
704
  ok: false,
704
705
  reason: "Nao consegui ler as mensagens visiveis apos o envio no WhatsApp.",
705
706
  };
706
707
  }
707
- const normalizedExpected = normalizeText(expectedText).slice(0, 60);
708
- const matched = chat.messages.some((item) => normalizeText(item.text).includes(normalizedExpected));
709
- if (!matched) {
708
+ const normalizedExpected = normalizeText(expectedText).slice(0, 120);
709
+ const normalizeMessage = (item) => `${normalizeText(item.author)}|${normalizeText(item.text)}`;
710
+ const beforeSignature = baseline.map(normalizeMessage).join("\n");
711
+ const afterSignature = chat.messages.map(normalizeMessage).join("\n");
712
+ const changed = beforeSignature !== afterSignature;
713
+ const beforeMatches = baseline.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
714
+ const afterMatches = chat.messages.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
715
+ const latest = chat.messages[chat.messages.length - 1] || null;
716
+ const latestAuthor = normalizeText(latest?.author || "");
717
+ const latestText = normalizeText(latest?.text || "");
718
+ const latestMatches = latestText.includes(normalizedExpected) && (latestAuthor === "voce" || latestAuthor === "você");
719
+ if ((changed && latestMatches) || (changed && afterMatches > beforeMatches)) {
720
+ return { ok: true, reason: "" };
721
+ }
722
+ if (!changed) {
710
723
  return {
711
724
  ok: false,
712
- reason: "Nao consegui confirmar na conversa do WhatsApp se a mensagem foi enviada.",
725
+ reason: "O WhatsApp nao mostrou mudanca visivel na conversa depois da tentativa de envio.",
713
726
  };
714
727
  }
715
- return { ok: true, reason: "" };
728
+ if (afterMatches <= beforeMatches) {
729
+ return {
730
+ ok: false,
731
+ reason: "A conversa mudou, mas nao apareceu uma nova mensagem com o texto esperado no WhatsApp.",
732
+ };
733
+ }
734
+ return {
735
+ ok: false,
736
+ reason: "Nao consegui confirmar na conversa do WhatsApp se a nova mensagem foi enviada.",
737
+ };
716
738
  }
717
739
  async ensureWhatsAppPage() {
718
740
  const page = this.page;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leg3ndy/otto-bridge",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local companion for Otto Bridge device pairing and WebSocket runtime.",