Накатил человеческий дизайн
This commit is contained in:
@@ -26,12 +26,25 @@ test.beforeEach(async ({ page }) => {
|
||||
|
||||
test("гостевой экран загружает публичный контент", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("heading", { name: "Помощь мигрантам" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Привет! Я HAN" })).toBeVisible();
|
||||
await expect(page.getByText("Добро пожаловать в HAN Chat")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Как оформить визу?" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Голосовое сообщение" })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "Прикрепить файл" })).toBeVisible();
|
||||
await expect(page.getByText("Продление патента через 14 дней")).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "Связь с оператором" })).toBeEnabled();
|
||||
await expect(page.getByText("+74950000000")).toHaveCount(0);
|
||||
await expect(page.getByText(/Гость/)).toBeVisible();
|
||||
});
|
||||
|
||||
test("История открывает текущий единый чат", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByRole("link", { name: "История" }).click();
|
||||
await expect(page).toHaveURL(/\/dialogs$/);
|
||||
await expect(page.getByRole("heading", { name: "Чат" })).toBeVisible();
|
||||
await expect(page.getByText(/доступен после авторизации/)).toBeVisible();
|
||||
});
|
||||
|
||||
test("первое сообщение требует обязательные согласия", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByLabel("Сообщение").fill("Здравствуйте");
|
||||
@@ -40,6 +53,21 @@ test("первое сообщение требует обязательные с
|
||||
await expect(page.getByRole("button", { name: "Продолжить" })).toBeDisabled();
|
||||
});
|
||||
|
||||
test("Enter отправляет введённое сообщение", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByLabel("Сообщение").fill("Отправка с клавиатуры");
|
||||
await page.getByLabel("Сообщение").press("Enter");
|
||||
await expect(page.getByRole("heading", { name: "Согласия перед входом" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("сообщение ограничено 4000 символами", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByLabel("Сообщение").fill("а".repeat(4001));
|
||||
await page.getByRole("button", { name: "Отправить" }).click();
|
||||
await expect(page.getByRole("alert")).toContainText("Максимум — 4000 символов");
|
||||
await expect(page.getByRole("heading", { name: "Согласия перед входом" })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("профиль гостя не делает защищённый запрос", async ({ page }) => {
|
||||
let protectedCalls = 0;
|
||||
await page.route("**/api/v1/me", (route) => { protectedCalls++; return route.abort(); });
|
||||
|
||||
@@ -3,6 +3,12 @@ import { reconcileMessages } from "../../src/reconcile";
|
||||
import { sessionMemory } from "../../src/session";
|
||||
import { SingleFlight } from "../../src/single-flight";
|
||||
import { websocketJwtProtocol } from "../../src/realtime";
|
||||
import {
|
||||
clearPendingTextIntent,
|
||||
createPendingTextIntent,
|
||||
loadPendingTextIntent,
|
||||
savePendingTextIntent,
|
||||
} from "../../src/pending-intent";
|
||||
import type { Message } from "../../src/types";
|
||||
|
||||
const message = (id: string, createdAt: string, status: Message["delivery_status"] = "accepted"): Message => ({
|
||||
@@ -26,6 +32,15 @@ describe("reconcileMessages", () => {
|
||||
expect(result.map((item) => item.message_id)).toEqual(["1", "2", "3"]);
|
||||
expect(result[1]?.delivery_status).toBe("delivered");
|
||||
});
|
||||
|
||||
it("сортирует realtime-сообщения по абсолютному времени при разных часовых поясах", () => {
|
||||
const result = reconcileMessages(
|
||||
[message("client", "2026-07-21T15:48:00+03:00")],
|
||||
[message("company", "2026-07-21T12:49:00Z")],
|
||||
);
|
||||
|
||||
expect(result.map((item) => item.message_id)).toEqual(["client", "company"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("UX session memory", () => {
|
||||
@@ -38,6 +53,24 @@ describe("UX session memory", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("pending message intent", () => {
|
||||
it("сохраняет ключи повтора и очищается после завершения", () => {
|
||||
const intent = createPendingTextIntent("Сообщение после входа");
|
||||
savePendingTextIntent(intent);
|
||||
|
||||
expect(loadPendingTextIntent()).toEqual(intent);
|
||||
|
||||
clearPendingTextIntent();
|
||||
expect(loadPendingTextIntent()).toBeNull();
|
||||
});
|
||||
|
||||
it("игнорирует повреждённое значение", () => {
|
||||
sessionStorage.setItem("han.pending-message", "{\"text\":42}");
|
||||
expect(loadPendingTextIntent()).toBeNull();
|
||||
clearPendingTextIntent();
|
||||
});
|
||||
});
|
||||
|
||||
describe("SingleFlight", () => {
|
||||
it("объединяет параллельные refresh операции", async () => {
|
||||
const flight = new SingleFlight<number>();
|
||||
|
||||
Reference in New Issue
Block a user