diff --git a/e2e/helper.ts b/e2e/helper.ts index feda9afa..de9d7802 100644 --- a/e2e/helper.ts +++ b/e2e/helper.ts @@ -1,12 +1,164 @@ import { Page } from '@playwright/test'; -// テスト用の設定 + export const TEST_URL = 'http://localhost:3000/e2e'; export const LOAD_TIMEOUT = 5000; -// ヘルパー関数 +/** 地図コンテナと canvas の出現を待つ。既存テスト互換。 */ export async function waitForMapLoad(page: Page, selector = '.geolonia') { - // 地図コンテナの存在を確認 await page.waitForSelector(selector); - // Maplibreのcanvasが表示されるまで待機 await page.waitForSelector(`${selector} canvas`, { timeout: LOAD_TIMEOUT }); } + +/** Maplibre Map の load + isStyleLoaded を待つ。スタイル依存テスト向け。 */ +export async function waitForStyleLoad(page: Page, selector = '#map') { + await page.waitForFunction( + (sel) => { + const el = document.querySelector(sel) as any; + return !!(el && el.geoloniaMap && el.geoloniaMap.loaded() && el.geoloniaMap.isStyleLoaded()); + }, + selector, + { timeout: LOAD_TIMEOUT }, + ); +} + +/** container.geoloniaMap が生成済みか確認。 */ +export async function hasMapInstance(page: Page, selector = '#map'): Promise { + return page.evaluate((sel) => { + const el = document.querySelector(sel) as any; + return !!(el && el.geoloniaMap); + }, selector); +} + +export async function getCenter(page: Page, selector = '#map'): Promise<{ lat: number; lng: number }> { + return page.evaluate((sel) => { + const el = document.querySelector(sel) as any; + const c = el.geoloniaMap.getCenter(); + return { lat: c.lat, lng: c.lng }; + }, selector); +} + +export async function getZoom(page: Page, selector = '#map'): Promise { + return page.evaluate((sel) => { + const el = document.querySelector(sel) as any; + return el.geoloniaMap.getZoom(); + }, selector); +} + +export async function getBearing(page: Page, selector = '#map'): Promise { + return page.evaluate((sel) => { + const el = document.querySelector(sel) as any; + return el.geoloniaMap.getBearing(); + }, selector); +} + +export async function getPitch(page: Page, selector = '#map'): Promise { + return page.evaluate((sel) => { + const el = document.querySelector(sel) as any; + return el.geoloniaMap.getPitch(); + }, selector); +} + +export async function getStyleLayerIds(page: Page, selector = '#map'): Promise { + return page.evaluate((sel) => { + const el = document.querySelector(sel) as any; + return el.geoloniaMap.getStyle().layers.map((l: any) => l.id); + }, selector); +} + +export async function hasLayer(page: Page, layerId: string, selector = '#map'): Promise { + return page.evaluate( + ({ sel, id }) => { + const el = document.querySelector(sel) as any; + return !!el.geoloniaMap.getLayer(id); + }, + { sel: selector, id: layerId }, + ); +} + +export async function hasSource(page: Page, sourceId: string, selector = '#map'): Promise { + return page.evaluate( + ({ sel, id }) => { + const el = document.querySelector(sel) as any; + return !!el.geoloniaMap.getSource(id); + }, + { sel: selector, id: sourceId }, + ); +} + +// Geolonia ベクトルタイルのホスト。TileJSON で返す URL とリクエスト捕捉用 regex の両方が +// この定数から派生するので、両者が乖離することは構造上ありえない。 +const GEOLONIA_TILE_HOST = 'tiles.geolonia.com'; +const GEOLONIA_TILE_URL_TEMPLATE = `https://${GEOLONIA_TILE_HOST}/{z}/{x}/{y}.pbf`; +// サブドメイン付き (`osm.v3.tiles.geolonia.com` 等) もサブドメイン無しも同一 regex で捕捉。 +const GEOLONIA_TILE_PBF_REGEX = new RegExp( + String.raw`(?:[^/]+\.)?` + GEOLONIA_TILE_HOST.replace(/\./g, String.raw`\.`) + String.raw`\/.*\.pbf`, +); + +/** + * Geolonia のスタイル/タイル/スプライト等を最小レスポンスでモックする。 + * テスト先頭の beforeEach で呼ぶ前提。 + * + * 返すスタイルは layers=[background] のみのミニマル style v8。 + * 認証URL検証など transformRequest の挙動は実リクエスト前にここで握る。 + */ +export async function mockGeoloniaTiles(page: Page) { + // スタイル JSON (cdn.geolonia.com/style/...) + await page.route(/cdn\.geolonia\.com\/style\/.+\.json/, (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + version: 8, + sources: {}, + layers: [ + { id: 'background', type: 'background', paint: { 'background-color': '#cccccc' } }, + ], + }), + }), + ); + + // スプライト / グリフ / フォント関連は 404 でよい (background のみなので参照されない) + await page.route(/cdn\.geolonia\.com\/(sprite|glyph|font)/, (route) => + route.fulfill({ status: 404, body: '' }), + ); + + // TileJSON + await page.route(/tileserver\.geolonia\.com\/.*/, (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + tilejson: '2.2.0', + tiles: [GEOLONIA_TILE_URL_TEMPLATE], + minzoom: 0, + maxzoom: 14, + }), + }), + ); + + // ベクトルタイル (上記 TileJSON が返した URL を必ず捕捉) + await page.route(GEOLONIA_TILE_PBF_REGEX, (route) => + route.fulfill({ status: 204, body: '' }), + ); + + // api.geolonia.com (legacy / permission check 等) + await page.route(/api\.geolonia\.com\/.*/, (route) => + route.fulfill({ status: 200, contentType: 'application/json', body: '{}' }), + ); +} + +/** + * 指定 predicate にマッチしたリクエスト URL を記録する。 + * 呼び出し時点で配列を返し、以降の page 操作中に push される。 + * 認証パラメータ注入の検証などに使用。 + */ +export function recordRequests( + page: Page, + predicate: (url: string) => boolean = () => true, +): string[] { + const urls: string[] = []; + page.on('request', (req) => { + if (predicate(req.url())) urls.push(req.url()); + }); + return urls; +} diff --git a/e2e/sample.spec.ts b/e2e/sample.spec.ts new file mode 100644 index 00000000..de989f04 --- /dev/null +++ b/e2e/sample.spec.ts @@ -0,0 +1,35 @@ +/** + * PR0 sanity-check: helper.ts のヘルパー群が機能していることを確認する最小テスト。 + * 後続フェーズで具体的なテストが充実したら削除してよい。 + */ +import { test, expect } from '@playwright/test'; +import { + TEST_URL, + mockGeoloniaTiles, + waitForStyleLoad, + hasMapInstance, + getStyleLayerIds, + recordRequests, +} from './helper'; + +test.describe('PR0 helper sanity', () => { + test('mockGeoloniaTiles を適用すると minimal style だけが読み込まれる', async ({ page }) => { + await mockGeoloniaTiles(page); + await page.goto(`${TEST_URL}/nocontrol.html`); + await waitForStyleLoad(page); + + expect(await hasMapInstance(page)).toBe(true); + const layers = await getStyleLayerIds(page); + expect(layers).toEqual(['background']); + }); + + test('recordRequests で geolonia 系へのリクエストを観測できる', async ({ page }) => { + await mockGeoloniaTiles(page); + const styleRequests = recordRequests(page, (url) => url.includes('cdn.geolonia.com/style/')); + await page.goto(`${TEST_URL}/nocontrol.html`); + await waitForStyleLoad(page); + + expect(styleRequests.length).toBeGreaterThan(0); + expect(styleRequests[0]).toMatch(/cdn\.geolonia\.com\/style\/.+\.json/); + }); +}); diff --git a/playwright.config.ts b/playwright.config.ts index ea100657..3fcd7a62 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -32,42 +32,16 @@ export default defineConfig({ trace: 'on-first-retry', }, - /* Configure projects for major browsers */ + /* + * embed は maplibre-gl の薄いラッパーであり、クロスブラウザ互換は + * maplibre 本体のテストに委ねる。embed 側の E2E は chromium のみで + * 回し、iteration 速度を優先する。 + */ projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, - - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, ], /* Run your local dev server before starting the tests */