diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..97efb08 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,99 @@ +name: Tests + +on: + push: + branches: + - '**' + +permissions: + contents: read + +env: + NODE_VERSION: '20' + # The app schema requires a non-empty key; map-focused E2E tests intercept + # the SDK request with a deterministic local stub. + VITE_YMAP_KEY: test-yandex-maps-key + +jobs: + unit-tests: + name: Unit and component tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci --legacy-peer-deps + + - name: Run Vitest + run: npm test + + e2e-tests: + name: Playwright e2e tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci --legacy-peer-deps + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Run Playwright e2e + run: npm run test:e2e + + - name: Upload Playwright report + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: | + playwright-report/ + test-results/ + if-no-files-found: ignore + + real-api-tests: + name: Playwright real API smoke tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci --legacy-peer-deps + + - name: Run Playwright real API smoke + run: npm run test:e2e:real-api + + - name: Upload Playwright real API report + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-real-api-report + path: | + phase-05-uat/real-api-report/ + test-results/ + if-no-files-found: ignore diff --git a/playwright.config.ts b/playwright.config.ts index 51bfcb7..c6aabc9 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from '@playwright/test'; export default defineConfig({ testDir: './tests/e2e', + testIgnore: 'real-api.spec.ts', fullyParallel: true, retries: process.env.CI ? 2 : 0, reporter: 'html', diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 425e21f..7b729c7 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -1,4 +1,4 @@ -import { http, HttpResponse } from 'msw'; +import { delay, http, HttpResponse } from 'msw'; import { env, MAX_PAST_DAYS, MAX_FUTURE_HOURS } from '@/shared/config'; import { generateMockUserProfile } from './generators/users'; import { generateMockAuthUser } from './generators/auth'; @@ -338,7 +338,13 @@ export const handlers = [ // hide_location_types) и применяет их через applyMockFilters после filterByBbox. // Это эмулирует server-side filter path D-12 — E2E тест видит реальное // изменение количества зон при переключении фильтров. - http.get(`${baseUrl}/zones`, ({ request }) => { + http.get(`${baseUrl}/zones`, async ({ request }) => { + // Keep the browser mock asynchronous enough for rapid viewport/filter + // changes to exercise React Query's AbortSignal cascade. An immediate + // in-memory response always wins the race and makes cancellation E2E tests + // measure mock speed instead of application behaviour. + await delay(250); + const url = new URL(request.url); const bboxRaw = url.searchParams.get('bbox'); const view = url.searchParams.get('view') ?? 'full'; diff --git a/src/shared/lib/yandex/suggest.ts b/src/shared/lib/yandex/suggest.ts index 5822906..6ed0e0b 100644 --- a/src/shared/lib/yandex/suggest.ts +++ b/src/shared/lib/yandex/suggest.ts @@ -1,4 +1,3 @@ -import { searchGeo } from '@/shared/lib/ymaps'; import { SUGGEST_MIN_QUERY_LENGTH } from '@/shared/config'; export interface SuggestResult { @@ -44,6 +43,11 @@ export async function suggestAddresses( ): Promise { if (text.trim().length < SUGGEST_MIN_QUERY_LENGTH) return []; try { + // Loading the Yandex Maps runtime at module evaluation time blocks the + // whole application shell when the external SDK is unavailable. Address + // search is user-initiated, so load its runtime only when it is needed. + const { searchGeo } = await import('@/shared/lib/ymaps'); + // bbox = [west, south, east, north] (наш канонический формат) → bounds // [[swLon, swLat], [neLon, neLat]] для ymaps3.search. Передаём viewport // как bias: улицы рядом с тем, что юзер видит на карте, идут первыми. diff --git a/src/widgets/map-canvas/index.ts b/src/widgets/map-canvas/index.ts index 7b70749..57075bb 100644 --- a/src/widgets/map-canvas/index.ts +++ b/src/widgets/map-canvas/index.ts @@ -1,9 +1,5 @@ -export { MapCanvas } from './ui/MapCanvas'; -export { MapSkeleton } from './ui/MapSkeleton'; -export { ZoneLayer } from './ui/ZoneLayer'; -export { ParallelZoneLayer } from './ui/ParallelZoneLayer'; -export { ZoneBadgesLayer } from './ui/ZoneBadgesLayer'; -export { ZoneClusterLayer } from './ui/ZoneClusterLayer'; -export { ZoneStateOverlay } from './ui/ZoneStateOverlay'; +// Keep this public entry point free of map UI exports. MapCanvas loads the +// external Yandex Maps runtime with top-level await and must only be imported +// through the lazy boundaries in the page layouts. export { MapRefContext } from './model/map-ref-context'; export { useZoomToZone } from './model/useZoomToZone'; diff --git a/src/widgets/time-selector/ui/TimeSelectorChip.tsx b/src/widgets/time-selector/ui/TimeSelectorChip.tsx index 257b825..b50dd40 100644 --- a/src/widgets/time-selector/ui/TimeSelectorChip.tsx +++ b/src/widgets/time-selector/ui/TimeSelectorChip.tsx @@ -25,6 +25,7 @@ export function TimeSelectorChip({ onClick }: Props) { return (