Files
mechanic-pwa/driver-pwa/frontend/dev-proxy.test.ts
T
tremble7681andClaude Opus 5 ceec3db364 feat(pwa): вход для сотрудника, просмотр от лица водителя и вкладка «Отладка»
Приложение обкатывается на боевом контуре, поэтому интерфейс надо проверять на
живых данных — а живые данные лежат у настоящих водителей.

* «Вход для сотрудника» (/dev) — логин учётки CRM, поддержан второй шаг 2FA;
  дальше поиск водителя по ФИО/телефону/id и просмотр его экранов.
* Плашка «Смотрю как …» над всеми экранами: интерфейс в этом режиме неотличим
  от водительского, и чужой баланс легко принять за свой.
* Вкладка «Отладка» (/debug, водителю недоступна): смена водителя, driver_id,
  режим, срок токена, адрес API, коммит и время сборки, журнал последних
  40 ответивших запросов, сброс кэша сервис-воркера.
* Пополнение под просмотром идёт в учебный контур: экран /mock-pay/:orderId
  выбирает, что «ответит» банк, а настоящий экран оплаты разбирает эти статусы
  своим боевым кодом. Ни рубля, ни чека 54-ФЗ.

Заодно доезжает незакоммиченная работа прошлой сессии: дев-прокси в
vite.config.ts, его тест и разобранный .env.example.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:15:02 +10:00

104 lines
5.5 KiB
TypeScript

// @vitest-environment node
/**
* Локальная разработка PWA водителя.
*
* Гейт CORS на бэкенде (CORS_ALLOW_LOCALHOST, по умолчанию выключен) закрыл
* дыру, но заодно оставил этот фронт без рабочего дева: прокси не было, а
* client.ts по умолчанию уходит на боевой https://crm.pptaxi.ru/api — то есть
* «включи CORS_ALLOW_LOCALHOST» тут не помогало, запрос вообще не доходил до
* локального бэкенда.
*
* Тесты держат обе половины договорённости:
* 1) `npm run dev` доводит фронт до локального бэкенда сам, без правки .env;
* 2) боевая сборка от этого не меняется — относительная дев-база в бандл
* попасть не может (в проде он живёт на app.pptaxi.ru, где /api нет).
*
* Конфиг резолвится по-настоящему (resolveConfig читает vite.config.ts и
* env-файлы) — иначе тест проверял бы не то, что увидит vite.
*
* Файл лежит рядом с vite.config.ts и НЕ называется vite.config.test.ts:
* в дефолтном exclude vitest есть `**\/{...,vite,...}.config.*`, под который
* такое имя попадает целиком — тест молча не запускался бы.
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import path from "path";
import { fileURLToPath } from "url";
import { resolveConfig } from "vite";
const ROOT = path.dirname(fileURLToPath(import.meta.url));
/** Обе переменные — process.env, и dev-дефолт их же и трогает: сохраняем. */
const SAVED = {
base: process.env.VITE_API_BASE,
target: process.env.VITE_API_TARGET,
};
function restore(key: "VITE_API_BASE" | "VITE_API_TARGET", value: string | undefined): void {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
beforeEach(() => {
delete process.env.VITE_API_BASE;
delete process.env.VITE_API_TARGET;
});
afterEach(() => {
restore("VITE_API_BASE", SAVED.base);
restore("VITE_API_TARGET", SAVED.target);
});
const resolve = (command: "serve" | "build", mode: "development" | "production") =>
resolveConfig({ root: ROOT, configFile: path.join(ROOT, "vite.config.ts"), logLevel: "silent" },
command, mode, mode);
describe("dev-сервер доводит фронт до локального бэкенда", () => {
it("подставляет относительную базу — запрос идёт со своего origin, CORS не участвует", async () => {
const cfg = await resolve("serve", "development");
expect(cfg.env.VITE_API_BASE).toBe("/api");
});
it("проксирует /api на локальный бэкенд, а не на боевой домен", async () => {
const cfg = await resolve("serve", "development");
const proxy = cfg.server.proxy?.["/api"];
expect(typeof proxy === "object" && proxy.target).toBe("http://127.0.0.1:8000");
});
it("цель прокси переопределяется VITE_API_TARGET (стенд, тайлнет)", async () => {
process.env.VITE_API_TARGET = "http://127.0.0.1:8090";
const cfg = await resolve("serve", "development");
const proxy = cfg.server.proxy?.["/api"];
expect(typeof proxy === "object" && proxy.target).toBe("http://127.0.0.1:8090");
});
it("база и путь прокси — одно и то же", async () => {
// Две половины рецепта живут в разных местах конфига: базу подставляем в
// env, путь перечисляем в server.proxy. Разъедутся (например, база станет
// "/backend") — dev-сервер отдаст на этот путь index.html вместо ответа
// бэкенда, и падать будет уже в разборе JSON, далеко от причины.
const cfg = await resolve("serve", "development");
expect(Object.keys(cfg.server.proxy ?? {})).toContain(cfg.env.VITE_API_BASE);
});
it("не перебивает базу, заданную разработчиком", async () => {
process.env.VITE_API_BASE = "https://stand.example/api";
const cfg = await resolve("serve", "development");
expect(cfg.env.VITE_API_BASE).toBe("https://stand.example/api");
});
});
describe("боевая сборка остаётся боевой", () => {
it("дев-дефолт в сборку не течёт: base пустая → client.ts берёт прод-адрес", async () => {
const cfg = await resolve("build", "production");
expect(cfg.env.VITE_API_BASE).toBeUndefined();
expect(process.env.VITE_API_BASE).toBeUndefined();
});
it("ни один env-файл не подсовывает сборке относительную базу", async () => {
// Относительный /api в проде означал бы app.pptaxi.ru/api — такого нет.
// Ловит и коммит .env с дев-значением, и локальный .env на машине сборки.
const cfg = await resolve("build", "production");
expect(cfg.env.VITE_API_BASE).not.toBe("/api");
});
});