CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/chrome-extension-messaging-tests

Asserts Chrome extension message-passing behaviour against a running extension: one-shot `chrome.runtime.sendMessage` plus the literal `return true` that holds the response channel open for an async `sendResponse`, `chrome.tabs.sendMessage` into one tab's content script, long-lived `chrome.runtime.connect` ports and their `onDisconnect` triggers, web-page messages gated by `externally_connectable`, and `chrome.runtime.connectNative` native-messaging hosts. Covers the payload rules a test must respect (Chrome uses JSON serialization rather than structured clone, so `Map` / `Set` / `Date` do not round-trip; maximum message size is 64 MiB) and the first-listener-wins rule when several `onMessage` listeners are registered. Scope is messaging behaviour on an already-running extension, not the install or reload step. Use when a message reaches no listener, a `sendResponse` callback never fires, a port disconnects mid-test, or a page origin has to be proven allowed before publishing.

79

Quality

99%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

long-lived-ports.mdreferences/

Long-lived ports and service-worker lifecycle

Deep reference for chrome-extension-messaging-tests SKILL.md. Consult when a long-lived chrome.runtime.connect port drops mid-session or a port test has to account for service-worker suspension.

Per the Chrome Extensions message passing page, a port is opened with chrome.runtime.connect and answered by onConnect:

// Content script
const port = chrome.runtime.connect({ name: 'knockknock' });
port.onMessage.addListener(msg => {
  if (msg.question === "Who's there?") port.postMessage({ answer: 'Madame' });
});
port.postMessage({ joke: 'Knock knock' });

// Service worker
chrome.runtime.onConnect.addListener(port => {
  if (port.name !== 'knockknock') return;
  port.onMessage.addListener(msg => {
    if (msg.joke === 'Knock knock') port.postMessage({ question: "Who's there?" });
  });
});

The chrome.runtime reference gives chrome.runtime.connect(extensionId?, connectInfo?) returning a Port, where connectInfo.name "will be passed into onConnect for processes that are listening for the connection event". That name is what the port.name !== 'knockknock' guard above filters on, so a test that connects with the wrong name should observe no reply.

Per cr-msg, onDisconnect fires when: no listeners exist for onConnect, the tab unloads, the originating frame unloads, all receiving frames unload, or disconnect() is called. A port test should cover at least the first and last of those, because they are the two a bug produces:

test('connecting with an unknown port name disconnects immediately', async () => {
  const events = [];
  const port = chrome.runtime.connect({ name: 'no-such-listener' });
  port.onDisconnect.addListener(() => events.push('disconnect'));
  await settle();
  expect(events).toEqual(['disconnect']);
});

Accumulate disconnect events into an array rather than setting a boolean. A port can have multiple receiving frames per cr-msg, so "exactly one disconnect" is an assertion you should make explicitly rather than assume.

Service-worker suspension interacts with ports

Per the extension service worker lifecycle page, the service worker terminates after 30 seconds of inactivity, and "Opening a port no longer resets the timers" as of Chrome 114. What keeps it alive is traffic: cr-sw states "Sending a message with long-lived messaging keeps the service worker alive". A port test that idles longer than 30 seconds without exchanging a message is asserting against a terminated worker, and the disconnect it observes is the platform, not a bug.

Sources

SKILL.md

tile.json