feat(04-04): the destination resolver — two carriers, one validation, memory only
- resolve-destination.ts: reads ?dest= (URL, primary — the inline web_app-button carrier) then initData's start_param (the signed string, secondary — the direct-link/home-screen carrier), ONE slug validation, dest_ prefix stripped; resolves against the webview's own build-time @araharah/search copy; honest null for unknown ids; module-scope memory-only holder (GEO-03/D-07) - destination-card.ts: the confirmation + honest-unavailable cards, createElement/textContent only, existing theme classes (T-04-32) - webapp.ts: TelegramWebAppLike gains startParam, addToHomeScreen?, checkHomeScreenStatus? + HomeScreenStatus (Bot API 8.0+ members) - telegram-mock.ts: satisfies the extended contract in the same change (startParam initData wiring, homeScreenStatus, __homeScreenAdded) - strings.ts: 5 new copy fields in en+ms; web depends on @araharah/search - DP1-DP12 green (45/45 web suite), web:typecheck green
This commit is contained in:
@@ -19,8 +19,18 @@
|
||||
*
|
||||
* This module ships nothing to production: `no-storage.test.mjs` and
|
||||
* the build-artifact gate both assert it is excluded from `web/dist`.
|
||||
*
|
||||
* Phase 4 (04-04): the mock satisfies the contract's three new OPTIONAL
|
||||
* members in the SAME change that introduces them — `startParam` (appended
|
||||
* to the fake initData as `&start_param=<encoded>`, the direct-link carrier
|
||||
* the ONBD-02 harness leg exercises), `homeScreenStatus` (what the mock's
|
||||
* checkHomeScreenStatus callback delivers; default "unknown", the iOS
|
||||
* reality), and the addToHomeScreen stub whose `__homeScreenAdded` counter
|
||||
* tests read (the `__events` / `__backButtonClicks` exposure precedent).
|
||||
* The mock's initData is HMAC-unvalidated by design — the runner page
|
||||
* stubs `/api/session`.
|
||||
*/
|
||||
import type { TelegramWebAppLike } from "../telegram/webapp.ts";
|
||||
import type { TelegramWebAppLike, HomeScreenStatus } from "../telegram/webapp.ts";
|
||||
|
||||
export interface MockOptions {
|
||||
platform?: "android" | "ios" | "tdesktop" | "macos" | "windows" | "linux" | "web";
|
||||
@@ -40,6 +50,15 @@ export interface MockOptions {
|
||||
telegramFix?: { lat: number; lng: number; accuracyM?: number | null; headingDeg?: number | null } | null;
|
||||
webviewFix?: { lat: number; lng: number; accuracyM?: number } | null;
|
||||
grantLocation?: boolean;
|
||||
/** The direct-link start parameter (04-04): appended to the mock initData
|
||||
* as `&start_param=<encoded>` — the SECONDARY destination carrier's
|
||||
* harness leg. The URL query carrier needs no mock: page.goto preserves
|
||||
* `?dest=` in window.location.search. */
|
||||
startParam?: string | null;
|
||||
/** What the mock's checkHomeScreenStatus callback delivers (04-04).
|
||||
* Defaults to "unknown" — the documented iOS reality, treated by the
|
||||
* app as "supported but indeterminate", never an error. */
|
||||
homeScreenStatus?: HomeScreenStatus;
|
||||
}
|
||||
|
||||
interface MockLocationManager {
|
||||
@@ -54,6 +73,7 @@ interface MockLocationManager {
|
||||
interface MockWebApp extends TelegramWebAppLike {
|
||||
__events: Map<string, Array<() => void>>;
|
||||
__backButtonClicks: Array<() => void>;
|
||||
__homeScreenAdded: number;
|
||||
}
|
||||
|
||||
/* ————— Web-Crypto dev signer (mirror of bot/src/security/initdata.ts's HMAC path) ————— */
|
||||
@@ -131,12 +151,15 @@ let installed: MockWebApp | null = null;
|
||||
* localeFromInitData parses the real wire format instead of falling
|
||||
* back to "en" (locale = options.languageCode, defaulting to "en").
|
||||
* Untouched when no languageCode is given (the plain-dev fallback mock
|
||||
* keeps initData "" exactly as before).
|
||||
* keeps initData "" exactly as before) — unless a startParam arrives
|
||||
* (04-04), in which case the wire carries it as `&start_param=<encoded>`
|
||||
* so the SECONDARY destination carrier is exercisable end-to-end.
|
||||
*/
|
||||
function buildMockInitData(languageCode?: string): string {
|
||||
if (languageCode === undefined) return "";
|
||||
const user = JSON.stringify({ language_code: languageCode, id: 1, first_name: "Harness" });
|
||||
return `user=${encodeURIComponent(user)}&auth_date=1&query_id=h&hash=harness`;
|
||||
function buildMockInitData(languageCode?: string, startParam?: string | null): string {
|
||||
if (languageCode === undefined && !startParam) return "";
|
||||
const user = JSON.stringify({ language_code: languageCode ?? "en", id: 1, first_name: "Harness" });
|
||||
const base = `user=${encodeURIComponent(user)}&auth_date=1&query_id=h&hash=harness`;
|
||||
return startParam ? `${base}&start_param=${encodeURIComponent(startParam)}` : base;
|
||||
}
|
||||
|
||||
export function installMockWebApp(options: MockOptions = {}): TelegramWebAppLike {
|
||||
@@ -178,20 +201,31 @@ export function installMockWebApp(options: MockOptions = {}): TelegramWebAppLike
|
||||
};
|
||||
const events = new Map<string, Array<() => void>>();
|
||||
const backButtonClicks: Array<() => void> = [];
|
||||
let homeScreenAddedCount = 0;
|
||||
const homeScreenStatus: HomeScreenStatus = options.homeScreenStatus ?? "unknown";
|
||||
const fake: MockWebApp = {
|
||||
initData: buildMockInitData(options.languageCode),
|
||||
initData: buildMockInitData(options.languageCode, options.startParam),
|
||||
startParam: options.startParam ?? undefined,
|
||||
colorScheme: options.colorScheme ?? "light",
|
||||
themeParams: {},
|
||||
version,
|
||||
platform: options.platform ?? "android",
|
||||
__events: events,
|
||||
__backButtonClicks: backButtonClicks,
|
||||
__homeScreenAdded: 0,
|
||||
isVersionAtLeast(v: string) {
|
||||
return versionAtLeast(version, v);
|
||||
},
|
||||
close() {
|
||||
/* the runner page listens to window-event hooks; nothing to close here */
|
||||
},
|
||||
addToHomeScreen() {
|
||||
homeScreenAddedCount += 1;
|
||||
fake.__homeScreenAdded = homeScreenAddedCount;
|
||||
},
|
||||
checkHomeScreenStatus(cb: (status: HomeScreenStatus) => void) {
|
||||
setTimeout(() => cb(homeScreenStatus), 0);
|
||||
},
|
||||
onEvent(event: string, cb: () => void) {
|
||||
const list = events.get(event) ?? [];
|
||||
list.push(cb);
|
||||
|
||||
Reference in New Issue
Block a user