Files
roboco/panel/src/lib/telegram/__tests__/hooks.test.tsx
T
3c5ee46347 feat(tg): Mini App V6 — premium overhaul (#609)
* feat(tg): Mini App V6 — premium overhaul (design system, Chat parity, Metrics drilldown, CEO verbs)

Design system: native type with tabular-numeral heroes (mono demoted to
the wordmark), borderless elevated cards, floating dock, Telegram
window-chrome painting via the theme bridge; Inbox moves behind a header
bell with humanized notifications (UUIDs resolve to task names).

Chat: honest Mine/Fleet split — participant-scoped CEO threads with real
unread counts and mark-read, watched fleet threads with reply-as-CEO on
task-linked conversations (watch-only otherwise), markdown transcripts,
live pulse flashes, and a pinned Secretary live chat on the panel's SSE
session runtime.

Metrics: new tab with period-segmented spend hero, by-agent/team/model
breakdowns, delivery + efficiency health, and a per-agent drilldown over
usage time-series (agent_slug) + member scorecard.

Board: tg-native grouped pipeline replacing the MobileTaskBoard wrapper;
task sheet gains the CEO decide verbs (approve / request changes /
unblock).

Security: /api/dashboard router now require_panel_token-gated at router
level (mirrors /api/usage), closing unauthenticated metrics exposure.

* fix(tg): restore Share Tech Mono brand voice, Phosphor icon set, borderless avatars

The mono returns as the numeral/brand voice (.tg-display — heroes, stat
values, wordmark) while labels stay native sentence case. The hand-drawn
duotone glyphs and lucide feature icons are replaced by Phosphor (MIT):
duotone at rest via an IconContext at the shell, filled weight on the
dock's active tab; row glyph maps (board statuses, inbox kinds, approval
kinds, quick actions) all move over. Team avatar tiles drop their borders
— tint-only squircles.

* fix(tg): fleet avatar strip breathes — spaced tiles instead of overlap

* polish(tg): taste-skill audit pass — em-dash purge, one icon family, separator rationing

Applied the design-taste audit against the cockpit: every em-dash in
visible UI copy rewritten (periods/commas/colons), the remaining lucide
chrome (carets, arrows, send, close, spinners) moved to Phosphor so the
tg tree ships one icon family (send is the native paper-plane, carets
bold), the hand-rolled chevron SVG deleted, and metadata lines rationed
to a single middle-dot separator.

* polish(tg): pipeline chip strip scrolls without a visible scrollbar

* fix(tests): metrics observability fixture uses a relative timestamp

The hardcoded _T0 (2026-06-20) aged out of the service's 30-day window
exactly 30 days later, detonating the suite on every branch. Two days
back from now() stays inside every window (30d metrics, 7d scorecards)
permanently.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-20 17:29:22 +02:00

152 lines
4.1 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { render } from "@testing-library/react";
import {
TgWebAppProvider,
useMainButton,
useBackButton,
type MainButtonOptions,
} from "../hooks";
import type { TelegramWebApp } from "../webapp";
function fakeMainButton() {
return {
setText: vi.fn(),
show: vi.fn(),
hide: vi.fn(),
enable: vi.fn(),
disable: vi.fn(),
showProgress: vi.fn(),
hideProgress: vi.fn(),
onClick: vi.fn(),
offClick: vi.fn(),
};
}
function fakeBackButton() {
return {
show: vi.fn(),
hide: vi.fn(),
onClick: vi.fn(),
offClick: vi.fn(),
};
}
function webAppWith(overrides: Partial<TelegramWebApp>): TelegramWebApp {
return {
ready: () => undefined,
expand: () => undefined,
initData: "",
...overrides,
};
}
function MainButtonHarness(props: MainButtonOptions) {
useMainButton(props);
return null;
}
function BackButtonHarness({ onBack }: { onBack: (() => void) | null }) {
useBackButton(onBack);
return null;
}
describe("useMainButton", () => {
let mainButton: ReturnType<typeof fakeMainButton>;
let webApp: TelegramWebApp;
beforeEach(() => {
mainButton = fakeMainButton();
webApp = webAppWith({ MainButton: mainButton });
});
it("configures and shows the button declaratively", () => {
render(
<TgWebAppProvider webApp={webApp}>
<MainButtonHarness text="Approve" visible onClick={() => undefined} />
</TgWebAppProvider>,
);
expect(mainButton.setText).toHaveBeenCalledWith("Approve");
expect(mainButton.enable).toHaveBeenCalled();
expect(mainButton.hideProgress).toHaveBeenCalled();
expect(mainButton.show).toHaveBeenCalled();
expect(mainButton.onClick).toHaveBeenCalledTimes(1);
});
it("reflects loading/disabled and invokes the latest onClick closure", () => {
const first = vi.fn();
const second = vi.fn();
const { rerender } = render(
<TgWebAppProvider webApp={webApp}>
<MainButtonHarness text="Approve" visible onClick={first} />
</TgWebAppProvider>,
);
rerender(
<TgWebAppProvider webApp={webApp}>
<MainButtonHarness
text="Approve"
visible
loading
disabled
onClick={second}
/>
</TgWebAppProvider>,
);
expect(mainButton.showProgress).toHaveBeenCalled();
expect(mainButton.disable).toHaveBeenCalled();
// Same subscribed handler survives rerenders but calls the fresh closure.
expect(mainButton.onClick).toHaveBeenCalledTimes(1);
const handler = mainButton.onClick.mock.calls[0][0] as () => void;
handler();
expect(first).not.toHaveBeenCalled();
expect(second).toHaveBeenCalledTimes(1);
});
it("unhooks and hides on unmount", () => {
const { unmount } = render(
<TgWebAppProvider webApp={webApp}>
<MainButtonHarness text="Approve" visible onClick={() => undefined} />
</TgWebAppProvider>,
);
unmount();
expect(mainButton.offClick).toHaveBeenCalledTimes(1);
expect(mainButton.hide).toHaveBeenCalled();
});
it("no-ops without a provider (outside Telegram)", () => {
expect(() =>
render(
<MainButtonHarness text="Approve" visible onClick={() => undefined} />,
),
).not.toThrow();
});
});
describe("useBackButton", () => {
it("shows while a handler is set, hides when null, unhooks on unmount", () => {
const backButton = fakeBackButton();
const webApp = webAppWith({ BackButton: backButton });
const onBack = vi.fn();
const { rerender, unmount } = render(
<TgWebAppProvider webApp={webApp}>
<BackButtonHarness onBack={onBack} />
</TgWebAppProvider>,
);
expect(backButton.show).toHaveBeenCalled();
const handler = backButton.onClick.mock.calls[0][0] as () => void;
handler();
expect(onBack).toHaveBeenCalledTimes(1);
rerender(
<TgWebAppProvider webApp={webApp}>
<BackButtonHarness onBack={null} />
</TgWebAppProvider>,
);
expect(backButton.hide).toHaveBeenCalled();
unmount();
expect(backButton.offClick).toHaveBeenCalledTimes(1);
});
});