feat: migrate to Next.js for Mini App and enhance project structure

- Replaced the previous webapp with a new Mini App built using Next.js, improving performance and maintainability.
- Updated the `.gitignore` to exclude Next.js build artifacts and node modules.
- Revised documentation in `AGENTS.md`, `README.md`, and `architecture.md` to reflect the new Mini App structure and technology stack.
- Enhanced Dockerfile to support the new build process for the Next.js application.
- Updated CI workflow to build and test the Next.js application.
- Added new configuration options for the Mini App, including `MINI_APP_SHORT_NAME` for improved deep linking.
- Refactored frontend testing setup to accommodate the new structure and testing framework.
- Removed legacy webapp files and dependencies to streamline the project.
This commit is contained in:
2026-03-03 16:04:08 +03:00
parent 2de5c1cb81
commit 16bf1a1043
148 changed files with 20240 additions and 7270 deletions

View File

@@ -0,0 +1,140 @@
/**
* Unit tests for useTelegramTheme, getFallbackScheme, and applyTheme.
* Ported from webapp/js/theme.test.js (getTheme, applyTheme).
*/
import { describe, it, expect, vi, afterEach } from "vitest";
import { renderHook } from "@testing-library/react";
import {
useTelegramTheme,
getFallbackScheme,
applyTheme,
} from "./use-telegram-theme";
vi.mock("@telegram-apps/sdk-react", () => ({
useSignal: vi.fn(() => undefined),
isThemeParamsDark: vi.fn(),
setMiniAppBackgroundColor: { isAvailable: vi.fn(() => false) },
setMiniAppHeaderColor: { isAvailable: vi.fn(() => false) },
}));
describe("getFallbackScheme", () => {
const originalMatchMedia = window.matchMedia;
const originalGetComputedStyle = window.getComputedStyle;
afterEach(() => {
window.matchMedia = originalMatchMedia;
window.getComputedStyle = originalGetComputedStyle;
document.documentElement.removeAttribute("data-theme");
vi.clearAllMocks();
});
it("returns dark when prefers-color-scheme is dark", () => {
window.matchMedia = vi.fn((query: string) => ({
matches: query === "(prefers-color-scheme: dark)",
media: query,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
addListener: vi.fn(),
removeListener: vi.fn(),
dispatchEvent: vi.fn(),
onchange: null,
})) as unknown as typeof window.matchMedia;
expect(getFallbackScheme()).toBe("dark");
});
it("returns light when prefers-color-scheme is light", () => {
window.matchMedia = vi.fn((query: string) => ({
matches: query === "(prefers-color-scheme: light)",
media: query,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
addListener: vi.fn(),
removeListener: vi.fn(),
dispatchEvent: vi.fn(),
onchange: null,
})) as unknown as typeof window.matchMedia;
expect(getFallbackScheme()).toBe("light");
});
it("uses --tg-color-scheme when set on document", () => {
window.getComputedStyle = vi.fn(() =>
Object.assign(
{},
{
getPropertyValue: (prop: string) =>
prop === "--tg-color-scheme" ? " light " : "",
}
)
) as unknown as typeof window.getComputedStyle;
expect(getFallbackScheme()).toBe("light");
});
it("uses --tg-color-scheme dark when set", () => {
window.getComputedStyle = vi.fn(() =>
Object.assign(
{},
{
getPropertyValue: (prop: string) =>
prop === "--tg-color-scheme" ? "dark" : "",
}
)
) as unknown as typeof window.getComputedStyle;
expect(getFallbackScheme()).toBe("dark");
});
});
describe("applyTheme", () => {
afterEach(() => {
document.documentElement.removeAttribute("data-theme");
vi.clearAllMocks();
});
it("sets data-theme to given scheme", () => {
applyTheme("light");
expect(document.documentElement.getAttribute("data-theme")).toBe("light");
applyTheme("dark");
expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
});
it("resolves scheme via getFallbackScheme when no argument", () => {
window.matchMedia = vi.fn((query: string) => ({
matches: query === "(prefers-color-scheme: dark)",
media: query,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
addListener: vi.fn(),
removeListener: vi.fn(),
dispatchEvent: vi.fn(),
onchange: null,
})) as unknown as typeof window.matchMedia;
applyTheme();
expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
});
});
describe("useTelegramTheme", () => {
const originalMatchMedia = window.matchMedia;
const originalGetComputedStyle = window.getComputedStyle;
afterEach(() => {
window.matchMedia = originalMatchMedia;
window.getComputedStyle = originalGetComputedStyle;
document.documentElement.removeAttribute("data-theme");
vi.clearAllMocks();
});
it("sets data-theme to dark when useSignal returns true", async () => {
const { useSignal } = await import("@telegram-apps/sdk-react");
vi.mocked(useSignal).mockReturnValue(true);
renderHook(() => useTelegramTheme());
expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
});
it("sets data-theme to light when useSignal returns false", async () => {
const { useSignal } = await import("@telegram-apps/sdk-react");
vi.mocked(useSignal).mockReturnValue(false);
renderHook(() => useTelegramTheme());
expect(document.documentElement.getAttribute("data-theme")).toBe("light");
});
});