WEB-COMPONENTS TESTING UNIT-TESTING INTEGRATION-TESTING E2E-TESTING FRONTEND QUALITY-ASSURANCE JAVASCRIPT SHADOW-DOM

Menguji Web Components: Panduan Praktis untuk Unit, Integrasi, dan End-to-End Testing

⏱️ 15 menit baca
👨‍💻

Menguji Web Components: Panduan Praktis untuk Unit, Integrasi, dan End-to-End Testing

Halo para developer!

Pernahkah Anda merasa seperti sedang membangun sebuah LEGO raksasa di proyek web Anda? Setiap komponen adalah sebuah balok, dan Web Components hadir sebagai standar emas untuk membuat balok-balok ini benar-benar independen dan reusable. Mereka menawarkan janji modularitas yang luar biasa, bekerja di framework apa pun atau bahkan tanpa framework sama sekali.

Namun, seperti halnya balok LEGO, jika ada satu balok yang cacat atau tidak terpasang dengan benar, seluruh struktur bisa runtuh. Di sinilah testing menjadi sangat krusial. Menguji Web Components memiliki keunikan tersendiri dibandingkan menguji komponen framework tradisional (seperti React atau Vue), karena sifatnya yang framework-agnostic dan penggunaan Shadow DOM untuk enkapsulasi.

Artikel ini akan memandu Anda melalui strategi dan praktik terbaik untuk menguji Web Components Anda, mulai dari unit testing yang detail hingga end-to-end testing yang menyeluruh. Mari kita pastikan “balok LEGO” Anda kokoh dan siap untuk proyek apa pun! 🚀

1. Pendahuluan: Mengapa Testing Web Components Itu Penting dan Unik?

Web Components membawa angin segar dalam pengembangan frontend dengan empat spesifikasi utamanya: Custom Elements, Shadow DOM, HTML Templates, dan ES Modules. Mereka memungkinkan kita membuat komponen UI yang benar-benar terisolasi, yang berarti style dan perilaku komponen tidak akan bocor atau memengaruhi komponen lain. Ini adalah kekuatan sekaligus tantangan dalam testing.

Tantangan Unik dalam Menguji Web Components:

Meskipun ada tantangan, testing Web Components akan memastikan bahwa komponen Anda berfungsi sesuai harapan, tahan terhadap perubahan, dan memberikan pengalaman pengguna yang konsisten.

2. Tooling untuk Testing Web Components

Memilih tool yang tepat adalah langkah pertama. Karena Web Components berbasis standar web, tool yang berinteraksi langsung dengan browser atau DOM standar adalah pilihan terbaik.

a. Web Test Runner (WTR)

📌 Web Test Runner (WTR) adalah test runner berbasis browser yang sangat cocok untuk Web Components. Dikembangkan oleh tim Open Web Components, WTR dapat menjalankan tes di browser sungguhan (Chrome, Firefox, Safari) atau headless browser (misalnya Chromium via Playwright).

Kelebihan:

Contoh setup dasar dengan WTR dan Mocha/Chai:

npm install --save-dev @web/test-runner mocha chai

web-test-runner.config.js:

import { fromRollup } from '@web/dev-server-rollup';
import rollupCommonjs from '@rollup/plugin-commonjs';

const commonjs = fromRollup(rollupCommonjs);

export default {
  // Jalankan tes di Chrome Headless
  browsers: ['chromium'],
  // Aktifkan laporan coverage
  coverage: true,
  // Tentukan file tes
  files: ['test/**/*.test.js'],
  // Optional: untuk library yang menggunakan CommonJS
  plugins: [
    commonjs(),
  ],
};

test/my-component.test.js:

import { html, fixture, expect } from '@open-wc/testing'; // Jika menggunakan @open-wc/testing
import '../src/my-component.js'; // Import Web Component Anda

describe('MyComponent', () => {
  it('renders a heading', async () => {
    const el = await fixture(html`<my-component></my-component>`);
    expect(el.shadowRoot.querySelector('h1')).to.exist;
    expect(el.shadowRoot.querySelector('h1').textContent).to.equal('Hello, World!');
  });

  it('updates the name property', async () => {
    const el = await fixture(html`<my-component name="Alice"></my-component>`);
    expect(el.shadowRoot.querySelector('h1').textContent).to.equal('Hello, Alice!');

    el.name = 'Bob';
    await el.updateComplete; // Jika komponen menggunakan LitElement
    expect(el.shadowRoot.querySelector('h1').textContent).to.equal('Hello, Bob!');
  });
});

💡 @open-wc/testing adalah set utilitas yang sangat direkomendasikan jika Anda bekerja dengan Web Components, terutama untuk mempermudah fixture dan expect di dalam Shadow DOM.

b. Playwright / Cypress (untuk E2E Testing)

Untuk end-to-end testing, tool seperti Playwright atau Cypress adalah pilihan yang sangat baik. Mereka berinteraksi dengan aplikasi Anda seperti pengguna sungguhan dan memiliki fitur khusus untuk berinteraksi dengan Shadow DOM.

Kita akan melihat contoh penggunaannya di bagian E2E testing.

3. Unit Testing Web Components: Memastikan Fungsi Internal

Unit testing berfokus pada pengujian bagian terkecil dari kode Anda secara terisolasi. Untuk Web Components, ini berarti menguji satu komponen tanpa interaksi dengan komponen lain atau bagian aplikasi yang lebih besar.

🎯 Tujuan: Memastikan logika internal, properti, atribut, dan event yang dipancarkan berfungsi dengan benar.

Strategi Unit Testing:

  1. Rendering dan Properti: Pastikan komponen merender dengan benar berdasarkan properti yang diberikan.
  2. Interaksi Pengguna (Events): Uji bagaimana komponen merespons interaksi pengguna (klik, input, dll.) dan memancarkan event kustom.
  3. Lifecycle Hooks: Verifikasi bahwa logika dalam connectedCallback, disconnectedCallback, dll., dieksekusi dengan benar.
  4. Shadow DOM Selection: Gunakan shadowRoot.querySelector untuk mengakses elemen di dalam Shadow DOM.

Contoh Unit Test (dengan WTR dan @open-wc/testing):

Misalkan kita punya my-button.js:

// src/my-button.js
class MyButton extends HTMLElement {
  static get observedAttributes() {
    return ['label', 'disabled'];
  }

  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.render();
    this.shadowRoot.querySelector('button').addEventListener('click', this._handleClick.bind(this));
  }

  disconnectedCallback() {
    this.shadowRoot.querySelector('button').removeEventListener('click', this._handleClick.bind(this));
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this.render(); // Re-render jika atribut berubah
    }
  }

  _handleClick() {
    this.dispatchEvent(new CustomEvent('button-click', {
      detail: { message: 'Button clicked!' },
      bubbles: true,
      composed: true, // Agar event bisa keluar dari Shadow DOM
    }));
  }

  render() {
    const label = this.getAttribute('label') || 'Click Me';
    const disabled = this.hasAttribute('disabled') ? 'disabled' : '';
    this.shadowRoot.innerHTML = `
      <style>
        button {
          padding: 10px 20px;
          border: none;
          background-color: #007bff;
          color: white;
          cursor: pointer;
          border-radius: 5px;
        }
        button:disabled {
          background-color: #cccccc;
          cursor: not-allowed;
        }
      </style>
      <button ${disabled}>${label}</button>
    `;
  }
}

customElements.define('my-button', MyButton);

Unit Test untuk my-button.js (test/my-button.test.js):

import { html, fixture, expect, oneEvent } from '@open-wc/testing';
import '../src/my-button.js'; // Pastikan komponen terdaftar

describe('MyButton', () => {
  // Test 1: Merender dengan label default
  it('renders with a default label', async () => {
    const el = await fixture(html`<my-button></my-button>`);
    const button = el.shadowRoot.querySelector('button');
    expect(button).to.exist;
    expect(button.textContent).to.equal('Click Me');
  });

  // Test 2: Merender dengan label kustom
  it('renders with a custom label', async () => {
    const el = await fixture(html`<my-button label="Submit"></my-button>`);
    const button = el.shadowRoot.querySelector('button');
    expect(button.textContent).to.equal('Submit');
  });

  // Test 3: Memancarkan event 'button-click' saat diklik
  it('fires a "button-click" event when clicked', async () => {
    const el = await fixture(html`<my-button></my-button>`);
    const button = el.shadowRoot.querySelector('button');

    // oneEvent menunggu event tertentu dan mengembalikan promise
    setTimeout(() => button.click()); // Klik tombol setelah event listener siap

    const { detail } = await oneEvent(el, 'button-click');
    expect(detail.message).to.equal('Button clicked!');
  });

  // Test 4: Merender dalam keadaan disabled
  it('renders in a disabled state', async () => {
    const el = await fixture(html`<my-button disabled></my-button>`);
    const button = el.shadowRoot.querySelector('button');
    expect(button.disabled).to.be.true;
    expect(button.getAttribute('disabled')).to.exist;
  });

  // Test 5: Atribut disabled dapat diubah
  it('can toggle the disabled attribute', async () => {
    const el = await fixture(html`<my-button disabled></my-button>`);
    expect(el.shadowRoot.querySelector('button').disabled).to.be.true;

    el.removeAttribute('disabled');
    await el.updateComplete; // Jika komponen menggunakan LitElement, gunakan ini. Untuk vanilla, mungkin perlu menunggu tick DOM.
    expect(el.shadowRoot.querySelector('button').disabled).to.be.false;
  });

  // Test 6: Event tidak terpancar saat disabled
  it('does not fire a "button-click" event when disabled', async () => {
    const el = await fixture(html`<my-button disabled></my-button>`);
    const button = el.shadowRoot.querySelector('button');

    let eventFired = false;
    el.addEventListener('button-click', () => {
      eventFired = true;
    });

    button.click();
    await new Promise(resolve => setTimeout(resolve, 50)); // Tunggu sebentar untuk memastikan event tidak terpancar

    expect(eventFired).to.be.false;
  });
});

Tips:

4. Integration Testing: Komponen Berinteraksi

Integration testing berfokus pada bagaimana beberapa Web Components bekerja sama. Ini menguji interaksi antar komponen, atau antara Web Component dan DOM standar.

🎯 Tujuan: Memastikan data dan event mengalir dengan benar di antara komponen yang saling terkait.

Strategi Integration Testing:

  1. Komposisi Komponen: Render beberapa komponen yang saling terkait dan uji alur data atau event di antara mereka.
  2. Slotting: Jika komponen Anda menggunakan <slot>, uji bagaimana konten yang dislotkan memengaruhi perilaku komponen.
  3. Event Propagasi: Pastikan event yang dipancarkan dari satu komponen (terutama dengan composed: true) dapat ditangkap oleh komponen induk atau listener di luar Shadow DOM.

Contoh Integration Test:

Misalkan kita punya my-form.js yang menggunakan my-button.js:

// src/my-form.js
import './my-button.js'; // Import button component

class MyForm extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        div { margin-bottom: 10px; }
      </style>
      <div>
        <label for="name">Nama:</label>
        <input type="text" id="name">
      </div>
      <my-button label="Kirim Data"></my-button>
      <p id="message"></p>
    `;
    this._handleSubmit = this._handleSubmit.bind(this);
  }

  connectedCallback() {
    this.shadowRoot.querySelector('my-button').addEventListener('button-click', this._handleSubmit);
  }

  disconnectedCallback() {
    this.shadowRoot.querySelector('my-button').removeEventListener('button-click', this._handleSubmit);
  }

  _handleSubmit() {
    const nameInput = this.shadowRoot.querySelector('#name');
    const messageDisplay = this.shadowRoot.querySelector('#message');
    messageDisplay.textContent = `Data berhasil dikirim: ${nameInput.value}`;
  }
}

customElements.define('my-form', MyForm);

Integration Test untuk my-form.js (test/my-form.test.js):

import { html, fixture, expect } from '@open-wc/testing';
import '../src/my-form.js'; // Pastikan form component terdaftar
import '../src/my-button.js'; // Pastikan button component juga terdaftar

describe('MyForm', () => {
  it('handles form submission via button click and displays message', async () => {
    const el = await fixture(html`<my-form></my-form>`);

    const nameInput = el.shadowRoot.querySelector('#name');
    const submitButton = el.shadowRoot.querySelector('my-button'); // Mengakses Web Component lain
    const messageDisplay = el.shadowRoot.querySelector('#message');

    // 1. Isi input
    nameInput.value = 'Budi';
    nameInput.dispatchEvent(new Event('input')); // Simulasi event input

    // 2. Klik tombol di dalam my-button (yang ada di dalam Shadow DOM my-form)
    // Kita perlu mengakses Shadow DOM dari my-button terlebih dahulu
    const innerButton = submitButton.shadowRoot.querySelector('button');
    innerButton.click();

    await el.updateComplete; // Jika ada re-render di my-form
    // Tunggu DOM diperbarui setelah event diproses
    await new Promise(resolve => setTimeout(resolve, 0));

    // 3. Verifikasi pesan ditampilkan
    expect(messageDisplay.textContent).to.equal('Data berhasil dikirim: Budi');
  });

  it('renders my-button with correct label', async () => {
    const el = await fixture(html`<my-form></my-form>`);
    const submitButton = el.shadowRoot.querySelector('my-button');
    const buttonInShadow = submitButton.shadowRoot.querySelector('button');

    expect(buttonInShadow.textContent).to.equal('Kirim Data');
  });
});

⚠️ Perhatian: Untuk mengakses elemen di dalam Shadow DOM dari Web Component yang bersarang, Anda perlu “menembus” Shadow DOM secara berurutan, seperti el.shadowRoot.querySelector('my-button').shadowRoot.querySelector('button').

5. End-to-End Testing: Mensimulasikan Pengguna Nyata

End-to-end (E2E) testing mensimulasikan alur pengguna yang lengkap di seluruh aplikasi Anda. Ini adalah level testing tertinggi yang melibatkan interaksi dengan UI, navigasi, dan interaksi backend (jika ada).

🎯 Tujuan: Memastikan seluruh sistem bekerja sebagai satu kesatuan dari perspektif pengguna.

Strategi E2E Testing:

  1. Gunakan Tool E2E: Playwright atau Cypress adalah pilihan yang sangat baik.
  2. Akses Shadow DOM: Kedua tool ini memiliki cara khusus untuk berinteraksi dengan elemen di dalam Shadow DOM.
  3. Simulasi Alur Pengguna: Klik tombol, isi form, navigasi halaman, dan verifikasi perubahan UI.

Contoh E2E Test dengan Playwright:

Misalkan Anda memiliki aplikasi yang menggunakan my-form di halaman utama.

// playwright.config.js
// ... (konfigurasi Playwright standar Anda)

tests/form-submission.spec.js:

import { test, expect } from '@playwright/test';

test('should submit form and display success message', async ({ page }) => {
  await page.goto('http://localhost:8000'); // Ganti dengan URL aplikasi Anda

  // Mengakses input di dalam Shadow DOM dari my-form
  // Playwright dapat menembus Shadow DOM secara otomatis jika elemennya unik
  // atau menggunakan kombinasi CSS selector dan pseudo-class ::part() atau ::slotted()
  // atau selector `:nth-match()` untuk elemen yang sama
  
  // Cara umum untuk mengakses elemen di Shadow DOM dengan Playwright:
  // Gunakan page.locator() dengan selector CSS biasa, Playwright akan mencoba menembus Shadow DOM.
  // Jika tidak berhasil, bisa gunakan JavaScript eksekusi di browser:
  await page.evaluate(() => {
    const form = document.querySelector('my-form');
    if (form && form.shadowRoot) {
      const input = form.shadowRoot.querySelector('#name');
      if (input) input.value = 'Dian';
    }
  });

  // Atau, jika komponen Anda diimplementasikan dengan Lit atau framework lain yang membuat elemen dapat diakses
  // oleh selector standar (terkadang Playwright bisa langsung menemukan)
  // await page.locator('my-form').locator('#name').fill('Dian'); // Ini bisa bekerja jika #name unik di Shadow DOM my-form
  // Tapi pendekatan `evaluate` lebih aman untuk Web Components murni

  // Klik tombol di dalam my-button, yang ada di dalam Shadow DOM my-form.
  // Playwright bisa menembus Shadow DOM secara berjenjang.
  await page.locator('my-form') // Cari my-form
             .locator('my-button') // Cari my-button di Shadow DOM my-form
             .locator('button') // Cari button di Shadow DOM my-button
             .click();

  // Verifikasi pesan sukses
  // Lagi-lagi, bisa pakai evaluate atau locator jika Playwright bisa menembusnya
  const message = await page.evaluate(() => {
    const form = document.querySelector('my-form');
    if (form && form.shadowRoot) {
      const msg = form.shadowRoot.querySelector('#message');
      return msg ? msg.textContent : '';
    }
    return '';
  });

  expect(message).toBe('Data berhasil dikirim: Dian');
});

Kesalahan Umum: Mencoba mengakses elemen Shadow DOM dengan selektor CSS standar seperti #name tanpa instruksi khusus. Ingat, Shadow DOM mengisolasi!

Tips untuk E2E Testing Web Components:

Kesimpulan

Menguji Web Components mungkin terasa sedikit berbeda pada awalnya karena enkapsulasi Shadow DOM dan sifatnya yang framework-agnostic. Namun, dengan tool yang tepat seperti Web Test Runner untuk unit/integrasi dan Playwright/Cypress untuk E2E, Anda dapat membangun test suite yang kuat dan andal.

Ingatlah, investasi dalam testing adalah investasi dalam kualitas dan maintainability aplikasi Anda. Dengan komponen yang teruji dengan baik, Anda bisa lebih percaya diri dalam membangun aplikasi web yang kompleks dan skalabel, balok demi balok. Selamat mencoba! 🏗️🧪

🔗 Baca Juga