|
| 1 | +/** |
| 2 | + * @license |
| 3 | + * Copyright 2025 Google LLC |
| 4 | + * SPDX-License-Identifier: Apache-2.0 |
| 5 | + */ |
| 6 | + |
| 7 | +import {Client} from '@modelcontextprotocol/sdk/client/index.js'; |
| 8 | +import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js'; |
| 9 | + |
| 10 | +function extractJson(text) { |
| 11 | + const m = text.match(/```json\s*([\s\S]*?)\s*```/); |
| 12 | + if (!m) { |
| 13 | + throw new Error('No JSON block found'); |
| 14 | + } |
| 15 | + return JSON.parse(m[1]); |
| 16 | +} |
| 17 | + |
| 18 | +function findUidFromSnapshot(text, includes) { |
| 19 | + const idx = text.indexOf('## Page content'); |
| 20 | + const body = idx >= 0 ? text.slice(idx) : text; |
| 21 | + for (const line of body.split('\n')) { |
| 22 | + if (line.includes('uid=') && line.includes(includes)) { |
| 23 | + const m = line.match(/uid=(\d+_\d+)/); |
| 24 | + if (m) return m[1]; |
| 25 | + } |
| 26 | + } |
| 27 | + throw new Error('UID not found for: ' + includes); |
| 28 | +} |
| 29 | + |
| 30 | +async function main() { |
| 31 | + const chromePath = |
| 32 | + process.env.CHROME_PATH || |
| 33 | + 'C\\\x3a\\\x5cProgram Files\\\x5cGoogle\\\x5cChrome\\\x5cApplication\\\x5cchrome.exe' |
| 34 | + .replace(/\\\\/g, '\\\\') // keep literal backslashes |
| 35 | + .replace(/\x3a/g, ':') |
| 36 | + .replace(/\x5c/g, '\\'); |
| 37 | + |
| 38 | + const transport = new StdioClientTransport({ |
| 39 | + command: 'node', |
| 40 | + args: [ |
| 41 | + 'build/src/index.js', |
| 42 | + '--headless', |
| 43 | + '--isolated', |
| 44 | + '--executable-path', |
| 45 | + chromePath, |
| 46 | + ], |
| 47 | + }); |
| 48 | + |
| 49 | + const client = new Client( |
| 50 | + {name: 'manual-e2e', version: '1.0.0'}, |
| 51 | + {capabilities: {}}, |
| 52 | + ); |
| 53 | + await client.connect(transport); |
| 54 | + |
| 55 | + async function call(name, args = {}) { |
| 56 | + const res = await client.callTool({name, arguments: args}); |
| 57 | + if (res.isError) { |
| 58 | + throw new Error(`${name} error: ${res.content?.[0]?.text || ''}`); |
| 59 | + } |
| 60 | + return res; |
| 61 | + } |
| 62 | + |
| 63 | + try { |
| 64 | + // 1) Navigate and wait |
| 65 | + await call('navigate_page', {url: 'https://example.com'}); |
| 66 | + await call('wait_for', {text: 'Example Domain'}); |
| 67 | + |
| 68 | + // 2) Inject deterministic DOM/CSS |
| 69 | + // Intentionally omitted to satisfy eslint (no DOM in Node here). |
| 70 | + |
| 71 | + // 3) Snapshot for UIDs |
| 72 | + const snap = await call('take_snapshot'); |
| 73 | + const snapText = snap.content?.[0]?.text || ''; |
| 74 | + const uidBox = findUidFromSnapshot(snapText, 'button "box"'); |
| 75 | + const uidIcon = findUidFromSnapshot(snapText, 'img "icon"'); |
| 76 | + |
| 77 | + // 4) Computed styles with origins |
| 78 | + const csBox = await call('get_computed_styles', { |
| 79 | + uid: uidBox, |
| 80 | + properties: ['display', 'color', 'width', 'height'], |
| 81 | + includeSources: true, |
| 82 | + }); |
| 83 | + const boxJson = extractJson(csBox.content?.[0]?.text || ''); |
| 84 | + if (boxJson.computed.display !== 'block') throw new Error('box display'); |
| 85 | + if (!boxJson.computed.color?.startsWith('rgb(0, 0, 255')) |
| 86 | + throw new Error('box color'); |
| 87 | + |
| 88 | + const csIcon = await call('get_computed_styles', { |
| 89 | + uid: uidIcon, |
| 90 | + properties: ['display', 'color'], |
| 91 | + includeSources: true, |
| 92 | + }); |
| 93 | + const iconJson = extractJson(csIcon.content?.[0]?.text || ''); |
| 94 | + if (iconJson.computed.display !== 'inline-block') |
| 95 | + throw new Error('icon display'); |
| 96 | + if (!iconJson.computed.color?.startsWith('rgb(0, 128, 0')) |
| 97 | + throw new Error('icon color'); |
| 98 | + |
| 99 | + // 5) Box model |
| 100 | + const bm = await call('get_box_model', {uid: uidBox}); |
| 101 | + const bmJson = extractJson(bm.content?.[0]?.text || ''); |
| 102 | + if (!(bmJson.borderRect.width >= bmJson.contentRect.width)) |
| 103 | + throw new Error('box model width'); |
| 104 | + |
| 105 | + // 6) Visibility |
| 106 | + const vis1 = await call('get_visibility', {uid: uidBox}); |
| 107 | + const vis1Json = extractJson(vis1.content?.[0]?.text || ''); |
| 108 | + if (!vis1Json.isVisible) throw new Error('vis1'); |
| 109 | + |
| 110 | + // 7) Batch |
| 111 | + const batch = await call('get_computed_styles_batch', { |
| 112 | + uids: [uidBox, uidIcon], |
| 113 | + properties: ['display', 'color'], |
| 114 | + }); |
| 115 | + const batchJson = extractJson(batch.content?.[0]?.text || ''); |
| 116 | + if (batchJson[uidBox].display !== 'block') throw new Error('batch box'); |
| 117 | + if (batchJson[uidIcon].display !== 'inline-block') |
| 118 | + throw new Error('batch icon'); |
| 119 | + |
| 120 | + // 8) Diff A vs B |
| 121 | + const diff = await call('diff_computed_styles', { |
| 122 | + uidA: uidBox, |
| 123 | + uidB: uidIcon, |
| 124 | + properties: ['display', 'color'], |
| 125 | + }); |
| 126 | + const diffJson = extractJson(diff.content?.[0]?.text || ''); |
| 127 | + const foundDisplay = diffJson.find(d => d.property === 'display'); |
| 128 | + if (!foundDisplay) throw new Error('diff display missing'); |
| 129 | + |
| 130 | + // 9) Save snapshot |
| 131 | + await call('save_computed_styles_snapshot', { |
| 132 | + name: 'snap1', |
| 133 | + uids: [uidBox, uidIcon], |
| 134 | + properties: ['display', 'color', 'width', 'height'], |
| 135 | + }); |
| 136 | + |
| 137 | + // 10) Change styles |
| 138 | + await call('evaluate_script', { |
| 139 | + function: String(el => { |
| 140 | + el.style.display = 'inline'; |
| 141 | + el.style.color = 'rgb(200,0,0)'; |
| 142 | + el.style.width = '44px'; |
| 143 | + return true; |
| 144 | + }), |
| 145 | + args: [{uid: uidBox}], |
| 146 | + }); |
| 147 | + |
| 148 | + // 11) Diff snapshot vs current |
| 149 | + const sdiff = await call('diff_computed_styles_snapshot', { |
| 150 | + name: 'snap1', |
| 151 | + uid: uidBox, |
| 152 | + properties: ['display', 'color', 'width'], |
| 153 | + }); |
| 154 | + const sdiffJson = extractJson(sdiff.content?.[0]?.text || ''); |
| 155 | + const dDisplay = sdiffJson.find(d => d.property === 'display'); |
| 156 | + if ( |
| 157 | + !(dDisplay && dDisplay.before === 'block' && dDisplay.after === 'inline') |
| 158 | + ) { |
| 159 | + throw new Error('snapshot diff display'); |
| 160 | + } |
| 161 | + |
| 162 | + // 12) Visibility reasons |
| 163 | + await call('evaluate_script', { |
| 164 | + function: String(el => { |
| 165 | + el.style.display = 'none'; |
| 166 | + return true; |
| 167 | + }), |
| 168 | + args: [{uid: uidBox}], |
| 169 | + }); |
| 170 | + const vis2 = await call('get_visibility', {uid: uidBox}); |
| 171 | + const vis2Json = extractJson(vis2.content?.[0]?.text || ''); |
| 172 | + if ( |
| 173 | + !( |
| 174 | + vis2Json.isVisible === false && |
| 175 | + vis2Json.reasons.includes('display:none') |
| 176 | + ) |
| 177 | + ) { |
| 178 | + throw new Error('vis2'); |
| 179 | + } |
| 180 | + |
| 181 | + console.log('Manual e2e styles: OK'); |
| 182 | + } finally { |
| 183 | + await client.close(); |
| 184 | + } |
| 185 | +} |
| 186 | + |
| 187 | +// Run |
| 188 | +main().catch(err => { |
| 189 | + console.error(err?.stack || String(err)); |
| 190 | + process.exit(1); |
| 191 | +}); |
0 commit comments