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に出力します。
ページの構造を見ると、チャットメッセージのコンテナはdiv#messages
で、チャットメッセージはその配下のdiv.message
です。
これが分かれば、早速作成します。
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}`);
}
}
});
結果
$ 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で一気に大量なアクセスが発生し、エラーになる可能性があるので、lodash の debounce()
や throttle()
でアクセス制限するといいです。