【Salesforce自動操作 第4回】CDPによるキー操作【vba, chrome拡張機能】

目次

前回のおさらい

Salesforceからテキストボックスのノードを取得することはできましたが、文字列の設定が反映されませんでした。

今回やること

前回の問題を解決するため、別の手法で文字列をテキストボックスに設定していきます。

ソースコード 修正箇所

    , "permissions": [
        "activeTab"
        , "scripting"
        , "contextMenus"
        , "debugger"
    ]
/**
 * 右クリックメニュー選択イベント
 */
chrome.contextMenus.onClicked.addListener((info, tab) => {
    chrome.debugger.attach({tabId: tab.id}, '1.3');
    chrome.scripting.executeScript({
        target: { tabId: tab.id },
        files: ["contentsScript.js"]
      });
});

/**
 * 現在選択中のTabを取得
 */
async function getCurrentTab() {
    let queryOptions = { active: true, lastFocusedWindow: true };
    let [tab] = await chrome.tabs.query(queryOptions);
    return tab;
}

/**
 * メッセージの受信
 */
chrome.runtime.onMessage.addListener((req, send, res) => {
    // 現在のタブを取得
    // onMessage内ではasync/awaitが動作しないためthenで代用
    getCurrentTab().then((tab) => {
        const id = tab.id;

        // キー入力 (Backspace)
        if (req.process_type === 'key') {
            const processType = 'Input.dispatchKeyEvent';
            let param = {
                "type": "keyDown"
                , "key": "Backspace"
                , "code": "Backspace"
                , "windowsVirtualKeyCode": 8
             };
            chrome.debugger.sendCommand({tabId:id}, processType, param);
            res({result:'OK'});
        }
    });
    return true;
});
/**
 * テキストボックス、テキストエリアに入力する
 * @param {string} target_label_name 画面上の表示ラベル名
 * @param {string} text 設定値
 */
async function inputText(target_label_name, text) {
    const container = document.querySelector('.actionBody');
    const items = container.querySelectorAll('input[part="input"], textarea.slds-textarea');
    for (let i = 0; i < items.length; i++) {
        const item = items[i];
        const label = item.parentNode.parentNode;
        if (label.innerText == target_label_name) {
            // item.value = text;
            await CDPInputText(item, text);
            return;
        }
    }
}

/**
 * CDP経由のテキスト入力
 * @param {HTMLElement} item DOM要素
 */
async function CDPInputText(item, text) {
    item.value =  text + '★';
    item.focus();
    const param =  {process_type:'key'};
    sendCommand(param);
}

/**
 * background.jsへコマンドを送信する
 * @param {} param bgへ連携するパラメータ
 */
async function sendCommand(param) {
    try {
        const result = await chrome.runtime.sendMessage(param);
        return result;
    } catch (err) {
        throw EventFailedError();
    }
}

解説

CDP(Chrome DevTools Protocol)

  • テキスト入力やマウスクリックにはCDPを利用します。
    ブラウザ上のネットワーク通信の監視やデバッグ処理(ブレークポイントの設定等)といった開発者ツールに備わっているような機能をjsから操作することができます。
  • CDPはデフォルトでは機能が無効なため、有効にする必要があります。
  • CDPは多機能ですが、今回利用するのは「キー入力」の機能のみです。

テキスト入力までの流れ

  1. CDPを有効にする。
  2. 対象のDOMを取得する。(第3回で既にできている)
  3. 対象のDOMに(入力したい文字列 + 余分な1文字)を代入する。
  4. 対象のDOMにフォーカスを当てる。
  5. contentsScript.jsからbackscript.jsへキー入力リクエスト(BackSpace)を送信する。
  6. background.jsからCDPへキー入力リクエスト(BackSpace)を送信する。

CDPの有効化

    , "permissions": [
        , "debugger"
    ]

background.jsからCDPを利用するために、権限にdebuggerを追加します。

chrome.contextMenus.onClicked.addListener((info, tab) => {
    chrome.debugger.attach({tabId: tab.id}, '1.3');
});
  • chrome.debugger.attach:CDPの機能を有効にします。
  • '1.3':CDPのバージョンで公式HPのトップページに利用できるバージョンが記載されています。
    (記載箇所 : stable 1.3 protocol (1-3))

メッセージ受信処理(Background.js)

/**
 * 現在選択中のTabを取得
 */
async function getCurrentTab() { // (3)
    let queryOptions = { active: true, lastFocusedWindow: true };
    let [tab] = await chrome.tabs.query(queryOptions);
    return tab;
}

/**
 * メッセージの受信
 */
chrome.runtime.onMessage.addListener((req, send, res) => { // (1)(2)
    getCurrentTab().then((tab) => {  // (3)
        const id = tab.id;
        (中略)
  }
}
  1. contentsScript.jsでchrome.runtime.sendMessage(param)を呼ぶと、background.js側でchrome.runtime.onMessage.addListener((req, send, res))が呼ばれます。
  2. 引数paramはそのまま引数resとして渡されます。
  3. background.jsでは、ブラウザの何番目のタブにSalesforceの画面が表示されているか情報がないため、タブを特定する必要があります。
    chrome.tabs.query
    で現在アクティブなタブを取得します。
        // キー入力 (Backspace)
        if (req.process_type === 'key') {
            const processType = 'Input.dispatchKeyEvent';
            let param = {                    // (1)
                "type": "keyDown"
                , "key": "Backspace"         // (2)
                , "code": "Backspace"
                , "windowsVirtualKeyCode": 8
             };
            chrome.debugger.sendCommand({tabId:id}, processType, param);
            res({result:'OK'}); // (3)
        }
    });
    return true; // (4)
  1. CDP公式HPのInput.dispatchKeyEventでかなり多くの引数が用意されていることが確認できます。
    反面、サンプルの記載がないためトライ&エラーで設定しています。
  2. CDPにより「BackSpace」のみ入力をさせます。Salesforceでは1文字でも手入力扱いのキー入力ができればそれ以前に設定されていた全ての文字列が手入力されたものであると認識されます。そのため”佐藤 太郎”をテキストに入力させたいのであれば、”佐藤 太郎”と入力をするのではなく、”佐藤 太郎★”をあらかじめ設定しておき、CDPに末尾の★だけを削除させます。
  3. contentsScript.jsへjsonデータを返却することができます。今回のケースでは特に返却すべきデータがないので適当な値を設定しています。
  4. onMessage内に非同期メソッドを渡すとうまく動作しないという仕様があります。
    (async (req, send, res) => {}という記述では動作しない)
    代わりにreturn true;と記述することで非同期処理に切り替えることができます。

メッセージ受信処理(contentsScript.js)

            // item.value = text;
            await CDPInputText(item, text);
  • 前回の値代入からメソッド呼び出しに切り替えています。
/**
 * CDP経由のテキスト入力
 * @param {HTMLElement} item DOM要素
 */
async function CDPInputText(item, text) {
    item.value =  text + '★';
    item.focus();
    const param =  {process_type:'key'};
    await sendCommand(param);
}

/**
 * background.jsへコマンドを送信する
 * @param {} param bgへ連携するパラメータ
 */
async function sendCommand(param) {
    try {
        const result = await chrome.runtime.sendMessage(param);
        return result;
    } catch (err) {
    }
}
  • 『テキスト入力までの流れ』に記述した(3) ~ (5)をそのままコードに起こしています。
  • background.jsでは非同期で処理を行いますので、async-awaitを忘れずに記述します。

実行結果

Web会社名に値が入り、かつ背景が黄色に切り替わっていればOKです。

はまりやすいポイント

contentsScript

  • chrome.runtime.sendMessage(param)の処理にasync/awaitを忘れないこと。

background

  • chrome.debugger.sendCommand実行時に必要なパラメータはキーごとに異なる可能性がある。
    トライアンドエラーで対処する。
  • chrome.runtime.onMessage.addListener内部ではasync/awaitが使用できないため、then形式での記述が必要である。
  • chrome.runtime.onMessage.addListenerを非同期で処理させるためにはreturn true;を返却する必要がある。