linkedin-automation-cli 1.0.0

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 (314) hide show
  1. package/.env.example +12 -0
  2. package/.github/workflows/ci.yml +66 -0
  3. package/.github/workflows/publish.yml +48 -0
  4. package/.husky/pre-commit +6 -0
  5. package/.prettierignore +4 -0
  6. package/.prettierrc +10 -0
  7. package/AGENTS.md +294 -0
  8. package/CHANGELOG.md +40 -0
  9. package/GIT_RELEASE.md +167 -0
  10. package/LICENSE +21 -0
  11. package/Makefile +30 -0
  12. package/NPM_PUBLISHING.md +230 -0
  13. package/PYEOF +0 -0
  14. package/README.md +295 -0
  15. package/TESTING-GUIDE.md +151 -0
  16. package/cmd/linkedin/main.go +9 -0
  17. package/dist/agent/action-executor.d.ts +81 -0
  18. package/dist/agent/action-executor.d.ts.map +1 -0
  19. package/dist/agent/action-executor.js +170 -0
  20. package/dist/agent/action-executor.js.map +1 -0
  21. package/dist/agent/action-executor.test.d.ts +2 -0
  22. package/dist/agent/action-executor.test.d.ts.map +1 -0
  23. package/dist/agent/action-executor.test.js +366 -0
  24. package/dist/agent/action-executor.test.js.map +1 -0
  25. package/dist/agent/claude-client.d.ts +74 -0
  26. package/dist/agent/claude-client.d.ts.map +1 -0
  27. package/dist/agent/claude-client.js +314 -0
  28. package/dist/agent/claude-client.js.map +1 -0
  29. package/dist/agent/claude-client.test.d.ts +2 -0
  30. package/dist/agent/claude-client.test.d.ts.map +1 -0
  31. package/dist/agent/claude-client.test.js +590 -0
  32. package/dist/agent/claude-client.test.js.map +1 -0
  33. package/dist/agent/dom-extractor.d.ts +50 -0
  34. package/dist/agent/dom-extractor.d.ts.map +1 -0
  35. package/dist/agent/dom-extractor.js +374 -0
  36. package/dist/agent/dom-extractor.js.map +1 -0
  37. package/dist/agent/dom-extractor.test.d.ts +7 -0
  38. package/dist/agent/dom-extractor.test.d.ts.map +1 -0
  39. package/dist/agent/dom-extractor.test.js +504 -0
  40. package/dist/agent/dom-extractor.test.js.map +1 -0
  41. package/dist/agent/extension-client.d.ts +75 -0
  42. package/dist/agent/extension-client.d.ts.map +1 -0
  43. package/dist/agent/extension-client.js +245 -0
  44. package/dist/agent/extension-client.js.map +1 -0
  45. package/dist/agent/index.d.ts +8 -0
  46. package/dist/agent/index.d.ts.map +1 -0
  47. package/dist/agent/index.js +16 -0
  48. package/dist/agent/index.js.map +1 -0
  49. package/dist/agent/page-agent.d.ts +76 -0
  50. package/dist/agent/page-agent.d.ts.map +1 -0
  51. package/dist/agent/page-agent.js +236 -0
  52. package/dist/agent/page-agent.js.map +1 -0
  53. package/dist/agent/types.d.ts +236 -0
  54. package/dist/agent/types.d.ts.map +1 -0
  55. package/dist/agent/types.js +37 -0
  56. package/dist/agent/types.js.map +1 -0
  57. package/dist/cli/agent-commands.d.ts +3 -0
  58. package/dist/cli/agent-commands.d.ts.map +1 -0
  59. package/dist/cli/agent-commands.js +250 -0
  60. package/dist/cli/agent-commands.js.map +1 -0
  61. package/dist/cli/auth.d.ts +3 -0
  62. package/dist/cli/auth.d.ts.map +1 -0
  63. package/dist/cli/auth.js +288 -0
  64. package/dist/cli/auth.js.map +1 -0
  65. package/dist/cli/company.d.ts +3 -0
  66. package/dist/cli/company.d.ts.map +1 -0
  67. package/dist/cli/company.js +55 -0
  68. package/dist/cli/company.js.map +1 -0
  69. package/dist/cli/connection.d.ts +3 -0
  70. package/dist/cli/connection.d.ts.map +1 -0
  71. package/dist/cli/connection.js +79 -0
  72. package/dist/cli/connection.js.map +1 -0
  73. package/dist/cli/index.d.ts +7 -0
  74. package/dist/cli/index.d.ts.map +1 -0
  75. package/dist/cli/index.js +17 -0
  76. package/dist/cli/index.js.map +1 -0
  77. package/dist/cli/messages.d.ts +3 -0
  78. package/dist/cli/messages.d.ts.map +1 -0
  79. package/dist/cli/messages.js +268 -0
  80. package/dist/cli/messages.js.map +1 -0
  81. package/dist/cli/profile.d.ts +3 -0
  82. package/dist/cli/profile.d.ts.map +1 -0
  83. package/dist/cli/profile.js +81 -0
  84. package/dist/cli/profile.js.map +1 -0
  85. package/dist/cli/profile.test.d.ts +2 -0
  86. package/dist/cli/profile.test.d.ts.map +1 -0
  87. package/dist/cli/profile.test.js +15 -0
  88. package/dist/cli/profile.test.js.map +1 -0
  89. package/dist/cli/reply.d.ts +3 -0
  90. package/dist/cli/reply.d.ts.map +1 -0
  91. package/dist/cli/reply.js +129 -0
  92. package/dist/cli/reply.js.map +1 -0
  93. package/dist/core/audit.d.ts +17 -0
  94. package/dist/core/audit.d.ts.map +1 -0
  95. package/dist/core/audit.js +121 -0
  96. package/dist/core/audit.js.map +1 -0
  97. package/dist/core/audit.test.d.ts +2 -0
  98. package/dist/core/audit.test.d.ts.map +1 -0
  99. package/dist/core/audit.test.js +142 -0
  100. package/dist/core/audit.test.js.map +1 -0
  101. package/dist/core/browser-cookies.d.ts +19 -0
  102. package/dist/core/browser-cookies.d.ts.map +1 -0
  103. package/dist/core/browser-cookies.js +181 -0
  104. package/dist/core/browser-cookies.js.map +1 -0
  105. package/dist/core/browser.d.ts +50 -0
  106. package/dist/core/browser.d.ts.map +1 -0
  107. package/dist/core/browser.js +318 -0
  108. package/dist/core/browser.js.map +1 -0
  109. package/dist/core/config.d.ts +20 -0
  110. package/dist/core/config.d.ts.map +1 -0
  111. package/dist/core/config.js +103 -0
  112. package/dist/core/config.js.map +1 -0
  113. package/dist/core/config.test.d.ts +2 -0
  114. package/dist/core/config.test.d.ts.map +1 -0
  115. package/dist/core/config.test.js +111 -0
  116. package/dist/core/config.test.js.map +1 -0
  117. package/dist/core/storage.d.ts +19 -0
  118. package/dist/core/storage.d.ts.map +1 -0
  119. package/dist/core/storage.js +124 -0
  120. package/dist/core/storage.js.map +1 -0
  121. package/dist/core/storage.test.d.ts +2 -0
  122. package/dist/core/storage.test.d.ts.map +1 -0
  123. package/dist/core/storage.test.js +142 -0
  124. package/dist/core/storage.test.js.map +1 -0
  125. package/dist/index.d.ts +3 -0
  126. package/dist/index.d.ts.map +1 -0
  127. package/dist/index.js +63 -0
  128. package/dist/index.js.map +1 -0
  129. package/dist/linkedin/auth.d.ts +22 -0
  130. package/dist/linkedin/auth.d.ts.map +1 -0
  131. package/dist/linkedin/auth.js +167 -0
  132. package/dist/linkedin/auth.js.map +1 -0
  133. package/dist/linkedin/company-extractor.d.ts +36 -0
  134. package/dist/linkedin/company-extractor.d.ts.map +1 -0
  135. package/dist/linkedin/company-extractor.js +211 -0
  136. package/dist/linkedin/company-extractor.js.map +1 -0
  137. package/dist/linkedin/company-extractor.test.d.ts +2 -0
  138. package/dist/linkedin/company-extractor.test.d.ts.map +1 -0
  139. package/dist/linkedin/company-extractor.test.js +52 -0
  140. package/dist/linkedin/company-extractor.test.js.map +1 -0
  141. package/dist/linkedin/connector.d.ts +45 -0
  142. package/dist/linkedin/connector.d.ts.map +1 -0
  143. package/dist/linkedin/connector.js +245 -0
  144. package/dist/linkedin/connector.js.map +1 -0
  145. package/dist/linkedin/message-sender.d.ts +32 -0
  146. package/dist/linkedin/message-sender.d.ts.map +1 -0
  147. package/dist/linkedin/message-sender.js +112 -0
  148. package/dist/linkedin/message-sender.js.map +1 -0
  149. package/dist/linkedin/messages.d.ts +78 -0
  150. package/dist/linkedin/messages.d.ts.map +1 -0
  151. package/dist/linkedin/messages.js +745 -0
  152. package/dist/linkedin/messages.js.map +1 -0
  153. package/dist/linkedin/profile.d.ts +37 -0
  154. package/dist/linkedin/profile.d.ts.map +1 -0
  155. package/dist/linkedin/profile.js +268 -0
  156. package/dist/linkedin/profile.js.map +1 -0
  157. package/dist/linkedin/profile.test.d.ts +2 -0
  158. package/dist/linkedin/profile.test.d.ts.map +1 -0
  159. package/dist/linkedin/profile.test.js +68 -0
  160. package/dist/linkedin/profile.test.js.map +1 -0
  161. package/dist/linkedin/reply.d.ts +21 -0
  162. package/dist/linkedin/reply.d.ts.map +1 -0
  163. package/dist/linkedin/reply.js +76 -0
  164. package/dist/linkedin/reply.js.map +1 -0
  165. package/dist/linkedin/selector-engine.d.ts +69 -0
  166. package/dist/linkedin/selector-engine.d.ts.map +1 -0
  167. package/dist/linkedin/selector-engine.js +339 -0
  168. package/dist/linkedin/selector-engine.js.map +1 -0
  169. package/dist/linkedin/selector-engine.test.d.ts +2 -0
  170. package/dist/linkedin/selector-engine.test.d.ts.map +1 -0
  171. package/dist/linkedin/selector-engine.test.js +135 -0
  172. package/dist/linkedin/selector-engine.test.js.map +1 -0
  173. package/dist/linkedin/selectors.d.ts +65 -0
  174. package/dist/linkedin/selectors.d.ts.map +1 -0
  175. package/dist/linkedin/selectors.js +261 -0
  176. package/dist/linkedin/selectors.js.map +1 -0
  177. package/dist/templates/engine.d.ts +37 -0
  178. package/dist/templates/engine.d.ts.map +1 -0
  179. package/dist/templates/engine.js +215 -0
  180. package/dist/templates/engine.js.map +1 -0
  181. package/dist/templates/engine.test.d.ts +2 -0
  182. package/dist/templates/engine.test.d.ts.map +1 -0
  183. package/dist/templates/engine.test.js +212 -0
  184. package/dist/templates/engine.test.js.map +1 -0
  185. package/dist/templates/index.d.ts +2 -0
  186. package/dist/templates/index.d.ts.map +1 -0
  187. package/dist/templates/index.js +7 -0
  188. package/dist/templates/index.js.map +1 -0
  189. package/dist/types/index.d.ts +113 -0
  190. package/dist/types/index.d.ts.map +1 -0
  191. package/dist/types/index.js +3 -0
  192. package/dist/types/index.js.map +1 -0
  193. package/dist/types/index.test.d.ts +2 -0
  194. package/dist/types/index.test.d.ts.map +1 -0
  195. package/dist/types/index.test.js +90 -0
  196. package/dist/types/index.test.js.map +1 -0
  197. package/dist/utils/paths.d.ts +8 -0
  198. package/dist/utils/paths.d.ts.map +1 -0
  199. package/dist/utils/paths.js +68 -0
  200. package/dist/utils/paths.js.map +1 -0
  201. package/dist/utils/rate-limiter.d.ts +22 -0
  202. package/dist/utils/rate-limiter.d.ts.map +1 -0
  203. package/dist/utils/rate-limiter.js +57 -0
  204. package/dist/utils/rate-limiter.js.map +1 -0
  205. package/dist/utils/retry.d.ts +18 -0
  206. package/dist/utils/retry.d.ts.map +1 -0
  207. package/dist/utils/retry.js +49 -0
  208. package/dist/utils/retry.js.map +1 -0
  209. package/docs/connection-command.md +52 -0
  210. package/docs/plans/2025-03-03-linkedin-cli-design.md +280 -0
  211. package/docs/plans/2025-03-03-linkedin-cli-implementation-plan.md +2087 -0
  212. package/docs/plans/2025-03-03-linkedin-cli-implementation.md +2420 -0
  213. package/docs/plans/2026-02-19-linkedin-connection-feature.md +596 -0
  214. package/docs/plans/2026-02-28-messages-send-feature.md +480 -0
  215. package/docs/plans/2026-02-28-messages-show-design.md +243 -0
  216. package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-design.md +394 -0
  217. package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-plan.md +1592 -0
  218. package/docs/superpowers/plans/2026-03-13-linkedin-automation-resilience-migration.md +425 -0
  219. package/docs/superpowers/plans/2026-03-13-playwright-fara-migration.md +1112 -0
  220. package/docs/superpowers/plans/2026-03-14-page-agent-plan.md +1598 -0
  221. package/docs/superpowers/plans/2026-03-15-company-profile-extraction.md +591 -0
  222. package/docs/superpowers/plans/2026-03-15-profile-extraction-plan.md +943 -0
  223. package/docs/superpowers/specs/2026-03-14-company-profile-extraction-design.md +371 -0
  224. package/docs/superpowers/specs/2026-03-14-page-agent-design.md +385 -0
  225. package/docs/superpowers/specs/2026-03-15-profile-extraction-design.md +409 -0
  226. package/eslint.config.mjs +58 -0
  227. package/go.mod +9 -0
  228. package/go.sum +10 -0
  229. package/import-cookies.js +376 -0
  230. package/internal/cmd/actions.go +123 -0
  231. package/internal/cmd/auth.go +108 -0
  232. package/internal/cmd/connect.go +42 -0
  233. package/internal/cmd/message.go +44 -0
  234. package/internal/cmd/people.go +454 -0
  235. package/internal/cmd/profiles.go +121 -0
  236. package/internal/cmd/root.go +89 -0
  237. package/internal/cmd/sequence.go +192 -0
  238. package/internal/config/config.go +187 -0
  239. package/internal/config/config_test.go +121 -0
  240. package/internal/config/profile.go +65 -0
  241. package/internal/linkedin/navigator.go +195 -0
  242. package/internal/linkedin/selectors.go +39 -0
  243. package/internal/linkedin/validator.go +69 -0
  244. package/internal/pinchtab/client.go +183 -0
  245. package/internal/pinchtab/client_test.go +67 -0
  246. package/internal/pinchtab/types.go +50 -0
  247. package/internal/ratelimit/limiter.go +115 -0
  248. package/internal/ratelimit/limits.go +32 -0
  249. package/package.json +67 -0
  250. package/release.sh +66 -0
  251. package/scripts/debug-linkedin.js +156 -0
  252. package/scripts/debug-login.js +193 -0
  253. package/scripts/extract-from-edge.js +96 -0
  254. package/scripts/import-cookies.js +101 -0
  255. package/scripts/poc-show-data.js +205 -0
  256. package/scripts/proof-of-access.js +87 -0
  257. package/scripts/prove-connection.js +110 -0
  258. package/scripts/show-linkedin-data.js +173 -0
  259. package/src/agent/action-executor.test.ts +464 -0
  260. package/src/agent/action-executor.ts +203 -0
  261. package/src/agent/claude-client.test.ts +707 -0
  262. package/src/agent/claude-client.ts +422 -0
  263. package/src/agent/dom-extractor.test.ts +574 -0
  264. package/src/agent/dom-extractor.ts +437 -0
  265. package/src/agent/extension-client.ts +306 -0
  266. package/src/agent/index.ts +28 -0
  267. package/src/agent/page-agent.ts +292 -0
  268. package/src/agent/types.ts +288 -0
  269. package/src/cli/agent-commands.ts +274 -0
  270. package/src/cli/auth.ts +343 -0
  271. package/src/cli/company.ts +66 -0
  272. package/src/cli/connection.ts +89 -0
  273. package/src/cli/index.ts +7 -0
  274. package/src/cli/messages.ts +338 -0
  275. package/src/cli/profile.test.ts +14 -0
  276. package/src/cli/profile.ts +95 -0
  277. package/src/cli/reply.ts +110 -0
  278. package/src/core/audit.test.ts +134 -0
  279. package/src/core/audit.ts +98 -0
  280. package/src/core/browser-cookies.ts +203 -0
  281. package/src/core/browser.ts +304 -0
  282. package/src/core/config.test.ts +90 -0
  283. package/src/core/config.ts +81 -0
  284. package/src/core/storage.test.ts +129 -0
  285. package/src/core/storage.ts +100 -0
  286. package/src/index.ts +70 -0
  287. package/src/linkedin/auth.ts +218 -0
  288. package/src/linkedin/company-extractor.test.ts +58 -0
  289. package/src/linkedin/company-extractor.ts +222 -0
  290. package/src/linkedin/connector.ts +336 -0
  291. package/src/linkedin/message-sender.ts +141 -0
  292. package/src/linkedin/messages.ts +894 -0
  293. package/src/linkedin/profile.test.ts +79 -0
  294. package/src/linkedin/profile.ts +314 -0
  295. package/src/linkedin/reply.ts +96 -0
  296. package/src/linkedin/selector-engine.test.ts +167 -0
  297. package/src/linkedin/selector-engine.ts +393 -0
  298. package/src/linkedin/selectors.ts +268 -0
  299. package/src/templates/defaults/followup.txt +14 -0
  300. package/src/templates/defaults/meeting.txt +16 -0
  301. package/src/templates/defaults/welcome.txt +14 -0
  302. package/src/templates/engine.test.ts +228 -0
  303. package/src/templates/engine.ts +208 -0
  304. package/src/templates/index.ts +1 -0
  305. package/src/types/index.test.ts +94 -0
  306. package/src/types/index.ts +143 -0
  307. package/src/types/sql.js.d.ts +23 -0
  308. package/src/utils/paths.ts +33 -0
  309. package/src/utils/rate-limiter.ts +75 -0
  310. package/src/utils/retry.ts +78 -0
  311. package/test-cli.sh +85 -0
  312. package/test-real-data.sh +97 -0
  313. package/tsconfig.json +23 -0
  314. package/vitest.config.ts +35 -0
@@ -0,0 +1,574 @@
1
+ /**
2
+ * DOM Extractor Tests
3
+ *
4
+ * Tests for the DOMExtractor class using mocked Playwright page.
5
+ */
6
+
7
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
8
+ import { DOMExtractor, createDOMExtractor } from './dom-extractor';
9
+ import type { Page } from 'playwright';
10
+
11
+ // Mock Page type for testing
12
+ type MockPage = {
13
+ url: ReturnType<typeof vi.fn>;
14
+ title: ReturnType<typeof vi.fn>;
15
+ evaluate: ReturnType<typeof vi.fn>;
16
+ };
17
+
18
+ describe('DOMExtractor', () => {
19
+ let extractor: DOMExtractor;
20
+ let mockPage: MockPage;
21
+
22
+ beforeEach(() => {
23
+ extractor = new DOMExtractor();
24
+ mockPage = {
25
+ url: vi.fn(),
26
+ title: vi.fn(),
27
+ evaluate: vi.fn(),
28
+ };
29
+ });
30
+
31
+ describe('extract', () => {
32
+ it('should return correct DOM representation structure', async () => {
33
+ mockPage.url.mockReturnValue('https://www.linkedin.com/in/test-profile/');
34
+ mockPage.title.mockResolvedValue('Test Profile | LinkedIn');
35
+ mockPage.evaluate.mockImplementation((fn: unknown) => {
36
+ if ((fn as { toString(): string }).toString().includes('pageType')) {
37
+ return Promise.resolve({
38
+ pageType: 'profile',
39
+ profileName: 'John Doe',
40
+ profileTitle: 'Software Engineer',
41
+ connectionState: 'connected',
42
+ statusMessages: [],
43
+ });
44
+ }
45
+ return Promise.resolve([]);
46
+ });
47
+
48
+ const result = await extractor.extract(mockPage as unknown as Page);
49
+
50
+ expect(result).toHaveProperty('url');
51
+ expect(result).toHaveProperty('title');
52
+ expect(result).toHaveProperty('elements');
53
+ expect(result).toHaveProperty('metadata');
54
+ expect(result.url).toBe('https://www.linkedin.com/in/test-profile/');
55
+ expect(result.title).toBe('Test Profile | LinkedIn');
56
+ expect(Array.isArray(result.elements)).toBe(true);
57
+ });
58
+
59
+ it('should extract elements from the page', async () => {
60
+ mockPage.url.mockReturnValue('https://www.linkedin.com/in/test/');
61
+ mockPage.title.mockResolvedValue('Test | LinkedIn');
62
+
63
+ const mockElements = [
64
+ {
65
+ id: 'elem-button-connect-0',
66
+ tag: 'button',
67
+ role: 'button',
68
+ ariaLabel: 'Connect',
69
+ text: 'Connect',
70
+ type: undefined,
71
+ value: undefined,
72
+ visible: true,
73
+ enabled: true,
74
+ bbox: { x: 100, y: 200, width: 120, height: 40 },
75
+ },
76
+ {
77
+ id: 'elem-a-message-1',
78
+ tag: 'a',
79
+ role: 'link',
80
+ ariaLabel: 'Message',
81
+ text: 'Message',
82
+ type: undefined,
83
+ value: undefined,
84
+ visible: true,
85
+ enabled: true,
86
+ bbox: { x: 240, y: 200, width: 100, height: 40 },
87
+ },
88
+ ];
89
+
90
+ const mockMetadata = {
91
+ pageType: 'profile',
92
+ profileName: 'Jane Doe',
93
+ profileTitle: 'Product Manager',
94
+ connectionState: 'connected',
95
+ statusMessages: [],
96
+ };
97
+
98
+ mockPage.evaluate.mockImplementation((fn: unknown) => {
99
+ const fnStr = (fn as { toString(): string }).toString();
100
+ if (fnStr.includes('pageType') || fnStr.includes('detectPageType')) {
101
+ return Promise.resolve(mockMetadata);
102
+ }
103
+ return Promise.resolve(mockElements);
104
+ });
105
+
106
+ const result = await extractor.extract(mockPage as unknown as Page);
107
+
108
+ expect(result.elements).toHaveLength(2);
109
+ expect(result.elements[0].id).toBe('elem-button-connect-0');
110
+ expect(result.elements[1].id).toBe('elem-a-message-1');
111
+ });
112
+
113
+ it('should extract metadata correctly', async () => {
114
+ mockPage.url.mockReturnValue('https://www.linkedin.com/in/john-doe/');
115
+ mockPage.title.mockResolvedValue('John Doe | LinkedIn');
116
+
117
+ const mockMetadata = {
118
+ pageType: 'profile',
119
+ profileName: 'John Doe',
120
+ profileTitle: 'Senior Engineer',
121
+ connectionState: 'not_connected',
122
+ statusMessages: ['Profile view limit reached'],
123
+ };
124
+
125
+ mockPage.evaluate.mockImplementation((fn: unknown) => {
126
+ const fnStr = (fn as { toString(): string }).toString();
127
+ if (fnStr.includes('pageType') || fnStr.includes('detectPageType')) {
128
+ return Promise.resolve(mockMetadata);
129
+ }
130
+ return Promise.resolve([]);
131
+ });
132
+
133
+ const result = await extractor.extract(mockPage as unknown as Page);
134
+
135
+ expect(result.metadata.pageType).toBe('profile');
136
+ expect(result.metadata.profileName).toBe('John Doe');
137
+ expect(result.metadata.profileTitle).toBe('Senior Engineer');
138
+ expect(result.metadata.connectionState).toBe('not_connected');
139
+ expect(result.metadata.statusMessages).toContain('Profile view limit reached');
140
+ });
141
+
142
+ it('should handle messaging page type', async () => {
143
+ mockPage.url.mockReturnValue('https://www.linkedin.com/messaging/');
144
+ mockPage.title.mockResolvedValue('Messaging | LinkedIn');
145
+
146
+ const mockMetadata = {
147
+ pageType: 'messaging',
148
+ connectionState: 'unknown',
149
+ statusMessages: [],
150
+ };
151
+
152
+ mockPage.evaluate.mockImplementation((fn: unknown) => {
153
+ const fnStr = (fn as { toString(): string }).toString();
154
+ if (fnStr.includes('pageType') || fnStr.includes('detectPageType')) {
155
+ return Promise.resolve(mockMetadata);
156
+ }
157
+ return Promise.resolve([]);
158
+ });
159
+
160
+ const result = await extractor.extract(mockPage as unknown as Page);
161
+
162
+ expect(result.metadata.pageType).toBe('messaging');
163
+ });
164
+
165
+ it('should handle feed page type', async () => {
166
+ mockPage.url.mockReturnValue('https://www.linkedin.com/feed/');
167
+ mockPage.title.mockResolvedValue('Feed | LinkedIn');
168
+
169
+ const mockMetadata = {
170
+ pageType: 'feed',
171
+ connectionState: 'unknown',
172
+ statusMessages: ['New posts available'],
173
+ };
174
+
175
+ mockPage.evaluate.mockImplementation((fn: unknown) => {
176
+ const fnStr = (fn as { toString(): string }).toString();
177
+ if (fnStr.includes('pageType') || fnStr.includes('detectPageType')) {
178
+ return Promise.resolve(mockMetadata);
179
+ }
180
+ return Promise.resolve([]);
181
+ });
182
+
183
+ const result = await extractor.extract(mockPage as unknown as Page);
184
+
185
+ expect(result.metadata.pageType).toBe('feed');
186
+ });
187
+ });
188
+
189
+ describe('element ID generation', () => {
190
+ it('should generate unique IDs for elements', async () => {
191
+ mockPage.url.mockReturnValue('https://www.linkedin.com/in/test/');
192
+ mockPage.title.mockResolvedValue('Test | LinkedIn');
193
+
194
+ const mockElements = [
195
+ {
196
+ id: 'elem-button-submit-0',
197
+ tag: 'button',
198
+ text: 'Submit',
199
+ visible: true,
200
+ enabled: true,
201
+ bbox: { x: 0, y: 0, width: 100, height: 50 },
202
+ },
203
+ {
204
+ id: 'elem-button-cancel-1',
205
+ tag: 'button',
206
+ text: 'Cancel',
207
+ visible: true,
208
+ enabled: true,
209
+ bbox: { x: 110, y: 0, width: 100, height: 50 },
210
+ },
211
+ ];
212
+
213
+ mockPage.evaluate.mockImplementation((fn: unknown) => {
214
+ const fnStr = (fn as { toString(): string }).toString();
215
+ if (fnStr.includes('pageType') || fnStr.includes('detectPageType')) {
216
+ return Promise.resolve({ pageType: 'profile', connectionState: 'unknown' });
217
+ }
218
+ return Promise.resolve(mockElements);
219
+ });
220
+
221
+ const result = await extractor.extract(mockPage as unknown as Page);
222
+
223
+ expect(result.elements[0].id).toBe('elem-button-submit-0');
224
+ expect(result.elements[1].id).toBe('elem-button-cancel-1');
225
+ });
226
+
227
+ it('should format IDs with lowercase and dashes', async () => {
228
+ mockPage.url.mockReturnValue('https://www.linkedin.com/in/test/');
229
+ mockPage.title.mockResolvedValue('Test | LinkedIn');
230
+
231
+ const mockElements = [
232
+ {
233
+ id: 'elem-button-hello-world-0',
234
+ tag: 'button',
235
+ text: 'Hello World!',
236
+ visible: true,
237
+ enabled: true,
238
+ bbox: { x: 0, y: 0, width: 100, height: 50 },
239
+ },
240
+ ];
241
+
242
+ mockPage.evaluate.mockImplementation((fn: unknown) => {
243
+ const fnStr = (fn as { toString(): string }).toString();
244
+ if (fnStr.includes('pageType') || fnStr.includes('detectPageType')) {
245
+ return Promise.resolve({ pageType: 'profile', connectionState: 'unknown' });
246
+ }
247
+ return Promise.resolve(mockElements);
248
+ });
249
+
250
+ const result = await extractor.extract(mockPage as unknown as Page);
251
+
252
+ expect(result.elements[0].id).toContain('elem-button');
253
+ });
254
+ });
255
+
256
+ describe('findElementById', () => {
257
+ it('should find element by ID', () => {
258
+ const dom = {
259
+ url: 'https://test.com',
260
+ title: 'Test',
261
+ elements: [
262
+ {
263
+ id: 'elem-1',
264
+ tag: 'button',
265
+ text: 'Click',
266
+ visible: true,
267
+ enabled: true,
268
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
269
+ },
270
+ {
271
+ id: 'elem-2',
272
+ tag: 'a',
273
+ text: 'Link',
274
+ visible: true,
275
+ enabled: true,
276
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
277
+ },
278
+ ],
279
+ metadata: { pageType: 'profile' as const },
280
+ };
281
+
282
+ const found = extractor.findElementById(dom, 'elem-1');
283
+
284
+ expect(found).toBeDefined();
285
+ expect(found?.id).toBe('elem-1');
286
+ });
287
+
288
+ it('should return undefined for non-existent ID', () => {
289
+ const dom = {
290
+ url: 'https://test.com',
291
+ title: 'Test',
292
+ elements: [],
293
+ metadata: { pageType: 'profile' as const },
294
+ };
295
+
296
+ const found = extractor.findElementById(dom, 'non-existent');
297
+
298
+ expect(found).toBeUndefined();
299
+ });
300
+ });
301
+
302
+ describe('findElementsByTag', () => {
303
+ it('should find elements by tag', () => {
304
+ const dom = {
305
+ url: 'https://test.com',
306
+ title: 'Test',
307
+ elements: [
308
+ {
309
+ id: '1',
310
+ tag: 'button',
311
+ text: 'Btn1',
312
+ visible: true,
313
+ enabled: true,
314
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
315
+ },
316
+ {
317
+ id: '2',
318
+ tag: 'button',
319
+ text: 'Btn2',
320
+ visible: true,
321
+ enabled: true,
322
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
323
+ },
324
+ {
325
+ id: '3',
326
+ tag: 'a',
327
+ text: 'Link',
328
+ visible: true,
329
+ enabled: true,
330
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
331
+ },
332
+ ],
333
+ metadata: { pageType: 'profile' as const },
334
+ };
335
+
336
+ const buttons = extractor.findElementsByTag(dom, 'button');
337
+
338
+ expect(buttons).toHaveLength(2);
339
+ expect(buttons.every((el) => el.tag === 'button')).toBe(true);
340
+ });
341
+
342
+ it('should be case insensitive', () => {
343
+ const dom = {
344
+ url: 'https://test.com',
345
+ title: 'Test',
346
+ elements: [
347
+ {
348
+ id: '1',
349
+ tag: 'button',
350
+ text: 'Btn1',
351
+ visible: true,
352
+ enabled: true,
353
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
354
+ },
355
+ ],
356
+ metadata: { pageType: 'profile' as const },
357
+ };
358
+
359
+ const found = extractor.findElementsByTag(dom, 'BUTTON');
360
+
361
+ expect(found).toHaveLength(1);
362
+ });
363
+ });
364
+
365
+ describe('findElementsByText', () => {
366
+ it('should find elements by text content', () => {
367
+ const dom = {
368
+ url: 'https://test.com',
369
+ title: 'Test',
370
+ elements: [
371
+ {
372
+ id: '1',
373
+ tag: 'button',
374
+ text: 'Connect',
375
+ visible: true,
376
+ enabled: true,
377
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
378
+ },
379
+ {
380
+ id: '2',
381
+ tag: 'button',
382
+ text: 'Message',
383
+ visible: true,
384
+ enabled: true,
385
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
386
+ },
387
+ {
388
+ id: '3',
389
+ tag: 'button',
390
+ text: 'Reconnect',
391
+ visible: true,
392
+ enabled: true,
393
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
394
+ },
395
+ ],
396
+ metadata: { pageType: 'profile' as const },
397
+ };
398
+
399
+ const found = extractor.findElementsByText(dom, 'conn');
400
+
401
+ expect(found).toHaveLength(2); // Connect and Reconnect
402
+ });
403
+
404
+ it('should be case insensitive', () => {
405
+ const dom = {
406
+ url: 'https://test.com',
407
+ title: 'Test',
408
+ elements: [
409
+ {
410
+ id: '1',
411
+ tag: 'button',
412
+ text: 'CONNECT',
413
+ visible: true,
414
+ enabled: true,
415
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
416
+ },
417
+ ],
418
+ metadata: { pageType: 'profile' as const },
419
+ };
420
+
421
+ const found = extractor.findElementsByText(dom, 'connect');
422
+
423
+ expect(found).toHaveLength(1);
424
+ });
425
+
426
+ it('should search ariaLabel when text is empty', () => {
427
+ const dom = {
428
+ url: 'https://test.com',
429
+ title: 'Test',
430
+ elements: [
431
+ {
432
+ id: '1',
433
+ tag: 'button',
434
+ text: '',
435
+ ariaLabel: 'Send message',
436
+ visible: true,
437
+ enabled: true,
438
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
439
+ },
440
+ ],
441
+ metadata: { pageType: 'profile' as const },
442
+ };
443
+
444
+ const found = extractor.findElementsByText(dom, 'message');
445
+
446
+ expect(found).toHaveLength(1);
447
+ });
448
+ });
449
+
450
+ describe('getClickableElements', () => {
451
+ it('should return buttons and links that are visible and enabled', () => {
452
+ const dom = {
453
+ url: 'https://test.com',
454
+ title: 'Test',
455
+ elements: [
456
+ {
457
+ id: '1',
458
+ tag: 'button',
459
+ text: 'Btn',
460
+ visible: true,
461
+ enabled: true,
462
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
463
+ },
464
+ {
465
+ id: '2',
466
+ tag: 'a',
467
+ text: 'Link',
468
+ visible: true,
469
+ enabled: true,
470
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
471
+ },
472
+ {
473
+ id: '3',
474
+ tag: 'div',
475
+ role: 'button',
476
+ text: 'DivBtn',
477
+ visible: true,
478
+ enabled: true,
479
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
480
+ },
481
+ {
482
+ id: '4',
483
+ tag: 'button',
484
+ text: 'Hidden',
485
+ visible: false,
486
+ enabled: true,
487
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
488
+ },
489
+ {
490
+ id: '5',
491
+ tag: 'button',
492
+ text: 'Disabled',
493
+ visible: true,
494
+ enabled: false,
495
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
496
+ },
497
+ ],
498
+ metadata: { pageType: 'profile' as const },
499
+ };
500
+
501
+ const clickable = extractor.getClickableElements(dom);
502
+
503
+ expect(clickable).toHaveLength(3);
504
+ expect(clickable.some((el) => el.id === '1')).toBe(true);
505
+ expect(clickable.some((el) => el.id === '2')).toBe(true);
506
+ expect(clickable.some((el) => el.id === '3')).toBe(true);
507
+ });
508
+ });
509
+
510
+ describe('getInputElements', () => {
511
+ it('should return input, textarea, and select elements', () => {
512
+ const dom = {
513
+ url: 'https://test.com',
514
+ title: 'Test',
515
+ elements: [
516
+ {
517
+ id: '1',
518
+ tag: 'input',
519
+ type: 'text',
520
+ text: '',
521
+ visible: true,
522
+ enabled: true,
523
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
524
+ },
525
+ {
526
+ id: '2',
527
+ tag: 'textarea',
528
+ text: '',
529
+ visible: true,
530
+ enabled: true,
531
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
532
+ },
533
+ {
534
+ id: '3',
535
+ tag: 'select',
536
+ text: '',
537
+ visible: true,
538
+ enabled: true,
539
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
540
+ },
541
+ {
542
+ id: '4',
543
+ tag: 'div',
544
+ role: 'textbox',
545
+ text: '',
546
+ visible: true,
547
+ enabled: true,
548
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
549
+ },
550
+ {
551
+ id: '5',
552
+ tag: 'button',
553
+ text: 'Btn',
554
+ visible: true,
555
+ enabled: true,
556
+ bbox: { x: 0, y: 0, width: 1, height: 1 },
557
+ },
558
+ ],
559
+ metadata: { pageType: 'profile' as const },
560
+ };
561
+
562
+ const inputs = extractor.getInputElements(dom);
563
+
564
+ expect(inputs).toHaveLength(4);
565
+ });
566
+ });
567
+
568
+ describe('createDOMExtractor', () => {
569
+ it('should create a DOMExtractor instance', () => {
570
+ const extractor = createDOMExtractor();
571
+ expect(extractor).toBeInstanceOf(DOMExtractor);
572
+ });
573
+ });
574
+ });