mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
fix: sidebar auto-open retry with backoff + welcome page tests
- Replace single-attempt sidePanel.open() with autoOpenSidePanel() that retries up to 5 times with 500ms-5000ms backoff - Fire on both onInstalled AND every service worker startup - Remove misaligned arrow from welcome page, replace with text fallback - Add 12 tests: welcome page structure, /welcome endpoint, headed launch navigation timing, sidebar auto-open retry logic, extension-ready event
This commit is contained in:
@@ -1192,3 +1192,133 @@ describe('LLM-based cleanup (smart agent cleanup)', () => {
|
||||
expect(wcSrc).toContain("role') === 'navigation'");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Welcome page + sidebar auto-open ────────────────────────────
|
||||
|
||||
describe('welcome page', () => {
|
||||
const welcomePath = path.join(ROOT, 'src', 'welcome.html');
|
||||
const welcomeExists = fs.existsSync(welcomePath);
|
||||
const welcomeSrc = welcomeExists ? fs.readFileSync(welcomePath, 'utf-8') : '';
|
||||
|
||||
test('welcome.html exists in browse/src/', () => {
|
||||
expect(welcomeExists).toBe(true);
|
||||
});
|
||||
|
||||
test('welcome page has GStack Browser branding', () => {
|
||||
expect(welcomeSrc).toContain('GStack Browser');
|
||||
});
|
||||
|
||||
test('welcome page has extension-ready listener to hide prompt', () => {
|
||||
expect(welcomeSrc).toContain('gstack-extension-ready');
|
||||
expect(welcomeSrc).toContain('sidebar-prompt');
|
||||
});
|
||||
|
||||
test('welcome page does NOT have a misaligned arrow', () => {
|
||||
// Arrow was removed because it can never align with browser chrome
|
||||
expect(welcomeSrc).not.toContain('arrow-up');
|
||||
expect(welcomeSrc).not.toContain('↑');
|
||||
});
|
||||
|
||||
test('welcome page has left-aligned text (no center-align on headings)', () => {
|
||||
// User preference: always left-align, never center
|
||||
expect(welcomeSrc).not.toMatch(/text-align:\s*center/);
|
||||
});
|
||||
|
||||
test('welcome page uses dark theme', () => {
|
||||
expect(welcomeSrc).toContain('#0C0C0C'); // --base (near-black)
|
||||
expect(welcomeSrc).toContain('#141414'); // --surface (card bg)
|
||||
});
|
||||
});
|
||||
|
||||
describe('server /welcome endpoint', () => {
|
||||
const serverSrc = fs.readFileSync(path.join(ROOT, 'src', 'server.ts'), 'utf-8');
|
||||
|
||||
test('/welcome endpoint exists in server.ts', () => {
|
||||
expect(serverSrc).toContain("url.pathname === '/welcome'");
|
||||
});
|
||||
|
||||
test('/welcome serves HTML content type', () => {
|
||||
const welcomeSection = serverSrc.slice(
|
||||
serverSrc.indexOf("url.pathname === '/welcome'"),
|
||||
serverSrc.indexOf("url.pathname === '/health'"),
|
||||
);
|
||||
expect(welcomeSection).toContain("'Content-Type': 'text/html");
|
||||
});
|
||||
|
||||
test('/welcome redirects to about:blank if no welcome file found', () => {
|
||||
const welcomeSection = serverSrc.slice(
|
||||
serverSrc.indexOf("url.pathname === '/welcome'"),
|
||||
serverSrc.indexOf("url.pathname === '/health'"),
|
||||
);
|
||||
expect(welcomeSection).toContain('302');
|
||||
expect(welcomeSection).toContain('about:blank');
|
||||
});
|
||||
});
|
||||
|
||||
describe('headed launch navigates to welcome page', () => {
|
||||
const serverSrc = fs.readFileSync(path.join(ROOT, 'src', 'server.ts'), 'utf-8');
|
||||
|
||||
test('server navigates to /welcome after startup in headed mode', () => {
|
||||
// Navigation must happen AFTER Bun.serve() starts (not during launchHeaded)
|
||||
// because the HTTP server needs to be listening before the browser requests /welcome
|
||||
const afterServe = serverSrc.slice(serverSrc.indexOf('Bun.serve('));
|
||||
expect(afterServe).toContain('/welcome');
|
||||
expect(afterServe).toContain("getConnectionMode() === 'headed'");
|
||||
});
|
||||
|
||||
test('welcome navigation does NOT happen in browser-manager (too early)', () => {
|
||||
const bmSrc = fs.readFileSync(path.join(ROOT, 'src', 'browser-manager.ts'), 'utf-8');
|
||||
// browser-manager.ts should NOT navigate to /welcome because the server
|
||||
// isn't listening yet when launchHeaded() runs
|
||||
const launchHeadedSection = bmSrc.slice(
|
||||
bmSrc.indexOf('async launchHeaded('),
|
||||
bmSrc.indexOf('// Browser disconnect handler'),
|
||||
);
|
||||
expect(launchHeadedSection).not.toContain('/welcome');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sidebar auto-open (background.js)', () => {
|
||||
const bgSrc = fs.readFileSync(path.join(ROOT, '..', 'extension', 'background.js'), 'utf-8');
|
||||
|
||||
test('autoOpenSidePanel function exists with retry logic', () => {
|
||||
expect(bgSrc).toContain('async function autoOpenSidePanel');
|
||||
expect(bgSrc).toContain('attempt < 5');
|
||||
});
|
||||
|
||||
test('auto-open fires on install AND on every service worker startup', () => {
|
||||
// onInstalled fires on first install / extension update
|
||||
expect(bgSrc).toContain('chrome.runtime.onInstalled.addListener');
|
||||
expect(bgSrc).toContain('autoOpenSidePanel()');
|
||||
// Top-level call fires on every service worker startup
|
||||
const topLevelCalls = bgSrc.match(/^autoOpenSidePanel\(\)/gm);
|
||||
expect(topLevelCalls).not.toBeNull();
|
||||
expect(topLevelCalls!.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test('retry uses backoff delays (not fixed interval)', () => {
|
||||
expect(bgSrc).toContain('500');
|
||||
expect(bgSrc).toContain('1000');
|
||||
expect(bgSrc).toContain('2000');
|
||||
expect(bgSrc).toContain('3000');
|
||||
expect(bgSrc).toContain('5000');
|
||||
});
|
||||
|
||||
test('auto-open uses chrome.sidePanel.open with windowId', () => {
|
||||
expect(bgSrc).toContain('chrome.sidePanel.open');
|
||||
expect(bgSrc).toContain('windowId');
|
||||
});
|
||||
|
||||
test('auto-open logs success and failure for debugging', () => {
|
||||
expect(bgSrc).toContain('Side panel opened on attempt');
|
||||
expect(bgSrc).toContain('Side panel auto-open failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extension dispatches gstack-extension-ready event', () => {
|
||||
const contentSrc = fs.readFileSync(path.join(ROOT, '..', 'extension', 'content.js'), 'utf-8');
|
||||
|
||||
test('content.js dispatches gstack-extension-ready CustomEvent', () => {
|
||||
expect(contentSrc).toContain("new CustomEvent('gstack-extension-ready')");
|
||||
});
|
||||
});
|
||||
|
||||
+25
-19
@@ -406,29 +406,35 @@ if (chrome.sidePanel && chrome.sidePanel.setPanelBehavior) {
|
||||
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {});
|
||||
}
|
||||
|
||||
// Auto-open side panel on install/update AND every service worker startup.
|
||||
// onInstalled fires on first install / extension update.
|
||||
// The startup block below fires every time the browser launches (persistent context reuse).
|
||||
chrome.runtime.onInstalled.addListener(async () => {
|
||||
setTimeout(async () => {
|
||||
// Auto-open side panel with retry. chrome.sidePanel.open() can fail silently
|
||||
// if the window/tab isn't fully ready yet. Retry up to 5 times with backoff.
|
||||
async function autoOpenSidePanel() {
|
||||
if (!chrome.sidePanel?.open) return;
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
try {
|
||||
const [win] = await chrome.windows.getAll({ windowTypes: ['normal'] });
|
||||
if (win && chrome.sidePanel?.open) {
|
||||
await chrome.sidePanel.open({ windowId: win.id });
|
||||
const wins = await chrome.windows.getAll({ windowTypes: ['normal'] });
|
||||
if (wins.length > 0) {
|
||||
await chrome.sidePanel.open({ windowId: wins[0].id });
|
||||
console.log(`[gstack] Side panel opened on attempt ${attempt + 1}`);
|
||||
return; // success
|
||||
}
|
||||
} catch {}
|
||||
}, 1000);
|
||||
} catch (e) {
|
||||
// May throw if window isn't ready or user gesture required
|
||||
console.log(`[gstack] Side panel open attempt ${attempt + 1} failed:`, e.message);
|
||||
}
|
||||
// Backoff: 500ms, 1000ms, 2000ms, 3000ms, 5000ms
|
||||
await new Promise(r => setTimeout(r, [500, 1000, 2000, 3000, 5000][attempt]));
|
||||
}
|
||||
console.log('[gstack] Side panel auto-open failed after 5 attempts');
|
||||
}
|
||||
|
||||
// Fire on install/update
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
autoOpenSidePanel();
|
||||
});
|
||||
|
||||
// Also auto-open on every browser launch (not just install)
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const [win] = await chrome.windows.getAll({ windowTypes: ['normal'] });
|
||||
if (win && chrome.sidePanel?.open) {
|
||||
await chrome.sidePanel.open({ windowId: win.id });
|
||||
}
|
||||
} catch {}
|
||||
}, 1500);
|
||||
// Fire on every service worker startup (covers persistent context reuse)
|
||||
autoOpenSidePanel();
|
||||
|
||||
// ─── Tab Switch Detection ────────────────────────────────────────
|
||||
// Notify sidepanel instantly when the user switches tabs in the browser.
|
||||
|
||||
Reference in New Issue
Block a user