diff --git a/coverage/base.css b/coverage/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/coverage/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/coverage/block-navigation.js b/coverage/block-navigation.js new file mode 100644 index 0000000..530d1ed --- /dev/null +++ b/coverage/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/coverage/browser.ts.html b/coverage/browser.ts.html new file mode 100644 index 0000000..dd2f915 --- /dev/null +++ b/coverage/browser.ts.html @@ -0,0 +1,691 @@ + + + + +
++ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 | + + + + +6x +6x +6x + + + +6x + + + + + + + + + + + + + + + + + + +1x + + + + + + + + +12x +12x + + + + + +1x +1x + + + + +1x +1x + + + + + +1x +1x + + + +1x +1x + + + + + + + + + + + + + + + +1x +1x +1x + + + + +1x +1x + + + +1x +1x + + + + +1x +1x + + + + +1x +1x + + + + +1x +1x + + + + +1x +1x + + + +1x +1x + + + + + +1x +1x + + + +14x +14x + +1x + + + +1x + + + +1x + + + +1x + + + +1x + + + +1x + + + +1x + + + +1x + + + +1x + + + +1x + + + +1x + + + +1x + + + +1x +1x + + +1x + + + + + + + + +13x + + + + | import { resolve } from 'node:path';
+import { type Browser, type BrowserContext, type Page, webkit } from 'playwright';
+import type { BrowserCommand, BrowserOptions, CommandResponse, ElementInfo } from './types.js';
+
+export class ClaudeBrowser {
+ private browser: Browser | null = null;
+ private context: BrowserContext | null = null;
+ private page: Page | null = null;
+ private options: Required<BrowserOptions>;
+
+ constructor(options: BrowserOptions = {}) {
+ this.options = {
+ headless: options.headless ?? true,
+ width: options.width ?? 1280,
+ height: options.height ?? 800,
+ };
+ }
+
+ async launch(): Promise<void> {
+ this.browser = await webkit.launch({ headless: this.options.headless });
+ this.context = await this.browser.newContext({
+ viewport: {
+ width: this.options.width,
+ height: this.options.height,
+ },
+ });
+ this.page = await this.context.newPage();
+ }
+
+ async close(): Promise<void> {
+ Iif (this.browser) {
+ await this.browser.close();
+ this.browser = null;
+ this.context = null;
+ this.page = null;
+ }
+ }
+
+ private ensurePage(): Page {
+ Eif (!this.page) {
+ throw new Error('Browser not launched. Call launch() first.');
+ }
+ return this.page;
+ }
+
+ async goto(url: string): Promise<{ url: string; title: string }> {
+ const page = this.ensurePage();
+ await page.goto(url, { waitUntil: 'networkidle' });
+ return { url: page.url(), title: await page.title() };
+ }
+
+ async click(selector: string): Promise<{ url: string }> {
+ const page = this.ensurePage();
+ await page.click(selector);
+ await page.waitForLoadState('networkidle').catch(() => {});
+ return { url: page.url() };
+ }
+
+ async type(selector: string, text: string): Promise<void> {
+ const page = this.ensurePage();
+ await page.fill(selector, text);
+ }
+
+ async query(selector: string): Promise<ElementInfo[]> {
+ const page = this.ensurePage();
+ return page.$$eval(selector, (nodes) =>
+ nodes.map((el) => {
+ const attrs: Record<string, string> = {};
+ for (const attr of el.attributes) {
+ attrs[attr.name] = attr.value;
+ }
+ return {
+ tag: el.tagName.toLowerCase(),
+ text: el.textContent?.trim().slice(0, 200) || '',
+ attributes: attrs,
+ };
+ })
+ );
+ }
+
+ async screenshot(path?: string, fullPage = false): Promise<{ path: string; buffer?: Buffer }> {
+ const page = this.ensurePage();
+ const resolvedPath = resolve(path || 'screenshot.png');
+ const buffer = await page.screenshot({ path: resolvedPath, fullPage });
+ return { path: resolvedPath, buffer };
+ }
+
+ async getUrl(): Promise<{ url: string; title: string }> {
+ const page = this.ensurePage();
+ return { url: page.url(), title: await page.title() };
+ }
+
+ async getHtml(full = false): Promise<string> {
+ const page = this.ensurePage();
+ const html = await page.content();
+ return full ? html : html.slice(0, 10000);
+ }
+
+ async back(): Promise<{ url: string }> {
+ const page = this.ensurePage();
+ await page.goBack();
+ return { url: page.url() };
+ }
+
+ async forward(): Promise<{ url: string }> {
+ const page = this.ensurePage();
+ await page.goForward();
+ return { url: page.url() };
+ }
+
+ async reload(): Promise<{ url: string }> {
+ const page = this.ensurePage();
+ await page.reload();
+ return { url: page.url() };
+ }
+
+ async wait(ms = 1000): Promise<void> {
+ const page = this.ensurePage();
+ await page.waitForTimeout(ms);
+ }
+
+ async newPage(): Promise<void> {
+ Eif (!this.context) {
+ throw new Error('Browser not launched. Call launch() first.');
+ }
+ this.page = await this.context.newPage();
+ }
+
+ async eval(script: string): Promise<unknown> {
+ const page = this.ensurePage();
+ return page.evaluate(script);
+ }
+
+ async executeCommand(cmd: BrowserCommand): Promise<CommandResponse> {
+ try {
+ switch (cmd.cmd) {
+ case 'goto': {
+ const result = await this.goto(cmd.url);
+ return { ok: true, ...result };
+ }
+ case 'click': {
+ const result = await this.click(cmd.selector);
+ return { ok: true, ...result };
+ }
+ case 'type': {
+ await this.type(cmd.selector, cmd.text);
+ return { ok: true };
+ }
+ case 'query': {
+ const elements = await this.query(cmd.selector);
+ return { ok: true, count: elements.length, elements };
+ }
+ case 'screenshot': {
+ const result = await this.screenshot(cmd.path, cmd.fullPage);
+ return { ok: true, path: result.path };
+ }
+ case 'url': {
+ const result = await this.getUrl();
+ return { ok: true, ...result };
+ }
+ case 'html': {
+ const html = await this.getHtml(cmd.full);
+ return { ok: true, html };
+ }
+ case 'back': {
+ const result = await this.back();
+ return { ok: true, ...result };
+ }
+ case 'forward': {
+ const result = await this.forward();
+ return { ok: true, ...result };
+ }
+ case 'reload': {
+ const result = await this.reload();
+ return { ok: true, ...result };
+ }
+ case 'wait': {
+ await this.wait(cmd.ms);
+ return { ok: true };
+ }
+ case 'newpage': {
+ await this.newPage();
+ return { ok: true };
+ }
+ case 'close': {
+ await this.close();
+ return { ok: true };
+ }
+ case 'eval': {
+ const result = await this.eval(cmd.script);
+ return { ok: true, result };
+ }
+ default: {
+ const _exhaustive: never = cmd;
+ return { ok: false, error: `Unknown command: ${(_exhaustive as { cmd: string }).cmd}` };
+ }
+ }
+ } catch (err) {
+ return { ok: false, error: (err as Error).message };
+ }
+ }
+}
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| browser.ts | +
+
+ |
+ 58.42% | +52/89 | +75.67% | +28/37 | +80.95% | +17/21 | +58.42% | +52/89 | +
| logger.ts | +
+
+ |
+ 89.47% | +34/38 | +66.66% | +28/42 | +81.25% | +13/16 | +91.89% | +34/37 | +
| server.ts | +
+
+ |
+ 44.23% | +23/52 | +70% | +7/10 | +61.53% | +8/13 | +43.13% | +22/51 | +
| types.ts | +
+
+ |
+ 0% | +0/0 | +0% | +0/0 | +0% | +0/0 | +0% | +0/0 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 | + + + +2x + + + + + + + + + + + + + + + + + +2x + + + + + + + + + + + + + + + + + +23x + + + +3x + + + + + + + + + + + + + + +18x + +4x + + +5x + +2x + +2x + +1x + +1x + + + +3x + + + + +11x +11x +11x +11x +11x + + + + + + + + + + + + + +2x +3x +2x +2x + +2x +1x + + + + +12x +1x + +11x +11x +12x +12x + + + + + +6x + +9x + + +9x + + + + + +2x + + +2x + | import chalk from 'chalk';
+import logSymbols from 'log-symbols';
+
+// Icons for commands
+export const icons: Record<string, string> = {
+ goto: '→',
+ click: '◉',
+ type: '⌨',
+ query: '?',
+ screenshot: '📷',
+ url: '🔗',
+ html: '<>',
+ back: '←',
+ forward: '→',
+ reload: '↻',
+ wait: '⏳',
+ newpage: '+',
+ close: '✕',
+ eval: '⚡',
+};
+
+// Colors for command types
+export const cmdColor: Record<string, (s: string) => string> = {
+ goto: chalk.cyan,
+ click: chalk.yellow,
+ type: chalk.magenta,
+ query: chalk.blue,
+ screenshot: chalk.green,
+ url: chalk.cyan,
+ html: chalk.blue,
+ back: chalk.yellow,
+ forward: chalk.yellow,
+ reload: chalk.yellow,
+ wait: chalk.gray,
+ newpage: chalk.green,
+ close: chalk.red,
+ eval: chalk.magenta,
+};
+
+export function ts(): string {
+ return chalk.gray(`[${new Date().toISOString()}]`);
+}
+
+export function truncate(str: string, max: number): string {
+ return str.length > max ? `${str.slice(0, max)}...` : str;
+}
+
+export interface CommandLike {
+ cmd: string;
+ url?: string;
+ selector?: string;
+ text?: string;
+ path?: string;
+ full?: boolean;
+ ms?: number;
+ script?: string;
+}
+
+export function getCommandDetail(cmd: CommandLike): string | undefined {
+ switch (cmd.cmd) {
+ case 'goto':
+ return chalk.white(cmd.url);
+ case 'click':
+ case 'query':
+ return chalk.white(cmd.selector);
+ case 'type':
+ return `${chalk.white(cmd.selector)} ${chalk.dim(`="${cmd.text}"`)}`;
+ case 'screenshot':
+ return chalk.dim(cmd.path || 'screenshot.png');
+ case 'html':
+ return cmd.full ? chalk.dim('(full)') : undefined;
+ case 'wait':
+ return chalk.dim(`${cmd.ms || 1000}ms`);
+ case 'eval':
+ return chalk.dim(truncate(cmd.script || '', 50));
+ default:
+ return undefined;
+ }
+}
+
+export function formatCommand(cmd: CommandLike): string {
+ const color = cmdColor[cmd.cmd] || chalk.white;
+ const icon = icons[cmd.cmd] || '•';
+ const detail = getCommandDetail(cmd);
+ const suffix = detail ? ` ${detail}` : '';
+ return `${ts()} ${chalk.bold(color(icon))} ${color(cmd.cmd.toUpperCase())}${suffix}`;
+}
+
+export interface ResultLike {
+ ok: boolean;
+ error?: string;
+ title?: string;
+ url?: string;
+ count?: number;
+ path?: string;
+ html?: string;
+ result?: unknown;
+}
+
+const resultFormatters: Record<string, (r: ResultLike) => string | undefined> = {
+ goto: (r) => r.title,
+ click: (r) => (r.url ? `→ ${r.url}` : undefined),
+ query: (r) => (r.count !== undefined ? `Found ${r.count} element(s)` : undefined),
+ screenshot: (r) => (r.path ? `Saved to ${r.path}` : undefined),
+ url: (r) => r.url,
+ html: (r) => (r.html !== undefined ? `${r.html.length} chars` : undefined),
+ eval: (r) => (r.result !== undefined ? truncate(JSON.stringify(r.result), 80) : undefined),
+};
+
+export function formatResult(cmd: CommandLike, result: ResultLike): string {
+ if (!result.ok) {
+ return `${ts()} ${logSymbols.error} ${chalk.red(result.error)}`;
+ }
+ const formatter = resultFormatters[cmd.cmd];
+ const msg = formatter ? formatter(result) : undefined;
+ const suffix = msg ? ` ${chalk.dim(msg)}` : '';
+ return `${ts()} ${logSymbols.success}${suffix}`;
+}
+
+export type LogFn = (msg: string) => void;
+
+export function createLogger(logFn: LogFn = console.log) {
+ return {
+ command(cmd: CommandLike): void {
+ logFn(formatCommand(cmd));
+ },
+ result(cmd: CommandLike, result: ResultLike): void {
+ logFn(formatResult(cmd, result));
+ },
+ };
+}
+
+// Default logger to stdout
+export const logger = createLogger();
+
+// Logger to stderr (for MCP)
+export const stderrLogger = createLogger((msg) => process.stderr.write(`${msg}\n`));
+ |