477a00db1a
Testing / remote-protocol-compat (0.9.5) (push) Successful in 48s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 47s
Build & Publish Package / publish (push) Successful in 46s
Package Extension / package-extension (push) Successful in 59s
Testing / test (push) Failing after 50s
- Add a neutral WebExtension API adapter that uses Firefox browser.* or Chromium chrome.* without mutating globals. - Switch extension runtime code to the adapter and add Firefox-specific typings for tabs, windows, tab groups, storage, scripting, and native messaging ports. - Fix Firefox temporary add-on instructions to load the packaged manifest with background.scripts instead of the Chromium service worker manifest. - Detect Firefox in clients.list via runtime.getBrowserInfo and keep Chromium user-agent fallback support. - Make navigate.open wait briefly for Firefox to replace initial about:blank with the requested URL. - Add JS coverage for API selection, clients.list browser detection, and Firefox navigate.open URL polling. - Bump package and extension version to 0.15.2.
97 lines
3.7 KiB
TypeScript
97 lines
3.7 KiB
TypeScript
import { webExtApi as api } from '../browser-api';
|
|
import { fetchTabHtml, getActiveTab, getAliases, isBrowserErrorUrl, tabInfo } from '../core';
|
|
import { CommandGroup } from '../classes/CommandGroup';
|
|
import type { CommandEntry } from '../classes/CommandGroup';
|
|
import type { TabIdArgs, TabsActiveInWindowArgs, TabsPatternArgs, TabsQueryArgs, TabsWatchUrlArgs } from '../types';
|
|
|
|
export class TabsQueryCommands extends CommandGroup {
|
|
readonly namespace = "tabs";
|
|
readonly commands: Record<string, CommandEntry> = {
|
|
"tabs.list": () => this.tabsList(),
|
|
"tabs.active_in_window": (a: TabsActiveInWindowArgs) => this.tabsActiveInWindow(a),
|
|
"tabs.status": (a: TabIdArgs) => this.tabsStatus(a),
|
|
"tabs.filter": (a: TabsPatternArgs) => this.tabsFilter(a),
|
|
"tabs.count": (a: TabsPatternArgs) => this.tabsCount(a),
|
|
"tabs.query": (a: TabsQueryArgs) => this.tabsQuery(a),
|
|
"tabs.html": (a: TabIdArgs) => fetchTabHtml(a.tabId),
|
|
"tabs.watch_url": (a: TabsWatchUrlArgs) => this.tabsWatchUrl(a),
|
|
};
|
|
|
|
private async tabsList() {
|
|
const windows = await api.windows.getAll({ populate: true });
|
|
const aliases = await getAliases();
|
|
const tabs = [];
|
|
for (const w of windows) {
|
|
for (const t of w.tabs) {
|
|
tabs.push({
|
|
...tabInfo(t),
|
|
windowAlias: aliases[t.windowId] || null,
|
|
pinned: t.pinned,
|
|
favIconUrl: t.favIconUrl,
|
|
});
|
|
}
|
|
}
|
|
return tabs;
|
|
}
|
|
|
|
private async tabsActiveInWindow({ windowId }: TabsActiveInWindowArgs) {
|
|
const activeTabs = await api.tabs.query({ windowId, active: true });
|
|
const tab = activeTabs[0];
|
|
if (!tab) {
|
|
throw new Error(`No active tab found for window ${windowId}`);
|
|
}
|
|
return tabInfo(tab);
|
|
}
|
|
|
|
private async tabsStatus({ tabId }: TabIdArgs) {
|
|
const tab = tabId ? await api.tabs.get(tabId) : await getActiveTab();
|
|
return tabInfo(tab);
|
|
}
|
|
|
|
private async tabsFilter({ pattern }: TabsPatternArgs) {
|
|
const all = await api.tabs.query({});
|
|
return all.filter(t => t.url && t.url.includes(pattern)).map(tabInfo);
|
|
}
|
|
|
|
private async tabsCount({ pattern }: TabsPatternArgs) {
|
|
const all = await api.tabs.query({});
|
|
if (pattern) return all.filter(t => t.url && t.url.includes(pattern)).length;
|
|
return all.length;
|
|
}
|
|
|
|
private async tabsQuery({ search }: TabsQueryArgs) {
|
|
const q = search.toLowerCase();
|
|
const all = await api.tabs.query({});
|
|
return all.filter(t =>
|
|
(t.url && t.url.toLowerCase().includes(q)) ||
|
|
(t.title && t.title.toLowerCase().includes(q))
|
|
).map(tabInfo);
|
|
}
|
|
|
|
private async tabsWatchUrl({ pattern, timeout = 30000, tabId }: TabsWatchUrlArgs = {}) {
|
|
const tab = tabId ? await api.tabs.get(tabId) : await getActiveTab();
|
|
const deadline = Date.now() + timeout;
|
|
const regex = new RegExp(pattern);
|
|
let lastUrl = tab.url || tab.pendingUrl || "";
|
|
let lastStatus = tab.status || "unknown";
|
|
|
|
const matches = (url: string) => {
|
|
regex.lastIndex = 0;
|
|
return Boolean(url && regex.test(url));
|
|
};
|
|
if (matches(lastUrl)) return tabInfo(tab);
|
|
|
|
while (Date.now() < deadline) {
|
|
const t = await api.tabs.get(tab.id);
|
|
lastUrl = t.url || t.pendingUrl || "";
|
|
lastStatus = t.status || "unknown";
|
|
if (matches(t.pendingUrl || "") || matches(t.url || "")) return tabInfo(t);
|
|
if (isBrowserErrorUrl(t.url || "")) {
|
|
throw new Error(`Tab ${tab.id} is showing an error page while waiting for URL to match '${pattern}'`);
|
|
}
|
|
await new Promise(r => setTimeout(r, 200));
|
|
}
|
|
throw new Error(`Tab ${tab.id} URL did not match '${pattern}' within ${timeout}ms (last URL: '${lastUrl}', status: '${lastStatus}')`);
|
|
}
|
|
}
|