Що таке WebSocket і чим він відрізняється від звичайного HTTP запиту?

Щоб зрозуміти WebSocket — спочатку треба зрозуміти як працює звичайний HTTP.

HTTP запит — як SMS:

Ти відправляєш повідомлення → сервер відповідає → з’єднання закривається. Щоразу коли треба щось отримати — треба відправити новий запит. Кожен запит це окреме з’єднання яке відкривається і закривається. Ефективно для сайтів де дані рідко змінюються — завантажив сторінку, все, чекати нічого.

WebSocket — як телефонна розмова:

З’єднання відкривається один раз і лишається відкритим. Обидві сторони можуть надсилати повідомлення одна одній в будь-який момент без нового запиту. Ефективно коли потрібен постійний обмін даними в реальному часі — чати, онлайн-ігри, фінансові тікери, і саме Playwright для керування браузером.

Чому Playwright використовує WebSocket: Playwright повинен постійно надсилати команди браузеру і отримувати результати. Якби використовував HTTP — кожна дія (clickfillevaluate) потребувала б нового з’єднання. WebSocket відкривається один раз і через нього проходять всі команди протягом всього тесту — швидко і ефективно.

Схема: HTTP vs WebSocket

// ── HTTP — нове з'єднання на кожен запит ─────────────────────────
//
// Клієнт         Сервер
// ───────        ──────
// "GET /page" ──────►  з'єднання відкрилось
// ◄────── HTML         з'єднання закрилось
//
// "GET /api/data" ───► нове з'єднання
// ◄──── JSON           закрилось
//
// Кожен запит = відкрити + закрити з'єднання

// ── WebSocket — одне з'єднання на весь час ───────────────────────
//
// Клієнт         Сервер
// ───────        ──────
// "привіт" ────────►   з'єднання відкрилось — І ЛИШАЄТЬСЯ ВІДКРИТИМ
// ◄──── "привіт"
// "клікни .btn" ───►   (те саме з'єднання)
// ◄──── "ok"
// "заповни #input" ►   (те саме з'єднання)
// ◄──── "ok"
// ...                  з'єднання закривається тільки в кінці

Як WebSocket виглядає в Playwright — під капотом

// Ти пишеш звичайний код тесту:
await page.goto('/products');
await page.locator('.add-to-cart').click();
await page.fill('#promo', 'SAVE10');

// Під капотом Playwright надсилає через WebSocket команди браузеру:
// → { method: "Page.navigate", params: { url: "/products" } }
// ← { result: { frameId: "..." } }
//
// → { method: "DOM.querySelector", params: { selector: ".add-to-cart" } }
// ← { result: { nodeId: 42 } }
//
// → { method: "Input.dispatchMouseEvent", params: { type: "click", ... } }
// ← { result: {} }
//
// Всі ці повідомлення йдуть через ОДНЕ WebSocket з'єднання
// Це і є Chrome DevTools Protocol (CDP) — мова на якій Playwright говорить з браузером

TypeScript — WebSocket в тестах: page.on(‘websocket’)

// Окрема тема: WebSocket на самій сторінці
// Якщо сторінка використовує WebSocket для реалтайм даних (чат, тікери) —
// Playwright дозволяє слухати ці з'єднання через page.on('websocket')

test('WebSocket отримує оновлення ціни', async ({ page }) => {
  const wsMessages: string[] = [];

  // Підписуємось на WebSocket з'єднання сторінки
  page.on('websocket', ws => {
    console.log('WS відкрито:', ws.url());

    // Слухаємо фрейми які приходять від сервера
    ws.on('framereceived', event => {
      wsMessages.push(event.payload as string);
    });

    ws.on('close', () => console.log('WS закрито'));
  });

  await page.goto('/prices');

  // Чекаємо поки прийде повідомлення про ціну
  // ✅ Правильно: wsMessages живе в Node.js — перевіряємо через expect.poll
  await expect.poll(() => wsMessages.length, { timeout: 10_000 })
    .toBeGreaterThan(0);
  // ❌ НЕ ПРАВИЛЬНО: window.wsMessages не існує в браузері!
  // await page.waitForFunction(() => (window as any).wsMessages?.length > 0);
  // wsMessages — це змінна Node.js, вона недоступна в контексті браузера

  // Перевіряємо вміст WebSocket повідомлення
  const priceUpdate = JSON.parse(wsMessages[0]);
  expect(priceUpdate).toHaveProperty('price');
});

💡 Два різних WebSocket в контексті Playwright:
1. WebSocket між Playwright і браузером — внутрішній, завжди є, через нього Playwright надсилає команди. Це Chrome DevTools Protocol (CDP). Ти його не бачиш, він просто існує під капотом.
2. WebSocket на самій сторінці — якщо сайт використовує WebSocket для реалтайм даних (чат, стрімінг цін). Цей можна слухати через page.on('websocket').

🔧 TypeScript / JS патерни в цьому прикладіpage.on('event', callback) — патерн підписки на події (Observer/EventEmitter): передаємо callback-функцію яка викличеться коли подія відбудеться; ws => — стрілочна функція як callback; ws.on('framereceived', event => {}) — вкладені підписки на події; new DataTransfer() — створення екземпляру класу через new.

🗣 Як читати вголос: WebSocket — це протокол для постійного двостороннього з’єднання між двома програмами. Відрізняється від звичайного HTTP тим що HTTP це запит-відповідь-закрито, кожна взаємодія це окреме з’єднання — як SMS. WebSocket відкривається один раз і лишається відкритим, обидві сторони можуть надсилати повідомлення в будь-який момент — як телефонна розмова. Playwright використовує WebSocket щоб керувати браузером: на початку тесту відкривається одне з’єднання через яке проходять всі команди — navigate, click, fill, evaluate — і всі відповіді від браузера. Це Chrome DevTools Protocol, CDP — мова на якій Playwright говорить з браузером. Це внутрішнє з’єднання яке ти не бачиш але воно є завжди. Окремо від цього, якщо сама сторінка яку ти тестуєш використовує WebSocket для реалтайм даних — наприклад чат або стрімінг цін — ти можеш слухати ці з’єднання через page.on('websocket') і перевіряти вміст повідомлень які приходять від сервера.