|
| 1 | +import { describe, it, expect } from "vitest"; |
| 2 | +import { extractRoomIdFromHtml } from "../extractRoomIdFromHtml"; |
| 3 | + |
| 4 | +describe("extractRoomIdFromHtml", () => { |
| 5 | + describe("Superhuman reply with conversation link in quoted content", () => { |
| 6 | + it("extracts roomId from Superhuman reply with wbr tags in link text", () => { |
| 7 | + // This is the actual HTML from a Superhuman reply where the link text |
| 8 | + // contains <wbr /> tags for word breaking |
| 9 | + const html = `<html> |
| 10 | +
|
| 11 | +<head></head> |
| 12 | +
|
| 13 | +<body> |
| 14 | + <div> |
| 15 | + <div> |
| 16 | + <div> |
| 17 | + <div class="">Send a picture of him <br /></div> |
| 18 | + <div class=""><br /></div> |
| 19 | + </div> |
| 20 | + <div> |
| 21 | + <div style="display: none; border: 0px; width: 0px; height: 0px; overflow: hidden; visibility: hidden;"><img src="https://r.superhuman.com/4640qXWivTiaNi_anz1bstqoUbWlYj8nnSM0Y-NWmoL_OZdXZ1Zq-_DSPSu7r6M_NMQJAgHCnrKL5OisY6deh83uz8MfXoijSTOwhFcnM5Ya0RU8q8kZDoD0MVTLFtwDxERoN1wu0T-LgI8TDjcWI8K1HEns5_8ETb2EF1fetEenZgrj73FE6Q.gif" alt=" " width="1" height="0" style="display: none; border: 0px; width: 0px; height: 0px; overflow: hidden; visibility: hidden;" /><!-- --></div><br /> |
| 22 | + <div class="gmail_signature"> |
| 23 | + <div style="clear:both">Sent via <a href="https://sprh.mn/?vip=sidney@recoupable.com" target="_blank">Superhuman</a></div><br /> |
| 24 | + </div> |
| 25 | + </div><br /> |
| 26 | + <div> |
| 27 | + <div class="gmail_quote">On Fri, Jan 09, 2026 at 11:59 AM, Agent by Recoup <span dir="ltr"><<a href="mailto:agent@recoupable.com" target="_blank">agent@recoupable.com</a>></span> wrote:<br /> |
| 28 | + <blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex"> |
| 29 | + <div class="gmail_extra"> |
| 30 | + <div class="gmail_quote sh-color-black sh-color"> |
| 31 | + <p class="sh-color-black sh-color">Short answer: Brian Kernighan.</p> |
| 32 | + <p class="sh-color-black sh-color">Details: the earliest known use in computing appears in Kernighan's 1972 tutorial for the B language (the "hello, world!" example). It was then popularized by Kernighan & Ritchie's 1978 book The C Programming Language. (There are older claims—BCPL examples from the late 1960s and the exact phrase appeared as a radio catchphrase in the 1950s—but Kernighan is usually credited for putting it into programming tradition.)</p> |
| 33 | + <p cor-black sh-color">Want the sources/links?</p> |
| 34 | +
|
| 35 | +
|
| 36 | + <hr style="margin-top:24px;margin-bottom:16px;border:none;border-top:1px solid #e5e7eb;" class="sh-color-grey sh-color" /> |
| 37 | + <p style="font-size:12px;color:#6b7280;margin:0 0 4px;" class="sh-color-grey sh-color"> |
| 38 | + Note: you can reply directly to this email to continue the conversation. |
| 39 | + </p> |
| 40 | + <p style="font-size:12px;color:#6b7280;margin:0;" class="sh-color-grey sh-color"> |
| 41 | + Or continue the conversation on Recoup: |
| 42 | + <a href="https://14158f8b1cbe93481ac078c1f43f3792.us-east-1.resend-links.com/CL0/https:%2F%2Fchat.recoupable.com%2Fchat%2Fd5c473ec-04cf-4a23-a577-e0dc71542392/1/0100019ba3b2dbec-832401f0-a3c6-4478-b6bf-3b0b06b7251a-000000/OomH25B53Pym0ykT2YYxbKx0c_NEhvJ3oFfBzpKKdVk=439" rel="noopener noreferrer" target="_blank" class="sh-color-blue sh-color"> |
| 43 | + https:/<wbr />/<wbr />chat.<wbr />recoupable.<wbr />com/<wbr />chat/<wbr />d5c473ec-04cf-4a23-a577-e0dc71542392 |
| 44 | + </a> |
| 45 | + </p> |
| 46 | + </div> |
| 47 | + </div> |
| 48 | + </blockquote> |
| 49 | + </div> |
| 50 | + </div><br /> |
| 51 | + </div> |
| 52 | + </div> |
| 53 | +</body> |
| 54 | +
|
| 55 | +</html>`; |
| 56 | + |
| 57 | + const result = extractRoomIdFromHtml(html); |
| 58 | + |
| 59 | + expect(result).toBe("d5c473ec-04cf-4a23-a577-e0dc71542392"); |
| 60 | + }); |
| 61 | + }); |
| 62 | + |
| 63 | + describe("Gmail reply with proper threading", () => { |
| 64 | + it("extracts roomId from Gmail reply with quoted content", () => { |
| 65 | + const html = ` |
| 66 | + <html> |
| 67 | + <body> |
| 68 | + <p>Thanks for the info!</p> |
| 69 | + <div class="gmail_quote"> |
| 70 | + <blockquote> |
| 71 | + <p>Original message here</p> |
| 72 | + <p>Continue the conversation: <a href="https://chat.recoupable.com/chat/a1b2c3d4-e5f6-7890-abcd-ef1234567890">https://chat.recoupable.com/chat/a1b2c3d4-e5f6-7890-abcd-ef1234567890</a></p> |
| 73 | + </blockquote> |
| 74 | + </div> |
| 75 | + </body> |
| 76 | + </html> |
| 77 | + `; |
| 78 | + |
| 79 | + const result = extractRoomIdFromHtml(html); |
| 80 | + |
| 81 | + expect(result).toBe("a1b2c3d4-e5f6-7890-abcd-ef1234567890"); |
| 82 | + }); |
| 83 | + }); |
| 84 | + |
| 85 | + describe("no conversation ID", () => { |
| 86 | + it("returns undefined for undefined input", () => { |
| 87 | + const result = extractRoomIdFromHtml(undefined); |
| 88 | + |
| 89 | + expect(result).toBeUndefined(); |
| 90 | + }); |
| 91 | + |
| 92 | + it("returns undefined for empty string", () => { |
| 93 | + const result = extractRoomIdFromHtml(""); |
| 94 | + |
| 95 | + expect(result).toBeUndefined(); |
| 96 | + }); |
| 97 | + |
| 98 | + it("returns undefined when no chat link present", () => { |
| 99 | + const html = "<html><body><p>This email has no Recoup chat link.</p></body></html>"; |
| 100 | + |
| 101 | + const result = extractRoomIdFromHtml(html); |
| 102 | + |
| 103 | + expect(result).toBeUndefined(); |
| 104 | + }); |
| 105 | + |
| 106 | + it("returns undefined for invalid UUID format in link", () => { |
| 107 | + const html = |
| 108 | + '<a href="https://chat.recoupable.com/chat/not-a-valid-uuid">link</a>'; |
| 109 | + |
| 110 | + const result = extractRoomIdFromHtml(html); |
| 111 | + |
| 112 | + expect(result).toBeUndefined(); |
| 113 | + }); |
| 114 | + |
| 115 | + it("returns undefined for wrong domain", () => { |
| 116 | + const html = |
| 117 | + '<a href="https://chat.otherdomain.com/chat/550e8400-e29b-41d4-a716-446655440000">link</a>'; |
| 118 | + |
| 119 | + const result = extractRoomIdFromHtml(html); |
| 120 | + |
| 121 | + expect(result).toBeUndefined(); |
| 122 | + }); |
| 123 | + }); |
| 124 | + |
| 125 | + describe("edge cases", () => { |
| 126 | + it("handles URL-encoded link in href attribute", () => { |
| 127 | + // Resend tracking redirects URL-encode the destination |
| 128 | + const html = |
| 129 | + '<a href="https://tracking.example.com/redirect/https:%2F%2Fchat.recoupable.com%2Fchat%2F12345678-1234-1234-1234-123456789abc">Click here</a>'; |
| 130 | + |
| 131 | + const result = extractRoomIdFromHtml(html); |
| 132 | + |
| 133 | + expect(result).toBe("12345678-1234-1234-1234-123456789abc"); |
| 134 | + }); |
| 135 | + |
| 136 | + it("extracts first roomId when multiple links present", () => { |
| 137 | + const html = ` |
| 138 | + <a href="https://chat.recoupable.com/chat/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee">First</a> |
| 139 | + <a href="https://chat.recoupable.com/chat/11111111-2222-3333-4444-555555555555">Second</a> |
| 140 | + `; |
| 141 | + |
| 142 | + const result = extractRoomIdFromHtml(html); |
| 143 | + |
| 144 | + expect(result).toBe("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"); |
| 145 | + }); |
| 146 | + |
| 147 | + it("handles link text with wbr tags breaking up the URL", () => { |
| 148 | + const html = ` |
| 149 | + <a href="#"> |
| 150 | + https:/<wbr />/<wbr />chat.<wbr />recoupable.<wbr />com/<wbr />chat/<wbr />abcdef12-3456-7890-abcd-ef1234567890 |
| 151 | + </a> |
| 152 | + `; |
| 153 | + |
| 154 | + const result = extractRoomIdFromHtml(html); |
| 155 | + |
| 156 | + expect(result).toBe("abcdef12-3456-7890-abcd-ef1234567890"); |
| 157 | + }); |
| 158 | + |
| 159 | + it("handles mixed case in URL", () => { |
| 160 | + const html = |
| 161 | + '<a href="HTTPS://CHAT.RECOUPABLE.COM/CHAT/12345678-1234-1234-1234-123456789abc">link</a>'; |
| 162 | + |
| 163 | + const result = extractRoomIdFromHtml(html); |
| 164 | + |
| 165 | + expect(result).toBe("12345678-1234-1234-1234-123456789abc"); |
| 166 | + }); |
| 167 | + }); |
| 168 | +}); |
0 commit comments