chore: auto-commit (17 archivos)
- app.md - applog.go - frontend/package.json - frontend/package.json.md5 - frontend/vite.config.ts - go.mod - main.go - matrix_service.go - sqlite_driver.go - .wails_dev.log - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
const E2E_API = process.env.MATRIX_CLIENT_PC_E2E_API || "http://127.0.0.1:8767";
|
||||
|
||||
async function api(path: string, init: RequestInit = {}): Promise<any> {
|
||||
const res = await fetch(E2E_API + path, init);
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
throw new Error(`E2E API ${path} -> ${res.status}: ${body}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
test.describe("Entry flow — LoginScreen → click Sign in → HomeScreen with rooms", () => {
|
||||
test.beforeEach(async () => {
|
||||
// Reset backend state: wipe last_user so frontend lands on LoginScreen.
|
||||
await api("/wipe_session", { method: "POST" });
|
||||
});
|
||||
|
||||
test("user can sign in and see their rooms", async ({ page }) => {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
// LoginScreen visible
|
||||
await expect(page.getByRole("heading", { name: "matrix_client_pc" })).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
const signInBtn = page.getByRole("button", { name: /Sign in with Matrix/i });
|
||||
await expect(signInBtn).toBeVisible();
|
||||
|
||||
// Click Sign in → shim calls /signin_admin → returns user_id
|
||||
await signInBtn.click();
|
||||
|
||||
// HomeScreen header buttons appear once authenticated.
|
||||
await expect(page.getByRole("button", { name: /Health/i })).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(page.getByRole("button", { name: /Logs/i })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /Logout/i })).toBeVisible();
|
||||
|
||||
// Sidebar has at least one room (rooms are fetched after Start triggers sync).
|
||||
const firstRoom = page.locator('nav a, [role="navigation"] a').first();
|
||||
await expect(firstRoom).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
// Backend sanity: diagnostics says we're synced with rooms.
|
||||
const diag = await api("/diagnostics");
|
||||
expect(diag.started).toBe(true);
|
||||
expect(diag.rooms_count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("Logout returns to LoginScreen", async ({ page }) => {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
// Sign in first
|
||||
await page.getByRole("button", { name: /Sign in with Matrix/i }).click();
|
||||
await expect(page.getByRole("button", { name: /Logout/i })).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
||||
// Logout
|
||||
await page.getByRole("button", { name: /Logout/i }).click();
|
||||
|
||||
// Back to LoginScreen
|
||||
await expect(page.getByRole("button", { name: /Sign in with Matrix/i })).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@
|
||||
"test:ui": "vitest --ui",
|
||||
"e2e": "playwright test",
|
||||
"e2e:ui": "playwright test --ui",
|
||||
"e2e:wails": "playwright test --config playwright.cdp.config.ts",
|
||||
"e2e:report": "playwright show-report"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1 +1 @@
|
||||
9a66d5a5186912b91bb20602c66c7f8e
|
||||
68223a3dca6c9351bad4d13d8f189cf0
|
||||
@@ -0,0 +1,32 @@
|
||||
import { defineConfig } from "@playwright/test";
|
||||
|
||||
// Drives the Vite dev server (frontend with HTTP-shim bindings) against a real
|
||||
// Windows-side matrix_client_pc.exe running with MATRIX_CLIENT_PC_E2E=1 +
|
||||
// BIND_ALL=1 (E2E HTTP API reachable on 127.0.0.1:8767 cross-WSL).
|
||||
//
|
||||
// Prerequisites:
|
||||
// 1. bash scripts/launch_e2e.sh (Windows .exe up, :8767 listening)
|
||||
// 2. VITE_E2E_API=http://localhost:8767 pnpm dev --host 0.0.0.0
|
||||
// 3. pnpm e2e:wails
|
||||
//
|
||||
// Default URL targets the Vite dev port. Set BASE_URL env if vite picked a
|
||||
// different port (5173 if free, otherwise 5174 etc.).
|
||||
|
||||
const BASE_URL = process.env.BASE_URL || "http://localhost:5174";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./e2e_cdp",
|
||||
timeout: 60_000,
|
||||
fullyParallel: false,
|
||||
workers: 1,
|
||||
retries: 0,
|
||||
reporter: "list",
|
||||
use: {
|
||||
baseURL: BASE_URL,
|
||||
headless: process.env.HEADED ? false : true,
|
||||
trace: "retain-on-failure",
|
||||
screenshot: "only-on-failure",
|
||||
viewport: { width: 1280, height: 800 },
|
||||
actionTimeout: 10_000,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
// HTTP shim of the Wails MatrixService bindings — used in dev/E2E mode when
|
||||
// running the frontend via `pnpm dev` against a real Wails app's E2E HTTP
|
||||
// server (default http://127.0.0.1:8767). Vite resolve.alias swaps the
|
||||
// `wailsjs/go/main/MatrixService` import to this file when VITE_E2E_API is set.
|
||||
|
||||
const API: string =
|
||||
(import.meta as any).env?.VITE_E2E_API || "http://127.0.0.1:8767";
|
||||
|
||||
async function callJSON<T>(
|
||||
path: string,
|
||||
init: RequestInit = {},
|
||||
): Promise<T> {
|
||||
const res = await fetch(API + path, {
|
||||
...init,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(init.headers || {}),
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
throw new Error(`E2E API ${path} -> ${res.status}: ${body}`);
|
||||
}
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
// --- read endpoints ---
|
||||
export async function GetLastUserID(): Promise<string> {
|
||||
// /last_user reads last_user.txt directly so /wipe_session causes the
|
||||
// frontend to fall back to LoginScreen even if sync is still active.
|
||||
const r: any = await callJSON("/last_user");
|
||||
return r.user_id || "";
|
||||
}
|
||||
|
||||
export async function GetSession(user_id: string): Promise<any> {
|
||||
// E2E server does not expose per-user session. Approximate with diagnostics.
|
||||
const d: any = await callJSON("/diagnostics");
|
||||
return {
|
||||
user_id,
|
||||
has_token: !!d.user_id,
|
||||
homeserver_url: d.homeserver_url || "",
|
||||
};
|
||||
}
|
||||
|
||||
export async function GetDiagnostics(): Promise<any> {
|
||||
return callJSON("/diagnostics");
|
||||
}
|
||||
|
||||
export async function GetLogPath(): Promise<string> {
|
||||
const r: any = await callJSON("/logs?n=1");
|
||||
return r.path || "";
|
||||
}
|
||||
|
||||
export async function GetLogTail(n: number): Promise<string[]> {
|
||||
const r: any = await callJSON(`/logs?n=${encodeURIComponent(n)}`);
|
||||
return r.lines || [];
|
||||
}
|
||||
|
||||
export async function ListRooms(): Promise<any[]> {
|
||||
const r: any = await callJSON("/rooms");
|
||||
return r.rooms || [];
|
||||
}
|
||||
|
||||
export async function LoadTimeline(room_id: string, limit: number): Promise<any[]> {
|
||||
const r: any = await callJSON(
|
||||
`/timeline?room_id=${encodeURIComponent(room_id)}&limit=${limit}`,
|
||||
);
|
||||
return r.events || [];
|
||||
}
|
||||
|
||||
// --- write endpoints ---
|
||||
// Dev mode forces skip_crypto: MatrixCryptoInit hangs indefinitely when MAS is
|
||||
// active (the in-flight UIA roundtrip does not respect ctx cancellation). The
|
||||
// frontend still sees rooms + can send plaintext to non-E2EE rooms; encrypted
|
||||
// timelines render the wrapper events with "Encrypted" placeholders.
|
||||
export async function Start(user_id: string): Promise<void> {
|
||||
await callJSON("/start?skip_crypto=true", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ user_id }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function StartNoCrypto(user_id: string): Promise<void> {
|
||||
await callJSON("/start?skip_crypto=true", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ user_id }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function Stop(): Promise<void> {
|
||||
await fetch(API + "/stop", { method: "POST" });
|
||||
}
|
||||
|
||||
export async function SendText(room_id: string, body: string): Promise<string> {
|
||||
const r: any = await callJSON("/send", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ room_id, body }),
|
||||
});
|
||||
return r.event_id || "";
|
||||
}
|
||||
|
||||
export async function SendMarkdown(room_id: string, body: string): Promise<string> {
|
||||
const r: any = await callJSON("/send", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ room_id, body, markdown: true }),
|
||||
});
|
||||
return r.event_id || "";
|
||||
}
|
||||
|
||||
// --- login flow ---
|
||||
// In real Wails the Login() call opens the OIDC loopback flow. In dev/E2E we
|
||||
// short-circuit via /signin_admin: the backend uses its env-stored
|
||||
// MATRIX_SYNAPSE_ADMIN_TOKEN to whoami and persist the token. user_id is
|
||||
// resolved server-side — no frontend prompt needed.
|
||||
export async function Login(): Promise<string> {
|
||||
const r: any = await callJSON("/signin_admin", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
if (!r.user_id) throw new Error("signin_admin returned no user_id");
|
||||
return r.user_id;
|
||||
}
|
||||
|
||||
export async function Logout(_user_id: string): Promise<void> {
|
||||
await fetch(API + "/wipe_session", { method: "POST" });
|
||||
}
|
||||
|
||||
export async function SetContext(_arg: any): Promise<void> {
|
||||
// no-op in dev
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// HTTP shim of `wailsjs/runtime/runtime` — only the event APIs the frontend
|
||||
// uses are exposed. `EventsOn` polls /diagnostics every 1.5s and emits stub
|
||||
// events with the new room/timeline counts so consumers see updates without
|
||||
// needing the real Wails event bus.
|
||||
|
||||
type Handler = (...args: any[]) => void;
|
||||
const handlers = new Map<string, Set<Handler>>();
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let lastDiag: any = {};
|
||||
|
||||
const API: string =
|
||||
(import.meta as any).env?.VITE_E2E_API || "http://127.0.0.1:8767";
|
||||
|
||||
async function pollOnce() {
|
||||
try {
|
||||
const r = await fetch(API + "/diagnostics");
|
||||
if (!r.ok) return;
|
||||
const d = await r.json();
|
||||
if (d.rooms_count !== lastDiag.rooms_count) {
|
||||
emit("matrix:rooms_changed", d);
|
||||
}
|
||||
if (d.sync_active !== lastDiag.sync_active) {
|
||||
emit("matrix:sync_changed", d);
|
||||
}
|
||||
lastDiag = d;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
function emit(event: string, ...args: any[]) {
|
||||
const set = handlers.get(event);
|
||||
if (!set) return;
|
||||
for (const h of set) {
|
||||
try {
|
||||
h(...args);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensurePolling() {
|
||||
if (pollTimer) return;
|
||||
pollTimer = setInterval(pollOnce, 1500);
|
||||
}
|
||||
|
||||
export function EventsOn(event: string, handler: Handler): () => void {
|
||||
if (!handlers.has(event)) handlers.set(event, new Set());
|
||||
handlers.get(event)!.add(handler);
|
||||
ensurePolling();
|
||||
return () => {
|
||||
handlers.get(event)?.delete(handler);
|
||||
};
|
||||
}
|
||||
|
||||
export function EventsOnce(event: string, handler: Handler): () => void {
|
||||
const off = EventsOn(event, (...args) => {
|
||||
off();
|
||||
handler(...args);
|
||||
});
|
||||
return off;
|
||||
}
|
||||
|
||||
export function EventsEmit(_event: string, ..._args: any[]): void {
|
||||
// no-op in dev
|
||||
}
|
||||
|
||||
export function EventsOff(event: string, ..._handlers: Handler[]): void {
|
||||
handlers.delete(event);
|
||||
}
|
||||
+41
-6
@@ -1,7 +1,42 @@
|
||||
import {defineConfig} from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import path from "node:path";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()]
|
||||
})
|
||||
// E2E/dev mode: when VITE_E2E_API is set, swap Wails bindings for HTTP shims
|
||||
// that hit the matrix_client_pc E2E HTTP server. The production `wails build`
|
||||
// run does NOT set VITE_E2E_API -> bindings resolve to the real generated files.
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), "");
|
||||
const useShim = !!env.VITE_E2E_API;
|
||||
|
||||
const aliases = useShim
|
||||
? {
|
||||
"../wailsjs/go/main/MatrixService": path.resolve(
|
||||
__dirname,
|
||||
"src/shims/MatrixServiceShim.ts",
|
||||
),
|
||||
"../../wailsjs/go/main/MatrixService": path.resolve(
|
||||
__dirname,
|
||||
"src/shims/MatrixServiceShim.ts",
|
||||
),
|
||||
"../wailsjs/runtime/runtime": path.resolve(
|
||||
__dirname,
|
||||
"src/shims/RuntimeShim.ts",
|
||||
),
|
||||
"../../wailsjs/runtime/runtime": path.resolve(
|
||||
__dirname,
|
||||
"src/shims/RuntimeShim.ts",
|
||||
),
|
||||
}
|
||||
: {};
|
||||
|
||||
return {
|
||||
plugins: [react()],
|
||||
resolve: { alias: aliases },
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
port: 5173,
|
||||
strictPort: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user