diff --git a/backend/alembic/versions/a6df6b88ef81_remove_recent_assistants.py b/backend/alembic/versions/a6df6b88ef81_remove_recent_assistants.py new file mode 100644 index 000000000..399709442 --- /dev/null +++ b/backend/alembic/versions/a6df6b88ef81_remove_recent_assistants.py @@ -0,0 +1,29 @@ +"""remove recent assistants + +Revision ID: a6df6b88ef81 +Revises: 4d58345da04a +Create Date: 2025-01-29 10:25:52.790407 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "a6df6b88ef81" +down_revision = "4d58345da04a" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.drop_column("user", "recent_assistants") + + +def downgrade() -> None: + op.add_column( + "user", + sa.Column( + "recent_assistants", postgresql.JSONB(), server_default="[]", nullable=False + ), + ) diff --git a/backend/onyx/db/models.py b/backend/onyx/db/models.py index 6727a6e8d..cdc96fd54 100644 --- a/backend/onyx/db/models.py +++ b/backend/onyx/db/models.py @@ -161,9 +161,7 @@ class User(SQLAlchemyBaseUserTableUUID, Base): hidden_assistants: Mapped[list[int]] = mapped_column( postgresql.JSONB(), nullable=False, default=[] ) - recent_assistants: Mapped[list[dict]] = mapped_column( - postgresql.JSONB(), nullable=False, default=list, server_default="[]" - ) + pinned_assistants: Mapped[list[int] | None] = mapped_column( postgresql.JSONB(), nullable=True, default=None ) diff --git a/backend/onyx/server/manage/models.py b/backend/onyx/server/manage/models.py index 2ab8c6608..81a04708e 100644 --- a/backend/onyx/server/manage/models.py +++ b/backend/onyx/server/manage/models.py @@ -44,7 +44,6 @@ class UserPreferences(BaseModel): chosen_assistants: list[int] | None = None hidden_assistants: list[int] = [] visible_assistants: list[int] = [] - recent_assistants: list[int] | None = None default_model: str | None = None auto_scroll: bool | None = None pinned_assistants: list[int] | None = None diff --git a/backend/onyx/server/manage/users.py b/backend/onyx/server/manage/users.py index 74438a9fd..3988c90e4 100644 --- a/backend/onyx/server/manage/users.py +++ b/backend/onyx/server/manage/users.py @@ -572,59 +572,6 @@ class ChosenDefaultModelRequest(BaseModel): default_model: str | None = None -class RecentAssistantsRequest(BaseModel): - current_assistant: int - - -def update_recent_assistants( - recent_assistants: list[int] | None, current_assistant: int -) -> list[int]: - if recent_assistants is None: - recent_assistants = [] - else: - recent_assistants = [x for x in recent_assistants if x != current_assistant] - - # Add current assistant to start of list - recent_assistants.insert(0, current_assistant) - - # Keep only the 5 most recent assistants - recent_assistants = recent_assistants[:5] - return recent_assistants - - -@router.patch("/user/recent-assistants") -def update_user_recent_assistants( - request: RecentAssistantsRequest, - user: User | None = Depends(current_user), - db_session: Session = Depends(get_session), -) -> None: - if user is None: - if AUTH_TYPE == AuthType.DISABLED: - store = get_kv_store() - no_auth_user = fetch_no_auth_user(store) - preferences = no_auth_user.preferences - recent_assistants = preferences.recent_assistants - updated_preferences = update_recent_assistants( - recent_assistants, request.current_assistant - ) - preferences.recent_assistants = updated_preferences - set_no_auth_user_preferences(store, preferences) - return - else: - raise RuntimeError("This should never happen") - - recent_assistants = UserInfo.from_model(user).preferences.recent_assistants - updated_recent_assistants = update_recent_assistants( - recent_assistants, request.current_assistant - ) - db_session.execute( - update(User) - .where(User.id == user.id) # type: ignore - .values(recent_assistants=updated_recent_assistants) - ) - db_session.commit() - - @router.patch("/shortcut-enabled") def update_user_shortcut_enabled( shortcut_enabled: bool, @@ -731,30 +678,6 @@ class ChosenAssistantsRequest(BaseModel): chosen_assistants: list[int] -@router.patch("/user/assistant-list") -def update_user_assistant_list( - request: ChosenAssistantsRequest, - user: User | None = Depends(current_user), - db_session: Session = Depends(get_session), -) -> None: - if user is None: - if AUTH_TYPE == AuthType.DISABLED: - store = get_kv_store() - no_auth_user = fetch_no_auth_user(store) - no_auth_user.preferences.chosen_assistants = request.chosen_assistants - set_no_auth_user_preferences(store, no_auth_user.preferences) - return - else: - raise RuntimeError("This should never happen") - - db_session.execute( - update(User) - .where(User.id == user.id) # type: ignore - .values(chosen_assistants=request.chosen_assistants) - ) - db_session.commit() - - def update_assistant_visibility( preferences: UserPreferences, assistant_id: int, show: bool ) -> UserPreferences: diff --git a/web/playwright.config.ts b/web/playwright.config.ts index b55b8396d..403871a9d 100644 --- a/web/playwright.config.ts +++ b/web/playwright.config.ts @@ -1,41 +1,16 @@ import { defineConfig, devices } from "@playwright/test"; export default defineConfig({ - workers: 1, // temporary change to see if single threaded testing stabilizes the tests - testDir: "./tests/e2e", // Folder for test files - reporter: "list", - // Configure paths for screenshots - // expect: { - // toMatchSnapshot: { - // threshold: 0.2, // Adjust the threshold for visual diffs - // }, - // }, - // reporter: [["html", { outputFolder: "test-results/output/report" }]], // HTML report location - // outputDir: "test-results/output/screenshots", // Set output folder for test artifacts + globalSetup: require.resolve("./tests/e2e/global-setup"), + projects: [ { - // dependency for admin workflows - name: "admin_setup", - testMatch: /.*\admin_auth\.setup\.ts/, - }, - { - // tests admin workflows - name: "chromium-admin", - grep: /@admin/, + name: "admin", use: { ...devices["Desktop Chrome"], - // Use prepared auth state. storageState: "admin_auth.json", }, - dependencies: ["admin_setup"], - }, - { - // tests logged out / guest workflows - name: "chromium-guest", - grep: /@guest/, - use: { - ...devices["Desktop Chrome"], - }, + testIgnore: ["**/codeUtils.test.ts"], }, ], }); diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index ad162a23e..d6cc7e0b6 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -111,6 +111,7 @@ import { import AssistantModal from "../assistants/mine/AssistantModal"; import { getSourceMetadata } from "@/lib/sources"; import { UserSettingsModal } from "./modal/UserSettingsModal"; +import { AlignStartVertical } from "lucide-react"; const TEMP_USER_MESSAGE_ID = -1; const TEMP_ASSISTANT_MESSAGE_ID = -2; @@ -189,7 +190,11 @@ export function ChatPage({ const [userSettingsToggled, setUserSettingsToggled] = useState(false); - const { assistants: availableAssistants, finalAssistants } = useAssistants(); + const { + assistants: availableAssistants, + finalAssistants, + pinnedAssistants, + } = useAssistants(); const [showApiKeyModal, setShowApiKeyModal] = useState( !shouldShowWelcomeModal @@ -272,16 +277,6 @@ export function ChatPage({ SEARCH_PARAM_NAMES.TEMPERATURE ); - const defaultTemperature = search_param_temperature - ? parseFloat(search_param_temperature) - : selectedAssistant?.tools.some( - (tool) => - tool.in_code_tool_id === SEARCH_TOOL_ID || - tool.in_code_tool_id === INTERNET_SEARCH_TOOL_ID - ) - ? 0 - : 0.7; - const setSelectedAssistantFromId = (assistantId: number) => { // NOTE: also intentionally look through available assistants here, so that // even if the user has hidden an assistant they can still go back to it @@ -297,20 +292,21 @@ export function ChatPage({ const [presentingDocument, setPresentingDocument] = useState(null); - const { recentAssistants, refreshRecentAssistants } = useAssistants(); - + // Current assistant is decided based on this ordering + // 1. Alternative assistant (assistant selected explicitly by user) + // 2. Selected assistant (assistnat default in this chat session) + // 3. First pinned assistants (ordered list of pinned assistants) + // 4. Available assistants (ordered list of available assistants) const liveAssistant: Persona | undefined = useMemo( () => alternativeAssistant || selectedAssistant || - recentAssistants[0] || - finalAssistants[0] || + pinnedAssistants[0] || availableAssistants[0], [ alternativeAssistant, selectedAssistant, - recentAssistants, - finalAssistants, + pinnedAssistants, availableAssistants, ] ); @@ -816,7 +812,6 @@ export function ChatPage({ setMaxTokens(maxTokens); } } - refreshRecentAssistants(liveAssistant?.id); fetchMaxTokens(); }, [liveAssistant]); diff --git a/web/src/app/chat/sessionSidebar/HistorySidebar.tsx b/web/src/app/chat/sessionSidebar/HistorySidebar.tsx index bc42bfd39..f443066d0 100644 --- a/web/src/app/chat/sessionSidebar/HistorySidebar.tsx +++ b/web/src/app/chat/sessionSidebar/HistorySidebar.tsx @@ -19,9 +19,7 @@ import { import { useRouter, useSearchParams } from "next/navigation"; import { ChatSession } from "../interfaces"; -import { NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA } from "@/lib/constants"; import { Folder } from "../folders/interfaces"; -import { usePopup } from "@/components/admin/connectors/Popup"; import { SettingsContext } from "@/components/settings/SettingsProvider"; import { DocumentIcon2, NewChatIcon } from "@/components/icons/icons"; @@ -251,9 +249,11 @@ export const HistorySidebar = forwardRef( const handleNewChat = () => { reset(); + console.log("currentChatSession", currentChatSession); + const newChatUrl = `/${page}` + - (NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA && currentChatSession + (currentChatSession ? `?assistantId=${currentChatSession.persona_id}` : ""); router.push(newChatUrl); @@ -294,8 +294,7 @@ export const HistorySidebar = forwardRef( className="w-full px-2 py-1 rounded-md items-center hover:bg-hover cursor-pointer transition-all duration-150 flex gap-x-2" href={ `/${page}` + - (NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA && - currentChatSession?.persona_id + (currentChatSession ? `?assistantId=${currentChatSession?.persona_id}` : "") } @@ -320,14 +319,6 @@ export const HistorySidebar = forwardRef( { - if (e.metaKey || e.ctrlKey) { - return; - } - if (handleNewChat) { - handleNewChat(); - } - }} > )} - {/*
- -
*/} -
- {/* className="fixed cursor-pointer flex z-40 left-4 bg-black top-3 h-8" */} -
{setSharingModalVisible && !hideUserDropdown && (
Promise; isImageGenerationAvailable: boolean; - recentAssistants: Persona[]; - refreshRecentAssistants: (currentAssistant: number) => Promise; // Admin only editablePersonas: Persona[]; allAssistants: Persona[]; @@ -56,35 +54,28 @@ export const AssistantsProvider: React.FC<{ const [editablePersonas, setEditablePersonas] = useState([]); const [allAssistants, setAllAssistants] = useState([]); - const [pinnedAssistants, setPinnedAssistants] = useState( - user?.preferences.pinned_assistants - ? assistants.filter((assistant) => - user?.preferences?.pinned_assistants?.includes(assistant.id) - ) - : assistants.filter((a) => a.builtin_persona) - ); + const [pinnedAssistants, setPinnedAssistants] = useState(() => { + if (user?.preferences.pinned_assistants) { + return user.preferences.pinned_assistants + .map((id) => assistants.find((assistant) => assistant.id === id)) + .filter((assistant): assistant is Persona => assistant !== undefined); + } else { + return assistants.filter((a) => a.builtin_persona); + } + }); useEffect(() => { - setPinnedAssistants( - user?.preferences.pinned_assistants - ? assistants.filter((assistant) => - user?.preferences?.pinned_assistants?.includes(assistant.id) - ) - : assistants.filter((a) => a.builtin_persona) - ); + setPinnedAssistants(() => { + if (user?.preferences.pinned_assistants) { + return user.preferences.pinned_assistants + .map((id) => assistants.find((assistant) => assistant.id === id)) + .filter((assistant): assistant is Persona => assistant !== undefined); + } else { + return assistants.filter((a) => a.builtin_persona); + } + }); }, [user?.preferences?.pinned_assistants, assistants]); - const [recentAssistants, setRecentAssistants] = useState( - user?.preferences.recent_assistants - ?.filter((assistantId) => - assistants.find((assistant) => assistant.id === assistantId) - ) - .map( - (assistantId) => - assistants.find((assistant) => assistant.id === assistantId)! - ) || [] - ); - const [isImageGenerationAvailable, setIsImageGenerationAvailable] = useState(false); @@ -135,28 +126,6 @@ export const AssistantsProvider: React.FC<{ fetchPersonas(); }, [isAdmin, isCurator]); - const refreshRecentAssistants = async (currentAssistant: number) => { - const response = await fetch("/api/user/recent-assistants", { - method: "PATCH", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - current_assistant: currentAssistant, - }), - }); - if (!response.ok) { - return; - } - setRecentAssistants((recentAssistants) => [ - assistants.find((assistant) => assistant.id === currentAssistant)!, - - ...recentAssistants.filter( - (assistant) => assistant.id !== currentAssistant - ), - ]); - }; - const refreshAssistants = async () => { try { const response = await fetch("/api/persona", { @@ -181,13 +150,6 @@ export const AssistantsProvider: React.FC<{ } catch (error) { console.error("Error refreshing assistants:", error); } - - setRecentAssistants( - assistants.filter( - (assistant) => - user?.preferences.recent_assistants?.includes(assistant.id) || false - ) - ); }; const { @@ -230,8 +192,6 @@ export const AssistantsProvider: React.FC<{ editablePersonas, allAssistants, isImageGenerationAvailable, - recentAssistants, - refreshRecentAssistants, setPinnedAssistants, pinnedAssistants, }} diff --git a/web/src/components/header/LogoWithText.tsx b/web/src/components/header/LogoWithText.tsx index a1bb3823b..607204810 100644 --- a/web/src/components/header/LogoWithText.tsx +++ b/web/src/components/header/LogoWithText.tsx @@ -2,7 +2,6 @@ import { useContext } from "react"; import { FiSidebar } from "react-icons/fi"; import { SettingsContext } from "../settings/SettingsProvider"; -import { NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA } from "@/lib/constants"; import { LeftToLineIcon, NewChatIcon, RightToLineIcon } from "../icons/icons"; import { Tooltip, @@ -90,9 +89,7 @@ export default function LogoWithText({ className="my-auto mobile:hidden" href={ `/${page}` + - (NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA && assistantId - ? `?assistantId=${assistantId}` - : "") + (assistantId ? `?assistantId=${assistantId}` : "") } onClick={(e) => { if (e.metaKey || e.ctrlKey) { diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index f14df3ac5..7d76a7a4c 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -18,10 +18,6 @@ export const NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED = process.env.NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED?.toLowerCase() === "true"; -export const NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA = - process.env.NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA?.toLowerCase() === - "true"; - export const GMAIL_AUTH_IS_ADMIN_COOKIE_NAME = "gmail_auth_is_admin"; export const GOOGLE_DRIVE_AUTH_IS_ADMIN_COOKIE_NAME = diff --git a/web/tests/e2e/admin_auth.setup.ts b/web/tests/e2e/admin_auth.setup.ts index 500b87c0f..593ee7d1f 100644 --- a/web/tests/e2e/admin_auth.setup.ts +++ b/web/tests/e2e/admin_auth.setup.ts @@ -1,24 +1,9 @@ // dependency for all admin user tests +import { test as setup } from "@playwright/test"; -import { test as setup, expect } from "@playwright/test"; -import { TEST_CREDENTIALS } from "./constants"; - -setup("authenticate", async ({ page }) => { - const { email, password } = TEST_CREDENTIALS; - +setup("authenticate as admin", async ({ browser }) => { + const context = await browser.newContext({ storageState: "admin_auth.json" }); + const page = await context.newPage(); await page.goto("http://localhost:3000/chat"); - - await page.waitForURL("http://localhost:3000/auth/login?next=%2Fchat"); - - await expect(page).toHaveTitle("Onyx"); - - await page.fill("#email", email); - await page.fill("#password", password); - - // Click the login button - await page.click('button[type="submit"]'); - await page.waitForURL("http://localhost:3000/chat"); - - await page.context().storageState({ path: "admin_auth.json" }); }); diff --git a/web/tests/e2e/admin_oauth_redirect_uri.spec.ts b/web/tests/e2e/admin_oauth_redirect_uri.spec.ts index 2ebf332ab..fdec61ee3 100644 --- a/web/tests/e2e/admin_oauth_redirect_uri.spec.ts +++ b/web/tests/e2e/admin_oauth_redirect_uri.spec.ts @@ -1,65 +1,43 @@ -import { test, expect } from "@chromatic-com/playwright"; +import { test, expect } from "@playwright/test"; -test( - "Admin - OAuth Redirect - Missing Code", - { - tag: "@admin", - }, - async ({ page }, testInfo) => { - await page.goto( - "http://localhost:3000/admin/connectors/slack/oauth/callback?state=xyz" - ); +test.use({ storageState: "admin_auth.json" }); - await expect(page.locator("p.text-text-500")).toHaveText( - "Missing authorization code." - ); - } -); +test("Admin - OAuth Redirect - Missing Code", async ({ page }) => { + await page.goto( + "http://localhost:3000/admin/connectors/slack/oauth/callback?state=xyz" + ); -test( - "Admin - OAuth Redirect - Missing State", - { - tag: "@admin", - }, - async ({ page }, testInfo) => { - await page.goto( - "http://localhost:3000/admin/connectors/slack/oauth/callback?code=123" - ); + await expect(page.locator("p.text-text-500")).toHaveText( + "Missing authorization code." + ); +}); - await expect(page.locator("p.text-text-500")).toHaveText( - "Missing state parameter." - ); - } -); +test("Admin - OAuth Redirect - Missing State", async ({ page }) => { + await page.goto( + "http://localhost:3000/admin/connectors/slack/oauth/callback?code=123" + ); -test( - "Admin - OAuth Redirect - Invalid Connector", - { - tag: "@admin", - }, - async ({ page }, testInfo) => { - await page.goto( - "http://localhost:3000/admin/connectors/invalid-connector/oauth/callback?code=123&state=xyz" - ); + await expect(page.locator("p.text-text-500")).toHaveText( + "Missing state parameter." + ); +}); - await expect(page.locator("p.text-text-500")).toHaveText( - "invalid_connector is not a valid source type." - ); - } -); +test("Admin - OAuth Redirect - Invalid Connector", async ({ page }) => { + await page.goto( + "http://localhost:3000/admin/connectors/invalid-connector/oauth/callback?code=123&state=xyz" + ); -test( - "Admin - OAuth Redirect - No Session", - { - tag: "@admin", - }, - async ({ page }, testInfo) => { - await page.goto( - "http://localhost:3000/admin/connectors/slack/oauth/callback?code=123&state=xyz" - ); + await expect(page.locator("p.text-text-500")).toHaveText( + "invalid_connector is not a valid source type." + ); +}); - await expect(page.locator("p.text-text-500")).toHaveText( - "An error occurred during the OAuth process. Please try again." - ); - } -); +test("Admin - OAuth Redirect - No Session", async ({ page }) => { + await page.goto( + "http://localhost:3000/admin/connectors/slack/oauth/callback?code=123&state=xyz" + ); + + await expect(page.locator("p.text-text-500")).toHaveText( + "An error occurred during the OAuth process. Please try again." + ); +}); diff --git a/web/tests/e2e/admin_pages.spec.ts b/web/tests/e2e/admin_pages.spec.ts index c5c80984b..8c6198070 100644 --- a/web/tests/e2e/admin_pages.spec.ts +++ b/web/tests/e2e/admin_pages.spec.ts @@ -2,6 +2,8 @@ import { test, expect } from "@playwright/test"; import chromaticSnpashots from "./chromaticSnpashots.json"; import type { Page } from "@playwright/test"; +test.use({ storageState: "admin_auth.json" }); + async function verifyAdminPageNavigation( page: Page, path: string, @@ -13,7 +15,10 @@ async function verifyAdminPageNavigation( } ) { await page.goto(`http://localhost:3000/admin/${path}`); - await expect(page.locator("h1.text-3xl")).toHaveText(pageTitle); + + await expect(page.locator("h1.text-3xl")).toHaveText(pageTitle, { + timeout: 2000, + }); if (options?.paragraphText) { await expect(page.locator("p.text-sm").nth(0)).toHaveText( @@ -35,18 +40,12 @@ async function verifyAdminPageNavigation( } for (const chromaticSnapshot of chromaticSnpashots) { - test( - `Admin - ${chromaticSnapshot.name}`, - { - tag: "@admin", - }, - async ({ page }) => { - await verifyAdminPageNavigation( - page, - chromaticSnapshot.path, - chromaticSnapshot.pageTitle, - chromaticSnapshot.options - ); - } - ); + test(`Admin - ${chromaticSnapshot.name}`, async ({ page }) => { + await verifyAdminPageNavigation( + page, + chromaticSnapshot.path, + chromaticSnapshot.pageTitle, + chromaticSnapshot.options + ); + }); } diff --git a/web/tests/e2e/assisant_ordering.spec.ts b/web/tests/e2e/assisant_ordering.spec.ts new file mode 100644 index 000000000..5da091e6b --- /dev/null +++ b/web/tests/e2e/assisant_ordering.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from "@playwright/test"; + +// Use pre-signed in "admin" storage state +test.use({ + storageState: "admin_auth.json", +}); + +test("Chat workflow", async ({ page }) => { + // Initial setup + await page.goto("http://localhost:3000/chat", { timeout: 3000 }); + + // Interact with Art assistant + await page.locator("button").filter({ hasText: "Art" }).click(); + await page.getByPlaceholder("Message Art assistant...").fill("Hi"); + await page.keyboard.press("Enter"); + await page.waitForTimeout(3000); + + // Start a new chat + await page.getByRole("link", { name: "Start New Chat" }).click(); + await page.waitForNavigation({ waitUntil: "networkidle" }); + + // Check for expected text + await expect(page.getByText("Assistant for generating")).toBeVisible(); + + // Interact with General assistant + await page.locator("button").filter({ hasText: "General" }).click(); + + // Check URL after clicking General assistant + await expect(page).toHaveURL("http://localhost:3000/chat?assistantId=-1", { + timeout: 5000, + }); + + // Create a new assistant + await page.getByRole("button", { name: "Explore Assistants" }).click(); + await page.getByRole("button", { name: "Create" }).click(); + await page.getByTestId("name").click(); + await page.getByTestId("name").fill("Test Assistant"); + await page.getByTestId("description").click(); + await page.getByTestId("description").fill("Test Assistant Description"); + await page.getByTestId("system_prompt").click(); + await page.getByTestId("system_prompt").fill("Test Assistant Instructions"); + await page.getByRole("button", { name: "Create" }).click(); + + // Verify new assistant creation + await expect(page.getByText("Test Assistant Description")).toBeVisible({ + timeout: 5000, + }); + + // Start another new chat + await page.getByRole("link", { name: "Start New Chat" }).click(); + await expect(page.getByText("Assistant with access to")).toBeVisible({ + timeout: 5000, + }); +}); diff --git a/web/tests/e2e/constants.js b/web/tests/e2e/constants.js index 847ebf338..e22b6356b 100644 --- a/web/tests/e2e/constants.js +++ b/web/tests/e2e/constants.js @@ -1,4 +1,9 @@ -export const TEST_CREDENTIALS = { +export const TEST_USER_CREDENTIALS = { + email: "user1@test.com", + password: "User1Password123!", +}; + +export const TEST_ADMIN_CREDENTIALS = { email: "admin_user@test.com", password: "TestPassword123!", }; diff --git a/web/tests/e2e/global-setup.ts b/web/tests/e2e/global-setup.ts new file mode 100644 index 000000000..ab8eabb50 --- /dev/null +++ b/web/tests/e2e/global-setup.ts @@ -0,0 +1,22 @@ +import { chromium, FullConfig } from "@playwright/test"; +import { loginAs } from "./utils/auth"; + +async function globalSetup(config: FullConfig) { + const browser = await chromium.launch(); + + const adminContext = await browser.newContext(); + const adminPage = await adminContext.newPage(); + await loginAs(adminPage, "admin"); + await adminContext.storageState({ path: "admin_auth.json" }); + await adminContext.close(); + + const userContext = await browser.newContext(); + const userPage = await userContext.newPage(); + await loginAs(userPage, "user"); + await userContext.storageState({ path: "user_auth.json" }); + await userContext.close(); + + await browser.close(); +} + +export default globalSetup; diff --git a/web/tests/e2e/home.spec.ts b/web/tests/e2e/home.spec.ts deleted file mode 100644 index a57f19ad6..000000000 --- a/web/tests/e2e/home.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -// ➕ Add this line -import { test, expect, takeSnapshot } from "@chromatic-com/playwright"; -import { TEST_CREDENTIALS } from "./constants"; - -// Then use as normal 👇 -test( - "Homepage", - { - tag: "@guest", - }, - async ({ page }, testInfo) => { - // Test redirect to login, and redirect to search after login - const { email, password } = TEST_CREDENTIALS; - - await page.goto("http://localhost:3000/chat"); - - await page.waitForURL("http://localhost:3000/auth/login?next=%2Fchat"); - - await expect(page).toHaveTitle("Onyx"); - - await takeSnapshot(page, "Before login", testInfo); - - await page.fill("#email", email); - await page.fill("#password", password); - - // Click the login button - await page.click('button[type="submit"]'); - - await page.waitForURL("http://localhost:3000/chat"); - - await page.getByPlaceholder("Send a message or try using @ or /"); - - await expect(page.locator("body")).not.toContainText("Initializing Onyx"); - } -); diff --git a/web/tests/e2e/utils/auth.ts b/web/tests/e2e/utils/auth.ts new file mode 100644 index 000000000..131c1e5b4 --- /dev/null +++ b/web/tests/e2e/utils/auth.ts @@ -0,0 +1,37 @@ +import { Page } from "@playwright/test"; +import { TEST_ADMIN_CREDENTIALS, TEST_USER_CREDENTIALS } from "../constants"; + +// Basic function which logs in a user (either admin or regular user) to the application +// It handles both successful login attempts and potential timeouts, with a retry mechanism +export async function loginAs(page: Page, userType: "admin" | "user") { + const { email, password } = + userType === "admin" ? TEST_ADMIN_CREDENTIALS : TEST_USER_CREDENTIALS; + await page.goto("http://localhost:3000/auth/login", { timeout: 1000 }); + + await page.fill("#email", email); + await page.fill("#password", password); + + // Click the login button + await page.click('button[type="submit"]'); + + try { + await page.waitForURL("http://localhost:3000/chat", { timeout: 4000 }); + } catch (error) { + console.log(`Timeout occurred. Current URL: ${page.url()}`); + + // If redirect to /chat doesn't happen, go to /auth/login + await page.goto("http://localhost:3000/auth/signup", { timeout: 1000 }); + + await page.fill("#email", email); + await page.fill("#password", password); + + // Click the login button + await page.click('button[type="submit"]'); + + try { + await page.waitForURL("http://localhost:3000/chat", { timeout: 4000 }); + } catch (error) { + console.log(`Timeout occurred again. Current URL: ${page.url()}`); + } + } +} diff --git a/web/user_auth.json b/web/user_auth.json new file mode 100644 index 000000000..7ce3b10b8 --- /dev/null +++ b/web/user_auth.json @@ -0,0 +1,15 @@ +{ + "cookies": [ + { + "name": "fastapiusersauth", + "value": "n_EMYYKHn4tQbuPTEbtN1gJ6dQTGek9omJPhO2GhHoA", + "domain": "localhost", + "path": "/", + "expires": 1738801376.508558, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + } + ], + "origins": [] +} \ No newline at end of file