Skip to content

Commit c2069ef

Browse files
committed
fix(core): navigate to loginUrl before isLoggedIn when browser is at about:blank
On fresh daemon launch every browser starts at about:blank. Adapters that use URL or DOM selectors for auth checking would always return false at the 500ms startup banner check, showing a spurious "not logged in" warning. Fix: getStatus() and isSessionValid() now navigate to adapter.loginUrl before calling isLoggedIn() when the page is at about:blank. Every adapter gets correct startup auth detection without needing adapter-level workarounds. Also updates the create-adapter scaffold template with: - Selector stability hierarchy (ARIA > test-id > semantic > class names) - page.evaluate() + walk-up-from-aria-button extraction pattern - Comment that framework handles about:blank so isLoggedIn() is reliable Made-with: Cursor
1 parent 3a02467 commit c2069ef

4 files changed

Lines changed: 64 additions & 7 deletions

File tree

.context/progress.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,30 @@
88
- Archived original brainstorm MHTML to `docs/reference/`
99
- Scaffolded full AI-native project structure (CLAUDE.md, .context/, .claude/, .cursor/)
1010

11-
## Session 4 — Google Discover adapter + core improvements (2026-03-23)
11+
## Session 5 — LinkedIn adapter: live testing & bug fixes (2026-03-24)
12+
13+
### LinkedIn adapter bugs fixed
14+
15+
- **`isLoggedIn` false negative on startup**: Browser starts at `about:blank`; URL wasn't an authenticated path so `isLoggedIn` always returned `false`. Fix: navigate to `linkedin.com/feed/` first if on `about:blank` — cookies load and auth check succeeds. LinkedIn now shows `logged in` in the daemon startup banner.
16+
- **`get_feed` selector failure**: `div[data-id^="urn:li:activity"]` stopped matching LinkedIn's current DOM. Rewrote feed extraction using `page.evaluate()` to find post cards by walking up from social action buttons (`aria-label` containing "like"/"comment"/"repost"/"send") — resilient to React component version changes and class-name churn.
17+
- **Updated `selectors.ts`**: Expanded `feedPost` to a multi-selector comma list as fallback; updated `feedPostAuthorName`, `feedPostText`, `feedPostReactions` with modern class alternatives.
18+
19+
### Verified working tools (all 7)
20+
21+
- `get_feed` — returns real feed posts with author, text, context blocks
22+
- `get_person_profile` — works (Bill Gates test: returns `main_profile`, `experience`, etc.)
23+
- `search_people` — works (`keywords` param; returns paginated LinkedIn search results)
24+
- `search_jobs` — works (returns job listings with title, company, location)
25+
- `get_company_profile`, `get_company_posts`, `get_job_details` — not yet live-tested but share same `extractPage`/`extractOverlay` path
26+
27+
### Daemon status at end of session
28+
29+
All 3 adapters logged in and serving tools:
30+
- `hackernews` → port 52741 ✓
31+
- `google-discover` → port 52743 ✓
32+
- `linkedin` → port 52744 ✓
33+
34+
1235

1336
### Core framework additions
1437

packages/core/src/adapter-server.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,11 @@ export async function createAdapterServer(
542542
let loggedIn = false;
543543
try {
544544
const page = await sessionManager.getPage(sessionConfig);
545+
// On fresh launch the page is at about:blank — navigate to the site
546+
// first so URL-based isLoggedIn checks work correctly.
547+
if (page.url() === "about:blank" && adapter.loginUrl) {
548+
await page.goto(adapter.loginUrl, { waitUntil: "domcontentloaded", timeout: 20_000 }).catch(() => {});
549+
}
545550
loggedIn = await adapter.isLoggedIn(page);
546551
} catch { loggedIn = false; }
547552
return {

packages/core/src/create-adapter.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,15 +133,25 @@ function selectorsTs(name: string): string {
133133
return `/**
134134
* DOM selectors for ${name}.
135135
*
136-
* Primary strategy: getByRole / getByTestId (resilient to DOM changes).
137-
* CSS selectors as fallback only.
136+
* Selector stability hierarchy (most → least stable):
137+
* 1. ARIA attributes: [aria-label="..."], [role="..."] ← prefer these
138+
* 2. Test IDs: [data-testid="..."], [data-test-id="..."]
139+
* 3. Semantic HTML: button, nav, article, main
140+
* 4. CSS class names / data-* attributes ← avoid, rotate frequently
138141
*
139-
* Validate with: browserkit test-selectors ${name}
140-
* Update fixtures with: browserkit test-selectors ${name} --snapshot
142+
* For feed/list pages on JS-heavy apps (LinkedIn, Twitter, etc.) prefer
143+
* walking up from stable ARIA-labelled action buttons rather than targeting
144+
* the card container directly by class name:
145+
*
146+
* const cards = Array.from(document.querySelectorAll('button[aria-label*="like"]'))
147+
* .map(btn => { let el = btn; while (el.offsetHeight < 150) el = el.parentElement; return el; })
148+
*
149+
* Validate selectors against a live page with: browserkit test-selectors ${name}
141150
*/
142151
143152
export const SELECTORS = {
144-
// Auth detection
153+
// Auth detection — pick an element that is ONLY visible when logged in
154+
// Prefer ARIA/role selectors: e.g. 'nav[aria-label="Main"]', '[data-test-id="nav-top"]'
145155
loggedInIndicator: "TODO: selector for element that only appears when logged in",
146156
loginForm: "TODO: selector for login form",
147157
@@ -165,6 +175,8 @@ export default defineAdapter({
165175
rateLimit: { minDelayMs: 2000 },
166176
167177
async isLoggedIn(page: Page): Promise<boolean> {
178+
// The framework navigates to loginUrl before calling this if the
179+
// browser is at about:blank, so page.url() is reliable here.
168180
try {
169181
return await page.locator(SELECTORS.loggedInIndicator).isVisible({ timeout: 3000 });
170182
} catch {
@@ -188,7 +200,19 @@ export default defineAdapter({
188200
timeout: 30_000,
189201
});
190202
191-
// TODO: implement extraction logic
203+
// Prefer page.evaluate() + innerText over CSS class selectors.
204+
// CSS classes rotate on JS-heavy apps; innerText and ARIA labels are stable.
205+
// Example: get items from a list by walking up from action buttons:
206+
//
207+
// const items = await page.evaluate((max) => {
208+
// return Array.from(document.querySelectorAll('button[aria-label*="save"]'))
209+
// .slice(0, max)
210+
// .map(btn => {
211+
// let el = btn as HTMLElement;
212+
// while (el.parentElement && el.offsetHeight < 100) el = el.parentElement;
213+
// return el.innerText.trim().slice(0, 500);
214+
// });
215+
// }, count);
192216
const results: unknown[] = [];
193217
194218
return {

packages/core/src/session-manager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ export class SessionManager {
4040
async isSessionValid(config: SessionConfig, adapter: SiteAdapter): Promise<boolean> {
4141
try {
4242
const page = await this.getPage(config);
43+
// Navigate to the site if the browser hasn't loaded any page yet,
44+
// otherwise URL-based isLoggedIn checks always return false.
45+
if (page.url() === "about:blank" && adapter.loginUrl) {
46+
await page.goto(adapter.loginUrl, { waitUntil: "domcontentloaded", timeout: 20_000 }).catch(() => {});
47+
}
4348
return await adapter.isLoggedIn(page);
4449
} catch {
4550
return false;

0 commit comments

Comments
 (0)