BATONZ Tech Blog

M&Aプラットフォーム運営の株式会社バトンズによる技術ブログです。

ArcブラウザBoost機能でMagicPodを自分好みにカスタマイズ!

こんにちは。株式会社バトンズでエンジニアをしている鈴木です。

バトンズでは2024年度からMagicPodを導入し、QAチームでのテスト自動化を推進しています。

私自身も今年の11月からMagicPodを使用し始め、日々のテスト業務をより効率化し、快適にする方法を模索しています。その中で、MagicPodのUIや挙動をカスタマイズすることで、さらに使い勝手を向上させられるのではないかと考えました。

そこで活用したのが、私が普段愛用しているWebブラウザArcに搭載されているBoost機能です。この機能を活用して、以下のようなカスタマイズを実現しました。

  1. テスト実行時のスクロール追従
    テスト実行中のステップにスクロール位置を自動で追従させることで、手動でスクロールする手間を削減しました。この工夫により、テストの確認作業をスムーズに進められるようになりました。

  2. コメントステップの視認性向上
    コメントを強調表示することで、重要なメモや注意点を一目で把握できるようにしました。これにより、コメントの見落としを防ぎ、作業の精度が向上しました。

  3. 実行結果から編集画面へのスムーズな遷移
    実行結果のキャプチャから、対応する編集画面のステップを直接開けるようにしました。このカスタマイズにより、手間を省き、作業効率が向上しました。

(ArcでなくともTampermonkeyのようなChrome拡張を利用すると、同様のカスタマイズはできそうです。)

※MagicPodのWebページの内部実装に依存したカスタマイズ手法になっているため、MagicPodの仕様変更により動作しなくなる可能性があります。その点ご注意ください。

以降では、まずArcのBoost機能について簡単に紹介した後、カスタマイズ内容の詳細について説明いたします。

1. ArcのBoost機能について

Boost機能を使うには、カスタマイズ対象のWebサイトを開いた状態で、サイドバーのブラシアイコンをクリックします。

すると、以下のようなBoostのウィンドウが表示されます。

上記ウィンドウの「Code」をクリックすると、WebサイトをカスタマイズするためのCSS、JavaScriptを記述するためのウィンドウが表示されます。

ここに記載したコードが、Webサイトの表示時に実行されます。

保存したBoostについては、以下の「My Boost」からアクセスします。

2. カスタマイズ内容

ここからは具体的なカスタマイズ内容についてご紹介します。

2.1 テスト実行時、実行しているステップにスクロール位置を自動追従させる

1つめは、テスト実行時の挙動の変更の話です。

MagicPod上でテストケースを実行すると、現在実行しているステップに対して、下図のような矢印アイコンが表示されるかと思います。

長いテストケースですと、この矢印を見失うことがあったので、自動でスクロール位置を追従させてみました。

以下のJavaScriptで実現しています。

// テスト実行中に、現在実行しているステップに自動スクロールする
function enableScrollToSelectedDuringExecution() {
  // "no_scroll" パラメータが存在する場合はテスト実行時のスクロール追従をしない
  const params = new URLSearchParams(window.location.search);
  if (params.has('no_scroll')) {
    return
  }

  // MutationObserverのコールバック関数
  function scrollToSelected() {
    const selectedElement = document.querySelector('.test-case.selected');
    if (selectedElement) {
      const statusElement = document.querySelector('.test-run-status-name');
      if (statusElement && statusElement.textContent.trim() === "実行中") {
        selectedElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
    }
  }

  // 監視する対象要素(body全体を監視)
  const targetNode = document.body;

  // オプション: 子孫ノードと属性の変化も監視
  const config = { childList: true, subtree: true, attributes: true };

  // MutationObserverを作成し、コールバック関数を登録
  const observer = new MutationObserver(scrollToSelected);

  // 監視の開始
  observer.observe(targetNode, config);

  // 初回実行
  scrollToSelected();
}

document.addEventListener('DOMContentLoaded', () => {

  const currentPath = window.location.pathname;

  if (currentPath.endsWith('/edit-testcase/')) {
    enableScrollToSelectedDuringExecution();
  }
});

補足としては、テストケースの編集画面のURLに?no_scrollというクエリパラメータを付与すると、自動スクロールはしないようにする考慮を入れています。

2.2コメントのステップを目立たせる

続いてはコメントに関するカスタマイズです。

テストの意図の補足等のため、コメントをステップの間に記載することがあると思います。

可読性のために、このコメントを目立たせたいと感じました。
コメントステップの上に空のコメントステップを追加したり、絵文字を追加したりして目立たせる手法があるかと思うのですが、今回は思い切って赤線を入れて目立たせてみました。

以下のJavaScriptで実現しています。

// コメントコマンドをわかりやすくする
function highlightCommentCommands() {
  function applyStyles() {
    const elements = document.querySelectorAll('.comment-instruction');

    elements.forEach(child => {
      const targetElement = child.parentElement?.parentElement;
      if (targetElement) {
        targetElement.style.borderTop = '2px solid red';
        targetElement.style.borderBottom = '2px solid red';
      }
    });
  }

  // MutationObserverのコールバック関数
  const observerCallback = (mutations) => {
    let shouldApplyStyles = false;
    mutations.forEach(mutation => {
      if (mutation.type === 'childList' || mutation.type === 'attributes') {
        shouldApplyStyles = true;
      }
    });
    if (shouldApplyStyles) {
      applyStyles();
    }
  };

  // 監視する対象要素(body全体を監視)
  const targetNode = document.body;

  // オプション: 子孫ノードの変化と属性の変化を監視
  const config = { childList: true, subtree: true, attributes: true };

  // MutationObserverを作成し、コールバック関数を登録
  const observer = new MutationObserver(observerCallback);

  // 監視の開始
  observer.observe(targetNode, config);
}

document.addEventListener('DOMContentLoaded', () => {

  const currentPath = window.location.pathname;

  if (currentPath.endsWith('/edit-testcase/')) {
    setTimeout(() => {
      highlightCommentCommands();
    }, 1000);
  }
});

2.3 実行結果のキャプチャから、編集画面のステップを開く

最後はテストの実行結果画面と編集画面を連携するカスタマイズです。

テストが失敗したとき、テスト結果の画面キャプチャのうち、失敗したステップの編集画面を開きたい場面がありました。

通常のテスト結果画面の画面キャプチャは以下のような表示かと思います。

この画面キャプチャの各ステップ番号にリンクを付与し、リンクをクリックすると、編集画面を開き、該当のステップまで自動スクロールするようにしてみました。また、該当のステップ番号をわかりやすくするため、赤で番号を点滅させています。

CSS

/* ハイライト用のスタイル */
.arc-boast-highlight {
  animation: redBlink 0.5s step-end infinite; /* 点滅を繰り返す */
}

/* 赤点滅のアニメーション */
@keyframes redBlink {
  50% {
    color: red; /* テキストを赤く */
  }
  100% {
    color: initial; /* 元の色に戻す */
  }
}

JavaScript

// クエリパラメータで指定されたステップ番号までスクロールする
function scrollToNumber() {
  // URLSearchParams を使ってクエリパラメータを取得
  const params = new URLSearchParams(window.location.search);

  // クエリパラメータ "no" の値を取得
  const noValue = params.get('no');

  // "no" パラメータが指定されている場合に処理を実行
  if (noValue) {
    // test-case-number クラスを持つすべての要素を取得
    const elements = document.querySelectorAll('.test-case-number');

    // 一致する要素を探してフォーカス
    elements.forEach(element => {
      if (element.textContent.trim() === noValue) {
        // フォーカスを設定
        element.scrollIntoView({ behavior: 'smooth', block: 'center' });
        element.focus();

        // 一時的に光らせるためのクラスを付与
        element.classList.add('arc-boast-highlight');

        // 一定時間後にクラスを削除
        setTimeout(() => {
          element.classList.remove('arc-boast-highlight');
        }, 3000); // 3秒間目立たせる
      }
    });
  }
}

// 画面キャプチャのステップ番号にリンクを付与する
function appendLinkToEditPage() {
  // ページ内のすべての<a>タグを取得
  const links = document.querySelectorAll('a');

  // hrefが"/edit-testcase/"で終わる最初の要素を探す
  const matchingHref = Array.from(links).find(link => link.href.endsWith('/edit-testcase/'))?.href;

  if (!matchingHref) {
    return;
  }

  // step-number クラスを持つすべての <td> 要素を取得
  const stepNumberCells = document.querySelectorAll('td.step-number');

  // 各 <td> 要素に <a> タグを追加
  stepNumberCells.forEach(td => {
    // <td> 内のテキストをトリム(前後のスペースを削除)
    const stepNumber = td.textContent.trim();

    // クエリパラメータ付きのURLを作成
    const linkHref = `${matchingHref}?no=${encodeURIComponent(stepNumber)}`;

    // <a> 要素を作成
    const link = document.createElement('a');
    link.href = linkHref;
    link.textContent = stepNumber; // 元のテキストをリンクテキストに設定
    link.style.textDecoration = 'none'; // 必要ならスタイルを追加

    // <td> の中身をクリアして <a> を挿入
    td.textContent = '';
    td.appendChild(link);
  });
}


document.addEventListener('DOMContentLoaded', () => {

  const currentPath = window.location.pathname;

  if (currentPath.endsWith('/edit-testcase/')) {
    setTimeout(() => {
      scrollToNumber();
    }, 3000);

  } else {
    setTimeout(() => {
      appendLinkToEditPage();
    }, 1000);
  }
});

3. まとめ

ArcブラウザのBoost機能を活用し、MagicPodの見た目や挙動を自分好みにカスタマイズした事例をご紹介しました。

少し強引なアプローチではありますが、こうした工夫が日々の業務改善のヒントになれば幸いです!

4. おまけ

私がBoostに登録しているCSS、JavaScriptの全量をまとめると以下の通りとなります。

CSS

/* ハイライト用のスタイル */
.arc-boast-highlight {
  animation: redBlink 0.5s step-end infinite; /* 点滅を繰り返す */
}

/* 赤点滅のアニメーション */
@keyframes redBlink {
  50% {
    color: red; /* テキストを赤く */
  }
  100% {
    color: initial; /* 元の色に戻す */
  }
}

JavaScript

// テスト実行中に、現在実行しているステップに自動スクロールする
function enableScrollToSelectedDuringExecution() {
  // "no_scroll" パラメータが存在する場合はテスト実行時のスクロール追従をしない
  const params = new URLSearchParams(window.location.search);
  if (params.has('no_scroll')) {
    return
  }

  // MutationObserverのコールバック関数
  function scrollToSelected() {
    const selectedElement = document.querySelector('.test-case.selected');
    if (selectedElement) {
      const statusElement = document.querySelector('.test-run-status-name');
      if (statusElement && statusElement.textContent.trim() === "実行中") {
        selectedElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
    }
  }

  // 監視する対象要素(body全体を監視)
  const targetNode = document.body;

  // オプション: 子孫ノードと属性の変化も監視
  const config = { childList: true, subtree: true, attributes: true };

  // MutationObserverを作成し、コールバック関数を登録
  const observer = new MutationObserver(scrollToSelected);

  // 監視の開始
  observer.observe(targetNode, config);

  // 初回実行
  scrollToSelected();
}

// コメントコマンドをわかりやすくする
function highlightCommentCommands() {
  function applyStyles() {
    const elements = document.querySelectorAll('.comment-instruction');

    elements.forEach(child => {
      const targetElement = child.parentElement?.parentElement;
      if (targetElement) {
        targetElement.style.borderTop = '2px solid red';
        targetElement.style.borderBottom = '2px solid red';
      }
    });
  }

  // MutationObserverのコールバック関数
  const observerCallback = (mutations) => {
    let shouldApplyStyles = false;
    mutations.forEach(mutation => {
      if (mutation.type === 'childList' || mutation.type === 'attributes') {
        shouldApplyStyles = true;
      }
    });
    if (shouldApplyStyles) {
      applyStyles();
    }
  };

  // 監視する対象要素(body全体を監視)
  const targetNode = document.body;

  // オプション: 子孫ノードの変化と属性の変化を監視
  const config = { childList: true, subtree: true, attributes: true };

  // MutationObserverを作成し、コールバック関数を登録
  const observer = new MutationObserver(observerCallback);

  // 監視の開始
  observer.observe(targetNode, config);
}

// クエリパラメータで指定されたステップ番号までスクロールする
function scrollToNumber() {
  // URLSearchParams を使ってクエリパラメータを取得
  const params = new URLSearchParams(window.location.search);

  // クエリパラメータ "no" の値を取得
  const noValue = params.get('no');

  // "no" パラメータが指定されている場合に処理を実行
  if (noValue) {
    // test-case-number クラスを持つすべての要素を取得
    const elements = document.querySelectorAll('.test-case-number');

    // 一致する要素を探してフォーカス
    elements.forEach(element => {
      if (element.textContent.trim() === noValue) {
        // フォーカスを設定
        element.scrollIntoView({ behavior: 'smooth', block: 'center' });
        element.focus();

        // 一時的に光らせるためのクラスを付与
        element.classList.add('arc-boast-highlight');

        // 一定時間後にクラスを削除
        setTimeout(() => {
          element.classList.remove('arc-boast-highlight');
        }, 3000); // 3秒間目立たせる
      }
    });
  }
}

// 画面キャプチャのステップ番号にリンクを付与する
function appendLinkToEditPage() {
  // ページ内のすべての<a>タグを取得
  const links = document.querySelectorAll('a');

  // hrefが"/edit-testcase/"で終わる最初の要素を探す
  const matchingHref = Array.from(links).find(link => link.href.endsWith('/edit-testcase/'))?.href;

  if (!matchingHref) {
    return;
  }

  // step-number クラスを持つすべての <td> 要素を取得
  const stepNumberCells = document.querySelectorAll('td.step-number');

  // 各 <td> 要素に <a> タグを追加
  stepNumberCells.forEach(td => {
    // <td> 内のテキストをトリム(前後のスペースを削除)
    const stepNumber = td.textContent.trim();

    // クエリパラメータ付きのURLを作成
    const linkHref = `${matchingHref}?no=${encodeURIComponent(stepNumber)}`;

    // <a> 要素を作成
    const link = document.createElement('a');
    link.href = linkHref;
    link.textContent = stepNumber; // 元のテキストをリンクテキストに設定
    link.style.textDecoration = 'none'; // 必要ならスタイルを追加

    // <td> の中身をクリアして <a> を挿入
    td.textContent = '';
    td.appendChild(link);
  });
}

document.addEventListener('DOMContentLoaded', () => {

  const currentPath = window.location.pathname;

  if (currentPath.endsWith('/edit-testcase/')) {
    enableScrollToSelectedDuringExecution();

    setTimeout(() => {
      highlightCommentCommands();
    }, 1000);

    setTimeout(() => {
      scrollToNumber();
    }, 3000);

  } else {
    setTimeout(() => {
      appendLinkToEditPage();
    }, 1000);
  }
});