Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/e2e/cleanup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="../style.css">
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<title>@geolonia/embed - cleanup</title>
</head>
<body>
<div class="example">
<h2>Cleanup</h2>
<div id="map" class="geolonia" style="width:400px;height:300px;" data-lazy-loading="off"></div>
</div>
<script src="../embed?geolonia-api-key=YOUR-API-KEY"></script>
</body>
</html>
26 changes: 26 additions & 0 deletions docs/e2e/data-attributes.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="../style.css">
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<title>@geolonia/embed - data attributes</title>
</head>
<body>
<div class="example">
<h2>data-* attributes</h2>
<div
id="map"
class="geolonia"
style="width: 400px; height: 300px;"
data-lat="35"
data-lng="139"
data-zoom="10"
data-bearing="30"
data-pitch="20"
data-lazy-loading="off"
></div>
</div>
<script src="../embed?geolonia-api-key=YOUR-API-KEY"></script>
</body>
</html>
25 changes: 25 additions & 0 deletions docs/e2e/lazy-loading.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="../style.css">
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<title>@geolonia/embed - lazy loading</title>
<style>
.spacer { height: 2000px; }
.geolonia { width: 400px; height: 250px; }
</style>
</head>
<body>
<div class="example">
<h2>Lazy loading</h2>
<div class="spacer"></div>
<!-- data-lazy-loading="off": viewport 外でも即時初期化されるべき -->
<div id="eager-map" class="geolonia" data-lazy-loading="off" data-lat="35" data-lng="139"></div>
<div class="spacer"></div>
<!-- デフォルト (data-lazy-loading 無し): viewport に入るまで未初期化 -->
<div id="lazy-map" class="geolonia" data-lat="35" data-lng="139"></div>
</div>
<script src="../embed?geolonia-api-key=YOUR-API-KEY"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions docs/e2e/multiple-maps.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="../style.css">
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<title>@geolonia/embed - multiple maps</title>
<style>
.geolonia { width: 400px; height: 250px; margin-bottom: 16px; }
</style>
</head>
<body>
<div class="example">
<h2>Multiple maps</h2>
<div id="map-a" class="geolonia" data-lazy-loading="off" data-lat="35" data-lng="139"></div>
<div id="map-b" class="geolonia" data-lazy-loading="off" data-lat="40" data-lng="140"></div>
<div id="map-c" class="geolonia" data-lazy-loading="off" data-lat="43" data-lng="141"></div>
</div>
<script src="../embed?geolonia-api-key=YOUR-API-KEY"></script>
</body>
</html>
30 changes: 30 additions & 0 deletions docs/e2e/plugin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="../style.css">
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<title>@geolonia/embed - plugin</title>
<style>
.geolonia { width: 400px; height: 250px; margin-bottom: 16px; }
</style>
<script>
// registerPlugin の観測フック。embed.js より前に DOMContentLoaded リスナを仕込む。
// embed.js は同期読込なので、DOMContentLoaded 発火時には window.geolonia が存在する。
window.__pluginCalls = [];
document.addEventListener('DOMContentLoaded', function () {
window.geolonia.registerPlugin(function (map, target, atts) {
window.__pluginCalls.push({ targetId: target.id, lat: atts.lat });
});
});
</script>
</head>
<body>
<div class="example">
<h2>Plugin</h2>
<div id="map-1" class="geolonia" data-lazy-loading="off" data-lat="35" data-lng="139"></div>
<div id="map-2" class="geolonia" data-lazy-loading="off" data-lat="40" data-lng="140"></div>
</div>
<script src="../embed?geolonia-api-key=YOUR-API-KEY"></script>
</body>
</html>
44 changes: 44 additions & 0 deletions e2e/auto-render.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { test, expect } from '@playwright/test';
import { TEST_URL, mockGeoloniaTiles, waitForStyleLoad, hasMapInstance } from './helper';

// embed (wrapper) 固有: script タグを読むだけで .geolonia 要素が自動描画される
// (renderGeoloniaMap の DOM 走査)。maps-core は new GeoloniaMap() の明示呼び出しのみで
// この自動走査を持たない。
test.describe('DOM 自動走査 / auto-render (wrapper-only)', () => {
test('複数の .geolonia 要素が独立に描画される', async ({ page }) => {
await mockGeoloniaTiles(page);
await page.goto(`${TEST_URL}/multiple-maps.html`);

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 されない (one-shot)', async ({ page }) => {
await mockGeoloniaTiles(page);
await page.goto(`${TEST_URL}/multiple-maps.html`);
await waitForStyleLoad(page, '#map-a');

// 後から .geolonia を追加 (即時描画されるはずの data-lazy-loading="off" 指定)
await page.evaluate(() => {
const el = document.createElement('div');
el.id = 'late-map';
el.className = 'geolonia';
el.setAttribute('data-lazy-loading', 'off');
el.setAttribute('style', 'width:400px;height:250px;');
document.body.appendChild(el);
});
await page.waitForTimeout(500);

// renderGeoloniaMap は読込時に一度だけ走査するため拾われない
expect(await hasMapInstance(page, '#late-map')).toBe(false);
});
});
40 changes: 40 additions & 0 deletions e2e/cleanup.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { test, expect } from '@playwright/test';
import { TEST_URL, mockGeoloniaTiles, waitForStyleLoad } from './helper';

// embed (wrapper) 固有: MutationObserver でコンテナの DOM 削除を検知し map.remove() を呼ぶ。
// maps-core にはこの自動 cleanup は無い (明示 remove() のみ)。
test.describe('MutationObserver cleanup (wrapper-only)', () => {
test.beforeEach(async ({ page }) => {
await mockGeoloniaTiles(page);
await page.goto(`${TEST_URL}/cleanup.html`);
await waitForStyleLoad(page, '#map');
});

test('コンテナを DOM から削除すると map.remove() が発火する', async ({ page }) => {
await page.evaluate(() => {
const map = (document.querySelector('#map') as any).geoloniaMap;
(window as any).__removeFired = false;
map.on('remove', () => {
(window as any).__removeFired = true;
});
});

await page.evaluate(() => document.querySelector('#map')?.remove());

// MutationObserver は非同期で発火するため待つ
await page.waitForFunction(() => (window as any).__removeFired === true, undefined, {
timeout: 1000,
});
expect(await page.evaluate(() => (window as any).__removeFired)).toBe(true);
});

test('同じコンテナへ再度 new すると同一インスタンスが返る (二重初期化防止)', async ({ page }) => {
const sameInstance = await page.evaluate(() => {
const container = document.querySelector('#map') as any;
const a = container.geoloniaMap;
const b = new (window as any).geolonia.Map(container);
return a === b;
});
expect(sameInstance).toBe(true);
});
});
37 changes: 37 additions & 0 deletions e2e/data-attributes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { test, expect } from '@playwright/test';
import {
TEST_URL,
mockGeoloniaTiles,
waitForStyleLoad,
getCenter,
getZoom,
getBearing,
getPitch,
} from './helper';

// embed (wrapper) 固有: HTML の data-* 属性を読んで GeoloniaMapOptions に渡すのは
// parse-atts.ts (embed のみ)。maps-core は options オブジェクト受け取りで data-* を読まない。
// よってここで守るのは「data-* を正しくパースして地図に転送できているか」というフォワーディング層。
// 各オプションが実際にどう描画されるか (option → 挙動) は maps-core 側 E2E が担保する。
test.describe('data-* → options フォワーディング (wrapper-only)', () => {
test.beforeEach(async ({ page }) => {
await mockGeoloniaTiles(page);
await page.goto(`${TEST_URL}/data-attributes.html`);
await waitForStyleLoad(page, '#map');
});

test('data-lat / data-lng が中心座標に反映される', async ({ page }) => {
const center = await getCenter(page, '#map');
expect(center.lat).toBeCloseTo(35, 4);
expect(center.lng).toBeCloseTo(139, 4);
});

test('data-zoom が反映される', async ({ page }) => {
expect(await getZoom(page, '#map')).toBeCloseTo(10, 4);
});

test('data-bearing / data-pitch が反映される', async ({ page }) => {
expect(await getBearing(page, '#map')).toBeCloseTo(30, 4);
expect(await getPitch(page, '#map')).toBeCloseTo(20, 4);
});
});
51 changes: 51 additions & 0 deletions e2e/globals.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { test, expect } from '@playwright/test';
import { Geolonia } from '../src/embed';
import { TEST_URL, mockGeoloniaTiles, waitForMapLoad } from './helper';

declare global {
interface Window {
geolonia: Geolonia;
maplibregl?: Geolonia;
mapboxgl?: Geolonia;
}
}

// embed (wrapper) 固有: embed.js を script タグで読むとグローバルが公開される。
// 表示・機能そのものは maps-core 側 E2E が担保するため、ここでは「公開」だけを守る。
test.describe('window グローバル公開 (wrapper-only)', () => {
test.beforeEach(async ({ page }) => {
await mockGeoloniaTiles(page);
await page.goto(`${TEST_URL}/basic.html`);
await waitForMapLoad(page);
});

test('window.geolonia がセットされ、主要メンバを含む', async ({ 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+/);
});

test('window.maplibregl / window.mapboxgl が window.geolonia と同一参照', async ({ 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);
});
});
35 changes: 35 additions & 0 deletions e2e/iframe.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { test, expect } from '@playwright/test';
import { TEST_URL, mockGeoloniaTiles, waitForMapLoad, hasMapInstance } from './helper';

// embed (wrapper) 固有: checkPermission() による iframe 埋め込み制限。
// maps-core にこのゲートは無い。
//
// 拒否される側 (iframe + API key 無し + 非特例 origin) の E2E 再現は難しいため skip し、
// ここでは「非 iframe では拒否されず描画される」= 許可側の経路のみを守る。
test.describe('iframe 許可チェック / checkPermission (wrapper-only)', () => {
test('非 iframe では拒否されず地図が描画される (iframe 拒否メッセージが出ない)', async ({ page }) => {
const consoleErrors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') consoleErrors.push(msg.text());
});

await mockGeoloniaTiles(page);
await page.goto(`${TEST_URL}/basic.html`);
await waitForMapLoad(page);

// 描画されている = checkPermission が許可した
expect(await hasMapInstance(page, '#map')).toBe(true);

// iframe 拒否のメッセージは出ていないこと
expect(consoleErrors.some((t) => t.includes("can't display our map in iframe"))).toBe(false);
});

// 拒否ケース (iframe + API key 無し → console.error) は E2E では再現が難しい:
// - ローカルでは特例 origin (codepen 等) + referrer を満たせない
// - child frame の console は親フレームから capture しにくい
// このゲートは checkPermission() のユニットテスト (window.self/parent/location をモック) で
// 検証する方が現実的。E2E では skip する (silent に落とさず明示)。
test.skip('iframe + API key 無し → 拒否される (ユニットテストで担保)', async () => {
// intentionally skipped — see comment above
});
});
32 changes: 32 additions & 0 deletions e2e/lazy-loading.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { test, expect } from '@playwright/test';
import { TEST_URL, mockGeoloniaTiles, waitForStyleLoad, hasMapInstance } from './helper';

// embed (wrapper) 固有: IntersectionObserver による遅延読み込み。
// maps-core は new GeoloniaMap() の明示初期化のみで、この lazy 機構を持たない。
test.describe('遅延読み込み / IntersectionObserver (wrapper-only)', () => {
test.beforeEach(async ({ page }) => {
await mockGeoloniaTiles(page);
});

test('data-lazy-loading="off" は viewport 外でも即時初期化される', async ({ page }) => {
await page.goto(`${TEST_URL}/lazy-loading.html`);
// スクロールせずとも初期化済み
await waitForStyleLoad(page, '#eager-map');
expect(await hasMapInstance(page, '#eager-map')).toBe(true);
});

test('デフォルト (lazy) は viewport 外では未初期化、スクロールで初期化される', async ({ page }) => {
await page.goto(`${TEST_URL}/lazy-loading.html`);

// スクロール前: lazy-map はまだ初期化されていない
// (IntersectionObserver の callback は非同期なので、誤発火しないことも含め少し待つ)
await page.waitForTimeout(300);
expect(await hasMapInstance(page, '#lazy-map')).toBe(false);

// viewport に入れる
await page.locator('#lazy-map').scrollIntoViewIfNeeded();
await waitForStyleLoad(page, '#lazy-map');

expect(await hasMapInstance(page, '#lazy-map')).toBe(true);
});
});
25 changes: 25 additions & 0 deletions e2e/plugin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { test, expect } from '@playwright/test';
import { TEST_URL, mockGeoloniaTiles, waitForStyleLoad } from './helper';

// embed (wrapper) 固有: registerPlugin で登録した callback が auto-render された全 map に対し
// (map, target, atts) 付きで呼ばれる。maps-core にプラグイン機構は無い。
test.describe('プラグインシステム (wrapper-only)', () => {
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');

// plugin は DOMContentLoaded 時に登録 → キューされた全 map に対して実行される
await page.waitForFunction(() => (window as any).__pluginCalls?.length === 2, undefined, {
timeout: 2000,
});

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']);
// atts は parse-atts が container.dataset を spread したもの。data-lat は文字列で渡る。
expect(calls.find((c: any) => c.targetId === 'map-1').lat).toBe('35');
});
});
Loading
Loading