Update e2e frontend tests (#3843)

* fix input prompts

* assistant ordering validation

* k

* Revert "fix input prompts"

This reverts commit a4b577bdd7.

* fix alembic

* foreign key updates

* Revert "foreign key updates"

This reverts commit fe17795a037f831790d69229e1067ccb5aab5bd9.

* improve e2e tests

* fix admin
This commit is contained in:
pablonyx
2025-01-30 12:15:29 -08:00
committed by GitHub
parent 0ed2886ad0
commit a70d472b5c
20 changed files with 260 additions and 361 deletions

View File

@@ -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
),
)

View File

@@ -161,9 +161,7 @@ class User(SQLAlchemyBaseUserTableUUID, Base):
hidden_assistants: Mapped[list[int]] = mapped_column( hidden_assistants: Mapped[list[int]] = mapped_column(
postgresql.JSONB(), nullable=False, default=[] 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( pinned_assistants: Mapped[list[int] | None] = mapped_column(
postgresql.JSONB(), nullable=True, default=None postgresql.JSONB(), nullable=True, default=None
) )

View File

@@ -44,7 +44,6 @@ class UserPreferences(BaseModel):
chosen_assistants: list[int] | None = None chosen_assistants: list[int] | None = None
hidden_assistants: list[int] = [] hidden_assistants: list[int] = []
visible_assistants: list[int] = [] visible_assistants: list[int] = []
recent_assistants: list[int] | None = None
default_model: str | None = None default_model: str | None = None
auto_scroll: bool | None = None auto_scroll: bool | None = None
pinned_assistants: list[int] | None = None pinned_assistants: list[int] | None = None

View File

@@ -572,59 +572,6 @@ class ChosenDefaultModelRequest(BaseModel):
default_model: str | None = None 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") @router.patch("/shortcut-enabled")
def update_user_shortcut_enabled( def update_user_shortcut_enabled(
shortcut_enabled: bool, shortcut_enabled: bool,
@@ -731,30 +678,6 @@ class ChosenAssistantsRequest(BaseModel):
chosen_assistants: list[int] 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( def update_assistant_visibility(
preferences: UserPreferences, assistant_id: int, show: bool preferences: UserPreferences, assistant_id: int, show: bool
) -> UserPreferences: ) -> UserPreferences:

View File

@@ -1,41 +1,16 @@
import { defineConfig, devices } from "@playwright/test"; import { defineConfig, devices } from "@playwright/test";
export default defineConfig({ export default defineConfig({
workers: 1, // temporary change to see if single threaded testing stabilizes the tests globalSetup: require.resolve("./tests/e2e/global-setup"),
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
projects: [ projects: [
{ {
// dependency for admin workflows name: "admin",
name: "admin_setup",
testMatch: /.*\admin_auth\.setup\.ts/,
},
{
// tests admin workflows
name: "chromium-admin",
grep: /@admin/,
use: { use: {
...devices["Desktop Chrome"], ...devices["Desktop Chrome"],
// Use prepared auth state.
storageState: "admin_auth.json", storageState: "admin_auth.json",
}, },
dependencies: ["admin_setup"], testIgnore: ["**/codeUtils.test.ts"],
},
{
// tests logged out / guest workflows
name: "chromium-guest",
grep: /@guest/,
use: {
...devices["Desktop Chrome"],
},
}, },
], ],
}); });

View File

@@ -111,6 +111,7 @@ import {
import AssistantModal from "../assistants/mine/AssistantModal"; import AssistantModal from "../assistants/mine/AssistantModal";
import { getSourceMetadata } from "@/lib/sources"; import { getSourceMetadata } from "@/lib/sources";
import { UserSettingsModal } from "./modal/UserSettingsModal"; import { UserSettingsModal } from "./modal/UserSettingsModal";
import { AlignStartVertical } from "lucide-react";
const TEMP_USER_MESSAGE_ID = -1; const TEMP_USER_MESSAGE_ID = -1;
const TEMP_ASSISTANT_MESSAGE_ID = -2; const TEMP_ASSISTANT_MESSAGE_ID = -2;
@@ -189,7 +190,11 @@ export function ChatPage({
const [userSettingsToggled, setUserSettingsToggled] = useState(false); const [userSettingsToggled, setUserSettingsToggled] = useState(false);
const { assistants: availableAssistants, finalAssistants } = useAssistants(); const {
assistants: availableAssistants,
finalAssistants,
pinnedAssistants,
} = useAssistants();
const [showApiKeyModal, setShowApiKeyModal] = useState( const [showApiKeyModal, setShowApiKeyModal] = useState(
!shouldShowWelcomeModal !shouldShowWelcomeModal
@@ -272,16 +277,6 @@ export function ChatPage({
SEARCH_PARAM_NAMES.TEMPERATURE 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) => { const setSelectedAssistantFromId = (assistantId: number) => {
// NOTE: also intentionally look through available assistants here, so that // 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 // 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] = const [presentingDocument, setPresentingDocument] =
useState<OnyxDocument | null>(null); useState<OnyxDocument | null>(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( const liveAssistant: Persona | undefined = useMemo(
() => () =>
alternativeAssistant || alternativeAssistant ||
selectedAssistant || selectedAssistant ||
recentAssistants[0] || pinnedAssistants[0] ||
finalAssistants[0] ||
availableAssistants[0], availableAssistants[0],
[ [
alternativeAssistant, alternativeAssistant,
selectedAssistant, selectedAssistant,
recentAssistants, pinnedAssistants,
finalAssistants,
availableAssistants, availableAssistants,
] ]
); );
@@ -816,7 +812,6 @@ export function ChatPage({
setMaxTokens(maxTokens); setMaxTokens(maxTokens);
} }
} }
refreshRecentAssistants(liveAssistant?.id);
fetchMaxTokens(); fetchMaxTokens();
}, [liveAssistant]); }, [liveAssistant]);

View File

@@ -19,9 +19,7 @@ import {
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { ChatSession } from "../interfaces"; import { ChatSession } from "../interfaces";
import { NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA } from "@/lib/constants";
import { Folder } from "../folders/interfaces"; import { Folder } from "../folders/interfaces";
import { usePopup } from "@/components/admin/connectors/Popup";
import { SettingsContext } from "@/components/settings/SettingsProvider"; import { SettingsContext } from "@/components/settings/SettingsProvider";
import { DocumentIcon2, NewChatIcon } from "@/components/icons/icons"; import { DocumentIcon2, NewChatIcon } from "@/components/icons/icons";
@@ -251,9 +249,11 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
const handleNewChat = () => { const handleNewChat = () => {
reset(); reset();
console.log("currentChatSession", currentChatSession);
const newChatUrl = const newChatUrl =
`/${page}` + `/${page}` +
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA && currentChatSession (currentChatSession
? `?assistantId=${currentChatSession.persona_id}` ? `?assistantId=${currentChatSession.persona_id}`
: ""); : "");
router.push(newChatUrl); router.push(newChatUrl);
@@ -294,8 +294,7 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
className="w-full px-2 py-1 rounded-md items-center hover:bg-hover cursor-pointer transition-all duration-150 flex gap-x-2" 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={ href={
`/${page}` + `/${page}` +
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA && (currentChatSession
currentChatSession?.persona_id
? `?assistantId=${currentChatSession?.persona_id}` ? `?assistantId=${currentChatSession?.persona_id}`
: "") : "")
} }
@@ -320,14 +319,6 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
<Link <Link
className="w-full px-2 py-1 rounded-md items-center hover:bg-hover cursor-pointer transition-all duration-150 flex gap-x-2" 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="/chat/input-prompts" href="/chat/input-prompts"
onClick={(e) => {
if (e.metaKey || e.ctrlKey) {
return;
}
if (handleNewChat) {
handleNewChat();
}
}}
> >
<DocumentIcon2 <DocumentIcon2
size={20} size={20}

View File

@@ -2,7 +2,6 @@
import { UserDropdown } from "../UserDropdown"; import { UserDropdown } from "../UserDropdown";
import { FiShare2 } from "react-icons/fi"; import { FiShare2 } from "react-icons/fi";
import { SetStateAction, useContext, useEffect } from "react"; import { SetStateAction, useContext, useEffect } from "react";
import { NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA } from "@/lib/constants";
import { ChatSession } from "@/app/chat/interfaces"; import { ChatSession } from "@/app/chat/interfaces";
import Link from "next/link"; import Link from "next/link";
import { pageType } from "@/app/chat/sessionSidebar/types"; import { pageType } from "@/app/chat/sessionSidebar/types";
@@ -42,8 +41,7 @@ export default function FunctionalHeader({
event.preventDefault(); event.preventDefault();
window.open( window.open(
`/${page}` + `/${page}` +
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA && (currentChatSession
currentChatSession
? `?assistantId=${currentChatSession.persona_id}` ? `?assistantId=${currentChatSession.persona_id}`
: ""), : ""),
"_self" "_self"
@@ -63,7 +61,7 @@ export default function FunctionalHeader({
reset(); reset();
const newChatUrl = const newChatUrl =
`/${page}` + `/${page}` +
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA && currentChatSession (currentChatSession
? `?assistantId=${currentChatSession.persona_id}` ? `?assistantId=${currentChatSession.persona_id}`
: ""); : "");
router.push(newChatUrl); router.push(newChatUrl);
@@ -128,25 +126,6 @@ export default function FunctionalHeader({
</div> </div>
)} )}
{/* <div
className={`absolute
${
documentSidebarToggled && !sidebarToggled
? "left-[calc(50%-125px)]"
: !documentSidebarToggled && sidebarToggled
? "left-[calc(50%+125px)]"
: "left-1/2"
}
${
documentSidebarToggled || sidebarToggled
? "mobile:w-[40vw] max-w-[50vw]"
: "mobile:w-[50vw] max-w-[60vw]"
}
top-1/2 transform -translate-x-1/2 -translate-y-1/2 transition-all duration-300`}
>
<ChatBanner />
</div> */}
<div className="invisible"> <div className="invisible">
<LogoWithText <LogoWithText
page={page} page={page}
@@ -156,8 +135,6 @@ export default function FunctionalHeader({
/> />
</div> </div>
{/* className="fixed cursor-pointer flex z-40 left-4 bg-black top-3 h-8" */}
<div className="absolute right-2 mobile:top-1 desktop:top-1 h-8 flex"> <div className="absolute right-2 mobile:top-1 desktop:top-1 h-8 flex">
{setSharingModalVisible && !hideUserDropdown && ( {setSharingModalVisible && !hideUserDropdown && (
<div <div
@@ -179,8 +156,7 @@ export default function FunctionalHeader({
className="desktop:hidden ml-2 my-auto" className="desktop:hidden ml-2 my-auto"
href={ href={
`/${page}` + `/${page}` +
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA && (currentChatSession
currentChatSession
? `?assistantId=${currentChatSession.persona_id}` ? `?assistantId=${currentChatSession.persona_id}`
: "") : "")
} }

View File

@@ -25,8 +25,6 @@ interface AssistantsContextProps {
ownedButHiddenAssistants: Persona[]; ownedButHiddenAssistants: Persona[];
refreshAssistants: () => Promise<void>; refreshAssistants: () => Promise<void>;
isImageGenerationAvailable: boolean; isImageGenerationAvailable: boolean;
recentAssistants: Persona[];
refreshRecentAssistants: (currentAssistant: number) => Promise<void>;
// Admin only // Admin only
editablePersonas: Persona[]; editablePersonas: Persona[];
allAssistants: Persona[]; allAssistants: Persona[];
@@ -56,35 +54,28 @@ export const AssistantsProvider: React.FC<{
const [editablePersonas, setEditablePersonas] = useState<Persona[]>([]); const [editablePersonas, setEditablePersonas] = useState<Persona[]>([]);
const [allAssistants, setAllAssistants] = useState<Persona[]>([]); const [allAssistants, setAllAssistants] = useState<Persona[]>([]);
const [pinnedAssistants, setPinnedAssistants] = useState<Persona[]>( const [pinnedAssistants, setPinnedAssistants] = useState<Persona[]>(() => {
user?.preferences.pinned_assistants if (user?.preferences.pinned_assistants) {
? assistants.filter((assistant) => return user.preferences.pinned_assistants
user?.preferences?.pinned_assistants?.includes(assistant.id) .map((id) => assistants.find((assistant) => assistant.id === id))
) .filter((assistant): assistant is Persona => assistant !== undefined);
: assistants.filter((a) => a.builtin_persona) } else {
); return assistants.filter((a) => a.builtin_persona);
}
});
useEffect(() => { useEffect(() => {
setPinnedAssistants( setPinnedAssistants(() => {
user?.preferences.pinned_assistants if (user?.preferences.pinned_assistants) {
? assistants.filter((assistant) => return user.preferences.pinned_assistants
user?.preferences?.pinned_assistants?.includes(assistant.id) .map((id) => assistants.find((assistant) => assistant.id === id))
) .filter((assistant): assistant is Persona => assistant !== undefined);
: assistants.filter((a) => a.builtin_persona) } else {
); return assistants.filter((a) => a.builtin_persona);
}
});
}, [user?.preferences?.pinned_assistants, assistants]); }, [user?.preferences?.pinned_assistants, assistants]);
const [recentAssistants, setRecentAssistants] = useState<Persona[]>(
user?.preferences.recent_assistants
?.filter((assistantId) =>
assistants.find((assistant) => assistant.id === assistantId)
)
.map(
(assistantId) =>
assistants.find((assistant) => assistant.id === assistantId)!
) || []
);
const [isImageGenerationAvailable, setIsImageGenerationAvailable] = const [isImageGenerationAvailable, setIsImageGenerationAvailable] =
useState<boolean>(false); useState<boolean>(false);
@@ -135,28 +126,6 @@ export const AssistantsProvider: React.FC<{
fetchPersonas(); fetchPersonas();
}, [isAdmin, isCurator]); }, [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 () => { const refreshAssistants = async () => {
try { try {
const response = await fetch("/api/persona", { const response = await fetch("/api/persona", {
@@ -181,13 +150,6 @@ export const AssistantsProvider: React.FC<{
} catch (error) { } catch (error) {
console.error("Error refreshing assistants:", error); console.error("Error refreshing assistants:", error);
} }
setRecentAssistants(
assistants.filter(
(assistant) =>
user?.preferences.recent_assistants?.includes(assistant.id) || false
)
);
}; };
const { const {
@@ -230,8 +192,6 @@ export const AssistantsProvider: React.FC<{
editablePersonas, editablePersonas,
allAssistants, allAssistants,
isImageGenerationAvailable, isImageGenerationAvailable,
recentAssistants,
refreshRecentAssistants,
setPinnedAssistants, setPinnedAssistants,
pinnedAssistants, pinnedAssistants,
}} }}

View File

@@ -2,7 +2,6 @@
import { useContext } from "react"; import { useContext } from "react";
import { FiSidebar } from "react-icons/fi"; import { FiSidebar } from "react-icons/fi";
import { SettingsContext } from "../settings/SettingsProvider"; 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 { LeftToLineIcon, NewChatIcon, RightToLineIcon } from "../icons/icons";
import { import {
Tooltip, Tooltip,
@@ -90,9 +89,7 @@ export default function LogoWithText({
className="my-auto mobile:hidden" className="my-auto mobile:hidden"
href={ href={
`/${page}` + `/${page}` +
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA && assistantId (assistantId ? `?assistantId=${assistantId}` : "")
? `?assistantId=${assistantId}`
: "")
} }
onClick={(e) => { onClick={(e) => {
if (e.metaKey || e.ctrlKey) { if (e.metaKey || e.ctrlKey) {

View File

@@ -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() === process.env.NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED?.toLowerCase() ===
"true"; "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 GMAIL_AUTH_IS_ADMIN_COOKIE_NAME = "gmail_auth_is_admin";
export const GOOGLE_DRIVE_AUTH_IS_ADMIN_COOKIE_NAME = export const GOOGLE_DRIVE_AUTH_IS_ADMIN_COOKIE_NAME =

View File

@@ -1,24 +1,9 @@
// dependency for all admin user tests // dependency for all admin user tests
import { test as setup } from "@playwright/test";
import { test as setup, expect } from "@playwright/test"; setup("authenticate as admin", async ({ browser }) => {
import { TEST_CREDENTIALS } from "./constants"; const context = await browser.newContext({ storageState: "admin_auth.json" });
const page = await context.newPage();
setup("authenticate", async ({ page }) => {
const { email, password } = TEST_CREDENTIALS;
await page.goto("http://localhost:3000/chat"); 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.waitForURL("http://localhost:3000/chat");
await page.context().storageState({ path: "admin_auth.json" });
}); });

View File

@@ -1,65 +1,43 @@
import { test, expect } from "@chromatic-com/playwright"; import { test, expect } from "@playwright/test";
test( test.use({ storageState: "admin_auth.json" });
"Admin - OAuth Redirect - Missing Code",
{
tag: "@admin",
},
async ({ page }, testInfo) => {
await page.goto(
"http://localhost:3000/admin/connectors/slack/oauth/callback?state=xyz"
);
await expect(page.locator("p.text-text-500")).toHaveText( test("Admin - OAuth Redirect - Missing Code", async ({ page }) => {
"Missing authorization code." await page.goto(
); "http://localhost:3000/admin/connectors/slack/oauth/callback?state=xyz"
} );
);
test( await expect(page.locator("p.text-text-500")).toHaveText(
"Admin - OAuth Redirect - Missing State", "Missing authorization code."
{ );
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( test("Admin - OAuth Redirect - Missing State", async ({ page }) => {
"Missing state parameter." await page.goto(
); "http://localhost:3000/admin/connectors/slack/oauth/callback?code=123"
} );
);
test( await expect(page.locator("p.text-text-500")).toHaveText(
"Admin - OAuth Redirect - Invalid Connector", "Missing state parameter."
{ );
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( test("Admin - OAuth Redirect - Invalid Connector", async ({ page }) => {
"invalid_connector is not a valid source type." await page.goto(
); "http://localhost:3000/admin/connectors/invalid-connector/oauth/callback?code=123&state=xyz"
} );
);
test( await expect(page.locator("p.text-text-500")).toHaveText(
"Admin - OAuth Redirect - No Session", "invalid_connector is not a valid source type."
{ );
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( test("Admin - OAuth Redirect - No Session", async ({ page }) => {
"An error occurred during the OAuth process. Please try again." 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."
);
});

View File

@@ -2,6 +2,8 @@ import { test, expect } from "@playwright/test";
import chromaticSnpashots from "./chromaticSnpashots.json"; import chromaticSnpashots from "./chromaticSnpashots.json";
import type { Page } from "@playwright/test"; import type { Page } from "@playwright/test";
test.use({ storageState: "admin_auth.json" });
async function verifyAdminPageNavigation( async function verifyAdminPageNavigation(
page: Page, page: Page,
path: string, path: string,
@@ -13,7 +15,10 @@ async function verifyAdminPageNavigation(
} }
) { ) {
await page.goto(`http://localhost:3000/admin/${path}`); 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) { if (options?.paragraphText) {
await expect(page.locator("p.text-sm").nth(0)).toHaveText( await expect(page.locator("p.text-sm").nth(0)).toHaveText(
@@ -35,18 +40,12 @@ async function verifyAdminPageNavigation(
} }
for (const chromaticSnapshot of chromaticSnpashots) { for (const chromaticSnapshot of chromaticSnpashots) {
test( test(`Admin - ${chromaticSnapshot.name}`, async ({ page }) => {
`Admin - ${chromaticSnapshot.name}`, await verifyAdminPageNavigation(
{ page,
tag: "@admin", chromaticSnapshot.path,
}, chromaticSnapshot.pageTitle,
async ({ page }) => { chromaticSnapshot.options
await verifyAdminPageNavigation( );
page, });
chromaticSnapshot.path,
chromaticSnapshot.pageTitle,
chromaticSnapshot.options
);
}
);
} }

View File

@@ -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,
});
});

View File

@@ -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", email: "admin_user@test.com",
password: "TestPassword123!", password: "TestPassword123!",
}; };

View File

@@ -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;

View File

@@ -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");
}
);

View File

@@ -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()}`);
}
}
}

15
web/user_auth.json Normal file
View File

@@ -0,0 +1,15 @@
{
"cookies": [
{
"name": "fastapiusersauth",
"value": "n_EMYYKHn4tQbuPTEbtN1gJ6dQTGek9omJPhO2GhHoA",
"domain": "localhost",
"path": "/",
"expires": 1738801376.508558,
"httpOnly": true,
"secure": false,
"sameSite": "Lax"
}
],
"origins": []
}