Fix stale app UI (supabase) & overall E2E test infra improvements (#337)
Fixes #269
This commit is contained in:
@@ -202,14 +202,22 @@ export function SupabaseConnector({ appId }: { appId: number }) {
|
||||
<div className="flex flex-col md:flex-row items-center justify-between">
|
||||
<h2 className="text-lg font-medium">Integrations</h2>
|
||||
<img
|
||||
onClick={() => {
|
||||
IpcClient.getInstance().openExternalUrl(
|
||||
"https://supabase-oauth.dyad.sh/api/connect-supabase/login",
|
||||
);
|
||||
onClick={async () => {
|
||||
if (settings?.isTestMode) {
|
||||
await IpcClient.getInstance().fakeHandleSupabaseConnect({
|
||||
appId,
|
||||
fakeProjectId: "fake-project-id",
|
||||
});
|
||||
} else {
|
||||
await IpcClient.getInstance().openExternalUrl(
|
||||
"https://supabase-oauth.dyad.sh/api/connect-supabase/login",
|
||||
);
|
||||
}
|
||||
}}
|
||||
src={isDarkMode ? connectSupabaseDark : connectSupabaseLight}
|
||||
alt="Connect to Supabase"
|
||||
className="w-full h-10 min-h-8 min-w-20 cursor-pointer"
|
||||
data-testid="connect-supabase-button"
|
||||
// className="h-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,8 @@ import React from "react";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
||||
import { useAtomValue, atom, useAtom } from "jotai";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { showError } from "@/lib/toast";
|
||||
import { useStreamChat } from "@/hooks/useStreamChat";
|
||||
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
|
||||
import { useLoadApp } from "@/hooks/useLoadApp";
|
||||
|
||||
interface DyadAddIntegrationProps {
|
||||
@@ -17,20 +15,15 @@ interface DyadAddIntegrationProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const isSetupAtom = atom(false);
|
||||
|
||||
export const DyadAddIntegration: React.FC<DyadAddIntegrationProps> = ({
|
||||
node,
|
||||
children,
|
||||
}) => {
|
||||
const { streamMessage } = useStreamChat();
|
||||
const [isSetup, setIsSetup] = useAtom(isSetupAtom);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { provider } = node.properties;
|
||||
const appId = useAtomValue(selectedAppIdAtom);
|
||||
const { app } = useLoadApp(appId);
|
||||
const selectedChatId = useAtomValue(selectedChatIdAtom);
|
||||
|
||||
const handleSetupClick = () => {
|
||||
if (!appId) {
|
||||
@@ -38,7 +31,6 @@ export const DyadAddIntegration: React.FC<DyadAddIntegrationProps> = ({
|
||||
return;
|
||||
}
|
||||
navigate({ to: "/app-details", search: { appId } });
|
||||
setIsSetup(true);
|
||||
};
|
||||
|
||||
if (app?.supabaseProjectName) {
|
||||
@@ -71,36 +63,18 @@ export const DyadAddIntegration: React.FC<DyadAddIntegrationProps> = ({
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-green-900">
|
||||
This app is connected to Supabase project:{" "}
|
||||
<span className="font-mono font-medium bg-green-100 px-1 py-0.5 rounded">
|
||||
{app.supabaseProjectName}
|
||||
</span>
|
||||
<p>
|
||||
This app is connected to Supabase project:{" "}
|
||||
<span className="font-mono font-medium bg-green-100 px-1 py-0.5 rounded">
|
||||
{app.supabaseProjectName}
|
||||
</span>
|
||||
</p>
|
||||
<p>Click the chat suggestion "Keep going" to continue.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSetup) {
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsSetup(false);
|
||||
if (!selectedChatId) {
|
||||
showError("No chat ID found");
|
||||
return;
|
||||
}
|
||||
streamMessage({
|
||||
prompt: "OK, I've setup Supabase. Continue",
|
||||
chatId: selectedChatId,
|
||||
});
|
||||
}}
|
||||
className="my-1"
|
||||
>
|
||||
Continue | I've setup {provider}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 my-2 p-3 border rounded-md bg-secondary/10">
|
||||
<div className="text-sm">
|
||||
|
||||
@@ -36,7 +36,7 @@ export const CodeView = ({ loading, app }: CodeViewProps) => {
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center p-2 border-b space-x-2">
|
||||
<button
|
||||
onClick={refreshApp}
|
||||
onClick={() => refreshApp()}
|
||||
className="p-1 rounded hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={loading || !app.id}
|
||||
title="Refresh Files"
|
||||
|
||||
@@ -1,61 +1,46 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useQuery, QueryClient } from "@tanstack/react-query";
|
||||
import { IpcClient } from "@/ipc/ipc_client";
|
||||
|
||||
import { useAtom } from "jotai";
|
||||
import { currentAppAtom } from "@/atoms/appAtoms";
|
||||
import { App } from "@/ipc/ipc_types";
|
||||
|
||||
export function useLoadApp(appId: number | null) {
|
||||
const [app, setApp] = useAtom(currentAppAtom);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [, setApp] = useAtom(currentAppAtom);
|
||||
|
||||
const {
|
||||
data: appData,
|
||||
isLoading: loading,
|
||||
error,
|
||||
refetch: refreshApp,
|
||||
} = useQuery<App | null, Error>({
|
||||
queryKey: ["app", appId],
|
||||
queryFn: async () => {
|
||||
if (appId === null) {
|
||||
return null;
|
||||
}
|
||||
const ipcClient = IpcClient.getInstance();
|
||||
return ipcClient.getApp(appId);
|
||||
},
|
||||
enabled: appId !== null,
|
||||
meta: { showErrorToast: true },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const loadApp = async () => {
|
||||
if (appId === null) {
|
||||
setApp(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const ipcClient = IpcClient.getInstance();
|
||||
const appData = await ipcClient.getApp(appId);
|
||||
|
||||
setApp(appData);
|
||||
setError(null);
|
||||
} catch (error) {
|
||||
console.error(`Error loading app ${appId}:`, error);
|
||||
setError(error instanceof Error ? error : new Error(String(error)));
|
||||
setApp(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadApp();
|
||||
}, [appId]);
|
||||
|
||||
const refreshApp = async () => {
|
||||
if (appId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log("Refreshing app", appId);
|
||||
const ipcClient = IpcClient.getInstance();
|
||||
const appData = await ipcClient.getApp(appId);
|
||||
console.log("App data", appData);
|
||||
setApp(null);
|
||||
} else if (appData !== undefined) {
|
||||
setApp(appData);
|
||||
setError(null);
|
||||
} catch (error) {
|
||||
console.error(`Error refreshing app ${appId}:`, error);
|
||||
setError(error instanceof Error ? error : new Error(String(error)));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [appId, appData, setApp]);
|
||||
|
||||
return { app, loading, error, refreshApp };
|
||||
return { app: appData, loading, error, refreshApp };
|
||||
}
|
||||
|
||||
// Function to invalidate the app query
|
||||
export const invalidateAppQuery = (
|
||||
queryClient: QueryClient,
|
||||
{ appId }: { appId: number | null },
|
||||
) => {
|
||||
return queryClient.invalidateQueries({ queryKey: ["app", appId] });
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import log from "electron-log";
|
||||
import { createLoggedHandler } from "./safe_handle";
|
||||
import { readSettings } from "../../main/settings"; // Assuming settings are read this way
|
||||
import { UserBudgetInfo, UserBudgetInfoSchema } from "../ipc_types";
|
||||
import { IS_TEST_BUILD } from "../utils/test_utils";
|
||||
|
||||
const logger = log.scope("pro_handlers");
|
||||
const handle = createLoggedHandler(logger);
|
||||
@@ -13,6 +14,10 @@ export function registerProHandlers() {
|
||||
// This method should try to avoid throwing errors because this is auxiliary
|
||||
// information and isn't critical to using the app
|
||||
handle("get-user-budget", async (): Promise<UserBudgetInfo | null> => {
|
||||
if (IS_TEST_BUILD) {
|
||||
// Avoid spamming the API in E2E tests.
|
||||
return null;
|
||||
}
|
||||
logger.info("Attempting to fetch user budget information.");
|
||||
|
||||
const settings = readSettings();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ipcMain, IpcMainInvokeEvent } from "electron";
|
||||
import log from "electron-log";
|
||||
import { IS_TEST_BUILD } from "../utils/test_utils";
|
||||
|
||||
export function createLoggedHandler(logger: log.LogFunctions) {
|
||||
return (
|
||||
@@ -27,3 +28,11 @@ export function createLoggedHandler(logger: log.LogFunctions) {
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function createTestOnlyLoggedHandler(logger: log.LogFunctions) {
|
||||
if (!IS_TEST_BUILD) {
|
||||
// Returns a no-op function for non-e2e test builds.
|
||||
return () => {};
|
||||
}
|
||||
return createLoggedHandler(logger);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,15 @@ import { db } from "../../db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { apps } from "../../db/schema";
|
||||
import { getSupabaseClient } from "../../supabase_admin/supabase_management_client";
|
||||
import { createLoggedHandler } from "./safe_handle";
|
||||
import {
|
||||
createLoggedHandler,
|
||||
createTestOnlyLoggedHandler,
|
||||
} from "./safe_handle";
|
||||
import { handleSupabaseOAuthReturn } from "../../supabase_admin/supabase_return_handler";
|
||||
|
||||
const logger = log.scope("supabase_handlers");
|
||||
const handle = createLoggedHandler(logger);
|
||||
const testOnlyHandle = createTestOnlyLoggedHandler(logger);
|
||||
|
||||
export function registerSupabaseHandlers() {
|
||||
handle("supabase:list-projects", async () => {
|
||||
@@ -36,4 +41,42 @@ export function registerSupabaseHandlers() {
|
||||
|
||||
logger.info(`Removed Supabase project association for app ${app}`);
|
||||
});
|
||||
|
||||
testOnlyHandle(
|
||||
"supabase:fake-connect-and-set-project",
|
||||
async (
|
||||
event,
|
||||
{ appId, fakeProjectId }: { appId: number; fakeProjectId: string },
|
||||
) => {
|
||||
// Call handleSupabaseOAuthReturn with fake data
|
||||
handleSupabaseOAuthReturn({
|
||||
token: "fake-access-token",
|
||||
refreshToken: "fake-refresh-token",
|
||||
expiresIn: 3600, // 1 hour
|
||||
});
|
||||
logger.info(
|
||||
`Called handleSupabaseOAuthReturn with fake data for app ${appId} during testing.`,
|
||||
);
|
||||
|
||||
// Set the supabase project for the currently selected app
|
||||
await db
|
||||
.update(apps)
|
||||
.set({
|
||||
supabaseProjectId: fakeProjectId,
|
||||
})
|
||||
.where(eq(apps.id, appId));
|
||||
logger.info(
|
||||
`Set fake Supabase project ${fakeProjectId} for app ${appId} during testing.`,
|
||||
);
|
||||
|
||||
// Simulate the deep link event
|
||||
event.sender.send("deep-link-received", {
|
||||
type: "supabase-oauth-return",
|
||||
url: "https://supabase-oauth.dyad.sh/api/connect-supabase/login",
|
||||
});
|
||||
logger.info(
|
||||
`Sent fake deep-link-received event for app ${appId} during testing.`,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -647,6 +647,17 @@ export class IpcClient {
|
||||
app,
|
||||
});
|
||||
}
|
||||
|
||||
public async fakeHandleSupabaseConnect(params: {
|
||||
appId: number;
|
||||
fakeProjectId: string;
|
||||
}): Promise<void> {
|
||||
await this.ipcRenderer.invoke(
|
||||
"supabase:fake-connect-and-set-project",
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
// --- End Supabase Management ---
|
||||
|
||||
public async getSystemDebugInfo(): Promise<SystemDebugInfo> {
|
||||
|
||||
1
src/ipc/utils/test_utils.ts
Normal file
1
src/ipc/utils/test_utils.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const IS_TEST_BUILD = process.env.E2E_TEST_BUILD === "true";
|
||||
@@ -9,6 +9,7 @@ import log from "electron-log";
|
||||
import { readSettings, writeSettings } from "./main/settings";
|
||||
import { handleSupabaseOAuthReturn } from "./supabase_admin/supabase_return_handler";
|
||||
import { handleDyadProReturn } from "./main/pro";
|
||||
import { IS_TEST_BUILD } from "./ipc/utils/test_utils";
|
||||
|
||||
log.errorHandler.startCatching();
|
||||
log.eventLogger.startLogging();
|
||||
@@ -58,7 +59,7 @@ export async function onFirstRunMaybe() {
|
||||
hasRunBefore: true,
|
||||
});
|
||||
}
|
||||
if (process.env.E2E_TEST_BUILD) {
|
||||
if (IS_TEST_BUILD) {
|
||||
writeSettings({
|
||||
isTestMode: true,
|
||||
});
|
||||
@@ -73,7 +74,7 @@ async function promptMoveToApplicationsFolder(): Promise<void> {
|
||||
// Why not in e2e tests?
|
||||
// There's no way to stub this dialog in time, so we just skip it
|
||||
// in e2e testing mode.
|
||||
if (process.env.E2E_TEST_BUILD) return;
|
||||
if (IS_TEST_BUILD) return;
|
||||
if (process.platform !== "darwin") return;
|
||||
if (app.isInApplicationsFolder()) return;
|
||||
logger.log("Prompting user to move to applications folder");
|
||||
|
||||
@@ -26,6 +26,8 @@ import { Button } from "@/components/ui/button";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { ImportAppButton } from "@/components/ImportAppButton";
|
||||
import { showError } from "@/lib/toast";
|
||||
import { invalidateAppQuery } from "@/hooks/useLoadApp";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
// Adding an export for attachments
|
||||
export interface HomeSubmitOptions {
|
||||
@@ -47,7 +49,7 @@ export default function HomePage() {
|
||||
const [releaseNotesOpen, setReleaseNotesOpen] = useState(false);
|
||||
const [releaseUrl, setReleaseUrl] = useState("");
|
||||
const { theme } = useTheme();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
useEffect(() => {
|
||||
const updateLastVersionLaunched = async () => {
|
||||
if (
|
||||
@@ -132,6 +134,7 @@ export default function HomePage() {
|
||||
setSelectedAppId(result.app.id);
|
||||
setIsPreviewOpen(false);
|
||||
await refreshApps(); // Ensure refreshApps is awaited if it's async
|
||||
await invalidateAppQuery(queryClient, { appId: result.app.id });
|
||||
posthog.capture("home:chat-submit");
|
||||
navigate({ to: "/chat", search: { id: result.chatId } });
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import { IS_TEST_BUILD } from "../ipc/utils/test_utils";
|
||||
|
||||
export function getDyadAppPath(appPath: string): string {
|
||||
if (process.env.E2E_TEST_BUILD) {
|
||||
if (IS_TEST_BUILD) {
|
||||
const electron = getElectron();
|
||||
return path.join(electron!.app.getPath("userData"), "dyad-apps", appPath);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,12 @@ const validInvokeChannels = [
|
||||
"rename-branch",
|
||||
"clear-session-data",
|
||||
"get-user-budget",
|
||||
] as const;
|
||||
// Test-only channels
|
||||
// These should ALWAYS be guarded with IS_TEST_BUILD in the main process.
|
||||
// We can't detect with IS_TEST_BUILD in the preload script because
|
||||
// it's a separate process from the main process.
|
||||
"supabase:fake-connect-and-set-project",
|
||||
];
|
||||
|
||||
// Add valid receive channels
|
||||
const validReceiveChannels = [
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
SupabaseManagementAPIError,
|
||||
} from "@dyad-sh/supabase-management-js";
|
||||
import log from "electron-log";
|
||||
import { IS_TEST_BUILD } from "../ipc/utils/test_utils";
|
||||
|
||||
const logger = log.scope("supabase_management_client");
|
||||
|
||||
@@ -122,6 +123,10 @@ export async function getSupabaseClient(): Promise<SupabaseManagementAPI> {
|
||||
export async function getSupabaseProjectName(
|
||||
projectId: string,
|
||||
): Promise<string> {
|
||||
if (IS_TEST_BUILD) {
|
||||
return "Fake Supabase Project";
|
||||
}
|
||||
|
||||
const supabase = await getSupabaseClient();
|
||||
const projects = await supabase.getProjects();
|
||||
const project = projects?.find((p) => p.id === projectId);
|
||||
|
||||
@@ -3,6 +3,7 @@ import fsAsync from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { isIgnored } from "isomorphic-git";
|
||||
import log from "electron-log";
|
||||
import { IS_TEST_BUILD } from "../ipc/utils/test_utils";
|
||||
|
||||
const logger = log.scope("utils/codebase");
|
||||
|
||||
@@ -369,7 +370,7 @@ export async function extractCodebase(appPath: string): Promise<{
|
||||
|
||||
const endTime = Date.now();
|
||||
logger.log("extractCodebase: time taken", endTime - startTime);
|
||||
if (process.env.E2E_TEST_BUILD) {
|
||||
if (IS_TEST_BUILD) {
|
||||
// Why? For some reason, file ordering is not stable on Windows.
|
||||
// This is a workaround to ensure stable ordering, although
|
||||
// ideally we'd like to sort it by modification time which is
|
||||
@@ -400,7 +401,7 @@ async function sortFilesByModificationTime(files: string[]): Promise<string[]> {
|
||||
}),
|
||||
);
|
||||
|
||||
if (process.env.E2E_TEST_BUILD) {
|
||||
if (IS_TEST_BUILD) {
|
||||
// Why? For some reason, file ordering is not stable on Windows.
|
||||
// This is a workaround to ensure stable ordering, although
|
||||
// ideally we'd like to sort it by modification time which is
|
||||
|
||||
Reference in New Issue
Block a user