9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
173 lines
6.8 KiB
JavaScript
173 lines
6.8 KiB
JavaScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
import './salesnav-message.js';
|
|
|
|
const {
|
|
parseSalesProfileUrn,
|
|
parseRecipient,
|
|
salesLeadUrlFromParts,
|
|
profileApiUrl,
|
|
buildCreateMessagePayload,
|
|
extractRemainingCredits,
|
|
profileSummary,
|
|
requireProfileSummary,
|
|
salesPageShowsSentMessage,
|
|
} = await import('./salesnav-message.js').then((m) => m.__test__);
|
|
|
|
function createPageMock(evaluateResults = []) {
|
|
const evaluate = vi.fn();
|
|
for (const result of evaluateResults) evaluate.mockResolvedValueOnce(result);
|
|
evaluate.mockResolvedValue(undefined);
|
|
return {
|
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
evaluate,
|
|
getCookies: vi.fn().mockResolvedValue([{ name: 'JSESSIONID', value: '"csrf"', domain: '.linkedin.com' }]),
|
|
};
|
|
}
|
|
|
|
describe('linkedin salesnav-message command', () => {
|
|
it('parses Sales Navigator profile urns and lead URLs', () => {
|
|
const urn = 'urn:li:fs_salesProfile:(ACwAAAJS8TABxyz,NAME_SEARCH,Enlo)';
|
|
expect(parseSalesProfileUrn(urn)).toMatchObject({
|
|
profileId: 'ACwAAAJS8TABxyz',
|
|
authType: 'NAME_SEARCH',
|
|
authToken: 'Enlo',
|
|
entityUrn: urn,
|
|
});
|
|
expect(parseSalesProfileUrn('urn:li:fs_salesProfile:(ACwAAAJS8TABxyz,undefined,undefined)')).toBeNull();
|
|
|
|
const parsed = parseRecipient('https://www.linkedin.com/sales/lead/ACwAAAJS8TABxyz,NAME_SEARCH,Enlo');
|
|
expect(parsed).toMatchObject({ profileId: 'ACwAAAJS8TABxyz', authType: 'NAME_SEARCH', authToken: 'Enlo' });
|
|
expect(parsed.entityUrn).toBe(urn);
|
|
expect(salesLeadUrlFromParts(parsed)).toBe('https://www.linkedin.com/sales/lead/ACwAAAJS8TABxyz,NAME_SEARCH,Enlo');
|
|
});
|
|
|
|
it('accepts LinkedIn /in tokens as unresolved recipients', () => {
|
|
expect(parseRecipient('https://www.linkedin.com/in/ACwAAAJS8TABxyz/')).toMatchObject({
|
|
profileId: 'ACwAAAJS8TABxyz',
|
|
authType: '',
|
|
authToken: '',
|
|
entityUrn: '',
|
|
});
|
|
});
|
|
|
|
it('builds profile API URLs with the Sales Navigator auth key', () => {
|
|
const url = profileApiUrl({ profileId: 'P1', authType: 'NAME_SEARCH', authToken: 'T1' });
|
|
expect(url).toContain('/sales-api/salesApiProfiles/(profileId:P1,authType:NAME_SEARCH,authToken:T1)');
|
|
expect(url).toContain('decoration=');
|
|
});
|
|
|
|
it('constructs the createMessage action payload used by Sales Navigator', () => {
|
|
const payload = buildCreateMessagePayload({
|
|
recipientUrn: 'urn:li:fs_salesProfile:(P1,NAME_SEARCH,T1)',
|
|
subject: 'Quick QA doc question',
|
|
body: 'Hi Jane, can I ask a quick question?',
|
|
trackingId: '0123456789abcdef',
|
|
copyToCrm: false,
|
|
});
|
|
expect(payload).toEqual({
|
|
createMessageRequest: {
|
|
recipients: ['urn:li:fs_salesProfile:(P1,NAME_SEARCH,T1)'],
|
|
subject: 'Quick QA doc question',
|
|
body: 'Hi Jane, can I ask a quick question?',
|
|
copyToCrm: false,
|
|
trackingId: '0123456789abcdef',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('validates payload fields before any send attempt', () => {
|
|
expect(() => buildCreateMessagePayload({ recipientUrn: '', subject: 's', body: 'b' })).toThrow();
|
|
expect(() => buildCreateMessagePayload({ recipientUrn: 'urn:li:fs_salesProfile:(P,A,T)', subject: '', body: 'b' })).toThrow();
|
|
expect(() => buildCreateMessagePayload({ recipientUrn: 'urn:li:fs_salesProfile:(P,A,T)', subject: 's', body: '' })).toThrow();
|
|
expect(() => buildCreateMessagePayload({ recipientUrn: 'urn:li:fs_salesProfile:(P,A,T)', subject: 'x'.repeat(201), body: 'b' })).toThrow();
|
|
expect(() => buildCreateMessagePayload({ recipientUrn: 'urn:li:fs_salesProfile:(P,undefined,undefined)', subject: 's', body: 'b' })).toThrow();
|
|
});
|
|
|
|
it('detects the Sales Navigator sent activity on a verified lead page', () => {
|
|
expect(salesPageShowsSentMessage('5/18/2026 You sent a Sales Navigator message to Jane', 'Jane Q')).toBe(true);
|
|
expect(salesPageShowsSentMessage('No recent activity', 'Jane Q')).toBe(false);
|
|
});
|
|
|
|
it('extracts a plausible remaining InMail credit count', () => {
|
|
expect(extractRemainingCredits({ elements: [{ type: 'LSS_INMAIL', value: 149, id: 1 }], paging: { count: 10 } })).toBe(149);
|
|
expect(extractRemainingCredits({ data: { remaining: 149, used: 1 } })).toBe(149);
|
|
expect(extractRemainingCredits({ elements: [{ availableCount: 12 }] })).toBe(12);
|
|
expect(extractRemainingCredits({})).toBe(null);
|
|
});
|
|
|
|
it('summarizes decorated Sales Navigator profile data', () => {
|
|
expect(profileSummary({ data: {
|
|
fullName: 'Rayki Goh',
|
|
headline: 'Food Safety',
|
|
degree: 3,
|
|
defaultPosition: { title: 'FSQA Manager', companyName: 'Acme Foods' },
|
|
memberBadges: { openLink: false },
|
|
} })).toMatchObject({
|
|
recipient: 'Rayki Goh',
|
|
title: 'FSQA Manager',
|
|
company: 'Acme Foods',
|
|
degree: '3',
|
|
open_link: false,
|
|
});
|
|
});
|
|
|
|
it('fails typed when decorated profile data has no recipient identity', () => {
|
|
expect(() => requireProfileSummary({ data: { defaultPosition: { title: 'FSQA' } } }))
|
|
.toThrow(CommandExecutionError);
|
|
});
|
|
|
|
it('keeps manifest columns aligned with dry-run rows and fails typed on malformed profile API', async () => {
|
|
const cmd = getRegistry().get('linkedin/salesnav-message');
|
|
expect(cmd?.columns).toEqual([
|
|
'status',
|
|
'recipient',
|
|
'title',
|
|
'company',
|
|
'credits_remaining',
|
|
'credits_before',
|
|
'credits_after',
|
|
'sent_in_salesnav',
|
|
'message_chars',
|
|
'subject_chars',
|
|
'recipient_urn',
|
|
'degree',
|
|
'inmail_restriction',
|
|
'open_link',
|
|
]);
|
|
|
|
const goodPage = createPageMock([
|
|
{ status: 200, json: { data: { fullName: 'Jane Doe', defaultPosition: { title: 'QA', companyName: 'Acme' }, degree: 2, inmailRestriction: 'NO_RESTRICTION', memberBadges: { openLink: true } } } },
|
|
{ status: 200, json: { elements: [{ type: 'LSS_INMAIL', value: 12 }] } },
|
|
]);
|
|
const rows = await cmd.func(goodPage, {
|
|
recipient: 'urn:li:fs_salesProfile:(P1,NAME_SEARCH,T1)',
|
|
subject: 'Hello',
|
|
body: 'Quick question',
|
|
});
|
|
expect(Object.keys(rows[0]).sort()).toEqual([...cmd.columns].sort());
|
|
expect(rows[0]).toMatchObject({
|
|
status: 'validated_dry_run',
|
|
recipient: 'Jane Doe',
|
|
credits_remaining: 12,
|
|
credits_before: 12,
|
|
credits_after: '',
|
|
sent_in_salesnav: false,
|
|
degree: '2',
|
|
inmail_restriction: 'NO_RESTRICTION',
|
|
open_link: true,
|
|
});
|
|
|
|
const malformedPage = createPageMock([
|
|
{ status: 200, json: { data: { defaultPosition: { title: 'QA' } } } },
|
|
]);
|
|
await expect(cmd.func(malformedPage, {
|
|
recipient: 'urn:li:fs_salesProfile:(P1,NAME_SEARCH,T1)',
|
|
subject: 'Hello',
|
|
body: 'Quick question',
|
|
})).rejects.toThrow(CommandExecutionError);
|
|
});
|
|
});
|