Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/adapters/browser/linkedin.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@
| Command | Description |
|---------|-------------|
| `opencli linkedin search` | |
| `opencli linkedin timeline` | Read posts from your LinkedIn home feed |

## Usage Examples

```bash
# Quick start
opencli linkedin search --limit 5

# Read your home timeline
opencli linkedin timeline --limit 5

# JSON output
opencli linkedin search -f json

opencli linkedin timeline -f json

# Verbose mode
opencli linkedin search -v
```
Expand Down
2 changes: 1 addition & 1 deletion docs/adapters/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Run `opencli list` for the live registry.
| **[v2ex](/adapters/browser/v2ex)** | `hot` `latest` `topic` `node` `user` `member` `replies` `nodes` `daily` `me` `notifications` | 🌐 / 🔐 |
| **[bloomberg](/adapters/browser/bloomberg)** | `main` `markets` `economics` `industries` `tech` `politics` `businessweek` `opinions` `feeds` `news` | 🌐 / 🔐 |
| **[weibo](/adapters/browser/weibo)** | `hot` `search` | 🔐 Browser |
| **[linkedin](/adapters/browser/linkedin)** | `search` | 🔐 Browser |
| **[linkedin](/adapters/browser/linkedin)** | `search` `timeline` | 🔐 Browser |
| **[coupang](/adapters/browser/coupang)** | `search` `add-to-cart` | 🔐 Browser |
| **[boss](/adapters/browser/boss)** | `search` `detail` `recommend` `joblist` `greet` `batchgreet` `send` `chatlist` `chatmsg` `invite` `mark` `exchange` `resume` `stats` | 🔐 Browser |
| **[ctrip](/adapters/browser/ctrip)** | `search` | 🔐 Browser |
Expand Down
99 changes: 99 additions & 0 deletions src/clis/linkedin/timeline.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { describe, expect, it } from 'vitest';
import { getRegistry } from '../../registry.js';
import './timeline.js';

const { parseMetric, buildPostId, mergeTimelinePosts } = await import('./timeline.js').then(
(m) => (m as any).__test__,
);

describe('linkedin timeline adapter', () => {
const command = getRegistry().get('linkedin/timeline');

it('registers the command with correct shape', () => {
expect(command).toBeDefined();
expect(command!.site).toBe('linkedin');
expect(command!.name).toBe('timeline');
expect(command!.domain).toBe('www.linkedin.com');
expect(command!.strategy).toBe('cookie');
expect(command!.browser).toBe(true);
expect(typeof command!.func).toBe('function');
});

it('has limit arg with default 20', () => {
const limitArg = command!.args.find((a) => a.name === 'limit');
expect(limitArg).toBeDefined();
expect(limitArg!.default).toBe(20);
});

it('includes expected columns', () => {
expect(command!.columns).toEqual(
expect.arrayContaining(['author', 'text', 'reactions', 'comments', 'url']),
);
});
});

describe('parseMetric', () => {
it('parses plain numbers', () => {
expect(parseMetric('42')).toBe(42);
expect(parseMetric('1,234')).toBe(1234);
});

it('handles k/m suffixes', () => {
expect(parseMetric('2.5k')).toBe(2500);
expect(parseMetric('1.2M')).toBe(1200000);
});

it('returns 0 for empty/undefined', () => {
expect(parseMetric('')).toBe(0);
expect(parseMetric(undefined)).toBe(0);
expect(parseMetric(null)).toBe(0);
});
});

describe('buildPostId', () => {
it('uses url when present', () => {
expect(buildPostId({ url: 'https://linkedin.com/post/123' })).toBe(
'https://linkedin.com/post/123',
);
});

it('falls back to composite key', () => {
const id = buildPostId({ author: 'Alice', posted_at: '2h', text: 'Hello world' });
expect(id).toBe('Alice::2h::Hello world');
});
});

describe('mergeTimelinePosts', () => {
it('deduplicates by url', () => {
const url = 'https://linkedin.com/post/1';
const a = {
id: url,
author: 'Alice',
author_url: '',
headline: '',
text: 'Hello',
posted_at: '1h',
reactions: 5,
comments: 1,
url,
};
const result = mergeTimelinePosts([a], [a]);
expect(result).toHaveLength(1);
});

it('skips posts without author or text', () => {
const empty = {
id: '2',
author: '',
author_url: '',
headline: '',
text: 'some text',
posted_at: '',
reactions: 0,
comments: 0,
url: '',
};
const result = mergeTimelinePosts([], [empty]);
expect(result).toHaveLength(0);
});
});
Loading