Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions scripts/fixtures/oauth-ipc.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Loaded only by oauth-link-smoke.mjs; no real account, clipboard or OAuth request.
const responses = { get_accounts: [], get_current_account_id: null, get_settings: {}, check_codex_login: false };
const state = window.__compat = { calls: [], denied: [], clipboard: null, copyFailure: false, loginDelay: 0,
url: 'https://example.invalid/authorize?response_type=code&state=unchanged&code_challenge=' + 'a'.repeat(1800) + '&extra=%25%26%22%3C' };
export async function invoke(command, args = {}) {
state.calls.push({ command, args });
if (command === 'start_oauth_login' || command === 'start_antigravity_oauth_login') {
await new Promise(resolve => setTimeout(resolve, state.loginDelay));
return state.url;
}
if (command === 'copy_to_clipboard') {
if (state.copyFailure) throw 'Mock clipboard unavailable';
state.clipboard = args.text;
return null;
}
if (Object.hasOwn(responses, command)) return structuredClone(responses[command]);
state.denied.push(command);
throw new Error('MOCK_DENIED: ' + command);
}
const handlers = new Map();
export async function listen(name, callback) {
const callbacks = handlers.get(name) ?? new Set();
callbacks.add(callback); handlers.set(name, callbacks);
return () => callbacks.delete(callback);
}
export async function emit(name, payload) { for (const callback of handlers.get(name) ?? []) callback({ payload }); }
export const open = async () => { throw new Error('Fixture native dialog denied'); };
export const readFile = async () => { throw new Error('Fixture filesystem access denied'); };
3 changes: 3 additions & 0 deletions scripts/fixtures/oauth-link.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<!doctype html>
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"></head>
<body><div id="root"></div><script type="module" src="/scripts/fixtures/oauth-link.tsx"></script></body></html>
11 changes: 11 additions & 0 deletions scripts/fixtures/oauth-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';
import { AddAccountModal } from '../../src/components/AddAccountModal';
import '../../src/App.css';

function Fixture() {
const [open, setOpen] = useState(true);
return <><button id="fixture-open" onClick={() => setOpen(true)}>Open fixture</button>
<AddAccountModal isOpen={open} onClose={() => setOpen(false)} onAdd={async () => {}} /></>;
}
ReactDOM.createRoot(document.getElementById('root')!).render(<Fixture />);
144 changes: 144 additions & 0 deletions scripts/oauth-link-smoke.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import fs from 'node:fs';
import path from 'node:path';
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { once } from 'node:events';
import { setTimeout as delay } from 'node:timers/promises';
import { createServer } from 'vite';

const browser = process.env.BROWSER_PATH || [
'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
'C:/Program Files/Google/Chrome/Application/chrome.exe',
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/usr/bin/chromium', '/usr/bin/google-chrome',
].find(file => fs.existsSync(file));
assert.ok(browser, 'Set BROWSER_PATH to a Chromium-compatible browser');
const output = path.resolve('.oauth-smoke.local');
fs.mkdirSync(output, { recursive: true });
const profile = fs.mkdtempSync(path.join(output, 'profile-'));
const fixture = path.resolve('scripts/fixtures/oauth-ipc.mjs');
const server = await createServer({
logLevel: 'error', optimizeDeps: { entries: ['scripts/fixtures/oauth-link.html'] },
server: { host: '127.0.0.1', port: 0, strictPort: false, open: false, watch: { ignored: ['**/*.local/**'] } },
plugins: [{ name: 'oauth-link-fixture', enforce: 'pre', resolveId(id) { return id.startsWith('@tauri-apps/') ? fixture : undefined; } }],
});
let child;
let socket;
let sequence = 0;
const pending = new Map();
const errors = [];
const scenes = [];
try {
await server.listen();
const base = server.resolvedUrls.local[0] + 'scripts/fixtures/oauth-link.html';
child = spawn(browser, ['--headless=new', '--disable-gpu', '--no-first-run', '--no-default-browser-check',
'--disable-extensions', '--disable-background-networking', '--remote-debugging-port=0',
`--user-data-dir=${profile}`, 'about:blank'], { stdio: ['ignore', 'ignore', 'pipe'] });
child.on('error', error => errors.push(String(error)));
child.stderr.on('data', () => {});
const portFile = path.join(profile, 'DevToolsActivePort');
for (let i = 0; !fs.existsSync(portFile) && i < 100; i++) await delay(100);
assert.ok(fs.existsSync(portFile), 'Isolated browser did not start');
const port = fs.readFileSync(portFile, 'utf8').split('\n')[0];
const targets = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json();
socket = new WebSocket(targets.find(target => target.type === 'page').webSocketDebuggerUrl);
await once(socket, 'open');
socket.addEventListener('message', event => {
const data = JSON.parse(event.data);
if (data.id) {
const task = pending.get(data.id); pending.delete(data.id);
if (data.error) task?.reject(new Error(JSON.stringify(data.error)));
else task?.resolve(data.result);
} else if (data.method === 'Runtime.exceptionThrown') errors.push(data.params.exceptionDetails.exception?.description ?? data.params.exceptionDetails.text);
});
function call(method, params = {}) {
return new Promise((resolve, reject) => {
const id = ++sequence;
const timer = setTimeout(() => { pending.delete(id); reject(new Error(`CDP timeout: ${method}`)); }, 20000);
pending.set(id, { resolve: value => { clearTimeout(timer); resolve(value); }, reject: error => { clearTimeout(timer); reject(error); } });
socket.send(JSON.stringify({ id, method, params }));
});
}
async function evaluate(expression) {
const result = await call('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
if (result.exceptionDetails) throw new Error(result.exceptionDetails.exception?.description ?? result.exceptionDetails.text);
return result.result.value;
}
async function waitFor(expression) {
for (let i = 0; i < 100; i++) { if (await evaluate(expression)) return; await delay(100); }
throw new Error(`Condition not met: ${expression}`);
}
async function click(selector) {
await evaluate(`(() => {const node=document.querySelector(${JSON.stringify(selector)});if(!node)throw Error('Missing '+${JSON.stringify(selector)});node.click();})()`);
}
async function capture(name) {
await delay(150);
const state = await evaluate(`({ denied: window.__compat.denied, text: document.body.innerText,
overflow: ['html','.modal-content','.modal-body','.oauth-content','.oauth-status','.oauth-link-panel'].filter(selector=>{
const e=document.querySelector(selector);return e && e.clientWidth>0 && e.scrollWidth>e.clientWidth+2;
}) })`);
assert.deepEqual(state.denied, [], `${name}: unmocked operation`);
assert.deepEqual(state.overflow, [], `${name}: horizontal overflow`);
const image = await call('Page.captureScreenshot', { format: 'png' });
fs.writeFileSync(path.join(output, name + '.png'), Buffer.from(image.data, 'base64'));
scenes.push(name);
}
async function navigate(query = '') {
await call('Page.navigate', { url: base + query });
await waitFor('!!window.__compat && !!document.querySelector("#root > *")');
}
async function metrics(width, height, scale = 1) {
await call('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: scale, mobile: false });
}
async function prepareCopy(failure) {
await evaluate(`window.__compat.copyFailure=${failure}`);
await click('.oauth-content > button[title]');
await waitFor('!!document.querySelector(".oauth-link-value") && !document.querySelector(".oauth-link-panel button").disabled');
assert.equal(await evaluate('document.querySelector(".oauth-link-value").value === window.__compat.url'), true);
}
await call('Runtime.enable'); await call('Page.enable'); await call('Network.enable');
await call('Network.setBlockedURLs', { urls: ['https://*', 'http://*.invalid/*', 'http://*.com/*', 'http://*.cn/*'] });
await call('Emulation.setUserAgentOverride', { userAgent: 'Mozilla/5.0 Windows compatibility fixture', platform: 'Win32' });
await metrics(1200, 760);
await navigate();
await waitFor('!!document.querySelector(".oauth-content > button[title]")');
await prepareCopy(true);
await capture('oauth-copy-failure');
await evaluate('window.__compat.copyFailure=false');
await click('.oauth-link-panel button');
await waitFor('window.__compat.clipboard === window.__compat.url');
assert.equal(await evaluate('window.__compat.calls.filter(c=>c.command==="start_oauth_login").length'), 1, 'Copy retry must not rotate PKCE/state');
await capture('oauth-copy-retry');
await click('.oauth-content > button:last-child');
await waitFor('!!document.querySelector(".oauth-content textarea.text-input")');
assert.equal(await evaluate('document.querySelector(".oauth-content textarea.text-input").value'), '', 'Authorization URL must not become callback input');
for (const [width, height, scale] of [[960, 640, 1.25], [800, 600, 1.5], [600, 500, 2]]) {
await metrics(width, height, scale);
await capture(`oauth-layout-${scale}`);
}
await metrics(1200, 760);
await click('.close-btn'); await waitFor('!document.querySelector(".modal-overlay")');
await click('#fixture-open'); await waitFor('!!document.querySelector(".modal-overlay")');
await evaluate('window.__compat.loginDelay=600');
await click('.oauth-content > button[title]');
await click('.close-btn'); await click('#fixture-open'); await delay(900);
assert.equal(await evaluate('!!document.querySelector(".oauth-link-value")'), false, 'Closed login attempt updated reopened modal');
await capture('oauth-close-reopen');
await evaluate('window.__compat.loginDelay=0; document.querySelectorAll(".tab-item")[2].click()');
await prepareCopy(true);
await capture('google-copy-failure');
await evaluate('window.__compat.copyFailure=false; window.__compat.clipboard=null');
await click('.oauth-link-panel button');
await waitFor('window.__compat.clipboard===window.__compat.url');
assert.equal(await evaluate('window.__compat.calls.filter(c=>c.command==="start_antigravity_oauth_login").length'), 1);
await capture('google-copy-retry');
assert.deepEqual(errors, [], 'Unhandled browser exception');
console.log(`OAuth link regression: ${scenes.length} scenes passed; native IPC and account data mocked.`);
} finally {
fs.writeFileSync(path.join(output, 'result.json'), JSON.stringify({ scenes, errors }, null, 2));
if (socket?.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ id: ++sequence, method: 'Browser.close' })); await delay(500); }
socket?.close();
if (child && child.exitCode === null) { child.kill(); await Promise.race([once(child, 'exit'), delay(5000)]); }
await server.close();
try { fs.rmSync(profile, { recursive: true, force: true }); } catch { /* Browser shutdown can briefly hold the isolated profile. */ }
}
27 changes: 27 additions & 0 deletions src/components/AddAccountModal.css
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@
}

.oauth-status {
max-width: 100%;
box-sizing: border-box;
overflow-wrap: anywhere;
white-space: pre-wrap;
margin-top: 16px;
font-size: 13px;
font-weight: 500;
Expand All @@ -168,6 +172,29 @@
border-radius: 20px;
}

.oauth-link-panel {
width: 100%;
min-width: 0;
margin-top: 12px;
text-align: left;
overflow-wrap: anywhere;
}

.oauth-link-value {
display: block;
width: 100%;
max-width: 100%;
box-sizing: border-box;
margin: 6px 0;
padding: 10px;
font-family: monospace;
font-size: 12px;
overflow-wrap: anywhere;
white-space: pre-wrap;
resize: vertical;
user-select: text;
}

.form-group {
margin-bottom: 20px;
}
Expand Down
Loading