feat: full Firefox DOM control + cross-platform E2E matrix

- Implement 50+ missing methods in Firefox background script
  (dblclick, hover, clear, select, check, uncheck, getText, getHTML,
  getAttribute, querySelectorAll, waitForSelector, fetch, cookies,
  localStorage, injectScript, injectCSS, observeMutations, etc.)
- Add runInPage() helper to fix undefined→{} JSON serialization bug
- Handle extension-request messages in onmessage (was silently dropped)
- Fix client.ws.send() bug in CDP bridge sendToExtension()
- Add cross-platform Playwright E2E suite (45 tests x 5 browsers)
- Add GitHub Actions cross-platform-e2e.yml (3 OS x 3 browsers matrix)
- Bump to v1.7.30
This commit is contained in:
Hanzo Dev
2026-03-10 19:38:55 -07:00
parent b48fa62f5a
commit 3a6c052491
8 changed files with 1591 additions and 133 deletions
+178
View File
@@ -0,0 +1,178 @@
name: Cross-Platform E2E
on:
push:
branches: [main, dev]
paths:
- 'packages/browser/src/**'
- 'packages/browser/e2e/**'
- '.github/workflows/cross-platform-e2e.yml'
pull_request:
branches: [main]
paths:
- 'packages/browser/src/**'
- 'packages/browser/e2e/**'
workflow_dispatch:
schedule:
# Daily at 06:00 UTC
- cron: '0 6 * * *'
jobs:
# ═══════════════════════════════════════════════════════════════════
# Cross-browser parity matrix
# Tests core browser tool actions across Chrome, Firefox, WebKit
# on Linux, macOS, and Windows.
# ═══════════════════════════════════════════════════════════════════
e2e-matrix:
name: ${{ matrix.browser }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
browser: [chromium, firefox, webkit]
exclude:
# WebKit on Windows is not supported by Playwright
- os: windows-latest
browser: webkit
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Install Playwright ${{ matrix.browser }}
working-directory: packages/browser
run: npx playwright install --with-deps ${{ matrix.browser }}
- name: Build browser extension
working-directory: packages/browser
run: node src/build.js
- name: Start xvfb (Linux only)
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq xvfb
Xvfb :99 -screen 0 1920x1080x24 &
echo "DISPLAY=:99" >> $GITHUB_ENV
- name: Run parity tests (${{ matrix.browser }})
working-directory: packages/browser
run: |
npx playwright test \
--config=e2e/cross-platform.config.ts \
--project=${{ matrix.browser }} \
e2e/browser-tool-parity.spec.ts
env:
CI: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: results-${{ matrix.os }}-${{ matrix.browser }}
path: |
packages/browser/playwright-report/
packages/browser/e2e/cross-platform-results.json
packages/browser/test-results/
retention-days: 14
# ═══════════════════════════════════════════════════════════════════
# Extension-specific E2E (Chrome + Firefox)
# Tests extension popup, sidebar, overlay, CDP bridge
# ═══════════════════════════════════════════════════════════════════
extension-e2e:
name: Extension E2E (${{ matrix.browser }})
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
browser: [chrome, firefox]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Install Playwright browsers
working-directory: packages/browser
run: npx playwright install --with-deps chromium firefox
- name: Build browser extension
working-directory: packages/browser
run: node src/build.js
- name: Run extension E2E (${{ matrix.browser }})
working-directory: packages/browser
run: |
xvfb-run npx playwright test \
--config=e2e/playwright.config.ts \
e2e/popup.spec.ts e2e/sidebar.spec.ts
env:
CI: true
EXTENSION_BROWSER: ${{ matrix.browser }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: extension-report-${{ matrix.browser }}
path: packages/browser/playwright-report/
retention-days: 14
# ═══════════════════════════════════════════════════════════════════
# Summary — aggregate results from all matrix jobs
# ═══════════════════════════════════════════════════════════════════
summary:
name: Parity Summary
runs-on: ubuntu-latest
needs: [e2e-matrix, extension-e2e]
if: always()
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Generate parity report
run: |
echo "## Cross-Platform E2E Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| OS | Browser | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-----|---------|--------|" >> $GITHUB_STEP_SUMMARY
for dir in artifacts/results-*; do
[ -d "$dir" ] || continue
name=$(basename "$dir" | sed 's/results-//')
os=$(echo "$name" | cut -d- -f1-2)
browser=$(echo "$name" | cut -d- -f3-)
if [ -f "$dir/cross-platform-results.json" ]; then
passed=$(cat "$dir/cross-platform-results.json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(sum(1 for s in d.get('suites',[]) for t in s.get('specs',[]) if all(r.get('status')=='passed' for r in t.get('tests',[]))))" 2>/dev/null || echo "?")
failed=$(cat "$dir/cross-platform-results.json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(sum(1 for s in d.get('suites',[]) for t in s.get('specs',[]) if any(r.get('status')=='failed' for r in t.get('tests',[]))))" 2>/dev/null || echo "?")
echo "| $os | $browser | ${passed} passed / ${failed} failed |" >> $GITHUB_STEP_SUMMARY
else
status="${{ needs.e2e-matrix.result == 'success' && '✅' || '❌' }}"
echo "| $os | $browser | $status |" >> $GITHUB_STEP_SUMMARY
fi
done
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Extension E2E" >> $GITHUB_STEP_SUMMARY
echo "| Browser | Status |" >> $GITHUB_STEP_SUMMARY
echo "|---------|--------|" >> $GITHUB_STEP_SUMMARY
for dir in artifacts/extension-report-*; do
[ -d "$dir" ] || continue
browser=$(basename "$dir" | sed 's/extension-report-//')
echo "| $browser | ${{ needs.extension-e2e.result == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY
done
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hanzo-ai-monorepo",
"version": "1.7.29",
"version": "1.7.30",
"private": true,
"description": "Hanzo AI Development Platform Monorepo",
"license": "MIT",
@@ -0,0 +1,477 @@
/**
* Cross-platform browser tool parity tests.
*
* Validates that ALL core browser actions (the same ones hanzo-mcp sends)
* produce identical results across Chrome, Firefox, and WebKit.
*
* Each test maps directly to a CDP bridge action.
* These run on every platform in the matrix.
*/
import { test, expect, type Page } from '@playwright/test'
const TEST_URL = 'https://example.com'
// Helper: navigate and wait for load
async function loadTestPage(page: Page) {
await page.goto(TEST_URL, { waitUntil: 'domcontentloaded' })
await expect(page.locator('body')).toBeVisible()
}
// ═══════════════════════════════════════════════════════════════════════
// Navigation
// ═══════════════════════════════════════════════════════════════════════
test.describe('Navigation', () => {
test('navigate to URL', async ({ page }) => {
await page.goto(TEST_URL)
await expect(page).toHaveURL(/example\.com/)
})
test('get page title', async ({ page }) => {
await loadTestPage(page)
const title = await page.title()
expect(title).toContain('Example')
})
test('get page URL', async ({ page }) => {
await loadTestPage(page)
expect(page.url()).toContain('example.com')
})
test('reload page', async ({ page }) => {
await loadTestPage(page)
await page.reload()
await expect(page.locator('body')).toBeVisible()
expect(page.url()).toContain('example.com')
})
test('go back and forward', async ({ page }) => {
await page.goto(TEST_URL, { waitUntil: 'domcontentloaded' })
await page.goto('https://example.org', { waitUntil: 'domcontentloaded' })
expect(page.url()).toContain('example.org')
await page.goBack()
await page.waitForLoadState('domcontentloaded')
expect(page.url()).toContain('example.com')
await page.goForward()
await page.waitForLoadState('domcontentloaded')
expect(page.url()).toContain('example.org')
})
})
// ═══════════════════════════════════════════════════════════════════════
// JavaScript Evaluation
// ═══════════════════════════════════════════════════════════════════════
test.describe('Evaluate', () => {
test('evaluate simple expression', async ({ page }) => {
await loadTestPage(page)
const result = await page.evaluate(() => 1 + 1)
expect(result).toBe(2)
})
test('evaluate returns string', async ({ page }) => {
await loadTestPage(page)
const result = await page.evaluate(() => document.title)
expect(result).toContain('Example')
})
test('evaluate returns object', async ({ page }) => {
await loadTestPage(page)
const result = await page.evaluate(() => ({
url: location.href,
title: document.title,
}))
expect(result.url).toContain('example.com')
expect(result.title).toContain('Example')
})
test('evaluate returns null for undefined', async ({ page }) => {
await loadTestPage(page)
const result = await page.evaluate(() => (document as any).nonExistentProp ?? null)
expect(result).toBeNull()
})
test('evaluate accesses DOM', async ({ page }) => {
await loadTestPage(page)
const text = await page.evaluate(() => document.querySelector('h1')?.textContent || '')
expect(text).toContain('Example')
})
})
// ═══════════════════════════════════════════════════════════════════════
// DOM Interaction — Click / Fill / Type
// ═══════════════════════════════════════════════════════════════════════
test.describe('DOM Interaction', () => {
test('click an element', async ({ page }) => {
await loadTestPage(page)
// example.com has a link — click it
const link = page.locator('a').first()
if (await link.count()) {
await expect(link).toBeVisible()
// Just verify click doesn't throw
await link.click().catch(() => {
// External navigation may fail in test, that's OK
})
}
})
test('fill an input field', async ({ page }) => {
// Create a page with an input
await page.setContent(`
<html><body>
<input id="name" type="text" />
<textarea id="bio"></textarea>
<select id="color">
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
</body></html>
`)
// Fill input
await page.fill('#name', 'Hanzo AI')
const inputValue = await page.inputValue('#name')
expect(inputValue).toBe('Hanzo AI')
// Fill textarea
await page.fill('#bio', 'AI infrastructure')
const textareaValue = await page.inputValue('#bio')
expect(textareaValue).toBe('AI infrastructure')
})
test('clear an input field', async ({ page }) => {
await page.setContent('<html><body><input id="x" value="hello" /></body></html>')
await page.fill('#x', '')
const value = await page.inputValue('#x')
expect(value).toBe('')
})
test('select option in dropdown', async ({ page }) => {
await page.setContent(`
<html><body>
<select id="color">
<option value="red">Red</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
</select>
</body></html>
`)
await page.selectOption('#color', 'blue')
const selected = await page.evaluate(() => (document.getElementById('color') as HTMLSelectElement).value)
expect(selected).toBe('blue')
})
test('check and uncheck checkbox', async ({ page }) => {
await page.setContent('<html><body><input id="agree" type="checkbox" /></body></html>')
await page.check('#agree')
expect(await page.isChecked('#agree')).toBe(true)
await page.uncheck('#agree')
expect(await page.isChecked('#agree')).toBe(false)
})
test('type into focused element', async ({ page }) => {
await page.setContent('<html><body><input id="search" type="text" /></body></html>')
await page.click('#search')
await page.keyboard.type('hello world')
const value = await page.inputValue('#search')
expect(value).toBe('hello world')
})
test('press key', async ({ page }) => {
await page.setContent('<html><body><input id="k" type="text" /><input id="k2" type="text" /></body></html>')
await page.click('#k')
await page.keyboard.press('Tab')
// Tab should move focus to second input
const focused = await page.evaluate(() => document.activeElement?.id)
expect(focused).toBe('k2')
})
test('hover triggers mouseenter', async ({ page }) => {
await page.setContent(`
<html><body>
<div id="target" style="width:100px;height:100px;background:red">hover me</div>
<div id="result"></div>
<script>
document.getElementById('target').addEventListener('mouseenter', () => {
document.getElementById('result').textContent = 'hovered';
});
</script>
</body></html>
`)
await page.hover('#target')
await expect(page.locator('#result')).toHaveText('hovered')
})
test('double click', async ({ page }) => {
await page.setContent(`
<html><body>
<div id="target" style="width:100px;height:100px">dblclick me</div>
<div id="result"></div>
<script>
document.getElementById('target').addEventListener('dblclick', () => {
document.getElementById('result').textContent = 'double-clicked';
});
</script>
</body></html>
`)
await page.dblclick('#target')
await expect(page.locator('#result')).toHaveText('double-clicked')
})
})
// ═══════════════════════════════════════════════════════════════════════
// DOM Read — getText, getHTML, getAttribute, querySelector
// ═══════════════════════════════════════════════════════════════════════
test.describe('DOM Read', () => {
test('get text content', async ({ page }) => {
await loadTestPage(page)
const text = await page.locator('h1').textContent()
expect(text).toContain('Example')
})
test('get inner HTML', async ({ page }) => {
await page.setContent('<html><body><div id="box"><span>hi</span></div></body></html>')
const html = await page.locator('#box').innerHTML()
expect(html).toContain('<span>hi</span>')
})
test('get outer HTML', async ({ page }) => {
await page.setContent('<html><body><div id="box"><span>hi</span></div></body></html>')
const html = await page.evaluate(() => document.getElementById('box')?.outerHTML)
expect(html).toContain('<div id="box">')
expect(html).toContain('<span>hi</span>')
})
test('get attribute', async ({ page }) => {
await page.setContent('<html><body><a id="link" href="https://hanzo.ai" data-test="yes">click</a></body></html>')
const href = await page.getAttribute('#link', 'href')
expect(href).toBe('https://hanzo.ai')
const data = await page.getAttribute('#link', 'data-test')
expect(data).toBe('yes')
})
test('querySelector returns element info', async ({ page }) => {
await page.setContent('<html><body><button id="btn" class="primary" disabled>Submit</button></body></html>')
const info = await page.evaluate(() => {
const el = document.querySelector('#btn') as HTMLButtonElement
return el ? { tagName: el.tagName, id: el.id, className: el.className, disabled: el.disabled, text: el.textContent } : null
})
expect(info).toEqual({ tagName: 'BUTTON', id: 'btn', className: 'primary', disabled: true, text: 'Submit' })
})
test('querySelectorAll returns multiple elements', async ({ page }) => {
await page.setContent('<html><body><ul><li>A</li><li>B</li><li>C</li></ul></body></html>')
const count = await page.locator('li').count()
expect(count).toBe(3)
const texts = await page.locator('li').allTextContents()
expect(texts).toEqual(['A', 'B', 'C'])
})
test('get bounding rect', async ({ page }) => {
await page.setContent('<html><body><div id="box" style="width:200px;height:100px;margin:10px">box</div></body></html>')
const rect = await page.locator('#box').boundingBox()
expect(rect).not.toBeNull()
expect(rect!.width).toBeCloseTo(200, 0)
expect(rect!.height).toBeCloseTo(100, 0)
})
test('get computed style', async ({ page }) => {
await page.setContent('<html><body><div id="styled" style="color: rgb(255, 0, 0); font-size: 20px">red</div></body></html>')
const color = await page.evaluate(() => getComputedStyle(document.getElementById('styled')!).color)
expect(color).toBe('rgb(255, 0, 0)')
})
test('element visibility checks', async ({ page }) => {
await page.setContent(`
<html><body>
<div id="visible">I am visible</div>
<div id="hidden" style="display:none">I am hidden</div>
</body></html>
`)
expect(await page.isVisible('#visible')).toBe(true)
expect(await page.isVisible('#hidden')).toBe(false)
expect(await page.isHidden('#hidden')).toBe(true)
})
})
// ═══════════════════════════════════════════════════════════════════════
// DOM Write — set attributes, styles, classes, innerHTML
// ═══════════════════════════════════════════════════════════════════════
test.describe('DOM Write', () => {
test('set attribute', async ({ page }) => {
await page.setContent('<html><body><div id="el">hello</div></body></html>')
await page.evaluate(() => document.getElementById('el')!.setAttribute('data-custom', 'value'))
const attr = await page.getAttribute('#el', 'data-custom')
expect(attr).toBe('value')
})
test('remove attribute', async ({ page }) => {
await page.setContent('<html><body><div id="el" data-remove="yes">hello</div></body></html>')
await page.evaluate(() => document.getElementById('el')!.removeAttribute('data-remove'))
const attr = await page.getAttribute('#el', 'data-remove')
expect(attr).toBeNull()
})
test('set innerHTML', async ({ page }) => {
await page.setContent('<html><body><div id="container"></div></body></html>')
await page.evaluate(() => {
document.getElementById('container')!.innerHTML = '<p>injected</p>'
})
await expect(page.locator('#container p')).toHaveText('injected')
})
test('add and remove CSS class', async ({ page }) => {
await page.setContent('<html><body><div id="el" class="base">hello</div></body></html>')
await page.evaluate(() => document.getElementById('el')!.classList.add('active', 'highlighted'))
let cls = await page.getAttribute('#el', 'class')
expect(cls).toContain('active')
expect(cls).toContain('highlighted')
await page.evaluate(() => document.getElementById('el')!.classList.remove('highlighted'))
cls = await page.getAttribute('#el', 'class')
expect(cls).not.toContain('highlighted')
expect(cls).toContain('active')
})
test('set inline style', async ({ page }) => {
await page.setContent('<html><body><div id="el">hello</div></body></html>')
await page.evaluate(() => {
const el = document.getElementById('el')!
el.style.backgroundColor = 'red'
el.style.fontSize = '24px'
})
const bg = await page.evaluate(() => document.getElementById('el')!.style.backgroundColor)
expect(bg).toBe('red')
})
test('insert element', async ({ page }) => {
await page.setContent('<html><body><ul id="list"><li>first</li></ul></body></html>')
await page.evaluate(() => {
document.getElementById('list')!.insertAdjacentHTML('beforeend', '<li>second</li>')
})
const items = await page.locator('#list li').allTextContents()
expect(items).toEqual(['first', 'second'])
})
test('remove element', async ({ page }) => {
await page.setContent('<html><body><div id="parent"><span id="child">bye</span></div></body></html>')
await page.evaluate(() => document.getElementById('child')!.remove())
await expect(page.locator('#child')).toHaveCount(0)
})
})
// ═══════════════════════════════════════════════════════════════════════
// Screenshots
// ═══════════════════════════════════════════════════════════════════════
test.describe('Screenshots', () => {
test('capture screenshot as buffer', async ({ page }) => {
await loadTestPage(page)
const screenshot = await page.screenshot({ type: 'png' })
expect(screenshot).toBeInstanceOf(Buffer)
expect(screenshot.length).toBeGreaterThan(1000)
})
test('capture full page screenshot', async ({ page }) => {
await loadTestPage(page)
const screenshot = await page.screenshot({ type: 'png', fullPage: true })
expect(screenshot.length).toBeGreaterThan(1000)
})
})
// ═══════════════════════════════════════════════════════════════════════
// Wait / Selectors
// ═══════════════════════════════════════════════════════════════════════
test.describe('Waiting', () => {
test('wait for selector', async ({ page }) => {
await page.setContent(`
<html><body>
<script>setTimeout(() => { document.body.innerHTML += '<div id="delayed">loaded</div>'; }, 500);</script>
</body></html>
`)
await page.waitForSelector('#delayed', { timeout: 5000 })
await expect(page.locator('#delayed')).toHaveText('loaded')
})
test('wait for load state', async ({ page }) => {
await page.goto(TEST_URL)
await page.waitForLoadState('domcontentloaded')
const title = await page.title()
expect(title).toBeTruthy()
})
})
// ═══════════════════════════════════════════════════════════════════════
// Storage — localStorage, cookies
// ═══════════════════════════════════════════════════════════════════════
test.describe('Storage', () => {
test('get and set localStorage', async ({ page }) => {
await loadTestPage(page)
await page.evaluate(() => localStorage.setItem('test_key', 'test_value'))
const value = await page.evaluate(() => localStorage.getItem('test_key'))
expect(value).toBe('test_value')
})
test('clear localStorage', async ({ page }) => {
await loadTestPage(page)
await page.evaluate(() => {
localStorage.setItem('a', '1')
localStorage.setItem('b', '2')
localStorage.clear()
})
const len = await page.evaluate(() => localStorage.length)
expect(len).toBe(0)
})
test('get cookies', async ({ page, context }) => {
await loadTestPage(page)
await context.addCookies([{ name: 'test_cookie', value: 'yum', url: TEST_URL }])
const cookies = await context.cookies(TEST_URL)
const found = cookies.find((c) => c.name === 'test_cookie')
expect(found).toBeTruthy()
expect(found!.value).toBe('yum')
})
})
// ═══════════════════════════════════════════════════════════════════════
// Page Content
// ═══════════════════════════════════════════════════════════════════════
test.describe('Content', () => {
test('get full page HTML', async ({ page }) => {
await loadTestPage(page)
const html = await page.content()
expect(html).toContain('<html')
expect(html).toContain('Example')
expect(html.length).toBeGreaterThan(100)
})
test('inject CSS', async ({ page }) => {
await page.setContent('<html><body><div id="el">hello</div></body></html>')
await page.evaluate(() => {
const style = document.createElement('style')
style.textContent = '#el { color: rgb(0, 128, 0); }'
document.head.appendChild(style)
})
const color = await page.evaluate(() => getComputedStyle(document.getElementById('el')!).color)
expect(color).toBe('rgb(0, 128, 0)')
})
test('inject script', async ({ page }) => {
await page.setContent('<html><body><div id="result"></div></body></html>')
await page.evaluate(() => {
(window as any).__injected = true
})
const val = await page.evaluate(() => (window as any).__injected)
expect(val).toBe(true)
})
})
@@ -0,0 +1,80 @@
import { defineConfig, devices } from '@playwright/test'
import path from 'path'
const chromeExtPath = path.resolve(__dirname, '../dist/browser-extension/chrome')
/**
* Cross-platform E2E test config.
*
* Tests core browser tool functionality (DOM interaction, navigation,
* screenshots, evaluate, fill, click) across Chrome, Firefox, and WebKit
* to ensure 100% parity.
*
* The parity tests validate raw browser capabilities that the extension
* must replicate — they run on plain browsers, NOT with extensions loaded.
* Extension-specific tests (popup, sidebar, CDP bridge) live in the
* default playwright.config.ts and only run on Chrome (Playwright
* limitation: can't load extensions in Firefox/WebKit).
*
* Run all: npx playwright test --config=e2e/cross-platform.config.ts
* Run one: npx playwright test --config=e2e/cross-platform.config.ts --project=firefox
*/
export default defineConfig({
testDir: '.',
testMatch: ['browser-tool-parity.spec.ts', 'cross-platform-*.spec.ts'],
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 2 : 4,
reporter: process.env.CI
? [['html', { open: 'never' }], ['github'], ['json', { outputFile: 'cross-platform-results.json' }]]
: 'html',
timeout: 60_000,
expect: {
timeout: 10_000,
},
use: {
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'on-first-retry',
},
projects: [
// ── Desktop browsers ──
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// ── Chrome with extension loaded (extension-specific tests) ──
{
name: 'chrome-extension',
testMatch: ['popup.spec.ts', 'sidebar.spec.ts', 'auth-chat.spec.ts'],
use: {
launchOptions: {
args: [
`--disable-extensions-except=${chromeExtPath}`,
`--load-extension=${chromeExtPath}`,
'--no-first-run',
'--disable-default-apps',
],
},
},
},
// ── Mobile ──
{
name: 'mobile-chrome',
use: { ...devices['Pixel 7'] },
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 14'] },
},
],
})
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -592,7 +592,7 @@ class CDPBridgeServer {
this.pendingExtensionRequests.set(id, { resolve, timeout });
client.send(JSON.stringify({
client.ws.send(JSON.stringify({
type: 'extension-request',
id,
action,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Hanzo AI",
"version": "1.7.23",
"version": "1.7.30",
"description": "AI-powered dev assistant — click-to-code navigation, source maps, WebGPU AI, and MCP integration for Claude Code.",
"permissions": [
"activeTab",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Hanzo AI",
"version": "1.7.23",
"version": "1.7.30",
"description": "AI-powered dev assistant — click-to-code navigation, source maps, WebGPU AI, and MCP integration for Claude Code.",
"permissions": [
"activeTab",