Разработана первая версия приложений

This commit is contained in:
mi
2026-07-10 18:06:14 +03:00
parent aa8761d1b3
commit 8c7b4074c4
162 changed files with 12178 additions and 16 deletions
@@ -0,0 +1,49 @@
import { expect, test } from "@playwright/test";
test.beforeEach(async ({ page }) => {
await page.route("**/api/v1/public/app-config", (route) => route.fulfill({
json: {
auth: { phone_enabled: true, password_enabled: false },
operator: { call_phone: "+74950000000" },
ux: { idle_timeout_minutes: 15 },
attachments: { max_size_mb: 5, allowed_extensions: ["png", "pdf"], allowed_mime_types: ["image/png", "application/pdf"] },
consents: {
personal_data: { required: true, version: "2026-07-01", document_url: "https://example.test/personal" },
user_agreement: { required: true, version: "2026-07-01", document_url: "https://example.test/agreement" },
marketing: { required: false, version: "2026-07-01", document_url: "https://example.test/marketing" },
},
},
}));
await page.route("**/api/v1/public/content", (route) => route.fulfill({
json: {
locale: "ru",
texts: { welcome: "Добро пожаловать в HAN Chat" },
popular_questions: [{ id: "visa", mnemonic: "visa", text: "Как оформить визу?" }],
version: "1",
},
}));
});
test("гостевой экран загружает публичный контент", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("heading", { name: "Помощь мигрантам" })).toBeVisible();
await expect(page.getByText("Добро пожаловать в HAN Chat")).toBeVisible();
await expect(page.getByRole("button", { name: "Как оформить визу?" })).toBeVisible();
await expect(page.getByText(/Гость/)).toBeVisible();
});
test("первое сообщение требует обязательные согласия", async ({ page }) => {
await page.goto("/");
await page.getByLabel("Сообщение").fill("Здравствуйте");
await page.getByRole("button", { name: "Отправить" }).click();
await expect(page.getByRole("heading", { name: "Согласия перед входом" })).toBeVisible();
await expect(page.getByRole("button", { name: "Продолжить" })).toBeDisabled();
});
test("профиль гостя не делает защищённый запрос", async ({ page }) => {
let protectedCalls = 0;
await page.route("**/api/v1/me", (route) => { protectedCalls++; return route.abort(); });
await page.goto("/profile");
await expect(page.getByText("Профиль доступен после авторизации.")).toBeVisible();
expect(protectedCalls).toBe(0);
});
@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import { reconcileMessages } from "../../src/reconcile";
import { sessionMemory } from "../../src/session";
import { SingleFlight } from "../../src/single-flight";
import { websocketJwtProtocol } from "../../src/realtime";
import type { Message } from "../../src/types";
const message = (id: string, createdAt: string, status: Message["delivery_status"] = "accepted"): Message => ({
message_id: id,
dialog_id: "dialog",
sender_type: "client",
content_kind: "text",
text: "Тест",
attachments: [],
safety_status: "allowed",
delivery_status: status,
created_at: createdAt,
});
describe("reconcileMessages", () => {
it("устраняет дубли, обновляет статус и сортирует сообщения", () => {
const result = reconcileMessages(
[message("2", "2026-01-02T00:00:00Z"), message("1", "2026-01-01T00:00:00Z")],
[message("2", "2026-01-02T00:00:00Z", "delivered"), message("3", "2026-01-03T00:00:00Z")],
);
expect(result.map((item) => item.message_id)).toEqual(["1", "2", "3"]);
expect(result[1]?.delivery_status).toBe("delivered");
});
});
describe("UX session memory", () => {
it("хранит идентификатор только в памяти и очищает его", () => {
sessionMemory.set("ux-test");
expect(sessionMemory.id).toBe("ux-test");
sessionMemory.clear();
expect(sessionMemory.id).toBeNull();
expect(localStorage.getItem("ux_session_id")).toBeNull();
});
});
describe("SingleFlight", () => {
it("объединяет параллельные refresh операции", async () => {
const flight = new SingleFlight<number>();
let calls = 0;
const operation = async () => {
calls++;
await Promise.resolve();
return 42;
};
const [first, second, third] = await Promise.all([
flight.run(operation), flight.run(operation), flight.run(operation),
]);
expect([first, second, third]).toEqual([42, 42, 42]);
expect(calls).toBe(1);
});
});
describe("WebSocket authentication protocol", () => {
it("кодирует JWT как canonical han.jwt.<base64url(jwt)>", () => {
const protocol = websocketJwtProtocol("header.payload.signature");
expect(protocol).toBe("han.jwt.aGVhZGVyLnBheWxvYWQuc2lnbmF0dXJl");
expect(protocol).not.toContain("=");
});
});