1bc92e1f7d
CDP protocol fixes:
- session.ts: getDocumentRootNodeId() now pushes {depth:-1} full DOM tree
(was shallow default, causing querySelectorAll to find zero nodes)
- extraction.ts: serialize resolveSelector calls (was concurrent Promise.all,
CDP DOM.querySelectorAll is not safe for concurrent calls on same session)
Topology ID mismatch fixes (critical — backendNodeId vs solver subject ID):
- extraction.ts: add remapTopologyIds() — builds Map<backendNodeId,solverId>
from world.subjects.domNodeId/ids, remaps all 6 topology arrays +
clipping.subjectId + scroll.containerId
- topology.ts: createsStackingContext() now checks contain:paint|layout
(both create stacking contexts per CSS spec, was missing)
- topology.ts: evaluateInStackingContext() handles unary (no reference)
Predicate completeness:
- predicates.ts: 3 new evaluators — intersects, touches, hasGap (indices 31-33)
- predicates.ts: escapeClippingChainOf replaced indeterminate stub with
deterministic 2-ary evaluator (overflow + clip chain + ancestry checks)
- predicates.ts: attachedToScrollContainer arity 1→2, checks reference match
- predicates.ts: 7 aliases + 4 alignment aliases promoted to BUILTIN_PREDICATES
DSL grammar fixes:
- grammar.ts: isKeywordThatCanBePredicate() extended with all missing keywords
- lower-to-canonical.ts: add TopologyAssertion lowering (was silently dropped)
- compiler.ts: add intersects/touches/hasGap relation codes
- validator.ts: add option sets for new predicates
- lexer.ts: add intersects/touches/hasGap token kinds
Safety + infrastructure:
- joins.ts: MAX_CARTESIAN_TUPLES=100,000 guard with descriptive error
- extraction.ts: computeRequiredFacts() registers defaults + broadens topology gate
- package.json: add missing imhotep-state dependency to imhotep-playwright
Tests:
- 9 new predicate tests (intersects, touches, hasGap, escapeClippingChainOf)
- Convert stale separatedFrom/contains GAP tests to working assertions
- Add computeRequiredFacts topology gating tests (public.test.ts)
150 lines
4.2 KiB
TypeScript
150 lines
4.2 KiB
TypeScript
/**
|
|
* CDP Session Management
|
|
*
|
|
* Wraps Playwright's CDP session access to provide a typed, injectable
|
|
* interface for sending Chrome DevTools Protocol commands.
|
|
*
|
|
* Design: dependency injection over optional imports. The extractor receives
|
|
* a session factory rather than importing Playwright directly.
|
|
*/
|
|
|
|
/**
|
|
* Minimal CDP session interface. Any object that can send CDP commands
|
|
* and return JSON-like responses satisfies this contract.
|
|
*/
|
|
export interface CDPSession {
|
|
send<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>
|
|
on(event: string, handler: (payload: unknown) => void): void
|
|
off(event: string, handler: (payload: unknown) => void): void
|
|
detach(): Promise<void>
|
|
}
|
|
|
|
/**
|
|
* Factory that creates a CDP session for a given Playwright page.
|
|
* This is injected so the package does not hard-depend on Playwright.
|
|
*/
|
|
export type CDPSessionFactory = (pageRef: unknown) => Promise<CDPSession>
|
|
|
|
/**
|
|
* Playwright-backed session factory implementation.
|
|
*
|
|
* Usage:
|
|
* const session = await createPlaywrightCDPSession(page)
|
|
*
|
|
* Where `page` is a Playwright Page object.
|
|
*/
|
|
export async function createPlaywrightCDPSession(
|
|
page: unknown
|
|
): Promise<CDPSession> {
|
|
// Playwright pages expose context() and a CDP session method.
|
|
// We access it dynamically to avoid a hard import dependency.
|
|
const pwPage = page as {
|
|
context: () => {
|
|
newCDPSession: (p: unknown) => Promise<CDPSession>
|
|
}
|
|
}
|
|
|
|
const context = pwPage.context()
|
|
const session = await context.newCDPSession(page)
|
|
return session
|
|
}
|
|
|
|
/**
|
|
* Session manager that owns the lifecycle of a CDP session.
|
|
*
|
|
* Handles attach, detach, and basic error recovery.
|
|
* Caches document root nodeId and enabled domains to minimize round-trips.
|
|
*/
|
|
export class CDPSessionManager {
|
|
private session: CDPSession | null = null
|
|
private attachPromise: Promise<CDPSession> | null = null
|
|
private readonly factory: CDPSessionFactory
|
|
private readonly pageRef: unknown
|
|
private documentRootNodeId: number | null = null
|
|
private enabledDomains = new Set<string>()
|
|
|
|
constructor(factory: CDPSessionFactory, pageRef: unknown) {
|
|
this.factory = factory
|
|
this.pageRef = pageRef
|
|
}
|
|
|
|
/**
|
|
* Ensure a session is attached. Idempotent and concurrency-safe.
|
|
*/
|
|
async attach(): Promise<CDPSession> {
|
|
if (this.session) return this.session
|
|
if (this.attachPromise) return this.attachPromise
|
|
this.attachPromise = this.factory(this.pageRef).then((s) => {
|
|
this.session = s
|
|
this.documentRootNodeId = null
|
|
this.enabledDomains.clear()
|
|
return s
|
|
})
|
|
return this.attachPromise
|
|
}
|
|
|
|
/**
|
|
* Get cached document root nodeId, or fetch and cache it.
|
|
*/
|
|
async getDocumentRootNodeId(): Promise<number> {
|
|
if (this.documentRootNodeId !== null) {
|
|
return this.documentRootNodeId
|
|
}
|
|
const s = await this.attach()
|
|
const result = await s.send<{ root: { nodeId: number } }>('DOM.getDocument', { depth: -1 })
|
|
this.documentRootNodeId = result.root.nodeId
|
|
return this.documentRootNodeId
|
|
}
|
|
|
|
/**
|
|
* Enable a CDP domain if not already enabled.
|
|
*/
|
|
async enableDomain(domain: string): Promise<void> {
|
|
if (this.enabledDomains.has(domain)) {
|
|
return
|
|
}
|
|
const s = await this.attach()
|
|
await s.send(`${domain}.enable`)
|
|
this.enabledDomains.add(domain)
|
|
}
|
|
|
|
/**
|
|
* Get the current session, throwing if not attached.
|
|
*/
|
|
getSession(): CDPSession {
|
|
if (!this.session) {
|
|
throw new Error('CDP session not attached. Call attach() first.')
|
|
}
|
|
return this.session
|
|
}
|
|
|
|
/**
|
|
* Send a CDP command, automatically attaching if needed.
|
|
*/
|
|
async send<T = unknown>(
|
|
method: string,
|
|
params?: Record<string, unknown>
|
|
): Promise<T> {
|
|
const s = await this.attach()
|
|
return s.send<T>(method, params)
|
|
}
|
|
|
|
/**
|
|
* Detach and clean up the session.
|
|
*/
|
|
async detach(): Promise<void> {
|
|
if (this.session) {
|
|
await this.session.detach()
|
|
this.session = null
|
|
}
|
|
this.attachPromise = null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convenient builder for a manager using the Playwright factory.
|
|
*/
|
|
export function createSessionManager(page: unknown): CDPSessionManager {
|
|
return new CDPSessionManager(createPlaywrightCDPSession, page)
|
|
}
|