@ddwang/magnitude-core 0.3.1-ddwang.3 → 0.3.1-ddwang.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@ import { partitionHtml, serializeToMarkdown } from 'magnitude-extract';
6
6
  import EventEmitter from "eventemitter3";
7
7
  import { retry } from "@/common/retry";
8
8
  import { checkOperation } from '@/common/operation';
9
+ import { getVisiblePageContent } from '@/web/pageContent';
9
10
  // export interface StartAgentWithWebOptions {
10
11
  // agentBaseOptions?: Partial<AgentOptions>;
11
12
  // webConnectorOptions?: BrowserConnectorOptions;
@@ -29,47 +30,6 @@ export async function startBrowserAgent(options //StartAgentWithWebOptions = {}
29
30
  //console.log('agent started');
30
31
  return agent;
31
32
  }
32
- async function getFullPageContent(page) {
33
- checkOperation();
34
- // 1. Get all iframe element handles
35
- const iframeHandles = await page.locator('iframe').elementHandles();
36
- // 2. Iterate through each iframe handle
37
- for (const iframeHandle of iframeHandles) {
38
- checkOperation();
39
- // 3. Get the Frame object for the iframe
40
- const frame = await iframeHandle.contentFrame();
41
- checkOperation();
42
- if (frame) {
43
- // 4. Get the HTML content of the iframe
44
- const iframeContent = await frame.content();
45
- checkOperation();
46
- // 5. Use evaluate to replace the iframe element with its content.
47
- // We pass the content as an argument to avoid issues with string escaping.
48
- await iframeHandle.evaluate((iframeNode, { content }) => {
49
- // Create a new div element to hold the iframe's content
50
- const div = document.createElement('div');
51
- // Use DOMParser to handle Trusted Types restrictions
52
- const parser = new DOMParser();
53
- const doc = parser.parseFromString(content, 'text/html');
54
- // Move all body children to the div
55
- while (doc.body.firstChild) {
56
- div.appendChild(doc.body.firstChild);
57
- }
58
- // Also preserve any head elements that might be important (styles, etc)
59
- const headElements = doc.head.querySelectorAll('style, link[rel="stylesheet"]');
60
- headElements.forEach(el => div.appendChild(el.cloneNode(true)));
61
- // Add a data-attribute to mark that this was an expanded iframe
62
- div.dataset.expandedFromIframe = 'true';
63
- div.dataset.iframeSrc = iframeNode.getAttribute('src') || '';
64
- // Replace the iframeNode with the new div
65
- iframeNode.parentNode?.replaceChild(div, iframeNode);
66
- }, { content: iframeContent });
67
- }
68
- }
69
- // 6. Return the final, modified page content
70
- checkOperation();
71
- return page.content();
72
- }
73
33
  export class BrowserAgent extends Agent {
74
34
  browserAgentEvents = new EventEmitter();
75
35
  constructor({ agentOptions, browserOptions }) {
@@ -98,8 +58,7 @@ export class BrowserAgent extends Agent {
98
58
  }
99
59
  async _extract(instructions, schema) {
100
60
  this.browserAgentEvents.emit('extractStarted', instructions, schema);
101
- //const htmlContent = await this.page.content();
102
- const htmlContent = await retry(async () => await getFullPageContent(this.page), { retries: 5, delay: 200, exponential: true });
61
+ const htmlContent = await retry(() => getVisiblePageContent(this.page), { retries: 5, delay: 200, exponential: true });
103
62
  // const accessibilityTree = await this.page.accessibility.snapshot({ interestingOnly: true });
104
63
  // const pageRepr = renderMinimalAccessibilityTree(accessibilityTree);
105
64
  const partitionOptions = {
package/dist/index.cjs CHANGED
@@ -7200,6 +7200,72 @@ function narrateBrowserAgent(agent) {
7200
7200
  });
7201
7201
  }
7202
7202
 
7203
+ async function getVisiblePageContent(page) {
7204
+ return readFrame(page.mainFrame());
7205
+ }
7206
+ async function readFrame(frame) {
7207
+ checkOperation();
7208
+ const marker = crypto.randomUUID();
7209
+ const snapshot = await frame.evaluateHandle((marker2) => {
7210
+ const documentCopy = document.implementation.createHTMLDocument();
7211
+ const frames = [];
7212
+ const visibility = { checkOpacity: true, checkVisibility: true };
7213
+ function copy(source) {
7214
+ if (["script", "style", "template", "noscript"].includes(source.localName)) return null;
7215
+ const style = getComputedStyle(source);
7216
+ if (style.display === "none" || style.opacity === "0" || style.contentVisibility === "hidden") return null;
7217
+ const visible = style.visibility === "visible" && (style.display === "contents" || source.checkVisibility(visibility) || ["option", "optgroup"].includes(source.localName) && source.closest("select")?.checkVisibility(visibility));
7218
+ if (source instanceof HTMLIFrameElement || source instanceof HTMLFrameElement) {
7219
+ if (!visible) return null;
7220
+ frames.push(source);
7221
+ return documentCopy.createComment(`${marker2}:${frames.length - 1}`);
7222
+ }
7223
+ const target = documentCopy.importNode(source, false);
7224
+ if (source instanceof HTMLInputElement) {
7225
+ if (source.type === "password") target.removeAttribute("value");
7226
+ else target.setAttribute("value", source.value);
7227
+ target.toggleAttribute("checked", source.checked);
7228
+ }
7229
+ if (source instanceof HTMLOptionElement) target.toggleAttribute("selected", source.selected);
7230
+ if (source instanceof HTMLTextAreaElement) {
7231
+ target.textContent = visible ? source.value : "";
7232
+ return visible ? target : null;
7233
+ }
7234
+ const summary = source instanceof HTMLDetailsElement && !source.open ? source.querySelector("summary") : null;
7235
+ for (const child of source.childNodes) {
7236
+ if (source instanceof HTMLDetailsElement && !source.open && child !== summary) continue;
7237
+ if (child instanceof Element) {
7238
+ const copied = copy(child);
7239
+ if (copied) target.appendChild(copied);
7240
+ } else if (visible && child.nodeType === Node.TEXT_NODE) {
7241
+ target.appendChild(documentCopy.importNode(child, false));
7242
+ }
7243
+ }
7244
+ return visible || target.hasChildNodes() ? target : null;
7245
+ }
7246
+ const body = document.body && copy(document.body);
7247
+ return { html: body instanceof Element ? body.innerHTML : "", frames };
7248
+ }, marker);
7249
+ const handles = [snapshot];
7250
+ try {
7251
+ let html = await snapshot.evaluate(({ html: html2 }) => html2);
7252
+ const frames = await snapshot.getProperty("frames");
7253
+ handles.push(frames);
7254
+ const entries = await frames.getProperties();
7255
+ handles.push(...entries.values());
7256
+ for (const [index, handle] of entries) {
7257
+ checkOperation();
7258
+ const child = await handle.asElement()?.contentFrame();
7259
+ const content = child ? await readFrame(child) : "";
7260
+ html = html.replace(`<!--${marker}:${index}-->`, () => `<div>${content}</div>`);
7261
+ }
7262
+ checkOperation();
7263
+ return html;
7264
+ } finally {
7265
+ await Promise.all(handles.map((handle) => handle.dispose()));
7266
+ }
7267
+ }
7268
+
7203
7269
  async function startBrowserAgent(options) {
7204
7270
  const { agentOptions, browserOptions } = buildDefaultBrowserAgentOptions({ agentOptions: options ?? {}, browserOptions: options ?? {} });
7205
7271
  const agent = new BrowserAgent({
@@ -7212,34 +7278,6 @@ async function startBrowserAgent(options) {
7212
7278
  await agent.start();
7213
7279
  return agent;
7214
7280
  }
7215
- async function getFullPageContent(page) {
7216
- checkOperation();
7217
- const iframeHandles = await page.locator("iframe").elementHandles();
7218
- for (const iframeHandle of iframeHandles) {
7219
- checkOperation();
7220
- const frame = await iframeHandle.contentFrame();
7221
- checkOperation();
7222
- if (frame) {
7223
- const iframeContent = await frame.content();
7224
- checkOperation();
7225
- await iframeHandle.evaluate((iframeNode, { content }) => {
7226
- const div = document.createElement("div");
7227
- const parser = new DOMParser();
7228
- const doc = parser.parseFromString(content, "text/html");
7229
- while (doc.body.firstChild) {
7230
- div.appendChild(doc.body.firstChild);
7231
- }
7232
- const headElements = doc.head.querySelectorAll('style, link[rel="stylesheet"]');
7233
- headElements.forEach((el) => div.appendChild(el.cloneNode(true)));
7234
- div.dataset.expandedFromIframe = "true";
7235
- div.dataset.iframeSrc = iframeNode.getAttribute("src") || "";
7236
- iframeNode.parentNode?.replaceChild(div, iframeNode);
7237
- }, { content: iframeContent });
7238
- }
7239
- }
7240
- checkOperation();
7241
- return page.content();
7242
- }
7243
7281
  class BrowserAgent extends Agent {
7244
7282
  browserAgentEvents = new EventEmitter();
7245
7283
  constructor({ agentOptions, browserOptions }) {
@@ -7267,7 +7305,7 @@ class BrowserAgent extends Agent {
7267
7305
  async _extract(instructions, schema) {
7268
7306
  this.browserAgentEvents.emit("extractStarted", instructions, schema);
7269
7307
  const htmlContent = await retry(
7270
- async () => await getFullPageContent(this.page),
7308
+ () => getVisiblePageContent(this.page),
7271
7309
  { retries: 5, delay: 200, exponential: true }
7272
7310
  );
7273
7311
  const partitionOptions = {
package/dist/index.mjs CHANGED
@@ -7177,6 +7177,72 @@ function narrateBrowserAgent(agent) {
7177
7177
  });
7178
7178
  }
7179
7179
 
7180
+ async function getVisiblePageContent(page) {
7181
+ return readFrame(page.mainFrame());
7182
+ }
7183
+ async function readFrame(frame) {
7184
+ checkOperation();
7185
+ const marker = randomUUID();
7186
+ const snapshot = await frame.evaluateHandle((marker2) => {
7187
+ const documentCopy = document.implementation.createHTMLDocument();
7188
+ const frames = [];
7189
+ const visibility = { checkOpacity: true, checkVisibility: true };
7190
+ function copy(source) {
7191
+ if (["script", "style", "template", "noscript"].includes(source.localName)) return null;
7192
+ const style = getComputedStyle(source);
7193
+ if (style.display === "none" || style.opacity === "0" || style.contentVisibility === "hidden") return null;
7194
+ const visible = style.visibility === "visible" && (style.display === "contents" || source.checkVisibility(visibility) || ["option", "optgroup"].includes(source.localName) && source.closest("select")?.checkVisibility(visibility));
7195
+ if (source instanceof HTMLIFrameElement || source instanceof HTMLFrameElement) {
7196
+ if (!visible) return null;
7197
+ frames.push(source);
7198
+ return documentCopy.createComment(`${marker2}:${frames.length - 1}`);
7199
+ }
7200
+ const target = documentCopy.importNode(source, false);
7201
+ if (source instanceof HTMLInputElement) {
7202
+ if (source.type === "password") target.removeAttribute("value");
7203
+ else target.setAttribute("value", source.value);
7204
+ target.toggleAttribute("checked", source.checked);
7205
+ }
7206
+ if (source instanceof HTMLOptionElement) target.toggleAttribute("selected", source.selected);
7207
+ if (source instanceof HTMLTextAreaElement) {
7208
+ target.textContent = visible ? source.value : "";
7209
+ return visible ? target : null;
7210
+ }
7211
+ const summary = source instanceof HTMLDetailsElement && !source.open ? source.querySelector("summary") : null;
7212
+ for (const child of source.childNodes) {
7213
+ if (source instanceof HTMLDetailsElement && !source.open && child !== summary) continue;
7214
+ if (child instanceof Element) {
7215
+ const copied = copy(child);
7216
+ if (copied) target.appendChild(copied);
7217
+ } else if (visible && child.nodeType === Node.TEXT_NODE) {
7218
+ target.appendChild(documentCopy.importNode(child, false));
7219
+ }
7220
+ }
7221
+ return visible || target.hasChildNodes() ? target : null;
7222
+ }
7223
+ const body = document.body && copy(document.body);
7224
+ return { html: body instanceof Element ? body.innerHTML : "", frames };
7225
+ }, marker);
7226
+ const handles = [snapshot];
7227
+ try {
7228
+ let html = await snapshot.evaluate(({ html: html2 }) => html2);
7229
+ const frames = await snapshot.getProperty("frames");
7230
+ handles.push(frames);
7231
+ const entries = await frames.getProperties();
7232
+ handles.push(...entries.values());
7233
+ for (const [index, handle] of entries) {
7234
+ checkOperation();
7235
+ const child = await handle.asElement()?.contentFrame();
7236
+ const content = child ? await readFrame(child) : "";
7237
+ html = html.replace(`<!--${marker}:${index}-->`, () => `<div>${content}</div>`);
7238
+ }
7239
+ checkOperation();
7240
+ return html;
7241
+ } finally {
7242
+ await Promise.all(handles.map((handle) => handle.dispose()));
7243
+ }
7244
+ }
7245
+
7180
7246
  async function startBrowserAgent(options) {
7181
7247
  const { agentOptions, browserOptions } = buildDefaultBrowserAgentOptions({ agentOptions: options ?? {}, browserOptions: options ?? {} });
7182
7248
  const agent = new BrowserAgent({
@@ -7189,34 +7255,6 @@ async function startBrowserAgent(options) {
7189
7255
  await agent.start();
7190
7256
  return agent;
7191
7257
  }
7192
- async function getFullPageContent(page) {
7193
- checkOperation();
7194
- const iframeHandles = await page.locator("iframe").elementHandles();
7195
- for (const iframeHandle of iframeHandles) {
7196
- checkOperation();
7197
- const frame = await iframeHandle.contentFrame();
7198
- checkOperation();
7199
- if (frame) {
7200
- const iframeContent = await frame.content();
7201
- checkOperation();
7202
- await iframeHandle.evaluate((iframeNode, { content }) => {
7203
- const div = document.createElement("div");
7204
- const parser = new DOMParser();
7205
- const doc = parser.parseFromString(content, "text/html");
7206
- while (doc.body.firstChild) {
7207
- div.appendChild(doc.body.firstChild);
7208
- }
7209
- const headElements = doc.head.querySelectorAll('style, link[rel="stylesheet"]');
7210
- headElements.forEach((el) => div.appendChild(el.cloneNode(true)));
7211
- div.dataset.expandedFromIframe = "true";
7212
- div.dataset.iframeSrc = iframeNode.getAttribute("src") || "";
7213
- iframeNode.parentNode?.replaceChild(div, iframeNode);
7214
- }, { content: iframeContent });
7215
- }
7216
- }
7217
- checkOperation();
7218
- return page.content();
7219
- }
7220
7258
  class BrowserAgent extends Agent {
7221
7259
  browserAgentEvents = new EventEmitter();
7222
7260
  constructor({ agentOptions, browserOptions }) {
@@ -7244,7 +7282,7 @@ class BrowserAgent extends Agent {
7244
7282
  async _extract(instructions, schema) {
7245
7283
  this.browserAgentEvents.emit("extractStarted", instructions, schema);
7246
7284
  const htmlContent = await retry(
7247
- async () => await getFullPageContent(this.page),
7285
+ () => getVisiblePageContent(this.page),
7248
7286
  { retries: 5, delay: 200, exponential: true }
7249
7287
  );
7250
7288
  const partitionOptions = {
@@ -0,0 +1,2 @@
1
+ import type { Page } from 'playwright';
2
+ export declare function getVisiblePageContent(page: Page): Promise<string>;
@@ -0,0 +1,78 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { checkOperation } from '@/common/operation';
3
+ export async function getVisiblePageContent(page) {
4
+ return readFrame(page.mainFrame());
5
+ }
6
+ async function readFrame(frame) {
7
+ checkOperation();
8
+ const marker = randomUUID();
9
+ const snapshot = await frame.evaluateHandle((marker) => {
10
+ const documentCopy = document.implementation.createHTMLDocument();
11
+ const frames = [];
12
+ const visibility = { checkOpacity: true, checkVisibility: true };
13
+ function copy(source) {
14
+ if (['script', 'style', 'template', 'noscript'].includes(source.localName))
15
+ return null;
16
+ const style = getComputedStyle(source);
17
+ if (style.display === 'none' || style.opacity === '0' || style.contentVisibility === 'hidden')
18
+ return null;
19
+ const visible = style.visibility === 'visible' && (style.display === 'contents' || source.checkVisibility(visibility) ||
20
+ (['option', 'optgroup'].includes(source.localName) && source.closest('select')?.checkVisibility(visibility)));
21
+ if (source instanceof HTMLIFrameElement || source instanceof HTMLFrameElement) {
22
+ if (!visible)
23
+ return null;
24
+ frames.push(source);
25
+ return documentCopy.createComment(`${marker}:${frames.length - 1}`);
26
+ }
27
+ const target = documentCopy.importNode(source, false);
28
+ if (source instanceof HTMLInputElement) {
29
+ if (source.type === 'password')
30
+ target.removeAttribute('value');
31
+ else
32
+ target.setAttribute('value', source.value);
33
+ target.toggleAttribute('checked', source.checked);
34
+ }
35
+ if (source instanceof HTMLOptionElement)
36
+ target.toggleAttribute('selected', source.selected);
37
+ if (source instanceof HTMLTextAreaElement) {
38
+ target.textContent = visible ? source.value : '';
39
+ return visible ? target : null;
40
+ }
41
+ const summary = source instanceof HTMLDetailsElement && !source.open ? source.querySelector('summary') : null;
42
+ for (const child of source.childNodes) {
43
+ if (source instanceof HTMLDetailsElement && !source.open && child !== summary)
44
+ continue;
45
+ if (child instanceof Element) {
46
+ const copied = copy(child);
47
+ if (copied)
48
+ target.appendChild(copied);
49
+ }
50
+ else if (visible && child.nodeType === Node.TEXT_NODE) {
51
+ target.appendChild(documentCopy.importNode(child, false));
52
+ }
53
+ }
54
+ return visible || target.hasChildNodes() ? target : null;
55
+ }
56
+ const body = document.body && copy(document.body);
57
+ return { html: body instanceof Element ? body.innerHTML : '', frames };
58
+ }, marker);
59
+ const handles = [snapshot];
60
+ try {
61
+ let html = await snapshot.evaluate(({ html }) => html);
62
+ const frames = await snapshot.getProperty('frames');
63
+ handles.push(frames);
64
+ const entries = await frames.getProperties();
65
+ handles.push(...entries.values());
66
+ for (const [index, handle] of entries) {
67
+ checkOperation();
68
+ const child = await handle.asElement()?.contentFrame();
69
+ const content = child ? await readFrame(child) : '';
70
+ html = html.replace(`<!--${marker}:${index}-->`, () => `<div>${content}</div>`);
71
+ }
72
+ checkOperation();
73
+ return html;
74
+ }
75
+ finally {
76
+ await Promise.all(handles.map(handle => handle.dispose()));
77
+ }
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ddwang/magnitude-core",
3
- "version": "0.3.1-ddwang.3",
3
+ "version": "0.3.1-ddwang.4",
4
4
  "description": "Magnitude e2e testing agent",
5
5
  "publishConfig": {
6
6
  "access": "public"