-
Notifications
You must be signed in to change notification settings - Fork 7
E2E for Embed Only Features
このページは embed の E2E 範囲の正本。 責任分担: 機能・表示の E2E は geolonia/maps-core の
e2e/が担保(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})直接呼び出しでも検証可
- wrapper 機能 (DOM 走査・プラグイン・グローバル) → script タグ +
-
HTML 内で plugin を仕込む: registerPlugin の挙動を観測するためには HTML に
<script>でフックする必要あり (現状の helper では補えない)
守りたい挙動: 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+/);
});守りたい挙動: 旧コードが 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 参照)。テストを残せばリグレッションを検知できる。
守りたい挙動: <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 参照)。現状の挙動を守るための回帰テストとして書く。
守りたい挙動:
- デフォルト (
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 でも触れた通り、設計判断次第。仕様明文化のため検証する価値はある。
守りたい挙動: 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 にする。
守りたい挙動:
-
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参照)
守りたい挙動: 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 は通常書かない (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.evaluateでwindow.geolonia経由でアクセス (embed-core にしかないSimpleStyleVector/keyringは対象外) - スタイル初期化を
waitForStyleLoad(page, '#map')で待つ - アサーションは「DOM 状態」「container.geoloniaMap」「
window.__Xフック」のいずれか - テスト間で
keyring/plugins配列の汚染が起きないように、HTML ページごとに独立 (Playwright は test ごとに新 page でリセット)
- DOMContentLoaded のタイミング: registerPlugin が早期 register かどうかで挙動が変わる。HTML 内の script 順序で 早期 / 遅延 の両方をカバーする HTML を別々に用意するのが堅実
-
シングルトンのリーク:
keyring,plugins配列,sessionIdキャッシュは全 map で共有。Playwright の各 test は別 page なので問題ないが、同 page 内で複数 HTML を遷移する場合は注意 -
container.geoloniaMap重複防止: 同 container に 2 回newするとシングルトン返却 (一見奇妙)。意図して破壊しようとすると分かりにくい -
page.goto直後の plugin タイミング: Playwright が DOMContentLoaded を待ってから次の操作を始めるので、waitForStyleLoadの前にプラグインは既に呼ばれている可能性大 -
iframe 制約: Playwright で iframe 内の console は capture しにくい。
page.on('console')は親フレームのみ、frameLocator経由でも限定的 -
lazy-loading の検知タイミング:
IntersectionObserverの callback は microtask、scrollIntoViewIfNeeded直後に DOM を確認すると間に合わない場合あり →waitForFunctionを使う
- Core-vs-Wrapper — lib/wrapper 境界と移植チェックリスト
-
Module-Render —
renderGeoloniaMap, Observer, プラグインの内部実装 -
Module-Embed-Entry —
embed.tsとembed-core.tsの export 差 - E2E-Plan — テスト拡充計画全体 (#411 umbrella、各 PR の分割方針)
- Suspicious-Logic — 怪しいロジック (現状を仕様として固める判断材料)
- Architecture — グローバル状態、状態フラグの遷移