diff --git a/lib/apify/__tests__/startLinkedinProfileScraping.test.ts b/lib/apify/__tests__/startLinkedinProfileScraping.test.ts index dcd48700..07c05dc5 100644 --- a/lib/apify/__tests__/startLinkedinProfileScraping.test.ts +++ b/lib/apify/__tests__/startLinkedinProfileScraping.test.ts @@ -35,4 +35,32 @@ describe("startLinkedinProfileScraping", () => { ); expect(start).not.toHaveBeenCalled(); }); + + it("runs the posts actor with maxPosts when a posts depth is requested", async () => { + const r = await startLinkedinProfileScraping("sweetmaneth", 20); + expect(apifyClient.actor).toHaveBeenCalledWith("harvestapi/linkedin-profile-posts"); + expect(start).toHaveBeenCalledWith( + { targetUrls: ["https://www.linkedin.com/in/sweetmaneth"], maxPosts: 20 }, + { webhooks: expect.any(Array) }, + ); + expect(r).toEqual({ runId: "run-1", datasetId: "ds-1" }); + }); + + it("posts depth keeps the full-URL passthrough and the handle guards", async () => { + await startLinkedinProfileScraping("https://www.linkedin.com/in/drew-thurlow", 5); + expect(start).toHaveBeenCalledWith( + { targetUrls: ["https://www.linkedin.com/in/drew-thurlow"], maxPosts: 5 }, + { webhooks: expect.any(Array) }, + ); + await expect(startLinkedinProfileScraping("in", 5)).rejects.toThrow(/Invalid LinkedIn handle/); + }); + + it("posts omitted keeps the legacy profile actor and input byte-identical", async () => { + await startLinkedinProfileScraping("sweetmaneth", undefined); + expect(apifyClient.actor).toHaveBeenCalledWith("harvestapi/linkedin-profile-scraper"); + expect(start).toHaveBeenCalledWith( + { urls: ["https://www.linkedin.com/in/sweetmaneth"] }, + { webhooks: expect.any(Array) }, + ); + }); }); diff --git a/lib/apify/linkedin/startLinkedinProfileScraping.ts b/lib/apify/linkedin/startLinkedinProfileScraping.ts index ff323194..d9752d0d 100644 --- a/lib/apify/linkedin/startLinkedinProfileScraping.ts +++ b/lib/apify/linkedin/startLinkedinProfileScraping.ts @@ -4,14 +4,24 @@ import { OUTSTANDING_ERROR } from "@/lib/apify/errors"; import { ApifyRunInfo } from "@/lib/apify/types"; /** - * Starts a LinkedIn profile scrape via the harvestapi actor. + * Starts a LinkedIn scrape via the harvestapi actors. * Mirrors the other `startProfileScraping` modules. * + * The profile actor's dataset has no post fields, so a requested `posts` + * depth switches to the posts actor (post items with engagement counts) — + * the same semantics as X/YouTube: depth requested → post items in the + * dataset; omitted → the legacy profile snapshot. + * * @param handle - A LinkedIn vanity slug (e.g. `drew-thurlow`) or full profile URL. + * @param posts - Max recent posts to scrape; omitted → profile snapshot. * @returns Apify run info, or null if the run failed to start. * @see https://apify.com/harvestapi/linkedin-profile-scraper + * @see https://apify.com/harvestapi/linkedin-profile-posts */ -const startLinkedinProfileScraping = async (handle: string): Promise => { +const startLinkedinProfileScraping = async ( + handle: string, + posts?: number, +): Promise => { const cleanHandle = handle.trim().replace(/^@/, ""); // Legacy rows stored the LinkedIn path prefix ("in") as the username — never // scrape those (linkedin.com/in/in is a real, wrong profile URL); fail loudly. @@ -23,12 +33,15 @@ const startLinkedinProfileScraping = async (handle: string): Promise