import assert from 'node:assert/strict' import { createRequire } from 'node:module' import { test } from 'node:test' import * as page from '../extension/page-agent.js' import { CAPABILITIES, checkUrl, createController, parseKey, ToolError } from '../extension/controller.js' const { JSDOM } = createRequire(import.meta.url)('jsdom') const GLOBALS = ['window', 'document', 'location', 'getComputedStyle', 'HTMLInputElement', 'HTMLTextAreaElement', 'Event', 'innerHeight', 'innerWidth'] function pageFixture(t, html) { const dom = new JSDOM(`Sipariş formu${html}`, { url: 'https://shop.example.test/checkout?session=gizli' }) const { window } = dom window.HTMLElement.prototype.getBoundingClientRect = function () { if (this.dataset.size === '0') return { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 } return { left: 10, top: 20, right: 110, bottom: 40, width: 100, height: 20 } } window.HTMLElement.prototype.scrollIntoView = () => {} window.document.elementFromPoint = () => null window.scrollBy = () => {} for (const key of GLOBALS) globalThis[key] = key === 'innerHeight' ? 800 : key === 'innerWidth' ? 1200 : window[key] t.after(() => { delete globalThis.__aiturkRefs; window.close(); for (const key of GLOBALS) delete globalThis[key] }) return window } const FORM = `

Ödeme

Belgeler

Toplam tutar: 120 TL

Ürün` test('snapshot lists visible controls with refs, masks passwords and skips hidden parts', t => { pageFixture(t, FORM) const result = page.snapshotPage({ full: false }) const lines = result.snapshot.split('\n') assert.equal(lines[0], 'Sayfa: Sipariş formu') assert.ok(lines.includes('- heading "Ödeme" [level=1]')) assert.ok(lines.includes('- link "Belgeler" [ref=e1]')) assert.ok(lines.includes('- textbox "E-posta" [ref=e2] (değer: "ali@example.test")')) const password = lines.find(line => line.includes('"Şifre"')) assert.ok(password.includes('(değer: "••••")') && password.includes('şifre alanı: ajan yazamaz')) assert.ok(!result.snapshot.includes('cok-gizli')) assert.ok(!result.snapshot.includes('Gizli menü')) assert.ok(!result.snapshot.includes('Görünmez') && !result.snapshot.includes('Sıfır boyut')) assert.ok(lines.some(line => line.startsWith('- combobox "Şehir"') && line.includes('(değer: "İzmir")'))) assert.ok(lines.some(line => line.startsWith('- checkbox "Koşulları kabul ediyorum"') && line.includes('[checked]'))) assert.ok(!result.snapshot.includes('Toplam tutar'), 'compact view leaves plain text out') assert.equal(result.element_count, 7) assert.equal(globalThis.__aiturkRefs.size, 7) const full = page.snapshotPage({ full: true }) assert.ok(full.snapshot.includes('- text: Toplam tutar: 120 TL')) assert.ok(full.snapshot.includes('→ https://shop.example.test/docs')) assert.ok(full.snapshot.includes('- option "Ankara"')) const short = page.snapshotPage({ limit: 2000, full: true }) assert.ok(short.snapshot.length <= 2300) }) test('a long page is cut with an explicit note instead of silently', t => { pageFixture(t, Array.from({ length: 400 }, (_, index) => ``).join('')) const result = page.snapshotPage({ limit: 2000 }) assert.equal(result.truncated, true) assert.match(result.snapshot, /satır daha var; kısaltıldı/) }) test('refs resolve to coordinates; stale refs fail with a recoverable message', t => { const window = pageFixture(t, FORM) page.snapshotPage({}) const located = page.locateRef('@e2') assert.deepEqual([located.x, located.y, located.tag, located.password], [60, 30, 'input', false]) window.document.getElementById('mail').remove() assert.match(page.locateRef('@e2').error, /browser_snapshot/) assert.match(page.locateRef('e99').error, /bulunamadı/) }) test('typing refuses password and card fields, focuses text fields and fires framework events', t => { const window = pageFixture(t, FORM) page.snapshotPage({}) const refs = [...globalThis.__aiturkRefs.keys()] const refOf = predicate => `@${refs.find(ref => predicate(globalThis.__aiturkRefs.get(ref).deref()))}` const passwordRef = refOf(element => element.type === 'password') const cardRef = refOf(element => element.getAttribute('autocomplete') === 'cc-number') assert.equal(page.prepareTyping(passwordRef).handoff, true) assert.match(page.prepareTyping(cardRef).error, /Kart/) assert.match(page.prepareTyping('@e1').error, /yazı alanı değil/) const mail = window.document.getElementById('mail') let inputs = 0 mail.addEventListener('input', () => inputs++) assert.equal(page.prepareTyping('@e2').focused, true) assert.deepEqual(page.typeWithEvents('@e2', 'veli@example.test'), { value: 'veli@example.test' }) assert.equal(inputs, 1) assert.deepEqual(page.readValue('@e2'), { value: 'veli@example.test' }) const selectRef = refOf(element => element.tagName === 'SELECT') assert.deepEqual(page.prepareTyping(selectRef), { select: true }) assert.deepEqual(page.typeWithEvents(selectRef, 'ankara'), { value: 'Ankara' }) assert.match(page.typeWithEvents(selectRef, 'Paris').error, /seçeneği yok/) }) test('images are listed with data URLs shortened; annotation labels come and go', t => { const window = pageFixture(t, FORM) const images = page.listImages() assert.equal(images.count, 1) assert.ok(images.images[0].src.length < 70 && images.images[0].alt === 'Ürün') page.snapshotPage({}) const labels = page.annotateRefs(true) assert.equal(labels.annotations.length, 7) assert.ok(window.document.getElementById('__aiturk_labels')) page.annotateRefs(false) assert.equal(window.document.getElementById('__aiturk_labels'), null) }) test('keys and addresses are validated before anything touches the page', () => { assert.deepEqual(parseKey('Enter'), { key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, text: '\r', modifiers: 0 }) assert.deepEqual(parseKey('ctrl+a'), { key: 'a', code: 'KeyA', windowsVirtualKeyCode: 65, text: undefined, modifiers: 2 }) assert.equal(parseKey('Shift+Tab').modifiers, 8) assert.equal(parseKey('esc').key, 'Escape') assert.throws(() => parseKey('Hyper+x'), ToolError) assert.throws(() => parseKey('NotAKey'), ToolError) assert.equal(checkUrl('example.test/a?b=1'), 'https://example.test/a?b=1') assert.equal(checkUrl('http://localhost:5173/'), 'http://localhost:5173/') for (const blocked of ['chrome://settings', 'file:///C:/Windows/win.ini', 'javascript:alert(1)', 'chrome-extension://abc/panel.html', 'https://chromewebstore.google.com/detail/x', 'https://user:pass@example.test/']) { assert.throws(() => checkUrl(blocked), ToolError, blocked) } }) // ── controller with a fake Chrome ────────────────────────────────────── const hub = () => ({ listeners: new Set(), addListener(fn) { this.listeners.add(fn) }, removeListener(fn) { this.listeners.delete(fn) }, emit(...args) { for (const fn of [...this.listeners]) fn(...args) } }) const pick = (store, keys) => Object.fromEntries([keys].flat().filter(key => key in store).map(key => [key, store[key]])) const until = async (condition, label) => { for (let index = 0; index < 400; index++) { if (condition()) return; await new Promise(resolve => setTimeout(resolve, 5)) } assert.fail(`zaman aşımı: ${label}`) } function fakeChrome({ pageScript = {}, tabs: initialTabs = [] } = {}) { const local = {}, session = {}, native = [], debuggerCalls = [], notifications = [], groups = new Map() const tabs = new Map(initialTabs.map(tab => [tab.id, { status: 'complete', windowId: 1, active: true, ...tab }])) let nextTab = 100, nextGroup = 1, port = null const onUpdated = hub() const respond = message => { const result = message.action === 'status' ? { profile: 'default', local: true } : message.action === 'browser.register' ? { registered: message.params.enabled } : message.action === 'sessions.list' ? { sessions: [{ id: 'durable-1', title: 'Rapor işi' }] } : { accepted: true } port.onMessage.emit({ id: message.id, result }) } const chrome = { runtime: { connectNative: () => { port = { onMessage: hub(), onDisconnect: hub(), disconnected: false, disconnect() { this.disconnected = true }, postMessage: message => { native.push(message); queueMicrotask(() => respond(message)) } } return port }, sendMessage: async () => {}, }, storage: { local: { get: async keys => pick(local, keys), set: async value => Object.assign(local, value) }, session: { get: async keys => structuredClone(pick(session, keys)), set: async value => Object.assign(session, structuredClone(value)) }, }, windows: { getLastFocused: async () => ({ id: 1 }) }, tabs: { onUpdated, create: async ({ url, active, windowId }) => { const tab = { id: nextTab++, url, active, windowId, status: 'complete', title: '' } tabs.set(tab.id, tab) return { ...tab } }, get: async id => { if (!tabs.has(id)) throw new Error(`No tab with id: ${id}`); return { ...tabs.get(id) } }, update: async (id, { url }) => { const tab = tabs.get(id) Object.assign(tab, { pendingUrl: url, status: 'loading' }) setTimeout(() => { Object.assign(tab, { url, pendingUrl: undefined, status: 'complete', title: 'Açılan sayfa' }); onUpdated.emit(id, { status: 'complete' }) }, 20) return { ...tab } }, goBack: async () => {}, query: async () => [...tabs.values()].filter(tab => tab.active).map(tab => ({ ...tab })), group: async ({ tabIds, groupId }) => { const id = groupId ?? nextGroup++; groups.set(id, { windowId: tabs.get(tabIds[0]).windowId }); return id }, captureVisibleTab: async () => 'data:image/jpeg;base64,QUJD', }, tabGroups: { get: async id => ({ id, ...groups.get(id) }), update: async (id, value) => Object.assign(groups.get(id), value) }, scripting: { executeScript: async ({ target, func, args }) => { const handler = pageScript[func.name] if (!handler) throw new Error(`script not expected: ${func.name}`) return [{ result: await handler(...args, target.tabId) }] }, }, debugger: { attach: async () => {}, detach: async () => {}, sendCommand: async (target, method, params) => { debuggerCalls.push([method, params]); return method === 'Page.captureScreenshot' ? { data: 'QUJD' } : {} }, }, notifications: { create: async (...args) => { notifications.push(args) } }, alarms: { create() {}, clear() {} }, action: { setBadgeText: async () => {} }, } return { chrome, native, debuggerCalls, notifications, tabs, groups, local, session, get port() { return port } } } const SNAPSHOT = { snapshot: 'Sayfa: Açılan sayfa\n- button "Gönder" [ref=e1]', element_count: 1, url: 'https://example.test/', title: 'Açılan sayfa' } async function connected(fake) { const controller = createController({ chrome: fake.chrome, random: () => 'instance-1' }) await controller.connect() await until(() => controller._state().registered, 'registration') return controller } function command(fake, action, args, commandId = 'ab'.repeat(16)) { fake.port.onMessage.emit({ event: 'browser.command', commandId, action, arguments: args, sessionId: 'durable-1' }) } const resultFor = (fake, commandId = 'ab'.repeat(16)) => fake.native.find(message => message.action === 'browser.result' && message.params.commandId === commandId) test('controller registers this Chrome for the IDE agent with a stable identity', async () => { const fake = fakeChrome() await connected(fake) const register = fake.native.find(message => message.action === 'browser.register') assert.deepEqual(register.params, { enabled: true, capabilities: [...CAPABILITIES], controllerId: 'aiturk-chrome-instance-1', browserProfileId: 'instance-1' }) assert.equal(fake.local.controllerInstance, 'instance-1') }) test('navigate opens the agent its own background tab in an AITURK group and returns a snapshot', async () => { const fake = fakeChrome({ pageScript: { snapshotPage: () => SNAPSHOT } }) await connected(fake) command(fake, 'browser_navigate', { url: 'example.test' }) await until(() => resultFor(fake), 'navigate result') const { params } = resultFor(fake) assert.equal(params.ok, true) assert.equal(params.result.success, true) assert.equal(params.result.snapshot, SNAPSHOT.snapshot) const tab = fake.tabs.get(100) assert.equal(tab.active, false) assert.equal(tab.url, 'https://example.test/') assert.equal(fake.groups.get(1).title, 'AITURK · Rapor işi') assert.equal(fake.groups.get(1).color, 'cyan') }) test('blocked pages and missing tabs come back as tool errors the agent can read', async () => { const fake = fakeChrome() await connected(fake) command(fake, 'browser_navigate', { url: 'chrome://settings/passwords' }, '01'.repeat(16)) command(fake, 'browser_click', { ref: '@e1' }, '02'.repeat(16)) await until(() => resultFor(fake, '01'.repeat(16)) && resultFor(fake, '02'.repeat(16)), 'refusals') assert.deepEqual(resultFor(fake, '01'.repeat(16)).params.result.success, false) assert.match(resultFor(fake, '01'.repeat(16)).params.result.error, /çalışamaz/) assert.match(resultFor(fake, '02'.repeat(16)).params.result.error, /browser_navigate/) assert.equal(fake.tabs.size, 0, 'a refused navigation never opens a tab') }) test('click is a real mouse click at the element; a covering element is reported', async () => { const fake = fakeChrome({ tabs: [{ id: 7, url: 'https://example.test/', title: 'Örnek' }], pageScript: { locateRef: () => ({ x: 60, y: 30, name: 'Gönder', covered: 'Çerez bildirimi' }) } }) const controller = await connected(fake) await controller.shareTab(await fake.chrome.tabs.get(7), 'durable-1') command(fake, 'browser_click', { ref: '@e1' }) await until(() => resultFor(fake), 'click result') const { result } = resultFor(fake).params assert.equal(result.success, true) assert.match(result.warning, /Çerez bildirimi/) const mouse = fake.debuggerCalls.filter(([method]) => method === 'Input.dispatchMouseEvent').map(([, params]) => [params.type, params.x, params.y]) assert.deepEqual(mouse, [['mousePressed', 60, 30], ['mouseReleased', 60, 30]]) }) test('password fields are handed to the user: nothing is typed and a notification is shown', async () => { const fake = fakeChrome({ tabs: [{ id: 7, url: 'https://example.test/login' }], pageScript: { prepareTyping: () => ({ error: 'Şifre alanına ajan yazamaz. Bu adımı kullanıcıya bırakın.', handoff: true }) } }) const controller = await connected(fake) await controller.shareTab(await fake.chrome.tabs.get(7), 'durable-1') command(fake, 'browser_type', { ref: '@e2', text: 'parola' }) await until(() => resultFor(fake), 'type result') assert.equal(resultFor(fake).params.result.success, false) assert.ok(!fake.debuggerCalls.some(([method]) => method === 'Input.insertText')) assert.equal(fake.notifications.length, 1) }) test('typing inserts trusted text and reports what the field now holds', async () => { const fake = fakeChrome({ tabs: [{ id: 7, url: 'https://example.test/' }], pageScript: { prepareTyping: () => ({ focused: true }), readValue: () => ({ value: 'İstanbul' }) } }) const controller = await connected(fake) await controller.shareTab(await fake.chrome.tabs.get(7), 'durable-1') command(fake, 'browser_type', { ref: '@e3', text: 'İstanbul' }) await until(() => resultFor(fake), 'type result') assert.deepEqual(resultFor(fake).params.result, { success: true, typed: '@e3', value: 'İstanbul' }) assert.deepEqual(fake.debuggerCalls.find(([method]) => method === 'Input.insertText')[1], { text: 'İstanbul' }) }) test('vision returns a JPEG capture of the agent tab', async () => { const fake = fakeChrome({ tabs: [{ id: 7, url: 'https://example.test/', title: 'Örnek' }] }) const controller = await connected(fake) await controller.shareTab(await fake.chrome.tabs.get(7), 'durable-1') command(fake, 'browser_vision', { question: 'Ne var?' }) await until(() => resultFor(fake), 'vision result') assert.deepEqual(resultFor(fake).params.result, { success: true, screenshot: 'QUJD', mime: 'image/jpeg', url: 'https://example.test/', title: 'Örnek' }) }) test('a cancelled command never answers late; stopping turns control off at the IDE', async () => { let release const fake = fakeChrome({ tabs: [{ id: 7, url: 'https://example.test/' }], pageScript: { snapshotPage: () => new Promise(resolve => { release = () => resolve(SNAPSHOT) }) } }) const controller = await connected(fake) await controller.shareTab(await fake.chrome.tabs.get(7), 'durable-1') command(fake, 'browser_snapshot', {}) await until(() => release, 'snapshot started') fake.port.onMessage.emit({ event: 'browser.cancel', commandId: 'ab'.repeat(16) }) release() await new Promise(resolve => setTimeout(resolve, 50)) assert.equal(resultFor(fake), undefined) const port = fake.port const state = await controller.onMessage({ type: 'control.stop' }) assert.equal(state.enabled, false) assert.deepEqual(fake.native.filter(message => message.action === 'browser.register').at(-1).params, { enabled: false }) assert.equal(port.disconnected, true) assert.equal(fake.local.controlEnabled, false) }) test('cancelling Chrome’s debugging bar is treated as the user stopping the agent', async () => { const fake = fakeChrome({ tabs: [{ id: 7, url: 'https://example.test/' }], pageScript: { locateRef: () => ({ x: 1, y: 1, name: 'x' }) } }) const controller = await connected(fake) await controller.shareTab(await fake.chrome.tabs.get(7), 'durable-1') command(fake, 'browser_click', { ref: '@e1' }) await until(() => resultFor(fake), 'click result') controller.onDebuggerDetach({ tabId: 7 }, 'canceled_by_user') await until(() => fake.local.controlEnabled === false, 'disabled') assert.equal(fake.notifications.length, 1) })