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.
- package/.env.example +12 -0
- package/.github/workflows/ci.yml +66 -0
- package/.github/workflows/publish.yml +48 -0
- package/.husky/pre-commit +6 -0
- package/.prettierignore +4 -0
- package/.prettierrc +10 -0
- package/AGENTS.md +294 -0
- package/CHANGELOG.md +40 -0
- package/GIT_RELEASE.md +167 -0
- package/LICENSE +21 -0
- package/Makefile +30 -0
- package/NPM_PUBLISHING.md +230 -0
- package/PYEOF +0 -0
- package/README.md +295 -0
- package/TESTING-GUIDE.md +151 -0
- package/cmd/linkedin/main.go +9 -0
- package/dist/agent/action-executor.d.ts +81 -0
- package/dist/agent/action-executor.d.ts.map +1 -0
- package/dist/agent/action-executor.js +170 -0
- package/dist/agent/action-executor.js.map +1 -0
- package/dist/agent/action-executor.test.d.ts +2 -0
- package/dist/agent/action-executor.test.d.ts.map +1 -0
- package/dist/agent/action-executor.test.js +366 -0
- package/dist/agent/action-executor.test.js.map +1 -0
- package/dist/agent/claude-client.d.ts +74 -0
- package/dist/agent/claude-client.d.ts.map +1 -0
- package/dist/agent/claude-client.js +314 -0
- package/dist/agent/claude-client.js.map +1 -0
- package/dist/agent/claude-client.test.d.ts +2 -0
- package/dist/agent/claude-client.test.d.ts.map +1 -0
- package/dist/agent/claude-client.test.js +590 -0
- package/dist/agent/claude-client.test.js.map +1 -0
- package/dist/agent/dom-extractor.d.ts +50 -0
- package/dist/agent/dom-extractor.d.ts.map +1 -0
- package/dist/agent/dom-extractor.js +374 -0
- package/dist/agent/dom-extractor.js.map +1 -0
- package/dist/agent/dom-extractor.test.d.ts +7 -0
- package/dist/agent/dom-extractor.test.d.ts.map +1 -0
- package/dist/agent/dom-extractor.test.js +504 -0
- package/dist/agent/dom-extractor.test.js.map +1 -0
- package/dist/agent/extension-client.d.ts +75 -0
- package/dist/agent/extension-client.d.ts.map +1 -0
- package/dist/agent/extension-client.js +245 -0
- package/dist/agent/extension-client.js.map +1 -0
- package/dist/agent/index.d.ts +8 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +16 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/page-agent.d.ts +76 -0
- package/dist/agent/page-agent.d.ts.map +1 -0
- package/dist/agent/page-agent.js +236 -0
- package/dist/agent/page-agent.js.map +1 -0
- package/dist/agent/types.d.ts +236 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +37 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/cli/agent-commands.d.ts +3 -0
- package/dist/cli/agent-commands.d.ts.map +1 -0
- package/dist/cli/agent-commands.js +250 -0
- package/dist/cli/agent-commands.js.map +1 -0
- package/dist/cli/auth.d.ts +3 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +288 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/company.d.ts +3 -0
- package/dist/cli/company.d.ts.map +1 -0
- package/dist/cli/company.js +55 -0
- package/dist/cli/company.js.map +1 -0
- package/dist/cli/connection.d.ts +3 -0
- package/dist/cli/connection.d.ts.map +1 -0
- package/dist/cli/connection.js +79 -0
- package/dist/cli/connection.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +17 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/messages.d.ts +3 -0
- package/dist/cli/messages.d.ts.map +1 -0
- package/dist/cli/messages.js +268 -0
- package/dist/cli/messages.js.map +1 -0
- package/dist/cli/profile.d.ts +3 -0
- package/dist/cli/profile.d.ts.map +1 -0
- package/dist/cli/profile.js +81 -0
- package/dist/cli/profile.js.map +1 -0
- package/dist/cli/profile.test.d.ts +2 -0
- package/dist/cli/profile.test.d.ts.map +1 -0
- package/dist/cli/profile.test.js +15 -0
- package/dist/cli/profile.test.js.map +1 -0
- package/dist/cli/reply.d.ts +3 -0
- package/dist/cli/reply.d.ts.map +1 -0
- package/dist/cli/reply.js +129 -0
- package/dist/cli/reply.js.map +1 -0
- package/dist/core/audit.d.ts +17 -0
- package/dist/core/audit.d.ts.map +1 -0
- package/dist/core/audit.js +121 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/audit.test.d.ts +2 -0
- package/dist/core/audit.test.d.ts.map +1 -0
- package/dist/core/audit.test.js +142 -0
- package/dist/core/audit.test.js.map +1 -0
- package/dist/core/browser-cookies.d.ts +19 -0
- package/dist/core/browser-cookies.d.ts.map +1 -0
- package/dist/core/browser-cookies.js +181 -0
- package/dist/core/browser-cookies.js.map +1 -0
- package/dist/core/browser.d.ts +50 -0
- package/dist/core/browser.d.ts.map +1 -0
- package/dist/core/browser.js +318 -0
- package/dist/core/browser.js.map +1 -0
- package/dist/core/config.d.ts +20 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +103 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/config.test.d.ts +2 -0
- package/dist/core/config.test.d.ts.map +1 -0
- package/dist/core/config.test.js +111 -0
- package/dist/core/config.test.js.map +1 -0
- package/dist/core/storage.d.ts +19 -0
- package/dist/core/storage.d.ts.map +1 -0
- package/dist/core/storage.js +124 -0
- package/dist/core/storage.js.map +1 -0
- package/dist/core/storage.test.d.ts +2 -0
- package/dist/core/storage.test.d.ts.map +1 -0
- package/dist/core/storage.test.js +142 -0
- package/dist/core/storage.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/linkedin/auth.d.ts +22 -0
- package/dist/linkedin/auth.d.ts.map +1 -0
- package/dist/linkedin/auth.js +167 -0
- package/dist/linkedin/auth.js.map +1 -0
- package/dist/linkedin/company-extractor.d.ts +36 -0
- package/dist/linkedin/company-extractor.d.ts.map +1 -0
- package/dist/linkedin/company-extractor.js +211 -0
- package/dist/linkedin/company-extractor.js.map +1 -0
- package/dist/linkedin/company-extractor.test.d.ts +2 -0
- package/dist/linkedin/company-extractor.test.d.ts.map +1 -0
- package/dist/linkedin/company-extractor.test.js +52 -0
- package/dist/linkedin/company-extractor.test.js.map +1 -0
- package/dist/linkedin/connector.d.ts +45 -0
- package/dist/linkedin/connector.d.ts.map +1 -0
- package/dist/linkedin/connector.js +245 -0
- package/dist/linkedin/connector.js.map +1 -0
- package/dist/linkedin/message-sender.d.ts +32 -0
- package/dist/linkedin/message-sender.d.ts.map +1 -0
- package/dist/linkedin/message-sender.js +112 -0
- package/dist/linkedin/message-sender.js.map +1 -0
- package/dist/linkedin/messages.d.ts +78 -0
- package/dist/linkedin/messages.d.ts.map +1 -0
- package/dist/linkedin/messages.js +745 -0
- package/dist/linkedin/messages.js.map +1 -0
- package/dist/linkedin/profile.d.ts +37 -0
- package/dist/linkedin/profile.d.ts.map +1 -0
- package/dist/linkedin/profile.js +268 -0
- package/dist/linkedin/profile.js.map +1 -0
- package/dist/linkedin/profile.test.d.ts +2 -0
- package/dist/linkedin/profile.test.d.ts.map +1 -0
- package/dist/linkedin/profile.test.js +68 -0
- package/dist/linkedin/profile.test.js.map +1 -0
- package/dist/linkedin/reply.d.ts +21 -0
- package/dist/linkedin/reply.d.ts.map +1 -0
- package/dist/linkedin/reply.js +76 -0
- package/dist/linkedin/reply.js.map +1 -0
- package/dist/linkedin/selector-engine.d.ts +69 -0
- package/dist/linkedin/selector-engine.d.ts.map +1 -0
- package/dist/linkedin/selector-engine.js +339 -0
- package/dist/linkedin/selector-engine.js.map +1 -0
- package/dist/linkedin/selector-engine.test.d.ts +2 -0
- package/dist/linkedin/selector-engine.test.d.ts.map +1 -0
- package/dist/linkedin/selector-engine.test.js +135 -0
- package/dist/linkedin/selector-engine.test.js.map +1 -0
- package/dist/linkedin/selectors.d.ts +65 -0
- package/dist/linkedin/selectors.d.ts.map +1 -0
- package/dist/linkedin/selectors.js +261 -0
- package/dist/linkedin/selectors.js.map +1 -0
- package/dist/templates/engine.d.ts +37 -0
- package/dist/templates/engine.d.ts.map +1 -0
- package/dist/templates/engine.js +215 -0
- package/dist/templates/engine.js.map +1 -0
- package/dist/templates/engine.test.d.ts +2 -0
- package/dist/templates/engine.test.d.ts.map +1 -0
- package/dist/templates/engine.test.js +212 -0
- package/dist/templates/engine.test.js.map +1 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +7 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/types/index.d.ts +113 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.test.d.ts +2 -0
- package/dist/types/index.test.d.ts.map +1 -0
- package/dist/types/index.test.js +90 -0
- package/dist/types/index.test.js.map +1 -0
- package/dist/utils/paths.d.ts +8 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +68 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +22 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +57 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/retry.d.ts +18 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +49 -0
- package/dist/utils/retry.js.map +1 -0
- package/docs/connection-command.md +52 -0
- package/docs/plans/2025-03-03-linkedin-cli-design.md +280 -0
- package/docs/plans/2025-03-03-linkedin-cli-implementation-plan.md +2087 -0
- package/docs/plans/2025-03-03-linkedin-cli-implementation.md +2420 -0
- package/docs/plans/2026-02-19-linkedin-connection-feature.md +596 -0
- package/docs/plans/2026-02-28-messages-send-feature.md +480 -0
- package/docs/plans/2026-02-28-messages-show-design.md +243 -0
- package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-design.md +394 -0
- package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-plan.md +1592 -0
- package/docs/superpowers/plans/2026-03-13-linkedin-automation-resilience-migration.md +425 -0
- package/docs/superpowers/plans/2026-03-13-playwright-fara-migration.md +1112 -0
- package/docs/superpowers/plans/2026-03-14-page-agent-plan.md +1598 -0
- package/docs/superpowers/plans/2026-03-15-company-profile-extraction.md +591 -0
- package/docs/superpowers/plans/2026-03-15-profile-extraction-plan.md +943 -0
- package/docs/superpowers/specs/2026-03-14-company-profile-extraction-design.md +371 -0
- package/docs/superpowers/specs/2026-03-14-page-agent-design.md +385 -0
- package/docs/superpowers/specs/2026-03-15-profile-extraction-design.md +409 -0
- package/eslint.config.mjs +58 -0
- package/go.mod +9 -0
- package/go.sum +10 -0
- package/import-cookies.js +376 -0
- package/internal/cmd/actions.go +123 -0
- package/internal/cmd/auth.go +108 -0
- package/internal/cmd/connect.go +42 -0
- package/internal/cmd/message.go +44 -0
- package/internal/cmd/people.go +454 -0
- package/internal/cmd/profiles.go +121 -0
- package/internal/cmd/root.go +89 -0
- package/internal/cmd/sequence.go +192 -0
- package/internal/config/config.go +187 -0
- package/internal/config/config_test.go +121 -0
- package/internal/config/profile.go +65 -0
- package/internal/linkedin/navigator.go +195 -0
- package/internal/linkedin/selectors.go +39 -0
- package/internal/linkedin/validator.go +69 -0
- package/internal/pinchtab/client.go +183 -0
- package/internal/pinchtab/client_test.go +67 -0
- package/internal/pinchtab/types.go +50 -0
- package/internal/ratelimit/limiter.go +115 -0
- package/internal/ratelimit/limits.go +32 -0
- package/package.json +67 -0
- package/release.sh +66 -0
- package/scripts/debug-linkedin.js +156 -0
- package/scripts/debug-login.js +193 -0
- package/scripts/extract-from-edge.js +96 -0
- package/scripts/import-cookies.js +101 -0
- package/scripts/poc-show-data.js +205 -0
- package/scripts/proof-of-access.js +87 -0
- package/scripts/prove-connection.js +110 -0
- package/scripts/show-linkedin-data.js +173 -0
- package/src/agent/action-executor.test.ts +464 -0
- package/src/agent/action-executor.ts +203 -0
- package/src/agent/claude-client.test.ts +707 -0
- package/src/agent/claude-client.ts +422 -0
- package/src/agent/dom-extractor.test.ts +574 -0
- package/src/agent/dom-extractor.ts +437 -0
- package/src/agent/extension-client.ts +306 -0
- package/src/agent/index.ts +28 -0
- package/src/agent/page-agent.ts +292 -0
- package/src/agent/types.ts +288 -0
- package/src/cli/agent-commands.ts +274 -0
- package/src/cli/auth.ts +343 -0
- package/src/cli/company.ts +66 -0
- package/src/cli/connection.ts +89 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/messages.ts +338 -0
- package/src/cli/profile.test.ts +14 -0
- package/src/cli/profile.ts +95 -0
- package/src/cli/reply.ts +110 -0
- package/src/core/audit.test.ts +134 -0
- package/src/core/audit.ts +98 -0
- package/src/core/browser-cookies.ts +203 -0
- package/src/core/browser.ts +304 -0
- package/src/core/config.test.ts +90 -0
- package/src/core/config.ts +81 -0
- package/src/core/storage.test.ts +129 -0
- package/src/core/storage.ts +100 -0
- package/src/index.ts +70 -0
- package/src/linkedin/auth.ts +218 -0
- package/src/linkedin/company-extractor.test.ts +58 -0
- package/src/linkedin/company-extractor.ts +222 -0
- package/src/linkedin/connector.ts +336 -0
- package/src/linkedin/message-sender.ts +141 -0
- package/src/linkedin/messages.ts +894 -0
- package/src/linkedin/profile.test.ts +79 -0
- package/src/linkedin/profile.ts +314 -0
- package/src/linkedin/reply.ts +96 -0
- package/src/linkedin/selector-engine.test.ts +167 -0
- package/src/linkedin/selector-engine.ts +393 -0
- package/src/linkedin/selectors.ts +268 -0
- package/src/templates/defaults/followup.txt +14 -0
- package/src/templates/defaults/meeting.txt +16 -0
- package/src/templates/defaults/welcome.txt +14 -0
- package/src/templates/engine.test.ts +228 -0
- package/src/templates/engine.ts +208 -0
- package/src/templates/index.ts +1 -0
- package/src/types/index.test.ts +94 -0
- package/src/types/index.ts +143 -0
- package/src/types/sql.js.d.ts +23 -0
- package/src/utils/paths.ts +33 -0
- package/src/utils/rate-limiter.ts +75 -0
- package/src/utils/retry.ts +78 -0
- package/test-cli.sh +85 -0
- package/test-real-data.sh +97 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +35 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
const { chromium } = require('playwright');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
|
|
6
|
+
async function showLinkedInData() {
|
|
7
|
+
console.log('🎯 Fetching Your LinkedIn Data');
|
|
8
|
+
console.log('==============================\n');
|
|
9
|
+
|
|
10
|
+
// Decrypt session
|
|
11
|
+
const sessionFile = path.join(require('os').homedir(), '.linkedin-cli/sessions/linkedin-session.json');
|
|
12
|
+
const encrypted = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
|
|
13
|
+
|
|
14
|
+
const machineData = [process.env.USER, process.env.HOME, process.platform].join('|');
|
|
15
|
+
const salt = Buffer.from(encrypted.salt, 'base64');
|
|
16
|
+
const key = crypto.pbkdf2Sync(machineData, salt, 100000, 32, 'sha256');
|
|
17
|
+
const iv = Buffer.from(encrypted.iv, 'base64');
|
|
18
|
+
const encryptedData = Buffer.from(encrypted.encrypted, 'base64');
|
|
19
|
+
const tag = Buffer.from(encrypted.tag, 'base64');
|
|
20
|
+
|
|
21
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
22
|
+
decipher.setAuthTag(tag);
|
|
23
|
+
const session = JSON.parse(Buffer.concat([decipher.update(encryptedData), decipher.final()]).toString());
|
|
24
|
+
|
|
25
|
+
console.log('✅ Session decrypted successfully');
|
|
26
|
+
console.log(`📅 Session created: ${new Date(session.timestamp).toLocaleString()}`);
|
|
27
|
+
console.log(`🍪 Cookies: ${session.cookies.length} LinkedIn cookies loaded\n`);
|
|
28
|
+
|
|
29
|
+
// Launch Edge browser
|
|
30
|
+
console.log('🌐 Opening browser...');
|
|
31
|
+
const browser = await chromium.launch({
|
|
32
|
+
headless: true,
|
|
33
|
+
executablePath: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const context = await browser.newContext({
|
|
37
|
+
userAgent: session.userAgent
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Set cookies
|
|
41
|
+
await context.addCookies(session.cookies.map(c => ({
|
|
42
|
+
name: c.name,
|
|
43
|
+
value: c.value,
|
|
44
|
+
domain: c.domain,
|
|
45
|
+
path: c.path || '/',
|
|
46
|
+
secure: c.secure,
|
|
47
|
+
httpOnly: false,
|
|
48
|
+
sameSite: 'Lax'
|
|
49
|
+
})));
|
|
50
|
+
|
|
51
|
+
// Check Feed
|
|
52
|
+
console.log('\n📰 CHECKING LINKEDIN FEED...');
|
|
53
|
+
console.log('─────────────────────────────────────');
|
|
54
|
+
const feedPage = await context.newPage();
|
|
55
|
+
await feedPage.goto('https://www.linkedin.com/feed/', { waitUntil: 'networkidle', timeout: 30000 });
|
|
56
|
+
|
|
57
|
+
// Wait for feed to load
|
|
58
|
+
await feedPage.waitForTimeout(5000);
|
|
59
|
+
|
|
60
|
+
// Get feed posts (try multiple selectors)
|
|
61
|
+
const posts = await feedPage.evaluate(() => {
|
|
62
|
+
const selectors = [
|
|
63
|
+
'.feed-shared-update-v2',
|
|
64
|
+
'[data-testid="feed-shared-update-v2"]',
|
|
65
|
+
'.scaffold-finite-scroll__content > div',
|
|
66
|
+
'[class*="update-v2"]'
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
for (const selector of selectors) {
|
|
70
|
+
const elements = document.querySelectorAll(selector);
|
|
71
|
+
if (elements.length > 0) {
|
|
72
|
+
return Array.from(elements).slice(0, 3).map(el => {
|
|
73
|
+
const authorEl = el.querySelector('[class*="actor"], [class*="author"], [class*="name"]');
|
|
74
|
+
const textEl = el.querySelector('[class*="update-text"], [class*="content"], p');
|
|
75
|
+
return {
|
|
76
|
+
author: authorEl?.innerText?.trim() || 'Unknown',
|
|
77
|
+
text: textEl?.innerText?.trim()?.substring(0, 150) || 'No text'
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return [];
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (posts.length > 0) {
|
|
86
|
+
console.log(`✅ Found ${posts.length} posts in your feed:\n`);
|
|
87
|
+
posts.forEach((post, i) => {
|
|
88
|
+
console.log(`${i + 1}. 👤 ${post.author}`);
|
|
89
|
+
console.log(` 📝 ${post.text}...\n`);
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
console.log('⚠️ Could not extract feed posts');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check Messages
|
|
96
|
+
console.log('\n💬 CHECKING PRIVATE MESSAGES...');
|
|
97
|
+
console.log('─────────────────────────────────────');
|
|
98
|
+
const msgPage = await context.newPage();
|
|
99
|
+
await msgPage.goto('https://www.linkedin.com/messaging/', { waitUntil: 'networkidle', timeout: 30000 });
|
|
100
|
+
|
|
101
|
+
await msgPage.waitForTimeout(5000);
|
|
102
|
+
|
|
103
|
+
// Get conversations
|
|
104
|
+
const conversations = await msgPage.evaluate(() => {
|
|
105
|
+
const selectors = [
|
|
106
|
+
'.msg-conversation-card',
|
|
107
|
+
'[data-testid="conversation-card"]',
|
|
108
|
+
'[class*="conversation-card"]',
|
|
109
|
+
'[class*="msg-conversation-listitem"]'
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
for (const selector of selectors) {
|
|
113
|
+
const elements = document.querySelectorAll(selector);
|
|
114
|
+
if (elements.length > 0) {
|
|
115
|
+
return Array.from(elements).slice(0, 5).map(el => {
|
|
116
|
+
const nameSelectors = ['[class*="participant"]', '[class*="name"]', 'h3', 'span'];
|
|
117
|
+
const previewSelectors = ['[class*="preview"]', '[class*="message"]', 'p'];
|
|
118
|
+
|
|
119
|
+
let name = 'Unknown';
|
|
120
|
+
let preview = '';
|
|
121
|
+
|
|
122
|
+
for (const ns of nameSelectors) {
|
|
123
|
+
const ne = el.querySelector(ns);
|
|
124
|
+
if (ne?.innerText?.trim()) {
|
|
125
|
+
name = ne.innerText.trim();
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const ps of previewSelectors) {
|
|
131
|
+
const pe = el.querySelector(ps);
|
|
132
|
+
if (pe?.innerText?.trim()) {
|
|
133
|
+
preview = pe.innerText.trim();
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const isUnread = el.querySelector('[class*="unread"]') !== null;
|
|
139
|
+
|
|
140
|
+
return { name, preview: preview?.substring(0, 100), isUnread };
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return [];
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (conversations.length > 0) {
|
|
148
|
+
console.log(`✅ Found ${conversations.length} conversations:\n`);
|
|
149
|
+
conversations.forEach((conv, i) => {
|
|
150
|
+
const unreadBadge = conv.isUnread ? ' 🔴 UNREAD' : '';
|
|
151
|
+
console.log(`${i + 1}. 👤 ${conv.name}${unreadBadge}`);
|
|
152
|
+
if (conv.preview) {
|
|
153
|
+
console.log(` 💬 ${conv.preview}...\n`);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
console.log('⚠️ No conversations found');
|
|
158
|
+
console.log(' (LinkedIn might have changed their page structure)');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await browser.close();
|
|
162
|
+
|
|
163
|
+
console.log('\n✅ DONE!');
|
|
164
|
+
console.log('═════════════════════════════════════');
|
|
165
|
+
console.log('Your LinkedIn CLI is WORKING! 🎉');
|
|
166
|
+
console.log('═════════════════════════════════════');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
showLinkedInData().catch(error => {
|
|
170
|
+
console.error('❌ Error:', error.message);
|
|
171
|
+
console.error(error.stack);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
});
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { ActionExecutor, createActionExecutor } from './action-executor';
|
|
3
|
+
import type { Page } from 'playwright';
|
|
4
|
+
import type {
|
|
5
|
+
DOMRepresentation,
|
|
6
|
+
Action,
|
|
7
|
+
ClickAction,
|
|
8
|
+
TypeAction,
|
|
9
|
+
WaitAction,
|
|
10
|
+
NavigateAction,
|
|
11
|
+
} from './types';
|
|
12
|
+
|
|
13
|
+
describe('ActionExecutor', () => {
|
|
14
|
+
let mockPage: Page;
|
|
15
|
+
let mockDOM: DOMRepresentation;
|
|
16
|
+
let executor: ActionExecutor;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
// Create mock Page
|
|
20
|
+
mockPage = {
|
|
21
|
+
url: vi.fn().mockReturnValue('https://linkedin.com/in/test-user'),
|
|
22
|
+
title: vi.fn().mockResolvedValue('Test User - LinkedIn'),
|
|
23
|
+
mouse: {
|
|
24
|
+
click: vi.fn().mockResolvedValue(undefined),
|
|
25
|
+
},
|
|
26
|
+
keyboard: {
|
|
27
|
+
type: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
},
|
|
29
|
+
waitForTimeout: vi.fn().mockResolvedValue(undefined),
|
|
30
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
31
|
+
} as unknown as Page;
|
|
32
|
+
|
|
33
|
+
// Create mock DOM representation
|
|
34
|
+
mockDOM = {
|
|
35
|
+
url: 'https://linkedin.com/in/test-user',
|
|
36
|
+
title: 'Test User - LinkedIn',
|
|
37
|
+
elements: [
|
|
38
|
+
{
|
|
39
|
+
id: 'btn-connect',
|
|
40
|
+
tag: 'button',
|
|
41
|
+
role: 'button',
|
|
42
|
+
ariaLabel: 'Connect',
|
|
43
|
+
text: 'Connect',
|
|
44
|
+
visible: true,
|
|
45
|
+
enabled: true,
|
|
46
|
+
bbox: { x: 100, y: 200, width: 120, height: 40 },
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'input-message',
|
|
50
|
+
tag: 'textarea',
|
|
51
|
+
role: 'textbox',
|
|
52
|
+
text: '',
|
|
53
|
+
visible: true,
|
|
54
|
+
enabled: true,
|
|
55
|
+
bbox: { x: 50, y: 300, width: 400, height: 100 },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'btn-submit',
|
|
59
|
+
tag: 'button',
|
|
60
|
+
text: 'Submit',
|
|
61
|
+
visible: true,
|
|
62
|
+
enabled: true,
|
|
63
|
+
bbox: { x: 200, y: 420, width: 80, height: 35 },
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
metadata: {
|
|
67
|
+
pageType: 'profile',
|
|
68
|
+
profileName: 'Test User',
|
|
69
|
+
connectionState: 'not_connected',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
executor = new ActionExecutor(mockPage, mockDOM);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('constructor', () => {
|
|
77
|
+
it('should create an ActionExecutor with page and DOM', () => {
|
|
78
|
+
const executor = new ActionExecutor(mockPage, mockDOM);
|
|
79
|
+
expect(executor).toBeInstanceOf(ActionExecutor);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should create via factory function', () => {
|
|
83
|
+
const executor = createActionExecutor(mockPage, mockDOM);
|
|
84
|
+
expect(executor).toBeInstanceOf(ActionExecutor);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('execute - click action', () => {
|
|
89
|
+
it('should execute click action with correct coordinates', async () => {
|
|
90
|
+
const action: ClickAction = {
|
|
91
|
+
type: 'click',
|
|
92
|
+
elementId: 'btn-connect',
|
|
93
|
+
description: 'Click connect button',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const result = await executor.execute(action);
|
|
97
|
+
|
|
98
|
+
expect(result.success).toBe(true);
|
|
99
|
+
expect(result.error).toBeUndefined();
|
|
100
|
+
|
|
101
|
+
// Verify click was called with center coordinates
|
|
102
|
+
// centerX = 100 + 120/2 = 160, centerY = 200 + 40/2 = 220
|
|
103
|
+
expect(mockPage.mouse.click).toHaveBeenCalledWith(160, 220);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should handle click on element with different bbox', async () => {
|
|
107
|
+
const action: ClickAction = {
|
|
108
|
+
type: 'click',
|
|
109
|
+
elementId: 'btn-submit',
|
|
110
|
+
description: 'Click submit button',
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const result = await executor.execute(action);
|
|
114
|
+
|
|
115
|
+
expect(result.success).toBe(true);
|
|
116
|
+
// centerX = 200 + 80/2 = 240, centerY = 420 + 35/2 = 437.5
|
|
117
|
+
expect(mockPage.mouse.click).toHaveBeenCalledWith(240, 437.5);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should return error when element not found', async () => {
|
|
121
|
+
const action: ClickAction = {
|
|
122
|
+
type: 'click',
|
|
123
|
+
elementId: 'non-existent-id',
|
|
124
|
+
description: 'Click non-existent button',
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const result = await executor.execute(action);
|
|
128
|
+
|
|
129
|
+
expect(result.success).toBe(false);
|
|
130
|
+
expect(result.error).toContain('Element not found');
|
|
131
|
+
expect(result.error).toContain('non-existent-id');
|
|
132
|
+
expect(mockPage.mouse.click).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('execute - type action', () => {
|
|
137
|
+
it('should execute type action by clicking then typing', async () => {
|
|
138
|
+
const action: TypeAction = {
|
|
139
|
+
type: 'type',
|
|
140
|
+
elementId: 'input-message',
|
|
141
|
+
text: 'Hello, this is a test message',
|
|
142
|
+
description: 'Type message in textarea',
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const result = await executor.execute(action);
|
|
146
|
+
|
|
147
|
+
expect(result.success).toBe(true);
|
|
148
|
+
|
|
149
|
+
// Should click to focus first
|
|
150
|
+
// centerX = 50 + 400/2 = 250, centerY = 300 + 100/2 = 350
|
|
151
|
+
expect(mockPage.mouse.click).toHaveBeenCalledWith(250, 350);
|
|
152
|
+
|
|
153
|
+
// Should wait 100ms for focus
|
|
154
|
+
expect(mockPage.waitForTimeout).toHaveBeenCalledWith(100);
|
|
155
|
+
|
|
156
|
+
// Should type the text
|
|
157
|
+
expect(mockPage.keyboard.type).toHaveBeenCalledWith('Hello, this is a test message');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should handle empty text', async () => {
|
|
161
|
+
const action: TypeAction = {
|
|
162
|
+
type: 'type',
|
|
163
|
+
elementId: 'input-message',
|
|
164
|
+
text: '',
|
|
165
|
+
description: 'Type empty string',
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const result = await executor.execute(action);
|
|
169
|
+
|
|
170
|
+
expect(result.success).toBe(true);
|
|
171
|
+
expect(mockPage.keyboard.type).toHaveBeenCalledWith('');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should return error when element not found for type action', async () => {
|
|
175
|
+
const action: TypeAction = {
|
|
176
|
+
type: 'type',
|
|
177
|
+
elementId: 'non-existent-input',
|
|
178
|
+
text: 'Some text',
|
|
179
|
+
description: 'Type in non-existent field',
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const result = await executor.execute(action);
|
|
183
|
+
|
|
184
|
+
expect(result.success).toBe(false);
|
|
185
|
+
expect(result.error).toContain('Element not found');
|
|
186
|
+
expect(mockPage.mouse.click).not.toHaveBeenCalled();
|
|
187
|
+
expect(mockPage.keyboard.type).not.toHaveBeenCalled();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('execute - wait action', () => {
|
|
192
|
+
it('should execute wait action with specified duration', async () => {
|
|
193
|
+
const action: WaitAction = {
|
|
194
|
+
type: 'wait',
|
|
195
|
+
durationMs: 2000,
|
|
196
|
+
description: 'Wait for modal to appear',
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const result = await executor.execute(action);
|
|
200
|
+
|
|
201
|
+
expect(result.success).toBe(true);
|
|
202
|
+
expect(mockPage.waitForTimeout).toHaveBeenCalledWith(2000);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should handle zero duration wait', async () => {
|
|
206
|
+
const action: WaitAction = {
|
|
207
|
+
type: 'wait',
|
|
208
|
+
durationMs: 0,
|
|
209
|
+
description: 'Wait 0ms',
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const result = await executor.execute(action);
|
|
213
|
+
|
|
214
|
+
expect(result.success).toBe(true);
|
|
215
|
+
expect(mockPage.waitForTimeout).toHaveBeenCalledWith(0);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should handle short duration wait', async () => {
|
|
219
|
+
const action: WaitAction = {
|
|
220
|
+
type: 'wait',
|
|
221
|
+
durationMs: 100,
|
|
222
|
+
description: 'Wait 100ms',
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const result = await executor.execute(action);
|
|
226
|
+
|
|
227
|
+
expect(result.success).toBe(true);
|
|
228
|
+
expect(mockPage.waitForTimeout).toHaveBeenCalledWith(100);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('execute - navigate action', () => {
|
|
233
|
+
it('should execute navigate action with correct options', async () => {
|
|
234
|
+
const action: NavigateAction = {
|
|
235
|
+
type: 'navigate',
|
|
236
|
+
url: 'https://linkedin.com/in/another-user',
|
|
237
|
+
description: 'Navigate to another profile',
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const result = await executor.execute(action);
|
|
241
|
+
|
|
242
|
+
expect(result.success).toBe(true);
|
|
243
|
+
expect(mockPage.goto).toHaveBeenCalledWith('https://linkedin.com/in/another-user', {
|
|
244
|
+
waitUntil: 'domcontentloaded',
|
|
245
|
+
timeout: 30000,
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should handle navigation to root URL', async () => {
|
|
250
|
+
const action: NavigateAction = {
|
|
251
|
+
type: 'navigate',
|
|
252
|
+
url: 'https://linkedin.com',
|
|
253
|
+
description: 'Navigate to LinkedIn home',
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const result = await executor.execute(action);
|
|
257
|
+
|
|
258
|
+
expect(result.success).toBe(true);
|
|
259
|
+
expect(mockPage.goto).toHaveBeenCalledWith('https://linkedin.com', {
|
|
260
|
+
waitUntil: 'domcontentloaded',
|
|
261
|
+
timeout: 30000,
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('execute - unknown action type', () => {
|
|
267
|
+
it('should return error for unknown action type', async () => {
|
|
268
|
+
const action = {
|
|
269
|
+
type: 'unknown' as const,
|
|
270
|
+
description: 'Unknown action',
|
|
271
|
+
} as unknown as Action;
|
|
272
|
+
|
|
273
|
+
const result = await executor.execute(action);
|
|
274
|
+
|
|
275
|
+
expect(result.success).toBe(false);
|
|
276
|
+
expect(result.error).toContain('Unknown action type');
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('execute - error handling', () => {
|
|
281
|
+
it('should handle mouse click errors', async () => {
|
|
282
|
+
const errorMessage = 'Element not interactable';
|
|
283
|
+
vi.mocked(mockPage.mouse.click).mockRejectedValueOnce(new Error(errorMessage));
|
|
284
|
+
|
|
285
|
+
const action: ClickAction = {
|
|
286
|
+
type: 'click',
|
|
287
|
+
elementId: 'btn-connect',
|
|
288
|
+
description: 'Click button',
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const result = await executor.execute(action);
|
|
292
|
+
|
|
293
|
+
expect(result.success).toBe(false);
|
|
294
|
+
expect(result.error).toBe(errorMessage);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should handle keyboard type errors', async () => {
|
|
298
|
+
const errorMessage = 'Element is not focused';
|
|
299
|
+
vi.mocked(mockPage.keyboard.type).mockRejectedValueOnce(new Error(errorMessage));
|
|
300
|
+
|
|
301
|
+
const action: TypeAction = {
|
|
302
|
+
type: 'type',
|
|
303
|
+
elementId: 'input-message',
|
|
304
|
+
text: 'Test',
|
|
305
|
+
description: 'Type text',
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const result = await executor.execute(action);
|
|
309
|
+
|
|
310
|
+
expect(result.success).toBe(false);
|
|
311
|
+
expect(result.error).toBe(errorMessage);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should handle navigation errors', async () => {
|
|
315
|
+
const errorMessage = 'net::ERR_NAME_NOT_RESOLVED';
|
|
316
|
+
vi.mocked(mockPage.goto).mockRejectedValueOnce(new Error(errorMessage));
|
|
317
|
+
|
|
318
|
+
const action: NavigateAction = {
|
|
319
|
+
type: 'navigate',
|
|
320
|
+
url: 'https://invalid-url',
|
|
321
|
+
description: 'Navigate to invalid URL',
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const result = await executor.execute(action);
|
|
325
|
+
|
|
326
|
+
expect(result.success).toBe(false);
|
|
327
|
+
expect(result.error).toBe(errorMessage);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should handle non-Error exceptions', async () => {
|
|
331
|
+
vi.mocked(mockPage.mouse.click).mockRejectedValueOnce('String error');
|
|
332
|
+
|
|
333
|
+
const action: ClickAction = {
|
|
334
|
+
type: 'click',
|
|
335
|
+
elementId: 'btn-connect',
|
|
336
|
+
description: 'Click button',
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const result = await executor.execute(action);
|
|
340
|
+
|
|
341
|
+
expect(result.success).toBe(false);
|
|
342
|
+
expect(result.error).toBe('String error');
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe('executeAll', () => {
|
|
347
|
+
it('should execute multiple actions sequentially', async () => {
|
|
348
|
+
const actions: Action[] = [
|
|
349
|
+
{ type: 'click', elementId: 'btn-connect', description: 'Click connect' },
|
|
350
|
+
{ type: 'wait', durationMs: 1000, description: 'Wait for modal' },
|
|
351
|
+
{ type: 'click', elementId: 'btn-submit', description: 'Click submit' },
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
const results = await executor.executeAll(actions);
|
|
355
|
+
|
|
356
|
+
expect(results).toHaveLength(3);
|
|
357
|
+
expect(results[0].success).toBe(true);
|
|
358
|
+
expect(results[0].action.type).toBe('click');
|
|
359
|
+
expect(results[1].success).toBe(true);
|
|
360
|
+
expect(results[1].action.type).toBe('wait');
|
|
361
|
+
expect(results[2].success).toBe(true);
|
|
362
|
+
expect(results[2].action.type).toBe('click');
|
|
363
|
+
|
|
364
|
+
// Each result should have a timestamp
|
|
365
|
+
results.forEach((result) => {
|
|
366
|
+
expect(result.timestamp).toBeInstanceOf(Date);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should add 500ms delay between actions', async () => {
|
|
371
|
+
const actions: Action[] = [
|
|
372
|
+
{ type: 'click', elementId: 'btn-connect', description: 'Click connect' },
|
|
373
|
+
{ type: 'click', elementId: 'btn-submit', description: 'Click submit' },
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
await executor.executeAll(actions);
|
|
377
|
+
|
|
378
|
+
// Should have waitForTimeout called for the 500ms delay between actions
|
|
379
|
+
// plus potentially 100ms for type actions (not in this test)
|
|
380
|
+
const waitCalls = vi.mocked(mockPage.waitForTimeout).mock.calls;
|
|
381
|
+
const hasDelay = waitCalls.some((call) => call[0] === 500);
|
|
382
|
+
expect(hasDelay).toBe(true);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should stop on first failure', async () => {
|
|
386
|
+
const actions: Action[] = [
|
|
387
|
+
{ type: 'click', elementId: 'btn-connect', description: 'Click connect' },
|
|
388
|
+
{ type: 'click', elementId: 'non-existent', description: 'Click non-existent' },
|
|
389
|
+
{ type: 'click', elementId: 'btn-submit', description: 'Should not execute' },
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
const results = await executor.executeAll(actions);
|
|
393
|
+
|
|
394
|
+
// Should only have 2 results (stopped after failure)
|
|
395
|
+
expect(results).toHaveLength(2);
|
|
396
|
+
expect(results[0].success).toBe(true);
|
|
397
|
+
expect(results[1].success).toBe(false);
|
|
398
|
+
expect(results[1].error).toContain('Element not found');
|
|
399
|
+
|
|
400
|
+
// Third action should not have been executed
|
|
401
|
+
expect(mockPage.mouse.click).toHaveBeenCalledTimes(1);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('should log each action before execution', async () => {
|
|
405
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
406
|
+
|
|
407
|
+
const actions: Action[] = [
|
|
408
|
+
{ type: 'click', elementId: 'btn-connect', description: 'Click connect button' },
|
|
409
|
+
];
|
|
410
|
+
|
|
411
|
+
await executor.executeAll(actions);
|
|
412
|
+
|
|
413
|
+
expect(consoleSpy).toHaveBeenCalledWith('Executing action: Click connect button');
|
|
414
|
+
|
|
415
|
+
consoleSpy.mockRestore();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('should log error when action fails', async () => {
|
|
419
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
420
|
+
|
|
421
|
+
const actions: Action[] = [
|
|
422
|
+
{ type: 'click', elementId: 'non-existent', description: 'Click non-existent' },
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
await executor.executeAll(actions);
|
|
426
|
+
|
|
427
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Action failed:'));
|
|
428
|
+
|
|
429
|
+
consoleErrorSpy.mockRestore();
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('should handle empty action array', async () => {
|
|
433
|
+
const results = await executor.executeAll([]);
|
|
434
|
+
|
|
435
|
+
expect(results).toHaveLength(0);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('should handle single action', async () => {
|
|
439
|
+
const actions: Action[] = [{ type: 'wait', durationMs: 500, description: 'Wait briefly' }];
|
|
440
|
+
|
|
441
|
+
const results = await executor.executeAll(actions);
|
|
442
|
+
|
|
443
|
+
expect(results).toHaveLength(1);
|
|
444
|
+
expect(results[0].success).toBe(true);
|
|
445
|
+
|
|
446
|
+
// No delay after the last action
|
|
447
|
+
const waitCalls = vi.mocked(mockPage.waitForTimeout).mock.calls;
|
|
448
|
+
expect(waitCalls).toHaveLength(1); // Only the wait action itself, no delay after
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('should include error in ExecutedAction when action fails', async () => {
|
|
452
|
+
const actions: Action[] = [
|
|
453
|
+
{ type: 'click', elementId: 'non-existent', description: 'Click non-existent' },
|
|
454
|
+
];
|
|
455
|
+
|
|
456
|
+
const results = await executor.executeAll(actions);
|
|
457
|
+
|
|
458
|
+
expect(results[0].success).toBe(false);
|
|
459
|
+
expect(results[0].error).toBeDefined();
|
|
460
|
+
expect(results[0].error).toContain('Element not found');
|
|
461
|
+
expect(results[0].timestamp).toBeInstanceOf(Date);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
});
|