mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* 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>
152 lines
4.1 KiB
TypeScript
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);
|
|
});
|
|
});
|