2026年11月7日 9時58分

ブラウザ操作の自動化を理解する

最終更新:2026年11月7日 10時28分

ブラウザ操作の自動化を理解する
(async () => {
  const doc = $0?.ownerDocument ?? document;

  const sleep = ms =>
    new Promise(resolve => setTimeout(resolve, ms));

  const isVisible = element =>
    element && element.getClientRects().length > 0;

  const waitFor = async (callback, timeout = 5000) => {
    const startedAt = Date.now();

    while (Date.now() - startedAt < timeout) {
      const result = callback();

      if (result) {
        return result;
      }

      await sleep(100);
    }

    return null;
  };

  const getRows = () =>
    [...document.querySelectorAll(
      'tbody.mdc-data-table__content > tr.mat-mdc-row'
    )];

  const initialCount = getRows().length;

  if (
    !confirm(
      `${initialCount}件のNotebookをすべて完全に削除しますか?`
    )
  ) {
    console.log('削除を中止しました');
    return;
  }

  let deletedCount = 0;

  while (getRows().length > 0) {
    const row = getRows()[0];

    const title =
      row.querySelector('a.project-table-title')
        ?.textContent.trim() ?? 'タイトル不明';

    console.log(`処理中: ${title}`);

    // 1. 三点メニューを開く
    const menuButton = row.querySelector(
      'button[aria-label="プロジェクトの操作メニュー"]'
    );

    if (!menuButton) {
      console.error('三点メニューが見つかりません:', title);
      break;
    }

    menuButton.click();

    // 2. メニューの表示を待つ
    const menu = await waitFor(() =>
      [...document.querySelectorAll(
        '[role="menu"], .mat-mdc-menu-content'
      )].find(isVisible)
    );

    if (!menu) {
      console.error('操作メニューが表示されません:', title);
      break;
    }

    // 3. メニュー内の削除ボタンを探す
    const deleteMenuButton = [
      ...menu.querySelectorAll(
        'button, [role="menuitem"]'
      )
    ].find(element =>
      isVisible(element) &&
      element.innerText.includes('削除')
    );

    if (!deleteMenuButton) {
      console.error(
        'メニューの「削除」が見つかりません:',
        title,
        menu.innerText
      );
      break;
    }

    deleteMenuButton.click();

    // 4. 最終確認ダイアログを待つ
    const confirmDialog = await waitFor(() => {
      const dialog = doc.querySelector(
        'confirm-dialog .dialog-container[role="dialog"]'
      );

      return isVisible(dialog) ? dialog : null;
    }, 10000);

    if (!confirmDialog) {
      console.error(
        '最終確認ダイアログが表示されません:',
        title
      );
      break;
    }

    // 5. 最終確認の削除ボタンが表示されるまで待つ
    const finalDeleteButton = await waitFor(() => {
      const buttons = [
        ...confirmDialog.querySelectorAll('button')
      ];

      return (
        buttons.find(button =>
          isVisible(button) &&
          !button.disabled &&
          button.textContent.trim() === '削除'
        ) ?? null
      );
    }, 10000);

    if (!finalDeleteButton) {
      console.error(
        '最終確認の削除ボタンが見つかりません:',
        title,
        [...confirmDialog.querySelectorAll('button')]
          .map(button => button.textContent.trim())
      );
      break;
    }

    finalDeleteButton.click();

    // 6. 対象行が一覧から消えるまで待つ
    const removed = await waitFor(
      () => (!document.contains(row) ? true : null),
      10000
    );

    if (!removed) {
      console.error(
        '削除後もNotebookが一覧に残っています:',
        title
      );
      break;
    }

    deletedCount++;

    console.log(
      `${deletedCount}/${initialCount}件削除完了: ${title}`
    );

    await sleep(500);
  }

  console.log(
    `処理終了:${deletedCount}/${initialCount}件を削除しました`
  );
})();

で

が実現した

その理由をソースコードベースで理解していこう

consoleに先ほどのコードを入力すると、「このノートブックを削除しますか?」が表示される

説明用のコメントアウトを追加した


(async () => {

  // ==========================================
  // 1. 初期設定
  // ==========================================

  // Chrome DevToolsで選択中のHTML要素を表す「$0」から、
  // その要素が属するdocumentを取得する。
  //
  // ?. は「$0が存在する場合だけ参照する」という意味。
  // ?? は「左側がnullまたはundefinedなら右側を使う」という意味。
  //
  // $0が選択されていなければ、通常のdocumentを使う。
  const doc = $0?.ownerDocument ?? document;


  // 指定したミリ秒だけ処理を待機する関数。
  //
  // 例:await sleep(500);
  // → 0.5秒待ってから次の処理へ進む。
  //
  // Promiseは、非同期処理の完了を表すオブジェクト。
  const sleep = ms =>
    new Promise(resolve => setTimeout(resolve, ms));


  // HTML要素が画面上に表示されているか確認する関数。
  //
  // elementが存在し、
  // getClientRects()で表示領域が取得できればtrueを返す。
  //
  // ※画面外にスクロールされている場合なども
  // trueになることがあるため、完全な可視性判定ではない。
  const isVisible = element =>
    element && element.getClientRects().length > 0;


  // 指定した条件が成立するまで繰り返し確認する関数。
  //
  // callback:成立を確認したい条件
  // timeout:最大待機時間(初期値5000ミリ秒=5秒)
  //
  // 条件が成立したら、その結果を返す。
  // 時間切れになったらnullを返す。
  const waitFor = async (callback, timeout = 5000) => {

    // 待機を開始した時刻を記録する。
    const startedAt = Date.now();

    // 最大待機時間を超えるまで繰り返す。
    while (Date.now() - startedAt < timeout) {

      // 条件を確認する関数を実行する。
      const result = callback();

      // 条件が成立した場合は結果を返して終了。
      if (result) {
        return result;
      }

      // 条件が成立していなければ0.1秒待つ。
      await sleep(100);
    }

    // 制限時間内に条件が成立しなかった場合。
    return null;
  };


  // ==========================================
  // 2. Notebook一覧を取得する
  // ==========================================

  // HTMLの中からNotebook一覧の行(tr)を取得する関数。
  //
  // querySelectorAll()は、
  // CSSセレクターに一致するHTML要素をすべて取得する。
  //
  // tbody.mdc-data-table__content
  // → 指定されたclassを持つtbody
  //
  // > tr.mat-mdc-row
  // → その直下にある指定classのtr
  //
  // [...] は取得結果を配列に変換する書き方。
  const getRows = () =>
    [...document.querySelectorAll(
      'tbody.mdc-data-table__content > tr.mat-mdc-row'
    )];


  // 最初に表示されているNotebookの件数を取得する。
  //
  // .length は配列の要素数を表す。
  const initialCount = getRows().length;


  // ==========================================
  // 3. 削除してよいか確認する
  // ==========================================

  // confirm()で確認ダイアログを表示する。
  //
  // OKを押した場合:true
  // キャンセルした場合:false
  //
  // ! はtrueとfalseを反転する演算子。
  // キャンセルされた場合にifの中へ入る。
  if (
    !confirm(
      `${initialCount}件のNotebookをすべて完全に削除しますか?`
    )
  ) {

    // キャンセルされたことをConsoleに表示。
    console.log('削除を中止しました');

    // returnでプログラム全体を終了する。
    return;
  }


  // 削除に成功した件数を記録する変数。
  //
  // letは後から値を変更できる変数の宣言方法。
  let deletedCount = 0;


  // ==========================================
  // 4. Notebookを1件ずつ削除する
  // ==========================================

  // Notebookの行が1件以上残っている間、
  // 以下の処理を繰り返す。
  //
  // whileは条件がtrueの間、処理を繰り返す構文。
  while (getRows().length > 0) {

    // 現在のNotebook一覧の先頭行を取得する。
    //
    // [0] は配列の最初の要素。
    //
    // 削除に成功すると行が消えるため、
    // 次のNotebookが先頭になる。
    const row = getRows()[0];


    // Notebookのタイトルを取得する。
    //
    // row.querySelector()でタイトルのHTML要素を探す。
    //
    // ?.textContent
    // → 要素が存在する場合だけ文字列を取得。
    //
    // trim()
    // → 文字列の前後の空白や改行を除去。
    //
    // ?? 'タイトル不明'
    // → タイトルを取得できなかった場合の代替文字列。
    const title =
      row.querySelector('a.project-table-title')
        ?.textContent.trim() ?? 'タイトル不明';


    // 現在処理しているNotebookをConsoleに表示。
    console.log(`処理中: ${title}`);


    // ------------------------------------------
    // 4-1. 三点メニューを開く
    // ------------------------------------------

    // 現在の行の中から三点メニューボタンを探す。
    //
    // [aria-label="..."] は、
    // 指定したaria-label属性を持つ要素を意味する。
    const menuButton = row.querySelector(
      'button[aria-label="プロジェクトの操作メニュー"]'
    );


    // 三点メニューが見つからなかった場合。
    if (!menuButton) {

      // エラー内容とNotebookタイトルを表示する。
      console.error('三点メニューが見つかりません:', title);

      // breakでwhileループを終了する。
      // これ以上のNotebookは削除しない。
      break;
    }


    // 三点メニューをクリックする。
    menuButton.click();


    // ------------------------------------------
    // 4-2. メニューの表示を待つ
    // ------------------------------------------

    // メニューが表示されるまで最大5秒待つ。
    //
    // メニューはクリック直後には表示されない場合がある。
    //
    // document.querySelectorAll()で候補を取得し、
    // find()で表示中の最初のメニューを探す。
    const menu = await waitFor(() =>
      [...document.querySelectorAll(
        '[role="menu"], .mat-mdc-menu-content'
      )].find(isVisible)
    );


    // メニューが表示されなかった場合は中止する。
    if (!menu) {
      console.error('操作メニューが表示されません:', title);
      break;
    }


    // ------------------------------------------
    // 4-3. メニュー内の「削除」を探す
    // ------------------------------------------

    // メニュー内のbutton要素と、
    // role="menuitem"を持つ要素を取得する。
    //
    // その中から以下の条件を満たす要素を探す。
    //
    // ① 画面に表示されている
    // ② 表示文字列に「削除」が含まれている
    const deleteMenuButton = [
      ...menu.querySelectorAll(
        'button, [role="menuitem"]'
      )
    ].find(element =>
      isVisible(element) &&
      element.innerText.includes('削除')
    );


    // 削除メニューが見つからなかった場合。
    if (!deleteMenuButton) {
      console.error(
        'メニューの「削除」が見つかりません:',
        title,
        menu.innerText
      );
      break;
    }


    // メニュー内の「削除」をクリックする。
    //
    // ここでは通常、最終確認ダイアログが開く。
    deleteMenuButton.click();


    // ------------------------------------------
    // 4-4. 最終確認ダイアログを待つ
    // ------------------------------------------

    // 削除確認ダイアログが表示されるまで
    // 最大10秒待つ。
    //
    // confirm-dialog
    // → 確認ダイアログのコンポーネント
    //
    // .dialog-container[role="dialog"]
    // → ダイアログ本体のHTML要素
    const confirmDialog = await waitFor(() => {

      const dialog = doc.querySelector(
        'confirm-dialog .dialog-container[role="dialog"]'
      );

      // ダイアログが表示されていれば返す。
      // 表示されていなければnullを返す。
      return isVisible(dialog) ? dialog : null;

    }, 10000);


    // 確認ダイアログが表示されなかった場合。
    if (!confirmDialog) {
      console.error(
        '最終確認ダイアログが表示されません:',
        title
      );
      break;
    }


    // ------------------------------------------
    // 4-5. 最終確認の「削除」を探す
    // ------------------------------------------

    // ダイアログ内のボタンを取得し、
    // 以下の条件を満たすボタンを探す。
    //
    // ① 表示されている
    // ② disabled(無効状態)ではない
    // ③ ボタンの文字列が「削除」と完全一致する
    //
    // 最大10秒待つ。
    const finalDeleteButton = await waitFor(() => {

      const buttons = [
        ...confirmDialog.querySelectorAll('button')
      ];

      return (
        buttons.find(button =>
          isVisible(button) &&
          !button.disabled &&
          button.textContent.trim() === '削除'
        ) ?? null
      );

    }, 10000);


    // 最終確認ボタンが見つからなかった場合。
    if (!finalDeleteButton) {
      console.error(
        '最終確認の削除ボタンが見つかりません:',
        title,

        // 調査のため、ダイアログ内の
        // すべてのボタンの文字列を出力する。
        [...confirmDialog.querySelectorAll('button')]
          .map(button => button.textContent.trim())
      );
      break;
    }


    // 最終確認の「削除」ボタンをクリックする。
    //
    // ここで実際のNotebook削除が実行される。
    finalDeleteButton.click();


    // ------------------------------------------
    // 4-6. Notebookが一覧から消えたか確認
    // ------------------------------------------

    // 削除したNotebookの行が
    // HTMLから取り除かれるまで最大10秒待つ。
    //
    // document.contains(row)
    // → document内にrowが存在すればtrue
    //
    // !document.contains(row)
    // → rowが存在しなければtrue
    const removed = await waitFor(
      () => (!document.contains(row) ? true : null),
      10000
    );


    // 行が消えなかった場合は処理を中止する。
    if (!removed) {
      console.error(
        '削除後もNotebookが一覧に残っています:',
        title
      );
      break;
    }


    // 削除成功件数を1増やす。
    //
    // deletedCount++ は
    // deletedCount = deletedCount + 1 と同じ。
    deletedCount++;


    // 現在の進捗をConsoleに表示する。
    //
    // 例:3/192件削除完了: Notebook A
    console.log(
      `${deletedCount}/${initialCount}件削除完了: ${title}`
    );


    // 次のNotebookを処理する前に0.5秒待つ。
    await sleep(500);
  }


  // ==========================================
  // 5. 最終結果を表示する
  // ==========================================

  // whileループが終了した後に、
  // 削除成功件数と最初の件数を表示する。
  console.log(
    `処理終了:${deletedCount}/${initialCount}件を削除しました`
  );

})();

全体の流れ

1. 初期設定

画面の状態を確認しながら処理を進める仕組みを最初に用意しています。

2. Notebook一覧を取得する

ブラウザに表示されているNotebookの一覧を取得し、削除対象が何件あるかを確認する準備を行っています。

3. 削除してよいか確認する

4. Notebookを1件ずつ削除する

処理中: 💻 フリーランス独立の歩みと戦略の記録

4-1. 三点メニューを開く

4-2. メニューの表示を待つ

4-3. メニュー内の「削除」を探す

4-4. 最終確認ダイアログを待つ

4-5. 最終確認の「削除」を探す

4-6. Notebookが一覧から消えたか確認

1/15件削除完了: 💻 フリーランス独立の歩みと戦略の記録

5. 最終結果を表示する

処理終了:36/15件を削除しました

※Notebook一覧が動的に更新されているため、実際の件数が最初の母数を上回っています

カテゴリー:技術・開発