【Salesforce自動操作 第5回】検索項目の自動化【vba, chrome拡張機能】

目次

前回のおさらい

CDPを用いて、テキストの入力を自動化することが出来ました。

今回やること

最後の入力項目「検索項目」の自動化を行います。
検索項目のうち「取引先責任者名」に値を設定します。

ソースコード 修正箇所

/**
 * コンボボックスを選択する
 * @param {string} target_label_name 画面上の表示ラベル名 ("名"の除いた文字列)
 * @param {string} text 入力値 
 */
function async selectCombobox(target_label_name, text) {
    const container = document.querySelector('.actionBody');
    const items = container.querySelectorAll('input[role="combobox"]');
    for (let i = 0; i < items.length; i++) { 
        const item = items[i]; 
        const placeholder = item.getAttribute('placeholder'); 
        const label_name = placeholder.replace('を検索...', ''); 
        if (label_name == target_label_name) { 
            // テキスト入力
            item.focus();
            await CommandRequest.InputText(item, text); 
            await CommandRequest.InputText(item, text); 
            // 検索ボタン押下 
            await asyncClick((args) => {
                return args[0].parentNode.parentNode.querySelector('lightning-base-combobox-item[data-value="actionAdvancedSearch"]');
            }, [item]); 

            // モーダル画面が表示されるまで待機 
            await asyncConditionalWait((args) => {
                const modal = document.querySelector('.modal-container');
                const item1 = modal.querySelector('a[data-refid="recordId"]');
                const item2 = modal.querySelector('div[role="region"]');
                return item1 || item2;
            });

            // アイテム または キャンセルボタンをクリック
            const modal = document.querySelector('.modal-container');
            const item1 = modal.querySelector('a[data-refid="recordId"]');
            if (item1) {
                item1.click();
            } else {
                const cancel_button = modal.querySelector('button[title="キャンセル"]');
                cancel_button.click();
            }
                
            // モーダル画面が閉じるまで待機
            await Async.ConditionalWait((args) => {
                const modal = document.querySelector('.modal-container');
                return (!modal);
            });
            return;
        }
    }
}

/**
 * DOMノードを取得する(非同期)
 */
async function asyncGetNode(selector, args = {}, timeout = 10000) {
    const begin_time = new Date().getTime();
    let diff_time = 0;
    do {
        try {
            const item = selector(args);
            if (item) {
                return item;
            }
        }
        catch (err) {
            // do nothing
        }
        diff_time = new Date().getTime() - begin_time;
        await Async.Wait(100);
    } while(diff_time < timeout);
}

/**
 * function selectorがtrueを返却するまで処理を中断する
 */ 
async function asyncConditionalWait(selector, args = {}, timeout = 10000) {
    const result = await asyncGetNode(selector, args, timeout);
    if (result) {
        return true;
    }
    return false;
}

/**
 * ノードのクリックを行う(非同期)
 */
async function asyncClick(selector, args = {}, timeout = 10000) {
    const result = await asyncGetNode(selector, args, timeout);
    result.click();
}

async function main() {
    await selectPulldown('優先度', 'High');
    inputText('Web 会社名', 'ABCソリューション')
    await selectCombobox('取引先責任者', '雪村アオイ');
}

解説

ノード階層

<div class="actionBody">
    <records-record-layout-item field-label="取引先責任者名">
        <lightning-grouped-combobox>
            <label part="label">
                取引先責任者名
            </label>
            <div lightning-groupedcombobox_groupedcombobox>
                <div>
                    <lightning-base-combobox>
                        <div>
                            <div>
                                <input type="text" role="combobox" placeholder="取引先責任者を検索..." class="slds-combobox__input slds-input" />
                            </div>
                        </div>
                    </lightning-base-combobox>
                </div>
            </div>
        </lightning-grouped-combobox>
    </records-record-layout-item>
</div>
  • テキストの時と同様、inputタグを取得し、そのあとにlabelタグを取得します。

inputタグの取得

    const container = document.querySelector('.actionBody');
    const items = container.querySelectorAll('input[role="combobox"]');

ラベル情報の取得(失敗例)

  • labelタグはinputタグから見て5階層上にあります。
    そのため、parentNodeを6回呼び出してlabelタグで検索を掛ければうまくいきそうです。
  • 上記は、コンソールログから実行したコードです。3階層遡ったところで、#document-fragmentと表示され、親ノードの取得に失敗しました。
  • 「documentFragment parentNode」で検索を掛けてみるとdocumentFragmentは親を持たないノードであることが分かりました。そのため、labelタグの参照はできません。

ラベル情報の取得(妥協案)

        const placeholder = item.getAttribute('placeholder');
        const label_name = placeholder.replace('を検索...', '');
  • labelタグからラベル名の取得ができないため、placeholderから値を参照することにします。

テキスト入力 > 検索ボタンのクリック

            // キーワード入力
            item.focus();
            await CDPInputText(item, text); // (1)
            await CDPInputText(item, text); // (1)

            // 検索ボタン押下
            await asyncClick((args) => { // (2)
                return args[0].parentNode.parentNode.querySelector('lightning-base-combobox-item[data-value="actionAdvancedSearch"]');
            }, [item]);
/**
 * DOMノードを取得する(非同期)
 */
async function asyncGetNode(selector, args = {}, timeout = 10000) {
    const begin_time = new Date().getTime();
    let diff_time = 0;
    do {
        try {
            const item = selector(args); // (3)
            if (item) {
                return item;
            }
        }
        catch (err) {
            // do nothing
        }
        diff_time = new Date().getTime() - begin_time;
        await sleep(100);
    } while(diff_time < timeout);
}

/**
 * function selectorがtrueを返却するまで処理を中断する
 */ 
async function asyncConditionalWait(selector, args = {}, timeout = 10000) {
    const result = await asyncGetNode(selector, args, timeout);
    if (result) {
        return true;
    }
    return false;
}

/**
 * ノードのクリックを行う(非同期)
 */
async function asyncClick(selector, args = {}, timeout = 10000) { // (3)
    const result = await asyncGetNode(selector, args, timeout);
    result.click();
}

  1. CDPInputText()でテキストの入力を行います。どういうわけか、1回ではうまくいかず、2回入力で文字列が反映されます。
  2. テキスト入力をすると検索候補が表示されます。「雪村 アオイ」の列をクリックすれば、今回の記事は完成なのですが、人物検索であるため同姓同名のレコードが2件以上表示される可能性もあります。そのため今回は虫眼鏡マークの列をクリックします。
  3. ただし、この検索候補が表示されるまでには時間がかかる場合があるため、表示されるまで待機する処理を加える必要があります。
    asyncGetNode()ではタイムアウトになるまで繰り返し要素の取得処理を試みます。
    存在していない要素にアクセスをするとNotExistsなエラーが発生するため、例外で握りつぶすようにしています。

それぞれのメソッドの役割は以下の通りです。

  • asyncGetNode:非同期で待機し、ノードが取得出来たときにそのノードを返却する。
  • asyncConditionalWait:非同期で待機し、ノードが取得出来たときにtrueを返却する。
  • asyncClick: 非同期で待機し、ノードが取得できたとき、そのノードをクリックする。

モーダルの操作

            // モーダル画面が表示されるまで待機 
            await asyncConditionalWait((args) => { // (1)
                const modal = document.querySelector('.modal-container');
                const item1 = modal.querySelector('a[data-refid="recordId"]');
                const item2 = modal.querySelector('div[role="region"]');
                return item1 || item2;
            });

            // アイテム または キャンセルボタンをクリック
            const modal = document.querySelector('.modal-container'); // (2)
            const item1 = modal.querySelector('a[data-refid="recordId"]');
            if (item1) {
                item1.click();
            } else {
                const cancel_button = modal.querySelector('button[title="キャンセル"]');
                cancel_button.click();
            }
            
            // モーダル画面が閉じるまで待機
            await asyncConditionalWait((args) => { // (3)
                const modal = document.querySelector('.modal-container');
                return (!modal);
            });

検索候補をクリックすると、キャプチャのような画面が表示されます。
注意点として、モーダル自体が表示されてから検索結果が表示されるまでの間も時間がかかります。
そのため、モーダルのルートノードだけ存在チェックを行い、検索結果のノードを参照する際に取得失敗でエラーになる可能性があります。

  1. モーダルの検索結果「結果がありません」または「1件のレコード」が表示されるまで待機します。
  2. 検索結果なしであればキャンセルボタンを、ありであればレコードのリンク化している名前をクリックします。
  3. モーダルは閉じるときも少しだけ時間がかかります。1. の処理とは反対で、否定演算子を付けて(!modal)ノードが存在しなくなるまで待機します。

はまりやすいポイント

  • ノード取得で「undefined」になってしまうようであれば、まだノードが存在していない可能性を疑う。