Skip to content

E2E for Embed Only Features

niryuu edited this page Jun 4, 2026 · 2 revisions

Embed-Only 機能の E2E ガイド

このページは embed の E2E 範囲の正本。 責任分担: 機能・表示の E2E は geolonia/maps-coree2e/ が担保new GeoloniaMap() 経由、WebGL ピクセル検証まで実施)。 embed の E2E は maps-core が構造的に持てない wrapper グルー専任に縮約する(#493 / 移行は #491)。 かつての網羅的拡充計画は E2E-Plan にあったが、maps-core E2E と重複するため supersede 済み(E2E-Plan はインフラ参照のみ残置)。

embed-core 切り出し / embed 書き直しを前提に、embed (wrapper) にしかない機能を E2E でどう守るかを整理する。lib 側 (将来の maps-core) で書ける部分とは別に、wrapper 固有の挙動を回帰させないためのドキュメント。

参照: Core-vs-Wrapper / Module-Render / Module-Embed-Entry / E2E-Plan

境界の再確認

Core-vs-Wrapper の表を E2E 観点でフィルタしたもの。

機能 embed-core embed (wrapper) E2E 観点でテスト必須
GeoloniaMap クラス本体 lib 側で書ける
GeoloniaMarker クラス lib 側で書ける
SimpleStyle / SimpleStyleVector ✓ (Vector は再 export なし) lib 側で書ける
keyring シングルトン ✓ (export) ✓ (内部のみ) lib 側
registerPlugin 関数 lib 側 + wrapper 経路両方
transformRequest / 認証注入 lib 側
window.geolonia 公開 wrapper のみ
window.maplibregl / mapboxgl 後方互換 wrapper のみ
renderGeoloniaMap() 自動実行 (script タグ読込で発火) wrapper のみ
DOM 走査 (.geolonia クラス) wrapper のみ
data-lazy-loading + IntersectionObserver wrapper のみ
MutationObserver による cleanup wrapper のみ
DOMContentLoaded 待ち + プラグインキュー wrapper のみ
iframe 許可チェック (checkPermission) wrapper のみ
pmtiles プロトコル登録 ✓ (import 時) ✓ (renderGeoloniaMap 内) タイミング差。両方で testable

「wrapper のみ」の 8 機能がこのドキュメントで扱う対象

戦略の前提

  • テスト方式: <div class="geolonia"> を含む HTML を docs/e2e/ に置き、<script src="../embed?geolonia-api-key=YOUR-API-KEY"> で読み込む形 (= 現在の embed.js テストパターン)
  • モック: ヘルパー mockGeoloniaTiles(page)beforeEach で必ず呼ぶ (e2e/helper.ts)
  • HTML 経由か new GeoloniaMap() 経由か:
    • wrapper 機能 (DOM 走査・プラグイン・グローバル) → script タグ + .geolonia 要素のパターン必須
    • lib 機能 → new GeoloniaMap({container}) 直接呼び出しでも検証可
  • HTML 内で plugin を仕込む: registerPlugin の挙動を観測するためには HTML に <script> でフックする必要あり (現状の helper では補えない)

機能別 E2E 戦略

1. window.geolonia 公開

守りたい挙動: embed.js を script タグで読むと window.geolonia が即座にセットされ、Map / Marker / SimpleStyle / registerPlugin / embedVersion を含む。

test('window.geolonia がセットされる', async ({ page }) => {
  await mockGeoloniaTiles(page);
  await page.goto(`${TEST_URL}/basic.html`);
  await waitForMapLoad(page);

  const shape = await page.evaluate(() => ({
    geolonia: typeof window.geolonia,
    Map: typeof window.geolonia?.Map,
    Marker: typeof window.geolonia?.Marker,
    SimpleStyle: typeof window.geolonia?.SimpleStyle,
    registerPlugin: typeof window.geolonia?.registerPlugin,
    embedVersion: window.geolonia?.embedVersion,
  }));

  expect(shape.geolonia).toBe('object');
  expect(shape.Map).toBe('function');
  expect(shape.Marker).toBe('function');
  expect(shape.SimpleStyle).toBe('function');
  expect(shape.registerPlugin).toBe('function');
  expect(shape.embedVersion).toMatch(/^\d+\.\d+/);
});

2. 後方互換 (window.maplibregl, window.mapboxgl)

守りたい挙動: 旧コードが mapboxgl.Map / maplibregl.Map を直接参照しても embed の Map クラスに繋がる。

test('window.maplibregl と window.mapboxgl が window.geolonia と同一参照', async ({ page }) => {
  await mockGeoloniaTiles(page);
  await page.goto(`${TEST_URL}/basic.html`);
  await waitForMapLoad(page);

  const aliases = await page.evaluate(() => ({
    maplibregl: window.maplibregl === window.geolonia,
    mapboxgl: window.mapboxgl === window.geolonia,
    mapClass: window.maplibregl?.Map === window.geolonia.Map,
  }));

  expect(aliases.maplibregl).toBe(true);
  expect(aliases.mapboxgl).toBe(true);
  expect(aliases.mapClass).toBe(true);
});

注意: 書き直し時にこの後方互換を削除する案がある (Core-vs-Wrapper 参照)。テストを残せばリグレッションを検知できる。

3. DOM 自動走査 (renderGeoloniaMap の auto-render)

守りたい挙動: <div class="geolonia"> を置けば script タグ読込だけで全要素が描画される。

単一マップは現状の basic.spec.ts:1.1 で実質カバー。追加すべきは 複数マップ動的追加が拾われない (one-shot 性):

test('複数の .geolonia 要素が独立に描画される', async ({ page }) => {
  await mockGeoloniaTiles(page);
  await page.goto(`${TEST_URL}/multiple-maps.html`);

  // 各 container に対応する map が container.geoloniaMap として存在
  await waitForStyleLoad(page, '#map-a');
  await waitForStyleLoad(page, '#map-b');
  await waitForStyleLoad(page, '#map-c');

  const result = await page.evaluate(() =>
    ['#map-a', '#map-b', '#map-c'].map((sel) => {
      const el = document.querySelector(sel) as any;
      return !!el?.geoloniaMap;
    }),
  );
  expect(result).toEqual([true, true, true]);
});

test('DOMContentLoaded 後に追加された .geolonia は自動 render されない', async ({ page }) => {
  await mockGeoloniaTiles(page);
  await page.goto(`${TEST_URL}/basic.html`);
  await waitForMapLoad(page);

  // 後から追加
  await page.evaluate(() => {
    const el = document.createElement('div');
    el.id = 'late-map';
    el.className = 'geolonia';
    el.setAttribute('data-lazy-loading', 'off');
    document.body.appendChild(el);
  });
  await page.waitForTimeout(500);

  const hasLateMap = await page.evaluate(() => {
    const el = document.querySelector('#late-map') as any;
    return !!el?.geoloniaMap;
  });
  expect(hasLateMap).toBe(false);
});

「動的追加が拾われない」は仕様としては議論余地あり (Suspicious-Logic 参照)。現状の挙動を守るための回帰テストとして書く。

4. 遅延読み込み (IntersectionObserver)

守りたい挙動:

  • デフォルト (data-lazy-loading 無し or 任意の値) では viewport 外の .geolonia は未初期化
  • スクロールで viewport に入ると 1 回だけ初期化される
  • data-lazy-loading="off" のときは即時初期化

HTML 例 (docs/e2e/lazy-loading.html):

<div style="height: 2000px"><!-- spacer --></div>
<div id="lazy-map" class="geolonia" data-lat="35" data-lng="139"></div>
test('viewport 外の .geolonia は未初期化', async ({ page }) => {
  await mockGeoloniaTiles(page);
  await page.goto(`${TEST_URL}/lazy-loading.html`);

  // スクロール前
  const beforeScroll = await page.evaluate(() => {
    const el = document.querySelector('#lazy-map') as any;
    return !!el?.geoloniaMap;
  });
  expect(beforeScroll).toBe(false);

  // スクロール
  await page.locator('#lazy-map').scrollIntoViewIfNeeded();
  await waitForStyleLoad(page, '#lazy-map');

  const afterScroll = await page.evaluate(() => {
    const el = document.querySelector('#lazy-map') as any;
    return !!el?.geoloniaMap;
  });
  expect(afterScroll).toBe(true);
});

test('data-lazy-loading="off" で即時初期化', async ({ page }) => {
  await mockGeoloniaTiles(page);
  await page.goto(`${TEST_URL}/lazy-off.html`);

  // viewport 外でも canvas が出ている
  const hasCanvas = await page.locator('#eager-map canvas').count();
  expect(hasCanvas).toBeGreaterThan(0);
});

once-only 動作 (一度可視化したらスクロールアウト → 再表示で再初期化しない) のテストは Suspicious-Logic でも触れた通り、設計判断次第。仕様明文化のため検証する価値はある。

5. MutationObserver による cleanup

守りたい挙動: container を DOM から削除すると map.remove() が自動的に呼ばれる。

test('container 削除で map.remove() が発火', async ({ page }) => {
  await mockGeoloniaTiles(page);
  await page.goto(`${TEST_URL}/cleanup.html`);
  await waitForStyleLoad(page, '#map');

  // remove イベント listener を仕込む
  await page.evaluate(() => {
    const map = (document.querySelector('#map') as any).geoloniaMap;
    (window as any).__removeFired = false;
    map.on('remove', () => {
      (window as any).__removeFired = true;
    });
  });

  // container を削除
  await page.evaluate(() => document.querySelector('#map')?.remove());

  // MutationObserver は microtask で fire するので少し待つ
  await page.waitForFunction(() => (window as any).__removeFired === true, undefined, {
    timeout: 1000,
  });

  expect(await page.evaluate(() => (window as any).__removeFired)).toBe(true);
});

test('同じ container を再使用しても 2 度目の new GeoloniaMap は同一インスタンス (シングルトン)', async ({ page }) => {
  await mockGeoloniaTiles(page);
  await page.goto(`${TEST_URL}/basic.html`);
  await waitForStyleLoad(page);

  const sameInstance = await page.evaluate(() => {
    const container = document.querySelector('.geolonia') as any;
    const a = container.geoloniaMap;
    const b = new (window as any).geolonia.Map(container);
    return a === b;
  });
  expect(sameInstance).toBe(true);
});

Suspicious-Logic で議論されている「MutationObserver が parentNode を観察するので親ごと消えると検知できない」は、まずは現状の挙動 (子削除なら検知) をテストして、親削除のケースは "known limitation" として書かない or skip with comment にする。

6. プラグインシステム

守りたい挙動:

  • registerPlugin(fn) で登録した callback が全マップに対して呼ばれる
  • DOMContentLoaded 前に登録 → DOMContentLoaded 後にまとめて実行
  • DOMContentLoaded 後に登録 → 即時実行 (新規 map 生成時)
  • 引数 (map, target, atts) が正しく渡される

HTML 例 (docs/e2e/plugin.html):

<script>
  window.__pluginCalls = [];
  // この時点では window.geolonia は未定義なので、embed.js 読込後を待つ
  document.addEventListener('DOMContentLoaded', () => {
    // ↓ この時点では geolonia は登録済み
    geolonia.registerPlugin((map, target, atts) => {
      window.__pluginCalls.push({ targetId: target.id, lat: atts.lat });
    });
  });
</script>
<script src="../embed?geolonia-api-key=YOUR-API-KEY"></script>
<div id="map-1" class="geolonia" data-lat="35" data-lng="139"></div>
<div id="map-2" class="geolonia" data-lat="40" data-lng="140"></div>
test('registerPlugin で登録した callback が各 map に対して呼ばれる', async ({ page }) => {
  await mockGeoloniaTiles(page);
  await page.goto(`${TEST_URL}/plugin.html`);

  await waitForStyleLoad(page, '#map-1');
  await waitForStyleLoad(page, '#map-2');

  const calls = await page.evaluate(() => (window as any).__pluginCalls);
  expect(calls).toHaveLength(2);
  expect(calls.map((c: any) => c.targetId).sort()).toEqual(['map-1', 'map-2']);
  expect(calls.find((c: any) => c.targetId === 'map-1').lat).toBe('35');
});

早期 register / 遅延 register:

test('DOMContentLoaded 前の registerPlugin もキューされて実行される', async ({ page }) => {
  // plugin-early.html では <script> でいきなり window.geolonia?.registerPlugin?.(fn) を呼ぶ
  // ただし embed.js より前に読まれる場合 window.geolonia は未定義 → 工夫が必要:
  //   <script>
  //     // embed.js より前に Hook を仕込む
  //     window.__earlyPluginCalls = [];
  //     // embed.js 読み込み後に registerPlugin を呼ぶ
  //     // (geolonia は同期で window にセットされるので embed.js の直下に script を置けば OK)
  //   </script>
  //   <script src="../embed?geolonia-api-key=YOUR-API-KEY"></script>
  //   <script>
  //     geolonia.registerPlugin((map) => {
  //       window.__earlyPluginCalls.push(map.getCenter());
  //     });
  //   </script>
  //   <div class="geolonia" data-lazy-loading="off"></div>
  await mockGeoloniaTiles(page);
  await page.goto(`${TEST_URL}/plugin-early.html`);
  await waitForStyleLoad(page);

  await page.waitForFunction(
    () => (window as any).__earlyPluginCalls.length > 0,
  );
  const calls = await page.evaluate(() => (window as any).__earlyPluginCalls);
  expect(calls.length).toBe(1);
});

注意:

  • registerPlugin は window.geolonia.registerPlugin 経由でアクセス
  • embed.js のロード完了 (同期 <script>) 直後に window.geolonia が存在する
  • ただし plugin が map.on('load') 等を期待する場合は実行順序に注意 (Suspicious-Logic 参照)

7. iframe 許可チェック

守りたい挙動: iframe からの埋め込みは API key 無しの場合、特定の origin (Codepen 等) 以外は拒否される。

Playwright の iframe テストは page.frameLocator() 経由で可能だが、特例の発火条件 (origin + referrer) をテスト環境で再現するのは困難:

test.skip('iframe + API key 無し → console.error', async ({ page }) => {
  // iframe.html で <iframe src="basic-no-key.html"></iframe>
  // basic-no-key.html は <script src="../embed">  (key 無し)
  // → console.error: '[Geolonia] We are very sorry, but we cannot display ...'
  //
  // 注: ローカルでは window.parent === window のため iframe 判定が機能しない場合あり
  // 実 iframe 検証は Playwright の page.frameLocator で行うが、
  // childFrame の console は capture できない場合あり
});

このカテゴリは ユニットテストcheckPermission() を window.self / window.parent / location をモックして直接テストする方が現実的。E2E では skip または「iframe 不使用時に許可される」だけテスト:

test('iframe ではない場合は API key 無しでも render 試行する (style 取得は失敗するが crash しない)', async ({ page }) => {
  await mockGeoloniaTiles(page);
  await page.goto(`${TEST_URL}/no-key.html`);
  // canvas が出る、もしくは [Geolonia] Missing API key. のログ
});

embed-core 経路の E2E (補足)

embed-core 単体の E2E は通常書かない (lib なので consumer 経由で検証)。ただし以下のシナリオは embed.js 経由のテストでも embed-core 由来の挙動を扱う:

  • new geolonia.Map({ container, center, zoom }) のプログラム的初期化 (現 basic.spec.ts:1.2 で部分カバー)
  • setStyle(newStyle) → styledata 再ロード (__styleExtensionLoadRequired の挙動)
  • map.remove() 後の container.geoloniaMap クリア
  • transformRequest の URL 書き換え動作 (現 external-style.spec.ts で外部 URL 検証)

これらは embed-core テスト (将来 maps-core 側) にも持って行ける単体テストで充分。embed の E2E ではあえてカバーしない (二重に書かない方が保守しやすい)。

テスト追加チェックリスト

embed-only 機能のテストを書くときの最低限の確認:

  • mockGeoloniaTiles(page)test.beforeEach で呼ぶ
  • <script src="../embed?geolonia-api-key=YOUR-API-KEY"> を含む HTML を docs/e2e/ に新規作成
  • page.evaluatewindow.geolonia 経由でアクセス (embed-core にしかない SimpleStyleVector / keyring は対象外)
  • スタイル初期化を waitForStyleLoad(page, '#map') で待つ
  • アサーションは「DOM 状態」「container.geoloniaMap」「window.__X フック」のいずれか
  • テスト間で keyring / plugins 配列の汚染が起きないように、HTML ページごとに独立 (Playwright は test ごとに新 page でリセット)

落とし穴

  1. DOMContentLoaded のタイミング: registerPlugin が早期 register かどうかで挙動が変わる。HTML 内の script 順序で 早期 / 遅延 の両方をカバーする HTML を別々に用意するのが堅実
  2. シングルトンのリーク: keyring, plugins 配列, sessionId キャッシュは全 map で共有。Playwright の各 test は別 page なので問題ないが、同 page 内で複数 HTML を遷移する場合は注意
  3. container.geoloniaMap 重複防止: 同 container に 2 回 new するとシングルトン返却 (一見奇妙)。意図して破壊しようとすると分かりにくい
  4. page.goto 直後の plugin タイミング: Playwright が DOMContentLoaded を待ってから次の操作を始めるので、waitForStyleLoad の前にプラグインは既に呼ばれている可能性大
  5. iframe 制約: Playwright で iframe 内の console は capture しにくい。page.on('console') は親フレームのみ、frameLocator 経由でも限定的
  6. lazy-loading の検知タイミング: IntersectionObserver の callback は microtask、scrollIntoViewIfNeeded 直後に DOM を確認すると間に合わない場合あり → waitForFunction を使う

関連ドキュメント

  • Core-vs-Wrapper — lib/wrapper 境界と移植チェックリスト
  • Module-RenderrenderGeoloniaMap, Observer, プラグインの内部実装
  • Module-Embed-Entryembed.tsembed-core.ts の export 差
  • E2E-Plan — テスト拡充計画全体 (#411 umbrella、各 PR の分割方針)
  • Suspicious-Logic — 怪しいロジック (現状を仕様として固める判断材料)
  • Architecture — グローバル状態、状態フラグの遷移

Clone this wiki locally