Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/mobile-smoke-polish/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

# Проверка исправлений после мобильного smoke-test

Этот отчёт и скриншоты фиксируют PR #405 (`09f9bc1d`). После уточнения, что чаты скрыты, глобальное предупреждение о фоновом соединении удалено отдельным исправлением. Актуальный `connection.cjs` проверяет отсутствие этого уведомления и сохранение переподключения/онлайн-статусов; [проверка исправления](hidden-chat-notice.md). Описание уведомления ниже относится к историческому состоянию #405.

Эталон desktop: `origin/dev` на `706a574ed916c6b0a78e99402588ee4889657e74`, после восстановления desktop в #403 и форматирования в #404. Эта проверка подтверждает отсутствие новых desktop-регрессий относительно уже восстановленного dev; сравнение с состоянием до #401 находится в `docs/desktop-regression`.

## Исправления и acceptance criteria
Expand Down
81 changes: 62 additions & 19 deletions docs/mobile-smoke-polish/connection.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ const directory = path.resolve(process.argv[3] || ".desktop-regression/mobile-sm
window.qaChatOffline = true;
window.qaChatConnected = false;
window.qaChatSockets = [];
window.qaToastCount = 0;
new MutationObserver(records => {
for (const record of records) {
for (const node of record.addedNodes) {
if (
node.nodeType === 1 &&
(node.matches(".snackbar__item") || node.querySelector(".snackbar__item"))
) {
window.qaToastCount++;
}
}
}
}).observe(document, { childList: true, subtree: true });
window.WebSocket = class {
static OPEN = 1;
static CONNECTING = 0;
Expand Down Expand Up @@ -48,37 +61,68 @@ const directory = path.resolve(process.argv[3] || ".desktop-regression/mobile-sm
});
const page = harness.page;
await page.goto(baseUrl + "/office/feed");
await page.locator("app-open-vacancy").first().waitFor({ state: "attached" });
const toast = page.locator(".snackbar__item");
await toast.waitFor({ state: "visible", timeout: 30000 });
assert.equal(await toast.count(), 1);
assert.match(await toast.innerText(), /Чат временно недоступен/);
// Проходим порог быстрых retry и ещё две попытки; фон продолжает работать без toast.
await page.waitForFunction(() => window.qaChatSockets.length >= 7, null, { timeout: 20000 });
assert.equal(
await toast.count(),
0,
"Hidden chat outage must not create a global notification",
);
await page.evaluate(() => document.fonts.ready);
await toast.screenshot({ path: path.join(directory, "unavailable-390.png") });
await page.getByLabel("Закрыть уведомление").click();
await toast.waitFor({ state: "hidden" });
const attempts = await page.evaluate(() => window.qaChatSockets.length);
await page.waitForFunction(attempts => window.qaChatSockets.length > attempts, attempts, {
timeout: 10000,
await page.screenshot({
path: path.join(directory, "background-outage-390.png"),
fullPage: true,
});
assert.equal(await toast.count(), 0, "Dismissed toast must not reappear during same outage");
await page.evaluate(() => {
window.qaChatOffline = false;
});
await page.waitForFunction(() => window.qaChatConnected, null, { timeout: 10000 });
assert.equal(await toast.count(), 0);

await page.evaluate(() => {
history.pushState({}, "", "/office/profile/1");
dispatchEvent(new PopStateEvent("popstate"));
});
await page.locator("app-profile-left-side").waitFor({ state: "attached" });
const onlineBadge = page.locator(".info__avatar .avatar__online");
assert.equal(await onlineBadge.count(), 0);
const emitStatus = type =>
page.evaluate(type => {
window.qaChatSockets.at(-1).onmessage(
new MessageEvent("message", {
data: JSON.stringify({ type, content: { user_id: 1 } }),
}),
);
}, type);
await emitStatus("set_online");
await onlineBadge.waitFor({ state: "visible" });
await emitStatus("set_offline");
await onlineBadge.waitFor({ state: "hidden" });

const attempts = await page.evaluate(() => window.qaChatSockets.length);
await page.evaluate(() => {
window.qaChatOffline = true;
window.qaChatConnected = false;
window.qaChatSockets.at(-1).onerror(new Event("error"));
});
await toast.waitFor({ state: "visible", timeout: 30000 });
assert.equal(await toast.count(), 1, "New outage must produce one new notification");
await page.waitForFunction(attempts => window.qaChatSockets.length >= attempts + 6, attempts, {
timeout: 20000,
});
assert.equal(await toast.count(), 0, "Second outage must also stay silent");
await page.evaluate(() => {
window.qaChatOffline = false;
});
await page.waitForFunction(() => window.qaChatConnected, null, { timeout: 10000 });
await toast.waitFor({ state: "hidden" });
assert.equal(await toast.count(), 0, "Reconnect must dismiss notification automatically");
await emitStatus("set_online");
await onlineBadge.waitFor({ state: "visible" });
assert.equal(await toast.count(), 0);
assert.equal(
await page.evaluate(() => window.qaToastCount),
0,
"No transient toast during any outage",
);
assert.deepEqual(harness.errors, []);
fs.writeFileSync(
path.join(directory, "result.json"),
Expand All @@ -87,11 +131,10 @@ const directory = path.resolve(process.argv[3] || ".desktop-regression/mobile-sm
width: 390,
syntheticWebSocket: true,
httpAvailable: true,
oneNotificationPerOutage: true,
manualDismiss: true,
noRedisplayAfterDismiss: true,
secondOutage: true,
autoDismissOnReconnect: true,
noGlobalNotification: true,
automaticReconnect: true,
secondOutageSilent: true,
onlineStatusesAfterReconnect: true,
passed: true,
},
null,
Expand Down
44 changes: 44 additions & 0 deletions docs/mobile-smoke-polish/hidden-chat-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!-- @format -->

# Удаление уведомления о фоновом соединении скрытых чатов

База: `dev` на `c2413ce6`, после merge #405. Чаты скрыты в навигации, но `OfficeInfoService` продолжает подключаться к `/ws/chat/` для обновления онлайн-статусов пользователей. Потеря этого канала не доказывает недоступность HTTP API или остальных разделов платформы.

## Изменение

Удалён `ConnectionStatusToastService` и его глобальная инициализация в `AppComponent`. Уведомление о недоступности скрытых чатов больше не появляется. Автоматический retry, очередь сообщений, обновление онлайн-статусов, API-контракты, бизнес-правила, навигация и responsive-стили не изменены. Сервис обычных snackbar и сообщения об ошибках пользовательских операций сохранены.

## Acceptance criteria и review

| AC | Требование | Evidence |
| --- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Фоновый сбой не создаёт глобальное предупреждение | AppComponent regression test; browser: первый и второй обрыв, включая retry после порога; MutationObserver подтверждает отсутствие даже кратковременного snackbar |
| 2 | Сохраняются переподключение и онлайн-статусы | WebsocketService tests; browser: после обоих восстановлений `set_online` обновляет badge профиля, `set_offline` его убирает |
| 3 | Обычные ошибки не подавляются | AppComponent test подтверждает доставку другого error snackbar; SnackbarComponent tests; source review: общий обработчик уведомлений и HTTP/API paths не менялись |
| 4 | Desktop/mobile layout сохранён | Source review: HTML/SCSS, компоненты страниц и их раскладка не изменены; единственное исчезновение UI — согласованное предупреждение при фоновом сбое |

Review: все AC проверены указанным способом, существенных незакрытых замечаний нет. Изменение frontend-only; permissions, миграции данных и backend deployment не затронуты.

## Проверки

- Адресные tests: **14 tests / 4 files, exit 0** (`AppComponent`, `WebsocketService`, `ChatRealtimeRepository`, `SnackbarComponent`).
- `npm run build:pr`: exit 0.
- `npm run lint:ts`: exit 0, 0 errors и 6 существующих warnings вне diff.
- Browser на 390 px: синтетический недоступный WebSocket при рабочем HTTP → retry после порога без snackbar → reconnect → online/offline badge → повторный обрыв без snackbar → reconnect и online badge; все assertions прошли, console errors отсутствуют.
- `npm run test:ci`: **2014 tests / 406 files, exit 0**. Три существующих исключения legacy specs в конфигурации Vitest не изменены (`projects.component.spec.ts`, `program.component.spec.ts`, `editor-submit-button.directive.spec.ts`).
- `npm run format:check`: проверка полного tracked-снимка Git index с LF, как в CI; приватные build/QA artifacts исключены из снимка.

## Воспроизведение browser check

Собрать текущую ветку через `npm run build:pr`, установить `PLAYWRIGHT_MODULE`/`CHROME_PATH` на доступные Playwright и Chrome. В PowerShell:

```powershell
$env:QA_BUILD_ROOT = 'dist/social_platform'
node docs/mobile-smoke-polish/connection.cjs http://127.0.0.1:4452 .desktop-regression/hidden-chat-notice/browser
```

`preview.cjs` доставляет неизменённые build-файлы напрямую через Playwright route, поэтому локальный HTTP server не нужен. API и WebSocket заменены синтетическими fixtures; live-запросов нет. JSON результата и screenshot находятся в указанном приватном output-каталоге.

## Не проверено

Реальные iOS/Android, Safari/WebKit и сетевые события на dev-стенде. В GitHub нет workflow для PR: проверочный deploy workflow запускается по push в dev после merge. На устройствах после merge проверить, что возврат из фона не вызывает глобального предупреждения о чатах.

This file was deleted.

This file was deleted.

28 changes: 26 additions & 2 deletions projects/social_platform/src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
import { TestBed } from "@angular/core/testing";
import { AppComponent } from "./app.component";
import { AuthRepositoryPort } from "@domain/auth/ports/auth.repository.port";
import { of } from "rxjs";
import { TokenService } from "@corelib";
import { BehaviorSubject, of } from "rxjs";
import { TokenService, WebsocketService } from "@corelib";
import { LoadingService } from "@api/shared/loading.service";
import { SnackbarService } from "@domain/shared/snackbar.service";
import { Snack } from "@domain/shared/snack.model";
import { beforeEach, describe, expect, it, vi } from "vitest";

describe("AppComponent", () => {
let connectionStatus: BehaviorSubject<"idle" | "connected" | "unavailable">;

beforeEach(async () => {
connectionStatus = new BehaviorSubject<"idle" | "connected" | "unavailable">("connected");
const authPortSpy = {
fetchProfile: of({} as any),
fetchUserRoles: of([]),
Expand All @@ -30,6 +35,10 @@ describe("AppComponent", () => {
{ provide: AuthRepositoryPort, useValue: authPortSpy },
{ provide: TokenService, useValue: tokenSpy },
{ provide: LoadingService, useValue: loadingSpy },
{
provide: WebsocketService,
useValue: { connectionStatus$: connectionStatus.asObservable() },
},
],
}).compileComponents();
});
Expand All @@ -39,4 +48,19 @@ describe("AppComponent", () => {
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});

it("не уведомляет о фоне скрытых чатов и сохраняет остальные сообщения ошибок", () => {
const snackbar = TestBed.inject(SnackbarService);
const emitted: Snack[] = [];
snackbar.snacks.subscribe(snack => emitted.push(snack));
TestBed.createComponent(AppComponent);

connectionStatus.next("unavailable");
connectionStatus.next("connected");
connectionStatus.next("unavailable");
expect(emitted).toEqual([]);

snackbar.error("Не удалось загрузить программу", { timeout: 0 });
expect(emitted.map(snack => snack.text)).toEqual(["Не удалось загрузить программу"]);
});
});
6 changes: 1 addition & 5 deletions projects/social_platform/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { AuthRepositoryPort } from "@domain/auth/ports/auth.repository.port";
import { LoadingService } from "@api/shared/loading.service";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { AppRoutes } from "@api/paths/app-routes";
import { ConnectionStatusToastService } from "@api/connection-status/connection-status-toast.service";

/**
* Корневой компонент приложения
Expand All @@ -44,10 +43,7 @@ export class AppComponent implements OnInit {
private tokenService: TokenService,
private router: Router,
private loadingService: LoadingService,
) {
// Инстанциируем listener для toast'а при потере WS-соединения.
inject(ConnectionStatusToastService);
}
) {}

ngOnInit(): void {
const showLoaderEvents = this.router.events.pipe(
Expand Down