framewatch-mcp-server 0.1.0 → 0.2.0
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.
- package/README.md +895 -15
- package/dist/constants.d.ts +274 -0
- package/dist/constants.js +279 -0
- package/dist/constants.js.map +1 -1
- package/dist/engine/browser.d.ts +20 -4
- package/dist/engine/browser.js +26 -9
- package/dist/engine/browser.js.map +1 -1
- package/dist/engine/clicks.d.ts +221 -0
- package/dist/engine/clicks.js +801 -0
- package/dist/engine/clicks.js.map +1 -0
- package/dist/engine/forms.d.ts +137 -0
- package/dist/engine/forms.js +474 -0
- package/dist/engine/forms.js.map +1 -0
- package/dist/engine/hmr.d.ts +41 -0
- package/dist/engine/hmr.js +91 -0
- package/dist/engine/hmr.js.map +1 -0
- package/dist/engine/inspect.d.ts +31 -0
- package/dist/engine/inspect.js +383 -0
- package/dist/engine/inspect.js.map +1 -0
- package/dist/engine/interaction.d.ts +12 -7
- package/dist/engine/interaction.js +110 -18
- package/dist/engine/interaction.js.map +1 -1
- package/dist/engine/links.d.ts +134 -0
- package/dist/engine/links.js +384 -0
- package/dist/engine/links.js.map +1 -0
- package/dist/engine/mocks.d.ts +53 -0
- package/dist/engine/mocks.js +148 -0
- package/dist/engine/mocks.js.map +1 -0
- package/dist/engine/rtl.d.ts +129 -0
- package/dist/engine/rtl.js +540 -0
- package/dist/engine/rtl.js.map +1 -0
- package/dist/engine/seo.d.ts +189 -0
- package/dist/engine/seo.js +398 -0
- package/dist/engine/seo.js.map +1 -0
- package/dist/engine/snapshot.d.ts +29 -0
- package/dist/engine/snapshot.js +10 -0
- package/dist/engine/snapshot.js.map +1 -0
- package/dist/engine/vue.d.ts +54 -0
- package/dist/engine/vue.js +419 -0
- package/dist/engine/vue.js.map +1 -0
- package/dist/index.js +45 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/accessibility.d.ts +4 -0
- package/dist/tools/accessibility.js +9 -2
- package/dist/tools/accessibility.js.map +1 -1
- package/dist/tools/api-mock.d.ts +405 -0
- package/dist/tools/api-mock.js +186 -0
- package/dist/tools/api-mock.js.map +1 -0
- package/dist/tools/capture.d.ts +90 -26
- package/dist/tools/capture.js +109 -58
- package/dist/tools/capture.js.map +1 -1
- package/dist/tools/compare.d.ts +4 -0
- package/dist/tools/compare.js +16 -5
- package/dist/tools/compare.js.map +1 -1
- package/dist/tools/dead-clicks.d.ts +128 -0
- package/dist/tools/dead-clicks.js +570 -0
- package/dist/tools/dead-clicks.js.map +1 -0
- package/dist/tools/form-test.d.ts +112 -0
- package/dist/tools/form-test.js +477 -0
- package/dist/tools/form-test.js.map +1 -0
- package/dist/tools/index.d.ts +17 -1
- package/dist/tools/index.js +45 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inspect.d.ts +78 -0
- package/dist/tools/inspect.js +136 -0
- package/dist/tools/inspect.js.map +1 -0
- package/dist/tools/interact.d.ts +37 -18
- package/dist/tools/interact.js +113 -13
- package/dist/tools/interact.js.map +1 -1
- package/dist/tools/links.d.ts +129 -0
- package/dist/tools/links.js +640 -0
- package/dist/tools/links.js.map +1 -0
- package/dist/tools/responsive.d.ts +10 -6
- package/dist/tools/responsive.js +21 -4
- package/dist/tools/responsive.js.map +1 -1
- package/dist/tools/rtl.d.ts +241 -0
- package/dist/tools/rtl.js +410 -0
- package/dist/tools/rtl.js.map +1 -0
- package/dist/tools/save-auth.d.ts +263 -0
- package/dist/tools/save-auth.js +253 -0
- package/dist/tools/save-auth.js.map +1 -0
- package/dist/tools/screenshot.d.ts +4 -0
- package/dist/tools/screenshot.js +15 -4
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/seo.d.ts +113 -0
- package/dist/tools/seo.js +281 -0
- package/dist/tools/seo.js.map +1 -0
- package/dist/tools/snapshot.d.ts +122 -0
- package/dist/tools/snapshot.js +183 -0
- package/dist/tools/snapshot.js.map +1 -0
- package/dist/tools/wait-for.d.ts +107 -0
- package/dist/tools/wait-for.js +167 -0
- package/dist/tools/wait-for.js.map +1 -0
- package/dist/utils/arabic-text.d.ts +14 -0
- package/dist/utils/arabic-text.js +193 -0
- package/dist/utils/arabic-text.js.map +1 -0
- package/dist/utils/budget.d.ts +41 -0
- package/dist/utils/budget.js +182 -0
- package/dist/utils/budget.js.map +1 -0
- package/dist/utils/format.d.ts +11 -1
- package/dist/utils/format.js +27 -4
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/highlight.d.ts +69 -0
- package/dist/utils/highlight.js +181 -0
- package/dist/utils/highlight.js.map +1 -0
- package/dist/utils/link-rules.d.ts +100 -0
- package/dist/utils/link-rules.js +284 -0
- package/dist/utils/link-rules.js.map +1 -0
- package/dist/utils/mock-rules.d.ts +144 -0
- package/dist/utils/mock-rules.js +224 -0
- package/dist/utils/mock-rules.js.map +1 -0
- package/dist/utils/rtl-rules.d.ts +142 -0
- package/dist/utils/rtl-rules.js +296 -0
- package/dist/utils/rtl-rules.js.map +1 -0
- package/dist/utils/seo-rules.d.ts +129 -0
- package/dist/utils/seo-rules.js +726 -0
- package/dist/utils/seo-rules.js.map +1 -0
- package/dist/utils/snapshot-rules.d.ts +33 -0
- package/dist/utils/snapshot-rules.js +111 -0
- package/dist/utils/snapshot-rules.js.map +1 -0
- package/dist/utils/storage-state.d.ts +76 -0
- package/dist/utils/storage-state.js +195 -0
- package/dist/utils/storage-state.js.map +1 -0
- package/dist/utils/style-rules.d.ts +107 -0
- package/dist/utils/style-rules.js +223 -0
- package/dist/utils/style-rules.js.map +1 -0
- package/dist/utils/test-data.d.ts +75 -0
- package/dist/utils/test-data.js +294 -0
- package/dist/utils/test-data.js.map +1 -0
- package/dist/utils/vue-rules.d.ts +72 -0
- package/dist/utils/vue-rules.js +108 -0
- package/dist/utils/vue-rules.js.map +1 -0
- package/package.json +6 -4
package/dist/constants.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"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","sourcesContent":["/** Default capture interval between raw frames (10 fps). */\nexport const CAPTURE_INTERVAL_MS = 100;\n\n/** Grid used by the smart diff engine (GRID_SIZE x GRID_SIZE cells). */\nexport const GRID_SIZE = 8;\n\n/** Mean absolute per-pixel difference (0–255) for a grid cell to count as changed. */\nexport const CELL_THRESHOLD = 15;\n\n/** Absolute per-pixel grayscale difference (0–255) for a single pixel to count as changed (full-res bbox). */\nexport const PIXEL_THRESHOLD = 15;\n\n/** Fraction of grid cells that must change for a frame to be kept. */\nexport const DEFAULT_SENSITIVITY = 0.06;\n\n/** Kept frames closer than this are merged, keeping the later \"settled\" one. */\nexport const MERGE_WINDOW_MS = 200;\n\n/** Padding added around the change bounding box when cropping. */\nexport const CROP_PADDING_PX = 20;\n\n/** Skip the crop image when the padded bounding box covers at least this fraction of the frame. */\nexport const CROP_SKIP_COVERAGE = 0.9;\n\n/** Low-res size used for fast frame comparison. */\nexport const DIFF_WIDTH = 320;\nexport const DIFF_HEIGHT = 240;\n\n/** Maximum width of images returned to the MCP client. */\nexport const OUTPUT_MAX_WIDTH = 800;\n\n/** Hard cap on diff cards returned by a single capture. */\nexport const MAX_FRAMES_CAP = 30;\n\n/** Default number of diff cards returned by a capture. */\nexport const DEFAULT_MAX_FRAMES = 20;\n\n/** Capture recording length bounds and default. */\nexport const DEFAULT_CAPTURE_DURATION_MS = 5000;\nexport const MIN_CAPTURE_DURATION_MS = 500;\nexport const MAX_CAPTURE_DURATION_MS = 30_000;\n\n/**\n * Upper bound on a capture viewport. A recording holds every raw PNG in\n * memory until the cards are built, so an unbounded viewport is an\n * out-of-memory risk (a 30s 4K recording already holds hundreds of MB).\n */\nexport const MAX_VIEWPORT_WIDTH = 3840;\nexport const MAX_VIEWPORT_HEIGHT = 2160;\n\n/** Default viewport for all page-based tools. */\nexport const DEFAULT_VIEWPORT = { width: 1280, height: 720 } as const;\n\n/** Default wait after page load before a screenshot is taken. */\nexport const DEFAULT_SCREENSHOT_WAIT_MS = 1000;\n\n/** Default navigation timeout for page.goto. */\nexport const NAVIGATION_TIMEOUT_MS = 30_000;\n\n/**\n * Floor for the per-screenshot timeout. Chromium will not produce a screenshot\n * while the main frame has a pending cross-document navigation or a blocked\n * main thread, and Playwright's 30s default would stall the whole recording.\n */\nexport const SCREENSHOT_TIMEOUT_MS = 2000;\n\n/**\n * Bound on the final screenshot taken by stop() when the previous attempt\n * timed out. Short, so an genuinely wedged page cannot stall shutdown, but\n * still enough for a page that has since recovered (e.g. after a navigation).\n */\nexport const SCREENSHOT_FINAL_TIMEOUT_MS = 500;\n\n/**\n * Chromium refuses a screenshot until it has produced its first frame (right\n * after a navigation commits). Retry that specific failure a few times.\n */\nexport const SCREENSHOT_RETRY_ATTEMPTS = 4;\nexport const SCREENSHOT_RETRY_DELAY_MS = 15;\n\n/**\n * Bound on the cosmetic page metadata read after a recording (the title).\n * `page.title()` takes no timeout of its own and blocks for Playwright's full\n * 30s default while the page's main thread is busy.\n */\nexport const PAGE_INFO_TIMEOUT_MS = 1000;\n\n/** Default timeout when waiting for a selector to appear. */\nexport const SELECTOR_TIMEOUT_MS = 10_000;\n\n/**\n * A swipe is dispatched as touchStart → SWIPE_STEPS touchMoves → touchEnd,\n * with each move about a frame apart so the page's own velocity maths (a\n * carousel, pull-to-refresh) sees a plausible gesture rather than a teleport.\n */\nexport const SWIPE_STEPS = 10;\nexport const SWIPE_STEP_DELAY_MS = 16;\n\n/**\n * Cap on the length of a replayed interaction script. Each step can carry its\n * own delay, so an unbounded script would sidestep MAX_CAPTURE_DURATION_MS.\n */\nexport const MAX_INTERACTIONS = 50;\n\n/** Default settle time between an interaction and its \"after\" screenshot. */\nexport const DEFAULT_INTERACT_WAIT_MS = 500;\n\n/* ── Context layers (Phase 4) ─────────────────────────────────────────────\n * Every layer is bounded twice over: in the page (so a runaway app cannot\n * grow the tab's memory) and in Node (so one capture cannot flood the MCP\n * response). The Node-side caps are the ones a user notices, and each layer\n * reports what it had to drop.\n */\n\n/** Console entries kept per capture. Errors evict older non-errors once full. */\nexport const MAX_CONSOLE_ENTRIES = 100;\n\n/** Console text longer than this is elided — one runaway log must not fill the response. */\nexport const MAX_CONSOLE_TEXT_LENGTH = 300;\n\n/** Network events kept per capture. Failed/error responses evict older successful ones once full. */\nexport const MAX_NETWORK_EVENTS = 100;\n\n/** URLs longer than this are shortened in the middle (query strings and data: URIs are unbounded). */\nexport const MAX_NETWORK_URL_LENGTH = 120;\n\n/** DOM mutation records kept per capture, before grouping. */\nexport const MAX_DOM_RECORDS = 500;\n\n/** Grouped DOM lines rendered on a single card. */\nexport const MAX_DOM_LINES_PER_CARD = 12;\n\n/** Performance entries (paint, LCP, layout shift) kept per capture. */\nexport const MAX_PERF_SAMPLES = 500;\n\n/**\n * How long the in-page probes batch records before pushing them to Node.\n * Roughly one animation frame: long enough to coalesce a burst of mutations\n * into one binding call, short enough that little is lost if the document is\n * replaced. Record timestamps are stamped when the record is made, not when\n * the batch is flushed, so batching never affects which card a record lands on.\n */\nexport const LAYER_FLUSH_MS = 32;\n\n/** Records one in-page batch may carry. A page that mutates more than this per flush is reporting a storm, not detail. */\nexport const MAX_LAYER_BATCH = 200;\n\n/** Records an in-page probe may push over the lifetime of one document. */\nexport const MAX_LAYER_RECORDS_PER_DOCUMENT = 2000;\n\n/* ── Responsive (Phase 5) ─────────────────────────────────────────────── */\n\n/** Viewports `framewatch_responsive` uses when the caller names none. */\nexport const DEFAULT_RESPONSIVE_VIEWPORTS = [\n { name: \"mobile\", width: 375, height: 812 },\n { name: \"tablet\", width: 768, height: 1024 },\n { name: \"desktop\", width: 1440, height: 900 },\n] as const;\n\n/** Viewports one responsive call may capture. Each one is a live browser context. */\nexport const MAX_RESPONSIVE_VIEWPORTS = 8;\n\n/** Default settle time after load before each responsive screenshot. */\nexport const DEFAULT_RESPONSIVE_WAIT_MS = 2000;\n\n/**\n * Slack (px) allowed before content counts as overflowing its viewport.\n * Sub-pixel layout rounding routinely puts scrollWidth one pixel over\n * clientWidth on a page that is perfectly fine.\n */\nexport const OVERFLOW_TOLERANCE_PX = 1;\n\n/* ── Compare (Phase 5) ────────────────────────────────────────────────── */\n\n/** Default settle time after load before each compared screenshot. */\nexport const DEFAULT_COMPARE_WAIT_MS = 2000;\n\n/** Colour painted over changed pixels in the compare overlay, and its opacity (0–255). */\nexport const OVERLAY_COLOUR = { r: 255, g: 0, b: 200 } as const;\nexport const OVERLAY_ALPHA = 190;\n\n/* ── Accessibility (Phase 5) ──────────────────────────────────────────── */\n\n/** Default settle time after load before the audit runs. */\nexport const DEFAULT_A11Y_WAIT_MS = 1000;\n\n/** Violations reported by one audit, and elements listed under each. */\nexport const MAX_A11Y_VIOLATIONS = 25;\nexport const MAX_A11Y_NODES_PER_VIOLATION = 3;\n\n/** Length of one element's HTML in a violation report before it is elided. */\nexport const MAX_A11Y_HTML_LENGTH = 160;\n\n/** Bound on the axe-core run itself (a huge DOM can take a while). */\nexport const A11Y_RUN_TIMEOUT_MS = 60_000;\n\n/** How long axe waits for an iframe to answer before auditing without it. */\nexport const A11Y_FRAME_WAIT_MS = 5000;\n\n/* ── Dev server (Phase 5) ─────────────────────────────────────────────── */\n\n/** Default regex matched against dev server output to spot its \"ready\" line. */\nexport const DEFAULT_READY_PATTERN = \"ready|started|listening|Local:\";\n\n/** Bounds and default for how long `framewatch_start_server` waits for the port. */\nexport const DEFAULT_SERVER_TIMEOUT_MS = 30_000;\nexport const MIN_SERVER_TIMEOUT_MS = 100;\nexport const MAX_SERVER_TIMEOUT_MS = 300_000;\n\n/** Output lines kept from a dev server. Lines mentioning errors evict ordinary ones. */\nexport const MAX_SERVER_LOG_LINES = 200;\n\n/** Length of one captured output line before it is elided. */\nexport const MAX_SERVER_LINE_LENGTH = 300;\n\n/** Output lines quoted back when a server fails to start or is stopped. */\nexport const SERVER_OUTPUT_TAIL = 15;\n\n/** How often the port is probed while waiting for the server, and the bound on one probe. */\nexport const SERVER_PORT_POLL_MS = 200;\nexport const SERVER_PORT_PROBE_TIMEOUT_MS = 1000;\n\n/** Time a stopped server gets to exit on SIGTERM before it is killed outright. */\nexport const SERVER_STOP_GRACE_MS = 5000;\n"]}
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"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","sourcesContent":["/** Default capture interval between raw frames (10 fps). */\nexport const CAPTURE_INTERVAL_MS = 100;\n\n/** Grid used by the smart diff engine (GRID_SIZE x GRID_SIZE cells). */\nexport const GRID_SIZE = 8;\n\n/** Mean absolute per-pixel difference (0–255) for a grid cell to count as changed. */\nexport const CELL_THRESHOLD = 15;\n\n/** Absolute per-pixel grayscale difference (0–255) for a single pixel to count as changed (full-res bbox). */\nexport const PIXEL_THRESHOLD = 15;\n\n/** Fraction of grid cells that must change for a frame to be kept. */\nexport const DEFAULT_SENSITIVITY = 0.06;\n\n/** Kept frames closer than this are merged, keeping the later \"settled\" one. */\nexport const MERGE_WINDOW_MS = 200;\n\n/** Padding added around the change bounding box when cropping. */\nexport const CROP_PADDING_PX = 20;\n\n/** Skip the crop image when the padded bounding box covers at least this fraction of the frame. */\nexport const CROP_SKIP_COVERAGE = 0.9;\n\n/** Low-res size used for fast frame comparison. */\nexport const DIFF_WIDTH = 320;\nexport const DIFF_HEIGHT = 240;\n\n/** Maximum width of images returned to the MCP client. */\nexport const OUTPUT_MAX_WIDTH = 800;\n\n/** Hard cap on diff cards returned by a single capture. */\nexport const MAX_FRAMES_CAP = 30;\n\n/** Default number of diff cards returned by a capture. */\nexport const DEFAULT_MAX_FRAMES = 20;\n\n/** Capture recording length bounds and default. */\nexport const DEFAULT_CAPTURE_DURATION_MS = 5000;\nexport const MIN_CAPTURE_DURATION_MS = 500;\nexport const MAX_CAPTURE_DURATION_MS = 30_000;\n\n/**\n * Upper bound on a capture viewport. A recording holds every raw PNG in\n * memory until the cards are built, so an unbounded viewport is an\n * out-of-memory risk (a 30s 4K recording already holds hundreds of MB).\n */\nexport const MAX_VIEWPORT_WIDTH = 3840;\nexport const MAX_VIEWPORT_HEIGHT = 2160;\n\n/** Default viewport for all page-based tools. */\nexport const DEFAULT_VIEWPORT = { width: 1280, height: 720 } as const;\n\n/** Default wait after page load before a screenshot is taken. */\nexport const DEFAULT_SCREENSHOT_WAIT_MS = 1000;\n\n/** Default navigation timeout for page.goto. */\nexport const NAVIGATION_TIMEOUT_MS = 30_000;\n\n/**\n * Floor for the per-screenshot timeout. Chromium will not produce a screenshot\n * while the main frame has a pending cross-document navigation or a blocked\n * main thread, and Playwright's 30s default would stall the whole recording.\n */\nexport const SCREENSHOT_TIMEOUT_MS = 2000;\n\n/**\n * Bound on the final screenshot taken by stop() when the previous attempt\n * timed out. Short, so an genuinely wedged page cannot stall shutdown, but\n * still enough for a page that has since recovered (e.g. after a navigation).\n */\nexport const SCREENSHOT_FINAL_TIMEOUT_MS = 500;\n\n/**\n * Chromium refuses a screenshot until it has produced its first frame (right\n * after a navigation commits). Retry that specific failure a few times.\n */\nexport const SCREENSHOT_RETRY_ATTEMPTS = 4;\nexport const SCREENSHOT_RETRY_DELAY_MS = 15;\n\n/**\n * Bound on the cosmetic page metadata read after a recording (the title).\n * `page.title()` takes no timeout of its own and blocks for Playwright's full\n * 30s default while the page's main thread is busy.\n */\nexport const PAGE_INFO_TIMEOUT_MS = 1000;\n\n/** Default timeout when waiting for a selector to appear. */\nexport const SELECTOR_TIMEOUT_MS = 10_000;\n\n/**\n * A swipe is dispatched as touchStart → SWIPE_STEPS touchMoves → touchEnd,\n * with each move about a frame apart so the page's own velocity maths (a\n * carousel, pull-to-refresh) sees a plausible gesture rather than a teleport.\n */\nexport const SWIPE_STEPS = 10;\nexport const SWIPE_STEP_DELAY_MS = 16;\n\n/**\n * Cap on the length of a replayed interaction script. Each step can carry its\n * own delay, so an unbounded script would sidestep MAX_CAPTURE_DURATION_MS.\n */\nexport const MAX_INTERACTIONS = 50;\n\n/** Default settle time between an interaction and its \"after\" screenshot. */\nexport const DEFAULT_INTERACT_WAIT_MS = 500;\n\n/* ── Context layers (Phase 4) ─────────────────────────────────────────────\n * Every layer is bounded twice over: in the page (so a runaway app cannot\n * grow the tab's memory) and in Node (so one capture cannot flood the MCP\n * response). The Node-side caps are the ones a user notices, and each layer\n * reports what it had to drop.\n */\n\n/** Console entries kept per capture. Errors evict older non-errors once full. */\nexport const MAX_CONSOLE_ENTRIES = 100;\n\n/** Console text longer than this is elided — one runaway log must not fill the response. */\nexport const MAX_CONSOLE_TEXT_LENGTH = 300;\n\n/** Network events kept per capture. Failed/error responses evict older successful ones once full. */\nexport const MAX_NETWORK_EVENTS = 100;\n\n/** URLs longer than this are shortened in the middle (query strings and data: URIs are unbounded). */\nexport const MAX_NETWORK_URL_LENGTH = 120;\n\n/** DOM mutation records kept per capture, before grouping. */\nexport const MAX_DOM_RECORDS = 500;\n\n/** Grouped DOM lines rendered on a single card. */\nexport const MAX_DOM_LINES_PER_CARD = 12;\n\n/** Performance entries (paint, LCP, layout shift) kept per capture. */\nexport const MAX_PERF_SAMPLES = 500;\n\n/**\n * How long the in-page probes batch records before pushing them to Node.\n * Roughly one animation frame: long enough to coalesce a burst of mutations\n * into one binding call, short enough that little is lost if the document is\n * replaced. Record timestamps are stamped when the record is made, not when\n * the batch is flushed, so batching never affects which card a record lands on.\n */\nexport const LAYER_FLUSH_MS = 32;\n\n/** Records one in-page batch may carry. A page that mutates more than this per flush is reporting a storm, not detail. */\nexport const MAX_LAYER_BATCH = 200;\n\n/** Records an in-page probe may push over the lifetime of one document. */\nexport const MAX_LAYER_RECORDS_PER_DOCUMENT = 2000;\n\n/* ── Responsive (Phase 5) ─────────────────────────────────────────────── */\n\n/** Viewports `framewatch_responsive` uses when the caller names none. */\nexport const DEFAULT_RESPONSIVE_VIEWPORTS = [\n { name: \"mobile\", width: 375, height: 812 },\n { name: \"tablet\", width: 768, height: 1024 },\n { name: \"desktop\", width: 1440, height: 900 },\n] as const;\n\n/** Viewports one responsive call may capture. Each one is a live browser context. */\nexport const MAX_RESPONSIVE_VIEWPORTS = 8;\n\n/** Default settle time after load before each responsive screenshot. */\nexport const DEFAULT_RESPONSIVE_WAIT_MS = 2000;\n\n/**\n * Slack (px) allowed before content counts as overflowing its viewport.\n * Sub-pixel layout rounding routinely puts scrollWidth one pixel over\n * clientWidth on a page that is perfectly fine.\n */\nexport const OVERFLOW_TOLERANCE_PX = 1;\n\n/* ── Compare (Phase 5) ────────────────────────────────────────────────── */\n\n/** Default settle time after load before each compared screenshot. */\nexport const DEFAULT_COMPARE_WAIT_MS = 2000;\n\n/** Colour painted over changed pixels in the compare overlay, and its opacity (0–255). */\nexport const OVERLAY_COLOUR = { r: 255, g: 0, b: 200 } as const;\nexport const OVERLAY_ALPHA = 190;\n\n/* ── Accessibility (Phase 5) ──────────────────────────────────────────── */\n\n/** Default settle time after load before the audit runs. */\nexport const DEFAULT_A11Y_WAIT_MS = 1000;\n\n/** Violations reported by one audit, and elements listed under each. */\nexport const MAX_A11Y_VIOLATIONS = 25;\nexport const MAX_A11Y_NODES_PER_VIOLATION = 3;\n\n/** Length of one element's HTML in a violation report before it is elided. */\nexport const MAX_A11Y_HTML_LENGTH = 160;\n\n/** Bound on the axe-core run itself (a huge DOM can take a while). */\nexport const A11Y_RUN_TIMEOUT_MS = 60_000;\n\n/** How long axe waits for an iframe to answer before auditing without it. */\nexport const A11Y_FRAME_WAIT_MS = 5000;\n\n/* ── Auth state (v0.1.1) ──────────────────────────────────────────────── */\n\n/** Where `framewatch_save_auth` writes its state file when the caller names no path. */\nexport const DEFAULT_AUTH_STATE_PATH = \".framewatch/auth.json\";\n\n/**\n * Settle time before each step of a saved login flow. Higher than a capture\n * script's 0: this runs blind (nothing is watching the frames), so each step\n * has to leave the next one something to act on.\n */\nexport const SAVE_AUTH_STEP_DELAY_MS = 500;\n\n/** How long `wait_for` may take to prove the flow signed in. Logins wait on a network round trip. */\nexport const SAVE_AUTH_WAIT_FOR_TIMEOUT_MS = 15_000;\n\n/* ── Dev server (Phase 5) ─────────────────────────────────────────────── */\n\n/** Default regex matched against dev server output to spot its \"ready\" line. */\nexport const DEFAULT_READY_PATTERN = \"ready|started|listening|Local:\";\n\n/** Bounds and default for how long `framewatch_start_server` waits for the port. */\nexport const DEFAULT_SERVER_TIMEOUT_MS = 30_000;\nexport const MIN_SERVER_TIMEOUT_MS = 100;\nexport const MAX_SERVER_TIMEOUT_MS = 300_000;\n\n/** Output lines kept from a dev server. Lines mentioning errors evict ordinary ones. */\nexport const MAX_SERVER_LOG_LINES = 200;\n\n/** Length of one captured output line before it is elided. */\nexport const MAX_SERVER_LINE_LENGTH = 300;\n\n/** Output lines quoted back when a server fails to start or is stopped. */\nexport const SERVER_OUTPUT_TAIL = 15;\n\n/** How often the port is probed while waiting for the server, and the bound on one probe. */\nexport const SERVER_PORT_POLL_MS = 200;\nexport const SERVER_PORT_PROBE_TIMEOUT_MS = 1000;\n\n/** Time a stopped server gets to exit on SIGTERM before it is killed outright. */\nexport const SERVER_STOP_GRACE_MS = 5000;\n\n/* ── Form testing (Tier 1) ────────────────────────────────────────────── */\n\n/** How much text a `maxlength` strategy puts in a field that declares no limit of its own. */\nexport const MAX_FORM_FILL_LENGTH = 10_000;\n\n/** Default settle time after a submit before the result is read and photographed. */\nexport const DEFAULT_FORM_WAIT_MS = 2000;\n\n/**\n * Settle time between filling the last field and the \"after fill\" screenshot.\n * Live validation runs on blur/input and often on a debounce, so a shot taken\n * the instant the last field is filled shows a form that has not reacted yet.\n */\nexport const FORM_FILL_SETTLE_MS = 300;\n\n/** Fields one strategy will fill. A form longer than this is filled up to the cap and says so. */\nexport const MAX_FORM_FIELDS = 60;\n\n/** Fields listed by name in the report for one strategy, before the rest are counted instead. */\nexport const MAX_FORM_FIELDS_LISTED = 10;\n\n/** Validation messages (browser-side and on-page alike) listed per strategy. */\nexport const MAX_FORM_ERRORS = 8;\n\n/** Network requests listed per strategy. */\nexport const MAX_FORM_NETWORK_EVENTS = 8;\n\n/** Console entries listed per strategy. */\nexport const MAX_FORM_CONSOLE_ENTRIES = 8;\n\n/** Length of one echoed field value or error message before it is elided. */\nexport const MAX_FORM_TEXT_LENGTH = 80;\n\n/** Validation messages read off the page in one scan, and the longest text counted as a message. */\nexport const MAX_FORM_MESSAGES = 20;\nexport const MAX_FORM_MESSAGE_LENGTH = 200;\n\n/** Bound on one form-test screenshot. A page wedged by its own submit handler must not stall the run. */\nexport const FORM_SCREENSHOT_TIMEOUT_MS = 5000;\n\n/* ── SEO (Tier 1) ─────────────────────────────────────────────────────── */\n\n/** Default settle time after load before the page is read. */\nexport const DEFAULT_SEO_WAIT_MS = 1000;\n\n/**\n * Title length bounds. Search results are laid out in pixels, not characters,\n * so these are the usual rules of thumb rather than hard limits: under 30\n * characters is leaving the strongest ranking signal on the page unused, and\n * past 60 the tail is generally cut off.\n */\nexport const SEO_TITLE_MIN = 30;\nexport const SEO_TITLE_MAX = 60;\n\n/** Meta description bounds — the snippet is cut somewhere around 160 characters. */\nexport const SEO_DESCRIPTION_MIN = 70;\nexport const SEO_DESCRIPTION_MAX = 160;\n\n/** What a share image should be: the 1.91:1 card the networks render, and the floor below which they render none. */\nexport const SEO_OG_IMAGE_IDEAL = { width: 1200, height: 630 } as const;\nexport const SEO_OG_IMAGE_MIN = { width: 200, height: 200 } as const;\n\n/** Headings listed in the outline before the rest are counted instead. */\nexport const MAX_SEO_HEADINGS = 40;\n\n/** Images listed by source when they have no alt text. */\nexport const MAX_SEO_IMAGES_LISTED = 10;\n\n/** JSON-LD blocks parsed, and the length of one before it is elided. */\nexport const MAX_SEO_JSONLD_BLOCKS = 10;\nexport const MAX_SEO_JSONLD_LENGTH = 20_000;\n\n/** Length of one echoed value (title, heading, URL) before it is elided. */\nexport const MAX_SEO_TEXT_LENGTH = 120;\n\n/** Bounds on the two extra requests an audit makes: robots.txt and the share image. */\nexport const SEO_FETCH_TIMEOUT_MS = 8000;\n\n/** A share image larger than this is measured and reported but never returned as an image block. */\nexport const MAX_SEO_IMAGE_BYTES = 8_000_000;\n\n/**\n * Core Web Vitals thresholds — Google's own \"good\" and \"poor\" boundaries.\n * These are lab numbers from one headless load, so they are reported as\n * measurements with a verdict attached, never as a score.\n */\nexport const SEO_LCP_GOOD_MS = 2500;\nexport const SEO_LCP_POOR_MS = 4000;\nexport const SEO_CLS_GOOD = 0.1;\nexport const SEO_CLS_POOR = 0.25;\nexport const SEO_TTFB_GOOD_MS = 800;\nexport const SEO_TTFB_POOR_MS = 1800;\n\n/** Resource types listed in the page-weight breakdown. */\nexport const MAX_SEO_RESOURCE_TYPES = 6;\n\n/** DOM size Lighthouse starts warning about, and the size it calls excessive. */\nexport const SEO_DOM_NODES_WARN = 1400;\nexport const SEO_DOM_NODES_POOR = 3000;\n\n/* ── Dead clicks (Tier 1) ─────────────────────────────────────────────── */\n\n/** Default settle time after load before the page is swept for clickable elements. */\nexport const DEFAULT_DEAD_CLICK_WAIT_MS = 1000;\n\n/**\n * How long the page is watched after each click before the verdict is taken.\n * Long enough for a handler, a render and a fetch to start; short enough that\n * forty of them do not turn one tool call into a minute of waiting.\n */\nexport const DEFAULT_DEAD_CLICK_SETTLE_MS = 500;\n\n/** Elements one sweep clicks by default, and the hard cap. Every one costs a click and a settle. */\nexport const DEFAULT_DEAD_CLICK_ELEMENTS = 40;\nexport const MAX_DEAD_CLICK_ELEMENTS = 100;\n\n/**\n * Bound on one click and on re-finding the element it belongs to. Much shorter\n * than SELECTOR_TIMEOUT_MS: a control that cannot be clicked within this is a\n * finding in its own right, and waiting ten seconds for each of forty of them\n * is not a test anybody would run twice.\n */\nexport const DEAD_CLICK_TIMEOUT_MS = 3000;\n\n/** Elements walked when looking for clickable ones. Past this the page is reporting a tree, not a screen. */\nexport const MAX_DEAD_CLICK_SCAN = 5000;\n\n/** Settle time after hovering an element before its hover styles are read (CSS transitions). */\nexport const DEAD_CLICK_HOVER_SETTLE_MS = 250;\n\n/** DOM mutations quoted under one element before the rest are counted instead. */\nexport const MAX_DEAD_CLICK_CHANGES = 6;\n\n/**\n * Distinct mutation descriptions the page keeps per window.\n *\n * Comfortably more than are ever printed, and for a reason: what a click did\n * is decided by which of these the page does *not* also do on its own, so a\n * list that filled up with a ticking clock's churn would hide the one change\n * that mattered.\n */\nexport const MAX_DEAD_CLICK_CHANGE_SAMPLES = 24;\n\n/** Elements listed in each section of the report before the rest are counted instead. */\nexport const MAX_DEAD_CLICK_LISTED = 20;\n\n/** Effects listed for one element before the rest are counted instead. */\nexport const MAX_DEAD_CLICK_EFFECTS = 5;\n\n/** Length of one element's visible text, and of one generated selector, before it is elided. */\nexport const MAX_DEAD_CLICK_TEXT_LENGTH = 60;\nexport const MAX_DEAD_CLICK_SELECTOR_LENGTH = 120;\n\n/* ── Highlight overlay ────────────────────────────────────────────────── */\n\n/** Boxes one overlay draws. Past this the screenshot is a colour field, not a finding. */\nexport const MAX_HIGHLIGHTS = 30;\n\n/** The overlay's colours: what a dead element is painted, and what a broken one is. */\nexport const HIGHLIGHT_DEAD_COLOUR = \"#e5194b\";\nexport const HIGHLIGHT_BROKEN_COLOUR = \"#f08c00\";\n\n/* ── Links (Tier 1) ───────────────────────────────────────────────────── */\n\n/** Default settle time after each page load before its links are read. */\nexport const DEFAULT_LINKS_WAIT_MS = 1000;\n\n/** Bound on one link check, and the bounds the caller may set. */\nexport const DEFAULT_LINK_TIMEOUT_MS = 5000;\nexport const MIN_LINK_TIMEOUT_MS = 100;\nexport const MAX_LINK_TIMEOUT_MS = 30_000;\n\n/** Link checks in flight at once. Every one of them is a real request to somebody's server. */\nexport const DEFAULT_LINK_CONCURRENCY = 5;\nexport const MAX_LINK_CONCURRENCY = 10;\n\n/**\n * Redirects followed before a chain is called endless. Generous next to the\n * two or three a real site uses, and far below the point where following one\n * more would tell anybody anything.\n */\nexport const MAX_LINK_REDIRECTS = 10;\n\n/** Distinct URLs one run will check, and how deep a crawl may go. */\nexport const DEFAULT_MAX_LINKS = 200;\nexport const MAX_LINKS_CAP = 500;\nexport const MAX_LINK_DEPTH = 3;\n\n/** Pages one crawl will open. Each is a full browser navigation, not a fetch. */\nexport const DEFAULT_LINK_PAGES = 10;\nexport const MAX_LINK_PAGES = 25;\n\n/** Elements walked when collecting links. Past this the page is a document dump, not a screen. */\nexport const MAX_LINK_SCAN = 5000;\n\n/** Links listed in one section of the report before the rest are counted instead. */\nexport const MAX_LINKS_LISTED = 25;\n\n/** Places one link is named as appearing before the rest are counted instead. */\nexport const MAX_LINK_SOURCES_LISTED = 3;\n\n/** Length of one link's text, URL and selector before each is elided. */\nexport const MAX_LINK_TEXT_LENGTH = 60;\nexport const MAX_LINK_URL_LENGTH = 120;\nexport const MAX_LINK_SELECTOR_LENGTH = 120;\n\n/* ── API mocking (Tier 2) ─────────────────────────────────────────────── */\n\n/** Mocks one call may declare. Each is a live route on the page. */\nexport const MAX_MOCKS = 20;\n\n/**\n * Delay the `slow` scenario applies. Five seconds is past every spinner\n * timeout worth having and comfortably inside the default recording, so the\n * frames show the waiting state and then the arrival.\n */\nexport const MOCK_SLOW_DELAY_MS = 5000;\n\n/** Longest delay a mock may declare. Beyond the recording it is simply never delivered. */\nexport const MAX_MOCK_DELAY_MS = MAX_CAPTURE_DURATION_MS;\n\n/**\n * Size of one mock body. Generous, because \"how does this render 1000 rows\"\n * is one of the scenarios this tool exists for, and still bounded — the body\n * is held in Node, sent over stdio and echoed in the report.\n */\nexport const MAX_MOCK_BODY_BYTES = 2_000_000;\n\n/** Requests named under one mock in the report before the rest are counted instead. */\nexport const MAX_MOCK_URLS_LISTED = 3;\n\n/** Unmatched requests named in the report before the rest are counted instead. */\nexport const MAX_UNMATCHED_LISTED = 10;\n\n/** Unmatched requests recorded at all. A page that never stops polling must not fill the response. */\nexport const MAX_UNMATCHED_TRACKED = 200;\n\n/* ── RTL (Tier 2) ─────────────────────────────────────────────────────── */\n\n/** Default settle time after each load before the page is measured. */\nexport const DEFAULT_RTL_WAIT_MS = 1000;\n\n/**\n * Slack (px) before a box counts as \"did not move\".\n *\n * Sub-pixel layout, fractional scaling and font metrics routinely shift a box\n * by a pixel between two renders of the same page. A tolerance under about\n * two pixels reports that rounding as a mirroring bug on almost every element.\n */\nexport const RTL_MIRROR_TOLERANCE_PX = 2;\n\n/** Slack (px) on padding and margin comparisons, for the same reason. */\nexport const RTL_ALIGN_TOLERANCE_PX = 1;\n\n/**\n * Slack (px) before content counts as overflowing in RTL.\n *\n * Matches OVERFLOW_TOLERANCE_PX in spirit but is looser: an RTL relayout\n * reflows every line box, so a page routinely lands a pixel or two wider than\n * it did in LTR without anything being wrong.\n */\nexport const RTL_OVERFLOW_TOLERANCE_PX = 2;\n\n/** Elements walked when measuring a page. Past this the page is a document dump, not a screen. */\nexport const MAX_RTL_SCAN = 5000;\n\n/**\n * Elements actually measured and compared, by default and at the cap.\n *\n * Every one of these is measured twice and compared, so the cost is real but\n * bounded — and a page with more than a few hundred laid-out boxes is one\n * where the first hundred findings are the whole story anyway.\n */\nexport const DEFAULT_RTL_ELEMENTS = 400;\nexport const MAX_RTL_ELEMENTS = 1500;\n\n/** Findings listed in one section of the report before the rest are counted instead. */\nexport const MAX_RTL_LISTED = 25;\n\n/** Issues listed under one element before the rest are counted instead. */\nexport const MAX_RTL_ISSUES_PER_ELEMENT = 4;\n\n/** Length of one element's text, and of one generated selector, before it is elided. */\nexport const MAX_RTL_TEXT_LENGTH = 60;\nexport const MAX_RTL_SELECTOR_LENGTH = 120;\n\n/** Text nodes one Arabic injection will replace. A page longer than this is filled up to the cap. */\nexport const MAX_ARABIC_INJECTION_NODES = 2000;\n\n/** Longest single string the Arabic injector will build, however long the original was. */\nexport const MAX_ARABIC_INJECTION_LENGTH = 300;\n\n/** What a finding is painted in the overlay: a problem, and a warning. */\nexport const HIGHLIGHT_RTL_PROBLEM_COLOUR = \"#e5194b\";\nexport const HIGHLIGHT_RTL_WARNING_COLOUR = \"#f08c00\";\n\n/* ── Snapshot & inspect ───────────────────────────────────────────────── */\n\n/** Characters of aria tree one snapshot returns before it is cut, with a note. */\nexport const DEFAULT_SNAPSHOT_MAX_CHARS = 12_000;\nexport const MAX_SNAPSHOT_MAX_CHARS = 100_000;\n\n/** Elements one inspect call measures. Each is a full evaluate and a block of lines. */\nexport const MAX_INSPECT_TARGETS = 12;\n\n/** Length of an element's own text quoted in an inspection before it is elided. */\nexport const MAX_INSPECT_TEXT_LENGTH = 60;\n\n/** Elements the design inventory walks before it stops counting. */\nexport const MAX_INVENTORY_ELEMENTS = 3000;\n\n/** Values listed per inventory dimension; the rest are counted into a tail. */\nexport const MAX_INVENTORY_VALUES = 12;\n\n/** Settle time after opening a page for snapshot/inspect, so a client-rendered app has drawn. */\nexport const DEFAULT_SNAPSHOT_WAIT_MS = 500;\n\n/** What inspect's boxes are painted. */\nexport const HIGHLIGHT_INSPECT_COLOUR = \"#e5194b\";\n\n/* ── Vue & Vite ───────────────────────────────────────────────────────── */\n\n/** How long a session tool looks for a Vue app after opening a page before treating it as a plain page. */\nexport const VUE_DETECT_MS = 500;\n\n/** Once an app is found: max wait for vue-router to be ready. */\nexport const VUE_READY_TIMEOUT_MS = 5000;\n\n/** framewatch_wait_for's default and ceiling. */\nexport const DEFAULT_WAIT_FOR_TIMEOUT_MS = 10_000;\nexport const MAX_WAIT_FOR_TIMEOUT_MS = 120_000;\n\n/** One serialised prop or state value, and how many of each are listed per component. */\nexport const MAX_COMPONENT_VALUE_LENGTH = 40;\nexport const MAX_COMPONENT_ENTRIES = 12;\n\n/** Components the tree walk names before it stops. */\nexport const MAX_COMPONENT_TREE_NODES = 300;\n\n/** Vite events remembered per session page. */\nexport const MAX_HMR_EVENTS = 50;\n\n/* ── Image budget ─────────────────────────────────────────────────────── */\n\n/** Claude Code's default cap on one MCP tool result; base64 image data counts toward it. */\nexport const DEFAULT_MCP_OUTPUT_TOKENS = 25_000;\n\n/** How base64 tokenises, conservatively: a result sized on 3 chars/token never lands over the cap. */\nexport const BUDGET_CHARS_PER_TOKEN = 3;\n\n/** Tokens left for JSON framing and the budget note itself. */\nexport const BUDGET_MARGIN_TOKENS = 1500;\n\n/** Widths a frame is stepped down through before any frame is dropped, then after the last one. */\nexport const BUDGET_WIDTHS = [640, 480] as const;\nexport const BUDGET_LAST_RESORT_WIDTHS = [320, 240, 160] as const;\n\n/** JPEG quality for the lossy candidate. */\nexport const BUDGET_JPEG_QUALITY = 78;\n\n/** What to tell the user to set for full results. */\nexport const BUDGET_SUGGESTED_TOKENS = 100_000;\n"]}
|
package/dist/engine/browser.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Browser, type BrowserContext, type BrowserContextOptions, type Page } from "playwright";
|
|
2
2
|
import type { Viewport } from "../types.js";
|
|
3
|
+
import type { StorageState } from "../utils/storage-state.js";
|
|
3
4
|
export declare function getBrowser(): Promise<Browser>;
|
|
4
5
|
export declare function closeBrowser(): Promise<void>;
|
|
5
6
|
export interface PageOptions {
|
|
@@ -12,11 +13,22 @@ export interface PageOptions {
|
|
|
12
13
|
* always closed afterwards, even if `fn` throws.
|
|
13
14
|
*/
|
|
14
15
|
export declare function withPage<T>(options: PageOptions, fn: (page: Page, context: BrowserContext) => Promise<T>): Promise<T>;
|
|
16
|
+
/** A loaded auth state and the path it was read from — the path is its identity. */
|
|
17
|
+
export interface SessionStorageState {
|
|
18
|
+
path: string;
|
|
19
|
+
state: StorageState;
|
|
20
|
+
}
|
|
15
21
|
export interface SessionOptions {
|
|
16
22
|
/** Resize the current page to this. Omit to leave the page exactly as it is. */
|
|
17
23
|
viewport?: Viewport;
|
|
18
24
|
/** Require a touch-capable page. A session without touch is reopened to get it. */
|
|
19
25
|
hasTouch?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Open the page with this saved auth. Cookies and storage are fixed when the
|
|
28
|
+
* context is created, so a session that was opened with different auth (or
|
|
29
|
+
* none) is reopened to take it. Omit to leave whatever the session has.
|
|
30
|
+
*/
|
|
31
|
+
storageState?: SessionStorageState;
|
|
20
32
|
}
|
|
21
33
|
export interface SessionPage {
|
|
22
34
|
page: Page;
|
|
@@ -26,16 +38,20 @@ export interface SessionPage {
|
|
|
26
38
|
* everything the old page held, which is worth telling the user about.
|
|
27
39
|
*/
|
|
28
40
|
previousUrl?: string;
|
|
41
|
+
/** Why it had to be discarded, so the caller can say so. */
|
|
42
|
+
reopenedFor?: "touch" | "auth";
|
|
29
43
|
}
|
|
30
44
|
/**
|
|
31
45
|
* The current page, opening one if there is none.
|
|
32
46
|
*
|
|
33
47
|
* An existing page is reused, resized only when `viewport` is given and
|
|
34
48
|
* differs — an omitted viewport means "leave the page alone", never "reset it
|
|
35
|
-
* to the default". The
|
|
36
|
-
* `hasTouch
|
|
37
|
-
*
|
|
38
|
-
*
|
|
49
|
+
* to the default". The two things that cannot be changed in place are
|
|
50
|
+
* `hasTouch` and the saved auth: both are fixed when the context is created,
|
|
51
|
+
* so a session that lacks touch, or that was opened with different auth than
|
|
52
|
+
* the caller now asks for, has to be reopened. That loses whatever the old
|
|
53
|
+
* page held, which is why the discarded URL and the reason come back to the
|
|
54
|
+
* caller.
|
|
39
55
|
*/
|
|
40
56
|
export declare function getSessionPage(options?: SessionOptions): Promise<SessionPage>;
|
|
41
57
|
/**
|
package/dist/engine/browser.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { chromium } from "playwright";
|
|
2
2
|
import { DEFAULT_VIEWPORT } from "../constants.js";
|
|
3
|
+
import { hmrFor } from "./hmr.js";
|
|
3
4
|
/**
|
|
4
5
|
* Playwright browser lifecycle.
|
|
5
6
|
*
|
|
@@ -67,15 +68,21 @@ let session = null;
|
|
|
67
68
|
*
|
|
68
69
|
* An existing page is reused, resized only when `viewport` is given and
|
|
69
70
|
* differs — an omitted viewport means "leave the page alone", never "reset it
|
|
70
|
-
* to the default". The
|
|
71
|
-
* `hasTouch
|
|
72
|
-
*
|
|
73
|
-
*
|
|
71
|
+
* to the default". The two things that cannot be changed in place are
|
|
72
|
+
* `hasTouch` and the saved auth: both are fixed when the context is created,
|
|
73
|
+
* so a session that lacks touch, or that was opened with different auth than
|
|
74
|
+
* the caller now asks for, has to be reopened. That loses whatever the old
|
|
75
|
+
* page held, which is why the discarded URL and the reason come back to the
|
|
76
|
+
* caller.
|
|
74
77
|
*/
|
|
75
78
|
export async function getSessionPage(options = {}) {
|
|
76
79
|
const wantsTouch = options.hasTouch === true;
|
|
77
80
|
const live = session !== null && !session.page.isClosed();
|
|
78
|
-
|
|
81
|
+
const hasTouch = live && session.hasTouch;
|
|
82
|
+
// An omitted `storageState` means "leave the session's auth alone"; a named
|
|
83
|
+
// one has to match the file the context was actually created from.
|
|
84
|
+
const hasAuth = !live || options.storageState === undefined || session.storageState?.path === options.storageState.path;
|
|
85
|
+
if (live && (hasTouch || !wantsTouch) && hasAuth) {
|
|
79
86
|
const page = session.page;
|
|
80
87
|
if (options.viewport && !sameSize(page.viewportSize(), options.viewport)) {
|
|
81
88
|
await page.setViewportSize(options.viewport);
|
|
@@ -83,20 +90,30 @@ export async function getSessionPage(options = {}) {
|
|
|
83
90
|
}
|
|
84
91
|
return { page };
|
|
85
92
|
}
|
|
86
|
-
// Either nothing is open, or what is open cannot do what was asked. Carry
|
|
87
|
-
//
|
|
93
|
+
// Either nothing is open, or what is open cannot do what was asked. Carry the
|
|
94
|
+
// old size and auth over so reopening reproduces the page as closely as it can.
|
|
88
95
|
const previousUrl = live ? safeUrl(session.page) : undefined;
|
|
96
|
+
const reopenedFor = live ? (wantsTouch && !hasTouch ? "touch" : "auth") : undefined;
|
|
89
97
|
const viewport = options.viewport ?? session?.viewport ?? { ...DEFAULT_VIEWPORT };
|
|
98
|
+
const storageState = options.storageState ?? session?.storageState;
|
|
90
99
|
await closeSession();
|
|
91
100
|
const browser = await getBrowser();
|
|
92
101
|
const context = await browser.newContext({
|
|
93
102
|
viewport,
|
|
94
103
|
deviceScaleFactor: 1,
|
|
95
104
|
...(wantsTouch ? { hasTouch: true } : {}),
|
|
105
|
+
...(storageState ? { storageState: storageState.state } : {}),
|
|
96
106
|
});
|
|
97
107
|
const page = await context.newPage();
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
// Vite's hot updates are announced in the console; watch from the first
|
|
109
|
+
// request so framewatch_wait_for can tell "changed since you last looked".
|
|
110
|
+
hmrFor(page);
|
|
111
|
+
session = { context, page, hasTouch: wantsTouch, viewport, ...(storageState ? { storageState } : {}) };
|
|
112
|
+
return {
|
|
113
|
+
page,
|
|
114
|
+
...(previousUrl !== undefined ? { previousUrl } : {}),
|
|
115
|
+
...(reopenedFor !== undefined ? { reopenedFor } : {}),
|
|
116
|
+
};
|
|
100
117
|
}
|
|
101
118
|
function sameSize(a, b) {
|
|
102
119
|
return a !== null && a.width === b.width && a.height === b.height;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/engine/browser.ts"],"names":[],"mappings":"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","sourcesContent":["import { chromium, type Browser, type BrowserContext, type BrowserContextOptions, type Page } from \"playwright\";\nimport { DEFAULT_VIEWPORT } from \"../constants.js\";\nimport type { Viewport } from \"../types.js\";\n\n/**\n * Playwright browser lifecycle.\n *\n * A single Chromium instance is shared across all tool calls: it is launched\n * lazily on first use and kept alive until `closeBrowser()` (called on MCP\n * server shutdown). Each tool call gets its own BrowserContext via `withPage`\n * so state (cookies, storage, viewport) never leaks between calls.\n */\n\nlet browserPromise: Promise<Browser> | null = null;\n\nexport async function getBrowser(): Promise<Browser> {\n if (!browserPromise) {\n browserPromise = chromium.launch({ headless: true, handleSIGINT: false }).then((browser) => {\n // If Chromium dies (crash, external kill), forget it so the next call relaunches.\n browser.on(\"disconnected\", () => {\n if (browserPromise === thisLaunch) browserPromise = null;\n });\n return browser;\n });\n const thisLaunch = browserPromise;\n // If launch itself fails, clear the cached rejection so callers can retry.\n browserPromise.catch(() => {\n if (browserPromise === thisLaunch) browserPromise = null;\n });\n }\n return browserPromise;\n}\n\nexport async function closeBrowser(): Promise<void> {\n const pending = browserPromise;\n browserPromise = null;\n session = null;\n if (!pending) return;\n try {\n const browser = await pending;\n await browser.close();\n } catch {\n // Already closed or never launched successfully — nothing to do.\n }\n}\n\nexport interface PageOptions {\n viewport?: Viewport;\n /** Extra Playwright context options (user agent, locale, etc.). */\n contextOptions?: Omit<BrowserContextOptions, \"viewport\">;\n}\n\n/**\n * Run `fn` with a page inside a brand-new browser context. The context is\n * always closed afterwards, even if `fn` throws.\n */\nexport async function withPage<T>(options: PageOptions, fn: (page: Page, context: BrowserContext) => Promise<T>): Promise<T> {\n const browser = await getBrowser();\n const context = await browser.newContext({\n viewport: options.viewport ?? { ...DEFAULT_VIEWPORT },\n deviceScaleFactor: 1,\n ...options.contextOptions,\n });\n try {\n const page = await context.newPage();\n return await fn(page, context);\n } finally {\n await context.close().catch(() => {});\n }\n}\n\n/**\n * The interaction session — \"the current page\".\n *\n * `framewatch_interact` is for iterative testing: click, look, type, look\n * again. That only works if the page survives between tool calls, so unlike\n * `withPage` (a fresh context per call) the session keeps one context and one\n * page alive until the browser closes. Cookies, storage, scroll position and\n * anything the app has in memory carry over from call to call.\n */\ninterface Session {\n context: BrowserContext;\n page: Page;\n hasTouch: boolean;\n viewport: Viewport;\n}\n\nlet session: Session | null = null;\n\nexport interface SessionOptions {\n /** Resize the current page to this. Omit to leave the page exactly as it is. */\n viewport?: Viewport;\n /** Require a touch-capable page. A session without touch is reopened to get it. */\n hasTouch?: boolean;\n}\n\nexport interface SessionPage {\n page: Page;\n /**\n * URL of the page that had to be discarded to satisfy `options`, if any.\n * The caller decides whether to navigate back to it — reopening resets\n * everything the old page held, which is worth telling the user about.\n */\n previousUrl?: string;\n}\n\n/**\n * The current page, opening one if there is none.\n *\n * An existing page is reused, resized only when `viewport` is given and\n * differs — an omitted viewport means \"leave the page alone\", never \"reset it\n * to the default\". The one thing that cannot be changed in place is\n * `hasTouch`: it is fixed when the context is created, so a session that lacks\n * touch has to be reopened to get it. That loses whatever the old page held,\n * which is why the discarded URL comes back to the caller.\n */\nexport async function getSessionPage(options: SessionOptions = {}): Promise<SessionPage> {\n const wantsTouch = options.hasTouch === true;\n const live = session !== null && !session.page.isClosed();\n\n if (live && (session!.hasTouch || !wantsTouch)) {\n const page = session!.page;\n if (options.viewport && !sameSize(page.viewportSize(), options.viewport)) {\n await page.setViewportSize(options.viewport);\n session!.viewport = { ...options.viewport };\n }\n return { page };\n }\n\n // Either nothing is open, or what is open cannot do what was asked. Carry\n // the old size over so reopening reproduces the page as closely as it can.\n const previousUrl = live ? safeUrl(session!.page) : undefined;\n const viewport = options.viewport ?? session?.viewport ?? { ...DEFAULT_VIEWPORT };\n await closeSession();\n\n const browser = await getBrowser();\n const context = await browser.newContext({\n viewport,\n deviceScaleFactor: 1,\n ...(wantsTouch ? { hasTouch: true } : {}),\n });\n const page = await context.newPage();\n session = { context, page, hasTouch: wantsTouch, viewport };\n return { page, ...(previousUrl !== undefined ? { previousUrl } : {}) };\n}\n\nfunction sameSize(a: Viewport | null, b: Viewport): boolean {\n return a !== null && a.width === b.width && a.height === b.height;\n}\n\n/** Serialises everything that touches the session page — see `withSessionLock`. */\nlet sessionQueue: Promise<unknown> = Promise.resolve();\n\n/**\n * Run `fn` with exclusive use of the session page.\n *\n * There is one session, one page and one hand: two callers at once would both\n * find no session and open a context each (orphaning all but the last), and a\n * step that needs touch could close the page another caller is half way\n * through. An MCP client may well call tools in parallel, and\n * `framewatch_compare` reads the same page `framewatch_interact` is driving,\n * so the lock lives here with the session rather than inside either tool.\n *\n * `fn` is run whatever happened to the call before it, and the chain survives\n * a rejection.\n */\nexport function withSessionLock<T>(fn: () => Promise<T>): Promise<T> {\n const result = sessionQueue.then(fn, fn);\n sessionQueue = result.then(\n () => undefined,\n () => undefined,\n );\n return result;\n}\n\n/** Close the current page, if any. The browser itself stays up. */\nexport async function closeSession(): Promise<void> {\n const current = session;\n session = null;\n if (!current) return;\n await current.context.close().catch(() => {});\n}\n\n/** `page.url()` throws once the page is gone; a dead page simply has no url. */\nfunction safeUrl(page: Page): string | undefined {\n try {\n const url = page.url();\n return url === \"about:blank\" ? undefined : url;\n } catch {\n return undefined;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/engine/browser.ts"],"names":[],"mappings":"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","sourcesContent":["import { chromium, type Browser, type BrowserContext, type BrowserContextOptions, type Page } from \"playwright\";\nimport { DEFAULT_VIEWPORT } from \"../constants.js\";\nimport type { Viewport } from \"../types.js\";\nimport type { StorageState } from \"../utils/storage-state.js\";\nimport { hmrFor } from \"./hmr.js\";\n\n/**\n * Playwright browser lifecycle.\n *\n * A single Chromium instance is shared across all tool calls: it is launched\n * lazily on first use and kept alive until `closeBrowser()` (called on MCP\n * server shutdown). Each tool call gets its own BrowserContext via `withPage`\n * so state (cookies, storage, viewport) never leaks between calls.\n */\n\nlet browserPromise: Promise<Browser> | null = null;\n\nexport async function getBrowser(): Promise<Browser> {\n if (!browserPromise) {\n browserPromise = chromium.launch({ headless: true, handleSIGINT: false }).then((browser) => {\n // If Chromium dies (crash, external kill), forget it so the next call relaunches.\n browser.on(\"disconnected\", () => {\n if (browserPromise === thisLaunch) browserPromise = null;\n });\n return browser;\n });\n const thisLaunch = browserPromise;\n // If launch itself fails, clear the cached rejection so callers can retry.\n browserPromise.catch(() => {\n if (browserPromise === thisLaunch) browserPromise = null;\n });\n }\n return browserPromise;\n}\n\nexport async function closeBrowser(): Promise<void> {\n const pending = browserPromise;\n browserPromise = null;\n session = null;\n if (!pending) return;\n try {\n const browser = await pending;\n await browser.close();\n } catch {\n // Already closed or never launched successfully — nothing to do.\n }\n}\n\nexport interface PageOptions {\n viewport?: Viewport;\n /** Extra Playwright context options (user agent, locale, etc.). */\n contextOptions?: Omit<BrowserContextOptions, \"viewport\">;\n}\n\n/**\n * Run `fn` with a page inside a brand-new browser context. The context is\n * always closed afterwards, even if `fn` throws.\n */\nexport async function withPage<T>(options: PageOptions, fn: (page: Page, context: BrowserContext) => Promise<T>): Promise<T> {\n const browser = await getBrowser();\n const context = await browser.newContext({\n viewport: options.viewport ?? { ...DEFAULT_VIEWPORT },\n deviceScaleFactor: 1,\n ...options.contextOptions,\n });\n try {\n const page = await context.newPage();\n return await fn(page, context);\n } finally {\n await context.close().catch(() => {});\n }\n}\n\n/**\n * The interaction session — \"the current page\".\n *\n * `framewatch_interact` is for iterative testing: click, look, type, look\n * again. That only works if the page survives between tool calls, so unlike\n * `withPage` (a fresh context per call) the session keeps one context and one\n * page alive until the browser closes. Cookies, storage, scroll position and\n * anything the app has in memory carry over from call to call.\n */\ninterface Session {\n context: BrowserContext;\n page: Page;\n hasTouch: boolean;\n viewport: Viewport;\n /** The saved auth this context was created with, if any (see SessionOptions). */\n storageState?: SessionStorageState;\n}\n\n/** A loaded auth state and the path it was read from — the path is its identity. */\nexport interface SessionStorageState {\n path: string;\n state: StorageState;\n}\n\nlet session: Session | null = null;\n\nexport interface SessionOptions {\n /** Resize the current page to this. Omit to leave the page exactly as it is. */\n viewport?: Viewport;\n /** Require a touch-capable page. A session without touch is reopened to get it. */\n hasTouch?: boolean;\n /**\n * Open the page with this saved auth. Cookies and storage are fixed when the\n * context is created, so a session that was opened with different auth (or\n * none) is reopened to take it. Omit to leave whatever the session has.\n */\n storageState?: SessionStorageState;\n}\n\nexport interface SessionPage {\n page: Page;\n /**\n * URL of the page that had to be discarded to satisfy `options`, if any.\n * The caller decides whether to navigate back to it — reopening resets\n * everything the old page held, which is worth telling the user about.\n */\n previousUrl?: string;\n /** Why it had to be discarded, so the caller can say so. */\n reopenedFor?: \"touch\" | \"auth\";\n}\n\n/**\n * The current page, opening one if there is none.\n *\n * An existing page is reused, resized only when `viewport` is given and\n * differs — an omitted viewport means \"leave the page alone\", never \"reset it\n * to the default\". The two things that cannot be changed in place are\n * `hasTouch` and the saved auth: both are fixed when the context is created,\n * so a session that lacks touch, or that was opened with different auth than\n * the caller now asks for, has to be reopened. That loses whatever the old\n * page held, which is why the discarded URL and the reason come back to the\n * caller.\n */\nexport async function getSessionPage(options: SessionOptions = {}): Promise<SessionPage> {\n const wantsTouch = options.hasTouch === true;\n const live = session !== null && !session.page.isClosed();\n const hasTouch = live && session!.hasTouch;\n // An omitted `storageState` means \"leave the session's auth alone\"; a named\n // one has to match the file the context was actually created from.\n const hasAuth = !live || options.storageState === undefined || session!.storageState?.path === options.storageState.path;\n\n if (live && (hasTouch || !wantsTouch) && hasAuth) {\n const page = session!.page;\n if (options.viewport && !sameSize(page.viewportSize(), options.viewport)) {\n await page.setViewportSize(options.viewport);\n session!.viewport = { ...options.viewport };\n }\n return { page };\n }\n\n // Either nothing is open, or what is open cannot do what was asked. Carry the\n // old size and auth over so reopening reproduces the page as closely as it can.\n const previousUrl = live ? safeUrl(session!.page) : undefined;\n const reopenedFor: \"touch\" | \"auth\" | undefined = live ? (wantsTouch && !hasTouch ? \"touch\" : \"auth\") : undefined;\n const viewport = options.viewport ?? session?.viewport ?? { ...DEFAULT_VIEWPORT };\n const storageState = options.storageState ?? session?.storageState;\n await closeSession();\n\n const browser = await getBrowser();\n const context = await browser.newContext({\n viewport,\n deviceScaleFactor: 1,\n ...(wantsTouch ? { hasTouch: true } : {}),\n ...(storageState ? { storageState: storageState.state } : {}),\n });\n const page = await context.newPage();\n // Vite's hot updates are announced in the console; watch from the first\n // request so framewatch_wait_for can tell \"changed since you last looked\".\n hmrFor(page);\n session = { context, page, hasTouch: wantsTouch, viewport, ...(storageState ? { storageState } : {}) };\n return {\n page,\n ...(previousUrl !== undefined ? { previousUrl } : {}),\n ...(reopenedFor !== undefined ? { reopenedFor } : {}),\n };\n}\n\nfunction sameSize(a: Viewport | null, b: Viewport): boolean {\n return a !== null && a.width === b.width && a.height === b.height;\n}\n\n/** Serialises everything that touches the session page — see `withSessionLock`. */\nlet sessionQueue: Promise<unknown> = Promise.resolve();\n\n/**\n * Run `fn` with exclusive use of the session page.\n *\n * There is one session, one page and one hand: two callers at once would both\n * find no session and open a context each (orphaning all but the last), and a\n * step that needs touch could close the page another caller is half way\n * through. An MCP client may well call tools in parallel, and\n * `framewatch_compare` reads the same page `framewatch_interact` is driving,\n * so the lock lives here with the session rather than inside either tool.\n *\n * `fn` is run whatever happened to the call before it, and the chain survives\n * a rejection.\n */\nexport function withSessionLock<T>(fn: () => Promise<T>): Promise<T> {\n const result = sessionQueue.then(fn, fn);\n sessionQueue = result.then(\n () => undefined,\n () => undefined,\n );\n return result;\n}\n\n/** Close the current page, if any. The browser itself stays up. */\nexport async function closeSession(): Promise<void> {\n const current = session;\n session = null;\n if (!current) return;\n await current.context.close().catch(() => {});\n}\n\n/** `page.url()` throws once the page is gone; a dead page simply has no url. */\nfunction safeUrl(page: Page): string | undefined {\n try {\n const url = page.url();\n return url === \"about:blank\" ? undefined : url;\n } catch {\n return undefined;\n }\n}\n"]}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { ElementHandle, Page } from "playwright";
|
|
2
|
+
import type { ConsoleEntry, NetworkEvent } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Click engine.
|
|
5
|
+
*
|
|
6
|
+
* Finds everything on a page that looks clickable, watches what a click
|
|
7
|
+
* actually does, and decides whether anything happened at all. Nothing here
|
|
8
|
+
* decides how to *report* it; that is `tools/dead-clicks.ts`.
|
|
9
|
+
*
|
|
10
|
+
* The hard part is not clicking — it is knowing that nothing happened. A
|
|
11
|
+
* handler can navigate, mutate the DOM, fetch, open a dialog, toggle a
|
|
12
|
+
* checkbox, scroll, move focus, write to localStorage or throw, and only some
|
|
13
|
+
* of those are visible. So the answer is assembled from every channel at once:
|
|
14
|
+
* the URL, an in-page MutationObserver, a signature of every field's value, of
|
|
15
|
+
* storage and of scroll position, plus Playwright's own view of the console,
|
|
16
|
+
* the network, dialogs, popups and downloads. "Dead" means every one of them
|
|
17
|
+
* was silent.
|
|
18
|
+
*
|
|
19
|
+
* Pages are rarely silent on their own, though — a clock ticks, a carousel
|
|
20
|
+
* advances, analytics beacons fire — so the same measurement is taken once
|
|
21
|
+
* with nobody clicking, and whatever the page does by itself is subtracted
|
|
22
|
+
* from every result (see `IdleNoise`). Without that, one `setInterval` makes
|
|
23
|
+
* every element on the page look alive and the tool finds nothing.
|
|
24
|
+
*/
|
|
25
|
+
/** The `window` key the in-page watcher keeps its counters under. */
|
|
26
|
+
export declare const CLICK_WATCH_KEY = "__framewatch_clicks";
|
|
27
|
+
/** Why an element counts as clickable. */
|
|
28
|
+
export type ClickableKind = "link" | "button" | "role" | "handler" | "pointer";
|
|
29
|
+
/** Everything known about one candidate before it is clicked. */
|
|
30
|
+
export interface Clickable {
|
|
31
|
+
/** 1-based position in document order, and the number the report uses. */
|
|
32
|
+
index: number;
|
|
33
|
+
/**
|
|
34
|
+
* A CSS selector for the element, recomputed rather than remembered: the
|
|
35
|
+
* page is reloaded whenever a click changes it, and an ElementHandle does
|
|
36
|
+
* not survive that.
|
|
37
|
+
*/
|
|
38
|
+
selector: string;
|
|
39
|
+
/** Which match of `selector` this is, for the (common) case where it is not unique. */
|
|
40
|
+
match_index: number;
|
|
41
|
+
/** How the element is named in the report — `button "Save draft"`. */
|
|
42
|
+
description: string;
|
|
43
|
+
kind: ClickableKind;
|
|
44
|
+
tag: string;
|
|
45
|
+
text: string;
|
|
46
|
+
href?: string;
|
|
47
|
+
role?: string;
|
|
48
|
+
/** The computed `cursor`. `pointer` on a dead element is the whole finding. */
|
|
49
|
+
cursor: string;
|
|
50
|
+
/** The element says it is disabled without being disabled — clicking it is the test. */
|
|
51
|
+
aria_disabled: boolean;
|
|
52
|
+
/** Set when the element must not be clicked, and why. */
|
|
53
|
+
skip?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface DiscoverClickablesOptions {
|
|
56
|
+
/** Only look inside this. Defaults to the whole document. */
|
|
57
|
+
selector?: string;
|
|
58
|
+
/** Never click anything matching this, or inside it. */
|
|
59
|
+
exclude?: string;
|
|
60
|
+
/** Include elements that are only clickable-looking because of `cursor: pointer`. */
|
|
61
|
+
include_pointer?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Find everything on the page a user could reasonably expect to click.
|
|
65
|
+
*
|
|
66
|
+
* Links, buttons, ARIA click roles and `onclick` attributes are the semantic
|
|
67
|
+
* half. The other half is `cursor: pointer`, which is how a `<div>` announces
|
|
68
|
+
* itself as a button — and which is where dead clicks actually live, because a
|
|
69
|
+
* div has no default behaviour to fall back on when its handler never got
|
|
70
|
+
* attached.
|
|
71
|
+
*
|
|
72
|
+
* `cursor` is an inherited property, so a pointer-styled card makes every one
|
|
73
|
+
* of its descendants look clickable too. Only the outermost element of each
|
|
74
|
+
* pointer subtree is kept, and a pointer element that wraps (or sits inside) a
|
|
75
|
+
* real link or button is dropped entirely — that link is the thing being
|
|
76
|
+
* clicked, and it is tested on its own.
|
|
77
|
+
*/
|
|
78
|
+
export declare function discoverClickables(page: Page, options?: DiscoverClickablesOptions): Promise<Clickable[]>;
|
|
79
|
+
/**
|
|
80
|
+
* How an element is named in the report: `button "Save draft"`, `a "Pricing"`.
|
|
81
|
+
*
|
|
82
|
+
* An icon-only control has no text at all, and a bare selector is the least
|
|
83
|
+
* recognisable thing to call it by — so a link falls back to where it points,
|
|
84
|
+
* and everything else to its selector.
|
|
85
|
+
*/
|
|
86
|
+
export declare function describeClickable(item: {
|
|
87
|
+
tag: string;
|
|
88
|
+
text: string;
|
|
89
|
+
kind: ClickableKind;
|
|
90
|
+
href?: string;
|
|
91
|
+
selector: string;
|
|
92
|
+
}): string;
|
|
93
|
+
/** Re-find a candidate on the page as it is now. Null when it is no longer there. */
|
|
94
|
+
export declare function resolveClickable(page: Page, clickable: Clickable): Promise<ElementHandle | null>;
|
|
95
|
+
/** Everything about the page that a click could change, read in one go. */
|
|
96
|
+
export interface PageState {
|
|
97
|
+
/** From `page.url()`, not from the page — a document mid-navigation cannot be asked. */
|
|
98
|
+
url: string;
|
|
99
|
+
/** Identifies this document. A reload keeps the URL and changes this. */
|
|
100
|
+
doc: string;
|
|
101
|
+
/** False when the MutationObserver could not be installed; `elements` is then the fallback. */
|
|
102
|
+
watching: boolean;
|
|
103
|
+
mutations: number;
|
|
104
|
+
/** The first few mutations, described — `+ div.modal in #app`. */
|
|
105
|
+
changes: string[];
|
|
106
|
+
elements: number;
|
|
107
|
+
/** Hash of every field's value and checked state. */
|
|
108
|
+
fields: number;
|
|
109
|
+
/** Hash of localStorage and sessionStorage. */
|
|
110
|
+
storage: number;
|
|
111
|
+
scroll_x: number;
|
|
112
|
+
scroll_y: number;
|
|
113
|
+
title: string;
|
|
114
|
+
}
|
|
115
|
+
/** What Playwright saw while the click was settling, which the page cannot report on itself. */
|
|
116
|
+
export interface ClickEvidence {
|
|
117
|
+
console: ConsoleEntry[];
|
|
118
|
+
network: NetworkEvent[];
|
|
119
|
+
dialogs: string[];
|
|
120
|
+
popups: string[];
|
|
121
|
+
downloads: string[];
|
|
122
|
+
/**
|
|
123
|
+
* True when focus stayed on the clicked element (or inside it), which is
|
|
124
|
+
* what clicking anything focusable does and therefore says nothing. Focus
|
|
125
|
+
* landing somewhere *else* — a label handing it to its input, a skip link —
|
|
126
|
+
* is a real effect.
|
|
127
|
+
*/
|
|
128
|
+
focus_self: boolean;
|
|
129
|
+
}
|
|
130
|
+
export declare const EMPTY_EVIDENCE: ClickEvidence;
|
|
131
|
+
/** What the page does with nobody clicking, so it can be subtracted from what it does when clicked. */
|
|
132
|
+
export interface IdleNoise {
|
|
133
|
+
/** Mutations observed in one idle window. A click has to beat this to count. */
|
|
134
|
+
mutations: number;
|
|
135
|
+
/**
|
|
136
|
+
* The mutations the page makes by itself, described. This, rather than the
|
|
137
|
+
* count, is what a click is judged against: timer jitter means an idle
|
|
138
|
+
* window and a click window never see the same *number* of ticks, but they
|
|
139
|
+
* do see the same *kind* of change.
|
|
140
|
+
*/
|
|
141
|
+
changes: string[];
|
|
142
|
+
/** Requests the page makes by itself — polling, analytics, lazy loading. */
|
|
143
|
+
network: string[];
|
|
144
|
+
/** What it logs by itself. */
|
|
145
|
+
console: string[];
|
|
146
|
+
/** Signals that fired with nobody clicking, and are therefore worthless here. */
|
|
147
|
+
unstable: EffectKind[];
|
|
148
|
+
}
|
|
149
|
+
export declare const NO_NOISE: IdleNoise;
|
|
150
|
+
export type EffectKind = "error" | "navigated" | "reloaded" | "dialog" | "popup" | "download" | "dom" | "fields" | "storage" | "scroll" | "focus" | "title" | "network" | "console";
|
|
151
|
+
export interface ClickEffect {
|
|
152
|
+
kind: EffectKind;
|
|
153
|
+
detail: string;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Everything the click did. An empty list is a dead click.
|
|
157
|
+
*
|
|
158
|
+
* Pure, so every verdict this tool reaches is unit-testable without a browser.
|
|
159
|
+
*
|
|
160
|
+
* When the URL changed, the two states describe two different documents and
|
|
161
|
+
* comparing their innards would be meaningless — the navigation is the answer,
|
|
162
|
+
* and only the out-of-page evidence is still read.
|
|
163
|
+
*/
|
|
164
|
+
export declare function diffPageState(before: PageState, after: PageState, evidence?: ClickEvidence, idle?: IdleNoise): ClickEffect[];
|
|
165
|
+
/**
|
|
166
|
+
* What the page did to itself. Anything here fires without a click, so it
|
|
167
|
+
* cannot be evidence that a click did something.
|
|
168
|
+
*/
|
|
169
|
+
export declare function measureNoise(before: PageState, after: PageState, evidence: ClickEvidence): IdleNoise;
|
|
170
|
+
/** One line describing what a page does on its own, or undefined when it does nothing. */
|
|
171
|
+
export declare function describeNoise(noise: IdleNoise): string | undefined;
|
|
172
|
+
/**
|
|
173
|
+
* Install the mutation counter, on this document and on every one after it.
|
|
174
|
+
*
|
|
175
|
+
* An init script alone would only reach the *next* document, and the page is
|
|
176
|
+
* usually already open by the time the sweep starts; running it once by hand
|
|
177
|
+
* covers the one that is there. The script refuses to install twice, so the
|
|
178
|
+
* two paths cannot double-count.
|
|
179
|
+
*/
|
|
180
|
+
export declare function installClickWatcher(page: Page): Promise<void>;
|
|
181
|
+
/** Zero the counters, so the next reading measures one click and nothing before it. */
|
|
182
|
+
export declare function resetClickWatcher(page: Page): Promise<void>;
|
|
183
|
+
/**
|
|
184
|
+
* Read everything the page can say about its own state.
|
|
185
|
+
*
|
|
186
|
+
* Returns null when the page cannot be asked at all — mid-navigation, closed,
|
|
187
|
+
* crashed. A caller that gets null has still learned something (the URL, the
|
|
188
|
+
* console, the network), so this is not an error.
|
|
189
|
+
*/
|
|
190
|
+
export declare function readPageState(page: Page): Promise<PageState | null>;
|
|
191
|
+
export interface HoverProbe {
|
|
192
|
+
/** The computed cursor while hovered — `pointer` is the page inviting the click. */
|
|
193
|
+
cursor: string;
|
|
194
|
+
/** Which visual properties the page changes on hover, in words. */
|
|
195
|
+
changed: string[];
|
|
196
|
+
/** Why the probe could not run. */
|
|
197
|
+
failed?: string;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Does the page react when the pointer is over this element?
|
|
201
|
+
*
|
|
202
|
+
* Asked only of elements that turned out dead, and for one reason: an element
|
|
203
|
+
* that does nothing *and* lights up under the pointer is actively inviting the
|
|
204
|
+
* click that will do nothing, which is the difference between a missing
|
|
205
|
+
* feature and a broken one.
|
|
206
|
+
*
|
|
207
|
+
* Safe to run afterwards because a dead element, by definition, left the page
|
|
208
|
+
* exactly as it was.
|
|
209
|
+
*/
|
|
210
|
+
export declare function probeHover(page: Page, handle: ElementHandle, options?: {
|
|
211
|
+
timeout_ms?: number;
|
|
212
|
+
settle_ms?: number;
|
|
213
|
+
}): Promise<HoverProbe>;
|
|
214
|
+
/**
|
|
215
|
+
* Two URLs that differ only by an empty fragment are the same place.
|
|
216
|
+
*
|
|
217
|
+
* `<a href="#">` is the canonical dead link, and clicking one appends a bare
|
|
218
|
+
* `#` to the address bar. Counting that as a navigation would report every
|
|
219
|
+
* dead link in the world as working.
|
|
220
|
+
*/
|
|
221
|
+
export declare function sameUrl(a: string, b: string): boolean;
|