0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TL;DR

// 該当Webpageで対象Dom要素の変化を監視
await page.locator('[id=chat-container]').evaluate(element => {
  const observer = new MutationObserver(mutationsList => {
    if (mutationsList[0]?.type !== 'childList') return;
    // 該当変化をConsoleに出力
    console.log(`@@NewMessage@@ ${mutationsList[0].addedNodes[0].textContent}`);
  });
  observer.observe(element, { childList: true, subtree: true });
});

// PlaywrightでConsoleの出力を監視
page.on('console', msg => {
  if (msg.type() === 'log') {
    const text = msg.text();
    if (text.startsWith('@@NewMessage@@')) {
      const message = text.replace('@@NewMessage@@', '').trim();
      console.log(`Received message: ${message}`);
    }
  }
});

はじめに

PlaywrightはMicrosoft社が開発したOpen sourceのブラウザ自動化ツールです。E2E テストをはじめ、いろんな便利な使い方があります。

PlaywrightはいくつかのEventListenerを提供していますが、特定のDom要素の変化を監視するListenerはありません。

方法

Playwrightが提供しているListenerの中、ConsoleMessageは比較的簡単に発火できて、Dialogのようなページの動きを停止させる副作用もありません。

そのConsoleMessage Eventを活用して、Dom要素変化のListenerを作成します。

下準備

デモンストレーションのために、Google Geminiで定期にメッセージを送信するダミーのチャットアプリを作りました。

一枚のHTMLですが、長いので折りたたみます。

Auto-Chat.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Auto Chat Demo</title>
  <style>
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      margin: 20px;
      background-color: #f0f0f0;
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
    }

    #chat-container {
      width: 90%;
      max-width: 600px;
      height: 80vh;
      border: 1px solid #ddd;
      border-radius: 10px;
      background-color: #fff;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
      display: flex;
      flex-direction: column;
      overflow: hidden;
    }

    #messages {
      flex-grow: 1;
      overflow-y: auto;
      padding: 15px;
      display: flex;
      flex-direction: column;
    }

    .message {
      clear: both;
      margin-bottom: 10px;
      padding: 10px 15px;
      border-radius: 20px;
      max-width: 80%;
      word-wrap: break-word;
      position: relative;
    }

    .sent {
      background-color: #DCF8C6;
      color: #333;
      align-self: flex-end;
      border-bottom-right-radius: 5px;
    }

    .received {
      background-color: #E1F5FE;
      color: #333;
      align-self: flex-start;
      border-bottom-left-radius: 5px;
    }

    .message .username {
      font-size: 0.9em;
      font-weight: bold;
      color: #555;
      margin-right: 5px;
    }

    .message .timestamp {
      font-size: 0.7em;
      color: #888;
      bottom: 3px;
      right: 8px;
    }

    .received .timestamp {
      left: 10px;
    }

    #message-form {
      padding: 10px 15px;
      border-top: 1px solid #eee;
      display: flex;
    }

    #message-form label {
      margin-right: 10px;
      font-size: 0.9em;
      color: #555;
    }

    #message-form input[type="range"] {
      flex-grow: 1;
      margin-right: 10px;
    }
  </style>
</head>
<body>
  <div id="chat-container">
    <div id="messages"></div>
    <form id="message-form">
      <label for="send-speed">Send Speed: ( <span id="current-speed">12</span> per 60s )</label>
      <input type="range" name="send-speed" id="send-speed" min="1" max="20" value="12" step="1" onchange="sendSpeedChanged(this.value)">
    </form>
  </div>

  <script>
    const messagesDiv = document.getElementById('messages');

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

    let sendSpeed = parseInt(document.getElementById('send-speed').value, 10);
    function sendSpeedChanged(speed) {
      const currentSpeed = parseInt(speed, 10);
      document.getElementById('current-speed').textContent = currentSpeed;
      sendSpeed = currentSpeed;
    }

    (async () => {
      let timeout = 10000;
      let randValue = 0; 
      while (true) {
        timeout = 60000 / sendSpeed; // 60秒を送信速度で割る
        randValue = Math.floor(Math.random() * 100) + 1 // 1から100のランダムな値を生成

        if (randValue % 2 === 0) { // 偶数なら送信メッセージ
          let timestamp = new Date().toLocaleTimeString();
          appendMessage('User', 'Hello, this is a test message!', timestamp, true); // trueは送信メッセージを示す
        } else { // 奇数なら受信メッセージ
          let timestamp = new Date().toLocaleTimeString();
          appendMessage('Server', 'This is a response message!', timestamp, false); // falseは受信メッセージを示す
        }

        await dealy(timeout); // 指定された時間待機
      }
    })();

    function appendMessage(username, text, timestamp, isSent) {
      const messageElement = document.createElement('div');
      messageElement.classList.add('message');
      messageElement.classList.add(isSent ? 'sent' : 'received');
      messageElement.innerHTML = `
        <span class="username">
          <span class="name">${username}</span>
          <span class="timestamp">${timestamp}</span>
        </span>
        <span class="content">${text}</span>`;
      messagesDiv.appendChild(messageElement);
      messagesDiv.scrollTop = messagesDiv.scrollHeight; // スクロールを一番下へ
    }
  </script>
</body>
</html>

目的ページでDom要素の監視

Playwrightはpage.evaluate()でページ画面上にJSコードの実行ができるので、そこでMutationObserverを使って、目標のDom要素を監視し、その変化を特定のフォーマットのメッセージでConsoleに出力します。

image.png

ページの構造を見ると、チャットメッセージのコンテナはdiv#messagesで、チャットメッセージはその配下のdiv.messageです。

これが分かれば、早速作成します。

playwright.js
import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: false });
const context = await browser.newContext({ viewport: { width: 1024, height: 600 } });
const page = await context.newPage();

await page.goto('http://127.0.0.1:13579/Auto-Chat.html');

const chatContainer = page.locator('#messages');
await chatContainer.scrollIntoViewIfNeeded();

// メッセージコンテナの変化を監視
await chatContainer.evaluate(element => {
  const observer = new MutationObserver(mutationsList => {
    mutationsList.forEach(mutation => {
      if (mutation.type === 'childList') {
        mutation.addedNodes.forEach(node => {
          const res = {
            username: node.querySelector('.username .name')?.textContent,
            content: node.querySelector('.content')?.textContent,
            timestamp: node.querySelector('.timestamp')?.textContent,
          }
          console.log(`@@NewMessage@@ ${JSON.stringify(res)}`); // 情報をJSON形式でコンソールに出力
        });
      }
    });
  });
  observer.observe(element, { childList: true, /* subtree: true */}); // 孫要素も監視する場合はsubtree: trueを有効にする
});

PlaywrightでConsoleMessageの監視

下記のロジックをなるべく page.goto() でページ移動する前に追加した方がいいです。
またはpage.on('load', pg => {})で包むようにしてください。

// コンソールEventを監視
page.on('console', msg => {
  if (msg.type() === 'log') {
    const text = msg.text();
    if (text.startsWith('@@NewMessage@@')) {
      const res = JSON.parse(text.replace('@@NewMessage@@', '').trim());
      console.log(`Received message: ${res.username}: ${res.content} at ${res.timestamp}`);
    }
  }
});

結果

image.png

$ node playwright.mjs
Received message: User: Hello, this is a test message! at 14:45:50
Received message: Server: This is a response message! at 14:45:55
Received message: User: Hello, this is a test message! at 14:46:00
Received message: User: Hello, this is a test message! at 14:46:05
Received message: Server: This is a response message! at 14:46:10
Received message: Server: This is a response message! at 14:46:15
Received message: User: Hello, this is a test message! at 14:46:20
Received message: Server: This is a response message! at 14:46:25
Received message: User: Hello, this is a test message! at 14:46:30

余談

監視したいページのセキュリティポリシーにより、MutationObserverでDom要素の変化こそ監視できたが、中身は空のnodeListの可能性もあります。

その時はEventだけ発火させ、Eventを検知した際Playwirghtの方で要素の中身を取得するようにしてください。

ただし、Playwrightで一気に大量なアクセスが発生し、エラーになる可能性があるので、lodashdebounce()throttle() でアクセス制限するといいです。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?