Skip to content
Open
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
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ temp/
# Coverage directory used by tools like istanbul
coverage/

# Playwright / local e2e (dev-only, not for upstream PR)
test-results/
playwright-report/
playwright.config.js
jest.config.js
tests/e2e/
demo/deep-select-test.html

# Optional npm cache directory
.npm

Expand Down Expand Up @@ -84,3 +92,9 @@ demo/images/

# Standalone UI style guide
drawbridge-ui/

# Windsurf configuration
.windsurf/

# Codex CLI configuration
.codex/
28 changes: 25 additions & 3 deletions chrome-extension/background.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// chrome-extension/background.js

importScripts('utils/buildInfo.js');

const RESTRICTED_URL_PREFIXES = [
'chrome://',
'chrome-extension://',
Expand Down Expand Up @@ -101,8 +103,19 @@ async function injectMoatScript(tabId) {
});
}

function getExpectedContentScriptBuild() {
const buildInfo = globalThis.DrawbridgeBuildInfo;
return buildInfo.getContentScriptBuild(chrome.runtime.getManifest().version);
}

function needsManualInjection(pingResponse) {
return !pingResponse?.ready || pingResponse.moatLoaded === false;
if (!pingResponse?.ready || pingResponse.moatLoaded === false) {
return true;
}
if (pingResponse.contentScriptBuild !== getExpectedContentScriptBuild()) {
return true;
}
return false;
}

async function ensureContentScriptsReady(tabId) {
Expand All @@ -113,15 +126,23 @@ async function ensureContentScriptsReady(tabId) {
return;
}

await injectMoatScript(tabId);
console.warn(
'Drawbridge: Injecting content scripts (missing, outdated moat, or stale build:',
pingResponse?.contentScriptBuild,
'expected:',
getExpectedContentScriptBuild() + ')'
);
await injectManifestContentScripts(tabId);
} catch (error) {
console.warn('Drawbridge: Content script not ready, injecting into active tab:', error.message);
await injectManifestContentScripts(tabId);
}

const pingResponse = await sendTabMessage(tabId, { action: 'ping' });
if (needsManualInjection(pingResponse)) {
throw new Error('Content script did not report ready after injection');
throw new Error(
'Content script did not report ready after injection. Refresh the page (Ctrl+Shift+R).'
);
}
}

Expand Down Expand Up @@ -167,6 +188,7 @@ if (typeof module !== 'undefined' && module.exports) {
RESTRICTED_URL_PREFIXES,
isRestrictedTab,
needsManualInjection,
getExpectedContentScriptBuild,
handleActionClick,
ensureContentScriptsReady,
injectManifestContentScripts,
Expand Down
57 changes: 50 additions & 7 deletions chrome-extension/background.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
const manifest = require('./manifest.json');
const { getContentScriptBuild } = require('./utils/buildInfo.js');

function getReadyPing(overrides = {}) {
return {
success: true,
ready: true,
moatLoaded: true,
contentScriptBuild: getContentScriptBuild(manifest.version),
...overrides
};
}

function loadBackground({ sendMessage, insertCSS, executeScript } = {}) {
jest.resetModules();

global.self = global;
global.importScripts = jest.fn(() => {
require('./utils/buildInfo.js');
});
require('./utils/buildInfo.js');

global.chrome = {
action: {
onClicked: {
Expand All @@ -17,7 +34,7 @@ function loadBackground({ sendMessage, insertCSS, executeScript } = {}) {
}
},
tabs: {
sendMessage: sendMessage || jest.fn((tabId, message, callback) => callback({ success: true, ready: true, moatLoaded: true })),
sendMessage: sendMessage || jest.fn((tabId, message, callback) => callback(getReadyPing())),
captureVisibleTab: jest.fn()
},
scripting: {
Expand Down Expand Up @@ -53,7 +70,7 @@ describe('background action click handling', () => {
test('toggles immediately when the content script is already ready', async () => {
const sendMessage = jest.fn((tabId, message, callback) => {
if (message.action === 'ping') {
callback({ success: true, ready: true, moatLoaded: true });
callback(getReadyPing());
return;
}

Expand All @@ -79,7 +96,7 @@ describe('background action click handling', () => {
return;
}

callback({ success: true, ready: true, moatLoaded: true });
callback({ ...getReadyPing(), moatLoaded: true });
return;
}

Expand All @@ -100,14 +117,13 @@ describe('background action click handling', () => {
expect(sendMessage.mock.calls.map(call => call[1].action)).toEqual(['ping', 'ping', 'toggleMoat']);
});

test('injects only moat when the content script is ready but moat has not loaded yet', async () => {
test('injects full manifest when moat has not loaded yet', async () => {
let pingCount = 0;
const sendMessage = jest.fn((tabId, message, callback) => {
if (message.action === 'ping') {
pingCount += 1;
callback({
success: true,
ready: true,
...getReadyPing(),
moatLoaded: pingCount > 1
});
return;
Expand All @@ -125,7 +141,34 @@ describe('background action click handling', () => {
}, expect.any(Function));
expect(chrome.scripting.executeScript).toHaveBeenCalledWith({
target: { tabId: 7 },
files: ['moat.js']
files: manifest.content_scripts[0].js
}, expect.any(Function));
expect(sendMessage.mock.calls.map(call => call[1].action)).toEqual(['ping', 'ping', 'toggleMoat']);
});

test('injects full manifest when content script build is stale', async () => {
let pingCount = 0;
const sendMessage = jest.fn((tabId, message, callback) => {
if (message.action === 'ping') {
pingCount += 1;
callback({
success: true,
ready: true,
moatLoaded: true,
contentScriptBuild: pingCount > 1 ? getContentScriptBuild(manifest.version) : '1.0.0-outdated'
});
return;
}

callback({ success: true });
});
const { handleActionClick } = loadBackground({ sendMessage });

await handleActionClick({ id: 7, url: 'https://example.com/' });

expect(chrome.scripting.executeScript).toHaveBeenCalledWith({
target: { tabId: 7 },
files: manifest.content_scripts[0].js
}, expect.any(Function));
expect(sendMessage.mock.calls.map(call => call[1].action)).toEqual(['ping', 'ping', 'toggleMoat']);
});
Expand Down
Loading