All files / entrypoints background.ts

96.69% Statements 117/121
83.33% Branches 20/24
100% Functions 2/2
96.69% Lines 117/121

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 1221x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 4x 4x 4x 4x 4x 4x 4x 4x 3x 3x 3x 3x 3x 3x 2x 1x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 4x 1x 1x 1x 1x 3x 1x 1x 1x 1x 1x 1x 1x 1x 3x 1x 1x 3x     3x     3x 1x 1x 2x 2x 3x 1x 1x 1x 1x 4x 4x 1x 4x 4x  
import {
  analyzeJobPage,
  analyzePastedText,
  analyzeScopedRegion,
  cancelAnalysis,
  handleActionClick,
  resolveActiveTab,
  trackContentTabs,
} from "../services/jobFlow";
import { readLiveSelection } from "../services/selectionService";
import { MessageType } from "../types/messages";
import type { ActiveTabResponse, ExtensionMessage } from "../types/messages";
 
const ANALYZE_SELECTION_MENU_ID = "analyze-selection";
 
declare const WXT_AZURE_FUNCTION_URL: string;
 
export default defineBackground({
  main() {
    console.log("[background] service URL configured:", !!WXT_AZURE_FUNCTION_URL);
    // Do NOT use setPanelBehavior({ openPanelOnActionClick: true }): panels
    // opened that way never receive the activeTab grant (crbug.com/40926394),
    // so page extraction would always fail. Instead the click lands on
    // action.onClicked — which does grant activeTab — and the panel is opened
    // here, synchronously within the user gesture as sidePanel.open requires.
    chrome.action.onClicked.addListener((tab) => {
      if (tab.windowId !== undefined) {
        void chrome.sidePanel.open({ windowId: tab.windowId });
      }
      handleActionClick(tab, broadcastToSidePanel);
    });
    trackContentTabs(broadcastToSidePanel);
 
    // "Analyze selection" (spec FR-023a): works without the panel already
    // open, because the contextMenus onClicked handler carries its own
    // activeTab grant — unlike everything else here, which depends on the
    // toolbar-click grant.
    chrome.contextMenus.create({
      id: ANALYZE_SELECTION_MENU_ID,
      title: "Analyze selection",
      contexts: ["selection"],
    });
    chrome.contextMenus.onClicked.addListener((info, tab) => {
      if (info.menuItemId !== ANALYZE_SELECTION_MENU_ID || tab?.id === undefined) return;
      if (tab.windowId !== undefined) {
        void chrome.sidePanel.open({ windowId: tab.windowId });
      }
      void (async () => {
        const tabId = tab.id as number;
        const selection = await readLiveSelection(tabId);
        if (!selection.ok) {
          // FR-036a: cause-specific wording, not one generic message — an
          // empty selection is fixed by selecting again; a shadow-root or
          // cross-origin-iframe selection never can be, so it points at the
          // one path that does work instead (paste).
          const action =
            selection.cause === "unscopeable"
              ? "Try a different part of the page, or paste the text instead."
              : "Select the posting text on the page, then try again.";
          broadcastToSidePanel({
            type: MessageType.JOB_ANALYSIS_ERROR,
            error: {
              code: "thin-content",
              message: selection.reason,
              action,
              retryable: false,
            },
            fallback: null,
            canonicalUrl: null,
            sourceUrl: null,
          });
          return;
        }
        await analyzeScopedRegion(
          {
            type: MessageType.ANALYZE_SCOPED_REGION,
            tabId,
            text: selection.text,
            selector: selection.selector,
            mechanism: "selection",
          },
          broadcastToSidePanel
        );
      })();
    });
 
    chrome.runtime.onMessage.addListener(
      (message: ExtensionMessage, sender, sendResponse) => {
        if (message.type === MessageType.GET_ACTIVE_TAB) {
          // sender.tab is set when the panel page runs as a regular tab; its
          // URL is not readable without the "tabs" permission, so the id is
          // the only reliable way to keep the panel from analyzing itself.
          void resolveActiveTab(sender.tab?.id ?? null).then(
            (response: ActiveTabResponse) => sendResponse(response)
          );
          return true; // async response
        }
        if (message.type === MessageType.ANALYZE_JOB_PAGE) {
          void analyzeJobPage(message, broadcastToSidePanel);
        }
        if (message.type === MessageType.CANCEL_ANALYSIS) {
          cancelAnalysis(message);
        }
        if (message.type === MessageType.ANALYZE_PASTED_TEXT) {
          void analyzePastedText(message, broadcastToSidePanel);
        }
        if (message.type === MessageType.ANALYZE_SCOPED_REGION) {
          void analyzeScopedRegion(message, broadcastToSidePanel);
        }
        sendResponse({ received: true });
        return true;
      }
    );
  },
});
 
function broadcastToSidePanel(message: ExtensionMessage): void {
  chrome.runtime.sendMessage(message).catch(() => {
    // Side panel may not be open
  });
}